Skip to content

Commit d71d825

Browse files
committed
feat: write a simple crypto.subtle based hmac implementation to stop relying on old std
1 parent ef67ec9 commit d71d825

File tree

4 files changed

+142
-12
lines changed

4 files changed

+142
-12
lines changed

lib/helpers.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
/**
22
* @todo document this module
33
*/
4-
import { crypto } from "https://deno.land/std@0.152.0/crypto/mod.ts";
5-
import { decode, encode } from "https://deno.land/std@0.152.0/encoding/base64.ts";
6-
import { HmacSha256 } from "https://deno.land/[email protected]/hash/sha256.ts";
4+
import { crypto } from "std/crypto/mod.ts";
5+
import { decodeBase64, encodeBase64 } from "std/encoding/base64.ts";
6+
import { hmacSHA256 } from "./hmac.ts";
77
// dprint-ignore-next-line
8+
// deno-fmt-ignore
89
export type logN = 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;
910
export interface ScryptParameters {
1011
logN?: logN;
@@ -26,27 +27,31 @@ export function formatScrypt(
2627
const encoder = new TextEncoder();
2728
const result = new Uint8Array(96);
2829
const dataview = new DataView(result.buffer);
29-
const hmac = new HmacSha256(new Uint8Array(decode(rawHash)).subarray(32));
3030
// first 6 bytes are the word "scrypt", 7th byte is 0
3131
result.set([115, 99, 114, 121, 112, 116, 0], 0);
3232
dataview.setUint8(7, logN);
3333
dataview.setUint32(8, r, false);
3434
dataview.setUint32(12, p, false);
3535
result.set(typeof salt === "string" ? encoder.encode(salt) : salt, 16);
36-
const hashedResult = crypto.subtle.digestSync("SHA-256", result.subarray(0, 48));
36+
const hashedResult = crypto.subtle.digestSync(
37+
"SHA-256",
38+
result.subarray(0, 48),
39+
);
3740
result.set(new Uint8Array(hashedResult), 48);
38-
hmac.update(result.subarray(0, 64));
3941
result.set(
40-
hmac.array(),
42+
hmacSHA256(
43+
new Uint8Array(decodeBase64(rawHash)).subarray(32),
44+
result.subarray(0, 64),
45+
),
4146
64,
4247
);
4348
// encode the result as a base64 string
44-
return encode(result);
49+
return encodeBase64(result);
4550
}
4651
function decomposeScrypt(
4752
formattedHash: string,
4853
): ScryptParameters {
49-
const bytes: Uint8Array = new Uint8Array(decode(formattedHash));
54+
const bytes: Uint8Array = new Uint8Array(decodeBase64(formattedHash));
5055
const dataview: DataView = new DataView(bytes.buffer);
5156
const parameters: ScryptParameters = {};
5257
parameters.logN = bytes[7] as logN;
@@ -74,15 +79,16 @@ export function formatPHC(
7479
salt: string | Uint8Array,
7580
): string {
7681
// convert salt to base64 without padding
77-
salt = encode(salt).replace(/=/g, "");
82+
salt = encodeBase64(salt).replace(/=/g, "");
7883
rawHash = rawHash.replace(/=/g, "");
7984
return `\$scrypt\$ln=${logN},r=${r},p=${p}\$${salt}\$${rawHash}`;
8085
}
8186
function decomposePHC(formattedHash: string): ScryptParameters {
82-
const regex = /\$scrypt\$ln=(?<logN>\d+),r=(?<r>\d+),p=(?<p>\d+)\$(?<salt>[a-zA-Z0-9\-\_\+\/\=]*)\$/;
87+
const regex =
88+
/\$scrypt\$ln=(?<logN>\d+),r=(?<r>\d+),p=(?<p>\d+)\$(?<salt>[a-zA-Z0-9\-\_\+\/\=]*)\$/;
8389
const parameters: ScryptParameters = formattedHash.match(regex)
8490
?.groups as ScryptParameters;
85-
parameters.salt = new Uint8Array(decode(parameters.salt as string));
91+
parameters.salt = new Uint8Array(decodeBase64(parameters.salt as string));
8692
// the PHC format from passlib always uses 32 bytes hashes
8793
parameters.dklen = 32;
8894
return parameters;

lib/hmac.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* A very primitive crypto.subtle.digestSync-based HMAC-SHA256 synchronous implementation.
3+
*/
4+
import { crypto } from "std/crypto/mod.ts";
5+
6+
function mergeArrays(a: Uint8Array, b: Uint8Array): Uint8Array {
7+
const result = new Uint8Array(a.length + b.length);
8+
result.set(a);
9+
result.set(b, a.length);
10+
return result;
11+
}
12+
function blockSizedKey(key: Uint8Array, blockSize: number): Uint8Array {
13+
if (key.length > blockSize) {
14+
return new Uint8Array(crypto.subtle.digestSync("SHA-256", key));
15+
} else if (key.length < blockSize) {
16+
const result = new Uint8Array(blockSize);
17+
result.set(key);
18+
return result;
19+
}
20+
return key;
21+
}
22+
23+
export function hmacSHA256(key: Uint8Array, data: Uint8Array): Uint8Array {
24+
const b_key = blockSizedKey(key, 64);
25+
const o_pad = new Uint8Array(64);
26+
const i_pad = new Uint8Array(64);
27+
for (let i = 0; i < 64; i++) {
28+
o_pad[i] = b_key[i] ^ 0x5c;
29+
i_pad[i] = b_key[i] ^ 0x36;
30+
}
31+
return new Uint8Array(crypto.subtle.digestSync(
32+
"SHA-256",
33+
mergeArrays(
34+
o_pad,
35+
new Uint8Array(
36+
crypto.subtle.digestSync("SHA-256", mergeArrays(i_pad, data)),
37+
),
38+
),
39+
));
40+
}

lib/hmac_bench.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { decodeHex } from "std/encoding/hex.ts";
2+
import { hmacSHA256 } from "./hmac.ts";
3+
import { HmacSha256 } from "https://deno.land/[email protected]/hash/sha256.ts";
4+
5+
Deno.bench("hmac - subtle", { group: "small hmac", baseline: true }, () => {
6+
hmacSHA256(
7+
decodeHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"),
8+
decodeHex("4869205468657265"),
9+
);
10+
});
11+
Deno.bench("hmac - old", { group: "small hmac" }, () => {
12+
const hmac = new HmacSha256(
13+
decodeHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"),
14+
);
15+
hmac.update(decodeHex("4869205468657265"));
16+
hmac.digest();
17+
});
18+
19+
Deno.bench("hmac - subtle", { group: "larger hmac", baseline: true }, () => {
20+
hmacSHA256(
21+
decodeHex(
22+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
23+
),
24+
decodeHex(
25+
"5468697320697320612074657374207573696e672061206c6172676572207468616e20626c6f636b2d73697a65206b657920616e642061206c6172676572207468616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565647320746f20626520686173686564206265666f7265206265696e6720757365642062792074686520484d414320616c676f726974686d2e",
26+
),
27+
);
28+
});
29+
Deno.bench("hmac - old", { group: "larger hmac" }, () => {
30+
const hmac = new HmacSha256(
31+
decodeHex(
32+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
33+
),
34+
);
35+
hmac.update(
36+
decodeHex(
37+
"5468697320697320612074657374207573696e672061206c6172676572207468616e20626c6f636b2d73697a65206b657920616e642061206c6172676572207468616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565647320746f20626520686173686564206265666f7265206265696e6720757365642062792074686520484d414320616c676f726974686d2e",
38+
),
39+
);
40+
hmac.digest();
41+
});
42+
43+
// appears to be comparable to the old implementation in practice
44+
Deno.bench("hmac - subtle", { group: "hmac as used", baseline: true }, () => {
45+
hmacSHA256(
46+
decodeHex(
47+
"834680896aab19cf86a4c0edf4cef4db8af5bd05a40d42e768e658057ee521e63acadefa59fb2f3133def01d2c3dd5d1",
48+
).subarray(0, 48),
49+
decodeHex(
50+
"024b5b12a8b3d622c289ad69536a30cda848074c82d06ff05775d653bf0fc48033ddafba8071b7f119810dd57619553e87aff3bc8c237669523dc6530b8ee267",
51+
).subarray(0, 64),
52+
);
53+
});
54+
Deno.bench("hmac - old", { group: "hmac as used" }, () => {
55+
const hmac = new HmacSha256(
56+
decodeHex(
57+
"834680896aab19cf86a4c0edf4cef4db8af5bd05a40d42e768e658057ee521e63acadefa59fb2f3133def01d2c3dd5d1",
58+
).subarray(0, 48),
59+
);
60+
hmac.update(
61+
decodeHex(
62+
"024b5b12a8b3d622c289ad69536a30cda848074c82d06ff05775d653bf0fc48033ddafba8071b7f119810dd57619553e87aff3bc8c237669523dc6530b8ee267",
63+
).subarray(0, 64),
64+
);
65+
hmac.digest();
66+
});

lib/hmac_test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { hmacSHA256 } from "./hmac.ts";
2+
import { decodeHex } from "std/encoding/hex.ts";
3+
import { assertEquals } from "std/assert/assert_equals.ts";
4+
5+
const encoder = new TextEncoder();
6+
7+
Deno.test("basic hmacSHA256", (): void => {
8+
const result = hmacSHA256(
9+
decodeHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"),
10+
decodeHex("4869205468657265"),
11+
);
12+
assertEquals(
13+
result,
14+
decodeHex(
15+
"b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7",
16+
),
17+
);
18+
});

0 commit comments

Comments
 (0)