Thursday, June 19, 2014

Utility for two-factor verification with Google Authenticator

There are a lot of code samples out there for this particular task.  However I had trouble finding a good one that showed how to authenticate against several time intervals, for better user experience, so here is a code sample.

Some references:

Comments:

  • This class uses the Singleton Pattern, which I prefer over static methods because the singleton can be mocked more easily.
  • The GenerateSecretKey method creates an encrypted key to store with an individual user (e.g., using a custom SimpleMembership configuration)
  • The IsTwoFactorVerificationCodeValid method called by an external class such as a custom validator
  • The GetCurrentCountersWithBeforeAndAfterIntervals method is what calculates the previous, current, and future intervals

Code sample:

1 using System;
2 using System.Linq;
3 using System.Collections.Generic;
4 using System.Security.Cryptography;
5 using System.Text;
6
7 namespace MyNamespace
8 {
9 public class GoogleAuthenticatorUtility
10 {
11 private const int NUMBER_OF_DIGITS = 6;
12 private const int NUMBER_OF_SECONDS_IN_INTERVAL = 30;
13
14 private readonly DateTime _unixEpoch;
15
16 private GoogleAuthenticatorUtility()
17 {
18 _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
19 }
20
21 private static GoogleAuthenticatorUtility _instance;
22 public static GoogleAuthenticatorUtility Instance
23 {
24 get { _instance = _instance ?? new GoogleAuthenticatorUtility(); return _instance; }
25 set { _instance = value; }
26 }
27
28
29 public virtual bool IsTwoFactorVerificationCodeValid(string userSecret, string verificationCode)
30 {
31 int[] passwords = GenerateTimeBasedPasswords(userSecret);
32 string[] allPasswords = GeneratePasswords(passwords);
33 return allPasswords.Contains(verificationCode);
34 }
35
36 public virtual string GenerateSecretKey()
37 {
38 byte[] buffer = new byte[9];
39
40 using (RandomNumberGenerator rng = RNGCryptoServiceProvider.Create())
41 {
42 rng.GetBytes(buffer);
43 }
44
45 return Convert.ToBase64String(buffer)
46 .Substring(0, 10)
47 .Replace('/', '0')
48 .Replace('+', '1');
49 }
50
51
52 private int[] GenerateTimeBasedPasswords(string secret)
53 {
54 if (string.IsNullOrEmpty(secret))
55 {
56 throw new ArgumentException("Secret must not be null or empty", "secret");
57 }
58
59 long[] counters = GetCurrentCountersWithBeforeAndAfterIntervals();
60 int[] passwords = new int[counters.Length];
61
62 for (int i = 0; i < counters.Length; i++)
63 {
64 byte[] counter = BitConverter.GetBytes(counters[i]);
65
66 if (BitConverter.IsLittleEndian)
67 {
68 Array.Reverse(counter);
69 }
70
71 byte[] key = Encoding.ASCII.GetBytes(secret);
72
73 HMACSHA1 hmac = new HMACSHA1(key, true);
74
75 byte[] hash = hmac.ComputeHash(counter);
76
77 int offset = hash[hash.Length - 1] & 0xf;
78
79 int binary =
80 ((hash[offset] & 0x7f) << 24)
81 | ((hash[offset + 1] & 0xff) << 16)
82 | ((hash[offset + 2] & 0xff) << 8)
83 | (hash[offset + 3] & 0xff);
84
85 passwords[i] = binary % (int)Math.Pow(10, NUMBER_OF_DIGITS); // 6 digits
86 }
87
88 return passwords;
89 }
90
91 private long[] GetCurrentCountersWithBeforeAndAfterIntervals()
92 {
93 var counters = new long[3];
94 double totalSeconds = (DateTime.UtcNow - _unixEpoch).TotalSeconds;
95
96 counters[0] = (long)((totalSeconds - NUMBER_OF_SECONDS_IN_INTERVAL) / NUMBER_OF_SECONDS_IN_INTERVAL);
97 counters[1] = (long)(totalSeconds / NUMBER_OF_SECONDS_IN_INTERVAL);
98 counters[2] = (long)((totalSeconds + NUMBER_OF_SECONDS_IN_INTERVAL) / NUMBER_OF_SECONDS_IN_INTERVAL);
99
100 return counters;
101 }
102
103 private string[] GeneratePasswords(int[] timeBasedPasswords)
104 {
105 var passwords = new string[timeBasedPasswords.Length];
106 for (int i = 0; i < timeBasedPasswords.Length; i++)
107 {
108 passwords[i] = timeBasedPasswords[i].ToString(new string('0', NUMBER_OF_DIGITS));
109 }
110
111 return passwords;
112 }
113 }
114 }
115

No comments:

Post a Comment