12345678910111213141516import {17 computeVaultId,18 deriveSyncIdentityKeypair,19 deriveSyncSigningKeypair,20 pubkeyToHex,21} from "../shared/sync-identity";22import type { LocalIdentity } from "../shared/pairing-handshake";23import type { DeviceType } from "../shared/sync-types";24import { getRootKeyHex, isUnlocked } from "./vault";2526const DEVICE_ID_KEY = "peach_device_id";27const DEVICE_NAME_KEY = "peach_device_name";2829export interface LocalDeviceIdentity {30 deviceId: string;31 deviceName: string;32 deviceType: DeviceType;33 longTermPublicKeyHex: string;34 signingPublicKeyHex: string;35}3637export async function getOrCreateDeviceId(): Promise<string> {38 39 40 41 42 const local = await chrome.storage.local.get(DEVICE_ID_KEY);43 const localId = (local as Record<string, unknown>)[DEVICE_ID_KEY];44 if (typeof localId === "string" && localId.length > 0) return localId;4546 const fresh = generateUuidV4();47 await chrome.storage.local.set({ [DEVICE_ID_KEY]: fresh });48 return fresh;49}5051export async function getDeviceName(): Promise<string> {52 const result = await chrome.storage.sync.get(DEVICE_NAME_KEY);53 const stored = (result as Record<string, unknown>)[DEVICE_NAME_KEY];54 if (typeof stored === "string" && stored.length > 0) return stored;55 return defaultDeviceName();56}5758export async function setDeviceName(name: string): Promise<void> {59 const trimmed = name.trim();60 if (!trimmed) throw new Error("device name must not be empty");61 if (trimmed.length > 64) throw new Error("device name must be 64 chars or fewer");62 await chrome.storage.sync.set({ [DEVICE_NAME_KEY]: trimmed });63}64656667686970export async function buildLocalIdentity(71 masterKey: Uint8Array,72): Promise<LocalDeviceIdentity> {73 const deviceId = await getOrCreateDeviceId();74 const deviceName = await getDeviceName();75 const kp = deriveSyncIdentityKeypair(masterKey, deviceId);76 const signing = await deriveSyncSigningKeypair(masterKey, deviceId);77 return {78 deviceId,79 deviceName,80 deviceType: "extension",81 longTermPublicKeyHex: pubkeyToHex(kp.publicKey),82 signingPublicKeyHex: pubkeyToHex(signing.publicKeyRaw),83 };84}85868788899091export function deriveLocalSyncKeypair(masterKey: Uint8Array, deviceId: string) {92 return deriveSyncIdentityKeypair(masterKey, deviceId);93}9495function defaultDeviceName(): string {96 97 98 const ua = (typeof navigator !== "undefined" ? navigator.userAgent : "") || "";99 if (/Firefox/i.test(ua)) return "Firefox";100 if (/Edg\//i.test(ua)) return "Edge";101 if (/Chrome/i.test(ua)) return "Chrome";102 if (/Safari/i.test(ua)) return "Safari";103 return "Browser";104}105106function generateUuidV4(): string {107 108 109 if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {110 return crypto.randomUUID();111 }112 const bytes = new Uint8Array(16);113 crypto.getRandomValues(bytes as Uint8Array<ArrayBuffer>);114 bytes[6] = (bytes[6] & 0x0f) | 0x40;115 bytes[8] = (bytes[8] & 0x3f) | 0x80;116 const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");117 return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;118}119120121122123124125126127128export async function getLocalPairingIdentity(): Promise<{129 identity: LocalIdentity;130 privateKey: Uint8Array;131 publicKey: Uint8Array;132}> {133 if (!isUnlocked()) throw new Error("vault_locked");134 const masterKeyHex = getRootKeyHex();135 if (!masterKeyHex) throw new Error("vault_locked");136 const masterKey = new Uint8Array(masterKeyHex.length / 2);137 for (let i = 0; i < masterKey.length; i++) {138 masterKey[i] = parseInt(masterKeyHex.slice(i * 2, i * 2 + 2), 16);139 }140 try {141 const base = await buildLocalIdentity(masterKey);142 const vaultId = await computeVaultId(masterKey);143 const keys = deriveSyncIdentityKeypair(masterKey, base.deviceId);144 return {145 identity: { ...base, vaultId },146 privateKey: keys.secretKey,147 publicKey: keys.publicKey,148 };149 } finally {150 masterKey.fill(0);151 }152}