@@ -20,135 +20,124 @@ SOFTWARE.
20
20
21
21
module gauthenticator ;
22
22
23
- import std.stdio ;
24
- import std.digest.sha ;
23
+ import std.digest.hmac : HMAC ;
24
+ import std.digest.sha : SHA1 ,toHexString;
25
+ import std.bitmanip : nativeToBigEndian,bigEndianToNative;
26
+ import std.format : format;
25
27
26
- class GAuthenticator
27
- {
28
-
29
- // HMAC-based One Time Password(HOTP)
30
- public string getHOTPToken (const string secret, const ulong interval)
31
- {
32
-
33
- auto key=base32decode(secret);
34
- SHA1 hash;
35
- hash.start();
36
- hash.put(key);
37
- ubyte [20 ] sha1sum = hash.finish();
38
- int offset=(sha1sum[19 ] & 15 );
39
-
40
-
41
- /*
42
- hash = HMAC-SHA1(key)
43
- offset = last nibble of hash
44
- truncatedHash := hash[offset..offset+3] //4 bytes starting at the offset
45
- Set the first bit of truncatedHash to zero //remove the most significant bit
46
- code := truncatedHash mod 1000000
47
- pad code with 0 from the left until length of code is 6
48
- return code */
49
-
50
- return " 000000" ;
51
- }
28
+ // code adapted from https://github.com/tilaklodha/google-authenticator
52
29
53
- // Time-based One Time Password(TOTP)
54
- public string getTOTPToken (const string secret)
55
- {
56
- // The TOTP token is just a HOTP token seeded with every 30 seconds.
57
- import std.datetime : Clock ;
30
+ // HMAC-based One Time Password(HOTP)
31
+ public string getHOTPToken (const string secret, const ulong interval)
32
+ {
33
+ // secret is a base32 encoded string. Converts to a byte array
34
+ auto key = base32decode(secret);
35
+ // Signing the value using HMAC-SHA1 Algorithm
36
+ auto hm = HMAC ! SHA1 (key);
37
+ hm.put(nativeToBigEndian(interval));
38
+ ubyte [20 ] sha1sum = hm.finish();
39
+ // We're going to use a subset of the generated hash.
40
+ // Using the last nibble (half-byte) to choose the index to start from.
41
+ // This number is always appropriate as it's maximum decimal 15, the hash will
42
+ // have the maximum index 19 (20 bytes of SHA1) and we need 4 bytes.
43
+ const int offset = (sha1sum[19 ] & 15 );
44
+ ubyte [4 ] h = sha1sum[offset .. offset + 4 ];
45
+ // Ignore most significant bits as per RFC 4226.
46
+ // Takes division from one million to generate a remainder less than < 7 digits
47
+ const uint h12 = (bigEndianToNative! uint (h) & 0x7fffffff ) % 1_000_000;
48
+ return format (" %06d" ,h12);
49
+ }
58
50
59
- immutable ulong interval = Clock .currTime().toUnixTime() / 30 ;
60
- return getHOTPToken (secret, interval);
61
- }
51
+ // Time-based One Time Password(TOTP)
52
+ public string getTOTPToken (const string secret)
53
+ {
54
+ // The TOTP token is just a HOTP token seeded with every 30 seconds.
55
+ import std.datetime : Clock ;
56
+ immutable ulong interval = Clock .currTime().toUnixTime() / 30 ;
57
+ return getHOTPToken (secret, interval);
58
+ }
62
59
63
- // RFC 4648
64
- package ubyte [] base32decode(const string message)
60
+ // RFC 4648
61
+ private ubyte [] base32decode (const string message)
62
+ {
63
+ int buffer = 0 ;
64
+ int bitsLeft = 0 ;
65
+ ubyte [] result;
66
+ for (int i = 0 ; i < message.length; i++ )
65
67
{
66
- int buffer = 0 ;
67
- int bitsLeft = 0 ;
68
- ubyte [] result ;
69
- for ( int i = 0 ; i < message.length; i ++ )
68
+ int ch = message[i] ;
69
+ if (ch == ' = ' )
70
+ break ;
71
+ if (ch == ' ' || ch == ' \t ' || ch == ' \r ' || ch == ' \n ' || ch == ' - ' )
70
72
{
71
- int ch = message[i];
72
- if (ch == ' =' )
73
- break ;
74
- if (ch == ' ' || ch == ' \t ' || ch == ' \r ' || ch == ' \n ' || ch == ' -' )
75
- {
76
- continue ;
77
- }
78
- buffer = buffer << 5 ;
79
-
80
- // Deal with commonly mistyped characters
81
- if (ch == ' 0' )
82
- {
83
- ch = ' O' ;
84
- }
85
- else if (ch == ' 1' )
86
- {
87
- ch = ' L' ;
88
- }
89
- else if (ch == ' 8' )
90
- {
91
- ch = ' B' ;
92
- }
93
-
94
- // Look up one base32 digit
95
- if ((ch >= ' A' && ch <= ' Z' ) || (ch >= ' a' && ch <= ' z' ))
96
- {
97
- ch = (ch & 0x1F ) - 1 ;
98
- }
99
- else if (ch >= ' 2' && ch <= ' 7' )
100
- {
101
- ch -= (' 2' - 26 );
102
- }
103
-
104
- buffer |= ch;
105
- bitsLeft += 5 ;
106
- if (bitsLeft >= 8 )
107
- {
108
- int c = (buffer >> (bitsLeft - 8 ));
109
- result ~= cast (byte )(c & 0xff );
110
- bitsLeft -= 8 ;
111
- }
73
+ continue ;
74
+ }
75
+ buffer = buffer << 5 ;
112
76
77
+ // Deal with commonly mistyped characters
78
+ if (ch == ' 0' )
79
+ {
80
+ ch = ' O' ;
81
+ }
82
+ else if (ch == ' 1' )
83
+ {
84
+ ch = ' L' ;
85
+ }
86
+ else if (ch == ' 8' )
87
+ {
88
+ ch = ' B' ;
113
89
}
114
- return result;
115
90
116
- }
117
- // test base32 decoder
118
- unittest
119
- {
120
- auto ga = new GAuthenticator;
121
- byte [] expected = [' F' , ' O' , ' O' , ' B' , ' A' , ' R' ];
122
- auto message = ga.base32decode(" IZHU6QSBKI" );
123
- assert (message == expected);
124
- expected = [' 1' , ' 2' , ' 3' , ' 4' , ' 5' , ' t' , ' e' , ' s' , ' t' ];
125
- message = ga.base32decode(" GEZDGNBVORSXG5A=" );
126
- assert (message == expected);
127
- }
91
+ // Look up one base32 digit
92
+ if ((ch >= ' A' && ch <= ' Z' ) || (ch >= ' a' && ch <= ' z' ))
93
+ {
94
+ ch = (ch & 0x1F ) - 1 ;
95
+ }
96
+ else if (ch >= ' 2' && ch <= ' 7' )
97
+ {
98
+ ch -= (' 2' - 26 );
99
+ }
128
100
101
+ buffer |= ch;
102
+ bitsLeft += 5 ;
103
+ if (bitsLeft >= 8 )
104
+ {
105
+ const c = (buffer >> (bitsLeft - 8 ));
106
+ result ~= cast (byte )(c & 0xff );
107
+ bitsLeft -= 8 ;
108
+ }
129
109
130
- // SHA1 test
131
- unittest
132
- {
133
- SHA1 hash;
134
- hash.start();
135
- ubyte [] data = [' a' , ' b' , ' c' ];
136
- hash.put(data);
137
- ubyte [20 ] result = hash.finish();
138
- // writeln(toHexString(result));
139
- assert (toHexString(result)== " A9993E364706816ABA3E25717850C26C9CD0D89D" );
140
110
}
111
+ return result;
141
112
142
- /*
143
- unittest
144
- {
145
- auto ga = new GAuthenticator;
146
- auto secret = "dummySECRETdummy";
147
- auto interval = ulong(50780342);
148
- auto otp = "971294";
149
- assert(otp == ga.getHOTPToken(secret, interval));
150
- }
151
- */
113
+ }
114
+ // test for base32 decoder
115
+ unittest
116
+ {
117
+ auto expected = cast (byte [])(" FOOBAR" );
118
+ auto message = base32decode(" IZHU6QSBKI" );
119
+ assert (message == expected);
120
+ expected = cast (byte [])(" 12345test" );
121
+ message = base32decode(" GEZDGNBVORSXG5A=" );
122
+ assert (message == expected);
123
+ }
152
124
125
+ // test for SHA1 func
126
+ unittest
127
+ {
128
+ SHA1 hash;
129
+ hash.start();
130
+ auto data = cast (ubyte [])" abc" ;
131
+ hash.put(data);
132
+ ubyte [20 ] result = hash.finish();
133
+ assert (toHexString(result) == " A9993E364706816ABA3E25717850C26C9CD0D89D" );
153
134
}
154
135
136
+ // final test for OTP functionality (with a fixed time)
137
+ unittest
138
+ {
139
+ auto secret = " dummySECRETdummy" ;
140
+ auto interval = ulong (50_780_342); // D allows underscore in numbers to improve readability
141
+ const otp = " 971294" ;
142
+ assert (otp == getHOTPToken(secret, interval));
143
+ }
0 commit comments