arcbtc / LNURLPoS

Offline lightning PoS
GNU General Public License v3.0
205 stars 34 forks source link

implement better xor_encrypt, update uBitcoin #16

Closed stepansnigirev closed 2 years ago

stepansnigirev commented 2 years ago

Overview

In this PR I suggest a few changes in the encoding format of the lnurl data:

Other changes:

Encoding format

Suggested data encoding has the following format:

Keys

Keys are derived from a shared secret. There are two keys - for encryption and for authentication. Round secret for encryption is calculated as hmac(key, "Round secret:" | nonce), HMAC at the end is calculated as hmac(key, "Data:" | payload).

Payload

Payload is a simple XOR of the round key with actual data contains the following items:

Python decoding implementation

Resulting LNURL can be decoded with the following python script (using embit library here, but can be easily adopted to any other bitcoin library):

from embit import bech32
from embit import compact
import base64
from io import BytesIO
import hmac

def bech32_decode(bech):
    """tweaked version of bech32_decode that ignores length limitations"""
    if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
            (bech.lower() != bech and bech.upper() != bech)):
        return
    bech = bech.lower()
    pos = bech.rfind('1')
    if pos < 1 or pos + 7 > len(bech):
        return
    if not all(x in bech32.CHARSET for x in bech[pos+1:]):
        return
    hrp = bech[:pos]
    data = [bech32.CHARSET.find(x) for x in bech[pos+1:]]
    encoding = bech32.bech32_verify_checksum(hrp, data)
    if encoding is None:
        return
    return bytes(bech32.convertbits(data[:-6], 5, 8, False))

USD_CENTS = b'$'

def xor_decrypt(key, blob):
    s = BytesIO(blob)
    variant = s.read(1)[0]
    if variant != 1:
        raise RuntimeError("Not implemented")
    # reading nonce
    l = s.read(1)[0]
    nonce = s.read(l)
    if len(nonce) != l:
        raise RuntimeError("Missing nonce bytes")
    if l < 8:
        raise RuntimeError("Nonce is too short")
    # reading payload
    l = s.read(1)[0]
    payload = s.read(l)
    if len(payload) > 32:
        raise RuntimeError("Payload is too long for this encryption method")
    if len(payload) != l:
        raise RuntimeError("Missing payload bytes")
    hmacval = s.read()
    expected = hmac.new(key, b"Data:" + blob[:-len(hmacval)], digestmod="sha256").digest()
    if len(hmacval) < 8:
        raise RuntimeError("HMAC is too short")
    if hmacval != expected[:len(hmacval)]:
        raise RuntimeError("HMAC is invalid")
    secret = hmac.new(key, b"Round secret:" + nonce, digestmod="sha256").digest()
    payload = bytearray(payload)
    for i in range(len(payload)):
        payload[i] = payload[i] ^ secret[i]
    s = BytesIO(payload)
    pin = compact.read_from(s)
    # currency
    currency = s.read(1)
    if currency != USD_CENTS:
        raise RuntimeError("Unsupported currency: %s" % currency)
    amount_in_cent = compact.read_from(s)
    if s.read():
        raise RuntimeError("Unexpected data")
    return pin, amount_in_cent

def extract_pin_and_amount(key, lnurl):
    # get normal url from lnurl
    url = bech32_decode(lnurl).decode()
    # get payload part
    payload = url.split("?p=")[1]
    # add padding
    if len(payload) % 4 > 0:
        payload += "="*(4-(len(payload)%4))
    # decode from urlsafe
    data = base64.urlsafe_b64decode(payload)
    pin, amount_in_cent = xor_decrypt(key, data)
    return pin, amount_in_cent/100

if __name__ == "__main__":
    # shared key
    key = b"Enrt4QzajadmSu6hbwTxFz"

    # two example LNURLs
    lnurlarr = [
        "LNURL1DP68GURN8GHJ7MR9VAJKUEPWD3HXY6T5WVHXXMMD9AKXUATJD3CX7UE0V9CXJTMKXGHKCMN4WFKZ7JJCF4595EPCD9G5V46K895KU4RNVGM8VJMR8ACR6S23VA58QMR0VER4Q335F3G4VCTCDFQKUS242C6XXCN88PF9WE66PAN3K5",
        "LNURL1DP68GURN8GHJ7MR9VAJKUEPWD3HXY6T5WVHXXMMD9AKXUATJD3CX7UE0V9CXJTMKXGHKCMN4WFKZ7JJCF4595EPCD9G5V46K895KU4RNVGM8VJMR8ACR6S23D999YNN4FAV5KJJ8WFQK2D332F2NVTTND4RY7425DUC97KRSX5MKKA89JNW",
    ]
    for lnurl in lnurlarr:
        pin, amount_in_usd = extract_pin_and_amount(key, lnurl)
        print(f"Pin: {pin}, amount: ${amount_in_usd}")
arcbtc commented 2 years ago

Merged, but took out currency byte on both encrypt and decrypt, as currency is stored server side in the pos record