123456789101112131415import { deflate } from "pako";1617import { isProUser } from "./license";18import * as vault from "./vault";19import { isNativePlatform, nativePlugin } from "../shared/native";20import type { VaultEntry } from "../shared/types";21import {22 type CodexRestorePayload,23 type CodexVaultSnapshot,24 CHUNK_SIZE,25 CODES_PER_PAGE,26 PARITY_SHARDS,27 STORAGE_KEY_LAST_RESTORED,28 STORAGE_KEY_VERIFY_CODE,29} from "../shared/codex-types";30313233const VERIFY_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; 3435363738394041function mintVerifyCode(): string {42 const random = new Uint8Array(8);43 crypto.getRandomValues(random);44 let group = "";45 for (let i = 0; i < 8; i++) {46 group += VERIFY_CODE_ALPHABET[random[i] % VERIFY_CODE_ALPHABET.length];47 }48 return `PX-${group.slice(0, 4)}-${group.slice(4, 8)}`;49}5051async function getOrCreateVerifyCode(): Promise<string> {52 const raw = await chrome.storage.local.get(STORAGE_KEY_VERIFY_CODE);53 const existing = (raw as Record<string, unknown>)[STORAGE_KEY_VERIFY_CODE];54 if (typeof existing === "string" && existing.length > 0) {55 return existing;56 }57 const fresh = mintVerifyCode();58 await chrome.storage.local.set({ [STORAGE_KEY_VERIFY_CODE]: fresh });59 return fresh;60}6162636465666768function base64urlToString(b64url: string): string {69 const pad = b64url.length % 4 === 0 ? "" : "=".repeat(4 - (b64url.length % 4));70 const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/") + pad;71 const binary = atob(b64);72 const bytes = new Uint8Array(binary.length);73 for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);74 return new TextDecoder().decode(bytes);75}767778798081async function publishInMemoryVault(entries: VaultEntry[]): Promise<void> {82 const plugin = nativePlugin<{83 setEntries?: (args: {84 entries: Array<{85 id: string;86 title: string;87 username: string;88 email: string;89 password: string;90 url: string;91 totp: string;92 additionalUrls: string[];93 }>;94 }) => Promise<void>;95 }>("InMemoryVaultPlugin");96 if (!plugin?.setEntries) return;97 await plugin.setEntries({98 entries: entries.map((e) => ({99 id: e.id,100 title: e.title ?? "",101 username: e.username ?? "",102 email: (e as VaultEntry & { email?: string }).email ?? "",103 password: e.password ?? "",104 url: e.url ?? "",105 totp: (e as VaultEntry & { totp?: string }).totp ?? "",106 additionalUrls:107 (e as VaultEntry & { additionalUrls?: string[] }).additionalUrls ?? [],108 })),109 });110}111112113114115116117118119export async function handleGetCodexVaultData(): Promise<{120 ok: boolean;121 error?: string;122 data?: CodexVaultSnapshot;123}> {124 if (!(await isProUser())) {125 return { ok: false, error: "pro_required" };126 }127 if (!vault.isUnlocked()) {128 return { ok: false, error: "VAULT_LOCKED: Unlock your vault first." };129 }130131 const raw = await chrome.storage.local.get([132 "peach_vault_a",133 "peach_vault_b",134 "peach_last_modified_at",135 ]);136 const slotARaw = (raw as Record<string, unknown>)["peach_vault_a"];137 const slotBRaw = (raw as Record<string, unknown>)["peach_vault_b"];138 const lastModRaw = (raw as Record<string, unknown>)["peach_last_modified_at"];139140 if (!slotARaw) {141 return { ok: false, error: "VAULT_EMPTY: No vault to print." };142 }143144 145 146 const slotA = JSON.stringify(slotARaw);147 const slotB = slotBRaw ? JSON.stringify(slotBRaw) : null;148 const lastModified = typeof lastModRaw === "string" ? lastModRaw : null;149150 151 152 153 let recoveryWords: string[] | null = null;154 try {155 recoveryWords = await vault.getRecoveryWords();156 } catch {157 recoveryWords = null;158 }159160 const verifyCode = await getOrCreateVerifyCode();161 const entryCount = vault.getEntries().length;162163 const snapshot: CodexVaultSnapshot = {164 slotA,165 slotB,166 lastModified,167 entryCount,168 recoveryWords,169 verifyCode,170 vaultUnlocked: true,171 };172 return { ok: true, data: snapshot };173}174175176177178179180181182183export async function handleEstimateCodexPages(): Promise<{184 ok: boolean;185 error?: string;186 pages?: number;187}> {188 if (!(await isProUser())) {189 return { ok: false, error: "pro_required" };190 }191 try {192 const raw = await chrome.storage.local.get(["peach_vault_a", "peach_vault_b"]);193 const slotA = (raw as Record<string, unknown>)["peach_vault_a"];194 const slotB = (raw as Record<string, unknown>)["peach_vault_b"];195 if (!slotA) return { ok: true, pages: 3 };196197 198 199 200 201 const blobJson = JSON.stringify({ a: slotA, b: slotB ?? null });202 const compressed = deflate(new TextEncoder().encode(blobJson));203 const dataChunks = Math.ceil(compressed.length / CHUNK_SIZE);204 const totalChunks = dataChunks + PARITY_SHARDS;205 const pages = Math.max(1, Math.ceil(totalChunks / CODES_PER_PAGE));206 return { ok: true, pages };207 } catch {208 return { ok: true, pages: 3 };209 }210}211212213214215216217218219export async function handleRestoreFromCodex(220 payload: CodexRestorePayload,221): Promise<{222 ok: boolean;223 error?: string;224 data?: { entryCount: number; verifyCode: string };225}> {226 227 228 let slotAJson: string;229 let slotBJson: string | null;230 try {231 const decoded = base64urlToString(payload.vaultBlob);232 const parsed = JSON.parse(decoded) as { a: unknown; b: unknown };233 if (!parsed || typeof parsed !== "object" || parsed.a == null) {234 return { ok: false, error: "INVALID_VAULT_BLOB" };235 }236 slotAJson = JSON.stringify(parsed.a);237 slotBJson = parsed.b == null ? null : JSON.stringify(parsed.b);238 } catch {239 return { ok: false, error: "INVALID_VAULT_BLOB" };240 }241242 const result = await vault.unlockFromCodex({243 slotA: slotAJson,244 slotB: slotBJson,245 timestamp: payload.timestamp,246 masterPassphrase: payload.masterPassphrase,247 });248249 if (!result.ok) {250 if (result.reason === "wrong_passphrase") {251 return { ok: false, error: "wrong_passphrase" };252 }253 return { ok: false, error: "INVALID_VAULT_BLOB" };254 }255256 257 258 259 await chrome.storage.local.set({260 [STORAGE_KEY_VERIFY_CODE]: payload.verifyCode,261 [STORAGE_KEY_LAST_RESTORED]: Date.now(),262 });263264 265 266 if (isNativePlatform()) {267 try {268 await publishInMemoryVault(vault.getEntries());269 } catch {270 271 }272 }273274 return {275 ok: true,276 data: {277 entryCount: result.entryCount,278 verifyCode: payload.verifyCode,279 },280 };281}