Merge branch 'wallet' into 'main'

Add an HD wallet

See merge request soapbox-pub/ditto!532
This commit is contained in:
Alex Gleason 2024-10-03 22:56:58 +00:00
commit 205b9a77fe
4 changed files with 95 additions and 0 deletions

View file

@ -44,6 +44,7 @@
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.36.0",
"@nostrify/policies": "jsr:@nostrify/policies@^0.35.0",
"@scure/base": "npm:@scure/base@^1.1.6",
"@scure/bip32": "npm:@scure/bip32@^1.5.0",
"@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs",
"@soapbox/kysely-pglite": "jsr:@soapbox/kysely-pglite@^1.0.0",
"@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0",

24
deno.lock generated
View file

@ -73,6 +73,7 @@
"npm:@noble/secp256k1@^2.0.0": "npm:@noble/secp256k1@2.1.0",
"npm:@scure/base@^1.1.6": "npm:@scure/base@1.1.6",
"npm:@scure/bip32@^1.4.0": "npm:@scure/bip32@1.4.0",
"npm:@scure/bip32@^1.5.0": "npm:@scure/bip32@1.5.0",
"npm:@scure/bip39@^1.3.0": "npm:@scure/bip39@1.3.0",
"npm:@types/node": "npm:@types/node@18.16.19",
"npm:comlink-async-generator": "npm:comlink-async-generator@0.0.1",
@ -604,6 +605,12 @@
"@noble/hashes": "@noble/hashes@1.4.0"
}
},
"@noble/curves@1.6.0": {
"integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==",
"dependencies": {
"@noble/hashes": "@noble/hashes@1.5.0"
}
},
"@noble/hashes@1.3.1": {
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"dependencies": {}
@ -616,6 +623,10 @@
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
"dependencies": {}
},
"@noble/hashes@1.5.0": {
"integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==",
"dependencies": {}
},
"@noble/secp256k1@2.1.0": {
"integrity": "sha512-XLEQQNdablO0XZOIniFQimiXsZDNwaYgL96dZwC54Q30imSbAOFf3NKtepc+cXyuZf5Q1HCgbqgZ2UFFuHVcEw==",
"dependencies": {}
@ -632,6 +643,10 @@
"integrity": "sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==",
"dependencies": {}
},
"@scure/base@1.1.9": {
"integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==",
"dependencies": {}
},
"@scure/bip32@1.3.1": {
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
"dependencies": {
@ -648,6 +663,14 @@
"@scure/base": "@scure/base@1.1.6"
}
},
"@scure/bip32@1.5.0": {
"integrity": "sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw==",
"dependencies": {
"@noble/curves": "@noble/curves@1.6.0",
"@noble/hashes": "@noble/hashes@1.5.0",
"@scure/base": "@scure/base@1.1.9"
}
},
"@scure/bip39@1.2.1": {
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
"dependencies": {
@ -2132,6 +2155,7 @@
"npm:@isaacs/ttlcache@^1.4.1",
"npm:@noble/secp256k1@^2.0.0",
"npm:@scure/base@^1.1.6",
"npm:@scure/bip32@^1.5.0",
"npm:comlink-async-generator@^0.0.1",
"npm:comlink@^4.4.1",
"npm:commander@12.1.0",

51
src/DittoWallet.ts Normal file
View file

@ -0,0 +1,51 @@
import { HDKey } from '@scure/bip32';
import { Conf } from '@/config.ts';
/**
* HD wallet based on the `DITTO_NSEC`.
* The wallet is used to derive keys for various purposes.
* It is a singleton with static methods, and the keys are cached.
*/
export class DittoWallet {
static #root = HDKey.fromMasterSeed(Conf.seckey);
static #keys = new Map<string, HDKey>();
/** Derive the key cached. */
static derive(path: string): HDKey {
const existing = this.#keys.get(path);
if (existing) {
return existing;
} else {
const key = this.#root.derive(path);
this.#keys.set(path, key);
return key;
}
}
/** Derive the key and return the bytes. */
static deriveKey(path: string): Uint8Array {
const { privateKey } = this.derive(path);
if (!privateKey) {
throw new Error('Private key not available');
}
return privateKey;
}
/** Database encryption key for AES-GCM encryption of database columns. */
static get dbKey(): Uint8Array {
return this.deriveKey(Conf.wallet.dbKeyPath);
}
/** Captcha encryption key for encrypting answer data in AES-GCM. */
static get captchaKey(): Uint8Array {
return this.deriveKey(Conf.wallet.captchaKeyPath);
}
/** VAPID secret key, used for web push notifications. ES256. */
static get vapidKey(): Uint8Array {
return this.deriveKey(Conf.wallet.vapidKeyPath);
}
}

View file

@ -99,6 +99,25 @@ class Conf {
},
},
};
/**
* BIP-32 derivation paths for different crypto use-cases.
* The `DITTO_NSEC` is used as the seed.
* Keys can be rotated by changing the derviation path.
*/
static wallet = {
/** Private key for AES-GCM encryption in the Postgres database. */
get dbKeyPath(): string {
return Deno.env.get('WALLET_DB_KEY_PATH') || "m/0'/1'";
},
/** Private key for AES-GCM encryption of captcha answer data. */
get captchaKeyPath(): string {
return Deno.env.get('WALLET_CAPTCHA_KEY_PATH') || "m/0'/2'";
},
/** VAPID private key path. */
get vapidKeyPath(): string {
return Deno.env.get('WALLET_VAPID_KEY_PATH') || "m/0'/3'";
},
};
/** Character limit to enforce for posts made through Mastodon API. */
static get postCharLimit(): number {
return Number(Deno.env.get('POST_CHAR_LIMIT') || 5000);