From 87b7d7eb324da9ed3d5bab54592c53bf4dd9aaa5 Mon Sep 17 00:00:00 2001 From: Alexander Grahn Date: Thu, 30 Jul 2026 18:13:16 +0200 Subject: [PATCH] one unlock PIN per PGP ID --- CHANGELOG.md | 2 + README.md | 2 +- .../java/app/passwordstore/Application.kt | 4 +- .../injection/prefs/PreferenceModule.kt | 5 + .../injection/prefs/as UnlockPins.kt | 10 + .../ui/crypto/BasePGPActivity.kt | 176 +++++++++++------- .../app/passwordstore/ui/crypto/PinDialog.kt | 26 ++- .../ui/settings/PasswordSettings.kt | 8 +- .../util/extensions/AndroidExtensions.kt | 4 + .../passwordstore/util/settings/Migrations.kt | 9 +- .../util/settings/MigrationsTest.kt | 27 +++ 11 files changed, 195 insertions(+), 78 deletions(-) create mode 100644 app/src/main/java/app/passwordstore/injection/prefs/as UnlockPins.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index c9755c809f..e8e466ed41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file ### Fixed - Autofill fast unlocking setup: PIN setting dialog/biometric prompt were not shown after passphrase verification +- Shortcuts on the home screen (pinned passwords) were pointing to the containing folder, not the actual password +- There is now one fast unlocking PIN per PGP ID to prevent password leakage in case of folders that were initialised with different PGP IDs ## [2.0.0] - 2026-07-26 diff --git a/README.md b/README.md index 2e5c727a2d..bb3b170916 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The original documentation can be found [here](https://docs.passwordstore.app) a To activate passkey (Android 14+) and autofill support, go to Settings → Autofill & Passkeys and choose Password Store as your preferred service. For Chrome and Chromium-based browsers, you might additionally need to enable "Autofill using another service" within the browser's own settings. -Utilizing the standard `pass` file structure, passkey data is stored on the first line, followed by optional extra content, as line-oriented plain text secured by PGP encryption. Details on passkey encoding and storage are given [here](PasskeyStorage.md). +Utilising the standard `pass` file structure, passkey data is stored on the first line, followed by optional extra content, as line-oriented plain text secured by PGP encryption. Details on passkey encoding and storage are given in file [`PasskeyStorage.md`](PasskeyStorage.md). ## How-To: Transfer a PGP key to Password Store securely diff --git a/app/src/main/java/app/passwordstore/Application.kt b/app/src/main/java/app/passwordstore/Application.kt index 9a5732d510..76782eb167 100644 --- a/app/src/main/java/app/passwordstore/Application.kt +++ b/app/src/main/java/app/passwordstore/Application.kt @@ -20,6 +20,7 @@ import android.view.WindowInsetsController import androidx.appcompat.app.AppCompatDelegate import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.injection.context.FilesDirPath +import app.passwordstore.injection.prefs.PGPPassphrases import app.passwordstore.injection.prefs.SettingsPreferences import app.passwordstore.ui.crypto.BasePGPActivity.Companion.cachedPassphrases import app.passwordstore.util.coroutines.DispatcherProvider @@ -47,6 +48,7 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere @Inject @SettingsPreferences lateinit var prefs: SharedPreferences @Inject @FilesDirPath lateinit var filesDirPath: String + @Inject @PGPPassphrases lateinit var persistentPassphrases: SharedPreferences @Inject lateinit var dispatcherProvider: DispatcherProvider @Inject lateinit var gitSettings: GitSettings @Inject lateinit var proxyUtils: ProxyUtils @@ -73,7 +75,7 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere prefs.registerOnSharedPreferenceChangeListener(this) setNightMode() - runMigrations(filesDirPath, prefs, gitSettings) + runMigrations(filesDirPath, prefs, gitSettings, persistentPassphrases) proxyUtils.setDefaultProxy() DynamicColors.applyToActivitiesIfAvailable(this) setupScreenOffHandler() diff --git a/app/src/main/java/app/passwordstore/injection/prefs/PreferenceModule.kt b/app/src/main/java/app/passwordstore/injection/prefs/PreferenceModule.kt index a75b3f8bab..88246dc4bc 100644 --- a/app/src/main/java/app/passwordstore/injection/prefs/PreferenceModule.kt +++ b/app/src/main/java/app/passwordstore/injection/prefs/PreferenceModule.kt @@ -42,6 +42,11 @@ class PreferenceModule { return context.getSharedPreferences("${BuildConfig.APPLICATION_ID}_passphrases", MODE_PRIVATE) } + @[Provides UnlockPins Reusable] + fun provideUnlockPins(@ApplicationContext context: Context): SharedPreferences { + return context.getSharedPreferences("${BuildConfig.APPLICATION_ID}_unlock_pins", MODE_PRIVATE) + } + @[Provides GitSecrets Reusable] fun provideGitSecrets(@ApplicationContext context: Context): SharedPreferences { return context.getSharedPreferences("${BuildConfig.APPLICATION_ID}_git_secrets", MODE_PRIVATE) diff --git a/app/src/main/java/app/passwordstore/injection/prefs/as UnlockPins.kt b/app/src/main/java/app/passwordstore/injection/prefs/as UnlockPins.kt new file mode 100644 index 0000000000..c36a2a4c4d --- /dev/null +++ b/app/src/main/java/app/passwordstore/injection/prefs/as UnlockPins.kt @@ -0,0 +1,10 @@ +/* + * Copyright © 2014-2026 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.injection.prefs + +import javax.inject.Qualifier + +@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class UnlockPins diff --git a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt index e20d7df8f4..9d24ae50f7 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt @@ -26,6 +26,7 @@ import app.passwordstore.data.passfile.PasswordEntry import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.injection.prefs.PGPPassphrases import app.passwordstore.injection.prefs.SettingsPreferences +import app.passwordstore.injection.prefs.UnlockPins import app.passwordstore.ui.dialogs.PasswordDialog import app.passwordstore.ui.pgp.PGPKeyListActivity import app.passwordstore.util.auth.BiometricAuthenticator @@ -57,6 +58,7 @@ import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit import javax.inject.Inject +import kotlin.math.max import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -158,6 +160,8 @@ open class BasePGPActivity : AppCompatActivity() { */ @PGPPassphrases @Inject lateinit var persistentPassphrases: SharedPreferences + @UnlockPins @Inject lateinit var unlockPins: SharedPreferences + @Inject lateinit var repository: CryptoRepository @Inject lateinit var dispatcherProvider: DispatcherProvider @@ -554,7 +558,7 @@ open class BasePGPActivity : AppCompatActivity() { ) { /* Ask user for setting a PIN if not yet existing, encrypt and store it on the * device, then update passphrase in cache */ - if (persistentPassphrases.getString("unlock_pin", null) == null) { + if (unlockPins.getString(id, null) == null) { fastUnlockingSetupCompletion = CompletableDeferred() val pinDialog = PinDialog.newInstance( @@ -566,15 +570,17 @@ open class BasePGPActivity : AppCompatActivity() { if (key == PinDialog.PIN_RESULT_KEY) { val pin = bundle.getCharArray(PinDialog.PIN_KEY) if (pin != null && pin.size >= 4) { - persistentPassphrases.edit { + unlockPins.edit { putString( - "unlock_pin", // reset and prepend PIN attempt counter + id, // reset and prepend PIN attempt counter AESEncryption.encrypt( charArrayOf('0', ':') + pin, keyType = KeyType.PERSISTENT, ) ?.concatToString(), ) + } + persistentPassphrases.edit { putString( id, AESEncryption.encrypt(passphrase, keyType = KeyType.PERSISTENT) @@ -645,12 +651,18 @@ open class BasePGPActivity : AppCompatActivity() { if ( biometrics_and_pin_timeout > 0L && now - biometrics_and_pin_last_use >= TimeUnit.DAYS.toMillis(biometrics_and_pin_timeout) - ) + ) { persistentPassphrases.edit { clear() } + unlockPins.edit { clear() } + } val persistentIds = identifiers.map { it.toString() }.filter { persistentPassphrases.contains(it) } - val pinEncrypted = persistentPassphrases.getString("unlock_pin", null)?.toCharArray() + val encryptedPins = + unlockPins + .getAll() + .filterKeys { persistentIds.contains(it) } + .mapValues { (it.value as String).toCharArray() } if ( !persistentIds.none() && identifiers.map { it.toString() }.filter { cachedPassphrases.containsKey(it) }.none() && @@ -684,13 +696,12 @@ open class BasePGPActivity : AppCompatActivity() { if (result !is BiometricResult.Retry) decrypt(identifiers) } } else if ( - !persistentIds.none() && + !encryptedPins.none() && identifiers.map { it.toString() }.filter { cachedPassphrases.containsKey(it) }.none() && AESEncryption.isHardwareBacked(KeyType.PERSISTENT) && - settings.getString(PreferenceKeys.PREF_FAST_UNLOCK_OPTION, "disabled") == "PIN" && - pinEncrypted != null + settings.getString(PreferenceKeys.PREF_FAST_UNLOCK_OPTION, "disabled") == "PIN" ) { - verifyPin(pinEncrypted, persistentIds, identifiers, action) + verifyPin(encryptedPins, identifiers, action) } else { decrypt(identifiers) } @@ -698,8 +709,7 @@ open class BasePGPActivity : AppCompatActivity() { /* Asks for and verifies the user PIN for unlocking a store entry. */ private fun verifyPin( - pinEncrypted: CharArray, - ids: List, + encryptedPins: Map, identifiers: List, action: String?, isError: Boolean = false, @@ -718,69 +728,107 @@ open class BasePGPActivity : AppCompatActivity() { pinDialog.show(supportFragmentManager, "PIN_DIALOG") pinDialog.setFragmentResultListener(PinDialog.PIN_RESULT_KEY) { key, bundle -> if (key == PinDialog.PIN_RESULT_KEY) { - val pin = requireNotNull(bundle.getCharArray(PinDialog.PIN_KEY)) { "returned PIN is null" } - var (pinRetries, cachedPin) = - AESEncryption.decrypt(pinEncrypted, keyType = KeyType.PERSISTENT)?.let { cached -> - if (cached[1] == ':') { - Pair(cached[0].digitToInt(), cached.filterIndexed { i, _ -> i > 1 }.toCharArray()) - } else { - // fix PIN cache that does not have an attempt count prepended (old app version) - persistentPassphrases.edit { - putString( - "unlock_pin", - AESEncryption.encrypt( - charArrayOf('0', ':') + cached, - keyType = KeyType.PERSISTENT, + if (bundle.getBoolean(PinDialog.PIN_CANCEL)) + decrypt(identifiers) // decrypt with passphrase verification + else { + val pin = + requireNotNull(bundle.getCharArray(PinDialog.PIN_KEY)) { "returned PIN is null" } + var pinRetries = 0 + + var pinOk = false + // verify user-entered PIN against cached PINs + for ((id, encryptedPin) in encryptedPins) { + var cachedPin = + AESEncryption.decrypt(encryptedPin, keyType = KeyType.PERSISTENT)?.let { cached -> + cached.copyOfRange(cached.indexOf(':') + 1, cached.size).also { + pinRetries = + max( + pinRetries, + cached.copyOfRange(0, cached.indexOf(':')).concatToString().toIntOrNull() + ?: MAX_RETRIES, ) - ?.concatToString(), - ) + cached?.wipe() + } } - Pair(0, cached) + pinOk = cachedPin?.let { it.contentEquals(pin) } ?: false + cachedPin?.wipe() + if (pinOk) { + // PIN verifies successfully against one of the cached ones + updatePinAttemptCounter(encryptedPins, 0) // reset attempt counter + // re-encrypt and cache passphrase temporarily for use until screen-off + persistentPassphrases + .getString(id, null) + ?.toCharArray() + ?.let { passEncrypted -> + AESEncryption.decrypt(passEncrypted, keyType = KeyType.PERSISTENT) + } + ?.let { pass -> + AESEncryption.encrypt(pass)?.let { + cachedPassphrases.put(id, it) + } + pass.wipe() + } + break } - } ?: Pair(MAX_RETRIES, null) - if (cachedPin?.let { it.contentEquals(pin) } ?: false) { // PIN verifies successfully - persistentPassphrases.edit { - putString( - "unlock_pin", // reset to zero and prepend attempt counter - AESEncryption.encrypt(charArrayOf('0', ':') + pin, keyType = KeyType.PERSISTENT) - ?.concatToString(), - ) - putLong(PreferenceKeys.BIOMETRICS_AND_PIN_LAST_USE, Instant.now().toEpochMilli()) - } - ids.forEach { id -> - val passEncrypted = persistentPassphrases.getString(id, null)?.toCharArray() - val pass = - // re-encrypt passphrase for use until screen-off - AESEncryption.encrypt( - // decrypt persistently cached passphrase - AESEncryption.decrypt(passEncrypted, keyType = KeyType.PERSISTENT) - ) - pass?.let { cachedPassphrases.put(id, it) } } - decrypt(identifiers) - } else if ( - cachedPin != null && ++pinRetries < MAX_RETRIES - ) { // PIN verification failed, try again - val pinEncryptedUpdate = - AESEncryption.encrypt( - charArrayOf(pinRetries.digitToChar(), ':') + cachedPin, - keyType = KeyType.PERSISTENT, - ) - pinEncryptedUpdate?.let { // update PIN cache with incremented attempt counter - persistentPassphrases.edit { - putString("unlock_pin", pinEncryptedUpdate.concatToString()) + + pin.wipe() + + if (pinOk) decrypt(identifiers) + else { + if (++pinRetries < MAX_RETRIES) { // try again + val encryptedPinsUpdated = updatePinAttemptCounter(encryptedPins, pinRetries) + verifyPin(encryptedPinsUpdated, identifiers, action, isError = true) + } else { + // reset PIN and cached passphrase(s) to prevent bruteforcing + encryptedPins.keys.forEach { id -> + cachedPassphrases.remove(id) + persistentPassphrases.edit { remove(id) } + unlockPins.edit { remove(id) } + } + decrypt(identifiers) } - verifyPin(pinEncryptedUpdate, ids, identifiers, action, isError = true) - } ?: throw NullPointerException() - } else { // PIN verification failed, do not try again - persistentPassphrases.edit { clear() } // reset PIN to prevent bruteforcing - decrypt(identifiers) // decrypt with passphrase verification + } } - pin.wipe() } } } + // updates attempt counter and prepends it to the cached PINs + private fun updatePinAttemptCounter( + encryptedPins: Map, + attempts: Int, + ): Map { + var updatedEncryptedPins = mutableMapOf() + unlockPins.edit { + encryptedPins.forEach { id, encryptedPin -> + AESEncryption.decrypt(encryptedPin, keyType = KeyType.PERSISTENT) + ?.let { cached -> + cached.copyOfRange(cached.indexOf(':') + 1, cached.size).also { cached.wipe() } + } + ?.let { pin -> + AESEncryption.encrypt( + (attempts.toString() + ":").toCharArray() + pin, + keyType = KeyType.PERSISTENT, + ) + ?.let { updated -> + putString(id, updated.concatToString()) + updatedEncryptedPins.put(id, updated) + } + pin?.wipe() + } + ?: run { + remove(id) + } + } + } + if (attempts == 0) + persistentPassphrases.edit { + putLong(PreferenceKeys.BIOMETRICS_AND_PIN_LAST_USE, Instant.now().toEpochMilli()) + } + return updatedEncryptedPins + } + protected fun decrypt(identifiers: List, isError: Boolean = false) { val passphrases = cachedPassphrases.filterKeys { identifiers.map { it.toString() }.contains(it) diff --git a/app/src/main/java/app/passwordstore/ui/crypto/PinDialog.kt b/app/src/main/java/app/passwordstore/ui/crypto/PinDialog.kt index d1e232efc7..73947e5498 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/PinDialog.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/PinDialog.kt @@ -24,6 +24,11 @@ class PinDialog : DialogFragment() { private val binding by unsafeLazy { DialogPinEntryBinding.inflate(layoutInflater) } private var isError: Boolean = false + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + isCancelable = false // disable tapping BACK / fragment cancel + } + override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog { val builder = MaterialAlertDialogBuilder(requireContext()) builder.setView(binding.root) @@ -35,9 +40,13 @@ class PinDialog : DialogFragment() { binding.descriptionText.setText(descriptionText) builder.setPositiveButton(android.R.string.ok) { _, _ -> setPinAndDismiss() } + builder.setNegativeButton(android.R.string.cancel) { d, _ -> d.cancel() } + val dialog = builder.create() + + dialog.setCanceledOnTouchOutside(false) + dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) - dialog.setCancelable(false) dialog.setOnShowListener { var pinLength = 0 if (isError) { @@ -55,9 +64,9 @@ class PinDialog : DialogFragment() { setOnKeyListener { _, keyCode, _ -> if (keyCode == KeyEvent.KEYCODE_ENTER && pinLength >= 4) { setPinAndDismiss() - return@setOnKeyListener true + return@setOnKeyListener true // return true to consume Enter } - false + keyCode == KeyEvent.KEYCODE_BACK // return true to consume Back } } } @@ -79,12 +88,18 @@ class PinDialog : DialogFragment() { override fun onCancel(dialog: DialogInterface) { super.onCancel(dialog) - setFragmentResult(PIN_RESULT_KEY, Bundle()) + setFragmentResult(PIN_RESULT_KEY, Bundle().also { it.putBoolean(PIN_CANCEL, true) }) } private fun setPinAndDismiss() { val pin = binding.pinEditText.text?.let { CharArray(it.length) { i -> it[i] } } - setFragmentResult(PIN_RESULT_KEY, Bundle().also { it.putCharArray(PIN_KEY, pin) }) + setFragmentResult( + PIN_RESULT_KEY, + Bundle().also { + it.putCharArray(PIN_KEY, pin) + it.putBoolean(PIN_CANCEL, false) + }, + ) dismissAllowingStateLoss() } @@ -95,6 +110,7 @@ class PinDialog : DialogFragment() { const val PIN_RESULT_KEY = "pin_result" const val PIN_KEY = "pin" + const val PIN_CANCEL = "cancel" fun newInstance( title: String, diff --git a/app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt b/app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt index b4f7aadffb..1e97d72ead 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt @@ -5,15 +5,14 @@ package app.passwordstore.ui.settings -import android.content.SharedPreferences import android.text.InputType import androidx.core.content.edit import androidx.fragment.app.FragmentActivity import app.passwordstore.R -import app.passwordstore.injection.prefs.PGPPassphrases import app.passwordstore.util.auth.BiometricAuthenticator import app.passwordstore.util.extensions.persistentPassphrases import app.passwordstore.util.extensions.sharedPrefs +import app.passwordstore.util.extensions.unlockPins import app.passwordstore.util.settings.PreferenceKeys import de.Maxr1998.modernpreferences.PreferenceScreen import de.Maxr1998.modernpreferences.helpers.editText @@ -21,12 +20,9 @@ import de.Maxr1998.modernpreferences.helpers.onClick import de.Maxr1998.modernpreferences.helpers.singleChoice import de.Maxr1998.modernpreferences.helpers.switch import de.Maxr1998.modernpreferences.preferences.choice.SelectionItem -import javax.inject.Inject class PasswordSettings(private val activity: FragmentActivity) : SettingsProvider { - @PGPPassphrases @Inject lateinit var persistentPassphrases: SharedPreferences - override fun provideSettings(builder: PreferenceScreen.Builder) { builder.apply { val values = activity.resources.getStringArray(R.array.pwgen_provider_values) @@ -57,6 +53,7 @@ class PasswordSettings(private val activity: FragmentActivity) : SettingsProvide initialSelection = "disabled" onClick { activity.persistentPassphrases.edit { clear() } + activity.unlockPins.edit { clear() } true } } @@ -68,6 +65,7 @@ class PasswordSettings(private val activity: FragmentActivity) : SettingsProvide textInputType = InputType.TYPE_CLASS_NUMBER onClick { activity.persistentPassphrases.edit { clear() } + activity.unlockPins.edit { clear() } true } } diff --git a/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt b/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt index db0100f827..8dfb4fe781 100644 --- a/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt +++ b/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt @@ -54,6 +54,10 @@ val Context.sharedPrefs: SharedPreferences val Context.persistentPassphrases: SharedPreferences get() = getSharedPreferences("${BuildConfig.APPLICATION_ID}_passphrases", MODE_PRIVATE) +/** Get the persistent unlock PINs [SharedPreferences] instance */ +val Context.unlockPins: SharedPreferences + get() = getSharedPreferences("${BuildConfig.APPLICATION_ID}_unlock_pins", MODE_PRIVATE) + /** Get the persistent Git server secrets [SharedPreferences] instance */ val Context.gitSecrets: SharedPreferences get() = getSharedPreferences("${BuildConfig.APPLICATION_ID}_git_secrets", MODE_PRIVATE) diff --git a/app/src/main/java/app/passwordstore/util/settings/Migrations.kt b/app/src/main/java/app/passwordstore/util/settings/Migrations.kt index dcbf70e7c8..b739f498e9 100644 --- a/app/src/main/java/app/passwordstore/util/settings/Migrations.kt +++ b/app/src/main/java/app/passwordstore/util/settings/Migrations.kt @@ -31,6 +31,7 @@ fun runMigrations( filesDirPath: String, sharedPrefs: SharedPreferences, gitSettings: GitSettings, + persistentPassphrases: SharedPreferences, context: Context = Application.instance.applicationContext, runTest: Boolean = false, ) { @@ -43,7 +44,7 @@ fun runMigrations( removePersistentCredentialCache(sharedPrefs, gitSettings, context, runTest) if (!runTest) moveToPasswordGeneratorPrefs(sharedPrefs, context) deleteKeystoreWrappedEd25519Key(sharedPrefs, context) - migrateToFastUnlockOptions(sharedPrefs) + migrateToFastUnlockOptions(sharedPrefs, persistentPassphrases) } private fun deleteKeystoreWrappedEd25519Key(sharedPrefs: SharedPreferences, context: Context) { @@ -295,7 +296,11 @@ private fun createEncryptedPreferences(context: Context, fileName: String): Shar ) } -private fun migrateToFastUnlockOptions(sharedPrefs: SharedPreferences) { +private fun migrateToFastUnlockOptions( + sharedPrefs: SharedPreferences, + persistentPassphrases: SharedPreferences, +) { + persistentPassphrases.edit { remove("unlock_pin") } sharedPrefs.edit { if (sharedPrefs.getBoolean(PreferenceKeys.UNLOCK_PASSWORDS_WITH_PIN, false)) putString(PreferenceKeys.PREF_FAST_UNLOCK_OPTION, "fingerprint") diff --git a/app/src/test/java/app/passwordstore/util/settings/MigrationsTest.kt b/app/src/test/java/app/passwordstore/util/settings/MigrationsTest.kt index c3d83b7a1e..04463d3b70 100644 --- a/app/src/test/java/app/passwordstore/util/settings/MigrationsTest.kt +++ b/app/src/test/java/app/passwordstore/util/settings/MigrationsTest.kt @@ -44,6 +44,7 @@ class MigrationsTest { assertNull(getString(PreferenceKeys.GIT_REMOTE_SERVER)) assertNull(getString(PreferenceKeys.GIT_REMOTE_LOCATION)) assertNull(getString(PreferenceKeys.GIT_REMOTE_PROTOCOL)) + assertNull(getString("unlock_pin")) } @Test @@ -60,6 +61,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -83,6 +85,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -106,6 +109,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -122,6 +126,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -136,6 +141,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -150,6 +156,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -164,6 +171,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -177,6 +185,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -193,6 +202,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -211,10 +221,27 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) assertFalse { sharedPrefs.contains(PreferenceKeys.CLEAR_PASSPHRASE_CACHE) } assertFalse { sharedPrefs.contains(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE) } } + + @Test + fun verifyPersistentUnlockPin() { + sharedPrefs.edit { + putString("unlock_pin", "abcdefg") + } + runMigrations( + filesDir, + sharedPrefs, + GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, + context, + runTest = true, + ) + checkOldKeysAreRemoved() + } }