├── .flake8 ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── __init__.py ├── emu_flipper_zero_iso14443_a.py ├── emu_flipper_zero_iso14443_a_relay.py ├── reader_flipper_zero_iso14443_a.py ├── reader_flipper_zero_iso14443_b.py ├── reader_hydranfc_iso14443_a.py ├── reader_hydranfc_iso15693.py ├── reader_hydranfc_v2_iso14443_a.py └── reader_hydranfc_v2_iso14443_b.py ├── img └── scheme-man-in-the-middle-python-proxy.png ├── pynfcreader ├── __init__.py ├── devices │ ├── __init__.py │ ├── connection.py │ ├── devices.py │ ├── flipper_zero.py │ ├── hydra_nfc.py │ └── hydra_nfc_v2.py ├── sessions │ ├── __init__.py │ ├── iso14443 │ │ ├── __init__.py │ │ ├── iso14443.py │ │ ├── iso14443a.py │ │ ├── iso14443b.py │ │ └── tpdu.py │ └── iso15693 │ │ ├── __init__.py │ │ ├── iso15693.py │ │ └── requests.py └── tools │ ├── __init__.py │ └── utils.py ├── setup.py ├── tests ├── __init__.py ├── conftest.py ├── tests_iso_14443_a_card_1_hydranfc_v2.py └── tests_iso_14443_b_card_2_hydranfc_v2.py └── wiki └── iso14443 └── iso_14443_b.md /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 160 3 | exclude = test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | doc 3 | test 4 | .idea/ 5 | build/ 6 | dist/ 7 | pyNFCReader.egg-info/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyNFCReader 2 | 3 | [![PyPI version](https://badge.fury.io/py/pyNFCReader.svg)](https://github.com/gvinet/pynfcreader) 4 | 5 | A Python client for using the [Hydra NFC v1/v2](https://hydrabus.com/) as a low level contactless smart card reader: 6 | 7 | - It works with : 8 | 9 | - ISO 14443 A & ISO 15693 card. 10 | - ISO 14443 B only for hydra NFC v2 11 | 12 | ## Getting started 13 | 14 | ### Install from source 15 | 16 | Clone this repository, then run the following command : 17 | 18 | ``` 19 | $ python setup.py install --user 20 | ``` 21 | 22 | ### Install with `pip` 23 | 24 | ``` 25 | $ pip install pyNFCReader 26 | ``` 27 | 28 | ### Requirements 29 | 30 | * pip install crcmod pyscard 31 | 32 | ### Help 33 | 34 | * [ISO 14443 B](./wiki/iso14443/iso_14443_b.md) 35 | 36 | ## Example 37 | 38 | The file pynfcreader/examples/reader_hydranfc_iso14443_a.py contains an example. 39 | You can customize : 40 | - the port com and baudrate 41 | - the debug mode. 42 | 43 | It enables : 44 | - to select a Master Card card 45 | - to read the first 0x19 response bytes 46 | - to send the Get Processing Options and get the response 47 | 48 | Here's a log (where I've changed the UID and the historical bytes of the card :) ) 49 | 50 | INFO :: Hydra NFC python driver version : 0.0.1 - beta 51 | INFO :: Supported hydra firmware 11.02.2015 - [HydraFW v0.5 Beta] 52 | INFO :: ISO 14443 A only 53 | INFO :: Only one card in the field during a transaction 54 | INFO :: 55 | INFO :: Connect to HydraNFC 56 | INFO :: 57 | INFO :: Reset HydraNFC 58 | INFO :: 59 | INFO :: Configure HydraNFC 60 | INFO :: Configure gpio and spi... 61 | INFO :: Configure spi2... 62 | INFO :: 63 | INFO :: Set HydraNFC to ISO 14443 A mode 64 | INFO :: 65 | INFO :: REQA (7 bits): 66 | INFO :: 26 & 67 | INFO :: 68 | INFO :: ATQA: 69 | INFO :: 04 00 .. 70 | INFO :: 71 | INFO :: Select cascade level 1: 72 | INFO :: 93 20 .. 73 | INFO :: 74 | INFO :: Select cascade level 1 response: 75 | INFO :: XX XX XX XX 17 ..... 76 | INFO :: 77 | INFO :: Select cascade level 1: 78 | INFO :: 93 70 XX XX XX XX 17 .p..... 79 | INFO :: 80 | INFO :: Select cascade level 1 response: 81 | INFO :: 20 FC 70 ..p 82 | INFO :: 83 | INFO :: Request for Answer To Select (RATS): 84 | INFO :: PCD selected options: 85 | INFO :: FSDI : 0x0 => max PCD frame size : 16 bytes 86 | INFO :: CID : 0x0 87 | INFO :: RATS 88 | INFO :: E0 00 .. 89 | INFO :: 90 | INFO :: Answer to Select (ATS = RATS response): 91 | INFO :: 0A 78 80 82 02 XX XX XX XX XX 92 43 .x...... ...C 92 | INFO :: 93 | INFO :: T0 : 0x78 94 | INFO :: FSCI : 0x8 => max card frame size : 256 bytes 95 | INFO :: TA(1) present 96 | INFO :: TB(1) present 97 | INFO :: TC(1) present 98 | INFO :: TA(1) : 0x80 99 | INFO :: Interpretation : TODO... 100 | INFO :: TB(1) : 0x82 101 | INFO :: Interpretation : TODO... 102 | INFO :: TC(1) : 0x02 103 | INFO :: NAD not supported 104 | INFO :: CID supported 105 | INFO :: Historical bytes : XX XX XX XX XX 106 | INFO :: CRC : 92 43 107 | INFO :: 108 | INFO :: 109 | INFO :: 110 | INFO :: PPS 111 | INFO :: PCD selected options: 112 | INFO :: CID : 0x0 113 | INFO :: PPS1 not transmitted 114 | INFO :: PPS: 115 | INFO :: D0 . 116 | INFO :: 117 | INFO :: PPS response: 118 | INFO :: D0 73 87 .s. 119 | INFO :: 120 | INFO :: PPS accepted 121 | INFO :: 122 | INFO :: APDU command: 123 | INFO :: 00 A4 04 00 07 A0 00 00 00 04 10 10 19 ........ ..... 124 | INFO :: 125 | INFO :: TPDU command: 126 | INFO :: 0A 00 00 A4 04 00 07 A0 00 00 00 04 10 10 19 ........ ....... 127 | INFO :: 128 | INFO :: TPDU response: 129 | INFO :: 1A 00 6F 3F 84 07 A0 00 00 00 04 10 10 A5 7A 14 ..o?.... ......z. 130 | INFO :: 131 | INFO :: TPDU command: 132 | INFO :: A3 . 133 | INFO :: 134 | INFO :: TPDU response: 135 | INFO :: 13 34 50 0A 4D 41 53 54 45 52 43 41 52 44 69 1C .4P.MAST ERCARDi. 136 | INFO :: 137 | INFO :: TPDU command: 138 | INFO :: A2 . 139 | INFO :: 140 | INFO :: TPDU response: 141 | INFO :: 02 90 00 F1 09 ..... 142 | INFO :: 143 | INFO :: APDU response: 144 | INFO :: 6F 3F 84 07 A0 00 00 00 04 10 10 A5 34 50 0A 4D o?...... ....4P.M 145 | INFO :: 41 53 54 45 52 43 41 52 44 90 00 ASTERCAR D.. 146 | INFO :: 147 | INFO :: APDU command: 148 | INFO :: 80 A8 00 00 02 83 00 00 ........ 149 | INFO :: 150 | INFO :: TPDU command: 151 | INFO :: 0B 00 80 A8 00 00 02 83 00 00 ........ .. 152 | INFO :: 153 | INFO :: TPDU response: 154 | INFO :: 1B 00 77 16 82 02 19 80 94 10 08 01 01 00 C6 3D ..w..... .......= 155 | INFO :: 156 | INFO :: TPDU command: 157 | INFO :: A2 . 158 | INFO :: 159 | INFO :: TPDU response: 160 | INFO :: 12 10 01 01 01 18 01 02 00 20 01 02 00 90 99 5B ........ .......[ 161 | INFO :: 162 | INFO :: TPDU command: 163 | INFO :: A3 . 164 | INFO :: 165 | INFO :: TPDU response: 166 | INFO :: 03 00 C8 34 ...4 167 | INFO :: 168 | INFO :: APDU response: 169 | INFO :: 77 16 82 02 19 80 94 10 08 01 01 00 10 01 01 01 w....... ........ 170 | INFO :: 18 01 02 00 20 01 02 00 90 00 ........ .. 171 | INFO :: 172 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gvinet/pynfcreader/2d77ac041897141bbef4577f45d937c1b2d99ca5/examples/__init__.py -------------------------------------------------------------------------------- /examples/emu_flipper_zero_iso14443_a.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2024 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import time 16 | from pynfcreader.sessions.iso14443.tpdu import Tpdu 17 | from pynfcreader.devices import flipper_zero 18 | from pynfcreader.sessions.iso14443.iso14443a import Iso14443ASession 19 | 20 | fz = flipper_zero.FlipperZero("", debug=False) 21 | 22 | fz.connect() 23 | fz.set_mode_emu_iso14443A() 24 | 25 | def process_apdu(cmd: str): 26 | print(f"apdu {cmd}") 27 | if cmd == "00a404000e325041592e5359532e444446303100": 28 | rapdu = "6F57840E325041592E5359532E4444463031A545BF0C42611B4F07A0000000421010500243428701019F2808400200000000000061234F07A0000000041010500A4D4153544552434152448701029F280840002000000000009000" 29 | else: 30 | rapdu = "6F00" 31 | return rapdu 32 | 33 | class Emu(Iso14443ASession): 34 | def __init__(self, cid=0, nad=0, drv=None, block_size=16, process_function=None): 35 | Iso14443ASession.__init__(self, cid, nad, drv, block_size) 36 | self._addCID = False 37 | self.drv = self._drv 38 | self.process_function = process_function 39 | 40 | def run(self): 41 | self.drv.start_emulation() 42 | print("...go!") 43 | self.low_level_dispatcher() 44 | 45 | def low_level_dispatcher(self): 46 | capdu = bytes() 47 | ats_sent = False 48 | 49 | iblock_resp_lst = [] 50 | 51 | while 1: 52 | r = fz.emu_get_cmd() 53 | rtpdu = None 54 | print(f"tpdu < {r}") 55 | if r == "off": 56 | print("field off") 57 | elif r == "on": 58 | print("field on") 59 | ats_sent = False 60 | else: 61 | tpdu = Tpdu(bytes.fromhex(r)) 62 | 63 | if (tpdu.tpdu[0] == 0xE0) and (ats_sent is False): 64 | rtpdu, crc = "0A788082022063CBA3A0", True 65 | ats_sent = True 66 | elif tpdu.r: 67 | print("r block") 68 | if r == "BA00BED9": 69 | rtpdu, crc = "BA00", True 70 | elif r[0:2] in ["A2", "A3", "B2", "B3"]: 71 | rtpdu, crc = iblock_resp_lst.pop(0).hex(), True 72 | elif tpdu.s: 73 | print("s block") 74 | elif tpdu.i: 75 | print("i block") 76 | capdu += tpdu.get_inf_field() 77 | 78 | if tpdu.is_chaining() is False: 79 | rapdu = self.process_function(capdu.hex()) 80 | capdu = bytes() 81 | iblock_resp_lst = self.chaining_iblock(data=bytes.fromhex(rapdu)) 82 | rtpdu, crc = iblock_resp_lst.pop(0).hex(), True 83 | 84 | print(f">>> rtdpu {rtpdu}\n") 85 | fz.emu_send_resp(rtpdu.encode(), crc) 86 | 87 | 88 | emu = Emu(drv=fz, process_function=process_apdu) 89 | emu.run() 90 | -------------------------------------------------------------------------------- /examples/emu_flipper_zero_iso14443_a_relay.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2024 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import time 16 | from pynfcreader.sessions.iso14443.tpdu import Tpdu 17 | from pynfcreader.devices import flipper_zero 18 | from pynfcreader.sessions.iso14443.iso14443a import Iso14443ASession 19 | from typing import Tuple 20 | import sys 21 | from smartcard.System import readers 22 | 23 | 24 | class Reader(): 25 | 26 | def __init__(self): 27 | pass 28 | 29 | def connect(self): 30 | pass 31 | 32 | def field_off(self): 33 | pass 34 | 35 | def field_on(self): 36 | pass 37 | 38 | def process_apdu(self, data: str): 39 | pass 40 | 41 | 42 | class PCSCReader(Reader): 43 | def __init__(self): 44 | pass 45 | 46 | def connect(self): 47 | available_readers = readers() 48 | 49 | if len(available_readers) == 0: 50 | print("No card reader avaible.") 51 | sys.exit(1) 52 | 53 | # We use the first detected reader 54 | reader = available_readers[0] 55 | print(f"Reader detected : {reader}") 56 | 57 | # Se connecter à la carte 58 | self.connection = reader.createConnection() 59 | self.connection.connect() 60 | 61 | def process_apdu(self, data: bytes) -> bytes: 62 | print(f"apdu cmd: {data.hex()}") 63 | 64 | if data.hex() == "00b2010c00": 65 | resp = bytes.fromhex("70759f6c0200019f650200709f66020e0e9f6b136132770025856368d15062019000990000000f9f670103563442353133323737303032353835363336385e202f5e313530363230313333303030333333303030323232323230303031313131309f62060000003800009f630600000000e0e09f6401039000") 66 | elif data.hex() == "00b2011400": 67 | resp = bytes.fromhex( 68 | "7081a057136132770025856368d15062016583976410000f5a0861327700258563685f24031506305f25031305015f280202505f3401018c219f02069f03069f1a0295055f2a029a039c019f37049f35019f45029f4c089f34038d0c910a8a0295059f37049f4c088e0e00000000000000005e0342031f039f0702ff009f080200029f0d05b0000480009f0e050470a800009f0f05b0000480009f420209789f4a01829000") 69 | else: 70 | data, sw1, sw2 = self.connection.transmit(list(data)) 71 | resp = bytes(data + [sw1, sw2]) 72 | print(f"apdu resp: {resp.hex()}") 73 | return resp 74 | 75 | 76 | fz = flipper_zero.FlipperZero("", debug=False) 77 | fz.connect() 78 | fz.set_mode_emu_iso14443A() 79 | 80 | 81 | def process_apdu(cmd: str): 82 | print(f"apdu {cmd}") 83 | if cmd == "00a404000e325041592e5359532e444446303100": 84 | rapdu = "6F57840E325041592E5359532E4444463031A545BF0C42611B4F07A0000000421010500243428701019F2808400200000000000061234F07A0000000041010500A4D4153544552434152448701029F280840002000000000009000" 85 | else: 86 | rapdu = "6F00" 87 | return rapdu 88 | 89 | 90 | class Emu(Iso14443ASession): 91 | def __init__(self, cid=0, nad=0, drv=None, block_size=16, process_function=None, reader=None): 92 | Iso14443ASession.__init__(self, cid, nad, drv, block_size) 93 | self._addCID = False 94 | self.drv = self._drv 95 | self.process_function = self.process_apdu 96 | self._pcb_block_number: int = 1 97 | # Set to one for an ICC 98 | self._iblock_pcb_number = 1 99 | self.iblock_resp_lst = [] 100 | self.reader = reader 101 | if self.reader: 102 | self.reader.connect() 103 | 104 | def run(self): 105 | self.drv.start_emulation() 106 | print("...go!") 107 | self.low_level_dispatcher() 108 | 109 | def rblock_process(self, tpdu: Tpdu) -> Tuple[str, bool]: 110 | print("r block") 111 | if tpdu == "BA00BED9": 112 | rtpdu, crc = "BA00", True 113 | 114 | elif tpdu.pcb in [0xA2, 0xA3, 0xB2, 0xB3]: 115 | if len(self.iblock_resp_lst): 116 | rtpdu, crc = self.iblock_resp_lst.pop(0).hex(), True 117 | else: 118 | rtpdu = self.build_rblock(ack=True).hex() 119 | crc = True 120 | 121 | return rtpdu, crc 122 | 123 | def field_off(self): 124 | print("field off") 125 | if self.reader: 126 | self.reader.field_off() 127 | 128 | def field_on(self): 129 | print("field on") 130 | if self.reader: 131 | self.reader.field_on() 132 | 133 | def process_apdu(self, apdu): 134 | if self.reader: 135 | return self.reader.process_apdu(apdu) 136 | else: 137 | self.process_function(apdu) 138 | 139 | def low_level_dispatcher(self): 140 | capdu = bytes() 141 | ats_sent = False 142 | 143 | iblock_resp_lst = [] 144 | 145 | while 1: 146 | r = fz.emu_get_cmd() 147 | rtpdu = None 148 | print(f"tpdu < {r}") 149 | if r == "off": 150 | self.field_off() 151 | elif r == "on": 152 | self.field_on() 153 | ats_sent = False 154 | else: 155 | tpdu = Tpdu(bytes.fromhex(r)) 156 | 157 | if (tpdu.tpdu[0] == 0xE0) and (ats_sent is False): 158 | rtpdu, crc = "0A788082022063CBA3A0", True 159 | ats_sent = True 160 | elif tpdu.r: 161 | rtpdu, crc = self.rblock_process(tpdu) 162 | elif tpdu.s: 163 | print("s block") 164 | # Deselect 165 | if len(tpdu._inf_field) == 0: 166 | rtpdu, crc = "C2E0B4", False 167 | # Otherwise, it is a WTX 168 | 169 | elif tpdu.i: 170 | print("i block") 171 | capdu += tpdu.inf 172 | 173 | if tpdu.is_chaining() is False: 174 | rapdu = self.process_function(capdu) 175 | capdu = bytes() 176 | self.iblock_resp_lst = self.chaining_iblock(data=rapdu) 177 | rtpdu, crc = self.iblock_resp_lst.pop(0).hex(), True 178 | 179 | print(f">>> rtdpu {rtpdu}\n") 180 | fz.emu_send_resp(bytes.fromhex(rtpdu), crc) 181 | 182 | 183 | pcsc_reader = PCSCReader() 184 | 185 | emu = Emu(drv=fz, reader=pcsc_reader) 186 | emu.run() 187 | -------------------------------------------------------------------------------- /examples/reader_flipper_zero_iso14443_a.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2024 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import time 16 | 17 | from pynfcreader.devices import flipper_zero 18 | from pynfcreader.sessions.iso14443.iso14443a import Iso14443ASession 19 | 20 | fz = flipper_zero.FlipperZero("", debug=False) 21 | 22 | hn = Iso14443ASession(drv=fz, block_size=120) 23 | 24 | hn.connect() 25 | hn.field_off() 26 | time.sleep(0.1) 27 | hn.field_on() 28 | hn.polling() 29 | r = hn.send_apdu("00 a4 04 00 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 00") 30 | -------------------------------------------------------------------------------- /examples/reader_flipper_zero_iso14443_b.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2024 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import time 16 | 17 | from pynfcreader.devices import flipper_zero 18 | from pynfcreader.sessions.iso14443.iso14443b import Iso14443BSession 19 | 20 | fz = flipper_zero.FlipperZero("/dev/ttyACM0", debug=False) 21 | 22 | hn = Iso14443BSession(drv=fz, block_size=120) 23 | 24 | hn.connect() 25 | hn.field_off() 26 | time.sleep(0.1) 27 | hn.field_on() 28 | hn.polling() 29 | r = hn.send_apdu("00 a4 04 00 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 00") 30 | -------------------------------------------------------------------------------- /examples/reader_hydranfc_iso14443_a.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2020 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import time 16 | from pynfcreader.devices.hydra_nfc import HydraNFC 17 | from pynfcreader.sessions.iso14443.iso14443a import Iso14443ASession 18 | 19 | hydra_nfc = HydraNFC(port="/dev/ttyACM0", debug=False) 20 | hn = Iso14443ASession(drv=hydra_nfc, block_size=120) 21 | 22 | hn.connect() 23 | hn.field_off() 24 | time.sleep(0.1) 25 | hn.field_on() 26 | hn.polling() 27 | 28 | hn.send_apdu("00 a4 04 00 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 00") 29 | hn.send_apdu("00 a4 04 00 07 A0 00 00 00 42 10 10 00") 30 | hn.send_apdu("00 a4 04 00 07 A0 00 00 00 04 10 10 00") 31 | hn.send_apdu("00 a4 04 00 07 A0 00 00 00 03 10 10 00") 32 | hn.send_apdu("00 a4 04 00 05 A0 00 00 00 03 00") 33 | 34 | hn.field_off() 35 | -------------------------------------------------------------------------------- /examples/reader_hydranfc_iso15693.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2020 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import time 17 | from pynfcreader.devices.hydra_nfc import HydraNFC 18 | from pynfcreader.sessions.iso15693.iso15693 import Iso15693Session 19 | 20 | hydra_nfc = HydraNFC(port="/dev/ttyACM0", debug=False) 21 | hn = Iso15693Session(drv=hydra_nfc) 22 | hn.connect() 23 | hn.field_off() 24 | time.sleep(0.2) 25 | hn.field_on() 26 | hn.get_all_auto() 27 | -------------------------------------------------------------------------------- /examples/reader_hydranfc_v2_iso14443_a.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2020 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import time 16 | from pynfcreader.devices.hydra_nfc_v2 import HydraNFCv2 17 | from pynfcreader.sessions.iso14443.iso14443a import Iso14443ASession 18 | 19 | hydra_nfc = HydraNFCv2(port="", debug=False) 20 | hn = Iso14443ASession(drv=hydra_nfc, block_size=120) 21 | 22 | hn.connect() 23 | hn.field_off() 24 | time.sleep(0.1) 25 | hn.field_on() 26 | hn.polling() 27 | 28 | hn.send_apdu("00 a4 04 00 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 00") 29 | hn.send_apdu("00 a4 04 00 07 A0 00 00 00 42 10 10 00") 30 | hn.send_apdu("00 a4 04 00 07 A0 00 00 00 04 10 10 00") 31 | hn.send_apdu("00 a4 04 00 07 A0 00 00 00 03 10 10 00") 32 | hn.send_apdu("00 a4 04 00 05 A0 00 00 00 03 00") 33 | 34 | hn.field_off() 35 | -------------------------------------------------------------------------------- /examples/reader_hydranfc_v2_iso14443_b.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2020 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import time 16 | from pynfcreader.devices.hydra_nfc_v2 import HydraNFCv2 17 | from pynfcreader.sessions.iso14443.iso14443b import Iso14443BSession 18 | 19 | hydra_nfc = HydraNFCv2(port="/dev/ttyACM0", debug=False) 20 | hn = Iso14443BSession(drv=hydra_nfc, block_size=120) 21 | 22 | hn.connect() 23 | hn.field_off() 24 | time.sleep(0.1) 25 | hn.field_on() 26 | hn.polling() 27 | print(hn.pupi) 28 | 29 | r = hn.send_apdu("00 a4 04 00 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 00") 30 | print(r) 31 | r = hn.send_apdu("00 a4 04 00 07 A0 00 00 00 42 10 10 00") 32 | print(r) 33 | r = hn.send_apdu("00 a4 04 00 07 A0 00 00 00 04 10 10 00") 34 | print(r) 35 | 36 | hn.field_off() 37 | -------------------------------------------------------------------------------- /img/scheme-man-in-the-middle-python-proxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gvinet/pynfcreader/2d77ac041897141bbef4577f45d937c1b2d99ca5/img/scheme-man-in-the-middle-python-proxy.png -------------------------------------------------------------------------------- /pynfcreader/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2020 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | __version__ = "1.3.0" 17 | -------------------------------------------------------------------------------- /pynfcreader/devices/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gvinet/pynfcreader/2d77ac041897141bbef4577f45d937c1b2d99ca5/pynfcreader/devices/__init__.py -------------------------------------------------------------------------------- /pynfcreader/devices/connection.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from pathlib import Path 3 | 4 | import serial 5 | import serial.tools.list_ports 6 | 7 | 8 | class SerialCnx: 9 | def __init__(self, port: str, baudrate: int, timeout=None, recording: str = ""): 10 | self.port: str = port 11 | self.baudrate: int = baudrate 12 | self.timeout: int = timeout 13 | self.cnx = serial.Serial(self.port, baudrate=self.baudrate, timeout=self.timeout) 14 | self.recording_writer = None 15 | self._recording_init(recording) 16 | 17 | def _recording_init(self, recording): 18 | if recording != "": 19 | self.recording_writer = csv.writer(Path(recording).open(mode="w", newline="", encoding="utf-8")) 20 | self.recording_writer.writerow(["Type", "Data"]) 21 | 22 | def _recording_write(self, mode: str, data: str): 23 | if self.recording_writer: 24 | self.recording_writer.writerow([mode, data]) 25 | 26 | def reset_input_buffer(self): 27 | self.cnx.reset_input_buffer() 28 | 29 | def reset_output_buffer(self): 30 | self.cnx.reset_output_buffer() 31 | 32 | def set_timeout(self, timeout: int): 33 | self.cnx.timeout = timeout 34 | 35 | def close(self): 36 | self.cnx.close() 37 | 38 | def readline(self): 39 | data = self.cnx.readline() 40 | self._recording_write("R", data.decode()) 41 | return data 42 | 43 | def write(self, data: bytes): 44 | self._recording_write("W", data.decode()) 45 | self.cnx.write(data) 46 | 47 | 48 | class SerialCnxVirtual: 49 | def __init__(self, log: str = ""): 50 | self.reader = csv.reader(Path(log).open(mode="r", encoding="utf-8", newline="")) 51 | self._log_get_line() 52 | 53 | def _log_get_line(self): 54 | return next(self.reader) 55 | 56 | def reset_input_buffer(self): 57 | pass 58 | 59 | def reset_output_buffer(self): 60 | pass 61 | 62 | def set_timeout(self, timeout: int): 63 | pass 64 | 65 | def close(self): 66 | pass 67 | 68 | def readline(self): 69 | data = self._log_get_line() 70 | return data[1].encode() 71 | 72 | def write(self, data: bytes): 73 | a = self._log_get_line() 74 | -------------------------------------------------------------------------------- /pynfcreader/devices/devices.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2020 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from abc import ABCMeta, abstractmethod 17 | 18 | 19 | class Devices: 20 | __metaclass__ = ABCMeta 21 | 22 | @abstractmethod 23 | def connect(self): 24 | pass 25 | 26 | @abstractmethod 27 | def write(self, data, resp_len=20, transmitter_add_crc=True): 28 | pass 29 | 30 | @abstractmethod 31 | def get_logger(self): 32 | pass 33 | -------------------------------------------------------------------------------- /pynfcreader/devices/flipper_zero.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2024 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | import sys 17 | 18 | import serial 19 | import serial.tools.list_ports 20 | 21 | from pynfcreader.devices.devices import Devices 22 | from pynfcreader.devices.connection import SerialCnx, SerialCnxVirtual 23 | 24 | class FlipperZero(Devices): 25 | 26 | def __init__(self, port: str = "", baudrate: int = 115200 * 8, debug: bool = True, recording="", log=""): 27 | 28 | self._port = port if port != "" else self.auto_search() 29 | self._baudrate = baudrate 30 | if log == "": 31 | self.cnx = SerialCnx(self._port, baudrate=115200 * 8, timeout=None, recording=recording) 32 | else: 33 | self.cnx = SerialCnxVirtual(log) 34 | 35 | self.__logger = logging.getLogger() 36 | stream_handler = logging.StreamHandler() 37 | stream_debug_formatter = logging.Formatter('%(levelname)s :: %(message)s') 38 | stream_handler.setFormatter(stream_debug_formatter) 39 | 40 | self.__logger.setLevel(logging.INFO) 41 | stream_handler.setLevel(logging.INFO) 42 | 43 | self._python_ver = sys.version[0] 44 | 45 | if debug: 46 | self.__logger.setLevel(logging.DEBUG or logging.INFO) 47 | stream_handler.setLevel(logging.DEBUG or logging.INFO) 48 | 49 | self.__logger.addHandler(stream_handler) 50 | 51 | def auto_search(self) -> str: 52 | for port in serial.tools.list_ports.comports(): 53 | if "Flipper" in port.description: 54 | return port.device 55 | print("Error. No flipper zero device found") 56 | exit(1) 57 | 58 | def connect(self): 59 | 60 | self.__logger.info("Connect to Flipper Zero") 61 | self.__logger.info("") 62 | 63 | # self.cnx.connect() 64 | 65 | self.cnx.reset_input_buffer() 66 | 67 | r = self.cnx.readline().decode() 68 | 69 | while "Firmware version:" not in r: 70 | r = self.cnx.readline().decode() 71 | 72 | self.cnx.readline().decode() 73 | 74 | self.cnx.set_timeout(0.1) 75 | 76 | def close(self): 77 | self.cnx.close() 78 | 79 | def get_logger(self): 80 | return self.__logger 81 | 82 | def set_mode_iso14443A(self): 83 | self.cnx.reset_input_buffer() 84 | self.cnx.reset_output_buffer() 85 | self.cnx.write(b"nfc mode_14443_a\r\n") 86 | r = self.read_all() 87 | assert "Set mode ISO 14443 A" in r 88 | 89 | def set_mode_emu_iso14443A(self): 90 | self.cnx.reset_input_buffer() 91 | self.cnx.reset_output_buffer() 92 | self.cnx.write(b"nfc mode_emu_14443_a\r\n") 93 | return self.read_all() 94 | # assert "Set mode ISO 14443 A" in r 95 | 96 | def set_mode_iso14443B(self): 97 | self.cnx.reset_input_buffer() 98 | self.cnx.reset_output_buffer() 99 | self.cnx.write(b"nfc mode_14443_b\r\n") 100 | return self.read_all() 101 | 102 | def set_mode_iso15693(self): 103 | self.cnx.reset_input_buffer() 104 | self.cnx.reset_output_buffer() 105 | self.cnx.write(b"nfc mode_15693\r\n") 106 | return self.read_all() 107 | 108 | def set_mode_emu_iso15693(self): 109 | self.cnx.reset_input_buffer() 110 | self.cnx.reset_output_buffer() 111 | self.cnx.write(b"nfc mode_emu_15693\r\n") 112 | return self.read_all() 113 | 114 | def start_emulation(self): 115 | self.cnx.reset_input_buffer() 116 | self.cnx.reset_output_buffer() 117 | self.cnx.write(b"nfc run_emu\r\n") 118 | self.read_all() 119 | self.cnx.set_timeout(None) 120 | 121 | def emu_get_cmd(self) -> str: 122 | return str(self.cnx.readline().decode()).strip() 123 | 124 | def emu_send_resp(self, resp: bytes, flipper_add_crc=False) -> None: 125 | 126 | crc = b"1" if flipper_add_crc else b"0" 127 | self.cnx.write(crc + resp.hex().encode() + b"\n") 128 | 129 | def read_all(self): 130 | r = "" 131 | d = self.cnx.readline().decode() 132 | while d != "": 133 | r += d 134 | d = self.cnx.readline().decode() 135 | return r 136 | 137 | def field_off(self): 138 | self.__logger.debug("Field off") 139 | self.cnx.reset_input_buffer() 140 | self.cnx.reset_output_buffer() 141 | self.cnx.write(b"nfc off\r\n") 142 | r = self.read_all() 143 | assert "Field is off" in r 144 | 145 | def field_on(self): 146 | self.__logger.debug("Field on") 147 | self.cnx.reset_input_buffer() 148 | self.cnx.reset_output_buffer() 149 | self.cnx.write(b"nfc on\r\n") 150 | self.read_all() 151 | 152 | def write_bits(self, data=b"", num_bits=0): 153 | 154 | self.cnx.reset_input_buffer() 155 | self.cnx.reset_output_buffer() 156 | self.cnx.write(b"nfc reqa\r\n") 157 | resp = bytes.fromhex(self.read_all().split("\r\n")[1]) 158 | 159 | self.__logger.debug(f"\t<{' '.join(f'{hit:02X}' for hit in resp)}") 160 | self.__logger.debug("") 161 | 162 | return resp 163 | 164 | def set_uid(self, uid: str): 165 | self.__logger.debug(f"set uid: {uid}") 166 | self.cnx.reset_input_buffer() 167 | self.cnx.reset_output_buffer() 168 | self.cnx.write(b"nfc set_uid {uid}\r\n") 169 | r = self.read_all() 170 | 171 | def set_sak(self, sak: int): 172 | assert sak in range(256) 173 | self.__logger.debug(f"set sak: {sak}") 174 | self.cnx.reset_input_buffer() 175 | self.cnx.reset_output_buffer() 176 | self.cnx.write(b"nfc set_sak {sak:02X}\r\n") 177 | r = self.read_all() 178 | 179 | def set_atqa(self, atqa: str): 180 | self.__logger.debug(f"set atqa: {atqa}") 181 | self.cnx.reset_input_buffer() 182 | self.cnx.reset_output_buffer() 183 | self.cnx.write(b"nfc set_sak atqa\r\n") 184 | r = self.read_all() 185 | 186 | def write(self, data=b"", resp_len=None, transmitter_add_crc=True): 187 | self.cnx.reset_input_buffer() 188 | self.cnx.reset_output_buffer() 189 | add_crc = 1 if transmitter_add_crc else 0 190 | 191 | self.__logger.debug("write") 192 | self.__logger.debug(f"\t>{data.hex()}") 193 | 194 | self.cnx.write(f"nfc send {add_crc} {data.hex()}\r\n".encode()) 195 | resp = bytes.fromhex(self.read_all().split("\r\n")[1]) 196 | 197 | self.__logger.debug(f"\t<{data.hex()}") 198 | self.__logger.debug("") 199 | 200 | return resp 201 | -------------------------------------------------------------------------------- /pynfcreader/devices/hydra_nfc.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2020 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | import sys 17 | 18 | from pynfcreader.devices.devices import Devices 19 | from pyHydrabus import NFC 20 | 21 | 22 | class HydraNFC(Devices): 23 | 24 | def __init__(self, port="C0M8", debug: bool = True): 25 | self._port = port 26 | self._hydranfc = None 27 | 28 | self.__logger = logging.getLogger() 29 | stream_handler = logging.StreamHandler() 30 | stream_debug_formatter = logging.Formatter('%(levelname)s :: %(message)s') 31 | stream_handler.setFormatter(stream_debug_formatter) 32 | 33 | self.__logger.setLevel(logging.INFO) 34 | stream_handler.setLevel(logging.INFO) 35 | 36 | self._python_ver = sys.version[0] 37 | 38 | if debug: 39 | self.__logger.setLevel(logging.DEBUG or logging.INFO) 40 | stream_handler.setLevel(logging.DEBUG or logging.INFO) 41 | 42 | self.__logger.addHandler(stream_handler) 43 | 44 | def connect(self): 45 | self.__logger.info("Connect to HydraNFC") 46 | self.__logger.info("") 47 | self._hydranfc = NFC(self._port) 48 | 49 | def get_logger(self): 50 | return self.__logger 51 | 52 | def set_mode_iso14443A(self): 53 | self._hydranfc.mode = self._hydranfc.MODE_ISO_14443A 54 | 55 | def set_mode_iso15693(self): 56 | self._hydranfc.mode = self._hydranfc.MODE_ISO_15693 57 | 58 | def field_off(self): 59 | self.__logger.debug("Field off") 60 | self._hydranfc.rf = 0 61 | 62 | def field_on(self): 63 | self.__logger.debug("Field on") 64 | self._hydranfc.rf = 1 65 | 66 | def write_bits(self, data=b"", num_bits=0): 67 | resp = self._hydranfc.write_bits(data, num_bits) 68 | self.__logger.debug(f"\t<{' '.join(f'{hit:02X}' for hit in resp)}") 69 | self.__logger.debug("") 70 | return resp 71 | 72 | def write(self, data=b"", resp_len=None, transmitter_add_crc=True): 73 | self.__logger.debug("write") 74 | self.__logger.debug(f"\t>{data.hex()}") 75 | 76 | resp = self._hydranfc.write(data, transmitter_add_crc) 77 | self.__logger.debug(f"\t<{data.hex()}") 78 | self.__logger.debug("") 79 | 80 | return resp 81 | -------------------------------------------------------------------------------- /pynfcreader/devices/hydra_nfc_v2.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2020 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | import sys 17 | 18 | from pynfcreader.devices.devices import Devices 19 | import serial 20 | import serial.tools.list_ports 21 | 22 | class HydraNFCv2(Devices): 23 | 24 | def __init__(self, port="", debug=True): 25 | 26 | self._port = port if port != "" else self.auto_search() 27 | self._hydranfc = None 28 | 29 | self.__logger = logging.getLogger() 30 | stream_handler = logging.StreamHandler() 31 | stream_debug_formatter = logging.Formatter('%(levelname)s :: %(message)s') 32 | stream_handler.setFormatter(stream_debug_formatter) 33 | 34 | self.__logger.setLevel(logging.INFO) 35 | stream_handler.setLevel(logging.INFO) 36 | 37 | self._python_ver = sys.version[0] 38 | 39 | if debug: 40 | self.__logger.setLevel(logging.DEBUG or logging.INFO) 41 | stream_handler.setLevel(logging.DEBUG or logging.INFO) 42 | 43 | self.__logger.addHandler(stream_handler) 44 | 45 | def auto_search(self) -> str: 46 | for port in serial.tools.list_ports.comports(): 47 | if "HydraBus" in port.description: 48 | return port.device 49 | print("Error. No flipper zero device found") 50 | exit(1) 51 | 52 | def enter_bbio(self): 53 | self._hydranfc.timeout = 0.01 54 | for _ in range(20): 55 | self._hydranfc.write(b"\x00") 56 | if b"BBIO1" in self._hydranfc.read(5): 57 | self._hydranfc.reset_input_buffer() 58 | self._hydranfc.timeout = None 59 | 60 | # We enter reader mode 61 | self._hydranfc.write(b"\x0E") 62 | if self._hydranfc.read(4) != b"NFC2": 63 | raise Exception("Cannot enter BBIO Reader mode") 64 | return 65 | raise Exception("Cannot enter BBIO mode.") 66 | 67 | def connect(self): 68 | self.__logger.info("Connect to HydraNFC") 69 | self.__logger.info("") 70 | self._hydranfc = serial.Serial(self._port, timeout=None) 71 | self.enter_bbio() 72 | 73 | def get_logger(self): 74 | return self.__logger 75 | 76 | def set_mode_iso14443A(self): 77 | self._hydranfc.write(b"\x06") 78 | 79 | def set_mode_iso14443B(self): 80 | self._hydranfc.write(b"\x09") 81 | 82 | def set_mode_iso15693(self): 83 | self._hydranfc.write(b"\x07") 84 | 85 | def field_off(self): 86 | self.__logger.debug("Field off") 87 | self._hydranfc.write(b"\x02") 88 | 89 | def field_on(self): 90 | self.__logger.debug("Field on") 91 | self._hydranfc.write(b"\x03") 92 | 93 | def write_bits(self, data=b"", num_bits=0): 94 | # resp = self._hydranfc.write_bits(data, num_bits) 95 | 96 | self._hydranfc.write(b"\x08") 97 | rx_len = int.from_bytes(self._hydranfc.read(1), byteorder="little") 98 | resp = self._hydranfc.read(rx_len) 99 | 100 | self.__logger.debug(f"\t<{' '.join(f'{hit:02X}' for hit in resp)}") 101 | self.__logger.debug("") 102 | 103 | return resp 104 | 105 | def write(self, data=b"", resp_len=None, transmitter_add_crc=True): 106 | self.__logger.debug("write") 107 | self.__logger.debug(f"\t>{data.hex()}") 108 | 109 | self._hydranfc.write(b"\x05") 110 | self._hydranfc.write(int(transmitter_add_crc).to_bytes(1, byteorder="big")) 111 | self._hydranfc.write(len(data).to_bytes(1, byteorder="big")) 112 | self._hydranfc.write(data) 113 | 114 | rx_len = int.from_bytes(self._hydranfc.read(1), byteorder="little") 115 | 116 | resp = self._hydranfc.read(rx_len) 117 | 118 | self.__logger.debug(f"\t<{data.hex()}") 119 | self.__logger.debug("") 120 | 121 | return resp 122 | -------------------------------------------------------------------------------- /pynfcreader/sessions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gvinet/pynfcreader/2d77ac041897141bbef4577f45d937c1b2d99ca5/pynfcreader/sessions/__init__.py -------------------------------------------------------------------------------- /pynfcreader/sessions/iso14443/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gvinet/pynfcreader/2d77ac041897141bbef4577f45d937c1b2d99ca5/pynfcreader/sessions/iso14443/__init__.py -------------------------------------------------------------------------------- /pynfcreader/sessions/iso14443/iso14443.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2020 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from pynfcreader.sessions.iso14443.tpdu import Tpdu 17 | from pynfcreader.tools import utils 18 | 19 | 20 | class Iso14443Session(object): 21 | 22 | def __init__(self, cid=0, nad=0, drv=None, block_size=16, mode: str = "reader"): 23 | self._init_pcb_block_nb() 24 | self._addNAD: bool = False 25 | self._addCID: bool = False 26 | self._cid = cid 27 | self._nad = nad 28 | self._drv = drv 29 | self._iblock_pcb_number = 0x00 30 | self._pcb_block_number = None 31 | self._drv = drv 32 | self._logger = self._drv.get_logger() 33 | self.block_size = block_size 34 | assert mode in ["card", "reader"] 35 | if mode == "card": 36 | self.card_emu = True 37 | else: 38 | self.card_emu = False 39 | 40 | def connect(self): 41 | self._drv.connect() 42 | self._drv.set_mode_iso14443A() 43 | 44 | def field_on(self): 45 | self._logger.info("field on") 46 | self._drv.field_on() 47 | 48 | def field_off(self): 49 | self._logger.info("field off") 50 | self._drv.field_off() 51 | 52 | def polling(self): 53 | self.send_reqa() 54 | self.send_select_full() 55 | self.send_pps() 56 | 57 | @property 58 | def block_size(self): 59 | return self._block_size 60 | 61 | @block_size.setter 62 | def block_size(self, size): 63 | assert (0 <= size <= 256) 64 | self._block_size = size 65 | 66 | def get_and_update_iblock_pcb_number(self): 67 | self._iblock_pcb_number ^= 1 68 | if self.card_emu: 69 | return self._iblock_pcb_number 70 | else: 71 | return self._iblock_pcb_number ^ 1 72 | 73 | def send_pps(self, cid=0x0, pps1=False, dri=0x0, dsi=0x0): 74 | self._logger.info("PPS") 75 | self._logger.info("\tPCD selected options:") 76 | pps0_pps1 = [0x01] 77 | if pps1: 78 | pps0_pps1.append(dri << 4 + dsi) 79 | self._logger.info("\tCID : 0x%X" % cid) 80 | self._logger.info("\tPPS1 %stransmitted" % ("not " * (not pps1))) 81 | 82 | data = bytes([0xD0 + cid] + pps0_pps1) 83 | self.comment_data("PPS:", data) 84 | resp = self._drv.write(data=data, resp_len=3, transmitter_add_crc=True) 85 | self.comment_data("PPS response:", resp) 86 | if resp[0] == (0xD0 + cid): 87 | self._logger.info("\tPPS accepted") 88 | else: 89 | self._logger.info("\tPPS rejected") 90 | self._logger.info("") 91 | 92 | return resp 93 | 94 | def _init_pcb_block_nb(self): 95 | self._pcb_block_number = 0 96 | 97 | def _inc_pcb_block_number(self): 98 | self._pcb_block_number ^= 1 99 | 100 | def _get_pcb_block_number(self): 101 | return self._pcb_block_number 102 | 103 | def _get_and_update_pcb_block_number(self): 104 | nb = self._get_pcb_block_number() 105 | self._inc_pcb_block_number() 106 | return nb 107 | 108 | def build_rblock(self, ack: bool = True) -> bytes: 109 | return self.build_rblock_ll(ack, self._addCID) 110 | 111 | def build_rblock_ll(self, ack: bool = True, 112 | cid: bool = True, 113 | block_number=None, 114 | add_crc: bool = True) -> bytes: 115 | 116 | pcb = 0xA2 117 | data = "" 118 | if not ack: 119 | pcb |= 0x10 120 | 121 | if cid: 122 | pcb |= 0x08 123 | data = f"{self._cid:02X} " 124 | 125 | if block_number: 126 | pcb |= block_number 127 | else: 128 | pcb |= self.get_and_update_iblock_pcb_number() 129 | 130 | data = f"{pcb:02X}" + data 131 | 132 | return bytes.fromhex(data) 133 | 134 | def build_iblock(self, data, chaining_bit=False): 135 | """ 136 | - 0 137 | - 0 138 | - 0 139 | - Chaining if 1 140 | - CID following if 1 141 | - NAD following if 1 142 | - 1 143 | - Block number 144 | """ 145 | pcb = self.get_and_update_iblock_pcb_number() + 0x02 146 | 147 | cid = "" 148 | if self._addCID: 149 | cid = self._cid 150 | pcb |= 0x08 151 | 152 | if chaining_bit: 153 | pcb |= 0x10 154 | 155 | nad = "" 156 | if self._addNAD: 157 | nad = self._nad 158 | pcb |= 0x04 159 | 160 | header = [pcb] 161 | if nad != "": 162 | header.append(nad) 163 | if cid != "": 164 | header.append(cid) 165 | 166 | return bytes(header) + data 167 | 168 | def chaining_iblock(self, data: bytes = None, block_size: int = None): 169 | 170 | if not block_size: 171 | block_size = self.block_size 172 | 173 | block_lst = [] 174 | fragmented_data_index = range(0, len(data), block_size) 175 | for hit in fragmented_data_index[:-1]: 176 | inf_field = data[hit:hit + block_size] 177 | frame = self.build_iblock(inf_field, chaining_bit=True) 178 | block_lst.append(frame) 179 | 180 | if fragmented_data_index[-1]: 181 | index = fragmented_data_index[-1] 182 | else: 183 | index = 0 184 | inf_field = data[index:index + block_size] 185 | frame = self.build_iblock(inf_field, chaining_bit=False) 186 | block_lst.append(frame) 187 | 188 | return block_lst 189 | 190 | def _send_tpdu(self, tpdu: bytes, add_crc: bool = True) -> bytes: 191 | self._logger.info("\t\t" + "TPDU command:") 192 | for hit in utils.get_pretty_print_block(tpdu): 193 | self._logger.info("\t\t" + hit) 194 | 195 | resp = self._drv.write(data=tpdu, resp_len=16, transmitter_add_crc=add_crc) 196 | 197 | resp = Tpdu(resp) 198 | self._logger.info("\t\t" + "TPDU response:") 199 | for hit in utils.get_pretty_print_block(resp.get_tpdu()): 200 | self._logger.info("\t\t" + hit) 201 | return resp 202 | 203 | def send_apdu(self, apdu): 204 | apdu = bytes.fromhex(apdu) 205 | self._logger.info("APDU command:") 206 | for hit in utils.get_pretty_print_block(apdu): 207 | self._logger.info("\t" + hit) 208 | 209 | block_lst = self.chaining_iblock(data=apdu) 210 | 211 | if len(block_lst) == 1: 212 | resp = self._send_tpdu(block_lst[0]) 213 | else: 214 | self._logger.info("Block chaining, %d blocks to send" % len(block_lst)) 215 | for iblock in block_lst: 216 | resp = self._send_tpdu(iblock) 217 | 218 | while resp.is_wtx(): 219 | wtx_reply = resp.get_wtx_reply() 220 | resp = self._send_tpdu(wtx_reply) 221 | 222 | rapdu = resp.inf 223 | 224 | while resp.is_chaining(): 225 | rblock = self.build_rblock() 226 | 227 | resp = self._send_tpdu(rblock) 228 | 229 | rapdu += resp.inf 230 | 231 | self._logger.info("APDU response:") 232 | for hit in utils.get_pretty_print_block(rapdu): 233 | self._logger.info("\t" + hit) 234 | return rapdu 235 | 236 | def send_raw_bytes(self, data, transmitter_add_crc=True): 237 | self._logger.info("Send Raw Bytes:") 238 | 239 | for hit in utils.get_pretty_print_block(data): 240 | self._logger.info("\t" + hit) 241 | 242 | resp = self._drv.write(data=data, transmitter_add_crc=transmitter_add_crc) 243 | self._logger.info("Response:") 244 | 245 | for hit in utils.get_pretty_print_block(resp): 246 | self._logger.info("\t" + hit) 247 | 248 | return resp 249 | 250 | def comment_data(self, msg, data): 251 | self._logger.info(msg) 252 | for hit in utils.get_pretty_print_block(data): 253 | self._logger.info("\t" + hit) 254 | -------------------------------------------------------------------------------- /pynfcreader/sessions/iso14443/iso14443a.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2020 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from pynfcreader.sessions.iso14443.iso14443 import Iso14443Session 17 | from pynfcreader.tools import utils 18 | 19 | 20 | class Iso14443ASession(Iso14443Session): 21 | 22 | def __init__(self, cid=0, nad=0, drv=None, block_size=16): 23 | Iso14443Session.__init__(self, cid, nad, drv, block_size) 24 | 25 | def connect(self): 26 | self._drv.connect() 27 | self._drv.set_mode_iso14443A() 28 | 29 | def polling(self): 30 | self.send_reqa() 31 | self.send_select_full() 32 | self.send_pps() 33 | 34 | def send_reqa(self): 35 | """ 36 | REQA = REQ frame - Type A 37 | 0x26 - 7 bits - no CRC 38 | 39 | ret : 40 | - nothing 41 | - ATQA (Answer To Request - Type A) 42 | """ 43 | self.comment_data("REQA (7 bits):", b"\x26") 44 | resp = self._drv.write_bits(b'\x26', 7) 45 | if not resp: 46 | raise Exception("REQ A failure") 47 | self.comment_data("ATQA:", resp) 48 | return resp 49 | 50 | def send_wupa(self): 51 | """ 52 | REQA = REQ frame - Type A 53 | 0x26 - 7 bits - no CRC 54 | 55 | ret : 56 | - nothing 57 | - ATQA (Answer To Request - Type A) 58 | """ 59 | self.comment_data("WUPA (7 bits):", [0x52]) 60 | resp = self._drv.write_bits(b'\x52', 7) 61 | self.comment_data("ATQA:", resp) 62 | 63 | def send_select_full(self, fsdi="0", cid="0", do_rats=True): 64 | """ 65 | Select 66 | 0x9320 - 8 bits - no CRC 67 | """ 68 | 69 | uid1 = bytes() 70 | uid2 = bytes() 71 | uid3 = bytes() 72 | 73 | # 93 : Select cascade level 1 74 | # 20 : 2 * 8 + 0 = 16 bits = 2 bytes transmitted 75 | # No CRC 76 | data = bytes([0x93, 0x20]) 77 | self.comment_data("Select cascade level 1:", data) 78 | resp = self._drv.write(data=data, transmitter_add_crc=False) 79 | self.comment_data("Select cascade level 1 response:", resp) 80 | 81 | # resp : CT + UID (3 bytes) + BCC 82 | # 93 : Select cascade level 1 83 | # 70 : 7 * 8 + 0 = 56 bits = 7 bytes transmitted 84 | # 4 bytes : CT + UID 85 | # CRC_A (2 bytes) 86 | uid1 = resp 87 | data = bytes([0x93, 0x70]) + uid1 88 | self.comment_data("Select cascade level 1:", data) 89 | resp = self._drv.write(data=data, resp_len=3, transmitter_add_crc=True) 90 | self.comment_data("Select cascade level 1 response:", resp) 91 | 92 | # if uid1[0] == 0x88: 93 | # 94 | # # resp : SAK + CRC_A 95 | # # Cascade bit set? => UID not complete 96 | # if resp[0] & 0x04: 97 | # # 95 : Select cascade level 2 (SEL) 98 | # # 20 : 2 * 8 + 0 = 16 bits = 2 bytes transmitted 99 | # # No CRC 100 | # data = [0x95, 0x20] 101 | # self.comment_data("Select cascade level 2:", data) 102 | # resp = self.__drv.write(data = data, resp_len = 5) 103 | # self.comment_data("Select cascade level 2 response:", resp) 104 | # 105 | # # resp : 4 bytes (UID) (+BCC not see in this implementation) 106 | # # 95 : Select cascade level 2 (SEL) 107 | # # 70 : 7 * 8 + 0 = 56 bits = 7 bytes transmitted 108 | # # No CRC 109 | # uid2 = resp 110 | # data = [0x95, 0x70] + uid2 111 | # self.comment_data("Select cascade level 2:", data) 112 | # resp = self.__drv.write(data = data, resp_len = 3, crc_in_cmd=False) 113 | # self.comment_data("Select cascade level 2 response:", resp) 114 | # 115 | # # resp : SAK + CRC_A 116 | # # Cascade bit set? => UID not complete 117 | # if resp[0] & 0x04: 118 | # # 97 : Select cascade level 3 (SEL) 119 | # # 20 : 2 * 8 + 0 = 16 bits = 2 bytes transmitted 120 | # # No CRC 121 | # # resp = self.hf14a_raw(data = "9720", leaveSigOnAfterRec= True, fieldMode = "OnWithoutSelect") 122 | # self.__logger.info("Select cascade level 3") 123 | # resp = self.__drv.write(data = [0x97, 0x20], resp_len = 3) 124 | # 125 | # # resp : 4 bytes (UID) + BCC 126 | # # 97 : Select cascade level 32 (SEL) 127 | # # 70 : 7 * 8 + 0 = 56 bits = 7 bytes transmitted 128 | # # No CRC 129 | # uid2 = resp 130 | # # resp = self.hf14a_raw(data = "9770" + resp, leaveSigOnAfterRec= True, fieldMode = "OnWithoutSelect", AutoAddCRC = True) 131 | # self.__logger.info("Select cascade level 3") 132 | # resp = self.__drv.write(data = [0x97, 0x70] + resp, resp_len = 3, crc_in_cmd=False) 133 | # 134 | if do_rats: 135 | # RATS (Request Answer To Select) 136 | resp = self.send_rats_a(fsdi, cid) 137 | else: 138 | None 139 | return uid1 + uid2 + uid3, resp 140 | 141 | def send_rats_a(self, fsdi="0", cid="0"): 142 | """ 143 | Request for answer to select - Type A 144 | 0xE0 - fsdi | cid - CRC_A 145 | :param fsdi: defines the maximum size of a frame the PCD is able to receive. 146 | - 0 : 16 bytes 147 | - 1 : 24 bytes 148 | - 2 : 32 149 | - 3 : 40 150 | - 4 : 48 151 | - 5 : 64 152 | - 6 : 96 153 | - 7 : 128 154 | - 8 : 256 155 | - .. : RFU 156 | :param cid: logical number of the addressed PICC in the range from 0 to 14. 157 | :return: ATS (Answer to select) 158 | """ 159 | dico = {"0": 16, "1": 24, "2": 32, "3": 40, "4": 48, "5": 64, "6": 96, "7": 128, "8": 256} 160 | self._logger.info("Request for Answer To Select (RATS):") 161 | self._logger.info("\tPCD selected options:") 162 | self._logger.info("\t\tFSDI : 0x%s => max PCD frame size : %d bytes" % (fsdi, dico[fsdi])) 163 | self._logger.info("\t\tCID : 0x%s" % fsdi) 164 | 165 | data = bytes([0xE0, int(fsdi + cid, 16)]) 166 | self.comment_data("RATS", data) 167 | resp = self._drv.write(data=data, resp_len=20, transmitter_add_crc=True) 168 | 169 | # resp[0] = TL = length without counting the 2 CRC bytes 170 | resp = resp[:resp[0] + 2] 171 | self.comment_data("Answer to Select (ATS = RATS response):", resp) 172 | 173 | t0 = resp[1] 174 | self._logger.info("\tT0 : 0x%02X", t0) 175 | self._logger.info("\t\tFSCI : 0x%01X => max card frame size : %d bytes" % (t0 & 0xF, dico[str(t0 & 0xF)])) 176 | ta1 = None 177 | tb1 = None 178 | tc1 = None 179 | cmpt = 2 180 | if t0 & 0x10: 181 | self._logger.info("\t\tTA(1) present") 182 | ta1 = resp[cmpt] 183 | cmpt += 1 184 | if t0 & 0x20: 185 | self._logger.info("\t\tTB(1) present") 186 | tb1 = resp[cmpt] 187 | cmpt += 1 188 | if t0 & 0x40: 189 | self._logger.info("\t\tTC(1) present") 190 | tc1 = resp[cmpt] 191 | cmpt += 1 192 | 193 | if ta1: 194 | self._logger.info("\tTA(1) : 0x%02X" % ta1) 195 | self._logger.info("\t\tInterpretation : TODO...") 196 | if tb1: 197 | self._logger.info("\tTB(1) : 0x%02X" % tb1) 198 | self._logger.info("\t\tInterpretation : TODO...") 199 | if tc1: 200 | self._logger.info("\tTC(1) : 0x%02X" % tc1) 201 | self._addNAD = ((tc1 & 0x01) == 0x01) 202 | self._logger.info("\t\tNAD %ssupported" % ((not self._addNAD) * "not ")) 203 | self._addCID = ((tc1 & 0x02) == 0x02) 204 | self._logger.info("\t\tCID %ssupported" % ((not self._addCID) * "not ")) 205 | self._logger.info("\tHistorical bytes : %s" % utils.int_array_to_hex_str(resp[cmpt:-2])) 206 | self._logger.info("\tCRC : %s" % utils.int_array_to_hex_str(resp[-2:])) 207 | 208 | self._logger.info("") 209 | self._logger.info("") 210 | self._logger.info("") 211 | return resp 212 | -------------------------------------------------------------------------------- /pynfcreader/sessions/iso14443/iso14443b.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2020 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from pynfcreader.sessions.iso14443.iso14443 import Iso14443Session 17 | 18 | 19 | class Iso14443BSession(Iso14443Session): 20 | 21 | def __init__(self, cid=0, nad=0, drv=None, block_size=16): 22 | Iso14443Session.__init__(self, cid, nad, drv, block_size) 23 | self.pupi = None 24 | 25 | def connect(self): 26 | self._drv.connect() 27 | self._drv.set_mode_iso14443B() 28 | 29 | def polling(self): 30 | self.send_reqb() 31 | self.send_attrib() 32 | # self.send_select_full() 33 | # self.send_pps() 34 | 35 | def send_reqb(self): 36 | reqb = bytes.fromhex("050000") 37 | self.comment_data("REQB:", reqb) 38 | resp = self._drv.write(reqb, 1) 39 | if not resp: 40 | raise Exception("REQ B failure") 41 | self.comment_data("ATQB:", resp) 42 | self.pupi = resp[1:5] 43 | return resp 44 | 45 | def send_attrib(self, pupi=None): 46 | if pupi is None: 47 | pupi = self.pupi 48 | reqb = bytes.fromhex(f"1D {pupi.hex()} 00 00 01 00") 49 | self.comment_data("REQB:", reqb) 50 | resp = self._drv.write(reqb, 1) 51 | if not resp: 52 | raise Exception("REQ B failure") 53 | self.comment_data("ATQB:", resp) 54 | # self.pupi = resp[1:5] 55 | return resp 56 | -------------------------------------------------------------------------------- /pynfcreader/sessions/iso14443/tpdu.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2020 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | class Tpdu(object): 17 | 18 | def __init__(self, tpdu: bytes): 19 | self.tpdu: bytes = tpdu 20 | 21 | self.pcb, self._nad, self._cid, self._crc = None, None, None, None 22 | self._inf_field: bytes = b"" 23 | self.iblock_is_chaining, self.is_nad_present, self.is_cid_present = False, False, False 24 | self.ack_nack_bit: int = 0 25 | self.block_nb: int = 0 26 | self.i: bool = False 27 | self.r: bool = False 28 | self.s: bool = False 29 | self.parse_block() 30 | 31 | def parse_pcb(self): 32 | pcb = self.pcb 33 | # Iblock 34 | if (pcb & 0xC0) == 0x00: 35 | self.iblock_is_chaining = ((pcb & 0x10) == 0x10) 36 | self.is_nad_present = (pcb & 0x40) == 0x40 37 | self.is_cid_present = (pcb & 0x08) == 0x08 38 | self.i = True 39 | # Rblock 40 | elif (pcb & 0xC0) == 0x80: 41 | self.is_cid_present = (pcb & 0x08) == 0x08 42 | self.is_nad_present = False 43 | self.ack_nack_bit = (pcb & 0x10) >> 4 44 | self.block_nb = pcb & 1 45 | self.r = True 46 | # Sblock 47 | elif (pcb & 0xC0) == 0xC0: 48 | self.is_cid_present = (pcb & 0x08) == 0x08 49 | self.is_nad_present = False 50 | self.s = True 51 | 52 | def parse_block(self): 53 | self.pcb = self.tpdu[0] 54 | self.parse_pcb() 55 | cmpt = 1 56 | if self.is_cid_present: 57 | self._cid = self.tpdu[cmpt] 58 | cmpt += 1 59 | 60 | if self.is_nad_present: 61 | self._nad = self.tpdu[cmpt] 62 | cmpt += 1 63 | 64 | self._inf_field = self.tpdu[cmpt:-2] 65 | self._crc = self.tpdu[-2:] 66 | 67 | def get_tpdu(self): 68 | return self.tpdu 69 | 70 | @property 71 | def inf(self) -> bytes: 72 | return self._inf_field 73 | 74 | @inf.setter 75 | def inf(self, data: bytes): 76 | self._inf_field = data 77 | 78 | def is_chaining(self): 79 | return self.iblock_is_chaining 80 | 81 | def is_wtx(self): 82 | return (self.pcb & 0xF0) == 0xF0 83 | 84 | def get_wtx_reply(self): 85 | 86 | resp = [self.pcb] 87 | 88 | if self.is_cid_present: 89 | resp.append(self._cid) 90 | 91 | if self.is_nad_present: 92 | resp.append(self._nad) 93 | 94 | # inf field 95 | resp.append(self._inf_field[0] & 0x3F) 96 | 97 | return resp 98 | -------------------------------------------------------------------------------- /pynfcreader/sessions/iso15693/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gvinet/pynfcreader/2d77ac041897141bbef4577f45d937c1b2d99ca5/pynfcreader/sessions/iso15693/__init__.py -------------------------------------------------------------------------------- /pynfcreader/sessions/iso15693/iso15693.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2020 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from pynfcreader.sessions.iso15693.requests import \ 16 | RequestInventory, \ 17 | RequestReadSingleBlock, \ 18 | RequestReadMultipleBlocks, \ 19 | RequestGetSystemInformation, \ 20 | RequestStayQuiet, \ 21 | RequestWriteSingleBlock, \ 22 | RequestWriteMultipleBlock, \ 23 | RequestSelect, \ 24 | RequestGetMultipleBlockSecurityStatus 25 | from pynfcreader.tools import utils 26 | 27 | 28 | class Iso15693Session(object): 29 | 30 | def __init__(self, drv=None): 31 | self._drv = drv 32 | self._logger = self._drv.get_logger() 33 | self.last_request = None 34 | self._memory_block = {} 35 | self._lock_status = {} 36 | 37 | def connect(self): 38 | self._drv.connect() 39 | self._drv.set_mode_iso15693() 40 | 41 | def field_on(self): 42 | self._logger.info("field on") 43 | self._drv.field_on() 44 | self._logger.info("") 45 | 46 | def field_off(self): 47 | self._logger.info("field off") 48 | self._drv.field_off() 49 | self._logger.info("") 50 | 51 | def send(self, cmd): 52 | 53 | return self._drv.write(data=cmd, resp_len=16, transmitter_add_crc=True) 54 | 55 | def send_cmd(self, cmd, no_answer=False): 56 | self._logger.info(f"Command {cmd.name}") 57 | 58 | for hit in utils.get_pretty_print_block(cmd()): 59 | self._logger.info(f"{hit}") 60 | 61 | for key, value in cmd.items.items(): 62 | if value != b"": 63 | self._logger.info(f"\t{key:20}: {utils.bytes_to_str(value)}") 64 | self._logger.info("") 65 | 66 | resp = self.send(cmd()) 67 | 68 | self.last_request = cmd 69 | 70 | if no_answer: 71 | return 72 | 73 | self._logger.info("Response:") 74 | if resp: 75 | for hit in utils.get_pretty_print_block(resp): 76 | self._logger.info(hit) 77 | else: 78 | self._logger.info("") 79 | try: 80 | cmd.resp_pretty_print(resp) 81 | except: # noqa: E722 82 | pass 83 | for key, value in cmd.resp.items(): 84 | if value != b"": 85 | self._logger.info( 86 | f"\t{key:25}: {utils.bytes_to_str(value['raw'])}") 87 | if value["pretty"] != "": 88 | self._logger.info(f"\t{' ' * 25}: {value['pretty']}") 89 | self._logger.info("") 90 | 91 | return resp 92 | 93 | def inventory(self, flags=b"\x26", afi_opt=b"", mask=b""): 94 | return self.send_cmd(RequestInventory(flags, afi_opt, mask)) 95 | 96 | def stay_quiet(self, flags=b"\x22", uid=b""): 97 | return self.send_cmd(RequestStayQuiet(flags, uid), no_answer=True) 98 | 99 | def read_single_block(self, flags=b"\x42", uid_opt=b"", block_nb=b""): 100 | return self.send_cmd(RequestReadSingleBlock(flags, uid_opt, block_nb)) 101 | 102 | def write_single_block(self, flags=b"\x42", uid_opt=b"", block_nb=b"", 103 | data=b""): 104 | return self.send_cmd( 105 | RequestWriteSingleBlock(flags, uid_opt, block_nb, data)) 106 | 107 | def get_system_info(self, flags=b"\x22", uid_opt=b""): 108 | return self.send_cmd(RequestGetSystemInformation(flags, uid_opt)) 109 | 110 | def read_multiple_blocks(self, flags=b"\x42", uid_opt=b"", 111 | first_block_nb=b"\x00", nb_blocks=b"\x00"): 112 | return self.send_cmd( 113 | RequestReadMultipleBlocks(flags, uid_opt, first_block_nb, 114 | nb_blocks)) 115 | 116 | def write_multiple_block(self, flags=b"\x02", uid_opt=b"", 117 | first_block_nb=b"\x00", nb_blocks=b"\x00", 118 | data=b""): 119 | return self.send_cmd( 120 | RequestWriteMultipleBlock(flags, uid_opt, first_block_nb, nb_blocks, 121 | data)) 122 | 123 | def select(self, flags=b"\x22", uid=b""): 124 | return self.send_cmd(RequestSelect(flags, uid)) 125 | 126 | def get_multiple_blocks_security_status(self, flags=b"\x02", uid_opt=b"", 127 | first_block_nb=b"\x00", 128 | nb_blocks=b"\x00"): 129 | return self.send_cmd( 130 | RequestGetMultipleBlockSecurityStatus(flags, uid_opt, 131 | first_block_nb, nb_blocks)) 132 | 133 | def get_all_auto(self): 134 | uid = self.inventory()[2:][::-1] 135 | self.get_system_info(uid_opt=uid) 136 | nb_block = self.last_request.nb_block 137 | self.get_all_memory_info(nb_block) 138 | 139 | self._logger.info("\tMemory dump") 140 | self._logger.info("") 141 | 142 | for hit in range(nb_block): 143 | block = utils.int_array_to_hex_str(self._memory_block[hit]) 144 | block_ascii = utils.bytes_to_ascii_printable_str( 145 | self._memory_block[hit]) 146 | status = "Locked " if ( 147 | self._lock_status[hit][0] & 0x1) else "Unlocked" 148 | self._logger.info( 149 | f"\t\t[{hit:3d}] - {status} - {block} | {block_ascii}") 150 | 151 | def get_all_memory_info(self, block_num): 152 | self._logger.info("Get and print all memory") 153 | 154 | self._memory_block = {} 155 | self._lock_status = {} 156 | 157 | self._logger.info("\tGet all memory") 158 | for hit in range(block_num): 159 | block_nb = bytes([hit]) 160 | resp = self.read_single_block(block_nb=block_nb)[2:] 161 | if len(resp) == 0: 162 | self.read_single_block(block_nb=block_nb)[2:] 163 | self._memory_block[hit] = self.last_request.resp["data"]["raw"] 164 | self._lock_status[hit] = \ 165 | self.last_request.resp["block_security_status"]["raw"] 166 | -------------------------------------------------------------------------------- /pynfcreader/sessions/iso15693/requests.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2020 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import collections 16 | from pynfcreader.tools.utils import manufacturer_codes_iso_7816_6 17 | 18 | 19 | class Request(object): 20 | cmd = {b"\x01": "Inventory", 21 | b"\x02": "Stay quiet", 22 | b"\x20": "Read single block", 23 | b"\x21": "Write single block", 24 | b"\x23": "Read multiple blocks", 25 | b"\x24": "Write multiple blocks", 26 | b"\x25": "Select", 27 | b"\x26": "Reset to ready", 28 | b"\x2B": "Get system information", 29 | b"\x2C": "Get multiple block security status"} 30 | 31 | error_code_def = {1: "The command is not supported, i.e. the request code is not recognised.", 32 | 2: "The command is not recognised, for example: a format error occurred.", 33 | 3: "The option is not supported.", 34 | 4: "Unknown error.", 35 | 5: "The specified block is not available (doesn’t exist).", 36 | 6: "The specified block is already -locked and thus cannot be locked again", 37 | 0xF: "Unknown error.", 38 | 0x10: "The specified block is not available (doesn’t exist).", 39 | 0x11: "The specified block is already -locked and thus cannot be locked again", 40 | 0x12: "The specified block is locked and its content cannot be changed.", 41 | 0x13: "The specified block was not successfully programmed.", 42 | 0x14: "The specified block was not successfully locked."} 43 | 44 | def __init__(self, **kwargs): 45 | 46 | self.items = collections.OrderedDict(kwargs) 47 | self.name = self.cmd[kwargs["command"]] 48 | self.resp = collections.OrderedDict() 49 | 50 | def get_error_code(self, error_code): 51 | if error_code == 0: 52 | return "No error" 53 | 54 | if error_code in self.error_code_def.keys(): 55 | return self.error_code_def[error_code] 56 | 57 | if error_code in range(0xA0, 0xE0): 58 | return "Custom command error codes" 59 | 60 | return "DFU" 61 | 62 | def get_pretty_security_status(self, status): 63 | return 'Locked' if status[0] == 1 else 'Unlocked' 64 | 65 | def pretty_print_request_info_flags(self, info_flags): 66 | info_flags_pretty = collections.OrderedDict({"DSFID": {0: "DSFID is not supported. DSFID field is not present", 67 | 1: "DSFID is supported. DSFID field is present"}, 68 | "AFI": {0: "AFI is not supported. AFI field is not present", 69 | 1: "AFI is supported. AFI field is present"}, 70 | "VICC memory size": { 71 | 0: "Information on VICC memory s ize is not supported. Memory size field is not present.", 72 | 1: "Information on VICC memory s ize is supported. Memory size field is present."}, 73 | "IC reference": {0: "Information on IC reference is not supported. IC reference field is not present.", 74 | 1: "Information on IC reference is supported. IC reference field is present."}}) 75 | 76 | pretty = "" 77 | bit = 0 78 | for key in info_flags_pretty.keys(): 79 | pretty += f"{info_flags_pretty[key][(info_flags >> bit) & 1]}\n" 80 | return pretty 81 | 82 | def pretty_print_request_flags(self, flag): 83 | info = collections.OrderedDict() 84 | request_flags_b1_to_b4 = {"Sub-carrier_flag": {0: "A single sub-carrier frequency shall be used by the VICC", 85 | 1: "Two sub-carriers shall be used by the VICC"}, 86 | "Data_rate_flag": {0: "Low data rate shall be used", 87 | 1: "High data rate shall be used"}, 88 | "Inventory_flag": {0: "Inventory flag off", 89 | 1: "Inventory flag on"}, 90 | "Protocol_Extension_flag": {0: "No protocol format extension", 91 | 1: "Protocol format is extended. Reserved for future use"}} 92 | 93 | request_flags_b5_to_b8_inventory = {"AFI_flag": {0: "AFI field is not present", 94 | 1: "AFI field is present"}, 95 | "Nb_slots_flag": {0: "16 slots", 96 | 1: "1 slots"}, 97 | "Option_flag": { 98 | 0: "Meaning is defined by the command description. It shall be set to 0 if not otherwise defined by the command.", 99 | 1: "Meaning is defined by the command description."}, 100 | "RFU": {0: "Shall be set to 0.", 101 | 1: "Shall be set to 0."}} 102 | 103 | request_flags_b5_to_b8_no_inventory = {"Select_flag": {0: "Request shall be executed by any VICC according to the setting of Address_flag", 104 | 1: "Request shall be executed only by VICC in selected state"}, 105 | "Address_flag": {0: "Request is not addressed. UID field is not present. It shall be executed by any VICC.", 106 | 1: "Request is addressed. UID field is present. It shall be executed only by the VICC whose UID matches the UID specified in the request."}, 107 | "Option_flag": { 108 | 0: "Meaning is defined by the command description. It shall be set to 0 if not otherwise defined by the command.", 109 | 1: "Meaning is defined by the command description."}, 110 | "RFU": {0: "Shall be set to 0.", 111 | 1: "Shall be set to 0."}} 112 | 113 | # for bit in range(4): 114 | # print(f"{key:20}: {' '.join(f'{hit:02X}' for hit in value)}") 115 | 116 | def cmd_pretty_print(self): 117 | for key, value in self.items.items(): 118 | if value != b"": 119 | print(f"{key:20}: {' '.join(f'{hit:02X}' for hit in value)}") 120 | 121 | def resp_pretty_print(self, resp=b""): 122 | flags = resp[0:1] 123 | self.resp["flags"] = {"raw": flags, 124 | "pretty": f"{self.get_error_code(flags)} ({flags:02X})"} 125 | 126 | # Error flag? 127 | if resp[0] & 1: 128 | error_code = resp[1:2] 129 | self.resp["error_code"] = {"raw": error_code, 130 | "pretty": f"{self.get_error_code(error_code)} ({error_code:02X})"} 131 | 132 | def __call__(self): 133 | return b"".join(hit for hit in self.items.values() if hit != b"") 134 | 135 | 136 | class RequestInventory(Request): 137 | 138 | def __init__(self, flags=b"\x00", afi_opt=b"", mask_opt=b""): 139 | Request.__init__(self, 140 | flags=flags, 141 | command=b"\x01", 142 | afi_opt=afi_opt, 143 | mask_len=bytes([len(mask_opt)]), 144 | mask_opt=mask_opt) 145 | 146 | def resp_pretty_print(self, resp=b""): 147 | flags = resp[0:1] 148 | self.resp["flags"] = {"raw": flags, 149 | "pretty": f"0x{flags[0]:02X}"} 150 | dsfid = resp[1:2] 151 | self.resp["dsfid"] = {"raw": dsfid, 152 | "pretty": f"0x{dsfid[0]:02X}"} 153 | 154 | uid = resp[2:10] 155 | self.resp["uid"] = {"raw": uid, 156 | "pretty": " ".join(f"{hit:02X}" for hit in resp[2:10][::-1])} 157 | 158 | 159 | class RequestStayQuiet(Request): 160 | 161 | def __init__(self, 162 | flags=b"\x00", 163 | uid=b""): 164 | Request.__init__(self, 165 | flags=flags, 166 | command=b"\x02", 167 | uid=uid) 168 | 169 | 170 | class RequestReadSingleBlock(Request): 171 | 172 | def __init__(self, 173 | flags=b"\x00", 174 | uid_opt=b"", 175 | block_nb=b""): 176 | Request.__init__(self, 177 | flags=flags, 178 | command=b"\x20", 179 | uid_opt=uid_opt, 180 | block_nb=block_nb) 181 | 182 | def resp_pretty_print(self, resp=b""): 183 | flags = resp[0:1] 184 | self.resp["flags"] = {"raw": flags, 185 | "pretty": f"0x{flags[0]:02X}"} 186 | 187 | # Error flag? 188 | if resp[0] & 1: 189 | error_code = resp[1:2] 190 | self.resp["flags"] = {"raw": error_code, 191 | "pretty": f"{self.get_error_code(error_code[0])} ({error_code[0]:02X})"} 192 | else: 193 | if self.items["flags"][0] & 0x40: 194 | self.resp["block_security_status"] = resp[1:2] 195 | block_security_status = resp[1:2] 196 | self.resp["block_security_status"] = {"raw": block_security_status, 197 | "pretty": f"{self.get_pretty_security_status(block_security_status)} ({block_security_status[0]:02X})"} 198 | self.resp["data"] = {"raw": resp[2:], 199 | "pretty": ""} 200 | 201 | 202 | class RequestWriteSingleBlock(Request): 203 | 204 | def __init__(self, 205 | flags=b"\x00", 206 | uid_opt=b"", 207 | block_nb=b"", 208 | data=b""): 209 | Request.__init__(self, 210 | flags=flags, 211 | command=b"\x21", 212 | uid_opt=uid_opt, 213 | block_nb=block_nb, 214 | data=data) 215 | 216 | 217 | class RequestReadMultipleBlocks(Request): 218 | 219 | def __init__(self, 220 | flags=b"\x00", 221 | uid_opt=b"", 222 | first_block_nb=b"\x00", 223 | nb_blocks=b"\x00"): 224 | Request.__init__(self, 225 | flags=flags, 226 | command=b"\x23", 227 | uid_opt=uid_opt, 228 | first_block_nb=first_block_nb, 229 | nb_blocks=nb_blocks) 230 | 231 | if uid_opt: 232 | self.items["uid_opt"] = uid_opt 233 | self.items["first_block_nb"] = first_block_nb 234 | self.items["nb_blocks"] = nb_blocks 235 | 236 | def resp_pretty_print(self, resp=b""): 237 | flags = resp[0:1] 238 | self.resp["flags"] = {"raw": flags, 239 | "pretty": f"0x{flags[0]:02X}"} 240 | 241 | # Error flag? 242 | if resp[0] & 1: 243 | error_code = resp[1:2] 244 | self.resp["flags"] = {"raw": error_code, 245 | "pretty": f"{self.get_error_code(error_code[0])} ({error_code[0]:02X})"} 246 | else: 247 | nb_blocks = self.items["nb_blocks"][0] + 1 248 | chunk_size = len(resp[1:]) // nb_blocks 249 | if self.items["flags"][0] & 0x40: 250 | chunk_size -= 1 251 | cmpt = 2 252 | for hit in range(nb_blocks): 253 | if self.items["flags"][0] & 0x40: 254 | block_security_status = resp[cmpt:cmpt + 1] 255 | cmpt += 1 256 | pretty = f"{self.get_pretty_security_status(block_security_status)} ({block_security_status[0]:02X})" 257 | self.resp[f"block_security_status {hit}"] = {"raw": block_security_status, 258 | "pretty": pretty} 259 | data = resp[cmpt:cmpt + chunk_size] 260 | self.resp[f"block {hit}"] = {"raw": data, 261 | "pretty": b" ".join(f"{hit:02X}" for hit in data)} 262 | cmpt += chunk_size 263 | 264 | 265 | class RequestWriteMultipleBlock(Request): 266 | 267 | def __init__(self, 268 | flags=b"\x00", 269 | uid_opt=b"", 270 | first_block_nb=b"\x00", 271 | nb_blocks=b"\x00", 272 | data=b""): 273 | Request.__init__(self, 274 | flags=flags, 275 | command=b"\x24", 276 | uid_opt=uid_opt, 277 | first_block_nb=first_block_nb, 278 | nb_blocks=nb_blocks, 279 | data=data) 280 | 281 | 282 | class RequestSelect(Request): 283 | 284 | def __init__(self, 285 | flags=b"\x00", 286 | uid=b""): 287 | Request.__init__(self, 288 | flags=flags, 289 | command=b"\x25", 290 | uid=uid) 291 | 292 | 293 | class RequestResetToReady(Request): 294 | 295 | def __init__(self, 296 | flags=b"\x00", 297 | uid_opt=b""): 298 | Request.__init__(self, 299 | flags=flags, 300 | command=b"\x25", 301 | uid_opt=uid_opt) 302 | 303 | 304 | class RequestGetMultipleBlockSecurityStatus(Request): 305 | 306 | def __init__(self, 307 | flags=b"\x00", 308 | uid_opt=b"", 309 | first_block_nb=b"\x00", 310 | nb_blocks=b"\x00"): 311 | Request.__init__(self, 312 | flags=flags, 313 | command=b"\x2C", 314 | uid_opt=uid_opt, 315 | first_block_nb=first_block_nb, 316 | nb_blocks=nb_blocks) 317 | 318 | def resp_pretty_print(self, resp=b""): 319 | flags = resp[0:1] 320 | self.resp["flags"] = {"raw": flags, 321 | "pretty": f"0x{flags[0]:02X}"} 322 | 323 | # Error flag? 324 | if resp[0] & 1: 325 | error_code = resp[1:2] 326 | self.resp["flags"] = {"raw": error_code, 327 | "pretty": f"{self.get_error_code(error_code[0])} ({error_code[0]:02X})"} 328 | else: 329 | nb = self.items["nb_blocks"][0] + 1 330 | for hit in range(1, len(resp)): 331 | block_security_status = resp[hit:hit + 1] 332 | pretty = f"{self.get_pretty_security_status(block_security_status)} ({block_security_status[0]:02X})" 333 | self.resp[f"block_security_status {nb}"] = {"raw": block_security_status, 334 | "pretty": pretty} 335 | nb += 1 336 | 337 | 338 | class RequestGetSystemInformation(Request): 339 | 340 | def __init__(self, flags=b"\x00", uid_opt=b""): 341 | Request.__init__(self, 342 | flags=flags, 343 | command=b"\x2B", 344 | uid_opt=uid_opt[::-1]) 345 | 346 | self.nb_block = None 347 | self.block_byte_size = None 348 | self.ic_reference = None 349 | 350 | def resp_pretty_print(self, resp=b""): 351 | flags = resp[0:1] 352 | self.resp["flags"] = {"raw": flags, 353 | "pretty": f"0x{flags[0]:02X}"} 354 | 355 | # Error flag? 356 | if resp[0] & 1: 357 | error_code = resp[1:2] 358 | self.resp["flags"] = {"raw": error_code, 359 | "pretty": f"{self.get_error_code(error_code[0])} ({error_code[0]:02X})"} 360 | else: 361 | info_flags = resp[1:2] 362 | self.resp["info_flags"] = {"raw": info_flags, 363 | "pretty": self.pretty_print_request_info_flags(info_flags[0])} 364 | 365 | uid = resp[2:10] 366 | self.resp["uid"] = {"raw": uid, 367 | "pretty": " ".join(f"{hit:02X}" for hit in resp[2:10][::-1])} 368 | 369 | cmpt = 10 370 | 371 | if resp[1] & 1: 372 | dsfid = resp[cmpt:cmpt + 1] 373 | self.resp["dsfid"] = {"raw": dsfid, 374 | "pretty": " ".join(f"{hit:02X}" for hit in dsfid)} 375 | cmpt += 1 376 | 377 | if resp[1] & 2: 378 | afi = resp[cmpt:cmpt + 1] 379 | self.resp["afi"] = {"raw": afi, 380 | "pretty": " ".join(f"{hit:02X}" for hit in dsfid)} 381 | cmpt += 1 382 | 383 | if resp[1] & 4: 384 | vicc_memory_size = resp[cmpt:cmpt + 2] 385 | self.nb_block = resp[cmpt] + 1 386 | cmpt += 1 387 | self.block_byte_size = (resp[cmpt] & 0x1F) + 1 388 | cmpt += 1 389 | 390 | self.resp["vicc_memory_size"] = {"raw": vicc_memory_size, 391 | "pretty": f"{self.nb_block} blocks of {self.block_byte_size} bytes"} 392 | 393 | if resp[1] & 8: 394 | ic_reference = resp[cmpt:cmpt + 1] 395 | try: 396 | ic_reference_pretty = manufacturer_codes_iso_7816_6(ic_reference.hex().upper()) 397 | except: # noqa: E722 398 | ic_reference_pretty = "Unknown code" 399 | self.resp["ic_reference"] = {"raw": ic_reference, 400 | "pretty": ic_reference_pretty} 401 | -------------------------------------------------------------------------------- /pynfcreader/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gvinet/pynfcreader/2d77ac041897141bbef4577f45d937c1b2d99ca5/pynfcreader/tools/__init__.py -------------------------------------------------------------------------------- /pynfcreader/tools/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2020 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import re 17 | 18 | import crcmod 19 | 20 | manufacturer_codes_iso_7816_6 = { 21 | "01": "Motorola", 22 | "02": "ST Microelectronics", 23 | "03": "Hitachi", 24 | "04": "NXP Semiconductors", 25 | "05": "Infineon Technologies", 26 | "06": "Cylinc", 27 | "07": "Texas Instruments Tag-it", 28 | "08": "Fujitsu Limited", 29 | "09": "Matsushita Electric Industrial", 30 | "0A": "NEC", 31 | "0B": "Oki Electric", 32 | "0C": "Toshiba", 33 | "0D": "Mitsubishi Electric", 34 | "0E": "Samsung Electronics", 35 | "0F": "Hyundai Electronics", 36 | "10": "LG Semiconductors", 37 | "16": "EM Microelectronic-Marin", 38 | "1F": "Melexis", 39 | "2B": "Maxim", 40 | "33": "AMIC"} 41 | 42 | 43 | def int_array_to_hex_str(array): 44 | return " ".join(f"{hit:02X}" for hit in array) 45 | 46 | 47 | def bytes_to_ascii_printable_str(data): 48 | data = data.decode(encoding='ascii', errors='replace') 49 | return re.sub(r'[^\x20-\x7E]', '.', data) 50 | 51 | 52 | def bytes_to_str(data_b): 53 | return ' '.join(f'{hit:02X}' for hit in data_b) 54 | 55 | 56 | def get_pretty_print_block(msg): 57 | lst = [] 58 | 59 | for hit in range(0, len(msg), 16): 60 | block1 = msg[hit:hit + 8] 61 | hex1 = " ".join(f"{c:02X}" for c in block1) 62 | str1 = bytes_to_ascii_printable_str(block1) 63 | block2 = msg[hit + 8:hit + 16] 64 | hex2 = " ".join(f"{c:02X}" for c in block2) 65 | str2 = bytes_to_ascii_printable_str(block2) 66 | 67 | lpart = f"{hex1} {hex2}" 68 | lst.append(f'{lpart}{" " * (49 - len(lpart))} {str1} {str2}') 69 | return lst 70 | 71 | 72 | def crc_iso14443a_get(data: bytes) -> bytes: 73 | crc16 = crcmod.mkCrcFun(0x11021, initCrc=0x6363, xorOut=0x0000, rev=True) 74 | return crc16(data).to_bytes(2, "little") 75 | 76 | def crc_iso14443a_append(data: bytes) -> bytes: 77 | crc16 = crcmod.mkCrcFun(0x11021, initCrc=0x6363, xorOut=0x0000, rev=True) 78 | return data + crc16(data).to_bytes(2, "little") 79 | 80 | def crc_iso14443a_check(data:bytes) -> bool: 81 | crc16 = crcmod.mkCrcFun(0x11021, initCrc=0x6363, xorOut=0x0000, rev=True) 82 | return crc16(data[:-2]).to_bytes(2, "little") == data[-2:] -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020 Guillaume VINET 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import setuptools 17 | 18 | import pynfcreader 19 | 20 | with open("README.md", "r") as fh: 21 | long_description = fh.read() 22 | 23 | name = 'pyNFCReader' 24 | version = str(pynfcreader.__version__) 25 | release = str(version) 26 | 27 | setuptools.setup( 28 | name=name, 29 | version=pynfcreader.__version__, 30 | author="Guillaume VINET", 31 | description="NFC reader", 32 | long_description=long_description, 33 | long_description_content_type="text/markdown", 34 | url="https://github.com/gvinet/pynfcreader", 35 | packages=setuptools.find_packages(), 36 | install_requires=['pyHydrabus'], 37 | classifiers=[ 38 | "Programming Language :: Python :: 3", 39 | "License :: OSI Approved :: Apache Software License", 40 | "Operating System :: OS Independent", 41 | ], 42 | ) 43 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gvinet/pynfcreader/2d77ac041897141bbef4577f45d937c1b2d99ca5/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pynfcreader.devices.hydra_nfc_v2 import HydraNFCv2 3 | 4 | 5 | @pytest.fixture 6 | def hydranfc_connection(): 7 | return HydraNFCv2(port="/dev/ttyACM0", debug=False) 8 | -------------------------------------------------------------------------------- /tests/tests_iso_14443_a_card_1_hydranfc_v2.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from pynfcreader.sessions.iso14443.iso14443a import Iso14443ASession 4 | 5 | 6 | def test_iso_14443_a_card_1_generic(hydranfc_connection): 7 | hn = Iso14443ASession(drv=hydranfc_connection, block_size=120) 8 | 9 | hn.connect() 10 | hn.field_off() 11 | time.sleep(0.1) 12 | hn.field_on() 13 | hn.polling() 14 | 15 | r = hn.send_apdu("00 a4 04 00 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 00") 16 | assert b'oW\x84\x0e2PAY.S.DDF01\xa5E\xbf\x0cBO\x07\xa0\x00\x00\x00B\x10\x10P\x02\x87\x01\x01\x9f(\x08@\x02\x00\x00\x00\x00a#O\x07\xa0\x00\x00\x00\x04\x10\nMASTERCARD\x02\x9f(\x08@\x00 \x00\x00\x00\x00' == r 17 | 18 | r = hn.send_apdu("00 a4 04 00 07 A0 00 00 00 42 10 10 00") 19 | assert b'o?\x84\x07\xa0\x00\x00\x00B\x104P\x02CB\x87\x01\x01\x9f\x11\x01\x12\x0eTransacti CB_-\x04fren\xbf\xdf`\x02\x0b\x14\x9fM\x02\x0b\x14\xdf\x04' == r 20 | 21 | r = hn.send_apdu("00 a4 04 00 07 A0 00 00 00 04 10 10 00") 22 | assert b'o?\x84\x07\xa0\x00\x00\x00\x04\x104P\nMASTERCA\x87\x01\x02\x9f\x11\x01\x01\x9f\x12\nMTERCARD_-\x04fn\xbf\x0c\n\xdf`\x02\x0b\x14\x9fM\x14' == r 23 | 24 | hn.field_off() 25 | -------------------------------------------------------------------------------- /tests/tests_iso_14443_b_card_2_hydranfc_v2.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from pynfcreader.sessions.iso14443.iso14443b import Iso14443BSession 4 | 5 | 6 | def test_iso_14443_a_card_1_generic(hydranfc_connection): 7 | hn = Iso14443BSession(drv=hydranfc_connection, block_size=120) 8 | 9 | hn.connect() 10 | hn.field_off() 11 | time.sleep(0.1) 12 | hn.field_on() 13 | hn.polling() 14 | 15 | r = hn.send_apdu("00 a4 04 00 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 00") 16 | assert b'o[\x84\x0e2PAY.S.DDF01\xa5I\xbf\x0cFO\x07\xa0\x00\x00\x00B\x10\x10P\x02\x87\x01\x01\x9f*\x01\x02\x9f\n\x08\x00\x01\x00\x00\x00\x00a#O\x07\xa0\x00\x04\x10\x10P\nMASTERRD\x87\x01\x01\x9f\n\x08\x00\x01\x05\x00\x00\x00' == r 17 | 18 | hn.field_off() 19 | -------------------------------------------------------------------------------- /wiki/iso14443/iso_14443_b.md: -------------------------------------------------------------------------------- 1 | # ISO 14443 B 2 | 3 | ## REQB/WUPB 4 | 5 | | Byte 1 | Byte 2 | Byte 3 | Byte 4 - 5 | 6 | |----------|--------------|--------------|-----------------| 7 | | APf=0x05 | AFI | Param | CRC_B | 8 | --------------------------------------------------------------------------------