Apple Pay Transactions
Ian Whiffin
Posted: 8th February 2020

Apple Pay is something I have been using on my phone for well over a year now; pretty much since it was launched in Canada. More recently, I have been using Apple Watch to also make purchases. I find both to fantastically convienient, especially for someone like myself who leaves my wallet in the house or office way too often.

I recently noticed that on all the phones I have examined, I have never seen Apple Pay transactions. Could it be that there is only me using this service? Or do my usual tools not parse this data?

So I extracted my own phone and armed with the knowledge of where I spend my money, I began looking into how my Apple Pay stores transactional data.


The Passes23.sqlite database, located at /private/var/mobile/Library/Passes/passes23.sqlite appears to be the main Apple Wallet database on the iPhone and stores information such as the various payment cards associated to the account as well as store cards, flight boarding cards etc. This blog post focuses on the payment transaction side of the database only.

The database consists of numerous tables, but the tables related to payment transactions are:

maps_merchant A list of common locations used for purchases. Includes GPS data and postal address information.
payment_application This appears related to the cards associated to the account.
payment_message This contains the messages received from Apple Pay such as "Card X is ready for Apple Pay" when setting a card up for use.
payment_transaction A list of transactions. More about this later.
plantains (!?) - A rather oddly titled table (along with the other oddly named "Papaya" which in my test device was empty). This table contains more merchant information.

As you can see, there is actually not a high number of tables related to purchases. And the only one I really care about is the payment_transactions table.

The most important fields as far as I'm concerned are:

payment_application_pid Related to the payment_application table. Defines which card was used to make the payment.
currency_code The type of currency.
amount The amount of the transaction - recorded in smallest units. So a value of 100 would be 100 cents / 1 dollar. No decimals are used.
locality / administrative_area Approximate location information.
transaction_date The time the transaction occured. Recorded in Mac Absolute time (Seconds since Jan 1st 2001).
location_date Also the time the transaction occured in Mac Absolute time. This timestamp is slightly different to the transaction date; but only ~10 seconds or so different.
location_latitude / location_longitude GPS coordinates of the merchant.
merchant_name The name of the merchant.
merchant_industry_category The merchant type; such as RETAIL or RESTURANT.
maps_merchant_pid Relates to the maps_merchant table to give the address information of the merchant.


On my device, there was only 12 records (which I know to be waaaay lower than the number of transactions made). But I also noticed that that although the records went back several months, there was only one transaction per merchant. So overall, pretty disappointing. Not the level of data I was hoping for.

So I continued to search...


During my digging, I found a cache database located at /private/var/mobile/Library/Caches/ which contained similar information to Passes23.sqlite.

After extracting the database and WAL file, I quickly searched both in a hex viewer and found multiple references to the stores I had visited. Awesome!

I then opened the database up in a SQL app to view the database structure;

cfurl_cache_blob_data Contains lots of bplists as blobs; but from what I can see, the data is either irrelevant or encoded.
cfurl_cache_reciever_data most of the blobs in this table appeared irrelevant. But two records contained relevant, readable JSON data.
cfurl_cache_response Appears to contain payment card information such as the card image.

The two records I found was vastly under what I expected to find compared to what I'd seen in hex. Opening the db file again, I now found hardly any matches and he WAL file had now gone. I repeated the above and confirmed what I had seen and assume that garbage collection was occuring once the SQL file was opened.

A new tactic was needed.

I had 2 full JSON records that were sucessfully extracted from the database:

