foronce 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base32.cjs +87 -123
- package/dist/base32.d.cts +4 -0
- package/dist/base32.d.mts +4 -0
- package/dist/base32.d.ts +4 -2
- package/dist/base32.mjs +114 -0
- package/dist/index.cjs +21 -60
- package/dist/index.d.cts +15 -0
- package/dist/index.d.mts +15 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.mjs +49 -0
- package/dist/universal/universal.cjs +82 -0
- package/dist/universal/universal.d.cts +10 -0
- package/dist/universal/universal.d.mts +10 -0
- package/dist/universal/universal.d.ts +10 -0
- package/dist/universal/universal.mjs +77 -0
- package/package.json +28 -14
- package/src/base32.js +36 -42
- package/src/lib/crypto.js +31 -0
- package/src/lib/utils.js +9 -0
- package/src/universal/hmac.ts +29 -0
- package/src/universal/universal.ts +85 -0
package/dist/base32.cjs
CHANGED
|
@@ -5,148 +5,112 @@
|
|
|
5
5
|
* Copyright(c) 2024 Reaper
|
|
6
6
|
* MIT Licensed
|
|
7
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
|
-
const base32alphaMapDecode = Object.fromEntries(
|
|
48
|
-
Object.entries(base32alphaMap).map(([k, v]) => [v, k])
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* @param {string} str
|
|
53
|
-
*/
|
|
54
|
-
const encode = str => {
|
|
55
|
-
const splits = str.split('');
|
|
56
|
-
|
|
8
|
+
const pad = "=";
|
|
9
|
+
const base32alphaMap = [
|
|
10
|
+
"A",
|
|
11
|
+
"B",
|
|
12
|
+
"C",
|
|
13
|
+
"D",
|
|
14
|
+
"E",
|
|
15
|
+
"F",
|
|
16
|
+
"G",
|
|
17
|
+
"H",
|
|
18
|
+
"I",
|
|
19
|
+
"J",
|
|
20
|
+
"K",
|
|
21
|
+
"L",
|
|
22
|
+
"M",
|
|
23
|
+
"N",
|
|
24
|
+
"O",
|
|
25
|
+
"P",
|
|
26
|
+
"Q",
|
|
27
|
+
"R",
|
|
28
|
+
"S",
|
|
29
|
+
"T",
|
|
30
|
+
"U",
|
|
31
|
+
"V",
|
|
32
|
+
"W",
|
|
33
|
+
"X",
|
|
34
|
+
"Y",
|
|
35
|
+
"Z",
|
|
36
|
+
"2",
|
|
37
|
+
"3",
|
|
38
|
+
"4",
|
|
39
|
+
"5",
|
|
40
|
+
"6",
|
|
41
|
+
"7"
|
|
42
|
+
];
|
|
43
|
+
const encode = (str) => {
|
|
44
|
+
const splits = str.split("");
|
|
57
45
|
if (!splits.length) {
|
|
58
|
-
return
|
|
46
|
+
return "";
|
|
59
47
|
}
|
|
60
|
-
|
|
61
48
|
let binaryGroup = [];
|
|
62
|
-
let bitText =
|
|
63
|
-
|
|
64
|
-
splits.forEach(c => {
|
|
49
|
+
let bitText = "";
|
|
50
|
+
splits.forEach((c) => {
|
|
65
51
|
bitText += toBinary(c);
|
|
66
|
-
|
|
67
52
|
if (bitText.length == 40) {
|
|
68
53
|
binaryGroup.push(bitText);
|
|
69
|
-
bitText =
|
|
54
|
+
bitText = "";
|
|
70
55
|
}
|
|
71
56
|
});
|
|
72
|
-
|
|
73
57
|
if (bitText.length > 0) {
|
|
74
58
|
binaryGroup.push(bitText);
|
|
75
|
-
bitText =
|
|
59
|
+
bitText = "";
|
|
76
60
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
lex
|
|
86
|
-
if (lex.length == 5) {
|
|
87
|
-
fiveBitGrouping.push(lex);
|
|
88
|
-
lex = '';
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
if (lex.length > 0) {
|
|
93
|
-
fiveBitGrouping.push(lex.padEnd(5, '0'));
|
|
94
|
-
lex = '';
|
|
61
|
+
return binaryGroup.map((x) => {
|
|
62
|
+
let fiveBitGrouping = [];
|
|
63
|
+
let lex = "";
|
|
64
|
+
let bitOn = x;
|
|
65
|
+
bitOn.split("").forEach((d) => {
|
|
66
|
+
lex += d;
|
|
67
|
+
if (lex.length == 5) {
|
|
68
|
+
fiveBitGrouping.push(lex);
|
|
69
|
+
lex = "";
|
|
95
70
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
})
|
|
108
|
-
.join('')
|
|
109
|
-
})
|
|
110
|
-
.join('')
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* @param {string} str
|
|
115
|
-
* @returns
|
|
116
|
-
*/
|
|
117
|
-
const decode = str => {
|
|
118
|
-
const overallBinary = str
|
|
119
|
-
.split('')
|
|
120
|
-
.map(x => {
|
|
121
|
-
if (x === pad) {
|
|
122
|
-
return '00000'
|
|
71
|
+
});
|
|
72
|
+
if (lex.length > 0) {
|
|
73
|
+
fiveBitGrouping.push(lex.padEnd(5, "0"));
|
|
74
|
+
lex = "";
|
|
75
|
+
}
|
|
76
|
+
let paddedArray = Array.from(fiveBitGrouping);
|
|
77
|
+
paddedArray.length = 8;
|
|
78
|
+
paddedArray = paddedArray.fill("-1", fiveBitGrouping.length, 8);
|
|
79
|
+
return paddedArray.map((f) => {
|
|
80
|
+
if (f == "-1") {
|
|
81
|
+
return pad;
|
|
123
82
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
83
|
+
return base32alphaMap[parseInt(f, 2).toString(10)];
|
|
84
|
+
}).join("");
|
|
85
|
+
}).join("");
|
|
86
|
+
};
|
|
87
|
+
const decode = (str) => {
|
|
88
|
+
const overallBinary = str.split("").map((x) => {
|
|
89
|
+
if (x === pad) {
|
|
90
|
+
return "00000";
|
|
91
|
+
}
|
|
92
|
+
const decodeIndex = base32alphaMap.indexOf(x);
|
|
93
|
+
const binary = decodeIndex.toString(2);
|
|
94
|
+
return binary.padStart(5, "0");
|
|
95
|
+
}).join("");
|
|
96
|
+
const characterBitGrouping = chunk(overallBinary.split(""), 8);
|
|
97
|
+
return characterBitGrouping.map((x) => {
|
|
98
|
+
const binaryL = x.join("");
|
|
99
|
+
const str2 = String.fromCharCode(+parseInt(binaryL, 2).toString(10));
|
|
100
|
+
return str2.replace("\0", "");
|
|
101
|
+
}).join("");
|
|
138
102
|
};
|
|
139
|
-
|
|
140
103
|
const toBinary = (char, padLimit = 8) => {
|
|
141
104
|
const binary = String(char).charCodeAt(0).toString(2);
|
|
142
|
-
return binary.padStart(padLimit,
|
|
105
|
+
return binary.padStart(padLimit, "0");
|
|
143
106
|
};
|
|
144
|
-
|
|
145
107
|
const chunk = (arr, chunkSize = 1, cache = []) => {
|
|
146
108
|
const tmp = [...arr];
|
|
147
|
-
if (chunkSize <= 0)
|
|
148
|
-
|
|
149
|
-
|
|
109
|
+
if (chunkSize <= 0)
|
|
110
|
+
return cache;
|
|
111
|
+
while (tmp.length)
|
|
112
|
+
cache.push(tmp.splice(0, chunkSize));
|
|
113
|
+
return cache;
|
|
150
114
|
};
|
|
151
115
|
|
|
152
116
|
exports.decode = decode;
|
package/dist/base32.d.ts
CHANGED
package/dist/base32.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* base-32.js
|
|
3
|
+
* Copyright(c) 2024 Reaper
|
|
4
|
+
* MIT Licensed
|
|
5
|
+
*/
|
|
6
|
+
const pad = "=";
|
|
7
|
+
const base32alphaMap = [
|
|
8
|
+
"A",
|
|
9
|
+
"B",
|
|
10
|
+
"C",
|
|
11
|
+
"D",
|
|
12
|
+
"E",
|
|
13
|
+
"F",
|
|
14
|
+
"G",
|
|
15
|
+
"H",
|
|
16
|
+
"I",
|
|
17
|
+
"J",
|
|
18
|
+
"K",
|
|
19
|
+
"L",
|
|
20
|
+
"M",
|
|
21
|
+
"N",
|
|
22
|
+
"O",
|
|
23
|
+
"P",
|
|
24
|
+
"Q",
|
|
25
|
+
"R",
|
|
26
|
+
"S",
|
|
27
|
+
"T",
|
|
28
|
+
"U",
|
|
29
|
+
"V",
|
|
30
|
+
"W",
|
|
31
|
+
"X",
|
|
32
|
+
"Y",
|
|
33
|
+
"Z",
|
|
34
|
+
"2",
|
|
35
|
+
"3",
|
|
36
|
+
"4",
|
|
37
|
+
"5",
|
|
38
|
+
"6",
|
|
39
|
+
"7"
|
|
40
|
+
];
|
|
41
|
+
const encode = (str) => {
|
|
42
|
+
const splits = str.split("");
|
|
43
|
+
if (!splits.length) {
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
let binaryGroup = [];
|
|
47
|
+
let bitText = "";
|
|
48
|
+
splits.forEach((c) => {
|
|
49
|
+
bitText += toBinary(c);
|
|
50
|
+
if (bitText.length == 40) {
|
|
51
|
+
binaryGroup.push(bitText);
|
|
52
|
+
bitText = "";
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
if (bitText.length > 0) {
|
|
56
|
+
binaryGroup.push(bitText);
|
|
57
|
+
bitText = "";
|
|
58
|
+
}
|
|
59
|
+
return binaryGroup.map((x) => {
|
|
60
|
+
let fiveBitGrouping = [];
|
|
61
|
+
let lex = "";
|
|
62
|
+
let bitOn = x;
|
|
63
|
+
bitOn.split("").forEach((d) => {
|
|
64
|
+
lex += d;
|
|
65
|
+
if (lex.length == 5) {
|
|
66
|
+
fiveBitGrouping.push(lex);
|
|
67
|
+
lex = "";
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
if (lex.length > 0) {
|
|
71
|
+
fiveBitGrouping.push(lex.padEnd(5, "0"));
|
|
72
|
+
lex = "";
|
|
73
|
+
}
|
|
74
|
+
let paddedArray = Array.from(fiveBitGrouping);
|
|
75
|
+
paddedArray.length = 8;
|
|
76
|
+
paddedArray = paddedArray.fill("-1", fiveBitGrouping.length, 8);
|
|
77
|
+
return paddedArray.map((f) => {
|
|
78
|
+
if (f == "-1") {
|
|
79
|
+
return pad;
|
|
80
|
+
}
|
|
81
|
+
return base32alphaMap[parseInt(f, 2).toString(10)];
|
|
82
|
+
}).join("");
|
|
83
|
+
}).join("");
|
|
84
|
+
};
|
|
85
|
+
const decode = (str) => {
|
|
86
|
+
const overallBinary = str.split("").map((x) => {
|
|
87
|
+
if (x === pad) {
|
|
88
|
+
return "00000";
|
|
89
|
+
}
|
|
90
|
+
const decodeIndex = base32alphaMap.indexOf(x);
|
|
91
|
+
const binary = decodeIndex.toString(2);
|
|
92
|
+
return binary.padStart(5, "0");
|
|
93
|
+
}).join("");
|
|
94
|
+
const characterBitGrouping = chunk(overallBinary.split(""), 8);
|
|
95
|
+
return characterBitGrouping.map((x) => {
|
|
96
|
+
const binaryL = x.join("");
|
|
97
|
+
const str2 = String.fromCharCode(+parseInt(binaryL, 2).toString(10));
|
|
98
|
+
return str2.replace("\0", "");
|
|
99
|
+
}).join("");
|
|
100
|
+
};
|
|
101
|
+
const toBinary = (char, padLimit = 8) => {
|
|
102
|
+
const binary = String(char).charCodeAt(0).toString(2);
|
|
103
|
+
return binary.padStart(padLimit, "0");
|
|
104
|
+
};
|
|
105
|
+
const chunk = (arr, chunkSize = 1, cache = []) => {
|
|
106
|
+
const tmp = [...arr];
|
|
107
|
+
if (chunkSize <= 0)
|
|
108
|
+
return cache;
|
|
109
|
+
while (tmp.length)
|
|
110
|
+
cache.push(tmp.splice(0, chunkSize));
|
|
111
|
+
return cache;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export { decode, encode };
|
package/dist/index.cjs
CHANGED
|
@@ -1,90 +1,51 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
const node_crypto = require('node:crypto');
|
|
4
|
+
const buffer = require('buffer');
|
|
5
|
+
const base32 = require('./base32.cjs');
|
|
6
6
|
|
|
7
7
|
/*!
|
|
8
8
|
* base-32.js
|
|
9
9
|
* Copyright(c) 2024 Reaper
|
|
10
10
|
* MIT Licensed
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
12
|
const { floor } = Math;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
18
|
-
* @param {string} secret
|
|
19
|
-
* @param {number} when
|
|
20
|
-
* @param {object} [options]
|
|
21
|
-
* @param {number} [options.period] in seconds (eg: 30 => 30 seconds)
|
|
22
|
-
* @param {"sha1" | "sha256" | "sha512"} [options.algorithm] (default: sha512)
|
|
23
|
-
* @returns {string}
|
|
24
|
-
*/
|
|
25
|
-
function totp(secret, when = floor(Date.now() / 1000), options = {}) {
|
|
26
|
-
const _options = Object.assign({ period: 30, algorithm: 'sha512' }, options);
|
|
13
|
+
function totp(secret, when = floor(Date.now() / 1e3), options = {}) {
|
|
14
|
+
const _options = Object.assign({ period: 30, algorithm: "sha512" }, options);
|
|
27
15
|
const now = floor(when / _options.period);
|
|
28
16
|
const key = base32.decode(secret);
|
|
29
17
|
const buff = bigEndian64(BigInt(now));
|
|
30
18
|
const hmac = node_crypto.createHmac(_options.algorithm, key).update(buff).digest();
|
|
31
|
-
const offset = hmac[hmac.length - 1] &
|
|
19
|
+
const offset = hmac[hmac.length - 1] & 15;
|
|
32
20
|
const truncatedHash = hmac.subarray(offset, offset + 4);
|
|
33
|
-
const otp = (
|
|
34
|
-
|
|
35
|
-
1_000_000
|
|
36
|
-
).toString(10);
|
|
37
|
-
return otp.length < 6 ? `${otp}`.padStart(6, '0') : otp
|
|
21
|
+
const otp = ((truncatedHash.readInt32BE() & 2147483647) % 1e6).toString(10);
|
|
22
|
+
return otp.length < 6 ? `${otp}`.padStart(6, "0") : otp;
|
|
38
23
|
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @param {string} secret
|
|
42
|
-
* @param {string} token
|
|
43
|
-
* @param {object} [options]
|
|
44
|
-
* @param {number} [options.period] in seconds (eg: 30 => 30 seconds)
|
|
45
|
-
* @param {"sha1" | "sha256" | "sha512"} [options.algorithm] (default: sha512)
|
|
46
|
-
* @returns {boolean}
|
|
47
|
-
*/
|
|
48
24
|
function isTOTPValid(secret, token, options = {}) {
|
|
49
|
-
const _options = Object.assign({ period: 30, algorithm:
|
|
25
|
+
const _options = Object.assign({ period: 30, algorithm: "sha512" }, options);
|
|
50
26
|
for (let index = -2; index < 3; index += 1) {
|
|
51
|
-
const fromSys = totp(secret, Date.now() /
|
|
27
|
+
const fromSys = totp(secret, Date.now() / 1e3 + index, _options);
|
|
52
28
|
const valid = fromSys === token;
|
|
53
|
-
if (valid)
|
|
29
|
+
if (valid)
|
|
30
|
+
return true;
|
|
54
31
|
}
|
|
55
|
-
return false
|
|
32
|
+
return false;
|
|
56
33
|
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* @param {string} secret
|
|
60
|
-
* @param {object} options
|
|
61
|
-
* @param {string} options.company
|
|
62
|
-
* @param {string} options.email
|
|
63
|
-
* @returns {string}
|
|
64
|
-
*/
|
|
65
34
|
function generateTOTPURL(secret, options) {
|
|
66
35
|
const parameters = new URLSearchParams();
|
|
67
|
-
parameters.append(
|
|
68
|
-
parameters.append(
|
|
69
|
-
parameters.append(
|
|
70
|
-
const url = `otpauth://totp/${options.company}:${
|
|
71
|
-
|
|
72
|
-
}?${parameters.toString()}`;
|
|
73
|
-
return new URL(url).toString()
|
|
36
|
+
parameters.append("secret", secret);
|
|
37
|
+
parameters.append("issuer", options.company);
|
|
38
|
+
parameters.append("digits", "6");
|
|
39
|
+
const url = `otpauth://totp/${options.company}:${options.email}?${parameters.toString()}`;
|
|
40
|
+
return new URL(url).toString();
|
|
74
41
|
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* @param {bigint} hash
|
|
78
|
-
* @returns {Buffer}
|
|
79
|
-
*/
|
|
80
42
|
function bigEndian64(hash) {
|
|
81
|
-
const buf =
|
|
43
|
+
const buf = buffer.Buffer.allocUnsafe(64 / 8);
|
|
82
44
|
buf.writeBigInt64BE(hash, 0);
|
|
83
|
-
return buf
|
|
45
|
+
return buf;
|
|
84
46
|
}
|
|
85
|
-
|
|
86
47
|
function generateTOTPSecret(num = 32) {
|
|
87
|
-
return base32.encode(node_crypto.randomBytes(num).toString(
|
|
48
|
+
return base32.encode(node_crypto.randomBytes(num).toString("ascii"));
|
|
88
49
|
}
|
|
89
50
|
|
|
90
51
|
exports.generateTOTPSecret = generateTOTPSecret;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
declare function totp(secret: string, when?: number, options?: {
|
|
2
|
+
period?: number;
|
|
3
|
+
algorithm?: "sha1" | "sha256" | "sha512";
|
|
4
|
+
}): string;
|
|
5
|
+
declare function isTOTPValid(secret: string, token: string, options?: {
|
|
6
|
+
period?: number;
|
|
7
|
+
algorithm?: "sha1" | "sha256" | "sha512";
|
|
8
|
+
}): boolean;
|
|
9
|
+
declare function generateTOTPURL(secret: string, options: {
|
|
10
|
+
company: string;
|
|
11
|
+
email: string;
|
|
12
|
+
}): string;
|
|
13
|
+
declare function generateTOTPSecret(num?: number): string;
|
|
14
|
+
|
|
15
|
+
export { generateTOTPSecret, generateTOTPURL, isTOTPValid, totp };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
declare function totp(secret: string, when?: number, options?: {
|
|
2
|
+
period?: number;
|
|
3
|
+
algorithm?: "sha1" | "sha256" | "sha512";
|
|
4
|
+
}): string;
|
|
5
|
+
declare function isTOTPValid(secret: string, token: string, options?: {
|
|
6
|
+
period?: number;
|
|
7
|
+
algorithm?: "sha1" | "sha256" | "sha512";
|
|
8
|
+
}): boolean;
|
|
9
|
+
declare function generateTOTPURL(secret: string, options: {
|
|
10
|
+
company: string;
|
|
11
|
+
email: string;
|
|
12
|
+
}): string;
|
|
13
|
+
declare function generateTOTPSecret(num?: number): string;
|
|
14
|
+
|
|
15
|
+
export { generateTOTPSecret, generateTOTPURL, isTOTPValid, totp };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
declare function totp(secret: string, when?: number, options?: {
|
|
2
2
|
period?: number;
|
|
3
3
|
algorithm?: "sha1" | "sha256" | "sha512";
|
|
4
4
|
}): string;
|
|
5
|
-
|
|
5
|
+
declare function isTOTPValid(secret: string, token: string, options?: {
|
|
6
6
|
period?: number;
|
|
7
7
|
algorithm?: "sha1" | "sha256" | "sha512";
|
|
8
8
|
}): boolean;
|
|
9
|
-
|
|
9
|
+
declare function generateTOTPURL(secret: string, options: {
|
|
10
10
|
company: string;
|
|
11
11
|
email: string;
|
|
12
12
|
}): string;
|
|
13
|
-
|
|
13
|
+
declare function generateTOTPSecret(num?: number): string;
|
|
14
|
+
|
|
15
|
+
export { generateTOTPSecret, generateTOTPURL, isTOTPValid, totp };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createHmac, randomBytes } from 'node:crypto';
|
|
2
|
+
import { Buffer } from 'buffer';
|
|
3
|
+
import { decode, encode } from './base32.mjs';
|
|
4
|
+
|
|
5
|
+
/*!
|
|
6
|
+
* base-32.js
|
|
7
|
+
* Copyright(c) 2024 Reaper
|
|
8
|
+
* MIT Licensed
|
|
9
|
+
*/
|
|
10
|
+
const { floor } = Math;
|
|
11
|
+
function totp(secret, when = floor(Date.now() / 1e3), options = {}) {
|
|
12
|
+
const _options = Object.assign({ period: 30, algorithm: "sha512" }, options);
|
|
13
|
+
const now = floor(when / _options.period);
|
|
14
|
+
const key = decode(secret);
|
|
15
|
+
const buff = bigEndian64(BigInt(now));
|
|
16
|
+
const hmac = createHmac(_options.algorithm, key).update(buff).digest();
|
|
17
|
+
const offset = hmac[hmac.length - 1] & 15;
|
|
18
|
+
const truncatedHash = hmac.subarray(offset, offset + 4);
|
|
19
|
+
const otp = ((truncatedHash.readInt32BE() & 2147483647) % 1e6).toString(10);
|
|
20
|
+
return otp.length < 6 ? `${otp}`.padStart(6, "0") : otp;
|
|
21
|
+
}
|
|
22
|
+
function isTOTPValid(secret, token, options = {}) {
|
|
23
|
+
const _options = Object.assign({ period: 30, algorithm: "sha512" }, options);
|
|
24
|
+
for (let index = -2; index < 3; index += 1) {
|
|
25
|
+
const fromSys = totp(secret, Date.now() / 1e3 + index, _options);
|
|
26
|
+
const valid = fromSys === token;
|
|
27
|
+
if (valid)
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
function generateTOTPURL(secret, options) {
|
|
33
|
+
const parameters = new URLSearchParams();
|
|
34
|
+
parameters.append("secret", secret);
|
|
35
|
+
parameters.append("issuer", options.company);
|
|
36
|
+
parameters.append("digits", "6");
|
|
37
|
+
const url = `otpauth://totp/${options.company}:${options.email}?${parameters.toString()}`;
|
|
38
|
+
return new URL(url).toString();
|
|
39
|
+
}
|
|
40
|
+
function bigEndian64(hash) {
|
|
41
|
+
const buf = Buffer.allocUnsafe(64 / 8);
|
|
42
|
+
buf.writeBigInt64BE(hash, 0);
|
|
43
|
+
return buf;
|
|
44
|
+
}
|
|
45
|
+
function generateTOTPSecret(num = 32) {
|
|
46
|
+
return encode(randomBytes(num).toString("ascii"));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { generateTOTPSecret, generateTOTPURL, isTOTPValid, totp };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const uncrypto = require('uncrypto');
|
|
4
|
+
const base32 = require('../base32.cjs');
|
|
5
|
+
|
|
6
|
+
function bigEndian64(hash) {
|
|
7
|
+
const buf = Buffer.allocUnsafe(64 / 8);
|
|
8
|
+
buf.writeBigInt64BE(hash, 0);
|
|
9
|
+
return buf;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const algoMap = {
|
|
13
|
+
sha1: "SHA-1",
|
|
14
|
+
sha256: "SHA-256",
|
|
15
|
+
sha512: "SHA-512"
|
|
16
|
+
};
|
|
17
|
+
async function createHmac(algorithm, secret, data) {
|
|
18
|
+
const key = await uncrypto.subtle.importKey(
|
|
19
|
+
"raw",
|
|
20
|
+
// raw format of the key - should be Uint8Array
|
|
21
|
+
secret,
|
|
22
|
+
{
|
|
23
|
+
// algorithm details
|
|
24
|
+
name: "HMAC",
|
|
25
|
+
hash: { name: algoMap[algorithm] }
|
|
26
|
+
},
|
|
27
|
+
false,
|
|
28
|
+
// export = false
|
|
29
|
+
["sign", "verify"]
|
|
30
|
+
// what this key can do
|
|
31
|
+
);
|
|
32
|
+
const signature = await uncrypto.subtle.sign("HMAC", key, data);
|
|
33
|
+
return Buffer.from(signature);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { floor } = Math;
|
|
37
|
+
async function totp(secret, when = floor(Date.now() / 1e3), options = {}) {
|
|
38
|
+
const _options = Object.assign(
|
|
39
|
+
{
|
|
40
|
+
period: 30,
|
|
41
|
+
algorithm: "sha512"
|
|
42
|
+
},
|
|
43
|
+
options
|
|
44
|
+
);
|
|
45
|
+
const now = floor(when / _options.period);
|
|
46
|
+
const key = base32.decode(secret);
|
|
47
|
+
const buff = bigEndian64(BigInt(now));
|
|
48
|
+
const hmac = await createHmac(_options.algorithm, key, buff);
|
|
49
|
+
const offset = hmac.at(-1) & 15;
|
|
50
|
+
const truncatedHash = hmac.subarray(offset, offset + 4);
|
|
51
|
+
const otp = ((truncatedHash.readInt32BE() & 2147483647) % 1e6).toString(10);
|
|
52
|
+
return otp.length < 6 ? `${otp}`.padStart(6, "0") : otp;
|
|
53
|
+
}
|
|
54
|
+
async function isTOTPValid(secret, totpToken, options = {}) {
|
|
55
|
+
const _options = Object.assign({ period: 30, algorithm: "sha512" }, options);
|
|
56
|
+
for (let index = -2; index < 3; index += 1) {
|
|
57
|
+
const fromSys = await totp(secret, Date.now() / 1e3 + index, _options);
|
|
58
|
+
const valid = fromSys === totpToken;
|
|
59
|
+
if (valid) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
function generateTOTPURL(secret, options) {
|
|
66
|
+
const parameters = new URLSearchParams();
|
|
67
|
+
parameters.append("secret", secret);
|
|
68
|
+
parameters.append("issuer", options.company);
|
|
69
|
+
parameters.append("digits", "6");
|
|
70
|
+
const url = `otpauth://totp/${options.company}:${options.email}?${parameters.toString()}`;
|
|
71
|
+
return new URL(url).toString();
|
|
72
|
+
}
|
|
73
|
+
function generateTOTPSecret(num = 32) {
|
|
74
|
+
const array = new Uint32Array(num);
|
|
75
|
+
const vals = uncrypto.getRandomValues(array);
|
|
76
|
+
return base32.encode(Buffer.from(vals).toString("ascii"));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
exports.generateTOTPSecret = generateTOTPSecret;
|
|
80
|
+
exports.generateTOTPURL = generateTOTPURL;
|
|
81
|
+
exports.isTOTPValid = isTOTPValid;
|
|
82
|
+
exports.totp = totp;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface TOTPURLOptions {
|
|
2
|
+
company: string;
|
|
3
|
+
email: string;
|
|
4
|
+
}
|
|
5
|
+
declare function totp(secret: string, when?: number, options?: {}): Promise<string>;
|
|
6
|
+
declare function isTOTPValid(secret: string, totpToken: string, options?: {}): Promise<boolean>;
|
|
7
|
+
declare function generateTOTPURL(secret: string, options: TOTPURLOptions): string;
|
|
8
|
+
declare function generateTOTPSecret(num?: number): string;
|
|
9
|
+
|
|
10
|
+
export { generateTOTPSecret, generateTOTPURL, isTOTPValid, totp };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface TOTPURLOptions {
|
|
2
|
+
company: string;
|
|
3
|
+
email: string;
|
|
4
|
+
}
|
|
5
|
+
declare function totp(secret: string, when?: number, options?: {}): Promise<string>;
|
|
6
|
+
declare function isTOTPValid(secret: string, totpToken: string, options?: {}): Promise<boolean>;
|
|
7
|
+
declare function generateTOTPURL(secret: string, options: TOTPURLOptions): string;
|
|
8
|
+
declare function generateTOTPSecret(num?: number): string;
|
|
9
|
+
|
|
10
|
+
export { generateTOTPSecret, generateTOTPURL, isTOTPValid, totp };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface TOTPURLOptions {
|
|
2
|
+
company: string;
|
|
3
|
+
email: string;
|
|
4
|
+
}
|
|
5
|
+
declare function totp(secret: string, when?: number, options?: {}): Promise<string>;
|
|
6
|
+
declare function isTOTPValid(secret: string, totpToken: string, options?: {}): Promise<boolean>;
|
|
7
|
+
declare function generateTOTPURL(secret: string, options: TOTPURLOptions): string;
|
|
8
|
+
declare function generateTOTPSecret(num?: number): string;
|
|
9
|
+
|
|
10
|
+
export { generateTOTPSecret, generateTOTPURL, isTOTPValid, totp };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { subtle, getRandomValues } from 'uncrypto';
|
|
2
|
+
import { decode, encode } from '../base32.mjs';
|
|
3
|
+
|
|
4
|
+
function bigEndian64(hash) {
|
|
5
|
+
const buf = Buffer.allocUnsafe(64 / 8);
|
|
6
|
+
buf.writeBigInt64BE(hash, 0);
|
|
7
|
+
return buf;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const algoMap = {
|
|
11
|
+
sha1: "SHA-1",
|
|
12
|
+
sha256: "SHA-256",
|
|
13
|
+
sha512: "SHA-512"
|
|
14
|
+
};
|
|
15
|
+
async function createHmac(algorithm, secret, data) {
|
|
16
|
+
const key = await subtle.importKey(
|
|
17
|
+
"raw",
|
|
18
|
+
// raw format of the key - should be Uint8Array
|
|
19
|
+
secret,
|
|
20
|
+
{
|
|
21
|
+
// algorithm details
|
|
22
|
+
name: "HMAC",
|
|
23
|
+
hash: { name: algoMap[algorithm] }
|
|
24
|
+
},
|
|
25
|
+
false,
|
|
26
|
+
// export = false
|
|
27
|
+
["sign", "verify"]
|
|
28
|
+
// what this key can do
|
|
29
|
+
);
|
|
30
|
+
const signature = await subtle.sign("HMAC", key, data);
|
|
31
|
+
return Buffer.from(signature);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { floor } = Math;
|
|
35
|
+
async function totp(secret, when = floor(Date.now() / 1e3), options = {}) {
|
|
36
|
+
const _options = Object.assign(
|
|
37
|
+
{
|
|
38
|
+
period: 30,
|
|
39
|
+
algorithm: "sha512"
|
|
40
|
+
},
|
|
41
|
+
options
|
|
42
|
+
);
|
|
43
|
+
const now = floor(when / _options.period);
|
|
44
|
+
const key = decode(secret);
|
|
45
|
+
const buff = bigEndian64(BigInt(now));
|
|
46
|
+
const hmac = await createHmac(_options.algorithm, key, buff);
|
|
47
|
+
const offset = hmac.at(-1) & 15;
|
|
48
|
+
const truncatedHash = hmac.subarray(offset, offset + 4);
|
|
49
|
+
const otp = ((truncatedHash.readInt32BE() & 2147483647) % 1e6).toString(10);
|
|
50
|
+
return otp.length < 6 ? `${otp}`.padStart(6, "0") : otp;
|
|
51
|
+
}
|
|
52
|
+
async function isTOTPValid(secret, totpToken, options = {}) {
|
|
53
|
+
const _options = Object.assign({ period: 30, algorithm: "sha512" }, options);
|
|
54
|
+
for (let index = -2; index < 3; index += 1) {
|
|
55
|
+
const fromSys = await totp(secret, Date.now() / 1e3 + index, _options);
|
|
56
|
+
const valid = fromSys === totpToken;
|
|
57
|
+
if (valid) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
function generateTOTPURL(secret, options) {
|
|
64
|
+
const parameters = new URLSearchParams();
|
|
65
|
+
parameters.append("secret", secret);
|
|
66
|
+
parameters.append("issuer", options.company);
|
|
67
|
+
parameters.append("digits", "6");
|
|
68
|
+
const url = `otpauth://totp/${options.company}:${options.email}?${parameters.toString()}`;
|
|
69
|
+
return new URL(url).toString();
|
|
70
|
+
}
|
|
71
|
+
function generateTOTPSecret(num = 32) {
|
|
72
|
+
const array = new Uint32Array(num);
|
|
73
|
+
const vals = getRandomValues(array);
|
|
74
|
+
return encode(Buffer.from(vals).toString("ascii"));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { generateTOTPSecret, generateTOTPURL, isTOTPValid, totp };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "foronce",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "The OTP Library",
|
|
5
5
|
"repository": "git@github.com:dumbjs/foronce.git",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,33 +8,46 @@
|
|
|
8
8
|
"type": "module",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
-
"types":
|
|
12
|
-
|
|
11
|
+
"types": {
|
|
12
|
+
"import": "./dist/index.d.mts",
|
|
13
|
+
"require": "./dist/index.d.cts"
|
|
14
|
+
},
|
|
15
|
+
"import": "./dist/index.mjs",
|
|
13
16
|
"require": "./dist/index.cjs"
|
|
14
17
|
},
|
|
15
18
|
"./base32": {
|
|
16
|
-
"types":
|
|
17
|
-
|
|
19
|
+
"types": {
|
|
20
|
+
"import": "./dist/base32.d.mts",
|
|
21
|
+
"require": "./dist/base32.d.cts"
|
|
22
|
+
},
|
|
23
|
+
"import": "./dist/base32.mjs",
|
|
18
24
|
"require": "./dist/base32.cjs"
|
|
19
25
|
},
|
|
26
|
+
"./universal": {
|
|
27
|
+
"types": {
|
|
28
|
+
"import": "./dist/universal/universal.d.mts",
|
|
29
|
+
"require": "./dist/universal/universal.d.cts"
|
|
30
|
+
},
|
|
31
|
+
"import": "./dist/universal/universal.mjs",
|
|
32
|
+
"require": "./dist/universal/universal.cjs"
|
|
33
|
+
},
|
|
20
34
|
"./package.json": "./package.json"
|
|
21
35
|
},
|
|
22
36
|
"main": "./dist/index.cjs",
|
|
23
|
-
"module": "./
|
|
37
|
+
"module": "./dist/index.mjs",
|
|
24
38
|
"types": "./dist/index.d.ts",
|
|
25
39
|
"files": [
|
|
26
40
|
"dist",
|
|
27
41
|
"src"
|
|
28
42
|
],
|
|
29
43
|
"scripts": {
|
|
30
|
-
"build": "
|
|
31
|
-
"dev": "rollup -c --watch",
|
|
44
|
+
"build": "unbuild",
|
|
32
45
|
"fix": "prettier --write .",
|
|
33
46
|
"next": "bumpp",
|
|
34
47
|
"prepare": "husky install",
|
|
35
48
|
"size": "sizesnap",
|
|
36
|
-
"test": "uvu tests",
|
|
37
|
-
"test:ci": "c8 uvu tests "
|
|
49
|
+
"test": "uvu -r tsm tests",
|
|
50
|
+
"test:ci": "c8 uvu -r tsm tests "
|
|
38
51
|
},
|
|
39
52
|
"keywords": [
|
|
40
53
|
"otp",
|
|
@@ -57,11 +70,9 @@
|
|
|
57
70
|
"lint-staged": "^14.0.1",
|
|
58
71
|
"prettier": "^2.7.1",
|
|
59
72
|
"publint": "^0.2.7",
|
|
60
|
-
"rollup": "^4.9.4",
|
|
61
|
-
"rollup-plugin-node-externals": "^6.1.2",
|
|
62
73
|
"sizesnap": "^0.2.1",
|
|
63
|
-
"
|
|
64
|
-
"
|
|
74
|
+
"tsm": "^2.3.0",
|
|
75
|
+
"unbuild": "^2.0.0",
|
|
65
76
|
"uvu": "^0.5.6"
|
|
66
77
|
},
|
|
67
78
|
"publishConfig": {
|
|
@@ -74,5 +85,8 @@
|
|
|
74
85
|
"dist/*.js",
|
|
75
86
|
"dist/*.cjs"
|
|
76
87
|
]
|
|
88
|
+
},
|
|
89
|
+
"dependencies": {
|
|
90
|
+
"uncrypto": "^0.1.3"
|
|
77
91
|
}
|
|
78
92
|
}
|
package/src/base32.js
CHANGED
|
@@ -7,44 +7,40 @@
|
|
|
7
7
|
// Simple implementation based of RFC 4648 for base32 encoding and decoding
|
|
8
8
|
|
|
9
9
|
const pad = '='
|
|
10
|
-
const base32alphaMap =
|
|
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
|
-
const base32alphaMapDecode = Object.fromEntries(
|
|
46
|
-
Object.entries(base32alphaMap).map(([k, v]) => [v, k])
|
|
47
|
-
)
|
|
10
|
+
const base32alphaMap = [
|
|
11
|
+
'A',
|
|
12
|
+
'B',
|
|
13
|
+
'C',
|
|
14
|
+
'D',
|
|
15
|
+
'E',
|
|
16
|
+
'F',
|
|
17
|
+
'G',
|
|
18
|
+
'H',
|
|
19
|
+
'I',
|
|
20
|
+
'J',
|
|
21
|
+
'K',
|
|
22
|
+
'L',
|
|
23
|
+
'M',
|
|
24
|
+
'N',
|
|
25
|
+
'O',
|
|
26
|
+
'P',
|
|
27
|
+
'Q',
|
|
28
|
+
'R',
|
|
29
|
+
'S',
|
|
30
|
+
'T',
|
|
31
|
+
'U',
|
|
32
|
+
'V',
|
|
33
|
+
'W',
|
|
34
|
+
'X',
|
|
35
|
+
'Y',
|
|
36
|
+
'Z',
|
|
37
|
+
'2',
|
|
38
|
+
'3',
|
|
39
|
+
'4',
|
|
40
|
+
'5',
|
|
41
|
+
'6',
|
|
42
|
+
'7',
|
|
43
|
+
]
|
|
48
44
|
|
|
49
45
|
/**
|
|
50
46
|
* @param {string} str
|
|
@@ -119,8 +115,8 @@ export const decode = str => {
|
|
|
119
115
|
if (x === pad) {
|
|
120
116
|
return '00000'
|
|
121
117
|
}
|
|
122
|
-
const
|
|
123
|
-
const binary =
|
|
118
|
+
const decodeIndex = base32alphaMap.indexOf(x)
|
|
119
|
+
const binary = decodeIndex.toString(2)
|
|
124
120
|
return binary.padStart(5, '0')
|
|
125
121
|
})
|
|
126
122
|
.join('')
|
|
@@ -133,8 +129,6 @@ export const decode = str => {
|
|
|
133
129
|
return str.replace('\x00', '')
|
|
134
130
|
})
|
|
135
131
|
.join('')
|
|
136
|
-
|
|
137
|
-
return ''
|
|
138
132
|
}
|
|
139
133
|
|
|
140
134
|
const toBinary = (char, padLimit = 8) => {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { subtle } from 'uncrypto'
|
|
2
|
+
|
|
3
|
+
const algoMap = {
|
|
4
|
+
sha1: 'SHA-1',
|
|
5
|
+
sha256: 'SHA-256',
|
|
6
|
+
sha512: 'SHA-512',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function createHmac(algorithm, secret, data) {
|
|
10
|
+
// let enc
|
|
11
|
+
// if (TextEncoder.constructor.length == 1) {
|
|
12
|
+
// // @ts-ignore
|
|
13
|
+
// enc = new TextEncoder('utf-8')
|
|
14
|
+
// } else {
|
|
15
|
+
// enc = new TextEncoder()
|
|
16
|
+
// }
|
|
17
|
+
|
|
18
|
+
const key = await subtle.importKey(
|
|
19
|
+
'raw', // raw format of the key - should be Uint8Array
|
|
20
|
+
secret,
|
|
21
|
+
{
|
|
22
|
+
// algorithm details
|
|
23
|
+
name: 'HMAC',
|
|
24
|
+
hash: { name: algoMap[algorithm] },
|
|
25
|
+
},
|
|
26
|
+
false, // export = false
|
|
27
|
+
['sign', 'verify'] // what this key can do
|
|
28
|
+
)
|
|
29
|
+
const signature = await subtle.sign('HMAC', key, data)
|
|
30
|
+
return Buffer.from(signature)
|
|
31
|
+
}
|
package/src/lib/utils.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { subtle } from 'uncrypto'
|
|
2
|
+
|
|
3
|
+
const algoMap = {
|
|
4
|
+
sha1: 'SHA-1',
|
|
5
|
+
sha256: 'SHA-256',
|
|
6
|
+
sha512: 'SHA-512',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type AlgoEnum = 'sha1' | 'sha256' | 'sha512'
|
|
10
|
+
|
|
11
|
+
export async function createHmac(
|
|
12
|
+
algorithm: AlgoEnum,
|
|
13
|
+
secret: string,
|
|
14
|
+
data: Buffer
|
|
15
|
+
) {
|
|
16
|
+
const key = await subtle.importKey(
|
|
17
|
+
'raw', // raw format of the key - should be Uint8Array
|
|
18
|
+
secret,
|
|
19
|
+
{
|
|
20
|
+
// algorithm details
|
|
21
|
+
name: 'HMAC',
|
|
22
|
+
hash: { name: algoMap[algorithm] },
|
|
23
|
+
},
|
|
24
|
+
false, // export = false
|
|
25
|
+
['sign', 'verify'] // what this key can do
|
|
26
|
+
)
|
|
27
|
+
const signature = await subtle.sign('HMAC', key, data)
|
|
28
|
+
return Buffer.from(signature)
|
|
29
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { getRandomValues } from 'uncrypto'
|
|
2
|
+
import { decode, encode } from '../base32.js'
|
|
3
|
+
import { bigEndian64 } from '../lib/utils.js'
|
|
4
|
+
import { AlgoEnum, createHmac } from './hmac.js'
|
|
5
|
+
|
|
6
|
+
interface TOTPURLOptions {
|
|
7
|
+
company: string
|
|
8
|
+
email: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { floor } = Math
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} secret - the secret to be used, needs to be a base32 encoded string
|
|
15
|
+
* @param {number} when - point of time in seconds (default: Date.now()/1000)
|
|
16
|
+
* @param {object} [options]
|
|
17
|
+
* @param {number} [options.period] in seconds (eg: 30 => 30 seconds)
|
|
18
|
+
* @param {import("./hmac.js").AlgoEnum} [options.algorithm] (default: sha512)
|
|
19
|
+
* @returns {Promise<string>}
|
|
20
|
+
*/
|
|
21
|
+
export async function totp(
|
|
22
|
+
secret: string,
|
|
23
|
+
when = floor(Date.now() / 1000),
|
|
24
|
+
options = {}
|
|
25
|
+
) {
|
|
26
|
+
const _options = Object.assign(
|
|
27
|
+
{
|
|
28
|
+
period: 30,
|
|
29
|
+
algorithm: 'sha512' as AlgoEnum,
|
|
30
|
+
},
|
|
31
|
+
options
|
|
32
|
+
)
|
|
33
|
+
const now = floor(when / _options.period)
|
|
34
|
+
const key = decode(secret)
|
|
35
|
+
const buff = bigEndian64(BigInt(now))
|
|
36
|
+
const hmac = await createHmac(_options.algorithm, key, buff)
|
|
37
|
+
const offset = hmac.at(-1)! & 0xf
|
|
38
|
+
const truncatedHash = hmac.subarray(offset, offset + 4)
|
|
39
|
+
const otp = (
|
|
40
|
+
(truncatedHash.readInt32BE() & 0x7f_ff_ff_ff) %
|
|
41
|
+
1_000_000
|
|
42
|
+
).toString(10)
|
|
43
|
+
return otp.length < 6 ? `${otp}`.padStart(6, '0') : otp
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} secret - the secret to be used, needs to be a base32 encoded string
|
|
48
|
+
* @param {string} totpToken - the totp token
|
|
49
|
+
* @param {object} [options]
|
|
50
|
+
* @param {number} [options.period] in seconds (eg: 30 => 30 seconds)
|
|
51
|
+
* @param {import("./hmac.js").AlgoEnum} [options.algorithm] (default: sha512)
|
|
52
|
+
* @returns {Promise<boolean>}
|
|
53
|
+
*/
|
|
54
|
+
export async function isTOTPValid(
|
|
55
|
+
secret: string,
|
|
56
|
+
totpToken: string,
|
|
57
|
+
options = {}
|
|
58
|
+
) {
|
|
59
|
+
const _options = Object.assign({ period: 30, algorithm: 'sha512' }, options)
|
|
60
|
+
for (let index = -2; index < 3; index += 1) {
|
|
61
|
+
const fromSys = await totp(secret, Date.now() / 1000 + index, _options)
|
|
62
|
+
const valid = fromSys === totpToken
|
|
63
|
+
if (valid) {
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function generateTOTPURL(secret: string, options: TOTPURLOptions) {
|
|
71
|
+
const parameters = new URLSearchParams()
|
|
72
|
+
parameters.append('secret', secret)
|
|
73
|
+
parameters.append('issuer', options.company)
|
|
74
|
+
parameters.append('digits', '6')
|
|
75
|
+
const url = `otpauth://totp/${options.company}:${
|
|
76
|
+
options.email
|
|
77
|
+
}?${parameters.toString()}`
|
|
78
|
+
return new URL(url).toString()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function generateTOTPSecret(num = 32) {
|
|
82
|
+
const array = new Uint32Array(num)
|
|
83
|
+
const vals = getRandomValues(array)
|
|
84
|
+
return encode(Buffer.from(vals).toString('ascii'))
|
|
85
|
+
}
|