Compare commits

...
Sign in to create a new pull request.

84 commits

Author SHA1 Message Date
6ef61fe1af add option to have external port
Some checks failed
Go / check-formatting (push) Has been cancelled
Go / build-and-test (push) Has been cancelled
Go / build-docker-images (push) Has been cancelled
2025-01-20 01:09:49 +02:00
Peter Rounce
3a2096345d fix formatting 2024-06-08 10:05:50 +01:00
Peter Rounce
70638045dd
Merge pull request #79 from xx979xx/main
LN invoice expiry param & use it for ctx timeout
2024-06-08 09:38:46 +01:00
xx979xx
b3ac9e0a6c LN invoice expiry param & use it for ctx timeout 2024-06-03 13:33:30 +03:00
Peter Rounce
5564bd95c3
Merge pull request #78 from NicolasDorier/patch-2
Update DEEPLINK.md
2024-03-03 10:28:46 +00:00
Nicolas Dorier
8ceff1f8b8
Update DEEPLINK.md 2024-03-03 18:26:29 +09:00
Rob Clarkson
d041de3b6a
Merge pull request #77 from NicolasDorier/testvectordeeplink2
Add test vector for deeplinks
2024-01-22 21:42:11 +13:00
nicolas.dorier
ef1e2953c3
Add test vector for deeplinks 2024-01-22 17:41:59 +09:00
Rob Clarkson
51bdfc5cb9
Merge pull request #76 from NicolasDorier/deeplink
Add Deeplink spec
2024-01-22 19:31:19 +13:00
nicolas.dorier
e75f95e0f0
Add Deeplink spec 2024-01-11 20:22:11 +09:00
Peter Rounce
e60705ba49
Merge pull request #75 from NicolasDorier/qfointq
Remove the need to generate random CardKey
2023-10-28 12:52:52 +01:00
nicolas.dorier
51f09a6295
Remove the need to generate random CardKey 2023-10-25 13:57:29 +09:00
Peter Rounce
777bdb6081
Merge pull request #74 from NicolasDorier/fqpgtnrq
Remove BatchId concept, improve reset
2023-10-24 18:10:28 +01:00
nicolas.dorier
808336e084
Remove BatchId concept, improve reset 2023-10-24 16:52:10 +09:00
Peter Rounce
b0bd474dfc
Merge pull request #72 from NicolasDorier/fiowgnb
Derive deterministic keys on IssuerKey
2023-10-20 15:29:43 +01:00
nicolas.dorier
bdb350a177
Derive deterministic keys on IssuerKey 2023-10-20 23:14:22 +09:00
Peter Rounce
8379b3f88e
Merge pull request #71 from NicolasDorier/non-fixed-k0
Do not use a fixed K0 for deterministic keys
2023-10-20 06:21:37 +01:00
nicolas.dorier
70854d4508
Do not used a fixed K0 for deterministic keys 2023-10-20 10:50:51 +09:00
Peter Rounce
acb0c13a54
Merge pull request #70 from NicolasDorier/fixupdeterministic
Add security considerations to deterministic keys
2023-10-10 07:51:16 +01:00
Peter Rounce
d591092967
Update TECHNOLOGY.md 2023-10-10 07:30:05 +01:00
Peter Rounce
ed4c5db552
Update TECHNOLOGY.md 2023-10-10 07:29:31 +01:00
nicolas.dorier
1eb2b622ac
Add security considerations to deterministic keys 2023-10-10 10:43:59 +09:00
Peter Rounce
f1a8af86b6
Merge pull request #69 from NicolasDorier/deterministic
Add specification for deterministic bolt card keys
2023-10-09 14:06:09 +01:00
root
29a0168573 Add specification for deterministic bolt card keys 2023-10-09 21:58:36 +09:00
Peter Rounce
7745c9f20d
Update CARD_MANUAL.md 2023-09-23 17:02:52 +01:00
Peter Rounce
d5a64a8dab
Add files via upload 2023-09-23 16:58:19 +01:00
Peter Rounce
27ba1756ae
Add files via upload 2023-09-23 16:55:49 +01:00
Peter Rounce
c5921f6544
Update CARD_MANUAL.md 2023-09-23 16:22:37 +01:00
Peter Rounce
dc60a816c0
Add files via upload 2023-09-23 16:22:13 +01:00
Peter Rounce
165a46b9c1
Add files via upload 2023-09-23 16:20:58 +01:00
Peter Rounce
0712449103
Update CARD_MANUAL.md 2023-09-23 15:55:55 +01:00
Peter Rounce
94117718bd
update example
fixes #67
2023-09-22 08:13:24 +01:00
Peter Rounce
534367153a fix formatting 2023-09-16 18:36:56 +01:00
Peter Rounce
c36e19405d add cmac intermediate values 2023-09-16 18:36:14 +01:00
Peter Rounce
2b0392ea44 fix formatting 2023-09-16 15:28:36 +01:00
Peter Rounce
7d51fac18e Merge branch 'main' of https://github.com/boltcard/boltcard 2023-09-16 15:26:29 +01:00
Peter Rounce
46a83398b4 clarify counter value 2023-09-16 15:26:22 +01:00
Peter Rounce
36fc086e25
Update TEST_VECTORS.md 2023-09-16 12:28:38 +01:00
Peter Rounce
ae967cc011 Merge branch 'main' of https://github.com/boltcard/boltcard 2023-09-16 12:27:13 +01:00
Peter Rounce
3a1262db82 format 2023-09-16 12:27:02 +01:00
Peter Rounce
78a441baf5
Update TEST_VECTORS.md 2023-09-16 12:24:38 +01:00
Peter Rounce
2da9275c9a create some test vectors 2023-09-16 12:20:24 +01:00
Peter Rounce
34618bd228
Update SPEC.md 2023-08-09 11:52:37 +01:00
Peter Rounce
53ce60cfc3
update spec 2023-08-09 11:46:02 +01:00
Peter Rounce
056a52e1ba update parameter 2023-08-04 04:27:48 +00:00
Peter Rounce
797e4db605 make updating pin optional 2023-08-03 22:52:14 +00:00
Peter Rounce
8f83e04564 internalapi backward compatibility 2023-08-02 18:54:07 +00:00
Peter Rounce
4828610a5d update internalAPI 2023-07-31 06:37:03 +00:00
Peter Rounce
d2b6c30f3d improve logging 2023-07-27 18:15:35 +00:00
Peter Rounce
85bde475a3
Merge pull request #63 from boltcard/card-pin-number
update logging
2023-07-26 08:33:47 +01:00
Peter Rounce
df94c60fee fix formatting 2023-07-26 07:31:07 +00:00
Peter Rounce
3be7264ff1 update logging 2023-07-26 07:29:36 +00:00
Peter Rounce
de14c32867
Merge pull request #60 from boltcard/card-pin-number
Card pin number
2023-07-20 12:05:51 +01:00
Peter Rounce
6c03c1c3d9 check pin in payment rules 2023-07-20 11:00:37 +00:00
Chloe Jung
87306ca6db Format 2023-07-17 13:59:37 +12:00
Chloe Jung
598919fc2a Fix parse error 2023-07-17 12:16:55 +12:00
Chloe Jung
6609558f96 When returning boltcard data through api, send pin_enable and pin_limit_sats values as well 2023-07-17 11:59:07 +12:00
Peter Rounce
9b02a40cf3
Update FAQ.md 2023-07-06 06:32:25 +01:00
Peter Rounce
b43f580530
Update FAQ.md for 6982 error 2023-07-06 05:43:50 +01:00
Peter Rounce
ee942667e3
add FAQ 2023-07-06 05:35:53 +01:00
Peter Rounce
3d9e742dc1 pinLimit added to lnurlw response 2023-07-02 13:11:38 +00:00
Peter Rounce
105323a680 add testing 2023-07-02 07:23:58 +00:00
Peter Rounce
b76252d6ef create & update card pin details 2023-06-29 19:34:18 +00:00
Peter Rounce
299ab696cc
Update TECHNOLOGY.md 2023-05-14 06:41:05 +01:00
Peter Rounce
3b9db705f5
Merge pull request #56 from ponthief/main
support for SendGrid payments/balance emails
2023-05-01 12:53:19 +01:00
Djordje Kovacevic
77f1de8b7e support for SendGrid payments/balance emails 2023-05-01 08:49:38 +01:00
Peter Rounce
24082c831f
Merge pull request #54 from MizukiSonoko/default-description-uses-db-settings
Make DefaultDescription uses db settings
2023-04-22 10:10:18 +01:00
Peter Rounce
6cabfbf0d8
Merge pull request #55 from MizukiSonoko/aws-region-uses-db-settings
Make Aws Region uses db settings
2023-04-22 10:10:00 +01:00
Sonoko Mizuki
d3439db85b Set defualt value of AWS_REGION 2023-04-22 16:27:45 +09:00
Sonoko Mizuki
e9ef6973d4 set default value of default description 2023-04-22 16:21:10 +09:00
Peter Rounce
16df735233
Create TECHNOLOGY.md 2023-04-21 06:57:26 +01:00
Sonoko Mizuki
294a4eb054 Udpate setting.sql and docs 2023-04-20 12:12:06 +09:00
Sonoko Mizuki
7f8229cad0 Update email.go 2023-04-20 12:11:49 +09:00
Sonoko Mizuki
78a3dde2ed Update settings.sql and docs 2023-04-20 11:31:50 +09:00
Sonoko Mizuki
6661ebc7a4 Upade lnurlw_request 2023-04-20 11:31:35 +09:00
Peter Rounce
aa49fce3ff
Update CARD_PRIVACY.md 2023-04-15 10:54:15 +01:00
Peter Rounce
2165e248ca
Update CARD_PRIVACY.md 2023-04-15 10:50:35 +01:00
Peter Rounce
e249324e64
Merge pull request #52 from boltcard/lndhub-check-limit
Lndhub check limit
2023-03-29 19:57:17 +01:00
Peter Rounce
68f38e2347
Update CARD_PRIVACY.md 2023-03-28 08:29:52 +01:00
Peter Rounce
cd7fc1338d
add 'privacy' doc link 2023-03-28 08:24:47 +01:00
Peter Rounce
d9dfb49a23
work in progress 2023-03-28 08:24:19 +01:00
Peter Rounce
d7258bb4ad
Create CARD_PRIVACY.md 2023-03-28 07:45:23 +01:00
Peter Rounce
f5328ab7ed
add 424 doc links 2023-03-28 07:31:15 +01:00
Peter Rounce
3034f1a65d
add NXP NTAG 424 docs 2023-03-28 07:24:40 +01:00
40 changed files with 1384 additions and 129 deletions

1
.gitignore vendored
View file

@ -7,6 +7,7 @@
boltcard boltcard
createboltcard/createboltcard createboltcard/createboltcard
wipeboltcard/wipeboltcard wipeboltcard/wipeboltcard
cli/cli
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test

View file

