From f02be5bc9de55d26320cc73a89f4f4247e0b8ae8 Mon Sep 17 00:00:00 2001 From: Jack Mallers Date: Sun, 1 Oct 2017 15:28:58 -0500 Subject: [PATCH] feature(bech32): use bech32 to determine if ln pareq is valid --- app/reducers/payform.js | 12 +++- app/utils/bech32.js | 148 ++++++++++++++++++++++++++++++++++++++++ app/utils/index.js | 4 +- 3 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 app/utils/bech32.js diff --git a/app/reducers/payform.js b/app/reducers/payform.js index 3ff64757..f1d1adad 100644 --- a/app/reducers/payform.js +++ b/app/reducers/payform.js @@ -1,7 +1,7 @@ import { createSelector } from 'reselect' import bitcoin from 'bitcoinjs-lib' import { tickerSelectors } from './ticker' -import { btc } from '../utils' +import { btc, bech32 } from '../utils' // Initial State const initialState = { @@ -94,11 +94,17 @@ payFormSelectors.isOnchain = createSelector( } ) -// TODO: Add more robust logic to detect a LN payment request payFormSelectors.isLn = createSelector( payInputSelector, input => { - return input.startsWith('ln') + if (!input.startWith('ln')) { return } + + try { + bech32.decode(input) + return true + } catch (e) { + return false + } } ) diff --git a/app/utils/bech32.js b/app/utils/bech32.js new file mode 100644 index 00000000..ff9f2de7 --- /dev/null +++ b/app/utils/bech32.js @@ -0,0 +1,148 @@ +// Using bech32 here just without the 90 char length: https://github.com/bitcoinjs/bech32/blob/master/index.js + +'use strict' +let ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' + +// pre-compute lookup table +let ALPHABET_MAP = {} +for (let z = 0; z < ALPHABET.length; z++) { + let x = ALPHABET.charAt(z) + + if (ALPHABET_MAP[x] !== undefined) throw new TypeError(x + ' is ambiguous') + ALPHABET_MAP[x] = z +} + +function polymodStep (pre) { + let b = pre >> 25 + return ((pre & 0x1FFFFFF) << 5) ^ + (-((b >> 0) & 1) & 0x3b6a57b2) ^ + (-((b >> 1) & 1) & 0x26508e6d) ^ + (-((b >> 2) & 1) & 0x1ea119fa) ^ + (-((b >> 3) & 1) & 0x3d4233dd) ^ + (-((b >> 4) & 1) & 0x2a1462b3) +} + +function prefixChk (prefix) { + let chk = 1 + for (let i = 0; i < prefix.length; ++i) { + let c = prefix.charCodeAt(i) + if (c < 33 || c > 126) throw new Error('Invalid prefix (' + prefix + ')') + + chk = polymodStep(chk) ^ (c >> 5) + } + chk = polymodStep(chk) + + for (let i = 0; i < prefix.length; ++i) { + let v = prefix.charCodeAt(i) + chk = polymodStep(chk) ^ (v & 0x1f) + } + return chk +} + +function encode (prefix, words) { + // too long? + if ((prefix.length + 7 + words.length) > 90) throw new TypeError('Exceeds Bech32 maximum length') + prefix = prefix.toLowerCase() + + // determine chk mod + let chk = prefixChk(prefix) + let result = prefix + '1' + for (let i = 0; i < words.length; ++i) { + let x = words[i] + if ((x >> 5) !== 0) throw new Error('Non 5-bit word') + + chk = polymodStep(chk) ^ x + result += ALPHABET.charAt(x) + } + + for (let i = 0; i < 6; ++i) { + chk = polymodStep(chk) + } + chk ^= 1 + + for (let i = 0; i < 6; ++i) { + let v = (chk >> ((5 - i) * 5)) & 0x1f + result += ALPHABET.charAt(v) + } + + return result +} + +function decode (str) { + if (str.length < 8) throw new TypeError(str + ' too short') + // LN payment requests can be longer than 90 chars + // if (str.length > 90) throw new TypeError(str + ' too long') + + // don't allow mixed case + let lowered = str.toLowerCase() + let uppered = str.toUpperCase() + if (str !== lowered && str !== uppered) throw new Error('Mixed-case string ' + str) + str = lowered + + let split = str.lastIndexOf('1') + if (split === 0) throw new Error('Missing prefix for ' + str) + + let prefix = str.slice(0, split) + let wordChars = str.slice(split + 1) + if (wordChars.length < 6) throw new Error('Data too short') + + let chk = prefixChk(prefix) + let words = [] + for (let i = 0; i < wordChars.length; ++i) { + let c = wordChars.charAt(i) + let v = ALPHABET_MAP[c] + if (v === undefined) throw new Error('Unknown character ' + c) + chk = polymodStep(chk) ^ v + + // not in the checksum? + if (i + 6 >= wordChars.length) continue + words.push(v) + } + + if (chk !== 1) throw new Error('Invalid checksum for ' + str) + return { prefix, words } +} + +function convert (data, inBits, outBits, pad) { + let value = 0 + let bits = 0 + let maxV = (1 << outBits) - 1 + + let result = [] + for (let i = 0; i < data.length; ++i) { + value = (value << inBits) | data[i] + bits += inBits + + while (bits >= outBits) { + bits -= outBits + result.push((value >> bits) & maxV) + } + } + + if (pad) { + if (bits > 0) { + result.push((value << (outBits - bits)) & maxV) + } + } else { + if (bits >= inBits) throw new Error('Excess padding') + if ((value << (outBits - bits)) & maxV) throw new Error('Non-zero padding') + } + + return result +} + +function toWords (bytes) { + return convert(bytes, 8, 5, true) +} + +function fromWords (words) { + return convert(words, 5, 8, false) +} + +export default { + decode, + encode, + toWords, + fromWords +} + diff --git a/app/utils/index.js b/app/utils/index.js index 44213478..3d162c0f 100644 --- a/app/utils/index.js +++ b/app/utils/index.js @@ -1,7 +1,9 @@ import btc from './btc' import usd from './usd' +import bech32 from './bech32' export default { btc, - usd + usd, + bech32 }