diff --git a/lib/validate.js b/lib/validate.js index b5dc562dee..4bcfb3be94 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -1,3 +1,4 @@ +import { secp256k1 } from '@noble/curves/secp256k1' import { string, ValidationError, number, object, array, addMethod, boolean } from 'yup' import { BOOST_MIN, MAX_POLL_CHOICE_LENGTH, MAX_TITLE_LENGTH, MAX_POLL_NUM_CHOICES, MIN_POLL_NUM_CHOICES, SUBS_NO_JOBS, MAX_FORWARDS } from './constants' import { NAME_QUERY } from '../fragments/users' @@ -260,3 +261,24 @@ export const pushSubscriptionSchema = object({ p256dh: string().required('required').trim(), auth: string().required('required').trim() }) + +export const lud18PayerDataSchema = (k1) => object({ + name: string(), + pubkey: string(), + auth: object({ + key: string().required('auth key required'), + k1: string().required('auth k1 required').equals(k1, 'must equal original k1 value'), + sig: string().required('auth sig required') + }) + .test('verify auth signature', auth => { + const { key, k1, sig } = auth + try { + return secp256k1.verify(sig, k1, key) + } catch (err) { + console.log('error caught validating auth signature', err) + return false + } + }), + email: string(), + identifier: string() +}) diff --git a/pages/api/lnurlp/[username]/index.js b/pages/api/lnurlp/[username]/index.js index 35cacc597f..2719e45d24 100644 --- a/pages/api/lnurlp/[username]/index.js +++ b/pages/api/lnurlp/[username]/index.js @@ -1,20 +1,35 @@ +import { randomBytes } from 'crypto' import { getPublicKey } from 'nostr' import models from '../../../../api/models' import { lnurlPayMetadataString } from '../../../../lib/lnurl' import { LNURLP_COMMENT_MAX_LENGTH } from '../../../../lib/constants' +const generateK1 = () => randomBytes(32).toString('hex') + export default async ({ query: { username } }, res) => { const user = await models.user.findUnique({ where: { name: username } }) if (!user) { return res.status(400).json({ status: 'ERROR', reason: `user @${username} does not exist` }) } + const k1 = generateK1() + return res.status(200).json({ - callback: `${process.env.PUBLIC_URL}/api/lnurlp/${username}/pay`, // The URL from LN SERVICE which will accept the pay request parameters + callback: `${process.env.PUBLIC_URL}/api/lnurlp/${username}/pay?k1=${k1}`, // The URL from LN SERVICE which will accept the pay request parameters minSendable: 1000, // Min amount LN SERVICE is willing to receive, can not be less than 1 or more than `maxSendable` maxSendable: 1000000000, metadata: lnurlPayMetadataString(username), // Metadata json which must be presented as raw string here, this is required to pass signature verification at a later step commentAllowed: LNURLP_COMMENT_MAX_LENGTH, // LUD-12 Comments for payRequests https://github.com/lnurl/luds/blob/luds/12.md + payerData: { // LUD-18 payer data for payRequests https://github.com/lnurl/luds/blob/luds/18.md + name: { mandatory: false }, + pubkey: { mandatory: false }, + identifier: { mandatory: false }, + email: { mandatory: false }, + auth: { + mandatory: false, + k1 + } + }, tag: 'payRequest', // Type of LNURL nostrPubkey: process.env.NOSTR_PRIVATE_KEY ? getPublicKey(process.env.NOSTR_PRIVATE_KEY) : undefined, allowsNostr: !!process.env.NOSTR_PRIVATE_KEY diff --git a/pages/api/lnurlp/[username]/pay.js b/pages/api/lnurlp/[username]/pay.js index f691edf3de..48b1a047c2 100644 --- a/pages/api/lnurlp/[username]/pay.js +++ b/pages/api/lnurlp/[username]/pay.js @@ -1,14 +1,15 @@ import models from '../../../../api/models' import lnd from '../../../../api/lnd' import { createInvoice } from 'ln-service' -import { lnurlPayDescriptionHashForUser } from '../../../../lib/lnurl' +import { lnurlPayDescriptionHashForUser, lnurlPayMetadataString, lnurlPayDescriptionHash } from '../../../../lib/lnurl' import serialize from '../../../../api/resolvers/serial' import { schnorr } from '@noble/curves/secp256k1' import { createHash } from 'crypto' import { datePivot } from '../../../../lib/time' import { BALANCE_LIMIT_MSATS, INV_PENDING_LIMIT, LNURLP_COMMENT_MAX_LENGTH } from '../../../../lib/constants' +import { ssValidate, lud18PayerDataSchema } from '../../../../lib/validate' -export default async ({ query: { username, amount, nostr, comment } }, res) => { +export default async ({ query: { username, amount, nostr, comment, payerdata: payerData, k1 } }, res) => { const user = await models.user.findUnique({ where: { name: username } }) if (!user) { return res.status(400).json({ status: 'ERROR', reason: `user @${username} does not exist` }) @@ -41,6 +42,26 @@ export default async ({ query: { username, amount, nostr, comment } }, res) => { return res.status(400).json({ status: 'ERROR', reason: `comment cannot exceed ${LNURLP_COMMENT_MAX_LENGTH} characters in length` }) } + if (payerData) { + let parsedPayerData + try { + parsedPayerData = JSON.parse(payerData) + } catch (err) { + console.error('failed to parse payerdata', err) + return res.status(400).json({ status: 'ERROR', reason: 'Invalid JSON supplied for payerdata parameter' }) + } + + try { + await ssValidate(lud18PayerDataSchema, parsedPayerData, k1) + } catch (err) { + return res.status(400).json({ status: 'ERROR', reason: err.toString() }) + } + + // Update description hash to include the passed payer data + const metadataStr = `${lnurlPayMetadataString(username)}${payerData}` + descriptionHash = lnurlPayDescriptionHash(metadataStr) + } + // generate invoice const expiresAt = datePivot(new Date(), { minutes: 1 }) const invoice = await createInvoice({