Skip to content

Commit d1c2680

Browse files
committed
Merge PR #2021
2 parents 862eaa6 + 455e917 commit d1c2680

File tree

3 files changed

+60
-36
lines changed

3 files changed

+60
-36
lines changed

android/app/src/main/kotlin/com/yubico/authenticator/fido/PersistentPinUvAuthTokenStore.kt

Lines changed: 58 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package com.yubico.authenticator.fido
1919
import android.content.Context
2020
import android.security.keystore.KeyGenParameterSpec
2121
import android.security.keystore.KeyProperties
22+
import org.slf4j.Logger
23+
import org.slf4j.LoggerFactory
2224
import java.io.File
2325
import java.security.KeyStore
2426
import javax.crypto.Cipher
@@ -37,6 +39,8 @@ class PersistentPinUvAuthTokenStore(private val context: Context) {
3739
private const val FILE_NAME = "ppuat.enc"
3840
private const val PAIR_SEPARATOR = "|"
3941
private const val ENTRY_SEPARATOR = ";"
42+
private val logger: Logger =
43+
LoggerFactory.getLogger(PersistentPinUvAuthTokenStore::class.java)
4044
}
4145

4246
// Add or update a token for an identifier
@@ -63,29 +67,34 @@ class PersistentPinUvAuthTokenStore(private val context: Context) {
6367
}
6468
}
6569

66-
private fun getOrCreateSecretKey(): SecretKey {
67-
val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
68-
keyStore.load(null)
69-
val entry = keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry
70-
return entry?.secretKey ?: run {
71-
val keyGenerator =
72-
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE)
73-
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
74-
KEY_ALIAS,
75-
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
76-
)
77-
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
78-
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
79-
.setKeySize(256)
80-
.build()
81-
keyGenerator.init(keyGenParameterSpec)
82-
keyGenerator.generateKey()
70+
private fun getSecretKey(): SecretKey? {
71+
val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE).apply { load(null) }
72+
return try {
73+
(keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry)?.secretKey
74+
} catch (e: Exception) {
75+
logger.error("Failed to get secret key: ", e)
76+
null
8377
}
8478
}
8579

80+
private fun createSecretKey(): SecretKey {
81+
val keyGenerator =
82+
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE)
83+
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
84+
KEY_ALIAS,
85+
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
86+
)
87+
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
88+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
89+
.setKeySize(256)
90+
.build()
91+
keyGenerator.init(keyGenParameterSpec)
92+
return keyGenerator.generateKey()
93+
}
94+
8695
// Serialize the map as "identifier1|ppuat1;identifier2|ppuat2;..."
8796
private fun saveTokens(context: Context, tokens: Map<String, String>) {
88-
val secretKey = getOrCreateSecretKey()
97+
val secretKey = getSecretKey() ?: createSecretKey()
8998
val cipher = Cipher.getInstance(TRANSFORMATION)
9099
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
91100
val iv = cipher.iv
@@ -105,23 +114,38 @@ class PersistentPinUvAuthTokenStore(private val context: Context) {
105114
private fun loadTokens(context: Context): Map<String, String> {
106115
val file = File(context.filesDir, FILE_NAME)
107116
if (!file.exists()) return emptyMap()
108-
file.inputStream().use { fis ->
109-
val ivSize = fis.read()
110-
val iv = ByteArray(ivSize)
111-
fis.read(iv)
112-
val cipher = Cipher.getInstance(TRANSFORMATION)
113-
val secretKey = getOrCreateSecretKey()
114-
cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, iv))
115-
CipherInputStream(fis, cipher).use { cis ->
116-
val data = cis.readBytes().toString(Charsets.UTF_8)
117-
if (data.isBlank()) return emptyMap()
118-
return data.split(ENTRY_SEPARATOR)
119-
.mapNotNull {
120-
val parts = it.split(PAIR_SEPARATOR, limit = 2)
121-
if (parts.size == 2) parts[0] to parts[1] else null
122-
}
123-
.toMap()
117+
118+
// Try to get the key. If it doesn't exist, we have stale data.
119+
val secretKey = getSecretKey() ?: run {
120+
logger.warn("PPUAT file exists, but Keystore key is missing. Deleting stale file.")
121+
file.delete()
122+
return emptyMap()
123+
}
124+
125+
return try {
126+
file.inputStream().use { fis ->
127+
val ivSize = fis.read()
128+
if (ivSize == -1) return emptyMap() // Empty file
129+
val iv = ByteArray(ivSize)
130+
fis.read(iv)
131+
val cipher = Cipher.getInstance(TRANSFORMATION)
132+
cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, iv))
133+
CipherInputStream(fis, cipher).use { cis ->
134+
val data = cis.readBytes().toString(Charsets.UTF_8)
135+
if (data.isBlank()) return emptyMap()
136+
return data.split(ENTRY_SEPARATOR)
137+
.mapNotNull {
138+
val parts = it.split(PAIR_SEPARATOR, limit = 2)
139+
if (parts.size == 2) parts[0] to parts[1] else null
140+
}
141+
.toMap()
142+
}
124143
}
144+
} catch (e: Exception) {
145+
// This catches AEADBadTagException and other potential IOExceptions during decryption.
146+
logger.error("Failed to decrypt PPUAT file, deleting it. ", e)
147+
file.delete()
148+
emptyMap()
125149
}
126150
}
127151
}

lib/version.dart

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
1818

1919
# This field is updated by running ./set-version.py <version>
2020
# DO NOT MANUALLY EDIT THIS!
21-
version: 7.3.0+70300
21+
version: 7.3.0+70301
2222

2323
environment:
2424
sdk: ^3.8.0

0 commit comments

Comments
 (0)