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