Skip to content

Commit 76de73f

Browse files
authored
Merge pull request #7 from halvardssm/feat/totp
feat(crypto): Add TOTP and HOTP
2 parents 86eca36 + 70de036 commit 76de73f

27 files changed

+515
-113
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ and this project adheres to
88

99
## [Unreleased]
1010

11+
- feat(crypto): added TOTP and HOTP
12+
- deprecated(http/header): Deprecated @stdext/http/header as it is added to
13+
@std/http/header
14+
- deprecated(http/method): Deprecated @stdext/http/method as it is added to
15+
@std/http/method
16+
1117
## [0.0.5] - 2024-05-06
1218

1319
### Changed

DEPRECATIONS.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
11
# Deprecations
22

33
This document contains information about deprecated modules and functions.
4+
5+
- `0.0.5`
6+
- `HttpHeaderPermanent`: Has been added to
7+
[@std/http@1.0.2/header](https://jsr.io/@std/http@1.0.2/doc/header/~)
8+
- `HttpMethodRfc9110`: Has been added to
9+
[@std/http@1.0.2/header](https://jsr.io/@std/http@1.0.2/doc/header/~)
10+
- `HttpHeaderDeprecated`: Has been added to
11+
[@std/http@1.0.2/header](https://jsr.io/@std/http@1.0.2/doc/header/~)
12+
- `HttpHeaderObsoleted`: Has been added to
13+
[@std/http@1.0.2/header](https://jsr.io/@std/http@1.0.2/doc/header/~)
14+
- `HttpHeaderProvisional`: Has been added to
15+
[@std/http@1.0.2/header](https://jsr.io/@std/http@1.0.2/doc/header/~)
16+
- `HttpHeader`: Has been added to
17+
[@std/http@1.0.2/header](https://jsr.io/@std/http@1.0.2/doc/header/~)
18+
- `HttpMethodRfc9110`: Has been added to
19+
[@std/http@1.0.2/method](https://jsr.io/@std/http@1.0.2/doc/method/~)
20+
- `HttpMethodIana`: Has been added to
21+
[@std/http@1.0.2/method](https://jsr.io/@std/http@1.0.2/doc/method/~)
22+
- `HttpMethod`: Has been added to
23+
[@std/http@1.0.2/method](https://jsr.io/@std/http@1.0.2/doc/method/~)

_tools/bump_version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ async function updateMetaVersion(filepath: string, version: string) {
1818
);
1919
}
2020

21-
const { workspaces } = meta;
21+
const { workspace: workspaces } = meta;
2222

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

crypto/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,43 @@ The following algorithms are provided:
1313

1414
- Argon2
1515
- Bcrypt
16+
- Scrypt
1617

1718
```ts
1819
import { hash, verify } from "@stdext/crypto/hash";
1920
const h = hash("argon2", "password");
2021
verify("argon2", "password", h);
2122
```
23+
24+
### HOTP (HMAC One-Time Password)
25+
26+
```ts
27+
import { generateHotp, verifyHotp } from "@stdext/crypto/hotp";
28+
import { generateSecret } from "@stdext/crypto/utils";
29+
30+
const secret = generateSecret();
31+
const hotp = generateHotp(secret, 42);
32+
verifyHotp(hotp, secret, 42);
33+
```
34+
35+
### TOTP (Time-based One-Time Password)
36+
37+
```ts
38+
import { generateTotp, verifyTotp } from "@stdext/crypto/totp";
39+
import { generateSecret } from "@stdext/crypto/utils";
40+
41+
const secret = generateSecret();
42+
const totp = generateTotp(secret, 42);
43+
verifyTotp(totp, secret, 42);
44+
```
45+
46+
### Utils
47+
48+
```ts
49+
import { generateSecretBytes } from "@stdext/crypto/utils";
50+
import { encodeBase64 } from "@std/encoding";
51+
52+
const secretBytes = generateSecretBytes();
53+
// You can select your own encoding
54+
const encodedSecret = encodeBase64(secretBytes);
55+
```

crypto/deno.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
{
22
"version": "0.0.5",
33
"name": "@stdext/crypto",
4-
"lock": false,
54
"exports": {
6-
"./hash": "./hash.ts"
5+
"./hash": "./hash.ts",
6+
"./hash/argon2": "./hash/argon2.ts",
7+
"./hash/bcrypt": "./hash/bcrypt.ts",
8+
"./hash/scrypt": "./hash/scrypt.ts",
9+
"./hotp": "./hotp.ts",
10+
"./totp": "./totp.ts",
11+
"./utils": "./utils.ts"
712
},
813
"tasks": {
914
"build": "deno task build:argon2 && deno task build:bcrypt && deno task build:scrypt",

crypto/hash.bench.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { hash, verify } from "./hash.ts";
2+
3+
Deno.bench("hash() with argon2", () => {
4+
hash("argon2", "password");
5+
});
6+
7+
Deno.bench("hash() with bcrypt", () => {
8+
hash("bcrypt", "password");
9+
});
10+
11+
Deno.bench("hash() with scrypt", () => {
12+
hash("scrypt", "password");
13+
});
14+
15+
Deno.bench("verify() with argon2", () => {
16+
verify(
17+
"argon2",
18+
"password",
19+
"$argon2id$v=19$m=19456,t=2,p=1$sgg3gflK2pkatSfTYkQTtA$UvKPnIcKDBfK9d4v4ItjRYra//s9uuFJgMisTNC+Wcw",
20+
);
21+
});
22+
23+
Deno.bench("verify() with bcrypt", () => {
24+
verify(
25+
"bcrypt",
26+
"password",
27+
"$2b$12$GUvwcP3VbNvmKDzl114sW.DVt.1xX9N7OmWk80OWLjigWIW/3n66G",
28+
);
29+
});
30+
31+
Deno.bench("verify() with scrypt", () => {
32+
verify(
33+
"scrypt",
34+
"password",
35+
"$scrypt$ln=17,r=8,p=1$y8d9gN0rKwW7z+hJb/vQAA$w+VLelvZVpZ0zt/+svlPbZFHDTl+jL5Xvp+YKrZEyKE",
36+
);
37+
});

crypto/hash.test.ts

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,38 @@
11
import { assert, assertMatch, assertThrows } from "@std/assert";
22
import { hash, verify } from "./hash.ts";
33

4-
Deno.test("hash", async (t) => {
5-
await t.step("unsupported", () => {
6-
// deno-lint-ignore ban-ts-comment
7-
// @ts-ignore
8-
assertThrows(() => hash("unsupported", "password"));
9-
// deno-lint-ignore ban-ts-comment
10-
// @ts-ignore
11-
assertThrows(() => verify("unsupported", "password", ""));
12-
});
4+
Deno.test("hash() and verify() with unsupported", () => {
5+
// deno-lint-ignore ban-ts-comment
6+
// @ts-ignore
7+
assertThrows(() => hash("unsupported", "password"));
8+
// deno-lint-ignore ban-ts-comment
9+
// @ts-ignore
10+
assertThrows(() => verify("unsupported", "password", ""));
11+
});
1312

14-
await t.step("argon2", () => {
15-
const h1 = hash("argon2", "password");
16-
assertMatch(h1, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/);
17-
assert(verify("argon2", "password", h1));
18-
const h2 = hash({ name: "argon2" }, "password");
19-
assertMatch(h2, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/);
20-
assert(verify({ name: "argon2" }, "password", h2));
21-
});
13+
Deno.test("hash() and verify() with argon2", () => {
14+
const h1 = hash("argon2", "password");
15+
assertMatch(h1, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/);
16+
assert(verify("argon2", "password", h1));
17+
const h2 = hash({ name: "argon2" }, "password");
18+
assertMatch(h2, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/);
19+
assert(verify({ name: "argon2" }, "password", h2));
20+
});
2221

23-
await t.step("bcrypt", () => {
24-
const h1 = hash("bcrypt", "password");
25-
assertMatch(h1, /^\$2b\$12\$/);
26-
assert(verify("bcrypt", "password", h1));
27-
const h2 = hash({ name: "bcrypt" }, "password");
28-
assertMatch(h2, /^\$2b\$12\$/);
29-
assert(verify({ name: "bcrypt" }, "password", h2));
30-
});
22+
Deno.test("hash() and verify() with bcrypt", () => {
23+
const h1 = hash("bcrypt", "password");
24+
assertMatch(h1, /^\$2b\$12\$/);
25+
assert(verify("bcrypt", "password", h1));
26+
const h2 = hash({ name: "bcrypt" }, "password");
27+
assertMatch(h2, /^\$2b\$12\$/);
28+
assert(verify({ name: "bcrypt" }, "password", h2));
29+
});
3130

32-
await t.step("scrypt", () => {
33-
const h1 = hash("scrypt", "password");
34-
assertMatch(h1, /^\$scrypt\$ln=17,r=8,p=1\$/);
35-
assert(verify("scrypt", "password", h1));
36-
const h2 = hash({ name: "scrypt" }, "password");
37-
assertMatch(h2, /^\$scrypt\$ln=17,r=8,p=1\$/);
38-
assert(verify({ name: "scrypt" }, "password", h2));
39-
});
31+
Deno.test("hash() and verify() with scrypt", () => {
32+
const h1 = hash("scrypt", "password");
33+
assertMatch(h1, /^\$scrypt\$ln=17,r=8,p=1\$/);
34+
assert(verify("scrypt", "password", h1));
35+
const h2 = hash({ name: "scrypt" }, "password");
36+
assertMatch(h2, /^\$scrypt\$ln=17,r=8,p=1\$/);
37+
assert(verify({ name: "scrypt" }, "password", h2));
4038
});

crypto/hash/argon2.test.ts

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,44 @@
11
import { assert, assertMatch } from "@std/assert";
22
import { type Argon2Options, hash, verify } from "./argon2.ts";
33

4-
Deno.test("Argon2", async (t) => {
5-
await t.step("defaults", () => {
6-
const h = hash("password", {});
7-
assertMatch(h, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/);
8-
assert(verify("password", h, {}));
9-
});
4+
Deno.test("hash() and verify() with default arguments", () => {
5+
const h = hash("password", {});
6+
assertMatch(h, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/);
7+
assert(verify("password", h, {}));
8+
});
109

11-
await t.step("Argon2i", () => {
12-
const o = { algorithm: "argon2i" } satisfies Argon2Options;
13-
const h = hash("password", o);
14-
assertMatch(h, /^\$argon2i\$v=19\$m=19456,t=2,p=1\$/);
15-
assert(verify("password", h, o));
16-
});
10+
Deno.test("hash() and verify() with argon2i", () => {
11+
const o = { algorithm: "argon2i" } satisfies Argon2Options;
12+
const h = hash("password", o);
13+
assertMatch(h, /^\$argon2i\$v=19\$m=19456,t=2,p=1\$/);
14+
assert(verify("password", h, o));
15+
});
1716

18-
await t.step("Argon2d", () => {
19-
const o = { algorithm: "argon2d" } satisfies Argon2Options;
20-
const h = hash("password", o);
21-
assertMatch(h, /^\$argon2d\$v=19\$m=19456,t=2,p=1\$/);
22-
assert(verify("password", h, o));
23-
});
17+
Deno.test("hash() and verify() with argon2d", () => {
18+
const o = { algorithm: "argon2d" } satisfies Argon2Options;
19+
const h = hash("password", o);
20+
assertMatch(h, /^\$argon2d\$v=19\$m=19456,t=2,p=1\$/);
21+
assert(verify("password", h, o));
22+
});
2423

25-
await t.step("wrong algoritm", () => {
26-
// deno-lint-ignore ban-ts-comment
27-
// @ts-ignore
28-
const o = { algorithm: "asdfasdf" } as Argon2Options;
29-
const h = hash("password", o);
30-
assertMatch(h, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/);
31-
assert(verify("password", h, o));
32-
});
24+
Deno.test("hash() and verify() with wrong algorithm", () => {
25+
// deno-lint-ignore ban-ts-comment
26+
// @ts-ignore
27+
const o = { algorithm: "asdfasdf" } as Argon2Options;
28+
const h = hash("password", o);
29+
assertMatch(h, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/);
30+
assert(verify("password", h, o));
31+
});
3332

34-
await t.step("all options", () => {
35-
const o = {
36-
algorithm: "argon2id",
37-
memoryCost: 10000,
38-
timeCost: 3,
39-
parallelism: 2,
40-
outputLength: 16,
41-
} satisfies Argon2Options;
42-
const h = hash("password", o);
43-
assertMatch(h, /^\$argon2id\$v=19\$m=10000,t=3,p=2\$/);
44-
assert(verify("password", h, o));
45-
});
33+
Deno.test("hash() and verify() with all options", () => {
34+
const o = {
35+
algorithm: "argon2id",
36+
memoryCost: 10000,
37+
timeCost: 3,
38+
parallelism: 2,
39+
outputLength: 16,
40+
} satisfies Argon2Options;
41+
const h = hash("password", o);
42+
assertMatch(h, /^\$argon2id\$v=19\$m=10000,t=3,p=2\$/);
43+
assert(verify("password", h, o));
4644
});

crypto/hash/argon2.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import {
22
type Argon2Algorithm,
33
type Argon2Options,
44
instantiate,
5+
type InstantiateResult,
56
} from "./_wasm/lib/deno_stdext_crypto_hash_wasm_argon2.generated.mjs";
67

7-
const instance = instantiate();
8+
const instance: InstantiateResult["exports"] = instantiate();
89

910
export type { Argon2Algorithm, Argon2Options };
1011

crypto/hash/bcrypt.test.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import { assert, assertMatch } from "@std/assert";
22
import { type BcryptOptions, hash, verify } from "./bcrypt.ts";
33

4-
Deno.test("Bcrypt", async (t) => {
5-
await t.step("defaults", () => {
6-
const o = {} as BcryptOptions;
7-
const h = hash("password", o);
8-
assertMatch(h, /^\$2b\$12\$/);
9-
assert(verify("password", h, o));
10-
});
4+
Deno.test("hash() and verify() with defaults", () => {
5+
const o = {} as BcryptOptions;
6+
const h = hash("password", o);
7+
assertMatch(h, /^\$2b\$12\$/);
8+
assert(verify("password", h, o));
9+
});
1110

12-
await t.step("cost 4", () => {
13-
const o = { cost: 4 } as BcryptOptions;
14-
const h = hash("password", o);
15-
assertMatch(h, /^\$2b\$04\$/);
16-
assert(verify("password", h, o));
17-
});
11+
Deno.test("hash() and verify() with all options", () => {
12+
const o = { cost: 4 } as BcryptOptions;
13+
const h = hash("password", o);
14+
assertMatch(h, /^\$2b\$04\$/);
15+
assert(verify("password", h, o));
1816
});

0 commit comments

Comments
 (0)