const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; function base58Decode(str: string): Uint8Array { let num = 0n; for (const char of str) { const idx = BASE58_ALPHABET.indexOf(char); if (idx === -1) throw new Error("Invalid base58 character"); num = num * 58n + BigInt(idx); } const hex = num.toString(16).padStart(2, "0"); const paddedHex = hex.length % 2 ? "0" + hex : hex; const result = new Uint8Array(paddedHex.length / 2); for (let i = 0; i < paddedHex.length; i += 2) { result[i / 2] = parseInt(paddedHex.slice(i, i + 2), 16); } return result; } function fromBase58Groups(groups: string[]): string { return groups.join(""); } export const PUBLIC_KEY_HEX = "c7dc52d2c484696cf8566dbe6cfe69025d114cb47f6cf53c25088ecfd539bdfa"; export interface LicenseInfo { valid: boolean; plan: "free" | "pro"; issued?: Date; /** Random issuer-assigned identifier embedded in the signed payload. */ identifier?: number; } const LICENSE_PREFIX = "PPK-"; const LICENSE_REVOCATIONS_KEY = "peach_license_revocations_v1"; const LICENSE_PAYLOAD_LENGTH = 12; /** Issuer-side helper: embed this random uint32 in the signed payload. */ export function generateLicenseIdentifier(): number { const bytes = new Uint32Array(1); crypto.getRandomValues(bytes); return bytes[0]; } export async function revokeLicenseIdentifier(identifier: number): Promise { if (!Number.isSafeInteger(identifier) || identifier < 0 || identifier > 0xffffffff) { throw new Error("invalid license identifier"); } const result = await chrome.storage.local.get(LICENSE_REVOCATIONS_KEY) as Record; const current = Array.isArray(result[LICENSE_REVOCATIONS_KEY]) ? result[LICENSE_REVOCATIONS_KEY].filter((value): value is number => Number.isSafeInteger(value)) : []; if (!current.includes(identifier)) current.push(identifier); await chrome.storage.local.set({ [LICENSE_REVOCATIONS_KEY]: current }); } async function importPublicKey(): Promise { const keyData = hexToBytes(PUBLIC_KEY_HEX); return (crypto.subtle as any).importKey( "raw", keyData, { name: "Ed25519" }, true, ["verify"] ); } function hexToBytes(hex: string): Uint8Array { const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16); } return bytes; } export async function validateLicense(keyString: string): Promise { if (!keyString || typeof keyString !== "string") { return { valid: false, plan: "free" }; } const trimmed = keyString.trim(); if (!trimmed.startsWith(LICENSE_PREFIX)) { return { valid: false, plan: "free" }; } try { const withoutPrefix = trimmed.slice(LICENSE_PREFIX.length); const groups = withoutPrefix.split("-"); if (groups.length !== 5) { return { valid: false, plan: "free" }; } const decoded = base58Decode(fromBase58Groups(groups)); if (decoded.length !== 76) { return { valid: false, plan: "free" }; } const payload = decoded.slice(0, LICENSE_PAYLOAD_LENGTH); const signature = decoded.slice(LICENSE_PAYLOAD_LENGTH, 76); if (payload[0] !== 0x01 || payload[1] !== 0x01 || payload[2] !== 0x01) { return { valid: false, plan: "free" }; } const issuedTimestamp = new DataView(payload.buffer, payload.byteOffset + 3, 4).getUint32(0, true); const issuedDate = new Date(issuedTimestamp * 86400 * 1000); const identifier = new DataView(payload.buffer, payload.byteOffset + 7, 4).getUint32(0, true); const publicKey = await importPublicKey(); const isValid = await (crypto.subtle as any).verify( "Ed25519", publicKey, signature, payload ); if (!isValid) { return { valid: false, plan: "free" }; } const revocations = await chrome.storage.local.get(LICENSE_REVOCATIONS_KEY) as Record; const revoked = Array.isArray(revocations[LICENSE_REVOCATIONS_KEY]) && revocations[LICENSE_REVOCATIONS_KEY].some((value) => value === identifier); if (revoked) return { valid: false, plan: "free", identifier }; return { valid: true, plan: "pro", issued: issuedDate, identifier }; } catch { return { valid: false, plan: "free" }; } } export async function getLicenseInfo(): Promise { try { const result = await chrome.storage.local.get("peach_license") as Record; const stored = result["peach_license"]; if (!stored) { return { valid: false, plan: "free" }; } return validateLicense(stored); } catch { return { valid: false, plan: "free" }; } } const CELEBRATION_KEY = "peach_pro_celebrated"; export async function activateLicense(keyString: string): Promise { const validated = await validateLicense(keyString); if (validated.valid && validated.plan === "pro") { // Reset the celebration flag so every new license activation triggers // the full-screen Pro celebration in the popup, even on devices that // have celebrated before (e.g., testers who activate repeatedly). await chrome.storage.local.set({ peach_license: keyString.trim(), [CELEBRATION_KEY]: false, }); return validated; } return { valid: false, plan: "free" }; } export async function isProUser(): Promise { const info = await getLicenseInfo(); return info.valid && info.plan === "pro"; } /** Returns the raw stored license key string, or null if none. */ export async function getLicenseKey(): Promise { try { const result = await chrome.storage.local.get("peach_license") as Record; return result["peach_license"] ?? null; } catch { return null; } } export async function deactivateLicense(): Promise { await chrome.storage.local.remove("peach_license"); } export async function shouldPlayCelebration(): Promise { const info = await getLicenseInfo(); if (!info.valid || info.plan !== "pro") return false; const result = await chrome.storage.local.get(CELEBRATION_KEY) as Record; return !result[CELEBRATION_KEY]; } export async function markCelebrationPlayed(): Promise { await chrome.storage.local.set({ [CELEBRATION_KEY]: true }); }