android-sync-identity.ts

2.4 KB · raw

1/**2 * Android sync identity — persistent X25519 keypair for the Capacitor side.3 *4 * The Kotlin SyncKey plugin stores the keypair in encrypted SharedPreferences5 * keyed by hardware-bound master key. JS handles the X25519 math; the plugin6 * is a simple persisted blob store. On first invocation we generate, persist,7 * and return; subsequent calls return the cached pair.8 *9 * Non-Android contexts (extension, dev WebView) throw — this identity is10 * Android-only. Desktop sync uses the native messaging host's identity11 * (`SyncIdentity` in the Rust host) which has its own key file.12 */1314import { Capacitor, registerPlugin } from "@capacitor/core";15import { generateKeyPair } from "@stablelib/x25519";1617interface SyncKeyPlugin {18  /**19   * Returns the stored keypair if one exists, else { privateKeyHex: '',20   * publicKeyHex: '' } so the JS side knows to generate + persist.21   */22  loadKeypair(): Promise<{ privateKeyHex: string; publicKeyHex: string }>;23  /** Persists a fresh keypair generated on the JS side. */24  saveKeypair(keys: { privateKeyHex: string; publicKeyHex: string }): Promise<void>;25}2627const SyncKey = registerPlugin<SyncKeyPlugin>("SyncKey");2829let cached: { privateKey: Uint8Array; publicKey: Uint8Array } | null = null;3031function bytesToHex(bytes: Uint8Array): string {32  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");33}3435function hexToBytes(hex: string): Uint8Array {36  const out = new Uint8Array(hex.length / 2);37  for (let i = 0; i < hex.length; i += 2) {38    out[i / 2] = parseInt(hex.slice(i, i + 2), 16);39  }40  return out;41}4243export async function getAndroidSyncIdentity(): Promise<{44  privateKey: Uint8Array;45  publicKey: Uint8Array;46}> {47  if (cached) return cached;48  if (!Capacitor.isNativePlatform()) {49    throw new Error("Android sync identity requested on non-native platform");50  }5152  const stored = await SyncKey.loadKeypair();53  if (stored.privateKeyHex && stored.publicKeyHex) {54    cached = {55      privateKey: hexToBytes(stored.privateKeyHex),56      publicKey: hexToBytes(stored.publicKeyHex),57    };58    return cached;59  }6061  const pair = generateKeyPair();62  const privateKeyHex = bytesToHex(pair.secretKey);63  const publicKeyHex = bytesToHex(pair.publicKey);64  await SyncKey.saveKeypair({ privateKeyHex, publicKeyHex });65  cached = { privateKey: pair.secretKey, publicKey: pair.publicKey };66  return cached;67}6869/** Test-only — wipe the in-memory cache. */70export function __resetForTests(): void {71  cached = null;72}