/*
* Copyright (C) 2015 Colin Mackie.
* This software is distributed under the terms of the GNU General Public License.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Reflection;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Text;
using System.Security.Cryptography;
using System.Resources;
using System.Threading;
namespace WinAuth
{
///
/// Store the current YubiKey data and seed from original decryption. This is so
/// we can update the config file without requiring the user key press (if enabled)
/// from the yubi. Although DP is used, pretty pointless since it is put into managed
/// memory anyway.
///
public class YubikeyData
{
///
/// Length of key
///
public int Length;
///
/// Random seed
///
public string Seed;
///
/// Protected data block
///
private byte[] _data;
///
/// Get/set the protected data into a managed array
///
public byte[] Data
{
get
{
if (Length == 0)
{
return null;
}
byte[] b = new byte[_data.Length];
Array.Copy(_data, b, _data.Length);
return ProtectedData.Unprotect(b, null, DataProtectionScope.CurrentUser);
}
set
{
if (value == null)
{
_data = null;
Length = 0;
}
else
{
Length = value.Length;
byte[] b = new byte[value.Length];
Array.Copy(value, b, value.Length);
_data = ProtectedData.Protect(b, null, DataProtectionScope.CurrentUser);
}
}
}
}
///
/// Class wrapping API around pinvoke YubiKey DLL
///
public class YubiKey : IDisposable
{
///
/// USB Device Vendor and Product IDs for YubiKeys
///
public const int VENDOR_ID = 0x1050; /* Global vendor ID */
public const int YUBIKEY_PID = 0x0010; /* Yubikey (version 1 and 2) */
public const int NEO_OTP_PID = 0x0110; /* Yubikey NEO - OTP only */
public const int NEO_OTP_CCID_PID = 0x0111; /* Yubikey NEO - OTP and CCID */
public const int NEO_CCID_PID = 0x0112; /* Yubikey NEO - CCID only */
public const int NEO_U2F_PID = 0x0113; /* Yubikey NEO - U2F only */
public const int NEO_OTP_U2F_PID = 0x0114; /* Yubikey NEO - OTP and U2F */
public const int NEO_U2F_CCID_PID = 0x0115; /* Yubikey NEO - U2F and CCID */
public const int NEO_OTP_U2F_CCID_PID = 0x0116; /* Yubikey NEO - OTP, U2F and CCID */
public const int YK4_OTP_PID = 0x0401; /* Yubikey 4 - OTP only */
public const int YK4_U2F_PID = 0x0402; /* Yubikey 4 - U2F only */
public const int YK4_OTP_U2F_PID = 0x0403; /* Yubikey 4 - OTP and U2F */
public const int YK4_CCID_PID = 0x0404; /* Yubikey 4 - CCID only */
public const int YK4_OTP_CCID_PID = 0x0405; /* Yubikey 4 - OTP and CCID */
public const int YK4_U2F_CCID_PID = 0x0406; /* Yubikey 4 - U2F and CCID */
public const int YK4_OTP_U2F_CCID_PID = 0x0407; /* Yubikey 4 - OTP, U2F and CCID */
public const int PLUS_U2F_OTP_PID = 0x0410; /* Yubikey plus - OTP+U2F */
///
/// Array of valid product IDs
///
public static int[] DEVICE_IDS = new int[] {
YUBIKEY_PID,
NEO_OTP_PID,
NEO_OTP_CCID_PID,
NEO_CCID_PID,
NEO_U2F_PID,
NEO_OTP_U2F_PID,
NEO_U2F_CCID_PID,
NEO_OTP_U2F_CCID_PID,
YK4_OTP_PID,
YK4_U2F_PID,
YK4_OTP_U2F_PID,
YK4_CCID_PID,
YK4_OTP_CCID_PID,
YK4_U2F_CCID_PID,
YK4_OTP_U2F_CCID_PID,
PLUS_U2F_OTP_PID
};
///
/// USDB device GUID for keyboard
///
public static Guid GUID_DEVINTERFACE_KEYBOARD = new Guid("884b96c3-56ef-11d1-bc8c-00a0c91405dd");
///
/// Name of AppData folder
///
public const string APPDATAFOLDER = "WinAuth";
///
/// Name of our native x86 YubiKey DLL
///
private const string YUBI_LIBRARY_NAME_X86 = "WinAuth.YubiKey.x86.dll";
///
/// Name of our native x64 YubiKey DLL
///
private const string YUBI_LIBRARY_NAME_X64 = "WinAuth.YubiKey.x64.dll";
///
/// Fix Status stuct returned from Yubkey DLLs
///
public struct STATUS
{
public byte VersionMajor; /* Firmware version information */
public byte VersionMinor;
public byte VersionBuild;
public byte PgmSeq; /* Programming sequence number. 0 if no valid configuration */
public UInt16 TouchLevel; /* Level from touch detector */
};
public struct INFO
{
public STATUS Status;
public UInt32 Serial;
public UInt32 Pid;
public string Error;
};
///
/// Handle to loaded pinvoke library
///
private IntPtr _library;
///
/// Path of file for native library
///
private string _libraryPath;
///
/// Current YubiKey info
///
public INFO Info { get; set; }
///
/// Native calls
///
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr LoadLibrary(string libname);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool FreeLibrary(IntPtr hModule);
[DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
private static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate int LastErrorDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate bool IsInsertedDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate int GetInfoDelegate([MarshalAs(UnmanagedType.Struct)] out INFO status);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate int SetChallengeResponseDelegate(
[MarshalAs(UnmanagedType.U4)] int slot,
byte[] secret,
[MarshalAs(UnmanagedType.U4)] int keysize,
[MarshalAs(UnmanagedType.Bool)] bool userpress,
byte[] access_code);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate int ChallengeResponseDelegate(
[MarshalAs(UnmanagedType.U4)] int slot,
[MarshalAs(UnmanagedType.Bool)] bool may_block,
byte[] challenge,
[MarshalAs(UnmanagedType.U4)] int challenge_len,
byte[] response,
[MarshalAs(UnmanagedType.U4)] int response_len);
///
/// Create the YubiKey instance
///
protected YubiKey()
{
YubiData = new YubikeyData();
}
///
/// Finalizer
///
~YubiKey()
{
Dispose(false);
}
///
/// Dispose this object
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Dispose any resources
///
/// called via Dispose
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// free managed resources
}
// free native resources
if (_library != IntPtr.Zero)
{
FreeLibrary(_library);
_library = IntPtr.Zero;
}
// delete temp library file
if (string.IsNullOrEmpty(_libraryPath) == false && File.Exists(_libraryPath) == true)
{
try
{
File.Delete(_libraryPath);
}
catch (Exception) { }
_libraryPath = null;
}
}
///
/// Mutex for the init method
///
private object _initLock = new object();
///
/// Stored data for Yubikey
///
public YubikeyData YubiData { get; set; }
///
/// Create a new Yubikey instance and initialise
///
///
///
public static YubiKey CreateInstance()
{
YubiKey instance = new YubiKey();
instance.Init();
return instance;
}
///
/// Initialise the Yubikey and get its status
///
///
public void Init()
{
lock (_initLock)
{
if (Info.Status.VersionMajor != 0)
{
return;
}
// load library and load info
LoadLibrary();
INFO info = new INFO();
GetInfoDelegate f = GetFunction("GetInfo");
int ret = f(out info);
if (ret > 1)
{
info.Error = string.Format("Error {0}", ret);
}
Info = info;
}
}
///
/// Load the 32 or 64 bit native YubiKey DLL depending on current platform, or download from winauth servers
///
/// option to download DLL if required
private void LoadLibrary()
{
if (_library != IntPtr.Zero)
{
return;
}
// load the library
var libraryname = (IntPtr.Size == 4 ? YUBI_LIBRARY_NAME_X86 : YUBI_LIBRARY_NAME_X64);
// create temp file
_libraryPath = Path.GetTempFileName();
File.Delete(_libraryPath);
_libraryPath += ".dll";
// extract the DLL from our resources
using (var ins = typeof(Authenticator).Assembly.GetManifestResourceStream("WinAuth.Resources." + libraryname))
{
using (var outs = new FileStream(_libraryPath, FileMode.Create, FileAccess.Write))
{
var buffer = new byte[4096];
int read;
while ((read = ins.Read(buffer, 0, buffer.Length)) != 0)
{
outs.Write(buffer, 0, read);
}
}
}
// load the library
_library = LoadLibrary(_libraryPath);
if (_library == IntPtr.Zero)
{
int error = Marshal.GetLastWin32Error();
throw new LibraryNotFoundException("Cannot load the YubiKey AddOn: " + error);
}
}
///
/// Dynamically get the function point to a native DLL entry point
///
/// delegate we require
/// name of function
/// function delegate
private TDelegate GetFunction(string name) where TDelegate : class
{
IntPtr p = GetProcAddress(_library, name);
if (p == IntPtr.Zero)
{
return null;
}
Delegate f = Marshal.GetDelegateForFunctionPointer(p, typeof(TDelegate));
object obj = f;
return (TDelegate)obj;
}
///
/// Get the last error from the native DLL
///
/// last error code
public int LastError()
{
LastErrorDelegate fe = GetFunction("LastError");
return fe();
}
///
/// Set the Challenge/Response for the Yubikey
///
/// slot number, only 1 or 2
/// 20 byte key
/// size of key - currently must be 20 bytes
/// if we require a keypress
/// current access code if required
public void SetChallengeResponse(int slot, byte[] key, int keysize, bool press, byte[] accesscode = null)
{
LoadLibrary();
SetChallengeResponseDelegate f = GetFunction("SetChallengeResponse");
int ret = f(slot, key, keysize, press, accesscode);
if (ret != 0)
{
throw new CannotSetChallengeResponseException(string.Format("Cannot set ChallengeResponse. Error {0}:{1}.", ret, LastError()));
}
}
///
/// Perform the Challenge/Response with the current data
///
/// slot number, 1 or 2
/// challenge array, up to 64 bytes
/// if we are allow to block (i.e. if keypress = true)
/// to save returned hash
/// hash result of challengeresponse
public byte[] ChallengeResponse(int slot, byte[] challenge, bool allowBlock = true, byte[] hash = null)
{
LoadLibrary();
byte[] maxhash = new byte[64];
ChallengeResponseDelegate f = GetFunction("ChallengeResponse");
int hashret = f(slot, allowBlock, challenge, challenge.Length, maxhash, maxhash.Length);
if (hashret != 0)
{
int lasterror = LastError();
throw new ChallengeResponseException(string.Format("Cannot perform ChallengeResponse. Error {0}:{1}.", hashret, lasterror));
}
if (hash == null)
{
hash = new byte[20];
}
Array.Copy(maxhash, 0, hash, 0, hash.Length);
return hash;
}
}
///
/// General YubiKey exception
///
public class YubKeyException : ApplicationException
{
public YubKeyException(string msg = null, Exception ex = null) : base(msg, ex) { }
}
///
/// Exception if the native library cannot be loaded
///
public class LibraryNotFoundException : YubKeyException
{
public LibraryNotFoundException(string msg = null, Exception ex = null) : base(msg, ex) { }
}
///
/// Exception if the Challenge/Response cannot be set
///
public class CannotSetChallengeResponseException : YubKeyException
{
public CannotSetChallengeResponseException(string msg = null, Exception ex = null) : base(msg, ex) { }
}
///
/// Exception if the ChallengeResponse cannot be executed
///
public class ChallengeResponseException : YubKeyException
{
public ChallengeResponseException(string msg = null, Exception ex = null) : base(msg, ex) { }
}
}