diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 9e3d025f..e7d97493 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -6,12 +6,11 @@ import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor' import lnpr from 'bolt11' import { SELECT } from './item' import { lnAddrOptions } from '../../lib/lnurl' -import { msatsToSats, msatsToSatsDecimal } from '../../lib/format' +import { msatsToSats, msatsToSatsDecimal, ensureB64 } from '../../lib/format' import { LNDAutowithdrawSchema, amountSchema, lnAddrAutowithdrawSchema, lnAddrSchema, ssValidate, withdrawlSchema } from '../../lib/validate' import { ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, ANON_USER_ID, BALANCE_LIMIT_MSATS, INVOICE_RETENTION_DAYS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT } from '../../lib/constants' import { datePivot } from '../../lib/time' import assertGofacYourself from './ofac' -import { HEX_REGEX } from '../../lib/macaroon' export async function getInvoice (parent, { id }, { me, models, lnd }) { const inv = await models.invoice.findUnique({ @@ -408,13 +407,9 @@ export default { return { id } }, upsertWalletLND: async (parent, { settings, ...data }, { me, models }) => { - // store hex inputs as base64 - if (HEX_REGEX.test(data.macaroon)) { - data.macaroon = Buffer.from(data.macaroon, 'hex').toString('base64') - } - if (HEX_REGEX.test(data.cert)) { - data.cert = Buffer.from(data.cert, 'hex').toString('base64') - } + // make sure inputs are base64 + data.macaroon = ensureB64(data.macaroon) + data.cert = ensureB64(data.cert) return await upsertWallet( { diff --git a/lib/format.js b/lib/format.js index b165758b..80678343 100644 --- a/lib/format.js +++ b/lib/format.js @@ -53,3 +53,42 @@ export const msatsToSatsDecimal = msats => { } return fixedDecimal(Number(msats) / 1000.0, 3) } + +export const hexToB64 = hexstring => { + return btoa(hexstring.match(/\w{2}/g).map(function (a) { + return String.fromCharCode(parseInt(a, 16)) + }).join('')) +} + +// some base64 encoders get fancy and remove padding +export const ensureB64Padding = str => { + return str + Array((4 - str.length % 4) % 4 + 1).join('=') +} + +export const B64_REGEX = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/ +export const B64_URL_REGEX = /^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}[.=]{2}|[A-Za-z0-9_-]{3}[.=])?$/ +export const HEX_REGEX = /^[0-9a-fA-F]+$/ + +export const ensureB64 = hexOrB64Url => { + if (HEX_REGEX.test(hexOrB64Url)) { + return hexToB64(hexOrB64Url) + } + + hexOrB64Url = ensureB64Padding(hexOrB64Url) + + // some folks use url-safe base64 + if (B64_URL_REGEX.test(hexOrB64Url)) { + // Convert from URL-safe base64 to regular base64 + hexOrB64Url = hexOrB64Url.replace(/-/g, '+').replace(/_/g, '/').replace(/\./g, '=') + switch (hexOrB64Url.length % 4) { + case 2: hexOrB64Url += '=='; break + case 3: hexOrB64Url += '='; break + } + } + + if (B64_REGEX.test(hexOrB64Url)) { + return hexOrB64Url + } + + throw new Error('not a valid hex or base64 url or base64 encoded string') +} diff --git a/lib/macaroon.js b/lib/macaroon.js index ab2798f3..d0137b52 100644 --- a/lib/macaroon.js +++ b/lib/macaroon.js @@ -2,20 +2,11 @@ import { importMacaroon, base64ToBytes } from 'macaroon' import { MacaroonId } from './macaroon-id' import isEqual from 'lodash/isEqual' import isEqualWith from 'lodash/isEqualWith' - -export const B64_REGEX = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/ -export const HEX_REGEX = /^[0-9a-fA-F]+$/ +import { ensureB64 } from './format' function decodeMacaroon (macaroon) { - if (HEX_REGEX.test(macaroon)) { - return importMacaroon(Buffer.from(macaroon, 'hex')) - } - - if (B64_REGEX.test(macaroon)) { - return importMacaroon(Buffer.from(macaroon, 'base64')) - } - - throw new Error('invalid macaroon encoding') + macaroon = ensureB64(macaroon) + return importMacaroon(Buffer.from(macaroon, 'base64')) } function macaroonOPs (macaroon) { diff --git a/lib/validate.js b/lib/validate.js index c2afc5f7..a49098d0 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -6,10 +6,10 @@ import { } from './constants' import { SUPPORTED_CURRENCIES } from './currency' import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, NOSTR_PUBKEY_HEX } from './nostr' -import { msatsToSats, numWithUnits, abbrNum } from './format' +import { msatsToSats, numWithUnits, abbrNum, ensureB64 } from './format' import * as usersFragments from '../fragments/users' import * as subsFragments from '../fragments/subs' -import { B64_REGEX, HEX_REGEX, isInvoicableMacaroon, isInvoiceMacaroon } from './macaroon' +import { isInvoicableMacaroon, isInvoiceMacaroon } from './macaroon' import { parseNwcUrl } from './url' const { SUB } = subsFragments @@ -154,10 +154,18 @@ const lightningAddressValidator = process.env.NODE_ENV === 'development' 'address is no good') : string().email('address is no good') -const hexOrBase64Validator = string().or([ - string().matches(HEX_REGEX), - string().matches(B64_REGEX) -], 'invalid encoding') +const hexOrBase64Validator = string().test({ + name: 'hex-or-base64', + message: 'invalid encoding', + test: (val) => { + try { + ensureB64(val) + return true + } catch { + return false + } + } +}) async function usernameExists (name, { client, models }) { if (!client && !models) { @@ -313,7 +321,7 @@ export function LNDAutowithdrawSchema ({ me } = {}) { macaroon: hexOrBase64Validator.required('required').test({ name: 'macaroon', test: v => isInvoiceMacaroon(v) || isInvoicableMacaroon(v), - message: 'not an invoice macaroon' + message: 'not an invoice macaroon or an invoicable macaroon' }), cert: hexOrBase64Validator, ...autowithdrawSchemaMembers({ me })