feat: modular domain driven boilerplate
This commit is contained in:
51
platform/vault/hmac.ts
Normal file
51
platform/vault/hmac.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Hash a value with given secret.
|
||||
*
|
||||
* @param value - Value to hash.
|
||||
* @param secret - Secret to hash the value against.
|
||||
*/
|
||||
export async function hash(value: string, secret: string): Promise<string> {
|
||||
const key = await getImportKey(secret, ["sign"]);
|
||||
const encoder = new TextEncoder();
|
||||
const valueData = encoder.encode(value);
|
||||
const signature = await crypto.subtle.sign("HMAC", key, valueData);
|
||||
return bufferToHex(signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the given value results in the expected hash using the provided secret.
|
||||
*
|
||||
* @param value - Value to verify.
|
||||
* @param expectedHash - Expected hash value.
|
||||
* @param secret - Secret used to hash the value.
|
||||
*/
|
||||
export async function verify(value: string, expectedHash: string, secret: string): Promise<boolean> {
|
||||
const key = await getImportKey(secret, ["verify"]);
|
||||
const encoder = new TextEncoder();
|
||||
const valueData = encoder.encode(value);
|
||||
const signature = hexToBuffer(expectedHash);
|
||||
return crypto.subtle.verify("HMAC", key, signature, valueData);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Utilities
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
async function getImportKey(secret: string, usages: KeyUsage[]): Promise<CryptoKey> {
|
||||
const encoder = new TextEncoder();
|
||||
const keyData = encoder.encode(secret);
|
||||
return crypto.subtle.importKey("raw", keyData, { name: "HMAC", hash: { name: "SHA-256" } }, false, usages);
|
||||
}
|
||||
|
||||
function bufferToHex(buffer: ArrayBuffer): string {
|
||||
return Array.from(new Uint8Array(buffer))
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
function hexToBuffer(hex: string): ArrayBuffer {
|
||||
const bytes = new Uint8Array(hex.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)));
|
||||
return bytes.buffer;
|
||||
}
|
||||
134
platform/vault/key-pair.ts
Normal file
134
platform/vault/key-pair.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import * as Jose from "jose";
|
||||
|
||||
export class KeyPair {
|
||||
readonly #public: PublicKey;
|
||||
readonly #private: PrivateKey;
|
||||
readonly #algorithm: string;
|
||||
|
||||
constructor({ publicKey, privateKey }: Jose.GenerateKeyPairResult, algorithm: string) {
|
||||
this.#public = new PublicKey(publicKey);
|
||||
this.#private = new PrivateKey(privateKey);
|
||||
this.#algorithm = algorithm;
|
||||
}
|
||||
|
||||
get public() {
|
||||
return this.#public;
|
||||
}
|
||||
|
||||
get private() {
|
||||
return this.#private;
|
||||
}
|
||||
|
||||
get algorithm() {
|
||||
return this.#algorithm;
|
||||
}
|
||||
|
||||
async toJSON() {
|
||||
return {
|
||||
publicKey: await this.public.toString(),
|
||||
privateKey: await this.private.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class PublicKey {
|
||||
readonly #key: Jose.CryptoKey;
|
||||
|
||||
constructor(key: Jose.CryptoKey) {
|
||||
this.#key = key;
|
||||
}
|
||||
|
||||
get key(): Jose.CryptoKey {
|
||||
return this.#key;
|
||||
}
|
||||
|
||||
async toString() {
|
||||
return Jose.exportSPKI(this.#key);
|
||||
}
|
||||
}
|
||||
|
||||
export class PrivateKey {
|
||||
readonly #key: Jose.CryptoKey;
|
||||
|
||||
constructor(key: Jose.CryptoKey) {
|
||||
this.#key = key;
|
||||
}
|
||||
|
||||
get key(): Jose.CryptoKey {
|
||||
return this.#key;
|
||||
}
|
||||
|
||||
async toString() {
|
||||
return Jose.exportPKCS8(this.#key);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Factories
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a new key pair using the provided algorithm.
|
||||
*
|
||||
* @param algorithm - Algorithm to use for key generation.
|
||||
*
|
||||
* @returns new key pair instance
|
||||
*/
|
||||
export async function createKeyPair(algorithm: string): Promise<KeyPair> {
|
||||
return new KeyPair(await Jose.generateKeyPair(algorithm, { extractable: true }), algorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a keypair from a previously exported keypair into a new KeyPair instance.
|
||||
*
|
||||
* @param keyPair - KeyPair to load into a new keyPair instance.
|
||||
* @param algorithm - Algorithm to use for key generation.
|
||||
*
|
||||
* @returns new key pair instance
|
||||
*/
|
||||
export async function loadKeyPair({ publicKey, privateKey }: ExportedKeyPair, algorithm: string): Promise<KeyPair> {
|
||||
return new KeyPair(
|
||||
{
|
||||
publicKey: await importPublicKey(publicKey, algorithm),
|
||||
privateKey: await importPrivateKey(privateKey, algorithm),
|
||||
},
|
||||
algorithm,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new Jose.KeyLike instance from a public key string.
|
||||
*
|
||||
* @param publicKey - Public key string.
|
||||
* @param algorithm - Algorithm to used for key generation.
|
||||
*
|
||||
* @returns new Jose.KeyLike instance
|
||||
*/
|
||||
export async function importPublicKey(publicKey: string, algorithm: string): Promise<Jose.CryptoKey> {
|
||||
return Jose.importSPKI(publicKey, algorithm, { extractable: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* get a new Jose.KeyLike instance from a private key string.
|
||||
*
|
||||
* @param privateKey - Private key string.
|
||||
* @param algorithm - Algorithm to used for key generation.
|
||||
*
|
||||
* @returns new Jose.KeyLike instance
|
||||
*/
|
||||
export async function importPrivateKey(privateKey: string, algorithm: string): Promise<Jose.CryptoKey> {
|
||||
return Jose.importPKCS8(privateKey, algorithm, { extractable: true });
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Types
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type ExportedKeyPair = {
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
};
|
||||
10
platform/vault/package.json
Normal file
10
platform/vault/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@platform/vault",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"jose": "6.1.0",
|
||||
"nanoid": "5.1.5"
|
||||
}
|
||||
}
|
||||
84
platform/vault/vault.ts
Normal file
84
platform/vault/vault.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import * as Jose from "jose";
|
||||
|
||||
import { createKeyPair, ExportedKeyPair, importPrivateKey, importPublicKey, KeyPair, loadKeyPair } from "./key-pair.ts";
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Security Settings
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const VAULT_ALGORITHM = "ECDH-ES+A256KW";
|
||||
const VAULT_ENCRYPTION = "A256GCM";
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Vault
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export class Vault {
|
||||
#keyPair: KeyPair;
|
||||
|
||||
constructor(keyPair: KeyPair) {
|
||||
this.#keyPair = keyPair;
|
||||
}
|
||||
|
||||
get keys() {
|
||||
return this.#keyPair;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enecrypt the given value with the vaults key pair.
|
||||
*
|
||||
* @param value - Value to encrypt.
|
||||
*/
|
||||
async encrypt<T extends Record<string, unknown> | unknown[] | string>(value: T): Promise<string> {
|
||||
const text = new TextEncoder().encode(JSON.stringify(value));
|
||||
return new Jose.CompactEncrypt(text)
|
||||
.setProtectedHeader({
|
||||
alg: VAULT_ALGORITHM,
|
||||
enc: VAULT_ENCRYPTION,
|
||||
})
|
||||
.encrypt(this.#keyPair.public.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the given cypher text with the vaults key pair.
|
||||
*
|
||||
* @param cypherText - String to decrypt.
|
||||
*/
|
||||
async decrypt<T>(cypherText: string): Promise<T> {
|
||||
const { plaintext } = await Jose.compactDecrypt(cypherText, this.#keyPair.private.key);
|
||||
return JSON.parse(new TextDecoder().decode(plaintext));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Factories
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export async function createVault(): Promise<Vault> {
|
||||
return new Vault(await createKeyPair(VAULT_ALGORITHM));
|
||||
}
|
||||
|
||||
export async function importVault(keyPair: ExportedKeyPair): Promise<Vault> {
|
||||
return new Vault(await loadKeyPair(keyPair, VAULT_ALGORITHM));
|
||||
}
|
||||
|
||||
export async function encrypt<T extends Record<string, unknown> | unknown[] | string>(value: T, publicKey: string) {
|
||||
const text = new TextEncoder().encode(JSON.stringify(value));
|
||||
return new Jose.CompactEncrypt(text)
|
||||
.setProtectedHeader({
|
||||
alg: VAULT_ALGORITHM,
|
||||
enc: VAULT_ENCRYPTION,
|
||||
})
|
||||
.encrypt(await importPublicKey(publicKey, VAULT_ALGORITHM));
|
||||
}
|
||||
|
||||
export async function decrypt<T>(cypherText: string, privateKey: string): Promise<T> {
|
||||
const { plaintext } = await Jose.compactDecrypt(cypherText, await importPrivateKey(privateKey, VAULT_ALGORITHM));
|
||||
return JSON.parse(new TextDecoder().decode(plaintext));
|
||||
}
|
||||
Reference in New Issue
Block a user