Skip to content

Commit

Permalink
Support plaintext UID and read counter mirroring (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
icedevml committed Aug 22, 2022
1 parent f692212 commit b6a52b3
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 8 deletions.
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,44 @@ If you are looking for a complete solution for tag configuration and management,
Use NXP's TagWriter application for Android. When writing an URL record, choose "Configure mirroring options". Refer to the tag's datasheet to understand particular options/flags.

## Supported cases
### UID and Read Counter Plaintext mirroring
**Example:**
```
http://myserver.example/tagpt?uid=041E3C8A2D6B80&ctr=000006&cmac=4B00064004B0B3D3
```

**Proposed SDM Settings for TagWriter:**
* [X] Enable SDM Mirroring (SDM Meta Read Access Right: `0E`)
* [X] Enable UID Mirroring
* [X] Enable Counter Mirroring (SDM Counter Retrieval Key: `0F`)
* [ ] Enable Read Counter Limit (Derivation Key for CMAC Calculation: `00`)
* [ ] Encrypted File Data Mirroring

**Input URL:**
```
http://myserver.example/tagpt?uid=00000000000000&ctr=000000&cmac=0000000000000000
```

**UID Offset:**
```
http://myserver.example/tagpt?uid=00000000000000&ctr=000000&cmac=0000000000000000
^ UID Offset
```

i.e.: in TagWriter, set the cursor between `uid=` and the first `0` when setting offset.

**Counter Offset:**
```
http://myserver.example/tagpt?uid=00000000000000&ctr=000000&cmac=0000000000000000
^ Counter Offset
```

**SDMMACInputOffset/SDMMACOffset:**
```
http://myserver.example/tagpt?uid=00000000000000&ctr=000000&cmac=0000000000000000
^ CMAC Offset
```

### PICCData Encrypted mirroring (`CMACInputOffset == CMACOffset`)
**Example:**
```
Expand All @@ -80,7 +118,7 @@ http://myserver.example/tag?picc_data=00000000000000000000000000000000&cmac=0000
^ PICCDataOffset
```

i.e.: in TagWriter, set the cursor between `=` and `0` when setting offset.
i.e.: in TagWriter, set the cursor between `=` and the first `0` when setting offset.

**SDMMACInputOffset/SDMMACOffset:**
```
Expand Down Expand Up @@ -117,6 +155,16 @@ http://myserver.example/tagtt?picc_data=FDD387BF32A33A7C40CF259675B3A1E2&enc=EA0

First two letters of `File data (UTF-8)` will describe TagTamper Status (`C` - loop closed, `O` - loop open, `I` - TagTamper not enabled yet).

### Notice about keys
In the examples above, whenever key `00` is mentioned, you can replace it with keys `01` - `04`. It's better not to use master key `00` in production-grade SDM configuration. Whenever possible, it's also better to use different key numbers for different features (e.g. key `01` for SDM Meta Read, key `02` for SDM File Read etc).

Key numbers `0E` and `0F` have a special meaning:

* `0E` - Free access
* `0F` - No access

The interpretation of this meaning is slightly different for each feature, see datasheet for reference.

## Further usage
1. Edit `config.py` to adjust the decryption keys.
2. Setup nginx (with obligatory SSL encryption).
Expand Down
26 changes: 24 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from flask import Flask, request, render_template
from werkzeug.exceptions import BadRequest

from config import SDMMAC_PARAM, ENC_FILE_DATA_PARAM, ENC_PICC_DATA_PARAM, SDM_FILE_READ_KEY, SDM_META_READ_KEY
from libsdm import decrypt_sun_message, InvalidMessage
from config import SDMMAC_PARAM, ENC_FILE_DATA_PARAM, ENC_PICC_DATA_PARAM, SDM_FILE_READ_KEY, SDM_META_READ_KEY, UID_PARAM, CTR_PARAM
from libsdm import decrypt_sun_message, validate_plain_sun, InvalidMessage

app = Flask(__name__)

Expand Down Expand Up @@ -97,6 +97,28 @@ def sdm_info():
return _internal_sdm(with_tt=False)


@app.route('/tagpt')
def sdm_info_plain():
try:
uid = binascii.unhexlify(request.args[UID_PARAM])
read_ctr = binascii.unhexlify(request.args[CTR_PARAM])
cmac = binascii.unhexlify(request.args[SDMMAC_PARAM])
except binascii.Error:
raise BadRequest("Failed to decode parameters.")

try:
uid, read_ctr_num = validate_plain_sun(uid=uid,
read_ctr=read_ctr,
sdmmac=cmac,
sdm_file_read_key=SDM_FILE_READ_KEY)
except InvalidMessage:
raise BadRequest("Invalid message (most probably wrong signature).")

return render_template('sdm_info.html',
uid=uid,
read_ctr_num=read_ctr_num)


if __name__ == '__main__':
parser = argparse.ArgumentParser(description='OTA NFC Server')
parser.add_argument('--host', type=str, nargs='?',
Expand Down
4 changes: 4 additions & 0 deletions config.docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@

ENC_PICC_DATA_PARAM = os.environ.get("ENC_PICC_DATA_PARAM", "picc_data")
ENC_FILE_DATA_PARAM = os.environ.get("ENC_FILE_DATA_PARAM", "enc")

UID_PARAM = os.environ.get("UID_PARAM", "uid")
CTR_PARAM = os.environ.get("CTR_PARAM", "ctr")

SDMMAC_PARAM = os.environ.get("SDMMAC_PARAM", "cmac")
23 changes: 20 additions & 3 deletions libsdm.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ class InvalidMessage(RuntimeError):

def calculate_sdmmac(sdm_file_read_key: bytes,
picc_data: bytes,
enc_file_data: bytes = None) -> bytes:
enc_file_data: Optional[bytes] = None) -> bytes:
"""
Calculate SDMMAC for NTAG 424 DNA
:param sdm_file_read_key: MAC calculation key (K_SDMFileReadKey)
:param picc_data: PICCDataTag [ || UID ][ || SDMReadCtr ]]
:param picc_data: [ UID ][ SDMReadCtr ]
:param enc_file_data: SDMEncFileData (if used)
:return: calculated SDMMAC (8 bytes)
"""
Expand Down Expand Up @@ -81,11 +81,28 @@ def decrypt_file_data(sdm_file_read_key: bytes,
.decrypt(enc_file_data)


def validate_plain_sun(uid: bytes, read_ctr: bytes, sdmmac: bytes, sdm_file_read_key: bytes):
read_ctr_ba = bytearray(read_ctr)
read_ctr_ba.reverse()

datastream = io.BytesIO()
datastream.write(uid)
datastream.write(read_ctr_ba)

proper_sdmmac = calculate_sdmmac(sdm_file_read_key, datastream.getvalue())

if sdmmac != proper_sdmmac:
raise InvalidMessage("Message is not properly signed - invalid MAC")

read_ctr_num = struct.unpack('>I', b"\x00" + read_ctr)[0]
return uid, read_ctr_num


def decrypt_sun_message(sdm_meta_read_key: bytes,
sdm_file_read_key: bytes,
picc_enc_data: bytes,
sdmmac: bytes,
enc_file_data: bytes = None) -> Tuple[bytes, bytes, int, Optional[bytes]]:
enc_file_data: Optional[bytes] = None) -> Tuple[bytes, bytes, int, Optional[bytes]]:
"""
Decrypt SUN message for NTAG 424 DNA
:param sdm_meta_read_key: SUN decryption key (K_SDMMetaReadKey)
Expand Down
8 changes: 7 additions & 1 deletion templates/sdm_info.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@
<body>
<h1>Secure Dynamic Messaging Backend Server Demo</h1>
<p>
<strong style="color: green;">Cryptographic signature validated.</strong>
</p>
<p>
{% if picc_data_tag %}
PICC Data Tag: <code>{{ picc_data_tag.hex() }}</code><br>
{% endif %}
NFC TAG UID: <code>{{ uid.hex() }}</code><br>
Read counter: <code>{{ read_ctr_num }}</code><br>
{% if file_data %}
File data (hex): <code>{{ file_data.hex() }}</code><br>
File data (UTF-8): <code>{{ file_data_utf8 }}</code><br>
{% if tt_status %}
Tamper Status: <strong style="color: {{ tt_color }};">{{ tt_status }}</strong>
Tamper Status: <strong style="color: {{ tt_color }};">{{ tt_status }}</strong><br>
(Assuming that "Tamper Status" is stored in the first two bytes of "File data")
{% endif %}
{% endif %}
</p>
Expand Down
4 changes: 4 additions & 0 deletions templates/sdm_main.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ <h1>Secure Dynamic Messaging Backend Server Example</h1>
<p>Some of the examples below were taken from <a href="https://www.nxp.com/docs/en/application-note/AN12196.pdf">AN12196: "NTAG 424 DNA and NTAG 424 DNA TagTamper features and
hints"</a></p>
<ul>
<li><strong>NTAG 424 DNA:</strong> Plaintext tag UID, read counter mirroring with SDMMAC (CMAC)<br>
<a href="/tagpt?uid=041E3C8A2D6B80&ctr=000006&cmac=4B00064004B0B3D3">
<code>/tagpt?uid=041E3C8A2D6B80&ctr=000006&cmac=4B00064004B0B3D3</code>
</a></li>
<li><strong>NTAG 424 DNA:</strong> Encrypted PICC Data mirroring with SDMMAC (CMAC)<br>
<a href="/tag?picc_data=EF963FF7828658A599F3041510671E88&cmac=94EED9EE65337086">
<code>/tag?picc_data=EF963FF7828658A599F3041510671E88&cmac=94EED9EE65337086</code>
Expand Down
27 changes: 26 additions & 1 deletion test_libsdm.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import binascii

import config
from libsdm import decrypt_sun_message, InvalidMessage
from libsdm import decrypt_sun_message, validate_plain_sun, InvalidMessage


def test_sun1():
Expand Down Expand Up @@ -77,3 +77,28 @@ def test_sun2_wrong_sdmmac():
raise RuntimeError("InvalidSDMMAC was not thrown as expected")
finally:
config.SDMMAC_PARAM = original_sdmmac_param


def test_plain_sdm():
validate_plain_sun(
uid=binascii.unhexlify('041E3C8A2D6B80'),
read_ctr=binascii.unhexlify('000006'),
sdmmac=binascii.unhexlify('4B00064004B0B3D3'),
sdm_file_read_key=binascii.unhexlify('00000000000000000000000000000000')
)


def test_plain_sdm_wrong():
try:
validate_plain_sun(
uid=binascii.unhexlify('041E3C8A2D6B80'),
read_ctr=binascii.unhexlify('000006'),
sdmmac=binascii.unhexlify('AB00064004B0B3AB'),
sdm_file_read_key=binascii.unhexlify('00000000000000000000000000000000')
)
except InvalidMessage as e:
# this is expected
pass
else:
raise RuntimeError("InvalidMessage was not thrown as expected")

0 comments on commit b6a52b3

Please sign in to comment.