Compare commits

...

293 commits
v0.0.1 ... main

Author SHA1 Message Date
6ef61fe1af add option to have external port
Some checks failed
Go / check-formatting (push) Has been cancelled
Go / build-and-test (push) Has been cancelled
Go / build-docker-images (push) Has been cancelled
2025-01-20 01:09:49 +02:00
Peter Rounce
3a2096345d fix formatting 2024-06-08 10:05:50 +01:00
Peter Rounce
70638045dd
Merge pull request #79 from xx979xx/main
LN invoice expiry param & use it for ctx timeout
2024-06-08 09:38:46 +01:00
xx979xx
b3ac9e0a6c LN invoice expiry param & use it for ctx timeout 2024-06-03 13:33:30 +03:00
Peter Rounce
5564bd95c3
Merge pull request #78 from NicolasDorier/patch-2
Update DEEPLINK.md
2024-03-03 10:28:46 +00:00
Nicolas Dorier
8ceff1f8b8
Update DEEPLINK.md 2024-03-03 18:26:29 +09:00
Rob Clarkson
d041de3b6a
Merge pull request #77 from NicolasDorier/testvectordeeplink2
Add test vector for deeplinks
2024-01-22 21:42:11 +13:00
nicolas.dorier
ef1e2953c3
Add test vector for deeplinks 2024-01-22 17:41:59 +09:00
Rob Clarkson
51bdfc5cb9
Merge pull request #76 from NicolasDorier/deeplink
Add Deeplink spec
2024-01-22 19:31:19 +13:00
nicolas.dorier
e75f95e0f0
Add Deeplink spec 2024-01-11 20:22:11 +09:00
Peter Rounce
e60705ba49
Merge pull request #75 from NicolasDorier/qfointq
Remove the need to generate random CardKey
2023-10-28 12:52:52 +01:00
nicolas.dorier
51f09a6295
Remove the need to generate random CardKey 2023-10-25 13:57:29 +09:00
Peter Rounce
777bdb6081
Merge pull request #74 from NicolasDorier/fqpgtnrq
Remove BatchId concept, improve reset
2023-10-24 18:10:28 +01:00
nicolas.dorier
808336e084
Remove BatchId concept, improve reset 2023-10-24 16:52:10 +09:00
Peter Rounce
b0bd474dfc
Merge pull request #72 from NicolasDorier/fiowgnb
Derive deterministic keys on IssuerKey
2023-10-20 15:29:43 +01:00
nicolas.dorier
bdb350a177
Derive deterministic keys on IssuerKey 2023-10-20 23:14:22 +09:00
Peter Rounce
8379b3f88e
Merge pull request #71 from NicolasDorier/non-fixed-k0
Do not use a fixed K0 for deterministic keys
2023-10-20 06:21:37 +01:00
nicolas.dorier
70854d4508
Do not used a fixed K0 for deterministic keys 2023-10-20 10:50:51 +09:00
Peter Rounce
acb0c13a54
Merge pull request #70 from NicolasDorier/fixupdeterministic
Add security considerations to deterministic keys
2023-10-10 07:51:16 +01:00
Peter Rounce
d591092967
Update TECHNOLOGY.md 2023-10-10 07:30:05 +01:00
Peter Rounce
ed4c5db552
Update TECHNOLOGY.md 2023-10-10 07:29:31 +01:00
nicolas.dorier
1eb2b622ac
Add security considerations to deterministic keys 2023-10-10 10:43:59 +09:00
Peter Rounce
f1a8af86b6
Merge pull request #69 from NicolasDorier/deterministic
Add specification for deterministic bolt card keys
2023-10-09 14:06:09 +01:00
root
29a0168573 Add specification for deterministic bolt card keys 2023-10-09 21:58:36 +09:00
Peter Rounce
7745c9f20d
Update CARD_MANUAL.md 2023-09-23 17:02:52 +01:00
Peter Rounce
d5a64a8dab
Add files via upload 2023-09-23 16:58:19 +01:00
Peter Rounce
27ba1756ae
Add files via upload 2023-09-23 16:55:49 +01:00
Peter Rounce
c5921f6544
Update CARD_MANUAL.md 2023-09-23 16:22:37 +01:00
Peter Rounce
dc60a816c0
Add files via upload 2023-09-23 16:22:13 +01:00
Peter Rounce
165a46b9c1
Add files via upload 2023-09-23 16:20:58 +01:00
Peter Rounce
0712449103
Update CARD_MANUAL.md 2023-09-23 15:55:55 +01:00
Peter Rounce
94117718bd
update example
fixes #67
2023-09-22 08:13:24 +01:00
Peter Rounce
534367153a fix formatting 2023-09-16 18:36:56 +01:00
Peter Rounce
c36e19405d add cmac intermediate values 2023-09-16 18:36:14 +01:00
Peter Rounce
2b0392ea44 fix formatting 2023-09-16 15:28:36 +01:00
Peter Rounce
7d51fac18e Merge branch 'main' of https://github.com/boltcard/boltcard 2023-09-16 15:26:29 +01:00
Peter Rounce
46a83398b4 clarify counter value 2023-09-16 15:26:22 +01:00
Peter Rounce
36fc086e25
Update TEST_VECTORS.md 2023-09-16 12:28:38 +01:00
Peter Rounce
ae967cc011 Merge branch 'main' of https://github.com/boltcard/boltcard 2023-09-16 12:27:13 +01:00
Peter Rounce
3a1262db82 format 2023-09-16 12:27:02 +01:00
Peter Rounce
78a441baf5
Update TEST_VECTORS.md 2023-09-16 12:24:38 +01:00
Peter Rounce
2da9275c9a create some test vectors 2023-09-16 12:20:24 +01:00
Peter Rounce
34618bd228
Update SPEC.md 2023-08-09 11:52:37 +01:00
Peter Rounce
53ce60cfc3
update spec 2023-08-09 11:46:02 +01:00
Peter Rounce
056a52e1ba update parameter 2023-08-04 04:27:48 +00:00
Peter Rounce
797e4db605 make updating pin optional 2023-08-03 22:52:14 +00:00
Peter Rounce
8f83e04564 internalapi backward compatibility 2023-08-02 18:54:07 +00:00
Peter Rounce
4828610a5d update internalAPI 2023-07-31 06:37:03 +00:00
Peter Rounce
d2b6c30f3d improve logging 2023-07-27 18:15:35 +00:00
Peter Rounce
85bde475a3
Merge pull request #63 from boltcard/card-pin-number
update logging
2023-07-26 08:33:47 +01:00
Peter Rounce
df94c60fee fix formatting 2023-07-26 07:31:07 +00:00
Peter Rounce
3be7264ff1 update logging 2023-07-26 07:29:36 +00:00
Peter Rounce
de14c32867
Merge pull request #60 from boltcard/card-pin-number
Card pin number
2023-07-20 12:05:51 +01:00
Peter Rounce
6c03c1c3d9 check pin in payment rules 2023-07-20 11:00:37 +00:00
Chloe Jung
87306ca6db Format 2023-07-17 13:59:37 +12:00
Chloe Jung
598919fc2a Fix parse error 2023-07-17 12:16:55 +12:00
Chloe Jung
6609558f96 When returning boltcard data through api, send pin_enable and pin_limit_sats values as well 2023-07-17 11:59:07 +12:00
Peter Rounce
9b02a40cf3
Update FAQ.md 2023-07-06 06:32:25 +01:00
Peter Rounce
b43f580530
Update FAQ.md for 6982 error 2023-07-06 05:43:50 +01:00
Peter Rounce
ee942667e3
add FAQ 2023-07-06 05:35:53 +01:00
Peter Rounce
3d9e742dc1 pinLimit added to lnurlw response 2023-07-02 13:11:38 +00:00
Peter Rounce
105323a680 add testing 2023-07-02 07:23:58 +00:00
Peter Rounce
b76252d6ef create & update card pin details 2023-06-29 19:34:18 +00:00
Peter Rounce
299ab696cc
Update TECHNOLOGY.md 2023-05-14 06:41:05 +01:00
Peter Rounce
3b9db705f5
Merge pull request #56 from ponthief/main
support for SendGrid payments/balance emails
2023-05-01 12:53:19 +01:00
Djordje Kovacevic
77f1de8b7e support for SendGrid payments/balance emails 2023-05-01 08:49:38 +01:00
Peter Rounce
24082c831f
Merge pull request #54 from MizukiSonoko/default-description-uses-db-settings
Make DefaultDescription uses db settings
2023-04-22 10:10:18 +01:00
Peter Rounce
6cabfbf0d8
Merge pull request #55 from MizukiSonoko/aws-region-uses-db-settings
Make Aws Region uses db settings
2023-04-22 10:10:00 +01:00
Sonoko Mizuki
d3439db85b Set defualt value of AWS_REGION 2023-04-22 16:27:45 +09:00
Sonoko Mizuki
e9ef6973d4 set default value of default description 2023-04-22 16:21:10 +09:00
Peter Rounce
16df735233
Create TECHNOLOGY.md 2023-04-21 06:57:26 +01:00
Sonoko Mizuki
294a4eb054 Udpate setting.sql and docs 2023-04-20 12:12:06 +09:00
Sonoko Mizuki
7f8229cad0 Update email.go 2023-04-20 12:11:49 +09:00
Sonoko Mizuki
78a3dde2ed Update settings.sql and docs 2023-04-20 11:31:50 +09:00
Sonoko Mizuki
6661ebc7a4 Upade lnurlw_request 2023-04-20 11:31:35 +09:00
Peter Rounce
aa49fce3ff
Update CARD_PRIVACY.md 2023-04-15 10:54:15 +01:00
Peter Rounce
2165e248ca
Update CARD_PRIVACY.md 2023-04-15 10:50:35 +01:00
Peter Rounce
e249324e64
Merge pull request #52 from boltcard/lndhub-check-limit
Lndhub check limit
2023-03-29 19:57:17 +01:00
Chloe Jung
cb4edfe259 Remove the day limit and add the 'Update_payment_paid' function to lndhub pay function 2023-03-29 10:28:53 +13:00
Peter Rounce
68f38e2347
Update CARD_PRIVACY.md 2023-03-28 08:29:52 +01:00
Peter Rounce
cd7fc1338d
add 'privacy' doc link 2023-03-28 08:24:47 +01:00
Peter Rounce
d9dfb49a23
work in progress 2023-03-28 08:24:19 +01:00
Peter Rounce
d7258bb4ad
Create CARD_PRIVACY.md 2023-03-28 07:45:23 +01:00
Peter Rounce
f5328ab7ed
add 424 doc links 2023-03-28 07:31:15 +01:00
Peter Rounce
3034f1a65d
add NXP NTAG 424 docs 2023-03-28 07:24:40 +01:00
Chloe Jung
576356ced0 Add missing code 2023-03-28 11:33:22 +13:00
Chloe Jung
96d9c3ba0d Add the tx limit and daily limit check in the lndhub payment function 2023-03-28 11:27:19 +13:00
Peter Rounce
500eb9e8cd
Merge pull request #50 from boltcard/async-ok
respond with ok async
2023-03-24 13:41:49 +00:00
Peter Rounce
38cfc33935 format files 2023-03-24 13:36:16 +00:00
Peter Rounce
de0e6f2fb8 respond with ok async 2023-03-24 13:22:47 +00:00
Peter Rounce
0d3216ad99
Merge pull request #48 from boltcard/lndhub-status-ok
Send OK response on successful BoltHub payment
2023-03-24 10:27:25 +00:00
Rob Clarkson
4e87012ea1 Send OK response on successful BoltHub payment 2023-03-24 15:02:09 +13:00
Peter Rounce
806acb6096
Merge pull request #45 from boltcard/update-payinvoice-loginid
Update the payinvoice api call to LND hub
2023-03-14 12:23:25 +00:00
Chloe Jung
e8e2fa982a when calling payinvoice api to the hub, send the login id. 2023-03-09 17:32:24 +13:00
Peter Rounce
06ced63eae
Update settings.sql 2023-03-05 19:06:48 +00:00
Peter Rounce
1cff4ef46e Merge branch 'main' of https://github.com/boltcard/boltcard into main 2023-03-01 23:28:20 +00:00
Peter Rounce
368d0b9f50 fixes #44 2023-03-01 23:28:14 +00:00
Peter Rounce
a7de080b7c
include day_max 2023-03-01 23:26:25 +00:00
Peter Rounce
1bce49c670
fix formatting 2023-03-01 23:22:23 +00:00
Peter Rounce
b1fab9f1c1
Merge pull request #43 from boltcard/cardname
fixes #42
2023-02-28 17:48:44 +00:00
Peter Rounce
c5f70a2866 working 2023-02-28 17:45:00 +00:00
Peter Rounce
59bfce7050
Update CARD_ANDROID.md 2023-02-28 17:32:57 +00:00
Peter Rounce
62c0db37d5 remove UNIQUE from cards.card_name 2023-02-28 15:47:37 +00:00
Peter Rounce
9dc6d70ba4
Merge pull request #41 from onesandzeros-nz/origin/getcardapi
Add 'getboltcard' api call
2023-02-23 20:35:43 +00:00
Chloe Jung
c298bf0a3a update docker install readme 2023-02-24 09:25:12 +13:00
Chloe Jung
3f8863c463 Remove spave 2023-02-24 09:22:51 +13:00
Chloe Jung
f9b52cac13 re-do unnecessary changes 2023-02-24 09:22:17 +13:00
Chloe Jung
715f01d161 Re-order imports on the main.go - visual studio reordered automatically on save 2023-02-24 09:14:18 +13:00
Chloe Jung
484dd75c44 Remove 'last_counter_value' from getboltcard api call 2023-02-24 09:12:34 +13:00
Chloe Jung
af26b86288 Merge branch 'main' of https://github.com/boltcard/boltcard into origin/getcardapi
# Conflicts:
#	db/db.go
#	internalapi/updateboltcard.go
2023-02-24 09:10:20 +13:00
Peter Rounce
4042c4174a
Update SETTINGS.md 2023-02-23 13:55:44 +00:00
Peter Rounce
002dfd2c4e
Update SETTINGS.md 2023-02-23 13:55:19 +00:00
Peter Rounce
fe0854b397
add key creation link 2023-02-23 13:51:43 +00:00
Peter Rounce
77f9a7daa0
add anchors 2023-02-23 13:50:49 +00:00
Peter Rounce
7888b71f8d add day_max to /updateboltcard 2023-02-23 12:10:49 +00:00
Chloe Jung
91478af75e Update 'Get_card_from_card_name' sql statement. Remove wiped='N' 2023-02-23 17:35:12 +13:00
Chloe Jung
1acbe9895a Update the 'updateboltcard' api call. Allow updating 'day_limit_sats' 2023-02-23 17:13:26 +13:00
Chloe Jung
90f7f7a64b Add a new internal api call "getboltcard" 2023-02-23 16:29:31 +13:00
Peter Rounce
566f8a7b8c update dockerfile 2023-02-22 16:55:55 +00:00
Peter Rounce
6c7b1e98e3
add docker build to CI 2023-02-22 16:45:16 +00:00
Peter Rounce
29c8babeee
Update check.yml 2023-02-22 16:35:52 +00:00
Peter Rounce
d58a95da63
Update and rename go.yml to check.yml 2023-02-22 16:33:47 +00:00
Peter Rounce
c36178a068 removed apidoc 2023-02-22 14:44:02 +00:00
Peter Rounce
abfa2f2cdf
Merge pull request #40 from boltcard/updateboltcard
Updateboltcard
2023-02-22 13:20:14 +00:00
Peter Rounce
185e1212c2
Merge pull request #39 from boltcard/main
Merge pull request #38 from boltcard/updateboltcard
2023-02-22 13:19:40 +00:00
Peter Rounce
542044338c remove CLI for create/wipe, use internalAPI 2023-02-22 13:18:33 +00:00
Peter Rounce
9898ca1450
Merge pull request #38 from boltcard/updateboltcard
addition to internal API for /updateboltcard
2023-02-22 12:41:11 +00:00
Peter Rounce
775996c8e4 working /updateboltcard 2023-02-22 12:38:56 +00:00
Peter Rounce
5c3bb68995 fix formatting 2023-02-22 08:13:01 +00:00
Peter Rounce
dadc76f0d3 add Updateboltcard() 2023-02-22 08:11:52 +00:00
Peter Rounce
0300a5aa30
Merge pull request #37 from boltcard/main
update branch
2023-02-22 07:13:01 +00:00
Peter Rounce
17627b25de
Merge pull request #36 from boltcard/update-lndhub-url
Use host domain from settings
2023-02-22 02:44:11 +00:00
Rob Clarkson
51adf26398 fix IDE import "autocorrect" issue 2023-02-22 15:08:21 +13:00
Rob Clarkson
daf7d5e5e4 Use host domain from settings as docker requests come in on non-resolvable hosts 2023-02-22 15:05:52 +13:00
Peter Rounce
5af1629cc5 partial 2023-02-21 12:44:27 +00:00
Peter Rounce
1ac076319a
Merge pull request #35 from boltcard/update-sql-path
Update sql files path for docker
2023-02-21 05:53:45 +00:00
Peter Rounce
5f39722679
Merge pull request #34 from boltcard/update-lndhub-url
Can we please remove the https specification
2023-02-21 05:52:26 +00:00
Chloe Jung
4070b38a58 Update sql files paths for docker and add 'FUNCTION_INTERNAL_API' into settings.sql 2023-02-21 17:07:40 +13:00
Rob Clarkson
d79d6eb0ba remove https specification so that this setting can be used with docker which requires http for internal comms 2023-02-21 15:53:11 +13:00
Peter Rounce
96372f0e90
Update README.md 2023-02-19 17:36:14 +00:00
Peter Rounce
f84014d2ea
Merge pull request #32 from boltcard/lndhub
Lndhub
2023-02-19 17:30:45 +00:00
Peter Rounce
0170019fee update formatting 2023-02-19 17:19:28 +00:00
Peter Rounce
54f1274fa6
Rename LNDHUB.MD to LNDHUB.md 2023-02-19 17:14:06 +00:00
Peter Rounce
acebd2c29c
Create LNDHUB.MD 2023-02-19 17:13:38 +00:00
Peter Rounce
940fd0f591 lndhub payinvoice works 2023-02-19 17:05:11 +00:00
Peter Rounce
976f5b9929 tidy 2023-02-19 08:53:15 +00:00
Peter Rounce
863b7543d5
Merge pull request #30 from mbio16/docker-compose-own-reverse-proxy
Docker compose own reverse proxy
2023-02-19 08:46:49 +00:00
Peter Rounce
bba519888b
Merge pull request #31 from boltcard/main
update branch
2023-02-19 06:56:48 +00:00
Peter Rounce
1fe32b1704 fix build 2023-02-19 06:54:51 +00:00
Peter Rounce
b4494c7ed3 fix formatting 2023-02-19 06:52:12 +00:00
Peter Rounce
f3949b1b59 merge 2023-02-19 06:47:28 +00:00
Peter Rounce
9a5016225f lndhub /auth 2023-02-18 21:09:35 +00:00
Peter Rounce
a0a62f738f fix build 2023-02-18 17:44:15 +00:00
Peter Rounce
d1559c61f0 update package 2023-02-18 14:49:38 +00:00
Peter Rounce
918b806567 update package 2023-02-18 14:49:09 +00:00
Peter Rounce
4f4e320999 new packages 2023-02-18 14:44:01 +00:00
Peter Rounce
aa5ccaded0 create sql folder 2023-02-18 14:17:14 +00:00
Peter Rounce
9545e5bd4e create db/lnd/email packages 2023-02-18 14:14:57 +00:00
Martin Biolek
7b4013764a Update DOCKER_INSTALL.md 2023-02-17 21:06:06 +01:00
Martin Biolek
140f794b43 docker compose own reverse proxy 2023-02-17 21:04:27 +01:00
Peter Rounce
91f6d388eb
Merge pull request #29 from boltcard/internal-api
an API option for internal/private use (no authentication)
this covers the createboltcard & wipeboltcard
console options have been left in but are a duplicate and should be reconsidered
2023-02-17 10:57:41 +00:00
Peter Rounce
10f2a756b9 fix formatting 2023-02-17 10:51:46 +00:00
Peter Rounce
c54328001f fix formatting 2023-02-17 10:50:14 +00:00
Peter Rounce
6153edd0c3
Update README.md 2023-02-14 10:05:02 +00:00
Peter Rounce
0a1634b7b6
simplify document 2023-02-14 10:00:33 +00:00
Peter Rounce
f20bb436c7
minor document tidy 2023-02-14 09:38:58 +00:00
Peter Rounce
2f00965655
Merge pull request #28 from titusz/patch-1
Wrong offsets for example data.
2023-02-09 19:32:24 +00:00
Titusz
4ed2c1b948
Wrong offsets for example data. 2023-02-09 20:22:36 +01:00
Peter Rounce
5c92fa2fa6
Update README.md 2023-02-09 09:13:06 +00:00
Peter Rounce
506b6c5b01
clarify steps to install service 2023-02-09 09:12:00 +00:00
Peter Rounce
ef4c16b14f
Merge pull request #27 from onesandzeros-nz/main
Dockerized service
2023-02-09 08:29:01 +00:00
chloehjung15
e3d006ffe0
Update DOCKER_INSTALL.md 2023-02-09 15:58:59 +13:00
chloehjung15
351a47790e
Merge branch 'boltcard:main' into main 2023-02-08 09:08:10 +13:00
Peter Rounce
f1b955db94
remove apidoc from actions 2023-02-07 16:07:00 +00:00
Peter Rounce
de6a0706d0
Update go.yml 2023-02-07 16:03:25 +00:00
Peter Rounce
9e99ac28d8
Update go.yml 2023-02-07 15:58:12 +00:00
Peter Rounce
b7dcae4837
Update go.yml 2023-02-07 15:53:44 +00:00
Peter Rounce
7c3bb1638b
Update go.yml 2023-02-07 15:47:01 +00:00
Peter Rounce
2ba5de6636
Update go.yml 2023-02-07 15:41:29 +00:00
Peter Rounce
1b67499da1
add apidocs 2023-02-07 15:29:35 +00:00
Peter Rounce
a5d4643bf2
fix JSON response 2023-02-07 08:03:49 +00:00
Chloe Jung
c423f96711 Update read me 2023-02-07 09:18:29 +13:00
Chloe Jung
3e3412233d Change cert.tls to tls.cert 2023-02-07 09:14:15 +13:00
Chloe Jung
4a66bbf3f1 Update the setting the db password in env script 2023-02-03 15:20:37 +13:00
Chloe Jung
9f601f59db Update docker install readme 2023-02-03 15:17:37 +13:00
chloehjung15
3c84738750
Merge branch 'boltcard:main' into main 2023-02-03 10:54:12 +13:00
Peter Rounce
4d7dfb481d API for wipeboltcard call added 2023-02-02 15:52:57 +00:00
Peter Rounce
fe0b6fafb4 API for createboltcard call added 2023-02-02 11:54:41 +00:00
Peter Rounce
58c074234c add internal API & ping 2023-02-02 09:16:23 +00:00
Chloe Jung
f0200d521d Merge branch 'main' of github.com:onesandzeros-nz/boltcard into main 2023-02-02 15:09:09 +13:00
Chloe Jung
004fc003d9 Do not expose db and boltcard service containers ports 2023-02-02 15:08:54 +13:00
Peter Rounce
53a8e1f581 take card wipe status into account 2023-02-01 12:06:03 +00:00
chloehjung15
7d76797154
Update DOCKER_INSTALL.md 2023-02-01 17:33:11 +13:00
Chloe Jung
216037bffa Merge branch 'main' of github.com:onesandzeros-nz/boltcard into main 2023-02-01 17:27:17 +13:00
Chloe Jung
76eabf453f Push 'docker_init.sh' 2023-02-01 17:27:03 +13:00
chloehjung15
8c47ef140b
Update DOCKER_INSTALL.md 2023-02-01 15:48:54 +13:00
chloehjung15
4cb0351f14
Update DOCKER_INSTALL.md 2023-02-01 15:47:42 +13:00
Chloe Jung
9ebc75671c Update the volumes for go container 2023-02-01 15:45:09 +13:00
Chloe Jung
61500781e9 Update the docker-compose. Remove the volumes for key files 2023-02-01 15:11:59 +13:00
Chloe Jung
59d1953db9 Change boltcard.macaroon to admin.macaroon 2023-02-01 14:37:56 +13:00
Chloe Jung
bdb0b5d74d Update docker-compose 2023-02-01 14:23:25 +13:00
Chloe Jung
2ea06393e6 Merge branch 'main' of github.com:onesandzeros-nz/boltcard into main 2023-02-01 13:24:26 +13:00
Chloe Jung
4af6cc7454 Put the caddy container on the same network 2023-02-01 13:24:10 +13:00
Rob Clarkson
9269b25fa5
Update DOCKER_INSTALL.md 2023-02-01 13:10:20 +13:00
Chloe Jung
fd7d4e5134 Update the readme 2023-02-01 11:55:11 +13:00
Chloe Jung
becc52d72b Add docker install readme 2023-02-01 11:45:29 +13:00
Chloe Jung
fb9c74cbdb Add a "Caddyfile_docker" file. Update docker-compose.yml file 2023-02-01 11:10:35 +13:00
Chloe Jung
6475460879 Change the port to 80 from 8080 2023-02-01 09:45:01 +13:00
Chloe Jung
20c7936615 Add the .env.example file 2023-01-31 16:51:03 +13:00
Chloe Jung
2ea481e281 Separate out the "DROP" sql code. In docker, it results an error "cannot drop the currently open database". 2023-01-31 16:50:55 +13:00
Chloe Jung
a0725fea0b Add in the dockerfile and the docker compose files 2023-01-31 16:49:18 +13:00
Peter Rounce
af2411ad6a
fix formatting check 2023-01-29 10:57:11 +00:00
Peter Rounce
db855d4ecc
add go format checking 2023-01-29 10:51:46 +00:00
Peter Rounce
00dfcb1070 Merge branch 'main' of https://github.com/boltcard/boltcard into main 2023-01-29 10:45:18 +00:00
Peter Rounce
dbc14a4dd3 fix for test 2023-01-29 10:45:11 +00:00
Peter Rounce
28be56a394
build & test 2023-01-29 10:43:13 +00:00
Peter Rounce
38050c8785 Merge branch 'main' of https://github.com/boltcard/boltcard into main 2023-01-29 10:33:42 +00:00
Peter Rounce
7816566d59 reformat with gofmt 2023-01-29 10:33:34 +00:00
Peter Rounce
e2073dccc0
add settings details 2023-01-28 15:38:21 +00:00
Peter Rounce
d82c17e0a1
move to settings table in database 2023-01-28 14:50:53 +00:00
Peter Rounce
226768557b
Merge pull request #25 from boltcard/db_settings
Move settings to database
2023-01-27 18:50:59 +00:00
Peter Rounce
5af715f553 update build, add AWS settings 2023-01-27 18:25:00 +00:00
Peter Rounce
0e3895a1f2 fix build 2023-01-27 13:30:22 +00:00
Peter Rounce
59151232dd use database settings for lookups 2023-01-27 12:01:47 +00:00
Peter Rounce
085b402fef add settings table to database 2023-01-27 08:45:57 +00:00
Peter Rounce
177b085c3f fix comment 2023-01-26 07:26:47 +00:00
Peter Rounce
b1ebacb9ce
Delete wipeboltcard 2023-01-08 21:11:37 +00:00
Peter Rounce
dd08241235
Update INSTALL.md 2022-12-27 10:10:27 +00:00
Peter Rounce
856fdc7a73
Update INSTALL.md 2022-12-27 10:09:35 +00:00
Peter Rounce
0bc056fdb0
fix cards.email_address example 2022-12-17 12:31:31 +00:00
Peter Rounce
39c5570f2b
add -allow_neg_bal to createboltcard example 2022-12-02 07:34:08 +00:00
Peter Rounce
bc5842374a add -uid_privacy and -allow_neg_bal to createboltcard 2022-12-02 07:31:15 +00:00
Peter Rounce
bfc6436098 add wipeboltcard 2022-11-21 10:59:36 +00:00
Peter Rounce
52533dd887 remove card record tidy as it would need updating - fixes #16 2022-11-20 13:08:38 +00:00
Peter Rounce
f685b4a248 check for unique card_name - fixes #17 2022-11-20 13:02:06 +00:00
Peter Rounce
50ddfd0726 ensure card name is set 2022-11-20 12:33:10 +00:00
Peter Rounce
ed7c3ae7cc
Merge pull request #20 from gimme/fee-limit-sum
Make the fee limit the sum of base + percentage
2022-11-19 16:34:49 +00:00
Peter Rounce
b6d37c6eb7 update dependencies 2022-11-19 16:29:24 +00:00
Anders Magnusson
6ab7468c25
Make the fee limit the sum of base + percentage 2022-11-19 16:58:19 +01:00
Peter Rounce
e0c19f38a0
Merge pull request #18 from gimme/fee-limit-percent
Add FEE_LIMIT_PERCENT setting
2022-11-19 15:40:02 +00:00
Peter Rounce
acf331a144 add message re. MAX_WITHDRAW_SATS 2022-11-19 15:22:41 +00:00
Anders Magnusson
9cb751e066
Add FEE_LIMIT_PERCENT setting 2022-11-19 11:06:29 +01:00
Peter Rounce
5de205c4d4
Update FAQ.md 2022-11-16 20:29:23 +00:00
Peter Rounce
efbcefce8c
Update INSTALL.md 2022-11-16 20:17:25 +00:00
Peter Rounce
e7ac3152c2
Update INSTALL.md 2022-11-16 14:31:41 +00:00
Peter Rounce
0ab3f4b747
Update INSTALL.md 2022-11-16 14:29:52 +00:00
Peter Rounce
6b3e482a06
Update INSTALL.md 2022-11-16 13:49:12 +00:00
Peter Rounce
8188ddde51
clarify environment variables & database settings 2022-11-16 08:39:42 +00:00
Peter Rounce
cef58329bb update schema 2022-10-09 20:37:12 +00:00
Peter Rounce
5359540862 Merge branch 'main' of https://github.com/boltcard/boltcard into main 2022-10-09 20:16:35 +00:00
Peter Rounce
b9faa49928 bugfix createcard 2022-10-09 20:11:45 +00:00
Peter Rounce
b65b9ac7e6
Update CARD_ANDROID.md
add hints for environment variable setup
2022-10-09 21:02:43 +01:00
Peter Rounce
53a02eddd3
Merge pull request #15 from boltcard/ln-addr
Ln addr
2022-09-25 20:17:39 +01:00
Peter Rounce
9c5149e373 add max txs setting for email, add setting to stop negative card balance 2022-09-25 19:01:52 +00:00
Peter Rounce
6db43987e6
add EMAIL_MAX_TXS setting 2022-09-25 20:00:12 +01:00
Peter Rounce
94267c47cf add transactions to email body 2022-09-25 10:33:24 +00:00
Peter Rounce
ab1b7c464f add balance to update email 2022-09-25 06:47:26 +00:00
Peter Rounce
9e7ea48c78
add install notes for LNURLp & email 2022-09-20 06:20:41 +01:00
Peter Rounce
3718025843
add env. vars. for LNURLp & email 2022-09-20 06:11:08 +01:00
Peter Rounce
5d3072fa6d
Merge pull request #14 from boltcard/ln-addr
adds support for lightning address receipts & email notifications
2022-09-20 04:03:54 +01:00
Peter Rounce
a42feca90d add emails to lnurlw & lnurlp 2022-09-20 02:59:14 +00:00
Peter Rounce
8d01474d5f add LNURLp switch 2022-09-19 06:40:24 +00:00
Peter Rounce
4a38533cec store lnurlp status in database 2022-09-17 12:30:01 +00:00
Peter Rounce
c368c1ef16 tidy lnurlp fields 2022-09-17 07:14:21 +00:00
Peter Rounce
e3c75d885f monitor lnurlp invoice state 2022-09-17 06:42:18 +00:00
Peter Rounce
f12c7c2888 separate lnurlp request & callback code 2022-09-16 02:50:07 +00:00
Peter Rounce
6b43c3f6aa separate lnurlp code 2022-09-16 02:44:00 +00:00
Peter Rounce
27be212010 lightning address receive works 2022-09-15 15:47:50 +00:00
Peter Rounce
524367ec3c separate grpc connection 2022-09-14 15:31:51 +00:00
Peter Rounce
e16fca179b start adding handling for lnurlp 2022-09-14 12:54:29 +00:00
Peter Rounce
79c6369c39 change http router 2022-09-13 07:35:28 +00:00
Peter Rounce
d6724d416e
Update boltcard.service 2022-09-09 07:12:44 +01:00
Peter Rounce
5f3c7cc661
Merge pull request #13 from dipunm/patch-1
Changes for allowing to run on raspiblitz
2022-09-09 06:58:44 +01:00
Dipun Mistry
c46d7c106f Added support for TOR addresses 2022-09-05 01:06:41 +01:00
Dipun Mistry
49dd82f302
'nother bug fix 2022-09-04 22:25:18 +01:00
Dipun Mistry
c61ce27d10
bug. 2022-09-04 22:18:53 +01:00
Dipun Mistry
17de764649
Allow configurability of host port 2022-09-04 16:20:27 +01:00
Peter Rounce
6ebd94762b
Update SPEC.md 2022-09-01 07:46:36 +01:00
Peter Rounce
d8dafb1d2e
update document for the new create card flow 2022-08-31 21:17:01 +01:00
Peter Rounce
c1c88e392a Merge branch 'main' of https://github.com/boltcard/boltcard into main 2022-08-29 07:38:01 +00:00
Peter Rounce
1c9a29a4dd added a log entry for service start 2022-08-29 07:37:49 +00:00
Peter Rounce
dd6cc20c3e
Update INSTALL.md 2022-08-26 15:40:30 +01:00
Peter Rounce
7709c31164
Create SECURITY.md 2022-08-26 06:55:05 +01:00
Peter Rounce
82569d097c ensure upgrade of github.com/miekg/dns 2022-08-26 05:34:01 +00:00
Peter Rounce
05ac1e8aea update lnd grpc 2022-08-26 05:14:36 +00:00
Peter Rounce
44b2f0ba6e add descriptive error 2022-08-25 21:22:27 +00:00
Peter Rounce
8d1d686355 ensure a response is always sent 2022-08-24 15:23:24 +00:00
Peter Rounce
8c1150468d add data warning 2022-08-24 13:24:28 +00:00
Peter Rounce
a5ef334349 simplify database creation 2022-08-24 13:04:49 +00:00
Peter Rounce
7209e0db1e update API docs 2022-08-24 09:39:58 +00:00
Peter Rounce
0736169b4c update API docs 2022-08-24 09:21:38 +00:00
Peter Rounce
16a4cf444b Merge branch 'main' of https://github.com/boltcard/boltcard into main 2022-08-24 09:04:00 +00:00
Peter Rounce
8edb9a3993 update create_bolt_card API 2022-08-24 09:03:50 +00:00
73 changed files with 5661 additions and 1288 deletions

