mirror of
https://github.com/android-password-store/Android-Password-Store.git
synced 2026-04-11 00:41:10 +02:00
Quick and dirty hardware key import
This commit is contained in:
parent
75040136ae
commit
a716ac9514
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue