taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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 }