Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data access without PIN #34

Closed
priska96 opened this issue Sep 4, 2024 · 20 comments
Closed

Data access without PIN #34

priska96 opened this issue Sep 4, 2024 · 20 comments

Comments

@priska96
Copy link

priska96 commented Sep 4, 2024

Hello gematik,

Is it possible to also access the data that is not protected by the PIN? Based on the current documentation it's not clear for me.

Currently I am trying to implement reading the insurance number, Card holder name, card holder address via NFC for ios devices in React Native.

Any help would be great!

@sigabrtz
Copy link
Contributor

sigabrtz commented Sep 6, 2024

Hi, you certainly can access some (but not all) personal data.

Please refer to #9 for more details. This should already help you out.

#24 also has some interesting findings.

In any case the user needs to provide his/her card’s CAN (card access number) for secure channel establishing.

@priska96
Copy link
Author

priska96 commented Sep 7, 2024

Hello,
Thank you for the references and your answer!
There are many things I don't really understand in the code and some better documentation would be really appreciated. I am fairly new to native development.

This part for example is not explained anywhere, I think. I was wondering why we have to sign the card with a payload and what kind of payload actually needs to be sent. Because ABC seems more liike a debug string?

//NFCDemo - NFCLoginController.swift
 session.updateAlert(message: NSLocalizedString("nfc_txt_msg_signing", comment: ""))
         let outcome = try await session.card.sign(
             payload: "ABC".data(using: .utf8)!, // swiftlint:disable:this force_unwrapping
             checkAlgorithm: checkBrainpoolAlgorithm
         )

I was wondering if you could explain to me with a proper example how the sent commands and responses can be interpreted.

In the beginning the following cmds are sent and responses are received.

SEND:     [00A4040CD2760001448000|ne:-1]
RESPONSE: [9000]
SEND:     [00B09C00|ne:256]
RESPONSE: [31143012060A04007F0007020204020202010202010D9000]

I would like to know how to decode these strings. I tried to check the gem Spezifikation but didn't really know where to start

@priska96
Copy link
Author

priska96 commented Sep 9, 2024

Also at wich point, can I start sending commands? When I check the DemoApp
in the login function this part of code starts already sending multiple commands to the healthCard. But as far as I understood the calling of executerOperation() is for establishing a secure PACE channel with the card?
Also what is the signedData that the method returns?

let signedData: Data
      do {
          // tag::nfcHealthCardSession_execute[]
          signedData = try await nfcHealthCardSession.executeOperation()
          // end::nfcHealthCardSession_execute[]

          Task { @MainActor in self.pState = .value(true) }
          // tag::nfcHealthCardSession_errorHandling[]
      } catch NFCHealthCardSessionError.coreNFC(.userCanceled) {
          // error type is always `NFCHealthCardSessionError`
          // here we especially handle when the user canceled the session
          Task { @MainActor in self.pState = .idle } // Do some view-property update
          // Calling .invalidateSession() is not strictly necessary
          //  since nfcHealthCardSession does it while it's de-initializing.
          nfcHealthCardSession.invalidateSession(with: nil)
          return
      } catch {
          Task { @MainActor in self.pState = .error(error) }
          nfcHealthCardSession.invalidateSession(with: error.localizedDescription)
          return
      }

@priska96
Copy link
Author

Hello @sigabrtz I think I got the correct data but I don't know how to decode the hexString.
I am reading the contents of the PD file and have some hexString value returned.

In the issues you tagged are some ideas and suggestions about decoding the data, but it doesn't seem clear to me.

If you could provide proper documentation how to decode the hexValues that would be great.

@priska96
Copy link
Author

@sigabrtz Hello again. I am still struggeling to find the correct way of decoding the data read from the HCA.PD file. Any kind of help would be highly appreciated!
Currently my code looks as follows:

func selectAndReadHcaPD() async throws -> Data {
    
    CommandLogger.commands.append(Command(message: "Select Personal Data and Read file", type: .description))
    let dedicatedFile = DedicatedFile(
      aid: EgkFileSystem.DF.HCA.aid,
      fid: EgkFileSystem.EF.hcaPD.fid // holds Personal Data of health card holder
    )
    let (responseStatus, fileControlParameter) = try await self.selectDedicatedAsync(file: dedicatedFile, fcp: true )
    print("responseStatus ", responseStatus)
    print("fileControlParam ", fileControlParameter ?? "nil")
    
    guard let fcp = fileControlParameter, let readSize = fcp.readSize
    else {
      throw ReadError.fcpMissingReadSize(state: responseStatus)
    }
    let data = try await self.readSelectedFileAsync(expected: Int(readSize))

    // unzip and decode here
    /*do{
     let lengthBytes = data.prefix(2)
     let length = UInt16(lengthBytes.withUnsafeBytes { $0.load(as: UInt16.self) })
     let compressedData = data.suffix(from: 1)//.prefix(Int(length))
     
     let uncompressedData = try data.gunzipped()
     
     if let xmlString = String(data: uncompressedData, encoding: .isoLatin1) {
     print("Read XML OutputData of MF/DF.HCA/EF.hcaPD: \(xmlString)")
     } else {
     fatalError("Error converting data to string")
     }
     }
     catch{
     fatalError("Error decompressing gzip data \(error)")
     }
     */
    
    return data // return xmlString
  }
}

@sigabrtz
Copy link
Contributor

sigabrtz commented Oct 7, 2024

Hi @priska96, I don't know that neither off the top of my head. Have you gotten any further by now?

I'll try to work it out and possibly extend the app. Reading the PD from card (or doing anything without entry of the PIN) is a common use case that should be showcased in the demo app/code for reference as well.

I see that the signing/login part you asked about is a bit confusing... What actually is accomplished here is to transmit some data to the card so that the card can execute a signing operation on it and we receive it's result. In this demo code we transmit just static debug-string (as you correctly assumed).

In real life (our E-Rezept app) we transmit a string we just requested from a identification server and use that result (the signed data) to eventually login to the data server. So that's the background on the sign/login confusion. I'll probably change a button label to mitigate that.

@priska96
Copy link
Author

priska96 commented Oct 7, 2024

@sigabrtz Hello again!
Yes I figured out a lot. And I'm happy to post my solution for reading the PD file. It is not the cleanest especially because I couldn't use gZip Library to unzip the actual data and I hat to implement my own decompress function.

However I realized, that the insurance number is not part of the data. It's not a big deal as the insurance number is already printed on the card. But I am wondering, if there is a file on the card that holds the insurance number. Do you know that by any chance?

Here now is my extended code:


//NFCReader.swift

func _readPersonalData(readPersonalDataOptions: ReadPersonalDataOptions) async {
    let can = readPersonalDataOptions.can //RCTConvert.nsString(readPersonalDataOptions["can"]) ?? "123456"
    
    if case .loading = await nfcReaderState { return }
    self.nfcReaderState = .loading(nil)
    
    // try open session and init operation
    // tag::nfcHealthCardSession_init[]
    guard let nfcHealthCardSession = NFCHealthCardSession(
      messages: messages,
      can: can!,
      operation: { session in
        session.updateAlert(message: NSLocalizedString("nfc_txt_selectHca", comment: ""))
        let outcome = try await session.card.selectAndReadHcaPD()
        
        return outcome
      }
    )
            
            // handle the case the Session could not be initialized
    else {
      // end::nfcHealthCardSession_init[]
      self.nfcReaderState = .error(NFCHealthCardSessionError.couldNotInitializeSession)
      let result = ReadingResult(result: self.nfcReaderState, commands: CommandLogger.commands)
      self.results.append(result)
      
      return
    }
    
    let personalDataRaw: Data
    let personalDataDict: [String:String]
    let personalData : PersonalData
    
    // excetue previously inited operation
    do {
      
      // tag::nfcHealthCardSession_execute[]
      personalDataRaw = try await nfcHealthCardSession.executeOperation()
      // end::nfcHealthCardSession_execute[]
      
      personalDataDict = try parseData(dataRaw: personalDataRaw)
      personalData = PersonalData(dictionary: personalDataDict)
      
      self.nfcReaderState = .value(.dictionary(personalData))
      
      let result = ReadingResult(result: self.nfcReaderState, commands: CommandLogger.commands)
      self.results.append(result)
      
      // tag::nfcHealthCardSession_errorHandling[]
    } catch NFCHealthCardSessionError.coreNFC(.userCanceled) {
      // error type is always `NFCHealthCardSessionError`
      // here we especially handle when the user canceled the session
      
      Logger.nfcDemo.debug("User cancled session. Reset state to idle")
      self.nfcReaderState = .idle
      // Do some view-property update
      // Calling .invalidateSession() is not strictly necessary
      //  since nfcHealthCardSession does it while it's de-initializing.
      nfcHealthCardSession.invalidateSession(with: nil)
      return
    } catch {
      self.nfcReaderState = .error(error)
      let result = ReadingResult(result: self.nfcReaderState, commands: CommandLogger.commands)
      self.results.append(result)
      
      nfcHealthCardSession.invalidateSession(with: error.localizedDescription)
      return
    }
    // end::nfcHealthCardSession_errorHandling[]
    Logger.nfcDemo.debug("Perosnal Data: \(personalData)")
  }
  
  func parseData(dataRaw: Data) throws -> [String:String]{
    var data = dataRaw
    // remove 2 bytes indicating length
    data.removeFirst(2)
    
    // uncompress and decode
    do{
      let uncompressedData = try decompressZlib(data)
      
      do{
        let xmlString = String(data: uncompressedData, encoding: .utf8)
        print("Read XML OutputData of MF/DF.HCA/EF.hcaPD: \(xmlString ?? "")")
        
        do{
          let extractor = XMLDataExtractor()
          let parsedData = try extractor.parse(xmlString: xmlString!)
          
          print("Extracted Data: \(parsedData ?? ["":""])")
          return parsedData!
        } catch {
          throw ParseResponseDataError.xmlParserError(error as! XMLParsingError)
        }
      } catch {
        throw ParseResponseDataError.convertToXMLString
      }
    }
    catch{
      throw ParseResponseDataError.zLibError(error as! ZlibError)
    }
  }
  
}


