mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-21 14:04:25 +01:00
adapt to bolt-nfc-android-app
This commit is contained in:
parent
293e5394a8
commit
0e5f6ac586
8 changed files with 504 additions and 351 deletions
|
@ -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.
|
||||
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.
|
||||
|
|
|
@ -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"])
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
+ """
|
||||
|
|
|
@ -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):
|
||||
|
|
299
lnbits/extensions/boltcards/static/js/index.js
Normal file
299
lnbits/extensions/boltcards/static/js/index.js
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
||||
})
|
|
@ -33,6 +33,7 @@
|
|||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
|
@ -42,6 +43,16 @@
|
|||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="visibility"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openQrCodeDialog(props.row.id)"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
|
@ -166,20 +177,33 @@
|
|||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="cardDialog.data.file_key"
|
||||
ref="k0"
|
||||
v-model.trim="cardDialog.data.k0"
|
||||
type="text"
|
||||
label="Card File key"
|
||||
hint="Used for CMAC of the message (16 bytes in HEX)."
|
||||
label="Card Auth key (K0)"
|
||||
hint="Used to authentificate with the card (16 bytes in HEX). "
|
||||
@randomkey
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="cardDialog.data.meta_key"
|
||||
ref="k1"
|
||||
v-model.trim="cardDialog.data.k1"
|
||||
type="text"
|
||||
label="Card Meta key"
|
||||
label="Card Meta key (K1)"
|
||||
hint="Used for encypting of the message (16 bytes in HEX)."
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
ref="k2"
|
||||
v-model.trim="cardDialog.data.k2"
|
||||
type="text"
|
||||
label="Card File key (K2)"
|
||||
hint="Used for CMAC of the message (16 bytes in HEX)."
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
|
@ -196,7 +220,7 @@
|
|||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
>Update Form</q-btn
|
||||
>Update Card</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-else
|
||||
|
@ -206,6 +230,14 @@
|
|||
type="submit"
|
||||
>Create Card</q-btn
|
||||
>
|
||||
<q-btn
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
v-on:click="generateKeys"
|
||||
v-on:click.right="debugKeys"
|
||||
>Generate keys</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
|
@ -213,249 +245,35 @@
|
|||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="qrCodeDialog.show" position="top">
|
||||
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
||||
{% raw %}
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode
|
||||
:value="qrCodeDialog.data.link"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
<p style="word-break: break-all" class="text-center">
|
||||
(QR code is for setting the keys with bolt-nfc-android-app)
|
||||
</p>
|
||||
<p style="word-break: break-all">
|
||||
<strong>Name:</strong> {{ qrCodeDialog.data.name }}<br />
|
||||
<strong>UID:</strong> {{ qrCodeDialog.data.uid }}<br />
|
||||
<strong>Lock key:</strong> {{ qrCodeDialog.data.k0 }}<br />
|
||||
<strong>Meta key:</strong> {{ qrCodeDialog.data.k1 }}<br />
|
||||
<strong>File key:</strong> {{ qrCodeDialog.data.k2 }}<br />
|
||||
</p>
|
||||
{% endraw %}
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
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: {}
|
||||
},
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
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)
|
||||
})
|
||||
},
|
||||
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.show = true
|
||||
},
|
||||
updateCard: function (wallet, data) {
|
||||
var self = this
|
||||
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()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<script src="/boltcards/static/js/index.js"></script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
# (use httpx just like requests, except instead of response.ok there's only the
|
||||
# response.is_error that is its inverse)
|
||||
|
||||
import secrets
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi.params import Depends, Query
|
||||
|
@ -23,10 +24,12 @@ from .crud import (
|
|||
delete_card,
|
||||
get_all_cards,
|
||||
get_card,
|
||||
get_card_by_otp,
|
||||
get_cards,
|
||||
get_hits,
|
||||
update_card,
|
||||
update_card_counter,
|
||||
update_card_otp,
|
||||
)
|
||||
from .models import CreateCardData
|
||||
from .nxp424 import decryptSUN, getSunMAC
|
||||
|
@ -46,7 +49,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(
|
||||
async def api_card_create_or_update(
|
||||
# req: Request,
|
||||
data: CreateCardData,
|
||||
card_id: str = None,
|
||||
|
@ -69,7 +72,7 @@ async def api_link_create_or_update(
|
|||
|
||||
|
||||
@boltcards_ext.delete("/api/v1/cards/{card_id}")
|
||||
async def api_link_delete(card_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
|
||||
async def api_card_delete(card_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
|
||||
card = await get_card(card_id)
|
||||
|
||||
if not card:
|
||||
|
@ -101,69 +104,17 @@ 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):
|
||||
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)
|
||||
|
||||
# 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"]
|
||||
|
||||
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):
|
||||
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
|
||||
@boltcards_ext.get("/api/v1/scan")
|
||||
async def api_scane(p, c, request: Request):
|
||||
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)
|
||||
)
|
||||
if cand.k1:
|
||||
card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(cand.k1))
|
||||
|
||||
if card_uid.hex().upper() == cand.uid:
|
||||
card = cand
|
||||
|
@ -172,9 +123,7 @@ async def api_scane(e, c, request: Request):
|
|||
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())
|
||||
if c != getSunMAC(card_uid, counter, bytes.fromhex(card.k2)).hex().upper():
|
||||
return {"status": "ERROR", "reason": "CMAC does not check."}
|
||||
|
||||
ctr_int = int.from_bytes(counter, "little")
|
||||
|
@ -196,3 +145,27 @@ async def api_scane(e, c, request: Request):
|
|||
|
||||
link = await get_withdraw_link(card.withdraw, 0)
|
||||
return link.lnurl_response(request)
|
||||
|
||||
|
||||
# /boltcards/api/v1/auth?a=00000000000000000000000000000000
|
||||
@boltcards_ext.get("/api/v1/auth")
|
||||
async def api_auth(a, request: Request):
|
||||
if a == "00000000000000000000000000000000":
|
||||
response = {"k0": "0" * 32, "k1": "1" * 32, "k2": "2" * 32}
|
||||
return response
|
||||
|
||||
card = await get_card_by_otp(a)
|
||||
|
||||
if not card:
|
||||
raise HTTPException(
|
||||
detail="Card does not exist.", status_code=HTTPStatus.NOT_FOUND
|
||||
)
|
||||
|
||||
new_otp = secrets.token_hex(16)
|
||||
print(card.otp)
|
||||
print(new_otp)
|
||||
await update_card_otp(new_otp, card.id)
|
||||
|
||||
response = {"k0": card.k0, "k1": card.k1, "k2": card.k2}
|
||||
|
||||
return response
|
||||
|
|
Loading…
Add table
Reference in a new issue