From 4fab2d31013dca9eb549e4cfb8e8e93320d1c24f Mon Sep 17 00:00:00 2001 From: iWarpBTC Date: Mon, 13 Jun 2022 21:08:06 +0200 Subject: [PATCH] new extension just proof of concept --- lnbits/extensions/boltcards/README.md | 11 + lnbits/extensions/boltcards/__init__.py | 19 + lnbits/extensions/boltcards/config.json | 6 + lnbits/extensions/boltcards/crud.py | 91 +++++ lnbits/extensions/boltcards/migrations.py | 20 + lnbits/extensions/boltcards/models.py | 21 + lnbits/extensions/boltcards/nxp424.py | 31 ++ .../templates/boltcards/_api_docs.html | 27 ++ .../boltcards/templates/boltcards/index.html | 375 ++++++++++++++++++ lnbits/extensions/boltcards/views.py | 18 + lnbits/extensions/boltcards/views_api.py | 161 ++++++++ 11 files changed, 780 insertions(+) create mode 100644 lnbits/extensions/boltcards/README.md create mode 100644 lnbits/extensions/boltcards/__init__.py create mode 100644 lnbits/extensions/boltcards/config.json create mode 100644 lnbits/extensions/boltcards/crud.py create mode 100644 lnbits/extensions/boltcards/migrations.py create mode 100644 lnbits/extensions/boltcards/models.py create mode 100644 lnbits/extensions/boltcards/nxp424.py create mode 100644 lnbits/extensions/boltcards/templates/boltcards/_api_docs.html create mode 100644 lnbits/extensions/boltcards/templates/boltcards/index.html create mode 100644 lnbits/extensions/boltcards/views.py create mode 100644 lnbits/extensions/boltcards/views_api.py diff --git a/lnbits/extensions/boltcards/README.md b/lnbits/extensions/boltcards/README.md new file mode 100644 index 000000000..7b9bd7218 --- /dev/null +++ b/lnbits/extensions/boltcards/README.md @@ -0,0 +1,11 @@ +

boltcards Extension

+

*tagline*

+This is an boltcards extension to help you organise and build you own. + +Try to include an image + + + +

If your extension has API endpoints, include useful ones here

