140 lines
3.4 KiB
TypeScript
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;
|
|
}
|
|
}
|