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
|
||||
RUST_LOG=info
|
||||
|
||||
# Feistel cipher for payment IDs (shared between pay + web)
|
||||
FEISTEL_SECRET=changeme-use-a-random-secret
|
||||
|
||||
# Pay app — crypto payments
|
||||
FREEDOM_API=https://api-v1.freedom.st
|
||||
XPUB_BTC=
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import sql from "./db";
|
||||
import { COINS } from "../../shared/plans";
|
||||
import { encodeId } from "../../shared/feistel";
|
||||
|
||||
export async function generateReceipt(paymentId: number): Promise<string> {
|
||||
const [payment] = await sql`SELECT * FROM payments WHERE id = ${paymentId}`;
|
||||
|
|
@ -40,7 +41,7 @@ export async function generateReceipt(paymentId: number): Promise<string> {
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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">
|
||||
<style>
|
||||
* { 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="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 class="section">
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { PLAN_PRICING as PLANS, COINS, planTier } from "../../shared/plans";
|
|||
import { generateReceipt } from "./receipt";
|
||||
import { watchPayment } from "./monitor";
|
||||
import { resolveKey as sharedResolveKey, extractAuthKey } from "../../shared/auth";
|
||||
import { encodeId, decodeId } from "../../shared/feistel";
|
||||
|
||||
function requireAuth(app: Elysia) {
|
||||
return app
|
||||
|
|
@ -100,7 +101,7 @@ export const routes = new Elysia()
|
|||
const uri = `${coinInfo.uri}:${address.replace(/^.*:/, '')}?amount=${amountCrypto}`;
|
||||
|
||||
return {
|
||||
id: payment.id,
|
||||
id: encodeId(payment.id),
|
||||
plan: payment.plan,
|
||||
months: payment.months,
|
||||
amount_usd: Number(payment.amount_usd),
|
||||
|
|
@ -126,8 +127,9 @@ export const routes = new Elysia()
|
|||
})
|
||||
|
||||
.get("/checkout/:id", async ({ accountId, params, set }) => {
|
||||
const dbId = decodeId(params.id);
|
||||
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" }; }
|
||||
|
||||
|
|
@ -148,7 +150,7 @@ export const routes = new Elysia()
|
|||
`;
|
||||
|
||||
return {
|
||||
id: payment.id,
|
||||
id: encodeId(payment.id),
|
||||
plan: payment.plan,
|
||||
months: payment.months,
|
||||
amount_usd: Number(payment.amount_usd),
|
||||
|
|
@ -170,8 +172,9 @@ export const routes = new Elysia()
|
|||
})
|
||||
|
||||
.get("/checkout/:id/receipt", async ({ accountId, params, set }) => {
|
||||
const dbId = decodeId(params.id);
|
||||
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.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 { sparklineFromPings, pickBestRegion } from "../utils/sparkline";
|
||||
import { PLAN_LABELS, REGION_COLORS, REGION_LABELS, REGIONS, planTier } from "../../../shared/plans";
|
||||
import { encodeId } from "../../../shared/feistel";
|
||||
|
||||
async function hashFile(path: string): Promise<string> {
|
||||
const bytes = await Bun.file(path).bytes();
|
||||
|
|
@ -311,6 +312,7 @@ export const dashboard = new Elysia()
|
|||
const acc = accountRows[0];
|
||||
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 });
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -194,10 +194,10 @@
|
|||
<div class="flex items-center gap-3">
|
||||
<span class="text-xs text-gray-500"><%= date %></span>
|
||||
<% 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') { %>
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue