1234567891011121314151617181920import { HKDF } from "@stablelib/hkdf";21import { SHA256 } from "@stablelib/sha256";22import { generateKeyPairFromSeed } from "@stablelib/x25519";23import { blake3 } from "hash-wasm";2425const SYNC_IDENTITY_SALT = new TextEncoder().encode("peach-sync-identity-v1");26const VAULT_ID_INFO = new TextEncoder().encode("peach-vault-id");27const VAULT_ID_HEX_LENGTH = 16; 2829export interface SyncIdentityKeypair {30 publicKey: Uint8Array; 31 secretKey: Uint8Array; 32}33343536373839export function deriveSyncIdentityKeypair(40 masterKey: Uint8Array,41 deviceId: string,42): SyncIdentityKeypair {43 if (masterKey.length === 0) throw new Error("masterKey must not be empty");44 if (!deviceId) throw new Error("deviceId must not be empty");45 const info = new TextEncoder().encode(deviceId);46 47 48 const seed = new HKDF(SHA256, masterKey, SYNC_IDENTITY_SALT, info).expand(32);49 const kp = generateKeyPairFromSeed(seed);50 return { publicKey: kp.publicKey, secretKey: kp.secretKey };51}5253545556575859export async function computeVaultId(masterKey: Uint8Array): Promise<string> {60 if (masterKey.length === 0) throw new Error("masterKey must not be empty");61 const buf = new Uint8Array(masterKey.length + VAULT_ID_INFO.length);62 buf.set(masterKey, 0);63 buf.set(VAULT_ID_INFO, masterKey.length);64 const fullHex = await blake3(buf);65 return fullHex.slice(0, VAULT_ID_HEX_LENGTH);66}6768export function pubkeyToHex(pk: Uint8Array): string {69 let s = "";70 for (let i = 0; i < pk.length; i++) {71 s += pk[i].toString(16).padStart(2, "0");72 }73 return s;74}7576export function hexToPubkey(hex: string): Uint8Array {77 if (hex.length % 2 !== 0) throw new Error("hex length must be even");78 const out = new Uint8Array(hex.length / 2);79 for (let i = 0; i < out.length; i++) {80 const byte = parseInt(hex.slice(i * 2, i * 2 + 2), 16);81 if (Number.isNaN(byte)) throw new Error(`invalid hex at offset ${i * 2}`);82 out[i] = byte;83 }84 return out;85}