foronce 0.0.0
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 +1 -0
- package/dist/index.cjs +92 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +89 -0
- package/package.json +69 -0
- package/src/index.js +84 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# foronce
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var node_crypto = require('node:crypto');
|
|
4
|
+
var node_buffer = require('node:buffer');
|
|
5
|
+
var base32 = require('hi-base32');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Simplified implementation of TOTP with existing node libs
|
|
9
|
+
*
|
|
10
|
+
* helpers to handle the following
|
|
11
|
+
* - generate QR codes with the otpauth:// URL scheme
|
|
12
|
+
* - algo and implmentation taken from https://drewdevault.com/2022/10/18/TOTP-is-easy.html
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { floor } = Math;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
*
|
|
19
|
+
* @param {string} secret
|
|
20
|
+
* @param {number} when
|
|
21
|
+
* @param {object} [options]
|
|
22
|
+
* @param {number} [options.period] in seconds (eg: 30 => 30 seconds)
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
function totp(secret, when = floor(Date.now() / 1000), options = {}) {
|
|
26
|
+
const _options = Object.assign({ period: 30 }, options);
|
|
27
|
+
const now = floor(when / _options.period);
|
|
28
|
+
const key = base32.decode(secret);
|
|
29
|
+
const buff = bigEndian64(BigInt(now));
|
|
30
|
+
const hmac = node_crypto.createHmac('sha512', key).update(buff).digest();
|
|
31
|
+
const offset = hmac[hmac.length - 1] & 0xf;
|
|
32
|
+
const truncatedHash = hmac.subarray(offset, offset + 4);
|
|
33
|
+
const otp = (
|
|
34
|
+
(truncatedHash.readInt32BE() & 0x7f_ff_ff_ff) %
|
|
35
|
+
1_000_000
|
|
36
|
+
).toString(10);
|
|
37
|
+
return otp.length < 6 ? `${otp}`.padStart(6, '0') : otp
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} secret
|
|
42
|
+
* @param {string} token
|
|
43
|
+
* @param {object} [options]
|
|
44
|
+
* @param {number} [options.period] in seconds (eg: 30 => 30 seconds)
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
function isValid(secret, token, options = {}) {
|
|
48
|
+
const _options = Object.assign({ period: 30 }, options);
|
|
49
|
+
for (let index = -2; index < 3; index += 1) {
|
|
50
|
+
const fromSys = totp(secret, Date.now() / 1000 + index, _options);
|
|
51
|
+
const valid = fromSys === token;
|
|
52
|
+
if (valid) return true
|
|
53
|
+
}
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} secret
|
|
59
|
+
* @param {object} options
|
|
60
|
+
* @param {string} options.company
|
|
61
|
+
* @param {string} options.email
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
function generateTOTPURL(secret, options) {
|
|
65
|
+
const parameters = new URLSearchParams();
|
|
66
|
+
parameters.append('secret', secret);
|
|
67
|
+
parameters.append('issuer', options.company);
|
|
68
|
+
parameters.append('digits', '6');
|
|
69
|
+
const url = `otpauth://totp/${options.company}:${
|
|
70
|
+
options.email
|
|
71
|
+
}?${parameters.toString()}`;
|
|
72
|
+
return new URL(url).toString()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {bigint} hash
|
|
77
|
+
* @returns {Buffer}
|
|
78
|
+
*/
|
|
79
|
+
function bigEndian64(hash) {
|
|
80
|
+
const buf = node_buffer.Buffer.allocUnsafe(64 / 8);
|
|
81
|
+
buf.writeBigInt64BE(hash, 0);
|
|
82
|
+
return buf
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function generateTOTPSecret(num = 32) {
|
|
86
|
+
return base32.encode(node_crypto.randomBytes(num).toString('ascii'))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
exports.generateTOTPSecret = generateTOTPSecret;
|
|
90
|
+
exports.generateTOTPURL = generateTOTPURL;
|
|
91
|
+
exports.isValid = isValid;
|
|
92
|
+
exports.totp = totp;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function totp(secret: string, when?: number, options?: {
|
|
2
|
+
period?: number;
|
|
3
|
+
}): string;
|
|
4
|
+
export function isValid(secret: string, token: string, options?: {
|
|
5
|
+
period?: number;
|
|
6
|
+
}): boolean;
|
|
7
|
+
export function generateTOTPURL(secret: string, options: {
|
|
8
|
+
company: string;
|
|
9
|
+
email: string;
|
|
10
|
+
}): string;
|
|
11
|
+
export function generateTOTPSecret(num?: number): string;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var node_crypto = require('node:crypto');
|
|
4
|
+
var node_buffer = require('node:buffer');
|
|
5
|
+
var base32 = require('hi-base32');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Simplified implementation of TOTP with existing node libs
|
|
9
|
+
*
|
|
10
|
+
* helpers to handle the following
|
|
11
|
+
* - generate QR codes with the otpauth:// URL scheme
|
|
12
|
+
* - algo and implmentation taken from https://drewdevault.com/2022/10/18/TOTP-is-easy.html
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { floor } = Math;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
*
|
|
19
|
+
* @param {string} secret
|
|
20
|
+
* @param {number} when
|
|
21
|
+
* @param {object} [options]
|
|
22
|
+
* @param {number} [options.period]
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
function totp(secret, when = floor(Date.now() / 1000), options = {}) {
|
|
26
|
+
const _options = Object.assign({ period: 30 }, options);
|
|
27
|
+
const now = floor(when / _options.period);
|
|
28
|
+
const key = base32.decode(secret);
|
|
29
|
+
const buff = bigEndian64(BigInt(now));
|
|
30
|
+
const hmac = node_crypto.createHmac('sha1', key).update(buff).digest();
|
|
31
|
+
const offset = hmac[hmac.length - 1] & 0xf;
|
|
32
|
+
const truncatedHash = hmac.subarray(offset, offset + 4);
|
|
33
|
+
const otp = (
|
|
34
|
+
(truncatedHash.readInt32BE() & 0x7f_ff_ff_ff) %
|
|
35
|
+
1_000_000
|
|
36
|
+
).toString(10);
|
|
37
|
+
return otp.length < 6 ? `${otp}`.padStart(6, '0') : otp
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} secret
|
|
42
|
+
* @param {string} token
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
function isValid(secret, token) {
|
|
46
|
+
for (let index = -2; index < 3; index += 1) {
|
|
47
|
+
const fromSys = totp(secret, Date.now() / 1000 + index);
|
|
48
|
+
const valid = fromSys === token;
|
|
49
|
+
if (valid) return true
|
|
50
|
+
}
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} secret
|
|
56
|
+
* @param {object} options
|
|
57
|
+
* @param {string} options.company
|
|
58
|
+
* @param {string} options.email
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
function generateTOTPURL(secret, options) {
|
|
62
|
+
const parameters = new URLSearchParams();
|
|
63
|
+
parameters.append('secret', secret);
|
|
64
|
+
parameters.append('issuer', options.company);
|
|
65
|
+
parameters.append('digits', '6');
|
|
66
|
+
const url = `otpauth://totp/${options.company}:${
|
|
67
|
+
options.email
|
|
68
|
+
}?${parameters.toString()}`;
|
|
69
|
+
return new URL(url).toString()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {bigint} hash
|
|
74
|
+
* @returns {Buffer}
|
|
75
|
+
*/
|
|
76
|
+
function bigEndian64(hash) {
|
|
77
|
+
const buf = node_buffer.Buffer.allocUnsafe(64 / 8);
|
|
78
|
+
buf.writeBigInt64BE(hash, 0);
|
|
79
|
+
return buf
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function generateTOTPSecret(num = 32) {
|
|
83
|
+
return base32.encode(node_crypto.randomBytes(num).toString('ascii'))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
exports.generateTOTPSecret = generateTOTPSecret;
|
|
87
|
+
exports.generateTOTPURL = generateTOTPURL;
|
|
88
|
+
exports.isValid = isValid;
|
|
89
|
+
exports.totp = totp;
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "foronce",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"repository": "git@github.com:dumbjs/foronce.git",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Reaper <ahoy@barelyhuman.dev>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./src/index.js",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.cjs",
|
|
17
|
+
"module": "./src/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "rollup -c; tsc",
|
|
25
|
+
"dev": "rollup -c --watch",
|
|
26
|
+
"fix": "prettier --write .",
|
|
27
|
+
"next": "bumpp",
|
|
28
|
+
"prepare": "husky install",
|
|
29
|
+
"size": "sizesnap",
|
|
30
|
+
"test": "uvu tests",
|
|
31
|
+
"test:ci": "c8 uvu tests "
|
|
32
|
+
},
|
|
33
|
+
"lint-staged": {
|
|
34
|
+
"*.{js,css,md,json}": "prettier --write"
|
|
35
|
+
},
|
|
36
|
+
"prettier": "@barelyhuman/prettier-config",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"hi-base32": "^0.5.1",
|
|
39
|
+
"uncrypto": "^0.1.3"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@barelyhuman/prettier-config": "^1.0.0",
|
|
43
|
+
"@types/node": "^20.10.8",
|
|
44
|
+
"buffer": "^6.0.3",
|
|
45
|
+
"bumpp": "^9.2.0",
|
|
46
|
+
"c8": "^8.0.1",
|
|
47
|
+
"esm": "^3.2.25",
|
|
48
|
+
"husky": "^8.0.3",
|
|
49
|
+
"lint-staged": "^14.0.1",
|
|
50
|
+
"prettier": "^2.7.1",
|
|
51
|
+
"publint": "^0.2.7",
|
|
52
|
+
"rollup": "^4.9.4",
|
|
53
|
+
"rollup-plugin-node-externals": "^6.1.2",
|
|
54
|
+
"sizesnap": "^0.2.1",
|
|
55
|
+
"tsup": "^6.1.2",
|
|
56
|
+
"typescript": "^4.7.4",
|
|
57
|
+
"uvu": "^0.5.6"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
},
|
|
62
|
+
"sizesnap": {
|
|
63
|
+
"files": [
|
|
64
|
+
"dist/*.dts",
|
|
65
|
+
"dist/*.ts",
|
|
66
|
+
"dist/*.js"
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simplified implementation of TOTP with existing node libs
|
|
3
|
+
*
|
|
4
|
+
* helpers to handle the following
|
|
5
|
+
* - generate QR codes with the otpauth:// URL scheme
|
|
6
|
+
* - algo and implmentation taken from https://drewdevault.com/2022/10/18/TOTP-is-easy.html
|
|
7
|
+
*/
|
|
8
|
+
import { createHmac, randomBytes } from 'node:crypto'
|
|
9
|
+
import { Buffer } from 'buffer'
|
|
10
|
+
import base32 from 'hi-base32'
|
|
11
|
+
|
|
12
|
+
const { floor } = Math
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
*
|
|
16
|
+
* @param {string} secret
|
|
17
|
+
* @param {number} when
|
|
18
|
+
* @param {object} [options]
|
|
19
|
+
* @param {number} [options.period] in seconds (eg: 30 => 30 seconds)
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
export function totp(secret, when = floor(Date.now() / 1000), options = {}) {
|
|
23
|
+
const _options = Object.assign({ period: 30 }, options)
|
|
24
|
+
const now = floor(when / _options.period)
|
|
25
|
+
const key = base32.decode(secret)
|
|
26
|
+
const buff = bigEndian64(BigInt(now))
|
|
27
|
+
const hmac = createHmac('sha512', key).update(buff).digest()
|
|
28
|
+
const offset = hmac[hmac.length - 1] & 0xf
|
|
29
|
+
const truncatedHash = hmac.subarray(offset, offset + 4)
|
|
30
|
+
const otp = (
|
|
31
|
+
(truncatedHash.readInt32BE() & 0x7f_ff_ff_ff) %
|
|
32
|
+
1_000_000
|
|
33
|
+
).toString(10)
|
|
34
|
+
return otp.length < 6 ? `${otp}`.padStart(6, '0') : otp
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} secret
|
|
39
|
+
* @param {string} token
|
|
40
|
+
* @param {object} [options]
|
|
41
|
+
* @param {number} [options.period] in seconds (eg: 30 => 30 seconds)
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
export function isValid(secret, token, options = {}) {
|
|
45
|
+
const _options = Object.assign({ period: 30 }, options)
|
|
46
|
+
for (let index = -2; index < 3; index += 1) {
|
|
47
|
+
const fromSys = totp(secret, Date.now() / 1000 + index, _options)
|
|
48
|
+
const valid = fromSys === token
|
|
49
|
+
if (valid) return true
|
|
50
|
+
}
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} secret
|
|
56
|
+
* @param {object} options
|
|
57
|
+
* @param {string} options.company
|
|
58
|
+
* @param {string} options.email
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
export function generateTOTPURL(secret, options) {
|
|
62
|
+
const parameters = new URLSearchParams()
|
|
63
|
+
parameters.append('secret', secret)
|
|
64
|
+
parameters.append('issuer', options.company)
|
|
65
|
+
parameters.append('digits', '6')
|
|
66
|
+
const url = `otpauth://totp/${options.company}:${
|
|
67
|
+
options.email
|
|
68
|
+
}?${parameters.toString()}`
|
|
69
|
+
return new URL(url).toString()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {bigint} hash
|
|
74
|
+
* @returns {Buffer}
|
|
75
|
+
*/
|
|
76
|
+
function bigEndian64(hash) {
|
|
77
|
+
const buf = Buffer.allocUnsafe(64 / 8)
|
|
78
|
+
buf.writeBigInt64BE(hash, 0)
|
|
79
|
+
return buf
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function generateTOTPSecret(num = 32) {
|
|
83
|
+
return base32.encode(randomBytes(num).toString('ascii'))
|
|
84
|
+
}
|