@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 CHANGED
@@ -1,41 +1,32 @@
1
- # πŸ” NowaraJS - TOTP
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
- - [πŸ” NowaraJS - TOTP](#-nowarajs---totp)
6
- - [πŸ“Œ Table of Contents](#-table-of-contents)
7
- - [πŸ“ Description](#-description)
8
- - [✨ Features](#-features)
9
- - [πŸ”§ Installation](#-installation)
10
- - [βš™οΈ Usage](#-usage)
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**: Full RFC-compliant Time-based One-Time Password implementation
31
- - πŸ”‘ **RFC 4226 HOTP**: Complete HMAC-based One-Time Password support
32
- - πŸ“± **QR Code Support**: Generate OTPAuth URIs for easy mobile app integration
33
- - πŸ”’ **Crypto Secure**: Uses Web Crypto API for secure random number generation
34
- - ⚑ **High Performance**: Optimized for speed with minimal dependencies
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
- ### Basic TOTP Generation
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); // 20 bytes = 160 bits
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
- algorithm: 'SHA-1',
60
- digits: 6,
61
- period: 30
51
+ algorithm: 'SHA-1',
52
+ digits: 6,
53
+ period: 30
62
54
  });
63
55
 
64
- console.log('TOTP Code:', code); // e.g., "123456"
56
+ console.log('Your code:', code); // e.g., "847263"
65
57
  ```
66
58
 
67
- ### TOTP Verification
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
- algorithm: 'SHA-1',
75
- digits: 6,
76
- period: 30,
77
- window: 1 // Allow 1 time step before/after current time
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
- console.log('βœ… Code is valid!');
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
- ### HOTP Support
77
+ ### Generate a QR Code URI
88
78
 
89
- ```ts
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
- secretBase32,
113
- label: 'user@example.com',
114
- issuer: 'MyApp',
115
- algorithm: 'SHA-1',
116
- digits: 6,
117
- period: 30
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
- console.log('QR Code URI:', uri);
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
- ### Secret Generation
101
+ ### HOTP (Counter-Based)
125
102
 
126
- ```ts
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
- // Use SHA-256 with 8 digits and 60-second period
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
- // Verify with larger time window for clock drift tolerance
154
- const isValid = await verifyTotp(secret, userCode, {
155
- algorithm: 'SHA-256',
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
- You can find the complete API reference documentation for `NowaraJS TOTP` at:
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
- Distributed under the MIT License. See [LICENSE](./LICENSE) for more information.
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
- if (typeof counter === "bigint")
7
- counterView.setBigUint64(0, counter, false);
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
- const code = ((hmacArray[offset] & 127) << 24 | (hmacArray[offset + 1] & 255) << 16 | (hmacArray[offset + 2] & 255) << 8 | hmacArray[offset + 3] & 255) % 10 ** digits;
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 };
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  TOTP_ERROR_KEYS
4
- } from "../chunk-sx6rwmqe.js";
4
+ } from "../chunk-zfdfmrcn.js";
5
5
  export {
6
6
  TOTP_ERROR_KEYS
7
7
  };
@@ -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
- * HMAC-based One-Time Password (HOTP) implementation
3
+ * Clear the CryptoKey cache
4
4
  *
5
- * @param secret - Secret key as bytes
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
@@ -1,3 +1,3 @@
1
- export { hotp } from './hotp';
1
+ export { clearKeyCache, hotp } from './hotp';
2
2
  export { buildOtpAuthUri, parseOtpAuthUri } from './otp-auth-uri';
3
3
  export { totp, verifyTotp } from './totp';
package/dist/index.js CHANGED
@@ -3,24 +3,51 @@ import {
3
3
  createCounterBuffer,
4
4
  dynamicTruncation,
5
5
  generateHmac
6
- } from "./chunk-q8z45f9z.js";
6
+ } from "./chunk-t1hx3d3e.js";
7
7
  import {
8
8
  TOTP_ERROR_KEYS
9
- } from "./chunk-sx6rwmqe.js";
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 webcrypto.subtle.importKey("raw", secret, { name: "HMAC", hash: algorithm }, false, ["sign"]);
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 InternalError(TOTP_ERROR_KEYS.INVALID_OTP_AUTH_URI);
75
+ throw new InternalError2(TOTP_ERROR_KEYS.INVALID_OTP_AUTH_URI, "Invalid protocol, expected otpauth:");
49
76
  if (url.hostname !== "totp")
50
- throw new InternalError(TOTP_ERROR_KEYS.INVALID_OTP_AUTH_URI);
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 InternalError(TOTP_ERROR_KEYS.MISSING_SECRET);
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 algorithm = url.searchParams.get("algorithm") || "SHA-1";
58
- const digits = parseInt(url.searchParams.get("digits") || "6", 10);
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
- for (let i = -window;i <= window; ++i) {
89
- const timeStep = currentTimeStep + i;
90
- const expectedCode = await hotp(secret, timeStep, { algorithm, digits });
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
- return false;
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
  };
@@ -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
- * @param secret - Secret key as bytes
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>;
@@ -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;
@@ -13,7 +13,7 @@ export interface OtpAuthUri {
13
13
  /**
14
14
  * Issuer name (app/service name)
15
15
  */
16
- issuer: string;
16
+ issuer?: string;
17
17
  /**
18
18
  * Hash algorithm
19
19
  *
@@ -6,7 +6,7 @@ export interface VerifyOptions extends TotpOptions {
6
6
  /**
7
7
  * Time window for verification (Β±window periods)
8
8
  *
9
- * @defaultValue 1
9
+ * @defaultValue 0
10
10
  */
11
11
  window?: number;
12
12
  /**
@@ -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
- * @param base32 - Base32 string to decode
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: number) => string;
12
+ export declare const dynamicTruncation: (hmacArray: Uint8Array, digits: OtpDigits) => string;
@@ -3,53 +3,63 @@ import {
3
3
  createCounterBuffer,
4
4
  dynamicTruncation,
5
5
  generateHmac
6
- } from "../chunk-q8z45f9z.js";
6
+ } from "../chunk-t1hx3d3e.js";
7
7
  import {
8
8
  TOTP_ERROR_KEYS
9
- } from "../chunk-sx6rwmqe.js";
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
- let result = "";
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
- const bytes = input instanceof Uint8Array ? input : new TextEncoder().encode(input);
19
- for (const byte of bytes) {
20
- value = value << 8 | byte;
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
- result += BASE32_ALPHABET[value >>> bits - 5 & 31];
28
+ chars[charIndex++] = BASE32_ALPHABET[value >>> bits - 5 & 31];
24
29
  bits -= 5;
25
30
  }
26
31
  }
27
32
  if (bits > 0)
28
- result += BASE32_ALPHABET[value << 5 - bits & 31];
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
- if (cleanBase32.length === 0)
41
+ const cleanBase32 = base32.toUpperCase().replace(/=+$/, "");
42
+ const inputLength = cleanBase32.length;
43
+ if (inputLength === 0)
37
44
  return new Uint8Array(0);
38
- const result = [];
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
- for (const char of cleanBase32) {
42
- const charValue = BASE32_ALPHABET.indexOf(char);
43
- if (charValue === -1)
44
- throw new InternalError(TOTP_ERROR_KEYS.INVALID_BASE32_CHARACTER, `Invalid Base32 character: ${char}`);
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.push(value >>> bits - 8 & 255);
58
+ result[outputIndex++] = value >>> bits - 8 & 255;
49
59
  bits -= 8;
50
60
  }
51
61
  }
52
- return new Uint8Array(result);
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.0",
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.1",
26
- "@nowarajs/error": "^1.3.10",
27
- "@stylistic/eslint-plugin": "^5.5.0",
28
- "@types/bun": "^1.3.2",
29
- "eslint": "^9.39.1",
30
- "globals": "^16.5.0",
31
- "typescript-eslint": "^8.46.4",
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.3.10"
35
+ "@nowarajs/error": "^1.4.0"
36
36
  },
37
37
  "exports": {
38
38
  "./enums": "./dist/enums/index.js",
@@ -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 };