1234567891011121314151617import {18 computeVaultId,19 deriveSyncIdentityKeypair,20 deriveSyncSigningKeypair,21 pubkeyToHex,22} from "../shared/sync-identity";23import type { LocalIdentity } from "../shared/pairing-handshake";24import type { DeviceType } from "../shared/sync-types";25import { getRootKeyHex, isUnlocked } from "./vault";2627const DEVICE_ID_KEY = "peach_device_id";28const DEVICE_NAME_KEY = "peach_device_name";2930export interface LocalDeviceIdentity {31 deviceId: string;32 deviceName: string;33 deviceType: DeviceType;34 longTermPublicKeyHex: string;35 signingPublicKeyHex: string;36}3738export async function getOrCreateDeviceId(): Promise<string> {39 40 41 42 43 const local = await chrome.storage.local.get(DEVICE_ID_KEY);44 const localId = (local as Record<string, unknown>)[DEVICE_ID_KEY];45 if (typeof localId === "string" && localId.length > 0) return localId;4647 48 49 50 const synced = await chrome.storage.sync.get(DEVICE_ID_KEY);51 const syncedId = (synced as Record<string, unknown>)[DEVICE_ID_KEY];52 if (typeof syncedId === "string" && syncedId.length > 0) {53 await chrome.storage.local.set({ [DEVICE_ID_KEY]: syncedId });54 await chrome.storage.sync.remove(DEVICE_ID_KEY);55 return syncedId;56 }5758 const fresh = generateUuidV4();59 await chrome.storage.local.set({ [DEVICE_ID_KEY]: fresh });60 return fresh;61}6263export async function getDeviceName(): Promise<string> {64 const result = await chrome.storage.sync.get(DEVICE_NAME_KEY);65 const stored = (result as Record<string, unknown>)[DEVICE_NAME_KEY];66 if (typeof stored === "string" && stored.length > 0) return stored;67 return defaultDeviceName();68}6970export async function setDeviceName(name: string): Promise<void> {71 const trimmed = name.trim();72 if (!trimmed) throw new Error("device name must not be empty");73 if (trimmed.length > 64) throw new Error("device name must be 64 chars or fewer");74 await chrome.storage.sync.set({ [DEVICE_NAME_KEY]: trimmed });75}76777879808182export async function buildLocalIdentity(83 masterKey: Uint8Array,84): Promise<LocalDeviceIdentity> {85 const deviceId = await getOrCreateDeviceId();86 const deviceName = await getDeviceName();87 const kp = deriveSyncIdentityKeypair(masterKey, deviceId);88 const signing = await deriveSyncSigningKeypair(masterKey, deviceId);89 return {90 deviceId,91 deviceName,92 deviceType: extensionDeviceType(),93 longTermPublicKeyHex: pubkeyToHex(kp.publicKey),94 signingPublicKeyHex: pubkeyToHex(signing.publicKeyRaw),95 };96}979899100101102103export function deriveLocalSyncKeypair(masterKey: Uint8Array, deviceId: string) {104 return deriveSyncIdentityKeypair(masterKey, deviceId);105}106107function defaultDeviceName(): string {108 109 110 const ua = (typeof navigator !== "undefined" ? navigator.userAgent : "") || "";111 if (/Firefox/i.test(ua)) return "Firefox";112 if (/Edg\//i.test(ua)) return "Edge";113 if (/Chrome/i.test(ua)) return "Chrome";114 if (/Safari/i.test(ua)) return "Safari";115 return "Browser";116}117118function extensionDeviceType(): DeviceType {119 120 121 122 try {123 const native = (globalThis as unknown as { Capacitor?: { isNativePlatform?: () => boolean } }).Capacitor;124 if (native?.isNativePlatform?.()) {125 const platform = (globalThis as unknown as { Capacitor?: { getPlatform?: () => string } }).Capacitor?.getPlatform?.();126 if (platform === "ios") return "ios";127 return "android";128 }129 } catch { }130 return "extension";131}132133function generateUuidV4(): string {134 135 136 137 if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {138 return crypto.randomUUID();139 }140 const bytes = new Uint8Array(16);141 crypto.getRandomValues(bytes as Uint8Array<ArrayBuffer>);142 bytes[6] = (bytes[6] & 0x0f) | 0x40;143 bytes[8] = (bytes[8] & 0x3f) | 0x80;144 const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");145 return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;146}147148149150151152153154155156export async function getLocalPairingIdentity(): Promise<{157 identity: LocalIdentity;158 privateKey: Uint8Array;159 publicKey: Uint8Array;160}> {161 if (!isUnlocked()) throw new Error("vault_locked");162 const masterKeyHex = getRootKeyHex();163 if (!masterKeyHex) throw new Error("vault_locked");164 const masterKey = new Uint8Array(masterKeyHex.length / 2);165 for (let i = 0; i < masterKey.length; i++) {166 masterKey[i] = parseInt(masterKeyHex.slice(i * 2, i * 2 + 2), 16);167 }168 try {169 const base = await buildLocalIdentity(masterKey);170 const vaultId = await computeVaultId(masterKey);171 const keys = deriveSyncIdentityKeypair(masterKey, base.deviceId);172 return {173 identity: { ...base, vaultId },174 privateKey: keys.secretKey,175 publicKey: keys.publicKey,176 };177 } finally {178 masterKey.fill(0);179 }180}