1
.env.example Normal file
View file

@ -0,0 +1 @@
DB_PASSWORD=password

46
.github/workflows/check.yml vendored Normal file
View file

@ -0,0 +1,46 @@
# https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
# https://github.com/marketplace/actions/check-code-formatting-using-gofmt
name: Go
on: [push, pull_request]
jobs:
check-formatting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check formatting
uses: Jerome1337/gofmt-action@v1.0.5
with:
gofmt-path: '.'
gofmt-flags: '-l -d'
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
build-docker-images:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build the Docker image
run: docker compose build

6
.gitignore vendored
View file

@ -6,6 +6,8 @@
*.dylib *.dylib
boltcard boltcard
createboltcard/createboltcard createboltcard/createboltcard
wipeboltcard/wipeboltcard
cli/cli
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test
@ -16,9 +18,9 @@ createboltcard/createboltcard
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
# secrets # possible secrets
tls.cert tls.cert
*.macaroon* *.macaroon*
add_test_data.sql
Caddyfile Caddyfile
boltcard.service boltcard.service
*.secret

2
Caddyfile_docker Normal file
View file

@ -0,0 +1,2 @@
https://card.yourdomain.com
reverse_proxy boltcard_main:9000

7
Dockerfile Normal file
View file

@ -0,0 +1,7 @@
FROM golang:1.19.0-bullseye
WORKDIR /App
ADD . /App
RUN go build
ENTRYPOINT ["/App/boltcard"]

