Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding functionality to read additional pages of device info #90

Closed
wants to merge 13 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -144,16 +144,18 @@ public void TestSpecializedKeyboardSupportsModhexString(KeyboardLayout layout)
}

#if Windows
#pragma warning disable CA1825
[Theory]
[MemberData(nameof(GetTestData))]
public void GetChar_GivenHidCode_ReturnsCorrectChar(KeyboardLayout layout, (char, byte)[] testData)
{
HidCodeTranslator hid = HidCodeTranslator.GetInstance(layout);
var hid = HidCodeTranslator.GetInstance(layout);
foreach ((char ch, byte code) item in testData)
{
Assert.Equal(item.ch, hid[item.code]);
}
}
#pragma warning restore CA1825
#endif

public static IEnumerable<object[]> GetTestData()
Expand Down
4 changes: 2 additions & 2 deletions Yubico.YubiKey/src/Yubico/YubiKey/ConnectionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ public bool TryCreateConnection(
{
IHidDevice { UsagePage: HidUsagePage.Fido } d => new FidoConnection(d),
IHidDevice { UsagePage: HidUsagePage.Keyboard } d => new KeyboardConnection(d),
ISmartCardDevice d => new CcidConnection(d, application),
ISmartCardDevice d => new SmartcardConnection(d, application),
_ => throw new NotSupportedException(ExceptionMessages.DeviceTypeNotRecognized)
};

Expand Down Expand Up @@ -266,7 +266,7 @@ public bool TryCreateConnection(
return false;
}

connection = new CcidConnection(smartCardDevice, applicationId);
connection = new SmartcardConnection(smartCardDevice, applicationId);
_ = _openConnections.Add(yubiKeyDevice);
}
finally
Expand Down
107 changes: 107 additions & 0 deletions Yubico.YubiKey/src/Yubico/YubiKey/DeviceInfoHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright 2023 Yubico AB
//
// Licensed under the Apache License, Version 2.0 (the "License").
// You may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Generic;
using Yubico.Core.Logging;
using Yubico.Core.Tlv;
using Yubico.YubiKey.Management.Commands;

