diff --git a/src/config.ts b/src/config.ts index 4183d96..aa55548 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,32 @@ +import { nip19, secp } from '@/deps.ts'; + /** Application-wide configuration. */ const Conf = { get nsec() { - return Deno.env.get('DITTO_NSEC'); + const value = Deno.env.get('DITTO_NSEC'); + if (!value) { + throw new Error('Missing DITTO_NSEC'); + } + if (!value.startsWith('nsec1')) { + throw new Error('Invalid DITTO_NSEC'); + } + return value as `nsec1${string}`; + }, + get seckey() { + const result = nip19.decode(Conf.nsec); + if (result.type !== 'nsec') { + throw new Error('Invalid DITTO_NSEC'); + } + return result.data; + }, + get cryptoKey() { + return crypto.subtle.importKey( + 'raw', + secp.utils.hexToBytes(Conf.seckey), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign', 'verify'], + ); }, get relay() { return Deno.env.get('DITTO_RELAY'); diff --git a/src/deps.ts b/src/deps.ts index 91833db..9e8e9b7 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -21,7 +21,7 @@ export { nip19, nip21, verifySignature, -} from 'npm:nostr-tools@^1.11.2'; +} from 'npm:nostr-tools@^1.12.1'; export { findReplyTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts'; export { parseFormData } from 'npm:formdata-helper@^0.3.0'; // @deno-types="npm:@types/lodash@4.14.194" @@ -39,3 +39,13 @@ export { default as sanitizeHtml } from 'npm:sanitize-html@^2.10.0'; export { default as ISO6391 } from 'npm:iso-639-1@2.1.15'; export { Dongoose } from 'https://raw.githubusercontent.com/alexgleason/dongoose/68b7ad9dd7b6ec0615e246a9f1603123c1709793/mod.ts'; export { createPentagon } from 'https://deno.land/x/pentagon@v0.1.1/mod.ts'; +export { + type ParsedSignature, + pemToPublicKey, + publicKeyToPem, + signRequest, + verifyRequest, +} from 'https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/mod.ts'; +export { generateSeededRsa } from 'https://gitlab.com/soapbox-pub/seeded-rsa/-/raw/v1.0.0/mod.ts'; +export * as secp from 'npm:@noble/secp256k1@^1.7.1'; +export { LRUCache } from 'npm:lru-cache@^10.0.0'; diff --git a/src/transformers/nostr-to-activitypub.ts b/src/transformers/nostr-to-activitypub.ts index d6e1796..40a374c 100644 --- a/src/transformers/nostr-to-activitypub.ts +++ b/src/transformers/nostr-to-activitypub.ts @@ -1,5 +1,6 @@ import { Conf } from '@/config.ts'; import { parseMetaContent } from '@/schema.ts'; +import { getPublicKeyPem } from '@/utils/rsa.ts'; import type { Event } from '@/event.ts'; import type { Actor } from '@/schemas/activitypub.ts'; @@ -32,6 +33,11 @@ async function toActor(event: Event<0>, username: string): Promise { summary: content.about ?? '', attachment: [], tag: [], + publicKey: { + id: Conf.local(`/users/${username}#main-key`), + owner: Conf.local(`/users/${username}`), + publicKeyPem: await getPublicKeyPem(event.pubkey), + }, endpoints: { sharedInbox: Conf.local('/inbox'), }, diff --git a/src/utils/rsa.ts b/src/utils/rsa.ts new file mode 100644 index 0000000..820e0a4 --- /dev/null +++ b/src/utils/rsa.ts @@ -0,0 +1,29 @@ +import { Conf } from '@/config.ts'; +import { generateSeededRsa, LRUCache, publicKeyToPem, secp } from '@/deps.ts'; + +const opts = { + bits: 1024, +}; + +const rsaCache = new LRUCache>({ max: 1000 }); + +async function buildSeed(pubkey: string): Promise { + const key = await Conf.cryptoKey; + const data = new TextEncoder().encode(pubkey); + const signature = await window.crypto.subtle.sign('HMAC', key, data); + return secp.utils.bytesToHex(new Uint8Array(signature)); +} + +async function getPublicKeyPem(pubkey: string): Promise { + const cached = await rsaCache.get(pubkey); + if (cached) return cached; + + const seed = await buildSeed(pubkey); + const { publicKey } = await generateSeededRsa(seed, opts); + const promise = publicKeyToPem(publicKey); + + rsaCache.set(pubkey, promise); + return promise; +} + +export { getPublicKeyPem };