license.ts

5.4 KB · raw

1const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";23function base58Encode(data: Uint8Array): string {4  let num = BigInt("0x" + Array.from(data).map(b => b.toString(16).padStart(2, "0")).join(""));5  let result = "";6  while (num > 0n) {7    const remainder = num % 58n;8    num = num / 58n;9    result = BASE58_ALPHABET[Number(remainder)] + result;10  }11  for (let i = 0; i < data.length && data[i] === 0; i++) {12    result = "1" + result;13  }14  return result;15}1617function base58Decode(str: string): Uint8Array {18  let num = 0n;19  for (const char of str) {20    const idx = BASE58_ALPHABET.indexOf(char);21    if (idx === -1) throw new Error("Invalid base58 character");22    num = num * 58n + BigInt(idx);23  }24  const hex = num.toString(16).padStart(2, "0");25  const paddedHex = hex.length % 2 ? "0" + hex : hex;26  const result = new Uint8Array(paddedHex.length / 2);27  for (let i = 0; i < paddedHex.length; i += 2) {28    result[i / 2] = parseInt(paddedHex.slice(i, i + 2), 16);29  }30  return result;31}3233function toBase58Groups(encoded: string): string[] {34  const groups: string[] = [];35  for (let i = 0; i < encoded.length; i += 12) {36    groups.push(encoded.slice(i, i + 12));37  }38  return groups;39}4041function fromBase58Groups(groups: string[]): string {42  return groups.join("");43}4445export const PUBLIC_KEY_HEX =46  "29d0f82eb3527b99635336f5e9bef4ae07f80b32de5206b312e74eb3632d3599";4748export interface LicenseInfo {49  valid: boolean;50  plan: "free" | "pro";51  issued?: Date;52}5354const LICENSE_PREFIX = "PPK-";5556async function importPublicKey(): Promise<CryptoKey> {57  const keyData = hexToBytes(PUBLIC_KEY_HEX);58  return (crypto.subtle as any).importKey(59    "raw",60    keyData,61    { name: "Ed25519" },62    true,63    ["verify"]64  );65}6667function hexToBytes(hex: string): Uint8Array {68  const bytes = new Uint8Array(hex.length / 2);69  for (let i = 0; i < hex.length; i += 2) {70    bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);71  }72  return bytes;73}7475function bytesToHex(bytes: Uint8Array): string {76  return Array.from(bytes)77    .map(b => b.toString(16).padStart(2, "0"))78    .join("");79}8081export async function validateLicense(keyString: string): Promise<LicenseInfo> {82  if (!keyString || typeof keyString !== "string") {83    return { valid: false, plan: "free" };84  }8586  const trimmed = keyString.trim();87  if (!trimmed.startsWith(LICENSE_PREFIX)) {88    return { valid: false, plan: "free" };89  }9091  try {92    const withoutPrefix = trimmed.slice(LICENSE_PREFIX.length);93    const groups = withoutPrefix.split("-");94    if (groups.length !== 5) {95      return { valid: false, plan: "free" };96    }9798    const base58String = fromBase58Groups(groups);99    const decoded = base58Decode(base58String);100101    if (decoded.length !== 76) {102      return { valid: false, plan: "free" };103    }104105    const payload = decoded.slice(0, 12);106    const signature = decoded.slice(12, 76);107108    if (payload[0] !== 0x01) {109      return { valid: false, plan: "free" };110    }111    if (payload[1] !== 0x01) {112      return { valid: false, plan: "free" };113    }114    if (payload[2] !== 0x01) {115      return { valid: false, plan: "free" };116    }117118    const issuedTimestamp = new DataView(payload.buffer, payload.byteOffset + 3, 4).getUint32(0, true);119    const issuedDate = new Date(issuedTimestamp * 86400 * 1000);120121    const publicKey = await importPublicKey();122    const isValid = await (crypto.subtle as any).verify(123      "Ed25519",124      publicKey,125      signature,126      payload127    );128129    if (!isValid) {130      return { valid: false, plan: "free" };131    }132133    return { valid: true, plan: "pro", issued: issuedDate };134  } catch {135    return { valid: false, plan: "free" };136  }137}138139export async function getLicenseInfo(): Promise<LicenseInfo> {140  try {141    const result = await chrome.storage.local.get("peach_license") as Record<string, string | undefined>;142    const stored = result["peach_license"];143    if (!stored) {144      return { valid: false, plan: "free" };145    }146    return validateLicense(stored);147  } catch {148    return { valid: false, plan: "free" };149  }150}151152export async function activateLicense(keyString: string): Promise<LicenseInfo> {153  const validated = await validateLicense(keyString);154  if (validated.valid && validated.plan === "pro") {155    await chrome.storage.local.set({ peach_license: keyString.trim() });156    return validated;157  }158  return { valid: false, plan: "free" };159}160161export async function isProUser(): Promise<boolean> {162  const info = await getLicenseInfo();163  return info.valid && info.plan === "pro";164}165166/** Returns the raw stored license key string, or null if none. */167export async function getLicenseKey(): Promise<string | null> {168  try {169    const result = await chrome.storage.local.get("peach_license") as Record<string, string | undefined>;170    return result["peach_license"] ?? null;171  } catch {172    return null;173  }174}175176export async function deactivateLicense(): Promise<void> {177  await chrome.storage.local.remove("peach_license");178}179180const CELEBRATION_KEY = "peach_pro_celebrated";181182export async function shouldPlayCelebration(): Promise<boolean> {183  const info = await getLicenseInfo();184  if (!info.valid || info.plan !== "pro") return false;185  const result = await chrome.storage.local.get(CELEBRATION_KEY) as Record<string, boolean | undefined>;186  return !result[CELEBRATION_KEY];187}188189export async function markCelebrationPlayed(): Promise<void> {190  await chrome.storage.local.set({ [CELEBRATION_KEY]: true });191}