Compare commits
84 commits
lndhub-che
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ef61fe1af | |||
|
|
3a2096345d | ||
|
|
70638045dd | ||
|
|
b3ac9e0a6c | ||
|
|
5564bd95c3 | ||
|
|
8ceff1f8b8 | ||
|
|
d041de3b6a | ||
|
|
ef1e2953c3 | ||
|
|
51bdfc5cb9 | ||
|
|
e75f95e0f0 | ||
|
|
e60705ba49 | ||
|
|
51f09a6295 | ||
|
|
777bdb6081 | ||
|
|
808336e084 | ||
|
|
b0bd474dfc | ||
|
|
bdb350a177 | ||
|
|
8379b3f88e | ||
|
|
70854d4508 | ||
|
|
acb0c13a54 | ||
|
|
d591092967 | ||
|
|
ed4c5db552 | ||
|
|
1eb2b622ac | ||
|
|
f1a8af86b6 | ||
|
|
29a0168573 | ||
|
|
7745c9f20d | ||
|
|
d5a64a8dab | ||
|
|
27ba1756ae | ||
|
|
c5921f6544 | ||
|
|
dc60a816c0 | ||
|
|
165a46b9c1 | ||
|
|
0712449103 | ||
|
|
94117718bd | ||
|
|
534367153a | ||
|
|
c36e19405d | ||
|
|
2b0392ea44 | ||
|
|
7d51fac18e | ||
|
|
46a83398b4 | ||
|
|
36fc086e25 | ||
|
|
ae967cc011 | ||
|
|
3a1262db82 | ||
|
|
78a441baf5 | ||
|
|
2da9275c9a | ||
|
|
34618bd228 | ||
|
|
53ce60cfc3 | ||
|
|
056a52e1ba | ||
|
|
797e4db605 | ||
|
|
8f83e04564 | ||
|
|
4828610a5d | ||
|
|
d2b6c30f3d | ||
|
|
85bde475a3 | ||
|
|
df94c60fee | ||
|
|
3be7264ff1 | ||
|
|
de14c32867 | ||
|
|
6c03c1c3d9 | ||
|
|
87306ca6db | ||
|
|
598919fc2a | ||
|
|
6609558f96 | ||
|
|
9b02a40cf3 | ||
|
|
b43f580530 | ||
|
|
ee942667e3 | ||
|
|
3d9e742dc1 | ||
|
|
105323a680 | ||
|
|
b76252d6ef | ||
|
|
299ab696cc | ||
|
|
3b9db705f5 | ||
|
|
77f1de8b7e | ||
|
|
24082c831f | ||
|
|
6cabfbf0d8 | ||
|
|
d3439db85b | ||
|
|
e9ef6973d4 | ||
|
|
16df735233 | ||
|
|
294a4eb054 | ||
|
|
7f8229cad0 | ||
|
|
78a3dde2ed | ||
|
|
6661ebc7a4 | ||
|
|
aa49fce3ff | ||
|
|
2165e248ca | ||
|
|
e249324e64 | ||
|
|
68f38e2347 | ||
|
|
cd7fc1338d | ||
|
|
d9dfb49a23 | ||
|
|
d7258bb4ad | ||
|
|
f5328ab7ed | ||
|
|
3034f1a65d |
40 changed files with 1384 additions and 129 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
198
cli/main.go
Normal 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
173
db/db.go
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
BIN
docs/AN12196.pdf
Normal file
Binary file not shown.
|
|
@ -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)
|
||||||
|

