Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: atm page, change encryption #5

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 1 addition & 32 deletions crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

async def create_fossa(data: CreateFossa) -> Fossa:
fossa_id = shortuuid.uuid()[:5]
fossa_key = urlsafe_short_hash()
fossa_key = urlsafe_short_hash()[:16]
fossa = Fossa(
id=fossa_id,
key=fossa_key,
Expand Down Expand Up @@ -87,36 +87,5 @@ async def get_fossa_payments(
)


async def get_fossa_payment_by_payhash(
payhash: str,
) -> Optional[FossaPayment]:
return await db.fetchone(
"SELECT * FROM fossa.fossa_payment WHERE payhash = :payhash",
{"payhash": payhash},
FossaPayment,
)


async def get_fossa_payment_by_payload(
payload: str,
) -> Optional[FossaPayment]:
return await db.fetchone(
"SELECT * FROM fossa.fossa_payment WHERE payload = :payload",
{"payload": payload},
FossaPayment,
)


async def get_recent_fossa_payment(payload: str) -> Optional[FossaPayment]:
return await db.fetchone(
"""
SELECT * FROM fossa.fossa_payment
WHERE payload = :payload ORDER BY timestamp DESC LIMIT 1
""",
{"payload": payload},
FossaPayment,
)


async def delete_atm_payment_link(atm_id: str) -> None:
await db.execute("DELETE FROM fossa.fossa_payment WHERE id = :id", {"id": atm_id})
135 changes: 56 additions & 79 deletions helpers.py
Original file line number Diff line number Diff line change
@@ -1,88 +1,65 @@
import base64
import hmac
from io import BytesIO
from typing import Optional
from base64 import b64decode
from urllib.parse import parse_qs, urlparse

from embit import compact
from lnbits.helpers import urlsafe_short_hash
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
from Cryptodome.Cipher import AES
from lnurl import decode as lnurl_decode
from pydantic import BaseModel

from .crud import create_fossa_payment, get_recent_fossa_payment
from .models import Fossa, FossaPayment

class LnurlPayload(BaseModel):
fossa_id: str
iv: str
payload: str

async def register_atm_payment(
fossa: Fossa, payload: str
) -> tuple[Optional[FossaPayment], Optional[int]]:
"""
Register an ATM payment to avoid double pull.
"""
# create a new lnurlpayment record
data = base64.urlsafe_b64decode(payload)
payload = payload.replace("=", "")
decrypted = xor_decrypt(fossa.key.encode(), data)

fossa_payment = await get_recent_fossa_payment(payload)
# If the payment is already registered and been paid, return None
if fossa_payment and fossa_payment.payload == fossa_payment.payment_hash:
return None, fossa_payment.sats * 1000
# If the payment is already registered and not been paid, return lnurlpayment record
if fossa_payment and fossa_payment.payload != fossa_payment.payment_hash:
return fossa_payment, fossa_payment.sats * 1000
class LnurlDecrypted(BaseModel):
pin: int
amount: float

price_msat = (
await fiat_amount_as_satoshis(float(decrypted[1]) / 100, fossa.currency) * 1000
if fossa.currency != "sat"
else decrypted[1] * 1000
)
price_msat = int(price_msat - ((price_msat / 100) * fossa.profit))
sats = int(price_msat / 1000)
fossa_payment = FossaPayment(
id=urlsafe_short_hash(),
fossa_id=fossa.id,
payload=payload,
sats=sats,
pin=int(decrypted[0]),
payment_hash="payment_hash",
)
await create_fossa_payment(fossa_payment)
price_msat = sats * 1000
return fossa_payment, price_msat

def parse_lnurl_payload(lnurl: str) -> LnurlPayload:

# Decode the lightning URL
try:
url = str(lnurl_decode(lnurl))
except Exception as e:
raise ValueError("Unable to decode lnurl.") from e

# Parse the URL to extract device ID and query parameters
parsed_url = urlparse(url)
query_string = parse_qs(parsed_url.query)

p = query_string.get("p", [None])[0]
if p is None:
raise ValueError("Missing 'p' parameter.")

# Extract and validate the 'iv' parameter
iv = query_string.get("iv", [None])[0]
if iv is None:
raise ValueError("Missing 'iv' parameter.")

fossa_id = parsed_url.path.split("/")[-1]

return LnurlPayload(
fossa_id=fossa_id,
iv=iv,
payload=p,
)

def xor_decrypt(key, blob):
s = BytesIO(blob)
variant = s.read(1)[0]
if variant != 1:
raise RuntimeError("Not implemented")
# reading nonce
nonce_len = s.read(1)[0]
nonce = s.read(nonce_len)
if len(nonce) != nonce_len:
raise RuntimeError("Missing nonce bytes")
if nonce_len < 8:
raise RuntimeError("Nonce is too short")

# reading payload
payload_len = s.read(1)[0]
payload = s.read(payload_len)
if len(payload) > 32:
raise RuntimeError("Payload is too long for this encryption method")
if len(payload) != payload_len:
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)
amount_in_cent = compact.read_from(s)
return str(pin), amount_in_cent
def decrypt_payload(key, iv, payload) -> LnurlDecrypted:
_iv = b64decode(iv)
_ct = b64decode(payload)
if len(_ct) % 16 != 0:
raise ValueError("Invalid payload length.")
if len(_iv) != 32:
raise ValueError("Invalid IV length.")
cipher = AES.new(key.encode(), AES.MODE_CBC, _iv)
pt = cipher.decrypt(_ct)
msg = pt.split(b"\x00")[0].decode()
pin, amount = msg.split(":")
if 1000 > int(pin) > 9999:
raise ValueError("Invalid pin")
if float(amount) < 0:
raise ValueError("Invalid amount")
return LnurlDecrypted(pin=int(pin), amount=float(amount))
14 changes: 13 additions & 1 deletion models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
from datetime import datetime, timezone
from typing import Optional

from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
from lnurl.types import LnurlPayMetadata
from pydantic import BaseModel, Field

Expand All @@ -26,11 +28,21 @@ class Fossa(BaseModel):
def lnurlpay_metadata(self) -> LnurlPayMetadata:
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))

async def amount_to_sats(self, amount: float) -> int:
sats = (
int(amount)
if self.currency == "sat"
else await fiat_amount_as_satoshis(float(amount) / 100, self.currency)
)
if self.profit <= 0:
return sats
return int(sats - ((sats / 100) * self.profit))


class FossaPayment(BaseModel):
id: str
fossa_id: str
payment_hash: str
payment_hash: Optional[str] = None
payload: str
pin: int
sats: int
Expand Down
6 changes: 2 additions & 4 deletions static/js/atm.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@ window.app = Vue.createApp({
boltz: boltz,
amount_sat: amount_sat,
used: used,
p: p,
tab: 'lnurl',
ln: '',
address: '',
onchain_liquid: 'BTC/BTC',
recentpay: recentpay,
payment_options: ['lnurl', 'ln', 'onchain', 'liquid']
}
},
Expand All @@ -23,7 +21,7 @@ window.app = Vue.createApp({
try {
const response = await LNbits.api.request(
'GET',
`/fossa/api/v1/ln/${this.fossa_id}/${this.p}/${this.ln}`,
`/fossa/api/v1/ln/${lnurl}/${this.ln}`,
''
)
if (response.data) {
Expand All @@ -48,7 +46,7 @@ window.app = Vue.createApp({
try {
const response = await LNbits.api.request(
'GET',
`/fossa/api/v1/boltz/${this.fossa_id}/${this.p}/${this.onchain_liquid}/${this.address}`,
`/fossa/api/v1/boltz/${lnurl}/${this.onchain_liquid}/${this.address}`,
''
)
if (response.data) {
Expand Down
15 changes: 2 additions & 13 deletions static/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,19 +254,8 @@ window.app = Vue.createApp({
exportAtmCSV() {
LNbits.utils.exportCSV(this.atmTable.columns, this.atmLinks)
},
openAtmLink(fossa_id, p) {
const url = `${this.protocol}//${this.location}/fossa/api/v1/lnurl/${fossa_id}?atm=1&p=${p}`
LNbits.api
.request(
'POST',
'/fossa/api/v1/lnurlencode',
this.g.user.wallets[0].adminkey,
{url: url}
)
.then(response => {
window.open('/fossa/atm?lightning=' + response.data)
})
.catch(LNbits.utils.notifyApiError)
openAtmLink(payload) {
window.open('/fossa/atm?lightning=' + payload)
}
},
created() {
Expand Down
2 changes: 0 additions & 2 deletions templates/fossa/atm.html
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,6 @@ <h3 class="text-h5">
const boltz = '{{ boltz }}' === 'True' ? true : false
const amount_sat = parseInt('{{ amount_sat }}')
const used = '{{ used }}' === 'True' ? true : false
const p = '{{ p }}'
const recentpay = '{{ recentpay }}' === 'True' ? true : false
</script>
<script src="{{ static_url_for('fossa/static', path='js/atm.js') }}"></script>
{% endblock %}
4 changes: 2 additions & 2 deletions templates/fossa/atm_receipt.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<page size="A4" id="pdfprint">
<div class="wrapper" style="padding-top: 30px; text-align: center">
<h3>ATM receipt for: "{{name}}"</h3>
<span>{{ amt }} sats</span>
<span>{{ sats }} sats</span>
<table>
<tr>
<td><b>Payment ID</b></td>
Expand All @@ -14,7 +14,7 @@ <h3>ATM receipt for: "{{name}}"</h3>

<tr>
<td><b>Amount</b></td>
<td>{{sats/1000}} Sats</td>
<td>{{sats}} Sats</td>
</tr>
<tr>
<td><b>Device</b></td>
Expand Down
2 changes: 1 addition & 1 deletion templates/fossa/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ <h5 class="text-subtitle1 q-my-none">ATM Payments</h5>
flat
dense
size="xs"
@click="openAtmLink(props.row.fossa_id, props.row.payload)"
@click="openAtmLink(props.row.payload)"
icon="link"
color="grey"
>
Expand Down
Loading