extension HealthCardType {
  
  func selectAndReadHcaPD() async throws -> Data {
    CommandLogger.commands.append(Command(message: "Select Personal Data and Read file", type: .description))
    
    let dedicatedFile = DedicatedFile(
      aid: EgkFileSystem.DF.HCA.aid,
      fid: EgkFileSystem.EF.hcaPD.fid // holds Personal Data of health card holder
    )
    let (responseStatus, fileControlParameter) = try await self.selectDedicatedAsync(file: dedicatedFile, fcp: true )
    
    guard let fcp = fileControlParameter, let readSize = fcp.readSize
    else {
      throw ReadError.fcpMissingReadSize(state: responseStatus)
    }
    let data = try await self.readSelectedFileAsync(expected: Int(readSize))
    return data
    
  }
  }
  
  
  //CustomZLib.swift
  
  private enum DataSize {
  
  static let chunk = 1 << 14
  static let stream = MemoryLayout<z_stream>.size
}

func decompressZlib(_ data: Data) throws -> Data {
  let bufferSize = 64 * 1024  // 64KB buffer size
  var decompressedData = Data()
  
  // Create a source buffer from the input data
  try data.withUnsafeBytes { (sourceBuffer: UnsafeRawBufferPointer) in
    guard let sourcePointer = sourceBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
      throw ZlibError.streamError
    }
    
    // Compression stream setup with all fields initialized
    var stream = z_stream(
      next_in: UnsafeMutablePointer<UInt8>(mutating: sourcePointer),
      avail_in: uInt(data.count),
      total_in: 0,
      next_out: nil,
      avail_out: 0,
      total_out: 0,
      msg: nil,
      state: nil,
      zalloc: nil,
      zfree: nil,
      opaque: nil,
      data_type: 0,
      adler: 0,
      reserved: 0
    )
    
    // Initialize decompression (use 32 for automatic zlib/gzip header detection)
    let initStatus = inflateInit2_(&stream, 32 + MAX_WBITS, ZLIB_VERSION, Int32(DataSize.stream))
    guard initStatus == Z_OK else {
      throw ZlibError.initializationFailed
    }
    
    // Perform the decompression
    var status: Int32 = Z_NULL
    repeat {
      // Output buffer
      var destinationBuffer = [UInt8](repeating: 0, count: bufferSize)
      
      // Use withUnsafeMutableBytes to get a mutable pointer to the destination buffer
      try destinationBuffer.withUnsafeMutableBytes { (destBuffer: UnsafeMutableRawBufferPointer) in
        guard let destinationPointer = destBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
          throw ZlibError.streamError
        }
        
        stream.next_out = destinationPointer
        stream.avail_out = uInt(bufferSize)
        
        // Decompress
        status = inflate(&stream, Z_NO_FLUSH)
        
        if status == Z_STREAM_ERROR {
          throw ZlibError.decompressionFailed
        }
      }
      
      // Append to decompressed data
      let bytesDecompressed = bufferSize - Int(stream.avail_out)
      decompressedData.append(destinationBuffer, count: bytesDecompressed)
      
    } while status != Z_STREAM_END
    
    // Finalize the decompression
    let endStatus = inflateEnd(&stream)
    guard endStatus == Z_OK else {
      throw ZlibError.incompleteDecompression
    }
  }
  
  return decompressedData
}