View file

@ -8,18 +8,27 @@ Each bolt card makes use of a service to receive the request from the merchant s
The 'bolt card service' software provided here can be used to host bolt cards for yourself and others. The 'bolt card service' software provided here can be used to host bolt cards for yourself and others.
The 'bolt card creation' instructions describe how to set up bolt cards for use with your bolt card service. The simplest way to understand and set up your own system is to read the main document set below.
## Documents ## Main document set
| Document | Description | | Document | Description |
| --- | --- | | --- | --- |
| [Specification](docs/SPEC.md) | Bolt card specifications |
| [System](docs/SYSTEM.md) | Bolt card system overview | | [System](docs/SYSTEM.md) | Bolt card system overview |
| [Service Install](docs/INSTALL.md) | Bolt card service installation | | [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 |
| [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|
| [Manual Card Creation](docs/CARD_MANUAL.md) | Bolt card creation using NXP TagXplorer software|
## Additional
| Document | Description |
| --- | --- |
| [Service Install](docs/INSTALL.md) | Bolt card service installation |
| [Manual Card Creation](docs/CARD_MANUAL.md) | Bolt card creation using NXP TagXplorer software |
| [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

4
SECURITY.md Normal file
View file

@ -0,0 +1,4 @@
# Security Policy
Report issues you find by Telegram secret chat to `@peter_1337` .
Please do not disclose any possible security vulnerabilities to third parties.

View file

@ -1,26 +0,0 @@
\c card_db
INSERT INTO cards (
lock_key, /* this is key 0 on the card */
aes_cmac, /* this is key 2 on the card */
uid, /* this can be discovered from the service log */
last_counter_value, /* can start at zero and will be updated on first use (before issue) */
lnurlw_request_timeout_sec, /* 60 seconds by default */
enable_flag, /* useful for quickly switching card hosting on/off */
tx_limit_sats, /* set at a reasonable value for small test payments in 2022 */
day_limit_sats, /* set at a reasonable value for small test payments in 2022 */
card_description, /* to store a human readable card description (optional) */
one_time_code_used /* used to indicate if the one_time_code for card creation is live */
)
VALUES (
'00000000000000000000000000000000',
'00000000000000000000000000000000',
'00000000000000',
0,
60,
'Y',
1000,
10000,
'bolt card',
'Y'
);

View file

@ -10,41 +10,14 @@ Restart=always
RestartSec=10 RestartSec=10
User=ubuntu User=ubuntu
# boltcard service settings # postgres database connection settings
# LOG_LEVEL is DEBUG or PRODUCTION
Environment="LOG_LEVEL=DEBUG"
# AES_DECRYPT_KEY is the hex value of the server decrypt key for hosted bolt cards
Environment="AES_DECRYPT_KEY=00000000000000000000000000000000"
# DB_ values are for the postgres database connection
Environment="DB_HOST=localhost" Environment="DB_HOST=localhost"
Environment="DB_PORT=5432" Environment="DB_PORT=5432"
Environment="DB_USER=cardapp" Environment="DB_USER=cardapp"
Environment="DB_PASSWORD=database_password" Environment="DB_PASSWORD=database_password"
Environment="DB_NAME=card_db" Environment="DB_NAME=card_db"
# HOST_DOMAIN is the URL base prefix for the lnurlw calls ExecStart=/bin/bash /home/ubuntu/boltcard/script/s_launch
Environment="HOST_DOMAIN=card.yourdomain.com"
# MIN_WITHDRAW_SATS & MAX_WITHDRAW_SATS set the values for the lnurlw response
Environment="MIN_WITHDRAW_SATS=1"
Environment="MAX_WITHDRAW_SATS=1000000"
# LN_ values are for the lightning server used for making payments
Environment="LN_HOST=ln.host.io"
Environment="LN_PORT=10009"
Environment="LN_TLS_FILE=/home/ubuntu/boltcard/tls.cert"
Environment="LN_MACAROON_FILE=/home/ubuntu/boltcard/SendPaymentV2.macaroon"
# FEE_LIMIT_SAT is the maximum lightning network fee to be paid
Environment="FEE_LIMIT_SAT=10"
# LN_TESTNODE may be used in testing and will then only pay to the defined test node pubkey
#Environment="LN_TESTNODE=000000000000000000000000000000000000000000000000000000000000000000"
ExecStart=/bin/bash /home/ubuntu/boltcard/s_launch
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

198
cli/main.go Normal file
View file

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

View file

@ -1,42 +0,0 @@
DROP DATABASE IF EXISTS card_db;
CREATE DATABASE card_db;
CREATE USER cardapp WITH PASSWORD 'database_password';
\c card_db;
CREATE TABLE cards (
card_id INT GENERATED ALWAYS AS IDENTITY,
lock_key CHAR(32) NOT NULL,
aes_cmac CHAR(32) NOT NULL,
uid CHAR(14) NOT NULL,
last_counter_value INTEGER NOT NULL,
lnurlw_request_timeout_sec INT NOT NULL,
enable_flag CHAR(1) NOT NULL DEFAULT 'N',
tx_limit_sats INT NOT NULL,
day_limit_sats INT NOT NULL,
card_description VARCHAR(100) NOT NULL DEFAULT '',
one_time_code CHAR(32) NOT NULL DEFAULT '',
one_time_code_expiry TIMESTAMPTZ DEFAULT NOW() + INTERVAL '1 DAY',
one_time_code_used CHAR(1) NOT NULL DEFAULT 'Y',
PRIMARY KEY(card_id)
);
CREATE TABLE card_payments (
card_payment_id INT GENERATED ALWAYS AS IDENTITY,
card_id INT NOT NULL,
k1 CHAR(32) UNIQUE NOT NULL,
lnurlw_request_time TIMESTAMPTZ NOT NULL,
ln_invoice VARCHAR(1024) NOT NULL DEFAULT '',
amount_msats BIGINT CHECK (amount_msats > 0),
paid_flag CHAR(1) NOT NULL,
payment_time TIMESTAMPTZ,
payment_status VARCHAR(100) NOT NULL DEFAULT '',
failure_reason VARCHAR(100) NOT NULL DEFAULT '',
payment_status_time TIMESTAMPTZ,
PRIMARY KEY(card_payment_id),
CONSTRAINT fk_card FOREIGN KEY(card_id) REFERENCES cards(card_id)
);
GRANT ALL PRIVILEGES ON TABLE cards TO cardapp;
GRANT ALL PRIVILEGES ON TABLE card_payments TO cardapp;

View file

@ -1,76 +0,0 @@
package main
import (
"database/sql"
"errors"
"fmt"
_ "github.com/lib/pq"
"os"
)
func db_open() (*sql.DB, error) {
// get connection string from environment variables
conn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
os.Getenv("DB_HOST"),
os.Getenv("DB_PORT"),
os.Getenv("DB_USER"),
os.Getenv("DB_PASSWORD"),
os.Getenv("DB_NAME"))
db, err := sql.Open("postgres", conn)
if err != nil {
return db, err
}
return db, nil
}
func db_delete_expired() error {
db, err := db_open()
if err != nil {
return err
}
defer db.Close()
// delete expired one time code records
sqlStatement := `DELETE FROM cards WHERE one_time_code_expiry < NOW() AND one_time_code_used = 'N';`
_, err = db.Exec(sqlStatement)
if err != nil {
return err
}
return nil
}
func db_insert_card(one_time_code string, lock_key string, aes_cmac string) error {
db, err := db_open()
if err != nil {
return err
}
defer db.Close()
// insert a new record into cards
sqlStatement := `INSERT INTO cards` +
` (one_time_code, lock_key, aes_cmac, uid, last_counter_value,` +
` lnurlw_request_timeout_sec, tx_limit_sats, day_limit_sats, one_time_code_used)` +
` VALUES ($1, $2, $3, '', 0, 60, 1000, 10000, 'N');`
res, err := db.Exec(sqlStatement, one_time_code, lock_key, aes_cmac)
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
}

View file

@ -1,51 +0,0 @@
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
log "github.com/sirupsen/logrus"
qrcode "github.com/skip2/go-qrcode"
"os"
)
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 main() {
one_time_code := random_hex()
lock_key := random_hex()
aes_cmac := random_hex()
// create the new card record
err := db_insert_card(one_time_code, lock_key, aes_cmac)
if err != nil {
log.Warn(err.Error())
return
}
// remove any expired records
err = db_delete_expired()
if err != nil {
log.Warn(err.Error())
return
}
// show a QR code on the console for the URI + one_time_code
hostdomain := os.Getenv("HOST_DOMAIN")
url := "https://" + hostdomain + "/new?a=" + one_time_code
fmt.Println(url)
q, err := qrcode.New(url, qrcode.Medium)
fmt.Println(q.ToSmallString(false))
}

View file

@ -1,4 +1,4 @@
package main package crypto
import ( import (
"bytes" "bytes"
@ -9,7 +9,7 @@ import (
"github.com/aead/cmac" "github.com/aead/cmac"
) )
func create_k1() (string, error) { func Create_k1() (string, error) {
// 16 bytes = 128 bits // 16 bytes = 128 bits
b := make([]byte, 16) b := make([]byte, 16)
@ -24,7 +24,7 @@ func create_k1() (string, error) {
} }
// decrypt p with aes_dec // decrypt p with aes_dec
func crypto_aes_decrypt(key_sdm_file_read []byte, ba_p []byte) ([]byte, error) { func Aes_decrypt(key_sdm_file_read []byte, ba_p []byte) ([]byte, error) {
dec_p := make([]byte, 16) dec_p := make([]byte, 16)
iv := make([]byte, 16) iv := make([]byte, 16)
@ -38,7 +38,7 @@ func crypto_aes_decrypt(key_sdm_file_read []byte, ba_p []byte) ([]byte, error) {
return dec_p, nil return dec_p, nil
} }
func crypto_aes_cmac(key_sdm_file_read_mac []byte, sv2 []byte, ba_c []byte) (bool, error) { 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) c2, err := aes.NewCipher(key_sdm_file_read_mac)
if err != nil { if err != nil {

View file

@ -1,343 +0,0 @@
package main
import (
"database/sql"
"errors"
"fmt"
_ "github.com/lib/pq"
"os"
)
type card struct {
card_id int
card_guid string
aes_dec string
aes_cmac string
db_uid string
last_counter_value uint32
lnurlw_request_timeout_sec int
enable_flag string
tx_limit_sats int
day_limit_sats int
one_time_code string
lock_key string
}
type payment struct {
card_payment_id int
card_id int
k1 string
paid_flag string
}
func db_open() (*sql.DB, error) {
// get connection string from environment variables
conn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
os.Getenv("DB_HOST"),
os.Getenv("DB_PORT"),
os.Getenv("DB_USER"),
os.Getenv("DB_PASSWORD"),
os.Getenv("DB_NAME"))
db, err := sql.Open("postgres", conn)
if err != nil {
return db, err
}
return db, nil
}
func db_get_new_card(one_time_code string) (*card, error) {
c := card{}
db, err := db_open()
if err != nil {
return &c, err
}
defer db.Close()
sqlStatement := `SELECT lock_key, aes_cmac` +
` FROM cards WHERE one_time_code=$1 AND` +
` one_time_code_expiry > NOW() AND one_time_code_used = 'N';`
row := db.QueryRow(sqlStatement, one_time_code)
err = row.Scan(
&c.lock_key,
&c.aes_cmac)
if err != nil {
return &c, err
}
sqlStatement = `UPDATE cards SET one_time_code_used = 'Y' WHERE one_time_code = $1;`
_, err = db.Exec(sqlStatement, one_time_code)
if err != nil {
return &c, err
}
return &c, nil
}
func db_get_card_from_uid(card_uid string) (*card, error) {
c := card{}
db, err := db_open()
if err != nil {
return &c, err
}
defer db.Close()
sqlStatement := `SELECT card_id, aes_cmac, uid,` +
` last_counter_value, lnurlw_request_timeout_sec,` +
` enable_flag, tx_limit_sats, day_limit_sats` +
` FROM cards WHERE uid=$1;`
row := db.QueryRow(sqlStatement, card_uid)
err = row.Scan(
&c.card_id,
&c.aes_cmac,
&c.db_uid,
&c.last_counter_value,
&c.lnurlw_request_timeout_sec,
&c.enable_flag,
&c.tx_limit_sats,
&c.day_limit_sats)
if err != nil {
return &c, err
}
return &c, nil
}
func db_get_card_from_card_id(card_id int) (*card, error) {
c := card{}
db, err := db_open()
if err != nil {
return &c, err
}
defer db.Close()
sqlStatement := `SELECT card_id, aes_cmac, uid,` +
` last_counter_value, lnurlw_request_timeout_sec,` +
` enable_flag, tx_limit_sats, day_limit_sats` +
` FROM cards WHERE card_id=$1;`
row := db.QueryRow(sqlStatement, card_id)
err = row.Scan(
&c.card_id,
&c.aes_cmac,
&c.db_uid,
&c.last_counter_value,
&c.lnurlw_request_timeout_sec,
&c.enable_flag,
&c.tx_limit_sats,
&c.day_limit_sats)
if err != nil {
return &c, err
}
return &c, nil
}
func db_check_lnurlw_timeout(card_payment_id int) (bool, error) {
db, err := db_open()
if err != nil {
return true, err
}
defer db.Close()
lnurlw_timeout := true
sqlStatement := `SELECT NOW() > cp.lnurlw_request_time + c.lnurlw_request_timeout_sec * INTERVAL '1 SECOND'` +
` FROM card_payments AS cp INNER JOIN cards AS c ON c.card_id = cp.card_id` +
` WHERE cp.card_payment_id=$1;`
row := db.QueryRow(sqlStatement, card_payment_id)
err = row.Scan(&lnurlw_timeout)
if err != nil {
return true, err
}
return lnurlw_timeout, nil
}
func db_check_and_update_counter(card_id int, new_counter_value uint32) (bool, error) {
db, err := db_open()
if err != nil {
return false, err
}
defer db.Close()
sqlStatement := `UPDATE cards SET last_counter_value = $2 WHERE card_id = $1` +
` AND last_counter_value < $2;`
res, err := db.Exec(sqlStatement, card_id, new_counter_value)
if err != nil {
return false, err
}
count, err := res.RowsAffected()
if err != nil {
return false, err
}
if count != 1 {
return false, nil
}
return true, nil
}
func db_insert_payment(card_id int, k1 string) error {
db, err := db_open()
if err != nil {
return err
}
defer db.Close()
// insert a new record into card_payments with card_id & k1 set
sqlStatement := `INSERT INTO card_payments` +
` (card_id, k1, paid_flag, lnurlw_request_time)` +
` VALUES ($1, $2, 'N', NOW());`
res, err := db.Exec(sqlStatement, card_id, k1)
if err != nil {
return err
}
count, err := res.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return errors.New("not one card_payments record inserted")
}
return nil
}
func db_get_payment_k1(k1 string) (*payment, error) {
p := payment{}
db, err := db_open()
if err != nil {
return &p, err
}
defer db.Close()
sqlStatement := `SELECT card_payment_id, card_id, paid_flag` +
` FROM card_payments WHERE k1=$1;`
row := db.QueryRow(sqlStatement, k1)
err = row.Scan(
&p.card_payment_id,
&p.card_id,
&p.paid_flag)
if err != nil {
return &p, err
}
return &p, nil
}
func db_update_payment_invoice(card_payment_id int, ln_invoice string, amount_msats int64) error {
db, err := db_open()
if err != nil {
return err
}
defer db.Close()
sqlStatement := `UPDATE card_payments SET ln_invoice = $2, amount_msats = $3 WHERE card_payment_id = $1;`
res, err := db.Exec(sqlStatement, card_payment_id, ln_invoice, amount_msats)
if err != nil {
return err
}
count, err := res.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return errors.New("not one card_payment record updated")
}
return nil
}
func db_update_payment_paid(card_payment_id int) error {
db, err := db_open()
if err != nil {
return err
}
defer db.Close()
sqlStatement := `UPDATE card_payments SET paid_flag = 'Y', payment_time = NOW() WHERE card_payment_id = $1;`
res, err := db.Exec(sqlStatement, card_payment_id)
if err != nil {
return err
}
count, err := res.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return errors.New("not one card_payment record updated")
}
return nil
}
func db_update_payment_status(card_payment_id int, payment_status string, failure_reason string) error {
db, err := db_open()
if err != nil {
return err
}
defer db.Close()
sqlStatement := `UPDATE card_payments SET payment_status = $2, failure_reason = $3, ` +
`payment_status_time = NOW() WHERE card_payment_id = $1;`
res, err := db.Exec(sqlStatement, card_payment_id, payment_status, failure_reason)
if err != nil {
return err
}
count, err := res.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return errors.New("not one card_payment record updated")
}
return nil
}
func db_get_card_totals(card_id int) (int, error) {
db, err := db_open()
if err != nil {
return 0, err
}
defer db.Close()
day_total_msats := 0
sqlStatement := `SELECT COALESCE(SUM(amount_msats),0) FROM card_payments ` +
`WHERE card_id=$1 AND paid_flag='Y' ` +
`AND payment_time > NOW() - INTERVAL '1 DAY';`
row := db.QueryRow(sqlStatement, card_id)
err = row.Scan(&day_total_msats)
if err != nil {
return 0, err
}
day_total_sats := day_total_msats / 1000
return day_total_sats, nil
}

1072
db/db.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,48 @@
version: '3'
services:
boltcard:
build:
context: ./
dockerfile: Dockerfile
container_name: boltcard_main
depends_on:
- db
restart: unless-stopped
environment:
- LOG_LEVEL=DEBUG
- DB_HOST=db
- DB_USER=cardapp
- DB_PASSWORD=${DB_PASSWORD}
- DB_PORT=5432
- DB_NAME=card_db
expose:
- "9000"
ports:
- "8080:9000"
volumes:
- ${PWD}/tls.cert:/boltcard/tls.cert
- ${PWD}/admin.macaroon:/boltcard/admin.macaroon
networks:
- boltnet
db:
image: postgres
container_name: boltcard_db
restart: unless-stopped
environment:
- POSTGRES_USER=cardapp
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=card_db
- PGDATA=/var/lib/postgresql/data/pgdata
volumes:
- db-data:/var/lib/postgresql/data
- ./sql/select_db.sql:/docker-entrypoint-initdb.d/select_db.sql
- ./sql/create_db.sql:/docker-entrypoint-initdb.d/create_db.sql
- ./sql/settings.sql:/docker-entrypoint-initdb.d/settings.sql
expose:
- "5432"
networks:
- boltnet
networks:
boltnet:
volumes:
db-data:

62
docker-compose.yml Normal file
View file

@ -0,0 +1,62 @@
version: '3'
services:
boltcard:
build:
context: ./
dockerfile: Dockerfile
container_name: boltcard_main
depends_on:
- db
restart: unless-stopped
environment:
- LOG_LEVEL=DEBUG
- DB_HOST=db
- DB_USER=cardapp
- DB_PASSWORD=${DB_PASSWORD}
- DB_PORT=5432
- DB_NAME=card_db
expose:
- "9000"
volumes:
- ${PWD}/tls.cert:/boltcard/tls.cert
- ${PWD}/admin.macaroon:/boltcard/admin.macaroon
networks:
- boltnet
db:
image: postgres
container_name: boltcard_db
restart: unless-stopped
environment:
- POSTGRES_USER=cardapp
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=card_db
- PGDATA=/var/lib/postgresql/data/pgdata
volumes:
- db-data:/var/lib/postgresql/data
- ./sql/select_db.sql:/docker-entrypoint-initdb.d/select_db.sql
- ./sql/create_db.sql:/docker-entrypoint-initdb.d/create_db.sql
- ./sql/settings.sql:/docker-entrypoint-initdb.d/settings.sql
expose:
- "5432"
networks:
- boltnet
webserver:
image: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ${PWD}/Caddyfile_docker:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- boltnet
networks:
boltnet:
volumes:
db-data:
caddy_data:
external: true
caddy_config:

35
docker_init.sh Executable file
View file

@ -0,0 +1,35 @@
#!/bin/bash
echo Enter the domain name excluding the protocol
read domainname
echo Enter your LND node gRPC domain
read lnd_host
echo LND node gRPC port
read lnd_port
sed -i "1s/.*/https:\/\/$domainname/" Caddyfile_docker
sed -i "s/[(]'HOST_DOMAIN'[^)]*[)]/(\'HOST_DOMAIN\', \'$domainname\')/" sql/settings.sql
echo writing the domain name to $domainname ...
PASSWORD=$(date +%s|sha256sum|base64|head -c 32)
if [[ ! -e .env ]]; then
cp .env.example .env
fi
sed -i "s/^DB_PASSWORD=.*$/DB_PASSWORD=$PASSWORD/g" .env
decrypt_key=$(hexdump -vn16 -e'4/4 "%08x" 1 "\n"' /dev/random)
echo $decrypt_key
sed -i "s/[(]'LOG_LEVEL'[^)]*[)]/(\'LOG_LEVEL\', \'DEBUG\')/" sql/settings.sql
sed -i "s/[(]'AES_DECRYPT_KEY'[^)]*[)]/(\'AES_DECRYPT_KEY\', \'$decrypt_key\')/" sql/settings.sql
sed -i "s/[(]'MIN_WITHDRAW_SATS'[^)]*[)]/(\'MIN_WITHDRAW_SATS\', \'1\')/" sql/settings.sql
sed -i "s/[(]'MAX_WITHDRAW_SATS'[^)]*[)]/(\'MAX_WITHDRAW_SATS\', \'1000000\')/" sql/settings.sql
sed -i "s/[(]'LN_HOST'[^)]*[)]/(\'LN_HOST\', \'$lnd_host\')/" sql/settings.sql
sed -i "s/[(]'LN_PORT'[^)]*[)]/(\'LN_PORT\', \'$lnd_port\')/" sql/settings.sql
sed -i "s/[(]'LN_TLS_FILE'[^)]*[)]/(\'LN_TLS_FILE\', \'\/boltcard\/tls.cert\')/" sql/settings.sql
sed -i "s/[(]'LN_MACAROON_FILE'[^)]*[)]/(\'LN_MACAROON_FILE\', \'\/boltcard\/admin.macaroon\')/" sql/settings.sql
sed -i "s/[(]'FEE_LIMIT_SAT'[^)]*[)]/(\'FEE_LIMIT_SAT\', \'10\')/" sql/settings.sql
sed -i "s/[(]'FEE_LIMIT_PERCENT'[^)]*[)]/(\'FEE_LIMIT_PERCENT\', \'0.5\')/" 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_EMAIL'[^)]*[)]/(\'FUNCTION_EMAIL\', \'DISABLE\')/" sql/settings.sql
sed -i "s/[(]'LN_INVOICE_EXPIRY_SEC'[^)]*[)]/(\'LN_INVOICE_EXPIRY_SEC\', \'3600\')/" sql/settings.sql

BIN
docs/AN12196.pdf Normal file

Binary file not shown.

View file

@ -2,7 +2,7 @@
## Introduction ## Introduction
Here we describe how to create your own bolt cards with the Bolt Card Android app and the Bolt Card service. Here we describe how to create your own bolt cards with the Bolt Card service and the Bolt Card Android app.
## Resources ## Resources
@ -16,46 +16,50 @@ Here we describe how to create your own bolt cards with the Bolt Card Android ap
### Install the app ### Install the app
- install the app from source or apk - install the app from
- source
### Write the URI template to the card - apk
on the app - Google Play Store [Boltcard NFC Card Creator](https://play.google.com/store/apps/details?id=com.lightningnfcapp)
- select `Write NFC`
- enter your domain and path in the text entry box given
```
card.yourdomain.com/ln
```
- bring the card to the device for programming the URI template
- select `Read NFC`
- check that the URI looks correct
```
lnurlw://card.yourdomain.com/ln?c=...&p=...
```
- note the UID value
### Write the key values to the card ### Write the key values to the card
on the bolt card server on the bolt card server
- ensure the environment variables for the database connection are set up (see `boltcard.service`) - ensure the environment variables for the database connection are set up (see `boltcard.service`)
- enter the `createboltcard` directory this can be achieved by writing these lines to the end of the `~/.bashrc` file
- `$ go build` ```
- `./createboltcard` echo "writing database_login to env vars"
- this will give you a one use link in text and QR code form
export DB_HOST=localhost
export DB_PORT=5432
export DB_USER=cardapp
export DB_PASSWORD=database_password
export DB_NAME=card_db
echo "writing host_domain to env vars"
export HOST_DOMAIN=card.yourdomain.com
```
- use the internal API to create a card
- `$ curl 'localhost:9001/createboltcard?card_name=card_5&enable=true&tx_max=1000&day_max=10000&uid_privacy=true&allow_neg_bal=true'`
- this will give you a one-time link
on the app on the app
- select `Key Management` - click `scan QR code`
- click `scan QR code from console`
- scan the QR code - scan the QR code
- bring the card to the device for programming the keys - the app will prompt you to hold the card for programming
- the app will test the card and show you the results
### Update the card record on the server
on the bolt card server
- `$ psql card_db`
- `card_db=# select card_id, one_time_code from cards order by card_id desc limit 1;`
- check that this is the correct record (one_time_code matches from before)
- `card_db=# update cards set uid = 'UID value from before without the 0x prefix' where card_id=card_id from before;`
- `card_db=# update cards set enable_flag = 'Y' where card_id=card_id from before;`
### Make a payment ### Make a payment
- monitor the bolt card service logs - monitor the bolt card service logs
- `$ journalctl -u boltcard.service -f` - `$ journalctl -u boltcard.service -f`
- use a PoS setup to read the bolt card, e.g. [Breez wallet](https://breez.technology/) - use a PoS setup to read the bolt card, e.g. [Breez wallet](https://breez.technology/)
### Update the card settings
- use the internal API to update settings for a card
- `$ curl 'localhost:9001/updateboltcard?card_name=card_5&enable=true&tx_max=100&day_max=1000'`
### Wipe a card
- use the internal API to wipe a card
- `$ curl 'localhost:9001/wipeboltcard?card_name=card_5'`
- this will mark the card as wiped and return the keys for the app to wipe the card

View file

@ -46,8 +46,10 @@ 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 (38 in this case)
- click after `c=` and note the c_position (73 in this case) - click after `p=` and note the p_position (41 in this case)
![find the p_position](images/posn-p.webp)
- click after `c=` and note the c_position (76 in this case)
- select `Write To Tag` - select `Write To Tag`
![NDEF message written successfully](images/nfwc.webp) ![NDEF message written successfully](images/nfwc.webp)
@ -101,7 +103,7 @@ lnurlw://card.yourdomain.com/ln?p=00000000000000000000000000000000&c=00000000000
- set up the values in the order shown - set up the values in the order shown
![file and SDM options with field entry order](images/fs-add.webp) ![file and SDM options with field entry order](images/fs-add-2.webp)
- select `Change File Settings` - select `Change File Settings`

64
docs/CARD_PRIVACY.md Normal file
View file

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

113
docs/DEEPLINK.md Normal file
View file

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

216
docs/DETERMINISTIC.md Normal file
View file

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

61
docs/DOCKER_INSTALL.md Normal file
View file

@ -0,0 +1,61 @@
# Bolt card service installation using Docker
### install Docker engine and Docker compose
- [Docker engine download &
install](https://docs.docker.com/engine/install/)
### Set up the boltcard server
- Run `./docker_init.sh` to set up the initial data
- Put the `tls.cert` file and `admin.macaroon` files in the project root directory
### https setup
- set up the domain A record to point to the server
- set up the server hosting firewall to allow open access to https (port 443) only
### database setup
- copy the `.env.example` file to `.env` and change the database password
### service bring-up and running
```
$ sudo groupadd docker
$ sudo usermod -aG docker ${USER}
(log out & in again)
$ docker volume create caddy_data
// add -d option for detached mode
$ docker compose up
```
### run boltcard server with own reverse proxy
If you already have reverse proxy in your enviroment which controls/terminates TLS connections, Boltcard server wont be ready to use, because of existence of own reverse proxy (Caddy). Caddy wont be abble to obtain TLS certificate for your domain name. Run different docker-compose, that will start Boltcard server without Caddy and your reverse proxy will handle TLS.
```
// add -d option for detached mode
$ docker-compose up -f docker-compose-own-reverse-proxy.yml
```
### stop docker
```
$ docker compose down
```
To delete the database and reset the docker volume, run `docker compose down --volumes`
*NOTE: caddy_data volume won't be removed even if you run `docker compose down --volumes` because it's an external volume. **Make sure to wipe your programmed cards before wiping the database***
### check container logs
- [Docker Logs](https://docs.docker.com/engine/reference/commandline/logs/)
```
$ docker logs [OPTIONS] CONTAINER
```
Run `$ docker ps` to list containers and get container names/ids
#### running internal API commands
- `docker exec boltcard_main curl 'localhost:9001/createboltcard?card_name=card_5&enable=false&tx_max=1000&day_max=10000&uid_privacy=true&allow_neg_bal=true'`
- `docker exec boltcard_main curl 'localhost:9001/updateboltcard?card_name=card_5&enable=true&tx_max=100&day_max=1000'`
- `docker exec boltcard_main curl 'localhost:9001/wipeboltcard?card_name=card_5'`
- `docker exec boltcard_main curl 'localhost:9001/getboltcard?card_name=card_5'`

View file

@ -1,26 +1,36 @@
# FAQ # How do you bech32 encode a string on the card ?
> How do you bech32 encode a string on the card ?
The LNURLw that comes from the bolt card is not bech32 encoded. The LNURLw that comes from the bolt card is not bech32 encoded.
It uses [LUD-17](https://github.com/fiatjaf/lnurl-rfc/blob/luds/17.md). It uses [LUD-17](https://github.com/fiatjaf/lnurl-rfc/blob/luds/17.md).
> How do I generate a random key value ? # How do I generate a random key value ?
This will give you a new 128 bit random key as a 32 character hex value. This will give you a new 128 bit random key as a 32 character hex value.
`$ hexdump -vn16 -e'4/4 "%08x" 1 "\n"' /dev/random` `$ hexdump -vn16 -e'4/4 "%08x" 1 "\n"' /dev/random`
> Why do I get a payment failure with NO_ROUTE ? # Why do I get a payment failure with NO_ROUTE ?
This is due to your payment lightning node not finding a route to the merchant lightning node. This is due to your payment lightning node not finding a route to the merchant lightning node.
It may help to open well funded channels to other well connected nodes. It may help to open well funded channels to other well connected nodes.
It may also help to increase your maximum network fee in your service variables, **FEE_LIMIT_SAT** . It may also help to increase your maximum network fee in your service variables, **FEE_LIMIT_SAT** / **FEE_LIMIT_PERCENT** .
It can be useful to test paying invoices directly from your lightning node. It can be useful to test paying invoices directly from your lightning node.
> Why do my payments take so long ? # Why do my payments take so long ?
This is due to the time taken for your payment lightning node to find a route. This is due to the time taken for your payment lightning node to find a route.
It can be improved by opening channels using clearnet rather than on the tor network. It can be improved by opening channels using clearnet rather than on the tor network.
It may also help to improve your lightning node hardware or software setup. It may also help to improve your lightning node hardware or software setup.
It can be useful to test paying invoices directly from your lightning node. 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) ?
When tested with LND in Nov 2022, the paying (customer, bolt card) lightning node must be a separate instance to the invoicing (merchant, POS) lightning node.
# I get a 6982 error when trying to program a blank card
A 6982 error is is known to happen after trying to use a 'blank' card which has been wiped with the CoinCorner customer app (July 2023) and happens because the card settings have not been cleared down correctly. It can also happen where a card is removed partway through programming (which can take a few seconds) or where the mobile device does not complete programming due to being in a low battery situation.
The card settings can be fixed by using the 'Bolt Card NFC Card Creator' app. The card will then be blank and usable again.
- Reset Keys
- Enter all '0's in Key 0 until the field is full and copy to Keys 1-4
- Reset Card Now
- present the card

View file

@ -43,18 +43,17 @@ $ xxd -r -p SendPaymentV2.macaroon.hex SendPaymentV2.macaroon
``` ```
### setup the boltcard server ### setup the boltcard server
edit `boltcard.service` in the section named `boltcard service settings` edit `boltcard.service` to set up the database connection
edit `insert_settings.sql` to set up [bolt card system settings](SETTINGS.md)
edit `Caddyfile` to set the boltcard domain name edit `Caddyfile` to set the boltcard domain name
edit `add_card_data.sql` to set up the individual bolt card records
### database creation ### database creation
edit `create_db.sql` to set the cardapp password edit `create_db.sql` to set the cardapp password
`$ sudo -u postgres createuser -s ubuntu` `$ sudo -u postgres createuser -s ubuntu`
`$ ./s_create_db` `$ script/s_create_db`
### boltcard service install ### boltcard service install
`$ sudo cp boltcard.service /etc/systemd/system/boltcard.service` `$ script/s_build`
`$ ./s_build`
`$ sudo systemctl enable boltcard` `$ sudo systemctl enable boltcard`
`$ sudo systemctl status boltcard` `$ sudo systemctl status boltcard`
@ -80,11 +79,41 @@ this should respond with 'bad request' and show up in the service log
navigate to the service URL from a browser, for example `https://card.yourdomain.com/ln?2` navigate to the service URL from a browser, for example `https://card.yourdomain.com/ln?2`
this should respond with 'bad request' and show up in the service log this should respond with 'bad request' and show up in the service log
#### bolt card #### bolt card
`Environment="FUNCTION_LNURLW=ENABLE"` in `boltcard.service`
[create a bolt card](CARD_ANDROID.md) with the URI pointing to this server [create a bolt card](CARD_ANDROID.md) with the URI pointing to this server
use a PoS setup to read the bolt card, e.g. [Breez wallet](https://breez.technology/) use a PoS setup to read the bolt card, e.g. [Breez wallet](https://breez.technology/)
monitor the service log to ensure decryption, authentication, payment rules and lightning payment work as expected monitor the service log to ensure decryption, authentication, payment rules and lightning payment work as expected
#### lightning address (optional)
add lightning address support to receive funds to cards
create an updated macaroon with limited permissions to the lightning node
```
$ lncli \
--rpcserver=lightning-node.io:10009 \
--macaroonpath=admin.macaroon \
--tlscertpath="tls.cert" \
bakemacaroon uri:/routerrpc.Router/SendPaymentV2 uri:/lnrpc.Lightning/AddInvoice \
uri:/invoicesrpc.Invoices/SubscribeSingleInvoice > SendAddMonitor.macaroon.hex
$ xxd -r -p SendAddMonitor.macaroon.hex SendAddMonitor.macaroon
```
`LN_MACAROON_FILE=...` (settings table) - update to point to new SendAddMonitor.macaroon
`FUNCTION_LNURLP=ENABLE` (settings table)
`cards.lnurlp_enable='Y'` (card record)
the lightning address will be *{cards.card_name}@{HOST_DOMAIN}*
#### email notifications (optional)
add email notifications for payments and fund receipt
`AWS_SES_ID=..."` (settings table)
`AWS_SES_SECRET=..."` (settings table)
`AWS_SES_EMAIL_FROM=..."` (settings table)
`AWS_REGION=...` (settings table)
`FUNCTION_EMAIL=ENABLE"` (settings table)
`cards.email_address='card.notifications@yourdomain.com'`
`cards.email_enable='Y'`
the email address will be *{cards.email_address}@{HOST_DOMAIN}*
#### production use #### production use
ensure that LOG_LEVEL is set to PRODUCTION ensure that LOG_LEVEL is set to PRODUCTION (settings table)
ensure that all secrets are minimally available ensure that all secrets are minimally available
ensure that you have good operational security practices ensure that you have good operational security practices
monitor the system for unusual activity monitor the system for unusual activity

13
docs/LNDHUB.md Normal file
View file

@ -0,0 +1,13 @@
## lndhub
### in the `settings` table
- set 'LNDHUB_URL' to 'lndhub.yourdomain.com'
- set 'FUNCTION_LNDHUB' to 'ENABLE'
### create card
- set the card_name to the 'login:password' for the lndhub account
e.g. '11111111111111111111:22222222222222222222'
### limits
- there are currently no payment rules in this code, only the lndhub account limit
i.e. tx_limit_sats & day_limit_sats are not enforced

3
docs/NOTES.md Normal file
View file

@ -0,0 +1,3 @@
- `$ apidoc -i . -o apidoc` to generate API documentation

BIN
docs/NT4H2421Gx.pdf Normal file

Binary file not shown.

37
docs/SETTINGS.md Normal file
View file

@ -0,0 +1,37 @@
# Settings
The database connection settings are in the system environment variables.
Other settings are in the database in a `settings` table.
Here are the descriptions of values available to use in the `settings` table:
| Name | Value | Description |
| --- | --- | --- |
| LOG_LEVEL | DEBUG | system logs are verbose to enable easier debug |
| | PRODUCTION | system logs are minimal |
| AES_DECRYPT_KEY | | hex encoded 128 bit AES key - see [FAQ](FAQ.md#how-do-i-generate-a-random-key-value-)|
| HOST_DOMAIN | yourdomain.com | the domain for hosting lnurlw & lnurlp services |
| MIN_WITHDRAW_SATS | 1 | minimum satoshis for lnurlw response |
| MAX_WITHDRAW_SATS | 1000000 | maximum satoshis for lnurlw response |
| LN_HOST | your_lnd_node.io | LND node gRPC domain |
| LN_PORT | 9001 | LND node gRPC port |
| LN_TLS_FILE | /home/ubuntu/boltcard/tls.cert | absolute path to your LND TLC certificate |
| LN_MACAROON_FILE | /home/ubuntu/boltcard/boltcard.macaroon | absolute path to your LND macaroon |
| FEE_LIMIT_SAT | 10 | the base fee limit amount for every invoice payment |
| FEE_LIMIT_PERCENT | 0.5 | the percentage fee limit amount added to the base fee limit amount |
| LN_TESTNODE | | lightning node pubkey for allowing only the defined test node |
| FUNCTION_LNURLW | ENABLE | system level switch for LNURLw (bolt card) services |
| FUNCTION_LNURLP | DISABLE | system level switch for LNURLp (lightning address) services |
| 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_SECRET | | Amazon Web Services - Simple Email Service - access secret |
| 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 |
| FUNCTION_LNDHUB | DISABLE | system level switch for using LNDHUB in place of LND |
| LNDHUB_URL | | URL for the LNDHUB service |
| FUNCTION_INTERNAL_API | DISABLE | system level switch for activating the internal API |
| SENDGRID_API_KEY | | User API Key from SendGrid.com |
| SENDGRID_EMAIL_SENDER | | Single Sender email address verified by SendGrid |
| LN_INVOICE_EXPIRY_SEC | 3600 | LN invoice's expiry time in seconds |

View file

@ -1,13 +1,26 @@
# Bolt card specification # Bolt card specification
The bolt card system is built on the open standards 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)
- Replay protection
- 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 an NDEF message from the card, for example the point-of-sale (POS) will read a NDEF message from the card, which changes with each use, for example
``` ```
lnurlw://card.yourdomain.com?p=A2EF40F6D46F1BB36E6EBF0114D4A464&c=F509EEA788E37E32 lnurlw://card.yourdomain.com?p=A2EF40F6D46F1BB36E6EBF0114D4A464&c=F509EEA788E37E32
``` ```
@ -15,9 +28,9 @@ the POS will then call your bolt card service here
``` ```
https://card.yourdomain.com?p=A2EF40F6D46F1BB36E6EBF0114D4A464&c=F509EEA788E37E32 https://card.yourdomain.com?p=A2EF40F6D46F1BB36E6EBF0114D4A464&c=F509EEA788E37E32
``` ```
your bolt card service should verify the payment request as below and continue the LNURLw protocol your bolt card service should verify the payment request as below and continue the standard LNURLw protocol as defined in LUD-03
## Server side verification ## Server side verification of the payment request
- for the `p` value and the `SDM Meta Read Access Key` value, decrypt the UID and counter with AES - for the `p` value and the `SDM Meta Read Access Key` value, decrypt the UID and counter with AES
- for the `c` value and the `SDM File Read Access Key` value, check with AES-CMAC - for the `c` value and the `SDM File Read Access Key` value, check with AES-CMAC

12
docs/TECHNOLOGY.md Normal file
View file

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

54
docs/TEST_VECTORS.md Normal file
View file

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Before After
Before After

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
docs/images/posn.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

179
email/email.go Normal file
View file

@ -0,0 +1,179 @@
package email
import (
"strconv"
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ses"
"github.com/boltcard/boltcard/db"
"github.com/sendgrid/sendgrid-go"
"github.com/sendgrid/sendgrid-go/helpers/mail"
log "github.com/sirupsen/logrus"
)
func Send_balance_email(recipient_email string, card_id int) {
c, err := db.Get_card_from_card_id(card_id)
if err != nil {
log.Warn(err.Error())
return
}
card_total_sats, err := db.Get_card_total_sats(card_id)
if err != nil {
log.Warn(err.Error())
return
}
email_max_txs, err := strconv.Atoi(db.Get_setting("EMAIL_MAX_TXS"))
if err != nil {
log.Warn(err.Error())
return
}
txs, err := db.Get_card_txs(card_id, email_max_txs+1)
if err != nil {
log.Warn(err.Error())
return
}
subject := c.Card_name + " balance = " + strconv.Itoa(card_total_sats) + " sats"
// add transactions to the email body
var html_body_sb strings.Builder
var text_body_sb strings.Builder
html_body_sb.WriteString("<!DOCTYPE html><html><head><style> table, " +
"th, td { border: 1px solid black; border-collapse: collapse; } " +
"</style></head><body>")
html_body_sb.WriteString("<h3>transactions</h3><table><tr><th>date</th><th>action</th><th>amount</th>")
text_body_sb.WriteString("transactions\n\n")
for i, tx := range txs {
if i < email_max_txs {
html_body_sb.WriteString(
"<tr>" +
"<td>" + tx.Tx_time + "</td>" +
"<td>" + tx.Tx_type + "</td>" +
"<td style='text-align:right'>" + strconv.Itoa(tx.Tx_amount_msats/1000) + "</td>" +
"</tr>")
} else {
html_body_sb.WriteString(
"<tr>" +
"<td style='text-align:center'> ... </td>" +
"<td style='text-align:center'> ... </td>" +
"<td style='text-align:center'> ... </td>" +
"</tr>")
}
text_body_sb.WriteString(tx.Tx_type +
" " + strconv.Itoa(tx.Tx_amount_msats/1000))
}
html_body_sb.WriteString("</table></body></html>")
html_body := html_body_sb.String()
text_body := text_body_sb.String()
Send_email(recipient_email,
subject,
html_body,
text_body)
}
// 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) {
send_grid_api_key := db.Get_setting("SENDGRID_API_KEY")
send_grid_email_sender := db.Get_setting("SENDGRID_EMAIL_SENDER")
if send_grid_api_key != "" && send_grid_email_sender != "" {
from := mail.NewEmail("", send_grid_email_sender)
subject := subject
to := mail.NewEmail("", recipient)
plainTextContent := textBody
htmlContent := htmlBody
message := mail.NewSingleEmail(from, subject, to, plainTextContent, htmlContent)
client := sendgrid.NewSendClient(send_grid_api_key)
response, err := client.Send(message)
if err != nil {
log.Warn(err.Error())
} else {
log.WithFields(log.Fields{"result": response}).Info("email sent")
}
} else {
aws_ses_id := db.Get_setting("AWS_SES_ID")
aws_ses_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")
}
}

162
go.mod
View file

@ -3,43 +3,143 @@ module github.com/boltcard/boltcard
go 1.18 go 1.18
require ( require (
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 // indirect github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1
github.com/aws/aws-sdk-go v1.44.142
github.com/fiatjaf/ln-decodepay v1.5.0
github.com/gorilla/mux v1.8.0
github.com/lib/pq v1.10.7
github.com/lightningnetwork/lnd v0.15.5-beta.rc1
github.com/sirupsen/logrus v1.9.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
google.golang.org/grpc v1.51.0
gopkg.in/macaroon.v2 v2.1.0
)
require (
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/aead/siphash v1.0.1 // indirect github.com/aead/siphash v1.0.1 // indirect
github.com/btcsuite/btcd v0.20.1-beta.0.20200515232429-9f0179fd2c46 // indirect github.com/benbjohnson/clock v1.3.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/btcsuite/btcd v0.23.4 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
github.com/btcsuite/btcd/btcutil/psbt v1.1.6 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
github.com/btcsuite/btcutil v1.0.2 // indirect github.com/btcsuite/btcwallet v0.16.4 // indirect
github.com/btcsuite/btcwallet v0.11.1-0.20200515224913-e0e62245ecbe // indirect github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2 // indirect
github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0 // indirect github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 // indirect
github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 // indirect github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 // indirect
github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0 // indirect github.com/btcsuite/btcwallet/walletdb v1.4.0 // indirect
github.com/btcsuite/btcwallet/walletdb v1.3.1 // indirect github.com/btcsuite/btcwallet/wtxmgr v1.5.0 // indirect
github.com/btcsuite/btcwallet/wtxmgr v1.1.1-0.20200515224913-e0e62245ecbe // indirect
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
github.com/btcsuite/winsvc v1.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fiatjaf/ln-decodepay v1.4.0 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/golang/protobuf v1.3.3 // indirect github.com/decred/dcrd/lru v1.1.1 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.8.6 // indirect github.com/dustin/go-humanize v1.0.0 // indirect
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 // indirect github.com/dvyukov/go-fuzz v0.0.0-20220726122315-1d375ef9f9f6 // indirect
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec // indirect github.com/fergusstrange/embedded-postgres v1.19.0 // indirect
github.com/lib/pq v1.10.6 // indirect github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.14.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.13.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.12.0 // indirect
github.com/jackc/pgx/v4 v4.17.2 // indirect
github.com/jessevdk/go-flags v1.5.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jonboulle/clockwork v0.3.0 // indirect
github.com/jrick/logrotate v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kkdai/bstream v1.0.0 // indirect
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
github.com/lightninglabs/neutrino v0.11.1-0.20200316235139-bffc52e8f200 // indirect github.com/lightninglabs/neutrino v0.14.2 // indirect
github.com/lightningnetwork/lnd v0.10.1-beta // indirect github.com/lightningnetwork/lightning-onion v1.2.0 // indirect
github.com/lightningnetwork/lnd/queue v1.0.3 // indirect github.com/lightningnetwork/lnd/clock v1.1.0 // indirect
github.com/lightningnetwork/lnd/ticker v1.0.0 // indirect github.com/lightningnetwork/lnd/healthcheck v1.2.2 // indirect
github.com/lncm/lnd-rpc v1.0.2 // indirect github.com/lightningnetwork/lnd/kvdb v1.3.1 // indirect
github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 // indirect github.com/lightningnetwork/lnd/queue v1.1.0 // indirect
github.com/lightningnetwork/lnd/ticker v1.1.0 // indirect
github.com/lightningnetwork/lnd/tlv v1.0.3 // indirect
github.com/lightningnetwork/lnd/tor v1.1.0 // indirect
github.com/ltcsuite/ltcd v0.22.1-beta // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/miekg/dns v1.1.50 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nbd-wtf/ln-decodepay v1.11.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.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/sirupsen/logrus v1.8.1 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 // indirect github.com/sendgrid/sendgrid-go v3.12.0+incompatible // indirect
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect github.com/soheilhy/cmux v0.1.5 // indirect
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 // indirect github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/text v0.3.2 // indirect github.com/stretchr/objx v0.5.0 // indirect
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce // indirect github.com/stretchr/testify v1.8.1 // indirect
google.golang.org/grpc v1.27.1 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.etcd.io/etcd/api/v3 v3.5.5 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.5 // indirect
go.etcd.io/etcd/client/v2 v2.305.5 // indirect
go.etcd.io/etcd/client/v3 v3.5.5 // indirect
go.etcd.io/etcd/pkg/v3 v3.5.5 // indirect
go.etcd.io/etcd/raft/v3 v3.5.5 // indirect
go.etcd.io/etcd/server/v3 v3.5.5 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.36.4 // indirect
go.opentelemetry.io/otel v1.11.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.11.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.11.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.11.1 // indirect
go.opentelemetry.io/otel/sdk v1.11.1 // indirect
go.opentelemetry.io/otel/trace v1.11.1 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/crypto v0.3.0 // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/term v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/time v0.2.0 // indirect
golang.org/x/tools v0.3.0 // indirect
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/errgo.v1 v1.0.1 // indirect gopkg.in/errgo.v1 v1.0.1 // indirect
gopkg.in/macaroon-bakery.v2 v2.0.1 // indirect gopkg.in/macaroon-bakery.v2 v2.3.0 // indirect
gopkg.in/macaroon.v2 v2.1.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
) )

1128
go.sum

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,127 @@
package internalapi
import (
"net/http"
"strconv"
"strings"
"github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/resp_err"
log "github.com/sirupsen/logrus"
)
// random_hex() from Createboltcardwithpin used here
func Createboltcard(w http.ResponseWriter, r *http.Request) {
if db.Get_setting("FUNCTION_INTERNAL_API") != "ENABLE" {
msg := "createboltcard: 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 := "createboltcard: 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 := "createboltcard: 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 := "createboltcard: 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 := "createboltcard: 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 := "createboltcard: 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 := "createboltcard: allow_neg_bal is not a valid boolean"
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}).Info("createboltcard 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(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)
if err != nil {
log.Warn(err.Error())
return
}
// return the URI + one_time_code
hostdomain := db.Get_setting("HOST_DOMAIN")
hostdomainPort := db.Get_setting("HOST_DOMAIN_PORT")
hostdomainsuffix := ""
if hostdomainPort != "" {
hostdomainsuffix = ":" + hostdomainPort
}
url := ""
if strings.HasSuffix(hostdomain, ".onion") {
url = "http://" + hostdomain + hostdomainsuffix + "/new?a=" + one_time_code
} else {
url = "https://" + hostdomain + hostdomainsuffix + "/new?a=" + one_time_code
}
// log the response
log.WithFields(log.Fields{
"card_name": card_name, "url": url}).Info("createboltcard API response")
jsonData := []byte(`{"status":"OK",` +
`"url":"` + url + `"}`)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonData)
}

View file

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

View file

@ -0,0 +1,47 @@
package internalapi
import (
"net/http"
"strconv"
"github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/resp_err"
log "github.com/sirupsen/logrus"
)
func Getboltcard(w http.ResponseWriter, r *http.Request) {
if db.Get_setting("FUNCTION_INTERNAL_API") != "ENABLE" {
msg := "getboltcard: internal API function is not enabled"
log.Debug(msg)
resp_err.Write_message(w, msg)
return
}
card_name := r.URL.Query().Get("card_name")
// log the request
log.WithFields(log.Fields{
"card_name": card_name}).Info("getboltcard API request")
// get the card record
c, err := db.Get_card_from_card_name(card_name)
if err != nil {
msg := "getboltcard: a non-wiped card with the card_name does not exist in the database"
log.Warn(msg)
resp_err.Write_message(w, msg)
return
}
jsonData := []byte(`{"status":"OK",` +
`"uid": "` + c.Db_uid + `",` +
`"lnurlw_enable": "` + c.Lnurlw_enable + `",` +
`"tx_limit_sats": "` + strconv.Itoa(c.Tx_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.WriteHeader(http.StatusOK)
w.Write(jsonData)
}

12
internalapi/ping.go Normal file
View file

@ -0,0 +1,12 @@
package internalapi
import (
"net/http"
)
func Internal_ping(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
jsonData := []byte(`{"status":"OK","pong":"internal API"}`)
w.Write(jsonData)
}

View file

@ -0,0 +1,83 @@
package internalapi
import (
"github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/resp_err"
log "github.com/sirupsen/logrus"
"net/http"
"strconv"
)
func Updateboltcard(w http.ResponseWriter, r *http.Request) {
if db.Get_setting("FUNCTION_INTERNAL_API") != "ENABLE" {
msg := "updateboltcard: 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 := "updateboltcard: 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 := "updateboltcard: 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 := "updateboltcard: day_max 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 := "updateboltcard: 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}).Info("updateboltcard API request")
// update the card record
err = db.Update_card(card_name, enable_flag, tx_max, day_max)
if err != nil {
log.Warn(err.Error())
return
}
// send a response
jsonData := []byte(`{"status":"OK"}`)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonData)
}

View file

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

View file

@ -0,0 +1,64 @@
package internalapi
import (
"github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/resp_err"
log "github.com/sirupsen/logrus"
"net/http"
"strconv"
)
func Wipeboltcard(w http.ResponseWriter, r *http.Request) {
if db.Get_setting("FUNCTION_INTERNAL_API") != "ENABLE" {
msg := "wipeboltcard: internal API function is not enabled"
log.Debug(msg)
resp_err.Write_message(w, msg)
return
}
card_name := r.URL.Query().Get("card_name")
// check if card_name has been given
if card_name == "" {
msg := "wipeboltcard: the card name must be set"
log.Warn(msg)
resp_err.Write_message(w, msg)
return
}
// set the card as wiped and disabled, get the keys
card_wipe_info_values, err := db.Wipe_card(card_name)
if err != nil {
log.Warn(err.Error())
return
}
// log the request
log.WithFields(log.Fields{
"card_name": card_name}).Info("wipeboltcard API request")
// generate a response
jsonData := `{"status":"OK",` +
`"action": "wipe",` +
`"id": ` + strconv.Itoa(card_wipe_info_values.Id) + `,` +
`"k0": "` + card_wipe_info_values.K0 + `",` +
`"k1": "` + card_wipe_info_values.K1 + `",` +
`"k2": "` + card_wipe_info_values.K2 + `",` +
`"k3": "` + card_wipe_info_values.K3 + `",` +
`"k4": "` + card_wipe_info_values.K4 + `",` +
`"uid": "` + card_wipe_info_values.Uid + `",` +
`"version": 1}`
// log the response
log.WithFields(log.Fields{
"card_name": card_name, "response": jsonData}).Info("wipeboltcard API response")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(jsonData))
}

View file

@ -1,131 +0,0 @@
package main
import (
"context"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strconv"
"time"
lnrpc "github.com/lncm/lnd-rpc/v0.10.0/lnrpc"
routerrpc "github.com/lncm/lnd-rpc/v0.10.0/routerrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"gopkg.in/macaroon.v2"
)
type rpcCreds map[string]string
func (m rpcCreds) RequireTransportSecurity() bool { return true }
func (m rpcCreds) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) {
return m, nil
}
func newCreds(bytes []byte) rpcCreds {
creds := make(map[string]string)
creds["macaroon"] = hex.EncodeToString(bytes)
return creds
}
func getRouterClient(hostname string, port int, tlsFile, macaroonFile string) routerrpc.RouterClient {
macaroonBytes, err := ioutil.ReadFile(macaroonFile)
if err != nil {
log.Println("Cannot read macaroon file .. ", err)
panic(err)
}
mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macaroonBytes); err != nil {
log.Println("Cannot unmarshal macaroon .. ", err)
panic(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
transportCredentials, err := credentials.NewClientTLSFromFile(tlsFile, hostname)
if err != nil {
panic(err)
}
fullHostname := fmt.Sprintf("%s:%d", hostname, port)
connection, err := grpc.DialContext(ctx, fullHostname, []grpc.DialOption{
grpc.WithBlock(),
grpc.WithTransportCredentials(transportCredentials),
grpc.WithPerRPCCredentials(newCreds(macaroonBytes)),
}...)
if err != nil {
log.Printf("unable to connect to %s: %w", fullHostname, err)
panic(err)
}
return routerrpc.NewRouterClient(connection)
}
func pay_invoice(invoice string) (payment_status string, failure_reason string, return_err error) {
payment_status = ""
failure_reason = ""
return_err = nil
// SendPaymentV2
ctx2, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// get node parameters from environment variables
ln_port, err := strconv.Atoi(os.Getenv("LN_PORT"))
if err != nil {
return_err = err
return
}
r_client := getRouterClient(
os.Getenv("LN_HOST"),
ln_port,
os.Getenv("LN_TLS_FILE"),
os.Getenv("LN_MACAROON_FILE"))
fee_limit_sat_str := os.Getenv("FEE_LIMIT_SAT")
fee_limit_sat, err := strconv.ParseInt(fee_limit_sat_str, 10, 64)
if err != nil {
return_err = err
return
}
stream, err := r_client.SendPaymentV2(ctx2, &routerrpc.SendPaymentRequest{
PaymentRequest: invoice,
NoInflightUpdates: true,
TimeoutSeconds: 30,
FeeLimitSat: fee_limit_sat})
if err != nil {
return_err = err
return
}
for {
update, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return_err = err
return
}
payment_status = lnrpc.Payment_PaymentStatus_name[int32(update.Status)]
failure_reason = lnrpc.PaymentFailureReason_name[int32(update.FailureReason)]
}
return
}

310
lnd/lnd.go Normal file
View file

@ -0,0 +1,310 @@
package lnd
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
log "github.com/sirupsen/logrus"
"io"
"io/ioutil"
"strconv"
"time"
decodepay "github.com/fiatjaf/ln-decodepay"
lnrpc "github.com/lightningnetwork/lnd/lnrpc"
invoicesrpc "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
routerrpc "github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"gopkg.in/macaroon.v2"
"github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/email"
)
type rpcCreds map[string]string
func (m rpcCreds) RequireTransportSecurity() bool { return true }
func (m rpcCreds) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) {
return m, nil
}
func newCreds(bytes []byte) rpcCreds {
creds := make(map[string]string)
creds["macaroon"] = hex.EncodeToString(bytes)
return creds
}
func getGrpcConn(hostname string, port int, tlsFile, macaroonFile string) *grpc.ClientConn {
macaroonBytes, err := ioutil.ReadFile(macaroonFile)
if err != nil {
log.Println("Cannot read macaroon file .. ", err)
panic(err)
}
mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macaroonBytes); err != nil {
log.Println("Cannot unmarshal macaroon .. ", err)
panic(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
transportCredentials, err := credentials.NewClientTLSFromFile(tlsFile, hostname)
if err != nil {
panic(err)
}
fullHostname := fmt.Sprintf("%s:%d", hostname, port)
connection, err := grpc.DialContext(ctx, fullHostname, []grpc.DialOption{
grpc.WithBlock(),
grpc.WithTransportCredentials(transportCredentials),
grpc.WithPerRPCCredentials(newCreds(macaroonBytes)),
}...)
if err != nil {
log.Printf("unable to connect to %s", fullHostname)
panic(err)
}
return connection
}
// https://api.lightning.community/?shell#addinvoice
func Add_invoice(amount_sat int64, metadata string) (payment_request string, r_hash []byte, return_err error) {
ln_port, err := strconv.Atoi(db.Get_setting("LN_PORT"))
if err != nil {
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))
connection := getGrpcConn(
db.Get_setting("LN_HOST"),
ln_port,
db.Get_setting("LN_TLS_FILE"),
db.Get_setting("LN_MACAROON_FILE"))
l_client := lnrpc.NewLightningClient(connection)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
result, err := l_client.AddInvoice(ctx, &lnrpc.Invoice{
Value: amount_sat,
DescriptionHash: dh[:],
Expiry: ln_invoice_expiry,
})
if err != nil {
return "", nil, err
}
return result.PaymentRequest, result.RHash, nil
}
// https://api.lightning.community/?shell#subscribesingleinvoice
func Monitor_invoice_state(r_hash []byte) {
// SubscribeSingleInvoice
// get node parameters from environment variables
ln_port, err := strconv.Atoi(db.Get_setting("LN_PORT"))
if err != nil {
log.Warn(err)
return
}
ln_invoice_expiry, err := strconv.Atoi(db.Get_setting("LN_INVOICE_EXPIRY_SEC"))
if err != nil {
log.Warn(err)
return
}
connection := getGrpcConn(
db.Get_setting("LN_HOST"),
ln_port,
db.Get_setting("LN_TLS_FILE"),
db.Get_setting("LN_MACAROON_FILE"))
i_client := invoicesrpc.NewInvoicesClient(connection)
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(ln_invoice_expiry)*time.Second)
defer cancel()
stream, err := i_client.SubscribeSingleInvoice(ctx, &invoicesrpc.SubscribeSingleInvoiceRequest{
RHash: r_hash})
if err != nil {
log.WithFields(log.Fields{"r_hash": hex.EncodeToString(r_hash)}).Warn(err)
return
}
for {
update, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.WithFields(log.Fields{"r_hash": hex.EncodeToString(r_hash)}).Warn(err)
return
}
invoice_state := lnrpc.Invoice_InvoiceState_name[int32(update.State)]
log.WithFields(
log.Fields{
"r_hash": hex.EncodeToString(r_hash),
"invoice_state": invoice_state,
}).Info("invoice state updated")
db.Update_receipt_state(hex.EncodeToString(r_hash), invoice_state)
}
connection.Close()
// send email
card_id, err := db.Get_card_id_for_r_hash(hex.EncodeToString(r_hash))
if err != nil {
log.WithFields(log.Fields{"r_hash": hex.EncodeToString(r_hash)}).Warn(err)
return
}
log.WithFields(log.Fields{"r_hash": hex.EncodeToString(r_hash), "card_id": card_id}).Debug("card found")
c, err := db.Get_card_from_card_id(card_id)
if err != nil {
log.WithFields(log.Fields{"r_hash": hex.EncodeToString(r_hash)}).Warn(err)
return
}
if c.Email_enable != "Y" {
log.Debug("email is not enabled for the card")
return
}
go email.Send_balance_email(c.Email_address, card_id)
return
}
// https://api.lightning.community/?shell#sendpaymentv2
func PayInvoice(card_payment_id int, invoice string) {
// SendPaymentV2
// get node parameters from environment variables
ln_port, err := strconv.Atoi(db.Get_setting("LN_PORT"))
if err != nil {
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Warn(err)
return
}
connection := getGrpcConn(
db.Get_setting("LN_HOST"),
ln_port,
db.Get_setting("LN_TLS_FILE"),
db.Get_setting("LN_MACAROON_FILE"))
r_client := routerrpc.NewRouterClient(connection)
fee_limit_sat_str := db.Get_setting("FEE_LIMIT_SAT")
fee_limit_sat, err := strconv.ParseInt(fee_limit_sat_str, 10, 64)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Warn(err)
return
}
fee_limit_percent_str := db.Get_setting("FEE_LIMIT_PERCENT")
fee_limit_percent, err := strconv.ParseFloat(fee_limit_percent_str, 64)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Warn(err)
return
}
bolt11, _ := decodepay.Decodepay(invoice)
invoice_msats := bolt11.MSatoshi
invoice_expiry := bolt11.Expiry
fee_limit_product := int64((fee_limit_percent / 100) * (float64(invoice_msats) / 1000))
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(invoice_expiry)*time.Second)
defer cancel()
stream, err := r_client.SendPaymentV2(ctx, &routerrpc.SendPaymentRequest{
PaymentRequest: invoice,
NoInflightUpdates: true,
TimeoutSeconds: 30,
FeeLimitSat: fee_limit_sat + fee_limit_product})
if err != nil {
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Warn(err)
return
}
for {
update, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Warn(err)
return
}
payment_status := lnrpc.Payment_PaymentStatus_name[int32(update.Status)]
failure_reason := lnrpc.PaymentFailureReason_name[int32(update.FailureReason)]
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Info("payment failure reason : ", failure_reason)
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Info("payment status : ", payment_status)
err = db.Update_payment_status(card_payment_id, payment_status, failure_reason)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Warn(err)
return
}
}
connection.Close()
// send email
card_id, err := db.Get_card_id_for_card_payment_id(card_payment_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Warn(err)
return
}
log.WithFields(log.Fields{"card_payment_id": card_payment_id, "card_id": card_id}).Debug("card found")
c, err := db.Get_card_from_card_id(card_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Warn(err)
return
}
if c.Email_enable != "Y" {
log.Debug("email is not enabled for the card")
return
}
go email.Send_balance_email(c.Email_address, card_id)
return
}

