1234567891011121314151617import {18 deriveSyncIdentityKeypair,19 deriveSyncSigningKeypair,20 pubkeyToHex,21} from "../shared/sync-identity";22import type { DeviceType } from "../shared/sync-types";2324const DEVICE_ID_KEY = "peach_device_id";25const DEVICE_NAME_KEY = "peach_device_name";2627export interface LocalDeviceIdentity {28 deviceId: string;29 deviceName: string;30 deviceType: DeviceType;31 longTermPublicKeyHex: string;32 signingPublicKeyHex: string;33}3435export async function getOrCreateDeviceId(): Promise<string> {36 37 38 39 40 const local = await chrome.storage.local.get(DEVICE_ID_KEY);41 const localId = (local as Record<string, unknown>)[DEVICE_ID_KEY];42 if (typeof localId === "string" && localId.length > 0) return localId;4344 45 46 47 const synced = await chrome.storage.sync.get(DEVICE_ID_KEY);48 const syncedId = (synced as Record<string, unknown>)[DEVICE_ID_KEY];49 if (typeof syncedId === "string" && syncedId.length > 0) {50 await chrome.storage.local.set({ [DEVICE_ID_KEY]: syncedId });51 await chrome.storage.sync.remove(DEVICE_ID_KEY);52 return syncedId;53 }5455 const fresh = generateUuidV4();56 await chrome.storage.local.set({ [DEVICE_ID_KEY]: fresh });57 return fresh;58}5960export async function getDeviceName(): Promise<string> {61 const result = await chrome.storage.sync.get(DEVICE_NAME_KEY);62 const stored = (result as Record<string, unknown>)[DEVICE_NAME_KEY];63 if (typeof stored === "string" && stored.length > 0) return stored;64 return defaultDeviceName();65}6667export async function setDeviceName(name: string): Promise<void> {68 const trimmed = name.trim();69 if (!trimmed) throw new Error("device name must not be empty");70 if (trimmed.length > 64) throw new Error("device name must be 64 chars or fewer");71 await chrome.storage.sync.set({ [DEVICE_NAME_KEY]: trimmed });72}73747576777879export async function buildLocalIdentity(80 masterKey: Uint8Array,81): Promise<LocalDeviceIdentity> {82 const deviceId = await getOrCreateDeviceId();83 const deviceName = await getDeviceName();84 const kp = deriveSyncIdentityKeypair(masterKey, deviceId);85 const signing = await deriveSyncSigningKeypair(masterKey, deviceId);86 return {87 deviceId,88 deviceName,89 deviceType: extensionDeviceType(),90 longTermPublicKeyHex: pubkeyToHex(kp.publicKey),91 signingPublicKeyHex: pubkeyToHex(signing.publicKeyRaw),92 };93}949596979899100export function deriveLocalSyncKeypair(masterKey: Uint8Array, deviceId: string) {101 return deriveSyncIdentityKeypair(masterKey, deviceId);102}103104function defaultDeviceName(): string {105 106 107 const ua = (typeof navigator !== "undefined" ? navigator.userAgent : "") || "";108 if (/Firefox/i.test(ua)) return "Firefox";109 if (/Edg\//i.test(ua)) return "Edge";110 if (/Chrome/i.test(ua)) return "Chrome";111 if (/Safari/i.test(ua)) return "Safari";112 return "Browser";113}114115function extensionDeviceType(): DeviceType {116 117 118 119 try {120 const native = (globalThis as unknown as { Capacitor?: { isNativePlatform?: () => boolean } }).Capacitor;121 if (native?.isNativePlatform?.()) {122 const platform = (globalThis as unknown as { Capacitor?: { getPlatform?: () => string } }).Capacitor?.getPlatform?.();123 if (platform === "ios") return "ios";124 return "android";125 }126 } catch { }127 return "extension";128}129130function generateUuidV4(): string {131 132 133 134 if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {135 return crypto.randomUUID();136 }137 const bytes = new Uint8Array(16);138 crypto.getRandomValues(bytes as Uint8Array<ArrayBuffer>);139 bytes[6] = (bytes[6] & 0x0f) | 0x40;140 bytes[8] = (bytes[8] & 0x3f) | 0x80;141 const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");142 return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;143}