Skip to content

Commit

Permalink
Various LUD-18 updates
Browse files Browse the repository at this point in the history
* don't cache the well-known response, since it includes randomly generated single use values

* validate k1 from well-known response to pay URL

* only keep k1's for 10 minutes if they go unused

* fix validation logic to make auth object optional
  • Loading branch information
SatsAllDay committed Sep 25, 2023
1 parent 6e8a795 commit 18d36a5
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 10 deletions.
10 changes: 7 additions & 3 deletions lib/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,12 +265,16 @@ export const pushSubscriptionSchema = object({
export const lud18PayerDataSchema = (k1) => object({
name: string(),
pubkey: string(),
auth: object({
auth: object().shape({
key: string().required('auth key required'),
k1: string().required('auth k1 required').equals(k1, 'must equal original k1 value'),
k1: string().required('auth k1 required').equals([k1], 'must equal original k1 value'),
sig: string().required('auth sig required')
})
.default(undefined)
.test('verify auth signature', auth => {
if (!auth) {
return true
}
const { key, k1, sig } = auth
try {
return secp256k1.verify(sig, k1, key)
Expand All @@ -279,6 +283,6 @@ export const lud18PayerDataSchema = (k1) => object({
return false
}
}),
email: string(),
email: string().email('bad email address'),
identifier: string()
})
15 changes: 9 additions & 6 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const corsHeaders = [
value: 'GET, HEAD, OPTIONS'
}
]
const noCacheHeader = {
key: 'Cache-Control',
value: 'no-cache'
}

let commitHash
if (isProd) {
Expand Down Expand Up @@ -62,17 +66,15 @@ module.exports = withPlausibleProxy()({
{
source: '/.well-known/:slug*',
headers: [
...corsHeaders
...corsHeaders,
noCacheHeader
]
},
// never cache service worker
// https://stackoverflow.com/questions/38843970/service-worker-javascript-update-frequency-every-24-hours/38854905#38854905
{
source: '/sw.js',
headers: [{
key: 'Cache-Control',
value: 'no-cache'
}]
headers: [noCacheHeader]
},
{
source: '/api/lnauth',
Expand All @@ -83,7 +85,8 @@ module.exports = withPlausibleProxy()({
{
source: '/api/lnurlp/:slug*',
headers: [
...corsHeaders
...corsHeaders,
noCacheHeader
]
},
{
Expand Down
8 changes: 8 additions & 0 deletions pages/api/lnurlp/[username]/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,21 @@ import { LNURLP_COMMENT_MAX_LENGTH } from '../../../../lib/constants'

const generateK1 = () => randomBytes(32).toString('hex')

export const k1Cache = new Map()

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` })
}

// Generate a random k1, cache it with the requested username for validation upon invoice request
const k1 = generateK1()
k1Cache.set(k1, username)
// Invalidate the k1 after 10 minutes, if unused
setTimeout(() => {
k1Cache.delete(k1)
}, 1000 * 60 * 10)

return res.status(200).json({
callback: `${process.env.PUBLIC_URL}/api/lnurlp/${username}/pay?k1=${k1}`, // The URL from LN SERVICE which will accept the pay request parameters
Expand Down
16 changes: 15 additions & 1 deletion pages/api/lnurlp/[username]/pay.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,22 @@ 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'
import { k1Cache } from './index'

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` })
}
if (!k1) {
return res.status(400).json({ status: 'ERROR', reason: `k1 value required` })
}
if (!k1Cache.has(k1)) {
return res.status(400).json({ status: 'ERROR', reason: `k1 has already been used or expired, request another` })
}
if (k1Cache.get(k1) !== username) {
return res.status(400).json({ status: 'ERROR', reason: `k1 value is not associated with user @${username}, request another for user @${username}` })
}
try {
// if nostr, decode, validate sig, check tags, set description hash
let description, descriptionHash, noteStr
Expand Down Expand Up @@ -45,7 +55,7 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa
if (payerData) {
let parsedPayerData
try {
parsedPayerData = JSON.parse(payerData)
parsedPayerData = JSON.parse(decodeURIComponent(payerData))
} catch (err) {
console.error('failed to parse payerdata', err)
return res.status(400).json({ status: 'ERROR', reason: 'Invalid JSON supplied for payerdata parameter' })
Expand All @@ -54,6 +64,7 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa
try {
await ssValidate(lud18PayerDataSchema, parsedPayerData, k1)
} catch (err) {
console.error('error validating payer data', err)
return res.status(400).json({ status: 'ERROR', reason: err.toString() })
}

Expand All @@ -77,6 +88,9 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa
${expiresAt}::timestamp, ${Number(amount)}, ${user.id}::INTEGER, ${noteStr || description},
${comment || null}, ${INV_PENDING_LIMIT}::INTEGER, ${BALANCE_LIMIT_MSATS})`)

// delete k1 after it's been used successfully
k1Cache.delete(k1)

return res.status(200).json({
pr: invoice.request,
routes: []
Expand Down

0 comments on commit 18d36a5

Please sign in to comment.