@sigabrtz
Copy link
Contributor

sigabrtz commented Oct 7, 2024

Thanks for sharing. This will help me figure out some stuff as well.

According to the xsd schema definition UC_PersoenlicheVersichertendatenXML.xsd (link) the PD xml data contains one element UC_PersoenlicheVersichertendatenXML.Versicherter.Versicherten_ID that holds the Versichertennummer.

<xs:documentation>Die Versicherten-ID ist der 10-stellige unveraenderliche Teil der 30-stelligen Krankenversichertennummer.</xs:documentation>

<xs:simpleType name="insurantIdType">
		<xs:annotation>
			<xs:documentation>1. Stelle: Alpha-Zeichen (Wertebereich A - Z, ohne Umlaute), 2. bis 9. Stelle: 8-stellige lfd. Zaehlnummer (Eine Ziffernfolge, in der mehr als drei gleiche Ziffern hintereinander auftreten, ist auszuschliessen), 10. Stelle: Pruefziffer</xs:documentation>
		</xs:annotation>
		<xs:restriction base="xs:string">
			<xs:pattern value="[A-Z][0-9]{8}[0-9]"/>
		</xs:restriction>
	</xs:simpleType>

@priska96
Copy link
Author

priska96 commented Oct 7, 2024

Oh, yes you are right! I simply just did not see that entry in the xml xD My bad!!

Very welcome! I'm glad I could be of some help. Do you perhaps also know if some file on the eGK hold information about which insurance company the person is registered at? That's also of course visible by just looking at the card. If it's a DAK-Gesundheit, or AOK or Barma etc. But again, it would be nice to read that data automatically as well. I checked the file (schema_VSD.xsd) you posted earlier and there it says smth about "UC_AllgemeineVersicherungsdatenXML" element and "Versicherter.Versicherungsschutz.Kostentraeger"

I just don't know which file on the card holds this "UC_AllgemeineVersicherungsdatenXML". Because on the PD file I can only find the personal data "UC_PersoenlicheVersichertendatenXML" of the card holder.

@sigabrtz
Copy link
Contributor

sigabrtz commented Oct 8, 2024

Do you perhaps also know if some file on the eGK hold information about which insurance company the person is registered at?

UC_PersoenlicheVersichertendatenXML.xsd states that there is a child Kostentraegerkennung which holds the information "Gibt den Kostentraeger des Versicherten an. Es handelt sich um das bundesweit gueltige Institutionskennzeichen (IK) des jeweiligen Kostentraegers."

You'll need a up-to-date list of IK-to-insurance-mapping I guess.
https://de.wikipedia.org/wiki/Institutionskennzeichen

@priska96
Copy link
Author

priska96 commented Oct 8, 2024

Okay thank you I will look into that.

Another thing popped up.
I am building this module so I can later install it in my expo app and it works just fine with my config plugin and everything. A thing that needs to be optimized however is the dependencies to the gematik OpenHealthCardKit.
So far I handled the dependencies manually and added the frameworks (HealthCardAccess.framework, NFCCardReaderProvider.framework, etc.) to my module ios folder and referenced them in my .podspec like so:

s.vendored_frameworks = 'ios/*.framework' #include all frameworks from gematik

That was due to the fact that your frameworks can only be added via SPM and there was no way to add an spm_dependency to a .podspec so far. I need the podspec since I am building a react native npm package.

A week ago react native added spm_dependency for the podspec. Which is great because now I can reference your frameworks like so:

spm_dependency(s,  
     url: "https://github.com/gematik/ref-OpenHealthCardKit", 
     requirement: {kind: "upToNextMajorVersion", minimumVersion: "5.6.0"}, 
     products: ["CardReaderProviderApi", "HealthCardAccess", "HealthCardControl", "Helper", "NFCCardReaderProvider"] 
   ) #include gemtaik frameworks via spm

However, I run into this error with OpenSSL.framework. I am not very familiar with xcode and the build process.

