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}505152export async function rotateDeviceId(): Promise<string> {53 const fresh = generateUuidV4();54 await chrome.storage.local.set({ [DEVICE_ID_KEY]: fresh });55 return fresh;56}5758export async function getDeviceName(): Promise<string> {59 const result = await chrome.storage.sync.get(DEVICE_NAME_KEY);60 const stored = (result as Record<string, unknown>)[DEVICE_NAME_KEY];61 if (typeof stored === "string" && stored.length > 0) return stored;62 return defaultDeviceName();63}6465export async function setDeviceName(name: string): Promise<void> {66 const trimmed = name.trim();67 if (!trimmed) throw new Error("device name must not be empty");68 if (trimmed.length > 64) throw new Error("device name must be 64 chars or fewer");69 await chrome.storage.sync.set({ [DEVICE_NAME_KEY]: trimmed });70}71727374757677export async function buildLocalIdentity(78 masterKey: Uint8Array,79): Promise<LocalDeviceIdentity> {80 const deviceId = await getOrCreateDeviceId();81 const deviceName = await getDeviceName();82 const kp = deriveSyncIdentityKeypair(masterKey, deviceId);83 const signing = await deriveSyncSigningKeypair(masterKey, deviceId);84 return {85 deviceId,86 deviceName,87 deviceType: "extension",88 longTermPublicKeyHex: pubkeyToHex(kp.publicKey),89 signingPublicKeyHex: pubkeyToHex(signing.publicKeyRaw),90 };91}92939495969798export function deriveLocalSyncKeypair(masterKey: Uint8Array, deviceId: string) {99 return deriveSyncIdentityKeypair(masterKey, deviceId);100}101102function defaultDeviceName(): string {103 104 105 const ua = (typeof navigator !== "undefined" ? navigator.userAgent : "") || "";106 if (/Firefox/i.test(ua)) return "Firefox";107 if (/Edg\//i.test(ua)) return "Edge";108 if (/Chrome/i.test(ua)) return "Chrome";109 if (/Safari/i.test(ua)) return "Safari";110 return "Browser";111}112113function generateUuidV4(): string {114 115 116 if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {117 return crypto.randomUUID();118 }119 const bytes = new Uint8Array(16);120 crypto.getRandomValues(bytes as Uint8Array<ArrayBuffer>);121 bytes[6] = (bytes[6] & 0x0f) | 0x40;122 bytes[8] = (bytes[8] & 0x3f) | 0x80;123 const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");124 return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;125}126127128129130131132133134135export async function getLocalPairingIdentity(): Promise<{136 identity: LocalIdentity;137 privateKey: Uint8Array;138 publicKey: Uint8Array;139}> {140 if (!isUnlocked()) throw new Error("vault_locked");141 const masterKeyHex = getRootKeyHex();142 if (!masterKeyHex) throw new Error("vault_locked");143 const masterKey = new Uint8Array(masterKeyHex.length / 2);144 for (let i = 0; i < masterKey.length; i++) {145 masterKey[i] = parseInt(masterKeyHex.slice(i * 2, i * 2 + 2), 16);146 }147 try {148 const base = await buildLocalIdentity(masterKey);149 const vaultId = await computeVaultId(masterKey);150 const keys = deriveSyncIdentityKeypair(masterKey, base.deviceId);151 return {152 identity: { ...base, vaultId },153 privateKey: keys.secretKey,154 publicKey: keys.publicKey,155 };156 } finally {157 masterKey.fill(0);158 }159}