namespace Yubico.YubiKey
{
internal static class DeviceInfoHelper
{
/// <summary>
/// Fetches and aggregates device configuration details from a YubiKey using multiple APDU commands,
/// paging through the data as needed until all configuration data is retrieved.
/// This method processes the responses, accumulating TLV-encoded data into a single dictionary.
/// </summary>
/// <typeparam name="TCommand">The specific type of IGetPagedDeviceInfoCommand, e.g. GetPagedDeviceInfoCommand, which will then allow for returning the appropriate response.</typeparam>
/// <param name="connection">The connection interface to communicate with a YubiKey.</param>
/// <returns>A YubiKeyDeviceInfo object containing all relevant device information.</returns>
/// <exception cref="InvalidOperationException">Thrown when the command fails to retrieve successful response statuses from the YubiKey.</exception>
public static YubiKeyDeviceInfo GetDeviceInfo<TCommand>(IYubiKeyConnection connection)
where TCommand : IGetPagedDeviceInfoCommand<IYubiKeyResponseWithData<Dictionary<int, ReadOnlyMemory<byte>>>>, new()
{
Logger log = Log.GetLogger();

int page = 0;
var pages = new Dictionary<int, ReadOnlyMemory<byte>>();

bool hasMoreData = true;
while (hasMoreData)
{
IYubiKeyResponseWithData<Dictionary<int, ReadOnlyMemory<byte>>> response = connection.SendCommand(new TCommand {Page = (byte)page++});
if (response.Status == ResponseStatus.Success)
{
Dictionary<int, ReadOnlyMemory<byte>> tlvData = response.GetData();
foreach (KeyValuePair<int, ReadOnlyMemory<byte>> tlv in tlvData)
{
pages.Add(tlv.Key, tlv.Value);
}

const int moreDataTag = 0x10;
hasMoreData = tlvData.TryGetValue(moreDataTag, out ReadOnlyMemory<byte> hasMoreDataByte)
&& hasMoreDataByte.Span.Length == 1
&& hasMoreDataByte.Span[0] == 1;
}
else
{
log.LogError("Failed to get device info page-{Page}: {Error} {Message}",
page, response.StatusWord, response.StatusMessage);

return new YubiKeyDeviceInfo(); // TODO What to return here? Null? Empty? Exception?
}
}

return YubiKeyDeviceInfo.CreateFromResponseData(pages);
}

/// <summary>
/// Attempts to create a dictionary from a TLV-encoded byte array by parsing and extracting tag-value pairs.
/// </summary>
/// <param name="tlvData">The byte array containing TLV-encoded data.</param>
/// <param name="result">When successful, contains a dictionary mapping integer tags to their corresponding values as byte arrays.</param>
/// <returns>True if the dictionary was successfully created; false otherwise.</returns>
public static bool TryCreateApduDictionaryFromResponseData(
ReadOnlyMemory<byte> tlvData, out Dictionary<int, ReadOnlyMemory<byte>> result)
{
Logger log = Log.GetLogger();
result = new Dictionary<int, ReadOnlyMemory<byte>>();

if (tlvData.IsEmpty)
{
log.LogWarning("ResponseAPDU data was empty!");
return false;
}

int tlvDataLength = tlvData.Span[0];
if (tlvDataLength == 0 || 1 + tlvDataLength > tlvData.Length)
{
log.LogWarning("TLV Data length was out of expected ranges. {Length}", tlvDataLength);
return false;
}

var tlvReader = new TlvReader(tlvData.Slice(1, tlvDataLength));
while (tlvReader.HasData)
{
int tag = tlvReader.PeekTag();
ReadOnlyMemory<byte> value = tlvReader.ReadValue(tag);
result.Add(tag, value);
}

return true;
}
}
}
52 changes: 29 additions & 23 deletions Yubico.YubiKey/src/Yubico/YubiKey/FidoDeviceInfoFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@
// limitations under the License.

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Yubico.Core.Devices.Hid;
using Yubico.Core.Logging;
using Yubico.YubiKey.DeviceExtensions;
using Yubico.YubiKey.U2f.Commands;

namespace Yubico.YubiKey
{
Expand Down Expand Up @@ -56,33 +55,30 @@ public static YubiKeyDeviceInfo GetDeviceInfo(IHidDevice device)
ykDeviceInfo.FirmwareVersion = firmwareVersion;
}

if (ykDeviceInfo.FirmwareVersion < FirmwareVersion.V4_0_0 && ykDeviceInfo.AvailableUsbCapabilities == YubiKeyCapabilities.None)
if (ykDeviceInfo.FirmwareVersion < FirmwareVersion.V4_0_0 &&
ykDeviceInfo.AvailableUsbCapabilities == YubiKeyCapabilities.None)
{
ykDeviceInfo.AvailableUsbCapabilities = YubiKeyCapabilities.FidoU2f;
}

return ykDeviceInfo;
}

