@bsv/sdk 1.1.6 → 1.1.7

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.
@@ -0,0 +1,142 @@
1
+ import { SHA1HMAC, SHA256HMAC, SHA512HMAC } from '../primitives/Hash.js'
2
+ import BigNumber from '../primitives/BigNumber.js'
3
+
4
+ export type TOTPAlgorithm = 'SHA-1' | 'SHA-256' | 'SHA-512'
5
+
6
+ /**
7
+ * Options for TOTP generation.
8
+ * @param {number} [digits=6] - The number of digits in the OTP.
9
+ * @param {TOTPAlgorithm} [algorithm="SHA-1"] - Algorithm used for hashing.
10
+ * @param {number} [period=30] - The time period for OTP validity in seconds.
11
+ * @param {number} [timestamp=Date.now()] - The current timestamp.
12
+ */
13
+ export interface TOTPOptions {
14
+ digits?: number
15
+ algorithm?: TOTPAlgorithm
16
+ period?: number
17
+ timestamp?: number
18
+ }
19
+
20
+ /**
21
+ * Options for TOTP validation.
22
+ * @param {number} [skew=1] - The number of time periods to check before and after the current time period.
23
+ */
24
+ export type TOTPValidateOptions = TOTPOptions & {
25
+ skew?: number
26
+ }
27
+
28
+ export class TOTP {
29
+ /**
30
+ * Generates a Time-based One-Time Password (TOTP).
31
+ * @param {number[]} secret - The secret key for TOTP.
32
+ * @param {TOTPOptions} options - Optional parameters for TOTP.
33
+ * @returns {string} The generated TOTP.
34
+ */
35
+ static generate (secret: number[], options?: TOTPOptions): string {
36
+ const _options = this.withDefaultOptions(options)
37
+
38
+ const counter = this.getCounter(_options.timestamp, _options.period)
39
+ const otp = generateHOTP(secret, counter, _options)
40
+ return otp
41
+ }
42
+
43
+ /**
44
+ * Validates a Time-based One-Time Password (TOTP).
45
+ * @param {number[]} secret - The secret key for TOTP.
46
+ * @param {string} passcode - The passcode to validate.
47
+ * @param {TOTPValidateOptions} options - Optional parameters for TOTP validation.
48
+ * @returns {boolean} A boolean indicating whether the passcode is valid.
49
+ */
50
+ static validate (
51
+ secret: number[],
52
+ passcode: string,
53
+ options?: TOTPValidateOptions
54
+ ): boolean {
55
+ const _options = this.withDefaultValidateOptions(options)
56
+ passcode = passcode.trim()
57
+ if (passcode.length != _options.digits) {
58
+ return false
59
+ }
60
+
61
+ const counter = this.getCounter(_options.timestamp, _options.period)
62
+
63
+ const counters = [counter]
64
+ for (let i = 1; i <= _options.skew; i++) {
65
+ counters.push(counter + i)
66
+ counters.push(counter - i)
67
+ }
68
+
69
+ for (const c of counters) {
70
+ if (passcode === generateHOTP(secret, c, _options)) {
71
+ return true
72
+ }
73
+ }
74
+
75
+ return false
76
+ }
77
+
78
+ private static getCounter (timestamp: number, period: number): number {
79
+ const epochSeconds = Math.floor(timestamp / 1000)
80
+ const counter = Math.floor(epochSeconds / period)
81
+ return counter
82
+ }
83
+
84
+ private static withDefaultOptions (
85
+ options?: TOTPOptions
86
+ ): Required<TOTPOptions> {
87
+ return {
88
+ digits: 2,
89
+ algorithm: 'SHA-1',
90
+ period: 30,
91
+ timestamp: Date.now(),
92
+ ...options
93
+ }
94
+ }
95
+
96
+ private static withDefaultValidateOptions (
97
+ options?: TOTPValidateOptions
98
+ ): Required<TOTPValidateOptions> {
99
+ return { skew: 1, ...this.withDefaultOptions(options) }
100
+ }
101
+ }
102
+
103
+ function generateHOTP (
104
+ secret: number[],
105
+ counter: number,
106
+ options: Required<TOTPOptions>
107
+ ): string {
108
+ const timePad = new BigNumber(counter).toArray('be', 8)
109
+ console.log({ timePad })
110
+ const hmac = calcHMAC(secret, timePad, options.algorithm)
111
+
112
+ const signature = hmac.digest()
113
+ const signatureHex = hmac.digestHex()
114
+
115
+ // RFC 4226 https://datatracker.ietf.org/doc/html/rfc4226#section-5.4
116
+ const offset = signature[signature.length - 1] & 0x0f // offset is the last byte in the hmac
117
+ const fourBytesRange = signature.slice(offset, offset + 4) // starting from offset, get 4 bytes
118
+ const mask = 0x7fffffff // 32-bit number with a leading 0 followed by 31 ones [0111 (...) 1111]
119
+ const masked = new BigNumber(fourBytesRange).toNumber() & mask
120
+
121
+ console.log({ signatureHex, signature, offset, fourBytesRange, mask, masked })
122
+
123
+ const otp = masked.toString().slice(-options.digits)
124
+ return otp
125
+ }
126
+
127
+ function calcHMAC (
128
+ secret: number[],
129
+ timePad: number[],
130
+ algorithm: TOTPAlgorithm
131
+ ) {
132
+ switch (algorithm) {
133
+ case 'SHA-1':
134
+ return new SHA1HMAC(secret).update(timePad)
135
+ case 'SHA-256':
136
+ return new SHA256HMAC(secret).update(timePad)
137
+ case 'SHA-512':
138
+ return new SHA512HMAC(secret).update(timePad)
139
+ default:
140
+ throw new Error('unsupported HMAC algorithm')
141
+ }
142
+ }