Skip to content
This repository was archived by the owner on Oct 15, 2024. It is now read-only.

Commit 751272b

Browse files
tadfishermsfjarvis
authored andcommitted
Quick and dirty hardware key import
1 parent 4dda3c2 commit 751272b

File tree

5 files changed

+164
-24
lines changed

5 files changed

+164
-24
lines changed

app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,26 @@
77
package app.passwordstore.ui.pgp
88

99
import android.os.Bundle
10+
import androidx.activity.ComponentActivity
1011
import androidx.activity.result.contract.ActivityResultContracts.OpenDocument
1112
import androidx.appcompat.app.AppCompatActivity
13+
import androidx.lifecycle.lifecycleScope
1214
import app.passwordstore.R
15+
import app.passwordstore.crypto.HWSecurityDeviceHandler
1316
import app.passwordstore.crypto.KeyUtils.tryGetId
1417
import app.passwordstore.crypto.PGPKey
1518
import app.passwordstore.crypto.PGPKeyManager
1619
import app.passwordstore.crypto.errors.KeyAlreadyExistsException
20+
import app.passwordstore.crypto.errors.NoSecretKeyException
1721
import com.github.michaelbull.result.Err
1822
import com.github.michaelbull.result.Ok
1923
import com.github.michaelbull.result.Result
24+
import com.github.michaelbull.result.getOrThrow
2025
import com.github.michaelbull.result.runCatching
2126
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2227
import dagger.hilt.android.AndroidEntryPoint
2328
import javax.inject.Inject
29+
import kotlinx.coroutines.launch
2430
import kotlinx.coroutines.runBlocking
2531

