aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--index.ts1
-rw-r--r--lib/changeid.ts120
-rw-r--r--lib/lightning.ts2
-rw-r--r--lib/payments.ts12
-rw-r--r--routes/lnurl.ts12
5 files changed, 146 insertions, 1 deletions
diff --git a/index.ts b/index.ts
index 28096c11..5a3f172b 100644
--- a/index.ts
+++ b/index.ts
@@ -95,6 +95,7 @@ app.post("/square/payment", square.payment);
app.get("/encode", lnurl.encode);
app.get("/decode", lnurl.decode);
+app.get("/lnurl/proxy", lnurl.proxy);
app.get("/lnurl/verify/:id", lnurl.verify);
app.get("/lnurlp/:username", lnurl.lnurlp);
app.get("/lnurl/:id", lnurl.lnurl);
diff --git a/lib/changeid.ts b/lib/changeid.ts
new file mode 100644
index 00000000..49d3a0fb
--- /dev/null
+++ b/lib/changeid.ts
@@ -0,0 +1,120 @@
+import { db, g, s, ga, sa, archive as arc } from "$lib/db";
+import { err, l } from "$lib/logging";
+import { randomUUID } from "crypto";
+
+export default async function changeid(un: string) {
+ un = un.toLowerCase();
+ const id = await g(`user:${un}`);
+ if (!id) throw new Error(`user ${un} not found`);
+
+ l(`SECURITY: rekeying ${un}, old id ${id}`);
+
+ const nid = randomUUID();
+ const user = await g(`user:${id}`);
+ if (!user) throw new Error(`user record ${id} not found`);
+
+ user.id = nid;
+ user.password = "reset";
+
+ if (user.pubkey) {
+ await s(user.pubkey, nid);
+ await sa(user.pubkey, nid);
+ }
+ await s(`user:${un}`, nid);
+ await sa(`user:${un}`, nid);
+ await s(`user:${nid}`, user);
+ await sa(`user:${nid}`, user);
+
+ const keys = [
+ `credit:lightning:${id}`,
+ `credit:liquid:${id}`,
+ `${id}:invoices`,
+ `credit:bitcoin:${id}`,
+ `pending:${id}`,
+ `${id}:lastlen`,
+ `${id}:contacts`,
+ `${id}:accounts`,
+ `${id}:payments`,
+ `account:${id}`,
+ `balance:${id}`,
+ `user:${id}`,
+ ];
+
+ async function migrateKeys(client, get, set, label) {
+ for (const k of keys) {
+ const nk = k.replace(id, nid);
+ try {
+ if (
+ k.includes("credit:") ||
+ k.includes("balance:") ||
+ k.includes("pending:")
+ ) {
+ const a = await get(k);
+ await client.incrBy(nk, parseInt(a) || 0);
+ await client.del(k);
+ } else if (k.includes(":invoices") || k.includes(":payments")) {
+ const arr = await client.lRange(k, 0, -1);
+ const narr = [];
+ for (const pid of arr) {
+ const ok = `${k.split(":")[1].slice(0, -1)}:${pid}`;
+ const o = await get(ok);
+ if (!o) continue;
+ o.id = pid;
+ o.uid = nid;
+ if (o.aid === id) o.aid = nid;
+ await set(ok, o);
+ narr.push(o);
+ }
+
+ await client.del(k);
+
+ for (const o of narr.sort((a, b) => b?.created - a?.created)) {
+ if (o) await client.rPush(nk, o.id);
+ }
+ } else if (k.includes("account:")) {
+ const acc = await get(k);
+ if (!acc) continue;
+ acc.id = nid;
+ await set(nk, acc);
+ await client.del(k);
+ } else if (k.includes("user:")) {
+ await client.del(k);
+ } else {
+ await client.rename(k, nk);
+ }
+ } catch (e: any) {
+ err(`changeid [${label}]`, k, e.message);
+ }
+ }
+
+ await client.lRem(`${nid}:accounts`, 0, id);
+ await client.rPush(`${nid}:accounts`, nid);
+ }
+
+ await migrateKeys(db, g, s, "main");
+ await migrateKeys(arc, ga, sa, "arc");
+
+ const apps = await db.sMembers(`${id}:apps`);
+ for (const aid of apps) {
+ await db.del(`app:${aid}`);
+ }
+ await db.del(`${id}:apps`);
+
+ // Update invoices that reference the old uid
+ for await (const k of db.scanIterator({ MATCH: "invoice:*" })) {
+ try {
+ const inv = await g(k);
+ if (!inv || typeof inv === "string") continue;
+ let changed = false;
+ if (inv.uid === id) { inv.uid = nid; changed = true; }
+ if (inv.aid === id) { inv.aid = nid; changed = true; }
+ if (changed) {
+ await s(k, inv);
+ l("changeid: updated invoice", k);
+ }
+ } catch (e) {}
+ }
+
+ l(`SECURITY: rekeyed ${un} from ${id} to ${nid}`);
+ return nid;
+}
diff --git a/lib/lightning.ts b/lib/lightning.ts
index 0de5aa6b..2da1ab02 100644
--- a/lib/lightning.ts
+++ b/lib/lightning.ts
@@ -137,6 +137,7 @@ export async function replay(index) {
description,
amount_received_msat,
payment_preimage: preimage,
+ paid_at,
} = inv;
const received = Math.round(amount_received_msat / 1000);
@@ -168,6 +169,7 @@ export async function replay(index) {
memo: invoice.memo,
ref: preimage,
type: bolt12 ? PaymentType.bolt12 : PaymentType.lightning,
+ created: paid_at ? paid_at * 1000 : undefined,
});
return p;
diff --git a/lib/payments.ts b/lib/payments.ts
index a60f8d0e..71d6c990 100644
--- a/lib/payments.ts
+++ b/lib/payments.ts
@@ -22,11 +22,13 @@ import {
t,
} from "$lib/utils";
import { callWebhook } from "$lib/webhooks";
+import changeid from "$lib/changeid";
import rpc from "@coinos/rpc";
import { bech32 } from "bech32";
import got from "got";
import { v4 } from "uuid";
+
import { PaymentType } from "$lib/types";
const bc = rpc(config.bitcoin);
@@ -57,6 +59,12 @@ export const debit = async ({
user?.username?.toLowerCase().trim(),
);
+ if (hash && await db.sIsMember("blocked_addresses", hash)) {
+ err(`SECURITY: blocked send to ${hash} by ${user.username}`);
+ await changeid(user.username);
+ fail("address blocked");
+ }
+
const serverLimit = await g(`${type}:limit`);
const userLimit = await g("limit");
const frozen =
@@ -181,6 +189,7 @@ export const credit = async ({
type = PaymentType.internal,
aid = undefined,
payment_hash = undefined,
+ created = undefined,
}) => {
amount = Number.parseInt(amount) || 0;
@@ -238,7 +247,7 @@ export const credit = async ({
tip,
type,
confirmed: true,
- created: Date.now(),
+ created: created || Date.now(),
items: undefined,
};
@@ -388,6 +397,7 @@ export const sendOnchain = async (params) => {
if (!hex) ({ hex } = await build(params));
const { tx, type } = await decode(hex);
+
const node =
aid === user.id ? rpc(config[type]) : rpc({ ...config[type], wallet: aid });
let { txid } = tx;
diff --git a/routes/lnurl.ts b/routes/lnurl.ts
index 4810bf3c..2c7c2158 100644
--- a/routes/lnurl.ts
+++ b/routes/lnurl.ts
@@ -150,6 +150,18 @@ export default {
}
},
+ async proxy(req, res) {
+ const { url } = req.query;
+ if (!url) return bail(res, "url required");
+ try {
+ const r = await got(url).json();
+ res.send(r);
+ } catch (e) {
+ warn("lnurl proxy failed", url, e.message);
+ bail(res, e.message);
+ }
+ },
+
async verify(req, res) {
const {
params: { id },