64
lndhub/lndhub.go Normal file
View file

@ -0,0 +1,64 @@
package lndhub
import (
"bytes"
"encoding/json"
"io"
"net/http"
"strconv"
"github.com/boltcard/boltcard/db"
log "github.com/sirupsen/logrus"
)
type LndhubPayInvoiceRequest struct {
Invoice string `json:"invoice"`
FreeAmount string `json:"freeamount"`
LoginId string `json:"loginid"`
}
func PayInvoice(cardPaymentId int, invoice string, amountSats int, loginId string, accessToken string) {
lndhub_url := db.Get_setting("LNDHUB_URL")
client := &http.Client{}
//lndhub.payinvoice API call
var payInvoiceRequest LndhubPayInvoiceRequest
payInvoiceRequest.Invoice = invoice
payInvoiceRequest.FreeAmount = strconv.Itoa(amountSats)
payInvoiceRequest.LoginId = loginId
req_payinvoice, err := json.Marshal(payInvoiceRequest)
log.Info(string(req_payinvoice))
if err != nil {
log.WithFields(log.Fields{"card_payment_id": cardPaymentId}).Warn(err)
return
}
req, err := http.NewRequest("POST", lndhub_url+"/payinvoice", bytes.NewBuffer(req_payinvoice))
if err != nil {
log.WithFields(log.Fields{"card_payment_id": cardPaymentId}).Warn(err)
return
}
req.Header.Add("Access-Control-Allow-Origin", "*")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+accessToken)
res2, err := client.Do(req)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": cardPaymentId}).Warn(err)
return
}
defer res2.Body.Close()
b2, err := io.ReadAll(res2.Body)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": cardPaymentId}).Warn(err)
return
}
log.Info(string(b2))
}

