-
Notifications
You must be signed in to change notification settings - Fork 1
/
hotp.ts
130 lines (112 loc) · 3.35 KB
/
hotp.ts
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
import { decode } from "./deps.ts";
import { IOptions } from "./mod.d.ts";
export enum Alg {
SHA1 = "SHA-1",
SHA256 = "SHA-256",
SHA512 = "SHA-512",
}
function padBase32(secret: string): string {
const max = Math.ceil(secret.length / 8) * 8;
return secret.padEnd(max, "=").toUpperCase();
}
async function digestOptions(
secret: string,
counter: number,
options?: IOptions,
): Promise<Uint8Array> {
const secretBytes = decode(padBase32(secret));
const alg = options?.alg || Alg.SHA1;
let counterBytes = new Uint8Array(8);
let tmp = counter;
for (let i = 0; i < 8; i++) {
counterBytes[7 - i] = tmp & 0xff;
tmp = tmp >> 8;
}
const key = await crypto.subtle.importKey(
"raw",
secretBytes,
{ name: "HMAC", hash: alg },
false,
["sign"],
);
const data = await crypto.subtle.sign(
{ name: "HMAC", hash: alg },
key,
counterBytes,
);
return new Uint8Array(data);
}
/**
* Generates a HMAC-based one-time password.
* Specify the key and counter, and receive the OTP for the given counter position.
*
* @param secret Shared secret between server and client.
* @param counter Counter value.
* @param options
* @returns OTP token
*/
export async function generate(
secret: string,
counter: number,
options?: IOptions,
): Promise<string> {
const digits = options?.digits || 6;
if (!secret) throw new Error("Behin: Missing secret");
if (!counter) throw new Error("Behin: Missing counter");
const digest = await digestOptions(secret, counter, options);
// calculate binary code (RFC4226 5.4)
var offset = digest[digest.length - 1] & 0xf;
const code = (digest[offset] & 0x7f) << 24 |
(digest[offset + 1] & 0xff) << 16 |
(digest[offset + 2] & 0xff) << 8 |
(digest[offset + 3] & 0xff);
// left-pad code
const lfCode = new Array(digits + 1).join("0") + code.toString(10);
// return length number off digits
return lfCode.slice(-digits);
}
/**
* Validates a OTP token against a given secret. By default verifies the token only at the given counter.
*
* A margin can be specified on the `window` option
* @param secret Shared secret between server and client.
* @param token OTP token to be verified
* @param counter Counter value.
* @param options
* @returns Returns the counter difference between the client and server. If token is not valid returns null
*/
export async function delta(
secret: string,
token: string,
counter: number,
options?: IOptions,
): Promise<number | null> {
const digits = options?.digits || 6;
const window = options?.window || 0;
if (!token) throw new Error("Behin: Missing token");
if (token.length !== digits) throw new Error("Behin: Wrong token length");
for (let i = counter; i <= counter + window; i++) {
if ((await generate(secret, i, options)) === token) {
return i - counter;
}
}
return null;
}
/**
* Verifies an OTP token against a base32 encoded secret. Uses the delta function in order to validate the token.
*
* @param secret Shared secret between server and client.
* @param token OTP token to be verified.
* @param counter Counter value.
* @param options
*
* @returns True if tokens matches for the given secret and counter for a given window.
*/
export function verify(
secret: string,
token: string,
counter: number,
options?: IOptions,
) {
return delta(secret, token, counter, options) !== null;
}