apdu.swift (13846B)
1 // 2 // apdu.swift 3 // HCEtest 4 // 5 // Created by Marc Stibane on 2024-07-27. 6 // 7 8 import Foundation 9 import os.log 10 11 enum APDUError: Error { 12 case invalidStringValue 13 } 14 15 @MainActor 16 class APDU { 17 let talerUri: String // contains only ASCII, never Unicode 18 nonisolated let logger = Logger(subsystem: "net.taler.gnu", category: "APDU") 19 private var readCapabilityContainerCheck = false 20 21 init(_ talerUri: String) throws { 22 // NDEF lengths count bytes, not Characters, and the emulated file must stay within 23 // the maximum NDEF file size advertised in READ_CAPABILITY_CONTAINER_RESPONSE 24 if talerUri.utf8.count < 255 { // TODO: < 8192 25 self.talerUri = talerUri 26 } else { 27 throw APDUError.invalidStringValue 28 } 29 } 30 31 private var APDU_SELECT: Data { 32 Data(fromUInt8Array: [ 33 0x00, // CLA - Class - Class of instruction 34 0xA4, // INS - Instruction - Instruction code 35 0x04, // P1 - Parameter 1 - Instruction parameter 1 36 0x00, // P2 - Parameter 2 - Instruction parameter 2 37 0x07, // Lc field - Number of bytes present in the data field of the command 38 0xD2, // NDEF Tag Application name 39 0x76, 40 0x00, 41 0x00, 42 0x85, 43 0x01, 44 0x01, 45 0x00, // Le field - Maximum number of bytes expected in the data field of the response to the command 46 ]) 47 } // 00a4 0400 07 d2760000850101 00 48 49 private var CAPABILITY_CONTAINER_OK: Data { 50 Data(fromUInt8Array: [ 51 0x00, // CLA - Class - Class of instruction 52 0xA4, // INS - Instruction - Instruction code 53 0x00, // P1 - Parameter 1 - Instruction parameter 1 54 0x0C, // P2 - Parameter 2 - Instruction parameter 2 55 0x02, // Lc field - Number of bytes present in the data field of the command 56 0xE1, // file identifier of the CC file 57 0x03, 58 ]) 59 } // 00a4 000c 02 e103 60 61 private var READ_CAPABILITY_CONTAINER: Data { 62 Data(fromUInt8Array: [ 63 0x00, // CLA - Class - Class of instruction 64 0xB0, // INS - Instruction - Instruction code 65 0x00, // P1 - Parameter 1 - Instruction parameter 1 66 0x00, // P2 - Parameter 2 - Instruction parameter 2 67 0x0F, // Lc field - Number of bytes present in the data field of the command 68 ]) 69 } // 00b0 0000 0f 70 71 private var READ_CAPABILITY_CONTAINER_RESPONSE: Data { 72 Data(fromUInt8Array: [ 73 0x00, 0x0f, // CCLEN length of the CC file 74 0x20, // Mapping Version 2.0 75 0x00, 0x3B, // MLe maximum 76 0x00, 0x34, // MLc maximum 77 0x04, // T field of the NDEF File Control TLV 78 0x06, // L field of the NDEF File Control TLV 79 0xE1, 0x04, // File Identifier of NDEF file 80 0x00, 0xFE, // Maximum NDEF file size of 254 bytes // TODO: 8191 81 // 0x20, 0x09, // Maximum NDEF file size: 8201 = 2 NLEN + 8 record header + 8191 URI bytes 82 0x00, // Read access without any security 83 0xFF, // Write access without any security 84 0x90, 0x00, // A_OKAY 85 ]) 86 } //000f 20 003b 0034 04 06 e104 00ff 00ff9000 87 88 private var NDEF_SELECT_OK: Data { 89 Data(fromUInt8Array: [ 90 0x00, // CLA - Class - Class of instruction 91 0xA4, // Instruction byte (INS) for Select command 92 0x00, // Parameter byte (P1), select by identifier 93 0x0C, // Parameter byte (P2), select by identifier 94 0x02, // Lc field - Number of bytes present in the data field of the command 95 0xE1, 96 0x04, // file identifier of the NDEF file retrieved from the CC file 97 ]) 98 } // 00a4 000c 02 e104 99 100 private var NDEF_READ_BINARY_NLEN: Data { 101 Data(fromUInt8Array: [ 102 0x00, // Class byte (CLA) 103 0xB0, // Instruction byte (INS) for ReadBinary command 104 0x00, 105 0x00, // Parameter byte (P1, P2), offset inside the CC file 106 0x02, // Le field 107 ]) 108 } // 00b0 0000 02 109 110 private var NDEF_READ_BINARY: Data { 111 Data(fromUInt8Array: [ 112 0x00, // Class byte (CLA) 113 0xB0, // Instruction byte (INS) for ReadBinary command 114 ]) 115 } // 00b0 116 // private let NDEF_READ_BINARY_DATA = Data(fromUInt8Array: NDEF_READ_BINARY) 117 118 private var NDEF_RECORD_HEADER_SHORT: Data { 119 Data(fromUInt8Array: [ 120 0xD1, // 1101 + 0001 = MessageBegin, MessageEnd, ShortRecord + Format "Well-Known" (but no ID Length) 121 0x01, // TypeLength 122 // 0x00, // Payload Length 1 byte only since ShortRecord is true 123 // no ID Length byte since the IDlength flag is false 124 // 0x55, // Payload Type = “U” for URI, 1 byte as specified in the TypeLength field 125 // since we have no ID length, there is no Payload ID 126 ]) 127 } // d101 128 129 private var NDEF_RECORD_HEADER_LONG: Data { 130 Data(fromUInt8Array: [ 131 0xC1, // 1100 + 0001 = MessageBegin, MessageEnd, no ShortRecord + Format "Well-Known" (but no ID Length) 132 0x01, // TypeLength 133 // 0x00, 0x00, 0x00, 0x00, // Payload Length 4 bytes since ShortRecord is false 134 // no ID Length byte since the IDlength flag is false 135 // 0x55, // Payload Type = “U” for URI, 1 byte as specified in the TypeLength field 136 // since we have no ID length, there is no Payload ID 137 ]) 138 } // c101 139 140 private var A_OKAY_ARRAY: [UInt8] { 141 [ 142 0x90, // SW1 Status byte 1 - Command processing status 143 0x00, // SW2 Status byte 2 - Command processing qualifier 144 ] 145 } // 9000 = OK 146 private var A_OKAY: Data { 147 Data(fromUInt8Array: A_OKAY_ARRAY) 148 } 149 150 private var A_ERROR: Data { 151 Data(fromUInt8Array: [ 152 0x6A, // SW1 Status byte 1 - Command processing status 153 0x82, // SW2 Status byte 2 - Command processing qualifier 154 ]) 155 } // 6A82 = File not found 156 157 private var APPLICATION_ERROR: Data { 158 Data(fromUInt8Array: [ 159 0x6A, // SW1 Status byte 1 - Command processing status 160 0x88, // SW2 Status byte 2 - Command processing qualifier 161 ]) 162 } // 6A88 = no application ID 163 164 private var GET_VERSION: Data { 165 Data(fromUInt8Array: [ 166 0x90, // CLA - Class - Class of instruction 167 0x60, // INS - Instruction - Instruction code 168 0x00, // P1 - Parameter 1 - Instruction parameter 1 169 0x00, // P2 - Parameter 2 - Instruction parameter 2 170 0x00, // Lc field - Number of bytes present in the data field of the command 171 ]) 172 } // 9060 0000 00 173 174 private var GET_VERSION_RESPONSE: Data { 175 Data(fromUInt8Array: [ 176 0x00, // fixed header 177 0x04, // vendor ID (04 = NXP Semiconductors) 178 0x04, // product type (04 = NTAG) 179 0x02, // product subtype (02 = NTAG215) 180 0x01, // major product version 181 0x00, // minor product version 182 0x11, // storage size 183 0x03, // protocol type: ISO/IEC 14443-3 compliant 184 0x90, 0x00, // A_OKAY 185 ]) 186 } // 187 188 private var MORE_INFO: Data { 189 Data(fromUInt8Array: [ 190 0x90, // CLA - Class - Class of instruction 191 0xAF, // INS - Instruction - Instruction code 192 0x00, // P1 - Parameter 1 - Instruction parameter 1 193 0x00, // P2 - Parameter 2 - Instruction parameter 2 194 0x00, // Lc field - Number of bytes present in the data field of the command 195 ]) 196 } // 90af 0000 00 197 198 /// process the received data and return a response as Data. 199 func processAPDU(_ commandApdu: Data) -> Data { 200 /// The following flow is based on Appendix E "Example of Mapping Version 2.0 Command Flow" 201 /// in the NFC Forum specification 202 logger.info("incoming commandApdu: \(commandApdu.hexEncodedString())") 203 204 /// First command: NDEF Tag Application select 205 if APDU_SELECT == commandApdu { 206 logger.info("APDU_SELECT triggered. Our Response: A_OKAY") 207 return A_OKAY 208 } // 00a4040007d276000085010100 209 210 /// Second command: Capability Container select 211 if CAPABILITY_CONTAINER_OK == commandApdu { 212 logger.info("CAPABILITY_CONTAINER_OK triggered. Our Response: A_OKAY") 213 return A_OKAY 214 } // 00a4000c02e103 215 216 /// Third command: ReadBinary data from CC file 217 if READ_CAPABILITY_CONTAINER == commandApdu && !readCapabilityContainerCheck { 218 logger.info("READ_CAPABILITY_CONTAINER triggered. Our Response: READ_CAPABILITY_CONTAINER_RESPONSE") 219 readCapabilityContainerCheck = true 220 return READ_CAPABILITY_CONTAINER_RESPONSE 221 } // 00b000000f 222 223 /// Fourth command: NDEF Select command 224 if NDEF_SELECT_OK == commandApdu { 225 logger.info("NDEF_SELECT_OK triggered. Our Response: A_OKAY") 226 return A_OKAY 227 } // 00a4000c02e104 228 229 // The NDEF payload is the URI identifier code byte 0 followed by the URI itself. 230 // A short record can only express a payload length of one byte, so anything longer 231 // has to go into a long record, whose payload length field is four bytes wide. 232 let payloadLen = talerUri.utf8.count + 1 233 let shortRecord = payloadLen <= 0xFF 234 // record header + TypeLength + payload length + "U" + payload 235 let ndefLen = UInt16(payloadLen + (shortRecord ? 4 : 7)) 236 /// Fifth Command: Read the Length of the NDEF File // 00b0 00 00 02 237 if NDEF_READ_BINARY_NLEN == commandApdu { 238 logger.info("NDEF_READ_BINARY_NLEN triggered. Our Response: length \(ndefLen) + A_OKAY") 239 readCapabilityContainerCheck = false 240 241 let ndefLenHi = UInt8(ndefLen >> 8) 242 let ndefLenLo = UInt8(ndefLen & 0xff) 243 let ndefLenLoData = withUnsafeBytes(of: ndefLenLo) { Data($0) } 244 var lenData = withUnsafeBytes(of: ndefLenHi) { Data($0) } 245 lenData.append(ndefLenLoData) 246 lenData.append(A_OKAY_ARRAY, count: 2) 247 return lenData 248 } // 00b0000002 249 250 guard commandApdu.count >= 2 else { return A_ERROR } 251 /// Sixth (and seventh) Command: Read the NDEF File // 00b0 00 02 3b, and 00b0 00 3d 27 252 if NDEF_READ_BINARY == commandApdu[0..<2] { 253 let count = commandApdu.count 254 if count > 4 { 255 var arrayApdu = Array<UInt8>(repeating: 0, count: count) 256 _ = arrayApdu.withUnsafeMutableBytes { commandApdu.copyBytes(to: $0) } 257 258 let wantedLength = UInt(arrayApdu[4]) 259 let offset = UInt(arrayApdu[3]) + (UInt(arrayApdu[2]) << 8) 260 261 var data = shortRecord ? NDEF_RECORD_HEADER_SHORT // 2 bytes 262 : NDEF_RECORD_HEADER_LONG // 2 bytes 263 let lenData: Data 264 if shortRecord { 265 let payloadLen8 = UInt8(payloadLen) 266 lenData = withUnsafeBytes(of: payloadLen8) { Data($0) } // 1 byte 267 } else { 268 let payloadLen32 = UInt32(payloadLen).bigEndian // MSB first 269 lenData = withUnsafeBytes(of: payloadLen32) { Data($0) } // 4 bytes 270 } 271 data.append(lenData) 272 data.append("U".data(using: .utf8)!) // 1 byte 273 let zero: UInt8 = 0 274 let zeroData = withUnsafeBytes(of: zero) { Data($0) } 275 data.append(zeroData) // 1 byte 276 data.append(talerUri.data(using: .utf8)!) 277 278 var result = A_ERROR 279 let lengthBytes = UInt(2) 280 // ensure wanted data starts after two length bytes 281 if offset >= lengthBytes { 282 // Swift.Data doesn't have the length bytes within the data 283 let diffOffset = offset - lengthBytes 284 let count = UInt(data.count) 285 // ensure wanted data doesn't exceed buffer 286 if diffOffset + wantedLength <= count { 287 logger.info("NDEF_READ_BINARY offset \(offset), length \(wantedLength) triggered. Our Response: data + A_OKAY") 288 result = Data(fromData: data, 289 offset: diffOffset, 290 size: wantedLength) 291 result.append(A_OKAY) // 2 bytes 292 } 293 } 294 readCapabilityContainerCheck = false 295 return result 296 } 297 } 298 299 // Reject GET_VERSION and MORE_INFO 300 if GET_VERSION == commandApdu { 301 logger.info("❗️GET_VERSION triggered. Our Response: GET_VERSION_RESPONSE❗️") 302 return GET_VERSION_RESPONSE 303 } 304 if MORE_INFO == commandApdu { 305 logger.info("❗️MORE_INFO triggered. Our Response: A_OKAY❗️") 306 return A_OKAY 307 } 308 309 // 310 // We're doing something outside our scope 311 // 312 logger.error("❗️processAPDU() | not yet implemented!") 313 return A_ERROR 314 } // processAPDU() 315 316 }