89
lnurlp/lnurlp_callback.go Normal file
View file

@ -0,0 +1,89 @@
package lnurlp
import (
"encoding/hex"
"net/http"
"strconv"
"github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/lnd"
"github.com/boltcard/boltcard/resp_err"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
)
func Callback(w http.ResponseWriter, r *http.Request) {
if db.Get_setting("FUNCTION_LNURLP") != "ENABLE" {
log.Debug("LNURLp function is not enabled")
return
}
name := mux.Vars(r)["name"]
amount := r.URL.Query().Get("amount")
card_id, err := db.Get_card_id_for_name(name)
if err != nil {
log.Info("card name not found")
resp_err.Write(w)
return
}
log.WithFields(
log.Fields{
"url_path": r.URL.Path,
"name": name,
"card_id": card_id,
"amount": amount,
"req.Host": r.Host,
}).Info("lnurlp_callback")
domain := db.Get_setting("HOST_DOMAIN")
hostdomainPort := db.Get_setting("HOST_DOMAIN_PORT")
hostdomainsuffix := ""
if hostdomainPort != "" {
hostdomainsuffix = ":" + hostdomainPort
}
if r.Host != domain {
log.Warn("wrong host domain")
resp_err.Write(w)
return
}
amount_msat, err := strconv.ParseInt(amount, 10, 64)
if err != nil {
log.Warn("amount is not a valid integer")
resp_err.Write(w)
return
}
amount_sat := amount_msat / 1000
metadata := "[[\"text/identifier\",\"" + name + "@" + domain + hostdomainsuffix + "\"],[\"text/plain\",\"bolt card deposit\"]]"
pr, r_hash, err := lnd.Add_invoice(amount_sat, metadata)
if err != nil {
log.Warn("could not add_invoice")
resp_err.Write(w)
return
}
err = db.Insert_receipt(card_id, pr, hex.EncodeToString(r_hash), amount_msat)
if err != nil {
log.Warn(err)
resp_err.Write(w)
return
}
go lnd.Monitor_invoice_state(r_hash)
log.Debug("sending 'status OK' response")
jsonData := []byte(`{` +
`"status":"OK",` +
`"routes":[],` +
`"pr":"` + pr + `"` +
`}`)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonData)
}

