1234567891011121314import {15 deriveSyncIdentityKeypair,16 deriveSyncSigningKeypair,17 pubkeyToHex,18} from "../shared/sync-identity";19import type { DeviceType } from "../shared/sync-types";2021const DEVICE_ID_KEY = "peach_chirp_device_id";22const DEVICE_NAME_KEY = "peach_chirp_device_name";2324export interface LocalChirpIdentity {25 deviceId: string;26 deviceName: string;27 deviceType: DeviceType;28 longTermPublicKeyHex: string;29 signingPublicKeyHex: string;30}3132export async function getOrCreateDeviceId(): Promise<string> {33 const result = await chrome.storage.sync.get(DEVICE_ID_KEY);34 const current = (result as Record<string, unknown>)[DEVICE_ID_KEY];35 if (typeof current === "string" && current.length > 0) return current;36 const fresh = generateUuidV4();37 await chrome.storage.sync.set({ [DEVICE_ID_KEY]: fresh });38 return fresh;39}4041export async function getDeviceName(): Promise<string> {42 const result = await chrome.storage.sync.get(DEVICE_NAME_KEY);43 const stored = (result as Record<string, unknown>)[DEVICE_NAME_KEY];44 if (typeof stored === "string" && stored.length > 0) return stored;45 return defaultDeviceName();46}4748export async function setDeviceName(name: string): Promise<void> {49 const trimmed = name.trim();50 if (!trimmed) throw new Error("device name must not be empty");51 if (trimmed.length > 64) throw new Error("device name must be 64 chars or fewer");52 await chrome.storage.sync.set({ [DEVICE_NAME_KEY]: trimmed });53}54555657585960export async function buildLocalIdentity(61 masterKey: Uint8Array,62): Promise<LocalChirpIdentity> {63 const deviceId = await getOrCreateDeviceId();64 const deviceName = await getDeviceName();65 const kp = deriveSyncIdentityKeypair(masterKey, deviceId);66 const signing = await deriveSyncSigningKeypair(masterKey, deviceId);67 return {68 deviceId,69 deviceName,70 deviceType: extensionDeviceType(),71 longTermPublicKeyHex: pubkeyToHex(kp.publicKey),72 signingPublicKeyHex: pubkeyToHex(signing.publicKeyRaw),73 };74}75767778798081export function deriveLocalSyncKeypair(masterKey: Uint8Array, deviceId: string) {82 return deriveSyncIdentityKeypair(masterKey, deviceId);83}8485function defaultDeviceName(): string {86 87 88 const ua = (typeof navigator !== "undefined" ? navigator.userAgent : "") || "";89 if (/Firefox/i.test(ua)) return "Firefox";90 if (/Edg\//i.test(ua)) return "Edge";91 if (/Chrome/i.test(ua)) return "Chrome";92 if (/Safari/i.test(ua)) return "Safari";93 return "Browser";94}9596function extensionDeviceType(): DeviceType {97 98 99 100 try {101 const native = (globalThis as unknown as { Capacitor?: { isNativePlatform?: () => boolean } }).Capacitor;102 if (native?.isNativePlatform?.()) {103 const platform = (globalThis as unknown as { Capacitor?: { getPlatform?: () => string } }).Capacitor?.getPlatform?.();104 if (platform === "ios") return "ios";105 return "android";106 }107 } catch { }108 return "extension";109}110111function generateUuidV4(): string {112 113 114 115 if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {116 return crypto.randomUUID();117 }118 const bytes = new Uint8Array(16);119 crypto.getRandomValues(bytes as Uint8Array<ArrayBuffer>);120 bytes[6] = (bytes[6] & 0x0f) | 0x40;121 bytes[8] = (bytes[8] & 0x3f) | 0x80;122 const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");123 return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;124}