Skip to content

Commit 2381ddd

Browse files
authored
Merge pull request #276 from Yubico/dennisdyallo/fixes-182
fix(otp): Handle case when last slot is deleted
2 parents 55fdf76 + f077510 commit 2381ddd

File tree

2 files changed

+130
-19
lines changed

2 files changed

+130
-19
lines changed

Yubico.YubiKey/src/Yubico/YubiKey/Pipelines/OtpErrorTransform.cs

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Microsoft.Extensions.Logging;
1717
using Yubico.Core.Iso7816;
1818
using Yubico.Core.Logging;
19+
using Yubico.YubiKey.Otp;
1920
using Yubico.YubiKey.Otp.Commands;
2021

2122
namespace Yubico.YubiKey.Pipelines
@@ -37,45 +38,70 @@ public ResponseApdu Invoke(CommandApdu command, Type commandType, Type responseT
3738
{
3839
// If this is just a regular ReadStatusCommand, or it's a command that doesn't ask for a status response
3940
// in return, invoke the pipeline as usual.
40-
if (commandType == typeof(ReadStatusCommand)
41-
|| responseType != typeof(ReadStatusResponse))
41+
if (commandType == typeof(ReadStatusCommand) ||
42+
responseType != typeof(ReadStatusResponse))
4243
{
4344
return _nextTransform.Invoke(command, commandType, responseType);
4445
}
4546

4647
// Otherwise we assume this to be a command that applies a config (and therefore looks for a status response).
4748
// In order to detect failures, we grab the status structure before applying said command so that we have a
4849
// sequence number to compare to.
49-
int beforeSequence = new ReadStatusResponse(
50-
_nextTransform.Invoke(
51-
new ReadStatusCommand().CreateCommandApdu(),
52-
typeof(ReadStatusCommand),
53-
typeof(ReadStatusResponse)))
54-
.GetData()
55-
.SequenceNumber;
50+
var beforeStatus = GetCurrentStatus();
51+
var responseApdu = _nextTransform.Invoke(command, commandType, responseType);
52+
var afterStatus = ReadStatus(responseApdu);
5653

5754
try
5855
{
59-
var responseApdu = _nextTransform.Invoke(command, commandType, responseType);
60-
int afterSequence = new ReadStatusResponse(responseApdu).GetData().SequenceNumber;
61-
int expectedSequence = (beforeSequence + 1) % 0x100;
62-
63-
// If we see the sequence number change, we can assume that the configuration was applied successfully. Otherwise
64-
// we just invent an error in the response.
65-
return afterSequence != expectedSequence
66-
? new ResponseApdu(responseApdu.Data.ToArray(), SWConstants.WarningNvmUnchanged)
67-
: responseApdu;
56+
// If we see the sequence number change, we can assume that the configuration was applied successfully.
57+
// Otherwise we just invent an error in the response.
58+
return IsValidSequenceProgression(beforeStatus, afterStatus)
59+
? responseApdu
60+
: CreateFailedApdu(responseApdu.Data.ToArray());
6861
}
6962
catch (KeyboardConnectionException e)
7063
{
7164
_logger.LogWarning(e, "Handling keyboard connection exception. Translating to APDU response.");
7265

73-
return new ResponseApdu(Array.Empty<byte>(), SWConstants.WarningNvmUnchanged);
66+
return CreateFailedApdu();
7467
}
7568
}
7669

7770
public void Setup() => _nextTransform.Setup();
7871

7972
public void Cleanup() => _nextTransform.Cleanup();
73+
74+
// Internal for testing
75+
internal static bool IsValidSequenceProgression(OtpStatus beforeStatus, OtpStatus afterStatus)
76+
{
77+
byte before = beforeStatus.SequenceNumber;
78+
byte after = afterStatus.SequenceNumber;
79+
80+
bool normalIncrement = after == before + 1;
81+
bool validReset = before > 0 && after == 0 &&
82+
afterStatus is { LongPressConfigured: false, ShortPressConfigured: false };
83+
84+
return normalIncrement || validReset;
85+
}
86+
87+
private OtpStatus GetCurrentStatus()
88+
{
89+
var command = new ReadStatusCommand();
90+
var responseApdu = _nextTransform.Invoke(
91+
command.CreateCommandApdu(),
92+
typeof(ReadStatusCommand),
93+
typeof(ReadStatusResponse));
94+
95+
return ReadStatus(responseApdu);
96+
}
97+
98+
private static OtpStatus ReadStatus(ResponseApdu responseApdu)
99+
{
100+
var readStatusResponse = new ReadStatusResponse(responseApdu);
101+
var afterStatus = readStatusResponse.GetData();
102+
return afterStatus;
103+
}
104+
105+
private static ResponseApdu CreateFailedApdu(byte[]? data = null) => new(data ?? [], SWConstants.WarningNvmUnchanged);
80106
}
81107
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using Xunit;
2+
using Yubico.YubiKey.Otp;
3+
4+
namespace Yubico.YubiKey.Pipelines;
5+
6+
public class OtpErrorTransformTests
7+
{
8+
[Theory]
9+
[InlineData(0, 1)] // First increment
10+
[InlineData(5, 6)] // Normal increment
11+
[InlineData(254, 255)] // Near byte boundary
12+
public void IsValidSequenceProgression_NormalIncrement_ReturnsTrue(
13+
byte before,
14+
byte after)
15+
{
16+
var beforeStatus = CreateOtpStatus(before, hasConfigs: true);
17+
var afterStatus = CreateOtpStatus(after, hasConfigs: true);
18+
19+
var isValid = OtpErrorTransform.IsValidSequenceProgression(beforeStatus, afterStatus);
20+
21+
Assert.True(isValid);
22+
}
23+
24+
[Theory]
25+
[InlineData(0, 2)] // Skip increment
26+
[InlineData(5, 7)] // Jump by 2
27+
[InlineData(10, 8)] // Backwards
28+
[InlineData(255, 0)] // Byte overflow (should be rejected as normal increment)
29+
public void IsValidSequenceProgression_InvalidIncrement_ReturnsFalse(
30+
byte before,
31+
byte after)
32+
{
33+
var beforeStatus = CreateOtpStatus(before, hasConfigs: true);
34+
var afterStatus = CreateOtpStatus(after, hasConfigs: true); // Still has configs
35+
36+
var isValid = OtpErrorTransform.IsValidSequenceProgression(beforeStatus, afterStatus);
37+
38+
Assert.False(isValid);
39+
}
40+
41+
[Theory]
42+
[InlineData(1)] // Delete from sequence 1
43+
[InlineData(5)] // Delete from sequence 5
44+
[InlineData(255)] // Delete from sequence 255
45+
public void IsValidSequenceProgression_ValidReset_ReturnsTrue(
46+
byte before)
47+
{
48+
var beforeStatus = CreateOtpStatus(before, hasConfigs: true);
49+
var afterStatus = CreateOtpStatus(0, hasConfigs: false); // No configs remain
50+
51+
var isValid = OtpErrorTransform.IsValidSequenceProgression(beforeStatus, afterStatus);
52+
53+
Assert.True(isValid);
54+
}
55+
56+
[Theory]
57+
[InlineData(0, false)] // Already zero
58+
[InlineData(1, true)] // Reset but configs still exist
59+
[InlineData(5, true)] // Reset but configs still exist
60+
public void IsValidSequenceProgression_InvalidReset_ReturnsFalse(
61+
byte before,
62+
bool afterHasConfigs)
63+
{
64+
var beforeStatus = CreateOtpStatus(before, hasConfigs: true);
65+
var afterStatus = CreateOtpStatus(0, hasConfigs: afterHasConfigs);
66+
67+
var isValid = OtpErrorTransform.IsValidSequenceProgression(beforeStatus, afterStatus);
68+
69+
Assert.False(isValid);
70+
}
71+
72+
private static OtpStatus CreateOtpStatus(
73+
byte sequenceNumber,
74+
bool hasConfigs) => new()
75+
{
76+
FirmwareVersion = new FirmwareVersion(),
77+
SequenceNumber = sequenceNumber,
78+
TouchLevel = 0x00,
79+
ShortPressConfigured = hasConfigs,
80+
LongPressConfigured = false, // Only test one slot for simplicity
81+
ShortPressRequiresTouch = false,
82+
LongPressRequiresTouch = false,
83+
LedBehaviorInverted = false
84+
};
85+
}

0 commit comments

Comments
 (0)