add feistel encryption
This commit is contained in:
parent
4bb8e394ef
commit
b2cc83538b
|
|
@ -10,6 +10,9 @@ COORDINATOR_URL=http://localhost:3000
|
||||||
MONITOR_TOKEN=changeme-use-a-random-secret
|
MONITOR_TOKEN=changeme-use-a-random-secret
|
||||||
RUST_LOG=info
|
RUST_LOG=info
|
||||||
|
|
||||||
|
# Feistel cipher for payment IDs (shared between pay + web)
|
||||||
|
FEISTEL_SECRET=changeme-use-a-random-secret
|
||||||
|
|
||||||
# Pay app — crypto payments
|
# Pay app — crypto payments
|
||||||
FREEDOM_API=https://api-v1.freedom.st
|
FREEDOM_API=https://api-v1.freedom.st
|
||||||
XPUB_BTC=
|
XPUB_BTC=
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import sql from "./db";
|
import sql from "./db";
|
||||||
import { COINS } from "../../shared/plans";
|
import { COINS } from "../../shared/plans";
|
||||||
|
import { encodeId } from "../../shared/feistel";
|
||||||
|
|
||||||
export async function generateReceipt(paymentId: number): Promise<string> {
|
export async function generateReceipt(paymentId: number): Promise<string> {
|
||||||
const [payment] = await sql`SELECT * FROM payments WHERE id = ${paymentId}`;
|
const [payment] = await sql`SELECT * FROM payments WHERE id = ${paymentId}`;
|
||||||
|
|
@ -40,7 +41,7 @@ export async function generateReceipt(paymentId: number): Promise<string> {
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>PingQL Receipt #${payment.id}</title>
|
<title>PingQL Receipt #${encodeId(payment.id)}</title>
|
||||||
<link rel="icon" href="https://pingql.com/favicon.svg" type="image/svg+xml">
|
<link rel="icon" href="https://pingql.com/favicon.svg" type="image/svg+xml">
|
||||||
<style>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
@ -71,7 +72,7 @@ export async function generateReceipt(paymentId: number): Promise<string> {
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="logo">Ping<span>QL</span></div>
|
<div class="logo">Ping<span>QL</span></div>
|
||||||
<div class="meta">Receipt #${payment.id} · Issued ${paidDate}</div>
|
<div class="meta">Receipt #${encodeId(payment.id)} · Issued ${paidDate}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { PLAN_PRICING as PLANS, COINS, planTier } from "../../shared/plans";
|
||||||
import { generateReceipt } from "./receipt";
|
import { generateReceipt } from "./receipt";
|
||||||
import { watchPayment } from "./monitor";
|
import { watchPayment } from "./monitor";
|
||||||
import { resolveKey as sharedResolveKey, extractAuthKey } from "../../shared/auth";
|
import { resolveKey as sharedResolveKey, extractAuthKey } from "../../shared/auth";
|
||||||
|
import { encodeId, decodeId } from "../../shared/feistel";
|
||||||
|
|
||||||
function requireAuth(app: Elysia) {
|
function requireAuth(app: Elysia) {
|
||||||
return app
|
return app
|
||||||
|
|
@ -100,7 +101,7 @@ export const routes = new Elysia()
|
||||||
const uri = `${coinInfo.uri}:${address.replace(/^.*:/, '')}?amount=${amountCrypto}`;
|
const uri = `${coinInfo.uri}:${address.replace(/^.*:/, '')}?amount=${amountCrypto}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: payment.id,
|
id: encodeId(payment.id),
|
||||||
plan: payment.plan,
|
plan: payment.plan,
|
||||||
months: payment.months,
|
months: payment.months,
|
||||||
amount_usd: Number(payment.amount_usd),
|
amount_usd: Number(payment.amount_usd),
|
||||||
|
|
@ -126,8 +127,9 @@ export const routes = new Elysia()
|
||||||
})
|
})
|
||||||
|
|
||||||
.get("/checkout/:id", async ({ accountId, params, set }) => {
|
.get("/checkout/:id", async ({ accountId, params, set }) => {
|
||||||
|
const dbId = decodeId(params.id);
|
||||||
const [payment] = await sql`
|
const [payment] = await sql`
|
||||||
SELECT * FROM payments WHERE id = ${params.id} AND account_id = ${accountId}
|
SELECT * FROM payments WHERE id = ${dbId} AND account_id = ${accountId}
|
||||||
`;
|
`;
|
||||||
if (!payment) { set.status = 404; return { error: "Payment not found" }; }
|
if (!payment) { set.status = 404; return { error: "Payment not found" }; }
|
||||||
|
|
||||||
|
|
@ -148,7 +150,7 @@ export const routes = new Elysia()
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: payment.id,
|
id: encodeId(payment.id),
|
||||||
plan: payment.plan,
|
plan: payment.plan,
|
||||||
months: payment.months,
|
months: payment.months,
|
||||||
amount_usd: Number(payment.amount_usd),
|
amount_usd: Number(payment.amount_usd),
|
||||||
|
|
@ -170,8 +172,9 @@ export const routes = new Elysia()
|
||||||
})
|
})
|
||||||
|
|
||||||
.get("/checkout/:id/receipt", async ({ accountId, params, set }) => {
|
.get("/checkout/:id/receipt", async ({ accountId, params, set }) => {
|
||||||
|
const dbId = decodeId(params.id);
|
||||||
const [payment] = await sql`
|
const [payment] = await sql`
|
||||||
SELECT * FROM payments WHERE id = ${params.id} AND account_id = ${accountId}
|
SELECT * FROM payments WHERE id = ${dbId} AND account_id = ${accountId}
|
||||||
`;
|
`;
|
||||||
if (!payment) { set.status = 404; return { error: "Payment not found" }; }
|
if (!payment) { set.status = 404; return { error: "Payment not found" }; }
|
||||||
if (payment.status !== "paid") { set.status = 400; return { error: "Receipt is only available for paid invoices" }; }
|
if (payment.status !== "paid") { set.status = 400; return { error: "Receipt is only available for paid invoices" }; }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
const SECRET = process.env.FEISTEL_SECRET || "change-me";
|
||||||
|
const ROUNDS = 8;
|
||||||
|
const HALF = 20;
|
||||||
|
const MASK = (1 << HALF) - 1;
|
||||||
|
const CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
function roundFn(right: number, round: number): number {
|
||||||
|
const h = new Bun.CryptoHasher("sha256").update(SECRET + ":" + round + ":" + right).digest();
|
||||||
|
return (h[0] << 12 | h[1] << 4 | h[2] >> 4) & MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBase62(n: number): string {
|
||||||
|
let s = "";
|
||||||
|
for (let i = 0; i < 7; i++) { s = CHARS[n % 62] + s; n = Math.floor(n / 62); }
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromBase62(s: string): number {
|
||||||
|
let n = 0;
|
||||||
|
for (const c of s) n = n * 62 + CHARS.indexOf(c);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeId(val: number): string {
|
||||||
|
let left = Math.floor(val / (MASK + 1)) & MASK;
|
||||||
|
let right = val & MASK;
|
||||||
|
for (let i = 0; i < ROUNDS; i++) {
|
||||||
|
const tmp = (left ^ roundFn(right, i)) & MASK;
|
||||||
|
left = right;
|
||||||
|
right = tmp;
|
||||||
|
}
|
||||||
|
return toBase62(left * (MASK + 1) + right);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeId(s: string): number {
|
||||||
|
const val = fromBase62(s);
|
||||||
|
let left = Math.floor(val / (MASK + 1)) & MASK;
|
||||||
|
let right = val & MASK;
|
||||||
|
for (let i = ROUNDS - 1; i >= 0; i--) {
|
||||||
|
const tmp = (right ^ roundFn(left, i)) & MASK;
|
||||||
|
right = left;
|
||||||
|
left = tmp;
|
||||||
|
}
|
||||||
|
return left * (MASK + 1) + right;
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import { resolveKey } from "./auth";
|
||||||
import sql from "../db";
|
import sql from "../db";
|
||||||
import { sparklineFromPings, pickBestRegion } from "../utils/sparkline";
|
import { sparklineFromPings, pickBestRegion } from "../utils/sparkline";
|
||||||
import { PLAN_LABELS, REGION_COLORS, REGION_LABELS, REGIONS, planTier } from "../../../shared/plans";
|
import { PLAN_LABELS, REGION_COLORS, REGION_LABELS, REGIONS, planTier } from "../../../shared/plans";
|
||||||
|
import { encodeId } from "../../../shared/feistel";
|
||||||
|
|
||||||
async function hashFile(path: string): Promise<string> {
|
async function hashFile(path: string): Promise<string> {
|
||||||
const bytes = await Bun.file(path).bytes();
|
const bytes = await Bun.file(path).bytes();
|
||||||
|
|
@ -311,6 +312,7 @@ export const dashboard = new Elysia()
|
||||||
const acc = accountRows[0];
|
const acc = accountRows[0];
|
||||||
const monitorCount = monitorCountRows[0].count;
|
const monitorCount = monitorCountRows[0].count;
|
||||||
|
|
||||||
|
for (const inv of invoices) inv.public_id = encodeId(inv.id);
|
||||||
return html("settings", { nav: "settings", account: acc, apiKeys, accountId, loginKey, isSubKey, monitorCount, invoices });
|
return html("settings", { nav: "settings", account: acc, apiKeys, accountId, loginKey, isSubKey, monitorCount, invoices });
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,10 +194,10 @@
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="text-xs text-gray-500"><%= date %></span>
|
<span class="text-xs text-gray-500"><%= date %></span>
|
||||||
<% if (inv.status === 'pending' || inv.status === 'underpaid' || inv.status === 'confirming') { %>
|
<% if (inv.status === 'pending' || inv.status === 'underpaid' || inv.status === 'confirming') { %>
|
||||||
<a href="/dashboard/checkout/<%= inv.id %>" class="text-xs text-blue-400 hover:text-blue-300">View</a>
|
<a href="/dashboard/checkout/<%= inv.public_id %>" class="text-xs text-blue-400 hover:text-blue-300">View</a>
|
||||||
<% } else if (inv.status === 'paid') { %>
|
<% } else if (inv.status === 'paid') { %>
|
||||||
<span class="text-xs text-green-500/70">Paid</span>
|
<span class="text-xs text-green-500/70">Paid</span>
|
||||||
<a href="/dashboard/checkout/<%= inv.id %>/receipt" target="_blank" class="text-xs text-blue-400 hover:text-blue-300 transition-colors">Receipt</a>
|
<a href="/dashboard/checkout/<%= inv.public_id %>/receipt" target="_blank" class="text-xs text-blue-400 hover:text-blue-300 transition-colors">Receipt</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue