Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transfer Cards V2 #709

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open

Transfer Cards V2 #709

wants to merge 2 commits into from

Conversation

michielderoos
Copy link
Contributor

@michielderoos michielderoos commented Jun 1, 2021

Transfer Cards V2

Presently there is no way to check transfer account limits offline. We need to add the ability to set limits which work completely when offline, and which match the logic we employ online so transactions don't fail at synchronization time. This is all based on the Mifare Ultralight EV1

Design Considerations

  • We do not want end users to be allowed to alter their own limits
  • Limits should be mutable on the backend, and synchronized to the cards as soon as possible
  • Limits must be uniformly applied in both offline, and online flow
  • [NEW] Cards should be reusable

Risks

  • A limited card could need need the contents of the cards to be different. This could present compatibility challenges if we want to move old cards to the new system.
    • A way to mitigate this would be to ignore the requirement for V1/V2 compatibility and start fresh on a new deployment. The app be altered to recognize V1 cards and run them in a compatibility mode using the old logic (without limits), but not allow migration to the new system.
  • Any solution will inevitably use more storage on the cards, so we have to make sure all of the cards we use have enough storage space

Per-Card Limits

To implement per-card limits, we need to store two things on the card: What the limit is, and where the user currently falls within that limit.

High-Level Goals

  • Be small. We want to use as little storage as possible. The smaller each limit is, the more limits we can actually impose
  • Be secure. We don't want a user with nfctools installed to be able to override their limits when dealing with offline vendors. Our model already relies on trusting the vendor-side to sign transactions, and we should be able to apply a similar model here as well.
  • Be accountable. It should be detectable if the limits are somehow mutated on the cards, and by whom. The backend should be able to look at when/where/how a limit was exceeded once the client data becomes synchronized
  • Be current. There should be version safety-- if limits change, they should be synchronized with the cards as soon as possible. Furthermore, an out-of-date offline vendor should not be able to accidentally update a card with out-of-date filter data.

Stored Items

To make this work, there's a few different data types we need to consider and keep track of

  • Limit types. A limit could be anything from "number of transactions allowed in a time period" or "dollar-value of allowed transactions per period"
  • The limits themselves. If we established a limit type, for example here "transactions per month", we need the actual limit value (I.e. 30 transactions per month)
  • Progress. The card would also have to hold where the user falls within that limit.
  • Version. To make sure the limits on the card are up-to-date, we need to store the current version of the limit-string being used. This string will only increment when the limit increases on the backend.
  • Hash. This is a 20 byte hash of the contents of the stored items, as well as the balances.

Sizes

Note: We don't have to worry about storing the balance, since the balance is already stored

  • Total storage: 48 Bytes (user accessible data fields)
  • sha1 Hash: 20 Bytes (hash)
  • last updated: 2 Byte (unsinged integer). This is the number of days since the start of the program (organisation creation time)
  • currency amount: 3 Bytes (unsigned integer). This is based on the fact that we use the 24 bit (3 byte) counter on the NFC cards-- the maximum theoretical balance a person can have is 16777215. As such, I think we should be able to standardize on 8 bytes to represent currency in our limits code as well.
  • count: 2 Bytes (unsigned integer). This will be used to represent the count in count-based limits (I.e. 'Allowed transactions per month') This can represent numbers up to 65535, which should more than facilitate any reasonable transaction limits we want to impose
  • version: 1 Byte (unsigned integer). The current version of limits imposed on a card. Using only one byte since it's unlikely that we'll change an individual card's transfer limits more than 255 times
  • limit type: 1 byte. This is how much storage we allocate for a given limit type. More on this later!

Schema

Now that we've established what we want to do, how large each component is, and how much space we have to work with, we need to consider our data schema. How much do we want to arrange these elements on the card to convey meaning? First we should establish limit types.

limit types:
For limit types, something can either be a count limit, which is like "number of transactions allowed per week", or a value limit, which would be "how much you're allowed to spend in a week". Therefore, for this piece of information we can allocate just one bit.

Limit Type Value
Count Limit 0
Value Limit 1

Next we want to think of timelines on which we might want to set these limits. Daily, Weekly, Biweekly (every two weeks), Monthly, Bimonthly (every two months), Quarterly, Yearly. This is 7 bits, so it fits nicely in an unsigned 3 bit integer. With a one bit limit-type, and 7 bits for timespan types, the entire limit type fits neatly into an into a single byte.

Timepsan Value
Daily 1
Weekly 2
Biweekly 3
Monthly 4
Bimonthly 5
Quarterly 6
Yearly 7

