update deps, cleanup

This commit is contained in:
2026-06-01 01:05:14 +02:00
parent 4c61c067e6
commit 51c3e3d05f
22 changed files with 1590 additions and 2542 deletions
+3 -3
View File
@@ -11,16 +11,16 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v6
- name: Install Node - name: Install Node
uses: actions/setup-node@v3 uses: actions/setup-node@v6
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Setup PNPM - name: Setup PNPM
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v6
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
+3 -3
View File
@@ -12,15 +12,15 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v6
- name: Install Node - name: Install Node
uses: actions/setup-node@v3 uses: actions/setup-node@v6
with: with:
node-version-file: .nvmrc node-version-file: .nvmrc
- name: Setup PNPM - name: Setup PNPM
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v6
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
+1 -1
View File
@@ -1 +1 @@
v22.5.1 v24
-6
View File
@@ -1,6 +0,0 @@
# Roadmap
## Todo
- Sym aes-gcm
- Hash sha1-512
+17 -22
View File
@@ -21,35 +21,30 @@
"sideEffects": false, "sideEffects": false,
"type": "module", "type": "module",
"exports": { "exports": {
"types": "./dist/index.d.ts", "types": "./dist/index.d.mts",
"import": "./dist/index.js" "import": "./dist/index.mjs"
}, },
"files": [ "files": [
"dist" "dist"
], ],
"scripts": { "scripts": {
"build": "tsdown",
"docs": "typedoc", "docs": "typedoc",
"test:node": "vitest", "typecheck": "tsc --noEmit",
"test:browsers": "zx test.browsers.js", "prepublishOnly": "CI=1 run-s typecheck test build",
"test": "CI=1 run-s build test:*", "test": "vitest"
"build": "tsc",
"clean": "rm -rf ./dist",
"dev": "vitest",
"prepublishOnly": "run-s clean test"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/strictest": "^2.0.5", "@testing-library/dom": "^10.4.1",
"@types/node": "^22.5.2", "@tsconfig/strictest": "^2.0.8",
"@vitest/browser": "^2.0.5", "@types/node": "^24.12.4",
"npm-run-all": "^4.1.5", "@vitest/browser-playwright": "^4.1.7",
"playwright": "^1.46.1", "npm-run-all2": "^9.0.1",
"typedoc": "^0.26.6", "playwright": "^1.60.0",
"typescript": "^5.5.4", "tsdown": "^0.22.1",
"vitest": "^2.0.5", "typedoc": "^0.28.19",
"zx": "^8.1.5" "typescript": "^6.0.3",
"vitest": "^4.1.7"
}, },
"packageManager": "pnpm@9.8.0", "packageManager": "pnpm@11.5.0"
"engines": {
"node": ">=18"
}
} }
+1128 -2115
View File
File diff suppressed because it is too large Load Diff
+100 -75
View File
@@ -1,133 +1,157 @@
import { type TypedArray } from '../utils/base.js' import { type TypedArray } from "../utils/base.js";
import { getCrypto } from './crypto.js' import { getCrypto } from "./crypto.js";
import { Base64, Bytes } from './encoding.js' import { Base64, Bytes } from "./encoding.js";
import { Hashes } from './hash.js' import { Hashes } from "./hash.js";
import { getRandomBytes } from './random.js' import { getRandomBytes } from "./random.js";
const Params = { const Params = {
GCM: { GCM: {
ivLength: 12, ivLength: 12,
tagLength: 128, tagLength: 128,
}, },
} };
export type KeyData = { export type KeyData = {
name: 'PBKDF2' name: "PBKDF2";
hash: Hashes hash: Hashes;
iterations: number iterations: number;
salt: TypedArray salt: TypedArray;
length: number length: number;
} };
/** /**
* AES operation modes. * AES operation modes.
*/ */
export enum Modes { export enum Modes {
AES_GCM = 'AES-GCM', AES_GCM = "AES-GCM",
} }
export class AES { export class AES {
static Modes = Modes static Modes = Modes;
// delimiter with a character that is not allowed in base64 or hex // delimiter with a character that is not allowed in base64 or hex
private static delimiter = '--' private static delimiter = "--";
private static delimiterEasy = '---' private static delimiterEasy = "---";
private static InvalidCiphertext = new Error('Invalid ciphertext') private static InvalidCiphertext = new Error("Invalid ciphertext");
private static async join(...args: TypedArray[]): Promise<string> { private static async join(...args: TypedArray[]): Promise<string> {
const strings = await Promise.all(args.map(Base64.encode)) const strings = await Promise.all(args.map(Base64.encode));
return strings.join(AES.delimiter) return strings.join(AES.delimiter);
} }
private static async split(ciphertext: string): Promise<TypedArray[]> { private static async split(ciphertext: string): Promise<TypedArray[]> {
const splitted = ciphertext.split(AES.delimiter) const splitted = ciphertext.split(AES.delimiter);
return Promise.all(splitted.map(Base64.decode)) return Promise.all(splitted.map(Base64.decode));
} }
/** /**
* Derive a key from a password. * Derive a key from a password.
* To be used if the password is not 128, 192 or 256 bits or human made, non generated keys. * To be used if the password is not 128, 192 or 256 bits or human made, non generated keys.
*/ */
static async derive(key: string, options?: KeyData): Promise<[TypedArray, KeyData]> { static async derive(
key: string,
options?: KeyData,
): Promise<[TypedArray, KeyData]> {
options ??= { options ??= {
name: 'PBKDF2', name: "PBKDF2",
hash: Hashes.SHA_512, hash: Hashes.SHA_512,
iterations: 100_000, iterations: 100_000,
length: 256, length: 256,
salt: await getRandomBytes(16), salt: await getRandomBytes(16),
} };
const c = await getCrypto() const c = await getCrypto();
const keyBuffer = await c.subtle.importKey('raw', Bytes.encode(key), options.name, false, [ const keyBuffer = await c.subtle.importKey(
'deriveBits', "raw",
'deriveKey', Bytes.encode(key),
]) options.name,
const bits = await c.subtle.deriveBits(options, keyBuffer, options.length) false,
return [new Uint8Array(bits), options] ["deriveBits", "deriveKey"],
);
const bits = await c.subtle.deriveBits(options, keyBuffer, options.length);
return [new Uint8Array(bits), options];
} }
static async encrypt(data: TypedArray, key: TypedArray, mode: Modes = Modes.AES_GCM): Promise<string> { static async encrypt(
const c = await getCrypto() data: TypedArray,
key: TypedArray,
mode: Modes = Modes.AES_GCM,
): Promise<string> {
const c = await getCrypto();
let iv: Uint8Array let iv: Uint8Array;
let alg: AlgorithmIdentifier let alg: AlgorithmIdentifier;
switch (mode) { switch (mode) {
case Modes.AES_GCM: case Modes.AES_GCM:
iv = c.getRandomValues(new Uint8Array(Params.GCM.ivLength)) iv = c.getRandomValues(new Uint8Array(Params.GCM.ivLength));
alg = mode alg = mode;
break break;
default: default:
throw new Error('Unsupported mode') throw new Error("Unsupported mode");
} }
const keyObj = await c.subtle.importKey('raw', key, alg, false, ['encrypt']) const keyObj = await c.subtle.importKey("raw", key, alg, false, [
const encrypted = await c.subtle.encrypt({ name: alg, iv }, keyObj, data) "encrypt",
const encryptedBuffer = new Uint8Array(encrypted) ]);
// @ts-expect-error
const encrypted = await c.subtle.encrypt({ name: alg, iv }, keyObj, data);
const encryptedBuffer = new Uint8Array(encrypted);
return AES.join(Bytes.encode(alg), iv, encryptedBuffer) // @ts-expect-error
return AES.join(Bytes.encode(alg), iv, encryptedBuffer);
} }
static async decrypt(ciphertext: string, key: TypedArray): Promise<TypedArray> { static async decrypt(
const c = await getCrypto() ciphertext: string,
key: TypedArray,
): Promise<TypedArray> {
const c = await getCrypto();
const [alg, iv, data] = await AES.split(ciphertext) const [alg, iv, data] = await AES.split(ciphertext);
if (!alg || !iv || !data) throw this.InvalidCiphertext if (!alg || !iv || !data) throw this.InvalidCiphertext;
const mode = Bytes.decode(alg) const mode = Bytes.decode(alg);
switch (mode) { switch (mode) {
case Modes.AES_GCM: case Modes.AES_GCM:
break break;
default: default:
throw new Error('Unsupported mode') throw new Error("Unsupported mode");
} }
const keyObj = await c.subtle.importKey('raw', key, mode, false, ['decrypt']) const keyObj = await c.subtle.importKey("raw", key, mode, false, [
const decrypted = await c.subtle.decrypt({ name: mode, iv }, keyObj, data) "decrypt",
return new Uint8Array(decrypted) ]);
const decrypted = await c.subtle.decrypt({ name: mode, iv }, keyObj, data);
return new Uint8Array(decrypted);
} }
static async encryptEasy(data: string | TypedArray, key: string, mode: Modes = Modes.AES_GCM): Promise<string> { static async encryptEasy(
const dataBuffer = typeof data === 'string' ? Bytes.encode(data) : data data: string | TypedArray,
const [keyDerived, options] = await AES.derive(key) key: string,
mode: Modes = Modes.AES_GCM,
): Promise<string> {
const dataBuffer = typeof data === "string" ? Bytes.encode(data) : data;
const [keyDerived, options] = await AES.derive(key);
const ciphertext = await this.encrypt(dataBuffer, keyDerived, mode) const ciphertext = await this.encrypt(dataBuffer, keyDerived, mode);
const header = await this.join( const header = await this.join(
Bytes.encode(options.name), Bytes.encode(options.name),
Bytes.encode(options.hash), Bytes.encode(options.hash),
Bytes.encode(options.iterations.toString()), Bytes.encode(options.iterations.toString()),
options.salt, options.salt,
Bytes.encode(options.length.toString()) Bytes.encode(options.length.toString()),
) );
return [header, ciphertext].join(this.delimiterEasy) return [header, ciphertext].join(this.delimiterEasy);
} }
static async decryptEasy(ciphertext: string, key: string): Promise<string> { static async decryptEasy(ciphertext: string, key: string): Promise<string> {
const [header, data] = ciphertext.split(this.delimiterEasy) const [header, data] = ciphertext.split(this.delimiterEasy);
if (!header || !data) throw this.InvalidCiphertext if (!header || !data) throw this.InvalidCiphertext;
const [name, hash, iterations, salt, length] = await this.split(header) const [name, hash, iterations, salt, length] = await this.split(header);
if (!name || !hash || !iterations || !salt || !length) throw this.InvalidCiphertext if (!name || !hash || !iterations || !salt || !length)
throw this.InvalidCiphertext;
const options: KeyData = { const options: KeyData = {
name: Bytes.decode(name) as any, name: Bytes.decode(name) as any,
@@ -135,25 +159,26 @@ export class AES {
iterations: parseInt(Bytes.decode(iterations)), iterations: parseInt(Bytes.decode(iterations)),
salt, salt,
length: parseInt(Bytes.decode(length)), length: parseInt(Bytes.decode(length)),
} };
if (isNaN(options.iterations) || isNaN(options.length)) throw this.InvalidCiphertext if (isNaN(options.iterations) || isNaN(options.length))
throw this.InvalidCiphertext;
const [keyDerived] = await AES.derive(key, options) const [keyDerived] = await AES.derive(key, options);
const decrypted = await this.decrypt(data, keyDerived) const decrypted = await this.decrypt(data, keyDerived);
return Bytes.decode(decrypted) return Bytes.decode(decrypted);
} }
static async generateKey(): Promise<TypedArray> { static async generateKey(): Promise<TypedArray> {
const c = await getCrypto() const c = await getCrypto();
const key = await c.subtle.generateKey( const key = await c.subtle.generateKey(
{ {
name: 'AES-GCM', name: "AES-GCM",
length: 256, length: 256,
}, },
true, true,
['encrypt', 'decrypt'] ["encrypt", "decrypt"],
) );
const buffer = await c.subtle.exportKey('raw', key) const buffer = await c.subtle.exportKey("raw", key);
return new Uint8Array(buffer) return new Uint8Array(buffer);
} }
} }
+10 -13
View File
@@ -1,20 +1,17 @@
import { isBrowser } from '../utils/base.js' import { isBrowser } from "../utils/base.js";
let crypto: typeof window.crypto | null = null let crypto: Crypto | null = null;
export async function getCrypto(): Promise<typeof window.crypto> { export async function getCrypto(): Promise<Crypto> {
if (!crypto) { if (!crypto) {
if (isBrowser) crypto = window.crypto if (isBrowser) {
else if (typeof require !== 'undefined') { crypto = window.crypto;
const { webcrypto } = await require('crypto') }
crypto = webcrypto if (typeof globalThis !== "undefined") {
} else { crypto = globalThis.crypto;
// @ts-ignore
const { webcrypto } = await import('crypto')
crypto = webcrypto as any
} }
} }
if (!crypto) throw new Error('No crypto available') if (!crypto) throw new Error("No crypto available");
return crypto return crypto;
} }
+34 -30
View File
@@ -1,60 +1,63 @@
import { split, type TypedArray } from '../utils/base.js' import { split, type TypedArray } from "../utils/base.js";
export class Base64 { export class Base64 {
private static prefix = 'data:application/octet-stream;base64,' private static prefix = "data:application/octet-stream;base64,";
static encode(s: TypedArray): Promise<string> { static encode(s: TypedArray): Promise<string> {
return split({ return split({
async node() { async node() {
return Buffer.from(s).toString('base64') // @ts-expect-error
return Buffer.from(s).toString("base64");
}, },
async browser() { async browser() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader() const reader = new FileReader();
reader.onload = function (event) { reader.onload = function (event) {
const data = event.target?.result const data = event.target?.result;
if (typeof data === 'string') resolve(data.slice(Base64.prefix.length)) if (typeof data === "string")
else reject(new Error('Failed to read file')) resolve(data.slice(Base64.prefix.length));
} else reject(new Error("Failed to read file"));
reader.readAsDataURL(new Blob([s])) };
}) reader.readAsDataURL(new Blob([s]));
});
}, },
}) });
} }
static decode(s: string): Promise<TypedArray> { static decode(s: string): Promise<TypedArray> {
return split({ return split({
async node() { async node() {
return Buffer.from(s, 'base64') return Buffer.from(s, "base64");
}, },
async browser() { async browser() {
const ab = await fetch(Base64.prefix + s) const ab = await fetch(Base64.prefix + s)
.then((r) => r.blob()) .then((r) => r.blob())
.then((b) => b.arrayBuffer()) .then((b) => b.arrayBuffer());
return new Uint8Array(ab) return new Uint8Array(ab);
}, },
}) });
} }
} }
export class Hex { export class Hex {
static encode(buffer: TypedArray): string { static encode(buffer: TypedArray): string {
let s = '' let s = "";
// @ts-expect-error
for (const i of new Uint8Array(buffer)) { for (const i of new Uint8Array(buffer)) {
s += i.toString(16).padStart(2, '0') s += i.toString(16).padStart(2, "0");
} }
return s return s;
} }
static decode(s: string): TypedArray { static decode(s: string): TypedArray {
const size = s.length / 2 const size = s.length / 2;
const buffer = new Uint8Array(size) const buffer = new Uint8Array(size);
for (let i = 0; i < size; i++) { for (let i = 0; i < size; i++) {
const idx = i * 2 const idx = i * 2;
const segment = s.slice(idx, idx + 2) const segment = s.slice(idx, idx + 2);
buffer[i] = parseInt(segment, 16) buffer[i] = parseInt(segment, 16);
} }
return buffer return buffer;
} }
} }
@@ -62,22 +65,23 @@ export class Bytes {
static decode(data: TypedArray): string { static decode(data: TypedArray): string {
return split({ return split({
node() { node() {
return Buffer.from(data).toString('utf-8') // @ts-expect-error
return Buffer.from(data).toString("utf-8");
}, },
browser() { browser() {
return new TextDecoder().decode(data) return new TextDecoder().decode(data);
}, },
}) });
} }
static encode(data: string): TypedArray { static encode(data: string): TypedArray {
return split({ return split({
node() { node() {
return Buffer.from(data) return Buffer.from(data);
}, },
browser() { browser() {
return new TextEncoder().encode(data) return new TextEncoder().encode(data);
}, },
}) });
} }
} }
+21 -15
View File
@@ -1,6 +1,6 @@
import { type TypedArray } from '../utils/base.js' import { type TypedArray } from "../utils/base.js";
import { getCrypto } from './crypto.js' import { getCrypto } from "./crypto.js";
import { Bytes, Hex } from './encoding.js' import { Bytes, Hex } from "./encoding.js";
/** /**
* List of available hash functions. * List of available hash functions.
@@ -13,20 +13,26 @@ export enum Hashes {
/** /**
* @remarks SHA-1 is not recommended for new applications as it's not cryptographically secure. * @remarks SHA-1 is not recommended for new applications as it's not cryptographically secure.
*/ */
SHA_1 = 'SHA-1', SHA_1 = "SHA-1",
SHA_256 = 'SHA-256', SHA_256 = "SHA-256",
SHA_384 = 'SHA-384', SHA_384 = "SHA-384",
SHA_512 = 'SHA-512', SHA_512 = "SHA-512",
} }
export class Hash { export class Hash {
static async hash(data: string, hash: Hashes): Promise<string> static async hash(data: string, hash: Hashes): Promise<string>;
static async hash(data: TypedArray, hash: Hashes): Promise<TypedArray> static async hash(data: TypedArray, hash: Hashes): Promise<TypedArray>;
static async hash(data: string | TypedArray, hash: Hashes): Promise<string | TypedArray> { static async hash(
const isString = typeof data === 'string' data: string | TypedArray,
const c = await getCrypto() hash: Hashes,
const result = await c.subtle.digest(hash, isString ? Bytes.encode(data) : data) ): Promise<string | TypedArray> {
const buf = new Uint8Array(result) const isString = typeof data === "string";
return isString ? Hex.encode(buf) : buf const c = await getCrypto();
const result = await c.subtle.digest(
hash,
isString ? Bytes.encode(data) : data,
);
const buf = new Uint8Array(result);
return isString ? Hex.encode(buf) : buf;
} }
} }
+7 -7
View File
@@ -1,11 +1,11 @@
import { type TypedArray } from '../utils/base.js' import { type TypedArray } from "../utils/base.js";
import { getCrypto } from './crypto.js' import { getCrypto } from "./crypto.js";
export async function getRandomBytes(bytes: number): Promise<TypedArray> { export async function getRandomBytes(bytes: number): Promise<TypedArray> {
if (bytes <= 0) throw new Error('Invalid number of bytes') if (bytes <= 0) throw new Error("Invalid number of bytes");
const buffer = new Uint8Array(bytes) const buffer = new Uint8Array(bytes);
const crypto = await getCrypto() const crypto = await getCrypto();
crypto.getRandomValues(buffer) crypto.getRandomValues(buffer);
return buffer return buffer;
} }
+71 -60
View File
@@ -1,64 +1,64 @@
import type { TypedArray } from '../utils/base.js' import type { TypedArray } from "../utils/base.js";
import { getCrypto } from './crypto.js' import { getCrypto } from "./crypto.js";
import { Base64 } from './encoding.js' import { Base64 } from "./encoding.js";
const Constants = Object.freeze({ const Constants = Object.freeze({
name: 'RSA-OAEP', name: "RSA-OAEP",
hash: 'SHA-512', hash: "SHA-512",
exponent: new Uint8Array([1, 0, 1]), exponent: new Uint8Array([1, 0, 1]),
error: { error: {
invalidKey: new Error('invalid key'), invalidKey: new Error("invalid key"),
shouldBePublicKey: new Error('should be a public key'), shouldBePublicKey: new Error("should be a public key"),
shouldBePrivateKey: new Error('should be a private key'), shouldBePrivateKey: new Error("should be a private key"),
dataTooLong: new Error('data too long'), dataTooLong: new Error("data too long"),
}, },
}) });
class Key { class Key {
/** /**
* Exports a key to a PEM string. * Exports a key to a PEM string.
*/ */
static async encode(key: CryptoKey): Promise<string> { static async encode(key: CryptoKey): Promise<string> {
const c = await getCrypto() const c = await getCrypto();
let type: 'pkcs8' | 'spki' let type: "pkcs8" | "spki";
let label: string let label: string;
switch (key.type) { switch (key.type) {
case 'private': case "private":
type = 'pkcs8' type = "pkcs8";
label = 'PRIVATE' label = "PRIVATE";
break break;
case 'public': case "public":
type = 'spki' type = "spki";
label = 'PUBLIC' label = "PUBLIC";
break break;
default: default:
throw new Error('invalid key type') throw new Error("invalid key type");
} }
const exported = await c.subtle.exportKey(type, key) const exported = await c.subtle.exportKey(type, key);
// Export in PEM format // Export in PEM format
const base64 = await Base64.encode(new Uint8Array(exported)) const base64 = await Base64.encode(new Uint8Array(exported));
const formatted = base64.match(/.{1,64}/g)?.join('\n') const formatted = base64.match(/.{1,64}/g)?.join("\n");
return `-----BEGIN ${label} KEY-----\n${formatted}\n-----END ${label} KEY-----` return `-----BEGIN ${label} KEY-----\n${formatted}\n-----END ${label} KEY-----`;
} }
static async decode(s: string): Promise<CryptoKey> { static async decode(s: string): Promise<CryptoKey> {
const isPrivate = s.includes('BEGIN PRIVATE KEY') const isPrivate = s.includes("BEGIN PRIVATE KEY");
const cleaned = s.replace(/-----.*?-----/g, '').replace(/\s/g, '') const cleaned = s.replace(/-----.*?-----/g, "").replace(/\s/g, "");
const bytes = await Base64.decode(cleaned) const bytes = await Base64.decode(cleaned);
const c = await getCrypto() const c = await getCrypto();
return await c.subtle.importKey( return await c.subtle.importKey(
isPrivate ? 'pkcs8' : 'spki', isPrivate ? "pkcs8" : "spki",
bytes, bytes,
{ {
name: Constants.name, name: Constants.name,
hash: Constants.hash, hash: Constants.hash,
}, },
true, true,
isPrivate ? ['decrypt'] : ['encrypt'] isPrivate ? ["decrypt"] : ["encrypt"],
) );
} }
/** /**
@@ -67,20 +67,22 @@ class Key {
* https://www.rfc-editor.org/rfc/rfc2437#section-7.1.1 * https://www.rfc-editor.org/rfc/rfc2437#section-7.1.1
*/ */
static getMaxMessageSize(key: CryptoKey): number { static getMaxMessageSize(key: CryptoKey): number {
if (key.type !== 'public') throw Constants.error.shouldBePublicKey if (key.type !== "public") throw Constants.error.shouldBePublicKey;
// @ts-ignore // @ts-ignore
const mod = key?.algorithm?.modulusLength const mod = key?.algorithm?.modulusLength;
if (isNaN(mod)) throw Constants.error.invalidKey if (isNaN(mod)) throw Constants.error.invalidKey;
return mod / 8 - (2 * 512) / 8 - 2 return mod / 8 - (2 * 512) / 8 - 2;
} }
} }
export class RSA { export class RSA {
static async generateKeyPair(bits: number = 2 ** 12): Promise<{ private: string; public: string }> { static async generateKeyPair(
const c = await getCrypto() bits: number = 2 ** 12,
): Promise<{ private: string; public: string }> {
const c = await getCrypto();
if (bits < 2 ** 11) { if (bits < 2 ** 11) {
throw new Error('bit sizes below 2048 are considered insecure.') throw new Error("bit sizes below 2048 are considered insecure.");
} }
const pair = await c.subtle.generateKey( const pair = await c.subtle.generateKey(
@@ -91,47 +93,56 @@ export class RSA {
hash: Constants.hash, hash: Constants.hash,
}, },
true, true,
['encrypt', 'decrypt'] ["encrypt", "decrypt"],
) );
return { return {
private: await Key.encode(pair.privateKey), private: await Key.encode(pair.privateKey),
public: await Key.encode(pair.publicKey), public: await Key.encode(pair.publicKey),
} };
} }
static async encrypt(data: TypedArray, key: string): Promise<TypedArray> { static async encrypt(data: TypedArray, key: string): Promise<TypedArray> {
let keyObj: CryptoKey let keyObj: CryptoKey;
try { try {
keyObj = await Key.decode(key) keyObj = await Key.decode(key);
} catch (e) { } catch (e) {
throw Constants.error.invalidKey throw Constants.error.invalidKey;
} }
if (keyObj.type !== 'public') { if (keyObj.type !== "public") {
throw Constants.error.shouldBePublicKey throw Constants.error.shouldBePublicKey;
} }
// Check if data is too large // Check if data is too large
if (data.length > Key.getMaxMessageSize(keyObj)) throw Constants.error.dataTooLong if (data.byteLength > Key.getMaxMessageSize(keyObj))
throw Constants.error.dataTooLong;
const c = await getCrypto() const c = await getCrypto();
const encrypted = await c.subtle.encrypt({ name: Constants.name }, keyObj, data) const encrypted = await c.subtle.encrypt(
return new Uint8Array(encrypted) { name: Constants.name },
keyObj,
data,
);
return new Uint8Array(encrypted);
} }
static async decrypt(data: TypedArray, key: string): Promise<TypedArray> { static async decrypt(data: TypedArray, key: string): Promise<TypedArray> {
let keyObj: CryptoKey let keyObj: CryptoKey;
try { try {
keyObj = await Key.decode(key) keyObj = await Key.decode(key);
} catch (e) { } catch (e) {
throw Constants.error.invalidKey throw Constants.error.invalidKey;
} }
if (keyObj.type !== 'private') { if (keyObj.type !== "private") {
throw Constants.error.shouldBePrivateKey throw Constants.error.shouldBePrivateKey;
} }
const c = await getCrypto() const c = await getCrypto();
const decrypted = await c.subtle.decrypt({ name: Constants.name }, keyObj, data) const decrypted = await c.subtle.decrypt(
return new Uint8Array(decrypted) { name: Constants.name },
keyObj,
data,
);
return new Uint8Array(decrypted);
} }
} }
+6 -6
View File
@@ -1,6 +1,6 @@
export * from './crypto/aes.js' export * from "./crypto/aes.js";
export * from './crypto/encoding.js' export * from "./crypto/encoding.js";
export * from './crypto/hash.js' export * from "./crypto/hash.js";
export * from './crypto/random.js' export * from "./crypto/random.js";
export * from './crypto/rsa.js' export * from "./crypto/rsa.js";
export { TypedArray } from './utils/base.js' export type { TypedArray } from "./utils/base.js";
+20 -14
View File
@@ -1,27 +1,33 @@
/** /**
* @internal * @internal
*/ */
export const isBrowser = typeof window !== 'undefined' export const isBrowser = typeof window !== "undefined";
/** /**
* @internal * @internal
*/ */
export type PromiseOrValue<T> = T | Promise<T> export type PromiseOrValue<T> = T | Promise<T>;
export type TypedArray = export type TypedArray = ArrayBufferView<ArrayBuffer>;
| Int8Array // | Int8Array
| Uint8Array // | Uint8Array
| Uint8ClampedArray // | Uint8ClampedArray
| Int16Array // | Int16Array
| Uint16Array // | Uint16Array
| Int32Array // | Int32Array
| Uint32Array // | Uint32Array
| BigInt64Array // | BigInt64Array
| BigUint64Array // | BigUint64Array;
/** /**
* @internal * @internal
*/ */
export function split<T>({ node, browser }: { node: () => T; browser: () => T }) { export function split<T>({
return isBrowser ? browser() : node() node,
browser,
}: {
node: () => T;
browser: () => T;
}) {
return isBrowser ? browser() : node();
} }
-10
View File
@@ -1,10 +0,0 @@
#!/usr/bin/env zx
import 'zx/globals'
$.verbose = true
const BROWSERS = ['firefox', 'chromium', 'webkit']
await Promise.all([
BROWSERS.map((browser) => $`pnpm vitest --browser.provider=playwright --browser.name=${browser} --browser.headless`),
])
+32 -32
View File
@@ -1,44 +1,44 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from "vitest";
import { AES, Bytes, Hashes, Hex } from '../dist/index.js' import { AES, Bytes, Hashes, Hex } from "../src/index.js";
import { Precomputed } from './values.js' import { Precomputed } from "./values.js";
describe('AES', () => { describe("AES", () => {
for (const message of Object.values(Precomputed.Crypto.Messages)) { for (const message of Object.values(Precomputed.Crypto.Messages)) {
describe(`Message: ${message.slice(0, 8)}...`, () => { describe(`Message: ${message.slice(0, 8)}...`, () => {
describe('Basic API', () => { describe("Basic API", () => {
for (const keySize of [128, 256]) { for (const keySize of [128, 256]) {
it('Key Size: ' + keySize, async () => { it("Key Size: " + keySize, async () => {
const data = Bytes.encode(message) const data = Bytes.encode(message);
const [key] = await AES.derive('foo', { const [key] = await AES.derive("foo", {
name: 'PBKDF2', name: "PBKDF2",
hash: Hashes.SHA_512, hash: Hashes.SHA_512,
iterations: 1000, iterations: 1000,
length: keySize, length: keySize,
salt: Hex.decode(Precomputed.Crypto.Bytes[16]), salt: Hex.decode(Precomputed.Crypto.Bytes[16]),
}) });
const ciphertext = await AES.encrypt(data, key, AES.Modes.AES_GCM) const ciphertext = await AES.encrypt(data, key, AES.Modes.AES_GCM);
const plaintext = await AES.decrypt(ciphertext, key) const plaintext = await AES.decrypt(ciphertext, key);
expect(data.buffer).toEqual(plaintext.buffer) expect(data.buffer).toEqual(plaintext.buffer);
expect(message).toEqual(Bytes.decode(plaintext)) expect(message).toEqual(Bytes.decode(plaintext));
}) });
} }
}) });
it('Generated Key', async () => { it("Generated Key", async () => {
const key = await AES.generateKey() const key = await AES.generateKey();
const data = Bytes.encode(message) const data = Bytes.encode(message);
const ciphertext = await AES.encrypt(data, key) const ciphertext = await AES.encrypt(data, key);
const plaintext = await AES.decrypt(ciphertext, key) const plaintext = await AES.decrypt(ciphertext, key);
expect(data.buffer).toEqual(plaintext.buffer) expect(data.buffer).toEqual(plaintext.buffer);
expect(message).toEqual(Bytes.decode(plaintext)) expect(message).toEqual(Bytes.decode(plaintext));
}) });
it('Easy API', async () => { it("Easy API", async () => {
const password = 'foobar' const password = "foobar";
const encrypted = await AES.encryptEasy(message, password) const encrypted = await AES.encryptEasy(message, password);
const decrypted = await AES.decryptEasy(encrypted, password) const decrypted = await AES.decryptEasy(encrypted, password);
expect(message).toEqual(decrypted) expect(message).toEqual(decrypted);
}) });
}) });
} }
}) });
+25 -25
View File
@@ -1,38 +1,38 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from "vitest";
import { Base64, Bytes, Hex } from '../dist/index.js' import { Base64, Bytes, Hex } from "../src/index.js";
import { Precomputed } from './values.js' import { Precomputed } from "./values.js";
describe('Encoding', () => { describe("Encoding", () => {
describe('Bytes', () => { describe("Bytes", () => {
for (const [input, output] of Object.entries(Precomputed.Encoding.Bytes)) { for (const [input, output] of Object.entries(Precomputed.Encoding.Bytes)) {
it(`Should encode ${input} to ${output}`, async () => { it(`Should encode ${input} to ${output}`, async () => {
expect(Bytes.encode(input).buffer).toEqual(output.buffer) expect(Bytes.encode(input).buffer).toEqual(output.buffer);
}) });
it(`Should decode ${output} to ${input}`, async () => { it(`Should decode ${output} to ${input}`, async () => {
expect(Bytes.decode(output)).toEqual(input) expect(Bytes.decode(output)).toEqual(input);
}) });
} }
}) });
describe('Hex', () => { describe("Hex", () => {
for (const [input, output] of Object.entries(Precomputed.Encoding.Hex)) { for (const [input, output] of Object.entries(Precomputed.Encoding.Hex)) {
const buffer = Bytes.encode(input) const buffer = Bytes.encode(input);
it(`Should encode ${input} to ${output}`, async () => { it(`Should encode ${input} to ${output}`, async () => {
expect(Hex.encode(buffer)).toEqual(output) expect(Hex.encode(buffer)).toEqual(output);
}) });
it(`Should decode ${output} to ${input}`, async () => { it(`Should decode ${output} to ${input}`, async () => {
expect(Hex.decode(output).buffer).toEqual(buffer.buffer) expect(Hex.decode(output).buffer).toEqual(buffer.buffer);
}) });
} }
}) });
describe('Base64', () => { describe("Base64", () => {
for (const [input, output] of Object.entries(Precomputed.Encoding.Base64)) { for (const [input, output] of Object.entries(Precomputed.Encoding.Base64)) {
const buffer = Bytes.encode(input) const buffer = Bytes.encode(input);
it(`Should encode ${input} to ${output}`, async () => { it(`Should encode ${input} to ${output}`, async () => {
expect(await Base64.encode(buffer)).toEqual(output) expect(await Base64.encode(buffer)).toEqual(output);
}) });
it(`Should decode ${output} to ${input}`, async () => { it(`Should decode ${output} to ${input}`, async () => {
expect((await Base64.decode(output)).buffer).toEqual(buffer.buffer) expect((await Base64.decode(output)).buffer).toEqual(buffer.buffer);
}) });
} }
}) });
}) });
+16 -16
View File
@@ -1,26 +1,26 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from "vitest";
import { Bytes, Hash, Hashes, Hex } from '../dist/index.js' import { Bytes, Hash, Hashes, Hex } from "../src/index.js";
import { Precomputed } from './values.js' import { Precomputed } from "./values.js";
describe('Hash', () => { describe("Hash", () => {
for (const type of Object.keys(Hashes)) { for (const type of Object.keys(Hashes)) {
describe(type, () => { describe(type, () => {
const values = Precomputed.Hash[type] const values = Precomputed.Hash[type];
for (const [input, output] of Object.entries(values)) { for (const [input, output] of Object.entries(values)) {
if (typeof output !== 'string') throw new Error('Bad test data') if (typeof output !== "string") throw new Error("Bad test data");
it(`Should hash "${input}" to "${output.slice(0, 8)}..."`, async () => { it(`Should hash "${input}" to "${output.slice(0, 8)}..."`, async () => {
const hashed = await Hash.hash(input, Hashes[type]) const hashed = await Hash.hash(input, Hashes[type]);
expect(hashed).toEqual(output) expect(hashed).toEqual(output);
}) });
it(`Should hash "${input}" to "${output.slice(0, 8)}..." as buffer`, async () => { it(`Should hash "${input}" to "${output.slice(0, 8)}..." as buffer`, async () => {
const outputBuffer = Hex.decode(output) const outputBuffer = Hex.decode(output);
const inputBuffer = Bytes.encode(input) const inputBuffer = Bytes.encode(input);
const hashed = await Hash.hash(inputBuffer, Hashes[type]) const hashed = await Hash.hash(inputBuffer, Hashes[type]);
expect(hashed).toEqual(outputBuffer) expect(hashed).toEqual(outputBuffer);
}) });
} }
}) });
} }
}) });
+19 -15
View File
@@ -1,18 +1,22 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from "vitest";
import { getRandomBytes } from '../dist/index.js' import { getRandomBytes } from "../src/index.js";
describe('Random', () => { describe("Random", () => {
it('Should be able to create random values', async () => { it("Should be able to create random values", async () => {
const buffer = await getRandomBytes(8) const buffer = await getRandomBytes(8);
expect(buffer).instanceOf(Uint8Array) expect(buffer).instanceOf(Uint8Array);
expect(buffer.byteLength).toEqual(8) expect(buffer.byteLength).toEqual(8);
}) });
it('Should throw error on empty array', async () => { it("Should throw error on empty array", async () => {
await expect(() => getRandomBytes(0)).rejects.toThrowErrorMatchingSnapshot() await expect(() =>
}) getRandomBytes(0),
).rejects.toThrowErrorMatchingSnapshot();
});
it('Should throw error on negative bytes', async () => { it("Should throw error on negative bytes", async () => {
await expect(() => getRandomBytes(-1)).rejects.toThrowErrorMatchingSnapshot() await expect(() =>
}) getRandomBytes(-1),
}) ).rejects.toThrowErrorMatchingSnapshot();
});
});
+37 -33
View File
@@ -1,40 +1,44 @@
import { describe } from 'vitest' import { describe } from "vitest";
import { Bytes, RSA } from '../dist/index.js' import { Bytes, RSA } from "../src/index.js";
import { Precomputed } from './values.js' import { Precomputed } from "./values.js";
import { it } from 'vitest' import { it } from "vitest";
import { expect } from 'vitest' import { expect } from "vitest";
describe('RSA', () => { describe("RSA", () => {
describe('Generate keys', function () { describe("Generate keys", function () {
it('Should be able to generate a keypair', async () => { it("Should be able to generate a keypair", async () => {
await RSA.generateKeyPair() await RSA.generateKeyPair();
}) });
it('Should be able to generate a keypair with 2048bit', async () => { it("Should be able to generate a keypair with 2048bit", async () => {
await RSA.generateKeyPair(2048) await RSA.generateKeyPair(2048);
}) });
it('Should be able to generate a keypair with 4096bit', async () => { it("Should be able to generate a keypair with 4096bit", async () => {
await RSA.generateKeyPair(4096) await RSA.generateKeyPair(4096);
}) });
it('Should not be able to generate a key below 2048bit', async () => { it("Should not be able to generate a key below 2048bit", async () => {
await expect(() => RSA.generateKeyPair(1024)).rejects.toThrowErrorMatchingSnapshot() await expect(() =>
}) RSA.generateKeyPair(1024),
it('Should not be able to generate a key below 2048bit', async () => { ).rejects.toThrowErrorMatchingSnapshot();
await expect(() => RSA.generateKeyPair(-1)).rejects.toThrowErrorMatchingSnapshot() });
}) it("Should not be able to generate a key below 2048bit", async () => {
}) await expect(() =>
RSA.generateKeyPair(-1),
).rejects.toThrowErrorMatchingSnapshot();
});
});
describe('Encryption', () => { describe("Encryption", () => {
for (const message of Object.values(Precomputed.Crypto.Messages)) { for (const message of Object.values(Precomputed.Crypto.Messages)) {
it(`Should be able to encrypt and decrypt "${message.slice(0, 8)}..."`, async () => { it(`Should be able to encrypt and decrypt "${message.slice(0, 8)}..."`, async () => {
const pair = await RSA.generateKeyPair(2 ** 11) const pair = await RSA.generateKeyPair(2 ** 11);
const bytes = Bytes.encode(message) const bytes = Bytes.encode(message);
try { try {
const encrypted = await RSA.encrypt(bytes, pair.public) const encrypted = await RSA.encrypt(bytes, pair.public);
const decrypted = await RSA.decrypt(encrypted, pair.private) const decrypted = await RSA.decrypt(encrypted, pair.private);
expect(decrypted).toEqual(bytes) expect(decrypted).toEqual(bytes);
expect(message).toEqual(Bytes.decode(decrypted)) expect(message).toEqual(Bytes.decode(decrypted));
} catch {} } catch {}
}) });
} }
}) });
}) });
+35 -30
View File
@@ -1,60 +1,65 @@
export const Precomputed = { export const Precomputed = {
Encoding: { Encoding: {
Base64: { Base64: {
occulto: 'b2NjdWx0bw==', occulto: "b2NjdWx0bw==",
test: 'dGVzdA==', test: "dGVzdA==",
'hello world': 'aGVsbG8gd29ybGQ=', "hello world": "aGVsbG8gd29ybGQ=",
}, },
Hex: { Hex: {
test: '74657374', test: "74657374",
occulto: '6f6363756c746f', occulto: "6f6363756c746f",
'hello world': '68656c6c6f20776f726c64', "hello world": "68656c6c6f20776f726c64",
}, },
Bytes: { Bytes: {
test: new Uint8Array([0x74, 0x65, 0x73, 0x74]), test: new Uint8Array([0x74, 0x65, 0x73, 0x74]),
occulto: new Uint8Array([0x6f, 0x63, 0x63, 0x75, 0x6c, 0x74, 0x6f]), occulto: new Uint8Array([0x6f, 0x63, 0x63, 0x75, 0x6c, 0x74, 0x6f]),
'entropy is king': new Uint8Array([ "entropy is king": new Uint8Array([
0x65, 0x6e, 0x74, 0x72, 0x6f, 0x70, 0x79, 0x20, 0x69, 0x73, 0x20, 0x6b, 0x69, 0x6e, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x6f, 0x70, 0x79, 0x20, 0x69, 0x73, 0x20, 0x6b,
0x69, 0x6e, 0x67,
]), ]),
}, },
}, },
Hash: { Hash: {
SHA_1: { SHA_1: {
test: 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3', test: "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3",
'hello world': '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed', "hello world": "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
occulto: 'f4b27cfb9e01492f409295fbbc339753fa839c0f', occulto: "f4b27cfb9e01492f409295fbbc339753fa839c0f",
}, },
SHA_256: { SHA_256: {
test: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', test: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
'hello world': 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9', "hello world":
occulto: 'df2b97515886051821e4375a33df10486ce55cb3d14acd05dd7465f820ef2481', "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
occulto:
"df2b97515886051821e4375a33df10486ce55cb3d14acd05dd7465f820ef2481",
}, },
SHA_384: { SHA_384: {
test: '768412320f7b0aa5812fce428dc4706b3cae50e02a64caa16a782249bfe8efc4b7ef1ccb126255d196047dfedf17a0a9', test: "768412320f7b0aa5812fce428dc4706b3cae50e02a64caa16a782249bfe8efc4b7ef1ccb126255d196047dfedf17a0a9",
'hello world': 'fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd', "hello world":
occulto: '133c1968e937462ed66732409fa305c63335ee62b1114dd2d0ae98b4dd8fa6aca4656c919b295e41efa2d63f0d3c9951', "fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd",
occulto:
"133c1968e937462ed66732409fa305c63335ee62b1114dd2d0ae98b4dd8fa6aca4656c919b295e41efa2d63f0d3c9951",
}, },
SHA_512: { SHA_512: {
test: 'ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff', test: "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff",
'hello world': "hello world":
'309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f', "309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f",
occulto: occulto:
'9f7ff06148d415b12290ab7c21f021964ed627574f94f66c994aad4a8e319aa3168a9871edace3e736096cbd957cafa42dbf3feb6efd7763bf936ddc933c9470', "9f7ff06148d415b12290ab7c21f021964ed627574f94f66c994aad4a8e319aa3168a9871edace3e736096cbd957cafa42dbf3feb6efd7763bf936ddc933c9470",
}, },
}, },
Crypto: { Crypto: {
Bytes: { Bytes: {
14: 'e0b28a24252963ff30dd2bb3ec9c', 14: "e0b28a24252963ff30dd2bb3ec9c",
16: '65eeb2044e9eb115956dbf4d0d70cd8f', 16: "65eeb2044e9eb115956dbf4d0d70cd8f",
24: '9fa9e0aace3b0bdcbc871aa3ee3ddb1bece759b811fa4603', 24: "9fa9e0aace3b0bdcbc871aa3ee3ddb1bece759b811fa4603",
32: '848ca08f01f82e28bfa91c85d55ef2a98afd8b32c707c9c790e86b1c53a177e4', 32: "848ca08f01f82e28bfa91c85d55ef2a98afd8b32c707c9c790e86b1c53a177e4",
}, },
Messages: { Messages: {
test: 'test', test: "test",
occulto: 'occulto', occulto: "occulto",
weird: 'Some 🃏 weird 🃏 text', weird: "Some 🃏 weird 🃏 text",
nietzscheIpsum: nietzscheIpsum:
'Marvelous intentions joy deceptions overcome sexuality spirit against. Selfish of marvelous play dead war snare eternal-return ultimate. Reason aversion suicide.', "Marvelous intentions joy deceptions overcome sexuality spirit against. Selfish of marvelous play dead war snare eternal-return ultimate. Reason aversion suicide.",
}, },
}, },
} as const } as const;
+5 -11
View File
@@ -1,16 +1,10 @@
{ {
"extends": ["@tsconfig/strictest"], "extends": ["@tsconfig/strictest"],
"compilerOptions": { "compilerOptions": {
"target": "ES2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, "target": "ESNext",
"module": "ES2015" /* Specify what module code is generated. */, "module": "ESNext",
"rootDir": "./src" /* Specify the root folder within your source files. */, "moduleResolution": "bundler",
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, "types": ["node"],
"declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
"declarationMap": true /* Create sourcemaps for d.ts files. */,
"sourceMap": true /* Create source map files for emitted JavaScript files. */,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
"isolatedModules": false
}, },
"include": ["src/**/*"] "include": ["./src"],
} }