@authcraft/totp-js 0.9.1
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/LICENSE +21 -0
- package/README.md +329 -0
- package/dist/index.d.mts +140 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.js +461 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +418 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +67 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// src/internal/base32.ts
|
|
2
|
+
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
3
|
+
var PAD = "=";
|
|
4
|
+
var DECODE_TABLE = new Uint8Array(128);
|
|
5
|
+
DECODE_TABLE.fill(255);
|
|
6
|
+
for (let i = 0; i < ALPHABET.length; i++) {
|
|
7
|
+
DECODE_TABLE[ALPHABET.charCodeAt(i)] = i;
|
|
8
|
+
DECODE_TABLE[ALPHABET.toLowerCase().charCodeAt(i)] = i;
|
|
9
|
+
}
|
|
10
|
+
function encode(data, padding = false) {
|
|
11
|
+
if (data.length === 0) return "";
|
|
12
|
+
let result = "";
|
|
13
|
+
let buffer = 0;
|
|
14
|
+
let bitsLeft = 0;
|
|
15
|
+
for (const byte of data) {
|
|
16
|
+
buffer = buffer << 8 | byte;
|
|
17
|
+
bitsLeft += 8;
|
|
18
|
+
while (bitsLeft >= 5) {
|
|
19
|
+
bitsLeft -= 5;
|
|
20
|
+
result += ALPHABET[buffer >>> bitsLeft & 31];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (bitsLeft > 0) {
|
|
24
|
+
result += ALPHABET[buffer << 5 - bitsLeft & 31];
|
|
25
|
+
}
|
|
26
|
+
if (padding) {
|
|
27
|
+
while (result.length % 8 !== 0) {
|
|
28
|
+
result += PAD;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
function decode(base32) {
|
|
34
|
+
const cleaned = base32.replace(/=+$/, "").replace(/\s/g, "");
|
|
35
|
+
if (cleaned.length === 0) return new Uint8Array(0);
|
|
36
|
+
validate(cleaned);
|
|
37
|
+
const output = new Uint8Array(Math.floor(cleaned.length * 5 / 8));
|
|
38
|
+
let buffer = 0;
|
|
39
|
+
let bitsLeft = 0;
|
|
40
|
+
let index = 0;
|
|
41
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
42
|
+
const val = DECODE_TABLE[cleaned.charCodeAt(i)];
|
|
43
|
+
buffer = buffer << 5 | val;
|
|
44
|
+
bitsLeft += 5;
|
|
45
|
+
if (bitsLeft >= 8) {
|
|
46
|
+
bitsLeft -= 8;
|
|
47
|
+
output[index++] = buffer >>> bitsLeft & 255;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return output.subarray(0, index);
|
|
51
|
+
}
|
|
52
|
+
function isValid(base32) {
|
|
53
|
+
const cleaned = base32.replace(/=+$/, "").replace(/\s/g, "");
|
|
54
|
+
if (cleaned.length === 0) return false;
|
|
55
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
56
|
+
const code = cleaned.charCodeAt(i);
|
|
57
|
+
if (code >= 128 || DECODE_TABLE[code] === 255) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
function validate(cleaned) {
|
|
64
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
65
|
+
const code = cleaned.charCodeAt(i);
|
|
66
|
+
if (code >= 128 || DECODE_TABLE[code] === 255) {
|
|
67
|
+
throw new Error(`Invalid Base32 character at position ${i}: '${cleaned[i]}'`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/internal/hmac.ts
|
|
73
|
+
import { createHmac } from "crypto";
|
|
74
|
+
var ALGORITHM_MAP = {
|
|
75
|
+
"HmacSHA1": "sha1",
|
|
76
|
+
"HmacSHA256": "sha256",
|
|
77
|
+
"HmacSHA512": "sha512"
|
|
78
|
+
};
|
|
79
|
+
function computeHmac(algorithm, key, data) {
|
|
80
|
+
const nodeAlgo = ALGORITHM_MAP[algorithm];
|
|
81
|
+
if (!nodeAlgo) {
|
|
82
|
+
throw new Error(`Unsupported HMAC algorithm: ${algorithm}`);
|
|
83
|
+
}
|
|
84
|
+
const hmac = createHmac(nodeAlgo, key);
|
|
85
|
+
hmac.update(data);
|
|
86
|
+
return new Uint8Array(hmac.digest());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/internal/engine.ts
|
|
90
|
+
import { timingSafeEqual } from "crypto";
|
|
91
|
+
var POWERS_OF_TEN = [1, 10, 100, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8];
|
|
92
|
+
function generateCode(secret, counter, algorithm, digits) {
|
|
93
|
+
validateSecret(secret);
|
|
94
|
+
const counterBytes = new Uint8Array(8);
|
|
95
|
+
const view = new DataView(counterBytes.buffer);
|
|
96
|
+
view.setBigUint64(0, BigInt(counter));
|
|
97
|
+
const hash = computeHmac(algorithm, secret, counterBytes);
|
|
98
|
+
const offset = hash[hash.length - 1] & 15;
|
|
99
|
+
const binary = (hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255;
|
|
100
|
+
const otp = binary % POWERS_OF_TEN[digits];
|
|
101
|
+
return otp.toString().padStart(digits, "0");
|
|
102
|
+
}
|
|
103
|
+
function verifyCode(secret, code, config, currentCounter) {
|
|
104
|
+
if (!isValidCodeFormat(code, config.digits)) {
|
|
105
|
+
return { valid: false, timeOffset: 0 };
|
|
106
|
+
}
|
|
107
|
+
let valid = false;
|
|
108
|
+
let matchOffset = 0;
|
|
109
|
+
for (let i = -config.allowedDrift; i <= config.allowedDrift; i++) {
|
|
110
|
+
const expected = generateCode(secret, currentCounter + i, config.algorithm.jcaName, config.digits);
|
|
111
|
+
if (constantTimeEquals(code, expected)) {
|
|
112
|
+
valid = true;
|
|
113
|
+
matchOffset = i;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { valid, timeOffset: matchOffset };
|
|
117
|
+
}
|
|
118
|
+
function constantTimeEquals(a, b) {
|
|
119
|
+
if (a.length !== b.length) return false;
|
|
120
|
+
const bufA = Buffer.from(a, "utf-8");
|
|
121
|
+
const bufB = Buffer.from(b, "utf-8");
|
|
122
|
+
return timingSafeEqual(bufA, bufB);
|
|
123
|
+
}
|
|
124
|
+
function validateSecret(secret) {
|
|
125
|
+
if (secret.length < 16) {
|
|
126
|
+
throw new Error("Secret must be at least 16 bytes (128 bits)");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function validateBase32Secret(base32) {
|
|
130
|
+
if (!base32 || base32.length < 26) {
|
|
131
|
+
throw new Error("Base32 secret must be at least 26 characters");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function isValidCodeFormat(code, digits) {
|
|
135
|
+
if (!code || code.length !== digits) return false;
|
|
136
|
+
for (let i = 0; i < code.length; i++) {
|
|
137
|
+
const c = code.charCodeAt(i);
|
|
138
|
+
if (c < 48 || c > 57) return false;
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/algorithm.ts
|
|
144
|
+
var Algorithm = {
|
|
145
|
+
SHA1: {
|
|
146
|
+
name: "SHA1",
|
|
147
|
+
jcaName: "HmacSHA1",
|
|
148
|
+
recommendedKeyBytes: 20,
|
|
149
|
+
otpauthName: "SHA1"
|
|
150
|
+
},
|
|
151
|
+
SHA256: {
|
|
152
|
+
name: "SHA256",
|
|
153
|
+
jcaName: "HmacSHA256",
|
|
154
|
+
recommendedKeyBytes: 32,
|
|
155
|
+
otpauthName: "SHA256"
|
|
156
|
+
},
|
|
157
|
+
SHA512: {
|
|
158
|
+
name: "SHA512",
|
|
159
|
+
jcaName: "HmacSHA512",
|
|
160
|
+
recommendedKeyBytes: 64,
|
|
161
|
+
otpauthName: "SHA512"
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
function algorithmFromName(name) {
|
|
165
|
+
const upper = name.toUpperCase().replace("-", "");
|
|
166
|
+
const key = upper;
|
|
167
|
+
if (key in Algorithm) {
|
|
168
|
+
return Algorithm[key];
|
|
169
|
+
}
|
|
170
|
+
throw new Error(`Unknown algorithm: ${name}. Supported: SHA1, SHA256, SHA512`);
|
|
171
|
+
}
|
|
172
|
+
function recommendedSecretLength(algo) {
|
|
173
|
+
return Math.ceil(algo.recommendedKeyBytes * 8 / 5);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/config.ts
|
|
177
|
+
var MIN_PERIOD = 15;
|
|
178
|
+
var MAX_PERIOD = 120;
|
|
179
|
+
var MIN_DIGITS = 6;
|
|
180
|
+
var MAX_DIGITS = 8;
|
|
181
|
+
var MAX_DRIFT = 5;
|
|
182
|
+
function defaultConfig() {
|
|
183
|
+
return { algorithm: Algorithm.SHA1, digits: 6, period: 30, allowedDrift: 1 };
|
|
184
|
+
}
|
|
185
|
+
function sha256Config() {
|
|
186
|
+
return { algorithm: Algorithm.SHA256, digits: 6, period: 30, allowedDrift: 1 };
|
|
187
|
+
}
|
|
188
|
+
function highSecurityConfig() {
|
|
189
|
+
return { algorithm: Algorithm.SHA512, digits: 8, period: 30, allowedDrift: 1 };
|
|
190
|
+
}
|
|
191
|
+
function createConfig(options = {}) {
|
|
192
|
+
const config = { ...defaultConfig(), ...options };
|
|
193
|
+
validateConfig(config);
|
|
194
|
+
return config;
|
|
195
|
+
}
|
|
196
|
+
function validateConfig(config) {
|
|
197
|
+
if (config.period < MIN_PERIOD || config.period > MAX_PERIOD) {
|
|
198
|
+
throw new Error(`Period must be between ${MIN_PERIOD} and ${MAX_PERIOD} seconds, got ${config.period}`);
|
|
199
|
+
}
|
|
200
|
+
if (config.digits < MIN_DIGITS || config.digits > MAX_DIGITS) {
|
|
201
|
+
throw new Error(`Digits must be between ${MIN_DIGITS} and ${MAX_DIGITS}, got ${config.digits}`);
|
|
202
|
+
}
|
|
203
|
+
if (config.allowedDrift < 0 || config.allowedDrift > MAX_DRIFT) {
|
|
204
|
+
throw new Error(`Allowed drift must be between 0 and ${MAX_DRIFT}, got ${config.allowedDrift}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/totp.ts
|
|
209
|
+
var TOTP = class _TOTP {
|
|
210
|
+
config;
|
|
211
|
+
replayGuard;
|
|
212
|
+
clock;
|
|
213
|
+
constructor(config, replayGuard, clock) {
|
|
214
|
+
this.config = config;
|
|
215
|
+
this.replayGuard = replayGuard;
|
|
216
|
+
this.clock = clock ?? (() => Date.now());
|
|
217
|
+
}
|
|
218
|
+
static create(options = {}) {
|
|
219
|
+
const { replayGuard, clock, ...configOpts } = options;
|
|
220
|
+
const config = createConfig(configOpts);
|
|
221
|
+
return new _TOTP(config, replayGuard, clock);
|
|
222
|
+
}
|
|
223
|
+
static defaultInstance() {
|
|
224
|
+
return new _TOTP(defaultConfig());
|
|
225
|
+
}
|
|
226
|
+
generate(base32Secret) {
|
|
227
|
+
validateBase32Secret(base32Secret);
|
|
228
|
+
const secret = decode(base32Secret);
|
|
229
|
+
const counter = this.getCurrentCounter();
|
|
230
|
+
return generateCode(secret, counter, this.config.algorithm.jcaName, this.config.digits);
|
|
231
|
+
}
|
|
232
|
+
generateAt(base32Secret, timestamp) {
|
|
233
|
+
validateBase32Secret(base32Secret);
|
|
234
|
+
const secret = decode(base32Secret);
|
|
235
|
+
const counter = Math.floor(timestamp / 1e3 / this.config.period);
|
|
236
|
+
return generateCode(secret, counter, this.config.algorithm.jcaName, this.config.digits);
|
|
237
|
+
}
|
|
238
|
+
generateForCounter(base32Secret, counter) {
|
|
239
|
+
validateBase32Secret(base32Secret);
|
|
240
|
+
const secret = decode(base32Secret);
|
|
241
|
+
return generateCode(secret, counter, this.config.algorithm.jcaName, this.config.digits);
|
|
242
|
+
}
|
|
243
|
+
verify(base32Secret, code, userId) {
|
|
244
|
+
const result = this.verifyWithDetails(base32Secret, code, userId);
|
|
245
|
+
return result.valid;
|
|
246
|
+
}
|
|
247
|
+
verifyWithDetails(base32Secret, code, userId) {
|
|
248
|
+
validateBase32Secret(base32Secret);
|
|
249
|
+
const secret = decode(base32Secret);
|
|
250
|
+
const currentCounter = this.getCurrentCounter();
|
|
251
|
+
const { valid, timeOffset } = verifyCode(secret, code, this.config, currentCounter);
|
|
252
|
+
if (!valid) {
|
|
253
|
+
return { valid: false, timeOffset: 0, message: "Invalid code" };
|
|
254
|
+
}
|
|
255
|
+
if (this.replayGuard && userId) {
|
|
256
|
+
const replayKey = `${userId}:${code}:${currentCounter + timeOffset}`;
|
|
257
|
+
if (!this.replayGuard.markUsed(replayKey)) {
|
|
258
|
+
return { valid: false, timeOffset, message: "Code already used" };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return { valid: true, timeOffset, message: "Valid" };
|
|
262
|
+
}
|
|
263
|
+
getCurrentCounter() {
|
|
264
|
+
return Math.floor(this.clock() / 1e3 / this.config.period);
|
|
265
|
+
}
|
|
266
|
+
getSecondsRemaining() {
|
|
267
|
+
const seconds = Math.floor(this.clock() / 1e3);
|
|
268
|
+
return this.config.period - seconds % this.config.period;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// src/secret-generator.ts
|
|
273
|
+
import { randomBytes } from "crypto";
|
|
274
|
+
var MIN_SECRET_BYTES = 16;
|
|
275
|
+
function generateSecret(algo = Algorithm.SHA1) {
|
|
276
|
+
return generateSecretBytes(algo.recommendedKeyBytes);
|
|
277
|
+
}
|
|
278
|
+
function generateSecretBytes(lengthBytes = 20) {
|
|
279
|
+
if (lengthBytes < MIN_SECRET_BYTES) {
|
|
280
|
+
throw new Error(`Secret length must be at least ${MIN_SECRET_BYTES} bytes, got ${lengthBytes}`);
|
|
281
|
+
}
|
|
282
|
+
const bytes = randomBytes(lengthBytes);
|
|
283
|
+
return encode(new Uint8Array(bytes));
|
|
284
|
+
}
|
|
285
|
+
function generateRawSecret(lengthBytes = 20) {
|
|
286
|
+
if (lengthBytes < MIN_SECRET_BYTES) {
|
|
287
|
+
throw new Error(`Secret length must be at least ${MIN_SECRET_BYTES} bytes, got ${lengthBytes}`);
|
|
288
|
+
}
|
|
289
|
+
return new Uint8Array(randomBytes(lengthBytes));
|
|
290
|
+
}
|
|
291
|
+
function isValidSecret(base32Secret) {
|
|
292
|
+
if (!base32Secret || base32Secret.length < 26) return false;
|
|
293
|
+
return isValid(base32Secret);
|
|
294
|
+
}
|
|
295
|
+
function entropyBits(base32Length) {
|
|
296
|
+
return base32Length * 5;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/replay-guard.ts
|
|
300
|
+
var InMemoryReplayGuard = class _InMemoryReplayGuard {
|
|
301
|
+
usedCodes = /* @__PURE__ */ new Map();
|
|
302
|
+
retentionMs;
|
|
303
|
+
cleanupTimer = null;
|
|
304
|
+
constructor(retentionMs = 12e4) {
|
|
305
|
+
this.retentionMs = retentionMs;
|
|
306
|
+
const cleanupInterval = Math.max(retentionMs / 2, 1e3);
|
|
307
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), cleanupInterval);
|
|
308
|
+
if (this.cleanupTimer.unref) {
|
|
309
|
+
this.cleanupTimer.unref();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
static withDefaultRetention() {
|
|
313
|
+
return new _InMemoryReplayGuard(12e4);
|
|
314
|
+
}
|
|
315
|
+
static forConfig(config) {
|
|
316
|
+
const retentionMs = config.period * (2 * config.allowedDrift + 1) * 1e3;
|
|
317
|
+
return new _InMemoryReplayGuard(retentionMs);
|
|
318
|
+
}
|
|
319
|
+
markUsed(key) {
|
|
320
|
+
if (this.usedCodes.has(key)) return false;
|
|
321
|
+
this.usedCodes.set(key, Date.now());
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
wasUsed(key) {
|
|
325
|
+
return this.usedCodes.has(key);
|
|
326
|
+
}
|
|
327
|
+
clear() {
|
|
328
|
+
this.usedCodes.clear();
|
|
329
|
+
}
|
|
330
|
+
size() {
|
|
331
|
+
return this.usedCodes.size;
|
|
332
|
+
}
|
|
333
|
+
destroy() {
|
|
334
|
+
if (this.cleanupTimer) {
|
|
335
|
+
clearInterval(this.cleanupTimer);
|
|
336
|
+
this.cleanupTimer = null;
|
|
337
|
+
}
|
|
338
|
+
this.usedCodes.clear();
|
|
339
|
+
}
|
|
340
|
+
cleanup() {
|
|
341
|
+
const now = Date.now();
|
|
342
|
+
for (const [key, timestamp] of this.usedCodes) {
|
|
343
|
+
if (now - timestamp > this.retentionMs) {
|
|
344
|
+
this.usedCodes.delete(key);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// src/errors.ts
|
|
351
|
+
var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
352
|
+
ErrorCode2["INVALID_SECRET"] = "INVALID_SECRET";
|
|
353
|
+
ErrorCode2["INVALID_CODE"] = "INVALID_CODE";
|
|
354
|
+
ErrorCode2["INVALID_CONFIG"] = "INVALID_CONFIG";
|
|
355
|
+
ErrorCode2["HMAC_ERROR"] = "HMAC_ERROR";
|
|
356
|
+
ErrorCode2["QR_GENERATION_ERROR"] = "QR_GENERATION_ERROR";
|
|
357
|
+
ErrorCode2["INTERNAL_ERROR"] = "INTERNAL_ERROR";
|
|
358
|
+
return ErrorCode2;
|
|
359
|
+
})(ErrorCode || {});
|
|
360
|
+
var TOTPError = class _TOTPError extends Error {
|
|
361
|
+
code;
|
|
362
|
+
constructor(code, message, cause) {
|
|
363
|
+
super(message, { cause });
|
|
364
|
+
this.code = code;
|
|
365
|
+
this.name = "TOTPError";
|
|
366
|
+
}
|
|
367
|
+
static invalidSecret(reason) {
|
|
368
|
+
return new _TOTPError("INVALID_SECRET" /* INVALID_SECRET */, `Invalid secret: ${reason}`);
|
|
369
|
+
}
|
|
370
|
+
static invalidCode(reason) {
|
|
371
|
+
return new _TOTPError("INVALID_CODE" /* INVALID_CODE */, `Invalid code: ${reason}`);
|
|
372
|
+
}
|
|
373
|
+
static invalidConfig(reason) {
|
|
374
|
+
return new _TOTPError("INVALID_CONFIG" /* INVALID_CONFIG */, `Invalid configuration: ${reason}`);
|
|
375
|
+
}
|
|
376
|
+
static hmacError(cause) {
|
|
377
|
+
return new _TOTPError("HMAC_ERROR" /* HMAC_ERROR */, "HMAC computation failed", cause);
|
|
378
|
+
}
|
|
379
|
+
static internalError(message, cause) {
|
|
380
|
+
return new _TOTPError("INTERNAL_ERROR" /* INTERNAL_ERROR */, message, cause);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// src/uri.ts
|
|
385
|
+
function buildOtpauthUri(secret, account, issuer, config = defaultConfig()) {
|
|
386
|
+
if (!secret) throw new Error("Secret is required");
|
|
387
|
+
if (!account) throw new Error("Account is required");
|
|
388
|
+
if (!issuer) throw new Error("Issuer is required");
|
|
389
|
+
const label = encodeURIComponent(`${issuer}:${account}`);
|
|
390
|
+
const params = new URLSearchParams({
|
|
391
|
+
secret,
|
|
392
|
+
issuer,
|
|
393
|
+
algorithm: config.algorithm.otpauthName,
|
|
394
|
+
digits: config.digits.toString(),
|
|
395
|
+
period: config.period.toString()
|
|
396
|
+
});
|
|
397
|
+
return `otpauth://totp/${label}?${params.toString()}`;
|
|
398
|
+
}
|
|
399
|
+
export {
|
|
400
|
+
Algorithm,
|
|
401
|
+
ErrorCode,
|
|
402
|
+
InMemoryReplayGuard,
|
|
403
|
+
TOTP,
|
|
404
|
+
TOTPError,
|
|
405
|
+
algorithmFromName,
|
|
406
|
+
buildOtpauthUri,
|
|
407
|
+
createConfig,
|
|
408
|
+
defaultConfig,
|
|
409
|
+
entropyBits,
|
|
410
|
+
generateRawSecret,
|
|
411
|
+
generateSecret,
|
|
412
|
+
generateSecretBytes,
|
|
413
|
+
highSecurityConfig,
|
|
414
|
+
isValidSecret,
|
|
415
|
+
recommendedSecretLength,
|
|
416
|
+
sha256Config
|
|
417
|
+
};
|
|
418
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/base32.ts","../src/internal/hmac.ts","../src/internal/engine.ts","../src/algorithm.ts","../src/config.ts","../src/totp.ts","../src/secret-generator.ts","../src/replay-guard.ts","../src/errors.ts","../src/uri.ts"],"sourcesContent":["/**\n * RFC 4648 Base32 encoding/decoding.\n * @internal\n */\n\nconst ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\nconst PAD = '=';\n\nconst DECODE_TABLE = new Uint8Array(128);\nDECODE_TABLE.fill(255);\nfor (let i = 0; i < ALPHABET.length; i++) {\n DECODE_TABLE[ALPHABET.charCodeAt(i)] = i;\n DECODE_TABLE[ALPHABET.toLowerCase().charCodeAt(i)] = i;\n}\n\nexport function encode(data: Uint8Array, padding = false): string {\n if (data.length === 0) return '';\n\n let result = '';\n let buffer = 0;\n let bitsLeft = 0;\n\n for (const byte of data) {\n buffer = (buffer << 8) | byte;\n bitsLeft += 8;\n while (bitsLeft >= 5) {\n bitsLeft -= 5;\n result += ALPHABET[(buffer >>> bitsLeft) & 0x1f];\n }\n }\n\n if (bitsLeft > 0) {\n result += ALPHABET[(buffer << (5 - bitsLeft)) & 0x1f];\n }\n\n if (padding) {\n while (result.length % 8 !== 0) {\n result += PAD;\n }\n }\n\n return result;\n}\n\nexport function decode(base32: string): Uint8Array {\n const cleaned = base32.replace(/=+$/, '').replace(/\\s/g, '');\n\n if (cleaned.length === 0) return new Uint8Array(0);\n\n validate(cleaned);\n\n const output = new Uint8Array(Math.floor((cleaned.length * 5) / 8));\n let buffer = 0;\n let bitsLeft = 0;\n let index = 0;\n\n for (let i = 0; i < cleaned.length; i++) {\n const val = DECODE_TABLE[cleaned.charCodeAt(i)];\n buffer = (buffer << 5) | val;\n bitsLeft += 5;\n if (bitsLeft >= 8) {\n bitsLeft -= 8;\n output[index++] = (buffer >>> bitsLeft) & 0xff;\n }\n }\n\n return output.subarray(0, index);\n}\n\nexport function isValid(base32: string): boolean {\n const cleaned = base32.replace(/=+$/, '').replace(/\\s/g, '');\n if (cleaned.length === 0) return false;\n\n for (let i = 0; i < cleaned.length; i++) {\n const code = cleaned.charCodeAt(i);\n if (code >= 128 || DECODE_TABLE[code] === 255) {\n return false;\n }\n }\n return true;\n}\n\nfunction validate(cleaned: string): void {\n for (let i = 0; i < cleaned.length; i++) {\n const code = cleaned.charCodeAt(i);\n if (code >= 128 || DECODE_TABLE[code] === 255) {\n throw new Error(`Invalid Base32 character at position ${i}: '${cleaned[i]}'`);\n }\n }\n}\n","/**\n * HMAC computation using Node.js crypto module.\n * Uses createHmac for synchronous, reliable HMAC generation.\n * @internal\n */\n\nimport { createHmac } from 'node:crypto';\n\nconst ALGORITHM_MAP: Record<string, string> = {\n 'HmacSHA1': 'sha1',\n 'HmacSHA256': 'sha256',\n 'HmacSHA512': 'sha512',\n};\n\nexport function computeHmac(\n algorithm: string,\n key: Uint8Array,\n data: Uint8Array,\n): Uint8Array {\n const nodeAlgo = ALGORITHM_MAP[algorithm];\n if (!nodeAlgo) {\n throw new Error(`Unsupported HMAC algorithm: ${algorithm}`);\n }\n\n const hmac = createHmac(nodeAlgo, key);\n hmac.update(data);\n return new Uint8Array(hmac.digest());\n}\n\nexport function isAlgorithmAvailable(algorithm: string): boolean {\n return algorithm in ALGORITHM_MAP;\n}\n","/**\n * Core TOTP/HOTP engine implementing RFC 6238 and RFC 4226.\n * @internal\n */\n\nimport { computeHmac } from './hmac.js';\nimport type { TOTPConfig } from '../config.js';\nimport { timingSafeEqual } from 'node:crypto';\n\nconst POWERS_OF_TEN = [1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000];\n\nexport function generateCode(\n secret: Uint8Array,\n counter: number,\n algorithm: string,\n digits: number,\n): string {\n validateSecret(secret);\n\n // Step 1: Convert counter to 8-byte big-endian\n const counterBytes = new Uint8Array(8);\n const view = new DataView(counterBytes.buffer);\n view.setBigUint64(0, BigInt(counter));\n\n // Step 2: Compute HMAC\n const hash = computeHmac(algorithm, secret, counterBytes);\n\n // Step 3: Dynamic truncation (RFC 4226 section 5.4)\n const offset = hash[hash.length - 1]! & 0x0f;\n const binary =\n ((hash[offset]! & 0x7f) << 24) |\n ((hash[offset + 1]! & 0xff) << 16) |\n ((hash[offset + 2]! & 0xff) << 8) |\n (hash[offset + 3]! & 0xff);\n\n // Step 4: Compute OTP\n const otp = binary % POWERS_OF_TEN[digits]!;\n\n // Step 5: Pad with leading zeros\n return otp.toString().padStart(digits, '0');\n}\n\nexport function verifyCode(\n secret: Uint8Array,\n code: string,\n config: TOTPConfig,\n currentCounter: number,\n): { valid: boolean; timeOffset: number } {\n if (!isValidCodeFormat(code, config.digits)) {\n return { valid: false, timeOffset: 0 };\n }\n\n let valid = false;\n let matchOffset = 0;\n\n // Check all drift windows for constant-time behavior\n for (let i = -config.allowedDrift; i <= config.allowedDrift; i++) {\n const expected = generateCode(secret, currentCounter + i, config.algorithm.jcaName, config.digits);\n if (constantTimeEquals(code, expected)) {\n valid = true;\n matchOffset = i;\n // Do NOT return early — constant-time requires checking all windows\n }\n }\n\n return { valid, timeOffset: matchOffset };\n}\n\nexport function constantTimeEquals(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n const bufA = Buffer.from(a, 'utf-8');\n const bufB = Buffer.from(b, 'utf-8');\n return timingSafeEqual(bufA, bufB);\n}\n\nexport function validateSecret(secret: Uint8Array): void {\n if (secret.length < 16) {\n throw new Error('Secret must be at least 16 bytes (128 bits)');\n }\n}\n\nexport function validateBase32Secret(base32: string): void {\n if (!base32 || base32.length < 26) {\n throw new Error('Base32 secret must be at least 26 characters');\n }\n}\n\nexport function isValidCodeFormat(code: string, digits: number): boolean {\n if (!code || code.length !== digits) return false;\n for (let i = 0; i < code.length; i++) {\n const c = code.charCodeAt(i);\n if (c < 48 || c > 57) return false; // not 0-9\n }\n return true;\n}\n","/**\n * Supported HMAC algorithms for TOTP generation.\n */\nexport interface AlgorithmDef {\n readonly name: string;\n readonly jcaName: string;\n readonly recommendedKeyBytes: number;\n readonly otpauthName: string;\n}\n\nexport const Algorithm = {\n SHA1: {\n name: 'SHA1',\n jcaName: 'HmacSHA1',\n recommendedKeyBytes: 20,\n otpauthName: 'SHA1',\n },\n SHA256: {\n name: 'SHA256',\n jcaName: 'HmacSHA256',\n recommendedKeyBytes: 32,\n otpauthName: 'SHA256',\n },\n SHA512: {\n name: 'SHA512',\n jcaName: 'HmacSHA512',\n recommendedKeyBytes: 64,\n otpauthName: 'SHA512',\n },\n} as const satisfies Record<string, AlgorithmDef>;\n\nexport type AlgorithmKey = keyof typeof Algorithm;\nexport type AlgorithmValue = (typeof Algorithm)[AlgorithmKey];\n\nexport function algorithmFromName(name: string): AlgorithmValue {\n const upper = name.toUpperCase().replace('-', '');\n const key = upper as AlgorithmKey;\n if (key in Algorithm) {\n return Algorithm[key];\n }\n throw new Error(`Unknown algorithm: ${name}. Supported: SHA1, SHA256, SHA512`);\n}\n\nexport function recommendedSecretLength(algo: AlgorithmValue): number {\n // Base32 encodes 5 bits per character\n return Math.ceil((algo.recommendedKeyBytes * 8) / 5);\n}\n","/**\n * Immutable TOTP configuration.\n */\nimport { Algorithm, type AlgorithmValue } from './algorithm.js';\n\nexport interface TOTPConfig {\n readonly algorithm: AlgorithmValue;\n readonly digits: number;\n readonly period: number;\n readonly allowedDrift: number;\n}\n\nconst MIN_PERIOD = 15;\nconst MAX_PERIOD = 120;\nconst MIN_DIGITS = 6;\nconst MAX_DIGITS = 8;\nconst MAX_DRIFT = 5;\n\nexport function defaultConfig(): TOTPConfig {\n return { algorithm: Algorithm.SHA1, digits: 6, period: 30, allowedDrift: 1 };\n}\n\nexport function sha256Config(): TOTPConfig {\n return { algorithm: Algorithm.SHA256, digits: 6, period: 30, allowedDrift: 1 };\n}\n\nexport function highSecurityConfig(): TOTPConfig {\n return { algorithm: Algorithm.SHA512, digits: 8, period: 30, allowedDrift: 1 };\n}\n\nexport function createConfig(options: Partial<TOTPConfig> = {}): TOTPConfig {\n const config: TOTPConfig = { ...defaultConfig(), ...options };\n validateConfig(config);\n return config;\n}\n\nfunction validateConfig(config: TOTPConfig): void {\n if (config.period < MIN_PERIOD || config.period > MAX_PERIOD) {\n throw new Error(`Period must be between ${MIN_PERIOD} and ${MAX_PERIOD} seconds, got ${config.period}`);\n }\n if (config.digits < MIN_DIGITS || config.digits > MAX_DIGITS) {\n throw new Error(`Digits must be between ${MIN_DIGITS} and ${MAX_DIGITS}, got ${config.digits}`);\n }\n if (config.allowedDrift < 0 || config.allowedDrift > MAX_DRIFT) {\n throw new Error(`Allowed drift must be between 0 and ${MAX_DRIFT}, got ${config.allowedDrift}`);\n }\n}\n","/**\n * Main TOTP class — the primary public API.\n */\nimport { decode } from './internal/base32.js';\nimport { generateCode, verifyCode, validateBase32Secret } from './internal/engine.js';\nimport { type TOTPConfig, defaultConfig, createConfig } from './config.js';\nimport type { ReplayGuard } from './replay-guard.js';\nimport type { AlgorithmValue } from './algorithm.js';\n\nexport interface VerificationResult {\n readonly valid: boolean;\n readonly timeOffset: number;\n readonly message: string;\n}\n\nexport interface TOTPOptions {\n algorithm?: AlgorithmValue;\n digits?: number;\n period?: number;\n allowedDrift?: number;\n replayGuard?: ReplayGuard;\n clock?: () => number; // Returns current time in milliseconds\n}\n\nexport class TOTP {\n readonly config: TOTPConfig;\n private readonly replayGuard: ReplayGuard | undefined;\n private readonly clock: () => number;\n\n private constructor(config: TOTPConfig, replayGuard?: ReplayGuard, clock?: () => number) {\n this.config = config;\n this.replayGuard = replayGuard;\n this.clock = clock ?? (() => Date.now());\n }\n\n static create(options: TOTPOptions = {}): TOTP {\n const { replayGuard, clock, ...configOpts } = options;\n const config = createConfig(configOpts);\n return new TOTP(config, replayGuard, clock);\n }\n\n static defaultInstance(): TOTP {\n return new TOTP(defaultConfig());\n }\n\n generate(base32Secret: string): string {\n validateBase32Secret(base32Secret);\n const secret = decode(base32Secret);\n const counter = this.getCurrentCounter();\n return generateCode(secret, counter, this.config.algorithm.jcaName, this.config.digits);\n }\n\n generateAt(base32Secret: string, timestamp: number): string {\n validateBase32Secret(base32Secret);\n const secret = decode(base32Secret);\n const counter = Math.floor(timestamp / 1000 / this.config.period);\n return generateCode(secret, counter, this.config.algorithm.jcaName, this.config.digits);\n }\n\n generateForCounter(base32Secret: string, counter: number): string {\n validateBase32Secret(base32Secret);\n const secret = decode(base32Secret);\n return generateCode(secret, counter, this.config.algorithm.jcaName, this.config.digits);\n }\n\n verify(base32Secret: string, code: string, userId?: string): boolean {\n const result = this.verifyWithDetails(base32Secret, code, userId);\n return result.valid;\n }\n\n verifyWithDetails(base32Secret: string, code: string, userId?: string): VerificationResult {\n validateBase32Secret(base32Secret);\n const secret = decode(base32Secret);\n const currentCounter = this.getCurrentCounter();\n\n const { valid, timeOffset } = verifyCode(secret, code, this.config, currentCounter);\n\n if (!valid) {\n return { valid: false, timeOffset: 0, message: 'Invalid code' };\n }\n\n // Replay protection\n if (this.replayGuard && userId) {\n const replayKey = `${userId}:${code}:${currentCounter + timeOffset}`;\n if (!this.replayGuard.markUsed(replayKey)) {\n return { valid: false, timeOffset, message: 'Code already used' };\n }\n }\n\n return { valid: true, timeOffset, message: 'Valid' };\n }\n\n getCurrentCounter(): number {\n return Math.floor(this.clock() / 1000 / this.config.period);\n }\n\n getSecondsRemaining(): number {\n const seconds = Math.floor(this.clock() / 1000);\n return this.config.period - (seconds % this.config.period);\n }\n}\n","/**\n * Cryptographically secure secret generation for TOTP.\n */\nimport { randomBytes } from 'node:crypto';\nimport { encode, isValid } from './internal/base32.js';\nimport { Algorithm, type AlgorithmValue } from './algorithm.js';\n\nconst MIN_SECRET_BYTES = 16;\n\nexport function generateSecret(algo: AlgorithmValue = Algorithm.SHA1): string {\n return generateSecretBytes(algo.recommendedKeyBytes);\n}\n\nexport function generateSecretBytes(lengthBytes: number = 20): string {\n if (lengthBytes < MIN_SECRET_BYTES) {\n throw new Error(`Secret length must be at least ${MIN_SECRET_BYTES} bytes, got ${lengthBytes}`);\n }\n const bytes = randomBytes(lengthBytes);\n return encode(new Uint8Array(bytes));\n}\n\nexport function generateRawSecret(lengthBytes: number = 20): Uint8Array {\n if (lengthBytes < MIN_SECRET_BYTES) {\n throw new Error(`Secret length must be at least ${MIN_SECRET_BYTES} bytes, got ${lengthBytes}`);\n }\n return new Uint8Array(randomBytes(lengthBytes));\n}\n\nexport function isValidSecret(base32Secret: string): boolean {\n if (!base32Secret || base32Secret.length < 26) return false;\n return isValid(base32Secret);\n}\n\nexport function entropyBits(base32Length: number): number {\n return base32Length * 5;\n}\n","/**\n * Replay attack prevention for TOTP codes.\n */\n\nexport interface ReplayGuard {\n markUsed(key: string): boolean;\n wasUsed(key: string): boolean;\n clear(): void;\n size(): number;\n}\n\nexport class InMemoryReplayGuard implements ReplayGuard {\n private readonly usedCodes = new Map<string, number>();\n private readonly retentionMs: number;\n private cleanupTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor(retentionMs: number = 120_000) {\n this.retentionMs = retentionMs;\n const cleanupInterval = Math.max(retentionMs / 2, 1000);\n this.cleanupTimer = setInterval(() => this.cleanup(), cleanupInterval);\n // Allow process to exit even if timer is active\n if (this.cleanupTimer.unref) {\n this.cleanupTimer.unref();\n }\n }\n\n static withDefaultRetention(): InMemoryReplayGuard {\n return new InMemoryReplayGuard(120_000); // 2 minutes\n }\n\n static forConfig(config: { period: number; allowedDrift: number }): InMemoryReplayGuard {\n const retentionMs = config.period * (2 * config.allowedDrift + 1) * 1000;\n return new InMemoryReplayGuard(retentionMs);\n }\n\n markUsed(key: string): boolean {\n if (this.usedCodes.has(key)) return false;\n this.usedCodes.set(key, Date.now());\n return true;\n }\n\n wasUsed(key: string): boolean {\n return this.usedCodes.has(key);\n }\n\n clear(): void {\n this.usedCodes.clear();\n }\n\n size(): number {\n return this.usedCodes.size;\n }\n\n destroy(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n this.usedCodes.clear();\n }\n\n private cleanup(): void {\n const now = Date.now();\n for (const [key, timestamp] of this.usedCodes) {\n if (now - timestamp > this.retentionMs) {\n this.usedCodes.delete(key);\n }\n }\n }\n}\n","/**\n * Structured error types for totp-js.\n */\n\nexport enum ErrorCode {\n INVALID_SECRET = 'INVALID_SECRET',\n INVALID_CODE = 'INVALID_CODE',\n INVALID_CONFIG = 'INVALID_CONFIG',\n HMAC_ERROR = 'HMAC_ERROR',\n QR_GENERATION_ERROR = 'QR_GENERATION_ERROR',\n INTERNAL_ERROR = 'INTERNAL_ERROR',\n}\n\nexport class TOTPError extends Error {\n readonly code: ErrorCode;\n\n constructor(code: ErrorCode, message: string, cause?: Error) {\n super(message, { cause });\n this.code = code;\n this.name = 'TOTPError';\n }\n\n static invalidSecret(reason: string): TOTPError {\n return new TOTPError(ErrorCode.INVALID_SECRET, `Invalid secret: ${reason}`);\n }\n\n static invalidCode(reason: string): TOTPError {\n return new TOTPError(ErrorCode.INVALID_CODE, `Invalid code: ${reason}`);\n }\n\n static invalidConfig(reason: string): TOTPError {\n return new TOTPError(ErrorCode.INVALID_CONFIG, `Invalid configuration: ${reason}`);\n }\n\n static hmacError(cause: Error): TOTPError {\n return new TOTPError(ErrorCode.HMAC_ERROR, 'HMAC computation failed', cause);\n }\n\n static internalError(message: string, cause?: Error): TOTPError {\n return new TOTPError(ErrorCode.INTERNAL_ERROR, message, cause);\n }\n}\n","/**\n * OTPAuth URI builder for QR code generation.\n * Follows the otpauth:// URI format used by Google Authenticator and other apps.\n */\nimport { type TOTPConfig, defaultConfig } from './config.js';\n\nexport function buildOtpauthUri(\n secret: string,\n account: string,\n issuer: string,\n config: TOTPConfig = defaultConfig(),\n): string {\n if (!secret) throw new Error('Secret is required');\n if (!account) throw new Error('Account is required');\n if (!issuer) throw new Error('Issuer is required');\n\n const label = encodeURIComponent(`${issuer}:${account}`);\n const params = new URLSearchParams({\n secret,\n issuer,\n algorithm: config.algorithm.otpauthName,\n digits: config.digits.toString(),\n period: config.period.toString(),\n });\n\n return `otpauth://totp/${label}?${params.toString()}`;\n}\n"],"mappings":"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","names":["ErrorCode"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@authcraft/totp-js",
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"description": "Security-hardened TOTP/2FA library for JavaScript and TypeScript. RFC 6238 compliant with replay protection, constant-time verification, and zero runtime dependencies.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"test:coverage": "vitest run --coverage",
|
|
25
|
+
"lint": "eslint src/ tests/",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"totp",
|
|
31
|
+
"hotp",
|
|
32
|
+
"otp",
|
|
33
|
+
"2fa",
|
|
34
|
+
"two-factor",
|
|
35
|
+
"authentication",
|
|
36
|
+
"mfa",
|
|
37
|
+
"multi-factor",
|
|
38
|
+
"rfc6238",
|
|
39
|
+
"rfc4226",
|
|
40
|
+
"security",
|
|
41
|
+
"google-authenticator",
|
|
42
|
+
"time-based-one-time-password"
|
|
43
|
+
],
|
|
44
|
+
"author": "Pratiyush Kumar Singh <pratiyush1@gmail.com>",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/Pratiyush/totp-js.git"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/Pratiyush/totp-js/issues"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://pratiyush.github.io/totp-js/",
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=22.0.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "^25.5.2",
|
|
59
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
60
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
61
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
62
|
+
"eslint": "^9.0.0",
|
|
63
|
+
"tsup": "^8.0.0",
|
|
64
|
+
"typescript": "^6.0.2",
|
|
65
|
+
"vitest": "^4.1.2"
|
|
66
|
+
}
|
|
67
|
+
}
|