Therefore, a weekly value limit would be written as 10000010, where the first 1 connotes that it's a value limit, followed by 0000010 (or 2), connoting weekly!

                   Limit Type
   ┌────────────┬──────────────────────────────┐
   │    Type    │         Timespan             │
   │   (1 Bit)  │         (7 Bits)             │
   │      1     │       (2) 0000010            │
   └────────────┴──────────────────────────────┘

limit type formats
You may have noticed in sizes that count is half the size of currency amount. This is because the highest number of transactions we're ever going to want to limit is likely much smaller than the highest amount of currency, and we want to save as much space as possible. One thing this does though, is forces us to be a bit more careful with memory allocation. Since a Count Limit will use less overall space than a Value Limit, therefore when parsing our memory we have to be a bit more clever than to just chop up the string by bits!
For Count Limit, the first 8 bits are going to be the limit type itself (see above), followed by two bytes for the limit count (number of transactions allowed), and another two bytes for how much of that count has already been consumed. The exact same applies for Value Limit types, but the next two sections of memory will be 3 bytes long (to connote currency rather than a count). The below example is a continuation of above, but will represent "A weekly value limit of 500, 250 has already been spent this period"

       Limit Object (5 to 7 Bytes)
 ┌───────────┬──────────────┬──────────────┐
 │ Limit Type│ Limit Value  │  Limit Used  │
 │  (8 Bits) │(2 or 3 Bytes)│(2 or 3 Bytes)│
 │(see above)│   (500)      │    (250)     │
 │  10000010 │0...111110100 │ 0...11111010 │
 └───────────┴──────────────┴──────────────┘

last updated:
The entire concept of limits in this system is based on having a non-rolling timeline in which there's restrictions placed on cardholders' spending activities. That is to say that the limits are essentially based on a "calendar week". I.e. you can spend $5 on Monday and $5 on Friday, that will not exceed your limit, but the counter will be reset Sunday at Midnight-- you can spend $10 the following Monday even though it's within a week from the $5 on Friday). The way this works is to have the "limit used" section in the "Limit Types" memory block.
When a cardholder transacts with a vendor, they'll check the "last updated" block of memory. This is the number of days since the start of the program since the vendor last used their card. If (for a weekly-limit) that date is in the same week as the current week, then check their transaction against the limit data on the card as they exist at the present. If the date is in a subsequent week, zero-out all the "limit used" sections in the cards' memory and work from there. If the "last updated" date is higher than the current date, raise alarms since that implies tampering with the card by the previous vendor!

hash
Each individual phone will have its own key (given by the backend, referred to from here on as "phone key"), as well as a shared key which is set on an organisation-level which will live on every vendor phone ("organisation key"). When a transaction happens, the entire contents of the card (including balances, excluding hashes) will be fed into a hash function twice-- once with the phone-key appended to the message (let's call this the "vendor hash"), and again with the organisation key (let's call this the "organisation hash"). Then the second 10 bytes of the vendor hash, and the first ten bytes of the organisation hash will be concatenated, and that will be the hash which will be written to the card.
At read-time, the entire card contents will fed through the hash function too, but this time only using the organisation key. Then the validity of the hash will be checked against the second ten bytes of the hash on the card. The reason for only half of the hash being used to verify at read-time is that vendors don't know each other's phone-keys.
When transactions are synchronized, the app is to send both the pre-transaction and post-transaction card contents to the backend along with the transaction data. This is to ensure that the entire state of the cards in the wild are exactly what we expect them to be. This would be done through the backend checking the hashes by testing it using both the phone key of the vendor reporting the transaction, as well as the organisation key.

                 Hash
               20 Bytes
 ┌──────────────────────────────────────┐
 │                                      │
┌┴─────────────────┬────────────────────┴┐
│ Phone-key signed │   Org-key signed    │
│   (10 Bits)      │     (10 Bits)       │
│    (random)      │     (random)        │
│101......100111011│100...110101011101011│
└──────────────────┴─────────────────────┘

This also comes with the benefit of knowing how much has been spent on the cards which hasn't been synchronized yet (I.e. if I spend at at vendor 1, 2, 3, and 4, then vendor 4 synchronizes with the backend, I can infer how much was cumulatively spent at vendors before).
There are a few reasons for this somewhat convoluted setup:

  • If the organisation key, is compromised, an attacker would still have to make up their own phone key. They will still be able to make unauthorized alterations to cards and allow them to transact with other offline vendors, but at sync-time, this will be immediately noticed, since both phone key-based, and org key-based hashes will be checked on the backend. This will also point us to exactly who the attacker is-- as soon as a non-compromised phone transacts with a card that a previous phone put into an invalid state, the customer knows for sure that the previous vendor was compromised.
  • If both an organisation key and a phone key are compromised, that will be detectable as soon as the card transacts with a vendor whose phone-key has not been compromised as well. Even if they have a compromised version of the app (lets say it writes one thing to the card, and then lies to the backend about what the card state actually is), any misuse of the key will be detected by the backend when a card interacts with a non-compromised vendor and that vendor syncs with the backend.
  • The per-phone key also serves a purpose.

