/** * Android sync identity — persistent X25519 keypair for the Capacitor side. * * The Kotlin SyncKey plugin stores the keypair in encrypted SharedPreferences * keyed by hardware-bound master key. JS handles the X25519 math; the plugin * is a simple persisted blob store. On first invocation we generate, persist, * and return; subsequent calls return the cached pair. * * Non-Android contexts (extension, dev WebView) throw — this identity is * Android-only. Desktop sync uses the native messaging host's identity * (`SyncIdentity` in the Rust host) which has its own key file. */ import { Capacitor, registerPlugin } from "@capacitor/core"; import { generateKeyPair } from "@stablelib/x25519"; interface SyncKeyPlugin { /** * Returns the stored keypair if one exists, else { privateKeyHex: '', * publicKeyHex: '' } so the JS side knows to generate + persist. */ loadKeypair(): Promise<{ privateKeyHex: string; publicKeyHex: string }>; /** Persists a fresh keypair generated on the JS side. */ saveKeypair(keys: { privateKeyHex: string; publicKeyHex: string }): Promise; /** Wipes the persisted keypair. The JS cache must be invalidated separately. */ clearKeypair(): Promise; } const SyncKey = registerPlugin("SyncKey"); let cached: { privateKey: Uint8Array; publicKey: Uint8Array } | null = null; function bytesToHex(bytes: Uint8Array): string { return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); } function hexToBytes(hex: string): Uint8Array { const out = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { out[i / 2] = parseInt(hex.slice(i, i + 2), 16); } return out; } export async function getAndroidSyncIdentity(): Promise<{ privateKey: Uint8Array; publicKey: Uint8Array; }> { if (cached) return cached; if (!Capacitor.isNativePlatform()) { throw new Error("Android sync identity requested on non-native platform"); } const stored = await SyncKey.loadKeypair(); if (stored.privateKeyHex && stored.publicKeyHex) { cached = { privateKey: hexToBytes(stored.privateKeyHex), publicKey: hexToBytes(stored.publicKeyHex), }; return cached; } const pair = generateKeyPair(); const privateKeyHex = bytesToHex(pair.secretKey); const publicKeyHex = bytesToHex(pair.publicKey); await SyncKey.saveKeypair({ privateKeyHex, publicKeyHex }); cached = { privateKey: pair.secretKey, publicKey: pair.publicKey }; return cached; } /** * Wipe the persisted keypair and the in-memory cache. The next call to * getAndroidSyncIdentity() will generate and persist a fresh pair. * * No-op on non-native platforms — desktop doesn't use this identity. */ export async function clearAndroidSyncIdentity(): Promise { cached = null; if (!Capacitor.isNativePlatform()) return; try { await SyncKey.clearKeypair(); } catch { // Plugin may be missing on older builds — best-effort. } } /** Test-only — wipe the in-memory cache. */ export function __resetForTests(): void { cached = null; }