forked from BrandonPotter/GoogleAuthenticator
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTwoFactorAuthenticator.cs
More file actions
371 lines (319 loc) · 19.8 KB
/
TwoFactorAuthenticator.cs
File metadata and controls
371 lines (319 loc) · 19.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
using QRCoder;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace Google.Authenticator
{
/// <summary>
/// modified from
/// http://brandonpotter.com/2014/09/07/implementing-free-two-factor-authentication-in-net-using-google-authenticator/
/// https://github.com/brandonpotter/GoogleAuthenticator
/// With elements borrowed from https://github.com/stephenlawuk/GoogleAuthenticator
/// </summary>
public class TwoFactorAuthenticator
{
private static readonly DateTime _epoch =
new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private readonly TimeSpan DefaultClockDriftTolerance;
private readonly HashType HashType;
private readonly int timeStep;
public TwoFactorAuthenticator() : this(HashType.SHA1)
{}
public TwoFactorAuthenticator(HashType hashType) : this(hashType, 30)
{
}
public TwoFactorAuthenticator(int timeStep) : this(HashType.SHA1, timeStep)
{}
/// <summary>
/// Initializes a new instance of the <see cref="TwoFactorAuthenticator"/> class.
/// </summary>
/// <param name="hashType">The type of Hash to generate (default is SHA1)</param>
/// <param name="timeStep">The length of the "time step" - i.e. how often the code changes. Default is 30.</param>
public TwoFactorAuthenticator(HashType hashType, int timeStep)
{
HashType = hashType;
DefaultClockDriftTolerance = TimeSpan.FromMinutes(5);
this.timeStep = timeStep;
}
/// <summary>
/// Generate a setup code for a Google Authenticator user to scan
/// </summary>
/// <param name="issuer">Issuer ID (the name of the system, i.e. 'MyApp'),
/// can be omitted but not recommended https://github.com/google/google-authenticator/wiki/Key-Uri-Format
/// </param>
/// <param name="accountTitleNoSpaces">Account Title (no spaces)</param>
/// <param name="accountSecretKey">Account Secret Key</param>
/// <param name="secretIsBase32">Flag saying if accountSecretKey is in Base32 format or original secret</param>
/// <param name="qrPixelsPerModule">Number of pixels per QR Module (2 pixels give ~ 100x100px QRCode,
/// should be 10 or less)</param>
/// <returns>SetupCode object</returns>
public SetupCode GenerateSetupCode(string issuer, string accountTitleNoSpaces, string accountSecretKey, bool secretIsBase32, int qrPixelsPerModule = 3) =>
GenerateSetupCode(issuer, accountTitleNoSpaces, ConvertSecretToBytes(accountSecretKey, secretIsBase32), qrPixelsPerModule);
/// <summary>
/// Generate a setup code for a Google Authenticator user to scan
/// </summary>
/// <param name="issuer">Issuer ID (the name of the system, i.e. 'MyApp'), can be omitted but not
/// recommended https://github.com/google/google-authenticator/wiki/Key-Uri-Format </param>
/// <param name="accountTitleNoSpaces">Account Title (no spaces)</param>
/// <param name="accountSecretKey">Account Secret Key as byte[]</param>
/// <param name="qrPixelsPerModule">Number of pixels per QR Module
/// (2 = ~120x120px QRCode, should be 10 or less)</param>
/// <param name="generateQrCode"></param>
/// <returns>SetupCode object</returns>
public SetupCode GenerateSetupCode(string issuer,
string accountTitleNoSpaces,
byte[] accountSecretKey,
int qrPixelsPerModule = 3,
bool generateQrCode = true)
{
if (string.IsNullOrWhiteSpace(accountTitleNoSpaces))
{
throw new NullReferenceException("Account Title is null");
}
accountTitleNoSpaces = RemoveWhitespace(Uri.EscapeDataString(accountTitleNoSpaces));
var encodedSecretKey = Base32Encoding.ToString(accountSecretKey);
var provisionUrl = string.IsNullOrWhiteSpace(issuer)
? $"otpauth://totp/{accountTitleNoSpaces}?secret={encodedSecretKey.Trim('=')}{(HashType == HashType.SHA1 ? "" : $"&algorithm={HashType}")}"
// https://github.com/google/google-authenticator/wiki/Conflicting-Accounts
// Added additional prefix to account otpauth://totp/Company:joe_example@gmail.com
// for backwards compatibility
: $"otpauth://totp/{UrlEncode(issuer)}:{accountTitleNoSpaces}?secret={encodedSecretKey.Trim('=')}&issuer={UrlEncode(issuer)}{(HashType == HashType.SHA1 ? "" : $"&algorithm={HashType}")}";
return new SetupCode(
accountTitleNoSpaces,
encodedSecretKey.Trim('='),
generateQrCode ? GenerateQrCodeUrl(qrPixelsPerModule, provisionUrl) : "",
provisionUrl);
}
private static string GenerateQrCodeUrl(int qrPixelsPerModule, string provisionUrl)
{
var qrCodeUrl = "";
try
{
using (var qrGenerator = new QRCodeGenerator())
using (var qrCodeData = qrGenerator.CreateQrCode(provisionUrl, QRCodeGenerator.ECCLevel.Q))
using (var qrCode = new PngByteQRCode(qrCodeData))
{
var qrCodeImage = qrCode.GetGraphic(qrPixelsPerModule);
qrCodeUrl = $"data:image/png;base64,{Convert.ToBase64String(qrCodeImage)}";
}
}
catch (System.Runtime.InteropServices.ExternalException e)
{
if (e.Message.Contains("GDI+") && qrPixelsPerModule > 10)
{
throw new QRException(
$"There was a problem generating a QR code. The value of {nameof(qrPixelsPerModule)}" +
" should be set to a value of 10 or less for optimal results.",
e);
}
else
{
throw;
}
}
return qrCodeUrl;
}
private static string RemoveWhitespace(string str) =>
new string(str.Where(c => !char.IsWhiteSpace(c)).ToArray());
private string UrlEncode(string value)
{
return Uri.EscapeDataString(value);
}
/// <summary>
/// This method is generally called via <see cref="GoogleAuthenticator.GetCurrentPIN()" />/>
/// </summary>
/// <param name="accountSecretKey">The acount secret key as a string</param>
/// <param name="counter">The number of 30-second (by default) intervals since the unix epoch</param>
/// <param name="digits">The desired length of the returned PIN</param>
/// <param name="secretIsBase32">Flag saying if accountSecretKey is in Base32 format or original secret</param>
/// <returns>A 'PIN' that is valid for the specified time interval</returns>
public string GeneratePINAtInterval(string accountSecretKey, long counter, int digits = 6, bool secretIsBase32 = false) =>
GeneratePINAtInterval(ConvertSecretToBytes(accountSecretKey, secretIsBase32), counter, digits);
/// <summary>
/// This method is generally called via <see cref="GoogleAuthenticator.GetCurrentPIN()" />/>
/// </summary>
/// <param name="accountSecretKey">The acount secret key as a byte array</param>
/// <param name="counter">The number of 30-second (by default) intervals since the unix epoch</param>
/// <param name="digits">The desired length of the returned PIN</param>
/// <returns>A 'PIN' that is valid for the specified time interval</returns>
public string GeneratePINAtInterval(byte[] accountSecretKey, long counter, int digits = 6) =>
GenerateHashedCode(accountSecretKey, counter, digits);
private string GenerateHashedCode(byte[] key, long iterationNumber, int digits = 6)
{
var counter = BitConverter.GetBytes(iterationNumber);
if (BitConverter.IsLittleEndian)
Array.Reverse(counter);
HMAC hmac;
if (HashType == HashType.SHA256)
hmac = new HMACSHA256(key);
else if (HashType == HashType.SHA512)
hmac = new HMACSHA512(key);
else
hmac = new HMACSHA1(key);
var hash = hmac.ComputeHash(counter);
var offset = hash[hash.Length - 1] & 0xf;
// Convert the 4 bytes into an integer, ignoring the sign.
var binary =
((hash[offset] & 0x7f) << 24)
| (hash[offset + 1] << 16)
| (hash[offset + 2] << 8)
| hash[offset + 3];
var password = binary % (int) Math.Pow(10, digits);
return password.ToString(new string('0', digits));
}
private long GetCurrentCounter() => GetCurrentCounter(DateTime.UtcNow, _epoch);
private long GetCurrentCounter(DateTime now, DateTime epoch) =>
(long) (now - epoch).TotalSeconds / timeStep;
/// <summary>
/// Given a PIN from a client, check if it is valid at the current time.
/// </summary>
/// <param name="accountSecretKey">Account Secret Key</param>
/// <param name="twoFactorCodeFromClient">The PIN from the client</param>
/// <param name="secretIsBase32">Flag saying if accountSecretKey is in Base32 format or original secret</param>
/// <returns>True if PIN is currently valid</returns>
public bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient, bool secretIsBase32 = false) =>
ValidateTwoFactorPIN(accountSecretKey, twoFactorCodeFromClient, DefaultClockDriftTolerance, secretIsBase32);
/// <summary>
/// Given a PIN from a client, check if it is valid at the current time.
/// </summary>
/// <param name="accountSecretKey">Account Secret Key</param>
/// <param name="twoFactorCodeFromClient">The PIN from the client</param>
/// <param name="timeTolerance">The time window within which to check to allow for clock drift between devices.</param>
/// <param name="secretIsBase32">Flag saying if accountSecretKey is in Base32 format or original secret</param>
/// <returns>True if PIN is currently valid</returns>
public bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient, TimeSpan timeTolerance, bool secretIsBase32 = false) =>
ValidateTwoFactorPIN(ConvertSecretToBytes(accountSecretKey, secretIsBase32), twoFactorCodeFromClient, timeTolerance);
/// <summary>
/// Given a PIN from a client, check if it is valid at the current time.
/// </summary>
/// <param name="accountSecretKey">Account Secret Key</param>
/// <param name="twoFactorCodeFromClient">The PIN from the client</param>
/// <returns>True if PIN is currently valid</returns>
public bool ValidateTwoFactorPIN(byte[] accountSecretKey, string twoFactorCodeFromClient) =>
ValidateTwoFactorPIN(accountSecretKey, twoFactorCodeFromClient, DefaultClockDriftTolerance);
/// <summary>
/// Given a PIN from a client, check if it is valid at the current time.
/// </summary>
/// <param name="accountSecretKey">Account Secret Key</param>
/// <param name="twoFactorCodeFromClient">The PIN from the client</param>
/// <param name="timeTolerance">The time window within which to check to allow for clock drift between devices.</param>
/// <returns>True if PIN is currently valid</returns>
public bool ValidateTwoFactorPIN(byte[] accountSecretKey, string twoFactorCodeFromClient, TimeSpan timeTolerance) =>
GetCurrentPINs(accountSecretKey, timeTolerance).Any(c => c == twoFactorCodeFromClient);
/// <summary>
/// Given a PIN from a client, check if it is valid at the current time.
/// </summary>
/// <param name="accountSecretKey">Account Secret Key</param>
/// <param name="twoFactorCodeFromClient">The PIN from the client</param>
/// <param name="iterationOffset">The counter window within which to check to allow for clock drift between devices.</param>
/// <param name="secretIsBase32">Flag saying if accountSecretKey is in Base32 format or original secret</param>
/// <returns>True if PIN is currently valid</returns>
public bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient, int iterationOffset, bool secretIsBase32 = false) =>
ValidateTwoFactorPIN(ConvertSecretToBytes(accountSecretKey, secretIsBase32), twoFactorCodeFromClient, iterationOffset);
/// <summary>
/// Given a PIN from a client, check if it is valid at the current time.
/// </summary>
/// <param name="accountSecretKey">Account Secret Key</param>
/// <param name="twoFactorCodeFromClient">The PIN from the client</param>
/// <param name="iterationOffset">The counter window within which to check to allow for clock drift between devices.</param>
/// <returns>True if PIN is currently valid</returns>
public bool ValidateTwoFactorPIN(byte[] accountSecretKey, string twoFactorCodeFromClient, int iterationOffset) =>
GetCurrentPINs(accountSecretKey, iterationOffset).Any(c => c == twoFactorCodeFromClient);
/// <summary>
/// Get the PIN for current time; the same code that a 2FA app would generate for the current time.
/// Do not validate directly against this as clockdrift may cause a a different PIN to be generated than one you did a second ago.
/// </summary>
/// <param name="accountSecretKey">Account Secret Key</param>
/// <param name="secretIsBase32">Flag saying if accountSecretKey is in Base32 format or original secret</param>
/// <returns>A 6-digit PIN</returns>
public string GetCurrentPIN(string accountSecretKey, bool secretIsBase32 = false) =>
GeneratePINAtInterval(accountSecretKey, GetCurrentCounter(), secretIsBase32: secretIsBase32);
/// <summary>
/// Get the PIN for current time; the same code that a 2FA app would generate for the current time.
/// Do not validate directly against this as clockdrift may cause a a different PIN to be generated than one you did a second ago.
/// </summary>
/// <param name="accountSecretKey">Account Secret Key</param>
/// <param name="now">The time you wish to generate the pin for</param>
/// <param name="secretIsBase32">Flag saying if accountSecretKey is in Base32 format or original secret</param>
/// <returns>A 6-digit PIN</returns>
public string GetCurrentPIN(string accountSecretKey, DateTime now, bool secretIsBase32 = false) =>
GeneratePINAtInterval(accountSecretKey, GetCurrentCounter(now, _epoch), secretIsBase32: secretIsBase32);
/// <summary>
/// Get the PIN for current time; the same code that a 2FA app would generate for the current time.
/// Do not validate directly against this as clockdrift may cause a a different PIN to be generated.
/// </summary>
/// <param name="accountSecretKey">Account Secret Key</param>
/// <returns>A 6-digit PIN</returns>
public string GetCurrentPIN(byte[] accountSecretKey) =>
GeneratePINAtInterval(accountSecretKey, GetCurrentCounter());
/// <summary>
/// Get the PIN for current time; the same code that a 2FA app would generate for the current time.
/// Do not validate directly against this as clockdrift may cause a a different PIN to be generated.
/// </summary>
/// <param name="accountSecretKey">Account Secret Key</param>
/// <param name="now">The time you wish to generate the pin for</param>
/// <returns>A 6-digit PIN</returns>
public string GetCurrentPIN(byte[] accountSecretKey, DateTime now) =>
GeneratePINAtInterval(accountSecretKey, GetCurrentCounter(now, _epoch));
/// <summary>
/// Get all the PINs that would be valid within the time window allowed for by the default clock drift.
/// </summary>
/// <param name="accountSecretKey">Account Secret Key</param>
/// <param name="secretIsBase32">Flag saying if accountSecretKey is in Base32 format or original secret</param>
/// <returns></returns>
public string[] GetCurrentPINs(string accountSecretKey, bool secretIsBase32 = false) =>
GetCurrentPINs(accountSecretKey, DefaultClockDriftTolerance, secretIsBase32);
/// <summary>
/// Get all the PINs that would be valid within the time window allowed for by the specified clock drift.
/// </summary>
/// <param name="accountSecretKey">Account Secret Key</param>
/// <param name="timeTolerance">The clock drift size you want to generate PINs for</param>
/// <param name="secretIsBase32">Flag saying if accountSecretKey is in Base32 format or original secret</param>
/// <returns></returns>
public string[] GetCurrentPINs(string accountSecretKey, TimeSpan timeTolerance, bool secretIsBase32 = false) =>
GetCurrentPINs(ConvertSecretToBytes(accountSecretKey, secretIsBase32), timeTolerance);
/// <summary>
/// Get all the PINs that would be valid within the time window allowed for by the default clock drift.
/// </summary>
/// <param name="accountSecretKey">Account Secret Key</param>
/// <returns></returns>
public string[] GetCurrentPINs(byte[] accountSecretKey) =>
GetCurrentPINs(accountSecretKey, DefaultClockDriftTolerance);
/// <summary>
/// Get all the PINs that would be valid within the time window allowed for by the specified clock drift.
/// </summary>
/// <param name="accountSecretKey">Account Secret Key</param>
/// <param name="timeTolerance">The clock drift size you want to generate PINs for</param>
/// <returns></returns>
public string[] GetCurrentPINs(byte[] accountSecretKey, TimeSpan timeTolerance)
{
var iterationOffset = 0;
if (timeTolerance.TotalSeconds >= timeStep)
iterationOffset = Convert.ToInt32(timeTolerance.TotalSeconds / timeStep);
return GetCurrentPINs(accountSecretKey, iterationOffset);
}
/// <summary>
/// Get all the PINs that would be valid within the time window allowed for by the specified clock drift.
/// </summary>
/// <param name="accountSecretKey">Account Secret Key</param>
/// <param name="iterationOffset">The counter drift size you want to generate PINs for</param>
/// <returns></returns>
public string[] GetCurrentPINs(byte[] accountSecretKey, int iterationOffset)
{
var codes = new List<string>();
var iterationCounter = GetCurrentCounter();
var iterationStart = iterationCounter - iterationOffset;
var iterationEnd = iterationCounter + iterationOffset;
for (var counter = iterationStart; counter <= iterationEnd; counter++)
{
codes.Add(GeneratePINAtInterval(accountSecretKey, counter));
}
return codes.ToArray();
}
private static byte[] ConvertSecretToBytes(string secret, bool secretIsBase32) =>
secretIsBase32 ? Base32Encoding.ToBytes(secret) : Encoding.UTF8.GetBytes(secret);
}
}