mirror of
https://github.com/cupcakearmy/occulto.git
synced 2026-06-11 05:06:48 +00:00
update deps, cleanup
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,6 +0,0 @@
|
|||||||
# Roadmap
|
|
||||||
|
|
||||||
## Todo
|
|
||||||
|
|
||||||
- Sym aes-gcm
|
|
||||||
- Hash sha1-512
|
|
||||||
+17
-22
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1128
-2115
File diff suppressed because it is too large
Load Diff
+100
-75
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"],
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user