Skip to content

Commit cdd800d

Browse files
committed
Refactor encrypted field decryption and add AuthenticatorCredStoreState to Fido2Session
This commit refactors the CTAP 2.2/2.3 encrypted field decryption logic and exposes credential store state at the session level for easier access. Refactoring (AuthenticatorInfo.cs): - Extracted common decryption logic into DecryptEncryptedField() helper method - Eliminated code duplication between GetIdentifier() and GetCredStoreState() - Both methods now delegate to the shared helper with field-specific HKDF info - Added comprehensive documentation to the helper method explaining the CTAP 2.2/2.3 decryption scheme (HKDF-SHA-256 + AES-128-CBC) - Reduced code from ~60 lines to ~25 lines while maintaining identical behavior New Property (Fido2Session.cs): - Added AuthenticatorCredStoreState property that mirrors AuthenticatorIdentifier - Provides convenient session-level access to credential store state - Automatically manages persistent PIN/UV auth token retrieval - Enables easy detection of credential store changes across sessions/resets - Follows the established pattern for encrypted field access New Test (Fido2Tests.cs): - Added Session_AuthenticatorCredStoreState_Returns_SameCredStoreState() - Mirrors the existing AuthenticatorIdentifier test pattern - Verifies credential store state consistency across sessions - Requires YubiKey firmware 5.8.0+ for execution Benefits: - DRY principle: Single source of truth for encrypted field decryption - Maintainability: Future encrypted fields can use the same helper - Consistency: Both identifier and cred store state use identical patterns - Simplicity: Session-level properties hide PPUAT management complexity - API parity: .NET SDK now matches Java SDK feature set The refactoring maintains 100% backward compatibility - all existing code continues to work without changes.
1 parent 3f4f29a commit cdd800d

File tree

3 files changed

+92
-31
lines changed

3 files changed

+92
-31
lines changed

Yubico.YubiKey/src/Yubico/YubiKey/Fido2/AuthenticatorInfo.cs

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -597,30 +597,8 @@ List<PinUvAuthProtocol> ParsePinUvAuthProtocols(CborMap<int> cborMap)
597597
/// <returns>
598598
/// The decrypted identifier as a read-only memory block of bytes, or null if the encrypted identifier is not set.
599599
/// </returns>
600-
public ReadOnlyMemory<byte>? GetIdentifier(ReadOnlyMemory<byte> persistentUvAuthToken)
601-
{
602-
if (EncIdentifier is null)
603-
{
604-
return null;
605-
}
606-
607-
if (persistentUvAuthToken.Length == 0)
608-
{
609-
return null;
610-
}
611-
612-
Span<byte> iv = stackalloc byte[16];
613-
Span<byte> ct = stackalloc byte[16];
614-
Span<byte> salt = stackalloc byte[32];
615-
EncIdentifier.Value.Span[..16].CopyTo(iv);
616-
EncIdentifier.Value.Span[16..].CopyTo(ct);
617-
618-
var key = HkdfUtilities.DeriveKey(persistentUvAuthToken.Span, salt, "encIdentifier"u8, 16);
619-
var decryptedIdentifier = AesUtilities.AesCbcDecrypt(key.Span, iv, ct);
620-
CryptographicOperations.ZeroMemory(key.Span);
621-
622-
return decryptedIdentifier;
623-
}
600+
public ReadOnlyMemory<byte>? GetIdentifier(ReadOnlyMemory<byte> persistentUvAuthToken) =>
601+
DecryptEncryptedField(EncIdentifier, persistentUvAuthToken, "encIdentifier"u8);
624602

