aboutsummaryrefslogtreecommitdiffstats
path: root/lib/changeid.ts
blob: c7343ab3db43d160d742006e8540a22f72a3f907 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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";

  // A rekey assumes the account's credentials are compromised, nostr key
  // included. Drop the pubkey's login mapping rather than pointing it at the
  // new id, and revoke the key so nostr login can't register a fresh account
  // with it either. user.pubkey stays on the record so an admin can re-link.
  if (user.pubkey) {
    for (const k of [`user:${user.pubkey}`, user.pubkey]) {
      await db.del(k);
      await arc.del(k);
    }
    await db.sAdd("revoked:pubkeys", user.pubkey);
  }
  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}:offer`,
    `${id}:trust`,
    `${id}:subscriptions`,
    `${id}:items`,
    `${id}:pins`,
    `${id}:square`,
    `${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 {
          if (await client.exists(k)) 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 uid/aid in invoice objects and their offer pointers
  const invoiceIds = await db.lRange(`${nid}:invoices`, 0, -1);
  for (const iid of invoiceIds) {
    try {
      const inv = await g(`invoice:${iid}`);
      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(`invoice:${iid}`, inv);
        l("changeid: updated invoice", iid);
      }
    } catch (e) {}
  }

  // Freezes and evictions are keyed by uid, so carry them to the new id or a
  // rekey (which the blocked-address check triggers on its own) lifts them.
  for (const set of ["blacklist", "evicted"])
    if (await db.sIsMember(set, id)) await db.sAdd(set, nid);

  l(`SECURITY: rekeyed ${un} from ${id} to ${nid}`);
  return nid;
}