@nowarajs/totp 1.2.0 β 1.2.2
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/README.md +61 -150
- package/dist/{chunk-q8z45f9z.js β chunk-t1hx3d3e.js} +19 -5
- package/dist/chunk-zfdfmrcn.js +17 -0
- package/dist/enums/index.js +1 -1
- package/dist/enums/totp-error-keys.d.ts +6 -0
- package/dist/hotp.d.ts +15 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +72 -15
- package/dist/otp-auth-uri.d.ts +7 -0
- package/dist/totp.d.ts +11 -3
- package/dist/types/index.d.ts +8 -0
- package/dist/types/otp-auth-uri.d.ts +1 -1
- package/dist/types/verify-options.d.ts +1 -1
- package/dist/utils/base32.d.ts +10 -1
- package/dist/utils/create-counter-buffer.d.ts +9 -1
- package/dist/utils/dynamic-truncation.d.ts +6 -3
- package/dist/utils/index.js +27 -17
- package/package.json +9 -9
- package/dist/chunk-sx6rwmqe.js +0 -11
package/README.md
CHANGED
|
@@ -1,41 +1,32 @@
|
|
|
1
|
-
# π NowaraJS
|
|
1
|
+
# π NowaraJS TOTP
|
|
2
|
+
|
|
3
|
+
Let's be honest: there are already packages like `totp-generator` that do this. I built this one mostly for myselfβto learn how TOTP/HOTP actually works under the hood, and to have a lightweight alternative I fully understand.
|
|
4
|
+
|
|
5
|
+
## Why this package?
|
|
6
|
+
|
|
7
|
+
No grand mission here. I wanted:
|
|
8
|
+
1. **To learn** how RFC 6238/4226 work in practice
|
|
9
|
+
2. **A tiny footprint** without pulling half of npm
|
|
10
|
+
3. **Something I control** for my own projects
|
|
11
|
+
|
|
12
|
+
If you're looking for battle-tested libraries, check out the established ones. If you want something small and readable, this might be for you.
|
|
2
13
|
|
|
3
14
|
## π Table of Contents
|
|
4
15
|
|
|
5
|
-
- [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- [Basic TOTP Generation](#basic-totp-generation)
|
|
12
|
-
- [TOTP Verification](#totp-verification)
|
|
13
|
-
- [HOTP Support](#hotp-support)
|
|
14
|
-
- [OTPAuth URI Generation](#otpauth-uri-generation)
|
|
15
|
-
- [Secret Generation](#secret-generation)
|
|
16
|
-
- [π Advanced Configuration](#-advanced-configuration)
|
|
17
|
-
- [π οΈ Utilities](#-utilities)
|
|
18
|
-
- [π API Reference](#-api-reference)
|
|
19
|
-
- [βοΈ License](#-license)
|
|
20
|
-
- [π§ Contact](#-contact)
|
|
21
|
-
|
|
22
|
-
## π Description
|
|
23
|
-
|
|
24
|
-
> A comprehensive Time-based One-Time Password (TOTP) and HMAC-based One-Time Password (HOTP) implementation for TypeScript/JavaScript.
|
|
25
|
-
|
|
26
|
-
**NowaraJS TOTP** provides a secure and RFC-compliant implementation of TOTP and HOTP algorithms with full support for QR code generation, secret management, and various authentication configurations. Perfect for implementing two-factor authentication (2FA) in your applications.
|
|
16
|
+
- [Features](#-features)
|
|
17
|
+
- [Installation](#-installation)
|
|
18
|
+
- [Usage](#-usage)
|
|
19
|
+
- [API Reference](#-api-reference)
|
|
20
|
+
- [License](#-license)
|
|
21
|
+
- [Contact](#-contact)
|
|
27
22
|
|
|
28
23
|
## β¨ Features
|
|
29
24
|
|
|
30
|
-
- π **RFC 6238 TOTP
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
- π οΈ **Configurable**: Support for different algorithms (SHA-1, SHA-256, SHA-512)
|
|
36
|
-
- π **Flexible Digits**: Support for 6-8 digit codes
|
|
37
|
-
- β° **Time Window**: Configurable time periods and verification windows
|
|
38
|
-
- π― **Base32 Encoding**: Built-in Base32 encoding/decoding utilities
|
|
25
|
+
- π **RFC Compliant**: Full RFC 6238 (TOTP) and RFC 4226 (HOTP) implementation.
|
|
26
|
+
- π± **QR Code Ready**: Generate OTPAuth URIs compatible with Google Authenticator, Authy, etc.
|
|
27
|
+
- π **Crypto Secure**: Uses Web Crypto API for truly random secret generation.
|
|
28
|
+
- π οΈ **Flexible**: SHA-1, SHA-256, SHA-512 algorithms with 6-8 digit codes.
|
|
29
|
+
- π¦ **Zero Dependencies**: Pure TypeScript, tiny footprint.
|
|
39
30
|
|
|
40
31
|
## π§ Installation
|
|
41
32
|
|
|
@@ -45,168 +36,88 @@ bun add @nowarajs/totp @nowarajs/error
|
|
|
45
36
|
|
|
46
37
|
## βοΈ Usage
|
|
47
38
|
|
|
48
|
-
###
|
|
39
|
+
### Generate a TOTP Code
|
|
40
|
+
|
|
41
|
+
Use this when you need to generate a one-time password for the current time window.
|
|
49
42
|
|
|
50
43
|
```ts
|
|
51
44
|
import { totp, generateSecretBytes, base32Encode } from '@nowarajs/totp';
|
|
52
45
|
|
|
53
|
-
// Generate a secret
|
|
54
|
-
const secret = generateSecretBytes(20);
|
|
55
|
-
const secretBase32 = base32Encode(secret);
|
|
46
|
+
// Generate a cryptographically secure secret
|
|
47
|
+
const secret = generateSecretBytes(20);
|
|
56
48
|
|
|
57
|
-
// Generate TOTP code
|
|
49
|
+
// Generate the current TOTP code
|
|
58
50
|
const code = await totp(secret, {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
algorithm: 'SHA-1',
|
|
52
|
+
digits: 6,
|
|
53
|
+
period: 30
|
|
62
54
|
});
|
|
63
55
|
|
|
64
|
-
console.log('
|
|
56
|
+
console.log('Your code:', code); // e.g., "847263"
|
|
65
57
|
```
|
|
66
58
|
|
|
67
|
-
###
|
|
59
|
+
### Verify a User's Code
|
|
60
|
+
|
|
61
|
+
Use this to validate the code your user just entered. The `window` option handles clock drift gracefully.
|
|
68
62
|
|
|
69
63
|
```ts
|
|
70
64
|
import { verifyTotp } from '@nowarajs/totp';
|
|
71
65
|
|
|
72
|
-
// Verify a TOTP code
|
|
73
66
|
const isValid = await verifyTotp(secret, userInputCode, {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
algorithm: 'SHA-1',
|
|
68
|
+
digits: 6,
|
|
69
|
+
period: 30,
|
|
70
|
+
window: 1 // Accept codes from Β±30 seconds
|
|
78
71
|
});
|
|
79
72
|
|
|
80
|
-
if (isValid)
|
|
81
|
-
|
|
82
|
-
} else {
|
|
83
|
-
console.log('β Invalid code');
|
|
84
|
-
}
|
|
73
|
+
if (isValid) console.log('β
Access granted');
|
|
74
|
+
else console.log('β Invalid code');
|
|
85
75
|
```
|
|
86
76
|
|
|
87
|
-
###
|
|
77
|
+
### Generate a QR Code URI
|
|
88
78
|
|
|
89
|
-
|
|
90
|
-
import { hotp } from '@nowarajs/totp';
|
|
91
|
-
|
|
92
|
-
// Generate HOTP code with counter
|
|
93
|
-
const counter = 123;
|
|
94
|
-
const hotpCode = await hotp(secret, counter, {
|
|
95
|
-
algorithm: 'SHA-1',
|
|
96
|
-
digits: 6
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
console.log('HOTP Code:', hotpCode);
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### OTPAuth URI Generation
|
|
79
|
+
Use this to let users scan a QR code with their authenticator app.
|
|
103
80
|
|
|
104
81
|
```ts
|
|
105
|
-
import { buildOtpAuthUri, base32Encode } from '@nowarajs/totp';
|
|
82
|
+
import { buildOtpAuthUri, generateSecretBytes, base32Encode } from '@nowarajs/totp';
|
|
106
83
|
|
|
107
84
|
const secret = generateSecretBytes(20);
|
|
108
85
|
const secretBase32 = base32Encode(secret);
|
|
109
86
|
|
|
110
|
-
// Create URI for QR code
|
|
111
87
|
const uri = buildOtpAuthUri({
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
88
|
+
secretBase32,
|
|
89
|
+
label: 'user@example.com',
|
|
90
|
+
issuer: 'MyApp',
|
|
91
|
+
algorithm: 'SHA-1',
|
|
92
|
+
digits: 6,
|
|
93
|
+
period: 30
|
|
118
94
|
});
|
|
119
95
|
|
|
120
|
-
|
|
96
|
+
// Feed this URI to any QR code library
|
|
97
|
+
console.log(uri);
|
|
121
98
|
// otpauth://totp/user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=MyApp
|
|
122
99
|
```
|
|
123
100
|
|
|
124
|
-
###
|
|
101
|
+
### HOTP (Counter-Based)
|
|
125
102
|
|
|
126
|
-
|
|
127
|
-
import { generateSecretBytes, base32Encode, base32Decode } from '@nowarajs/totp/utils';
|
|
128
|
-
|
|
129
|
-
// Generate cryptographically secure secret
|
|
130
|
-
const secret = generateSecretBytes(32); // 256 bits for extra security
|
|
131
|
-
|
|
132
|
-
// Encode for storage/transmission
|
|
133
|
-
const encoded = base32Encode(secret);
|
|
134
|
-
console.log('Base32 Secret:', encoded);
|
|
135
|
-
|
|
136
|
-
// Decode when needed
|
|
137
|
-
const decoded = base32Decode(encoded);
|
|
138
|
-
console.log('Original bytes match:', secret.every((byte, i) => byte === decoded[i]));
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
## π Advanced Configuration
|
|
142
|
-
|
|
143
|
-
### Custom Algorithm and Settings
|
|
103
|
+
Use this when you need counter-based OTPs instead of time-based ones.
|
|
144
104
|
|
|
145
105
|
```ts
|
|
146
|
-
|
|
147
|
-
const advancedCode = await totp(secret, {
|
|
148
|
-
algorithm: 'SHA-256',
|
|
149
|
-
digits: 8,
|
|
150
|
-
period: 60
|
|
151
|
-
});
|
|
106
|
+
import { hotp } from '@nowarajs/totp';
|
|
152
107
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
digits: 8,
|
|
157
|
-
period: 60,
|
|
158
|
-
window: 2 // Allow Β±2 time steps (Β±2 minutes)
|
|
108
|
+
const code = await hotp(secret, 123, {
|
|
109
|
+
algorithm: 'SHA-1',
|
|
110
|
+
digits: 6
|
|
159
111
|
});
|
|
160
112
|
```
|
|
161
113
|
|
|
162
|
-
### Parse Existing OTPAuth URIs
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
import { parseOtpAuthUri } from '@nowarajs/totp';
|
|
166
|
-
|
|
167
|
-
const uri = 'otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example';
|
|
168
|
-
const parsed = parseOtpAuthUri(uri);
|
|
169
|
-
|
|
170
|
-
console.log(parsed);
|
|
171
|
-
// {
|
|
172
|
-
// type: 'totp',
|
|
173
|
-
// label: 'Example:alice@google.com',
|
|
174
|
-
// secret: 'JBSWY3DPEHPK3PXP',
|
|
175
|
-
// issuer: 'Example',
|
|
176
|
-
// algorithm: 'SHA-1',
|
|
177
|
-
// digits: 6,
|
|
178
|
-
// period: 30
|
|
179
|
-
// }
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
## π οΈ Utilities
|
|
183
|
-
|
|
184
|
-
The package includes several utility functions available through subpath imports:
|
|
185
|
-
|
|
186
|
-
```ts
|
|
187
|
-
// Base32 encoding/decoding
|
|
188
|
-
import { base32Encode, base32Decode } from '@nowarajs/totp/utils';
|
|
189
|
-
|
|
190
|
-
// Secret generation
|
|
191
|
-
import { generateSecretBytes } from '@nowarajs/totp/utils';
|
|
192
|
-
|
|
193
|
-
// Time utilities
|
|
194
|
-
import { timeRemaining } from '@nowarajs/totp/utils';
|
|
195
|
-
|
|
196
|
-
// Get seconds until next TOTP generation
|
|
197
|
-
const remaining = timeRemaining(30); // for 30-second period
|
|
198
|
-
console.log(`Next code in ${remaining} seconds`);
|
|
199
|
-
```
|
|
200
|
-
|
|
201
114
|
## π API Reference
|
|
202
115
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
- [Reference Documentation](https://nowarajs.github.io/totp/)
|
|
116
|
+
Full docs: [nowarajs.github.io/totp](https://nowarajs.github.io/totp/)
|
|
206
117
|
|
|
207
118
|
## βοΈ License
|
|
208
119
|
|
|
209
|
-
|
|
120
|
+
MIT - Feel free to use it.
|
|
210
121
|
|
|
211
122
|
## π§ Contact
|
|
212
123
|
|
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
import {
|
|
3
|
+
TOTP_ERROR_KEYS
|
|
4
|
+
} from "./chunk-zfdfmrcn.js";
|
|
5
|
+
|
|
2
6
|
// source/utils/create-counter-buffer.ts
|
|
3
7
|
var createCounterBuffer = (counter) => {
|
|
4
8
|
const counterBuffer = new ArrayBuffer(8);
|
|
5
9
|
const counterView = new DataView(counterBuffer);
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
else
|
|
9
|
-
counterView.setUint32(4, counter, false);
|
|
10
|
+
const counterBigInt = typeof counter === "bigint" ? counter : BigInt(Math.floor(counter));
|
|
11
|
+
counterView.setBigUint64(0, counterBigInt, false);
|
|
10
12
|
return counterBuffer;
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
// source/utils/dynamic-truncation.ts
|
|
16
|
+
import { InternalError } from "@nowarajs/error";
|
|
17
|
+
var _DIGIT_MODULO = {
|
|
18
|
+
6: 1e6,
|
|
19
|
+
8: 1e8
|
|
20
|
+
};
|
|
14
21
|
var dynamicTruncation = (hmacArray, digits) => {
|
|
22
|
+
if (hmacArray.length < 20)
|
|
23
|
+
throw new InternalError(TOTP_ERROR_KEYS.INVALID_HMAC_LENGTH, "HMAC must be at least 20 bytes");
|
|
24
|
+
const modulo = _DIGIT_MODULO[digits];
|
|
25
|
+
if (modulo === undefined)
|
|
26
|
+
throw new InternalError(TOTP_ERROR_KEYS.INVALID_DIGITS, "Digits must be 6 or 8");
|
|
15
27
|
const offset = hmacArray[hmacArray.length - 1] & 15;
|
|
16
|
-
|
|
28
|
+
if (offset + 4 > hmacArray.length)
|
|
29
|
+
throw new InternalError(TOTP_ERROR_KEYS.INVALID_HMAC_LENGTH, "HMAC too short for computed offset");
|
|
30
|
+
const code = ((hmacArray[offset] & 127) << 24 | (hmacArray[offset + 1] & 255) << 16 | (hmacArray[offset + 2] & 255) << 8 | hmacArray[offset + 3] & 255) % modulo;
|
|
17
31
|
return code.toString().padStart(digits, "0");
|
|
18
32
|
};
|
|
19
33
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// source/enums/totp-error-keys.ts
|
|
3
|
+
var TOTP_ERROR_KEYS = {
|
|
4
|
+
INVALID_ALGORITHM: "nowarajs.totp.error.invalid_algorithm",
|
|
5
|
+
INVALID_BASE32_CHARACTER: "nowarajs.totp.error.invalid_base32_character",
|
|
6
|
+
INVALID_DIGITS: "nowarajs.totp.error.invalid_digits",
|
|
7
|
+
INVALID_HMAC_LENGTH: "nowarajs.totp.error.invalid_hmac_length",
|
|
8
|
+
INVALID_OTP_AUTH_URI: "nowarajs.totp.error.invalid_otp_auth_uri",
|
|
9
|
+
INVALID_PERIOD: "nowarajs.totp.error.invalid_period",
|
|
10
|
+
INVALID_SECRET_LENGTH: "nowarajs.totp.error.invalid_secret_length",
|
|
11
|
+
INVALID_WINDOW: "nowarajs.totp.error.invalid_window",
|
|
12
|
+
MISSING_LABEL: "nowarajs.totp.error.missing_label",
|
|
13
|
+
MISSING_SECRET: "nowarajs.totp.error.missing_secret",
|
|
14
|
+
WEAK_SECRET: "nowarajs.totp.error.weak_secret"
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export { TOTP_ERROR_KEYS };
|
package/dist/enums/index.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
export declare const TOTP_ERROR_KEYS: {
|
|
2
2
|
readonly INVALID_ALGORITHM: "nowarajs.totp.error.invalid_algorithm";
|
|
3
3
|
readonly INVALID_BASE32_CHARACTER: "nowarajs.totp.error.invalid_base32_character";
|
|
4
|
+
readonly INVALID_DIGITS: "nowarajs.totp.error.invalid_digits";
|
|
5
|
+
readonly INVALID_HMAC_LENGTH: "nowarajs.totp.error.invalid_hmac_length";
|
|
4
6
|
readonly INVALID_OTP_AUTH_URI: "nowarajs.totp.error.invalid_otp_auth_uri";
|
|
7
|
+
readonly INVALID_PERIOD: "nowarajs.totp.error.invalid_period";
|
|
5
8
|
readonly INVALID_SECRET_LENGTH: "nowarajs.totp.error.invalid_secret_length";
|
|
9
|
+
readonly INVALID_WINDOW: "nowarajs.totp.error.invalid_window";
|
|
10
|
+
readonly MISSING_LABEL: "nowarajs.totp.error.missing_label";
|
|
6
11
|
readonly MISSING_SECRET: "nowarajs.totp.error.missing_secret";
|
|
12
|
+
readonly WEAK_SECRET: "nowarajs.totp.error.weak_secret";
|
|
7
13
|
};
|
package/dist/hotp.d.ts
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import type { TotpOptions } from './types/totp-options';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Clear the CryptoKey cache
|
|
4
4
|
*
|
|
5
|
-
* @
|
|
5
|
+
* @remarks
|
|
6
|
+
* Useful for testing or when secrets should be purged from memory.
|
|
7
|
+
*/
|
|
8
|
+
export declare const clearKeyCache: () => void;
|
|
9
|
+
/**
|
|
10
|
+
* HMAC-based One-Time Password (HOTP) implementation per RFC 4226
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* Security: Validates minimum secret length (128 bits per RFC 4226).
|
|
14
|
+
* Performance: Caches CryptoKey objects for repeated calls.
|
|
15
|
+
*
|
|
16
|
+
* @param secret - Secret key as bytes (minimum 16 bytes)
|
|
6
17
|
* @param counter - Counter value
|
|
7
18
|
* @param opts - HOTP options
|
|
8
19
|
*
|
|
20
|
+
* @throws ({@link InternalError}) - if secret is too short
|
|
21
|
+
*
|
|
9
22
|
* @returns Promise resolving to the HOTP code
|
|
10
23
|
*/
|
|
11
24
|
export declare const hotp: (secret: Uint8Array, counter: number | bigint, { algorithm, digits }?: TotpOptions) => Promise<string>;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -3,24 +3,51 @@ import {
|
|
|
3
3
|
createCounterBuffer,
|
|
4
4
|
dynamicTruncation,
|
|
5
5
|
generateHmac
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-t1hx3d3e.js";
|
|
7
7
|
import {
|
|
8
8
|
TOTP_ERROR_KEYS
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-zfdfmrcn.js";
|
|
10
10
|
|
|
11
11
|
// source/hotp.ts
|
|
12
12
|
import { webcrypto } from "crypto";
|
|
13
|
+
import { InternalError } from "@nowarajs/error";
|
|
14
|
+
var _keyCache = new Map;
|
|
15
|
+
var _createCacheKey = (secret, algorithm) => {
|
|
16
|
+
const prefix = secret.slice(0, 8);
|
|
17
|
+
const suffix = secret.slice(-8);
|
|
18
|
+
const fingerprint = [...prefix, ...suffix, secret.length].join(",");
|
|
19
|
+
return `${fingerprint}:${algorithm}`;
|
|
20
|
+
};
|
|
21
|
+
var _getCryptoKey = async (secret, algorithm) => {
|
|
22
|
+
const cacheKey = _createCacheKey(secret, algorithm);
|
|
23
|
+
const cached = _keyCache.get(cacheKey);
|
|
24
|
+
if (cached)
|
|
25
|
+
return cached;
|
|
26
|
+
if (_keyCache.size >= 100) {
|
|
27
|
+
const firstKey = _keyCache.keys().next().value;
|
|
28
|
+
if (firstKey)
|
|
29
|
+
_keyCache.delete(firstKey);
|
|
30
|
+
}
|
|
31
|
+
const key = await webcrypto.subtle.importKey("raw", secret, { name: "HMAC", hash: algorithm }, false, ["sign"]);
|
|
32
|
+
_keyCache.set(cacheKey, key);
|
|
33
|
+
return key;
|
|
34
|
+
};
|
|
35
|
+
var clearKeyCache = () => {
|
|
36
|
+
_keyCache.clear();
|
|
37
|
+
};
|
|
13
38
|
var hotp = async (secret, counter, {
|
|
14
39
|
algorithm = "SHA-1",
|
|
15
40
|
digits = 6
|
|
16
41
|
} = {}) => {
|
|
42
|
+
if (secret.length < 16)
|
|
43
|
+
throw new InternalError(TOTP_ERROR_KEYS.WEAK_SECRET, "Secret must be at least 16 bytes (128 bits)");
|
|
17
44
|
const counterBuffer = createCounterBuffer(counter);
|
|
18
|
-
const key = await
|
|
45
|
+
const key = await _getCryptoKey(secret, algorithm);
|
|
19
46
|
const hmacArray = await generateHmac(key, counterBuffer);
|
|
20
47
|
return dynamicTruncation(hmacArray, digits);
|
|
21
48
|
};
|
|
22
49
|
// source/otp-auth-uri.ts
|
|
23
|
-
import { InternalError } from "@nowarajs/error";
|
|
50
|
+
import { InternalError as InternalError2 } from "@nowarajs/error";
|
|
24
51
|
var buildOtpAuthUri = ({
|
|
25
52
|
secretBase32,
|
|
26
53
|
label,
|
|
@@ -45,18 +72,28 @@ var buildOtpAuthUri = ({
|
|
|
45
72
|
var parseOtpAuthUri = (uri) => {
|
|
46
73
|
const url = new URL(uri);
|
|
47
74
|
if (url.protocol !== "otpauth:")
|
|
48
|
-
throw new
|
|
75
|
+
throw new InternalError2(TOTP_ERROR_KEYS.INVALID_OTP_AUTH_URI, "Invalid protocol, expected otpauth:");
|
|
49
76
|
if (url.hostname !== "totp")
|
|
50
|
-
throw new
|
|
77
|
+
throw new InternalError2(TOTP_ERROR_KEYS.INVALID_OTP_AUTH_URI, "Invalid type, expected totp");
|
|
51
78
|
const label = decodeURIComponent(url.pathname.slice(1));
|
|
79
|
+
if (!label)
|
|
80
|
+
throw new InternalError2(TOTP_ERROR_KEYS.MISSING_LABEL, "Label is required");
|
|
52
81
|
const secretBase32 = url.searchParams.get("secret");
|
|
53
82
|
if (!secretBase32)
|
|
54
|
-
throw new
|
|
83
|
+
throw new InternalError2(TOTP_ERROR_KEYS.MISSING_SECRET, "Secret is required");
|
|
55
84
|
const issuerParam = url.searchParams.get("issuer");
|
|
56
85
|
const issuer = issuerParam ? decodeURIComponent(issuerParam) : undefined;
|
|
57
|
-
const
|
|
58
|
-
|
|
86
|
+
const algorithmParam = url.searchParams.get("algorithm") || "SHA-1";
|
|
87
|
+
if (algorithmParam !== "SHA-1" && algorithmParam !== "SHA-256" && algorithmParam !== "SHA-512")
|
|
88
|
+
throw new InternalError2(TOTP_ERROR_KEYS.INVALID_ALGORITHM, "Algorithm must be SHA-1, SHA-256, or SHA-512");
|
|
89
|
+
const algorithm = algorithmParam;
|
|
90
|
+
const digitsParam = parseInt(url.searchParams.get("digits") || "6", 10);
|
|
91
|
+
if (digitsParam !== 6 && digitsParam !== 8)
|
|
92
|
+
throw new InternalError2(TOTP_ERROR_KEYS.INVALID_DIGITS, "Digits must be 6 or 8");
|
|
93
|
+
const digits = digitsParam;
|
|
59
94
|
const period = parseInt(url.searchParams.get("period") || "30", 10);
|
|
95
|
+
if (!Number.isFinite(period) || period <= 0)
|
|
96
|
+
throw new InternalError2(TOTP_ERROR_KEYS.INVALID_PERIOD, "Period must be a positive integer");
|
|
60
97
|
const result = {
|
|
61
98
|
secretBase32,
|
|
62
99
|
label,
|
|
@@ -68,6 +105,8 @@ var parseOtpAuthUri = (uri) => {
|
|
|
68
105
|
return result;
|
|
69
106
|
};
|
|
70
107
|
// source/totp.ts
|
|
108
|
+
import { timingSafeEqual } from "crypto";
|
|
109
|
+
import { InternalError as InternalError3 } from "@nowarajs/error";
|
|
71
110
|
var totp = async (secret, {
|
|
72
111
|
algorithm = "SHA-1",
|
|
73
112
|
digits = 6,
|
|
@@ -77,6 +116,13 @@ var totp = async (secret, {
|
|
|
77
116
|
const timeStep = Math.floor(now / 1000 / period);
|
|
78
117
|
return hotp(secret, timeStep, { algorithm, digits });
|
|
79
118
|
};
|
|
119
|
+
var _timingSafeCompare = (a, b) => {
|
|
120
|
+
if (a.length !== b.length)
|
|
121
|
+
return false;
|
|
122
|
+
const bufferA = Buffer.from(a);
|
|
123
|
+
const bufferB = Buffer.from(b);
|
|
124
|
+
return timingSafeEqual(bufferA, bufferB);
|
|
125
|
+
};
|
|
80
126
|
var verifyTotp = async (secret, code, {
|
|
81
127
|
algorithm = "SHA-1",
|
|
82
128
|
digits = 6,
|
|
@@ -84,19 +130,30 @@ var verifyTotp = async (secret, code, {
|
|
|
84
130
|
window = 0,
|
|
85
131
|
now = Date.now()
|
|
86
132
|
} = {}) => {
|
|
133
|
+
if (window < 0 || window > 10)
|
|
134
|
+
throw new InternalError3(TOTP_ERROR_KEYS.INVALID_WINDOW, "Window must be between 0 and 10");
|
|
135
|
+
if (!/^\d+$/.test(code) || code.length !== digits)
|
|
136
|
+
return false;
|
|
87
137
|
const currentTimeStep = Math.floor(now / 1000 / period);
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
if (expectedCode === code)
|
|
92
|
-
return true;
|
|
138
|
+
if (window === 0) {
|
|
139
|
+
const expectedCode = await hotp(secret, currentTimeStep, { algorithm, digits });
|
|
140
|
+
return _timingSafeCompare(expectedCode, code);
|
|
93
141
|
}
|
|
94
|
-
|
|
142
|
+
const timeSteps = [];
|
|
143
|
+
for (let i = -window;i <= window; ++i)
|
|
144
|
+
timeSteps.push(currentTimeStep + i);
|
|
145
|
+
const expectedCodes = await Promise.all(timeSteps.map((timeStep) => hotp(secret, timeStep, { algorithm, digits })));
|
|
146
|
+
let isValid = false;
|
|
147
|
+
for (const expectedCode of expectedCodes)
|
|
148
|
+
if (_timingSafeCompare(expectedCode, code))
|
|
149
|
+
isValid = true;
|
|
150
|
+
return isValid;
|
|
95
151
|
};
|
|
96
152
|
export {
|
|
97
153
|
verifyTotp,
|
|
98
154
|
totp,
|
|
99
155
|
parseOtpAuthUri,
|
|
100
156
|
hotp,
|
|
157
|
+
clearKeyCache,
|
|
101
158
|
buildOtpAuthUri
|
|
102
159
|
};
|
package/dist/otp-auth-uri.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ export declare const buildOtpAuthUri: ({ secretBase32, label, issuer, algorithm,
|
|
|
10
10
|
/**
|
|
11
11
|
* Parse an OTPAuth URI
|
|
12
12
|
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* Security: Validates all parameters to prevent injection or invalid configurations.
|
|
15
|
+
* - Algorithm must be SHA-1, SHA-256, or SHA-512
|
|
16
|
+
* - Digits must be 6 or 8
|
|
17
|
+
* - Period must be a positive integer
|
|
18
|
+
* - Label is required per otpauth specification
|
|
19
|
+
*
|
|
13
20
|
* @param uri - OTPAuth URI to parse
|
|
14
21
|
*
|
|
15
22
|
* @throws ({@link InternalError}) - if the URI is invalid or missing required parameters
|
package/dist/totp.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { TotpOptions } from './types/totp-options';
|
|
2
2
|
import type { VerifyOptions } from './types/verify-options';
|
|
3
3
|
/**
|
|
4
|
-
* Time-based One-Time Password (TOTP) implementation
|
|
4
|
+
* Time-based One-Time Password (TOTP) implementation per RFC 6238
|
|
5
5
|
*
|
|
6
|
-
* @param secret - Secret key as bytes
|
|
6
|
+
* @param secret - Secret key as bytes (minimum 16 bytes)
|
|
7
7
|
* @param opts - TOTP options including current time
|
|
8
8
|
*
|
|
9
9
|
* @returns Promise resolving to the TOTP code
|
|
@@ -14,10 +14,18 @@ export declare const totp: (secret: Uint8Array, { algorithm, digits, period, now
|
|
|
14
14
|
/**
|
|
15
15
|
* Verify a TOTP code against a secret
|
|
16
16
|
*
|
|
17
|
-
* @
|
|
17
|
+
* @remarks
|
|
18
|
+
* Security: Uses constant-time comparison to prevent timing attacks (CWE-208).
|
|
19
|
+
* Security: Always iterates through all windows to prevent leaking which time step matched.
|
|
20
|
+
* Security: Validates window size to prevent DoS attacks (CWE-400).
|
|
21
|
+
* Performance: Parallelizes code generation when window > 0.
|
|
22
|
+
*
|
|
23
|
+
* @param secret - Secret key as bytes (minimum 16 bytes)
|
|
18
24
|
* @param code - Code to verify
|
|
19
25
|
* @param opts - Verification options
|
|
20
26
|
*
|
|
27
|
+
* @throws ({@link InternalError}) - if window is invalid
|
|
28
|
+
*
|
|
21
29
|
* @returns Promise resolving to true if code is valid
|
|
22
30
|
*/
|
|
23
31
|
export declare const verifyTotp: (secret: Uint8Array, code: string, { algorithm, digits, period, window, now }?: VerifyOptions) => Promise<boolean>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
export type { OtpAuthUri } from './otp-auth-uri';
|
|
2
2
|
export type { TotpOptions } from './totp-options';
|
|
3
3
|
export type { VerifyOptions } from './verify-options';
|
|
4
|
+
/**
|
|
5
|
+
* Supported hash algorithms for TOTP/HOTP
|
|
6
|
+
*/
|
|
7
|
+
export type HashAlgorithm = 'SHA-1' | 'SHA-256' | 'SHA-512';
|
|
8
|
+
/**
|
|
9
|
+
* Valid digit counts for OTP codes
|
|
10
|
+
*/
|
|
11
|
+
export type OtpDigits = 6 | 8;
|
package/dist/utils/base32.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Encode bytes to Base32 string
|
|
3
3
|
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Performance: Uses array.join() instead of string concatenation
|
|
6
|
+
* to avoid creating intermediate strings in the hot path.
|
|
7
|
+
*
|
|
4
8
|
* @param input - Bytes or string to encode
|
|
5
9
|
* @param withPadding - Whether to include padding (default: true)
|
|
6
10
|
*
|
|
@@ -10,7 +14,12 @@ export declare const base32Encode: (input: string | Uint8Array, withPadding?: bo
|
|
|
10
14
|
/**
|
|
11
15
|
* Decode Base32 string to bytes
|
|
12
16
|
*
|
|
13
|
-
* @
|
|
17
|
+
* @remarks
|
|
18
|
+
* Security: Case-insensitive decoding per RFC 4648 recommendation.
|
|
19
|
+
* Performance: Uses Map lookup for O(1) character value retrieval.
|
|
20
|
+
* Performance: Pre-allocates output array based on input length.
|
|
21
|
+
*
|
|
22
|
+
* @param base32 - Base32 string to decode (case-insensitive)
|
|
14
23
|
*
|
|
15
24
|
* @throws ({@link InternalError}) - if invalid Base32 character is found
|
|
16
25
|
*
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Convert a counter value to an 8-byte big-endian buffer
|
|
3
3
|
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Always uses BigInt internally to support full 64-bit counter values.
|
|
6
|
+
* This fixes potential integer overflow issues when counter exceeds 2^32-1.
|
|
7
|
+
*
|
|
8
|
+
* For TOTP, time steps are calculated as Math.floor(Date.now() / 1000 / period).
|
|
9
|
+
* Current time steps are ~57 million, well within 32-bit range, but this
|
|
10
|
+
* ensures correctness for far-future dates and high-precision use cases.
|
|
11
|
+
*
|
|
4
12
|
* @param counter - Counter value as number or bigint
|
|
5
13
|
*
|
|
6
|
-
* @returns ArrayBuffer containing the counter in big-endian format
|
|
14
|
+
* @returns ArrayBuffer containing the counter in big-endian format (8 bytes)
|
|
7
15
|
*/
|
|
8
16
|
export declare const createCounterBuffer: (counter: number | bigint) => ArrayBuffer;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import type { OtpDigits } from '../types';
|
|
1
2
|
/**
|
|
2
3
|
* Perform dynamic truncation on HMAC result according to RFC 4226
|
|
3
4
|
*
|
|
4
|
-
* @param hmacArray - HMAC result as byte array
|
|
5
|
-
* @param digits - Number of digits in the final code
|
|
5
|
+
* @param hmacArray - HMAC result as byte array (minimum 20 bytes)
|
|
6
|
+
* @param digits - Number of digits in the final code (6 or 8)
|
|
7
|
+
*
|
|
8
|
+
* @throws ({@link InternalError}) - if HMAC array is too short or digits is invalid
|
|
6
9
|
*
|
|
7
10
|
* @returns Truncated code as string with leading zeros
|
|
8
11
|
*/
|
|
9
|
-
export declare const dynamicTruncation: (hmacArray: Uint8Array, digits:
|
|
12
|
+
export declare const dynamicTruncation: (hmacArray: Uint8Array, digits: OtpDigits) => string;
|
package/dist/utils/index.js
CHANGED
|
@@ -3,53 +3,63 @@ import {
|
|
|
3
3
|
createCounterBuffer,
|
|
4
4
|
dynamicTruncation,
|
|
5
5
|
generateHmac
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-t1hx3d3e.js";
|
|
7
7
|
import {
|
|
8
8
|
TOTP_ERROR_KEYS
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-zfdfmrcn.js";
|
|
10
10
|
|
|
11
11
|
// source/utils/base32.ts
|
|
12
12
|
import { InternalError } from "@nowarajs/error";
|
|
13
13
|
var BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
14
|
+
var _BASE32_LOOKUP = new Map(Array.from(BASE32_ALPHABET, (char, index) => [char, index]));
|
|
14
15
|
var base32Encode = (input, withPadding = true) => {
|
|
15
|
-
|
|
16
|
+
const bytes = input instanceof Uint8Array ? input : new TextEncoder().encode(input);
|
|
17
|
+
if (bytes.length === 0)
|
|
18
|
+
return "";
|
|
19
|
+
const estimatedLength = Math.ceil(bytes.length * 8 / 5);
|
|
20
|
+
const chars = new Array(estimatedLength);
|
|
16
21
|
let bits = 0;
|
|
17
22
|
let value = 0;
|
|
18
|
-
|
|
19
|
-
for (
|
|
20
|
-
value = value << 8 |
|
|
23
|
+
let charIndex = 0;
|
|
24
|
+
for (let i = 0;i < bytes.length; ++i) {
|
|
25
|
+
value = value << 8 | bytes[i];
|
|
21
26
|
bits += 8;
|
|
22
27
|
while (bits >= 5) {
|
|
23
|
-
|
|
28
|
+
chars[charIndex++] = BASE32_ALPHABET[value >>> bits - 5 & 31];
|
|
24
29
|
bits -= 5;
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
if (bits > 0)
|
|
28
|
-
|
|
33
|
+
chars[charIndex++] = BASE32_ALPHABET[value << 5 - bits & 31];
|
|
34
|
+
let result = chars.slice(0, charIndex).join("");
|
|
29
35
|
if (withPadding)
|
|
30
36
|
while (result.length % 8 !== 0)
|
|
31
37
|
result += "=";
|
|
32
38
|
return result;
|
|
33
39
|
};
|
|
34
40
|
var base32Decode = (base32) => {
|
|
35
|
-
const cleanBase32 = base32.replace(/=+$/, "");
|
|
36
|
-
|
|
41
|
+
const cleanBase32 = base32.toUpperCase().replace(/=+$/, "");
|
|
42
|
+
const inputLength = cleanBase32.length;
|
|
43
|
+
if (inputLength === 0)
|
|
37
44
|
return new Uint8Array(0);
|
|
38
|
-
const
|
|
45
|
+
const outputLength = Math.floor(inputLength * 5 / 8);
|
|
46
|
+
const result = new Uint8Array(outputLength);
|
|
39
47
|
let bits = 0;
|
|
40
48
|
let value = 0;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
let outputIndex = 0;
|
|
50
|
+
for (let i = 0;i < inputLength; ++i) {
|
|
51
|
+
const char = cleanBase32[i];
|
|
52
|
+
const charValue = _BASE32_LOOKUP.get(char);
|
|
53
|
+
if (charValue === undefined)
|
|
54
|
+
throw new InternalError(TOTP_ERROR_KEYS.INVALID_BASE32_CHARACTER, "Invalid Base32 character");
|
|
45
55
|
value = value << 5 | charValue;
|
|
46
56
|
bits += 5;
|
|
47
57
|
if (bits >= 8) {
|
|
48
|
-
result
|
|
58
|
+
result[outputIndex++] = value >>> bits - 8 & 255;
|
|
49
59
|
bits -= 8;
|
|
50
60
|
}
|
|
51
61
|
}
|
|
52
|
-
return
|
|
62
|
+
return result;
|
|
53
63
|
};
|
|
54
64
|
// source/utils/generate-secret-bytes.ts
|
|
55
65
|
import { InternalError as InternalError2 } from "@nowarajs/error";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowarajs/totp",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"author": "NowaraJS",
|
|
5
5
|
"description": "A comprehensive Time-based One-Time Password (TOTP) and HMAC-based One-Time Password (HOTP)",
|
|
6
6
|
"type": "module",
|
|
@@ -22,17 +22,17 @@
|
|
|
22
22
|
"test": "bun test --coverage"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@eslint/js": "^9.39.
|
|
26
|
-
"@nowarajs/error": "^1.
|
|
27
|
-
"@stylistic/eslint-plugin": "^5.
|
|
28
|
-
"@types/bun": "^1.3.
|
|
29
|
-
"eslint": "^9.39.
|
|
30
|
-
"globals": "^
|
|
31
|
-
"typescript-eslint": "^8.
|
|
25
|
+
"@eslint/js": "^9.39.2",
|
|
26
|
+
"@nowarajs/error": "^1.4.0",
|
|
27
|
+
"@stylistic/eslint-plugin": "^5.7.0",
|
|
28
|
+
"@types/bun": "^1.3.5",
|
|
29
|
+
"eslint": "^9.39.2",
|
|
30
|
+
"globals": "^17.0.0",
|
|
31
|
+
"typescript-eslint": "^8.52.0",
|
|
32
32
|
"typescript": "^5.9.3"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@nowarajs/error": "^1.
|
|
35
|
+
"@nowarajs/error": "^1.4.0"
|
|
36
36
|
},
|
|
37
37
|
"exports": {
|
|
38
38
|
"./enums": "./dist/enums/index.js",
|
package/dist/chunk-sx6rwmqe.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
// source/enums/totp-error-keys.ts
|
|
3
|
-
var TOTP_ERROR_KEYS = {
|
|
4
|
-
INVALID_ALGORITHM: "nowarajs.totp.error.invalid_algorithm",
|
|
5
|
-
INVALID_BASE32_CHARACTER: "nowarajs.totp.error.invalid_base32_character",
|
|
6
|
-
INVALID_OTP_AUTH_URI: "nowarajs.totp.error.invalid_otp_auth_uri",
|
|
7
|
-
INVALID_SECRET_LENGTH: "nowarajs.totp.error.invalid_secret_length",
|
|
8
|
-
MISSING_SECRET: "nowarajs.totp.error.missing_secret"
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export { TOTP_ERROR_KEYS };
|