From 4fab2d31013dca9eb549e4cfb8e8e93320d1c24f Mon Sep 17 00:00:00 2001 From: iWarpBTC Date: Mon, 13 Jun 2022 21:08:06 +0200 Subject: [PATCH 01/73] 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) From 3cb62d1899d6a15895d97cc30ec0ac8483649d5b Mon Sep 17 00:00:00 2001 From: iWarpBTC Date: Tue, 21 Jun 2022 18:03:20 +0200 Subject: [PATCH 02/73] recording card tapping --- lnbits/extensions/boltcards/crud.py | 61 +++++++++++++++++-- lnbits/extensions/boltcards/migrations.py | 16 +++++ lnbits/extensions/boltcards/models.py | 20 +++++- .../boltcards/templates/boltcards/index.html | 1 + lnbits/extensions/boltcards/views_api.py | 11 +++- 5 files changed, 102 insertions(+), 7 deletions(-) diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py index e8fb5477a..62aad3560 100644 --- a/lnbits/extensions/boltcards/crud.py +++ b/lnbits/extensions/boltcards/crud.py @@ -3,7 +3,7 @@ from typing import List, Optional, Union from lnbits.helpers import urlsafe_short_hash from . import db -from .models import Card, CreateCardData +from .models import Card, CreateCardData, Hit async def create_card( data: CreateCardData, wallet_id: str @@ -34,9 +34,9 @@ async def create_card( data.meta_key, ), ) - link = await get_card(card_id, 0) - assert link, "Newly created card couldn't be retrieved" - return link + card = await get_card(card_id, 0) + assert card, "Newly created card couldn't be retrieved" + return card async def update_card(card_id: str, **kwargs) -> Optional[Card]: if "is_unique" in kwargs: @@ -88,4 +88,55 @@ 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 + ) + +async def get_hit(hit_id: str) -> Optional[Hit]: + row = await db.fetchone( + f"SELECT * FROM boltcards.hits WHERE id = ?", (hit_id) + ) + if not row: + return None + + hit = dict(**row) + + return Hit.parse_obj(hit) + +async def get_hits(wallet_ids: Union[str, List[str]]) -> List[Hit]: + + cards = get_cards(wallet_ids) + + q = ",".join(["?"] * len(cards)) + rows = await db.fetchall( + f"SELECT * FROM boltcards.hits WHERE wallet IN ({q})", (*(card.card_id for card in cards),) + ) + + return [Card(**row) for row in rows] + +async def create_hit( + card_id, ip, useragent, old_ctr, new_ctr +) -> Hit: + hit_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO boltcards.hits ( + id, + card_id, + ip, + useragent, + old_ctr, + new_ctr + ) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + hit_id, + card_id, + ip, + useragent, + old_ctr, + new_ctr, + ), + ) + hit = await get_hit(hit_id) + assert hit, "Newly recorded hit couldn't be retrieved" + return hit diff --git a/lnbits/extensions/boltcards/migrations.py b/lnbits/extensions/boltcards/migrations.py index eedbb5d34..e7236ce7a 100644 --- a/lnbits/extensions/boltcards/migrations.py +++ b/lnbits/extensions/boltcards/migrations.py @@ -18,3 +18,19 @@ async def m001_initial(db): ); """ ) + + await db.execute( + """ + CREATE TABLE boltcards.hits ( + id TEXT PRIMARY KEY, + card_id TEXT NOT NULL, + ip TEXT NOT NULL, + useragent TEXT, + old_ctr INT NOT NULL DEFAULT 0, + new_ctr INT NOT NULL DEFAULT 0, + time TIMESTAMP NOT NULL DEFAULT """ + + db.timestamp_now + + """ + ); + """ + ) diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py index 6ef25d0c2..75621269f 100644 --- a/lnbits/extensions/boltcards/models.py +++ b/lnbits/extensions/boltcards/models.py @@ -18,4 +18,22 @@ class CreateCardData(BaseModel): counter: str = Query(...) withdraw: str = Query(...) file_key: str = Query(...) - meta_key: str = Query(...) \ No newline at end of file + meta_key: str = Query(...) + +class Hit(BaseModel): + id: str + card_id: str + ip: str + useragent: str + old_ctr: int + new_ctr: int + time: int + +''' +class CreateHitData(BaseModel): + card_id: str = Query(...) + ip: str = Query(...) + useragent: str = Query(...) + old_ctr: int = Query(...) + new_ctr: int = Query(...) +''' diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html index 4910cb66f..a6997a5d1 100644 --- a/lnbits/extensions/boltcards/templates/boltcards/index.html +++ b/lnbits/extensions/boltcards/templates/boltcards/index.html @@ -368,6 +368,7 @@ if (this.g.user.wallets.length) { this.getCards() this.getWithdraws() + this.getHits() } } }) diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py index 0acfb6858..75cfe129e 100644 --- a/lnbits/extensions/boltcards/views_api.py +++ b/lnbits/extensions/boltcards/views_api.py @@ -19,6 +19,7 @@ from lnbits.extensions.withdraw import get_withdraw_link from . import boltcards_ext from .nxp424 import decryptSUN, getSunMAC from .crud import ( + create_hit, get_all_cards, get_cards, get_card, @@ -43,7 +44,7 @@ async def api_cards( @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, +# req: Request, data: CreateCardData, card_id: str = None, wallet: WalletTypeInfo = Depends(require_admin_key), @@ -157,5 +158,13 @@ async def api_scane( await update_card_counter(counter_int, card.id) + ip = request.client.host + if request.headers['x-real-ip']: + ip = request.headers['x-real-ip'] + elif request.headers['x-forwarded-for']: + ip = request.headers['x-forwarded-for'] + + await create_hit(card.id, ip, request.headers['user-agent'], card.counter, counter_int) + link = await get_withdraw_link(card.withdraw, 0) return link.lnurl_response(request) From 2f497ac0eeee7b72ce24f6342f30694f5bebb353 Mon Sep 17 00:00:00 2001 From: iWarpBTC Date: Tue, 21 Jun 2022 22:04:43 +0200 Subject: [PATCH 03/73] retreiving hits --- lnbits/extensions/boltcards/crud.py | 11 +++--- lnbits/extensions/boltcards/models.py | 9 ----- .../boltcards/templates/boltcards/index.html | 1 - lnbits/extensions/boltcards/views_api.py | 36 ++++++++++++++++--- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py index 62aad3560..f34ce6594 100644 --- a/lnbits/extensions/boltcards/crud.py +++ b/lnbits/extensions/boltcards/crud.py @@ -101,16 +101,13 @@ async def get_hit(hit_id: str) -> Optional[Hit]: return Hit.parse_obj(hit) -async def get_hits(wallet_ids: Union[str, List[str]]) -> List[Hit]: - - cards = get_cards(wallet_ids) - - q = ",".join(["?"] * len(cards)) +async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]: + q = ",".join(["?"] * len(cards_ids)) rows = await db.fetchall( - f"SELECT * FROM boltcards.hits WHERE wallet IN ({q})", (*(card.card_id for card in cards),) + f"SELECT * FROM boltcards.hits WHERE card_id IN ({q})", (*cards_ids,) ) - return [Card(**row) for row in rows] + return [Hit(**row) for row in rows] async def create_hit( card_id, ip, useragent, old_ctr, new_ctr diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py index 75621269f..728aa2bb8 100644 --- a/lnbits/extensions/boltcards/models.py +++ b/lnbits/extensions/boltcards/models.py @@ -28,12 +28,3 @@ class Hit(BaseModel): old_ctr: int new_ctr: int time: int - -''' -class CreateHitData(BaseModel): - card_id: str = Query(...) - ip: str = Query(...) - useragent: str = Query(...) - old_ctr: int = Query(...) - new_ctr: int = Query(...) -''' diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html index a6997a5d1..4910cb66f 100644 --- a/lnbits/extensions/boltcards/templates/boltcards/index.html +++ b/lnbits/extensions/boltcards/templates/boltcards/index.html @@ -368,7 +368,6 @@ if (this.g.user.wallets.length) { this.getCards() this.getWithdraws() - this.getHits() } } }) diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py index 75cfe129e..8a8e33a2f 100644 --- a/lnbits/extensions/boltcards/views_api.py +++ b/lnbits/extensions/boltcards/views_api.py @@ -24,6 +24,7 @@ from .crud import ( get_cards, get_card, create_card, + get_hits, update_card, delete_card, update_card_counter @@ -102,6 +103,22 @@ async def api_link_delete(card_id, wallet: WalletTypeInfo = Depends(require_admi await delete_card(card_id) raise HTTPException(status_code=HTTPStatus.NO_CONTENT) +@boltcards_ext.get("/api/v1/hits") +async def api_hits( + 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 + + cards = await get_cards(wallet_ids) + cards_ids = [] + for card in cards: + cards_ids.append(card.id) + + return [hit.dict() for hit in await get_hits(cards_ids)] + @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, @@ -124,8 +141,17 @@ async def api_scan( await update_card_counter(ctr_int, card.id) - link = await get_withdraw_link(card.withdraw, 0) + ip = request.client.host + if request.headers['x-real-ip']: + ip = request.headers['x-real-ip'] + elif request.headers['x-forwarded-for']: + ip = request.headers['x-forwarded-for'] + agent = request.headers['user-agent'] if 'user-agent' in request.headers else '' + + await create_hit(card.id, ip, agent, card.counter, ctr_int) + + link = await get_withdraw_link(card.withdraw, 0) return link.lnurl_response(request) @boltcards_ext.get("/api/v1/scane/") @@ -152,8 +178,8 @@ async def api_scane( 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: + ctr_int = int.from_bytes(counter, "little") + if ctr_int <= card.counter: return {"status": "ERROR", "reason": "This link is already used."} await update_card_counter(counter_int, card.id) @@ -164,7 +190,9 @@ async def api_scane( elif request.headers['x-forwarded-for']: ip = request.headers['x-forwarded-for'] - await create_hit(card.id, ip, request.headers['user-agent'], card.counter, counter_int) + agent = request.headers['user-agent'] if 'user-agent' in request.headers else '' + + await create_hit(card.id, ip, agent, card.counter, ctr_int) link = await get_withdraw_link(card.withdraw, 0) return link.lnurl_response(request) From 5af49e38018594b16f46e9ab92acd575be9f42d8 Mon Sep 17 00:00:00 2001 From: iWarpBTC Date: Tue, 21 Jun 2022 23:41:08 +0200 Subject: [PATCH 04/73] comments and hints --- lnbits/extensions/boltcards/nxp424.py | 1 + .../boltcards/templates/boltcards/index.html | 12 ++++-------- lnbits/extensions/boltcards/views_api.py | 15 +++++++++++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/lnbits/extensions/boltcards/nxp424.py b/lnbits/extensions/boltcards/nxp424.py index a67b896f5..effa987d4 100644 --- a/lnbits/extensions/boltcards/nxp424.py +++ b/lnbits/extensions/boltcards/nxp424.py @@ -1,3 +1,4 @@ +# https://www.nxp.com/docs/en/application-note/AN12196.pdf from typing import Tuple from Cryptodome.Hash import CMAC from Cryptodome.Cipher import AES diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html index 4910cb66f..21ac4a45a 100644 --- a/lnbits/extensions/boltcards/templates/boltcards/index.html +++ b/lnbits/extensions/boltcards/templates/boltcards/index.html @@ -126,17 +126,15 @@ v-model.trim="cardDialog.data.card_name" type="text" label="Card name " - >The domain to use ex: "example.com" - Create a "Edit zone DNS" API token in cloudflare How much to charge per dayZero if you don't know.
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py index 8a8e33a2f..b13d9c351 100644 --- a/lnbits/extensions/boltcards/views_api.py +++ b/lnbits/extensions/boltcards/views_api.py @@ -51,6 +51,7 @@ async def api_link_create_or_update( wallet: WalletTypeInfo = Depends(require_admin_key), ): ''' + TODO: some checks if data.uses > 250: raise HTTPException( detail="250 uses max.", status_code=HTTPStatus.BAD_REQUEST @@ -119,7 +120,8 @@ async def api_hits( return [hit.dict() for hit in await get_hits(cards_ids)] -@boltcards_ext.get("/api/v1/scan/") # pay.btcslovnik.cz/boltcards/api/v1/scan/?uid=00000000000000&ctr=000000&c=0000000000000000 +# /boltcards/api/v1/scan/?uid=00000000000000&ctr=000000&c=0000000000000000 +@boltcards_ext.get("/api/v1/scan/") async def api_scan( uid, ctr, c, request: Request @@ -141,6 +143,7 @@ async def api_scan( await update_card_counter(ctr_int, card.id) + # gathering some info for hit record ip = request.client.host if request.headers['x-real-ip']: ip = request.headers['x-real-ip'] @@ -154,6 +157,7 @@ async def api_scan( link = await get_withdraw_link(card.withdraw, 0) return link.lnurl_response(request) +# /boltcards/api/v1/scane/?e=00000000000000000000000000000000&c=0000000000000000 @boltcards_ext.get("/api/v1/scane/") async def api_scane( e, c, @@ -162,6 +166,8 @@ async def api_scane( card = None counter = b'' + # since this route is common to all cards I don't know whitch 'meta key' to use + # so I try one by one until decrypted uid matches for cand in await get_all_cards(): if cand.meta_key: card_uid, counter = decryptSUN(bytes.fromhex(e), bytes.fromhex(cand.meta_key)) @@ -182,12 +188,13 @@ async def api_scane( if ctr_int <= card.counter: return {"status": "ERROR", "reason": "This link is already used."} - await update_card_counter(counter_int, card.id) + await update_card_counter(ctr_int, card.id) + # gathering some info for hit record ip = request.client.host - if request.headers['x-real-ip']: + if 'x-real-ip' in request.headers: ip = request.headers['x-real-ip'] - elif request.headers['x-forwarded-for']: + elif 'x-forwarded-for' in request.headers: ip = request.headers['x-forwarded-for'] agent = request.headers['user-agent'] if 'user-agent' in request.headers else '' From 5b8d317441b1df27068b6005dbbb95f27c6ecdbc Mon Sep 17 00:00:00 2001 From: Gene Takavic Date: Fri, 15 Jul 2022 16:43:06 +0200 Subject: [PATCH 05/73] black & isort --- lnbits/extensions/boltcards/__init__.py | 5 +- lnbits/extensions/boltcards/crud.py | 40 ++++---- lnbits/extensions/boltcards/migrations.py | 1 + lnbits/extensions/boltcards/models.py | 5 +- lnbits/extensions/boltcards/nxp424.py | 12 ++- lnbits/extensions/boltcards/views_api.py | 111 ++++++++++------------ 6 files changed, 88 insertions(+), 86 deletions(-) diff --git a/lnbits/extensions/boltcards/__init__.py b/lnbits/extensions/boltcards/__init__.py index 69326708f..f1ef972eb 100644 --- a/lnbits/extensions/boltcards/__init__.py +++ b/lnbits/extensions/boltcards/__init__.py @@ -5,10 +5,7 @@ from lnbits.helpers import template_renderer db = Database("ext_boltcards") -boltcards_ext: APIRouter = APIRouter( - prefix="/boltcards", - tags=["boltcards"] -) +boltcards_ext: APIRouter = APIRouter(prefix="/boltcards", tags=["boltcards"]) def boltcards_renderer(): diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py index f34ce6594..7cf5cad18 100644 --- a/lnbits/extensions/boltcards/crud.py +++ b/lnbits/extensions/boltcards/crud.py @@ -1,13 +1,13 @@ 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, Hit -async def create_card( - data: CreateCardData, wallet_id: str -) -> Card: + +async def create_card(data: CreateCardData, wallet_id: str) -> Card: card_id = urlsafe_short_hash() await db.execute( """ @@ -38,6 +38,7 @@ async def create_card( assert card, "Newly created card couldn't be retrieved" return card + async def update_card(card_id: str, **kwargs) -> Optional[Card]: if "is_unique" in kwargs: kwargs["is_unique"] = int(kwargs["is_unique"]) @@ -46,11 +47,10 @@ async def update_card(card_id: str, **kwargs) -> Optional[Card]: f"UPDATE boltcards.cards SET {q} WHERE id = ?", (*kwargs.values(), card_id), ) - row = await db.fetchone( - "SELECT * FROM boltcards.cards WHERE id = ?", (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] @@ -62,17 +62,20 @@ async def get_cards(wallet_ids: Union[str, List[str]]) -> List[Card]: return [Card(**row) for row in rows] + async def get_all_cards() -> List[Card]: - rows = await db.fetchall( - f"SELECT * FROM boltcards.cards" - ) + 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") + +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, + sql, + card_id, ) if not row: return None @@ -81,19 +84,20 @@ async def get_card(card_id: str, id_is_uid: bool=False) -> Optional[Card]: 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), ) + async def get_hit(hit_id: str) -> Optional[Hit]: - row = await db.fetchone( - f"SELECT * FROM boltcards.hits WHERE id = ?", (hit_id) - ) + row = await db.fetchone(f"SELECT * FROM boltcards.hits WHERE id = ?", (hit_id)) if not row: return None @@ -101,6 +105,7 @@ async def get_hit(hit_id: str) -> Optional[Hit]: return Hit.parse_obj(hit) + async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]: q = ",".join(["?"] * len(cards_ids)) rows = await db.fetchall( @@ -109,9 +114,8 @@ async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]: return [Hit(**row) for row in rows] -async def create_hit( - card_id, ip, useragent, old_ctr, new_ctr -) -> Hit: + +async def create_hit(card_id, ip, useragent, old_ctr, new_ctr) -> Hit: hit_id = urlsafe_short_hash() await db.execute( """ diff --git a/lnbits/extensions/boltcards/migrations.py b/lnbits/extensions/boltcards/migrations.py index e7236ce7a..6e0fa0723 100644 --- a/lnbits/extensions/boltcards/migrations.py +++ b/lnbits/extensions/boltcards/migrations.py @@ -1,5 +1,6 @@ from lnbits.helpers import urlsafe_short_hash + async def m001_initial(db): await db.execute( """ diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py index 728aa2bb8..b6d521c3c 100644 --- a/lnbits/extensions/boltcards/models.py +++ b/lnbits/extensions/boltcards/models.py @@ -1,5 +1,6 @@ -from pydantic import BaseModel from fastapi.params import Query +from pydantic import BaseModel + class Card(BaseModel): id: str @@ -12,6 +13,7 @@ class Card(BaseModel): meta_key: str time: int + class CreateCardData(BaseModel): card_name: str = Query(...) uid: str = Query(...) @@ -20,6 +22,7 @@ class CreateCardData(BaseModel): file_key: str = Query(...) meta_key: str = Query(...) + class Hit(BaseModel): id: str card_id: str diff --git a/lnbits/extensions/boltcards/nxp424.py b/lnbits/extensions/boltcards/nxp424.py index effa987d4..83f4e50d5 100644 --- a/lnbits/extensions/boltcards/nxp424.py +++ b/lnbits/extensions/boltcards/nxp424.py @@ -1,18 +1,21 @@ # https://www.nxp.com/docs/en/application-note/AN12196.pdf from typing import Tuple -from Cryptodome.Hash import CMAC + from Cryptodome.Cipher import AES +from Cryptodome.Hash import CMAC SV2 = "3CC300010080" -def myCMAC(key: bytes, msg: bytes=b'') -> bytes: + +def myCMAC(key: bytes, msg: bytes = b"") -> bytes: cobj = CMAC.new(key, ciphermod=AES) - if msg != b'': + if msg != b"": cobj.update(msg) return cobj.digest() + def decryptSUN(sun: bytes, key: bytes) -> Tuple[bytes, bytes]: - IVbytes = b"\x00" * 16 + IVbytes = b"\x00" * 16 cipher = AES.new(key, AES.MODE_CBC, IVbytes) sun_plain = cipher.decrypt(sun) @@ -22,6 +25,7 @@ def decryptSUN(sun: bytes, key: bytes) -> Tuple[bytes, bytes]: return UID, counter + def getSunMAC(UID: bytes, counter: bytes, key: bytes) -> bytes: sv2prefix = bytes.fromhex(SV2) sv2bytes = sv2prefix + UID + counter diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py index b13d9c351..fbd05cce0 100644 --- a/lnbits/extensions/boltcards/views_api.py +++ b/lnbits/extensions/boltcards/views_api.py @@ -17,19 +17,20 @@ 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 ( - create_hit, - get_all_cards, - get_cards, - get_card, create_card, + create_hit, + delete_card, + get_all_cards, + get_card, + get_cards, get_hits, update_card, - delete_card, - update_card_counter + update_card_counter, ) from .models import CreateCardData +from .nxp424 import decryptSUN, getSunMAC + @boltcards_ext.get("/api/v1/cards") async def api_cards( @@ -42,32 +43,15 @@ async def api_cards( 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, + # req: Request, data: CreateCardData, card_id: str = None, wallet: WalletTypeInfo = Depends(require_admin_key), ): - ''' - TODO: some checks - 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: @@ -78,15 +62,12 @@ async def api_link_create_or_update( raise HTTPException( detail="Not your card.", status_code=HTTPStatus.FORBIDDEN ) - card = await update_card( - card_id, **data.dict() - ) + card = await update_card(card_id, **data.dict()) else: - card = await create_card( - wallet_id=wallet.wallet.id, data=data - ) + 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) @@ -97,13 +78,12 @@ async def api_link_delete(card_id, wallet: WalletTypeInfo = Depends(require_admi ) if card.wallet != wallet.wallet.id: - raise HTTPException( - detail="Not your card.", status_code=HTTPStatus.FORBIDDEN - ) + 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/hits") async def api_hits( g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False) @@ -120,20 +100,33 @@ async def api_hits( return [hit.dict() for hit in await get_hits(cards_ids)] + # /boltcards/api/v1/scan/?uid=00000000000000&ctr=000000&c=0000000000000000 -@boltcards_ext.get("/api/v1/scan/") -async def api_scan( - uid, ctr, c, - request: Request -): +@boltcards_ext.get("/api/v1/scan/") +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(): + 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()) + 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) @@ -145,32 +138,32 @@ async def api_scan( # gathering some info for hit record ip = request.client.host - if request.headers['x-real-ip']: - ip = request.headers['x-real-ip'] - elif request.headers['x-forwarded-for']: - ip = request.headers['x-forwarded-for'] + if request.headers["x-real-ip"]: + ip = request.headers["x-real-ip"] + elif request.headers["x-forwarded-for"]: + ip = request.headers["x-forwarded-for"] - agent = request.headers['user-agent'] if 'user-agent' in request.headers else '' + agent = request.headers["user-agent"] if "user-agent" in request.headers else "" await create_hit(card.id, ip, agent, card.counter, ctr_int) link = await get_withdraw_link(card.withdraw, 0) return link.lnurl_response(request) + # /boltcards/api/v1/scane/?e=00000000000000000000000000000000&c=0000000000000000 @boltcards_ext.get("/api/v1/scane/") -async def api_scane( - e, c, - request: Request -): +async def api_scane(e, c, request: Request): card = None - counter = b'' + counter = b"" # since this route is common to all cards I don't know whitch 'meta key' to use # so I try one by one until decrypted uid matches for cand in await get_all_cards(): if cand.meta_key: - card_uid, counter = decryptSUN(bytes.fromhex(e), bytes.fromhex(cand.meta_key)) + card_uid, counter = decryptSUN( + bytes.fromhex(e), bytes.fromhex(cand.meta_key) + ) if card_uid.hex().upper() == cand.uid: card = cand @@ -187,17 +180,17 @@ async def api_scane( ctr_int = int.from_bytes(counter, "little") if ctr_int <= card.counter: return {"status": "ERROR", "reason": "This link is already used."} - + await update_card_counter(ctr_int, card.id) # gathering some info for hit record ip = request.client.host - if 'x-real-ip' in request.headers: - ip = request.headers['x-real-ip'] - elif 'x-forwarded-for' in request.headers: - ip = request.headers['x-forwarded-for'] + if "x-real-ip" in request.headers: + ip = request.headers["x-real-ip"] + elif "x-forwarded-for" in request.headers: + ip = request.headers["x-forwarded-for"] - agent = request.headers['user-agent'] if 'user-agent' in request.headers else '' + agent = request.headers["user-agent"] if "user-agent" in request.headers else "" await create_hit(card.id, ip, agent, card.counter, ctr_int) From c04b0a19055d6b29f5ac38e314d783e9498b6768 Mon Sep 17 00:00:00 2001 From: iWarpBTC Date: Sun, 17 Jul 2022 12:23:56 +0200 Subject: [PATCH 06/73] Update index.html prettier --- .../boltcards/templates/boltcards/index.html | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html index 21ac4a45a..165d72fba 100644 --- a/lnbits/extensions/boltcards/templates/boltcards/index.html +++ b/lnbits/extensions/boltcards/templates/boltcards/index.html @@ -126,8 +126,7 @@ v-model.trim="cardDialog.data.card_name" type="text" label="Card name " - > + >
- {% endblock %} {% block scripts %} {{ window_vars(user) }} -{% endblock %} +{% endblock %} \ No newline at end of file From 293e5394a81fa4040acddf64b31c77a006939c1f Mon Sep 17 00:00:00 2001 From: Lee Salminen Date: Sun, 14 Aug 2022 10:58:35 -0600 Subject: [PATCH 09/73] run make format --- lnbits/extensions/boltcards/templates/boltcards/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html index 8ce57398c..61a962fec 100644 --- a/lnbits/extensions/boltcards/templates/boltcards/index.html +++ b/lnbits/extensions/boltcards/templates/boltcards/index.html @@ -458,4 +458,4 @@ } }) -{% endblock %} \ No newline at end of file +{% endblock %} From 0e5f6ac586d03288f42887b63eeb1694c66adf61 Mon Sep 17 00:00:00 2001 From: Gene Takavic Date: Sun, 14 Aug 2022 23:52:55 +0200 Subject: [PATCH 10/73] adapt to bolt-nfc-android-app --- lnbits/extensions/boltcards/README.md | 61 ++-- lnbits/extensions/boltcards/__init__.py | 9 + lnbits/extensions/boltcards/crud.py | 45 ++- lnbits/extensions/boltcards/migrations.py | 9 +- lnbits/extensions/boltcards/models.py | 21 +- .../extensions/boltcards/static/js/index.js | 299 +++++++++++++++++ .../boltcards/templates/boltcards/index.html | 314 ++++-------------- lnbits/extensions/boltcards/views_api.py | 97 ++---- 8 files changed, 504 insertions(+), 351 deletions(-) create mode 100644 lnbits/extensions/boltcards/static/js/index.js diff --git a/lnbits/extensions/boltcards/README.md b/lnbits/extensions/boltcards/README.md index ca239e42e..5fa6a9784 100644 --- a/lnbits/extensions/boltcards/README.md +++ b/lnbits/extensions/boltcards/README.md @@ -2,13 +2,50 @@ This extension allows you to link your Bolt card with a LNbits instance and use it more securely then just with a static LNURLw on it. A technology called [Secure Unique NFC](https://mishka-scan.com/blog/secure-unique-nfc) is utilized in this workflow. -***In order to use this extension you need to be able setup your card first.*** There's a [guide](https://www.whitewolftech.com/articles/payment-card/) to set it up with your computer. Or it can be done with [https://play.google.com/store/apps/details?id=com.nxp.nfc.tagwriter](TagWriter app by NXP) Android app. +**Disclaim:** ***Use this only if you either know what you are doing or are enough reckless lightning pioneer. Only you are responsible for all your sats, cards and other devices. Always backup all your card keys!*** -## Setting the outside the extension - android -- Write tags +***In order to use this extension you need to be able setup your card.*** That is writting on the URL template pointing to your LNBits instance, configure some SUN (SDM) setting and optionaly changing the card keys. There's a [guide](https://www.whitewolftech.com/articles/payment-card/) to set it up with a card reader connected to your computer. It can be done (without setting the keys) with [TagWriter app by NXP](https://play.google.com/store/apps/details?id=com.nxp.nfc.tagwriter) Android app. Last but not least, an OSS android app by name [bolt-nfc-android-app](https://github.com/boltcard/bolt-nfc-android-app) is being developed for these purposes. + +## About the keys + +Up to five 16bytes keys can be stored on the card, numbered from 00 to 04. In the empty state they all should be set to zeros (00000000000000000000000000000000). For this extension only two keys need to be set: + +One for encrypting the card UID and the counter (p parameter), let's called it meta key, key #01or K1. + +One for calculating CMAC (c parameter), let's called it file key, key #02 or K2. + +The key #00, K0 or also auth key is skipped to be use as authentification key. Is not needed by this extension, but can be filled in order to write the keys in cooperation with bolt-nfc-android-app. + +***Always backup all keys that you're trying to write on the card. Without them you may not be able to change them in the future!*** + +## LNURLw +Create a withdraw link within the LNURLw extension before adding a card. Enable the `Use unique withdraw QR codes to reduce 'assmilking'` option. + +## Setting the card - bolt-nfc-android-app (easy way) +So far, regarding the keys, the app can only write a new key set on an empty card (with zero keys). **When you write non zero (and 'non debug') keys, they can't be rewrite with this app.** You have to do it on your computer. + +- Read the card with the app. Note UID so you can fill it in the extension later. +- Write the link on the card. It shoud be like `YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan` +- Add new card in the extension. + - Leaving any key array empty means that key is 16bytes of zero (00000000000000000000000000000000). + - GENERATE KEY button fill the keys randomly. If there is "debug" in the card name, a debug set of keys is filled instead. + - Leaving initial counter empty means zero. +- Open the card details. **Backup the keys.** Scan the QR with the app to write the keys on the card. + +## Setting the card - computer (hard way) + +Follow the guide. + +The URI should be `lnurlw://YOUR-DOMAIN.COM/boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000` + +Then fill up the card parameters in the extension. Card Auth key (K0) can be omitted. Initical counter can be 0. + +## Setting the card - android NXP app (hard way) +- If you don't know the card ID, use NXP TagInfo app to find it out. +- In the TagWriter app tap Write tags - New Data Set > Link - Set URI type to Custom URL -- URL should look like lnurlw://YOUR_LNBITS_DOMAIN/boltcards/api/v1/scane?e=00000000000000000000000000000000&c=0000000000000000 +- URL should look like lnurlw://YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000 - click Configure mirroring options - Select Card Type NTAG 424 DNA - Check Enable SDM Mirroring @@ -23,18 +60,4 @@ This extension allows you to link your Bolt card with a LNbits instance and use - Save & Write - Scan with compatible Wallet -## Setting the outside the extension - computer - -Follow the guide. - -The URI should be `lnurlw://YOUR-DOMAIN.COM/boltcards/api/v1/scane/?e=00000000000000000000000000000000&c=0000000000000000` - -(At this point the link is common to all cards. So the extension grabs one by one every added card's key and tries to decrypt the e parameter until there's a match.) - -Choose and note your Meta key and File key. - -## Adding the into the extension - -Create a withdraw link within the LNURLw extension before adding a card. Enable the `Use unique withdraw QR codes to reduce 'assmilking'` option. - -The card UID can be retrieve with `NFC TagInfo` mobile app or from `NXP TagXplorer` log. Use the keys you've set before. You can leave the counter zero, it gets synchronized with the first use. \ No newline at end of file +This app afaik cannot change the keys. If you cannot change them any other way, leave them empty in the extension dialog and remember you're not secure. Card Auth key (K0) can be omitted anyway. Initical counter can be 0. diff --git a/lnbits/extensions/boltcards/__init__.py b/lnbits/extensions/boltcards/__init__.py index f1ef972eb..f53363411 100644 --- a/lnbits/extensions/boltcards/__init__.py +++ b/lnbits/extensions/boltcards/__init__.py @@ -1,10 +1,19 @@ from fastapi import APIRouter +from starlette.staticfiles import StaticFiles from lnbits.db import Database from lnbits.helpers import template_renderer db = Database("ext_boltcards") +boltcards_static_files = [ + { + "path": "/boltcards/static", + "app": StaticFiles(packages=[("lnbits", "extensions/boltcards/static")]), + "name": "boltcards_static", + } +] + boltcards_ext: APIRouter = APIRouter(prefix="/boltcards", tags=["boltcards"]) diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py index 5c2824f4f..5affe3122 100644 --- a/lnbits/extensions/boltcards/crud.py +++ b/lnbits/extensions/boltcards/crud.py @@ -1,4 +1,4 @@ -from optparse import Option +import secrets from typing import List, Optional, Union from lnbits.helpers import urlsafe_short_hash @@ -18,10 +18,12 @@ async def create_card(data: CreateCardData, wallet_id: str) -> Card: uid, counter, withdraw, - file_key, - meta_key + k0, + k1, + k2, + otp ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( card_id, @@ -30,11 +32,13 @@ async def create_card(data: CreateCardData, wallet_id: str) -> Card: data.uid, data.counter, data.withdraw, - data.file_key, - data.meta_key, + data.k0, + data.k1, + data.k2, + secrets.token_hex(16), ), ) - card = await get_card(card_id, 0) + card = await get_card(card_id) assert card, "Newly created card couldn't be retrieved" return card @@ -69,14 +73,18 @@ async def get_all_cards() -> List[Card]: 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, - ) +async def get_card(card_id: str) -> Optional[Card]: + row = await db.fetchone("SELECT * FROM boltcards.cards WHERE id = ?", (card_id,)) + if not row: + return None + + card = dict(**row) + + return Card.parse_obj(card) + + +async def get_card_by_otp(otp: str) -> Optional[Card]: + row = await db.fetchone("SELECT * FROM boltcards.cards WHERE otp = ?", (otp,)) if not row: return None @@ -96,6 +104,13 @@ async def update_card_counter(counter: int, id: str): ) +async def update_card_otp(otp: str, id: str): + await db.execute( + "UPDATE boltcards.cards SET otp = ? WHERE id = ?", + (otp, id), + ) + + async def get_hit(hit_id: str) -> Optional[Hit]: row = await db.fetchone(f"SELECT * FROM boltcards.hits WHERE id = ?", (hit_id)) if not row: diff --git a/lnbits/extensions/boltcards/migrations.py b/lnbits/extensions/boltcards/migrations.py index 6e0fa0723..7dc5acb44 100644 --- a/lnbits/extensions/boltcards/migrations.py +++ b/lnbits/extensions/boltcards/migrations.py @@ -11,8 +11,13 @@ async def m001_initial(db): 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 '', + k0 TEXT NOT NULL DEFAULT '00000000000000000000000000000000', + k1 TEXT NOT NULL DEFAULT '00000000000000000000000000000000', + k2 TEXT NOT NULL DEFAULT '00000000000000000000000000000000', + prev_k0 TEXT NOT NULL DEFAULT '00000000000000000000000000000000', + prev_k1 TEXT NOT NULL DEFAULT '00000000000000000000000000000000', + prev_k2 TEXT NOT NULL DEFAULT '00000000000000000000000000000000', + otp 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 index b6d521c3c..6e1997545 100644 --- a/lnbits/extensions/boltcards/models.py +++ b/lnbits/extensions/boltcards/models.py @@ -1,6 +1,8 @@ from fastapi.params import Query from pydantic import BaseModel +ZERO_KEY = "00000000000000000000000000000000" + class Card(BaseModel): id: str @@ -9,18 +11,27 @@ class Card(BaseModel): uid: str counter: int withdraw: str - file_key: str - meta_key: str + k0: str + k1: str + k2: str + prev_k0: str + prev_k1: str + prev_k2: str + otp: str time: int class CreateCardData(BaseModel): card_name: str = Query(...) uid: str = Query(...) - counter: str = Query(...) + counter: int = Query(0) withdraw: str = Query(...) - file_key: str = Query(...) - meta_key: str = Query(...) + k0: str = Query(ZERO_KEY) + k1: str = Query(ZERO_KEY) + k2: str = Query(ZERO_KEY) + prev_k0: str = Query(ZERO_KEY) + prev_k1: str = Query(ZERO_KEY) + prev_k2: str = Query(ZERO_KEY) class Hit(BaseModel): diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js new file mode 100644 index 000000000..e2afbf1e4 --- /dev/null +++ b/lnbits/extensions/boltcards/static/js/index.js @@ -0,0 +1,299 @@ +Vue.component(VueQrcode.name, VueQrcode) + +const mapCards = obj => { + obj.date = Quasar.utils.date.formatDate( + new Date(obj.time * 1000), + 'YYYY-MM-DD HH:mm' + ) + + return obj +} + +new Vue({ + el: '#vue', + mixins: [windowMixin], + data: function () { + return { + cards: [], + hits: [], + withdrawsOptions: [], + cardDialog: { + show: false, + data: {}, + temp: {} + }, + cardsTable: { + columns: [ + { + name: 'card_name', + align: 'left', + label: 'Card name', + field: 'card_name' + }, + { + name: 'counter', + align: 'left', + label: 'Counter', + field: 'counter' + }, + { + name: 'withdraw', + align: 'left', + label: 'Withdraw ID', + field: 'withdraw' + } + ], + pagination: { + rowsPerPage: 10 + } + }, + hitsTable: { + columns: [ + { + name: 'card_name', + align: 'left', + label: 'Card name', + field: 'card_name' + }, + { + name: 'old_ctr', + align: 'left', + label: 'Old counter', + field: 'old_ctr' + }, + { + name: 'new_ctr', + align: 'left', + label: 'New counter', + field: 'new_ctr' + }, + { + name: 'date', + align: 'left', + label: 'Time', + field: 'date' + }, + { + name: 'ip', + align: 'left', + label: 'IP', + field: 'ip' + }, + { + name: 'useragent', + align: 'left', + label: 'User agent', + field: 'useragent' + } + ], + pagination: { + rowsPerPage: 10, + sortBy: 'date', + descending: true + } + }, + qrCodeDialog: { + show: false, + data: null + } + } + }, + methods: { + getCards: function () { + var self = this + + LNbits.api + .request( + 'GET', + '/boltcards/api/v1/cards?all_wallets=true', + this.g.user.wallets[0].inkey + ) + .then(function (response) { + self.cards = response.data.map(function (obj) { + return mapCards(obj) + }) + console.log(self.cards) + }) + }, + getHits: function () { + var self = this + + LNbits.api + .request( + 'GET', + '/boltcards/api/v1/hits?all_wallets=true', + this.g.user.wallets[0].inkey + ) + .then(function (response) { + self.hits = response.data.map(function (obj) { + obj.card_name = self.cards.find(d => d.id == obj.card_id).card_name + return mapCards(obj) + }) + console.log(self.hits) + }) + }, + getWithdraws: function () { + var self = this + + LNbits.api + .request( + 'GET', + '/withdraw/api/v1/links?all_wallets=true', + this.g.user.wallets[0].inkey + ) + .then(function (response) { + self.withdrawsOptions = response.data.map(function (obj) { + return { + label: [obj.title, ' - ', obj.id].join(''), + value: obj.id + } + }) + console.log(self.withdraws) + }) + }, + openQrCodeDialog(cardId) { + var card = _.findWhere(this.cards, {id: cardId}) + + this.qrCodeDialog.data = { + link: window.location.origin + '/boltcards/api/v1/auth?a=' + card.otp, + name: card.card_name, + uid: card.uid, + k0: card.k0, + k1: card.k1, + k2: card.k2 + } + this.qrCodeDialog.show = true + }, + generateKeys: function () { + const genRanHex = size => + [...Array(size)] + .map(() => Math.floor(Math.random() * 16).toString(16)) + .join('') + + debugcard = + typeof this.cardDialog.data.card_name === 'string' && + this.cardDialog.data.card_name.search('debug') > -1 + + this.cardDialog.data.k0 = debugcard + ? '11111111111111111111111111111111' + : genRanHex(32) + this.$refs['k0'].value = this.cardDialog.data.k0 + + this.cardDialog.data.k1 = debugcard + ? '22222222222222222222222222222222' + : genRanHex(32) + this.$refs['k1'].value = this.cardDialog.data.k1 + + this.cardDialog.data.k2 = debugcard + ? '33333333333333333333333333333333' + : genRanHex(32) + this.$refs['k2'].value = this.cardDialog.data.k2 + }, + closeFormDialog: function () { + this.cardDialog.data = {} + }, + sendFormData: function () { + let wallet = _.findWhere(this.g.user.wallets, { + id: this.cardDialog.data.wallet + }) + let data = this.cardDialog.data + if (data.id) { + this.updateCard(wallet, data) + } else { + this.createCard(wallet, data) + } + }, + createCard: function (wallet, data) { + var self = this + + LNbits.api + .request('POST', '/boltcards/api/v1/cards', wallet.adminkey, data) + .then(function (response) { + self.cards.push(mapCards(response.data)) + self.cardDialog.show = false + self.cardDialog.data = {} + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }, + updateCardDialog: function (formId) { + var card = _.findWhere(this.cards, {id: formId}) + console.log(card.id) + this.cardDialog.data = _.clone(card) + + this.cardDialog.temp.k0 = this.cardDialog.data.k0 + this.cardDialog.temp.k1 = this.cardDialog.data.k1 + this.cardDialog.temp.k2 = this.cardDialog.data.k2 + + this.cardDialog.show = true + }, + updateCard: function (wallet, data) { + var self = this + + if ( + this.cardDialog.temp.k0 != data.k0 || + this.cardDialog.temp.k1 != data.k1 || + this.cardDialog.temp.k2 != data.k2 + ) { + data.prev_k0 = this.cardDialog.temp.k0 + data.prev_k1 = this.cardDialog.temp.k1 + data.prev_k2 = this.cardDialog.temp.k2 + } + + console.log(data) + + LNbits.api + .request( + 'PUT', + '/boltcards/api/v1/cards/' + data.id, + wallet.adminkey, + data + ) + .then(function (response) { + self.cards = _.reject(self.cards, function (obj) { + return obj.id == data.id + }) + self.cards.push(mapCards(response.data)) + self.cardDialog.show = false + self.cardDialog.data = {} + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }, + deleteCard: function (cardId) { + let self = this + let cards = _.findWhere(this.cards, {id: cardId}) + + LNbits.utils + .confirmDialog('Are you sure you want to delete this card') + .onOk(function () { + LNbits.api + .request( + 'DELETE', + '/boltcards/api/v1/cards/' + cardId, + _.findWhere(self.g.user.wallets, {id: cards.wallet}).adminkey + ) + .then(function (response) { + self.cards = _.reject(self.cards, function (obj) { + return obj.id == cardId + }) + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }) + }, + exportCardsCSV: function () { + LNbits.utils.exportCSV(this.cardsTable.columns, this.cards) + } + }, + created: function () { + if (this.g.user.wallets.length) { + this.getCards() + this.getHits() + this.getWithdraws() + } + } +}) diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html index 61a962fec..a6961fe59 100644 --- a/lnbits/extensions/boltcards/templates/boltcards/index.html +++ b/lnbits/extensions/boltcards/templates/boltcards/index.html @@ -33,6 +33,7 @@ {% raw %} @@ -220,7 +237,7 @@ type="number" label="Max transaction (sats)" class="q-pr-sm" - > + >
+ filled + dense + emit-value + v-model.trim="cardDialog.data.card_name" + type="text" + label="Card name " + >
- - - Get from the card you'll use, using an NFC app - - + Get from the card you'll use, using an NFC app
Tap card to scan UID (coming soon) + outline + disable + color="grey" + icon="nfc" + :disable="nfcTagReading" + >Tap card to scan UID (coming soon) +
- - - + -
- - - - - - Zero if you don't know. - +
+ + + + + + Zero if you don't know. + Generate keys -
+
Date: Mon, 29 Aug 2022 07:34:56 -0600 Subject: [PATCH 45/73] make format --- lnbits/extensions/boltcards/lnurl.py | 19 ++++++------------- lnbits/extensions/boltcards/models.py | 3 +-- .../extensions/boltcards/static/js/index.js | 2 +- lnbits/extensions/boltcards/tasks.py | 2 +- .../boltcards/templates/boltcards/index.html | 2 +- lnbits/extensions/boltcards/views_api.py | 7 +++---- 6 files changed, 13 insertions(+), 22 deletions(-) diff --git a/lnbits/extensions/boltcards/lnurl.py b/lnbits/extensions/boltcards/lnurl.py index a1630e2be..9399fb369 100644 --- a/lnbits/extensions/boltcards/lnurl.py +++ b/lnbits/extensions/boltcards/lnurl.py @@ -2,21 +2,19 @@ import base64 import hashlib import hmac import json +import secrets from http import HTTPStatus from io import BytesIO from typing import Optional -from loguru import logger - from embit import bech32, compact from fastapi import Request from fastapi.param_functions import Query -from starlette.exceptions import HTTPException - -import secrets -from http import HTTPStatus - from fastapi.params import Depends, Query +from lnurl import Lnurl, LnurlWithdrawResponse +from lnurl import encode as lnurl_encode # type: ignore +from lnurl.types import LnurlPayMetadata # type: ignore +from loguru import logger from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.responses import HTMLResponse @@ -24,17 +22,12 @@ from starlette.responses import HTMLResponse from lnbits.core.services import create_invoice from lnbits.core.views.api import pay_invoice -from lnurl import Lnurl, LnurlWithdrawResponse -from lnurl import encode as lnurl_encode # type: ignore -from lnurl.types import LnurlPayMetadata # type: ignore - from . import boltcards_ext from .crud import ( create_hit, get_card, - get_card_by_uid, get_card_by_otp, - get_card, + get_card_by_uid, get_hit, get_hits_today, spend_hit, diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py index 80e3b9734..21096640a 100644 --- a/lnbits/extensions/boltcards/models.py +++ b/lnbits/extensions/boltcards/models.py @@ -1,9 +1,8 @@ -from fastapi.params import Query -from pydantic import BaseModel from sqlite3 import Row from typing import Optional from fastapi import Request +from fastapi.params import Query from lnurl import Lnurl from lnurl import encode as lnurl_encode # type: ignore from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js index 33704f3a2..27536304f 100644 --- a/lnbits/extensions/boltcards/static/js/index.js +++ b/lnbits/extensions/boltcards/static/js/index.js @@ -190,7 +190,7 @@ new Vue({ }) }) }, - openQrCodeDialog (cardId) { + openQrCodeDialog(cardId) { var card = _.findWhere(this.cards, {id: cardId}) this.qrCodeDialog.data = { link: window.location.origin + '/boltcards/api/v1/auth?a=' + card.otp, diff --git a/lnbits/extensions/boltcards/tasks.py b/lnbits/extensions/boltcards/tasks.py index bfe4f257b..a7eea026d 100644 --- a/lnbits/extensions/boltcards/tasks.py +++ b/lnbits/extensions/boltcards/tasks.py @@ -7,7 +7,7 @@ from lnbits.core import db as core_db from lnbits.core.models import Payment from lnbits.tasks import register_invoice_listener -from .crud import get_hit, create_refund +from .crud import create_refund, get_hit async def wait_for_paid_invoices(): diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html index 2a613fda2..73e5820b0 100644 --- a/lnbits/extensions/boltcards/templates/boltcards/index.html +++ b/lnbits/extensions/boltcards/templates/boltcards/index.html @@ -7,7 +7,7 @@
-
+
Cards
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py index f1e02810f..698e10948 100644 --- a/lnbits/extensions/boltcards/views_api.py +++ b/lnbits/extensions/boltcards/views_api.py @@ -2,6 +2,7 @@ import secrets from http import HTTPStatus from fastapi.params import Depends, Query +from loguru import logger from starlette.exceptions import HTTPException from starlette.requests import Request @@ -13,22 +14,20 @@ from .crud import ( create_card, create_hit, delete_card, + enable_disable_card, get_card, get_card_by_otp, get_card_by_uid, get_cards, get_hits, + get_refunds, update_card, update_card_counter, update_card_otp, - enable_disable_card, - get_refunds, ) from .models import CreateCardData from .nxp424 import decryptSUN, getSunMAC -from loguru import logger - @boltcards_ext.get("/api/v1/cards") async def api_cards( From c8b725830bee3e35302c454acbacd50e17208006 Mon Sep 17 00:00:00 2001 From: Lee Salminen Date: Mon, 29 Aug 2022 07:55:51 -0600 Subject: [PATCH 46/73] make copy lnurl to clipboard work --- lnbits/extensions/boltcards/static/js/index.js | 1 + lnbits/extensions/boltcards/templates/boltcards/index.html | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js index 27536304f..e6c052ace 100644 --- a/lnbits/extensions/boltcards/static/js/index.js +++ b/lnbits/extensions/boltcards/static/js/index.js @@ -16,6 +16,7 @@ new Vue({ return { toggleAdvanced: false, nfcTagReading: false, + lnurlLink: `lnurlw://${window.location.host}/boltcards/api/v1/scan/`, cards: [], hits: [], refunds: [], diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html index 73e5820b0..6938eb46a 100644 --- a/lnbits/extensions/boltcards/templates/boltcards/index.html +++ b/lnbits/extensions/boltcards/templates/boltcards/index.html @@ -68,8 +68,7 @@ outline color="grey" @click="copyText(lnurlLink + props.row.uid)" - lnurlLink - >lnurl://...lnurlw://...Click to copy, then add to NFC card From 7400d7f43046c660060977c858a35c96cda76b16 Mon Sep 17 00:00:00 2001 From: Lee Salminen Date: Mon, 29 Aug 2022 07:58:29 -0600 Subject: [PATCH 47/73] link to play store app in qr code dialog --- lnbits/extensions/boltcards/templates/boltcards/index.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html index 6938eb46a..0561cc018 100644 --- a/lnbits/extensions/boltcards/templates/boltcards/index.html +++ b/lnbits/extensions/boltcards/templates/boltcards/index.html @@ -370,7 +370,12 @@ >

- (QR code is for setting the keys with bolt-nfc-android-app) + (QR code is for setting the keys with + bolt-nfc-android-app)

Name: {{ qrCodeDialog.data.name }}
From 7ea3830fed2f6028263b8b91fdc06fe7ce44c20f Mon Sep 17 00:00:00 2001 From: ben Date: Mon, 29 Aug 2022 15:11:25 +0100 Subject: [PATCH 48/73] Added NFC to get UID --- .../extensions/boltcards/static/js/index.js | 59 +++++++++++++++ .../boltcards/templates/boltcards/index.html | 74 +++++++++++-------- 2 files changed, 104 insertions(+), 29 deletions(-) diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js index 33704f3a2..8f387f95d 100644 --- a/lnbits/extensions/boltcards/static/js/index.js +++ b/lnbits/extensions/boltcards/static/js/index.js @@ -26,6 +26,7 @@ new Vue({ k0: '', k1: '', k2: '', + uid: '', card_name: '' }, temp: {} @@ -146,6 +147,64 @@ new Vue({ } }, methods: { + readNfcTag: function () { + try { + const self = this + + if (typeof NDEFReader == 'undefined') { + throw { + toString: function () { + return 'NFC not supported on this device or browser.' + } + } + } + + const ndef = new NDEFReader() + + const readerAbortController = new AbortController() + readerAbortController.signal.onabort = event => { + console.log('All NFC Read operations have been aborted.') + } + + this.nfcTagReading = true + this.$q.notify({ + message: 'Tap your NFC tag to pay this invoice with LNURLw.' + }) + + return ndef.scan({signal: readerAbortController.signal}).then(() => { + ndef.onreadingerror = () => { + self.nfcTagReading = false + + this.$q.notify({ + type: 'negative', + message: 'There was an error reading this NFC tag.' + }) + + readerAbortController.abort() + } + + ndef.onreading = ({message, serialNumber}) => { + //Decode NDEF data from tag + var self = this + self.cardDialog.data.uid = serialNumber + .toUpperCase() + .replaceAll(':', '') + this.$q.notify({ + type: 'positive', + message: 'NFC tag read successfully.' + }) + } + }) + } catch (error) { + this.nfcTagReading = false + this.$q.notify({ + type: 'negative', + message: error + ? error.toString() + : 'An unexpected error has occurred.' + }) + } + }, getCards: function () { var self = this diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html index 2a613fda2..ec2611a98 100644 --- a/lnbits/extensions/boltcards/templates/boltcards/index.html +++ b/lnbits/extensions/boltcards/templates/boltcards/index.html @@ -43,7 +43,6 @@ @@ -257,7 +248,8 @@ v-model.trim="cardDialog.data.card_name" type="text" label="Card name " - > + > +

Get from the card you'll use, using an NFC app + Get from the card you'll use, using an NFC app +
Tap card to scan UID (coming soon) + Tap card to scan UID +
@@ -322,10 +317,11 @@ v-model.number="cardDialog.data.counter" type="number" label="Initial counter" - >Zero if you don't know. + Zero if you don't know. + Create Card + >Create Card + Cancel @@ -371,7 +367,7 @@ >

- (QR code is for setting the keys with bolt-nfc-android-app) + (Keys for bolt-nfc-android-app)

Name: {{ qrCodeDialog.data.name }}
@@ -380,6 +376,26 @@ Meta key: {{ qrCodeDialog.data.k1 }}
File key: {{ qrCodeDialog.data.k2 }}

+
+ + + + + + Click to copy, then add to NFC card + {% endraw %}
Close From a8ac90da5c79a76b16cc07c9bbf3cd6474700f4d Mon Sep 17 00:00:00 2001 From: Lee Salminen Date: Mon, 29 Aug 2022 08:19:39 -0600 Subject: [PATCH 49/73] make scan nfc button work --- .../extensions/boltcards/static/js/index.js | 63 +++++++++++++++++++ .../boltcards/templates/boltcards/index.html | 3 +- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js index e6c052ace..315ed59fd 100644 --- a/lnbits/extensions/boltcards/static/js/index.js +++ b/lnbits/extensions/boltcards/static/js/index.js @@ -230,6 +230,69 @@ new Vue({ ? '33333333333333333333333333333333' : genRanHex(32) }, + readNfcTag: function () { + try { + const self = this + + if (typeof NDEFReader == 'undefined') { + throw { + toString: function () { + return 'NFC not supported on this device or browser.' + } + } + } + + const ndef = new NDEFReader() + + const readerAbortController = new AbortController() + readerAbortController.signal.onabort = event => { + console.log('All NFC Read operations have been aborted.') + } + + this.nfcTagReading = true + this.$q.notify({ + message: 'Tap your NFC tag to read its UID' + }) + + return ndef.scan({signal: readerAbortController.signal}).then(() => { + ndef.onreadingerror = () => { + self.nfcTagReading = false + + this.$q.notify({ + type: 'negative', + message: 'There was an error reading this NFC tag.' + }) + + readerAbortController.abort() + } + + ndef.onreading = ({message, serialNumber}) => { + self.nfcTagReading = false + + self.cardDialog.data.uid = serialNumber + .replaceAll(':', '') + .toUpperCase() + + this.$q.notify({ + type: 'positive', + message: 'NFC tag read successfully.' + }) + + setTimeout(() => { + readerAbortController.abort() + }, 1000) + } + }) + } catch (error) { + this.nfcTagReading = false + this.$q.notify({ + type: 'negative', + message: error + ? error.toString() + : 'An unexpected error has occurred.' + }) + } + }, closeFormDialog: function () { this.cardDialog.data = {} }, diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html index 0561cc018..eb29e5436 100644 --- a/lnbits/extensions/boltcards/templates/boltcards/index.html +++ b/lnbits/extensions/boltcards/templates/boltcards/index.html @@ -278,7 +278,8 @@ color="grey" icon="nfc" :disable="nfcTagReading" - >Tap card to scan UID (coming soon)Tap card to scan UID
From c35ae109a4bb91b0d0beb004f49c0de44ca6943b Mon Sep 17 00:00:00 2001 From: Lee Salminen Date: Mon, 29 Aug 2022 08:21:47 -0600 Subject: [PATCH 50/73] remove v-el from element, this was removed in Vue v2, ref does not appear to be used anywhere --- lnbits/extensions/boltcards/templates/boltcards/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html index eb29e5436..c4a53bd7b 100644 --- a/lnbits/extensions/boltcards/templates/boltcards/index.html +++ b/lnbits/extensions/boltcards/templates/boltcards/index.html @@ -332,7 +332,6 @@ class="q-ml-auto" v-on:click="generateKeys" v-on:click.right="debugKeys" - v-el:keybtn >Generate keys
From 630cc296fb56ddb33995b6c659add7c8d26f4163 Mon Sep 17 00:00:00 2001 From: Lee Salminen Date: Mon, 29 Aug 2022 08:22:51 -0600 Subject: [PATCH 51/73] do not add lnurlw:// to the string --- lnbits/extensions/boltcards/static/js/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/boltcards/static/js/index.js b/lnbits/extensions/boltcards/static/js/index.js index 315ed59fd..b59b712b7 100644 --- a/lnbits/extensions/boltcards/static/js/index.js +++ b/lnbits/extensions/boltcards/static/js/index.js @@ -16,7 +16,7 @@ new Vue({ return { toggleAdvanced: false, nfcTagReading: false, - lnurlLink: `lnurlw://${window.location.host}/boltcards/api/v1/scan/`, + lnurlLink: `${window.location.host}/boltcards/api/v1/scan/`, cards: [], hits: [], refunds: [], From 33f9ae9f66f8e1185f907691d467a8a9486d2f64 Mon Sep 17 00:00:00 2001 From: ben Date: Mon, 29 Aug 2022 15:37:31 +0100 Subject: [PATCH 52/73] Added some unique checks so only 1 record can be made per card --- lnbits/extensions/boltcards/migrations.py | 8 +- .../boltcards/templates/boltcards/index.html | 272 ++++-------------- lnbits/extensions/boltcards/views_api.py | 6 +- 3 files changed, 59 insertions(+), 227 deletions(-) diff --git a/lnbits/extensions/boltcards/migrations.py b/lnbits/extensions/boltcards/migrations.py index c20ef449c..5be3d08fb 100644 --- a/lnbits/extensions/boltcards/migrations.py +++ b/lnbits/extensions/boltcards/migrations.py @@ -5,10 +5,10 @@ async def m001_initial(db): await db.execute( """ CREATE TABLE boltcards.cards ( - id TEXT PRIMARY KEY, + id TEXT PRIMARY KEY UNIQUE, wallet TEXT NOT NULL, card_name TEXT NOT NULL, - uid TEXT NOT NULL, + uid TEXT NOT NULL UNIQUE, counter INT NOT NULL DEFAULT 0, tx_limit TEXT NOT NULL, daily_limit TEXT NOT NULL, @@ -30,7 +30,7 @@ async def m001_initial(db): await db.execute( """ CREATE TABLE boltcards.hits ( - id TEXT PRIMARY KEY, + id TEXT PRIMARY KEY UNIQUE, card_id TEXT NOT NULL, ip TEXT NOT NULL, spent BOOL NOT NULL DEFAULT True, @@ -48,7 +48,7 @@ async def m001_initial(db): await db.execute( """ CREATE TABLE boltcards.refunds ( - id TEXT PRIMARY KEY, + id TEXT PRIMARY KEY UNIQUE, hit_id TEXT NOT NULL, refund_amount INT NOT NULL, time TIMESTAMP NOT NULL DEFAULT """ diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html index ec2611a98..43901f724 100644 --- a/lnbits/extensions/boltcards/templates/boltcards/index.html +++ b/lnbits/extensions/boltcards/templates/boltcards/index.html @@ -12,33 +12,18 @@
Cards
- + Add card
- Export to CSV + Export to CSV
- + {% raw %}