70
lnurlp/lnurlp_request.go Normal file
View file

@ -0,0 +1,70 @@
package lnurlp
import (
"net/http"
"github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/resp_err"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
)
func Response(w http.ResponseWriter, r *http.Request) {
if db.Get_setting("FUNCTION_LNURLP") != "ENABLE" {
log.Debug("LNURLp function is not enabled")
return
}
name := mux.Vars(r)["name"]
log.WithFields(
log.Fields{
"url_path": r.URL.Path,
"name": name,
"r.Host": r.Host,
}).Info("lnurlp_response")
// look up domain 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 {
log.Warn("wrong host domain")
resp_err.Write(w)
return
}
// look up name in database (table cards, field card_name)
card_count, err := db.Get_card_count_for_name_lnurlp(name)
if err != nil {
log.Warn("could not get card count for name")
resp_err.Write(w)
return
}
if card_count != 1 {
log.Info("not one enabled card with that name")
resp_err.Write(w)
return
}
metadata := "[[\\\"text/identifier\\\",\\\"" + name + "@" + domain + hostdomainsuffix + "\\\"],[\\\"text/plain\\\",\\\"bolt card deposit\\\"]]"
jsonData := []byte(`{"status":"OK",` +
`"callback":"https://` + domain + hostdomainsuffix + `/lnurlp/` + name + `",` +
`"tag":"payRequest",` +
`"maxSendable":1000000000,` +
`"minSendable":1000,` +
`"metadata":"` + metadata + `",` +
`"commentAllowed":0` +
`}`)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonData)
}

320
lnurlw/lnurlw_callback.go Normal file
View file

