This is a follow-up question to a previous one, trying to get a working decryption of NDEF URLs from an NXP NTAG424 tag according to this documentation from NXP (4.4.2.1).
I've put together an initial WebCrypto code based on @Topaco 's excellent suggestions and the code now succesfuly decodes the picc
and enc
from the response and verifies the CMAC.
However, this only works for some payloads, while others still throw a bad decrypt
error. Iv'e isolated the issue to the decryption of the enc
static data payload using the derived keys from sv1
, search for encDataFull
in the reference code.
The complete implementation follows a reference in Python which is also the backend for the online validation service I refer to later on: https://github.com/nfc-developer/sdm-backend/blob/master/libsdm/sdm.py
The only difference is the use of AES-CBC
vs. AES-ECB
in encoding the encIVFull
, since AES-ECB
is not supported by WebCrypto.
Following this post, I understood that an AES-CBC
with an IV of 0s would be equal to AES-ECB
since only the first block in needed:
const encIV = Buffer.from(encIVFull).subarray(0, 16)
Reference for ECB / CBC being the same for IV of zeros: CBC Example ECB Example
some payloads work and some fail. This is despite the fact that they all pass verification on an online utility (provided as well)
Working payload (link to online validation):
// https://sdm.nfcdeveloper.com/tagtt?_____TRIAL_VERSION______NOT_FOR_PRODUCTION_____&picc_data=FDE4AFA99B5C820A2C1BB0F1C792D0EB&enc=94592FDE69FA06E8E3B6CA686A22842B&cmac=C48B89C17A233B2C
const KEY = '00000000000000000000000000000000'
const PICC = 'FDE4AFA99B5C820A2C1BB0F1C792D0EB'
const ENC = '94592FDE69FA06E8E3B6CA686A22842B'
const CMAC = 'C48B89C17A233B2C'
const CMAC_ARG = 'cmac'
returns
{"dataTag":"c7","uid":"04958CAA5C5E80","cnt":"010000","cntInt":1,"cmacPass":true,"data":"78787878787878787878787878787878"}
Failing payload (link to online validation):
// https://sdm.nfcdeveloper.com/tagtt?_____TRIAL_VERSION______NOT_FOR_PRODUCTION_____&picc_data=6107DD7607B179270EAFFCA2F0911940&enc=37C1E399E0948BEA54138F92DDD1E743&cmac=0406016621FC6AC6
const KEY = '00000000000000000000000000000000'
const PICC = '6107DD7607B179270EAFFCA2F0911940'
const ENC = '37C1E399E0948BEA54138F92DDD1E743'
const CMAC = '0406016621FC6AC6'
const CMAC_ARG = 'cmac'
Fails with bad decrypt
Another failing payload (link to online validation):
// https://sdm.nfcdeveloper.com/tagtt?_____TRIAL_VERSION______NOT_FOR_PRODUCTION_____&picc_data=FD91EC264309878BE6345CBE53BADF40&enc=CEE9A53E3E463EF1F459635736738962&cmac=ECC1E7F6C6C73BF6
const KEY = '00000000000000000000000000000000'
const PICC = 'FD91EC264309878BE6345CBE53BADF40'
const ENC = 'CEE9A53E3E463EF1F459635736738962'
const CMAC = 'ECC1E7F6C6C73BF6'
const CMAC_ARG = 'cmac'
Would appreciate any further insight on how to make the decryption code more robust.
The bug is an incorrect calculation of the fake padding block in the workaround. You are using the final IV as IV instead of the
ENC
ciphertext, which is wrong.The wrong IV results in a wrong padding block. When decrypting the entire ciphertext, this produces internally random-like bytes at the end of the plaintext, which generally correspond to an incompatible PKCS#7 padding (as a reminder: PKCS#7 is the padding used by WebCrypto and cannot be disabled).
This then triggers a
bad decrypt
(as in your two failure cases). By chance, however, a compatible padding can also result so that nobad decrypt
is generated (as in your success case), but the data is generally still corrupted by the subsequent processing due to the incorrect (but compatible) padding bytes.Below is a reminder of how the workaround from this post works: If you have a plaintext in which the last block
pt_block_last
is completely filled, a complete blockpt_padding
consisting of 16 0x10 values is automatically appended when (PKCS#7) padding is enabled, which is encrypted with AES/CBC, wherebyct_block_last
is used as IV. The latter is the case because CBC applies the ciphertext of the previous blockct_block_n-1
as IV when encrypting a plaintext blockpt_block_n
, see CBC flowchart.The workaround does exactly that explicitly: When encrypting an empty block, the enabled (PKCS#7) padding automatically generates a complete padding block consisting of 16 0x10 values. By using the last block of the
ENC
ciphertext (or in the examples the entireENC
ciphertext, as this is only 1 block in size) as IV, encryption generates a ciphertext block that contains the encrypted padding and matches theENC
ciphertext.The concatenation of
ENC
and that ciphertext block produces a two-block ciphertext that can be decrypted with WebCrypto when padding is enabled.Test:
Your implementation already generates the correct final IV and key for decryption using CMAC. I calculated these two values with your code and use them to test the fixed decryption code below:
All three
ENC
ciphertexts are now correctly decrypted.