+ +curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/boltcards -d '{"amount":"100","memo":"boltcards"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY" diff --git a/lnbits/extensions/boltcards/__init__.py b/lnbits/extensions/boltcards/__init__.py new file mode 100644 index 000000000..69326708f --- /dev/null +++ b/lnbits/extensions/boltcards/__init__.py @@ -0,0 +1,19 @@ +from fastapi import APIRouter + +from lnbits.db import Database +from lnbits.helpers import template_renderer + +db = Database("ext_boltcards") + +boltcards_ext: APIRouter = APIRouter( + prefix="/boltcards", + tags=["boltcards"] +) + + +def boltcards_renderer(): + return template_renderer(["lnbits/extensions/boltcards/templates"]) + + +from .views import * # noqa +from .views_api import * # noqa diff --git a/lnbits/extensions/boltcards/config.json b/lnbits/extensions/boltcards/config.json new file mode 100644 index 000000000..ef98a35ad --- /dev/null +++ b/lnbits/extensions/boltcards/config.json @@ -0,0 +1,6 @@ +{ + "name": "Bolt Cards", + "short_description": "Self custody Bolt Cards with one time LNURLw", + "icon": "payment", + "contributors": ["iwarpbtc"] +} diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py new file mode 100644 index 000000000..e8fb5477a --- /dev/null +++ b/lnbits/extensions/boltcards/crud.py @@ -0,0 +1,91 @@ +from optparse import Option +from typing import List, Optional, Union +from lnbits.helpers import urlsafe_short_hash + +from . import db +from .models import Card, CreateCardData + +async def create_card( + data: CreateCardData, wallet_id: str +) -> Card: + card_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO boltcards.cards ( + id, + wallet, + card_name, + uid, + counter, + withdraw, + file_key, + meta_key + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + card_id, + wallet_id, + data.name, + data.uid, + data.counter, + data.withdraw, + data.file_key, + data.meta_key, + ), + ) + link = await get_card(card_id, 0) + assert link, "Newly created card couldn't be retrieved" + return link + +async def update_card(card_id: str, **kwargs) -> Optional[Card]: + if "is_unique" in kwargs: + kwargs["is_unique"] = int(kwargs["is_unique"]) + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE boltcards.cards SET {q} WHERE id = ?", + (*kwargs.values(), card_id), + ) + row = await db.fetchone( + "SELECT * FROM boltcards.cards WHERE id = ?", (card_id,) + ) + return Card(**row) if row else None + +async def get_cards(wallet_ids: Union[str, List[str]]) -> List[Card]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall( + f"SELECT * FROM boltcards.cards WHERE wallet IN ({q})", (*wallet_ids,) + ) + + return [Card(**row) for row in rows] + +async def get_all_cards() -> List[Card]: + rows = await db.fetchall( + f"SELECT * FROM boltcards.cards" + ) + + return [Card(**row) for row in rows] + +async def get_card(card_id: str, id_is_uid: bool=False) -> Optional[Card]: + sql = "SELECT * FROM boltcards.cards WHERE {} = ?".format("uid" if id_is_uid else "id") + row = await db.fetchone( + sql, card_id, + ) + if not row: + return None + + card = dict(**row) + + return Card.parse_obj(card) + +async def delete_card(card_id: str) -> None: + await db.execute("DELETE FROM boltcards.cards WHERE id = ?", (card_id,)) + +async def update_card_counter(counter: int, id: str): + await db.execute( + "UPDATE boltcards.cards SET counter = ? WHERE id = ?", + (counter, id), + ) \ No newline at end of file diff --git a/lnbits/extensions/boltcards/migrations.py b/lnbits/extensions/boltcards/migrations.py new file mode 100644 index 000000000..eedbb5d34 --- /dev/null +++ b/lnbits/extensions/boltcards/migrations.py @@ -0,0 +1,20 @@ +from lnbits.helpers import urlsafe_short_hash + +async def m001_initial(db): + await db.execute( + """ + CREATE TABLE boltcards.cards ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + card_name TEXT NOT NULL, + uid TEXT NOT NULL, + counter INT NOT NULL DEFAULT 0, + withdraw TEXT NOT NULL, + file_key TEXT NOT NULL DEFAULT '00000000000000000000000000000000', + meta_key TEXT NOT NULL DEFAULT '', + time TIMESTAMP NOT NULL DEFAULT """ + + db.timestamp_now + + """ + ); + """ + ) diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py new file mode 100644 index 000000000..6ef25d0c2 --- /dev/null +++ b/lnbits/extensions/boltcards/models.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel +from fastapi.params import Query + +class Card(BaseModel): + id: str + wallet: str + card_name: str + uid: str + counter: int + withdraw: str + file_key: str + meta_key: str + time: int + +class CreateCardData(BaseModel): + card_name: str = Query(...) + uid: str = Query(...) + counter: str = Query(...) + withdraw: str = Query(...) + file_key: str = Query(...) + meta_key: str = Query(...) \ No newline at end of file diff --git a/lnbits/extensions/boltcards/nxp424.py b/lnbits/extensions/boltcards/nxp424.py new file mode 100644 index 000000000..a67b896f5 --- /dev/null +++ b/lnbits/extensions/boltcards/nxp424.py @@ -0,0 +1,31 @@ +from typing import Tuple +from Cryptodome.Hash import CMAC +from Cryptodome.Cipher import AES + +SV2 = "3CC300010080" + +def myCMAC(key: bytes, msg: bytes=b'') -> bytes: + cobj = CMAC.new(key, ciphermod=AES) + if msg != b'': + cobj.update(msg) + return cobj.digest() + +def decryptSUN(sun: bytes, key: bytes) -> Tuple[bytes, bytes]: + IVbytes = b"\x00" * 16 + + cipher = AES.new(key, AES.MODE_CBC, IVbytes) + sun_plain = cipher.decrypt(sun) + + UID = sun_plain[1:8] + counter = sun_plain[8:11] + + return UID, counter + +def getSunMAC(UID: bytes, counter: bytes, key: bytes) -> bytes: + sv2prefix = bytes.fromhex(SV2) + sv2bytes = sv2prefix + UID + counter + + mac1 = myCMAC(key, sv2bytes) + mac2 = myCMAC(mac1) + + return mac2[1::2] diff --git a/lnbits/extensions/boltcards/templates/boltcards/_api_docs.html b/lnbits/extensions/boltcards/templates/boltcards/_api_docs.html new file mode 100644 index 000000000..f49392558 --- /dev/null +++ b/lnbits/extensions/boltcards/templates/boltcards/_api_docs.html @@ -0,0 +1,27 @@ + + + +
+ Be your own card association +
+

