-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathutil.ts
166 lines (156 loc) · 5.1 KB
/
util.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import { OtpAlgorithm } from "./otp.ts";
/**
* Converts a 64 bit number to a Uint8Array representing its bytes.
* Standard length is 8 bytes.
*
* @param value Number to convert
* @param byteArrayLen Length of the Uint8Array or false if the Uint8Array should only be of minimal length.
* @throws Errors if the value exceeds 64 bit
*/
export function numberToBytes(
value: number,
byteArrayLen: number | false = 8,
): Uint8Array {
// Fill the Array with bytes
const preparedArray = new Array<number>();
do {
const currentBytes = value & 0xff;
preparedArray.unshift(currentBytes);
value = (value - currentBytes) / 256;
} while (value !== 0);
// If byteArrayLen is false, use the needed length
byteArrayLen = byteArrayLen === false ? preparedArray.length : byteArrayLen;
// Prepare a Uint8Array for being filled. The "WebCrypto API seems to need an array with 8 bytes.
const byteArr = new Uint8Array(byteArrayLen);
const fillOffset = byteArrayLen - preparedArray.length;
if (fillOffset < 0) {
throw Error(
`The amount of bytes (${preparedArray.length}) of the provided value exceeds the limit of the arraylength (${byteArrayLen}) you provided!`,
);
}
byteArr.set(preparedArray, fillOffset);
return byteArr;
}
/**
* Converts bytes to a unsigned 32 bit integer.
*
* @param bytes Bytes to convert
*/
export function bytesToUInt32BE(bytes: Uint8Array): number {
// TODO: Maybe add useLastBytes?
return new DataView(bytes.buffer).getUint32(0);
}
/**
* Converts a code as string to a number while trimming it's whitespace.
*
* @param code Code to convert
*/
export function codeToNumber(code: string | number): number {
if (typeof code === "number") return code;
const unifiedCode = cleanUserInputFormat(code);
return parseInt(unifiedCode);
}
export interface CalculateHmacDigestOptions {
movingFactor: number;
secret: Uint8Array;
algorithm: OtpAlgorithm | AlgorithmIdentifier;
}
/**
* Calculates the HMAC digest based on the moving factor.
*
* @param options Options for calculating the HMAC digest
* @throws Errors if the movingFactor exceeds 64 bit
*/
export async function calculateHmacDigest(
options: CalculateHmacDigestOptions,
): Promise<Uint8Array> {
const hmacKey = await crypto.subtle.importKey(
"raw",
options.secret,
{ name: "HMAC", hash: options.algorithm },
false,
["sign"],
);
const bytesToSign = numberToBytes(options.movingFactor);
const signedDigest = new Uint8Array(
await crypto.subtle.sign("HMAC", hmacKey, bytesToSign),
);
return signedDigest;
}
/**
* Extracts an n-digit integer from the given HMAC-SHA digest using it's last byte as offset and the provided digits to limit it length.
*
* @param digest The digest to extract from
* @param digits The maximum amount of digits the result can have
* @throws Errors if the last digit of the Uint8Array is undefined
*/
export function extractCodeFromHmacShaDigest(
digest: Uint8Array,
digits: number,
): number {
let dynamicOffset = digest.at(digest.length - 1);
if (dynamicOffset === undefined) throw new Error("Digest not valid!");
// Limit the offset from 0 to 15 because SHA-1 produces a 20 byte digest
dynamicOffset &= 0xf;
// Get 32 bit from the digest
const codeBytes = digest.slice(dynamicOffset, dynamicOffset + 4);
const digestAsInt = bytesToUInt32BE(codeBytes);
// Shorten the code to 32 bit
const fullCode = digestAsInt & 0x7fffffff;
const shortCode = fullCode % Math.pow(10, digits);
return shortCode;
}
/**
* Adds padding and removes whitespace from the given Base32 secret and turns its characters to uppercase to prepare it for decoding.
* @param secret
*/
export function cleanUserInputFormatAndAddBase32Padding(
secret: string,
): string {
secret = cleanUserInputFormat(secret);
// Base32 has to be a multiple of 8
let amountOfMissingPadding = 8 - (secret.length % 8);
// Returns 8 if no missing padding is required, because it's 8 to the next multiple of 8
amountOfMissingPadding = amountOfMissingPadding === 8
? 0
: amountOfMissingPadding;
// `=` is used for Base32 padding
secret = secret.padEnd(secret.length + amountOfMissingPadding, "=");
return secret;
}
/**
* Removes whitespace and turns characters to uppercase
* @param input
*/
export function cleanUserInputFormat(input: string): string {
return input.replaceAll(" ", "").toUpperCase();
}
/**
* Check the string to only be Base32 alphabet not the "Extended Hex" Base 32 Alphabet (https://www.rfc-editor.org/rfc/rfc4648#section-7)
* @param b32
*/
export function isBase32(b32: string): boolean {
let includesBadChar = false;
for (const char of b32) {
const charCode = char.charCodeAt(0);
// Check if char is not in the Base32 alphabet
if (
// Invert the truth check
!(
// Check if charCode is not NaN
!isNaN(charCode) && (
// Is in range 2 to 7
charCode >= 50 && charCode <= 55 ||
// Is =
charCode === 61 ||
// Is in range A to Z
charCode >= 65 && charCode <= 90
)
)
) {
includesBadChar = true;
break;
}
}
return !includesBadChar;
}