private static bool TryGetDeviceInfoFromFido(IHidDevice device, [MaybeNullWhen(returnValue: false)] out YubiKeyDeviceInfo yubiKeyDeviceInfo)
private static bool TryGetDeviceInfoFromFido(IHidDevice device,
[MaybeNullWhen(returnValue: false)]
out YubiKeyDeviceInfo yubiKeyDeviceInfo)
{
Logger log = Log.GetLogger();

try
{
log.LogInformation("Attempting to read device info via the FIDO interface management command.");
using var FidoConnection = new FidoConnection(device);

U2f.Commands.GetDeviceInfoResponse response = FidoConnection.SendCommand(new U2f.Commands.GetDeviceInfoCommand());

if (response.Status == ResponseStatus.Success)
{
yubiKeyDeviceInfo = response.GetData();
log.LogInformation("Successfully read device info via FIDO interface management command.");
return true;
}

log.LogError("Failed to get device info from management application: {Error} {Message}", response.StatusWord, response.StatusMessage);
using var connection = new FidoConnection(device);
yubiKeyDeviceInfo = DeviceInfoHelper.GetDeviceInfo<GetPagedDeviceInfoCommand>(connection);

log.LogInformation("Successfully read device info via FIDO interface management command.");
return true;
//TODO Handle exceptions?
}
catch (NotImplementedException e)
{
Expand All @@ -100,29 +96,38 @@ private static bool TryGetDeviceInfoFromFido(IHidDevice device, [MaybeNullWhen(r
ErrorHandler(e, "Must have elevated privileges in Windows to access FIDO device directly.");
}

log.LogWarning("Failed to read device info through the management interface. This may be expected for older YubiKeys.");
log.LogWarning(
"Failed to read device info through the management interface. This may be expected for older YubiKeys.");

yubiKeyDeviceInfo = null;

return false;
}

private static bool TryGetFirmwareVersionFromFido(IHidDevice device, [MaybeNullWhen(returnValue: false)] out FirmwareVersion firmwareVersion)
private static bool TryGetFirmwareVersionFromFido(IHidDevice device,
[MaybeNullWhen(returnValue: false)]
out FirmwareVersion firmwareVersion)
{
Logger log = Log.GetLogger();

try
{
log.LogInformation("Attempting to read firmware version through FIDO.");
using var FidoConnection = new FidoConnection(device);
using var fidoConnection = new FidoConnection(device);

Fido2.Commands.VersionResponse response = FidoConnection.SendCommand(new Fido2.Commands.VersionCommand());
Fido2.Commands.VersionResponse response =
fidoConnection.SendCommand(new Fido2.Commands.VersionCommand());

if (response.Status == ResponseStatus.Success)
{
firmwareVersion = response.GetData();
log.LogInformation("Firmware version: {Version}", firmwareVersion.ToString());

return true;
}
log.LogError("Reading firmware version via FIDO failed with: {Error} {Message}", response.StatusWord, response.StatusMessage);

log.LogError("Reading firmware version via FIDO failed with: {Error} {Message}", response.StatusWord,
response.StatusMessage);
}
catch (NotImplementedException e)
{
Expand All @@ -142,10 +147,11 @@ private static bool TryGetFirmwareVersionFromFido(IHidDevice device, [MaybeNullW

log.LogWarning("Failed to read firmware version through FIDO.");
firmwareVersion = null;

return false;
}

private static void ErrorHandler(Exception exception, string message)
=> Log.GetLogger().LogWarning(exception, message);
private static void ErrorHandler(Exception exception, string message) =>
Log.GetLogger().LogWarning(exception, message);
}
}
13 changes: 13 additions & 0 deletions Yubico.YubiKey/src/Yubico/YubiKey/IGetPagedDeviceInfoCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;

namespace Yubico.YubiKey
{
public interface IGetPagedDeviceInfoCommand<out T> : IYubiKeyCommand<T>
where T : IYubiKeyResponseWithData<Dictionary<int, ReadOnlyMemory<byte>>>
{
public byte Page { get; set; }

}
}

2 changes: 2 additions & 0 deletions Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -662,5 +662,7 @@ void SetLegacyDeviceConfiguration(
byte challengeResponseTimeout,
bool touchEjectEnabled,
int autoEjectTimeout = 0);

void SetIsNfcRestricted(bool enabled);
}
}
5 changes: 5 additions & 0 deletions Yubico.YubiKey/src/Yubico/YubiKey/IYubiKeyDeviceInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ public interface IYubiKeyDeviceInfo
/// The NFC features that are currently enabled over NFC.
/// </summary>
public YubiKeyCapabilities EnabledNfcCapabilities { get; }

/// <summary>
/// TODO
/// </summary>
public bool IsNfcRestricted { get; }

/// <summary>
/// The serial number of the YubiKey, if one is present.
Expand Down
28 changes: 16 additions & 12 deletions Yubico.YubiKey/src/Yubico/YubiKey/KeyboardDeviceInfoFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using Yubico.Core.Devices.Hid;
using Yubico.Core.Logging;
using Yubico.YubiKey.DeviceExtensions;
using Yubico.YubiKey.Otp.Commands;

namespace Yubico.YubiKey
{
Expand Down Expand Up @@ -73,18 +74,21 @@ private static bool TryGetDeviceInfoFromKeyboard(IHidDevice device, [MaybeNullWh
try
{
log.LogInformation("Attempting to read device info via the management command over the keyboard interface.");
using var KeyboardConnection = new KeyboardConnection(device);

Otp.Commands.GetDeviceInfoResponse response = KeyboardConnection.SendCommand(new Otp.Commands.GetDeviceInfoCommand());

if (response.Status == ResponseStatus.Success)
{
yubiKeyDeviceInfo = response.GetData();
log.LogInformation("Successfully read device info via the keyboard management command.");
return true;
}

log.LogError("Failed to get device info from the keyboard management command: {Error} {Message}", response.StatusWord, response.StatusMessage);
using var connection = new KeyboardConnection(device);
yubiKeyDeviceInfo = DeviceInfoHelper.GetDeviceInfo<GetPagedDeviceInfoCommand>(connection);
//TODO Handle exceptions?
return true;

// Otp.Commands.GetDeviceInfoResponse response = keyboardConnection.SendCommand(new Otp.Commands.GetDeviceInfoCommand());
//
// if (response.Status == ResponseStatus.Success)
// {
// yubiKeyDeviceInfo = response.GetData();
// log.LogInformation("Successfully read device info via the keyboard management command.");
// return true;
// }

// log.LogError("Failed to get device info from the keyboard management command: {Error} {Message}", response.StatusWord, response.StatusMessage);
}
catch (KeyboardConnectionException e)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using Yubico.Core.Iso7816;

namespace Yubico.YubiKey.Management.Commands
Expand All @@ -22,6 +23,7 @@ namespace Yubico.YubiKey.Management.Commands
/// <remarks>
/// This class has a corresponding partner class <see cref="GetDeviceInfoResponse"/>
/// </remarks>
[Obsolete("This class has been replaced by GetPagedDeviceInfoCommand")]
public class GetDeviceInfoCommand : IYubiKeyCommand<GetDeviceInfoResponse>
{
private const byte GetDeviceInfoInstruction = 0x1D;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ namespace Yubico.YubiKey.Management.Commands
/// The response to the <see cref="GetDeviceInfoCommand"/> command, containing the YubiKey's
/// device configuration details.
/// </summary>
[Obsolete("This class has been replaced by GetPagedDeviceInfoResponse")]
public class GetDeviceInfoResponse : YubiKeyResponse, IYubiKeyResponseWithData<YubiKeyDeviceInfo>
{
/// <summary>
Expand Down Expand Up @@ -51,7 +52,7 @@ public YubiKeyDeviceInfo GetData()

if (ResponseApdu.Data.Length > 255)
{
throw new MalformedYubiKeyResponseException()
throw new MalformedYubiKeyResponseException
{
ResponseClass = nameof(GetDeviceInfoResponse),
ActualDataLength = ResponseApdu.Data.Length
Expand All @@ -60,7 +61,7 @@ public YubiKeyDeviceInfo GetData()

if (!YubiKeyDeviceInfo.TryCreateFromResponseData(ResponseApdu.Data, out YubiKeyDeviceInfo? deviceInfo))
{
throw new MalformedYubiKeyResponseException()
throw new MalformedYubiKeyResponseException
{
ResponseClass = nameof(GetDeviceInfoResponse),
};
Expand Down
Loading
Loading