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