diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9e25c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,220 @@ +# Created by https://www.gitignore.io/api/visualstudio + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..15c4a92 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Sam Cook + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8483943 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# u2fhost.net + +A library for communicating with U2F devices over USB. + +Makes use of [HidLibrary](https://github.com/mikeobrien/HidLibrary) and [u2flib](https://github.com/brucedog/u2flib). + +## Usage + +See [Sample.cs](https://github.com/samcook/u2fhost.net/blob/master/u2fhost.console/Sample.cs) \ No newline at end of file diff --git a/u2fhost.console/App.config b/u2fhost.console/App.config new file mode 100644 index 0000000..d6d91ae --- /dev/null +++ b/u2fhost.console/App.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/u2fhost.console/Program.cs b/u2fhost.console/Program.cs new file mode 100644 index 0000000..b3e3f0d --- /dev/null +++ b/u2fhost.console/Program.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; + +namespace u2fhost.console +{ + public class Program + { + private const int VendorId = 0x1050; + private const int ProductId = 0x0120; + + public static void Main(string[] args) + { + Sample.Run(VendorId, ProductId).Wait(); + } + + private static void Ping(U2FHidDevice u2F) + { + var r = new Random(); + var pingData = new byte[1024]; + r.NextBytes(pingData); + + var pingResponse = u2F.PingAsync(pingData).Result; + + Console.WriteLine($"Ping response data matches request: {pingData.SequenceEqual(pingResponse)}"); + } + + private static void PrintVersions(ApduDevice apdu) + { + Console.WriteLine("Supported versions:"); + foreach (var version in apdu.GetSupportedVersionsAsync().Result) + { + Console.WriteLine(version); + } + } + } +} diff --git a/u2fhost.console/Properties/AssemblyInfo.cs b/u2fhost.console/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..6b8e0c4 --- /dev/null +++ b/u2fhost.console/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("u2fhost.console")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("u2fhost.console")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("268080a7-5957-4498-ab25-80cf28d68378")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/u2fhost.console/Sample.cs b/u2fhost.console/Sample.cs new file mode 100644 index 0000000..aab18f7 --- /dev/null +++ b/u2fhost.console/Sample.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using HidLibrary; + +namespace u2fhost.console +{ + public static class Sample + { + public static async Task Run(int vendorId, int productId) + { + Console.WriteLine("Insert U2F device"); + + IHidDevice hidDevice = null; + + while (hidDevice == null) + { + using (hidDevice = HidDevices.Enumerate(vendorId, productId).FirstOrDefault()) + { + if (hidDevice == null) + { + await Task.Delay(250); + continue; + } + + var appId = "http://localhost"; + var facet = "http://localhost"; + + var registration = await U2FHost.RegisterAsync(hidDevice, appId, facet); + + await U2FHost.AuthenticateAsync(hidDevice, registration, appId, facet); + } + } + } + } +} \ No newline at end of file diff --git a/u2fhost.console/packages.config b/u2fhost.console/packages.config new file mode 100644 index 0000000..8d2b901 --- /dev/null +++ b/u2fhost.console/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/u2fhost.console/u2fhost.console.csproj b/u2fhost.console/u2fhost.console.csproj new file mode 100644 index 0000000..5a3b885 --- /dev/null +++ b/u2fhost.console/u2fhost.console.csproj @@ -0,0 +1,88 @@ + + + + + Debug + AnyCPU + {268080A7-5957-4498-AB25-80CF28D68378} + Exe + Properties + u2fhost.console + u2fhost.console + v4.5 + 512 + true + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + ..\packages\BouncyCastle.1.7.0\lib\Net40-Client\BouncyCastle.Crypto.dll + True + + + ..\packages\hidlibrary.3.2.31.0\lib\HidLibrary.dll + True + + + ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + + + + + + + + ..\packages\u2flib.1.0.1\lib\net45\u2flib.dll + True + + + + + + + + + + + + + + {F28D70D6-4109-4F8B-8303-6F253AB7E436} + u2fhost + + + + + \ No newline at end of file diff --git a/u2fhost.sln b/u2fhost.sln new file mode 100644 index 0000000..afb6c80 --- /dev/null +++ b/u2fhost.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "u2fhost", "u2fhost\u2fhost.csproj", "{F28D70D6-4109-4F8B-8303-6F253AB7E436}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "u2fhost.console", "u2fhost.console\u2fhost.console.csproj", "{268080A7-5957-4498-AB25-80CF28D68378}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7558A8E4-6AD7-4BDF-BDC9-2A01DB88537B}" + ProjectSection(SolutionItems) = preProject + LICENSE = LICENSE + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F28D70D6-4109-4F8B-8303-6F253AB7E436}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F28D70D6-4109-4F8B-8303-6F253AB7E436}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F28D70D6-4109-4F8B-8303-6F253AB7E436}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F28D70D6-4109-4F8B-8303-6F253AB7E436}.Release|Any CPU.Build.0 = Release|Any CPU + {268080A7-5957-4498-AB25-80CF28D68378}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {268080A7-5957-4498-AB25-80CF28D68378}.Debug|Any CPU.Build.0 = Debug|Any CPU + {268080A7-5957-4498-AB25-80CF28D68378}.Release|Any CPU.ActiveCfg = Release|Any CPU + {268080A7-5957-4498-AB25-80CF28D68378}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/u2fhost/ApduDevice.cs b/u2fhost/ApduDevice.cs new file mode 100644 index 0000000..a50a8db --- /dev/null +++ b/u2fhost/ApduDevice.cs @@ -0,0 +1,63 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace u2fhost +{ + public abstract class ApduDevice + { + public async Task GetSupportedVersionsAsync() + { + try + { + var versionBytes = await SendApduAsync(Constants.INS_GET_VERSION); + + return new[] {Encoding.ASCII.GetString(versionBytes)}; + } + catch (ApduException ex) + { + // v0 didn't support the instruction + return ex.StatusCode == 0x6d00 ? new[] {"v0"} : new string[0]; + } + } + + protected abstract Task DoSendApduAsync(byte[] data); + + /// + /// Sends an APDU to the device, and waits for a response + /// + public async Task SendApduAsync(byte ins, byte p1 = 0x00, byte p2 = 0x00, byte[] data = null) + { + if (data == null) + { + data = new byte[0]; + } + var size = data.Length; + var l0 = (byte) (size >> 16 & 0xff); + var l1 = (byte) (size >> 8 & 0xff); + var l2 = (byte) (size & 0xff); + + var byteArrayBuilder = new ByteArrayBuilder(); + byteArrayBuilder.Append(new byte[] {0x00, ins, p1, p2, l0, l1, l2}); + byteArrayBuilder.Append(data); + byteArrayBuilder.Append(new byte[] {0x04, 0x00}); + + var apduData = byteArrayBuilder.GetBytes(); + + var response = await DoSendApduAsync(apduData); + + var responseData = response.Take(response.Length - 2).ToArray(); + var status = response.Skip(response.Length - 2).Take(2).Reverse().ToArray(); + + var statusCode = BitConverter.ToUInt16(status, 0); + + if (statusCode != Constants.APDU_OK) + { + throw new ApduException(statusCode); + } + + return responseData; + } + } +} \ No newline at end of file diff --git a/u2fhost/ApduException.cs b/u2fhost/ApduException.cs new file mode 100644 index 0000000..601572b --- /dev/null +++ b/u2fhost/ApduException.cs @@ -0,0 +1,20 @@ +using System; + +namespace u2fhost +{ + public class ApduException : Exception + { + public ushort? StatusCode { get; } + + public ApduException(ushort? statusCode = null) + { + StatusCode = statusCode; + } + + public ApduException(string message, ushort? statusCode = null) + : base(message) + { + StatusCode = statusCode; + } + } +} \ No newline at end of file diff --git a/u2fhost/ByteArrayBuilder.cs b/u2fhost/ByteArrayBuilder.cs new file mode 100644 index 0000000..f45fe3f --- /dev/null +++ b/u2fhost/ByteArrayBuilder.cs @@ -0,0 +1,36 @@ +using System.IO; + +namespace u2fhost +{ + internal class ByteArrayBuilder + { + private MemoryStream stream; + + public ByteArrayBuilder() + { + stream = new MemoryStream(); + } + + public void Append(byte value) + { + stream.WriteByte(value); + } + + public void Append(byte[] values) + { + stream.Write(values, 0, values.Length); + } + + public byte[] GetBytes() + { + return stream.ToArray(); + } + + public long Length => stream.Length; + + public void Clear() + { + stream = new MemoryStream(); + } + } +} \ No newline at end of file diff --git a/u2fhost/Constants.cs b/u2fhost/Constants.cs new file mode 100644 index 0000000..6e57cd4 --- /dev/null +++ b/u2fhost/Constants.cs @@ -0,0 +1,14 @@ +namespace u2fhost +{ + public static class Constants + { + // APDU Instructions + public const byte INS_ENROLL = 0x01; + public const byte INS_SIGN = 0x02; + public const byte INS_GET_VERSION = 0x03; + + // APDU Response Codes + public const ushort APDU_OK = 0x9000; + public const ushort APDU_USE_NOT_SATISFIED = 0x6985; + } +} \ No newline at end of file diff --git a/u2fhost/Properties/AssemblyInfo.cs b/u2fhost/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..26157d9 --- /dev/null +++ b/u2fhost/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("u2fhost")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("u2fhost")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f28d70d6-4109-4f8b-8303-6f253ab7e436")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/u2fhost/U2FHidDevice.cs b/u2fhost/U2FHidDevice.cs new file mode 100644 index 0000000..ea49e21 --- /dev/null +++ b/u2fhost/U2FHidDevice.cs @@ -0,0 +1,251 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using HidLibrary; + +namespace u2fhost +{ + public class U2FHidDevice : ApduDevice, IDisposable + { + //DEVICES = [ + // (0x1050, 0x0200), # Gnubby + // (0x1050, 0x0113), # YubiKey NEO U2F + // (0x1050, 0x0114), # YubiKey NEO OTP+U2F + // (0x1050, 0x0115), # YubiKey NEO U2F+CCID + // (0x1050, 0x0116), # YubiKey NEO OTP+U2F+CCID + // (0x1050, 0x0120), # Security Key by Yubico + // (0x1050, 0x0410), # YubiKey Plus + // (0x1050, 0x0402), # YubiKey 4 U2F + // (0x1050, 0x0403), # YubiKey 4 OTP+U2F + // (0x1050, 0x0406), # YubiKey 4 U2F+CCID + // (0x1050, 0x0407), # YubiKey 4 OTP+U2F+CCID + //] + + private const byte TYPE_INIT = 0x80; + private const int HID_RPT_SIZE = 64; + private const byte STAT_ERR = 0xbf; + + private const byte CMD_PING = 0x01; + private const byte CMD_INIT = 0x06; + private const byte CMD_WINK = 0x08; + private const byte CMD_APDU = 0x03; + + private const uint BROADCAST_CID = 0xffffffff; + + private const int HidTimeoutMs = 1000; + + private readonly Random random = new Random(); + + private readonly IHidDevice hidDevice; + private byte[] cid; + + protected U2FHidDevice(IHidDevice hidDevice) + { + this.hidDevice = hidDevice; + this.cid = BitConverter.GetBytes(BROADCAST_CID); + } + + public static async Task OpenAsync(IHidDevice hidDevice) + { + var device = new U2FHidDevice(hidDevice); + await device.InitAsync(); + return device; + } + + protected async Task InitAsync() + { + Console.WriteLine("Init"); + + var nonce = new byte[8]; + random.NextBytes(nonce); + var response = await CallAsync(CMD_INIT, nonce); + + while (!response.Take(8).SequenceEqual(nonce)) + { + await Task.Delay(100); + Console.WriteLine("Wrong nonce, read again..."); + response = await CallAsync(CMD_INIT, nonce); + } + + this.cid = response.Skip(8).Take(4).ToArray(); + + Console.WriteLine("Cid: {0}", BitConverter.ToString(this.cid)); + } + + public void SetMode(string mode) + { + throw new NotImplementedException(); + } + + protected override async Task DoSendApduAsync(byte[] data) + { + return await CallAsync(CMD_APDU, data); + } + + public async Task Wink() + { + await CallAsync(CMD_WINK); + } + + public async Task PingAsync(byte[] data) + { + return await CallAsync(CMD_PING, data); + } + + private async Task CallAsync(byte command, byte[] data = null) + { + await SendRequestAsync(command, data); + return await ReadResponseAsync(command); + } + + private async Task SendRequestAsync(byte command, byte[] data = null) + { + //Console.WriteLine("SendRequest: {0:X2}", command); + + if (data == null) + { + data = new byte[0]; + } + + //Console.WriteLine($"Data: {BitConverter.ToString(data)}"); + + //var reportSize = hidDevice.Capabilities.InputReportByteLength; + var reportSize = HID_RPT_SIZE; + + var size = data.Length; + var bc_l = (byte)(size & 0xff); + var bc_h = (byte)(size >> 8 & 0xff); + var payloadData = data.Take(reportSize - 7).ToArray(); + + //Console.WriteLine($"Payload data: {BitConverter.ToString(payloadData)}"); + + var payloadBuilder = new ByteArrayBuilder(); + payloadBuilder.Append(cid); + payloadBuilder.Append((byte)(TYPE_INIT | command)); + payloadBuilder.Append(bc_h); + payloadBuilder.Append(bc_l); + payloadBuilder.Append(payloadData); + while (payloadBuilder.Length < reportSize) + { + payloadBuilder.Append(0x00); + } + + var payload = payloadBuilder.GetBytes(); + var report = hidDevice.CreateReport(); + report.Data = payload; + await hidDevice.WriteReportAsync(report, HidTimeoutMs); + + var remainingData = data.Skip(reportSize - 7).ToArray(); + var seq = 0; + while (remainingData.Length > 0) + { + payloadData = remainingData.Take(reportSize - 5).ToArray(); + //Console.WriteLine($"Payload data: {BitConverter.ToString(payloadData)}"); + + payloadBuilder.Clear(); + payloadBuilder.Append(cid); + payloadBuilder.Append((byte)(0x7f & seq)); + payloadBuilder.Append(payloadData); + while (payloadBuilder.Length < reportSize) + { + payloadBuilder.Append(0x00); + } + + payload = payloadBuilder.GetBytes(); + report = hidDevice.CreateReport(); + report.Data = payload; + if (!await hidDevice.WriteReportAsync(report, HidTimeoutMs)) + { + throw new Exception("Error writing to device"); + } + + remainingData = remainingData.Skip(reportSize - 5).ToArray(); + seq++; + } + } + + private async Task ReadResponseAsync(byte command) + { + //Console.WriteLine("ReadResponse"); + + //var reportSize = hidDevice.Capabilities.OutputReportByteLength; + var reportSize = HID_RPT_SIZE; + + var byteArrayBuilder = new ByteArrayBuilder(); + + byteArrayBuilder.Append(cid); + byteArrayBuilder.Append((byte)(TYPE_INIT | command)); + + var resp = Encoding.ASCII.GetBytes("."); + var header = byteArrayBuilder.GetBytes(); + + HidReport report = null; + + while (!resp.Take(header.Length).SequenceEqual(header)) + { + report = await hidDevice.ReadReportAsync(HidTimeoutMs); + + if (report.ReadStatus != HidDeviceData.ReadStatus.Success) + { + throw new Exception("Error reading from device"); + } + + resp = report.Data; + + byteArrayBuilder.Clear(); + byteArrayBuilder.Append(cid); + byteArrayBuilder.Append(STAT_ERR); + + if (resp.Take(header.Length).SequenceEqual(byteArrayBuilder.GetBytes())) + { + throw new Exception("Error in response header"); + } + } + + var dataLength = (report.Data[5] << 8) + report.Data[6]; + + var payloadData = report.Data.Skip(7).Take(Math.Min(dataLength, reportSize)).ToArray(); + //Console.WriteLine($"Payload data: {BitConverter.ToString(payloadData)}"); + + byteArrayBuilder.Clear(); + byteArrayBuilder.Append(payloadData); + dataLength -= (int)byteArrayBuilder.Length; + + var seq = 0; + while (dataLength > 0) + { + report = await hidDevice.ReadReportAsync(HidTimeoutMs); + + if (report.ReadStatus != HidDeviceData.ReadStatus.Success) + { + throw new Exception("Error reading from device"); + } + + if (!report.Data.Take(4).SequenceEqual(cid)) + { + throw new Exception("Wrong CID from device"); + } + if (report.Data[4] != (byte)(seq & 0x7f)) + { + throw new Exception("Wrong SEQ from device"); + } + seq++; + payloadData = report.Data.Skip(5).Take(Math.Min(dataLength, reportSize)).ToArray(); + //Console.WriteLine($"Payload data: {BitConverter.ToString(payloadData)}"); + + dataLength -= payloadData.Length; + byteArrayBuilder.Append(payloadData); + } + + var result = byteArrayBuilder.GetBytes(); + + return result; + } + + public void Dispose() + { + hidDevice.CloseDevice(); + } + } +} diff --git a/u2fhost/U2FHost.cs b/u2fhost/U2FHost.cs new file mode 100644 index 0000000..a809003 --- /dev/null +++ b/u2fhost/U2FHost.cs @@ -0,0 +1,78 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using HidLibrary; +using u2flib; +using u2flib.Data; + +namespace u2fhost +{ + public static class U2FHost + { + public static async Task RegisterAsync(IHidDevice hidDevice, string appId, string facet, CancellationToken? cancellationToken = null) + { + cancellationToken = cancellationToken ?? CancellationToken.None; + + if (hidDevice == null || !hidDevice.IsConnected) + { + throw new ArgumentException("Hid device not connected", nameof(hidDevice)); + } + + using (var u2fHidDevice = await U2FHidDevice.OpenAsync(hidDevice)) + { + var startRegistration = U2F.StartRegistration(appId); + + Console.WriteLine("Touch token to register"); + var registerResponse = await WaitForTokenInputAsync(() => U2Fv2.RegisterAsync(u2fHidDevice, startRegistration, facet), cancellationToken.Value); + + var deviceRegistration = U2F.FinishRegistration(startRegistration, registerResponse); + Console.WriteLine("Registered"); + + return deviceRegistration; + } + } + + public static async Task AuthenticateAsync(IHidDevice hidDevice, DeviceRegistration deviceRegistration, string appId, string facet, bool checkOnly = false, CancellationToken? cancellationToken = null) + { + cancellationToken = cancellationToken ?? CancellationToken.None; + + if (hidDevice == null || !hidDevice.IsConnected) + { + throw new ArgumentException("Hid device not connected", nameof(hidDevice)); + } + + using (var u2fHidDevice = await U2FHidDevice.OpenAsync(hidDevice)) + { + var startAuthentication = U2F.StartAuthentication(appId, deviceRegistration); + + Console.WriteLine("Touch token to authenticate"); + var authenticateResponse = await WaitForTokenInputAsync(() => U2Fv2.AuthenticateAsync(u2fHidDevice, startAuthentication, facet, checkOnly), cancellationToken.Value).ConfigureAwait(false); + + U2F.FinishAuthentication(startAuthentication, authenticateResponse, deviceRegistration); + Console.WriteLine("Authenticated"); + } + } + + public static async Task WaitForTokenInputAsync(Func> func, CancellationToken cancellationToken) + { + T response; + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + response = await func(); + } + catch (ApduException ex) when (ex.StatusCode == Constants.APDU_USE_NOT_SATISFIED) + { + await Task.Delay(250, cancellationToken); + continue; + } + break; + } + + return response; + } + } +} \ No newline at end of file diff --git a/u2fhost/U2Fv2.cs b/u2fhost/U2Fv2.cs new file mode 100644 index 0000000..bee482e --- /dev/null +++ b/u2fhost/U2Fv2.cs @@ -0,0 +1,122 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using u2flib; +using u2flib.Data.Messages; +using u2flib.Util; + +namespace u2fhost +{ + public class U2Fv2 + { + public static async Task RegisterAsync(U2FHidDevice u2fHidDevice, StartedRegistration request, string facet) + { + ValidateRequest(request, facet); + + var appParam = GetApplicationParameter(request.AppId); + + var clientData = GetRegistrationClientData(request.Challenge, facet); + var challengeParam = GetChallengeParameter(clientData); + + var data = challengeParam.Concat(appParam).ToArray(); + var p1 = (byte)0x03; + var p2 = (byte)0x00; + + var response = await u2fHidDevice.SendApduAsync(Constants.INS_ENROLL, p1, p2, data); + + var registrationDataBase64 = Utils.ByteArrayToBase64String(response); + var clientDataBase64 = Utils.ByteArrayToBase64String(Encoding.ASCII.GetBytes(clientData)); + + var registerResponse = new RegisterResponse(registrationDataBase64, clientDataBase64); + + return registerResponse; + } + + public static byte[] GetApplicationParameter(string appId) + { + var sha256 = new SHA256Managed(); + return sha256.ComputeHash(Encoding.ASCII.GetBytes(appId)); + } + + public static string GetRegistrationClientData(string challenge, string facet) + { + var clientData = new + { + typ = "navigator.id.finishEnrollment", + challenge = challenge, + origin = facet + }; + + return JsonConvert.SerializeObject(clientData); + } + + public static byte[] GetChallengeParameter(string clientData) + { + var sha256 = new SHA256Managed(); + return sha256.ComputeHash(Encoding.ASCII.GetBytes(clientData)); + } + + public static async Task AuthenticateAsync(U2FHidDevice u2fHidDevice, StartedAuthentication request, string facet, bool checkOnly) + { + ValidateRequest(request, facet); + + var sha256 = new SHA256Managed(); + var appParam = sha256.ComputeHash(Encoding.ASCII.GetBytes(request.AppId)); + + var clientDataString = GetAuthenticationClientData(request.Challenge, facet); + var clientParam = sha256.ComputeHash(Encoding.ASCII.GetBytes(clientDataString)); + + var keyHandleDecoded = Utils.Base64StringToByteArray(request.KeyHandle); + + var byteArrayBuilder = new ByteArrayBuilder(); + byteArrayBuilder.Append(clientParam); + byteArrayBuilder.Append(appParam); + byteArrayBuilder.Append((byte)keyHandleDecoded.Length); + byteArrayBuilder.Append(keyHandleDecoded); + + var data = byteArrayBuilder.GetBytes(); + var p1 = (byte)(checkOnly ? 0x07 : 0x03); + var p2 = (byte)0x00; + + var response = await u2fHidDevice.SendApduAsync(Constants.INS_SIGN, p1, p2, data); + + var responseBase64 = Utils.ByteArrayToBase64String(response); + var clientDataBase64 = Utils.ByteArrayToBase64String(Encoding.ASCII.GetBytes(clientDataString)); + + var authenticateResponse = new AuthenticateResponse(clientDataBase64, responseBase64, request.KeyHandle); + + return authenticateResponse; + } + + private static string GetAuthenticationClientData(string challenge, string facet) + { + var clientData = new + { + typ = "navigator.id.getAssertion", + challenge = challenge, + origin = facet + }; + + return JsonConvert.SerializeObject(clientData); + } + + private static void ValidateRequest(StartedRegistration request, string facet) + { + if (request.Version != U2F.U2FVersion) + { + throw new Exception($"Unsupported U2F version: {request.Version}"); + } + } + + private static void ValidateRequest(StartedAuthentication request, string facet) + { + if (request.Version != U2F.U2FVersion) + { + throw new Exception($"Unsupported U2F version: {request.Version}"); + } + } + } +} \ No newline at end of file diff --git a/u2fhost/app.config b/u2fhost/app.config new file mode 100644 index 0000000..3c575dd --- /dev/null +++ b/u2fhost/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/u2fhost/packages.config b/u2fhost/packages.config new file mode 100644 index 0000000..8d2b901 --- /dev/null +++ b/u2fhost/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/u2fhost/u2fhost.csproj b/u2fhost/u2fhost.csproj new file mode 100644 index 0000000..df40081 --- /dev/null +++ b/u2fhost/u2fhost.csproj @@ -0,0 +1,84 @@ + + + + + Debug + AnyCPU + {F28D70D6-4109-4F8B-8303-6F253AB7E436} + Library + Properties + u2fhost + u2fhost + v4.5 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + ..\packages\BouncyCastle.1.7.0\lib\Net40-Client\BouncyCastle.Crypto.dll + True + + + ..\packages\hidlibrary.3.2.31.0\lib\HidLibrary.dll + True + + + ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + + + + + + + + ..\packages\u2flib.1.0.1\lib\net45\u2flib.dll + True + + + + + + + + + + + + + + + + + + + \ No newline at end of file