@ -0,0 +1,320 @@
package lnurlw
import (
"bytes"
"encoding/json"
"io"
"net/http"
"strings"
"github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/lnd"
"github.com/boltcard/boltcard/lndhub"
"github.com/boltcard/boltcard/resp_err"
decodepay "github.com/fiatjaf/ln-decodepay"
log "github.com/sirupsen/logrus"
)
type LndhubAuthRequest struct {
Login string `json:"login"`
Password string `json:"password"`
}
type LndhubAuthResponse struct {
RefreshToken string `json:"refresh_token"`
AccessToken string `json:"access_token"`
}
func lndhub_payment(w http.ResponseWriter, p *db.Payment, bolt11 decodepay.Bolt11, param_pr string) {
//get setting for LNDHUB_URL
lndhub_url := db.Get_setting("LNDHUB_URL")
//get lndhub login details from database
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 amount limits
invoice_sats := int(bolt11.MSatoshi / 1000)
//check the tx limit
if invoice_sats > c.Tx_limit_sats {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Info("invoice_sats: ", invoice_sats)
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Info("tx_limit_sats: ", c.Tx_limit_sats)
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Info("over tx_limit_sats!")
resp_err.Write(w)
return
}
//lndhub.auth API call
//the login JSON is held in the Card_name field
// as "login:password"
card_name_parts := strings.Split(c.Card_name, ":")
if len(card_name_parts) != 2 {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn("login:password not found")
resp_err.Write(w)
return
}
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("login:password badly formed")
resp_err.Write(w)
return
}
var lhAuthRequest LndhubAuthRequest
lhAuthRequest.Login = card_name_parts[0]
lhAuthRequest.Password = card_name_parts[1]
authReq, err := json.Marshal(lhAuthRequest)
req_auth, err := http.NewRequest("POST", lndhub_url+"/auth", bytes.NewBuffer(authReq))
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn(err)
resp_err.Write(w)
return
}
req_auth.Header.Add("Access-Control-Allow-Origin", "*")
req_auth.Header.Add("Content-Type", "application/json")
client := &http.Client{}
resp_auth, err := client.Do(req_auth)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn(err)
resp_err.Write(w)
return
}
defer resp_auth.Body.Close()
resp_auth_bytes, err := io.ReadAll(resp_auth.Body)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn(err)
resp_err.Write(w)
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
err = json.Unmarshal([]byte(resp_auth_bytes), &auth_keys)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn(err)
resp_err.Write(w)
return
}
// update paid_flag so we only attempt payment once
err = db.Update_payment_paid(p.Card_payment_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn(err)
resp_err.Write(w)
return
}
// https://github.com/fiatjaf/lnurl-rfc/blob/luds/03.md
//
// LN SERVICE sends a {"status": "OK"} or
// {"status": "ERROR", "reason": "error details..."}
// JSON response and then attempts to pay the invoices asynchronously.
go lndhub.PayInvoice(p.Card_payment_id, param_pr, int(bolt11.MSatoshi/1000), card_name_parts[0], auth_keys.AccessToken)
log.Debug("sending 'status OK' response")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
jsonData := []byte(`{"status":"OK"}`)
w.Write(jsonData)
}
func lnd_payment(w http.ResponseWriter, p *db.Payment, bolt11 decodepay.Bolt11, param_pr string) {
// check amount limits
invoice_sats := int(bolt11.MSatoshi / 1000)
day_total_sats, err := db.Get_card_totals(p.Card_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn(err)
resp_err.Write(w)
return
}
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
}
if invoice_sats > c.Tx_limit_sats {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Info("invoice_sats: ", invoice_sats)
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Info("tx_limit_sats: ", c.Tx_limit_sats)
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Info("over tx_limit_sats!")
resp_err.Write(w)
return
}
if day_total_sats+invoice_sats > c.Day_limit_sats {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Info("invoice_sats: ", invoice_sats)
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Info("day_total_sats: ", day_total_sats)
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Info("day_limit_sats: ", c.Day_limit_sats)
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Info("over day_limit_sats!")
resp_err.Write(w)
return
}
// check the card balance if marked as 'must stay above zero' (default)
// i.e. cards.allow_negative_balance == 'N'
if c.Allow_negative_balance != "Y" {
card_total, err := db.Get_card_total_sats(p.Card_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn(err)
resp_err.Write(w)
return
}
if card_total-invoice_sats < 0 {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn("not enough balance")
resp_err.Write(w)
return
}
}
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Info("paying invoice")
// update paid_flag so we only attempt payment once
err = db.Update_payment_paid(p.Card_payment_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn(err)
resp_err.Write(w)
return
}
// https://github.com/fiatjaf/lnurl-rfc/blob/luds/03.md
//
// LN SERVICE sends a {"status": "OK"} or
// {"status": "ERROR", "reason": "error details..."}
// JSON response and then attempts to pay the invoices asynchronously.
go lnd.PayInvoice(p.Card_payment_id, param_pr)
log.Debug("sending 'status OK' response")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
jsonData := []byte(`{"status":"OK"}`)
w.Write(jsonData)
}
func Callback(w http.ResponseWriter, req *http.Request) {
env_host_domain := db.Get_setting("HOST_DOMAIN")
if req.Host != env_host_domain {
log.Warn("wrong host domain")
resp_err.Write(w)
return
}
url := req.URL.RequestURI()
log.WithFields(log.Fields{"url": url}).Debug("cb request")
// get k1 value
param_k1 := req.URL.Query().Get("k1")
if param_k1 == "" {
log.WithFields(log.Fields{"url": url}).Debug("k1 not found")
resp_err.Write(w)
return
}
p, err := db.Get_payment_k1(param_k1)
if err != nil {
log.WithFields(log.Fields{"url": url, "k1": param_k1}).Warn(err)
resp_err.Write(w)
return
}
// check that payment has not been made
if p.Paid_flag != "N" {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Info("payment already made")
resp_err.Write(w)
return
}
// check if lnurlw_request has timed out
lnurlw_timeout, err := db.Check_lnurlw_timeout(p.Card_payment_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn(err)
resp_err.Write(w)
return
}
if lnurlw_timeout == true {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Info("lnurlw request has timed out")
resp_err.Write(w)
return
}
// get the payment request
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")
resp_err.Write(w)
return
}
bolt11, _ := decodepay.Decodepay(param_pr)
// record the lightning invoice
err = db.Update_payment_invoice(p.Card_payment_id, param_pr, bolt11.MSatoshi)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn(err)
resp_err.Write(w)
return
}
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
testnode := db.Get_setting("LN_TESTNODE")
if testnode != "" && bolt11.Payee != testnode {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Info("rejected as not the defined test node")
resp_err.Write(w)
return
}
//check if we are using LND or LNDHUB for payment
lndhub := db.Get_setting("FUNCTION_LNDHUB")
if lndhub == "ENABLE" {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Info("initiating lndhub payment")
lndhub_payment(w, p, bolt11, param_pr)
} else {
log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Info("initiating lnd payment")
lnd_payment(w, p, bolt11, param_pr)
}
}

348
lnurlw/lnurlw_request.go Normal file
View file

@ -0,0 +1,348 @@
package lnurlw
import (
"encoding/hex"
"encoding/json"
"errors"
"net/http"
"os"
"strconv"
"strings"
"github.com/boltcard/boltcard/crypto"
"github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/resp_err"
log "github.com/sirupsen/logrus"
)
func get_p_c(req *http.Request, p_name string, c_name string) (p string, c string) {
params_p, ok := req.URL.Query()[p_name]
if !ok || len(params_p[0]) < 1 {
return "", ""
}
params_c, ok := req.URL.Query()[c_name]
if !ok || len(params_c[0]) < 1 {
return "", ""
}
p = params_p[0]
c = params_c[0]
return
}
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[0]
sv2[14] = ctr[1]
sv2[15] = ctr[2]
cmac_verified, err := crypto.Aes_cmac(k2_cmac_key, sv2, cmac)
if err != nil {
return false, err
}
return cmac_verified, nil
}
func setup_card_record(uid string, ctr uint32, uid_bin []byte, ctr_bin []byte, cmac []byte) error {
// find the card record by matching the cmac
// get possible card records from the database
cards, err := db.Get_cards_blank_uid()
if err != nil {
return errors.New("db.Get_cards_blank_uid errored")
}
// check card records for a matching cmac
for _, card := range cards {
// check the cmac
k2_cmac_key, err := hex.DecodeString(card.K2_cmac_key)
if err != nil {
log.WithFields(log.Fields{
"card.card_id": card.Card_id,
"card.k2_cmac_key": card.K2_cmac_key,
}).Warn("card.k2_cmac_key decode failed - remove the invalid record")
return err
}
cmac_valid, err := check_cmac(uid_bin, ctr_bin, k2_cmac_key, cmac)
if err != nil {
return err
}
if cmac_valid == true {
log.WithFields(log.Fields{
"card.card_id": card.Card_id,
"card.k2_cmac_key": card.K2_cmac_key,
}).Info("cmac match found")
// store the uid and ctr in the card record
err := db.Update_card_uid_ctr(card.Card_id, uid, ctr)
if err != nil {
return err
}
return nil
}
}
log.Info("card record not found")
return nil
}
func parse_request(req *http.Request) (int, error) {
pid := os.Getpid()
url := req.URL.RequestURI()
log.WithFields(log.Fields{"pid": pid, "url": url}).Debug("ln request")
param_p, param_c := get_p_c(req, "p", "c")
ba_p, err := hex.DecodeString(param_p)
if err != nil {
return 0, errors.New("p parameter not valid hex")
}
ba_c, err := hex.DecodeString(param_c)
if err != nil {
return 0, errors.New("c parameter not valid hex")
}
if len(ba_p) != 16 {
return 0, errors.New("p parameter length not valid")
}
if len(ba_c) != 8 {
return 0, errors.New("c parameter length not valid")
}
// decrypt p with aes_decrypt_key
aes_decrypt_key := db.Get_setting("AES_DECRYPT_KEY")
key_sdm_file_read, err := hex.DecodeString(aes_decrypt_key)
if err != nil {
return 0, err
}
dec_p, err := crypto.Aes_decrypt(key_sdm_file_read, ba_p)
if err != nil {
return 0, err
}
if dec_p[0] != 0xC7 {
return 0, errors.New("decrypted data not starting with 0xC7")
}
uid := dec_p[1:8]
ctr := dec_p[8:11]
ctr_int := uint32(ctr[2])<<16 | uint32(ctr[1])<<8 | uint32(ctr[0])
// set up uid & ctr for card record if needed
uid_str := hex.EncodeToString(uid)
log.WithFields(log.Fields{"uid": uid_str, "ctr": ctr_int}).Info("decrypted card data")
card_count, err := db.Get_card_count_for_uid(uid_str)
if err != nil {
return 0, errors.New("could not get card count for uid")
}
if card_count == 0 {
log.Info("check CMACs and set UID")
setup_card_record(uid_str, ctr_int, uid, ctr, ba_c)
}
if card_count > 1 {
return 0, errors.New("more than one card found for uid")
}
// check card payment rules and make payment if appropriate
// get card record from database for UID
c, err := db.Get_card_from_uid(uid_str)
if err != nil {
return 0, errors.New("card not found for uid")
}
// check if card is enabled
if c.Lnurlw_enable != "Y" {
return 0, errors.New("card lnurlw enable is not set to Y")
}
// check cmac
k2_cmac_key, err := hex.DecodeString(c.K2_cmac_key)
if err != nil {
return 0, err
}
cmac_valid, err := check_cmac(uid, ctr, k2_cmac_key, ba_c)
if err != nil {
return 0, err
}
if cmac_valid == false {
return 0, errors.New("cmac incorrect")
}
// check and update last_counter_value
counter_ok, err := db.Check_and_update_counter(c.Card_id, ctr_int)
if err != nil {
return 0, err
}
if counter_ok == false {
return 0, errors.New("counter not increasing")
}
log.WithFields(log.Fields{"card_id": c.Card_id, "counter": ctr_int}).Info("validated")
return c.Card_id, nil
}
func Response(w http.ResponseWriter, req *http.Request) {
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 {
log.Warn("wrong host domain")
resp_err.Write(w)
return
}
card_id, err := parse_request(req)
if err != nil {
log.Debug(err.Error())
resp_err.Write(w)
return
}
lnurlw_k1, err := crypto.Create_k1()
if err != nil {
log.Warn(err.Error())
resp_err.Write(w)
return
}
// store k1 in database and include in response
err = db.Insert_payment(card_id, lnurlw_k1)
if err != nil {
log.Warn(err.Error())
resp_err.Write(w)
return
}
lnurlw_cb_url := ""
if strings.HasSuffix(env_host_domain, ".onion") {
lnurlw_cb_url = "http://" + env_host_domain + hostdomainsuffix + "/cb"
} else {
lnurlw_cb_url = "https://" + env_host_domain + hostdomainsuffix + "/cb"
}
min_withdraw_sats_str := db.Get_setting("MIN_WITHDRAW_SATS")
min_withdraw_sats, err := strconv.Atoi(min_withdraw_sats_str)
if err != nil {
log.Warn(err.Error())
resp_err.Write(w)
return
}
max_withdraw_sats_str := db.Get_setting("MAX_WITHDRAW_SATS")
max_withdraw_sats, err := strconv.Atoi(max_withdraw_sats_str)
if err != nil {
log.Warn(err.Error())
resp_err.Write(w)
return
}
// get pin_enable & pin_limit_sats
c, err := db.Get_card_from_card_id(card_id)
if err != nil {
log.WithFields(log.Fields{"card_id": card_id}).Warn(err)
resp_err.Write(w)
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)
if err != nil {
log.Warn(err)
resp_err.Write(w)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonData)
}

View file

@ -1,142 +0,0 @@
package main
import (
decodepay "github.com/fiatjaf/ln-decodepay"
log "github.com/sirupsen/logrus"
"net/http"
"os"
)
func lnurlw_callback(w http.ResponseWriter, req *http.Request) {
url := req.URL.RequestURI()
log.WithFields(log.Fields{"url": url}).Debug("cb request")
// check k1 value
params_k1, ok := req.URL.Query()["k1"]
if !ok || len(params_k1[0]) < 1 {
log.WithFields(log.Fields{"url": url}).Debug("k1 not found")
return
}
param_k1 := params_k1[0]
p, err := db_get_payment_k1(param_k1)
if err != nil {
log.WithFields(log.Fields{"url": url, "k1": param_k1}).Warn(err)
return
}
// check that payment has not been made
if p.paid_flag != "N" {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("payment already made")
return
}
// check if lnurlw_request has timed out
lnurlw_timeout, err := db_check_lnurlw_timeout(p.card_payment_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn(err)
return
}
if lnurlw_timeout == true {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("lnurlw request has timed out")
return
}
params_pr, ok := req.URL.Query()["pr"]
if !ok || len(params_pr[0]) < 1 {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn("pr field not found")
return
}
param_pr := params_pr[0]
bolt11, _ := decodepay.Decodepay(param_pr)
// record the lightning invoice
err = db_update_payment_invoice(p.card_payment_id, param_pr, bolt11.MSatoshi)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn(err)
return
}
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Debug("checking payment rules")
// check if we are only sending funds to a defined test node
testnode := os.Getenv("LN_TESTNODE")
if testnode != "" && bolt11.Payee != testnode {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("rejected as not the defined test node")
return
}
// check amount limits
invoice_sats := int(bolt11.MSatoshi / 1000)
day_total_sats, err := db_get_card_totals(p.card_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn(err)
return
}
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)
return
}
if invoice_sats > c.tx_limit_sats {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("invoice_sats: ", invoice_sats)
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("tx_limit_sats: ", c.tx_limit_sats)
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("over tx_limit_sats!")
return
}
if day_total_sats+invoice_sats > c.day_limit_sats {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("invoice_sats: ", invoice_sats)
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("day_total_sats: ", day_total_sats)
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("day_limit_sats: ", c.day_limit_sats)
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("over day_limit_sats!")
return
}
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("paying invoice")
// update paid_flag so we only attempt payment once
err = db_update_payment_paid(p.card_payment_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn(err)
return
}
// https://github.com/fiatjaf/lnurl-rfc/blob/luds/03.md
//
// LN SERVICE sends a {"status": "OK"} or
// {"status": "ERROR", "reason": "error details..."}
// JSON response and then attempts to pay the invoices asynchronously.
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
jsonData := []byte(`{"status":"OK"}`)
w.Write(jsonData)
payment_status, failure_reason, err := pay_invoice(param_pr)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn(err)
return
}
if failure_reason != "FAILURE_REASON_NONE" {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("payment failure reason : ", failure_reason)
}
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("payment status : ", payment_status)
// store result in database
err = db_update_payment_status(p.card_payment_id, payment_status, failure_reason)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn(err)
return
}
}

View file

