This commit is contained in:
Philipp Crocoll
2017-10-24 06:57:47 +02:00
parent e491463862
commit 6eee282fa4
4 changed files with 594 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace KeeTrayTOTP.Libraries
{
/// <summary>
/// Utility to deal with Base32 encoding and decoding.
/// </summary>
/// <remarks>
/// http://tools.ietf.org/html/rfc4648
/// </remarks>
public static class Base32
{
/// <summary>
/// The number of bits in a base32 encoded character.
/// </summary>
private const int encodedBitCount = 5;
/// <summary>
/// The number of bits in a byte.
/// </summary>
private const int byteBitCount = 8;
/// <summary>
/// A string containing all of the base32 characters in order.
/// This allows a simple indexof or [index] to convert between
/// a numeric value and an encoded character and vice versa.
/// </summary>
private const string encodingChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
/// <summary>
/// Takes a block of data and converts it to a base 32 encoded string.
/// </summary>
/// <param name="data">Input data.</param>
/// <returns>base 32 string.</returns>
public static string Encode(byte[] data)
{
if (data == null)
throw new ArgumentNullException();
if (data.Length == 0)
throw new ArgumentNullException();
// The output character count is calculated in 40 bit blocks. That is because the least
// common blocks size for both binary (8 bit) and base 32 (5 bit) is 40. Padding must be used
// to fill in the difference.
int outputCharacterCount = (int)Math.Ceiling(data.Length / (decimal)encodedBitCount) * byteBitCount;
char[] outputBuffer = new char[outputCharacterCount];
byte workingValue = 0;
short remainingBits = encodedBitCount;
int currentPosition = 0;
foreach (byte workingByte in data)
{
workingValue = (byte)(workingValue | (workingByte >> (byteBitCount - remainingBits)));
outputBuffer[currentPosition++] = encodingChars[workingValue];
if (remainingBits <= byteBitCount - encodedBitCount)
{
workingValue = (byte)((workingByte >> (byteBitCount - encodedBitCount - remainingBits)) & 31);
outputBuffer[currentPosition++] = encodingChars[workingValue];
remainingBits += encodedBitCount;
}
remainingBits -= byteBitCount - encodedBitCount;
workingValue = (byte)((workingByte << remainingBits) & 31);
}
// If we didn't finish, write the last current working char.
if (currentPosition != outputCharacterCount)
outputBuffer[currentPosition++] = encodingChars[workingValue];
// RFC 4648 specifies that padding up to the end of the next 40 bit block must be provided
// Since the outputCharacterCount does account for the paddingCharacters, fill it out.
while (currentPosition < outputCharacterCount)
{
// The RFC defined paddinc char is '='.
outputBuffer[currentPosition++] = '=';
}
return new string(outputBuffer);
}
/// <summary>
/// Takes a base 32 encoded value and converts it back to binary data.
/// </summary>
/// <param name="base32">Base 32 encoded string.</param>
/// <returns>Binary data.</returns>
public static byte[] Decode(string base32)
{
if (string.IsNullOrEmpty(base32))
throw new ArgumentNullException();
var unpaddedBase32 = base32.ToUpperInvariant().TrimEnd('=');
foreach (var c in unpaddedBase32)
{
if (encodingChars.IndexOf(c) < 0)
throw new ArgumentException("Base32 contains illegal characters.");
}
// we have already removed the padding so this will tell us how many actual bytes there should be.
int outputByteCount = unpaddedBase32.Length * encodedBitCount / byteBitCount;
byte[] outputBuffer = new byte[outputByteCount];
byte workingByte = 0;
short bitsRemaining = byteBitCount;
int mask = 0;
int arrayIndex = 0;
foreach (char workingChar in unpaddedBase32)
{
int encodedCharacterNumericValue = encodingChars.IndexOf(workingChar);
if (bitsRemaining > encodedBitCount)
{
mask = encodedCharacterNumericValue << (bitsRemaining - encodedBitCount);
workingByte = (byte)(workingByte | mask);
bitsRemaining -= encodedBitCount;
}
else
{
mask = encodedCharacterNumericValue >> (encodedBitCount - bitsRemaining);
workingByte = (byte)(workingByte | mask);
outputBuffer[arrayIndex++] = workingByte;
workingByte = (byte)(encodedCharacterNumericValue << (byteBitCount - encodedBitCount + bitsRemaining));
bitsRemaining += byteBitCount - encodedBitCount;
}
}
return outputBuffer;
}
}
}

View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace KeeTrayTOTP.Libraries
{
class TOTPEncoder
{
/// <summary>
/// Character set for authenticator code
/// </summary>
private static readonly char[] STEAMCHARS = new char[] {
'2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C',
'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q',
'R', 'T', 'V', 'W', 'X', 'Y'};
private static uint OTP2UInt(byte[] totp)
{
uint fullcode = BitConverter.ToUInt32(totp, 0) & 0x7fffffff;
return fullcode;
}
public static readonly Func<byte[], int, string> rfc6238 = (byte[] bytes, int length) =>
{
uint fullcode = TOTPEncoder.OTP2UInt(bytes);
uint mask = (uint)Math.Pow(10, length);
return (fullcode % mask).ToString(new string('0', length));
};
public static readonly Func<byte[], int, string> steam = (byte[] bytes, int length) =>
{
uint fullcode = TOTPEncoder.OTP2UInt(bytes);
StringBuilder code = new StringBuilder();
for (var i = 0; i < length; i++)
{
code.Append(STEAMCHARS[fullcode % STEAMCHARS.Length]);
fullcode /= (uint)STEAMCHARS.Length;
}
return code.ToString();
};
}
}

View File

@@ -0,0 +1,264 @@
using System;
using System.Security;
using System.Security.Cryptography;
namespace KeeTrayTOTP.Libraries
{
/// <summary>
/// Provides Time-based One Time Passwords RFC 6238.
/// </summary>
public class TOTPProvider
{
/// <summary>
/// Time reference for TOTP generation.
/// </summary>
private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
/// <summary>
/// Duration of generation of each totp, in seconds.
/// </summary>
private int duration;
public int Duration
{
get
{
return this.duration;
}
set
{
if (!(value > 0)) throw new Exception("Invalid Duration."); //Throws an exception if the duration is invalid as the class cannot work without it.
this.duration = value; //Defines variable from argument.
}
}
/// <summary>
/// Length of the generated totp.
/// </summary>
private int length;
public int Length
{
get
{
return this.length;
}
set
{
//Throws an exception if the length is invalid as the class cannot work without it.
if (value < 4 || value > 8) throw new Exception("Invalid Length.");
this.length = value; //Defines variable from argument.
}
}
/// <summary>
/// TOTP Encoder.
/// </summary>
private Func<byte[], int, string> encoder;
public Func<byte[], int, string> Encoder
{
get
{
return this.encoder;
}
set
{
this.encoder = value; //Defines variable from argument.
}
}
/// <summary>
/// Sets the time span that is used to match the server's UTC time to ensure accurate generation of Time-based One Time Passwords.
/// </summary>
private TimeSpan timeCorrection;
public TimeSpan TimeCorrection
{
get
{
return this.timeCorrection;
}
set
{
this.timeCorrection = value; //Defines variable from argument.
}
}
private bool timeCorrectionError;
public bool TimeCorrectionError
{
get
{
return this.timeCorrectionError;
}
}
/// <summary>
/// Instanciates a new TOTP_Generator.
/// </summary>
/// <param name="initDuration">Duration of generation of each totp, in seconds.</param>
/// <param name="initLength">Length of the generated totp.</param>
/// <param name="initEncoder">The output encoder.</param>
/*public TOTPProvider(int initDuration, int initLength, Func<byte[], int, string> initEncoder)
{
this.Duration = initDuration;
this.Length = initLength;
this.encoder = initEncoder;
this.TimeCorrection = TimeSpan.Zero;
}*/
/// <summary>
/// Instanciates a new TOTP_Generator.
/// </summary>
/// <param name="initSettings">Saved Settings.</param>
public TOTPProvider(string[] Settings)
{
this.duration = Convert.ToInt16(Settings[0]);
if (Settings[1] == "S")
{
this.length = 5;
this.encoder = TOTPEncoder.steam;
}
else
{
this.length = Convert.ToInt16(Settings[1]);
this.encoder = TOTPEncoder.rfc6238;
}
if(Settings.Length > 2 && Settings[2] != String.Empty)
{
{
this.TimeCorrection = TimeSpan.Zero;
this.timeCorrectionError = false;
}
}
else
{
this.TimeCorrection = TimeSpan.Zero;
}
}
/// <summary>
/// Returns current time with correction int UTC format.
/// </summary>
public DateTime Now
{
get
{
return DateTime.UtcNow - timeCorrection; //Computes current time minus time correction giving the corrected time.
}
}
/// <summary>
/// Returns the time remaining before counter incrementation.
/// </summary>
public int Timer
{
get
{
var n = (duration - (int)((Now - UnixEpoch).TotalSeconds % duration)); //Computes the seconds left before counter incrementation.
return n == 0 ? duration : n; //Returns timer value from 30 to 1.
}
}
/// <summary>
/// Returns number of intervals that have elapsed.
/// </summary>
public long Counter
{
get
{
var ElapsedSeconds = (long)Math.Floor((Now - UnixEpoch).TotalSeconds); //Compute current counter for current time.
return ElapsedSeconds / duration; //Applies specified interval to computed counter.
}
}
/// <summary>
/// Converts an unsigned integer to binary data.
/// </summary>
/// <param name="n">Unsigned Integer.</param>
/// <returns>Binary data.</returns>
private byte[] GetBytes(ulong n)
{
byte[] b = new byte[8]; //Math.
b[0] = (byte)(n >> 56); //Math.
b[1] = (byte)(n >> 48); //Math.
b[2] = (byte)(n >> 40); //Math.
b[3] = (byte)(n >> 32); //Math.
b[4] = (byte)(n >> 24); //Math.
b[5] = (byte)(n >> 16); //Math.
b[6] = (byte)(n >> 8); //Math.
b[7] = (byte)(n); //Math.
return b;
}
/// <summary>
/// Generate a TOTP using provided binary data.
/// </summary>
/// <param name="key">Binary data.</param>
/// <returns>Time-based One Time Password encoded byte array.</returns>
public byte[] Generate(byte[] key)
{
System.Security.Cryptography.HMACSHA1 hmac = new System.Security.Cryptography.HMACSHA1(key, true); //Instanciates a new hash provider with a key.
byte[] hash = hmac.ComputeHash(GetBytes((ulong)Counter)); //Generates hash from key using counter.
hmac.Clear(); //Clear hash instance securing the key.
/*int binary = //Math.
((hash[offset] & 0x7f) << 24) //Math.
| ((hash[offset + 1] & 0xff) << 16) //Math.
| ((hash[offset + 2] & 0xff) << 8) //Math.
| (hash[offset + 3] & 0xff); //Math.
int password = binary % (int)Math.Pow(10, length); //Math.*/
int offset = hash[hash.Length - 1] & 0x0f; //Math.
byte[] totp = { hash[offset + 3], hash[offset + 2], hash[offset + 1], hash[offset] };
return totp;
/*
return password.ToString(new string('0', length)); //Math.*/
}
/// <summary>
/// Generate a TOTP using provided binary data.
/// </summary>
/// <param name="key">Key in String Format.</param>
/// <returns>Time-based One Time Password encoded byte array.</returns>
public string Generate(string key)
{
byte[] bkey = Base32.Decode(key);
return this.GenerateByByte(bkey);
}
/// <summary>
/// Generate a TOTP using provided binary data.
/// </summary>
/// <param name="key">Binary data.</param>
/// <returns>Time-based One Time Password encoded byte array.</returns>
public string GenerateByByte(byte[] key)
{
HMACSHA1 hmac = new HMACSHA1(key, true); //Instanciates a new hash provider with a key.
byte[] codeInterval = BitConverter.GetBytes((ulong)Counter);
if (BitConverter.IsLittleEndian)
Array.Reverse(codeInterval);
byte[] hash = hmac.ComputeHash(codeInterval); //Generates hash from key using counter.
hmac.Clear(); //Clear hash instance securing the key.
int start = hash[hash.Length - 1] & 0xf;
byte[] totp = new byte[4];
Array.Copy(hash, start, totp, 0, 4);
if (BitConverter.IsLittleEndian)
Array.Reverse(totp);
return this.encoder(totp, length);
}
}
}

