Skip to content

Commit

Permalink
feat: write a simple crypto.subtle based hmac implementation to stop …
Browse files Browse the repository at this point in the history
…relying on old std
  • Loading branch information
oplik0 committed Dec 26, 2023
1 parent ef67ec9 commit d71d825
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 12 deletions.
30 changes: 18 additions & 12 deletions lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/**
* @todo document this module
*/
import { crypto } from "https://deno.land/std@0.152.0/crypto/mod.ts";
import { decode, encode } from "https://deno.land/std@0.152.0/encoding/base64.ts";
import { HmacSha256 } from "https://deno.land/std@0.152.0/hash/sha256.ts";
import { crypto } from "std/crypto/mod.ts";
import { decodeBase64, encodeBase64 } from "std/encoding/base64.ts";
import { hmacSHA256 } from "./hmac.ts";
// dprint-ignore-next-line
// deno-fmt-ignore
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;
export interface ScryptParameters {
logN?: logN;
Expand All @@ -26,27 +27,31 @@ export function formatScrypt(
const encoder = new TextEncoder();
const result = new Uint8Array(96);
const dataview = new DataView(result.buffer);
const hmac = new HmacSha256(new Uint8Array(decode(rawHash)).subarray(32));
// first 6 bytes are the word "scrypt", 7th byte is 0
result.set([115, 99, 114, 121, 112, 116, 0], 0);
dataview.setUint8(7, logN);
dataview.setUint32(8, r, false);
dataview.setUint32(12, p, false);
result.set(typeof salt === "string" ? encoder.encode(salt) : salt, 16);
const hashedResult = crypto.subtle.digestSync("SHA-256", result.subarray(0, 48));
const hashedResult = crypto.subtle.digestSync(
"SHA-256",
result.subarray(0, 48),
);
result.set(new Uint8Array(hashedResult), 48);
hmac.update(result.subarray(0, 64));
result.set(
hmac.array(),
hmacSHA256(
new Uint8Array(decodeBase64(rawHash)).subarray(32),
result.subarray(0, 64),
),
64,
);
// encode the result as a base64 string
return encode(result);
return encodeBase64(result);
}
function decomposeScrypt(
formattedHash: string,
): ScryptParameters {
const bytes: Uint8Array = new Uint8Array(decode(formattedHash));
const bytes: Uint8Array = new Uint8Array(decodeBase64(formattedHash));
const dataview: DataView = new DataView(bytes.buffer);
const parameters: ScryptParameters = {};
parameters.logN = bytes[7] as logN;
Expand Down Expand Up @@ -74,15 +79,16 @@ export function formatPHC(
salt: string | Uint8Array,
): string {
// convert salt to base64 without padding
salt = encode(salt).replace(/=/g, "");
salt = encodeBase64(salt).replace(/=/g, "");
rawHash = rawHash.replace(/=/g, "");
return `\$scrypt\$ln=${logN},r=${r},p=${p}\$${salt}\$${rawHash}`;
}
function decomposePHC(formattedHash: string): ScryptParameters {
const regex = /\$scrypt\$ln=(?<logN>\d+),r=(?<r>\d+),p=(?<p>\d+)\$(?<salt>[a-zA-Z0-9\-\_\+\/\=]*)\$/;
const regex =
/\$scrypt\$ln=(?<logN>\d+),r=(?<r>\d+),p=(?<p>\d+)\$(?<salt>[a-zA-Z0-9\-\_\+\/\=]*)\$/;
const parameters: ScryptParameters = formattedHash.match(regex)
?.groups as ScryptParameters;
parameters.salt = new Uint8Array(decode(parameters.salt as string));
parameters.salt = new Uint8Array(decodeBase64(parameters.salt as string));
// the PHC format from passlib always uses 32 bytes hashes
parameters.dklen = 32;
return parameters;
Expand Down
40 changes: 40 additions & 0 deletions lib/hmac.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* A very primitive crypto.subtle.digestSync-based HMAC-SHA256 synchronous implementation.
*/
import { crypto } from "std/crypto/mod.ts";

function mergeArrays(a: Uint8Array, b: Uint8Array): Uint8Array {
const result = new Uint8Array(a.length + b.length);
result.set(a);
result.set(b, a.length);
return result;
}
function blockSizedKey(key: Uint8Array, blockSize: number): Uint8Array {
if (key.length > blockSize) {
return new Uint8Array(crypto.subtle.digestSync("SHA-256", key));
} else if (key.length < blockSize) {
const result = new Uint8Array(blockSize);
result.set(key);
return result;
}
return key;
}

export function hmacSHA256(key: Uint8Array, data: Uint8Array): Uint8Array {
const b_key = blockSizedKey(key, 64);
const o_pad = new Uint8Array(64);
const i_pad = new Uint8Array(64);
for (let i = 0; i < 64; i++) {
o_pad[i] = b_key[i] ^ 0x5c;
i_pad[i] = b_key[i] ^ 0x36;
}
return new Uint8Array(crypto.subtle.digestSync(
"SHA-256",
mergeArrays(
o_pad,
new Uint8Array(
crypto.subtle.digestSync("SHA-256", mergeArrays(i_pad, data)),
),
),
));
}
66 changes: 66 additions & 0 deletions lib/hmac_bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { decodeHex } from "std/encoding/hex.ts";
import { hmacSHA256 } from "./hmac.ts";
import { HmacSha256 } from "https://deno.land/std@0.160.0/hash/sha256.ts";

Deno.bench("hmac - subtle", { group: "small hmac", baseline: true }, () => {
hmacSHA256(
decodeHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"),
decodeHex("4869205468657265"),
);
});
Deno.bench("hmac - old", { group: "small hmac" }, () => {
const hmac = new HmacSha256(
decodeHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"),
);
hmac.update(decodeHex("4869205468657265"));
hmac.digest();
});

Deno.bench("hmac - subtle", { group: "larger hmac", baseline: true }, () => {
hmacSHA256(
decodeHex(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
),
decodeHex(
"5468697320697320612074657374207573696e672061206c6172676572207468616e20626c6f636b2d73697a65206b657920616e642061206c6172676572207468616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565647320746f20626520686173686564206265666f7265206265696e6720757365642062792074686520484d414320616c676f726974686d2e",
),
);
});
Deno.bench("hmac - old", { group: "larger hmac" }, () => {
const hmac = new HmacSha256(
decodeHex(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
),
);
hmac.update(
decodeHex(
"5468697320697320612074657374207573696e672061206c6172676572207468616e20626c6f636b2d73697a65206b657920616e642061206c6172676572207468616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565647320746f20626520686173686564206265666f7265206265696e6720757365642062792074686520484d414320616c676f726974686d2e",
),
);
hmac.digest();
});

// appears to be comparable to the old implementation in practice
Deno.bench("hmac - subtle", { group: "hmac as used", baseline: true }, () => {
hmacSHA256(
decodeHex(
"834680896aab19cf86a4c0edf4cef4db8af5bd05a40d42e768e658057ee521e63acadefa59fb2f3133def01d2c3dd5d1",
).subarray(0, 48),
decodeHex(
"024b5b12a8b3d622c289ad69536a30cda848074c82d06ff05775d653bf0fc48033ddafba8071b7f119810dd57619553e87aff3bc8c237669523dc6530b8ee267",
).subarray(0, 64),
);
});
Deno.bench("hmac - old", { group: "hmac as used" }, () => {
const hmac = new HmacSha256(
decodeHex(
"834680896aab19cf86a4c0edf4cef4db8af5bd05a40d42e768e658057ee521e63acadefa59fb2f3133def01d2c3dd5d1",
).subarray(0, 48),
);
hmac.update(
decodeHex(
"024b5b12a8b3d622c289ad69536a30cda848074c82d06ff05775d653bf0fc48033ddafba8071b7f119810dd57619553e87aff3bc8c237669523dc6530b8ee267",
).subarray(0, 64),
);
hmac.digest();
});
18 changes: 18 additions & 0 deletions lib/hmac_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { hmacSHA256 } from "./hmac.ts";
import { decodeHex } from "std/encoding/hex.ts";
import { assertEquals } from "std/assert/assert_equals.ts";

const encoder = new TextEncoder();

Deno.test("basic hmacSHA256", (): void => {
const result = hmacSHA256(
decodeHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"),
decodeHex("4869205468657265"),
);
assertEquals(
result,
decodeHex(
"b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7",
),
);
});

0 comments on commit d71d825

Please sign in to comment.