As you can see, the records I had contained the string {"transactionDetails":[{. I used this as a starting point to carve the database & WAL file manually.

Many records were found, but of course, the BLOB's were all different lengths and calculating the BLOB length caused a couple of issues.

I had assumed that the length was defined in a similar way to how I was able to carve data from KnowledgeC. In that case, the length was defined using a Varint calculated using a method described in my KnowledgeC blog post. In this database though, the length was not working out the same way, but it was very similar.

What I found was that this database is actually the standard way SQLite3 records the length of BLOB data, it is the KnowledgeC method which is unique.

As an example, I had the following information at the start of the extracted bplist:

You can see that the file starts with {"transactionDetails":[{. I was able to find this in Cache.db and as expected (Red), it is immidiately preceeded by the length of the blob (Green). That would mean that the length was was x2E (46) bytes which was clearly wrong. So I made the assumption that more than one byte was required to define the length and that I had to go back a further byte to 8A (Orange) to begin the calculation.

First byte of length 8A
First byte in Binary.
The first bit signifies that a second byte is required to express the full number.
1000 1010
First and second bytes 8A 2E
Converted to Binary
The first bit of Byte 2 signfies that a third byte is not required.
1000 1010 0010 1110
Lose the first bit of each byte. They have served their purpose. 000 1010 010 1110
Remove the last bit of byte 1 and append it to the start of byte 2 000 101 0010 1110
Convert the binary back to Hex 5 2E
Concatenate the values 52E
Convert the hex to Decimal 1326
Subtract 12 1314
Divide by 2 657

So the length of the Blob is 657 bytes which I knew to be correct by measuring the bplist itself..

Quickly calculate VARINT values here :
Answer :

Once I had this figured out, I was able to carve well over 100 records from the database & WAL file (including duplicates).

Each of the JSON records was structured as

Note that "transactionTimestamp" is in UNIX time.

I parsed each of the JSON objects and plucked out the fields I cared about (primarily the date, merchant, amount, currency and industry; along with additional information like GPS coordinates if present), applied a little logic to deduplicate (based on the transaction time) and found I was left with 43 records. A fair fall from 100+ but still more than two.

Bonus Points

As an additional bonus, I found that Apple Pay transactions that occur via a users Apple Watch are logged in the private/var/mobile/Library/DeviceRegistry/***/NanoPasses/nanopasses.sqlite3 database.

*The characters *** are used in the path as a replacement to a device ID which is unique to the device. Ie. if the unique ID of my watch was 123456 then the path would be private/var/mobile/Library/DeviceRegistry/123456/NanoPasses/nanopasses.sqlite3

This small database contained the following tables:

pass A list of the cards/passes associated to the account.
transactions A list of transactions which are stored as BLOBs within the table.

*First of all let me say how awesome (#sarcasm) it was to find YET ANOTHER way for apple to store this data. I honestly don't know if this is done to complicate extraction, because it makes sense programatically or because it's different people doing different modules and they have zero consistancy.

Anyhow, I extracted the BLOB bplist's and passed them to MUSHY for parsing. MUSHY dll still isn't quite ready for wide spread release like I would have liked but it did the job quite nicely and allowed my to fairly easily pull the data I needed.

The bplist stored data in a fairly typical way; So when you find the node named "Merchant" you find a pointer value you must go to.
In the image below, you see that "Merchant" has a value of 11.

So you must goto node 11 where you find lots of information relevant to the merchant.

Trying to view the displayName shows you a UID of 13. So you now have to go to node 13 where you find...

And so on.

The amount is stored in Node 2

The Mantissa is the number we really care about. Although presumably the NS.Negative would be true for a refund?

The interesting thing about the Mantissa I found was that it has a maximum of 128 bits. That may be required for the likes of Tim Cook, but I'm sure for normal folks, an Int32 would have been more than enough. Heck, I would guess that with the limitations on contactless payments a single byte would have probably been enough.

The value is Little Endian, so the value 40 0A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 is actually 0A 40; more commonly referred to as 2624. Even more commonly referred to as $26.24.

The transaction timestamp is stored both in the SQL table and at node 8 of the bplist. In both cases, the time is listed in Mac Absolute.

Most interestingly, the 18 transactions on my watch were not on my phone. And none of the records from my phone were present on the watch. Good to know that they host totally seperate databases and don't share data...

Wrapping Up

In the end, I found that if I extracted data from passes23.sqlite, Cache.db and nanopasses.sqlite3 and deduplicated the records, i had 57 unique payment transactions that I didn't have at the start.

For completeness, I ran my phone extraction in the two heavyweight tools in Digital Forensics (I won't name names but I think you all know who I mean) and got a combined total of 0 records.

Thank you for reading! Hopefully you can use this information to help with your own cases.

ArtEx has been updated to include Apple Pay transactions from all the above named databases and can be download FREE from the 'Software' section of my site.

Previous Article
"A Hundred Million TimeStamps"
Next Article
"So Long Lockdown!"