aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt37
-rw-r--r--app/src/main/res/values/strings.xml3
-rw-r--r--crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt91
-rw-r--r--crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt11
-rw-r--r--crypto-pgpainless/src/main/kotlin/org/bouncycastle/bcpg/GnuExtendedS2K.kt17
5 files changed, 150 insertions, 9 deletions
diff --git a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt b/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt
index 4c9060f7..b3edb1a9 100644
--- a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt
@@ -7,20 +7,26 @@
package app.passwordstore.ui.pgp
import android.os.Bundle
+import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts.OpenDocument
import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
import app.passwordstore.R
+import app.passwordstore.crypto.HWSecurityDeviceHandler
import app.passwordstore.crypto.KeyUtils.tryGetId
import app.passwordstore.crypto.PGPKey
import app.passwordstore.crypto.PGPKeyManager
import app.passwordstore.crypto.errors.KeyAlreadyExistsException
+import app.passwordstore.crypto.errors.NoSecretKeyException
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
+import com.github.michaelbull.result.getOrThrow
import com.github.michaelbull.result.runCatching
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
+import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@AndroidEntryPoint
@@ -32,9 +38,10 @@ class PGPKeyImportActivity : AppCompatActivity() {
*/
private var lastBytes: ByteArray? = null
@Inject lateinit var keyManager: PGPKeyManager
+ @Inject lateinit var deviceHandler: HWSecurityDeviceHandler
private val pgpKeyImportAction =
- registerForActivityResult(OpenDocument()) { uri ->
+ (this as ComponentActivity).registerForActivityResult(OpenDocument()) { uri ->
runCatching {
if (uri == null) {
return@runCatching null
@@ -50,6 +57,7 @@ class PGPKeyImportActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+
pgpKeyImportAction.launch(arrayOf("*/*"))
}
@@ -68,6 +76,16 @@ class PGPKeyImportActivity : AppCompatActivity() {
return key
}
+ private fun pairDevice(bytes: ByteArray) {
+ lifecycleScope.launch {
+ val result = keyManager.addKey(
+ deviceHandler.pairWithPublicKey(PGPKey(bytes)).getOrThrow(),
+ replace = true
+ )
+ handleImportResult(result)
+ }
+ }
+
private fun handleImportResult(result: Result<PGPKey?, Throwable>) {
when (result) {
is Ok<PGPKey?> -> {
@@ -85,8 +103,8 @@ class PGPKeyImportActivity : AppCompatActivity() {
.setCancelable(false)
.show()
}
- is Err<Throwable> -> {
- if (result.error is KeyAlreadyExistsException && lastBytes != null) {
+ is Err<Throwable> -> when {
+ result.error is KeyAlreadyExistsException && lastBytes != null ->
MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.pgp_key_import_failed))
.setMessage(getString(R.string.pgp_key_import_failed_replace_message))
@@ -96,14 +114,21 @@ class PGPKeyImportActivity : AppCompatActivity() {
.setNegativeButton(R.string.dialog_no) { _, _ -> finish() }
.setCancelable(false)
.show()
- } else {
+ result.error is NoSecretKeyException && lastBytes != null ->
+ MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.pgp_key_import_failed_no_secret)
+ .setMessage(R.string.pgp_key_import_failed_no_secret_message)
+ .setPositiveButton(R.string.dialog_yes) { _, _ -> pairDevice(lastBytes!!) }
+ .setNegativeButton(R.string.dialog_no) { _, _ -> finish() }
+ .setCancelable(false)
+ .show()
+ else ->
MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.pgp_key_import_failed))
- .setMessage(result.error.message)
+ .setMessage(result.error.message + "\n" + result.error.stackTraceToString())
.setPositiveButton(android.R.string.ok) { _, _ -> finish() }
.setCancelable(false)
.show()
- }
}
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9997f6aa..63f47f27 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -332,6 +332,7 @@
<string name="select_gpg_key_title">Select\nGPG Key</string>
<string name="select_gpg_key_message">Select a GPG key to initialize your store with</string>
<string name="gpg_key_select">Select key</string>
+ <string name="pair_hardware_key">Pair hardware key</string>
<!-- SSH port validation -->
<string name="ssh_scheme_needed_title">Potentially incorrect URL</string>
@@ -358,6 +359,8 @@
<string name="password_list_fab_content_description">Create new password or folder</string>
<string name="pgp_key_import_failed">Failed to import PGP key</string>
<string name="pgp_key_import_failed_replace_message">An existing key with this ID was found, do you want to replace it?</string>
+ <string name="pgp_key_import_failed_no_secret">No secret PGP key</string>
+ <string name="pgp_key_import_failed_no_secret_message">This is a public key. Would you like to pair a hardware security device?</string>
<string name="pgp_key_import_succeeded">Successfully imported PGP key</string>
<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>
<string name="pref_category_pgp_title">PGP settings</string>
diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt
index d1487903..bd303e66 100644
--- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt
+++ b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt
@@ -9,7 +9,15 @@ import app.passwordstore.crypto.GpgIdentifier.KeyId
import app.passwordstore.crypto.GpgIdentifier.UserId
import com.github.michaelbull.result.get
import com.github.michaelbull.result.runCatching
+import java.io.ByteArrayOutputStream
+import org.bouncycastle.bcpg.GnuExtendedS2K
+import org.bouncycastle.bcpg.S2K
+import org.bouncycastle.bcpg.SecretKeyPacket
+import org.bouncycastle.bcpg.SecretSubkeyPacket
+import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags
import org.bouncycastle.openpgp.PGPKeyRing
+import org.bouncycastle.openpgp.PGPPublicKey
+import org.bouncycastle.openpgp.PGPPublicKeyRing
import org.bouncycastle.openpgp.PGPSecretKey
import org.bouncycastle.openpgp.PGPSecretKeyRing
import org.pgpainless.algorithm.EncryptionPurpose
@@ -37,6 +45,29 @@ public object KeyUtils {
val keyRing = tryParseKeyring(key) ?: return null
return UserId(keyRing.publicKey.userIDs.next())
}
+
+ public fun tryCreateStubKey(
+ publicKey: PGPKey,
+ serial: ByteArray,
+ stubFingerprints: List<OpenPgpFingerprint>
+ ): PGPKey? {
+ val keyRing = tryParseKeyring(publicKey) as? PGPPublicKeyRing ?: return null
+ val secretKeyRing =
+ keyRing
+ .fold(PGPSecretKeyRing(emptyList())) { ring, key ->
+ PGPSecretKeyRing.insertSecretKey(
+ ring,
+ if (stubFingerprints.any { it == OpenPgpFingerprint.parseFromBinary(key.fingerprint) }) {
+ toCardSecretKey(key, serial)
+ } else {
+ toDummySecretKey(key)
+ }
+ )
+ }
+
+ return PGPKey(secretKeyRing.encoded)
+ }
+
public fun tryGetEncryptionKeyFingerprint(key: PGPKey): OpenPgpFingerprint? {
val keyRing = tryParseKeyring(key) ?: return null
val encryptionSubkey =
@@ -59,3 +90,63 @@ public object KeyUtils {
return info.getSecretKey(encryptionKey.keyID)
}
}
+
+private fun toDummySecretKey(publicKey: PGPPublicKey): PGPSecretKey {
+
+ return PGPSecretKey(
+ if (publicKey.isMasterKey) {
+ SecretKeyPacket(
+ publicKey.publicKeyPacket,
+ SymmetricKeyAlgorithmTags.NULL,
+ SecretKeyPacket.USAGE_CHECKSUM,
+ GnuExtendedS2K(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY),
+ byteArrayOf(),
+ byteArrayOf()
+ )
+ } else {
+ SecretSubkeyPacket(
+ publicKey.publicKeyPacket,
+ SymmetricKeyAlgorithmTags.NULL,
+ SecretKeyPacket.USAGE_CHECKSUM,
+ GnuExtendedS2K(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY),
+ byteArrayOf(),
+ byteArrayOf()
+ )
+ },
+ publicKey
+ )
+}
+
+@Suppress("MagicNumber")
+private fun toCardSecretKey(publicKey: PGPPublicKey, serial: ByteArray): PGPSecretKey {
+ return PGPSecretKey(
+ if (publicKey.isMasterKey) {
+ SecretKeyPacket(
+ publicKey.publicKeyPacket,
+ SymmetricKeyAlgorithmTags.NULL,
+ SecretKeyPacket.USAGE_CHECKSUM,
+ GnuExtendedS2K(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD),
+ ByteArray(8),
+ encodeSerial(serial),
+ )
+ } else {
+ SecretSubkeyPacket(
+ publicKey.publicKeyPacket,
+ SymmetricKeyAlgorithmTags.NULL,
+ SecretKeyPacket.USAGE_CHECKSUM,
+ GnuExtendedS2K(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD),
+ ByteArray(8),
+ encodeSerial(serial),
+ )
+ },
+ publicKey
+ )
+}
+
+@Suppress("MagicNumber")
+private fun encodeSerial(serial: ByteArray): ByteArray {
+ val out = ByteArrayOutputStream()
+ out.write(serial.size)
+ out.write(serial, 0, minOf(16, serial.size))
+ return out.toByteArray()
+}
diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt
index be2ec474..786694d4 100644
--- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt
+++ b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt
@@ -9,12 +9,12 @@ package app.passwordstore.crypto
import androidx.annotation.VisibleForTesting
import app.passwordstore.crypto.KeyUtils.tryGetId
import app.passwordstore.crypto.KeyUtils.tryParseKeyring
-import app.passwordstore.crypto.errors.InvalidKeyException
import app.passwordstore.crypto.errors.KeyAlreadyExistsException
import app.passwordstore.crypto.errors.KeyDeletionFailedException
import app.passwordstore.crypto.errors.KeyDirectoryUnavailableException
import app.passwordstore.crypto.errors.KeyNotFoundException
import app.passwordstore.crypto.errors.NoKeysAvailableException
+import app.passwordstore.crypto.errors.NoSecretKeyException
import app.passwordstore.util.coroutines.runSuspendCatching
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.unwrap
@@ -40,12 +40,17 @@ constructor(
withContext(dispatcher) {
runSuspendCatching {
if (!keyDirExists()) throw KeyDirectoryUnavailableException
- val incomingKeyRing = tryParseKeyring(key) ?: throw InvalidKeyException
+ val incomingKeyRing = tryParseKeyring(key)
+
+ if (incomingKeyRing is PGPPublicKeyRing) {
+ throw NoSecretKeyException(tryGetId(key)?.toString() ?: "Failed to retrieve key ID")
+ }
+
val keyFile = File(keyDir, "${tryGetId(key)}.$KEY_EXTENSION")
if (keyFile.exists()) {
val existingKeyBytes = keyFile.readBytes()
val existingKeyRing =
- tryParseKeyring(PGPKey(existingKeyBytes)) ?: throw InvalidKeyException
+ tryParseKeyring(PGPKey(existingKeyBytes))
when {
existingKeyRing is PGPPublicKeyRing && incomingKeyRing is PGPSecretKeyRing -> {
keyFile.writeBytes(key.contents)
diff --git a/crypto-pgpainless/src/main/kotlin/org/bouncycastle/bcpg/GnuExtendedS2K.kt b/crypto-pgpainless/src/main/kotlin/org/bouncycastle/bcpg/GnuExtendedS2K.kt
new file mode 100644
index 00000000..a5ebad78
--- /dev/null
+++ b/crypto-pgpainless/src/main/kotlin/org/bouncycastle/bcpg/GnuExtendedS2K.kt
@@ -0,0 +1,17 @@
+package org.bouncycastle.bcpg
+
+/**
+ * Add a constructor for GNU-extended S2K
+ *
+ * This extension is documented on GnuPG documentation DETAILS file,
+ * section "GNU extensions to the S2K algorithm". Its support is
+ * already present in S2K class but lack for a constructor.
+ *
+ * @author LĂ©onard Dallot <leonard.dallot@taztag.com>
+ */
+public class GnuExtendedS2K(mode: Int) : S2K(SIMPLE) {
+ init {
+ this.type = GNU_DUMMY_S2K
+ this.protectionMode = mode
+ }
+}