@ -16,6 +16,7 @@ The simplest way to understand and set up your own system is to read the main do
| --- | --- | | --- | --- |
| [System](docs/SYSTEM.md) | Bolt card system overview | | [System](docs/SYSTEM.md) | Bolt card system overview |
| [Specification](docs/SPEC.md) | Bolt card specifications | | [Specification](docs/SPEC.md) | Bolt card specifications |
| [Privacy](docs/CARD_PRIVACY.md) | Bolt card privacy |
| [Docker Service Install](docs/DOCKER_INSTALL.md) | Bolt card service docker installation | | [Docker Service Install](docs/DOCKER_INSTALL.md) | Bolt card service docker installation |
| [Automatic Card Creation](docs/CARD_ANDROID.md) | Bolt card creation using the Bolt Card app| | [Automatic Card Creation](docs/CARD_ANDROID.md) | Bolt card creation using the Bolt Card app|
@ -26,6 +27,8 @@ The simplest way to understand and set up your own system is to read the main do
| [Manual Card Creation](docs/CARD_MANUAL.md) | Bolt card creation using NXP TagXplorer software | | [Manual Card Creation](docs/CARD_MANUAL.md) | Bolt card creation using NXP TagXplorer software |
| [LndHub Payments](docs/LNDHUB.md) | How to use LndHub | | [LndHub Payments](docs/LNDHUB.md) | How to use LndHub |
| [FAQ](docs/FAQ.md) | Frequently asked questions | | [FAQ](docs/FAQ.md) | Frequently asked questions |
| [Datasheet](docs/NT4H2421Gx.pdf) | NXP NTAG424DNA datasheet |
| [Application Note](docs/NT4H2421Gx.pdf) | NXP NTAG424DNA features and hints |
## Telegram group ## Telegram group

198
cli/main.go Normal file
View file

@ -0,0 +1,198 @@
package main
import (
"bytes"
"crypto/aes"
"encoding/hex"
"fmt"
"github.com/aead/cmac"
"github.com/boltcard/boltcard/crypto"
"os"
)
// inspired by parse_request() in lnurlw_request.go
func aes_cmac(key_sdm_file_read_mac []byte, sv2 []byte, ba_c []byte) (bool, error) {
c2, err := aes.NewCipher(key_sdm_file_read_mac)
if err != nil {
return false, err
}
ks, err := cmac.Sum(sv2, c2, 16)
if err != nil {
return false, err
}
fmt.Println("ks = ", ks)
c3, err := aes.NewCipher(ks)
if err != nil {
return false, err
}
cm, err := cmac.Sum([]byte{}, c3, 16)
if err != nil {
return false, err
}
fmt.Println("cm = ", cm)
ct := make([]byte, 8)
ct[0] = cm[1]
ct[1] = cm[3]
ct[2] = cm[5]
ct[3] = cm[7]
ct[4] = cm[9]
ct[5] = cm[11]
ct[6] = cm[13]
ct[7] = cm[15]
fmt.Println("ct = ", ct)
res_cmac := bytes.Compare(ct, ba_c)
if res_cmac != 0 {
return false, nil
}
return true, nil
}
func check_cmac(uid []byte, ctr []byte, k2_cmac_key []byte, cmac []byte) (bool, error) {
sv2 := make([]byte, 16)
sv2[0] = 0x3c
sv2[1] = 0xc3
sv2[2] = 0x00
sv2[3] = 0x01
sv2[4] = 0x00
sv2[5] = 0x80
sv2[6] = uid[0]
sv2[7] = uid[1]
sv2[8] = uid[2]
sv2[9] = uid[3]
sv2[10] = uid[4]
sv2[11] = uid[5]
sv2[12] = uid[6]
sv2[13] = ctr[2]
sv2[14] = ctr[1]
sv2[15] = ctr[0]
fmt.Println("sv2 = ", sv2)
cmac_verified, err := aes_cmac(k2_cmac_key, sv2, cmac)
if err != nil {
return false, err
}
return cmac_verified, nil
}
func main() {
fmt.Println("-- bolt card crypto test vectors --")
fmt.Println()
args := os.Args[1:]
if len(args) != 4 {
fmt.Println("error: should have arguments for: p c aes_decrypt_key aes_cmac_key")
os.Exit(1)
}
// get from args
p_hex := args[0]
c_hex := args[1]
aes_decrypt_key_hex := args[2]
aes_cmac_key_hex := args[3]
fmt.Println("p = ", p_hex)
fmt.Println("c = ", c_hex)
fmt.Println("aes_decrypt_key = ", aes_decrypt_key_hex)
fmt.Println("aes_cmac_key = ", aes_cmac_key_hex)
fmt.Println()
p, err := hex.DecodeString(p_hex)
if err != nil {
fmt.Println("ERROR: p not valid hex", err)
os.Exit(1)
}
c, err := hex.DecodeString(c_hex)
if err != nil {
fmt.Println("ERROR: c not valid hex", err)
os.Exit(1)
}
if len(p) != 16 {
fmt.Println("ERROR: p length not valid")
os.Exit(1)
}
if len(c) != 8 {
fmt.Println("ERROR: c length not valid")
os.Exit(1)
}
// decrypt p with aes_decrypt_key
aes_decrypt_key, err := hex.DecodeString(aes_decrypt_key_hex)
if err != nil {
fmt.Println("ERROR: DecodeString() returned an error", err)
os.Exit(1)
}
dec_p, err := crypto.Aes_decrypt(aes_decrypt_key, p)
if err != nil {
fmt.Println("ERROR: Aes_decrypt() returned an error", err)
os.Exit(1)
}
if dec_p[0] != 0xC7 {
fmt.Println("ERROR: decrypted data does not start with 0xC7 so is invalid")
os.Exit(1)
}
uid := dec_p[1:8]
ctr := make([]byte, 3)
ctr[0] = dec_p[10]
ctr[1] = dec_p[9]
ctr[2] = dec_p[8]
// set up uid & ctr for card record if needed
uid_str := hex.EncodeToString(uid)
ctr_str := hex.EncodeToString(ctr)
fmt.Println("decrypted card data : uid", uid_str, ", ctr", ctr_str)
// check cmac
aes_cmac_key, err := hex.DecodeString(aes_cmac_key_hex)
if err != nil {
fmt.Println("ERROR: aes_cmac_key is not valid hex", err)
os.Exit(1)
}
cmac_valid, err := check_cmac(uid, ctr, aes_cmac_key, c)
if err != nil {
fmt.Println("ERROR: check_cmac() returned an error", err)
os.Exit(1)
}
if cmac_valid == false {
fmt.Println("ERROR: cmac incorrect")
os.Exit(1)
}
fmt.Println("cmac validates ok")
os.Exit(0)
}

173
db/db.go
View file

