123456789101112131415import { deflate } from "pako";1617import { isProUser } from "./license";18import * as vault from "./vault";19import {20 type CodexRestorePayload,21 type CodexVaultSnapshot,22 CHUNK_SIZE,23 CODES_PER_PAGE,24 computeParityShards,25 STORAGE_KEY_LAST_RESTORED,26 STORAGE_KEY_VERIFY_CODE,27} from "../shared/codex-types";28import { IMPORT_LIMITS } from "../shared/import-limits";29303132const VERIFY_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; 33343536const VERIFY_CODE_PATTERN = /^PX-[A-Z0-9]{4}-[A-Z0-9]{4}$/;3738394041424344function mintVerifyCode(): string {45 const random = new Uint8Array(8);46 crypto.getRandomValues(random);47 let group = "";48 for (let i = 0; i < 8; i++) {49 group += VERIFY_CODE_ALPHABET[random[i] % VERIFY_CODE_ALPHABET.length];50 }51 return `PX-${group.slice(0, 4)}-${group.slice(4, 8)}`;52}5354async function getOrCreateVerifyCode(): Promise<string> {55 const raw = await chrome.storage.local.get(STORAGE_KEY_VERIFY_CODE);56 const existing = (raw as Record<string, unknown>)[STORAGE_KEY_VERIFY_CODE];57 if (typeof existing === "string" && existing.length > 0) {58 return existing;59 }60 const fresh = mintVerifyCode();61 await chrome.storage.local.set({ [STORAGE_KEY_VERIFY_CODE]: fresh });62 return fresh;63}6465666768697071function base64urlToString(b64url: string): string {72 const pad = b64url.length % 4 === 0 ? "" : "=".repeat(4 - (b64url.length % 4));73 const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/") + pad;74 const binary = atob(b64);75 const bytes = new Uint8Array(binary.length);76 for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);77 return new TextDecoder().decode(bytes);78}7980818283848586878889909192export async function handleGetCodexVaultData(password?: string): Promise<{93 ok: boolean;94 error?: string;95 data?: CodexVaultSnapshot;96}> {97 if (!(await isProUser())) {98 return { ok: false, error: "pro_required" };99 }100 if (!vault.isUnlocked()) {101 return { ok: false, error: "VAULT_LOCKED: Unlock your vault first." };102 }103104 let passwordProof = false;105 if (typeof password === "string" && password) {106 if (!(await vault.verifyMainPassword(password))) {107 return { ok: false, error: "wrong_password" };108 }109 passwordProof = true;110 }111112 const raw = await chrome.storage.local.get([113 "peach_vault_a",114 "peach_vault_b",115 "peach_last_modified_at",116 ]);117 const slotARaw = (raw as Record<string, unknown>)["peach_vault_a"];118 const slotBRaw = (raw as Record<string, unknown>)["peach_vault_b"];119 const lastModRaw = (raw as Record<string, unknown>)["peach_last_modified_at"];120121 if (!slotARaw) {122 return { ok: false, error: "VAULT_EMPTY: No vault to print." };123 }124125 126 127 const slotA = JSON.stringify(slotARaw);128 const slotB = slotBRaw ? JSON.stringify(slotBRaw) : null;129 const lastModified = typeof lastModRaw === "string" ? lastModRaw : null;130131 132 133 let recoveryWords: string[] | null = null;134 if (passwordProof) {135 try {136 recoveryWords = await vault.getRecoveryWords();137 } catch {138 recoveryWords = null;139 }140 }141142 const verifyCode = await getOrCreateVerifyCode();143 const entryCount = vault.getEntries().length;144145 const snapshot: CodexVaultSnapshot = {146 slotA,147 slotB,148 lastModified,149 entryCount,150 recoveryWords,151 verifyCode,152 vaultUnlocked: true,153 };154 return { ok: true, data: snapshot };155}156157158159160161162163164165export async function handleEstimateCodexPages(): Promise<{166 ok: boolean;167 error?: string;168 pages?: number;169 parityShards?: number;170}> {171 if (!(await isProUser())) {172 return { ok: false, error: "pro_required" };173 }174 try {175 const raw = await chrome.storage.local.get(["peach_vault_a", "peach_vault_b"]);176 const slotA = (raw as Record<string, unknown>)["peach_vault_a"];177 const slotB = (raw as Record<string, unknown>)["peach_vault_b"];178 if (!slotA) return { ok: true, pages: 3, parityShards: 2 };179180 181 182 183 184 const blobJson = JSON.stringify({ a: slotA, b: slotB ?? null });185 const compressed = deflate(new TextEncoder().encode(blobJson));186 const dataChunks = Math.max(1, Math.ceil(compressed.length / CHUNK_SIZE));187 const parityShards = computeParityShards(dataChunks);188 const totalChunks = dataChunks + parityShards;189 const pages = Math.max(1, Math.ceil(totalChunks / CODES_PER_PAGE));190 return { ok: true, pages, parityShards };191 } catch {192 return { ok: true, pages: 3, parityShards: 2 };193 }194}195196197198199200201202203export async function handleRestoreFromCodex(204 payload: CodexRestorePayload,205): Promise<{206 ok: boolean;207 error?: string;208 data?: { entryCount: number; verifyCode: string };209}> {210 211 212 213 if (!VERIFY_CODE_PATTERN.test(payload.verifyCode)) {214 return { ok: false, error: "INVALID_VERIFY_CODE" };215 }216 if (!Number.isFinite(payload.timestamp)) {217 return { ok: false, error: "INVALID_VAULT_BLOB" };218 }219 if (payload.vaultBlob.length > IMPORT_LIMITS.MAX_JSON_BYTES) {220 return { ok: false, error: "INVALID_VAULT_BLOB" };221 }222223 224 225 let slotAJson: string;226 let slotBJson: string | null;227 try {228 const decoded = base64urlToString(payload.vaultBlob);229 const parsed = JSON.parse(decoded) as { a: unknown; b: unknown };230 if (!parsed || typeof parsed !== "object" || parsed.a == null) {231 return { ok: false, error: "INVALID_VAULT_BLOB" };232 }233 234 235 236 237 slotAJson = typeof parsed.a === "string" ? parsed.a : JSON.stringify(parsed.a);238 slotBJson =239 parsed.b == null240 ? null241 : typeof parsed.b === "string"242 ? parsed.b243 : JSON.stringify(parsed.b);244 } catch {245 return { ok: false, error: "INVALID_VAULT_BLOB" };246 }247248 const result = await vault.unlockFromCodex({249 slotA: slotAJson,250 slotB: slotBJson,251 timestamp: payload.timestamp,252 masterPassphrase: payload.masterPassphrase,253 });254255 if (!result.ok) {256 if (result.reason === "wrong_passphrase") {257 return { ok: false, error: "wrong_passphrase" };258 }259 return { ok: false, error: "INVALID_VAULT_BLOB" };260 }261262 263 264 265 await chrome.storage.local.set({266 [STORAGE_KEY_VERIFY_CODE]: payload.verifyCode,267 [STORAGE_KEY_LAST_RESTORED]: Date.now(),268 });269270 return {271 ok: true,272 data: {273 entryCount: result.entryCount,274 verifyCode: payload.verifyCode,275 },276 };277}