diff options
Diffstat (limited to 'pkg')
| -rw-r--r-- | pkg/constants.ts | 3 | ||||
| -rw-r--r-- | pkg/encoding.test.ts | 23 | ||||
| -rw-r--r-- | pkg/encoding.ts | 31 | ||||
| -rw-r--r-- | pkg/encryption.test.ts | 24 | ||||
| -rw-r--r-- | pkg/encryption.ts | 51 | ||||
| -rw-r--r-- | pkg/id.ts | 8 |
6 files changed, 140 insertions, 0 deletions
diff --git a/pkg/constants.ts b/pkg/constants.ts new file mode 100644 index 0000000..09cc451 --- /dev/null +++ b/pkg/constants.ts @@ -0,0 +1,3 @@ +export const ID_LENGTH = 16; +export const ENCRYPTION_KEY_LENGTH = 128; +export const LATEST_KEY_VERSION = 2; diff --git a/pkg/encoding.test.ts b/pkg/encoding.test.ts new file mode 100644 index 0000000..be0a7f8 --- /dev/null +++ b/pkg/encoding.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect, beforeAll } from "@jest/globals"; +import { decodeCompositeKey, encodeCompositeKey } from "./encoding"; +import { generateKey } from "./encryption"; +import { generateId } from "./id"; +import crypto from "node:crypto"; + +beforeAll(() => { + global.crypto = crypto.webcrypto; +}); +describe("composite key encoding", () => { + it("encodes and decodes composite keys", async () => { + for (let i = 0; i < 10000; i++) { + const id = generateId(); + const key = new Uint8Array(await crypto.subtle.exportKey("raw", await generateKey())); + + const encoded = encodeCompositeKey(1, id, key); + + const decoded = decodeCompositeKey(encoded); + expect(decoded.id).toEqual(id); + expect(decoded.encryptionKey).toEqual(key); + } + }); +}); diff --git a/pkg/encoding.ts b/pkg/encoding.ts new file mode 100644 index 0000000..2025133 --- /dev/null +++ b/pkg/encoding.ts @@ -0,0 +1,31 @@ +import { fromBase58, toBase58 } from "../util/base58"; +import { ID_LENGTH, ENCRYPTION_KEY_LENGTH } from "./constants"; +/** + * To share links easily, we encode the id, where the data is stored in redis, together with the secret encryption key. + */ +export function encodeCompositeKey(version: number, id: string, encryptionKey: Uint8Array): string { + if (version < 0 || version > 255) { + throw new Error("Version must fit in a byte"); + } + const compositeKey = new Uint8Array([version, ...fromBase58(id), ...encryptionKey]); + + return toBase58(compositeKey); +} + +/** + * To share links easily, we encode the id, where the data is stored in redis, together with the secret encryption key. + */ +export function decodeCompositeKey(compositeKey: string): { id: string; encryptionKey: Uint8Array; version: number } { + const decoded = fromBase58(compositeKey); + const version = decoded.at(0); + + if (version === 1 || version === 2) { + return { + id: toBase58(decoded.slice(1, 1 + ID_LENGTH)), + encryptionKey: decoded.slice(1 + ID_LENGTH, 1 + ID_LENGTH + ENCRYPTION_KEY_LENGTH), + version, + }; + } + + throw new Error(`Unsupported composite key version: ${version}`); +} diff --git a/pkg/encryption.test.ts b/pkg/encryption.test.ts new file mode 100644 index 0000000..5d4cf15 --- /dev/null +++ b/pkg/encryption.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect, beforeAll } from "@jest/globals"; +import { decrypt, encrypt } from "./encryption"; +import crypto from "node:crypto"; +import { toBase58 } from "../util/base58"; + +beforeAll(() => { + global.crypto = crypto.webcrypto; +}); +describe("aes", () => { + it("encrypts and decrypts correctly", async () => { + for (let i = 0; i < 500; i++) { + const buf = new Uint8Array(Math.ceil(Math.random() * 10 * i)); + crypto.getRandomValues(buf); + + const text = toBase58(buf); + + const { encrypted, key, iv } = await encrypt(text); + + const decrypted = await decrypt(toBase58(encrypted), key, toBase58(iv), 2); + + expect(decrypted).toEqual(text); + } + }, 30_000); +}); diff --git a/pkg/encryption.ts b/pkg/encryption.ts new file mode 100644 index 0000000..c9f0e9d --- /dev/null +++ b/pkg/encryption.ts @@ -0,0 +1,51 @@ +import { fromBase58 } from "../util/base58"; + +export async function generateKey() { + return await crypto.subtle.generateKey( + { + name: "AES-GCM", + length: 128, + }, + true, + ["encrypt", "decrypt"], + ); +} + +export async function encrypt(text: string): Promise<{ encrypted: Uint8Array; iv: Uint8Array; key: Uint8Array }> { + const key = await generateKey(); + + const iv = crypto.getRandomValues(new Uint8Array(16)); + + const encryptedBuffer = await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv, + }, + key, + new TextEncoder().encode(text), + ); + + const exportedKey = await crypto.subtle.exportKey("raw", key); + return { + encrypted: new Uint8Array(encryptedBuffer), + key: new Uint8Array(exportedKey), + iv, + }; +} + +export async function decrypt(encrypted: string, keyData: Uint8Array, iv: string, keyVersion: number): Promise<string> { + const algorithm = keyVersion === 1 ? "AES-CBC" : "AES-GCM"; + + const key = await crypto.subtle.importKey("raw", keyData, { name: algorithm, length: 128 }, false, ["decrypt"]); + + const decrypted = await crypto.subtle.decrypt( + { + name: algorithm, + iv: fromBase58(iv), + }, + key, + fromBase58(encrypted), + ); + + return new TextDecoder().decode(decrypted); +} diff --git a/pkg/id.ts b/pkg/id.ts new file mode 100644 index 0000000..efdfb5e --- /dev/null +++ b/pkg/id.ts @@ -0,0 +1,8 @@ +import { toBase58 } from "../util/base58"; +import { ID_LENGTH } from "./constants"; + +export function generateId(): string { + const bytes = new Uint8Array(ID_LENGTH); + crypto.getRandomValues(bytes); + return toBase58(bytes); +} |
