foronce 0.0.0 → 0.0.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/dist/base32.cjs +153 -0
- package/dist/base32.d.ts +2 -0
- package/dist/index.cjs +1 -1
- package/package.json +8 -2
- package/src/base32.js +150 -0
- package/src/index.js +3 -3
- package/dist/index.js +0 -89
package/dist/base32.cjs
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*!
|
|
4
|
+
* base-32.js
|
|
5
|
+
* Copyright(c) 2024 Reaper
|
|
6
|
+
* MIT Licensed
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Simple implementation based of RFC 4648 for base32 encoding and decoding
|
|
10
|
+
|
|
11
|
+
const pad = '=';
|
|
12
|
+
const base32alphaMap = {
|
|
13
|
+
0: 'A',
|
|
14
|
+
1: 'B',
|
|
15
|
+
2: 'C',
|
|
16
|
+
3: 'D',
|
|
17
|
+
4: 'E',
|
|
18
|
+
5: 'F',
|
|
19
|
+
6: 'G',
|
|
20
|
+
7: 'H',
|
|
21
|
+
8: 'I',
|
|
22
|
+
9: 'J',
|
|
23
|
+
10: 'K',
|
|
24
|
+
11: 'L',
|
|
25
|
+
12: 'M',
|
|
26
|
+
13: 'N',
|
|
27
|
+
14: 'O',
|
|
28
|
+
15: 'P',
|
|
29
|
+
16: 'Q',
|
|
30
|
+
17: 'R',
|
|
31
|
+
18: 'S',
|
|
32
|
+
19: 'T',
|
|
33
|
+
20: 'U',
|
|
34
|
+
21: 'V',
|
|
35
|
+
22: 'W',
|
|
36
|
+
23: 'X',
|
|
37
|
+
24: 'Y',
|
|
38
|
+
25: 'Z',
|
|
39
|
+
26: '2',
|
|
40
|
+
27: '3',
|
|
41
|
+
28: '4',
|
|
42
|
+
29: '5',
|
|
43
|
+
30: '6',
|
|
44
|
+
31: '7',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const base32alphaMapDecode = Object.fromEntries(
|
|
48
|
+
Object.entries(base32alphaMap).map(([k, v]) => [v, k])
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} str
|
|
53
|
+
*/
|
|
54
|
+
const encode = str => {
|
|
55
|
+
const splits = str.split('');
|
|
56
|
+
|
|
57
|
+
if (!splits.length) {
|
|
58
|
+
return ''
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let binaryGroup = [];
|
|
62
|
+
let bitText = '';
|
|
63
|
+
|
|
64
|
+
splits.forEach(c => {
|
|
65
|
+
bitText += toBinary(c);
|
|
66
|
+
|
|
67
|
+
if (bitText.length == 40) {
|
|
68
|
+
binaryGroup.push(bitText);
|
|
69
|
+
bitText = '';
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (bitText.length > 0) {
|
|
74
|
+
binaryGroup.push(bitText);
|
|
75
|
+
bitText = '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return binaryGroup
|
|
79
|
+
.map(x => {
|
|
80
|
+
let fiveBitGrouping = [];
|
|
81
|
+
let lex = '';
|
|
82
|
+
let bitOn = x;
|
|
83
|
+
|
|
84
|
+
bitOn.split('').forEach(d => {
|
|
85
|
+
lex += d;
|
|
86
|
+
if (lex.length == 5) {
|
|
87
|
+
fiveBitGrouping.push(lex);
|
|
88
|
+
lex = '';
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (lex.length > 0) {
|
|
93
|
+
fiveBitGrouping.push(lex.padEnd(5, '0'));
|
|
94
|
+
lex = '';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let paddedArray = Array.from(fiveBitGrouping);
|
|
98
|
+
paddedArray.length = 8;
|
|
99
|
+
paddedArray = paddedArray.fill('-1', fiveBitGrouping.length, 8);
|
|
100
|
+
|
|
101
|
+
return paddedArray
|
|
102
|
+
.map(f => {
|
|
103
|
+
if (f == '-1') {
|
|
104
|
+
return pad
|
|
105
|
+
}
|
|
106
|
+
return base32alphaMap[parseInt(f, 2).toString(10)]
|
|
107
|
+
})
|
|
108
|
+
.join('')
|
|
109
|
+
})
|
|
110
|
+
.join('')
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} str
|
|
115
|
+
* @returns
|
|
116
|
+
*/
|
|
117
|
+
const decode = str => {
|
|
118
|
+
const overallBinary = str
|
|
119
|
+
.split('')
|
|
120
|
+
.map(x => {
|
|
121
|
+
if (x === pad) {
|
|
122
|
+
return '00000'
|
|
123
|
+
}
|
|
124
|
+
const d = base32alphaMapDecode[x];
|
|
125
|
+
const binary = parseInt(d, 10).toString(2);
|
|
126
|
+
return binary.padStart(5, '0')
|
|
127
|
+
})
|
|
128
|
+
.join('');
|
|
129
|
+
|
|
130
|
+
const characterBitGrouping = chunk(overallBinary.split(''), 8);
|
|
131
|
+
return characterBitGrouping
|
|
132
|
+
.map(x => {
|
|
133
|
+
const binaryL = x.join('');
|
|
134
|
+
const str = String.fromCharCode(+parseInt(binaryL, 2).toString(10));
|
|
135
|
+
return str.replace('\x00', '')
|
|
136
|
+
})
|
|
137
|
+
.join('')
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const toBinary = (char, padLimit = 8) => {
|
|
141
|
+
const binary = String(char).charCodeAt(0).toString(2);
|
|
142
|
+
return binary.padStart(padLimit, '0')
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const chunk = (arr, chunkSize = 1, cache = []) => {
|
|
146
|
+
const tmp = [...arr];
|
|
147
|
+
if (chunkSize <= 0) return cache
|
|
148
|
+
while (tmp.length) cache.push(tmp.splice(0, chunkSize));
|
|
149
|
+
return cache
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
exports.decode = decode;
|
|
153
|
+
exports.encode = encode;
|
package/dist/base32.d.ts
ADDED
package/dist/index.cjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "foronce",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"repository": "git@github.com:dumbjs/foronce.git",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Reaper <ahoy@barelyhuman.dev>",
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
"import": "./src/index.js",
|
|
12
12
|
"require": "./dist/index.cjs"
|
|
13
13
|
},
|
|
14
|
+
"./base32": {
|
|
15
|
+
"types": "./dist/base32.d.ts",
|
|
16
|
+
"import": "./src/base32.js",
|
|
17
|
+
"require": "./dist/base32.cjs"
|
|
18
|
+
},
|
|
14
19
|
"./package.json": "./package.json"
|
|
15
20
|
},
|
|
16
21
|
"main": "./dist/index.cjs",
|
|
@@ -63,7 +68,8 @@
|
|
|
63
68
|
"files": [
|
|
64
69
|
"dist/*.dts",
|
|
65
70
|
"dist/*.ts",
|
|
66
|
-
"dist/*.js"
|
|
71
|
+
"dist/*.js",
|
|
72
|
+
"dist/*.cjs"
|
|
67
73
|
]
|
|
68
74
|
}
|
|
69
75
|
}
|
package/src/base32.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* base-32.js
|
|
3
|
+
* Copyright(c) 2024 Reaper
|
|
4
|
+
* MIT Licensed
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Simple implementation based of RFC 4648 for base32 encoding and decoding
|
|
8
|
+
|
|
9
|
+
const pad = '='
|
|
10
|
+
const base32alphaMap = {
|
|
11
|
+
0: 'A',
|
|
12
|
+
1: 'B',
|
|
13
|
+
2: 'C',
|
|
14
|
+
3: 'D',
|
|
15
|
+
4: 'E',
|
|
16
|
+
5: 'F',
|
|
17
|
+
6: 'G',
|
|
18
|
+
7: 'H',
|
|
19
|
+
8: 'I',
|
|
20
|
+
9: 'J',
|
|
21
|
+
10: 'K',
|
|
22
|
+
11: 'L',
|
|
23
|
+
12: 'M',
|
|
24
|
+
13: 'N',
|
|
25
|
+
14: 'O',
|
|
26
|
+
15: 'P',
|
|
27
|
+
16: 'Q',
|
|
28
|
+
17: 'R',
|
|
29
|
+
18: 'S',
|
|
30
|
+
19: 'T',
|
|
31
|
+
20: 'U',
|
|
32
|
+
21: 'V',
|
|
33
|
+
22: 'W',
|
|
34
|
+
23: 'X',
|
|
35
|
+
24: 'Y',
|
|
36
|
+
25: 'Z',
|
|
37
|
+
26: '2',
|
|
38
|
+
27: '3',
|
|
39
|
+
28: '4',
|
|
40
|
+
29: '5',
|
|
41
|
+
30: '6',
|
|
42
|
+
31: '7',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const base32alphaMapDecode = Object.fromEntries(
|
|
46
|
+
Object.entries(base32alphaMap).map(([k, v]) => [v, k])
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} str
|
|
51
|
+
*/
|
|
52
|
+
export const encode = str => {
|
|
53
|
+
const splits = str.split('')
|
|
54
|
+
|
|
55
|
+
if (!splits.length) {
|
|
56
|
+
return ''
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let binaryGroup = []
|
|
60
|
+
let bitText = ''
|
|
61
|
+
|
|
62
|
+
splits.forEach(c => {
|
|
63
|
+
bitText += toBinary(c)
|
|
64
|
+
|
|
65
|
+
if (bitText.length == 40) {
|
|
66
|
+
binaryGroup.push(bitText)
|
|
67
|
+
bitText = ''
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
if (bitText.length > 0) {
|
|
72
|
+
binaryGroup.push(bitText)
|
|
73
|
+
bitText = ''
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return binaryGroup
|
|
77
|
+
.map(x => {
|
|
78
|
+
let fiveBitGrouping = []
|
|
79
|
+
let lex = ''
|
|
80
|
+
let bitOn = x
|
|
81
|
+
|
|
82
|
+
bitOn.split('').forEach(d => {
|
|
83
|
+
lex += d
|
|
84
|
+
if (lex.length == 5) {
|
|
85
|
+
fiveBitGrouping.push(lex)
|
|
86
|
+
lex = ''
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
if (lex.length > 0) {
|
|
91
|
+
fiveBitGrouping.push(lex.padEnd(5, '0'))
|
|
92
|
+
lex = ''
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let paddedArray = Array.from(fiveBitGrouping)
|
|
96
|
+
paddedArray.length = 8
|
|
97
|
+
paddedArray = paddedArray.fill('-1', fiveBitGrouping.length, 8)
|
|
98
|
+
|
|
99
|
+
return paddedArray
|
|
100
|
+
.map(f => {
|
|
101
|
+
if (f == '-1') {
|
|
102
|
+
return pad
|
|
103
|
+
}
|
|
104
|
+
return base32alphaMap[parseInt(f, 2).toString(10)]
|
|
105
|
+
})
|
|
106
|
+
.join('')
|
|
107
|
+
})
|
|
108
|
+
.join('')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {string} str
|
|
113
|
+
* @returns
|
|
114
|
+
*/
|
|
115
|
+
export const decode = str => {
|
|
116
|
+
const overallBinary = str
|
|
117
|
+
.split('')
|
|
118
|
+
.map(x => {
|
|
119
|
+
if (x === pad) {
|
|
120
|
+
return '00000'
|
|
121
|
+
}
|
|
122
|
+
const d = base32alphaMapDecode[x]
|
|
123
|
+
const binary = parseInt(d, 10).toString(2)
|
|
124
|
+
return binary.padStart(5, '0')
|
|
125
|
+
})
|
|
126
|
+
.join('')
|
|
127
|
+
|
|
128
|
+
const characterBitGrouping = chunk(overallBinary.split(''), 8)
|
|
129
|
+
return characterBitGrouping
|
|
130
|
+
.map(x => {
|
|
131
|
+
const binaryL = x.join('')
|
|
132
|
+
const str = String.fromCharCode(+parseInt(binaryL, 2).toString(10))
|
|
133
|
+
return str.replace('\x00', '')
|
|
134
|
+
})
|
|
135
|
+
.join('')
|
|
136
|
+
|
|
137
|
+
return ''
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const toBinary = (char, padLimit = 8) => {
|
|
141
|
+
const binary = String(char).charCodeAt(0).toString(2)
|
|
142
|
+
return binary.padStart(padLimit, '0')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const chunk = (arr, chunkSize = 1, cache = []) => {
|
|
146
|
+
const tmp = [...arr]
|
|
147
|
+
if (chunkSize <= 0) return cache
|
|
148
|
+
while (tmp.length) cache.push(tmp.splice(0, chunkSize))
|
|
149
|
+
return cache
|
|
150
|
+
}
|
package/src/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { createHmac, randomBytes } from 'node:crypto'
|
|
9
9
|
import { Buffer } from 'buffer'
|
|
10
|
-
import
|
|
10
|
+
import { decode, encode } from './base32.js'
|
|
11
11
|
|
|
12
12
|
const { floor } = Math
|
|
13
13
|
|
|
@@ -22,7 +22,7 @@ const { floor } = Math
|
|
|
22
22
|
export function totp(secret, when = floor(Date.now() / 1000), options = {}) {
|
|
23
23
|
const _options = Object.assign({ period: 30 }, options)
|
|
24
24
|
const now = floor(when / _options.period)
|
|
25
|
-
const key =
|
|
25
|
+
const key = decode(secret)
|
|
26
26
|
const buff = bigEndian64(BigInt(now))
|
|
27
27
|
const hmac = createHmac('sha512', key).update(buff).digest()
|
|
28
28
|
const offset = hmac[hmac.length - 1] & 0xf
|
|
@@ -80,5 +80,5 @@ function bigEndian64(hash) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export function generateTOTPSecret(num = 32) {
|
|
83
|
-
return
|
|
83
|
+
return encode(randomBytes(num).toString('ascii'))
|
|
84
84
|
}
|
package/dist/index.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
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;
|