|
||||||
- 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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- select `Change File Settings`
|
- select `Change File Settings`
|
||||||
|
|
||||||
|
|
|
||||||
64
docs/CARD_PRIVACY.md
Normal file
64
docs/CARD_PRIVACY.md
Normal 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
113
docs/DEEPLINK.md
Normal 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> | </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
216
docs/DETERMINISTIC.md
Normal 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
|
||||||
|
```
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
BIN
docs/NT4H2421Gx.pdf
Normal file
Binary file not shown.
|
|
@ -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 |
|
||||||
|
|
|
||||||
10
docs/SPEC.md
10
docs/SPEC.md
|
|
@ -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
12
docs/TECHNOLOGY.md
Normal 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
54
docs/TEST_VECTORS.md
Normal 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
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 |
BIN
docs/images/posn-p.webp
Normal file
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
BIN
docs/images/posn.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
145
email/email.go
145
email/email.go
|
|
@ -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,69 +89,91 @@ 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) {
|
||||||
|
|
||||||
aws_ses_id := db.Get_setting("AWS_SES_ID")
|
send_grid_api_key := db.Get_setting("SENDGRID_API_KEY")
|
||||||
aws_ses_secret := db.Get_setting("AWS_SES_SECRET")
|
send_grid_email_sender := db.Get_setting("SENDGRID_EMAIL_SENDER")
|
||||||
sender := db.Get_setting("AWS_SES_EMAIL_FROM")
|
if send_grid_api_key != "" && send_grid_email_sender != "" {
|
||||||
|
from := mail.NewEmail("", send_grid_email_sender)
|
||||||
sess, err := session.NewSession(&aws.Config{
|
subject := subject
|
||||||
Region: aws.String("us-east-1"),
|
to := mail.NewEmail("", recipient)
|
||||||
Credentials: credentials.NewStaticCredentials(aws_ses_id, aws_ses_secret, ""),
|
plainTextContent := textBody
|
||||||
})
|
htmlContent := htmlBody
|
||||||
|
message := mail.NewSingleEmail(from, subject, to, plainTextContent, htmlContent)
|
||||||
svc := ses.New(sess)
|
client := sendgrid.NewSendClient(send_grid_api_key)
|
||||||
|
response, err := client.Send(message)
|
||||||
charSet := "UTF-8"
|
if err != nil {
|
||||||
|
|
||||||
input := &ses.SendEmailInput{
|
|
||||||
Destination: &ses.Destination{
|
|
||||||
CcAddresses: []*string{},
|
|
||||||
ToAddresses: []*string{
|
|
||||||
aws.String(recipient),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Message: &ses.Message{
|
|
||||||
Body: &ses.Body{
|
|
||||||
Html: &ses.Content{
|
|
||||||
Charset: aws.String(charSet),
|
|
||||||
Data: aws.String(htmlBody),
|
|
||||||
},
|
|
||||||
Text: &ses.Content{
|
|
||||||
Charset: aws.String(charSet),
|
|
||||||
Data: aws.String(textBody),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Subject: &ses.Content{
|
|
||||||
Charset: aws.String(charSet),
|
|
||||||
Data: aws.String(subject),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Source: aws.String(sender),
|
|
||||||
//ConfigurationSetName: aws.String(ConfigurationSet),
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := svc.SendEmail(input)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if aerr, ok := err.(awserr.Error); ok {
|
|
||||||
switch aerr.Code() {
|
|
||||||
case ses.ErrCodeMessageRejected:
|
|
||||||
log.Warn(ses.ErrCodeMessageRejected, aerr.Error())
|
|
||||||
case ses.ErrCodeMailFromDomainNotVerifiedException:
|
|
||||||
log.Warn(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error())
|
|
||||||
case ses.ErrCodeConfigurationSetDoesNotExistException:
|
|
||||||
log.Warn(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error())
|
|
||||||
default:
|
|
||||||
log.Warn(aerr.Error())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Warn(err.Error())
|
log.Warn(err.Error())
|
||||||
|
} else {
|
||||||
|
log.WithFields(log.Fields{"result": response}).Info("email sent")
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{"result": result}).Info("email sent")
|
aws_ses_id := db.Get_setting("AWS_SES_ID")
|
||||||
|
aws_ses_secret := db.Get_setting("AWS_SES_SECRET")
|
||||||
|
sender := db.Get_setting("AWS_SES_EMAIL_FROM")
|
||||||
|
region := db.Get_setting("AWS_REGION")
|
||||||
|
|
||||||
|
sess, err := session.NewSession(&aws.Config{
|
||||||
|
Region: aws.String(region),
|
||||||
|
Credentials: credentials.NewStaticCredentials(aws_ses_id, aws_ses_secret, ""),
|
||||||
|
})
|
||||||
|
|
||||||
|
svc := ses.New(sess)
|
||||||
|
|
||||||
|
charSet := "UTF-8"
|
||||||
|
|
||||||
|
input := &ses.SendEmailInput{
|
||||||
|
Destination: &ses.Destination{
|
||||||
|
CcAddresses: []*string{},
|
||||||
|
ToAddresses: []*string{
|
||||||
|
aws.String(recipient),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Message: &ses.Message{
|
||||||
|
Body: &ses.Body{
|
||||||
|
Html: &ses.Content{
|
||||||
|
Charset: aws.String(charSet),
|
||||||
|
Data: aws.String(htmlBody),
|
||||||
|
},
|
||||||
|
Text: &ses.Content{
|
||||||
|
Charset: aws.String(charSet),
|
||||||
|
Data: aws.String(textBody),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Subject: &ses.Content{
|
||||||
|
Charset: aws.String(charSet),
|
||||||
|
Data: aws.String(subject),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Source: aws.String(sender),
|
||||||
|
//ConfigurationSetName: aws.String(ConfigurationSet),
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := svc.SendEmail(input)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if aerr, ok := err.(awserr.Error); ok {
|
||||||
|
switch aerr.Code() {
|
||||||
|
case ses.ErrCodeMessageRejected:
|
||||||
|
log.Warn(ses.ErrCodeMessageRejected, aerr.Error())
|
||||||
|
case ses.ErrCodeMailFromDomainNotVerifiedException:
|
||||||
|
log.Warn(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error())
|
||||||
|
case ses.ErrCodeConfigurationSetDoesNotExistException:
|
||||||
|
log.Warn(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error())
|
||||||
|
default:
|
||||||
|
log.Warn(aerr.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Warn(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{"result": result}).Info("email sent")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -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
4
go.sum
|
|
@ -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=
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
159
internalapi/createboltcardwithpin.go
Normal file
159
internalapi/createboltcardwithpin.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
116
internalapi/updateboltcardwithpin.go
Normal file
116
internalapi/updateboltcardwithpin.go
Normal 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)
|
||||||
|
}
|
||||||
15
lnd/lnd.go
15
lnd/lnd.go
|
|
@ -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{
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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,` +
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
2
main.go
2
main.go
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
6
script/s_setup_test_data
Executable 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
|
||||||
|
|
@ -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
24
sql/data.test.sql
Normal 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);
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue