Skip to content

Commit 980cd31

Browse files
committed
Merge PR #2020
2 parents fcc3d83 + 358c679 commit 980cd31

File tree

7 files changed

+82
-19
lines changed

7 files changed

+82
-19
lines changed

android/app/src/main/kotlin/com/yubico/authenticator/piv/CertificateUtils.kt

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package com.yubico.authenticator.piv
1919
import com.yubico.yubikit.piv.KeyType
2020
import com.yubico.yubikit.piv.PivSession
2121
import com.yubico.yubikit.piv.Slot
22+
import com.yubico.yubikit.piv.jca.PivProvider
2223
import org.bouncycastle.asn1.ASN1ObjectIdentifier
2324
import org.bouncycastle.asn1.edec.EdECObjectIdentifiers
2425
import org.bouncycastle.cert.X509CertificateHolder
@@ -41,6 +42,8 @@ import java.util.Date
4142
import javax.security.auth.x500.X500Principal
4243
import java.security.cert.X509Certificate
4344
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
45+
import java.security.KeyStore
46+
import java.security.PrivateKey
4447
import java.security.SecureRandom
4548
import java.security.Signature
4649

@@ -148,10 +151,14 @@ class PivContentSigner(
148151
@Throws(OperatorCreationException::class)
149152
override fun getSignature(): ByteArray {
150153
try {
151-
val toBeSigned = buffer.toByteArray()
152-
val signature = Signature.getInstance(signatureAlgorithm.jcaName)
153-
// TODO use JCA
154-
return session.sign(slot, keyAlg, toBeSigned, signature)
154+
val provider = PivProvider(session)
155+
val keyStore = KeyStore.getInstance("YKPiv", provider)
156+
keyStore.load(null)
157+
158+
return Signature.getInstance(signatureAlgorithm.jcaName, provider).apply {
159+
initSign(keyStore.getKey(slot.stringAlias, null) as PrivateKey)
160+
update(buffer.toByteArray())
161+
}.sign()
155162
} catch (e: GeneralSecurityException) {
156163
throw OperatorCreationException("PIV signing failed", e)
157164
}

android/app/src/main/kotlin/com/yubico/authenticator/piv/PivConnectionHelper.kt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@
1717
package com.yubico.authenticator.piv
1818

1919
import com.yubico.authenticator.device.DeviceManager
20+
import com.yubico.authenticator.device.Info
21+
import com.yubico.authenticator.device.unknownDeviceWithCapability
22+
import com.yubico.authenticator.yubikit.DeviceInfoHelper.Companion.getDeviceInfo
2023
import com.yubico.authenticator.yubikit.NfcState
2124
import com.yubico.authenticator.yubikit.withConnection
25+
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
2226
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
2327
import com.yubico.yubikit.core.smartcard.SmartCardConnection
2428
import com.yubico.yubikit.core.util.Result
29+
import com.yubico.yubikit.support.DeviceUtil
2530
import org.slf4j.LoggerFactory
2631
import kotlin.coroutines.cancellation.CancellationException
2732
import kotlin.coroutines.suspendCoroutine
@@ -56,11 +61,13 @@ class PivConnectionHelper(private val deviceManager: DeviceManager) {
5661
suspend fun <T> useSmartCardConnection(
5762
onComplete: ((SmartCardConnection) -> Unit)? = null,
5863
waitForNfcKeyRemoval: Boolean = false,
64+
updateDeviceInfo: Boolean = false,
5965
block: (SmartCardConnection) -> T
6066
): T {
67+
PivManager.updateDeviceInfo.set(updateDeviceInfo)
6168
NfcState.waitForNfcKeyRemoval = waitForNfcKeyRemoval
6269
return deviceManager.withKey(
63-
onUsb = { useSmartCardConnectionUsb(it, onComplete, block) },
70+
onUsb = { useSmartCardConnectionUsb(it, onComplete, updateDeviceInfo, block) },
6471
onNfc = { useSmartCardConnectionNfc(onComplete, block) },
6572
onCancelled = {
6673
pendingAction?.invoke(Result.failure(CancellationException()))
@@ -72,9 +79,23 @@ class PivConnectionHelper(private val deviceManager: DeviceManager) {
7279
suspend fun <T> useSmartCardConnectionUsb(
7380
device: UsbYubiKeyDevice,
7481
onComplete: ((SmartCardConnection) -> Unit)? = null,
82+
updateDeviceInfo: Boolean,
7583
block: (SmartCardConnection) -> T
7684
): T = device.withConnection<SmartCardConnection, T> { connection ->
77-
block(connection).also { onComplete?.invoke(connection) }
85+
block(connection).also {
86+
onComplete?.invoke(connection)
87+
if (updateDeviceInfo) {
88+
val pid = device.pid
89+
runCatching {
90+
deviceManager.setDeviceInfo(runCatching {
91+
val deviceInfo = DeviceUtil.readInfo(connection, pid)
92+
val name = DeviceUtil.getName(deviceInfo, pid.type)
93+
Info(name, false, pid.value, deviceInfo)
94+
}.getOrNull())
95+
96+
}
97+
}
98+
}
7899
}
79100

80101
suspend fun <T> useSmartCardConnectionNfc(

android/app/src/main/kotlin/com/yubico/authenticator/piv/PivManager.kt

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import com.yubico.authenticator.piv.data.SlotMetadata
3232
import com.yubico.authenticator.piv.data.fingerprint
3333
import com.yubico.authenticator.piv.data.isoFormat
3434
import com.yubico.authenticator.setHandler
35+
import com.yubico.authenticator.yubikit.DeviceInfoHelper.Companion.getDeviceInfo
3536
import com.yubico.authenticator.yubikit.withConnection
3637
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
3738
import com.yubico.yubikit.core.YubiKeyConnection
@@ -69,6 +70,7 @@ import java.text.SimpleDateFormat
6970
import java.util.Arrays
7071
import java.util.Locale
7172
import java.util.TimeZone
73+
import java.util.concurrent.atomic.AtomicBoolean
7274

7375
typealias PivAction = (Result<SmartCardConnection, Exception>) -> Unit
7476

@@ -228,6 +230,10 @@ class PivManager(
228230
device.withConnection<SmartCardConnection, Unit> { connection ->
229231
requestHandled = processYubiKey(connection, device)
230232
}
233+
234+
if (updateDeviceInfo.getAndSet(false)) {
235+
deviceManager.setDeviceInfo(runCatching { getDeviceInfo(device) }.getOrNull())
236+
}
231237
} catch (e: Exception) {
232238

233239
logger.error("Cancelling pending action. Cause: ", e)
@@ -351,7 +357,10 @@ class PivManager(
351357

352358

353359
private suspend fun reset(): String =
354-
connectionHelper.useSmartCardConnection {
360+
connectionHelper.useSmartCardConnection(
361+
onComplete = ::updatePivState,
362+
updateDeviceInfo = true
363+
) {
355364
val piv = getPivSession(it)
356365
piv.reset()
357366
""
@@ -361,6 +370,7 @@ class PivManager(
361370
val defaultPin = "123456".toCharArray()
362371
val defaultManagementKey =
363372
"010203040506070801020304050607080102030405060708".hexToByteArray()
373+
val updateDeviceInfo = AtomicBoolean(false)
364374
}
365375

366376
private fun doAuthenticate(piv: PivSession, serial: String) =
@@ -485,7 +495,10 @@ class PivManager(
485495
}
486496

487497
private suspend fun changePin(pin: CharArray, newPin: CharArray): String =
488-
connectionHelper.useSmartCardConnection(::updatePivState) {
498+
connectionHelper.useSmartCardConnection(
499+
onComplete = ::updatePivState,
500+
updateDeviceInfo = true
501+
) {
489502
try {
490503
val piv = getPivSession(it)
491504
handlePinPukErrors { PivmanUtils.pivmanChangePin(piv, pin, newPin) }
@@ -496,7 +509,10 @@ class PivManager(
496509
}
497510

498511
private suspend fun changePuk(puk: CharArray, newPuk: CharArray): String =
499-
connectionHelper.useSmartCardConnection(::updatePivState) {
512+
connectionHelper.useSmartCardConnection(
513+
onComplete = ::updatePivState,
514+
updateDeviceInfo = true
515+
) {
500516
try {
501517
val piv = getPivSession(it)
502518
handlePinPukErrors { piv.changePuk(puk, newPuk) }
@@ -511,7 +527,10 @@ class PivManager(
511527
keyType: ManagementKeyType,
512528
storeKey: Boolean
513529
): String =
514-
connectionHelper.useSmartCardConnection(::updatePivState) {
530+
connectionHelper.useSmartCardConnection(
531+
onComplete = ::updatePivState,
532+
updateDeviceInfo = true
533+
) {
515534
val piv = getPivSession(it)
516535
doVerifyPin(piv, pivViewModel.currentSerial())
517536
doAuthenticate(piv, pivViewModel.currentSerial())
@@ -915,7 +934,17 @@ class PivManager(
915934
mapOf(
916935
"id" to slot.value,
917936
"name" to slot.stringAlias,
918-
"metadata" to pivViewModel.getMetadata(slot.stringAlias),
937+
"metadata" to pivViewModel.getMetadata(slot.stringAlias)?.let { slotMetadata ->
938+
JSONObject(
939+
mapOf(
940+
"key_type" to slotMetadata.keyType.toInt(),
941+
"pin_policy" to slotMetadata.pinPolicy,
942+
"touch_policy" to slotMetadata.touchPolicy,
943+
"generated" to slotMetadata.generated,
944+
"public_key" to slotMetadata.publicKey?.toPublicKey()?.toPem()
945+
)
946+
)
947+
},
919948
"certificate" to pivViewModel.getCertificate(slot.stringAlias)?.toPem(),
920949
)
921950
).toString()

integration_test/oath_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ extension on PatrolTester {
2424
bool requireTouch = false,
2525
}) async {
2626
if (isAndroid) {
27-
// The camera view breaks the test on Android, add programatically
27+
// The camera view breaks the test on Android, add programmatically
2828
await read(
2929
credentialListProvider(data.node.path).notifier,
3030
).addAccount(cred.toUri(), requireTouch: requireTouch);

lib/oath/state.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2022 Yubico.
2+
* Copyright (C) 2022-2025 Yubico.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -40,7 +40,7 @@ class AccountsSearchNotifier extends StateNotifier<String> {
4040
}
4141

4242
final oathLayoutProvider =
43-
StateNotifierProvider.autoDispose<OathLayoutNotfier, OathLayout>((ref) {
43+
StateNotifierProvider.autoDispose<OathLayoutNotifier, OathLayout>((ref) {
4444
final device = ref.watch(currentDeviceProvider);
4545
List<OathPair> credentials = device != null
4646
? ref.read(
@@ -53,18 +53,18 @@ final oathLayoutProvider =
5353
final pinnedCreds = credentials.where(
5454
(entry) => favorites.contains(entry.credential.id),
5555
);
56-
return OathLayoutNotfier(
56+
return OathLayoutNotifier(
5757
'OATH_STATE_LAYOUT',
5858
ref.watch(prefProvider),
5959
credentials,
6060
pinnedCreds.toList(),
6161
);
6262
});
6363

64-
class OathLayoutNotfier extends StateNotifier<OathLayout> {
64+
class OathLayoutNotifier extends StateNotifier<OathLayout> {
6565
final String _key;
6666
final SharedPreferences _prefs;
67-
OathLayoutNotfier(
67+
OathLayoutNotifier(
6868
this._key,
6969
this._prefs,
7070
List<OathPair> credentials,

lib/oath/views/add_account_page.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage>
192192
oathState = ref
193193
.watch(oathStateProvider(deviceNode.path))
194194
.maybeWhen(data: (data) => data, orElse: () => null);
195-
_credentials = ref
195+
_credentials ??= ref
196196
.watch(credentialListProvider(deviceNode.path))
197197
?.map((e) => e.credential)
198198
.toList();

lib/oath/views/rename_account_dialog.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,11 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
178178
// is this credential name/issuer of valid format
179179
final nameNotEmpty = name.isNotEmpty;
180180

181+
// issuer field does not contain a colon character
182+
final issuerNoColon = !_issuerController.text.contains(':');
183+
181184
// can we rename with the new values
182-
final isValid = isUnique && nameNotEmpty;
185+
final isValid = isUnique && nameNotEmpty && issuerNoColon;
183186

184187
return ResponsiveDialog(
185188
title: Text(l10n.s_rename_account),
@@ -209,6 +212,9 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
209212
labelText: l10n.s_issuer_optional,
210213
helperText:
211214
'', // Prevents dialog resizing when disabled
215+
errorText: issuerNoColon
216+
? null
217+
: l10n.l_invalid_character_issuer,
212218
icon: const Icon(Symbols.business),
213219
),
214220
textInputAction: TextInputAction.next,

0 commit comments

Comments
 (0)