From 33f2c43dfcc5a5e5b70b14745c8ffdd25f5badcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Fri, 9 Aug 2024 20:29:51 +0200 Subject: [PATCH 1/7] Improved parallelism of tests --- crypto/hash.test.ts | 64 ++++++++++++++++----------------- crypto/hash/argon2.test.ts | 74 +++++++++++++++++++------------------- crypto/hash/bcrypt.test.ts | 24 ++++++------- crypto/hash/scrypt.test.ts | 34 +++++++++--------- 4 files changed, 94 insertions(+), 102 deletions(-) diff --git a/crypto/hash.test.ts b/crypto/hash.test.ts index 896613c..c2fc339 100644 --- a/crypto/hash.test.ts +++ b/crypto/hash.test.ts @@ -1,40 +1,38 @@ import { assert, assertMatch, assertThrows } from "@std/assert"; import { hash, verify } from "./hash.ts"; -Deno.test("hash", async (t) => { - await t.step("unsupported", () => { - // deno-lint-ignore ban-ts-comment - // @ts-ignore - assertThrows(() => hash("unsupported", "password")); - // deno-lint-ignore ban-ts-comment - // @ts-ignore - assertThrows(() => verify("unsupported", "password", "")); - }); +Deno.test("hash() and verify() with unsupported", () => { + // deno-lint-ignore ban-ts-comment + // @ts-ignore + assertThrows(() => hash("unsupported", "password")); + // deno-lint-ignore ban-ts-comment + // @ts-ignore + assertThrows(() => verify("unsupported", "password", "")); +}); - await t.step("argon2", () => { - const h1 = hash("argon2", "password"); - assertMatch(h1, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/); - assert(verify("argon2", "password", h1)); - const h2 = hash({ name: "argon2" }, "password"); - assertMatch(h2, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/); - assert(verify({ name: "argon2" }, "password", h2)); - }); +Deno.test("hash() and verify() with argon2", () => { + const h1 = hash("argon2", "password"); + assertMatch(h1, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/); + assert(verify("argon2", "password", h1)); + const h2 = hash({ name: "argon2" }, "password"); + assertMatch(h2, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/); + assert(verify({ name: "argon2" }, "password", h2)); +}); - await t.step("bcrypt", () => { - const h1 = hash("bcrypt", "password"); - assertMatch(h1, /^\$2b\$12\$/); - assert(verify("bcrypt", "password", h1)); - const h2 = hash({ name: "bcrypt" }, "password"); - assertMatch(h2, /^\$2b\$12\$/); - assert(verify({ name: "bcrypt" }, "password", h2)); - }); +Deno.test("hash() and verify() with bcrypt", () => { + const h1 = hash("bcrypt", "password"); + assertMatch(h1, /^\$2b\$12\$/); + assert(verify("bcrypt", "password", h1)); + const h2 = hash({ name: "bcrypt" }, "password"); + assertMatch(h2, /^\$2b\$12\$/); + assert(verify({ name: "bcrypt" }, "password", h2)); +}); - await t.step("scrypt", () => { - const h1 = hash("scrypt", "password"); - assertMatch(h1, /^\$scrypt\$ln=17,r=8,p=1\$/); - assert(verify("scrypt", "password", h1)); - const h2 = hash({ name: "scrypt" }, "password"); - assertMatch(h2, /^\$scrypt\$ln=17,r=8,p=1\$/); - assert(verify({ name: "scrypt" }, "password", h2)); - }); +Deno.test("hash() and verify() with scrypt", () => { + const h1 = hash("scrypt", "password"); + assertMatch(h1, /^\$scrypt\$ln=17,r=8,p=1\$/); + assert(verify("scrypt", "password", h1)); + const h2 = hash({ name: "scrypt" }, "password"); + assertMatch(h2, /^\$scrypt\$ln=17,r=8,p=1\$/); + assert(verify({ name: "scrypt" }, "password", h2)); }); diff --git a/crypto/hash/argon2.test.ts b/crypto/hash/argon2.test.ts index 53c9da6..82e85ad 100644 --- a/crypto/hash/argon2.test.ts +++ b/crypto/hash/argon2.test.ts @@ -1,46 +1,44 @@ import { assert, assertMatch } from "@std/assert"; import { type Argon2Options, hash, verify } from "./argon2.ts"; -Deno.test("Argon2", async (t) => { - await t.step("defaults", () => { - const h = hash("password", {}); - assertMatch(h, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/); - assert(verify("password", h, {})); - }); +Deno.test("hash() and verify() with default arguments", () => { + const h = hash("password", {}); + assertMatch(h, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/); + assert(verify("password", h, {})); +}); - await t.step("Argon2i", () => { - const o = { algorithm: "argon2i" } satisfies Argon2Options; - const h = hash("password", o); - assertMatch(h, /^\$argon2i\$v=19\$m=19456,t=2,p=1\$/); - assert(verify("password", h, o)); - }); +Deno.test("hash() and verify() with argon2i", () => { + const o = { algorithm: "argon2i" } satisfies Argon2Options; + const h = hash("password", o); + assertMatch(h, /^\$argon2i\$v=19\$m=19456,t=2,p=1\$/); + assert(verify("password", h, o)); +}); - await t.step("Argon2d", () => { - const o = { algorithm: "argon2d" } satisfies Argon2Options; - const h = hash("password", o); - assertMatch(h, /^\$argon2d\$v=19\$m=19456,t=2,p=1\$/); - assert(verify("password", h, o)); - }); +Deno.test("hash() and verify() with argon2d", () => { + const o = { algorithm: "argon2d" } satisfies Argon2Options; + const h = hash("password", o); + assertMatch(h, /^\$argon2d\$v=19\$m=19456,t=2,p=1\$/); + assert(verify("password", h, o)); +}); - await t.step("wrong algoritm", () => { - // deno-lint-ignore ban-ts-comment - // @ts-ignore - const o = { algorithm: "asdfasdf" } as Argon2Options; - const h = hash("password", o); - assertMatch(h, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/); - assert(verify("password", h, o)); - }); +Deno.test("hash() and verify() with wrong algorithm", () => { + // deno-lint-ignore ban-ts-comment + // @ts-ignore + const o = { algorithm: "asdfasdf" } as Argon2Options; + const h = hash("password", o); + assertMatch(h, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/); + assert(verify("password", h, o)); +}); - await t.step("all options", () => { - const o = { - algorithm: "argon2id", - memoryCost: 10000, - timeCost: 3, - parallelism: 2, - outputLength: 16, - } satisfies Argon2Options; - const h = hash("password", o); - assertMatch(h, /^\$argon2id\$v=19\$m=10000,t=3,p=2\$/); - assert(verify("password", h, o)); - }); +Deno.test("hash() and verify() with all options", () => { + const o = { + algorithm: "argon2id", + memoryCost: 10000, + timeCost: 3, + parallelism: 2, + outputLength: 16, + } satisfies Argon2Options; + const h = hash("password", o); + assertMatch(h, /^\$argon2id\$v=19\$m=10000,t=3,p=2\$/); + assert(verify("password", h, o)); }); diff --git a/crypto/hash/bcrypt.test.ts b/crypto/hash/bcrypt.test.ts index 068bed4..3f0d325 100644 --- a/crypto/hash/bcrypt.test.ts +++ b/crypto/hash/bcrypt.test.ts @@ -1,18 +1,16 @@ import { assert, assertMatch } from "@std/assert"; import { type BcryptOptions, hash, verify } from "./bcrypt.ts"; -Deno.test("Bcrypt", async (t) => { - await t.step("defaults", () => { - const o = {} as BcryptOptions; - const h = hash("password", o); - assertMatch(h, /^\$2b\$12\$/); - assert(verify("password", h, o)); - }); +Deno.test("hash() and verify() with defaults", () => { + const o = {} as BcryptOptions; + const h = hash("password", o); + assertMatch(h, /^\$2b\$12\$/); + assert(verify("password", h, o)); +}); - await t.step("cost 4", () => { - const o = { cost: 4 } as BcryptOptions; - const h = hash("password", o); - assertMatch(h, /^\$2b\$04\$/); - assert(verify("password", h, o)); - }); +Deno.test("hash() and verify() with all options", () => { + const o = { cost: 4 } as BcryptOptions; + const h = hash("password", o); + assertMatch(h, /^\$2b\$04\$/); + assert(verify("password", h, o)); }); diff --git a/crypto/hash/scrypt.test.ts b/crypto/hash/scrypt.test.ts index 0ab1dcd..3558ebf 100644 --- a/crypto/hash/scrypt.test.ts +++ b/crypto/hash/scrypt.test.ts @@ -1,23 +1,21 @@ import { assert, assertMatch } from "@std/assert"; import { hash, type ScryptOptions, verify } from "./scrypt.ts"; -Deno.test("Scrypt", async (t) => { - await t.step("defaults", () => { - const o = {} as ScryptOptions; - const h = hash("password", o); - assertMatch(h, /^\$scrypt\$ln=17,r=8,p=1\$/); - assert(verify("password", h, o)); - }); +Deno.test("hash() and verify() with defaults", () => { + const o = {} as ScryptOptions; + const h = hash("password", o); + assertMatch(h, /^\$scrypt\$ln=17,r=8,p=1\$/); + assert(verify("password", h, o)); +}); - await t.step("all config", () => { - const o = { - logN: 1, - blockSize: 1, - parallelism: 2, - keyLenght: 16, - } as ScryptOptions; - const h = hash("password", o); - assertMatch(h, /^\$scrypt\$ln=1,r=1,p=2\$/); - assert(verify("password", h, o)); - }); +Deno.test("hash() and verify() with all options", () => { + const o = { + logN: 1, + blockSize: 1, + parallelism: 2, + keyLenght: 16, + } as ScryptOptions; + const h = hash("password", o); + assertMatch(h, /^\$scrypt\$ln=1,r=1,p=2\$/); + assert(verify("password", h, o)); }); From 81304d165bd900e32bd84f60bba144877edd2f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Fri, 9 Aug 2024 20:30:05 +0200 Subject: [PATCH 2/7] added bencmarks --- crypto/hash.bench.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 crypto/hash.bench.ts diff --git a/crypto/hash.bench.ts b/crypto/hash.bench.ts new file mode 100644 index 0000000..af3d832 --- /dev/null +++ b/crypto/hash.bench.ts @@ -0,0 +1,37 @@ +import { hash, verify } from "./hash.ts"; + +Deno.bench("hash() with argon2", () => { + hash("argon2", "password"); +}); + +Deno.bench("hash() with bcrypt", () => { + hash("bcrypt", "password"); +}); + +Deno.bench("hash() with scrypt", () => { + hash("scrypt", "password"); +}); + +Deno.bench("verify() with argon2", () => { + verify( + "argon2", + "password", + "$argon2id$v=19$m=19456,t=2,p=1$sgg3gflK2pkatSfTYkQTtA$UvKPnIcKDBfK9d4v4ItjRYra//s9uuFJgMisTNC+Wcw", + ); +}); + +Deno.bench("verify() with bcrypt", () => { + verify( + "bcrypt", + "password", + "$2b$12$GUvwcP3VbNvmKDzl114sW.DVt.1xX9N7OmWk80OWLjigWIW/3n66G", + ); +}); + +Deno.bench("verify() with scrypt", () => { + verify( + "scrypt", + "password", + "$scrypt$ln=17,r=8,p=1$y8d9gN0rKwW7z+hJb/vQAA$w+VLelvZVpZ0zt/+svlPbZFHDTl+jL5Xvp+YKrZEyKE", + ); +}); From c69d9355ceb1d00a389a787b3bcb158cc3ae55b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Fri, 9 Aug 2024 20:31:05 +0200 Subject: [PATCH 3/7] Updated workspace support --- crypto/deno.json | 1 - deno.json | 7 ++++--- encoding/deno.json | 1 - http/deno.json | 1 - 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/crypto/deno.json b/crypto/deno.json index 24dbe99..a30f2b1 100644 --- a/crypto/deno.json +++ b/crypto/deno.json @@ -1,7 +1,6 @@ { "version": "0.0.5", "name": "@stdext/crypto", - "lock": false, "exports": { "./hash": "./hash.ts" }, diff --git a/deno.json b/deno.json index 1df3aad..981d23b 100644 --- a/deno.json +++ b/deno.json @@ -1,8 +1,9 @@ { "lock": false, "imports": { - "@std/assert": "jsr:@std/assert@^0.224.0", - "@std/path": "jsr:@std/path@^0.224.0" + "@std/assert": "jsr:@std/assert@^1.0.2", + "@std/path": "jsr:@std/path@^1.0.2", + "@std/encoding": "jsr:@std/encoding@^1.0.1" }, "tasks": { "bump_version": "deno run --allow-env=VERSION --allow-read=. --allow-write=. ./_tools/bump_version.ts", @@ -16,7 +17,7 @@ "format": "deno fmt && deno task --cwd crypto format", "format:check": "deno fmt --check && deno task --cwd crypto format:check" }, - "workspaces": [ + "workspace": [ "./crypto", "./encoding", "./http" diff --git a/encoding/deno.json b/encoding/deno.json index 15ff8bd..439b462 100644 --- a/encoding/deno.json +++ b/encoding/deno.json @@ -1,7 +1,6 @@ { "version": "0.0.5", "name": "@stdext/encoding", - "lock": false, "exports": { "./hex": "./hex.ts" } diff --git a/http/deno.json b/http/deno.json index 230de17..8973c68 100644 --- a/http/deno.json +++ b/http/deno.json @@ -1,7 +1,6 @@ { "version": "0.0.5", "name": "@stdext/http", - "lock": false, "exports": { "./header": "./header.ts", "./method": "./method.ts" From d24cfc4a181756614361c0470e13ba399b030af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sat, 10 Aug 2024 01:52:11 +0200 Subject: [PATCH 4/7] Added totp and hotp --- crypto/README.md | 34 ++++++++++++++++++ crypto/deno.json | 8 ++++- crypto/hotp.bench.ts | 11 ++++++ crypto/hotp.test.ts | 63 +++++++++++++++++++++++++++++++++ crypto/hotp.ts | 81 +++++++++++++++++++++++++++++++++++++++++++ crypto/totp.bench.ts | 11 ++++++ crypto/totp.test.ts | 22 ++++++++++++ crypto/totp.ts | 31 +++++++++++++++++ crypto/utils.bench.ts | 14 ++++++++ crypto/utils.test.ts | 12 +++++++ crypto/utils.ts | 27 +++++++++++++++ 11 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 crypto/hotp.bench.ts create mode 100644 crypto/hotp.test.ts create mode 100644 crypto/hotp.ts create mode 100644 crypto/totp.bench.ts create mode 100644 crypto/totp.test.ts create mode 100644 crypto/totp.ts create mode 100644 crypto/utils.bench.ts create mode 100644 crypto/utils.test.ts create mode 100644 crypto/utils.ts diff --git a/crypto/README.md b/crypto/README.md index c1b5d5e..e95a624 100644 --- a/crypto/README.md +++ b/crypto/README.md @@ -13,9 +13,43 @@ The following algorithms are provided: - Argon2 - Bcrypt +- Scrypt ```ts import { hash, verify } from "@stdext/crypto/hash"; const h = hash("argon2", "password"); verify("argon2", "password", h); ``` + +### HOTP (HMAC One-Time Password) + +```ts +import { generateHotp, verifyHotp } from "@stdext/crypto/hotp"; +import { generateSecret } from "@stdext/crypto/utils"; + +const secret = generateSecret(); +const hotp = generateHotp(secret, 42); +verifyHotp(hotp, secret, 42); +``` + +### TOTP (Time-based One-Time Password) + +```ts +import { generateTotp, verifyTotp } from "@stdext/crypto/totp"; +import { generateSecret } from "@stdext/crypto/utils"; + +const secret = generateSecret(); +const totp = generateTotp(secret, 42); +verifyTotp(totp, secret, 42); +``` + +### Utils + +```ts +import { generateSecretBytes } from "@stdext/crypto/utils"; +import { encodeBase64 } from "@std/encoding"; + +const secretBytes = generateSecretBytes(); +// You can select your own encoding +const encodedSecret = encodeBase64(secretBytes); +``` diff --git a/crypto/deno.json b/crypto/deno.json index a30f2b1..a1c13c1 100644 --- a/crypto/deno.json +++ b/crypto/deno.json @@ -2,7 +2,13 @@ "version": "0.0.5", "name": "@stdext/crypto", "exports": { - "./hash": "./hash.ts" + "./hash": "./hash.ts", + "./hash/argon2": "./hash/argon2.ts", + "./hash/bcrypt": "./hash/bcrypt.ts", + "./hash/scrypt": "./hash/scrypt.ts", + "./hotp": "./hotp.ts", + "./totp": "./totp.ts", + "./utils": "./utils.ts" }, "tasks": { "build": "deno task build:argon2 && deno task build:bcrypt && deno task build:scrypt", diff --git a/crypto/hotp.bench.ts b/crypto/hotp.bench.ts new file mode 100644 index 0000000..72f0ca0 --- /dev/null +++ b/crypto/hotp.bench.ts @@ -0,0 +1,11 @@ +import { generateHotp, verifyHotp } from "./hotp.ts"; + +const secret = "OCOMBLGUREYUXFQJIL75FQFCKYFCKLQP"; + +Deno.bench("generateHotp()", async () => { + await generateHotp(secret, 1000000000), "270103"; +}); + +Deno.bench("verifyHotp()", async () => { + await verifyHotp("270103", secret, 1000000000); +}); diff --git a/crypto/hotp.test.ts b/crypto/hotp.test.ts new file mode 100644 index 0000000..793dd1c --- /dev/null +++ b/crypto/hotp.test.ts @@ -0,0 +1,63 @@ +import { assert, assertEquals } from "@std/assert"; +import { + counterToBuffer, + generateHmacSha1, + generateHotp, + truncate, + verifyHotp, +} from "./hotp.ts"; +import { decodeBase32, encodeHex } from "@std/encoding"; + +const secret = "OCOMBLGUREYUXFQJIL75FQFCKYFCKLQP"; +const secretBytes = decodeBase32(secret); + +Deno.test("counterToBuffer()", () => { + assertEquals( + new Uint8Array(counterToBuffer(0).buffer), + new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]), + ); + assertEquals( + new Uint8Array(counterToBuffer(100).buffer), + new Uint8Array([0, 0, 0, 0, 0, 0, 0, 100]), + ); + assertEquals( + new Uint8Array(counterToBuffer(1000).buffer), + new Uint8Array([0, 0, 0, 0, 0, 0, 3, 232]), + ); + assertEquals( + new Uint8Array(counterToBuffer(1000000000000).buffer), + new Uint8Array([0, 0, 0, 232, 212, 165, 16, 0]), + ); +}); + +Deno.test("generateHmacSha1() should generate hashes", async () => { + const data = new Uint8Array([0, 0, 0, 0, 0, 0, 3, 232]); + const hmac = await generateHmacSha1(secretBytes, data); + assertEquals(encodeHex(hmac), "a5a0ee362d45dcb90fba5efb57ac90c2903b2c59"); +}); + +Deno.test("truncate() should output a numbered string", () => { + const data = new Uint8Array([0, 0, 0, 232, 212, 165, 16, 0]); + const numberedString0 = truncate(data, 0); + assertEquals(numberedString0, "0"); + const numberedString1 = truncate(data, 1); + assertEquals(numberedString1, "2"); + const numberedString6 = truncate(data, 6); + assertEquals(numberedString6, "000232"); + const numberedString10 = truncate(data, 10); + assertEquals(numberedString10, "0000000232"); +}); + +Deno.test("generateHotp()", async () => { + assertEquals(await generateHotp(secret, 0), "187492"); + assertEquals(await generateHotp(secret, 100), "907306"); + assertEquals(await generateHotp(secret, 1000), "303255"); + assertEquals(await generateHotp(secret, 1000000000000), "270103"); +}); + +Deno.test("verifyHotp()", async () => { + assert(await verifyHotp("187492", secret, 0)); + assert(await verifyHotp("907306", secret, 100)); + assert(await verifyHotp("303255", secret, 1000)); + assert(await verifyHotp("270103", secret, 1000000000000)); +}); diff --git a/crypto/hotp.ts b/crypto/hotp.ts new file mode 100644 index 0000000..88fc1d9 --- /dev/null +++ b/crypto/hotp.ts @@ -0,0 +1,81 @@ +import { decodeBase32 } from "@std/encoding"; + +/** Converts a counter value to a DataView. + * + * @ignore + */ +export function counterToBuffer(counter: number): DataView { + const buffer = new DataView(new ArrayBuffer(8)); + buffer.setBigUint64(0, BigInt(counter), false); + return buffer; +} + +/** Generates a HMAC-SHA1 hash of the specified key and counter. + * + * @ignore + */ +export async function generateHmacSha1( + key: Uint8Array, + data: BufferSource, +): Promise { + const importedKey = await crypto.subtle.importKey( + "raw", + key, + { name: "HMAC", hash: "SHA-1" }, + false, + ["sign"], + ); + + const signedData = await crypto.subtle.sign( + "HMAC", + importedKey, + data, + ); + + return new Uint8Array(signedData); +} + +/** Truncates the HMAC value to a 6-digit HOTP value. + * + * @ignore + */ +export function truncate(value: Uint8Array, length: number): string { + const offset = value[19] & 0xf; + const code = (value[offset] & 0x7f) << 24 | + (value[offset + 1] & 0xff) << 16 | + (value[offset + 2] & 0xff) << 8 | + (value[offset + 3] & 0xff); + const digits = code % Math.pow(10, length); + return digits.toString().padStart(length, "0"); +} + +/** Generates a HMAC-based one-time password (HOTP) using the specified key and counter. + * + * @param key a secret key used to generate the HOTP. Can be a string in base32 encoding or a Uint8Array. + * @param counter a counter value used to generate the HOTP. + * @returns a 6-digit HOTP value. + */ +export async function generateHotp( + key: string | Uint8Array, + counter: number, +): Promise { + const parsedKey = typeof key === "string" ? decodeBase32(key) : key; + const buffer = counterToBuffer(counter); + + const hmac = await generateHmacSha1(parsedKey, buffer); + return truncate(hmac, 6); +} + +/** Verifies a HMAC-based one-time password (HOTP) using the specified key and counter. + * + * @param otp the one-time password to verify. + * @param key a secret key used to generate the HOTP. Can be a string in base32 encoding or a Uint8Array. + * @param counter a counter value used to generate the HOTP. + */ +export async function verifyHotp( + otp: string, + key: string | Uint8Array, + counter: number, +): Promise { + return otp === await generateHotp(key, counter); +} diff --git a/crypto/totp.bench.ts b/crypto/totp.bench.ts new file mode 100644 index 0000000..665f310 --- /dev/null +++ b/crypto/totp.bench.ts @@ -0,0 +1,11 @@ +import { generateTotp, verifyTotp } from "./totp.ts"; + +const secret = "OCOMBLGUREYUXFQJIL75FQFCKYFCKLQP"; + +Deno.bench("generateTotp()", async () => { + await generateTotp(secret, 1000000000), "270103"; +}); + +Deno.bench("verifyTotp()", async () => { + await verifyTotp("270103", secret, 1000000000); +}); diff --git a/crypto/totp.test.ts b/crypto/totp.test.ts new file mode 100644 index 0000000..aafdab3 --- /dev/null +++ b/crypto/totp.test.ts @@ -0,0 +1,22 @@ +import { assert, assertEquals } from "@std/assert"; +import { generateTotp, verifyTotp } from "./totp.ts"; + +const secret = "OCOMBLGUREYUXFQJIL75FQFCKYFCKLQP"; +const t = 1704067200000; + +Deno.test("generateTotp()", async () => { + console.log(); + assertEquals(await generateTotp(secret, 0, t), "342743"); + assertEquals(await generateTotp(secret, 944996400, t), "149729"); + assertEquals(await generateTotp(secret, 976618800, t), "372018"); + assertEquals(await generateTotp(secret, 1723245550, t), "665341"); + assertEquals(await generateTotp(secret, t, t), "187492"); +}); + +Deno.test("verifyTotp()", async () => { + assert(await verifyTotp("342743", secret, 0, t)); + assert(await verifyTotp("149729", secret, 944996400, t)); + assert(await verifyTotp("372018", secret, 976618800, t)); + assert(await verifyTotp("665341", secret, 1723245550, t)); + assert(await verifyTotp("187492", secret, t, t)); +}); diff --git a/crypto/totp.ts b/crypto/totp.ts new file mode 100644 index 0000000..e4e5378 --- /dev/null +++ b/crypto/totp.ts @@ -0,0 +1,31 @@ +import { generateHotp } from "./hotp.ts"; + +/** Generate a TOTP value from a key and a time. + * + * @param key a secret key used to generate the HOTP. Can be a string in base32 encoding or a Uint8Array. + * @param t0 the initial time to use for the counter. + * @returns the TOTP value. + */ +export function generateTotp( + key: string | Uint8Array, + t0 = 0, + t = Date.now(), +): Promise { + const counter = Math.floor((t - t0) / 30000); + return generateHotp(key, counter); +} + +/** Verifies a TOTP value from a key and a time. + * + * @param otp the one-time password to verify. + * @param key a secret key used to generate the HOTP. Can be a string in base32 encoding or a Uint8Array. + * @param t0 the initial time to use for the counter. + */ +export async function verifyTotp( + otp: string, + key: string | Uint8Array, + t0 = 0, + t = Date.now(), +): Promise { + return otp === await generateTotp(key, t0, t); +} diff --git a/crypto/utils.bench.ts b/crypto/utils.bench.ts new file mode 100644 index 0000000..9d0c590 --- /dev/null +++ b/crypto/utils.bench.ts @@ -0,0 +1,14 @@ +import { assertEquals } from "@std/assert"; +import { generateSecret, generateSecretBytes } from "./utils.ts"; + +for (const l of [1, 8, 20, 100, 1000]) { + Deno.bench(`generateSecretBytes - ${l}`, () => { + const secretBytes = generateSecretBytes(l); + assertEquals(secretBytes.length, l); + }); + + Deno.bench(`generateSecret - ${l}`, () => { + const secret = generateSecret(l); + assertEquals(secret.length, l); + }); +} diff --git a/crypto/utils.test.ts b/crypto/utils.test.ts new file mode 100644 index 0000000..217e8fa --- /dev/null +++ b/crypto/utils.test.ts @@ -0,0 +1,12 @@ +import { assertEquals } from "@std/assert"; +import { generateSecret, generateSecretBytes } from "./utils.ts"; + +Deno.test("generateSecretBytes - default", () => { + const secretBytes = generateSecretBytes(); + assertEquals(secretBytes.length, 20); +}); + +Deno.test("generateSecret - default", () => { + const secret = generateSecret(); + assertEquals(secret.length, 20); +}); diff --git a/crypto/utils.ts b/crypto/utils.ts new file mode 100644 index 0000000..513343b --- /dev/null +++ b/crypto/utils.ts @@ -0,0 +1,27 @@ +import { encodeBase32 } from "@std/encoding"; + +/** Generates a secret key of the specified length. + * + * @param length how many bytes the secret key should be. + * @returns a secret as a byte array + */ +export function generateSecretBytes(length: number = 20): Uint8Array { + const buffer = new Uint8Array(length); + crypto.getRandomValues(buffer); + return buffer; +} + +/** Generates a secret key of the specified length in selected encoding + * + * @param length how many characters the secret key should be. + * @returns a secret string + */ +export function generateSecret( + length: number = 20, +): string { + const buffer = generateSecretBytes(Math.max(6, length)); + + const encoded = encodeBase32(buffer); + + return encoded.replaceAll("=", "").slice(-length); +} From 0e12a24bf23b5db1c724be33a501145f15c88db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sat, 10 Aug 2024 01:57:42 +0200 Subject: [PATCH 5/7] Fixed typings --- _tools/bump_version.ts | 2 +- crypto/hash/argon2.ts | 3 ++- crypto/hash/bcrypt.ts | 3 ++- crypto/hash/scrypt.ts | 3 ++- crypto/totp.ts | 8 ++++---- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/_tools/bump_version.ts b/_tools/bump_version.ts index adab23b..04aee99 100644 --- a/_tools/bump_version.ts +++ b/_tools/bump_version.ts @@ -18,7 +18,7 @@ async function updateMetaVersion(filepath: string, version: string) { ); } -const { workspaces } = meta; +const { workspace: workspaces } = meta; const version = Deno.env.get("VERSION"); diff --git a/crypto/hash/argon2.ts b/crypto/hash/argon2.ts index 29347a6..1b5f92f 100644 --- a/crypto/hash/argon2.ts +++ b/crypto/hash/argon2.ts @@ -2,9 +2,10 @@ import { type Argon2Algorithm, type Argon2Options, instantiate, + type InstantiateResult, } from "./_wasm/lib/deno_stdext_crypto_hash_wasm_argon2.generated.mjs"; -const instance = instantiate(); +const instance: InstantiateResult["exports"] = instantiate(); export type { Argon2Algorithm, Argon2Options }; diff --git a/crypto/hash/bcrypt.ts b/crypto/hash/bcrypt.ts index a72ac53..d07dc92 100644 --- a/crypto/hash/bcrypt.ts +++ b/crypto/hash/bcrypt.ts @@ -1,9 +1,10 @@ import { type BcryptOptions, instantiate, + type InstantiateResult, } from "./_wasm/lib/deno_stdext_crypto_hash_wasm_bcrypt.generated.mjs"; -const instance = instantiate(); +const instance: InstantiateResult["exports"] = instantiate(); export type { BcryptOptions }; diff --git a/crypto/hash/scrypt.ts b/crypto/hash/scrypt.ts index 95aecff..7d05d85 100644 --- a/crypto/hash/scrypt.ts +++ b/crypto/hash/scrypt.ts @@ -1,9 +1,10 @@ import { instantiate, + type InstantiateResult, type ScryptOptions, } from "./_wasm/lib/deno_stdext_crypto_hash_wasm_scrypt.generated.mjs"; -const instance = instantiate(); +const instance: InstantiateResult["exports"] = instantiate(); export type { ScryptOptions }; diff --git a/crypto/totp.ts b/crypto/totp.ts index e4e5378..8d578ea 100644 --- a/crypto/totp.ts +++ b/crypto/totp.ts @@ -8,8 +8,8 @@ import { generateHotp } from "./hotp.ts"; */ export function generateTotp( key: string | Uint8Array, - t0 = 0, - t = Date.now(), + t0: number = 0, + t: number = Date.now(), ): Promise { const counter = Math.floor((t - t0) / 30000); return generateHotp(key, counter); @@ -24,8 +24,8 @@ export function generateTotp( export async function verifyTotp( otp: string, key: string | Uint8Array, - t0 = 0, - t = Date.now(), + t0: number = 0, + t: number = Date.now(), ): Promise { return otp === await generateTotp(key, t0, t); } From 6802519cb23673f8d51697748b978647987acc0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sat, 10 Aug 2024 02:21:04 +0200 Subject: [PATCH 6/7] Deprecated headers and methods --- DEPRECATIONS.md | 20 ++++++++++++++++++++ http/header.ts | 21 +++++++++++++++++++++ http/method.ts | 13 +++++++++++++ 3 files changed, 54 insertions(+) diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index 916dc70..9fe07a3 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -1,3 +1,23 @@ # Deprecations This document contains information about deprecated modules and functions. + +- `0.0.5` + - `HttpHeaderPermanent`: Has been added to + [@std/http@1.0.2/header](https://jsr.io/@std/http@1.0.2/doc/header/~) + - `HttpMethodRfc9110`: Has been added to + [@std/http@1.0.2/header](https://jsr.io/@std/http@1.0.2/doc/header/~) + - `HttpHeaderDeprecated`: Has been added to + [@std/http@1.0.2/header](https://jsr.io/@std/http@1.0.2/doc/header/~) + - `HttpHeaderObsoleted`: Has been added to + [@std/http@1.0.2/header](https://jsr.io/@std/http@1.0.2/doc/header/~) + - `HttpHeaderProvisional`: Has been added to + [@std/http@1.0.2/header](https://jsr.io/@std/http@1.0.2/doc/header/~) + - `HttpHeader`: Has been added to + [@std/http@1.0.2/header](https://jsr.io/@std/http@1.0.2/doc/header/~) + - `HttpMethodRfc9110`: Has been added to + [@std/http@1.0.2/method](https://jsr.io/@std/http@1.0.2/doc/method/~) + - `HttpMethodIana`: Has been added to + [@std/http@1.0.2/method](https://jsr.io/@std/http@1.0.2/doc/method/~) + - `HttpMethod`: Has been added to + [@std/http@1.0.2/method](https://jsr.io/@std/http@1.0.2/doc/method/~) diff --git a/http/header.ts b/http/header.ts index cfcda65..a0ca958 100644 --- a/http/header.ts +++ b/http/header.ts @@ -4,10 +4,12 @@ * This module is generated from {@link https://www.iana.org/assignments/http-fields/http-fields.xhtml#field-names | IANA Hypertext Transfer Protocol (HTTP) Field Name Registry} * * @module + * @deprecated (0.0.5) Use @std/http/header instead, see DEPRECATION.md for more info */ /** * HTTP Headers with status permanent + * @deprecated (0.0.5) Use @std/http/header instead, see DEPRECATION.md for more info */ export const HttpHeaderPermanent = { /** @@ -1192,11 +1194,15 @@ export const HttpHeaderPermanent = { XFrameOptions: "X-Frame-Options", } as const; +/** + * @deprecated (0.0.5) Use @std/http/header instead, see DEPRECATION.md for more info + */ export type HttpHeaderPermanent = typeof HttpHeaderPermanent[keyof typeof HttpHeaderPermanent]; /** * HTTP Headers with status deprecated + * @deprecated (0.0.5) Use @std/http/header instead, see DEPRECATION.md for more info */ export const HttpHeaderDeprecated = { /** @@ -1259,11 +1265,15 @@ export const HttpHeaderDeprecated = { ProtocolQuery: "Protocol-Query", } as const; +/** + * @deprecated (0.0.5) Use @std/http/header instead, see DEPRECATION.md for more info + */ export type HttpHeaderDeprecated = typeof HttpHeaderDeprecated[keyof typeof HttpHeaderDeprecated]; /** * HTTP Headers with status obsoleted + * @deprecated (0.0.5) Use @std/http/header instead, see DEPRECATION.md for more info */ export const HttpHeaderObsoleted = { /** @@ -1555,11 +1565,15 @@ export const HttpHeaderObsoleted = { Warning: "Warning", } as const; +/** + * @deprecated (0.0.5) Use @std/http/header instead, see DEPRECATION.md for more info + */ export type HttpHeaderObsoleted = typeof HttpHeaderObsoleted[keyof typeof HttpHeaderObsoleted]; /** * HTTP Headers with status provisional + * @deprecated (0.0.5) Use @std/http/header instead, see DEPRECATION.md for more info */ export const HttpHeaderProvisional = { /** @@ -1671,6 +1685,9 @@ export const HttpHeaderProvisional = { TimingAllowOrigin: "Timing-Allow-Origin", } as const; +/** + * @deprecated (0.0.5) Use @std/http/header instead, see DEPRECATION.md for more info + */ export type HttpHeaderProvisional = typeof HttpHeaderProvisional[keyof typeof HttpHeaderProvisional]; @@ -1678,6 +1695,7 @@ export type HttpHeaderProvisional = * All HTTP Headers according to {@link https://www.iana.org/assignments/http-fields/http-fields.xhtml#field-names | IANA Hypertext Transfer Protocol (HTTP) Field Name Registry} * * @see {@link https://www.iana.org/assignments/http-fields/http-fields.xhtml#field-names | IANA Hypertext Transfer Protocol (HTTP) Field Name Registry} + * @deprecated (0.0.5) Use @std/http/header instead, see DEPRECATION.md for more info */ export const HttpHeader = { ...HttpHeaderPermanent, @@ -1686,4 +1704,7 @@ export const HttpHeader = { ...HttpHeaderProvisional, }; +/** + * @deprecated (0.0.5) Use @std/http/header instead, see DEPRECATION.md for more info + */ export type HttpHeader = typeof HttpHeader[keyof typeof HttpHeader]; diff --git a/http/method.ts b/http/method.ts index 26d53db..275148c 100644 --- a/http/method.ts +++ b/http/method.ts @@ -4,12 +4,14 @@ * This module is generated from {@link https://www.iana.org/assignments/http-methods/http-methods.xhtml#methods | IANA Hypertext Transfer Protocol (HTTP) Method Registry} * * @module + * @deprecated (0.0.5) Use @std/http/method instead, see DEPRECATION.md for more info */ /** * HTTP Methods as defined by RFC 9110 * * @see {@link https://www.iana.org/go/rfc9110 | RFC9110, Section 9.3} + * @deprecated (0.0.5) Use @std/http/method instead, see DEPRECATION.md for more info */ export const HttpMethodRfc9110 = { /** @@ -69,6 +71,9 @@ export const HttpMethodRfc9110 = { Trace: "TRACE", }; +/** + * @deprecated (0.0.5) Use @std/http/method instead, see DEPRECATION.md for more info + */ export type HttpMethodRfc9110 = typeof HttpMethodRfc9110[keyof typeof HttpMethodRfc9110]; @@ -76,6 +81,7 @@ export type HttpMethodRfc9110 = * HTTP Methods as defined by IANA Hypertext Transfer Protocol (HTTP) Method Registry * * @see {@link https://www.iana.org/assignments/http-methods/http-methods.xhtml#methods | IANA Hypertext Transfer Protocol (HTTP) Method Registry} + * @deprecated (0.0.5) Use @std/http/method instead, see DEPRECATION.md for more info */ export const HttpMethodIana = { /** @@ -337,6 +343,9 @@ export const HttpMethodIana = { VersionControl: "VERSION-CONTROL", } as const; +/** + * @deprecated (0.0.5) Use @std/http/method instead, see DEPRECATION.md for more info + */ export type HttpMethodIana = typeof HttpMethodIana[keyof typeof HttpMethodIana]; /** @@ -344,10 +353,14 @@ export type HttpMethodIana = typeof HttpMethodIana[keyof typeof HttpMethodIana]; * * @see {@link https://www.iana.org/go/rfc9110 | RFC9110, Section 9.3} * @see {@link https://www.iana.org/assignments/http-methods/http-methods.xhtml#methods | IANA Hypertext Transfer Protocol (HTTP) Method Registry} + * @deprecated (0.0.5) Use @std/http/method instead, see DEPRECATION.md for more info */ export const HttpMethod = { ...HttpMethodRfc9110, ...HttpMethodIana, } as const; +/** + * @deprecated (0.0.5) Use @std/http/method instead, see DEPRECATION.md for more info + */ export type HttpMethod = typeof HttpMethod[keyof typeof HttpMethod]; From 70de03680e2deb961edcbaaad342f7f6aa1cf556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sat, 10 Aug 2024 02:21:37 +0200 Subject: [PATCH 7/7] Updated changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aa40fa..dfc5fab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to ## [Unreleased] +- feat(crypto): added TOTP and HOTP +- deprecated(http/header): Deprecated @stdext/http/header as it is added to + @std/http/header +- deprecated(http/method): Deprecated @stdext/http/method as it is added to + @std/http/method + ## [0.0.5] - 2024-05-06 ### Changed