From c87abef20fe7aeba8a29f4180eae94c92ff1cc14 Mon Sep 17 00:00:00 2001 From: benarc Date: Tue, 21 Dec 2021 15:33:52 +0000 Subject: [PATCH 1/7] Added Stepans decode functions --- lnbits/extensions/lnurlpos/lnurl.py | 139 +++++++++++++++------------- 1 file changed, 73 insertions(+), 66 deletions(-) diff --git a/lnbits/extensions/lnurlpos/lnurl.py b/lnbits/extensions/lnurlpos/lnurl.py index dccacef0..cf83afd3 100644 --- a/lnbits/extensions/lnurlpos/lnurl.py +++ b/lnbits/extensions/lnurlpos/lnurl.py @@ -3,6 +3,12 @@ import hashlib from http import HTTPStatus from typing import Optional +from embit import bech32 +from embit import compact +import base64 +from io import BytesIO +import hmac + from fastapi import Request from fastapi.param_functions import Query from starlette.exceptions import HTTPException @@ -18,39 +24,79 @@ from .crud import ( update_lnurlpospayment, ) +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)) + +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 @lnurlpos_ext.get( - "/api/v1/lnurl/{nonce}/{payload}/{pos_id}", + "/api/v1/lnurl/{pos_id}", status_code=HTTPStatus.OK, - name="lnurlpos.lnurl_response", + name="lnurlpos.lnurl_v1_params", ) -async def lnurl_response( - request: Request, - nonce: str = Query(None), - pos_id: str = Query(None), - payload: str = Query(None), -): - return await handle_lnurl_firstrequest( - request, pos_id, nonce, payload, verify_checksum=False - ) - - -@lnurlpos_ext.get( - "/api/v2/lnurl/{pos_id}", - status_code=HTTPStatus.OK, - name="lnurlpos.lnurl_v2_params", -) -async def lnurl_v2_params( +async def lnurl_v1_params( request: Request, pos_id: str = Query(None), - n: str = Query(None), p: str = Query(None), ): - return await handle_lnurl_firstrequest(request, pos_id, n, p, verify_checksum=True) + return await handle_lnurl_firstrequest(request, pos_id, p) async def handle_lnurl_firstrequest( - request: Request, pos_id: str, nonce: str, payload: str, verify_checksum: bool + request: Request, pos_id: str, payload: str ): pos = await get_lnurlpos(pos_id) if not pos: @@ -59,53 +105,14 @@ async def handle_lnurl_firstrequest( "reason": f"lnurlpos {pos_id} not found on this server", } - try: - nonceb = bytes.fromhex(nonce) - except ValueError: - try: - nonce += "=" * ((4 - len(nonce) % 4) % 4) - nonceb = base64.urlsafe_b64decode(nonce) - except: - return { - "status": "ERROR", - "reason": f"Invalid hex or base64 nonce: {nonce}", - } + if len(payload) % 4 > 0: + payload += "="*(4-(len(payload)%4)) - try: - payloadb = bytes.fromhex(payload) - except ValueError: - try: - payload += "=" * ((4 - len(payload) % 4) % 4) - payloadb = base64.urlsafe_b64decode(payload) - except: - return { - "status": "ERROR", - "reason": f"Invalid hex or base64 payload: {payload}", - } - - # check payload and nonce sizes - if len(payloadb) != 8 or len(nonceb) != 8: - return {"status": "ERROR", "reason": "Expected 8 bytes"} - - # verify hmac - if verify_checksum: - expected = hmac.new( - pos.key.encode(), payloadb[:-2], digestmod="sha256" - ).digest() - if expected[:2] != payloadb[-2:]: - return {"status": "ERROR", "reason": "Invalid HMAC"} - - # decrypt - s = hmac.new(pos.key.encode(), nonceb, digestmod="sha256").digest() - res = bytearray(payloadb) - for i in range(len(res)): - res[i] = res[i] ^ s[i] - - pin = int.from_bytes(res[0:2], "little") - amount = int.from_bytes(res[2:6], "little") + data = base64.urlsafe_b64decode(payload) + pin, amount_in_cent = xor_decrypt(key, data) price_msat = ( - await fiat_amount_as_satoshis(float(amount) / 100, pos.currency) + await fiat_amount_as_satoshis(float(amount_in_cent) / 100, pos.currency) if pos.currency != "sat" else amount ) * 1000 From 0b93811957046f1d65e948757365773eb6c798db Mon Sep 17 00:00:00 2001 From: benarc Date: Tue, 21 Dec 2021 15:38:19 +0000 Subject: [PATCH 2/7] added key --- lnbits/extensions/lnurlpos/lnurl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/lnurlpos/lnurl.py b/lnbits/extensions/lnurlpos/lnurl.py index cf83afd3..8e4d5c89 100644 --- a/lnbits/extensions/lnurlpos/lnurl.py +++ b/lnbits/extensions/lnurlpos/lnurl.py @@ -109,7 +109,7 @@ async def handle_lnurl_firstrequest( payload += "="*(4-(len(payload)%4)) data = base64.urlsafe_b64decode(payload) - pin, amount_in_cent = xor_decrypt(key, data) + pin, amount_in_cent = xor_decrypt(pos.key, data) price_msat = ( await fiat_amount_as_satoshis(float(amount_in_cent) / 100, pos.currency) From b04eea6463634ad6dfd15a1f6c1221f02f53488e Mon Sep 17 00:00:00 2001 From: benarc Date: Tue, 21 Dec 2021 15:47:39 +0000 Subject: [PATCH 3/7] encode key as byte --- lnbits/extensions/lnurlpos/lnurl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/lnurlpos/lnurl.py b/lnbits/extensions/lnurlpos/lnurl.py index 8e4d5c89..a2c8e6a6 100644 --- a/lnbits/extensions/lnurlpos/lnurl.py +++ b/lnbits/extensions/lnurlpos/lnurl.py @@ -109,7 +109,7 @@ async def handle_lnurl_firstrequest( payload += "="*(4-(len(payload)%4)) data = base64.urlsafe_b64decode(payload) - pin, amount_in_cent = xor_decrypt(pos.key, data) + pin, amount_in_cent = xor_decrypt(pos.key.encode(), data) price_msat = ( await fiat_amount_as_satoshis(float(amount_in_cent) / 100, pos.currency) From 1e171e660d272df4389fe00819655d5b0b1e6dec Mon Sep 17 00:00:00 2001 From: benarc Date: Tue, 21 Dec 2021 15:53:40 +0000 Subject: [PATCH 4/7] removed currency check --- lnbits/extensions/lnurlpos/lnurl.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/lnbits/extensions/lnurlpos/lnurl.py b/lnbits/extensions/lnurlpos/lnurl.py index a2c8e6a6..3ea33784 100644 --- a/lnbits/extensions/lnurlpos/lnurl.py +++ b/lnbits/extensions/lnurlpos/lnurl.py @@ -74,9 +74,6 @@ def xor_decrypt(key, blob): 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") From ee4f93d3ba6f5fac5cbcccb6381eb47dc3acfa86 Mon Sep 17 00:00:00 2001 From: benarc Date: Tue, 21 Dec 2021 15:55:24 +0000 Subject: [PATCH 5/7] removed s read check --- lnbits/extensions/lnurlpos/lnurl.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lnbits/extensions/lnurlpos/lnurl.py b/lnbits/extensions/lnurlpos/lnurl.py index 3ea33784..610d70f9 100644 --- a/lnbits/extensions/lnurlpos/lnurl.py +++ b/lnbits/extensions/lnurlpos/lnurl.py @@ -75,8 +75,6 @@ def xor_decrypt(key, blob): pin = compact.read_from(s) # currency amount_in_cent = compact.read_from(s) - if s.read(): - raise RuntimeError("Unexpected data") return pin, amount_in_cent @lnurlpos_ext.get( From d517fe03303e425a7396dca37ce707a2087d3bf5 Mon Sep 17 00:00:00 2001 From: benarc Date: Tue, 21 Dec 2021 16:20:53 +0000 Subject: [PATCH 6/7] chnaged req to request --- lnbits/extensions/lnurlpos/lnurl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/lnurlpos/lnurl.py b/lnbits/extensions/lnurlpos/lnurl.py index 610d70f9..eba7f50f 100644 --- a/lnbits/extensions/lnurlpos/lnurl.py +++ b/lnbits/extensions/lnurlpos/lnurl.py @@ -163,7 +163,7 @@ async def lnurl_callback(request: Request, paymentid: str = Query(None)): "successAction": { "tag": "url", "description": "Check the attached link", - "url": req.url_for("lnurlpos.displaypin", paymentid=paymentid), + "url": request.url_for("lnurlpos.displaypin", paymentid=paymentid), }, "routes": [], } From 0bb5d6769e4545423d04a39dfd131ca4ed96cdd7 Mon Sep 17 00:00:00 2001 From: benarc Date: Tue, 21 Dec 2021 17:14:17 +0000 Subject: [PATCH 7/7] removed currency --- lnbits/extensions/lnurlpos/lnurl.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lnbits/extensions/lnurlpos/lnurl.py b/lnbits/extensions/lnurlpos/lnurl.py index eba7f50f..2ca1b0f8 100644 --- a/lnbits/extensions/lnurlpos/lnurl.py +++ b/lnbits/extensions/lnurlpos/lnurl.py @@ -73,7 +73,6 @@ def xor_decrypt(key, blob): payload[i] = payload[i] ^ secret[i] s = BytesIO(payload) pin = compact.read_from(s) - # currency amount_in_cent = compact.read_from(s) return pin, amount_in_cent @@ -105,7 +104,6 @@ async def handle_lnurl_firstrequest( data = base64.urlsafe_b64decode(payload) pin, amount_in_cent = xor_decrypt(pos.key.encode(), data) - price_msat = ( await fiat_amount_as_satoshis(float(amount_in_cent) / 100, pos.currency) if pos.currency != "sat"