taler-ios

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

apdu.swift (12389B)


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