Skip to content

Commit

Permalink
Merge pull request #7 from halvardssm/feat/totp
Browse files Browse the repository at this point in the history
feat(crypto): Add TOTP and HOTP
  • Loading branch information
halvardssm authored Aug 10, 2024
2 parents 86eca36 + 70de036 commit 76de73f
Show file tree
Hide file tree
Showing 27 changed files with 515 additions and 113 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions DEPRECATIONS.md
Original file line number Diff line number Diff line change
@@ -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/[email protected]/doc/header/~)
- `HttpMethodRfc9110`: Has been added to
[@std/http@1.0.2/header](https://jsr.io/@std/[email protected]/doc/header/~)
- `HttpHeaderDeprecated`: Has been added to
[@std/http@1.0.2/header](https://jsr.io/@std/[email protected]/doc/header/~)
- `HttpHeaderObsoleted`: Has been added to
[@std/http@1.0.2/header](https://jsr.io/@std/[email protected]/doc/header/~)
- `HttpHeaderProvisional`: Has been added to
[@std/http@1.0.2/header](https://jsr.io/@std/[email protected]/doc/header/~)
- `HttpHeader`: Has been added to
[@std/http@1.0.2/header](https://jsr.io/@std/[email protected]/doc/header/~)
- `HttpMethodRfc9110`: Has been added to
[@std/http@1.0.2/method](https://jsr.io/@std/[email protected]/doc/method/~)
- `HttpMethodIana`: Has been added to
[@std/http@1.0.2/method](https://jsr.io/@std/[email protected]/doc/method/~)
- `HttpMethod`: Has been added to
[@std/http@1.0.2/method](https://jsr.io/@std/[email protected]/doc/method/~)
2 changes: 1 addition & 1 deletion _tools/bump_version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ async function updateMetaVersion(filepath: string, version: string) {
);
}

const { workspaces } = meta;
const { workspace: workspaces } = meta;

const version = Deno.env.get("VERSION");

Expand Down
34 changes: 34 additions & 0 deletions crypto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
```
9 changes: 7 additions & 2 deletions crypto/deno.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
{
"version": "0.0.5",
"name": "@stdext/crypto",
"lock": false,
"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",
Expand Down
37 changes: 37 additions & 0 deletions crypto/hash.bench.ts
Original file line number Diff line number Diff line change
@@ -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",
);
});
64 changes: 31 additions & 33 deletions crypto/hash.test.ts
Original file line number Diff line number Diff line change
@@ -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));
});
74 changes: 36 additions & 38 deletions crypto/hash/argon2.test.ts
Original file line number Diff line number Diff line change
@@ -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));
});
3 changes: 2 additions & 1 deletion crypto/hash/argon2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down
24 changes: 11 additions & 13 deletions crypto/hash/bcrypt.test.ts
Original file line number Diff line number Diff line change
@@ -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));
});
3 changes: 2 additions & 1 deletion crypto/hash/bcrypt.ts
Original file line number Diff line number Diff line change
@@ -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 };

Expand Down
34 changes: 16 additions & 18 deletions crypto/hash/scrypt.test.ts
Original file line number Diff line number Diff line change
@@ -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));
});
Loading

0 comments on commit 76de73f

Please sign in to comment.