Skip to content

Commit

Permalink
Merge pull request #2 from halvardssm/feat/crypto-hash
Browse files Browse the repository at this point in the history
Feat/crypto hash
halvardssm authored Apr 29, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents 4f5bab3 + 1f0ab8e commit 978fad4
Showing 27 changed files with 6,119 additions and 5 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -72,9 +72,12 @@ jobs:
with:
deno-version: canary

- name: Format
- name: Format & Lint
run: deno task check

- name: Build
run: deno task build:check

publish-dry-run:
runs-on: ubuntu-latest
timeout-minutes: 30
5 changes: 4 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -22,9 +22,12 @@ jobs:
with:
deno-version: canary

- name: Check
- name: Format & Lint
run: deno task check

- name: Build
run: deno task build:check

- name: Test
run: deno task test

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -9,3 +9,4 @@ html_cov/
cov.lcov
coverage/
docs/
crypto/hash/_wasm/target
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- feat(http): added package
- feat(http/header): added IANA HTTP headers
- feat(http/method): added IANA HTTP methods
- feat(crypto): added package
- feat(http/hash): added argon2 hash
- feat(http/hash): added bcrypt hash

## [0.0.2] - 2024-04-28

### Added

- chore(core): added documentation

## [0.0.1] - 2024-04-28

### Added

- feat(core): implemented initial codebase
- feat(encoding): added package
- feat(encoding/hex): added hexdump
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -44,8 +44,12 @@ console.log(dump(buffer));

## Packages

- [crypto](https://jsr.io/@stdext/crypto): The crypto package contains utility
for crypto and hashing
- [encoding](https://jsr.io/@stdext/encoding): The encoding package contains
helpers for text encoding.
utility for text encoding.
- [http](https://jsr.io/@stdext/http): The http package contains utility for
fetching and http servers

## Versioning

Empty file removed RELEASES.md
Empty file.
15 changes: 15 additions & 0 deletions crypto/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# @stdext/crypto

The Crypto package contains utilities for encryption and decryption as well as
hashing.

## Entrypoints

### Hash

The hash module contains helpers and implementations for password hashing.

The following algorithms are provided:

- Argon2
- Bcrypt
20 changes: 20 additions & 0 deletions crypto/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": "0.0.2",
"name": "@stdext/crypto",
"lock": false,
"exports": {
"./hash": "./hash.ts"
},
"tasks": {
"build": "deno task build:argon2 && deno task build:bcrypt",
"build:check": "deno task build:argon2:check && deno task build:bcrypt:check",
"build:wasm": "deno task --cwd hash/_wasm wasmbuild --js-ext mjs --sync",
"build:argon2": "deno task build:wasm --project deno_stdext_crypto_hash_wasm_argon2",
"build:argon2:check": "deno task build:argon2 --check",
"build:bcrypt": "deno task build:wasm --project deno_stdext_crypto_hash_wasm_bcrypt",
"build:bcrypt:check": "deno task build:bcrypt --check",
"wasmbuild": "deno run -A jsr:@deno/wasmbuild@0.17.1",
"format": "cd hash/_wasm && cargo fmt --all",
"format:check": "cd hash/_wasm && cargo fmt --all -- --check"
}
}
12 changes: 12 additions & 0 deletions crypto/hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Utilities for data hashing.
* @module
*
* ```ts
* const hasher = new PasswordHash();
* const hash = hasher.hash("password");
* hasher.verify("password", hash));
* ```
*/

export * from "./hash/mod.ts";
3 changes: 3 additions & 0 deletions crypto/hash/_wasm/.rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
max_width = 80
tab_spaces = 2
edition = "2021"
379 changes: 379 additions & 0 deletions crypto/hash/_wasm/Cargo.lock
12 changes: 12 additions & 0 deletions crypto/hash/_wasm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[workspace]
resolver = "2"
members = [
"argon2",
"bcrypt"
]

[profile.release]
codegen-units = 1
incremental = true
lto = true
opt-level = "z"
16 changes: 16 additions & 0 deletions crypto/hash/_wasm/argon2/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "deno_stdext_crypto_hash_wasm_argon2"
version = "0.0.0"
edition = "2021"

[lib]
crate_type = ["cdylib"]

[dependencies]
wasm-bindgen = "=0.2.92"
serde = { version = "1", features = ["derive"] }
serde-wasm-bindgen = "0.4"
getrandom = { version = "0.2", features = ["js"] }
argon2 = "0.5"
rand_core = { version = "0.6", features = ["std"] }
js-sys = "0.3"
106 changes: 106 additions & 0 deletions crypto/hash/_wasm/argon2/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use argon2::{
password_hash::{
rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier,
SaltString,
},
Argon2, Version,
};
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;

#[wasm_bindgen(typescript_custom_section)]
const ITEXT_STYLE: &'static str = r#"
export type StdextArgon2Algorithm = "argon2d" | "argon2i" | "argon2id"
export interface StdextArgon2Options {
algorithm?: StdextArgon2Algorithm;
memoryCost?: number;
timeCost?: number;
parallelism?: number;
outputLength?: number;
}
"#;

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(typescript_type = "StdextArgon2Options")]
pub type StdextArgon2Options;
}

#[wasm_bindgen]
#[derive(Default)]
pub struct StdextArgon2 {
options: argon2::Params,
algorithm: argon2::Algorithm,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct StdextArgon2OptionsIncoming {
#[serde(rename = "memoryCost")]
pub m_cost: Option<u32>,
#[serde(rename = "timeCost")]
pub t_cost: Option<u32>,
#[serde(rename = "parallelism")]
pub p: Option<u32>,
#[serde(rename = "outputLength")]
pub output_len: Option<usize>,
pub algorithm: Option<String>,
}

#[wasm_bindgen]
impl StdextArgon2 {
#[wasm_bindgen(constructor)]
pub fn new(i: StdextArgon2Options) -> StdextArgon2 {
let parsed_options: StdextArgon2OptionsIncoming =
serde_wasm_bindgen::from_value(i.into()).unwrap_throw();

let algorithm = match parsed_options
.algorithm
.unwrap_or("argon2id".to_string())
.as_str()
{
"argon2d" => argon2::Algorithm::Argon2d,
"argon2i" => argon2::Algorithm::Argon2i,
"argon2id" => argon2::Algorithm::Argon2id,
_ => argon2::Algorithm::Argon2id,
};

let default_params = argon2::Params::default();

let params = argon2::Params::new(
parsed_options.m_cost.unwrap_or(default_params.m_cost()),
parsed_options.t_cost.unwrap_or(default_params.t_cost()),
parsed_options.p.unwrap_or(default_params.p_cost()),
parsed_options.output_len,
)
.expect_throw("failed to create params");

Self {
options: params,
algorithm: algorithm,
}
}

#[wasm_bindgen]
pub fn hash(&self, password: &str) -> String {
let argon2 =
Argon2::new(self.algorithm, Version::V0x13, self.options.clone());
let salt = SaltString::generate(&mut OsRng);
let password_bytes = password.as_bytes();
argon2
.hash_password(password_bytes, &salt)
.expect("hashing failed")
.to_string()
}

#[wasm_bindgen]
pub fn verify(&self, password: &str, hash: &str) -> bool {
let password_bytes = password.as_bytes();
let parsed_hash = PasswordHash::new(&hash).expect("failed to parse hash");
let argon2 =
Argon2::new(self.algorithm, Version::V0x13, self.options.clone())
.verify_password(password_bytes, &parsed_hash)
.is_ok();

argon2
}
}
14 changes: 14 additions & 0 deletions crypto/hash/_wasm/bcrypt/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "deno_stdext_crypto_hash_wasm_bcrypt"
version = "0.0.0"
edition = "2021"

[lib]
crate_type = ["cdylib"]

[dependencies]
wasm-bindgen = "=0.2.92"
serde = { version = "1", features = ["derive"] }
serde-wasm-bindgen = "0.4"
getrandom = { version = "0.2", features = ["js"] }
bcrypt = "0.15"
55 changes: 55 additions & 0 deletions crypto/hash/_wasm/bcrypt/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use bcrypt::{hash, verify, DEFAULT_COST};
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;

#[wasm_bindgen(typescript_custom_section)]
const ITEXT_STYLE: &'static str = r#"
export interface StdextBcryptOptions {
/**
* Must be a number between 4 and 31
*
* @default 12
*/
cost?: number;
}
"#;

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(typescript_type = "StdextBcryptOptions")]
pub type StdextBcryptOptions;
}

