diff --git a/.gitignore b/.gitignore index 43f5114..69b774b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ boltcard createboltcard/createboltcard wipeboltcard/wipeboltcard +cli/cli # Test binary, built with `go test -c` *.test diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 0000000..b11bc1d --- /dev/null +++ b/cli/main.go @@ -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) +} diff --git a/docker_init.sh b/docker_init.sh index 926ed5b..708070c 100755 --- a/docker_init.sh +++ b/docker_init.sh @@ -32,3 +32,4 @@ sed -i "s/[(]'FEE_LIMIT_PERCENT'[^)]*[)]/(\'FEE_LIMIT_PERCENT\', \'0.5\')/" sql/ sed -i "s/[(]'FUNCTION_LNURLW'[^)]*[)]/(\'FUNCTION_LNURLW\', \'ENABLE\')/" sql/settings.sql sed -i "s/[(]'FUNCTION_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 diff --git a/docs/CARD_MANUAL.md b/docs/CARD_MANUAL.md index d7613be..6688650 100644 --- a/docs/CARD_MANUAL.md +++ b/docs/CARD_MANUAL.md @@ -46,7 +46,9 @@ lnurlw://card.yourdomain.com/ln ``` lnurlw://card.yourdomain.com/ln?p=00000000000000000000000000000000&c=0000000000000000 ``` + - click after `p=` and note the p_position (41 in this case) + - click after `c=` and note the c_position (76 in this case) - select `Write To Tag` @@ -101,7 +103,7 @@ lnurlw://card.yourdomain.com/ln?p=00000000000000000000000000000000&c=00000000000 - set up the values in the order shown - + - select `Change File Settings` diff --git a/docs/DEEPLINK.md b/docs/DEEPLINK.md new file mode 100644 index 0000000..4ac2b1a --- /dev/null +++ b/docs/DEEPLINK.md @@ -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 +
+ + Setup Boltcard + + | + + Reset Boltcard + +
+``` diff --git a/docs/DETERMINISTIC.md b/docs/DETERMINISTIC.md new file mode 100644 index 0000000..eec8171 --- /dev/null +++ b/docs/DETERMINISTIC.md @@ -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 +``` \ No newline at end of file diff --git a/docs/SETTINGS.md b/docs/SETTINGS.md index 371b4fc..1c52afa 100644 --- a/docs/SETTINGS.md +++ b/docs/SETTINGS.md @@ -34,3 +34,4 @@ Here are the descriptions of values available to use in the `settings` table: | 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 | diff --git a/docs/SPEC.md b/docs/SPEC.md index 2712de6..a5f9db4 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -3,11 +3,21 @@ 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) + - 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) - 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 the point-of-sale (POS) will read a NDEF message from the card, which changes with each use, for example diff --git a/docs/TECHNOLOGY.md b/docs/TECHNOLOGY.md index 9dfc777..9c1d248 100644 --- a/docs/TECHNOLOGY.md +++ b/docs/TECHNOLOGY.md @@ -4,6 +4,8 @@ | --- | --- | | [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 | diff --git a/docs/TEST_VECTORS.md b/docs/TEST_VECTORS.md new file mode 100644 index 0000000..646922a --- /dev/null +++ b/docs/TEST_VECTORS.md @@ -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 + +``` diff --git a/docs/images/fs-add-2.webp b/docs/images/fs-add-2.webp new file mode 100644 index 0000000..d395a7b Binary files /dev/null and b/docs/images/fs-add-2.webp differ diff --git a/docs/images/fs-add.webp b/docs/images/fs-add.webp index 6d15cec..d395a7b 100644 Binary files a/docs/images/fs-add.webp and b/docs/images/fs-add.webp differ diff --git a/docs/images/posn-p.webp b/docs/images/posn-p.webp new file mode 100644 index 0000000..0f9810a Binary files /dev/null and b/docs/images/posn-p.webp differ diff --git a/docs/images/posn.webp b/docs/images/posn.webp new file mode 100644 index 0000000..d395a7b Binary files /dev/null and b/docs/images/posn.webp differ diff --git a/internalapi/createboltcard.go b/internalapi/createboltcard.go index e0e1eb3..a8aa8f3 100644 --- a/internalapi/createboltcard.go +++ b/internalapi/createboltcard.go @@ -1,12 +1,13 @@ package internalapi import ( - "github.com/boltcard/boltcard/db" - "github.com/boltcard/boltcard/resp_err" - log "github.com/sirupsen/logrus" "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 @@ -100,11 +101,16 @@ func Createboltcard(w http.ResponseWriter, r *http.Request) { // 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 + "/new?a=" + one_time_code + url = "http://" + hostdomain + hostdomainsuffix + "/new?a=" + one_time_code } else { - url = "https://" + hostdomain + "/new?a=" + one_time_code + url = "https://" + hostdomain + hostdomainsuffix + "/new?a=" + one_time_code } // log the response diff --git a/internalapi/createboltcardwithpin.go b/internalapi/createboltcardwithpin.go index 64d9efd..a7e8cd8 100644 --- a/internalapi/createboltcardwithpin.go +++ b/internalapi/createboltcardwithpin.go @@ -3,12 +3,13 @@ package internalapi import ( "crypto/rand" "encoding/hex" - "github.com/boltcard/boltcard/db" - "github.com/boltcard/boltcard/resp_err" - log "github.com/sirupsen/logrus" "net/http" "strconv" "strings" + + "github.com/boltcard/boltcard/db" + "github.com/boltcard/boltcard/resp_err" + log "github.com/sirupsen/logrus" ) func random_hex() string { @@ -132,11 +133,16 @@ func Createboltcardwithpin(w http.ResponseWriter, r *http.Request) { // 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 + "/new?a=" + one_time_code + url = "http://" + hostdomain + hostdomainsuffix + "/new?a=" + one_time_code } else { - url = "https://" + hostdomain + "/new?a=" + one_time_code + url = "https://" + hostdomain + hostdomainsuffix + "/new?a=" + one_time_code } // log the response diff --git a/lnd/lnd.go b/lnd/lnd.go index c5e0c1a..f86548c 100644 --- a/lnd/lnd.go +++ b/lnd/lnd.go @@ -81,6 +81,10 @@ func Add_invoice(amount_sat int64, metadata string) (payment_request string, r_h 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)) @@ -98,6 +102,7 @@ func Add_invoice(amount_sat int64, metadata string) (payment_request string, r_h result, err := l_client.AddInvoice(ctx, &lnrpc.Invoice{ Value: amount_sat, DescriptionHash: dh[:], + Expiry: ln_invoice_expiry, }) if err != nil { @@ -120,6 +125,11 @@ func Monitor_invoice_state(r_hash []byte) { 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"), @@ -129,7 +139,7 @@ func Monitor_invoice_state(r_hash []byte) { i_client := invoicesrpc.NewInvoicesClient(connection) - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(ln_invoice_expiry)*time.Second) defer cancel() stream, err := i_client.SubscribeSingleInvoice(ctx, &invoicesrpc.SubscribeSingleInvoiceRequest{ @@ -228,10 +238,11 @@ func PayInvoice(card_payment_id int, invoice string) { 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(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(invoice_expiry)*time.Second) defer cancel() stream, err := r_client.SendPaymentV2(ctx, &routerrpc.SendPaymentRequest{ diff --git a/lnurlp/lnurlp_callback.go b/lnurlp/lnurlp_callback.go index 1984e8c..7d02eaf 100644 --- a/lnurlp/lnurlp_callback.go +++ b/lnurlp/lnurlp_callback.go @@ -2,13 +2,14 @@ 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" - "net/http" - "strconv" ) func Callback(w http.ResponseWriter, r *http.Request) { @@ -37,6 +38,11 @@ func Callback(w http.ResponseWriter, r *http.Request) { }).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) @@ -52,7 +58,7 @@ func Callback(w http.ResponseWriter, r *http.Request) { amount_sat := amount_msat / 1000 - metadata := "[[\"text/identifier\",\"" + name + "@" + domain + "\"],[\"text/plain\",\"bolt card deposit\"]]" + metadata := "[[\"text/identifier\",\"" + name + "@" + domain + hostdomainsuffix + "\"],[\"text/plain\",\"bolt card deposit\"]]" pr, r_hash, err := lnd.Add_invoice(amount_sat, metadata) if err != nil { log.Warn("could not add_invoice") diff --git a/lnurlp/lnurlp_request.go b/lnurlp/lnurlp_request.go index 6483388..853614d 100644 --- a/lnurlp/lnurlp_request.go +++ b/lnurlp/lnurlp_request.go @@ -1,11 +1,12 @@ 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" - "net/http" ) func Response(w http.ResponseWriter, r *http.Request) { @@ -26,6 +27,11 @@ func Response(w http.ResponseWriter, r *http.Request) { // look up domain setting (HOST_DOMAIN) 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) @@ -47,10 +53,10 @@ func Response(w http.ResponseWriter, r *http.Request) { return } - metadata := "[[\\\"text/identifier\\\",\\\"" + name + "@" + domain + "\\\"],[\\\"text/plain\\\",\\\"bolt card deposit\\\"]]" + metadata := "[[\\\"text/identifier\\\",\\\"" + name + "@" + domain + hostdomainsuffix + "\\\"],[\\\"text/plain\\\",\\\"bolt card deposit\\\"]]" jsonData := []byte(`{"status":"OK",` + - `"callback":"https://` + domain + `/lnurlp/` + name + `",` + + `"callback":"https://` + domain + hostdomainsuffix + `/lnurlp/` + name + `",` + `"tag":"payRequest",` + `"maxSendable":1000000000,` + `"minSendable":1000,` + diff --git a/lnurlw/lnurlw_request.go b/lnurlw/lnurlw_request.go index eb0b15f..ad392a9 100644 --- a/lnurlw/lnurlw_request.go +++ b/lnurlw/lnurlw_request.go @@ -4,14 +4,15 @@ import ( "encoding/hex" "encoding/json" "errors" - "github.com/boltcard/boltcard/crypto" - "github.com/boltcard/boltcard/db" - "github.com/boltcard/boltcard/resp_err" - log "github.com/sirupsen/logrus" "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) { @@ -246,6 +247,11 @@ func parse_request(req *http.Request) (int, error) { 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") @@ -280,10 +286,10 @@ func Response(w http.ResponseWriter, req *http.Request) { } lnurlw_cb_url := "" - if strings.HasSuffix(req.Host, ".onion") { - lnurlw_cb_url = "http://" + req.Host + "/cb" + if strings.HasSuffix(env_host_domain, ".onion") { + lnurlw_cb_url = "http://" + env_host_domain + hostdomainsuffix + "/cb" } else { - lnurlw_cb_url = "https://" + req.Host + "/cb" + lnurlw_cb_url = "https://" + env_host_domain + hostdomainsuffix + "/cb" } min_withdraw_sats_str := db.Get_setting("MIN_WITHDRAW_SATS") diff --git a/new_card_request.go b/new_card_request.go index c35bd43..1512e4f 100644 --- a/new_card_request.go +++ b/new_card_request.go @@ -3,10 +3,11 @@ package main import ( "database/sql" "encoding/json" + "net/http" + "github.com/boltcard/boltcard/db" "github.com/boltcard/boltcard/resp_err" log "github.com/sirupsen/logrus" - "net/http" ) /** @@ -55,7 +56,12 @@ func new_card_request(w http.ResponseWriter, req *http.Request) { a := params_a[0] - lnurlw_base := "lnurlw://" + db.Get_setting("HOST_DOMAIN") + "/ln" + hostdomainPort := db.Get_setting("HOST_DOMAIN_PORT") + hostdomainsuffix := "" + if hostdomainPort != "" { + hostdomainsuffix = ":" + hostdomainPort + } + lnurlw_base := "lnurlw://" + db.Get_setting("HOST_DOMAIN") + hostdomainsuffix + "/ln" c, err := db.Get_new_card(a) diff --git a/sql/settings.sql b/sql/settings.sql index 1f6a4e4..810a2bb 100644 --- a/sql/settings.sql +++ b/sql/settings.sql @@ -31,3 +31,4 @@ 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');