@ -1,216 +0,0 @@
package main
import (
"encoding/hex"
"encoding/json"
"errors"
log "github.com/sirupsen/logrus"
"net/http"
"os"
"strconv"
)
type Response struct {
Tag string `json:"tag"`
Callback string `json:"callback"`
K1 string `json:"k1"`
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) {
params_p, ok := req.URL.Query()[p_name]
if !ok || len(params_p[0]) < 1 {
return "", ""
}
params_c, ok := req.URL.Query()[c_name]
if !ok || len(params_c[0]) < 1 {
return "", ""
}
p = params_p[0]
c = params_c[0]
return
}
func parse_request(req *http.Request) (int, error) {
url := req.URL.RequestURI()
log.Debug("ln url: ", url)
param_p, param_c := get_p_c(req, "p", "c")
ba_p, err := hex.DecodeString(param_p)
if err != nil {
return 0, errors.New("p parameter not valid hex")
}
ba_c, err := hex.DecodeString(param_c)
if err != nil {
return 0, errors.New("c parameter not valid hex")
}
if len(ba_p) != 16 {
return 0, errors.New("p parameter length not valid")
}
if len(ba_c) != 8 {
return 0, errors.New("c parameter length not valid")
}
// decrypt p with aes_decrypt_key
aes_decrypt_key := os.Getenv("AES_DECRYPT_KEY")
key_sdm_file_read, err := hex.DecodeString(aes_decrypt_key)
if err != nil {
return 0, err
}
dec_p, err := crypto_aes_decrypt(key_sdm_file_read, ba_p)
if err != nil {
return 0, err
}
if dec_p[0] != 0xC7 {
return 0, errors.New("decrypted data not starting with 0xC7")
}
uid := dec_p[1:8]
ctr := dec_p[8:11]
ctr_int := uint32(ctr[2])<<16 | uint32(ctr[1])<<8 | uint32(ctr[0])
// get card record from database for UID
uid_str := hex.EncodeToString(uid)
log.Debug("card UID: ", uid_str)
c, err := db_get_card_from_uid(uid_str)
if err != nil {
return 0, errors.New("card not found for UID")
}
// check if card is enabled
if c.enable_flag != "Y" {
return 0, errors.New("card enable is not set to Y")
}
// check cmac
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[0]
sv2[14] = ctr[1]
sv2[15] = ctr[2]
key_sdm_file_read_mac, err := hex.DecodeString(c.aes_cmac)
if err != nil {
return 0, err
}
cmac_verified, err := crypto_aes_cmac(key_sdm_file_read_mac, sv2, ba_c)
if err != nil {
return 0, err
}
if cmac_verified == false {
return 0, errors.New("CMAC incorrect")
}
// check and update last_counter_value
counter_ok, err := db_check_and_update_counter(c.card_id, ctr_int)
if err != nil {
return 0, err
}
if counter_ok == false {
return 0, errors.New("counter not increasing")
}
log.WithFields(log.Fields{"card_id": c.card_id, "counter": ctr_int}).Info("validated")
return c.card_id, nil
}
func lnurlw_response(w http.ResponseWriter, req *http.Request) {
card_id, err := parse_request(req)
if err != nil {
log.Debug(err.Error())
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
jsonData := []byte(`{"status":"ERROR","reason":"bad request"}`)
w.Write(jsonData)
return
}
k1, err := create_k1()
if err != nil {
log.Warn(err.Error())
return
}
// store k1 in database and include in response
err = db_insert_payment(card_id, k1)
if err != nil {
log.Warn(err.Error())
return
}
host_domain := os.Getenv("HOST_DOMAIN")
lnurlw_cb_url := "https://" + host_domain + "/cb"
min_withdraw_sats_str := os.Getenv("MIN_WITHDRAW_SATS")
min_withdraw_sats, err := strconv.Atoi(min_withdraw_sats_str)
if err != nil {
log.Warn(err.Error())
return
}
max_withdraw_sats_str := os.Getenv("MAX_WITHDRAW_SATS")
max_withdraw_sats, err := strconv.Atoi(max_withdraw_sats_str)
if err != nil {
log.Warn(err.Error())
return
}
response := Response{}
response.Tag = "withdrawRequest"
response.Callback = lnurlw_cb_url
response.K1 = k1
response.DefaultDescription = "WWT withdrawal"
response.MinWithdrawable = min_withdraw_sats * 1000 // milliSats
response.MaxWithdrawable = max_withdraw_sats * 1000 // milliSats
jsonData, err := json.Marshal(response)
if err != nil {
log.Warn(err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonData)
}

77
main.go
View file

@ -1,28 +1,85 @@
package main package main
import ( import (
"github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/internalapi"
"github.com/boltcard/boltcard/lnurlp"
"github.com/boltcard/boltcard/lnurlw"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"net/http" "net/http"
"os" "time"
) )
func main() { var router = mux.NewRouter()
log_level := os.Getenv("LOG_LEVEL")
if log_level == "DEBUG" { func main() {
log_level := db.Get_setting("LOG_LEVEL")
switch log_level {
case "DEBUG":
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)
log.Info("bolt card service started - debug log level")
case "PRODUCTION":
log.Info("bolt card service started - production log level")
default:
// log.Fatal calls os.Exit(1) after logging the error
log.Fatal("error getting a valid LOG_LEVEL setting from the database")
} }
log.SetFormatter(&log.JSONFormatter{ log.SetFormatter(&log.JSONFormatter{
DisableHTMLEscape: true, DisableHTMLEscape: true,
}) })
mux := http.NewServeMux() var external_router = mux.NewRouter()
var internal_router = mux.NewRouter()
mux.HandleFunc("/new", new_card_request) // external API
mux.HandleFunc("/ln", lnurlw_response)
mux.HandleFunc("/cb", lnurlw_callback)
err := http.ListenAndServe(":9000", mux) // ping
log.Fatal(err) external_router.Path("/ping").Methods("GET").HandlerFunc(external_ping)
// createboltcard
external_router.Path("/new").Methods("GET").HandlerFunc(new_card_request)
// lnurlw for pos
external_router.Path("/ln").Methods("GET").HandlerFunc(lnurlw.Response)
external_router.Path("/cb").Methods("GET").HandlerFunc(lnurlw.Callback)
// lnurlp for lightning address
external_router.Path("/.well-known/lnurlp/{name}").Methods("GET").HandlerFunc(lnurlp.Response)
external_router.Path("/lnurlp/{name}").Methods("GET").HandlerFunc(lnurlp.Callback)
// internal API
// this has no authentication and is not to be exposed publicly
// it exists for use on a private virtual network within a docker container
internal_router.Path("/ping").Methods("GET").HandlerFunc(internalapi.Internal_ping)
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("/updateboltcardwithpin").Methods("GET").HandlerFunc(internalapi.Updateboltcardwithpin)
internal_router.Path("/wipeboltcard").Methods("GET").HandlerFunc(internalapi.Wipeboltcard)
internal_router.Path("/getboltcard").Methods("GET").HandlerFunc(internalapi.Getboltcard)
port := db.Get_setting("HOST_PORT")
if port == "" {
port = "9000"
}
external_server := &http.Server{
Handler: external_router,
Addr: ":" + port, // consider adding host
WriteTimeout: 30 * time.Second,
ReadTimeout: 30 * time.Second,
}
internal_server := &http.Server{
Handler: internal_router,
Addr: ":9001",
WriteTimeout: 5 * time.Second,
ReadTimeout: 5 * time.Second,
}
go external_server.ListenAndServe()
go internal_server.ListenAndServe()
select {}
} }

View file

@ -1,16 +1,45 @@
package main package main
import ( import (
"database/sql"
"encoding/json" "encoding/json"
log "github.com/sirupsen/logrus"
"net/http" "net/http"
"os"
"github.com/boltcard/boltcard/db"
"github.com/boltcard/boltcard/resp_err"
log "github.com/sirupsen/logrus"
) )
/**
* @api {get} /new/:a Request information to create a new bolt card
* @apiName NewBoltCard
* @apiGroup BoltCardService
*
* @apiParam {String} a one time authentication code
*
* @apiSuccess {String} protocol_name name of the protocol message
* @apiSuccess {Int} protocol_version version of the protocol message
* @apiSuccess {String} card_name user friendly card name
* @apiSuccess {String} lnurlw_base base for creating the lnurlw on the card
* @apiSuccess {String} k0 Key 0 - authorisation key
* @apiSuccess {String} k1 Key 1 - decryption key
* @apiSuccess {String} k2 Key 2 - authentication key
* @apiSuccess {String} k3 Key 3 - NXP documents say this must be set
* @apiSuccess {String} k4 Key 4 - NXP documents say this must be set
* @apiSuccess {String} uid_privacy - set up the card for the UID to be private
*/
type NewCardResponse struct { type NewCardResponse struct {
PROTOCOL_NAME string `json:"protocol_name"`
PROTOCOL_VERSION int `json:"protocol_version"`
CARD_NAME string `json:"card_name"`
LNURLW_BASE string `json:"lnurlw_base"`
K0 string `json:"k0"` K0 string `json:"k0"`
K1 string `json:"k1"` K1 string `json:"k1"`
K2 string `json:"k2"` K2 string `json:"k2"`
K3 string `json:"k3"`
K4 string `json:"k4"`
UID_PRIVACY string `json:"uid_privacy"`
} }
func new_card_request(w http.ResponseWriter, req *http.Request) { func new_card_request(w http.ResponseWriter, req *http.Request) {
@ -21,47 +50,54 @@ func new_card_request(w http.ResponseWriter, req *http.Request) {
params_a, ok := req.URL.Query()["a"] params_a, ok := req.URL.Query()["a"]
if !ok || len(params_a[0]) < 1 { if !ok || len(params_a[0]) < 1 {
log.Debug("a not found") log.Debug("a not found")
resp_err.Write(w)
return return
} }
a := params_a[0] a := params_a[0]
if a == "00000000000000000000000000000000" { hostdomainPort := db.Get_setting("HOST_DOMAIN_PORT")
response := NewCardResponse{} hostdomainsuffix := ""
response.K0 = "11111111111111111111111111111111" if hostdomainPort != "" {
response.K1 = "22222222222222222222222222222222" hostdomainsuffix = ":" + hostdomainPort
response.K2 = "33333333333333333333333333333333" }
log.Debug("special a = 0...0") lnurlw_base := "lnurlw://" + db.Get_setting("HOST_DOMAIN") + hostdomainsuffix + "/ln"
jsonData, err := json.Marshal(response) c, err := db.Get_new_card(a)
if err != nil {
log.Warn(err) if err == sql.ErrNoRows {
log.Debug(err)
resp_err.Write_message(w, "one time code was used or card was wiped or card does not exist")
return return
} }
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonData)
return;
}
c, err := db_get_new_card(a)
if err != nil { if err != nil {
log.Warn(err) log.Warn(err)
resp_err.Write(w)
return return
} }
aes_decrypt_key := os.Getenv("AES_DECRYPT_KEY") k1_decrypt_key := db.Get_setting("AES_DECRYPT_KEY")
response := NewCardResponse{} response := NewCardResponse{}
response.K0 = c.lock_key response.PROTOCOL_NAME = "create_bolt_card_response"
response.K1 = aes_decrypt_key response.PROTOCOL_VERSION = 2
response.K2 = c.aes_cmac response.CARD_NAME = c.Card_name
response.LNURLW_BASE = lnurlw_base
response.K0 = c.K0_auth_key
response.K1 = k1_decrypt_key
response.K2 = c.K2_cmac_key
response.K3 = c.K3
response.K4 = c.K4
response.UID_PRIVACY = c.Uid_privacy
log.SetFormatter(&log.JSONFormatter{
DisableHTMLEscape: true,
})
jsonData, err := json.Marshal(response) jsonData, err := json.Marshal(response)
if err != nil { if err != nil {
log.Warn(err) log.Warn(err)
resp_err.Write(w)
return return
} }

12
ping.go Normal file
View file

@ -0,0 +1,12 @@
package main
import (
"net/http"
)
func external_ping(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
jsonData := []byte(`{"status":"OK","pong":"external API"}`)
w.Write(jsonData)
}

19
resp_err/resp_err.go Normal file
View file

@ -0,0 +1,19 @@
package resp_err
import (
"net/http"
)
func Write(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
jsonData := []byte(`{"status":"ERROR","reason":"bad request"}`)
w.Write(jsonData)
}
func Write_message(w http.ResponseWriter, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
jsonData := []byte(`{"status":"ERROR","reason":"` + message + `"}`)
w.Write(jsonData)
}

View file

@ -1,4 +1,5 @@
go build go build
sudo cp boltcard.service /etc/systemd/system/boltcard.service
sudo systemctl daemon-reload sudo systemctl daemon-reload
sudo systemctl stop boltcard sudo systemctl stop boltcard
sudo systemctl start boltcard sudo systemctl start boltcard

23
script/s_create_db Executable file
View file

@ -0,0 +1,23 @@
# to close any database connections
sudo systemctl stop postgresql
sudo systemctl start postgresql
echo If you have previously created the database
echo then this will delete it and recreate it.
echo
echo Key values for cards may be in the database data.
echo
echo Continue? "(y or n)"
read x
if [ "$x" = "y" ]; then
psql postgres -f sql/create_db_init.sql
psql postgres -f sql/create_db.sql
psql postgres -f sql/create_db_user.sql
psql postgres -f sql/settings.sql.secret
echo Database created
else
echo No action
fi

View file

@ -2,5 +2,5 @@
sudo systemctl stop postgresql sudo systemctl stop postgresql
sudo systemctl start postgresql sudo systemctl start postgresql
psql postgres -f create_db.sql psql postgres -f sql/data.test.sql
psql postgres -f add_card_data.sql echo Test data added

71
sql/create_db.sql Normal file
View file

@ -0,0 +1,71 @@
\c card_db;
CREATE TABLE settings (
setting_id INT GENERATED ALWAYS AS IDENTITY,
name VARCHAR(30) UNIQUE NOT NULL DEFAULT '',
value VARCHAR(128) NOT NULL DEFAULT '',
PRIMARY KEY(setting_id)
);
CREATE TABLE cards (
card_id INT GENERATED ALWAYS AS IDENTITY,
k0_auth_key CHAR(32) NOT NULL,
k2_cmac_key CHAR(32) NOT NULL,
k3 CHAR(32) NOT NULL,
k4 CHAR(32) NOT NULL,
uid VARCHAR(14) NOT NULL DEFAULT '',
last_counter_value INTEGER NOT NULL,
lnurlw_request_timeout_sec INT NOT NULL,
lnurlw_enable CHAR(1) NOT NULL DEFAULT 'N',
tx_limit_sats INT NOT NULL,
day_limit_sats INT NOT NULL,
lnurlp_enable CHAR(1) NOT NULL DEFAULT 'N',
card_name VARCHAR(100) NOT NULL,
email_address VARCHAR(100) DEFAULT '',
email_enable CHAR(1) NOT NULL DEFAULT 'N',
uid_privacy CHAR(1) NOT NULL DEFAULT 'N',
one_time_code CHAR(32) NOT NULL DEFAULT '',
one_time_code_expiry TIMESTAMPTZ DEFAULT NOW() + INTERVAL '1 DAY',
one_time_code_used CHAR(1) NOT NULL DEFAULT 'Y',
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',
PRIMARY KEY(card_id)
);
CREATE TABLE card_payments (
card_payment_id INT GENERATED ALWAYS AS IDENTITY,
card_id INT NOT NULL,
lnurlw_k1 CHAR(32) UNIQUE NOT NULL,
lnurlw_request_time TIMESTAMPTZ NOT NULL,
ln_invoice VARCHAR(1024) NOT NULL DEFAULT '',
amount_msats BIGINT CHECK (amount_msats > 0),
paid_flag CHAR(1) NOT NULL,
payment_time TIMESTAMPTZ,
payment_status VARCHAR(100) NOT NULL DEFAULT '',
failure_reason VARCHAR(100) NOT NULL DEFAULT '',
payment_status_time TIMESTAMPTZ,
PRIMARY KEY(card_payment_id),
CONSTRAINT fk_card FOREIGN KEY(card_id) REFERENCES cards(card_id)
);
CREATE TABLE card_receipts (
card_receipt_id INT GENERATED ALWAYS AS IDENTITY,
card_id INT NOT NULL,
ln_invoice VARCHAR(1024) NOT NULL DEFAULT '',
r_hash_hex CHAR(64) UNIQUE NOT NULL DEFAULT '',
amount_msats BIGINT CHECK (amount_msats > 0),
receipt_status VARCHAR(100) NOT NULL DEFAULT '',
receipt_status_time TIMESTAMPTZ,
PRIMARY KEY(card_receipt_id),
CONSTRAINT fk_card FOREIGN KEY(card_id) REFERENCES cards(card_id)
);
GRANT ALL PRIVILEGES ON TABLE settings TO cardapp;
GRANT ALL PRIVILEGES ON TABLE cards TO cardapp;
GRANT ALL PRIVILEGES ON TABLE card_payments TO cardapp;
GRANT ALL PRIVILEGES ON TABLE card_receipts TO cardapp;

2
sql/create_db_init.sql Normal file
View file

@ -0,0 +1,2 @@
DROP DATABASE IF EXISTS card_db;
CREATE DATABASE card_db;

2
sql/create_db_user.sql Normal file
View file

@ -0,0 +1,2 @@
DROP USER cardapp;
CREATE USER cardapp WITH PASSWORD 'database_password';

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

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

1
sql/select_db.sql Normal file
View file

@ -0,0 +1 @@
SELECT 'CREATE DATABASE card_db' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'card_db');

34
sql/settings.sql Normal file
View file

@ -0,0 +1,34 @@
\c card_db;
DELETE FROM settings;
-- an explanation for each of the bolt card server settings can be found here
-- https://github.com/boltcard/boltcard/blob/main/docs/SETTINGS.md
INSERT INTO settings (name, value) VALUES ('LOG_LEVEL', '');
INSERT INTO settings (name, value) VALUES ('AES_DECRYPT_KEY', '');
INSERT INTO settings (name, value) VALUES ('HOST_DOMAIN', '');
INSERT INTO settings (name, value) VALUES ('MIN_WITHDRAW_SATS', '');
INSERT INTO settings (name, value) VALUES ('MAX_WITHDRAW_SATS', '');
INSERT INTO settings (name, value) VALUES ('LN_HOST', '');
INSERT INTO settings (name, value) VALUES ('LN_PORT', '');
INSERT INTO settings (name, value) VALUES ('LN_TLS_FILE', '');
INSERT INTO settings (name, value) VALUES ('LN_MACAROON_FILE', '');
INSERT INTO settings (name, value) VALUES ('FEE_LIMIT_SAT', '');
INSERT INTO settings (name, value) VALUES ('FEE_LIMIT_PERCENT', '');
INSERT INTO settings (name, value) VALUES ('LN_TESTNODE', '');
INSERT INTO settings (name, value) VALUES ('FUNCTION_LNURLW', '');
INSERT INTO settings (name, value) VALUES ('FUNCTION_LNURLP', '');
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_SECRET', '');
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 ('FUNCTION_LNDHUB', '');
INSERT INTO settings (name, value) VALUES ('LNDHUB_URL', '');
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');