625603
/// <summary>
626604
/// Retrieves the credential store state derived from the encrypted credential store state, using the provided persistent UV authentication token.
@@ -631,9 +609,38 @@ List<PinUvAuthProtocol> ParsePinUvAuthProtocols(CborMap<int> cborMap)
631609
/// <returns>
632610
/// The decrypted credential store state as a read-only memory block of bytes, or null if the encrypted credential store state is not set.
633611
/// </returns>
634-
public ReadOnlyMemory<byte>? GetCredStoreState(ReadOnlyMemory<byte> persistentPinUvAuthToken)
612+
public ReadOnlyMemory<byte>? GetCredStoreState(ReadOnlyMemory<byte> persistentPinUvAuthToken) =>
613+
DecryptEncryptedField(EncCredStoreState, persistentPinUvAuthToken, "encCredStoreState"u8);
614+
615+
/// <summary>
616+
/// Decrypts an encrypted field using the provided persistent PIN/UV authentication token.
617+
/// </summary>
618+
/// <remarks>
619+
/// This method implements the CTAP 2.2/2.3 decryption scheme for encrypted authenticator fields:
620+
/// - Uses HKDF-SHA-256 to derive a 16-byte decryption key from the persistent token
621+
/// - Salt: 32 bytes of zeros
622+
/// - Info parameter: Field-specific context string (e.g., "encIdentifier", "encCredStoreState")
623+
/// - Decrypts using AES-128-CBC
624+
/// - Encrypted data format: 16-byte IV + 16-byte ciphertext
625+
/// </remarks>
626+
/// <param name="encryptedData">
627+
/// The encrypted field data (32 bytes: 16-byte IV + 16-byte ciphertext), or null if not available.
628+
/// </param>
629+
/// <param name="persistentPinUvAuthToken">
630+
/// The persistent PIN/UV authentication token used to derive the decryption key.
631+
/// </param>
632+
/// <param name="hkdfInfo">
633+
/// The HKDF info parameter (context string) specific to the field being decrypted.
634+
/// </param>
635+
/// <returns>
636+
/// The decrypted plaintext (16 bytes), or null if the encrypted data is not available or the token is empty.
637+
/// </returns>
638+
private static ReadOnlyMemory<byte>? DecryptEncryptedField(
639+
ReadOnlyMemory<byte>? encryptedData,
640+
ReadOnlyMemory<byte> persistentPinUvAuthToken,
641+
ReadOnlySpan<byte> hkdfInfo)
635642
{
636-
if (EncCredStoreState is null)
643+
if (encryptedData is null)
637644
{
638645
return null;
639646
}
@@ -646,14 +653,14 @@ List<PinUvAuthProtocol> ParsePinUvAuthProtocols(CborMap<int> cborMap)
646653
Span<byte> iv = stackalloc byte[16];
647654
Span<byte> ct = stackalloc byte[16];
648655
Span<byte> salt = stackalloc byte[32];
649-
EncCredStoreState.Value.Span[..16].CopyTo(iv);
650-
EncCredStoreState.Value.Span[16..].CopyTo(ct);
656+
encryptedData.Value.Span[..16].CopyTo(iv);
657+
encryptedData.Value.Span[16..].CopyTo(ct);
651658

652-
var key = HkdfUtilities.DeriveKey(persistentPinUvAuthToken.Span, salt, "encCredStoreState"u8, 16);
653-
var decryptedCredStoreState = AesUtilities.AesCbcDecrypt(key.Span, iv, ct);
659+
var key = HkdfUtilities.DeriveKey(persistentPinUvAuthToken.Span, salt, hkdfInfo, 16);
660+
var decrypted = AesUtilities.AesCbcDecrypt(key.Span, iv, ct);
654661
CryptographicOperations.ZeroMemory(key.Span);
655662

656-
return decryptedCredStoreState;
663+
return decrypted;
657664
}
658665

659666
/// <summary>

Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,39 @@ public ReadOnlyMemory<byte>? AuthenticatorIdentifier
153153
}
154154
}
155155

156+
/// <summary>
157+
/// Retrieves and decrypts the authenticator's credential store state.
158+
/// </summary>
159+
/// <remarks>
160+
/// <para>
161+
/// This method leverages the <c>encCredStoreState</c> value obtained from the authenticator's
162+
/// <c>authenticatorGetInfo</c> response. The <c>encCredStoreState</c> is an encrypted byte string
163+
/// that platforms can use to detect credential store changes across resets.
164+
/// </para>
165+
/// <para>
166+
/// A valid and active persistent PIN/UV Authentication Token (<c>persistentPinUvAuthToken</c>) is required to decrypt the state.
167+
/// The authenticator must also support and return the `encCredStoreState` in its `getInfo` response (YubiKeys v5.8.0 and later).
168+
/// </para>
169+
/// <para>
170+
/// By comparing the credential store state before and after operations (or across sessions), platforms can detect
171+
/// when credentials have been added, removed, or when the authenticator has been reset.
172+
/// </para>
173+
/// </remarks>
174+
/// <returns>
175+
/// A byte array containing the decrypted 128-bit (16-byte) credential store state.
176+
/// Returns null if the state cannot be decrypted (e.g., if the PPUAT is invalid or the <c>encCredStoreState</c> is missing).
177+
/// </returns>
178+
public ReadOnlyMemory<byte>? AuthenticatorCredStoreState
179+
{
180+
get
181+
{
182+
var ppuat = GetReadOnlyCredMgmtToken();
183+
return ppuat.HasValue
184+
? AuthenticatorInfo.GetCredStoreState(ppuat.Value)
185+
: null;
186+
}
187+
}
188+
156189
/// <summary>
157190
/// Creates an instance of <see cref="Fido2Session" />, the object that represents the FIDO2 application on the
158191
/// YubiKey.

Yubico.YubiKey/tests/integration/Yubico/YubiKey/Fido2/Fido2Tests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,27 @@ public void Session_AuthenticatorIdentifier_Returns_SameIdentifier()
3737
}
3838
}
3939

40+
[SkippableFact(typeof(DeviceNotFoundException))]
41+
public void Session_AuthenticatorCredStoreState_Returns_SameCredStoreState()
42+
{
43+
ReadOnlyMemory<byte>? credStoreState1;
44+
45+
// First run
46+
using (var session = GetSession(minFw: FirmwareVersion.V5_8_0))
47+
{
48+
credStoreState1 = session.AuthenticatorCredStoreState;
49+
Assert.True(credStoreState1.HasValue);
50+
Assert.NotEmpty(credStoreState1.Value.ToArray());
51+
}
52+
53+
// Second run
54+
using (var session = GetSession())
55+
{
56+
var credStoreState2 = session.AuthenticatorCredStoreState;
57+
Assert.True(credStoreState2!.Value.Span.SequenceEqual(credStoreState1.Value.Span));
58+
}
59+
}
60+
4061
[SkippableFact(typeof(DeviceNotFoundException))]
4162
public void AuthenticatorInfo_GetIdentifier_BothRuns_Returns_SameIdentifier()
4263
{

0 commit comments

Comments
 (0)