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 computeParityShards,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 parityShards?: number;188}> {189 if (!(await isProUser())) {190 return { ok: false, error: "pro_required" };191 }192 try {193 const raw = await chrome.storage.local.get(["peach_vault_a", "peach_vault_b"]);194 const slotA = (raw as Record<string, unknown>)["peach_vault_a"];195 const slotB = (raw as Record<string, unknown>)["peach_vault_b"];196 if (!slotA) return { ok: true, pages: 3, parityShards: 2 };197198 199 200 201 202 const blobJson = JSON.stringify({ a: slotA, b: slotB ?? null });203 const compressed = deflate(new TextEncoder().encode(blobJson));204 const dataChunks = Math.max(1, Math.ceil(compressed.length / CHUNK_SIZE));205 const parityShards = computeParityShards(dataChunks);206 const totalChunks = dataChunks + parityShards;207 const pages = Math.max(1, Math.ceil(totalChunks / CODES_PER_PAGE));208 return { ok: true, pages, parityShards };209 } catch {210 return { ok: true, pages: 3, parityShards: 2 };211 }212}213214215216217218219220221export async function handleRestoreFromCodex(222 payload: CodexRestorePayload,223): Promise<{224 ok: boolean;225 error?: string;226 data?: { entryCount: number; verifyCode: string };227}> {228 229 230 let slotAJson: string;231 let slotBJson: string | null;232 try {233 const decoded = base64urlToString(payload.vaultBlob);234 const parsed = JSON.parse(decoded) as { a: unknown; b: unknown };235 if (!parsed || typeof parsed !== "object" || parsed.a == null) {236 return { ok: false, error: "INVALID_VAULT_BLOB" };237 }238 slotAJson = JSON.stringify(parsed.a);239 slotBJson = parsed.b == null ? null : JSON.stringify(parsed.b);240 } catch {241 return { ok: false, error: "INVALID_VAULT_BLOB" };242 }243244 const result = await vault.unlockFromCodex({245 slotA: slotAJson,246 slotB: slotBJson,247 timestamp: payload.timestamp,248 masterPassphrase: payload.masterPassphrase,249 });250251 if (!result.ok) {252 if (result.reason === "wrong_passphrase") {253 return { ok: false, error: "wrong_passphrase" };254 }255 return { ok: false, error: "INVALID_VAULT_BLOB" };256 }257258 259 260 261 await chrome.storage.local.set({262 [STORAGE_KEY_VERIFY_CODE]: payload.verifyCode,263 [STORAGE_KEY_LAST_RESTORED]: Date.now(),264 });265266 267 268 if (isNativePlatform()) {269 try {270 await publishInMemoryVault(vault.getEntries());271 } catch {272 273 }274 }275276 return {277 ok: true,278 data: {279 entryCount: result.entryCount,280 verifyCode: payload.verifyCode,281 },282 };283}