@punikonta/node-otp 0.0.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.md +9 -0
- package/README.md +182 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +143 -0
- package/dist/index.js.map +1 -0
- package/index.ts +169 -0
- package/package.json +48 -0
- package/test.mjs +66 -0
- package/tsconfig.json +19 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Marcel Jovic <punikonta@protonmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Dependency free OTP implementation
|
|
2
|
+
|
|
3
|
+
Simple and dependency free OTP implementation (HOTP, TOTP) for Node.js written in TypeScript. Passes RFC test vectors and works with common authenticator apps.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- HOTP
|
|
8
|
+
- TOTP (builds on top of HOTP)
|
|
9
|
+
- `otpauth://` URL generation
|
|
10
|
+
|
|
11
|
+
Generating QR codes for `otpauth://` URLs is **not** within the scope of this library. There are [plenty of good QR code libraries](https://www.npmjs.com/search?q=qr) out there. Just run your URL through one of them and you're good to go. There's an example QR code that works in conjunction with the example code below if you want to test it with your own authenticator app, though.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @punikonta/node-otp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Example
|
|
20
|
+
|
|
21
|
+
Example usage for TOTP in plain JavaScript:
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
import Otp from '@punikonta/node-otp'
|
|
25
|
+
|
|
26
|
+
// example 128 bit secret for demonstration only! don't use this in your application.
|
|
27
|
+
// this must be random, stored securely and be unique for each user.
|
|
28
|
+
const secret = Buffer.from('8eddb53e05fa936c2530c8045a58f81b', 'hex')
|
|
29
|
+
|
|
30
|
+
// options are optional. these are the defaults and common practice.
|
|
31
|
+
// just omit the options parameter if you're fine with the defaults.
|
|
32
|
+
const options = {
|
|
33
|
+
algorithm: Otp.HashAlgorithm.SHA1,
|
|
34
|
+
digits: 6,
|
|
35
|
+
period: 30,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const now = Date.now()
|
|
39
|
+
|
|
40
|
+
const token = Otp.Totp.generate(secret, now, options)
|
|
41
|
+
const remaining = Otp.Totp.remaining(options.period, now)
|
|
42
|
+
const url = Otp.Url.getTotpUrl('example.com', 'foobar', secret, options)
|
|
43
|
+
|
|
44
|
+
// default value. omit if you're fine with the default.
|
|
45
|
+
const window = 1
|
|
46
|
+
const valid = Otp.Totp.validate('123456', secret, now, window, options)
|
|
47
|
+
|
|
48
|
+
console.log(`token: ${token}`)
|
|
49
|
+
console.log(`valid for ${remaining.toFixed(2)} seconds`)
|
|
50
|
+
console.log(url)
|
|
51
|
+
console.log(`is 123456 valid? ${valid ? 'yes' : 'no'}`)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
If you want to verify the example right away with an authenticator app, you can use the following QR code without having to generate one yourself:
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
`otpauth://totp/example.com:foobar?secret=R3O3KPQF7KJWYJJQZACFUWHYDM&algorithm=SHA1&digits=6&period=30&issuer=example.com`
|
|
59
|
+
|
|
60
|
+
## Motivation
|
|
61
|
+
|
|
62
|
+
I wanted a project with a small scope to get my hands dirty with TypeScript and NPM packaging. Also I've been looking for a TOTP library at the same time. I couldn't find one that I liked, so I decided to create my own. TOTP builds on top of HOTP, so I implemented that as well.
|
|
63
|
+
|
|
64
|
+
## Remarks
|
|
65
|
+
|
|
66
|
+
I've successfully tested the TOTP functionality with the following authenticator apps:
|
|
67
|
+
|
|
68
|
+
- Authy
|
|
69
|
+
- Google Authenticator
|
|
70
|
+
- Microsoft Authenticator
|
|
71
|
+
- Proton Authenticator
|
|
72
|
+
- Bitwarden Authenticator
|
|
73
|
+
|
|
74
|
+
Double check if you want to use anything but `SHA1` as your hash algorithm. Some popular authenticator apps don't support anything else and even go as far as completely ignoring that parameter, silently giving the user wrong tokens. To catch this and other issues early, your onboarding flow should implement a successful token readback while setting up 2FA anyway.
|
|
75
|
+
|
|
76
|
+
Also don't use a `window` value other than `0` for HOTP unless you know what you're doing. Managing the HOTP counter is up to you.
|
|
77
|
+
|
|
78
|
+
The default `window` value of `1` for TOTP generally is fine and common practice to mitigate small clock drifts between the server and the user.
|
|
79
|
+
|
|
80
|
+
## API
|
|
81
|
+
|
|
82
|
+
The API uses the following types and enums:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
enum HashAlgorithm {
|
|
86
|
+
SHA1 = 'sha1',
|
|
87
|
+
SHA256 = 'sha256',
|
|
88
|
+
SHA384 = 'sha384',
|
|
89
|
+
SHA512 = 'sha512',
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type HotpOptions = {
|
|
93
|
+
algorithm: HashAlgorithm
|
|
94
|
+
digits: number
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type TotpOptions = {
|
|
98
|
+
algorithm: HashAlgorithm
|
|
99
|
+
digits: number
|
|
100
|
+
period: number
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### HOTP
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
Hotp.generate(
|
|
108
|
+
secret: Buffer,
|
|
109
|
+
counter: number,
|
|
110
|
+
options?: Partial<HotpOptions>
|
|
111
|
+
): string
|
|
112
|
+
|
|
113
|
+
Hotp.validate(
|
|
114
|
+
token: string,
|
|
115
|
+
secret: Buffer,
|
|
116
|
+
counter: number,
|
|
117
|
+
window: number = 0,
|
|
118
|
+
options?: Partial<HotpOptions>
|
|
119
|
+
): boolean
|
|
120
|
+
|
|
121
|
+
// default values for option parameters that are omitted
|
|
122
|
+
static readonly DEFAULTS: HotpOptions = {
|
|
123
|
+
algorithm: HashAlgorithm.SHA1,
|
|
124
|
+
digits: 6,
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### TOTP
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
Totp.generate(
|
|
132
|
+
secret: Buffer,
|
|
133
|
+
time: number = Date.now(),
|
|
134
|
+
options?: Partial<TotpOptions>
|
|
135
|
+
): string
|
|
136
|
+
|
|
137
|
+
Totp.validate(
|
|
138
|
+
token: string,
|
|
139
|
+
secret: Buffer,
|
|
140
|
+
time: number = Date.now(),
|
|
141
|
+
window: number = 1,
|
|
142
|
+
options?: Partial<TotpOptions>
|
|
143
|
+
): boolean
|
|
144
|
+
|
|
145
|
+
// remaining time until next token (seconds, fractional)
|
|
146
|
+
Totp.remaining(
|
|
147
|
+
period: number = 30,
|
|
148
|
+
time: number = Date.now()
|
|
149
|
+
): number
|
|
150
|
+
|
|
151
|
+
// default values for option parameters that are omitted
|
|
152
|
+
static readonly DEFAULTS: TotpOptions = {
|
|
153
|
+
algorithm: HashAlgorithm.SHA1,
|
|
154
|
+
digits: 6,
|
|
155
|
+
period: 30,
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### URL
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// HOTP
|
|
163
|
+
Url.getHotpUrl(
|
|
164
|
+
issuer: string, // usually a domain or application name
|
|
165
|
+
label: string, // usually the username or email of the user
|
|
166
|
+
secret: Buffer,
|
|
167
|
+
counter: number,
|
|
168
|
+
options?: Partial<HotpOptions>
|
|
169
|
+
): string
|
|
170
|
+
|
|
171
|
+
// TOTP
|
|
172
|
+
Url.getTotpUrl(
|
|
173
|
+
issuer: string, // usually a domain or application name
|
|
174
|
+
label: string, // usually the username or email of the user
|
|
175
|
+
secret: Buffer,
|
|
176
|
+
options?: Partial<TotpOptions>
|
|
177
|
+
): string
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## TODO
|
|
181
|
+
|
|
182
|
+
Probably a good idea to add some sanity checks in the future, e.g. for nonsensical stuff like negative periods, windows or missing mandatory parameters.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
declare namespace Otp {
|
|
3
|
+
const enum HashAlgorithm {
|
|
4
|
+
SHA1 = "sha1",
|
|
5
|
+
SHA256 = "sha256",
|
|
6
|
+
SHA384 = "sha384",
|
|
7
|
+
SHA512 = "sha512"
|
|
8
|
+
}
|
|
9
|
+
type HotpOptions = {
|
|
10
|
+
algorithm: HashAlgorithm;
|
|
11
|
+
digits: number;
|
|
12
|
+
};
|
|
13
|
+
type TotpOptions = {
|
|
14
|
+
algorithm: HashAlgorithm;
|
|
15
|
+
digits: number;
|
|
16
|
+
period: number;
|
|
17
|
+
};
|
|
18
|
+
class Hotp {
|
|
19
|
+
static readonly DEFAULTS: HotpOptions;
|
|
20
|
+
static generate(secret: Buffer, counter: number, options?: Partial<HotpOptions>): string;
|
|
21
|
+
static validate(token: string, secret: Buffer, counter: number, window?: number, options?: Partial<HotpOptions>): boolean;
|
|
22
|
+
}
|
|
23
|
+
class Totp {
|
|
24
|
+
static readonly DEFAULTS: TotpOptions;
|
|
25
|
+
static generate(secret: Buffer, time: number, options?: Partial<TotpOptions>): string;
|
|
26
|
+
static validate(token: string, secret: Buffer, time?: number, window?: number, options?: Partial<TotpOptions>): boolean;
|
|
27
|
+
static remaining(period?: number, time?: number): number;
|
|
28
|
+
static timeToCounter(time: number, period: number): number;
|
|
29
|
+
}
|
|
30
|
+
class Url {
|
|
31
|
+
static getHotpUrl(issuer: string, label: string, secret: Buffer, counter: number, options?: Partial<HotpOptions>): string;
|
|
32
|
+
static getTotpUrl(issuer: string, label: string, secret: Buffer, options?: Partial<TotpOptions>): string;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export default Otp;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
3
|
+
var Otp;
|
|
4
|
+
(function (Otp) {
|
|
5
|
+
let HashAlgorithm;
|
|
6
|
+
(function (HashAlgorithm) {
|
|
7
|
+
HashAlgorithm["SHA1"] = "sha1";
|
|
8
|
+
HashAlgorithm["SHA256"] = "sha256";
|
|
9
|
+
HashAlgorithm["SHA384"] = "sha384";
|
|
10
|
+
HashAlgorithm["SHA512"] = "sha512";
|
|
11
|
+
})(HashAlgorithm = Otp.HashAlgorithm || (Otp.HashAlgorithm = {}));
|
|
12
|
+
class Hotp {
|
|
13
|
+
static generate(secret, counter, options) {
|
|
14
|
+
const opts = Object.assign(Object.assign({}, this.DEFAULTS), options);
|
|
15
|
+
const bytes = Buffer.alloc(8);
|
|
16
|
+
bytes.writeBigUInt64BE(BigInt(counter));
|
|
17
|
+
const hmac = createHmac(opts.algorithm, secret);
|
|
18
|
+
hmac.update(bytes);
|
|
19
|
+
const hash = hmac.digest();
|
|
20
|
+
const offset = hash[hash.length - 1] & 0xf;
|
|
21
|
+
const token = ((hash[offset] & 0x7f) << 24 >>> 0)
|
|
22
|
+
| hash[offset + 1] << 16
|
|
23
|
+
| hash[offset + 2] << 8
|
|
24
|
+
| hash[offset + 3];
|
|
25
|
+
return (token % (Math.pow(10, opts.digits))).toString().padStart(opts.digits, '0');
|
|
26
|
+
}
|
|
27
|
+
static validate(token, secret, counter, window = 0, options) {
|
|
28
|
+
const opts = Object.assign(Object.assign({}, this.DEFAULTS), options);
|
|
29
|
+
if (token.length != opts.digits)
|
|
30
|
+
return false;
|
|
31
|
+
const compare = (counter) => {
|
|
32
|
+
const generated = Hotp.generate(secret, counter, opts);
|
|
33
|
+
const token_buffer = Buffer.from(token);
|
|
34
|
+
const generated_buffer = Buffer.from(generated);
|
|
35
|
+
if (token_buffer.length != generated_buffer.length)
|
|
36
|
+
return false;
|
|
37
|
+
return timingSafeEqual(token_buffer, generated_buffer);
|
|
38
|
+
};
|
|
39
|
+
if (compare(counter))
|
|
40
|
+
return true;
|
|
41
|
+
for (let i = 1; i <= window; i++) {
|
|
42
|
+
if (compare(counter + i))
|
|
43
|
+
return true;
|
|
44
|
+
if (compare(counter - i))
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
Hotp.DEFAULTS = {
|
|
51
|
+
algorithm: HashAlgorithm.SHA1,
|
|
52
|
+
digits: 6,
|
|
53
|
+
};
|
|
54
|
+
Otp.Hotp = Hotp;
|
|
55
|
+
class Totp {
|
|
56
|
+
static generate(secret, time, options) {
|
|
57
|
+
const opts = Object.assign(Object.assign({}, this.DEFAULTS), options);
|
|
58
|
+
const counter = Totp.timeToCounter(time, opts.period);
|
|
59
|
+
return Hotp.generate(secret, counter, opts);
|
|
60
|
+
}
|
|
61
|
+
static validate(token, secret, time = Date.now(), window = 1, options) {
|
|
62
|
+
const opts = Object.assign(Object.assign({}, this.DEFAULTS), options);
|
|
63
|
+
const counter = Totp.timeToCounter(time, opts.period);
|
|
64
|
+
return Hotp.validate(token, secret, counter, window, opts);
|
|
65
|
+
}
|
|
66
|
+
static remaining(period = 30, time = Date.now()) {
|
|
67
|
+
return period - (time / 1000) % period;
|
|
68
|
+
}
|
|
69
|
+
static timeToCounter(time, period) {
|
|
70
|
+
return Math.floor((time / 1000) / period);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
Totp.DEFAULTS = {
|
|
74
|
+
algorithm: HashAlgorithm.SHA1,
|
|
75
|
+
digits: 6,
|
|
76
|
+
period: 30,
|
|
77
|
+
};
|
|
78
|
+
Otp.Totp = Totp;
|
|
79
|
+
class Base32 {
|
|
80
|
+
static encode(buffer) {
|
|
81
|
+
let bits = 0;
|
|
82
|
+
let value = 0;
|
|
83
|
+
let output = '';
|
|
84
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
85
|
+
value = (value << 8) | buffer[i];
|
|
86
|
+
bits += 8;
|
|
87
|
+
while (bits >= 5) {
|
|
88
|
+
output += this.ALPHABET[(value >>> (bits - 5)) & 31];
|
|
89
|
+
bits -= 5;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (bits > 0) {
|
|
93
|
+
output += this.ALPHABET[(value << (5 - bits)) & 31];
|
|
94
|
+
}
|
|
95
|
+
return output;
|
|
96
|
+
}
|
|
97
|
+
static decode(str) {
|
|
98
|
+
const cleaned = str.toUpperCase().trim().replace(/[^A-Z2-7]/g, '');
|
|
99
|
+
const buffer = Buffer.alloc(Math.floor((cleaned.length * 5) / 8));
|
|
100
|
+
let bits = 0;
|
|
101
|
+
let value = 0;
|
|
102
|
+
let index = 0;
|
|
103
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
104
|
+
const val = this.ALPHABET.indexOf(cleaned[i]);
|
|
105
|
+
if (val === -1)
|
|
106
|
+
throw new Error('Invalid character in Base32 string');
|
|
107
|
+
value = (value << 5) | val;
|
|
108
|
+
bits += 5;
|
|
109
|
+
if (bits >= 8) {
|
|
110
|
+
buffer[index++] = (value >>> (bits - 8)) & 255;
|
|
111
|
+
bits -= 8;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return buffer;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
Base32.ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
118
|
+
class Url {
|
|
119
|
+
static getHotpUrl(issuer, label, secret, counter, options) {
|
|
120
|
+
const opts = Object.assign(Object.assign({}, Hotp.DEFAULTS), options);
|
|
121
|
+
const url = new URL(`otpauth://hotp/${issuer}:${label}`);
|
|
122
|
+
url.searchParams.set('secret', Base32.encode(secret));
|
|
123
|
+
url.searchParams.set('algorithm', opts.algorithm.toUpperCase());
|
|
124
|
+
url.searchParams.set('digits', opts.digits.toString());
|
|
125
|
+
url.searchParams.set('counter', counter.toString());
|
|
126
|
+
url.searchParams.set('issuer', issuer);
|
|
127
|
+
return url.toString();
|
|
128
|
+
}
|
|
129
|
+
static getTotpUrl(issuer, label, secret, options) {
|
|
130
|
+
const opts = Object.assign(Object.assign({}, Totp.DEFAULTS), options);
|
|
131
|
+
const url = new URL(`otpauth://totp/${issuer}:${label}`);
|
|
132
|
+
url.searchParams.set('secret', Base32.encode(secret));
|
|
133
|
+
url.searchParams.set('algorithm', opts.algorithm.toUpperCase());
|
|
134
|
+
url.searchParams.set('digits', opts.digits.toString());
|
|
135
|
+
url.searchParams.set('period', opts.period.toString());
|
|
136
|
+
url.searchParams.set('issuer', issuer);
|
|
137
|
+
return url.toString();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
Otp.Url = Url;
|
|
141
|
+
})(Otp || (Otp = {}));
|
|
142
|
+
export default Otp;
|
|
143
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
|
package/index.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto'
|
|
2
|
+
import { Buffer } from 'node:buffer'
|
|
3
|
+
|
|
4
|
+
namespace Otp {
|
|
5
|
+
export const enum HashAlgorithm {
|
|
6
|
+
SHA1 = 'sha1',
|
|
7
|
+
SHA256 = 'sha256',
|
|
8
|
+
SHA384 = 'sha384',
|
|
9
|
+
SHA512 = 'sha512',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type HotpOptions = {
|
|
13
|
+
algorithm: HashAlgorithm
|
|
14
|
+
digits: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type TotpOptions = {
|
|
18
|
+
algorithm: HashAlgorithm
|
|
19
|
+
digits: number
|
|
20
|
+
period: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class Hotp {
|
|
24
|
+
static readonly DEFAULTS: HotpOptions = {
|
|
25
|
+
algorithm: HashAlgorithm.SHA1,
|
|
26
|
+
digits: 6,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static generate(secret: Buffer, counter: number, options?: Partial<HotpOptions>): string {
|
|
30
|
+
const opts = { ...this.DEFAULTS, ...options }
|
|
31
|
+
const bytes = Buffer.alloc(8)
|
|
32
|
+
bytes.writeBigUInt64BE(BigInt(counter))
|
|
33
|
+
|
|
34
|
+
const hmac = createHmac(opts.algorithm, secret)
|
|
35
|
+
hmac.update(bytes)
|
|
36
|
+
const hash = hmac.digest()
|
|
37
|
+
|
|
38
|
+
const offset = hash[hash.length - 1] & 0xf
|
|
39
|
+
const token = ((hash[offset] & 0x7f) << 24 >>> 0)
|
|
40
|
+
| hash[offset + 1] << 16
|
|
41
|
+
| hash[offset + 2] << 8
|
|
42
|
+
| hash[offset + 3]
|
|
43
|
+
|
|
44
|
+
return (token % (10 ** opts.digits)).toString().padStart(opts.digits, '0')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static validate(token: string, secret: Buffer, counter: number, window: number = 0, options?: Partial<HotpOptions>): boolean {
|
|
48
|
+
const opts = { ...this.DEFAULTS, ...options }
|
|
49
|
+
if (token.length != opts.digits) return false
|
|
50
|
+
const compare = (counter: number) => {
|
|
51
|
+
const generated = Hotp.generate(secret, counter, opts)
|
|
52
|
+
const token_buffer = Buffer.from(token)
|
|
53
|
+
const generated_buffer = Buffer.from(generated)
|
|
54
|
+
if (token_buffer.length != generated_buffer.length) return false
|
|
55
|
+
return timingSafeEqual(token_buffer, generated_buffer)
|
|
56
|
+
}
|
|
57
|
+
if (compare(counter)) return true
|
|
58
|
+
for (let i = 1; i <= window; i++) {
|
|
59
|
+
if (compare(counter + i)) return true
|
|
60
|
+
if (compare(counter - i)) return true
|
|
61
|
+
}
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class Totp {
|
|
67
|
+
static readonly DEFAULTS: TotpOptions = {
|
|
68
|
+
algorithm: HashAlgorithm.SHA1,
|
|
69
|
+
digits: 6,
|
|
70
|
+
period: 30,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static generate(secret: Buffer, time: number, options?: Partial<TotpOptions>): string {
|
|
74
|
+
const opts = { ...this.DEFAULTS, ...options }
|
|
75
|
+
const counter = Totp.timeToCounter(time, opts.period)
|
|
76
|
+
return Hotp.generate(secret, counter, opts)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static validate(token: string, secret: Buffer, time: number = Date.now(), window: number = 1, options?: Partial<TotpOptions>): boolean {
|
|
80
|
+
const opts = { ...this.DEFAULTS, ...options }
|
|
81
|
+
const counter = Totp.timeToCounter(time, opts.period)
|
|
82
|
+
return Hotp.validate(token, secret, counter, window, opts)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static remaining(period: number = 30, time: number = Date.now()) {
|
|
86
|
+
return period - (time / 1000) % period
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static timeToCounter(time: number, period: number): number {
|
|
90
|
+
return Math.floor((time / 1000) / period)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
class Base32 {
|
|
95
|
+
private static readonly ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
|
|
96
|
+
|
|
97
|
+
public static encode(buffer: Buffer): string {
|
|
98
|
+
let bits = 0
|
|
99
|
+
let value = 0
|
|
100
|
+
let output = ''
|
|
101
|
+
|
|
102
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
103
|
+
value = (value << 8) | buffer[i]
|
|
104
|
+
bits += 8
|
|
105
|
+
|
|
106
|
+
while (bits >= 5) {
|
|
107
|
+
output += this.ALPHABET[(value >>> (bits - 5)) & 31]
|
|
108
|
+
bits -= 5
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (bits > 0) {
|
|
113
|
+
output += this.ALPHABET[(value << (5 - bits)) & 31]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return output
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public static decode(str: string): Buffer {
|
|
120
|
+
const cleaned = str.toUpperCase().trim().replace(/[^A-Z2-7]/g, '');
|
|
121
|
+
const buffer = Buffer.alloc(Math.floor((cleaned.length * 5) / 8))
|
|
122
|
+
|
|
123
|
+
let bits = 0
|
|
124
|
+
let value = 0
|
|
125
|
+
let index = 0
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
128
|
+
const val = this.ALPHABET.indexOf(cleaned[i])
|
|
129
|
+
if (val === -1) throw new Error('Invalid character in Base32 string')
|
|
130
|
+
|
|
131
|
+
value = (value << 5) | val
|
|
132
|
+
bits += 5
|
|
133
|
+
|
|
134
|
+
if (bits >= 8) {
|
|
135
|
+
buffer[index++] = (value >>> (bits - 8)) & 255
|
|
136
|
+
bits -= 8
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return buffer
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export class Url {
|
|
145
|
+
public static getHotpUrl(issuer: string, label: string, secret: Buffer, counter: number, options?: Partial<HotpOptions>): string {
|
|
146
|
+
const opts = { ...Hotp.DEFAULTS, ...options }
|
|
147
|
+
const url = new URL(`otpauth://hotp/${issuer}:${label}`)
|
|
148
|
+
url.searchParams.set('secret', Base32.encode(secret))
|
|
149
|
+
url.searchParams.set('algorithm', opts.algorithm.toUpperCase())
|
|
150
|
+
url.searchParams.set('digits', opts.digits.toString())
|
|
151
|
+
url.searchParams.set('counter', counter.toString())
|
|
152
|
+
url.searchParams.set('issuer', issuer)
|
|
153
|
+
return url.toString()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public static getTotpUrl(issuer: string, label: string, secret: Buffer, options?: Partial<TotpOptions>): string {
|
|
157
|
+
const opts = { ...Totp.DEFAULTS, ...options }
|
|
158
|
+
const url = new URL(`otpauth://totp/${issuer}:${label}`)
|
|
159
|
+
url.searchParams.set('secret', Base32.encode(secret))
|
|
160
|
+
url.searchParams.set('algorithm', opts.algorithm.toUpperCase())
|
|
161
|
+
url.searchParams.set('digits', opts.digits.toString())
|
|
162
|
+
url.searchParams.set('period', opts.period.toString())
|
|
163
|
+
url.searchParams.set('issuer', issuer)
|
|
164
|
+
return url.toString()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export default Otp
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@punikonta/node-otp",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Simple and dependency free OTP implementation (HOTP, TOTP) for Node.js written in TypeScript. Passes RFC test vectors and works with most common authenticator apps.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"otp",
|
|
7
|
+
"hotp",
|
|
8
|
+
"totp",
|
|
9
|
+
"2fa",
|
|
10
|
+
"mfa",
|
|
11
|
+
"security",
|
|
12
|
+
"typescript",
|
|
13
|
+
"node",
|
|
14
|
+
"rfc4226",
|
|
15
|
+
"rfc6238",
|
|
16
|
+
"authenticator",
|
|
17
|
+
"authy",
|
|
18
|
+
"google-authenticator",
|
|
19
|
+
"microsoft-authenticator",
|
|
20
|
+
"proton-authenticator",
|
|
21
|
+
"bitwarden-authenticator"
|
|
22
|
+
],
|
|
23
|
+
"main": "dist/index.js",
|
|
24
|
+
"types": "dist/index.d.ts",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "npx tsc",
|
|
29
|
+
"test": "node --test test.mjs"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/punikonta/node-otp"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/punikonta/node-otp/issues",
|
|
37
|
+
"email": "punikonta@protonmail.com"
|
|
38
|
+
},
|
|
39
|
+
"author": {
|
|
40
|
+
"name": "Marcel Jovic",
|
|
41
|
+
"email": "punikonta@protonmail.com",
|
|
42
|
+
"url": "https://punikonta.de"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^25.5.2",
|
|
46
|
+
"typescript": "^6.0.2"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/test.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
|
|
4
|
+
import Otp from './dist/index.js'
|
|
5
|
+
|
|
6
|
+
const rfc = {
|
|
7
|
+
hotp: [
|
|
8
|
+
// "RFC 4226 - Appendix D - HOTP Algorithm: Test Values"
|
|
9
|
+
// https://datatracker.ietf.org/doc/html/rfc4226
|
|
10
|
+
{ secret: '12345678901234567890', count: 0, expected: '755224', },
|
|
11
|
+
{ secret: '12345678901234567890', count: 1, expected: '287082', },
|
|
12
|
+
{ secret: '12345678901234567890', count: 2, expected: '359152', },
|
|
13
|
+
{ secret: '12345678901234567890', count: 3, expected: '969429', },
|
|
14
|
+
{ secret: '12345678901234567890', count: 4, expected: '338314', },
|
|
15
|
+
{ secret: '12345678901234567890', count: 5, expected: '254676', },
|
|
16
|
+
{ secret: '12345678901234567890', count: 6, expected: '287922', },
|
|
17
|
+
{ secret: '12345678901234567890', count: 7, expected: '162583', },
|
|
18
|
+
{ secret: '12345678901234567890', count: 8, expected: '399871', },
|
|
19
|
+
{ secret: '12345678901234567890', count: 9, expected: '520489', },
|
|
20
|
+
],
|
|
21
|
+
totp: [
|
|
22
|
+
// "RFC 6238 - Appendix B - Test Vectors"
|
|
23
|
+
// https://datatracker.ietf.org/doc/html/rfc6238
|
|
24
|
+
{ time: 59, expected: '94287082', algorithm: Otp.HashAlgorithm.SHA1, secret: '12345678901234567890', },
|
|
25
|
+
{ time: 59, expected: '46119246', algorithm: Otp.HashAlgorithm.SHA256, secret: '12345678901234567890123456789012', },
|
|
26
|
+
{ time: 59, expected: '90693936', algorithm: Otp.HashAlgorithm.SHA512, secret: '1234567890123456789012345678901234567890123456789012345678901234', },
|
|
27
|
+
{ time: 1111111109, expected: '07081804', algorithm: Otp.HashAlgorithm.SHA1, secret: '12345678901234567890', },
|
|
28
|
+
{ time: 1111111109, expected: '68084774', algorithm: Otp.HashAlgorithm.SHA256, secret: '12345678901234567890123456789012', },
|
|
29
|
+
{ time: 1111111109, expected: '25091201', algorithm: Otp.HashAlgorithm.SHA512, secret: '1234567890123456789012345678901234567890123456789012345678901234', },
|
|
30
|
+
{ time: 1111111111, expected: '14050471', algorithm: Otp.HashAlgorithm.SHA1, secret: '12345678901234567890', },
|
|
31
|
+
{ time: 1111111111, expected: '67062674', algorithm: Otp.HashAlgorithm.SHA256, secret: '12345678901234567890123456789012', },
|
|
32
|
+
{ time: 1111111111, expected: '99943326', algorithm: Otp.HashAlgorithm.SHA512, secret: '1234567890123456789012345678901234567890123456789012345678901234', },
|
|
33
|
+
{ time: 1234567890, expected: '89005924', algorithm: Otp.HashAlgorithm.SHA1, secret: '12345678901234567890', },
|
|
34
|
+
{ time: 1234567890, expected: '91819424', algorithm: Otp.HashAlgorithm.SHA256, secret: '12345678901234567890123456789012', },
|
|
35
|
+
{ time: 1234567890, expected: '93441116', algorithm: Otp.HashAlgorithm.SHA512, secret: '1234567890123456789012345678901234567890123456789012345678901234', },
|
|
36
|
+
{ time: 2000000000, expected: '69279037', algorithm: Otp.HashAlgorithm.SHA1, secret: '12345678901234567890', },
|
|
37
|
+
{ time: 2000000000, expected: '90698825', algorithm: Otp.HashAlgorithm.SHA256, secret: '12345678901234567890123456789012', },
|
|
38
|
+
{ time: 2000000000, expected: '38618901', algorithm: Otp.HashAlgorithm.SHA512, secret: '1234567890123456789012345678901234567890123456789012345678901234', },
|
|
39
|
+
{ time: 20000000000, expected: '65353130', algorithm: Otp.HashAlgorithm.SHA1, secret: '12345678901234567890', },
|
|
40
|
+
{ time: 20000000000, expected: '77737706', algorithm: Otp.HashAlgorithm.SHA256, secret: '12345678901234567890123456789012', },
|
|
41
|
+
{ time: 20000000000, expected: '47863826', algorithm: Otp.HashAlgorithm.SHA512, secret: '1234567890123456789012345678901234567890123456789012345678901234', },
|
|
42
|
+
],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test('hotp rfc4226 test values', () => {
|
|
46
|
+
for (const value of rfc.hotp) {
|
|
47
|
+
test(`test value count ${value.count}`, () => {
|
|
48
|
+
const key = Buffer.from(value.secret, 'utf8')
|
|
49
|
+
const actual = Otp.Hotp.generate(key, value.count)
|
|
50
|
+
|
|
51
|
+
assert.strictEqual(value.expected, actual)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('totp rfc6238 test values', () => {
|
|
57
|
+
for (const value of rfc.totp) {
|
|
58
|
+
test(`test value, time: ${value.time}, algorithm: ${value.algorithm}`, () => {
|
|
59
|
+
const key = Buffer.from(value.secret, 'utf8')
|
|
60
|
+
const time = value.time * 1000 // test values are given in seconds, but we expect milliseconds
|
|
61
|
+
const actual = Otp.Totp.generate(key, time, { algorithm: value.algorithm, digits: value.expected.length })
|
|
62
|
+
|
|
63
|
+
assert.strictEqual(value.expected, actual)
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es6",
|
|
4
|
+
"module": "preserve",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"noImplicitAny": true,
|
|
7
|
+
"removeComments": true,
|
|
8
|
+
"preserveConstEnums": true,
|
|
9
|
+
"isolatedModules": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"outDir": "dist",
|
|
12
|
+
"types": [
|
|
13
|
+
"node"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"./index.ts"
|
|
18
|
+
]
|
|
19
|
+
}
|