From 77112092f5823ad896ae2aa9efadd702f8ec7942 Mon Sep 17 00:00:00 2001 From: minibits-cash Date: Tue, 31 Oct 2023 23:35:09 +0100 Subject: [PATCH] Validate that invoice belongs to LNURL pay --- src/screens/TransferScreen.tsx | 30 +++++++++++++--- src/services/lightning/lightningUtils.ts | 45 +++++++++++++----------- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/src/screens/TransferScreen.tsx b/src/screens/TransferScreen.tsx index b4fc9d68..a2c53136 100644 --- a/src/screens/TransferScreen.tsx +++ b/src/screens/TransferScreen.tsx @@ -10,6 +10,7 @@ import { FlatList, TextInput, } from 'react-native' +import QuickCrypto from 'react-native-quick-crypto' import {spacing, useThemeColor, colors, typography} from '../theme' import {WalletStackScreenProps} from '../navigation' import { @@ -268,7 +269,7 @@ const onMintBalanceSelect = function (balance: MintBalance) { setMintBalanceToTransferFrom(balance) // this triggers effect to get estimated fees } - +// Amount is editable only in case of LNURL Pay, while invoice is not yet retrieved const onAmountEndEditing = async function () { try { const amount = parseInt(amountToTransfer) @@ -295,7 +296,27 @@ const onAmountEndEditing = async function () { setIsLoading(true) const encoded = await LnurlClient.getInvoice(lnurlPayParams, amount * 1000) - // TODO validate h + const invoice = LightningUtils.decodeInvoice(encoded) + const {description_hash} = LightningUtils.getInvoiceData(invoice) + + if(!description_hash || description_hash.length === 0) { + throw new AppError(Err.VALIDATION_ERROR, `Invoice from ${lnurlPayParams.domain} is invalid, missing description_hash`) + } + // check that retrieved invoice matches the previous LNURL pay link + const hashedMetadata = QuickCrypto.createHash('sha256') + .update(lnurlPayParams.metadata) + .digest('hex') + + log.trace('hashedMetadata', hashedMetadata) + log.trace('description_hash', description_hash) + + if(hashedMetadata !== description_hash) { + throw new AppError( + Err.VALIDATION_ERROR, + `Invoice from ${lnurlPayParams.domain} has invalid description_hash ${description_hash}, expected ${hashedMetadata}` + ) + } + setIsLoading(false) if(encoded) { return onEncodedInvoice(encoded) @@ -314,11 +335,12 @@ const onEncodedInvoice = async function (encoded: string, paymentRequestDesc: st navigation.setParams({encodedInvoice: undefined}) navigation.setParams({paymentRequest: undefined}) navigation.setParams({lnurlParams: undefined}) + navigation.setParams({paymentOption: undefined}) setEncodedInvoice(encoded) const invoice = LightningUtils.decodeInvoice(encoded) - const {amount, expiry, description, timestamp} = LightningUtils.getInvoiceData(invoice) + const {amount, expiry, description, description_hash, timestamp} = LightningUtils.getInvoiceData(invoice) // log.trace('Decoded invoice', invoice, 'onEncodedInvoice') log.trace('Invoice data', {amount, expiry, description}, 'onEncodedInvoice') @@ -491,7 +513,7 @@ const satsColor = colors.palette.primary200 LeftComponent={ diff --git a/src/services/lightning/lightningUtils.ts b/src/services/lightning/lightningUtils.ts index 7e03bbac..f4836b62 100644 --- a/src/services/lightning/lightningUtils.ts +++ b/src/services/lightning/lightningUtils.ts @@ -80,30 +80,35 @@ const getInvoiceExpiresAt = function (timestamp: number, expiry: number): Date { const getInvoiceData = function (decoded: DecodedLightningInvoice) { - let result: {amount?: number; description?: string; expiry?: number, payment_hash?: string, timestamp?: number} = {} + let result: {amount?: number; description?: string; expiry?: number, payment_hash?: string, description_hash?: string, timestamp?: number} = {} - for (const item of decoded.sections) { - switch (item.name) { - case 'amount': - result.amount = parseInt(item.value) / 1000 //sats - break - case 'description': - result.description = (item.value as string) || '' - break - case 'payment_hash': - result.payment_hash = (Buffer.from(item.value).toString('hex') as string) || '' - break - case 'timestamp': - result.timestamp = (item.value as number) || Math.floor(Date.now() / 1000) - break + // log.trace('decoded invoice', decoded) + + for (const item of decoded.sections) { + switch (item.name) { + case 'amount': + result.amount = parseInt(item.value) / 1000 //sats + break + case 'description': + result.description = (item.value as string) || '' + break + case 'payment_hash': + result.payment_hash = (Buffer.from(item.value).toString('hex') as string) || '' + break + case 'description_hash': + result.description_hash = (Buffer.from(item.value).toString('hex') as string) || '' + break + case 'timestamp': + result.timestamp = (item.value as number) || Math.floor(Date.now() / 1000) + break + } } - } - result.expiry = decoded.expiry || 600 + result.expiry = decoded.expiry || 600 - log.trace('Invoice data', result, 'getInvoiceData') - - return result + log.trace('Invoice data', result, 'getInvoiceData') + + return result } export const LightningUtils = {