Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(crypto): Add TOTP and HOTP #7

Merged
merged 7 commits into from
Aug 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/[email protected]/header](https://jsr.io/@std/[email protected]/doc/header/~)
- `HttpMethodRfc9110`: Has been added to
[@std/[email protected]/header](https://jsr.io/@std/[email protected]/doc/header/~)
- `HttpHeaderDeprecated`: Has been added to
[@std/[email protected]/header](https://jsr.io/@std/[email protected]/doc/header/~)
- `HttpHeaderObsoleted`: Has been added to
[@std/[email protected]/header](https://jsr.io/@std/[email protected]/doc/header/~)
- `HttpHeaderProvisional`: Has been added to
[@std/[email protected]/header](https://jsr.io/@std/[email protected]/doc/header/~)
- `HttpHeader`: Has been added to
[@std/[email protected]/header](https://jsr.io/@std/[email protected]/doc/header/~)
- `HttpMethodRfc9110`: Has been added to
[@std/[email protected]/method](https://jsr.io/@std/[email protected]/doc/method/~)
- `HttpMethodIana`: Has been added to
[@std/[email protected]/method](https://jsr.io/@std/[email protected]/doc/method/~)
- `HttpMethod`: Has been added to
[@std/[email protected]/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