From 09f8f7ef51ee90abe8b97a25d7400b22a7da8780 Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Fri, 29 May 2026 10:57:02 +0000 Subject: [PATCH] Add crypto module: encrypt, decrypt, generateEncryptionKey, EncryptedDataSchema --- deno.json | 1 + src/graphs/crypto.ts | 103 +++++++++++++++++++++++++++++++++++++++++++ src/graphs/mod.ts | 9 +++- 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 src/graphs/crypto.ts diff --git a/deno.json b/deno.json index bf8493b..387719e 100644 --- a/deno.json +++ b/deno.json @@ -16,6 +16,7 @@ "drizzle-orm/pg-core": "npm:drizzle-orm/pg-core", "@libsql/client": "npm:@libsql/client", "postgres": "npm:postgres", + "@std/encoding": "jsr:@std/encoding", "@std/assert": "jsr:@std/assert", "@std/flags": "jsr:@std/flags", "@std/path": "jsr:@std/path" diff --git a/src/graphs/crypto.ts b/src/graphs/crypto.ts new file mode 100644 index 0000000..98bc06b --- /dev/null +++ b/src/graphs/crypto.ts @@ -0,0 +1,103 @@ +import { type Static, Type } from "@alkdev/typebox"; +import { decodeBase64, encodeBase64 } from "@std/encoding"; + +export const EncryptedDataSchema = Type.Object({ + keyVersion: Type.Integer({ minimum: 1 }), + salt: Type.String(), + iv: Type.String(), + data: Type.String(), +}); + +export type EncryptedData = Static; + +function generateSalt(): Uint8Array { + return crypto.getRandomValues(new Uint8Array(16)); +} + +function generateIV(): Uint8Array { + return crypto.getRandomValues(new Uint8Array(12)); +} + +async function deriveKey( + password: string, + salt: Uint8Array, + keyVersion: number = 1, +): Promise { + const encoder = new TextEncoder(); + const keyMaterial = await crypto.subtle.importKey( + "raw", + encoder.encode(password), + { name: "PBKDF2" }, + false, + ["deriveBits", "deriveKey"], + ); + + const iterations = keyVersion === 1 ? 100000 : 200000; + + return crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt: new Uint8Array(salt), + iterations, + hash: "SHA-256", + }, + keyMaterial, + { name: "AES-GCM", length: 256 }, + true, + ["encrypt", "decrypt"], + ); +} + +export async function encrypt( + plaintext: string, + password: string, + keyVersion: number = 1, +): Promise { + const encoder = new TextEncoder(); + + const salt = generateSalt(); + const iv = generateIV(); + const key = await deriveKey(password, salt, keyVersion); + + const encrypted = await crypto.subtle.encrypt( + { name: "AES-GCM", iv: new Uint8Array(iv) }, + key, + encoder.encode(plaintext), + ); + + return { + keyVersion, + salt: encodeBase64(salt), + iv: encodeBase64(iv), + data: encodeBase64(new Uint8Array(encrypted)), + }; +} + +export async function decrypt( + encryptedData: EncryptedData, + password: string, +): Promise { + const decoder = new TextDecoder(); + + const salt = decodeBase64(encryptedData.salt); + const iv = decodeBase64(encryptedData.iv); + const data = decodeBase64(encryptedData.data); + + const key = await deriveKey(password, salt, encryptedData.keyVersion); + + try { + const decrypted = await crypto.subtle.decrypt( + { name: "AES-GCM", iv: new Uint8Array(iv) }, + key, + data, + ); + return decoder.decode(decrypted); + } catch (_error) { + throw new Error("Decryption failed: Invalid data or key"); + } +} + +export function generateEncryptionKey(): string { + const bytes = crypto.getRandomValues(new Uint8Array(32)); + return encodeBase64(bytes); +} diff --git a/src/graphs/mod.ts b/src/graphs/mod.ts index 50546d2..83b656c 100644 --- a/src/graphs/mod.ts +++ b/src/graphs/mod.ts @@ -3,4 +3,11 @@ export { Metagraph, GRAPH_STATUS, GraphStatus, -} from "./modules/metagraph.ts"; \ No newline at end of file +} from "./modules/metagraph.ts"; +export { + encrypt, + decrypt, + generateEncryptionKey, + EncryptedDataSchema, + type EncryptedData, +} from "./crypto.ts"; \ No newline at end of file