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";30import { IMPORT_LIMITS } from "../shared/import-limits";31323334const VERIFY_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; 35363738const VERIFY_CODE_PATTERN = /^PX-[A-Z0-9]{4}-[A-Z0-9]{4}$/;3940414243444546function mintVerifyCode(): string {47 const random = new Uint8Array(8);48 crypto.getRandomValues(random);49 let group = "";50 for (let i = 0; i < 8; i++) {51 group += VERIFY_CODE_ALPHABET[random[i] % VERIFY_CODE_ALPHABET.length];52 }53 return `PX-${group.slice(0, 4)}-${group.slice(4, 8)}`;54}5556async function getOrCreateVerifyCode(): Promise<string> {57 const raw = await chrome.storage.local.get(STORAGE_KEY_VERIFY_CODE);58 const existing = (raw as Record<string, unknown>)[STORAGE_KEY_VERIFY_CODE];59 if (typeof existing === "string" && existing.length > 0) {60 return existing;61 }62 const fresh = mintVerifyCode();63 await chrome.storage.local.set({ [STORAGE_KEY_VERIFY_CODE]: fresh });64 return fresh;65}6667686970717273function base64urlToString(b64url: string): string {74 const pad = b64url.length % 4 === 0 ? "" : "=".repeat(4 - (b64url.length % 4));75 const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/") + pad;76 const binary = atob(b64);77 const bytes = new Uint8Array(binary.length);78 for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);79 return new TextDecoder().decode(bytes);80}818283848586async function publishInMemoryVault(entries: VaultEntry[]): Promise<void> {87 const plugin = nativePlugin<{88 setEntries?: (args: {89 entries: Array<{90 id: string;91 title: string;92 username: string;93 email: string;94 password: string;95 url: string;96 totp: string;97 additionalUrls: string[];98 }>;99 }) => Promise<void>;100 }>("InMemoryVaultPlugin");101 if (!plugin?.setEntries) return;102 await plugin.setEntries({103 entries: entries.map((e) => ({104 id: e.id,105 title: e.title ?? "",106 username: e.username ?? "",107 email: (e as VaultEntry & { email?: string }).email ?? "",108 password: e.password ?? "",109 url: e.url ?? "",110 totp: (e as VaultEntry & { totp?: string }).totp ?? "",111 additionalUrls:112 (e as VaultEntry & { additionalUrls?: string[] }).additionalUrls ?? [],113 })),114 });115}116117118119120121122123124export async function handleGetCodexVaultData(): Promise<{125 ok: boolean;126 error?: string;127 data?: CodexVaultSnapshot;128}> {129 if (!(await isProUser())) {130 return { ok: false, error: "pro_required" };131 }132 if (!vault.isUnlocked()) {133 return { ok: false, error: "VAULT_LOCKED: Unlock your vault first." };134 }135136 const raw = await chrome.storage.local.get([137 "peach_vault_a",138 "peach_vault_b",139 "peach_last_modified_at",140 ]);141 const slotARaw = (raw as Record<string, unknown>)["peach_vault_a"];142 const slotBRaw = (raw as Record<string, unknown>)["peach_vault_b"];143 const lastModRaw = (raw as Record<string, unknown>)["peach_last_modified_at"];144145 if (!slotARaw) {146 return { ok: false, error: "VAULT_EMPTY: No vault to print." };147 }148149 150 151 const slotA = JSON.stringify(slotARaw);152 const slotB = slotBRaw ? JSON.stringify(slotBRaw) : null;153 const lastModified = typeof lastModRaw === "string" ? lastModRaw : null;154155 156 157 158 let recoveryWords: string[] | null = null;159 try {160 recoveryWords = await vault.getRecoveryWords();161 } catch {162 recoveryWords = null;163 }164165 const verifyCode = await getOrCreateVerifyCode();166 const entryCount = vault.getEntries().length;167168 const snapshot: CodexVaultSnapshot = {169 slotA,170 slotB,171 lastModified,172 entryCount,173 recoveryWords,174 verifyCode,175 vaultUnlocked: true,176 };177 return { ok: true, data: snapshot };178}179180181182183184185186187188export async function handleEstimateCodexPages(): Promise<{189 ok: boolean;190 error?: string;191 pages?: number;192 parityShards?: number;193}> {194 if (!(await isProUser())) {195 return { ok: false, error: "pro_required" };196 }197 try {198 const raw = await chrome.storage.local.get(["peach_vault_a", "peach_vault_b"]);199 const slotA = (raw as Record<string, unknown>)["peach_vault_a"];200 const slotB = (raw as Record<string, unknown>)["peach_vault_b"];201 if (!slotA) return { ok: true, pages: 3, parityShards: 2 };202203 204 205 206 207 const blobJson = JSON.stringify({ a: slotA, b: slotB ?? null });208 const compressed = deflate(new TextEncoder().encode(blobJson));209 const dataChunks = Math.max(1, Math.ceil(compressed.length / CHUNK_SIZE));210 const parityShards = computeParityShards(dataChunks);211 const totalChunks = dataChunks + parityShards;212 const pages = Math.max(1, Math.ceil(totalChunks / CODES_PER_PAGE));213 return { ok: true, pages, parityShards };214 } catch {215 return { ok: true, pages: 3, parityShards: 2 };216 }217}218219220221222223224225226export async function handleRestoreFromCodex(227 payload: CodexRestorePayload,228): Promise<{229 ok: boolean;230 error?: string;231 data?: { entryCount: number; verifyCode: string };232}> {233 234 235 236 if (!VERIFY_CODE_PATTERN.test(payload.verifyCode)) {237 return { ok: false, error: "INVALID_VERIFY_CODE" };238 }239 if (!Number.isFinite(payload.timestamp)) {240 return { ok: false, error: "INVALID_VAULT_BLOB" };241 }242 if (payload.vaultBlob.length > IMPORT_LIMITS.MAX_JSON_BYTES) {243 return { ok: false, error: "INVALID_VAULT_BLOB" };244 }245246 247 248 let slotAJson: string;249 let slotBJson: string | null;250 try {251 const decoded = base64urlToString(payload.vaultBlob);252 const parsed = JSON.parse(decoded) as { a: unknown; b: unknown };253 if (!parsed || typeof parsed !== "object" || parsed.a == null) {254 return { ok: false, error: "INVALID_VAULT_BLOB" };255 }256 257 258 259 260 slotAJson = typeof parsed.a === "string" ? parsed.a : JSON.stringify(parsed.a);261 slotBJson =262 parsed.b == null263 ? null264 : typeof parsed.b === "string"265 ? parsed.b266 : JSON.stringify(parsed.b);267 } catch {268 return { ok: false, error: "INVALID_VAULT_BLOB" };269 }270271 const result = await vault.unlockFromCodex({272 slotA: slotAJson,273 slotB: slotBJson,274 timestamp: payload.timestamp,275 masterPassphrase: payload.masterPassphrase,276 });277278 if (!result.ok) {279 if (result.reason === "wrong_passphrase") {280 return { ok: false, error: "wrong_passphrase" };281 }282 return { ok: false, error: "INVALID_VAULT_BLOB" };283 }284285 286 287 288 await chrome.storage.local.set({289 [STORAGE_KEY_VERIFY_CODE]: payload.verifyCode,290 [STORAGE_KEY_LAST_RESTORED]: Date.now(),291 });292293 294 295 if (isNativePlatform()) {296 try {297 await publishInMemoryVault(vault.getEntries());298 } catch {299 300 }301 }302303 return {304 ok: true,305 data: {306 entryCount: result.entryCount,307 verifyCode: payload.verifyCode,308 },309 };310}