#[wasm_bindgen]
#[derive(Default)]
pub struct StdextBcrypt {
cost: u32,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct StdextBcryptOptionsIncoming {
pub cost: Option<u32>,
}

#[wasm_bindgen]
impl StdextBcrypt {
#[wasm_bindgen(constructor)]
pub fn new(i: StdextBcryptOptions) -> StdextBcrypt {
let parsed_options: StdextBcryptOptionsIncoming =
serde_wasm_bindgen::from_value(i.into()).unwrap_throw();

Self {
cost: parsed_options.cost.unwrap_or(DEFAULT_COST),
}
}

#[wasm_bindgen]
pub fn hash(&self, password: &str) -> String {
hash(password, self.cost).expect_throw("failed to hash password")
}

#[wasm_bindgen]
pub fn verify(&self, password: &str, hash: &str) -> bool {
verify(password, hash).expect_throw("failed to verify password")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// deno-lint-ignore-file
// deno-fmt-ignore-file

export interface InstantiateResult {
instance: WebAssembly.Instance;
exports: {
StdextArgon2 : typeof StdextArgon2
};
}

/** Gets if the Wasm module has been instantiated. */
export function isInstantiated(): boolean;


/** Instantiates an instance of the Wasm module returning its functions.
* @remarks It is safe to call this multiple times and once successfully
* loaded it will always return a reference to the same object. */
export function instantiate(): InstantiateResult["exports"];

/** Instantiates an instance of the Wasm module along with its exports.
* @remarks It is safe to call this multiple times and once successfully
* loaded it will always return a reference to the same object. */
export function instantiateWithInstance(): InstantiateResult;


export type StdextArgon2Algorithm = "argon2d" | "argon2i" | "argon2id"
export interface StdextArgon2Options {
algorithm?: StdextArgon2Algorithm;
memoryCost?: number;
timeCost?: number;
parallelism?: number;
outputLength?: number;
}


/**
*/
export class StdextArgon2 {
free(): void;
/**
* @param {StdextArgon2Options} i
*/
constructor(i: StdextArgon2Options);
/**
* @param {string} password
* @returns {string}
*/
hash(password: string): string;
/**
* @param {string} password
* @param {string} hash
* @returns {boolean}
*/
verify(password: string, hash: string): boolean;
}
2,831 changes: 2,831 additions & 0 deletions crypto/hash/_wasm/lib/deno_stdext_crypto_hash_wasm_argon2.generated.mjs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// deno-lint-ignore-file
// deno-fmt-ignore-file

export interface InstantiateResult {
instance: WebAssembly.Instance;
exports: {
StdextBcrypt : typeof StdextBcrypt
};
}

/** Gets if the Wasm module has been instantiated. */
export function isInstantiated(): boolean;


/** Instantiates an instance of the Wasm module returning its functions.
* @remarks It is safe to call this multiple times and once successfully
* loaded it will always return a reference to the same object. */
export function instantiate(): InstantiateResult["exports"];

/** Instantiates an instance of the Wasm module along with its exports.
* @remarks It is safe to call this multiple times and once successfully
* loaded it will always return a reference to the same object. */
export function instantiateWithInstance(): InstantiateResult;


export interface StdextBcryptOptions {
/**
* Must be a number between 4 and 31
*
* @default 12
*/
cost?: number;
}


/**
*/
export class StdextBcrypt {
free(): void;
/**
* @param {StdextBcryptOptions} i
*/
constructor(i: StdextBcryptOptions);
/**
* @param {string} password
* @returns {string}
*/
hash(password: string): string;
/**
* @param {string} password
* @param {string} hash
* @returns {boolean}
*/
verify(password: string, hash: string): boolean;
}
2,331 changes: 2,331 additions & 0 deletions crypto/hash/_wasm/lib/deno_stdext_crypto_hash_wasm_bcrypt.generated.mjs

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions crypto/hash/argon2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { assert, assertMatch } from "@std/assert";
import { Argon2 } from "./argon2.ts";

Deno.test("Argon2", async (t) => {
await t.step("defaults", () => {
const r = new Argon2();
const h = r.hash("password");
assertMatch(h, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/);
assert(r.verify("password", h));
});

await t.step("Argon2i", () => {
const r = new Argon2({ algorithm: "argon2i" });
const h = r.hash("password");
assertMatch(h, /^\$argon2i\$v=19\$m=19456,t=2,p=1\$/);
assert(r.verify("password", h));
});
await t.step("Argon2d", () => {
const r = new Argon2({ algorithm: "argon2d" });
const h = r.hash("password");
assertMatch(h, /^\$argon2d\$v=19\$m=19456,t=2,p=1\$/);
assert(r.verify("password", h));
});
await t.step("wrong algoritm", () => {
// deno-lint-ignore ban-ts-comment
// @ts-ignore
const r = new Argon2({ algorithm: "asdfasdf" });
const h = r.hash("password");
assertMatch(h, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/);
assert(r.verify("password", h));
});
await t.step("all options", () => {
const r = new Argon2({
algorithm: "argon2id",
memoryCost: 10000,
timeCost: 3,
parallelism: 2,
outputLength: 32,
});
const h = r.hash("password");
assertMatch(h, /^\$argon2id\$v=19\$m=10000,t=3,p=2\$/);
assert(r.verify("password", h));
});
});
36 changes: 36 additions & 0 deletions crypto/hash/argon2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { PasswordHash } from "./shared.ts";
import {
instantiate,
type StdextArgon2,
type StdextArgon2Options,
} from "./_wasm/lib/deno_stdext_crypto_hash_wasm_argon2.generated.mjs";

const argon2 = instantiate();

/**
* Argon2 options
*/
export type Argon2Options = StdextArgon2Options;

/**
* Argon2 password hashing
*
* ```
* const hasher = new PasswordHash();
* const hash = hasher.hash("password");
* hasher.verify("password", hash));
* ```
*/
export class Argon2 implements PasswordHash {
#hasher: StdextArgon2;
constructor(options: Argon2Options = {}) {
this.#hasher = new argon2.StdextArgon2(options);
}

hash(password: string): string {
return this.#hasher.hash(password);
}
verify(password: string, hash: string): boolean {
return this.#hasher.verify(password, hash);
}
}
20 changes: 20 additions & 0 deletions crypto/hash/bcrypt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { assert, assertMatch } from "@std/assert";
import { Bcrypt } from "./bcrypt.ts";

Deno.test("Bcrypt", async (t) => {
await t.step("defaults", () => {
const r = new Bcrypt();
const h = r.hash("password");
assertMatch(h, /^\$2b\$12/);
assert(r.verify("password", h));
});

await t.step("cost 4", () => {
const r = new Bcrypt({
cost: 4,
});
const h = r.hash("password");
assertMatch(h, /^\$2b\$04/);
assert(r.verify("password", h));
});
});
36 changes: 36 additions & 0 deletions crypto/hash/bcrypt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { PasswordHash } from "./shared.ts";
import {
instantiate,
type StdextBcrypt,
type StdextBcryptOptions,
} from "./_wasm/lib/deno_stdext_crypto_hash_wasm_bcrypt.generated.mjs";

const bcrypt = instantiate();

/**
* Bcrypt options
*/
export type BcryptOptions = StdextBcryptOptions;

/**
* Bcrypt password hashing
*
* ```
* const hasher = new PasswordHash();
* const hash = hasher.hash("password");
* hasher.verify("password", hash));
* ```
*/
export class Bcrypt implements PasswordHash {
#hasher: StdextBcrypt;
constructor(options: BcryptOptions = {}) {
this.#hasher = new bcrypt.StdextBcrypt(options);
}

hash(password: string): string {
return this.#hasher.hash(password);
}
verify(password: string, hash: string): boolean {
return this.#hasher.verify(password, hash);
}
}
2 changes: 2 additions & 0 deletions crypto/hash/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Argon2 } from "./argon2.ts";
export { Bcrypt } from "./bcrypt.ts";
19 changes: 19 additions & 0 deletions crypto/hash/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* General interface for password hashes.
*
* ```
* const hasher = new PasswordHash();
* const hash = hasher.hash("password");
* hasher.verify("password", hash));
* ```
*/
export interface PasswordHash {
/**
* Hashes the password.
*/
hash(password: string): string;
/**
* Verifies the password against the hash.
*/
verify(password: string, hash: string): boolean;
}
14 changes: 12 additions & 2 deletions deno.json
Original file line number Diff line number Diff line change
@@ -6,12 +6,22 @@
},
"tasks": {
"bump_version": "deno run --allow-env=VERSION --allow-read=. --allow-write=. ./_tools/bump_version.ts",
"check": "deno fmt --check && deno lint && deno check **/*.ts",
"check": "deno task format:check && deno lint && deno check **/*.ts",
"test": "RUST_BACKTRACE=1 deno test --unstable-http --unstable-webgpu --allow-all --parallel --coverage --trace-leaks",
"cov:gen": "deno coverage coverage --lcov --output=cov.lcov"
"cov:gen": "deno coverage coverage --lcov --output=cov.lcov",
"build": "deno task build:wasm",
"build:check": "deno task build:wasm:check",
"build:wasm": "deno task --cwd crypto build",
"build:wasm:check": "deno task --cwd crypto build:check",
"format": "deno fmt && deno task --cwd crypto format",
"format:check": "deno fmt --check && deno task --cwd crypto format:check"
},
"workspaces": [
"./crypto",
"./encoding",
"./http"
],
"exclude": [
"./crypto/hash/_wasm/target"
]
}

0 comments on commit 978fad4

Please sign in to comment.