2632
@AndroidEntryPoint
@@ -32,9 +38,10 @@ class PGPKeyImportActivity : AppCompatActivity() {
3238
*/
3339
private var lastBytes: ByteArray? = null
3440
@Inject lateinit var keyManager: PGPKeyManager
41+
@Inject lateinit var deviceHandler: HWSecurityDeviceHandler
3542

3643
private val pgpKeyImportAction =
37-
registerForActivityResult(OpenDocument()) { uri ->
44+
(this as ComponentActivity).registerForActivityResult(OpenDocument()) { uri ->
3845
runCatching {
3946
if (uri == null) {
4047
return@runCatching null
@@ -50,6 +57,7 @@ class PGPKeyImportActivity : AppCompatActivity() {
5057

5158
override fun onCreate(savedInstanceState: Bundle?) {
5259
super.onCreate(savedInstanceState)
60+
5361
pgpKeyImportAction.launch(arrayOf("*/*"))
5462
}
5563

@@ -68,6 +76,17 @@ class PGPKeyImportActivity : AppCompatActivity() {
6876
return key
6977
}
7078

79+
private fun pairDevice(bytes: ByteArray) {
80+
lifecycleScope.launch {
81+
val result =
82+
keyManager.addKey(
83+
deviceHandler.pairWithPublicKey(PGPKey(bytes)).getOrThrow(),
84+
replace = true
85+
)
86+
handleImportResult(result)
87+
}
88+
}
89+
7190
private fun handleImportResult(result: Result<PGPKey?, Throwable>) {
7291
when (result) {
7392
is Ok<PGPKey?> -> {
@@ -89,26 +108,34 @@ class PGPKeyImportActivity : AppCompatActivity() {
89108
.setCancelable(false)
90109
.show()
91110
}
92-
is Err<Throwable> -> {
93-
if (result.error is KeyAlreadyExistsException && lastBytes != null) {
94-
MaterialAlertDialogBuilder(this)
95-
.setTitle(getString(R.string.pgp_key_import_failed))
96-
.setMessage(getString(R.string.pgp_key_import_failed_replace_message))
97-
.setPositiveButton(R.string.dialog_yes) { _, _ ->
98-
handleImportResult(runCatching { importKey(lastBytes!!, replace = true) })
99-
}
100-
.setNegativeButton(R.string.dialog_no) { _, _ -> finish() }
101-
.setCancelable(false)
102-
.show()
103-
} else {
104-
MaterialAlertDialogBuilder(this)
105-
.setTitle(getString(R.string.pgp_key_import_failed))
106-
.setMessage(result.error.message)
107-
.setPositiveButton(android.R.string.ok) { _, _ -> finish() }
108-
.setCancelable(false)
109-
.show()
111+
is Err<Throwable> ->
112+
when {
113+
result.error is KeyAlreadyExistsException && lastBytes != null ->
114+
MaterialAlertDialogBuilder(this)
115+
.setTitle(getString(R.string.pgp_key_import_failed))
116+
.setMessage(getString(R.string.pgp_key_import_failed_replace_message))
117+
.setPositiveButton(R.string.dialog_yes) { _, _ ->
118+
handleImportResult(runCatching { importKey(lastBytes!!, replace = true) })
119+
}
120+
.setNegativeButton(R.string.dialog_no) { _, _ -> finish() }
121+
.setCancelable(false)
122+
.show()
123+
result.error is NoSecretKeyException && lastBytes != null ->
124+
MaterialAlertDialogBuilder(this)
125+
.setTitle(R.string.pgp_key_import_failed_no_secret)
126+
.setMessage(R.string.pgp_key_import_failed_no_secret_message)
127+
.setPositiveButton(R.string.dialog_yes) { _, _ -> pairDevice(lastBytes!!) }
128+
.setNegativeButton(R.string.dialog_no) { _, _ -> finish() }
129+
.setCancelable(false)
130+
.show()
131+
else ->
132+
MaterialAlertDialogBuilder(this)
133+
.setTitle(getString(R.string.pgp_key_import_failed))
134+
.setMessage(result.error.message + "\n" + result.error.stackTraceToString())
135+
.setPositiveButton(android.R.string.ok) { _, _ -> finish() }
136+
.setCancelable(false)
137+
.show()
110138
}
111-
}
112139
}
113140
}
114141
}

app/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@
334334
<string name="select_gpg_key_title">Select\nGPG Key</string>
335335
<string name="select_gpg_key_message">Select a GPG key to initialize your store with</string>
336336
<string name="gpg_key_select">Select key</string>
337+
<string name="pair_hardware_key">Pair hardware key</string>
337338

338339
<!-- SSH port validation -->
339340
<string name="ssh_scheme_needed_title">Potentially incorrect URL</string>
@@ -360,6 +361,8 @@
360361
<string name="password_list_fab_content_description">Create new password or folder</string>
361362
<string name="pgp_key_import_failed">Failed to import PGP key</string>
362363
<string name="pgp_key_import_failed_replace_message">An existing key with this ID was found, do you want to replace it?</string>
364+
<string name="pgp_key_import_failed_no_secret">No secret PGP key</string>
365+
<string name="pgp_key_import_failed_no_secret_message">This is a public key. Would you like to pair a hardware security device?</string>
363366
<string name="pgp_key_import_succeeded">Successfully imported PGP key</string>
364367
<string name="pgp_key_import_succeeded_message">The key ID of the imported key is given below, please review it for correctness:\n%1$s</string>
365368
<string name="pref_category_pgp_title">PGP settings</string>

crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ import app.passwordstore.crypto.GpgIdentifier.KeyId
99
import app.passwordstore.crypto.GpgIdentifier.UserId
1010
import com.github.michaelbull.result.get
1111
import com.github.michaelbull.result.runCatching
12+
import java.io.ByteArrayOutputStream
13+
import org.bouncycastle.bcpg.GnuExtendedS2K
14+
import org.bouncycastle.bcpg.S2K
15+
import org.bouncycastle.bcpg.SecretKeyPacket
16+
import org.bouncycastle.bcpg.SecretSubkeyPacket
17+
import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags
1218
import org.bouncycastle.openpgp.PGPKeyRing
19+
import org.bouncycastle.openpgp.PGPPublicKey
20+
import org.bouncycastle.openpgp.PGPPublicKeyRing
1321
import org.bouncycastle.openpgp.PGPSecretKey
1422
import org.bouncycastle.openpgp.PGPSecretKeyRing
1523
import org.pgpainless.algorithm.EncryptionPurpose
@@ -37,6 +45,28 @@ public object KeyUtils {
3745
val keyRing = tryParseKeyring(key) ?: return null
3846
return UserId(keyRing.publicKey.userIDs.next())
3947
}
48+
49+
public fun tryCreateStubKey(
50+
publicKey: PGPKey,
51+
serial: ByteArray,
52+
stubFingerprints: List<OpenPgpFingerprint>
53+
): PGPKey? {
54+
val keyRing = tryParseKeyring(publicKey) as? PGPPublicKeyRing ?: return null
55+
val secretKeyRing =
56+
keyRing.fold(PGPSecretKeyRing(emptyList())) { ring, key ->
57+
PGPSecretKeyRing.insertSecretKey(
58+
ring,
59+
if (stubFingerprints.any { it == OpenPgpFingerprint.parseFromBinary(key.fingerprint) }) {
60+
toCardSecretKey(key, serial)
61+
} else {
62+
toDummySecretKey(key)
63+
}
64+
)
65+
}
66+
67+
return PGPKey(secretKeyRing.encoded)
68+
}
69+
4070
public fun tryGetEncryptionKeyFingerprint(key: PGPKey): OpenPgpFingerprint? {
4171
val keyRing = tryParseKeyring(key) ?: return null
4272
val encryptionSubkey =
@@ -59,3 +89,63 @@ public object KeyUtils {
5989
return info.getSecretKey(encryptionKey.keyID)
6090
}
6191
}
92+
93+
private fun toDummySecretKey(publicKey: PGPPublicKey): PGPSecretKey {
94+
95+
return PGPSecretKey(
96+
if (publicKey.isMasterKey) {
97+
SecretKeyPacket(
98+
publicKey.publicKeyPacket,
99+
SymmetricKeyAlgorithmTags.NULL,
100+
SecretKeyPacket.USAGE_CHECKSUM,
101+
GnuExtendedS2K(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY),
102+
byteArrayOf(),
103+
byteArrayOf()
104+
)
105+
} else {
106+
SecretSubkeyPacket(
107+
publicKey.publicKeyPacket,
108+
SymmetricKeyAlgorithmTags.NULL,
109+
SecretKeyPacket.USAGE_CHECKSUM,
110+
GnuExtendedS2K(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY),
111+
byteArrayOf(),
112+
byteArrayOf()
113+
)
114+
},
115+
publicKey
116+
)
117+
}
118+
119+
@Suppress("MagicNumber")
120+
private fun toCardSecretKey(publicKey: PGPPublicKey, serial: ByteArray): PGPSecretKey {
121+
return PGPSecretKey(
122+
if (publicKey.isMasterKey) {
123+
SecretKeyPacket(
124+
publicKey.publicKeyPacket,
125+
SymmetricKeyAlgorithmTags.NULL,
126+
SecretKeyPacket.USAGE_CHECKSUM,
127+
GnuExtendedS2K(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD),
128+
ByteArray(8),
129+
encodeSerial(serial),
130+
)
131+
} else {
132+
SecretSubkeyPacket(
133+
publicKey.publicKeyPacket,
134+
SymmetricKeyAlgorithmTags.NULL,
135+
SecretKeyPacket.USAGE_CHECKSUM,
136+
GnuExtendedS2K(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD),
137+
ByteArray(8),
138+
encodeSerial(serial),
139+
)
140+
},
141+
publicKey
142+
)
143+
}
144+
145+
@Suppress("MagicNumber")
146+
private fun encodeSerial(serial: ByteArray): ByteArray {
147+
val out = ByteArrayOutputStream()
148+
out.write(serial.size)
149+
out.write(serial, 0, minOf(16, serial.size))
150+
return out.toByteArray()
151+
}

crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ package app.passwordstore.crypto
99
import androidx.annotation.VisibleForTesting
1010
import app.passwordstore.crypto.KeyUtils.tryGetId
1111
import app.passwordstore.crypto.KeyUtils.tryParseKeyring
12-
import app.passwordstore.crypto.errors.InvalidKeyException
1312
import app.passwordstore.crypto.errors.KeyAlreadyExistsException
1413
import app.passwordstore.crypto.errors.KeyDeletionFailedException
1514
import app.passwordstore.crypto.errors.KeyDirectoryUnavailableException
1615
import app.passwordstore.crypto.errors.KeyNotFoundException
1716
import app.passwordstore.crypto.errors.NoKeysAvailableException
17+
import app.passwordstore.crypto.errors.NoSecretKeyException
1818
import app.passwordstore.util.coroutines.runSuspendCatching
1919
import com.github.michaelbull.result.Result
2020
import com.github.michaelbull.result.unwrap
@@ -40,12 +40,16 @@ constructor(
4040
withContext(dispatcher) {
4141
runSuspendCatching {
4242
if (!keyDirExists()) throw KeyDirectoryUnavailableException
43-
val incomingKeyRing = tryParseKeyring(key) ?: throw InvalidKeyException
43+
val incomingKeyRing = tryParseKeyring(key)
44+
45+
if (incomingKeyRing is PGPPublicKeyRing) {
46+
throw NoSecretKeyException(tryGetId(key)?.toString() ?: "Failed to retrieve key ID")
47+
}
48+
4449
val keyFile = File(keyDir, "${tryGetId(key)}.$KEY_EXTENSION")
4550
if (keyFile.exists()) {
4651
val existingKeyBytes = keyFile.readBytes()
47-
val existingKeyRing =
48-
tryParseKeyring(PGPKey(existingKeyBytes)) ?: throw InvalidKeyException
52+
val existingKeyRing = tryParseKeyring(PGPKey(existingKeyBytes))
4953
when {
5054
existingKeyRing is PGPPublicKeyRing && incomingKeyRing is PGPSecretKeyRing -> {
5155
keyFile.writeBytes(key.contents)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.bouncycastle.bcpg
2+
3+
/**
4+
* Add a constructor for GNU-extended S2K
5+
*
6+
* This extension is documented on GnuPG documentation DETAILS file, section "GNU extensions to the
7+
* S2K algorithm". Its support is already present in S2K class but lack for a constructor.
8+
*
9+
* @author Léonard Dallot <leonard.dallot@taztag.com>
10+
*/
11+
public class GnuExtendedS2K(mode: Int) : S2K(SIMPLE) {
12+
init {
13+
this.type = GNU_DUMMY_S2K
14+
this.protectionMode = mode
15+
}
16+
}

0 commit comments

Comments
 (0)