@ -29,6 +29,9 @@ type Card struct {
One_time_code string One_time_code string
Card_name string Card_name string
Allow_negative_balance string Allow_negative_balance string
Pin_enable string
Pin_number string
Pin_limit_sats int
} }
type Payment struct { type Payment struct {
@ -350,7 +353,8 @@ func Get_card_from_card_id(card_id int) (*Card, error) {
`last_counter_value, lnurlw_request_timeout_sec, ` + `last_counter_value, lnurlw_request_timeout_sec, ` +
`lnurlw_enable, tx_limit_sats, day_limit_sats, ` + `lnurlw_enable, tx_limit_sats, day_limit_sats, ` +
`email_enable, email_address, card_name, ` + `email_enable, email_address, card_name, ` +
`allow_negative_balance FROM cards WHERE card_id=$1;` `allow_negative_balance, pin_enable, pin_number, ` +
`pin_limit_sats FROM cards WHERE card_id=$1;`
row := db.QueryRow(sqlStatement, card_id) row := db.QueryRow(sqlStatement, card_id)
err = row.Scan( err = row.Scan(
&c.Card_id, &c.Card_id,
@ -364,7 +368,10 @@ func Get_card_from_card_id(card_id int) (*Card, error) {
&c.Email_enable, &c.Email_enable,
&c.Email_address, &c.Email_address,
&c.Card_name, &c.Card_name,
&c.Allow_negative_balance) &c.Allow_negative_balance,
&c.Pin_enable,
&c.Pin_number,
&c.Pin_limit_sats)
if err != nil { if err != nil {
return &c, err return &c, err
} }
@ -385,7 +392,7 @@ func Get_card_from_card_name(card_name string) (*Card, error) {
sqlStatement := `SELECT card_id, k2_cmac_key, uid,` + sqlStatement := `SELECT card_id, k2_cmac_key, uid,` +
` last_counter_value, lnurlw_request_timeout_sec,` + ` last_counter_value, lnurlw_request_timeout_sec,` +
` lnurlw_enable, tx_limit_sats, day_limit_sats` + ` lnurlw_enable, tx_limit_sats, day_limit_sats, pin_enable, pin_limit_sats` +
` FROM cards WHERE card_name=$1 AND wiped = 'N';` ` FROM cards WHERE card_name=$1 AND wiped = 'N';`
row := db.QueryRow(sqlStatement, card_name) row := db.QueryRow(sqlStatement, card_name)
err = row.Scan( err = row.Scan(
@ -396,7 +403,9 @@ func Get_card_from_card_name(card_name string) (*Card, error) {
&c.Lnurlw_request_timeout_sec, &c.Lnurlw_request_timeout_sec,
&c.Lnurlw_enable, &c.Lnurlw_enable,
&c.Tx_limit_sats, &c.Tx_limit_sats,
&c.Day_limit_sats) &c.Day_limit_sats,
&c.Pin_enable,
&c.Pin_limit_sats)
if err != nil { if err != nil {
return &c, err return &c, err
} }
@ -830,6 +839,71 @@ func Insert_card(one_time_code string, k0_auth_key string, k2_cmac_key string, k
return nil return nil
} }
func Insert_card_with_pin(one_time_code string, k0_auth_key string, k2_cmac_key string, k3 string, k4 string,
tx_limit_sats int, day_limit_sats int, lnurlw_enable bool, card_name string, uid_privacy bool,
allow_neg_bal_ptr bool, pin_enable bool, pin_number string, pin_limit_sats int) error {
lnurlw_enable_yn := "N"
if lnurlw_enable {
lnurlw_enable_yn = "Y"
}
uid_privacy_yn := "N"
if uid_privacy {
uid_privacy_yn = "Y"
}
allow_neg_bal_yn := "N"
if allow_neg_bal_ptr {
allow_neg_bal_yn = "Y"
}
pin_enable_yn := "N"
if pin_enable {
pin_enable_yn = "Y"
}
db, err := open()
if err != nil {
return err
}
defer db.Close()
// ensure any cards with the same card_name are wiped
sqlStatement := `UPDATE cards SET` +
` lnurlw_enable = 'N', lnurlp_enable = 'N', email_enable = 'N', wiped = 'Y'` +
` WHERE card_name = $1;`
res, err := db.Exec(sqlStatement, card_name)
if err != nil {
return err
}
// insert a new record into cards
sqlStatement = `INSERT INTO cards` +
` (one_time_code, k0_auth_key, k2_cmac_key, k3, k4, uid, last_counter_value,` +
` lnurlw_request_timeout_sec, tx_limit_sats, day_limit_sats, lnurlw_enable,` +
` one_time_code_used, card_name, uid_privacy, allow_negative_balance,` +
` pin_enable, pin_number, pin_limit_sats)` +
` VALUES ($1, $2, $3, $4, $5, '', 0, 60, $6, $7, $8, 'N', $9, $10, $11, $12, $13, $14);`
res, err = db.Exec(sqlStatement, one_time_code, k0_auth_key, k2_cmac_key, k3, k4,
tx_limit_sats, day_limit_sats, lnurlw_enable_yn, card_name, uid_privacy_yn,
allow_neg_bal_yn, pin_enable_yn, pin_number, pin_limit_sats)
if err != nil {
return err
}
count, err := res.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return errors.New("not one card record inserted")
}
return nil
}
func Wipe_card(card_name string) (*Card_wipe_info, error) { func Wipe_card(card_name string) (*Card_wipe_info, error) {
card_wipe_info := Card_wipe_info{} card_wipe_info := Card_wipe_info{}
@ -871,7 +945,8 @@ func Wipe_card(card_name string) (*Card_wipe_info, error) {
return &card_wipe_info, nil return &card_wipe_info, nil
} }
func Update_card(card_name string, lnurlw_enable bool, tx_limit_sats int, day_limit_sats int) error { func Update_card(card_name string, lnurlw_enable bool, tx_limit_sats int,
day_limit_sats int) error {
lnurlw_enable_yn := "N" lnurlw_enable_yn := "N"
if lnurlw_enable { if lnurlw_enable {
@ -907,3 +982,91 @@ func Update_card(card_name string, lnurlw_enable bool, tx_limit_sats int, day_li
return nil return nil
} }
func Update_card_with_pin(card_name string, lnurlw_enable bool, tx_limit_sats int, day_limit_sats int,
pin_enable bool, pin_number string, pin_limit_sats int) error {
lnurlw_enable_yn := "N"
if lnurlw_enable {
lnurlw_enable_yn = "Y"
}
pin_enable_yn := "N"
if pin_enable {
pin_enable_yn = "Y"
}
db, err := open()
if err != nil {
return err
}
defer db.Close()
sqlStatement := `UPDATE cards SET lnurlw_enable = $2, tx_limit_sats = $3, day_limit_sats = $4, ` +
`pin_enable = $5, pin_number = $6, pin_limit_sats = $7 WHERE card_name = $1 AND wiped = 'N';`
res, err := db.Exec(sqlStatement, card_name, lnurlw_enable_yn, tx_limit_sats, day_limit_sats,
pin_enable_yn, pin_number, pin_limit_sats)
if err != nil {
return err
}
count, err := res.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return errors.New("not one card record updated")
}
return nil
}
func Update_card_with_part_pin(card_name string, lnurlw_enable bool, tx_limit_sats int, day_limit_sats int,
pin_enable bool, pin_limit_sats int) error {
lnurlw_enable_yn := "N"
if lnurlw_enable {
lnurlw_enable_yn = "Y"
}
pin_enable_yn := "N"
if pin_enable {
pin_enable_yn = "Y"
}
db, err := open()
if err != nil {
return err
}
defer db.Close()
sqlStatement := `UPDATE cards SET lnurlw_enable = $2, tx_limit_sats = $3, day_limit_sats = $4, ` +
`pin_enable = $5, pin_limit_sats = $6 WHERE card_name = $1 AND wiped = 'N';`
res, err := db.Exec(sqlStatement, card_name, lnurlw_enable_yn, tx_limit_sats, day_limit_sats,
pin_enable_yn, pin_limit_sats)
if err != nil {
return err
}
count, err := res.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return errors.New("not one card record updated")
}
return nil
}

View file

@ -32,3 +32,4 @@ sed -i "s/[(]'FEE_LIMIT_PERCENT'[^)]*[)]/(\'FEE_LIMIT_PERCENT\', \'0.5\')/" sql/
sed -i "s/[(]'FUNCTION_LNURLW'[^)]*[)]/(\'FUNCTION_LNURLW\', \'ENABLE\')/" sql/settings.sql sed -i "s/[(]'FUNCTION_LNURLW'[^)]*[)]/(\'FUNCTION_LNURLW\', \'ENABLE\')/" sql/settings.sql
sed -i "s/[(]'FUNCTION_LNURLP'[^)]*[)]/(\'FUNCTION_LNURLP\', \'DISABLE\')/" sql/settings.sql sed -i "s/[(]'FUNCTION_LNURLP'[^)]*[)]/(\'FUNCTION_LNURLP\', \'DISABLE\')/" sql/settings.sql
sed -i "s/[(]'FUNCTION_EMAIL'[^)]*[)]/(\'FUNCTION_EMAIL\', \'DISABLE\')/" sql/settings.sql sed -i "s/[(]'FUNCTION_EMAIL'[^)]*[)]/(\'FUNCTION_EMAIL\', \'DISABLE\')/" sql/settings.sql
sed -i "s/[(]'LN_INVOICE_EXPIRY_SEC'[^)]*[)]/(\'LN_INVOICE_EXPIRY_SEC\', \'3600\')/" sql/settings.sql

BIN
docs/AN12196.pdf Normal file

Binary file not shown.

View file

@ -46,7 +46,9 @@ lnurlw://card.yourdomain.com/ln
``` ```
lnurlw://card.yourdomain.com/ln?p=00000000000000000000000000000000&c=0000000000000000 lnurlw://card.yourdomain.com/ln?p=00000000000000000000000000000000&c=0000000000000000
``` ```
- click after `p=` and note the p_position (41 in this case) - click after `p=` and note the p_position (41 in this case)
![find the p_position](images/posn-p.webp)
- click after `c=` and note the c_position (76 in this case) - click after `c=` and note the c_position (76 in this case)
- select `Write To Tag` - select `Write To Tag`
@ -101,7 +103,7 @@ lnurlw://card.yourdomain.com/ln?p=00000000000000000000000000000000&c=00000000000
- set up the values in the order shown - set up the values in the order shown
![file and SDM options with field entry order](images/fs-add.webp) ![file and SDM options with field entry order](images/fs-add-2.webp)
- select `Change File Settings` - select `Change File Settings`

64
docs/CARD_PRIVACY.md Normal file
View file

@ -0,0 +1,64 @@
# Card Privacy
## Payment tracking
This document describes the different levels of privacy possible with bolt card implementations.
## Card NDEF
The URI that is programmed into the card and returned as the NDEF consists of three parts.
1. The static part
2. The encrypted part
3. The authentication part
```
lnurlw://card.yourdomain.com/ln?p=A2EF40F6D46F1BB36E6EBF0114D4A464&c=F509EEA788E37E32 URI example
lnurlw://card.yourdomain.com/ln?p= &c= static
A2EF40F6D46F1BB36E6EBF0114D4A464 encrypted
F509EEA788E37E32 authentication
```
| part | use |
|------|-----|
| static | specfying the protocol and service location as a URI |
| encrypted | unique id and counter values encrypted by the card |
| authentication | a value to authenticate that the entire URI is as generated by the card |
## Card privacy levels
In order for the system to work, the card must provide the point-of-sale with a URL for the backend server.
For maximum privacy, it should not be possible for the point-of-sale to identify the card any further than this.
Unfortunately, early implementations do not have this fully built out.
You can check your card/s by reading the NDEF value (e.g. with the NXP TagInfo app) to check for a static identifier or a static UID value. This will enable you to find the level of privacy that has been implemented on creating the card.
| Privacy level | Static id | UID plaintext|
| ------------- | --------- | ------------ |
| minimal | yes | yes |
| good | no | yes |
| best | no | no |
### Minimal privacy (aka tracker)
An identifier is included in the static part of the lnurlw.
This is used on the server side to look up the decryption key and the authentication key per card.
This is how early systems were implemented and allows the point-of-sale devices to track the use of the card.
### Good privacy
There is no identifier included in the static part of the lnurlw.
This is made possible by holding the decryption key at database level.
The authentication key is still recorded per card.
This protects against leaking of point-of-sale databases and log files, however, a untrustworthy point-of-sale could still obtain the card UID using proprietary NXP commands.
### Best privacy
There is no identifier included in the static part of the lnurlw.
In addition, the UID field is made inaccessible by NXP proprietary commands by using the Random ID feature.
This protects against individual card tracking by trustworthy and untrustworthy point-of-sale systems.

113
docs/DEEPLINK.md Normal file
View file

@ -0,0 +1,113 @@
## Abstract
Boltcard NFC Programmer App is a native app on iOS and Android able to program or reset NTag424 into a Boltcard, the typical steps in the setup a Boltcard are:
1. The `Boltcard Service` generates the keys, and format them into a QR Code
2. The user opens the Boltcard NFC Programmer, go to `Create Bolt Card`, scans the QR code
3. The user then taps the card
The QR code contains all the keys necessary for the app to create the Boltcard.
Here are the shortcomings of this process that we aim to address in this specification:
1. If the QR code get displayed on the mobile device itself, it is difficult to scan it
2. The `Boltcard Service`, not knowing the UID when the keys are requested, isn't able to assign a specific pair of keys for the NTag424 being setup (for example, the [deterministic key generation](./DETERMINISTIC.md) needs the UID before generating the keys)
## Boltcard deeplinks
The solution is for the `Boltcard Service` to generate deep links with the following format: `boltcard://[program|reset]?url=[keys-request-url]`.
When clicked, `Boltcard NFC Programmer` would open and either allow the user to program their NTag424 or reset it after asking for the NTags keys to the `keys-request-url`.
The `Boltcard NFC Programmer` should send an HTTP POST request with `Content-Type: application/json` in the following format:
```json
{
"UID": "[UID]"
}
```
Or
```json
{
"LNURLW": "lnurlw://..."
}
```
In `curl`:
```bash
curl -X POST "[keys-request-url]" -H "Content-Type: application/json" -d '{"UID": "[UID]"}'
```
* `UID` needs to be 7 bytes. (Program action)
* `LNURLW` needs to be read from the Boltcard's NDEF and can be sent in place of `UID`. It must contains the `p=` and `c=` arguments of the Boltcard. (Reset action)
The response will be similar to the format of the QR code:
```json
{
"LNURLW": "lnurlw://...",
"K0":"[Key0]",
"K1":"[Key1]",
"K2":"[Key2]",
"K3":"[Key3]",
"K4":"[Key4]"
}
```
## The Program action
If `program` is specified in the Boltcard link, the `Boltcard NFC Programmer` must:
1. Check if the lnurlw `NDEF` can be read.
* If the record can be read, then the card isn't blank, an error should be displayed to the user to first reset the Boltcard.
* If the record can't be read, assume `K0` is `00000000000000000000000000000000` authenticate and call `GetUID` on the card again. (Since `GetUID` is called after authentication, the real `UID` will be returned even if `Random UID` has been activated)
2. Send a request to the `keys-request-url` using the UID as explained above to get the NTag424 app keys
3. Program the Boltcard with the app keys
## The Reset action
If `reset` is specified in the Boltcard link, the `Boltcard NFC Programmer` must:
1. Check if the lnurlw `NDEF` can be read.
* If the record can't be read, then the card is already reset, show an error message to the user.
* If the record can be read, continue to step 2.
2. Send a request to the `keys-request-url` using the lnurlw as explained above to get the NTag424 app keys
3. Reset the Boltcard to factory state with the app keys
## Handling setup/reset cycles for Boltcard Services
When a NTag424 is reset, its counter is reset too.
This means that if the user:
* Setup a Boltcard
* Make `5` payments
* Reset the Boltcard
* Setup the Boltcard on same `keys-request-url`
With a naive implementation, the server will expect the next counter to be above `5`, but the next payment will have a counter of `0`.
More precisely, the user will need to tap the card `5` times before being able to use the Boltcard for a payment successfully again.
To avoid this issue the `Boltcard Service`, if using [Deterministic key generation](./DETERMINISTIC.md), should ensure it updates the key version during a `program` action.
This can be done easily by the `Boltcard Service` by adding a parameter in the `keys-request-url` which specifies that the version need to be updated.
When the `Boltcard NFC Programmer` queries the URL with the UID of the card, the `Boltcard Service` will detect this parameter, and update the version.
## Test vectors
Here is an example of two links for respectively program the Boltcard and Reset it.
```html
<p>
<a id="SetupBoltcard" href="boltcard://program?url=https%3A%2F%2Flocalhost%3A14142%2Fapi%2Fv1%2Fpull-payments%2FfUDXsnySxvb5LYZ1bSLiWzLjVuT%2Fboltcards%3FonExisting%3DUpdateVersion" target="_blank">
Setup Boltcard
</a>
<span>&nbsp;|&nbsp;</span>
<a id="ResetBoltcard" href="boltcard://reset?url=https%3A%2F%2Flocalhost%3A14142%2Fapi%2Fv1%2Fpull-payments%2FfUDXsnySxvb5LYZ1bSLiWzLjVuT%2Fboltcards%3FonExisting%3DKeepVersion" target="_blank">
Reset Boltcard
</a>
</p>
```

216
docs/DETERMINISTIC.md Normal file
View file

@ -0,0 +1,216 @@
## Abstract
The NXP NTAG424DNA allows applications to configure five application keys, named `K0`, `K1`, `K2`, `K3`, and `K4`. In the BoltCard configuration:
* `K0` is the `App Master Key`, it is the only key permitted to change the application keys.
* `K1` serves as the `encryption key` for the `PICCData`, represented by the `p=` parameter.
* `K2` is the `authentication key` used for calculating the SUN MAC of the `PICCData`, represented by the `c=` parameter.
* `K3` and `K4` are not used but should be configured as recommended in the [NTag424 application notes](https://www.nxp.com/docs/en/application-note/AN12196.pdf).
A simple approach to issuing BoltCards would involve randomly generating the five different keys and storing them in a database.
When a validation request is made, the verifier would attempt to decrypt the `p=` parameter using all existing encryption keys until finding a match. Once decrypted, the `p=` parameter would reveal the card's uid, which can then be used to retrieve the remaining keys.
The primary drawback of this method is its lack of scalability. If many cards have been issued, identifying the correct encryption key could become a computationally intensive task.
In this document, we propose a solution to this issue.
## Keys generation
First, the `LNUrl Withdraw Service` generates a `IssuerKey` that it will use to generate the keys for every NTag424.
Then, configure a BoltCard as follows:
* `CardKey = PRF(IssuerKey, '2d003f75' || UID || Version)`
* `K0 = PRF(CardKey, '2d003f76')`
* `K1 = PRF(IssuerKey, '2d003f77')`
* `K2 = PRF(CardKey, '2d003f78')`
* `K3 = PRF(CardKey, '2d003f79')`
* `K4 = PRF(CardKey, '2d003f7a')`
* `ID = PRF(IssuerKey, '2d003f7b' || UID)`
With the following parameters:
* `IssuerKey`: This 16-bytes key is used by an `LNUrl Withdraw Service` to setup all its BoltCards.
* `UID`: This is the 7-byte ID of the card. You can retrieve it from the NTag424 using the `GetCardUID` function after identification with K1, or by decrypting the `p=` parameter, also known as `PICCData`.
* `Version`: A 4-bytes little endian version number. This must be incremented every time the user re-programs (reset/setup) the same BoltCard on the same `LNUrl Withdraw Service`.
The Pseudo Random Function `PRF(key, message)` applied during the key generation is the CMAC algorithm described in NIST Special Publication 800-38B. [See implementation notes](#notes)
## How to setup a new BoltCard
1. Execute `ReadData` or `ISOReaDBinary` on the BoltCard to ensure the card is blank.
2. Execute `AuthenticateEV2First` with the application key `00000000000000000000000000000000`
3. Fetch the `UID` with `GetCardUID`.
4. Calculate `ID`
5. Fetch the `State` and `Version` of the BoltCard with the specified `ID` from the database.
6. Ensure either:
* If no BoltCard is found, insert an entry in the database with `Version=0` and its state set to `Configured`.
* If a BoltCard is found and its state is `Reset` then increment `Version` by `1`, and change its state to `Configured`.
7. Generate `CardKey` with `UID` and `Version`.
8. Calculate `K0`, `K1`, `K2`, `K3`, `K4`.
9. [Setup the BoltCard](./CARD_MANUAL.md).
## How to implement a Reset feature
If a `LNUrl Withdraw Service` offers a factory reset feature for a user's BoltCard, here is the recommended procedure:
1. Read the NDEF lnurlw URL, extract `p=` and `c=`.
2. Derive `Encryption Key (K1)`, decrypt `p=` to obtain the `PICCData`.
3. Check `PICCData[0] == 0xc7`.
4. Calculate `ID` with the `UID` from the `PICCData`.
5. Fetch the BoltCard's `Version` with `ID` from the database.
6. Ensure the BoltCard's state is `Configured`.
7. Generate `CardKey` with `UID` and `Version`.
8. Derive `K0`, `K2`, `K3`, `K4` with `CardKey` and the `UID`.
9. Verify that the SUN MAC in `c=` matches the one calculated using `Authentication Key (K2)`.
10. Execute `AuthenticateEV2First` with `K0`
11. Erase the NDEF data file using `WriteData` or `ISOUpdateBinary`
12. Restore the NDEF file settings to default values with `ChangeFileSettings`.
13. Use `ChangeKey` with the recovered application keys to reset `K4` through `K0` to `00000000000000000000000000000000`.
14. Update the BoltCard's state to `Reset` in the database.
Rational: Attempting to call `AuthenticateEV2First` without validating the `p=` and `c=` parameters could render the NTag inoperable after a few attempts.
## How to implement a verification
If a `LNUrl Withdraw Service` needs to verify a payment request, follow these steps:
1. Read the NDEF lnurlw URL, extract `p=` and `c=`.
2. Derive `Encryption Key (K1)`, decrypts `p=` to get the `PICCData`.
3. Check `PICCData[0] == 0xc7`.
4. Calculate `ID` with the `UID` from the `PICCData`.
5. Fetch the BoltCard's `Version` with `ID` from the database.
6. Ensure the BoltCard's state in the database is not `Reset`.
7. Generate `CardKey` with `UID` and `Version`.
8. Derive `Authentication Key (K2)` with `CardKey` and the `UID`.
9. Verify that the SUN MAC in `c=` matches the one calculated using `Authentication Key (K2)`.
10. Confirm that the last-seen counter for `ID` is lower than what is stored in `counter=PICCData[8..11]`. (Little Endian)
11. Update the last-seen counter.
Rationale: The `ID` is calculated to prevent the exposure of the `UID` in the `LNUrl Withdraw Service` database. This approach provides both privacy and security. Specifically, because the `UID` is used to derive keys, it is preferable not to store it outside the NTag.
## Multiple IssuerKeys
A single `LNUrl Withdraw Service` can own multiple `IssuerKeys`. In such cases, it will need to attempt them all to decrypt `p=`, and pick the first one which satisfies `PICCData[0] == 0xc7` and verifies the `c=` checksum.
Using multiple `IssuerKeys` can decrease the impact of a compromised `Encryption Key (K1)` at the cost of performance.
## Security consideration
### K1 security
Since `K1` is shared among multiple BoltCards, the security of this scheme is based on the following assumptions:
* `K1` cannot be extracted from a legitimate NTag424.
* BoltCard setup occurs in a trusted environment.
While NXP gives assurance keys can't be extracted, a non genuine NTag424 could potentially expose these keys.
Furthermore, because blank NTag424 uses the well-known initial application keys `00000000000000000000000000000000`, communication between the PCD and the PICC could be intercepted. If the BoltCard setup does not occur in a trusted environment, `K1` could be exposed during the calls to `ChangeKey`.
However, if `K1` is compromised, the attacker still cannot produce a valid checksum and can only recover the `UID` for tracking purposes.
Note that verifying the signature returned by `Read_Sig` can only prove NXP issued a card with a specific `UID`. It cannot prove that the current communication channel is established with an authentic NTag424. This is because the signature returned by `Read_Sig` covers only the `UID` and can therefore be replayed by a non-genuine NTag424.
### Issuer database security
If the issuer's database is compromised, revealing both the IssuerKey and CardKeys, it would still be infeasible for an attacker to derive `K2` and thus to forge signatures for an arbitrary card.
This is because the database only stores `ID` and not the `UID` itself.
## Implementation notes {#notes}
Here is a C# implementation of the CMAC algorithm described in NIST Special Publication 800-38B.
```csharp
public byte[] CMac(byte[] data)
{
var key = _bytes;
// SubKey generation
// step 1, AES-128 with key K is applied to an all-zero input block.
byte[] L = AesEncrypt(key, new byte[16], new byte[16]);
// step 2, K1 is derived through the following operation:
byte[]
FirstSubkey =
RotateLeft(L); //If the most significant bit of L is equal to 0, K1 is the left-shift of L by 1 bit.
if ((L[0] & 0x80) == 0x80)
FirstSubkey[15] ^=
0x87; // Otherwise, K1 is the exclusive-OR of const_Rb and the left-shift of L by 1 bit.
// step 3, K2 is derived through the following operation:
byte[]
SecondSubkey =
RotateLeft(FirstSubkey); // If the most significant bit of K1 is equal to 0, K2 is the left-shift of K1 by 1 bit.
if ((FirstSubkey[0] & 0x80) == 0x80)
SecondSubkey[15] ^=
0x87; // Otherwise, K2 is the exclusive-OR of const_Rb and the left-shift of K1 by 1 bit.
// MAC computing
if (((data.Length != 0) && (data.Length % 16 == 0)) == true)
{
// If the size of the input message block is equal to a positive multiple of the block size (namely, 128 bits),
// the last block shall be exclusive-OR'ed with K1 before processing
for (int j = 0; j < FirstSubkey.Length; j++)
data[data.Length - 16 + j] ^= FirstSubkey[j];
}
else
{
// Otherwise, the last block shall be padded with 10^i
byte[] padding = new byte[16 - data.Length % 16];
padding[0] = 0x80;
data = data.Concat(padding.AsEnumerable()).ToArray();
// and exclusive-OR'ed with K2
for (int j = 0; j < SecondSubkey.Length; j++)
data[data.Length - 16 + j] ^= SecondSubkey[j];
}
// The result of the previous process will be the input of the last encryption.
byte[] encResult = AesEncrypt(key, new byte[16], data);
byte[] HashValue = new byte[16];
Array.Copy(encResult, encResult.Length - HashValue.Length, HashValue, 0, HashValue.Length);
return HashValue;
}
static byte[] RotateLeft(byte[] b)
{
byte[] r = new byte[b.Length];
byte carry = 0;
for (int i = b.Length - 1; i >= 0; i--)
{
ushort u = (ushort)(b[i] << 1);
r[i] = (byte)((u & 0xff) + carry);
carry = (byte)((u & 0xff00) >> 8);
}
return r;
}
```
## Implementation
* [BTCPayServer.BoltCardTools](https://github.com/btcpayserver/BTCPayServer.BoltCardTools), a BoltCard/NTag424 library in C#.
## Test vectors
Input:
```
UID: 04a39493cc8680
Issuer Key: 00000000000000000000000000000001
Version: 1
```
Expected:
```
K0: a29119fcb48e737d1591d3489557e49b
K1: 55da174c9608993dc27bb3f30a4a7314
K2: f4b404be700ab285e333e32348fa3d3b
K3: 73610ba4afe45b55319691cb9489142f
K4: addd03e52964369be7f2967736b7bdb5
ID: e07ce1279d980ecb892a81924b67bf18
CardKey: ebff5a4e6da5ee14cbfe720ae06fbed9
```

View file

@ -25,3 +25,12 @@ It can be useful to test paying invoices directly from your lightning node.
# Can I use the same lightning node for the customer (bolt card) and the merchant (POS) ? # Can I use the same lightning node for the customer (bolt card) and the merchant (POS) ?
When tested with LND in Nov 2022, the paying (customer, bolt card) lightning node must be a separate instance to the invoicing (merchant, POS) lightning node. When tested with LND in Nov 2022, the paying (customer, bolt card) lightning node must be a separate instance to the invoicing (merchant, POS) lightning node.
# I get a 6982 error when trying to program a blank card
A 6982 error is is known to happen after trying to use a 'blank' card which has been wiped with the CoinCorner customer app (July 2023) and happens because the card settings have not been cleared down correctly. It can also happen where a card is removed partway through programming (which can take a few seconds) or where the mobile device does not complete programming due to being in a low battery situation.
The card settings can be fixed by using the 'Bolt Card NFC Card Creator' app. The card will then be blank and usable again.
- Reset Keys
- Enter all '0's in Key 0 until the field is full and copy to Keys 1-4
- Reset Card Now
- present the card

View file

@ -106,6 +106,7 @@ add email notifications for payments and fund receipt
`AWS_SES_ID=..."` (settings table) `AWS_SES_ID=..."` (settings table)
`AWS_SES_SECRET=..."` (settings table) `AWS_SES_SECRET=..."` (settings table)
`AWS_SES_EMAIL_FROM=..."` (settings table) `AWS_SES_EMAIL_FROM=..."` (settings table)
`AWS_REGION=...` (settings table)
`FUNCTION_EMAIL=ENABLE"` (settings table) `FUNCTION_EMAIL=ENABLE"` (settings table)
`cards.email_address='card.notifications@yourdomain.com'` `cards.email_address='card.notifications@yourdomain.com'`
`cards.email_enable='Y'` `cards.email_enable='Y'`

BIN
docs/NT4H2421Gx.pdf Normal file

Binary file not shown.

View file

@ -23,10 +23,15 @@ Here are the descriptions of values available to use in the `settings` table:
| FUNCTION_LNURLW | ENABLE | system level switch for LNURLw (bolt card) services | | FUNCTION_LNURLW | ENABLE | system level switch for LNURLw (bolt card) services |
| FUNCTION_LNURLP | DISABLE | system level switch for LNURLp (lightning address) services | | FUNCTION_LNURLP | DISABLE | system level switch for LNURLp (lightning address) services |
| FUNCTION_EMAIL | DISABLE | system level switch for email updates on credits & debits | | FUNCTION_EMAIL | DISABLE | system level switch for email updates on credits & debits |
| DEFAULT_DESCRIPTION | 'bolt card service' | default description of payment |
| AWS_SES_ID | | Amazon Web Services - Simple Email Service - access id | | AWS_SES_ID | | Amazon Web Services - Simple Email Service - access id |
| AWS_SES_SECRET | | Amazon Web Services - Simple Email Service - access secret | | AWS_SES_SECRET | | Amazon Web Services - Simple Email Service - access secret |
| AWS_SES_EMAIL_FROM | | Amazon Web Services - Simple Email Service - email from field | | AWS_SES_EMAIL_FROM | | Amazon Web Services - Simple Email Service - email from field |
| AWS_REGION | us-east-1 | Amazon Web Services - Account region |
| EMAIL_MAX_TXS | | maximum number of transactions to include in the email body | | EMAIL_MAX_TXS | | maximum number of transactions to include in the email body |
| FUNCTION_LNDHUB | DISABLE | system level switch for using LNDHUB in place of LND | | FUNCTION_LNDHUB | DISABLE | system level switch for using LNDHUB in place of LND |
| LNDHUB_URL | | URL for the LNDHUB service | | LNDHUB_URL | | URL for the LNDHUB service |
| FUNCTION_INTERNAL_API | DISABLE | system level switch for activating the internal API | | FUNCTION_INTERNAL_API | DISABLE | system level switch for activating the internal API |
| SENDGRID_API_KEY | | User API Key from SendGrid.com |
| SENDGRID_EMAIL_SENDER | | Single Sender email address verified by SendGrid |
| LN_INVOICE_EXPIRY_SEC | 3600 | LN invoice's expiry time in seconds |

View file

@ -3,11 +3,21 @@
The bolt card system is built on the technologies listed below. The bolt card system is built on the technologies listed below.
- [LUD-03: withdrawRequest base spec.](https://github.com/fiatjaf/lnurl-rfc/blob/luds/03.md) - [LUD-03: withdrawRequest base spec.](https://github.com/fiatjaf/lnurl-rfc/blob/luds/03.md)
- with the exception of maxWithdrawable which must be returned as higher than the actual maximum amount
- [LUD-17: Protocol schemes and raw (non bech32-encoded) URLs.](https://github.com/fiatjaf/lnurl-rfc/blob/luds/17.md) - [LUD-17: Protocol schemes and raw (non bech32-encoded) URLs.](https://github.com/fiatjaf/lnurl-rfc/blob/luds/17.md)
- NFC Data Exchange Format (NDEF) - NFC Data Exchange Format (NDEF)
- Replay protection - Replay protection
- NXP Secure Unique NFC Message (SUN) technology as implemented in the NXP NTAG 424 DNA card - NXP Secure Unique NFC Message (SUN) technology as implemented in the NXP NTAG 424 DNA card
Bolt card systems should implement the best possible privacy.
- [Privacy levels](https://github.com/boltcard/boltcard/blob/main/docs/CARD_PRIVACY.md)
Bolt card systems may optionally support these technogies.
- [LUD-19: Pay link discoverable from withdraw link.](https://github.com/lnurl/luds/blob/luds/19.md)
- [LUD-21: pinLimit for withdrawRequest](https://github.com/bitcoin-ring/luds/blob/withdraw-pin/21.md)
## Bolt card and POS interaction ## Bolt card and POS interaction
the point-of-sale (POS) will read a NDEF message from the card, which changes with each use, for example the point-of-sale (POS) will read a NDEF message from the card, which changes with each use, for example

12
docs/TECHNOLOGY.md Normal file
View file

@ -0,0 +1,12 @@
## Bolt Card technology
| Document | Description |
| --- | --- |
| [System](SYSTEM.md) | Bolt card system overview |
| [Specification](SPEC.md) | Bolt card specifications |
| [Deterministic Keys (DRAFT FOR COMMENT)](DETERMINISTIC.md) | Consideration about key generation |
| [Boltcard Setup via Deeplink](DEEPLINK.md) | Deeplink for Boltcard creator apps |
| [Privacy](CARD_PRIVACY.md) | Bolt card privacy |
| [NXP 424 Datasheet](NT4H2421Gx.pdf) | NXP NTAG424DNA datasheet |
| [NXP 424 Application Note](NT4H2421Gx.pdf) | NXP NTAG424DNA features and hints |
| [FAQ](FAQ.md) | Bolt card FAQ |

54
docs/TEST_VECTORS.md Normal file
View file

@ -0,0 +1,54 @@
# test vectors
some test vectors to help with developing code to AES decode and validate lnurlw:// requests
these have been created by using an actual card and with [a small command line utility](https://github.com/boltcard/boltcard/blob/main/cli/main.go)
```
-- bolt card crypto test vectors --
p = 4E2E289D945A66BB13377A728884E867
c = E19CCB1FED8892CE
aes_decrypt_key = 0c3b25d92b38ae443229dd59ad34b85d
aes_cmac_key = b45775776cb224c75bcde7ca3704e933
decrypted card data : uid 04996c6a926980 , ctr 000003
sv2 = [60 195 0 1 0 128 4 153 108 106 146 105 128 3 0 0]
ks = [242 92 75 92 230 171 63 244 5 242 135 175 172 78 77 26]
cm = [118 225 233 156 238 203 64 31 163 237 110 136 112 146 124 206]
ct = [225 156 203 31 237 136 146 206]
cmac validates ok
-- bolt card crypto test vectors --
p = 00F48C4F8E386DED06BCDC78FA92E2FE
c = 66B4826EA4C155B4
aes_decrypt_key = 0c3b25d92b38ae443229dd59ad34b85d
aes_cmac_key = b45775776cb224c75bcde7ca3704e933
decrypted card data : uid 04996c6a926980 , ctr 000005
sv2 = [60 195 0 1 0 128 4 153 108 106 146 105 128 5 0 0]
ks = [73 70 39 105 116 24 126 152 96 101 139 189 130 16 200 190]
cm = [94 102 243 180 93 130 2 110 198 164 241 193 67 85 112 180]
ct = [102 180 130 110 164 193 85 180]
cmac validates ok
-- bolt card crypto test vectors --
p = 0DBF3C59B59B0638D60B5842A997D4D1
c = CC61660C020B4D96
aes_decrypt_key = 0c3b25d92b38ae443229dd59ad34b85d
aes_cmac_key = b45775776cb224c75bcde7ca3704e933
decrypted card data : uid 04996c6a926980 , ctr 000007
sv2 = [60 195 0 1 0 128 4 153 108 106 146 105 128 7 0 0]
ks = [97 189 177 81 15 79 217 5 102 95 162 58 192 199 38 97]
cm = [40 204 202 97 87 102 6 12 101 2 250 11 199 77 73 150]
ct = [204 97 102 12 2 11 77 150]
cmac validates ok
```

BIN
docs/images/fs-add-2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Before After
Before After

BIN
docs/images/posn-p.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
docs/images/posn.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -1,15 +1,18 @@
package email package email
import ( import (
"strconv"
"strings"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ses" "github.com/aws/aws-sdk-go/service/ses"
"github.com/boltcard/boltcard/db" "github.com/boltcard/boltcard/db"
"github.com/sendgrid/sendgrid-go"
"github.com/sendgrid/sendgrid-go/helpers/mail"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"strconv"
"strings"
) )
func Send_balance_email(recipient_email string, card_id int) { func Send_balance_email(recipient_email string, card_id int) {
@ -86,15 +89,36 @@ func Send_balance_email(recipient_email string, card_id int) {
} }
// https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/ses-example-send-email.html // https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/ses-example-send-email.html
// https://github.com/sendgrid/sendgrid-go
func Send_email(recipient string, subject string, htmlBody string, textBody string) { func Send_email(recipient string, subject string, htmlBody string, textBody string) {
send_grid_api_key := db.Get_setting("SENDGRID_API_KEY")
send_grid_email_sender := db.Get_setting("SENDGRID_EMAIL_SENDER")
if send_grid_api_key != "" && send_grid_email_sender != "" {
from := mail.NewEmail("", send_grid_email_sender)
subject := subject
to := mail.NewEmail("", recipient)
plainTextContent := textBody
htmlContent := htmlBody
message := mail.NewSingleEmail(from, subject, to, plainTextContent, htmlContent)
client := sendgrid.NewSendClient(send_grid_api_key)
response, err := client.Send(message)
if err != nil {
log.Warn(err.Error())
} else {
log.WithFields(log.Fields{"result": response}).Info("email sent")
}
} else {
aws_ses_id := db.Get_setting("AWS_SES_ID") aws_ses_id := db.Get_setting("AWS_SES_ID")
aws_ses_secret := db.Get_setting("AWS_SES_SECRET") aws_ses_secret := db.Get_setting("AWS_SES_SECRET")
sender := db.Get_setting("AWS_SES_EMAIL_FROM") sender := db.Get_setting("AWS_SES_EMAIL_FROM")
region := db.Get_setting("AWS_REGION")
sess, err := session.NewSession(&aws.Config{ sess, err := session.NewSession(&aws.Config{
Region: aws.String("us-east-1"), Region: aws.String(region),
Credentials: credentials.NewStaticCredentials(aws_ses_id, aws_ses_secret, ""), Credentials: credentials.NewStaticCredentials(aws_ses_id, aws_ses_secret, ""),
}) })
@ -152,3 +176,4 @@ func Send_email(recipient string, subject string, htmlBody string, textBody stri
log.WithFields(log.Fields{"result": result}).Info("email sent") log.WithFields(log.Fields{"result": result}).Info("email sent")
} }
}

2
go.mod
View file

@ -97,6 +97,8 @@ require (
github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect
github.com/rogpeppe/fastuuid v1.2.0 // indirect github.com/rogpeppe/fastuuid v1.2.0 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/sendgrid/sendgrid-go v3.12.0+incompatible // indirect
github.com/soheilhy/cmux v0.1.5 // indirect github.com/soheilhy/cmux v0.1.5 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect github.com/stretchr/objx v0.5.0 // indirect

4
go.sum
View file

@ -607,6 +607,10 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.12.0+incompatible h1:/N2vx18Fg1KmQOh6zESc5FJB8pYwt5QFBDflYPh1KVg=
github.com/sendgrid/sendgrid-go v3.12.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=

View file

@ -1,26 +1,16 @@
package internalapi package internalapi
import ( import (
"crypto/rand"
"encoding/hex"
"github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/resp_err"
log "github.com/sirupsen/logrus"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/resp_err"
log "github.com/sirupsen/logrus"
) )
func random_hex() string { // random_hex() from Createboltcardwithpin used here
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
log.Warn(err.Error())
return ""
}
return hex.EncodeToString(b)
}
func Createboltcard(w http.ResponseWriter, r *http.Request) { func Createboltcard(w http.ResponseWriter, r *http.Request) {
if db.Get_setting("FUNCTION_INTERNAL_API") != "ENABLE" { if db.Get_setting("FUNCTION_INTERNAL_API") != "ENABLE" {
@ -111,11 +101,16 @@ func Createboltcard(w http.ResponseWriter, r *http.Request) {
// return the URI + one_time_code // return the URI + one_time_code
hostdomain := db.Get_setting("HOST_DOMAIN") hostdomain := db.Get_setting("HOST_DOMAIN")
hostdomainPort := db.Get_setting("HOST_DOMAIN_PORT")
hostdomainsuffix := ""
if hostdomainPort != "" {
hostdomainsuffix = ":" + hostdomainPort
}
url := "" url := ""
if strings.HasSuffix(hostdomain, ".onion") { if strings.HasSuffix(hostdomain, ".onion") {
url = "http://" + hostdomain + "/new?a=" + one_time_code url = "http://" + hostdomain + hostdomainsuffix + "/new?a=" + one_time_code
} else { } else {
url = "https://" + hostdomain + "/new?a=" + one_time_code url = "https://" + hostdomain + hostdomainsuffix + "/new?a=" + one_time_code
} }
// log the response // log the response

View file

@ -0,0 +1,159 @@
package internalapi
import (
"crypto/rand"
"encoding/hex"
"net/http"
"strconv"
"strings"
"github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/resp_err"
log "github.com/sirupsen/logrus"
)
func random_hex() string {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
log.Warn(err.Error())
return ""
}
return hex.EncodeToString(b)
}
func Createboltcardwithpin(w http.ResponseWriter, r *http.Request) {
if db.Get_setting("FUNCTION_INTERNAL_API") != "ENABLE" {
msg := "createboltcardwithpin: internal API function is not enabled"
log.Debug(msg)
resp_err.Write_message(w, msg)
return
}
tx_max_str := r.URL.Query().Get("tx_max")
tx_max, err := strconv.Atoi(tx_max_str)
if err != nil {
msg := "createboltcardwithpin: tx_max is not a valid integer"
log.Warn(msg)
resp_err.Write_message(w, msg)
return
}
day_max_str := r.URL.Query().Get("day_max")
day_max, err := strconv.Atoi(day_max_str)
if err != nil {
msg := "createboltcardwithpin: day_max is not a valid integer"
log.Warn(msg)
resp_err.Write_message(w, msg)
return
}
enable_flag_str := r.URL.Query().Get("enable")
enable_flag, err := strconv.ParseBool(enable_flag_str)
if err != nil {
msg := "createboltcardwithpin: enable is not a valid boolean"
log.Warn(msg)
resp_err.Write_message(w, msg)
return
}
card_name := r.URL.Query().Get("card_name")
if card_name == "" {
msg := "createboltcardwithpin: the card name must be set"
log.Warn(msg)
resp_err.Write_message(w, msg)
return
}
uid_privacy_flag_str := r.URL.Query().Get("uid_privacy")
uid_privacy_flag, err := strconv.ParseBool(uid_privacy_flag_str)
if err != nil {
msg := "createboltcardwithpin: uid_privacy is not a valid boolean"
log.Warn(msg)
resp_err.Write_message(w, msg)
return
}
allow_neg_bal_flag_str := r.URL.Query().Get("allow_neg_bal")
allow_neg_bal_flag, err := strconv.ParseBool(allow_neg_bal_flag_str)
if err != nil {
msg := "createboltcardwithpin: allow_neg_bal is not a valid boolean"
log.Warn(msg)
resp_err.Write_message(w, msg)
return
}
pin_enable_flag_str := r.URL.Query().Get("enable_pin")
pin_enable_flag, err := strconv.ParseBool(pin_enable_flag_str)
if err != nil {
msg := "createboltcardwithpin: enable_pin is not a valid boolean"
log.Warn(msg)
resp_err.Write_message(w, msg)
return
}
pin_number := r.URL.Query().Get("pin_number")
pin_limit_sats_str := r.URL.Query().Get("pin_limit_sats")
pin_limit_sats, err := strconv.Atoi(pin_limit_sats_str)
if err != nil {
msg := "createboltcardwithpin: pin_limit_sats is not a valid integer"
log.Warn(msg)
resp_err.Write_message(w, msg)
return
}
// log the request
log.WithFields(log.Fields{
"card_name": card_name, "tx_max": tx_max, "day_max": day_max,
"enable": enable_flag, "uid_privacy": uid_privacy_flag,
"allow_neg_bal": allow_neg_bal_flag, "enable_pin": pin_enable_flag,
"pin_number": pin_number, "pin_limit_sats": pin_limit_sats}).Info("createboltcardwithpin API request")
// create the keys
one_time_code := random_hex()
k0_auth_key := random_hex()
k2_cmac_key := random_hex()
k3 := random_hex()
k4 := random_hex()
// create the new card record
err = db.Insert_card_with_pin(one_time_code, k0_auth_key, k2_cmac_key, k3, k4,
tx_max, day_max, enable_flag, card_name,
uid_privacy_flag, allow_neg_bal_flag, pin_enable_flag, pin_number, pin_limit_sats)
if err != nil {
log.Warn(err.Error())
return
}
// return the URI + one_time_code
hostdomain := db.Get_setting("HOST_DOMAIN")
hostdomainPort := db.Get_setting("HOST_DOMAIN_PORT")
hostdomainsuffix := ""
if hostdomainPort != "" {
hostdomainsuffix = ":" + hostdomainPort
}
url := ""
if strings.HasSuffix(hostdomain, ".onion") {
url = "http://" + hostdomain + hostdomainsuffix + "/new?a=" + one_time_code
} else {
url = "https://" + hostdomain + hostdomainsuffix + "/new?a=" + one_time_code
}
// log the response
log.WithFields(log.Fields{
"card_name": card_name, "url": url}).Info("createboltcard API response")
jsonData := []byte(`{"status":"OK",` +
`"url":"` + url + `"}`)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonData)
}

View file

@ -37,7 +37,9 @@ func Getboltcard(w http.ResponseWriter, r *http.Request) {
`"uid": "` + c.Db_uid + `",` + `"uid": "` + c.Db_uid + `",` +
`"lnurlw_enable": "` + c.Lnurlw_enable + `",` + `"lnurlw_enable": "` + c.Lnurlw_enable + `",` +
`"tx_limit_sats": "` + strconv.Itoa(c.Tx_limit_sats) + `",` + `"tx_limit_sats": "` + strconv.Itoa(c.Tx_limit_sats) + `",` +
`"day_limit_sats": "` + strconv.Itoa(c.Day_limit_sats) + `"}`) `"day_limit_sats": "` + strconv.Itoa(c.Day_limit_sats) + `", ` +
`"pin_enable": "` + c.Pin_enable + `", ` +
`"pin_limit_sats": "` + strconv.Itoa(c.Pin_limit_sats) + `"}`)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)

View file

@ -0,0 +1,116 @@
package internalapi
import (
"github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/resp_err"
log "github.com/sirupsen/logrus"
"net/http"
"strconv"
)
func Updateboltcardwithpin(w http.ResponseWriter, r *http.Request) {
if db.Get_setting("FUNCTION_INTERNAL_API") != "ENABLE" {
msg := "updateboltcardwithpin: internal API function is not enabled"
log.Debug(msg)
resp_err.Write_message(w, msg)
return
}
enable_flag_str := r.URL.Query().Get("enable")
enable_flag, err := strconv.ParseBool(enable_flag_str)
if err != nil {
msg := "updateboltcardwithpin: enable is not a valid boolean"
log.Warn(msg)
resp_err.Write_message(w, msg)
return
}
tx_max_str := r.URL.Query().Get("tx_max")
tx_max, err := strconv.Atoi(tx_max_str)
if err != nil {
msg := "updateboltcardwithpin: tx_max is not a valid integer"
log.Warn(msg)
resp_err.Write_message(w, msg)
return
}
day_max_str := r.URL.Query().Get("day_max")
day_max, err := strconv.Atoi(day_max_str)
if err != nil {
msg := "updateboltcardwithpin: day_max is not a valid integer"
log.Warn(msg)
resp_err.Write_message(w, msg)
return
}
pin_enable_flag_str := r.URL.Query().Get("enable_pin")
pin_enable_flag, err := strconv.ParseBool(pin_enable_flag_str)
if err != nil {
msg := "updateboltcardwithpin: enable_pin is not a valid boolean"
log.Warn(msg)
resp_err.Write_message(w, msg)
return
}
pin_number := r.URL.Query().Get("pin_number")
pin_limit_sats_str := r.URL.Query().Get("pin_limit_sats")
pin_limit_sats, err := strconv.Atoi(pin_limit_sats_str)
if err != nil {
msg := "updateboltcardwithpin: pin_limit_sats is not a valid integer"
log.Warn(msg)
resp_err.Write_message(w, msg)
return
}
card_name := r.URL.Query().Get("card_name")
// check if card_name exists
card_count, err := db.Get_card_name_count(card_name)
if err != nil {
log.Warn(err.Error())
return
}
if card_count == 0 {
msg := "updateboltcardwithpin: the card name does not exist in the database"
log.Warn(msg)
resp_err.Write_message(w, msg)
return
}
// log the request
log.WithFields(log.Fields{
"card_name": card_name, "tx_max": tx_max, "day_max": day_max,
"enable": enable_flag, "enable_pin": pin_enable_flag,
"pin_number": pin_number, "pin_limit_sats": pin_limit_sats}).Info("updateboltcardwithpin API request")
// update the card record
if pin_number == "" {
err = db.Update_card_with_part_pin(card_name, enable_flag, tx_max, day_max,
pin_enable_flag, pin_limit_sats)
if err != nil {
log.Warn(err.Error())
return
}
}
if pin_number != "" {
err = db.Update_card_with_pin(card_name, enable_flag, tx_max, day_max,
pin_enable_flag, pin_number, pin_limit_sats)
if err != nil {
log.Warn(err.Error())
return
}
}
// send a response
jsonData := []byte(`{"status":"OK"}`)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonData)
}

View file

@ -81,6 +81,10 @@ func Add_invoice(amount_sat int64, metadata string) (payment_request string, r_h
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
ln_invoice_expiry, err := strconv.ParseInt(db.Get_setting("LN_INVOICE_EXPIRY_SEC"), 10, 64)
if err != nil {
return "", nil, err
}
dh := sha256.Sum256([]byte(metadata)) dh := sha256.Sum256([]byte(metadata))
@ -98,6 +102,7 @@ func Add_invoice(amount_sat int64, metadata string) (payment_request string, r_h
result, err := l_client.AddInvoice(ctx, &lnrpc.Invoice{ result, err := l_client.AddInvoice(ctx, &lnrpc.Invoice{
Value: amount_sat, Value: amount_sat,
DescriptionHash: dh[:], DescriptionHash: dh[:],
Expiry: ln_invoice_expiry,
}) })
if err != nil { if err != nil {
@ -120,6 +125,11 @@ func Monitor_invoice_state(r_hash []byte) {
log.Warn(err) log.Warn(err)
return return
} }
ln_invoice_expiry, err := strconv.Atoi(db.Get_setting("LN_INVOICE_EXPIRY_SEC"))
if err != nil {
log.Warn(err)
return
}
connection := getGrpcConn( connection := getGrpcConn(
db.Get_setting("LN_HOST"), db.Get_setting("LN_HOST"),
@ -129,7 +139,7 @@ func Monitor_invoice_state(r_hash []byte) {
i_client := invoicesrpc.NewInvoicesClient(connection) i_client := invoicesrpc.NewInvoicesClient(connection)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(ln_invoice_expiry)*time.Second)
defer cancel() defer cancel()
stream, err := i_client.SubscribeSingleInvoice(ctx, &invoicesrpc.SubscribeSingleInvoiceRequest{ stream, err := i_client.SubscribeSingleInvoice(ctx, &invoicesrpc.SubscribeSingleInvoiceRequest{
@ -228,10 +238,11 @@ func PayInvoice(card_payment_id int, invoice string) {
bolt11, _ := decodepay.Decodepay(invoice) bolt11, _ := decodepay.Decodepay(invoice)
invoice_msats := bolt11.MSatoshi invoice_msats := bolt11.MSatoshi
invoice_expiry := bolt11.Expiry
fee_limit_product := int64((fee_limit_percent / 100) * (float64(invoice_msats) / 1000)) fee_limit_product := int64((fee_limit_percent / 100) * (float64(invoice_msats) / 1000))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(invoice_expiry)*time.Second)
defer cancel() defer cancel()
stream, err := r_client.SendPaymentV2(ctx, &routerrpc.SendPaymentRequest{ stream, err := r_client.SendPaymentV2(ctx, &routerrpc.SendPaymentRequest{

View file

@ -2,13 +2,14 @@ package lnurlp
import ( import (
"encoding/hex" "encoding/hex"
"net/http"
"strconv"
"github.com/boltcard/boltcard/db" "github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/lnd" "github.com/boltcard/boltcard/lnd"
"github.com/boltcard/boltcard/resp_err" "github.com/boltcard/boltcard/resp_err"
"github.com/gorilla/mux" "github.com/gorilla/mux"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"net/http"
"strconv"
) )
func Callback(w http.ResponseWriter, r *http.Request) { func Callback(w http.ResponseWriter, r *http.Request) {
@ -37,6 +38,11 @@ func Callback(w http.ResponseWriter, r *http.Request) {
}).Info("lnurlp_callback") }).Info("lnurlp_callback")
domain := db.Get_setting("HOST_DOMAIN") domain := db.Get_setting("HOST_DOMAIN")
hostdomainPort := db.Get_setting("HOST_DOMAIN_PORT")
hostdomainsuffix := ""
if hostdomainPort != "" {
hostdomainsuffix = ":" + hostdomainPort
}
if r.Host != domain { if r.Host != domain {
log.Warn("wrong host domain") log.Warn("wrong host domain")
resp_err.Write(w) resp_err.Write(w)
@ -52,7 +58,7 @@ func Callback(w http.ResponseWriter, r *http.Request) {
amount_sat := amount_msat / 1000 amount_sat := amount_msat / 1000
metadata := "[[\"text/identifier\",\"" + name + "@" + domain + "\"],[\"text/plain\",\"bolt card deposit\"]]" metadata := "[[\"text/identifier\",\"" + name + "@" + domain + hostdomainsuffix + "\"],[\"text/plain\",\"bolt card deposit\"]]"
pr, r_hash, err := lnd.Add_invoice(amount_sat, metadata) pr, r_hash, err := lnd.Add_invoice(amount_sat, metadata)
if err != nil { if err != nil {
log.Warn("could not add_invoice") log.Warn("could not add_invoice")

View file

@ -1,11 +1,12 @@
package lnurlp package lnurlp
import ( import (
"net/http"
"github.com/boltcard/boltcard/db" "github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/resp_err" "github.com/boltcard/boltcard/resp_err"
"github.com/gorilla/mux" "github.com/gorilla/mux"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"net/http"
) )
func Response(w http.ResponseWriter, r *http.Request) { func Response(w http.ResponseWriter, r *http.Request) {
@ -26,6 +27,11 @@ func Response(w http.ResponseWriter, r *http.Request) {
// look up domain setting (HOST_DOMAIN) // look up domain setting (HOST_DOMAIN)
domain := db.Get_setting("HOST_DOMAIN") domain := db.Get_setting("HOST_DOMAIN")
hostdomainPort := db.Get_setting("HOST_DOMAIN_PORT")
hostdomainsuffix := ""
if hostdomainPort != "" {
hostdomainsuffix = ":" + hostdomainPort
}
if r.Host != domain { if r.Host != domain {
log.Warn("wrong host domain") log.Warn("wrong host domain")
resp_err.Write(w) resp_err.Write(w)
@ -47,10 +53,10 @@ func Response(w http.ResponseWriter, r *http.Request) {
return return
} }
metadata := "[[\\\"text/identifier\\\",\\\"" + name + "@" + domain + "\\\"],[\\\"text/plain\\\",\\\"bolt card deposit\\\"]]" metadata := "[[\\\"text/identifier\\\",\\\"" + name + "@" + domain + hostdomainsuffix + "\\\"],[\\\"text/plain\\\",\\\"bolt card deposit\\\"]]"
jsonData := []byte(`{"status":"OK",` + jsonData := []byte(`{"status":"OK",` +
`"callback":"https://` + domain + `/lnurlp/` + name + `",` + `"callback":"https://` + domain + hostdomainsuffix + `/lnurlp/` + name + `",` +
`"tag":"payRequest",` + `"tag":"payRequest",` +
`"maxSendable":1000000000,` + `"maxSendable":1000000000,` +
`"minSendable":1000,` + `"minSendable":1000,` +

View file

@ -56,13 +56,13 @@ func lndhub_payment(w http.ResponseWriter, p *db.Payment, bolt11 decodepay.Bolt1
card_name_parts := strings.Split(c.Card_name, ":") card_name_parts := strings.Split(c.Card_name, ":")
if len(card_name_parts) != 2 { if len(card_name_parts) != 2 {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn(err) log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn("login:password not found")
resp_err.Write(w) resp_err.Write(w)
return return
} }
if len(card_name_parts[0]) != 20 || len(card_name_parts[1]) != 20 { if len(card_name_parts[0]) != 20 || len(card_name_parts[1]) != 20 {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn(err) log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn("login:password badly formed")
resp_err.Write(w) resp_err.Write(w)
return return
} }
@ -72,7 +72,6 @@ func lndhub_payment(w http.ResponseWriter, p *db.Payment, bolt11 decodepay.Bolt1
lhAuthRequest.Password = card_name_parts[1] lhAuthRequest.Password = card_name_parts[1]
authReq, err := json.Marshal(lhAuthRequest) authReq, err := json.Marshal(lhAuthRequest)
log.Info(string(authReq))
req_auth, err := http.NewRequest("POST", lndhub_url+"/auth", bytes.NewBuffer(authReq)) req_auth, err := http.NewRequest("POST", lndhub_url+"/auth", bytes.NewBuffer(authReq))
if err != nil { if err != nil {
@ -101,6 +100,9 @@ func lndhub_payment(w http.ResponseWriter, p *db.Payment, bolt11 decodepay.Bolt1
return return
} }
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id,
"resp_auth_bytes": string(resp_auth_bytes)}).Info("issue 62")
var auth_keys LndhubAuthResponse var auth_keys LndhubAuthResponse
err = json.Unmarshal([]byte(resp_auth_bytes), &auth_keys) err = json.Unmarshal([]byte(resp_auth_bytes), &auth_keys)
@ -225,17 +227,15 @@ func Callback(w http.ResponseWriter, req *http.Request) {
url := req.URL.RequestURI() url := req.URL.RequestURI()
log.WithFields(log.Fields{"url": url}).Debug("cb request") log.WithFields(log.Fields{"url": url}).Debug("cb request")
// check k1 value // get k1 value
params_k1, ok := req.URL.Query()["k1"] param_k1 := req.URL.Query().Get("k1")
if !ok || len(params_k1[0]) < 1 { if param_k1 == "" {
log.WithFields(log.Fields{"url": url}).Debug("k1 not found") log.WithFields(log.Fields{"url": url}).Debug("k1 not found")
resp_err.Write(w) resp_err.Write(w)
return return
} }
param_k1 := params_k1[0]
p, err := db.Get_payment_k1(param_k1) p, err := db.Get_payment_k1(param_k1)
if err != nil { if err != nil {
log.WithFields(log.Fields{"url": url, "k1": param_k1}).Warn(err) log.WithFields(log.Fields{"url": url, "k1": param_k1}).Warn(err)
@ -263,14 +263,14 @@ func Callback(w http.ResponseWriter, req *http.Request) {
return return
} }
params_pr, ok := req.URL.Query()["pr"] // get the payment request
if !ok || len(params_pr[0]) < 1 { param_pr := req.URL.Query().Get("pr")
if param_pr == "" {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn("pr field not found") log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn("pr field not found")
resp_err.Write(w) resp_err.Write(w)
return return
} }
param_pr := params_pr[0]
bolt11, _ := decodepay.Decodepay(param_pr) bolt11, _ := decodepay.Decodepay(param_pr)
// record the lightning invoice // record the lightning invoice
@ -283,6 +283,23 @@ func Callback(w http.ResponseWriter, req *http.Request) {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Debug("checking payment rules") log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Debug("checking payment rules")
// get the pin if it has been passed in
param_pin := req.URL.Query().Get("pin")
c, err := db.Get_card_from_card_id(p.Card_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn(err)
resp_err.Write(w)
return
}
// check the pin if needed
if c.Pin_enable == "Y" && int(bolt11.MSatoshi/1000) >= c.Pin_limit_sats && c.Pin_number != param_pin {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn("incorrect pin provided")
resp_err.Write(w)
return
}
// check if we are only sending funds to a defined test node // check if we are only sending funds to a defined test node
testnode := db.Get_setting("LN_TESTNODE") testnode := db.Get_setting("LN_TESTNODE")
if testnode != "" && bolt11.Payee != testnode { if testnode != "" && bolt11.Payee != testnode {

View file

@ -4,24 +4,16 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/boltcard/boltcard/crypto"
"github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/resp_err"
log "github.com/sirupsen/logrus"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"strings" "strings"
)
type ResponseData struct { "github.com/boltcard/boltcard/crypto"
Tag string `json:"tag"` "github.com/boltcard/boltcard/db"
Callback string `json:"callback"` "github.com/boltcard/boltcard/resp_err"
LnurlwK1 string `json:"k1"` log "github.com/sirupsen/logrus"
DefaultDescription string `json:"defaultDescription"` )
MinWithdrawable int `json:"minWithdrawable"`
MaxWithdrawable int `json:"maxWithdrawable"`
}
func get_p_c(req *http.Request, p_name string, c_name string) (p string, c string) { func get_p_c(req *http.Request, p_name string, c_name string) (p string, c string) {
@ -255,6 +247,12 @@ func parse_request(req *http.Request) (int, error) {
func Response(w http.ResponseWriter, req *http.Request) { func Response(w http.ResponseWriter, req *http.Request) {
env_host_domain := db.Get_setting("HOST_DOMAIN") env_host_domain := db.Get_setting("HOST_DOMAIN")
hostdomainPort := db.Get_setting("HOST_DOMAIN_PORT")
hostdomainsuffix := ""
if hostdomainPort != "" {
hostdomainsuffix = ":" + hostdomainPort
}
if req.Host != env_host_domain { if req.Host != env_host_domain {
log.Warn("wrong host domain") log.Warn("wrong host domain")
resp_err.Write(w) resp_err.Write(w)
@ -288,10 +286,10 @@ func Response(w http.ResponseWriter, req *http.Request) {
} }
lnurlw_cb_url := "" lnurlw_cb_url := ""
if strings.HasSuffix(req.Host, ".onion") { if strings.HasSuffix(env_host_domain, ".onion") {
lnurlw_cb_url = "http://" + req.Host + "/cb" lnurlw_cb_url = "http://" + env_host_domain + hostdomainsuffix + "/cb"
} else { } else {
lnurlw_cb_url = "https://" + req.Host + "/cb" lnurlw_cb_url = "https://" + env_host_domain + hostdomainsuffix + "/cb"
} }
min_withdraw_sats_str := db.Get_setting("MIN_WITHDRAW_SATS") min_withdraw_sats_str := db.Get_setting("MIN_WITHDRAW_SATS")
@ -312,13 +310,29 @@ func Response(w http.ResponseWriter, req *http.Request) {
return return
} }
response := ResponseData{} // get pin_enable & pin_limit_sats
response.Tag = "withdrawRequest"
response.Callback = lnurlw_cb_url c, err := db.Get_card_from_card_id(card_id)
response.LnurlwK1 = lnurlw_k1 if err != nil {
response.DefaultDescription = "WWT withdrawal" log.WithFields(log.Fields{"card_id": card_id}).Warn(err)
response.MinWithdrawable = min_withdraw_sats * 1000 // milliSats resp_err.Write(w)
response.MaxWithdrawable = max_withdraw_sats * 1000 // milliSats return
}
default_description := db.Get_setting("DEFAULT_DESCRIPTION")
response := make(map[string]interface{})
response["tag"] = "withdrawRequest"
response["callback"] = lnurlw_cb_url
response["k1"] = lnurlw_k1
response["defaultDescription"] = default_description
response["minWithdrawable"] = min_withdraw_sats * 1000 // milliSats
response["maxWithdrawable"] = max_withdraw_sats * 1000 // milliSats
if c.Pin_enable == "Y" {
response["pinLimit"] = c.Pin_limit_sats * 1000 // milliSats
}
jsonData, err := json.Marshal(response) jsonData, err := json.Marshal(response)

View file

@ -53,7 +53,9 @@ func main() {
internal_router.Path("/ping").Methods("GET").HandlerFunc(internalapi.Internal_ping) internal_router.Path("/ping").Methods("GET").HandlerFunc(internalapi.Internal_ping)
internal_router.Path("/createboltcard").Methods("GET").HandlerFunc(internalapi.Createboltcard) internal_router.Path("/createboltcard").Methods("GET").HandlerFunc(internalapi.Createboltcard)
internal_router.Path("/createboltcardwithpin").Methods("GET").HandlerFunc(internalapi.Createboltcardwithpin)
internal_router.Path("/updateboltcard").Methods("GET").HandlerFunc(internalapi.Updateboltcard) internal_router.Path("/updateboltcard").Methods("GET").HandlerFunc(internalapi.Updateboltcard)
internal_router.Path("/updateboltcardwithpin").Methods("GET").HandlerFunc(internalapi.Updateboltcardwithpin)
internal_router.Path("/wipeboltcard").Methods("GET").HandlerFunc(internalapi.Wipeboltcard) internal_router.Path("/wipeboltcard").Methods("GET").HandlerFunc(internalapi.Wipeboltcard)
internal_router.Path("/getboltcard").Methods("GET").HandlerFunc(internalapi.Getboltcard) internal_router.Path("/getboltcard").Methods("GET").HandlerFunc(internalapi.Getboltcard)

View file

@ -3,10 +3,11 @@ package main
import ( import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"net/http"
"github.com/boltcard/boltcard/db" "github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/resp_err" "github.com/boltcard/boltcard/resp_err"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"net/http"
) )
/** /**
@ -55,7 +56,12 @@ func new_card_request(w http.ResponseWriter, req *http.Request) {
a := params_a[0] a := params_a[0]
lnurlw_base := "lnurlw://" + db.Get_setting("HOST_DOMAIN") + "/ln" hostdomainPort := db.Get_setting("HOST_DOMAIN_PORT")
hostdomainsuffix := ""
if hostdomainPort != "" {
hostdomainsuffix = ":" + hostdomainPort
}
lnurlw_base := "lnurlw://" + db.Get_setting("HOST_DOMAIN") + hostdomainsuffix + "/ln"
c, err := db.Get_new_card(a) c, err := db.Get_new_card(a)

View file

@ -15,7 +15,7 @@ if [ "$x" = "y" ]; then
psql postgres -f sql/create_db_init.sql psql postgres -f sql/create_db_init.sql
psql postgres -f sql/create_db.sql psql postgres -f sql/create_db.sql
psql postgres -f sql/create_db_user.sql psql postgres -f sql/create_db_user.sql
psql postgres -f sql/settings.sql psql postgres -f sql/settings.sql.secret
echo Database created echo Database created
else else
echo No action echo No action

6
script/s_setup_test_data Executable file
View file

@ -0,0 +1,6 @@
# to close any database connections
sudo systemctl stop postgresql
sudo systemctl start postgresql
psql postgres -f sql/data.test.sql
echo Test data added

View file

@ -28,6 +28,9 @@ CREATE TABLE cards (
one_time_code_expiry TIMESTAMPTZ DEFAULT NOW() + INTERVAL '1 DAY', one_time_code_expiry TIMESTAMPTZ DEFAULT NOW() + INTERVAL '1 DAY',
one_time_code_used CHAR(1) NOT NULL DEFAULT 'Y', one_time_code_used CHAR(1) NOT NULL DEFAULT 'Y',
allow_negative_balance CHAR(1) NOT NULL DEFAULT 'N', allow_negative_balance CHAR(1) NOT NULL DEFAULT 'N',
pin_enable CHAR(1) NOT NULL DEFAULT 'N',
pin_number CHAR(4) NOT NULL DEFAULT '0000',
pin_limit_sats INT NOT NULL DEFAULT 0,
wiped CHAR(1) NOT NULL DEFAULT 'N', wiped CHAR(1) NOT NULL DEFAULT 'N',
PRIMARY KEY(card_id) PRIMARY KEY(card_id)
); );

24
sql/data.test.sql Normal file
View file

@ -0,0 +1,24 @@
-- connect to card_db
\c card_db;
-- clear out table data
DELETE FROM settings;
DELETE FROM card_payments;
DELETE FROM card_receipts;
DELETE FROM cards;
-- set up test data
INSERT INTO settings (name, value) VALUES ('LOG_LEVEL', 'DEBUG');
INSERT INTO settings (name, value) VALUES ('AES_DECRYPT_KEY', '994de7f8156609a0effafbdb049337b1');
INSERT INTO settings (name, value) VALUES ('HOST_DOMAIN', 'localhost:9000');
INSERT INTO settings (name, value) VALUES ('FUNCTION_INTERNAL_API', 'ENABLE');
INSERT INTO settings (name, value) VALUES ('MIN_WITHDRAW_SATS', '1');
INSERT INTO settings (name, value) VALUES ('MAX_WITHDRAW_SATS', '1000');
INSERT INTO cards
(k0_auth_key, k2_cmac_key, k3, k4, lnurlw_enable, last_counter_value, lnurlw_request_timeout_sec,
tx_limit_sats, day_limit_sats, card_name, pin_enable, pin_number, pin_limit_sats)
VALUES
('', 'd3dffa1e12d2477e443a6ee9fcfeab18', '', '', 'Y', 0, 10,
0, 0, 'test_card', 'Y', '1234', 1000);

View file

@ -20,10 +20,15 @@ INSERT INTO settings (name, value) VALUES ('LN_TESTNODE', '');
INSERT INTO settings (name, value) VALUES ('FUNCTION_LNURLW', ''); INSERT INTO settings (name, value) VALUES ('FUNCTION_LNURLW', '');
INSERT INTO settings (name, value) VALUES ('FUNCTION_LNURLP', ''); INSERT INTO settings (name, value) VALUES ('FUNCTION_LNURLP', '');
INSERT INTO settings (name, value) VALUES ('FUNCTION_EMAIL', ''); INSERT INTO settings (name, value) VALUES ('FUNCTION_EMAIL', '');
INSERT INTO settings (name, value) VALUES ('DEFAULT_DESCRIPTION', 'bolt card service');
INSERT INTO settings (name, value) VALUES ('AWS_SES_ID', ''); INSERT INTO settings (name, value) VALUES ('AWS_SES_ID', '');
INSERT INTO settings (name, value) VALUES ('AWS_SES_SECRET', ''); INSERT INTO settings (name, value) VALUES ('AWS_SES_SECRET', '');
INSERT INTO settings (name, value) VALUES ('AWS_SES_EMAIL_FROM', ''); INSERT INTO settings (name, value) VALUES ('AWS_SES_EMAIL_FROM', '');
INSERT INTO settings (name, value) VALUES ('AWS_REGION', 'us-east-1');
INSERT INTO settings (name, value) VALUES ('EMAIL_MAX_TXS', ''); INSERT INTO settings (name, value) VALUES ('EMAIL_MAX_TXS', '');
INSERT INTO settings (name, value) VALUES ('FUNCTION_LNDHUB', ''); INSERT INTO settings (name, value) VALUES ('FUNCTION_LNDHUB', '');
INSERT INTO settings (name, value) VALUES ('LNDHUB_URL', ''); INSERT INTO settings (name, value) VALUES ('LNDHUB_URL', '');
INSERT INTO settings (name, value) VALUES ('FUNCTION_INTERNAL_API', ''); INSERT INTO settings (name, value) VALUES ('FUNCTION_INTERNAL_API', '');
INSERT INTO settings (name, value) VALUES ('SENDGRID_API_KEY', '');
INSERT INTO settings (name, value) VALUES ('SENDGRID_EMAIL_SENDER', '');
INSERT INTO settings (name, value) VALUES ('LN_INVOICE_EXPIRY_SEC', '3600');