Library not loaded: @rpath/OpenSSL.framework/OpenSSL
Referenced from: <E7AC0210-177A-3DE1-8B7F-6A635A0B947E> /Users/USER/Library/Developer/CoreSimulator/Devices/7BBBC3E6-9E19-4FCB-8A20-32C0CF08D840/data/Containers/Bundle/Application/820E30D7-3CAB-41DE-B5D9-7835C81F91A8/exponativeconfigurationexample.app/Frameworks/gk_nfc_reader.framework/gk_nfc_reader
Reason: tried: 
'/Library/Developer/CoreSimulator/Volumes/iOS_21F79/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.5.simruntime/Contents/Resources/RuntimeRoot/usr/lib/swift/OpenSSL.framework/OpenSSL' (no such file), 
'/usr/lib/swift/OpenSSL.framework/OpenSSL' (no such file, not in dyld cache), 
'/Library/Developer/CoreSimulator/Volumes/iOS_21F79/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.5.simruntime/Contents/Resources/RuntimeRoot/Users/priskakohnen/Library/Developer/Xcode/DerivedData/exponativeconfigurationexample-efbhjbqhfqxnwpgtldkkolknmopi/Build/Products/Debug-iphonesimulator/gk-nfc-reader/PackageFrameworks/OpenSSL.framework

It assumes that the framework should be found in the PackagesFrameworks folder of my module that I am building (the gk-nfc-reader). Which is not the case though. I am wondering if there might be some bug on your side when the HealthCardAccess and HealthCardControl frameworks are build ?

@sigabrtz
Copy link
Contributor

sigabrtz commented Oct 9, 2024

Could you open a new issue for that problem? Seems interesting.

@priska96
Copy link
Author

priska96 commented Oct 9, 2024

Yes sure!

EDIT:
here is the newly created issue

OpenSSL not found

@priska96
Copy link
Author

priska96 commented Oct 9, 2024

@sigabrtz Another thing I'm curios about. In all your additional documents and files about the eGK is there a section about how the response data of the specific files is decoded?
Because after reading the PD file and getting the data back, I would have never come up with the idea to cut off the first 2 bytes of the response and then unzip the data like I am doing in the parseData function that I posted earlier.

I only figured it out, because some other guy in another issue posted this solution and I am really wonderin how he figured that out.

@sigabrtz
Copy link
Contributor

sigabrtz commented Oct 9, 2024

The first two bytes indicate the length of the data. That is (possibly a bit arbitrarily but) it's specified here: https://gemspec.gematik.de/docs/gemSpec/gemSpec_eGK_Fach_VSDM/gemSpec_eGK_Fach_VSDM_V1.2.1/#2.4

So you actually have to read these two bytes (before "cut off") to know how much further to read your data input for the "real" information.

In the smart card world such data+lentgth encoding is not uncommon, there is even a standard ASN.1 that is used by smart cards and in PKI that also employs this length-encoding.

We (gematik) have our own implentation of this standard https://github.com/gematik/ASN1Kit for now.
Apple (or swift-lang) launched theirs way later https://github.com/apple/swift-asn1/tree/main.

@priska96
Copy link
Author

priska96 commented Oct 9, 2024

Ohhh I was looking for that file!! Thanks a lot!!

In the smart card world such data+lentgth encoding is not uncommon, there is even a standard ASN.1 that is used by smart cards and in PKI that also employs this length-encoding.

I didn't know about that. It's actually my first time working with Swift, SmartCards etc.

Thanks for your help and this awesome repo!

@sigabrtz
Copy link
Contributor

I extended the demo App target by the use case "read personal data" (still in PR phase).
#36

In hindsight this would have helped out quite a bit I suppose.

I think this issue is resolved then?

@priska96
Copy link
Author

Great! Thanks a lot! I haven't checked it yet, but It would be great if you could also include proper decoding/unzipping of the data to get human readable data ^^
Also what is interesting to add as a further use case: Some of the information is PIN protected. I was wondering if you could add another use case of reading a PIN protected file.

An yes, this issue is resolved now! Thanks a lot!

@sigabrtz
Copy link
Contributor

but It would be great if you could also include proper decoding/unzipping of the data to get human readable data

Maybe I get something wrong, but it's included.

Some of the information is PIN protected. I was wondering if you could add another use case of reading a PIN protected file

Check out the other use cases how the PIN is transmitted to card. After that the card is in an "opened up" state (during the session) and you have access to more data and applications (like signing).

@priska96
Copy link
Author

Oh yes you are absolutely right! I guess I misunderstood the code. Thanks again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants