1const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";23function base58Decode(str: string): Uint8Array {4 let num = 0n;5 for (const char of str) {6 const idx = BASE58_ALPHABET.indexOf(char);7 if (idx === -1) throw new Error("Invalid base58 character");8 num = num * 58n + BigInt(idx);9 }10 const hex = num.toString(16).padStart(2, "0");11 const paddedHex = hex.length % 2 ? "0" + hex : hex;12 const result = new Uint8Array(paddedHex.length / 2);13 for (let i = 0; i < paddedHex.length; i += 2) {14 result[i / 2] = parseInt(paddedHex.slice(i, i + 2), 16);15 }16 return result;17}1819function fromBase58Groups(groups: string[]): string {20 return groups.join("");21}2223export const PUBLIC_KEY_HEX =24 "c7dc52d2c484696cf8566dbe6cfe69025d114cb47f6cf53c25088ecfd539bdfa";2526export interface LicenseInfo {27 valid: boolean;28 plan: "free" | "pro";29 issued?: Date;30 31 identifier?: number;32}3334const LICENSE_PREFIX = "PPK-";35const LICENSE_REVOCATIONS_KEY = "peach_license_revocations_v1";36const LICENSE_PAYLOAD_LENGTH = 12;3738export function generateLicenseIdentifier(): number {39 const bytes = new Uint32Array(1);40 crypto.getRandomValues(bytes);41 return bytes[0];42}4344export async function revokeLicenseIdentifier(identifier: number): Promise<void> {45 if (!Number.isSafeInteger(identifier) || identifier < 0 || identifier > 0xffffffff) {46 throw new Error("invalid license identifier");47 }48 const result = await chrome.storage.local.get(LICENSE_REVOCATIONS_KEY) as Record<string, unknown>;49 const current = Array.isArray(result[LICENSE_REVOCATIONS_KEY])50 ? result[LICENSE_REVOCATIONS_KEY].filter((value): value is number => Number.isSafeInteger(value))51 : [];52 if (!current.includes(identifier)) current.push(identifier);53 await chrome.storage.local.set({ [LICENSE_REVOCATIONS_KEY]: current });54}55565758async function importPublicKey(publicKeyHex = PUBLIC_KEY_HEX): Promise<CryptoKey> {59 const keyData = hexToBytes(publicKeyHex);60 return crypto.subtle.importKey(61 "raw",62 keyData.buffer as ArrayBuffer,63 { name: "Ed25519" },64 true,65 ["verify"],66 );67}6869function hexToBytes(hex: string): Uint8Array {70 if (!/^[0-9a-f]{64}$/i.test(hex)) throw new Error("invalid public key");71 const bytes = new Uint8Array(32);72 for (let i = 0; i < bytes.length; i += 1) bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);73 return bytes;74}7576export async function validateLicense(keyString: string, publicKeyHex = PUBLIC_KEY_HEX): Promise<LicenseInfo> {77 if (!keyString || typeof keyString !== "string") return { valid: false, plan: "free" };78 const trimmed = keyString.trim();79 if (!trimmed.startsWith(LICENSE_PREFIX)) return { valid: false, plan: "free" };8081 try {82 const groups = trimmed.slice(LICENSE_PREFIX.length).split("-");83 if (groups.length !== 5) return { valid: false, plan: "free" };84 const decoded = base58Decode(fromBase58Groups(groups));85 if (decoded.length !== 76) return { valid: false, plan: "free" };86 const payload = decoded.slice(0, LICENSE_PAYLOAD_LENGTH);87 const signature = decoded.slice(LICENSE_PAYLOAD_LENGTH);88 if (payload[0] !== 0x01 || payload[1] !== 0x01 || payload[2] !== 0x01) {89 return { valid: false, plan: "free" };90 }91 const issuedTimestamp = new DataView(payload.buffer, payload.byteOffset + 3, 4).getUint32(0, true);92 const identifier = new DataView(payload.buffer, payload.byteOffset + 7, 4).getUint32(0, true);93 const publicKey = await importPublicKey(publicKeyHex);94 const isValid = await crypto.subtle.verify("Ed25519", publicKey, signature, payload);95 if (!isValid) return { valid: false, plan: "free" };96 const revocations = await chrome.storage.local.get(LICENSE_REVOCATIONS_KEY) as Record<string, unknown>;97 if (Array.isArray(revocations[LICENSE_REVOCATIONS_KEY]) && revocations[LICENSE_REVOCATIONS_KEY].some((value) => value === identifier)) {98 return { valid: false, plan: "free", identifier };99 }100 return { valid: true, plan: "pro", issued: new Date(issuedTimestamp * 86400 * 1000), identifier };101 } catch {102 return { valid: false, plan: "free" };103 }104}105106export async function getLicenseInfo(): Promise<LicenseInfo> {107 try {108 const result = await chrome.storage.local.get("peach_license") as Record<string, string | undefined>;109 const stored = result["peach_license"];110 if (!stored) {111 return { valid: false, plan: "free" };112 }113 return validateLicense(stored);114 } catch {115 return { valid: false, plan: "free" };116 }117}118119const CELEBRATION_KEY = "peach_pro_celebrated";120121export async function activateLicense(keyString: string): Promise<LicenseInfo> {122 const validated = await validateLicense(keyString);123 if (validated.valid && validated.plan === "pro") {124 125 126 127 await chrome.storage.local.set({128 peach_license: keyString.trim(),129 [CELEBRATION_KEY]: false,130 });131 return validated;132 }133 return { valid: false, plan: "free" };134}135136export async function isProUser(): Promise<boolean> {137 const info = await getLicenseInfo();138 return info.valid && info.plan === "pro";139}140141142export async function getLicenseKey(): Promise<string | null> {143 try {144 const result = await chrome.storage.local.get("peach_license") as Record<string, string | undefined>;145 return result["peach_license"] ?? null;146 } catch {147 return null;148 }149}150151export async function deactivateLicense(): Promise<void> {152 await chrome.storage.local.remove("peach_license");153}154155export async function shouldPlayCelebration(): Promise<boolean> {156 const info = await getLicenseInfo();157 if (!info.valid || info.plan !== "pro") return false;158 const result = await chrome.storage.local.get(CELEBRATION_KEY) as Record<string, boolean | undefined>;159 return !result[CELEBRATION_KEY];160}161162export async function markCelebrationPlayed(): Promise<void> {163 await chrome.storage.local.set({ [CELEBRATION_KEY]: true });164}