From a716ac9514577110481434ac2afdc64d97e02375 Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sun, 9 Oct 2022 16:11:28 -0700 Subject: Quick and dirty hardware key import --- .../passwordstore/ui/pgp/PGPKeyImportActivity.kt | 37 +++++++-- app/src/main/res/values/strings.xml | 3 + .../kotlin/app/passwordstore/crypto/KeyUtils.kt | 91 ++++++++++++++++++++++ .../app/passwordstore/crypto/PGPKeyManager.kt | 11 ++- .../kotlin/org/bouncycastle/bcpg/GnuExtendedS2K.kt | 17 ++++ 5 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 crypto-pgpainless/src/main/kotlin/org/bouncycastle/bcpg/GnuExtendedS2K.kt 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) { when (result) { is Ok -> { @@ -85,8 +103,8 @@ class PGPKeyImportActivity : AppCompatActivity() { .setCancelable(false) .show() } - is Err -> { - if (result.error is KeyAlreadyExistsException && lastBytes != null) { + is Err -> 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 @@ Select\nGPG Key Select a GPG key to initialize your store with Select key + Pair hardware key Potentially incorrect URL @@ -358,6 +359,8 @@ Create new password or folder Failed to import PGP key An existing key with this ID was found, do you want to replace it? + No secret PGP key + This is a public key. Would you like to pair a hardware security device? Successfully imported PGP key The key ID of the imported key is given below, please review it for correctness:\n%1$s PGP settings 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 + ): 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 + */ +public class GnuExtendedS2K(mode: Int) : S2K(SIMPLE) { + init { + this.type = GNU_DUMMY_S2K + this.protectionMode = mode + } +} -- cgit v1.2.3