Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file

## [Unreleased]

### Fixed

- Passkey sign-in no longer fails for passphrase-protected PGP keys when the passphrase is cached with biometric Fast Unlock

## [1.25.2] - 2026-09-29

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler
import androidx.lifecycle.lifecycleScope
import app.passwordstore.R as AppR
import app.passwordstore.crypto.PGPIdentifier
import app.passwordstore.data.crypto.OpenPgpActivityInteractionHandler
import app.passwordstore.data.crypto.OpenPgpInteractionCoordinator
import app.passwordstore.data.repo.PasswordRepository
Expand Down Expand Up @@ -666,8 +667,9 @@ class AppPasskeyProviderActivity : BaseGitActivity() {

private fun findFirstPersistentPassphrase(): Pair<String, CharArray>? {
val allKeys = persistentPassphrases.all
for ((keyId, value) in allKeys) {
if (keyId == "unlock_pin" || keyId == PreferenceKeys.BIOMETRICS_AND_PIN_LAST_USE) continue
for ((id, value) in allKeys) {
if (id == "unlock_pin" || id == PreferenceKeys.BIOMETRICS_AND_PIN_LAST_USE) continue
val keyId = PGPIdentifier.fromString(id)?.let(repository::getLongKeyIdFromKeyId) ?: continue
if (passphraseCache.contains(keyId)) continue
val encrypted = (value as? String)?.toCharArray() ?: continue
return keyId to encrypted
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public class PgpainlessPasskeyDecryptor(
var lastError: PasskeyDecryptionError? = null

for ((key, keyId) in matchingKeys) {
val passphrase = unlockContext.unlockKey(keyId.toString())
val passphrase = unlockContext.unlockKey(KeyUtils.tryGetKeyId(key).toString())

try {
val plaintext = attemptBoundedDecryption(key, passphrase, ciphertext, limits)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2014-2026 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package app.passwordstore.crypto

import app.passwordstore.crypto.CryptoConstants.KEY_ID
import app.passwordstore.crypto.CryptoConstants.KEY_PASSPHRASE
import app.passwordstore.crypto.CryptoConstants.PLAIN_TEXT
import app.passwordstore.crypto.PGPIdentifier.KeyId
import app.passwordstore.passkeys.crypto.PgpUnlockContext
import com.github.michaelbull.result.unwrap
import java.io.ByteArrayOutputStream
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.rules.TemporaryFolder

class PgpainlessPasskeyDecryptorTest {

@get:Rule val temporaryFolder: TemporaryFolder = TemporaryFolder()

@Test
fun `unlocks a key encrypted to its subkey with the passphrase cached by primary key ID`() =
runTest {
val cryptoHandler = PGPainlessCryptoHandler()
val keyManager = PGPKeyManager(temporaryFolder.root.absolutePath)
keyManager.addKey(PGPKey(TestUtils.getArmoredSecretKey())).unwrap()
val ciphertext = ByteArrayOutputStream()
cryptoHandler
.encrypt(
listOf(PGPKey(TestUtils.getArmoredPublicKey())),
null,
PLAIN_TEXT.byteInputStream(),
ciphertext,
PGPEncryptOptions.Builder().build(),
)
.unwrap()
val unlockContext =
object : PgpUnlockContext {
override suspend fun unlockKey(keyId: String) =
KEY_PASSPHRASE.toCharArray().takeIf { keyId == KeyId(KEY_ID).toString() }
}

val plaintext =
PgpainlessPasskeyDecryptor(cryptoHandler, keyManager)
.decryptFromBytes(ciphertext.toByteArray(), unlockContext)
.unwrap()

assertEquals(PLAIN_TEXT, plaintext.bytes().decodeToString())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ public interface PasskeyPgpDecryptor {
}

public interface PgpUnlockContext {
/** [keyId] is the key's primary key ID, as formatted by `PGPIdentifier.KeyId`. */
public suspend fun unlockKey(keyId: String): CharArray?
}
Loading