+ Manage your Bolt Cards self custodian way
+ + More details +
+ + Created by, + iWarp +

+
+
+
diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html new file mode 100644 index 000000000..4910cb66f --- /dev/null +++ b/lnbits/extensions/boltcards/templates/boltcards/index.html @@ -0,0 +1,375 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} + +
+
+ + + Add Card + + + + +
+
+
Cards
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+
+
+ + +
+ {{SITE_TITLE}} Bolt Cards extension +
+
+ + + {% include "boltcards/_api_docs.html" %} + +
+
+ + + + + + + + The domain to use ex: "example.com" + + + + Create a "Edit zone DNS" API token in cloudflare + + + How much to charge per day +
+ Update Form + Create Card + Cancel +
+
+
+
+
+ + +{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/extensions/boltcards/views.py b/lnbits/extensions/boltcards/views.py new file mode 100644 index 000000000..8fcbb7def --- /dev/null +++ b/lnbits/extensions/boltcards/views.py @@ -0,0 +1,18 @@ +from fastapi import FastAPI, Request +from fastapi.params import Depends +from fastapi.templating import Jinja2Templates +from starlette.responses import HTMLResponse + +from lnbits.core.models import User +from lnbits.decorators import check_user_exists + +from . import boltcards_ext, boltcards_renderer + +templates = Jinja2Templates(directory="templates") + + +@boltcards_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return boltcards_renderer().TemplateResponse( + "boltcards/index.html", {"request": request, "user": user.dict()} + ) diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py new file mode 100644 index 000000000..0acfb6858 --- /dev/null +++ b/lnbits/extensions/boltcards/views_api.py @@ -0,0 +1,161 @@ +# views_api.py is for you API endpoints that could be hit by another service + +# add your dependencies here + +# import httpx +# (use httpx just like requests, except instead of response.ok there's only the +# response.is_error that is its inverse) + +from http import HTTPStatus + +from fastapi.params import Depends, Query +from starlette.exceptions import HTTPException +from starlette.requests import Request + +from lnbits.core.crud import get_user +from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key +from lnbits.extensions.withdraw import get_withdraw_link + +from . import boltcards_ext +from .nxp424 import decryptSUN, getSunMAC +from .crud import ( + get_all_cards, + get_cards, + get_card, + create_card, + update_card, + delete_card, + update_card_counter +) +from .models import CreateCardData + +@boltcards_ext.get("/api/v1/cards") +async def api_cards( + g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False) +): + wallet_ids = [g.wallet.id] + + if all_wallets: + wallet_ids = (await get_user(g.wallet.user)).wallet_ids + + return [card.dict() for card in await get_cards(wallet_ids)] + +@boltcards_ext.post("/api/v1/cards", status_code=HTTPStatus.CREATED) +@boltcards_ext.put("/api/v1/cards/{card_id}", status_code=HTTPStatus.OK) +async def api_link_create_or_update( + req: Request, + data: CreateCardData, + card_id: str = None, + wallet: WalletTypeInfo = Depends(require_admin_key), +): + ''' + if data.uses > 250: + raise HTTPException( + detail="250 uses max.", status_code=HTTPStatus.BAD_REQUEST + ) + + if data.min_withdrawable < 1: + raise HTTPException( + detail="Min must be more than 1.", status_code=HTTPStatus.BAD_REQUEST + ) + + if data.max_withdrawable < data.min_withdrawable: + raise HTTPException( + detail="`max_withdrawable` needs to be at least `min_withdrawable`.", + status_code=HTTPStatus.BAD_REQUEST, + ) + ''' + if card_id: + card = await get_card(card_id) + if not card: + raise HTTPException( + detail="Card does not exist.", status_code=HTTPStatus.NOT_FOUND + ) + if card.wallet != wallet.wallet.id: + raise HTTPException( + detail="Not your card.", status_code=HTTPStatus.FORBIDDEN + ) + card = await update_card( + card_id, **data.dict() + ) + else: + card = await create_card( + wallet_id=wallet.wallet.id, data=data + ) + return card.dict() + +@boltcards_ext.delete("/api/v1/cards/{card_id}") +async def api_link_delete(card_id, wallet: WalletTypeInfo = Depends(require_admin_key)): + card = await get_card(card_id) + + if not card: + raise HTTPException( + detail="Card does not exist.", status_code=HTTPStatus.NOT_FOUND + ) + + if card.wallet != wallet.wallet.id: + raise HTTPException( + detail="Not your card.", status_code=HTTPStatus.FORBIDDEN + ) + + await delete_card(card_id) + raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + +@boltcards_ext.get("/api/v1/scan/") # pay.btcslovnik.cz/boltcards/api/v1/scan/?uid=00000000000000&ctr=000000&c=0000000000000000 +async def api_scan( + uid, ctr, c, + request: Request +): + card = await get_card(uid, id_is_uid=True) + + if card == None: + return {"status": "ERROR", "reason": "Unknown card."} + + if c != getSunMAC(bytes.fromhex(uid), bytes.fromhex(ctr)[::-1], bytes.fromhex(card.file_key)).hex().upper(): + print(c) + print(getSunMAC(bytes.fromhex(uid), bytes.fromhex(ctr)[::-1], bytes.fromhex(card.file_key)).hex().upper()) + return {"status": "ERROR", "reason": "CMAC does not check."} + + ctr_int = int(ctr, 16) + + if ctr_int <= card.counter: + return {"status": "ERROR", "reason": "This link is already used."} + + await update_card_counter(ctr_int, card.id) + + link = await get_withdraw_link(card.withdraw, 0) + + return link.lnurl_response(request) + +@boltcards_ext.get("/api/v1/scane/") +async def api_scane( + e, c, + request: Request +): + card = None + counter = b'' + + for cand in await get_all_cards(): + if cand.meta_key: + card_uid, counter = decryptSUN(bytes.fromhex(e), bytes.fromhex(cand.meta_key)) + + if card_uid.hex().upper() == cand.uid: + card = cand + break + + if card == None: + return {"status": "ERROR", "reason": "Unknown card."} + + if c != getSunMAC(card_uid, counter, bytes.fromhex(card.file_key)).hex().upper(): + print(c) + print(getSunMAC(card_uid, counter, bytes.fromhex(card.file_key)).hex().upper()) + return {"status": "ERROR", "reason": "CMAC does not check."} + + counter_int = int.from_bytes(counter, "little") + if counter_int <= card.counter: + return {"status": "ERROR", "reason": "This link is already used."} + + await update_card_counter(counter_int, card.id) + + link = await get_withdraw_link(card.withdraw, 0) + return link.lnurl_response(request)