Copy architecture docs, ADRs, storage domain specs, research, reviews, and 56 storage architecture tasks from the alkhub_ts monorepo. Adapt for standalone @alkdev/hub repo structure (src/ not packages/hub/). Sanitize all sensitive information: - Replace private IPs (10.0.0.1) with localhost defaults - Remove internal server hostnames (dev1, ns528096) - Replace /workspace/ private paths with npm package references - Remove hardcoded credentials from examples - Rewrite infrastructure.md without private network details Add Deno project scaffolding: deno.json (pinned deps), .gitignore, AGENTS.md, entry point. Migrate existing code stubs (crypto, config types, logger) with updated import paths.
119 lines
2.7 KiB
TypeScript
119 lines
2.7 KiB
TypeScript
import { encodeBase64, decodeBase64 } from "@std/encoding";
|
|
|
|
/**
|
|
* Encrypted data structure with key version support
|
|
*/
|
|
export interface EncryptedData {
|
|
keyVersion: number;
|
|
salt: string;
|
|
iv: string;
|
|
data: string;
|
|
}
|
|
|
|
/**
|
|
* Generate a random salt (16 bytes recommended for PBKDF2)
|
|
*/
|
|
function generateSalt(): Uint8Array {
|
|
return crypto.getRandomValues(new Uint8Array(16));
|
|
}
|
|
|
|
/**
|
|
* Generate a random IV (12 bytes recommended for AES-GCM)
|
|
*/
|
|
function generateIV(): Uint8Array {
|
|
return crypto.getRandomValues(new Uint8Array(12));
|
|
}
|
|
|
|
/**
|
|
* Derive a key from a password using PBKDF2
|
|
*/
|
|
async function deriveKey(
|
|
password: string,
|
|
salt: Uint8Array,
|
|
keyVersion: number = 1
|
|
): Promise<CryptoKey> {
|
|
const encoder = new TextEncoder();
|
|
const keyMaterial = await crypto.subtle.importKey(
|
|
"raw",
|
|
encoder.encode(password),
|
|
{ name: "PBKDF2" },
|
|
false,
|
|
["deriveBits", "deriveKey"]
|
|
);
|
|
|
|
// Adjust iterations based on key version (for future rotation)
|
|
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"]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Encrypt data using AES-GCM with key version support
|
|
*/
|
|
export async function encrypt(
|
|
plaintext: string,
|
|
password:string,
|
|
keyVersion: number = 1
|
|
): Promise<EncryptedData> {
|
|
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))
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Decrypt data using AES-GCM with key version support
|
|
*/
|
|
export async function decrypt(encryptedData: EncryptedData, password:string): Promise<string> {
|
|
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");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a secure random encryption key
|
|
*/
|
|
export function generateEncryptionKey(): string {
|
|
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
return encodeBase64(bytes);
|
|
} |