It should be made clear, this system places trust in the hands of the vendors. The main design goal is to ensure that if somebody isn't using the system as designed, we'll be able to identify who they are.

versioning
We want to be able to make sure that each card has the newest version of its limits. That is, if the backend wants to adjust a user's limits (or set their initial limits), the system should make sure that they're put in place as soon as a cardholder interacts with a recently synchronized mobile app. Do do this, I propose we use a single byte integer stored on the card to indicate the "limit version" the user is currently on. The new limits data for all cardholders will be synchronized to the app with the transfer_card endpoint which we already use for things like balance data, and when an out-of-date or unlimited card interacts with an up-to-date version of the app, the new limits will be installed onto that card. The 1 byte integer would allow up to 255 versions, which I think should be enough for most applications given that limits do not change often.

Putting it all together!

Now that we've gone through the contents of the card, let's go through a practical example!
The parts we need are

  • 20 byte hash
  • 1 byte version
  • 2 byte "last updated"

This brings us to 23 bytes used by a card with no limits, so on a 48 byte card we're left with 25 bytes for limits. With limits being either 5 or 7 bytes, we can have between 3 and 5 limits depending on whether they're count-limits or amount-limits.



                                                             35 Byte total card payload
  ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
  │                                                                                                                                                              │
  │                                                        Limit 1                                   Limit 2                       Version         Last Updated  │
  │               Hash                         Cardholder can spend $500 per week        Cardholder can spend 5 times per week    Version 2     2 Days from epoch│
  │             20 Bytes                                   7 Bytes                                   5 Bytes                       1 Byte            2 Bytes     │
  ├──────────────────────────────────────┐  ┌───────────────────────────────────────┐ ┌───────────────────────────────────────┐ ┌────────────┐ ┌─────────────────┤
  │                                      │  │                                       │ │                                       │ │            │ │                 │
 ┌┴─────────────────┬────────────────────┴┬─┴──────────┬──────────────┬─────────────┴┬┴──────────┬──────────────┬─────────────┴┬┴────────────┴┬┴─────────────────┴┐
 │ Phone-key signed │   Org-key signed    │ Limit Type │ Limit Value  │  Limit Used  │ Limit Type│ Limit Value  │  Limit Used  │   Version    │  Last Updated     │
 │   (10 Bits)      │     (10 Bits)       │  (8 Bits)  │  (3 Bytes)   │  (3 Bytes)   │  (8 Bits) │  (2 Bytes)   │   (2 bytes)  │   (1 Byte)   │    (2 Bytes)      │
 │    (random)      │     (random)        │(dollars/wk)│   (500)      │    (250)     │(txns/week)│     (5)      │     (1)      │     (2)      │       (2)         │
 │101......100111011│100...110101011101011│  10000010  │0...111110100 │ 0...11111010 │  00000010 │0...000000101 │ 0...00000001 │   00000010   │   000...00000010  │
 └──────────────────┴─────────────────────┴────────────┴──────────────┴──────────────┴───────────┴──────────────┴──────────────┴──────────────┴───────────────────┘

@michielderoos michielderoos changed the title version Transfer Cards V2 Jun 1, 2021
@codecov
Copy link

codecov bot commented Jun 1, 2021

Codecov Report

Merging #709 (40270c7) into master (7e0c670) will decrease coverage by 0.00%.
The diff coverage is n/a.

@@            Coverage Diff             @@
##           master     #709      +/-   ##
==========================================
- Coverage   47.53%   47.52%   -0.01%     
==========================================
  Files         373      373              
  Lines       19391    19391              
  Branches     1784     1784              
==========================================
- Hits         9217     9216       -1     
- Misses      10159    10160       +1     
  Partials       15       15              
Flag Coverage Δ
javascript 4.51% <0.00%> (ø)
python 73.17% <0.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
app/server/utils/mock_data.py 78.48% <0.00%> (-0.64%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7e0c670...40270c7. Read the comment docs.

@michielderoos michielderoos changed the title Transfer Cards V2 Transfer Cards V2 (WIP) Jun 1, 2021
@michielderoos michielderoos changed the title Transfer Cards V2 (WIP) Transfer Cards V2 Jun 4, 2021
@michielderoos
Copy link
Contributor Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant