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/db/db.go b/db/db.go index 3ea6524..05041b1 100644 --- a/db/db.go +++ b/db/db.go @@ -31,6 +31,9 @@ type Card struct { Card_name string Allow_negative_balance string Nostr_priv_key string + Pin_enable string + Pin_number string + Pin_limit_sats int } type Payment struct { @@ -357,7 +360,8 @@ func Get_card_from_card_id(card_id int) (*Card, error) { `last_counter_value, lnurlw_request_timeout_sec, ` + `lnurlw_enable, tx_limit_sats, day_limit_sats, ` + `email_enable, email_address, card_name, ` + - `allow_negative_balance FROM cards WHERE card_id=$1;` + `allow_negative_balance, pin_enable, pin_number, ` + + `pin_limit_sats FROM cards WHERE card_id=$1;` row := db.QueryRow(sqlStatement, card_id) err = row.Scan( &c.Card_id, @@ -371,7 +375,10 @@ func Get_card_from_card_id(card_id int) (*Card, error) { &c.Email_enable, &c.Email_address, &c.Card_name, - &c.Allow_negative_balance) + &c.Allow_negative_balance, + &c.Pin_enable, + &c.Pin_number, + &c.Pin_limit_sats) if err != nil { return &c, err } @@ -392,7 +399,7 @@ func Get_card_from_card_name(card_name string) (*Card, error) { sqlStatement := `SELECT card_id, k2_cmac_key, uid,` + ` last_counter_value, lnurlw_request_timeout_sec,` + - ` lnurlw_enable, tx_limit_sats, day_limit_sats` + + ` lnurlw_enable, tx_limit_sats, day_limit_sats, pin_enable, pin_limit_sats` + ` FROM cards WHERE card_name=$1 AND wiped = 'N';` row := db.QueryRow(sqlStatement, card_name) err = row.Scan( @@ -403,7 +410,9 @@ func Get_card_from_card_name(card_name string) (*Card, error) { &c.Lnurlw_request_timeout_sec, &c.Lnurlw_enable, &c.Tx_limit_sats, - &c.Day_limit_sats) + &c.Day_limit_sats, + &c.Pin_enable, + &c.Pin_limit_sats) if err != nil { return &c, err } @@ -893,6 +902,71 @@ func Insert_card(one_time_code string, k0_auth_key string, k2_cmac_key string, k return nil } +func Insert_card_with_pin(one_time_code string, k0_auth_key string, k2_cmac_key string, k3 string, k4 string, + tx_limit_sats int, day_limit_sats int, lnurlw_enable bool, card_name string, uid_privacy bool, + allow_neg_bal_ptr bool, pin_enable bool, pin_number string, pin_limit_sats int) error { + + lnurlw_enable_yn := "N" + if lnurlw_enable { + lnurlw_enable_yn = "Y" + } + + uid_privacy_yn := "N" + if uid_privacy { + uid_privacy_yn = "Y" + } + + allow_neg_bal_yn := "N" + if allow_neg_bal_ptr { + allow_neg_bal_yn = "Y" + } + + pin_enable_yn := "N" + if pin_enable { + pin_enable_yn = "Y" + } + + db, err := open() + if err != nil { + return err + } + defer db.Close() + + // ensure any cards with the same card_name are wiped + + sqlStatement := `UPDATE cards SET` + + ` lnurlw_enable = 'N', lnurlp_enable = 'N', email_enable = 'N', wiped = 'Y'` + + ` WHERE card_name = $1;` + res, err := db.Exec(sqlStatement, card_name) + if err != nil { + return err + } + + // insert a new record into cards + + sqlStatement = `INSERT INTO cards` + + ` (one_time_code, k0_auth_key, k2_cmac_key, k3, k4, uid, last_counter_value,` + + ` lnurlw_request_timeout_sec, tx_limit_sats, day_limit_sats, lnurlw_enable,` + + ` one_time_code_used, card_name, uid_privacy, allow_negative_balance,` + + ` pin_enable, pin_number, pin_limit_sats)` + + ` VALUES ($1, $2, $3, $4, $5, '', 0, 60, $6, $7, $8, 'N', $9, $10, $11, $12, $13, $14);` + res, err = db.Exec(sqlStatement, one_time_code, k0_auth_key, k2_cmac_key, k3, k4, + tx_limit_sats, day_limit_sats, lnurlw_enable_yn, card_name, uid_privacy_yn, + allow_neg_bal_yn, pin_enable_yn, pin_number, pin_limit_sats) + if err != nil { + return err + } + count, err := res.RowsAffected() + if err != nil { + return err + } + if count != 1 { + return errors.New("not one card record inserted") + } + + return nil +} + func Wipe_card(card_name string) (*Card_wipe_info, error) { card_wipe_info := Card_wipe_info{} @@ -934,7 +1008,8 @@ func Wipe_card(card_name string) (*Card_wipe_info, error) { return &card_wipe_info, nil } -func Update_card(card_name string, lnurlw_enable bool, tx_limit_sats int, day_limit_sats int) error { +func Update_card(card_name string, lnurlw_enable bool, tx_limit_sats int, + day_limit_sats int) error { lnurlw_enable_yn := "N" if lnurlw_enable { @@ -970,3 +1045,91 @@ func Update_card(card_name string, lnurlw_enable bool, tx_limit_sats int, day_li return nil } + +func Update_card_with_pin(card_name string, lnurlw_enable bool, tx_limit_sats int, day_limit_sats int, + pin_enable bool, pin_number string, pin_limit_sats int) error { + + lnurlw_enable_yn := "N" + if lnurlw_enable { + lnurlw_enable_yn = "Y" + } + + pin_enable_yn := "N" + if pin_enable { + pin_enable_yn = "Y" + } + + db, err := open() + + if err != nil { + return err + } + + defer db.Close() + + sqlStatement := `UPDATE cards SET lnurlw_enable = $2, tx_limit_sats = $3, day_limit_sats = $4, ` + + `pin_enable = $5, pin_number = $6, pin_limit_sats = $7 WHERE card_name = $1 AND wiped = 'N';` + + res, err := db.Exec(sqlStatement, card_name, lnurlw_enable_yn, tx_limit_sats, day_limit_sats, + pin_enable_yn, pin_number, pin_limit_sats) + + if err != nil { + return err + } + + count, err := res.RowsAffected() + + if err != nil { + return err + } + + if count != 1 { + return errors.New("not one card record updated") + } + + return nil +} + +func Update_card_with_part_pin(card_name string, lnurlw_enable bool, tx_limit_sats int, day_limit_sats int, + pin_enable bool, pin_limit_sats int) error { + + lnurlw_enable_yn := "N" + if lnurlw_enable { + lnurlw_enable_yn = "Y" + } + + pin_enable_yn := "N" + if pin_enable { + pin_enable_yn = "Y" + } + + db, err := open() + + if err != nil { + return err + } + + defer db.Close() + + sqlStatement := `UPDATE cards SET lnurlw_enable = $2, tx_limit_sats = $3, day_limit_sats = $4, ` + + `pin_enable = $5, pin_limit_sats = $6 WHERE card_name = $1 AND wiped = 'N';` + + res, err := db.Exec(sqlStatement, card_name, lnurlw_enable_yn, tx_limit_sats, day_limit_sats, + pin_enable_yn, pin_limit_sats) + + if err != nil { + return err + } + + count, err := res.RowsAffected() + + if err != nil { + return err + } + + if count != 1 { + return errors.New("not one card record updated") + } + + return nil +} 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/FAQ.md b/docs/FAQ.md index ef30e43..350ef87 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -25,3 +25,12 @@ It can be useful to test paying invoices directly from your lightning node. # Can I use the same lightning node for the customer (bolt card) and the merchant (POS) ? 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 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 8275fcb..9c1d248 100644 --- a/docs/TECHNOLOGY.md +++ b/docs/TECHNOLOGY.md @@ -4,6 +4,9 @@ | --- | --- | | [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 | 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 ec4834a..e0e1eb3 100644 --- a/internalapi/createboltcard.go +++ b/internalapi/createboltcard.go @@ -1,8 +1,6 @@ package internalapi import ( - "crypto/rand" - "encoding/hex" "github.com/boltcard/boltcard/db" "github.com/boltcard/boltcard/resp_err" log "github.com/sirupsen/logrus" @@ -11,16 +9,7 @@ import ( "strings" ) -func random_hex() string { - b := make([]byte, 16) - _, err := rand.Read(b) - if err != nil { - log.Warn(err.Error()) - return "" - } - - return hex.EncodeToString(b) -} +// random_hex() from Createboltcardwithpin used here func Createboltcard(w http.ResponseWriter, r *http.Request) { if db.Get_setting("FUNCTION_INTERNAL_API") != "ENABLE" { diff --git a/internalapi/createboltcardwithpin.go b/internalapi/createboltcardwithpin.go new file mode 100644 index 0000000..64d9efd --- /dev/null +++ b/internalapi/createboltcardwithpin.go @@ -0,0 +1,153 @@ +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" +) + +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") + url := "" + if strings.HasSuffix(hostdomain, ".onion") { + url = "http://" + hostdomain + "/new?a=" + one_time_code + } else { + url = "https://" + hostdomain + "/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) +} diff --git a/internalapi/getboltcard.go b/internalapi/getboltcard.go index aef2847..0103752 100644 --- a/internalapi/getboltcard.go +++ b/internalapi/getboltcard.go @@ -37,7 +37,9 @@ func Getboltcard(w http.ResponseWriter, r *http.Request) { `"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) + `"}`) + `"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) diff --git a/internalapi/updateboltcardwithpin.go b/internalapi/updateboltcardwithpin.go new file mode 100644 index 0000000..26ab45a --- /dev/null +++ b/internalapi/updateboltcardwithpin.go @@ -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) +} diff --git a/lnd/lnd.go b/lnd/lnd.go index b16fe27..34cd9ad 100644 --- a/lnd/lnd.go +++ b/lnd/lnd.go @@ -84,6 +84,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)) @@ -101,6 +105,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 { @@ -123,6 +128,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"), @@ -132,7 +142,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{ @@ -231,10 +241,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/lnurlw/lnurlw_callback.go b/lnurlw/lnurlw_callback.go index b5d5a61..de8c74f 100644 --- a/lnurlw/lnurlw_callback.go +++ b/lnurlw/lnurlw_callback.go @@ -56,13 +56,13 @@ func lndhub_payment(w http.ResponseWriter, p *db.Payment, bolt11 decodepay.Bolt1 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(err) + 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(err) + log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn("login:password badly formed") resp_err.Write(w) return } @@ -72,7 +72,6 @@ func lndhub_payment(w http.ResponseWriter, p *db.Payment, bolt11 decodepay.Bolt1 lhAuthRequest.Password = card_name_parts[1] authReq, err := json.Marshal(lhAuthRequest) - log.Info(string(authReq)) req_auth, err := http.NewRequest("POST", lndhub_url+"/auth", bytes.NewBuffer(authReq)) if err != nil { @@ -101,6 +100,9 @@ func lndhub_payment(w http.ResponseWriter, p *db.Payment, bolt11 decodepay.Bolt1 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) @@ -225,17 +227,15 @@ func 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"] + // get k1 value + param_k1 := req.URL.Query().Get("k1") - if !ok || len(params_k1[0]) < 1 { + if param_k1 == "" { log.WithFields(log.Fields{"url": url}).Debug("k1 not found") resp_err.Write(w) 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) @@ -263,14 +263,14 @@ func Callback(w http.ResponseWriter, req *http.Request) { return } - params_pr, ok := req.URL.Query()["pr"] - if !ok || len(params_pr[0]) < 1 { + // 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 } - param_pr := params_pr[0] bolt11, _ := decodepay.Decodepay(param_pr) // record the lightning invoice @@ -283,6 +283,23 @@ func Callback(w http.ResponseWriter, req *http.Request) { log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Debug("checking payment rules") + // 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 { diff --git a/lnurlw/lnurlw_request.go b/lnurlw/lnurlw_request.go index 50bc025..eb0b15f 100644 --- a/lnurlw/lnurlw_request.go +++ b/lnurlw/lnurlw_request.go @@ -14,15 +14,6 @@ import ( "strings" ) -type ResponseData struct { - Tag string `json:"tag"` - Callback string `json:"callback"` - LnurlwK1 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] @@ -255,6 +246,7 @@ func parse_request(req *http.Request) (int, error) { func Response(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) @@ -312,15 +304,29 @@ func Response(w http.ResponseWriter, req *http.Request) { return } - defalut_description := db.Get_setting("DEFAULT_DESCRIPTION") + // get pin_enable & pin_limit_sats - response := ResponseData{} - response.Tag = "withdrawRequest" - response.Callback = lnurlw_cb_url - response.LnurlwK1 = lnurlw_k1 - response.DefaultDescription = defalut_description - response.MinWithdrawable = min_withdraw_sats * 1000 // milliSats - response.MaxWithdrawable = max_withdraw_sats * 1000 // milliSats + 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) diff --git a/main.go b/main.go index 4390985..d4e7148 100644 --- a/main.go +++ b/main.go @@ -53,7 +53,9 @@ func main() { 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) diff --git a/script/s_create_db b/script/s_create_db index 1e0b7d8..aecb7b6 100755 --- a/script/s_create_db +++ b/script/s_create_db @@ -15,7 +15,7 @@ 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 + psql postgres -f sql/settings.sql.secret echo Database created else echo No action diff --git a/script/s_setup_test_data b/script/s_setup_test_data new file mode 100755 index 0000000..7426df8 --- /dev/null +++ b/script/s_setup_test_data @@ -0,0 +1,6 @@ +# to close any database connections +sudo systemctl stop postgresql +sudo systemctl start postgresql + +psql postgres -f sql/data.test.sql +echo Test data added diff --git a/sql/create_db.sql b/sql/create_db.sql index 03c80f7..9cd6135 100644 --- a/sql/create_db.sql +++ b/sql/create_db.sql @@ -28,6 +28,9 @@ CREATE TABLE cards ( 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) ); diff --git a/sql/data.test.sql b/sql/data.test.sql new file mode 100644 index 0000000..cae568a --- /dev/null +++ b/sql/data.test.sql @@ -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); 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');