sonri/src/lib/crypto.ts
2025-06-13 19:29:45 +02:00

140 lines
3.4 KiB
TypeScript

import * as ed from '@noble/ed25519';
import { sha512 } from '@noble/hashes/sha2';
import { gzip, ungzip } from 'pako';
ed.etc.sha512Sync = sha512;
const SALT = 'SONRI-KEY-SALT';
export type KeyVault = Awaited<ReturnType<typeof deriveKeyVault>>;
// Helper: convert string to Uint8Array
export function strToUint8(str: string) {
return new TextEncoder().encode(str);
}
export function uint8ToStr(uint8: Uint8Array) {
return new TextDecoder().decode(uint8);
}
export function uint8ToHex(uint8: Uint8Array) {
return Array.from(uint8)
.map(byte => byte.toString(16).padStart(2, '0'))
.join('');
}
export function hexToUint8(hex: string): Uint8Array {
if (hex.length % 2 !== 0) {
throw new Error('Hex string must have an even length');
}
const length = hex.length / 2;
const uint8 = new Uint8Array(length);
for (let i = 0; i < length; i++) {
uint8[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
}
return uint8;
}
function uint8ToBase64(uint8: Uint8Array) {
return atob(uint8ToStr(uint8));
}
function base64ToUint8(base64: string) {
return strToUint8(btoa(base64));
}
export async function deriveKeyVault(secret: string) {
console.log(crypto);
let hash = strToUint8(secret), last = hash;
for (let i = 0; i < 10000; i++) {
const hashInput = Uint8Array.from([...strToUint8(SALT), ...hash]);
const hashBuffer = await crypto.subtle.digest('SHA-512', hashInput);
last = hash;
hash = new Uint8Array(hashBuffer);
}
const symmKey = last.slice(0, 32);
const privKey = hash.slice(0, 32);
const pubKey = ed.getPublicKey(privKey);
return {
pubKey,
privKey,
symmKey,
};
}
export function encodeKeyVault(keyVault: KeyVault) {
const jsonStr = JSON.stringify(Object.fromEntries(Object.entries(keyVault).map(([key, value]) => {
return [key, uint8ToHex(value)];
})));
return uint8ToHex(gzip(strToUint8(jsonStr)));
}
export function decodeKeyVault(encoded: string) {
const jsonStr = uint8ToStr(ungzip(hexToUint8(encoded)));
return Object.fromEntries(Object.entries(JSON.parse(jsonStr)).map(([key, value]) => {
return [key, hexToUint8(value as string)];
})) as KeyVault;
}
export async function encrypt(symmKey: Uint8Array, data: Uint8Array) {
const key = await crypto.subtle.importKey(
'raw',
symmKey,
{ name: 'AES-GCM' },
false,
['encrypt', 'decrypt'],
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedBuffer = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
data
);
return Uint8Array.from([...iv, ...new Uint8Array(encryptedBuffer)]);
}
export async function decrypt(symmKey: Uint8Array, cipher: Uint8Array) {
const key = await crypto.subtle.importKey(
'raw',
symmKey,
{ name: 'AES-GCM' },
false,
['encrypt', 'decrypt'],
);
const iv = cipher.slice(0, 12);
cipher = cipher.slice(12);
const dataBuffer = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
key,
cipher
);
return new Uint8Array(dataBuffer);
}
export async function sign(privKey: Uint8Array, data: Uint8Array): Promise<Uint8Array> {
return await ed.signAsync(data, privKey) as Uint8Array;
}
export async function verify(pubKey: Uint8Array, signature: Uint8Array, data: Uint8Array): Promise<boolean> {
try {
return await ed.verifyAsync(signature, data, pubKey);
} catch (_) {
return false;
}
}