View File

@@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace KeeTrayTOTP.Libraries
{
/// <summary>
/// Provides time correction for Time-based One Time Passwords that require accurate DateTime syncronisation with server.
/// </summary>
public class TimeCorrectionProvider
{
/// <summary>
/// Timer providing the delay between each time correction check.
/// </summary>
private System.Timers.Timer _Timer;
/// <summary>
/// Thread which handles the time correction check.
/// </summary>
private System.Threading.Thread Task;
private bool _Enable;
/// <summary>
/// Defines weither or not the class will attempt to get time correction from the server.
/// </summary>
public bool Enable { get { return _Enable; } set { _Enable = value; _Timer.Enabled = value; } }
private static int _Interval = 60;
/// <summary>
/// Gets or sets the interval in minutes between each online checks for time correction.
/// </summary>
/// <value>Time</value>
public static int Interval { get { return _Interval; } set { _Interval = value; } }
private long _IntervalStretcher;
private volatile string _Url;
/// <summary>
/// Returns the URL this instance is using to checks for time correction.
/// </summary>
public string Url { get { return _Url; } }
private TimeSpan _TimeCorrection;
/// <summary>
/// Returns the time span between server UTC time and this computer's UTC time of the last check for time correction.
/// </summary>
public TimeSpan TimeCorrection { get { return _TimeCorrection; } }
private DateTime _LastUpdateDateTime;
/// <summary>
/// Returns the date and time in universal format of the last online check for time correction.
/// </summary>
public DateTime LastUpdateDateTime { get { return _LastUpdateDateTime; } }
private bool _LastUpdateSucceded = false;
/// <summary>
/// Returns true if the last check for time correction was successful.
/// </summary>
public bool LastUpdateSucceded { get { return _LastUpdateSucceded; } }
/// <summary>
/// Instanciates a new TOTP_TimeCorrection using the specified URL to contact the server.
/// </summary>
/// <param name="Url">URL of the server to get check.</param>
/// <param name="Enable">Enable or disable the time correction check.</param>
public TimeCorrectionProvider(string Url, bool Enable = true)
{
if (Url == string.Empty) throw new Exception("Invalid URL."); //Throws exception if the URL is invalid as the class cannot work without it.
_Url = Url; //Defines variable from argument.
_Enable = Enable; //Defines variable from argument.
_LastUpdateDateTime = DateTime.MinValue; //Defines variable from non-constant default value.
_TimeCorrection = TimeSpan.Zero; //Defines variable from non-constant default value.
_Timer = new System.Timers.Timer(); //Instanciates timer.
_Timer.Elapsed += Timer_Elapsed; //Handles the timer event
_Timer.Interval = 1000; //Defines the timer interval to 1 seconds.
_Timer.Enabled = _Enable; //Defines the timer to run if the class is initially enabled.
Task = new System.Threading.Thread(Task_Thread); //Instanciate a new task.
if (_Enable) Task.Start(); //Starts the new thread if the class is initially enabled.
}
/// <summary>
/// Task that occurs every time the timer's interval has elapsed.
/// </summary>
private void Timer_Elapsed(object sender, EventArgs e)
{
_IntervalStretcher++; //Increments timer.
if (_IntervalStretcher >= (60 * _Interval)) //Checks if the specified delay has been reached.
{
_IntervalStretcher = 0; //Resets the timer.
Task_Do(); //Attempts to run a new task
}
}
/// <summary>
/// Instanciates a new task and starts it.
/// </summary>
/// <returns>Informs if reinstanciation of the task has succeeded or not. Will fail if the thread is still active from a previous time correction check.</returns>
private bool Task_Do()
{
if (!Task.IsAlive) //Checks if the task is still running.
{
Task = new System.Threading.Thread(Task_Thread); //Instanciate a new task.
Task.Start(); //Starts the new thread.
return true; //Informs if successful
}
return false; //Informs if failed
}
/// <summary>
/// Event that occurs when the timer has reached the required value. Attempts to get time correction from the server.
/// </summary>
private void Task_Thread()
{
try
{
var WebClient = new System.Net.WebClient(); //WebClient to connect to server.
WebClient.DownloadData(_Url); //Downloads the server's page using HTTP or HTTPS.
var DateHeader = WebClient.ResponseHeaders.Get("Date"); //Gets the date from the HTTP header of the downloaded page.
_TimeCorrection = DateTime.UtcNow - DateTime.Parse(DateHeader, System.Globalization.CultureInfo.InvariantCulture.DateTimeFormat).ToUniversalTime(); //Compares the downloaded date to the systems date giving us a timespan.
_LastUpdateSucceded = true; //Informs that the date check has succeeded.
}
catch (Exception)
{
_LastUpdateSucceded = false; //Informs that the date check has failed.
}
_LastUpdateDateTime = DateTime.Now; //Informs when the last update has been attempted (succeeded or not).
}
/// <summary>
/// Perform a time correction check, may a few seconds.
/// </summary>
/// <param name="ResetTimer">Resets the timer to 0. Occurs even if the attempt to attempt a new time correction fails.</param>
/// <param name="ForceCheck">Attempts to get time correction even if disabled.</param>
/// <returns>Informs if the time correction check was attempted or not. Will fail if the thread is still active from a previous time correction check.</returns>
public bool CheckNow(bool ResetTimer = true, bool ForceCheck = false)
{
if (ResetTimer) //Checks if the timer should be reset.
{
_IntervalStretcher = 0; //Resets the timer.
}
if (ForceCheck || _Enable) //Checks if this check is forced or if time correction is enabled.
{
return Task_Do(); //Attempts to run a new task and informs if attempt to attemp is a success of fail
}
return false; //Informs if not attempted to attempt
}
}
}