mirror of
https://github.com/selfxyz/self.git
synced 2026-02-15 16:55:17 -05:00
now reading and sending proof
This commit is contained in:
368
app/ios/NFCPassportReader/AES_3DES_DESEncryption.swift
Normal file
368
app/ios/NFCPassportReader/AES_3DES_DESEncryption.swift
Normal file
@@ -0,0 +1,368 @@
|
||||
import Foundation
|
||||
import CommonCrypto
|
||||
|
||||
|
||||
/// Encrypts a message using AES/CBC/NOPADDING with a specified key and initialisation vector
|
||||
/// - Parameter key: Key use to encrypt
|
||||
/// - Parameter message: Message to encrypt
|
||||
/// - Parameter iv: Initialisation vector
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func AESEncrypt(key:[UInt8], message:[UInt8], iv:[UInt8]) -> [UInt8] {
|
||||
|
||||
let dataLength = message.count
|
||||
|
||||
let cryptLen = message.count + kCCBlockSizeAES128
|
||||
var cryptData = Data(count: cryptLen)
|
||||
|
||||
let keyLength = size_t(key.count)
|
||||
let operation: CCOperation = CCOperation(kCCEncrypt)
|
||||
let algorithm: CCAlgorithm = CCAlgorithm(kCCAlgorithmAES)
|
||||
let options: CCOptions = CCOptions(0)
|
||||
|
||||
var numBytesEncrypted = 0
|
||||
|
||||
var cryptStatus: CCCryptorStatus = CCCryptorStatus(kCCSuccess)
|
||||
key.withUnsafeBytes {keyBytes in
|
||||
message.withUnsafeBytes{ dataBytes in
|
||||
iv.withUnsafeBytes{ ivBytes in
|
||||
cryptData.withUnsafeMutableBytes{ cryptBytes in
|
||||
|
||||
cryptStatus = CCCrypt(operation,
|
||||
algorithm,
|
||||
options,
|
||||
keyBytes.baseAddress,
|
||||
keyLength,
|
||||
ivBytes.baseAddress,
|
||||
dataBytes.baseAddress,
|
||||
dataLength,
|
||||
cryptBytes.bindMemory(to: UInt8.self).baseAddress,
|
||||
cryptLen,
|
||||
&numBytesEncrypted)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cryptStatus == kCCSuccess {
|
||||
cryptData.count = Int(numBytesEncrypted)
|
||||
|
||||
return [UInt8](cryptData)
|
||||
} else {
|
||||
Log.error("AES Encrypt Error: \(cryptStatus)")
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/// Decrypts a message using AES/CBC/NOPADDING with a specified key and initialisation vector
|
||||
/// - Parameter key: Key use to decrypt
|
||||
/// - Parameter message: Message to decrypt
|
||||
/// - Parameter iv: Initialisation vector
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func AESDecrypt(key:[UInt8], message:[UInt8], iv:[UInt8]) -> [UInt8] {
|
||||
var fixedKey = key
|
||||
if key.count == 16 {
|
||||
fixedKey += key[0..<8]
|
||||
}
|
||||
|
||||
let data = Data(message)
|
||||
let dataLength = message.count
|
||||
|
||||
let cryptLen = data.count + kCCBlockSizeAES128
|
||||
var cryptData = Data(count: cryptLen)
|
||||
|
||||
let keyLength = size_t(key.count)
|
||||
let operation: CCOperation = UInt32(kCCDecrypt)
|
||||
let algorithm: CCAlgorithm = UInt32(kCCAlgorithmAES)
|
||||
let options: CCOptions = UInt32(0)
|
||||
|
||||
var numBytesEncrypted = 0
|
||||
|
||||
let cryptStatus = fixedKey.withUnsafeBytes {keyBytes in
|
||||
message.withUnsafeBytes{ dataBytes in
|
||||
cryptData.withUnsafeMutableBytes{ cryptBytes in
|
||||
CCCrypt(operation,
|
||||
algorithm,
|
||||
options,
|
||||
keyBytes.baseAddress,
|
||||
keyLength,
|
||||
iv,
|
||||
dataBytes.baseAddress,
|
||||
dataLength,
|
||||
cryptBytes.bindMemory(to: UInt8.self).baseAddress,
|
||||
cryptLen,
|
||||
&numBytesEncrypted)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cryptStatus == kCCSuccess {
|
||||
cryptData.count = Int(numBytesEncrypted)
|
||||
|
||||
return [UInt8](cryptData)
|
||||
} else {
|
||||
Log.error("AES Decrypt Error: \(cryptStatus)")
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/// Decrypts a message using AES/ECB/NOPADDING with a specified key and initialisation vector
|
||||
/// - Parameter key: Key use to decrypt
|
||||
/// - Parameter message: Message to decrypt
|
||||
/// - Parameter iv: Initialisation vector
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func AESECBEncrypt(key:[UInt8], message:[UInt8]) -> [UInt8] {
|
||||
|
||||
let dataLength = message.count
|
||||
|
||||
let cryptLen = message.count + kCCBlockSizeAES128
|
||||
var cryptData = Data(count: cryptLen)
|
||||
|
||||
let keyLength = size_t(key.count)
|
||||
let operation: CCOperation = CCOperation(kCCEncrypt)
|
||||
let algorithm: CCAlgorithm = CCAlgorithm(kCCAlgorithmAES)
|
||||
let options: CCOptions = CCOptions(kCCOptionECBMode)
|
||||
|
||||
var numBytesEncrypted = 0
|
||||
|
||||
let cryptStatus = key.withUnsafeBytes {keyBytes in
|
||||
message.withUnsafeBytes{ dataBytes in
|
||||
cryptData.withUnsafeMutableBytes{ cryptBytes in
|
||||
|
||||
CCCrypt(operation,
|
||||
algorithm,
|
||||
options,
|
||||
keyBytes.baseAddress,
|
||||
keyLength,
|
||||
nil,
|
||||
dataBytes.baseAddress,
|
||||
dataLength,
|
||||
cryptBytes.bindMemory(to: UInt8.self).baseAddress,
|
||||
cryptLen,
|
||||
&numBytesEncrypted)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cryptStatus == kCCSuccess {
|
||||
cryptData.count = Int(numBytesEncrypted)
|
||||
|
||||
return [UInt8](cryptData)
|
||||
} else {
|
||||
Log.error("AESECBEncrypt Error: \(cryptStatus)")
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/// Encrypts a message using DES3 with a specified key and initialisation vector
|
||||
/// - Parameter key: Key use to encrypt
|
||||
/// - Parameter message: Message to encrypt
|
||||
/// - Parameter iv: Initialisation vector
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func tripleDESEncrypt(key:[UInt8], message:[UInt8], iv:[UInt8]) -> [UInt8] {
|
||||
// Fix key data - if length is 16 then take the first 98 bytes and append them to the end to make 24 bytes
|
||||
var fixedKey = key
|
||||
if key.count == 16 {
|
||||
fixedKey += key[0..<8]
|
||||
}
|
||||
|
||||
let dataLength = message.count
|
||||
|
||||
let cryptLen = message.count + kCCBlockSize3DES
|
||||
var cryptData = Data(count: cryptLen)
|
||||
|
||||
let keyLength = size_t(kCCKeySize3DES)
|
||||
let operation: CCOperation = UInt32(kCCEncrypt)
|
||||
let algorithm: CCAlgorithm = UInt32(kCCAlgorithm3DES)
|
||||
let options: CCOptions = UInt32(0)
|
||||
|
||||
var numBytesEncrypted = 0
|
||||
|
||||
let cryptStatus = fixedKey.withUnsafeBytes {keyBytes in
|
||||
message.withUnsafeBytes{ dataBytes in
|
||||
iv.withUnsafeBytes{ ivBytes in
|
||||
cryptData.withUnsafeMutableBytes{ cryptBytes in
|
||||
CCCrypt(operation,
|
||||
algorithm,
|
||||
options,
|
||||
keyBytes.baseAddress,
|
||||
keyLength,
|
||||
ivBytes.baseAddress,
|
||||
dataBytes.baseAddress,
|
||||
dataLength,
|
||||
cryptBytes.bindMemory(to: UInt8.self).baseAddress,
|
||||
cryptLen,
|
||||
&numBytesEncrypted)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cryptStatus == kCCSuccess {
|
||||
cryptData.count = Int(numBytesEncrypted)
|
||||
|
||||
return [UInt8](cryptData)
|
||||
} else {
|
||||
Log.error("Error: \(cryptStatus)")
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/// Decrypts a message using DES3 with a specified key and initialisation vector
|
||||
/// - Parameter key: Key use to decrypt
|
||||
/// - Parameter message: Message to decrypt
|
||||
/// - Parameter iv: Initialisation vector
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func tripleDESDecrypt(key:[UInt8], message:[UInt8], iv:[UInt8]) -> [UInt8] {
|
||||
var fixedKey = key
|
||||
if key.count == 16 {
|
||||
fixedKey += key[0..<8]
|
||||
}
|
||||
|
||||
let data = Data(message)
|
||||
let dataLength = message.count
|
||||
|
||||
let cryptLen = data.count + kCCBlockSize3DES
|
||||
var cryptData = Data(count: cryptLen)
|
||||
|
||||
let keyLength = size_t(kCCKeySize3DES)
|
||||
let operation: CCOperation = UInt32(kCCDecrypt)
|
||||
let algorithm: CCAlgorithm = UInt32(kCCAlgorithm3DES)
|
||||
let options: CCOptions = UInt32(0)
|
||||
|
||||
var numBytesEncrypted = 0
|
||||
|
||||
let cryptStatus = fixedKey.withUnsafeBytes {keyBytes in
|
||||
message.withUnsafeBytes{ dataBytes in
|
||||
cryptData.withUnsafeMutableBytes{ cryptBytes in
|
||||
CCCrypt(operation,
|
||||
algorithm,
|
||||
options,
|
||||
keyBytes.baseAddress,
|
||||
keyLength,
|
||||
iv,
|
||||
dataBytes.baseAddress,
|
||||
dataLength,
|
||||
cryptBytes.bindMemory(to: UInt8.self).baseAddress,
|
||||
cryptLen,
|
||||
&numBytesEncrypted)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cryptStatus == kCCSuccess {
|
||||
cryptData.count = Int(numBytesEncrypted)
|
||||
|
||||
return [UInt8](cryptData)
|
||||
} else {
|
||||
Log.error("Error: \(cryptStatus)")
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
/// Encrypts a message using DES with a specified key and initialisation vector
|
||||
/// - Parameter key: Key use to encrypt
|
||||
/// - Parameter message: Message to encrypt
|
||||
/// - Parameter iv: Initialisation vector
|
||||
/// - Parameter options: Encryption options to use
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func DESEncrypt(key:[UInt8], message:[UInt8], iv:[UInt8], options:UInt32 = 0) -> [UInt8] {
|
||||
|
||||
let dataLength = message.count
|
||||
|
||||
let cryptLen = message.count + kCCBlockSizeDES
|
||||
var cryptData = Data(count: cryptLen)
|
||||
|
||||
let keyLength = size_t(kCCKeySizeDES)
|
||||
let operation: CCOperation = UInt32(kCCEncrypt)
|
||||
let algorithm: CCAlgorithm = UInt32(kCCAlgorithmDES)
|
||||
let options: CCOptions = options
|
||||
|
||||
var numBytesEncrypted = 0
|
||||
|
||||
let cryptStatus = key.withUnsafeBytes {keyBytes in
|
||||
message.withUnsafeBytes{ dataBytes in
|
||||
iv.withUnsafeBytes{ ivBytes in
|
||||
cryptData.withUnsafeMutableBytes{ cryptBytes in
|
||||
CCCrypt(operation,
|
||||
algorithm,
|
||||
options,
|
||||
keyBytes.baseAddress,
|
||||
keyLength,
|
||||
ivBytes.baseAddress,
|
||||
dataBytes.baseAddress,
|
||||
dataLength,
|
||||
cryptBytes.bindMemory(to: UInt8.self).baseAddress,
|
||||
cryptLen,
|
||||
&numBytesEncrypted)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cryptStatus == kCCSuccess {
|
||||
cryptData.count = Int(numBytesEncrypted)
|
||||
|
||||
return [UInt8](cryptData)
|
||||
} else {
|
||||
Log.error("Error: \(cryptStatus)")
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/// Decrypts a message using DES with a specified key and initialisation vector
|
||||
/// - Parameter key: Key use to decrypt
|
||||
/// - Parameter message: Message to decrypt
|
||||
/// - Parameter iv: Initialisation vector
|
||||
/// - Parameter options: Decryption options to use
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func DESDecrypt(key:[UInt8], message:[UInt8], iv:[UInt8], options:UInt32 = 0) -> [UInt8] {
|
||||
|
||||
let dataLength = message.count
|
||||
|
||||
let cryptLen = message.count + kCCBlockSizeDES
|
||||
var cryptData = Data(count: cryptLen)
|
||||
|
||||
let keyLength = size_t(kCCKeySizeDES)
|
||||
let operation: CCOperation = UInt32(kCCDecrypt)
|
||||
let algorithm: CCAlgorithm = UInt32(kCCAlgorithmDES)
|
||||
let options: CCOptions = options
|
||||
|
||||
var numBytesEncrypted = 0
|
||||
|
||||
let cryptStatus = key.withUnsafeBytes {keyBytes in
|
||||
message.withUnsafeBytes{ dataBytes in
|
||||
iv.withUnsafeBytes{ ivBytes in
|
||||
cryptData.withUnsafeMutableBytes{ cryptBytes in
|
||||
CCCrypt(operation,
|
||||
algorithm,
|
||||
options,
|
||||
keyBytes.baseAddress,
|
||||
keyLength,
|
||||
nil,
|
||||
dataBytes.baseAddress,
|
||||
dataLength,
|
||||
cryptBytes.bindMemory(to: UInt8.self).baseAddress,
|
||||
cryptLen,
|
||||
&numBytesEncrypted)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cryptStatus == kCCSuccess {
|
||||
cryptData.count = Int(numBytesEncrypted)
|
||||
|
||||
return [UInt8](cryptData)
|
||||
} else {
|
||||
Log.error("Error: \(cryptStatus)")
|
||||
}
|
||||
return []
|
||||
}
|
||||
@@ -1,11 +1,3 @@
|
||||
//
|
||||
// BACHandler.swift
|
||||
// NFCTest
|
||||
//
|
||||
// Created by Andy Qua on 07/06/2019.
|
||||
// Copyright © 2019 Andy Qua. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
//
|
||||
// ChipAuthenticationHandler.swift
|
||||
// NFCPassportReader
|
||||
//
|
||||
// Created by Andy Qua on 25/02/2021.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OpenSSL
|
||||
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
//
|
||||
// DataGroupHash.swift
|
||||
// NFCPassportReader
|
||||
//
|
||||
// Created by Andy Qua on 09/02/2021.
|
||||
// Copyright © 2021 Andy Qua. All rights reserved.
|
||||
//
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public struct DataGroupHash {
|
||||
public var id: String
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
//
|
||||
// DataGroupParser.swift
|
||||
//
|
||||
// Created by Andy Qua on 14/06/2019.
|
||||
//
|
||||
|
||||
import OpenSSL
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import Foundation
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class ActiveAuthenticationInfo : SecurityInfo {
|
||||
|
||||
var oid : String
|
||||
var version : Int
|
||||
var signatureAlgorithmOID : String?
|
||||
|
||||
static func checkRequiredIdentifier(_ oid : String) -> Bool {
|
||||
return ID_AA_OID == oid
|
||||
}
|
||||
|
||||
init(oid: String, version: Int, signatureAlgorithmOID: String? = nil) {
|
||||
self.oid = oid
|
||||
self.version = version
|
||||
self.signatureAlgorithmOID = signatureAlgorithmOID
|
||||
}
|
||||
|
||||
public override func getObjectIdentifier() -> String {
|
||||
return oid
|
||||
}
|
||||
|
||||
public override func getProtocolOIDString() -> String {
|
||||
return ActiveAuthenticationInfo.toProtocolOIDString(oid:oid)
|
||||
}
|
||||
|
||||
public func getSignatureAlgorithmOIDString() -> String? {
|
||||
return ActiveAuthenticationInfo.toSignatureAlgorithmOIDString(oid: signatureAlgorithmOID)
|
||||
}
|
||||
|
||||
private static func toProtocolOIDString(oid : String) -> String {
|
||||
if ID_AA_OID == oid {
|
||||
return "id-AA"
|
||||
}
|
||||
|
||||
return oid
|
||||
}
|
||||
|
||||
private static func toSignatureAlgorithmOIDString(oid: String?) -> String? {
|
||||
if (ECDSA_PLAIN_SHA1_OID == oid) {
|
||||
return "ecdsa-plain-SHA1";
|
||||
}
|
||||
if (ECDSA_PLAIN_SHA224_OID == oid) {
|
||||
return "ecdsa-plain-SHA224";
|
||||
}
|
||||
if (ECDSA_PLAIN_SHA256_OID == oid) {
|
||||
return "ecdsa-plain-SHA256";
|
||||
}
|
||||
if (ECDSA_PLAIN_SHA384_OID == oid) {
|
||||
return "ecdsa-plain-SHA384";
|
||||
}
|
||||
if (ECDSA_PLAIN_SHA512_OID == oid) {
|
||||
return "ecdsa-plain-SHA512";
|
||||
}
|
||||
if (ECDSA_PLAIN_RIPEMD160_OID == oid) {
|
||||
return "ecdsa-plain-RIPEMD160";
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
59
app/ios/NFCPassportReader/DataGroups/COM.swift
Normal file
59
app/ios/NFCPassportReader/DataGroups/COM.swift
Normal file
@@ -0,0 +1,59 @@
|
||||
import Foundation
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class COM : DataGroup {
|
||||
public private(set) var version : String = "Unknown"
|
||||
public private(set) var unicodeVersion : String = "Unknown"
|
||||
public private(set) var dataGroupsPresent : [String] = []
|
||||
|
||||
required init( _ data : [UInt8] ) throws {
|
||||
try super.init(data)
|
||||
datagroupType = .COM
|
||||
}
|
||||
|
||||
override func parse(_ data: [UInt8]) throws {
|
||||
var tag = try getNextTag()
|
||||
if tag != 0x5F01 {
|
||||
throw NFCPassportReaderError.InvalidResponse
|
||||
}
|
||||
|
||||
// Version is 4 bytes (ascii) - AABB
|
||||
// AA is major number, BB is minor number
|
||||
// e.g. 48 49 48 55 -> 01 07 -> 1.7
|
||||
var versionBytes = try getNextValue()
|
||||
if versionBytes.count == 4 {
|
||||
let aa = Int( String(cString: Array(versionBytes[0..<2] + [0]) )) ?? -1
|
||||
let bb = Int( String(cString: Array(versionBytes[2...] + [0])) ) ?? -1
|
||||
if aa != -1 && bb != -1 {
|
||||
version = "\(aa).\(bb)"
|
||||
}
|
||||
}
|
||||
tag = try getNextTag()
|
||||
if tag != 0x5F36 {
|
||||
throw NFCPassportReaderError.InvalidResponse
|
||||
}
|
||||
|
||||
versionBytes = try getNextValue()
|
||||
if versionBytes.count == 6 {
|
||||
let aa = Int( String(cString: Array(versionBytes[0..<2] + [0])) ) ?? -1
|
||||
let bb = Int( String(cString: Array(versionBytes[2..<4] + [0])) ) ?? -1
|
||||
let cc = Int( String(cString: Array(versionBytes[4...]) + [0]) ) ?? -1
|
||||
if aa != -1 && bb != -1 && cc != -1 {
|
||||
unicodeVersion = "\(aa).\(bb).\(cc)"
|
||||
}
|
||||
}
|
||||
|
||||
tag = try getNextTag()
|
||||
if tag != 0x5C {
|
||||
throw NFCPassportReaderError.InvalidResponse
|
||||
}
|
||||
|
||||
let vals = try getNextValue()
|
||||
for v in vals {
|
||||
if let index = DataGroupParser.tags.firstIndex(of: v) {
|
||||
dataGroupsPresent.append( DataGroupParser.dataGroupNames[index] )
|
||||
}
|
||||
}
|
||||
Log.debug( "DG Found - \(dataGroupsPresent)" )
|
||||
}
|
||||
}
|
||||
31
app/ios/NFCPassportReader/DataGroups/CardAccess.swift
Normal file
31
app/ios/NFCPassportReader/DataGroups/CardAccess.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
import Foundation
|
||||
|
||||
// SecurityInfos ::= SET of SecurityInfo
|
||||
// SecurityInfo ::= SEQUENCE {
|
||||
// protocol OBJECT IDENTIFIER,
|
||||
// requiredData ANY DEFINED BY protocol,
|
||||
// optionalData ANY DEFINED BY protocol OPTIONAL
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class CardAccess {
|
||||
private var asn1 : ASN1Item!
|
||||
public private(set) var securityInfos : [SecurityInfo] = [SecurityInfo]()
|
||||
|
||||
var paceInfo : PACEInfo? {
|
||||
get {
|
||||
return (securityInfos.filter { ($0 as? PACEInfo) != nil }).first as? PACEInfo
|
||||
}
|
||||
}
|
||||
|
||||
required init( _ data : [UInt8] ) throws {
|
||||
let p = SimpleASN1DumpParser()
|
||||
asn1 = try p.parse(data: Data(data))
|
||||
|
||||
// Bit of a hack at the moment - passing in the body - if we had a decent ASN1 parser then this would be better! ;)
|
||||
for i in 0 ..< asn1.getNumberOfChildren() {
|
||||
if let child = asn1.getChild(i),
|
||||
let secInfo = SecurityInfo.getInstance( object:child, body : data ) {
|
||||
securityInfos.append(secInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import Foundation
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class ChipAuthenticationInfo : SecurityInfo {
|
||||
|
||||
var oid : String
|
||||
var version : Int
|
||||
var keyId : Int?
|
||||
|
||||
static func checkRequiredIdentifier(_ oid : String) -> Bool {
|
||||
return ID_CA_DH_3DES_CBC_CBC_OID == oid
|
||||
|| ID_CA_ECDH_3DES_CBC_CBC_OID == oid
|
||||
|| ID_CA_DH_AES_CBC_CMAC_128_OID == oid
|
||||
|| ID_CA_DH_AES_CBC_CMAC_192_OID == oid
|
||||
|| ID_CA_DH_AES_CBC_CMAC_256_OID == oid
|
||||
|| ID_CA_ECDH_AES_CBC_CMAC_128_OID == oid
|
||||
|| ID_CA_ECDH_AES_CBC_CMAC_192_OID == oid
|
||||
|| ID_CA_ECDH_AES_CBC_CMAC_256_OID == oid
|
||||
}
|
||||
|
||||
init(oid: String, version: Int, keyId: Int? = nil) {
|
||||
self.oid = oid
|
||||
self.version = version
|
||||
self.keyId = keyId
|
||||
}
|
||||
|
||||
public override func getObjectIdentifier() -> String {
|
||||
return oid
|
||||
}
|
||||
|
||||
public override func getProtocolOIDString() -> String {
|
||||
return ChipAuthenticationInfo.toProtocolOIDString(oid:oid)
|
||||
}
|
||||
|
||||
// The keyid refers to a specific key if there are multiple otherwise if not set, only one key is present so set to 0
|
||||
public func getKeyId() -> Int {
|
||||
return keyId ?? 0
|
||||
}
|
||||
|
||||
/// Returns the key agreement algorithm - DH or ECDH for the given Chip Authentication oid
|
||||
/// - Parameter oid: the object identifier
|
||||
/// - Returns: key agreement algorithm
|
||||
/// - Throws: InvalidDataPassed error if invalid oid specified
|
||||
public static func toKeyAgreementAlgorithm( oid : String ) throws -> String {
|
||||
if ID_CA_DH_3DES_CBC_CBC_OID == oid
|
||||
|| ID_CA_DH_AES_CBC_CMAC_128_OID == oid
|
||||
|| ID_CA_DH_AES_CBC_CMAC_192_OID == oid
|
||||
|| ID_CA_DH_AES_CBC_CMAC_256_OID == oid {
|
||||
return "DH";
|
||||
} else if ID_CA_ECDH_3DES_CBC_CBC_OID == oid
|
||||
|| ID_CA_ECDH_AES_CBC_CMAC_128_OID == oid
|
||||
|| ID_CA_ECDH_AES_CBC_CMAC_192_OID == oid
|
||||
|| ID_CA_ECDH_AES_CBC_CMAC_256_OID == oid {
|
||||
return "ECDH";
|
||||
}
|
||||
|
||||
throw NFCPassportReaderError.InvalidDataPassed( "Unable to lookup key agreement algorithm - invalid oid" )
|
||||
}
|
||||
|
||||
/// Returns the cipher algorithm - DESede or AES for the given Chip Authentication oid
|
||||
/// - Parameter oid: the object identifier
|
||||
/// - Returns: the cipher algorithm type
|
||||
/// - Throws: InvalidDataPassed error if invalid oid specified
|
||||
public static func toCipherAlgorithm( oid : String ) throws -> String {
|
||||
if ID_CA_DH_3DES_CBC_CBC_OID == oid
|
||||
|| ID_CA_ECDH_3DES_CBC_CBC_OID == oid {
|
||||
return "DESede";
|
||||
} else if ID_CA_DH_AES_CBC_CMAC_128_OID == oid
|
||||
|| ID_CA_DH_AES_CBC_CMAC_192_OID == oid
|
||||
|| ID_CA_DH_AES_CBC_CMAC_256_OID == oid
|
||||
|| ID_CA_ECDH_AES_CBC_CMAC_128_OID == oid
|
||||
|| ID_CA_ECDH_AES_CBC_CMAC_192_OID == oid
|
||||
|| ID_CA_ECDH_AES_CBC_CMAC_256_OID == oid {
|
||||
return "AES";
|
||||
}
|
||||
throw NFCPassportReaderError.InvalidDataPassed( "Unable to lookup cipher algorithm - invalid oid" )
|
||||
}
|
||||
|
||||
/// Returns the key length in bits (128, 192, or 256) for the given Chip Authentication oid
|
||||
/// - Parameter oid: the object identifier
|
||||
/// - Returns: the key length in bits
|
||||
/// - Throws: InvalidDataPassed error if invalid oid specified
|
||||
public static func toKeyLength( oid : String ) throws -> Int {
|
||||
if ID_CA_DH_3DES_CBC_CBC_OID == oid
|
||||
|| ID_CA_ECDH_3DES_CBC_CBC_OID == oid
|
||||
|| ID_CA_DH_AES_CBC_CMAC_128_OID == oid
|
||||
|| ID_CA_ECDH_AES_CBC_CMAC_128_OID == oid {
|
||||
return 128;
|
||||
} else if ID_CA_DH_AES_CBC_CMAC_192_OID == oid
|
||||
|| ID_CA_ECDH_AES_CBC_CMAC_192_OID == oid {
|
||||
return 192;
|
||||
} else if ID_CA_DH_AES_CBC_CMAC_256_OID == oid
|
||||
|| ID_CA_ECDH_AES_CBC_CMAC_256_OID == oid {
|
||||
return 256;
|
||||
}
|
||||
|
||||
throw NFCPassportReaderError.InvalidDataPassed( "Unable to get key length - invalid oid" )
|
||||
}
|
||||
|
||||
private static func toProtocolOIDString(oid : String) -> String {
|
||||
if ID_CA_DH_3DES_CBC_CBC_OID == oid {
|
||||
return "id-CA-DH-3DES-CBC-CBC"
|
||||
}
|
||||
if ID_CA_DH_AES_CBC_CMAC_128_OID == oid {
|
||||
return "id-CA-DH-AES-CBC-CMAC-128"
|
||||
}
|
||||
if ID_CA_DH_AES_CBC_CMAC_192_OID == oid {
|
||||
return "id-CA-DH-AES-CBC-CMAC-192"
|
||||
}
|
||||
if ID_CA_DH_AES_CBC_CMAC_256_OID == oid {
|
||||
return "id-CA-DH-AES-CBC-CMAC-256"
|
||||
}
|
||||
if ID_CA_ECDH_3DES_CBC_CBC_OID == oid {
|
||||
return "id-CA-ECDH-3DES-CBC-CBC"
|
||||
}
|
||||
if ID_CA_ECDH_AES_CBC_CMAC_128_OID == oid {
|
||||
return "id-CA-ECDH-AES-CBC-CMAC-128"
|
||||
}
|
||||
if ID_CA_ECDH_AES_CBC_CMAC_192_OID == oid {
|
||||
return "id-CA-ECDH-AES-CBC-CMAC-192"
|
||||
}
|
||||
if ID_CA_ECDH_AES_CBC_CMAC_256_OID == oid {
|
||||
return "id-CA-ECDH-AES-CBC-CMAC-256"
|
||||
}
|
||||
|
||||
return oid
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import Foundation
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class ChipAuthenticationPublicKeyInfo : SecurityInfo {
|
||||
var oid : String
|
||||
var pubKey : OpaquePointer
|
||||
var keyId : Int?
|
||||
|
||||
|
||||
static func checkRequiredIdentifier(_ oid : String) -> Bool {
|
||||
return ID_PK_DH_OID == oid
|
||||
|| ID_PK_ECDH_OID == oid
|
||||
}
|
||||
|
||||
init(oid:String, pubKey:OpaquePointer, keyId: Int? = nil) {
|
||||
self.oid = oid
|
||||
self.pubKey = pubKey
|
||||
self.keyId = keyId
|
||||
}
|
||||
|
||||
public override func getObjectIdentifier() -> String {
|
||||
return oid
|
||||
}
|
||||
|
||||
public override func getProtocolOIDString() -> String {
|
||||
return ChipAuthenticationPublicKeyInfo.toProtocolOIDString(oid:oid)
|
||||
}
|
||||
|
||||
// The keyid refers to a specific key if there are multiple otherwise if not set, only one key is present so set to 0
|
||||
public func getKeyId() -> Int {
|
||||
return keyId ?? 0
|
||||
}
|
||||
|
||||
|
||||
private static func toProtocolOIDString(oid : String) -> String {
|
||||
if ID_PK_DH_OID == oid {
|
||||
return "id-PK-DH"
|
||||
}
|
||||
if ID_PK_ECDH_OID == oid {
|
||||
return "id-PK-ECDH"
|
||||
}
|
||||
|
||||
return oid
|
||||
}
|
||||
|
||||
}
|
||||
78
app/ios/NFCPassportReader/DataGroups/DataGroup.swift
Normal file
78
app/ios/NFCPassportReader/DataGroups/DataGroup.swift
Normal file
@@ -0,0 +1,78 @@
|
||||
import Foundation
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class DataGroup {
|
||||
public var datagroupType : DataGroupId = .Unknown
|
||||
|
||||
/// Body contains the actual data
|
||||
public private(set) var body : [UInt8] = []
|
||||
|
||||
/// Data contains the whole DataGroup data (as that is what the hash is calculated from
|
||||
public private(set) var data : [UInt8] = []
|
||||
|
||||
var pos = 0
|
||||
|
||||
required init( _ data : [UInt8] ) throws {
|
||||
self.data = data
|
||||
|
||||
// Skip the first byte which is the header byte
|
||||
pos = 1
|
||||
let _ = try getNextLength()
|
||||
self.body = [UInt8](data[pos...])
|
||||
|
||||
try parse(data)
|
||||
}
|
||||
|
||||
func parse( _ data:[UInt8] ) throws {
|
||||
}
|
||||
|
||||
func getNextTag() throws -> Int {
|
||||
var tag = 0
|
||||
|
||||
// Fix for some passports that may have invalid data - ensure that we do have data!
|
||||
guard data.count > pos else {
|
||||
throw NFCPassportReaderError.TagNotValid
|
||||
}
|
||||
|
||||
if binToHex(data[pos]) & 0x0F == 0x0F {
|
||||
tag = Int(binToHex(data[pos..<pos+2]))
|
||||
pos += 2
|
||||
} else {
|
||||
tag = Int(data[pos])
|
||||
pos += 1
|
||||
}
|
||||
return tag
|
||||
}
|
||||
|
||||
func getNextLength() throws -> Int {
|
||||
let end = pos+4 < data.count ? pos+4 : data.count
|
||||
let (len, lenOffset) = try asn1Length([UInt8](data[pos..<end]))
|
||||
pos += lenOffset
|
||||
return len
|
||||
}
|
||||
|
||||
func getNextValue() throws -> [UInt8] {
|
||||
let length = try getNextLength()
|
||||
let value = [UInt8](data[pos ..< pos+length])
|
||||
pos += length
|
||||
return value
|
||||
}
|
||||
|
||||
public func hash( _ hashAlgorythm: String ) -> [UInt8] {
|
||||
var ret : [UInt8] = []
|
||||
if hashAlgorythm == "SHA1" {
|
||||
ret = calcSHA1Hash(self.data)
|
||||
} else if hashAlgorythm == "SHA224" {
|
||||
ret = calcSHA224Hash(self.data)
|
||||
} else if hashAlgorythm == "SHA256" {
|
||||
ret = calcSHA256Hash(self.data)
|
||||
} else if hashAlgorythm == "SHA384" {
|
||||
ret = calcSHA384Hash(self.data)
|
||||
} else if hashAlgorythm == "SHA512" {
|
||||
ret = calcSHA512Hash(self.data)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
}
|
||||
108
app/ios/NFCPassportReader/DataGroups/DataGroup1.swift
Normal file
108
app/ios/NFCPassportReader/DataGroups/DataGroup1.swift
Normal file
@@ -0,0 +1,108 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public enum DocTypeEnum: String {
|
||||
case TD1
|
||||
case TD2
|
||||
case OTHER
|
||||
|
||||
var desc: String {
|
||||
get {
|
||||
return self.rawValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class DataGroup1 : DataGroup {
|
||||
|
||||
public private(set) var elements : [String:String] = [:]
|
||||
|
||||
required init( _ data : [UInt8] ) throws {
|
||||
try super.init(data)
|
||||
datagroupType = .DG1
|
||||
}
|
||||
|
||||
override func parse(_ data: [UInt8]) throws {
|
||||
let tag = try getNextTag()
|
||||
if tag != 0x5F1F {
|
||||
throw NFCPassportReaderError.InvalidResponse
|
||||
}
|
||||
let body = try getNextValue()
|
||||
let docType = getMRZType(length:body.count)
|
||||
|
||||
switch docType {
|
||||
case .TD1:
|
||||
self.parseTd1(body)
|
||||
case .TD2:
|
||||
self.parseTd2(body)
|
||||
default:
|
||||
self.parseOther(body)
|
||||
}
|
||||
|
||||
// Store MRZ data
|
||||
elements["5F1F"] = String(bytes: body, encoding:.utf8)
|
||||
}
|
||||
|
||||
func parseTd1(_ data : [UInt8]) {
|
||||
elements["5F03"] = String(bytes: data[0..<2], encoding:.utf8)
|
||||
elements["5F28"] = String( bytes:data[2..<5], encoding:.utf8)
|
||||
elements["5A"] = String( bytes:data[5..<14], encoding:.utf8)
|
||||
elements["5F04"] = String( bytes:data[14..<15], encoding:.utf8)
|
||||
elements["53"] = (String( bytes:data[15..<30], encoding:.utf8) ?? "") +
|
||||
(String( bytes:data[48..<59], encoding:.utf8) ?? "")
|
||||
elements["5F57"] = String( bytes:data[30..<36], encoding:.utf8)
|
||||
elements["5F05"] = String( bytes:data[36..<37], encoding:.utf8)
|
||||
elements["5F35"] = String( bytes:data[37..<38], encoding:.utf8)
|
||||
elements["59"] = String( bytes:data[38..<44], encoding:.utf8)
|
||||
elements["5F06"] = String( bytes:data[44..<45], encoding:.utf8)
|
||||
elements["5F2C"] = String( bytes:data[45..<48], encoding:.utf8)
|
||||
elements["5F07"] = String( bytes:data[59..<60], encoding:.utf8)
|
||||
elements["5B"] = String( bytes:data[60...], encoding:.utf8)
|
||||
}
|
||||
|
||||
func parseTd2(_ data : [UInt8]) {
|
||||
elements["5F03"] = String( bytes:data[0..<2], encoding:.utf8)
|
||||
elements["5F28"] = String( bytes:data[2..<5], encoding:.utf8)
|
||||
elements["5B"] = String( bytes:data[5..<36], encoding:.utf8)
|
||||
elements["5A"] = String( bytes:data[36..<45], encoding:.utf8)
|
||||
elements["5F04"] = String( bytes:data[45..<46], encoding:.utf8)
|
||||
elements["5F2C"] = String( bytes:data[46..<49], encoding:.utf8)
|
||||
elements["5F57"] = String( bytes:data[49..<55], encoding:.utf8)
|
||||
elements["5F05"] = String( bytes:data[55..<56], encoding:.utf8)
|
||||
elements["5F35"] = String( bytes:data[56..<57], encoding:.utf8)
|
||||
elements["59"] = String( bytes:data[57..<63], encoding:.utf8)
|
||||
elements["5F06"] = String( bytes:data[63..<64], encoding:.utf8)
|
||||
elements["53"] = String( bytes:data[64..<71], encoding:.utf8)
|
||||
elements["5F07"] = String( bytes:data[71..<72], encoding:.utf8)
|
||||
}
|
||||
|
||||
func parseOther(_ data : [UInt8]) {
|
||||
elements["5F03"] = String( bytes:data[0..<2], encoding:.utf8)
|
||||
elements["5F28"] = String( bytes:data[2..<5], encoding:.utf8)
|
||||
elements["5B"] = String( bytes:data[5..<44], encoding:.utf8)
|
||||
elements["5A"] = String( bytes:data[44..<53], encoding:.utf8)
|
||||
elements["5F04"] = String( bytes:[data[53]], encoding:.utf8)
|
||||
elements["5F2C"] = String( bytes:data[54..<57], encoding:.utf8)
|
||||
elements["5F57"] = String( bytes:data[57..<63], encoding:.utf8)
|
||||
elements["5F05"] = String( bytes:[data[63]], encoding:.utf8)
|
||||
elements["5F35"] = String( bytes:[data[64]], encoding:.utf8)
|
||||
elements["59"] = String( bytes:data[65..<71], encoding:.utf8)
|
||||
elements["5F06"] = String( bytes:[data[71]], encoding:.utf8)
|
||||
elements["53"] = String( bytes:data[72..<86], encoding:.utf8)
|
||||
elements["5F02"] = String( bytes:[data[86]], encoding:.utf8)
|
||||
elements["5F07"] = String( bytes:[data[87]], encoding:.utf8)
|
||||
}
|
||||
|
||||
private func getMRZType(length: Int) -> DocTypeEnum {
|
||||
if length == 0x5A {
|
||||
return .TD1
|
||||
}
|
||||
if length == 0x48 {
|
||||
return .TD2
|
||||
}
|
||||
return .OTHER
|
||||
}
|
||||
|
||||
}
|
||||
62
app/ios/NFCPassportReader/DataGroups/DataGroup11.swift
Normal file
62
app/ios/NFCPassportReader/DataGroups/DataGroup11.swift
Normal file
@@ -0,0 +1,62 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class DataGroup11 : DataGroup {
|
||||
|
||||
public private(set) var fullName : String?
|
||||
public private(set) var personalNumber : String?
|
||||
public private(set) var dateOfBirth : String?
|
||||
public private(set) var placeOfBirth : String?
|
||||
public private(set) var address : String?
|
||||
public private(set) var telephone : String?
|
||||
public private(set) var profession : String?
|
||||
public private(set) var title : String?
|
||||
public private(set) var personalSummary : String?
|
||||
public private(set) var proofOfCitizenship : String?
|
||||
public private(set) var tdNumbers : String?
|
||||
public private(set) var custodyInfo : String?
|
||||
|
||||
required init( _ data : [UInt8] ) throws {
|
||||
try super.init(data)
|
||||
datagroupType = .DG11
|
||||
}
|
||||
|
||||
override func parse(_ data: [UInt8]) throws {
|
||||
var tag = try getNextTag()
|
||||
if tag != 0x5C {
|
||||
throw NFCPassportReaderError.InvalidResponse
|
||||
}
|
||||
_ = try getNextValue()
|
||||
|
||||
repeat {
|
||||
tag = try getNextTag()
|
||||
let val = try String( bytes:getNextValue(), encoding:.utf8)
|
||||
if tag == 0x5F0E {
|
||||
fullName = val
|
||||
} else if tag == 0x5F10 {
|
||||
personalNumber = val
|
||||
} else if tag == 0x5F11 {
|
||||
placeOfBirth = val
|
||||
} else if tag == 0x5F2B {
|
||||
dateOfBirth = val
|
||||
} else if tag == 0x5F42 {
|
||||
address = val
|
||||
} else if tag == 0x5F12 {
|
||||
telephone = val
|
||||
} else if tag == 0x5F13 {
|
||||
profession = val
|
||||
} else if tag == 0x5F14 {
|
||||
title = val
|
||||
} else if tag == 0x5F15 {
|
||||
personalSummary = val
|
||||
} else if tag == 0x5F16 {
|
||||
proofOfCitizenship = val
|
||||
} else if tag == 0x5F17 {
|
||||
tdNumbers = val
|
||||
} else if tag == 0x5F18 {
|
||||
custodyInfo = val
|
||||
}
|
||||
} while pos < data.count
|
||||
}
|
||||
}
|
||||
72
app/ios/NFCPassportReader/DataGroups/DataGroup12.swift
Normal file
72
app/ios/NFCPassportReader/DataGroups/DataGroup12.swift
Normal file
@@ -0,0 +1,72 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class DataGroup12 : DataGroup {
|
||||
|
||||
public private(set) var issuingAuthority : String?
|
||||
public private(set) var dateOfIssue : String?
|
||||
public private(set) var otherPersonsDetails : String?
|
||||
public private(set) var endorsementsOrObservations : String?
|
||||
public private(set) var taxOrExitRequirements : String?
|
||||
public private(set) var frontImage : [UInt8]?
|
||||
public private(set) var rearImage : [UInt8]?
|
||||
public private(set) var personalizationTime : String?
|
||||
public private(set) var personalizationDeviceSerialNr : String?
|
||||
|
||||
required init( _ data : [UInt8] ) throws {
|
||||
try super.init(data)
|
||||
datagroupType = .DG12
|
||||
}
|
||||
|
||||
override func parse(_ data: [UInt8]) throws {
|
||||
var tag = try getNextTag()
|
||||
if tag != 0x5C {
|
||||
throw NFCPassportReaderError.InvalidResponse
|
||||
}
|
||||
|
||||
// Skip the taglist - ideally we would check this but...
|
||||
let _ = try getNextValue()
|
||||
|
||||
repeat {
|
||||
tag = try getNextTag()
|
||||
let val = try getNextValue()
|
||||
|
||||
if tag == 0x5F19 {
|
||||
issuingAuthority = String( bytes:val, encoding:.utf8)
|
||||
} else if tag == 0x5F26 {
|
||||
dateOfIssue = parseDateOfIssue(value: val)
|
||||
} else if tag == 0xA0 {
|
||||
// Not yet handled
|
||||
} else if tag == 0x5F1B {
|
||||
endorsementsOrObservations = String( bytes:val, encoding:.utf8)
|
||||
} else if tag == 0x5F1C {
|
||||
taxOrExitRequirements = String( bytes:val, encoding:.utf8)
|
||||
} else if tag == 0x5F1D {
|
||||
frontImage = val
|
||||
} else if tag == 0x5F1E {
|
||||
rearImage = val
|
||||
} else if tag == 0x5F55 {
|
||||
personalizationTime = String( bytes:val, encoding:.utf8)
|
||||
} else if tag == 0x5F56 {
|
||||
personalizationDeviceSerialNr = String( bytes:val, encoding:.utf8)
|
||||
}
|
||||
} while pos < data.count
|
||||
}
|
||||
|
||||
private func parseDateOfIssue(value: [UInt8]) -> String? {
|
||||
if value.count == 4 {
|
||||
return decodeBCD(value: value)
|
||||
} else {
|
||||
return decodeASCII(value: value)
|
||||
}
|
||||
}
|
||||
|
||||
private func decodeASCII(value: [UInt8]) -> String? {
|
||||
return String(bytes:value, encoding:.utf8)
|
||||
}
|
||||
|
||||
private func decodeBCD(value: [UInt8]) -> String? {
|
||||
value.map({ String(format: "%02X", $0) }).joined()
|
||||
}
|
||||
}
|
||||
31
app/ios/NFCPassportReader/DataGroups/DataGroup14.swift
Normal file
31
app/ios/NFCPassportReader/DataGroups/DataGroup14.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
// SecurityInfos ::= SET of SecurityInfo
|
||||
// SecurityInfo ::= SEQUENCE {
|
||||
// protocol OBJECT IDENTIFIER,
|
||||
// requiredData ANY DEFINED BY protocol,
|
||||
// optionalData ANY DEFINED BY protocol OPTIONAL
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class DataGroup14 : DataGroup {
|
||||
private var asn1 : ASN1Item!
|
||||
public private(set) var securityInfos : [SecurityInfo] = [SecurityInfo]()
|
||||
|
||||
required init( _ data : [UInt8] ) throws {
|
||||
try super.init(data)
|
||||
datagroupType = .DG14
|
||||
}
|
||||
|
||||
override func parse(_ data: [UInt8]) throws {
|
||||
let p = SimpleASN1DumpParser()
|
||||
asn1 = try p.parse(data: Data(body))
|
||||
|
||||
// Bit of a hack at the moment - passing in the body - if we had a decent ASN1 parser then this would be better! ;)
|
||||
for i in 0 ..< asn1.getNumberOfChildren() {
|
||||
if let child = asn1.getChild(i),
|
||||
let secInfo = SecurityInfo.getInstance( object:child, body : body ) {
|
||||
securityInfos.append(secInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
app/ios/NFCPassportReader/DataGroups/DataGroup15.swift
Normal file
40
app/ios/NFCPassportReader/DataGroups/DataGroup15.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
import Foundation
|
||||
import OpenSSL
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class DataGroup15 : DataGroup {
|
||||
|
||||
public private(set) var rsaPublicKey : OpaquePointer?
|
||||
public private(set) var ecdsaPublicKey : OpaquePointer?
|
||||
|
||||
deinit {
|
||||
if ( ecdsaPublicKey != nil ) {
|
||||
EVP_PKEY_free(ecdsaPublicKey);
|
||||
}
|
||||
if ( rsaPublicKey != nil ) {
|
||||
EVP_PKEY_free(rsaPublicKey);
|
||||
}
|
||||
}
|
||||
|
||||
required init( _ data : [UInt8] ) throws {
|
||||
try super.init(data)
|
||||
datagroupType = .DG15
|
||||
}
|
||||
|
||||
|
||||
override func parse(_ data: [UInt8]) throws {
|
||||
|
||||
// the public key can either be in EC (elliptic curve) or RSA format
|
||||
// Try ec first and if this fails try RSA
|
||||
// Note - this will be improved in a later version to read the ASN1 body to
|
||||
// check the actual type
|
||||
if let key = try? OpenSSLUtils.readECPublicKey( data:body ) {
|
||||
// NOTE We are responsible for freeing the key!
|
||||
ecdsaPublicKey = key
|
||||
} else if let key = try? OpenSSLUtils.readRSAPublicKey( data:body ) {
|
||||
|
||||
rsaPublicKey = key
|
||||
}
|
||||
}
|
||||
}
|
||||
164
app/ios/NFCPassportReader/DataGroups/DataGroup2.swift
Normal file
164
app/ios/NFCPassportReader/DataGroups/DataGroup2.swift
Normal file
@@ -0,0 +1,164 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class DataGroup2 : DataGroup {
|
||||
public private(set) var nrImages : Int = 0
|
||||
public private(set) var versionNumber : Int = 0
|
||||
public private(set) var lengthOfRecord : Int = 0
|
||||
public private(set) var numberOfFacialImages : Int = 0
|
||||
public private(set) var facialRecordDataLength : Int = 0
|
||||
public private(set) var nrFeaturePoints : Int = 0
|
||||
public private(set) var gender : Int = 0
|
||||
public private(set) var eyeColor : Int = 0
|
||||
public private(set) var hairColor : Int = 0
|
||||
public private(set) var featureMask : Int = 0
|
||||
public private(set) var expression : Int = 0
|
||||
public private(set) var poseAngle : Int = 0
|
||||
public private(set) var poseAngleUncertainty : Int = 0
|
||||
public private(set) var faceImageType : Int = 0
|
||||
public private(set) var imageDataType : Int = 0
|
||||
public private(set) var imageWidth : Int = 0
|
||||
public private(set) var imageHeight : Int = 0
|
||||
public private(set) var imageColorSpace : Int = 0
|
||||
public private(set) var sourceType : Int = 0
|
||||
public private(set) var deviceType : Int = 0
|
||||
public private(set) var quality : Int = 0
|
||||
public private(set) var imageData : [UInt8] = []
|
||||
|
||||
|
||||
#if !os(macOS)
|
||||
func getImage() -> UIImage? {
|
||||
if imageData.count == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
let image = UIImage(data:Data(imageData) )
|
||||
return image
|
||||
}
|
||||
#endif
|
||||
|
||||
required init( _ data : [UInt8] ) throws {
|
||||
try super.init(data)
|
||||
datagroupType = .DG2
|
||||
}
|
||||
|
||||
override func parse(_ data: [UInt8]) throws {
|
||||
var tag = try getNextTag()
|
||||
if tag != 0x7F61 {
|
||||
throw NFCPassportReaderError.InvalidResponse
|
||||
}
|
||||
_ = try getNextLength()
|
||||
|
||||
// Tag should be 0x02
|
||||
tag = try getNextTag()
|
||||
if tag != 0x02 {
|
||||
throw NFCPassportReaderError.InvalidResponse
|
||||
}
|
||||
nrImages = try Int(getNextValue()[0])
|
||||
|
||||
// Next tag is 0x7F60
|
||||
tag = try getNextTag()
|
||||
if tag != 0x7F60 {
|
||||
throw NFCPassportReaderError.InvalidResponse
|
||||
}
|
||||
_ = try getNextLength()
|
||||
|
||||
// Next tag is 0xA1 (Biometric Header Template) - don't care about this
|
||||
tag = try getNextTag()
|
||||
if tag != 0xA1 {
|
||||
throw NFCPassportReaderError.InvalidResponse
|
||||
}
|
||||
_ = try getNextValue()
|
||||
|
||||
// Now we get to the good stuff - next tag is either 5F2E or 7F2E
|
||||
tag = try getNextTag()
|
||||
if tag != 0x5F2E && tag != 0x7F2E {
|
||||
throw NFCPassportReaderError.InvalidResponse
|
||||
}
|
||||
let value = try getNextValue()
|
||||
|
||||
try parseISO19794_5( data:value )
|
||||
}
|
||||
|
||||
func parseISO19794_5( data : [UInt8] ) throws {
|
||||
// Validate header - 'F', 'A' 'C' 0x00 - 0x46414300
|
||||
if data[0] != 0x46 && data[1] != 0x41 && data[2] != 0x43 && data[3] != 0x00 {
|
||||
throw NFCPassportReaderError.InvalidResponse
|
||||
}
|
||||
|
||||
var offset = 4
|
||||
versionNumber = binToInt(data[offset..<offset+4])
|
||||
offset += 4
|
||||
lengthOfRecord = binToInt(data[offset..<offset+4])
|
||||
offset += 4
|
||||
numberOfFacialImages = binToInt(data[offset..<offset+2])
|
||||
offset += 2
|
||||
|
||||
facialRecordDataLength = binToInt(data[offset..<offset+4])
|
||||
offset += 4
|
||||
nrFeaturePoints = binToInt(data[offset..<offset+2])
|
||||
offset += 2
|
||||
gender = binToInt(data[offset..<offset+1])
|
||||
offset += 1
|
||||
eyeColor = binToInt(data[offset..<offset+1])
|
||||
offset += 1
|
||||
hairColor = binToInt(data[offset..<offset+1])
|
||||
offset += 1
|
||||
featureMask = binToInt(data[offset..<offset+3])
|
||||
offset += 3
|
||||
expression = binToInt(data[offset..<offset+2])
|
||||
offset += 2
|
||||
poseAngle = binToInt(data[offset..<offset+3])
|
||||
offset += 3
|
||||
poseAngleUncertainty = binToInt(data[offset..<offset+3])
|
||||
offset += 3
|
||||
|
||||
// Features (not handled). There shouldn't be any but if for some reason there were,
|
||||
// then we are going to skip over them
|
||||
// The Feature block is 8 bytes
|
||||
offset += nrFeaturePoints * 8
|
||||
|
||||
faceImageType = binToInt(data[offset..<offset+1])
|
||||
offset += 1
|
||||
imageDataType = binToInt(data[offset..<offset+1])
|
||||
offset += 1
|
||||
imageWidth = binToInt(data[offset..<offset+2])
|
||||
offset += 2
|
||||
imageHeight = binToInt(data[offset..<offset+2])
|
||||
offset += 2
|
||||
imageColorSpace = binToInt(data[offset..<offset+1])
|
||||
offset += 1
|
||||
sourceType = binToInt(data[offset..<offset+1])
|
||||
offset += 1
|
||||
deviceType = binToInt(data[offset..<offset+2])
|
||||
offset += 2
|
||||
quality = binToInt(data[offset..<offset+2])
|
||||
offset += 2
|
||||
|
||||
|
||||
// Make sure that the image data at least has a valid header
|
||||
// Either JPG or JPEG2000
|
||||
|
||||
let jpegHeader : [UInt8] = [0xff,0xd8,0xff,0xe0,0x00,0x10,0x4a,0x46,0x49,0x46]
|
||||
let jpeg2000BitmapHeader : [UInt8] = [0x00,0x00,0x00,0x0c,0x6a,0x50,0x20,0x20,0x0d,0x0a]
|
||||
let jpeg2000CodestreamBitmapHeader : [UInt8] = [0xff,0x4f,0xff,0x51]
|
||||
|
||||
if data.count < offset+jpeg2000CodestreamBitmapHeader.count {
|
||||
throw NFCPassportReaderError.UnknownImageFormat
|
||||
}
|
||||
|
||||
|
||||
if [UInt8](data[offset..<offset+jpegHeader.count]) != jpegHeader &&
|
||||
[UInt8](data[offset..<offset+jpeg2000BitmapHeader.count]) != jpeg2000BitmapHeader &&
|
||||
[UInt8](data[offset..<offset+jpeg2000CodestreamBitmapHeader.count]) != jpeg2000CodestreamBitmapHeader {
|
||||
throw NFCPassportReaderError.UnknownImageFormat
|
||||
}
|
||||
|
||||
imageData = [UInt8](data[offset...])
|
||||
}
|
||||
}
|
||||
44
app/ios/NFCPassportReader/DataGroups/DataGroup7.swift
Normal file
44
app/ios/NFCPassportReader/DataGroups/DataGroup7.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class DataGroup7 : DataGroup {
|
||||
|
||||
public private(set) var imageData : [UInt8] = []
|
||||
|
||||
required init( _ data : [UInt8] ) throws {
|
||||
try super.init(data)
|
||||
datagroupType = .DG7
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
func getImage() -> UIImage? {
|
||||
if imageData.count == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
let image = UIImage(data:Data(imageData) )
|
||||
return image
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
override func parse(_ data: [UInt8]) throws {
|
||||
var tag = try getNextTag()
|
||||
if tag != 0x02 {
|
||||
throw NFCPassportReaderError.InvalidResponse
|
||||
}
|
||||
_ = try getNextValue()
|
||||
|
||||
tag = try getNextTag()
|
||||
if tag != 0x5F43 {
|
||||
throw NFCPassportReaderError.InvalidResponse
|
||||
}
|
||||
|
||||
imageData = try getNextValue()
|
||||
}
|
||||
}
|
||||
98
app/ios/NFCPassportReader/DataGroups/DataGroupId.swift
Normal file
98
app/ios/NFCPassportReader/DataGroups/DataGroupId.swift
Normal file
@@ -0,0 +1,98 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public enum DataGroupId : Int, CaseIterable {
|
||||
case COM = 0x60
|
||||
case DG1 = 0x61
|
||||
case DG2 = 0x75
|
||||
case DG3 = 0x63
|
||||
case DG4 = 0x76
|
||||
case DG5 = 0x65
|
||||
case DG6 = 0x66
|
||||
case DG7 = 0x67
|
||||
case DG8 = 0x68
|
||||
case DG9 = 0x69
|
||||
case DG10 = 0x6A
|
||||
case DG11 = 0x6B
|
||||
case DG12 = 0x6C
|
||||
case DG13 = 0x6D
|
||||
case DG14 = 0x6E
|
||||
case DG15 = 0x6F
|
||||
case DG16 = 0x70
|
||||
case SOD = 0x77
|
||||
case Unknown = 0x00
|
||||
|
||||
public func getName() -> String {
|
||||
switch( self ) {
|
||||
case .COM: return "COM"
|
||||
case .DG1: return "DG1"
|
||||
case .DG2: return "DG2"
|
||||
case .DG3: return "DG3"
|
||||
case .DG4: return "DG4"
|
||||
case .DG5: return "DG5"
|
||||
case .DG6: return "DG6"
|
||||
case .DG7: return "DG7"
|
||||
case .DG8: return "DG8"
|
||||
case .DG9: return "DG9"
|
||||
case .DG10: return "DG10"
|
||||
case .DG11: return "DG11"
|
||||
case .DG12: return "DG12"
|
||||
case .DG13: return "DG13"
|
||||
case .DG14: return "DG14"
|
||||
case .DG15: return "DG15"
|
||||
case .DG16: return "DG16"
|
||||
case .SOD: return "SOD"
|
||||
case .Unknown: return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
static public func getIDFromName( name: String ) -> DataGroupId {
|
||||
switch( name ) {
|
||||
case "COM": return .COM
|
||||
case "DG1": return .DG1
|
||||
case "DG2": return .DG2
|
||||
case "DG3": return .DG3
|
||||
case "DG4": return .DG4
|
||||
case "DG5": return .DG5
|
||||
case "DG6": return .DG6
|
||||
case "DG7": return .DG7
|
||||
case "DG8": return .DG8
|
||||
case "DG9": return .DG9
|
||||
case "DG10": return .DG10
|
||||
case "DG11": return .DG11
|
||||
case "DG12": return .DG12
|
||||
case "DG13": return .DG13
|
||||
case "DG14": return .DG14
|
||||
case "DG15": return .DG15
|
||||
case "DG16": return .DG16
|
||||
case "SOD": return .SOD
|
||||
default: return .Unknown
|
||||
}
|
||||
}
|
||||
|
||||
func getFileIDTag() -> [UInt8]? {
|
||||
switch( self ) {
|
||||
case .COM: return [0x01,0x1E]
|
||||
case .DG1: return [0x01,0x01]
|
||||
case .DG2: return [0x01,0x02]
|
||||
case .DG3: return [0x01,0x03]
|
||||
case .DG4: return [0x01,0x04]
|
||||
case .DG5: return [0x01,0x05]
|
||||
case .DG6: return [0x01,0x06]
|
||||
case .DG7: return [0x01,0x07]
|
||||
case .DG8: return [0x01,0x08]
|
||||
case .DG9: return [0x01,0x09]
|
||||
case .DG10: return [0x01,0x0A]
|
||||
case .DG11: return [0x01,0x0B]
|
||||
case .DG12: return [0x01,0x0C]
|
||||
case .DG13: return [0x01,0x0D]
|
||||
case .DG14: return [0x01,0x0E]
|
||||
case .DG15: return [0x01,0x0F]
|
||||
case .DG16: return [0x01,0x10]
|
||||
case .SOD: return [0x01,0x1D]
|
||||
case .Unknown: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
app/ios/NFCPassportReader/DataGroups/NotImplementedDG.swift
Normal file
11
app/ios/NFCPassportReader/DataGroups/NotImplementedDG.swift
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class NotImplementedDG : DataGroup {
|
||||
required init( _ data : [UInt8] ) throws {
|
||||
try super.init(data)
|
||||
datagroupType = .Unknown
|
||||
}
|
||||
}
|
||||
|
||||
397
app/ios/NFCPassportReader/DataGroups/PACEInfo.swift
Normal file
397
app/ios/NFCPassportReader/DataGroups/PACEInfo.swift
Normal file
@@ -0,0 +1,397 @@
|
||||
|
||||
import Foundation
|
||||
import OpenSSL
|
||||
|
||||
public enum PACEMappingType {
|
||||
case GM // Generic Mapping
|
||||
case IM // Integrated Mapping
|
||||
case CAM // Chip Authentication Mapping
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class PACEInfo : SecurityInfo {
|
||||
|
||||
// Standardized domain parameters. Based on Table 6.
|
||||
public static let PARAM_ID_GFP_1024_160 = 0
|
||||
public static let PARAM_ID_GFP_2048_224 = 1
|
||||
public static let PARAM_ID_GFP_2048_256 = 2
|
||||
public static let PARAM_ID_ECP_NIST_P192_R1 = 8
|
||||
public static let PARAM_ID_ECP_BRAINPOOL_P192_R1 = 9
|
||||
public static let PARAM_ID_ECP_NIST_P224_R1 = 10
|
||||
public static let PARAM_ID_ECP_BRAINPOOL_P224_R1 = 11
|
||||
public static let PARAM_ID_ECP_NIST_P256_R1 = 12
|
||||
public static let PARAM_ID_ECP_BRAINPOOL_P256_R1 = 13
|
||||
public static let PARAM_ID_ECP_BRAINPOOL_P320_R1 = 14
|
||||
public static let PARAM_ID_ECP_NIST_P384_R1 = 15
|
||||
public static let PARAM_ID_ECP_BRAINPOOL_P384_R1 = 16
|
||||
public static let PARAM_ID_ECP_BRAINPOOL_P512_R1 = 17
|
||||
public static let PARAM_ID_ECP_NIST_P521_R1 = 18
|
||||
|
||||
static let allowedIdentifiers = [
|
||||
ID_PACE_DH_GM_3DES_CBC_CBC,
|
||||
ID_PACE_DH_GM_AES_CBC_CMAC_128,
|
||||
ID_PACE_DH_GM_AES_CBC_CMAC_192,
|
||||
ID_PACE_DH_GM_AES_CBC_CMAC_256,
|
||||
ID_PACE_DH_IM_3DES_CBC_CBC,
|
||||
ID_PACE_DH_IM_AES_CBC_CMAC_128,
|
||||
ID_PACE_DH_IM_AES_CBC_CMAC_192,
|
||||
ID_PACE_DH_IM_AES_CBC_CMAC_256,
|
||||
ID_PACE_ECDH_GM_3DES_CBC_CBC,
|
||||
ID_PACE_ECDH_GM_AES_CBC_CMAC_128,
|
||||
ID_PACE_ECDH_GM_AES_CBC_CMAC_192,
|
||||
ID_PACE_ECDH_GM_AES_CBC_CMAC_256,
|
||||
ID_PACE_ECDH_IM_3DES_CBC_CBC,
|
||||
ID_PACE_ECDH_IM_AES_CBC_CMAC_128,
|
||||
ID_PACE_ECDH_IM_AES_CBC_CMAC_192,
|
||||
ID_PACE_ECDH_IM_AES_CBC_CMAC_256,
|
||||
ID_PACE_ECDH_CAM_AES_CBC_CMAC_128,
|
||||
ID_PACE_ECDH_CAM_AES_CBC_CMAC_192,
|
||||
ID_PACE_ECDH_CAM_AES_CBC_CMAC_256]
|
||||
|
||||
var oid : String
|
||||
var version : Int
|
||||
var parameterId : Int?
|
||||
|
||||
static func checkRequiredIdentifier(_ oid : String) -> Bool {
|
||||
return allowedIdentifiers.contains( oid )
|
||||
}
|
||||
|
||||
init(oid: String, version: Int, parameterId: Int?) {
|
||||
self.oid = oid
|
||||
self.version = version
|
||||
self.parameterId = parameterId
|
||||
}
|
||||
|
||||
public override func getObjectIdentifier() -> String {
|
||||
return oid
|
||||
}
|
||||
|
||||
public override func getProtocolOIDString() -> String {
|
||||
return PACEInfo.toProtocolOIDString(oid:oid)
|
||||
}
|
||||
|
||||
public func getVersion() -> Int {
|
||||
return version
|
||||
}
|
||||
|
||||
public func getParameterId() -> Int? {
|
||||
return parameterId
|
||||
}
|
||||
|
||||
public func getParameterSpec() throws -> Int32 {
|
||||
return try PACEInfo.getParameterSpec(stdDomainParam: self.parameterId ?? -1 )
|
||||
}
|
||||
|
||||
public func getMappingType() throws -> PACEMappingType {
|
||||
return try PACEInfo.toMappingType(oid: oid); // Either GM, CAM, or IM.
|
||||
}
|
||||
|
||||
public func getKeyAgreementAlgorithm() throws -> String {
|
||||
return try PACEInfo.toKeyAgreementAlgorithm(oid: oid); // Either DH or ECDH.
|
||||
}
|
||||
|
||||
public func getCipherAlgorithm() throws -> String {
|
||||
return try PACEInfo.toCipherAlgorithm(oid: oid); // Either DESede or AES.
|
||||
}
|
||||
|
||||
public func getDigestAlgorithm() throws -> String {
|
||||
return try PACEInfo.toDigestAlgorithm(oid: oid); // Either SHA-1 or SHA-256.
|
||||
}
|
||||
|
||||
public func getKeyLength() throws -> Int {
|
||||
return try PACEInfo.toKeyLength(oid: oid); // Of the enc cipher. Either 128, 192, or 256.
|
||||
}
|
||||
|
||||
/// Caller is required to free the returned EVP_PKEY value
|
||||
public func createMappingKey( ) throws -> OpaquePointer {
|
||||
// This will get freed later
|
||||
let mappingKey : OpaquePointer = EVP_PKEY_new()
|
||||
|
||||
switch try getKeyAgreementAlgorithm() {
|
||||
case "DH":
|
||||
Log.debug( "Generating DH mapping keys")
|
||||
//The EVP_PKEY_CTX_set_dh_rfc5114() and EVP_PKEY_CTX_set_dhx_rfc5114() macros are synonymous. They set the DH parameters to the values defined in RFC5114. The rfc5114 parameter must be 1, 2 or 3 corresponding to RFC5114 sections 2.1, 2.2 and 2.3. or 0 to clear the stored value. This macro can be called during parameter generation. The ctx must have a key type of EVP_PKEY_DHX. The rfc5114 parameter and the nid parameter are mutually exclusive.
|
||||
var dhKey : OpaquePointer? = nil
|
||||
switch try getParameterSpec() {
|
||||
case 0:
|
||||
Log.verbose( "Using DH_get_1024_160" )
|
||||
dhKey = DH_get_1024_160()
|
||||
case 1:
|
||||
Log.verbose( "Using DH_get_2048_224" )
|
||||
dhKey = DH_get_2048_224()
|
||||
case 2:
|
||||
Log.verbose( "Using DH_get_2048_256" )
|
||||
dhKey = DH_get_2048_256()
|
||||
default:
|
||||
// Error
|
||||
break
|
||||
}
|
||||
guard dhKey != nil else {
|
||||
throw NFCPassportReaderError.InvalidDataPassed("Unable to create DH mapping key")
|
||||
}
|
||||
defer{ DH_free( dhKey ) }
|
||||
|
||||
DH_generate_key(dhKey)
|
||||
EVP_PKEY_set1_DH(mappingKey, dhKey)
|
||||
|
||||
case "ECDH":
|
||||
let parameterSpec = try getParameterSpec()
|
||||
Log.debug( "Generating ECDH mapping keys from parameterSpec - \(parameterSpec)")
|
||||
guard let ecKey = EC_KEY_new_by_curve_name(parameterSpec) else {
|
||||
throw NFCPassportReaderError.InvalidDataPassed("Unable to create EC mapping key")
|
||||
}
|
||||
defer{ EC_KEY_free( ecKey ) }
|
||||
|
||||
EC_KEY_generate_key(ecKey)
|
||||
EVP_PKEY_set1_EC_KEY(mappingKey, ecKey)
|
||||
default:
|
||||
throw NFCPassportReaderError.InvalidDataPassed("Unsupported agreement algorithm")
|
||||
}
|
||||
|
||||
return mappingKey
|
||||
}
|
||||
|
||||
public static func getParameterSpec(stdDomainParam : Int) throws -> Int32 {
|
||||
switch (stdDomainParam) {
|
||||
case PARAM_ID_GFP_1024_160:
|
||||
return 0 // "rfc5114_1024_160";
|
||||
case PARAM_ID_GFP_2048_224:
|
||||
return 1 // "rfc5114_2048_224";
|
||||
case PARAM_ID_GFP_2048_256:
|
||||
return 2 // "rfc5114_2048_256";
|
||||
case PARAM_ID_ECP_NIST_P192_R1:
|
||||
return NID_X9_62_prime192v1 // "secp192r1";
|
||||
case PARAM_ID_ECP_NIST_P224_R1:
|
||||
return NID_secp224r1 // "secp224r1";
|
||||
case PARAM_ID_ECP_NIST_P256_R1:
|
||||
return NID_X9_62_prime256v1 //"secp256r1";
|
||||
case PARAM_ID_ECP_NIST_P384_R1:
|
||||
return NID_secp384r1 // "secp384r1";
|
||||
case PARAM_ID_ECP_BRAINPOOL_P192_R1:
|
||||
return NID_brainpoolP192r1 //"brainpoolp192r1";
|
||||
case PARAM_ID_ECP_BRAINPOOL_P224_R1:
|
||||
return NID_brainpoolP224r1 // "brainpoolp224r1";
|
||||
case PARAM_ID_ECP_BRAINPOOL_P256_R1:
|
||||
return NID_brainpoolP256r1 // "brainpoolp256r1";
|
||||
case PARAM_ID_ECP_BRAINPOOL_P320_R1:
|
||||
return NID_brainpoolP320r1 //"brainpoolp320r1";
|
||||
case PARAM_ID_ECP_BRAINPOOL_P384_R1:
|
||||
return NID_brainpoolP384r1 //"brainpoolp384r1";
|
||||
case PARAM_ID_ECP_BRAINPOOL_P512_R1:
|
||||
return NID_brainpoolP512r1 //"";
|
||||
case PARAM_ID_ECP_NIST_P521_R1:
|
||||
return NID_secp521r1 //"secp224r1";
|
||||
default:
|
||||
throw NFCPassportReaderError.InvalidDataPassed( "Unable to lookup p arameterSpec - invalid oid" )
|
||||
}
|
||||
}
|
||||
|
||||
public static func toMappingType( oid : String ) throws -> PACEMappingType {
|
||||
if ID_PACE_DH_GM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_DH_GM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_DH_GM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_DH_GM_AES_CBC_CMAC_256 == oid
|
||||
|| ID_PACE_ECDH_GM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_ECDH_GM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_ECDH_GM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_ECDH_GM_AES_CBC_CMAC_256 == oid {
|
||||
return PACEMappingType.GM
|
||||
} else if ID_PACE_DH_IM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_DH_IM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_DH_IM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_DH_IM_AES_CBC_CMAC_256 == oid
|
||||
|| ID_PACE_ECDH_IM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_ECDH_IM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_ECDH_IM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_ECDH_IM_AES_CBC_CMAC_256 == oid {
|
||||
return PACEMappingType.IM
|
||||
} else if ID_PACE_ECDH_CAM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_ECDH_CAM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_ECDH_CAM_AES_CBC_CMAC_256 == oid {
|
||||
return PACEMappingType.CAM
|
||||
}
|
||||
|
||||
throw NFCPassportReaderError.InvalidDataPassed( "Unable to lookup mapping type - invalid oid" )
|
||||
}
|
||||
|
||||
|
||||
/// Returns the key agreement algorithm - DH or ECDH for the given Chip Authentication oid
|
||||
/// - Parameter oid: the object identifier
|
||||
/// - Returns: key agreement algorithm
|
||||
/// - Throws: InvalidDataPassed error if invalid oid specified
|
||||
public static func toKeyAgreementAlgorithm( oid : String ) throws -> String {
|
||||
if ID_PACE_DH_GM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_DH_GM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_DH_GM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_DH_GM_AES_CBC_CMAC_256 == oid
|
||||
|| ID_PACE_DH_IM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_DH_IM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_DH_IM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_DH_IM_AES_CBC_CMAC_256 == oid {
|
||||
return "DH"
|
||||
} else if ID_PACE_ECDH_GM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_ECDH_GM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_ECDH_GM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_ECDH_GM_AES_CBC_CMAC_256 == oid
|
||||
|| ID_PACE_ECDH_IM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_ECDH_IM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_ECDH_IM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_ECDH_IM_AES_CBC_CMAC_256 == oid
|
||||
|| ID_PACE_ECDH_CAM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_ECDH_CAM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_ECDH_CAM_AES_CBC_CMAC_256 == oid {
|
||||
return "ECDH"
|
||||
}
|
||||
throw NFCPassportReaderError.InvalidDataPassed( "Unable to lookup key agreement algorithm - invalid oid" )
|
||||
}
|
||||
|
||||
/// Returns the cipher algorithm - DESede or AES for the given Chip Authentication oid
|
||||
/// - Parameter oid: the object identifier
|
||||
/// - Returns: the cipher algorithm type
|
||||
/// - Throws: InvalidDataPassed error if invalid oid specified
|
||||
public static func toCipherAlgorithm( oid : String ) throws -> String {
|
||||
if ID_PACE_DH_GM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_DH_IM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_ECDH_GM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_ECDH_IM_3DES_CBC_CBC == oid {
|
||||
return "DESede"
|
||||
} else if ID_PACE_DH_GM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_DH_GM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_DH_GM_AES_CBC_CMAC_256 == oid
|
||||
|| ID_PACE_DH_IM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_DH_IM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_DH_IM_AES_CBC_CMAC_256 == oid
|
||||
|| ID_PACE_ECDH_GM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_ECDH_GM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_ECDH_GM_AES_CBC_CMAC_256 == oid
|
||||
|| ID_PACE_ECDH_IM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_ECDH_IM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_ECDH_IM_AES_CBC_CMAC_256 == oid
|
||||
|| ID_PACE_ECDH_CAM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_ECDH_CAM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_ECDH_CAM_AES_CBC_CMAC_256 == oid {
|
||||
return "AES"
|
||||
}
|
||||
throw NFCPassportReaderError.InvalidDataPassed( "Unable to lookup cipher algorithm - invalid oid" )
|
||||
}
|
||||
|
||||
public static func toDigestAlgorithm( oid : String ) throws -> String {
|
||||
if ID_PACE_DH_GM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_DH_IM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_ECDH_GM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_ECDH_IM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_DH_GM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_DH_IM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_ECDH_GM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_ECDH_IM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_ECDH_CAM_AES_CBC_CMAC_128 == oid {
|
||||
return "SHA-1"
|
||||
} else if ID_PACE_DH_GM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_DH_IM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_ECDH_GM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_ECDH_IM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_ECDH_CAM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_DH_GM_AES_CBC_CMAC_256 == oid
|
||||
|| ID_PACE_DH_IM_AES_CBC_CMAC_256 == oid
|
||||
|| ID_PACE_ECDH_GM_AES_CBC_CMAC_256 == oid
|
||||
|| ID_PACE_ECDH_IM_AES_CBC_CMAC_256 == oid
|
||||
|| ID_PACE_ECDH_CAM_AES_CBC_CMAC_256 == oid {
|
||||
return "SHA-256"
|
||||
}
|
||||
|
||||
throw NFCPassportReaderError.InvalidDataPassed( "Unable to lookup digest algorithm - invalid oid" )
|
||||
|
||||
}
|
||||
/// Returns the key length in bits (128, 192, or 256) for the given Chip Authentication oid
|
||||
/// - Parameter oid: the object identifier
|
||||
/// - Returns: the key length in bits
|
||||
/// - Throws: InvalidDataPassed error if invalid oid specified
|
||||
public static func toKeyLength( oid : String ) throws -> Int {
|
||||
if ID_PACE_DH_GM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_DH_IM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_ECDH_GM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_ECDH_IM_3DES_CBC_CBC == oid
|
||||
|| ID_PACE_DH_GM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_DH_IM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_ECDH_GM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_ECDH_IM_AES_CBC_CMAC_128 == oid
|
||||
|| ID_PACE_ECDH_CAM_AES_CBC_CMAC_128 == oid {
|
||||
return 128
|
||||
} else if ID_PACE_DH_GM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_ECDH_GM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_DH_IM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_ECDH_IM_AES_CBC_CMAC_192 == oid
|
||||
|| ID_PACE_ECDH_CAM_AES_CBC_CMAC_192 == oid {
|
||||
return 192
|
||||
} else if ID_PACE_DH_GM_AES_CBC_CMAC_256 == oid
|
||||
|| ID_PACE_DH_IM_AES_CBC_CMAC_256 == oid
|
||||
|| ID_PACE_ECDH_GM_AES_CBC_CMAC_256 == oid
|
||||
|| ID_PACE_ECDH_IM_AES_CBC_CMAC_256 == oid
|
||||
|| ID_PACE_ECDH_CAM_AES_CBC_CMAC_256 == oid {
|
||||
return 256
|
||||
}
|
||||
throw NFCPassportReaderError.InvalidDataPassed( "Unable to get key length - invalid oid" )
|
||||
}
|
||||
|
||||
private static func toProtocolOIDString(oid : String) -> String {
|
||||
if ID_PACE_DH_GM_3DES_CBC_CBC == oid {
|
||||
return "id-PACE-DH-GM-3DES-CBC-CBC"
|
||||
}
|
||||
if ID_PACE_DH_GM_AES_CBC_CMAC_128 == oid {
|
||||
return "id-PACE-DH-GM-AES-CBC-CMAC-128"
|
||||
}
|
||||
if ID_PACE_DH_GM_AES_CBC_CMAC_192 == oid {
|
||||
return "id-PACE-DH-GM-AES-CBC-CMAC-192"
|
||||
}
|
||||
if ID_PACE_DH_GM_AES_CBC_CMAC_256 == oid {
|
||||
return "id-PACE-DH-GM-AES-CBC-CMAC-256"
|
||||
}
|
||||
if ID_PACE_DH_IM_3DES_CBC_CBC == oid {
|
||||
return "id-PACE-DH-IM-3DES-CBC-CBC"
|
||||
}
|
||||
if ID_PACE_DH_IM_AES_CBC_CMAC_128 == oid {
|
||||
return "id-PACE-DH-IM-AES-CBC-CMAC-128"
|
||||
}
|
||||
if ID_PACE_DH_IM_AES_CBC_CMAC_192 == oid {
|
||||
return "id-PACE-DH-IM-AES-CBC-CMAC-192"
|
||||
}
|
||||
if ID_PACE_DH_IM_AES_CBC_CMAC_256 == oid {
|
||||
return "id-PACE_DH-IM-AES-CBC-CMAC-256"
|
||||
}
|
||||
if ID_PACE_ECDH_GM_3DES_CBC_CBC == oid {
|
||||
return "id-PACE-ECDH-GM-3DES-CBC-CBC"
|
||||
}
|
||||
if ID_PACE_ECDH_GM_AES_CBC_CMAC_128 == oid {
|
||||
return "id-PACE-ECDH-GM-AES-CBC-CMAC-128"
|
||||
}
|
||||
if ID_PACE_ECDH_GM_AES_CBC_CMAC_192 == oid {
|
||||
return "id-PACE-ECDH-GM-AES-CBC-CMAC-192"
|
||||
}
|
||||
if ID_PACE_ECDH_GM_AES_CBC_CMAC_256 == oid {
|
||||
return "id-PACE-ECDH-GM-AES-CBC-CMAC-256"
|
||||
}
|
||||
if ID_PACE_ECDH_IM_3DES_CBC_CBC == oid {
|
||||
return "id-PACE-ECDH-IM_3DES-CBC-CBC"
|
||||
}
|
||||
if ID_PACE_ECDH_IM_AES_CBC_CMAC_128 == oid {
|
||||
return "id-PACE-ECDH-IM-AES-CBC-CMAC-128"
|
||||
}
|
||||
if ID_PACE_ECDH_IM_AES_CBC_CMAC_192 == oid {
|
||||
return "id-PACE-ECDH-IM-AES-CBC-CMAC-192"
|
||||
}
|
||||
if ID_PACE_ECDH_IM_AES_CBC_CMAC_256 == oid {
|
||||
return "id-PACE-ECDH-IM-AES-CBC-CMAC-256"
|
||||
}
|
||||
if ID_PACE_ECDH_CAM_AES_CBC_CMAC_128 == oid {
|
||||
return "id-PACE-ECDH-CAM-AES-CBC-CMAC-128"
|
||||
}
|
||||
if ID_PACE_ECDH_CAM_AES_CBC_CMAC_192 == oid {
|
||||
return "id-PACE-ECDH-CAM-AES-CBC-CMAC-192"
|
||||
}
|
||||
if ID_PACE_ECDH_CAM_AES_CBC_CMAC_256 == oid {
|
||||
return "id-PACE-ECDH-CAM-AES-CBC-CMAC-256"
|
||||
}
|
||||
|
||||
return oid
|
||||
}
|
||||
}
|
||||
234
app/ios/NFCPassportReader/DataGroups/SOD.swift
Normal file
234
app/ios/NFCPassportReader/DataGroups/SOD.swift
Normal file
@@ -0,0 +1,234 @@
|
||||
|
||||
import Foundation
|
||||
import OpenSSL
|
||||
|
||||
|
||||
// Format of SOD: ASN1 - Signed Data (taken from rfc5652 - https://tools.ietf.org/html/rfc5652):
|
||||
// The SOD is a CMS container of type Signed-data
|
||||
//
|
||||
// Note - ideally I'd be using a proper ASN1 parser, however currently there isn't a reliable one for Swift
|
||||
// and I haven't written on (yet?). So for the moment, I'm relying on the output from ASN1Dump and a
|
||||
// simple parser for that
|
||||
//
|
||||
// Sequence
|
||||
// Object ID: signedData
|
||||
// Content: SignedData
|
||||
// SignedData ::= SEQUENCE {
|
||||
// INTEGER version CMSVersion,
|
||||
// SET digestAlgorithms DigestAlgorithmIdentifiers,
|
||||
// SEQUENCE encapContentInfo EncapsulatedContentInfo,
|
||||
// certificates [0] IMPLICIT CertificateSet OPTIONAL,
|
||||
// crls [1] IMPLICIT RevocationInfoChoices OPTIONAL,
|
||||
// SET signerInfos SignerInfos }
|
||||
//
|
||||
// AlgorithmIdentifier ::= SEQUENCE {
|
||||
// algorithm OBJECT IDENTIFIER,
|
||||
// parameters ANY OPTIONAL
|
||||
// }
|
||||
//
|
||||
// EncapsulatedContentInfo ::= SEQUENCE {
|
||||
// eContentType ContentType,
|
||||
// eContent [0] EXPLICIT OCTET STRING OPTIONAL }
|
||||
//
|
||||
// ContentType ::= OBJECT IDENTIFIER
|
||||
//
|
||||
// SignerInfos ::= SET OF SignerInfo
|
||||
//
|
||||
// SignerInfo ::= SEQUENCE {
|
||||
// version CMSVersion,
|
||||
// sid SignerIdentifier,
|
||||
// digestAlgorithm DigestAlgorithmIdentifier,
|
||||
// signedAttrs [0] IMPLICIT SignedAttributes OPTIONAL,
|
||||
// signatureAlgorithm SignatureAlgorithmIdentifier,
|
||||
// signature SignatureValue,
|
||||
// unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL }
|
||||
//
|
||||
// SignerIdentifier ::= CHOICE {
|
||||
// issuerAndSerialNumber IssuerAndSerialNumber,
|
||||
// subjectKeyIdentifier [0] SubjectKeyIdentifier }
|
||||
//
|
||||
// SignedAttributes ::= SET SIZE (1..MAX) OF Attribute
|
||||
// UnsignedAttributes ::= SET SIZE (1..MAX) OF Attribute
|
||||
// Attribute ::= SEQUENCE {
|
||||
// attrType OBJECT IDENTIFIER,
|
||||
// attrValues SET OF AttributeValue }
|
||||
// AttributeValue ::= ANY
|
||||
// SignatureValue ::= OCTET STRING
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
class SOD : DataGroup {
|
||||
|
||||
public private(set) var pkcs7CertificateData : [UInt8] = []
|
||||
private var asn1 : ASN1Item!
|
||||
private var pubKey : OpaquePointer?
|
||||
|
||||
required init( _ data : [UInt8] ) throws {
|
||||
try super.init(data)
|
||||
self.pkcs7CertificateData = body
|
||||
datagroupType = .SOD
|
||||
}
|
||||
|
||||
deinit {
|
||||
if ( pubKey != nil ) {
|
||||
EVP_PKEY_free(pubKey);
|
||||
}
|
||||
}
|
||||
|
||||
override func parse(_ data: [UInt8]) throws {
|
||||
let p = SimpleASN1DumpParser()
|
||||
asn1 = try p.parse(data: Data(body))
|
||||
}
|
||||
|
||||
/// Returns the public key from the embedded X509 certificate
|
||||
/// - Returns pointer to the public key
|
||||
func getPublicKey( ) throws -> OpaquePointer {
|
||||
|
||||
if let key = pubKey {
|
||||
return key
|
||||
}
|
||||
|
||||
let certs = try OpenSSLUtils.getX509CertificatesFromPKCS7(pkcs7Der:Data(pkcs7CertificateData))
|
||||
if let key = X509_get_pubkey (certs[0].cert) {
|
||||
pubKey = key
|
||||
return key
|
||||
}
|
||||
|
||||
throw OpenSSLError.UnableToExtractSignedDataFromPKCS7("Unable to get public key")
|
||||
}
|
||||
|
||||
|
||||
/// Extracts the encapsulated content section from a SignedData PKCS7 container (if present)
|
||||
/// - Returns: The encapsulated content from a PKCS7 container if we could read it
|
||||
/// - Throws: Error if we can't find or read the encapsulated content
|
||||
func getEncapsulatedContent() throws -> Data {
|
||||
guard let signedData = asn1.getChild(1)?.getChild(0),
|
||||
let encContent = signedData.getChild(2)?.getChild(1),
|
||||
let content = encContent.getChild(0) else {
|
||||
|
||||
throw OpenSSLError.UnableToExtractSignedDataFromPKCS7("Data in invalid format")
|
||||
}
|
||||
|
||||
var sigData : Data?
|
||||
if content.type.hasPrefix("OCTET STRING" ) {
|
||||
sigData = Data(hexRepToBin( content.value ))
|
||||
}
|
||||
|
||||
guard let ret = sigData else { throw OpenSSLError.UnableToExtractSignedDataFromPKCS7("noDataReturned") }
|
||||
return ret
|
||||
}
|
||||
|
||||
/// Gets the digest algorithm used to hash the encapsulated content in the signed data section (if present)
|
||||
/// - Returns: The digest algorithm used to hash the encapsulated content in the signed data section
|
||||
/// - Throws: Error if we can't find or read the digest algorithm
|
||||
func getEncapsulatedContentDigestAlgorithm() throws -> String {
|
||||
guard let signedData = asn1.getChild(1)?.getChild(0),
|
||||
let digestAlgo = signedData.getChild(1)?.getChild(0)?.getChild(0) else {
|
||||
throw OpenSSLError.UnableToExtractSignedDataFromPKCS7("Data in invalid format")
|
||||
}
|
||||
|
||||
return String(digestAlgo.value)
|
||||
}
|
||||
|
||||
/// Gets the signed attributes section (if present)
|
||||
/// - Returns: the signed attributes section
|
||||
/// - Throws: Error if we can't find or read the signed attributes
|
||||
func getSignedAttributes( ) throws -> Data {
|
||||
|
||||
// Get the SignedAttributes section.
|
||||
guard let signedData = asn1.getChild(1)?.getChild(0),
|
||||
let signerInfo = signedData.getChild(4),
|
||||
let signedAttrs = signerInfo.getChild(0)?.getChild(3) else {
|
||||
|
||||
throw OpenSSLError.UnableToExtractSignedDataFromPKCS7("Data in invalid format")
|
||||
}
|
||||
|
||||
var bytes = [UInt8](self.pkcs7CertificateData[signedAttrs.pos ..< signedAttrs.pos + signedAttrs.headerLen + signedAttrs.length])
|
||||
|
||||
// The first byte will be 0xA0 -> as its a explicit tag for a contextual item which we need to convert
|
||||
// for the hash to calculate correctly
|
||||
// We know that the actual tag is a SET (0x31) - See section 5.4 of https://tools.ietf.org/html/rfc5652
|
||||
// So we need to change this from 0xA0 to 0x31
|
||||
if bytes[0] == 0xA0 {
|
||||
bytes[0] = 0x31
|
||||
}
|
||||
let signedAttribs = Data(bytes)
|
||||
|
||||
return signedAttribs
|
||||
}
|
||||
|
||||
/// Gets the message digest from the signed attributes section (if present)
|
||||
/// - Returns: the message digest
|
||||
/// - Throws: Error if we can't find or read the message digest
|
||||
func getMessageDigestFromSignedAttributes( ) throws -> Data {
|
||||
|
||||
// For the SOD, the SignedAttributes consists of:
|
||||
// A Content type Object (which has the value of the attributes content type)
|
||||
// A messageDigest Object which has the message digest as it value
|
||||
// We want the messageDigest value
|
||||
|
||||
guard let signedData = asn1.getChild(1)?.getChild(0),
|
||||
let signerInfo = signedData.getChild(4),
|
||||
let signedAttrs = signerInfo.getChild(0)?.getChild(3) else {
|
||||
|
||||
throw OpenSSLError.UnableToExtractSignedDataFromPKCS7("Data in invalid format")
|
||||
}
|
||||
|
||||
// Find the messageDigest in the signedAttributes section
|
||||
var sigData : Data?
|
||||
for i in 0 ..< signedAttrs.getNumberOfChildren() {
|
||||
let attrObj = signedAttrs.getChild(i)
|
||||
if attrObj?.getChild(0)?.value == "messageDigest" {
|
||||
if let set = attrObj?.getChild(1),
|
||||
let digestVal = set.getChild(0) {
|
||||
|
||||
if digestVal.type.hasPrefix("OCTET STRING" ) {
|
||||
sigData = Data(hexRepToBin( digestVal.value ) )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard let messageDigest = sigData else { throw OpenSSLError.UnableToExtractSignedDataFromPKCS7("No messageDigest Returned") }
|
||||
|
||||
return messageDigest
|
||||
}
|
||||
|
||||
/// Gets the signature data (if present)
|
||||
/// - Returns: the signature
|
||||
/// - Throws: Error if we can't find or read the signature
|
||||
func getSignature( ) throws -> Data {
|
||||
|
||||
guard let signedData = asn1.getChild(1)?.getChild(0),
|
||||
let signerInfo = signedData.getChild(4),
|
||||
let signature = signerInfo.getChild(0)?.getChild(5) else {
|
||||
|
||||
throw OpenSSLError.UnableToExtractSignedDataFromPKCS7("Data in invalid format")
|
||||
}
|
||||
|
||||
var sigData : Data?
|
||||
if signature.type.hasPrefix("OCTET STRING" ) {
|
||||
sigData = Data(hexRepToBin( signature.value ))
|
||||
}
|
||||
|
||||
guard let ret = sigData else { throw OpenSSLError.UnableToExtractSignedDataFromPKCS7("noDataReturned") }
|
||||
return ret
|
||||
}
|
||||
|
||||
/// Gets the signature algorithm used (if present)
|
||||
/// - Returns: the signature algorithm used
|
||||
/// - Throws: Error if we can't find or read the signature algorithm
|
||||
func getSignatureAlgorithm( ) throws -> String {
|
||||
|
||||
guard let signedData = asn1.getChild(1)?.getChild(0),
|
||||
let signerInfo = signedData.getChild(4),
|
||||
let signatureAlgo = signerInfo.getChild(0)?.getChild(4)?.getChild(0) else {
|
||||
|
||||
throw OpenSSLError.UnableToExtractSignedDataFromPKCS7("Data in invalid format")
|
||||
}
|
||||
|
||||
// Vals I've seen are:
|
||||
// sha1WithRSAEncryption => default pkcs1
|
||||
// sha256WithRSAEncryption => default pkcs1
|
||||
// rsassaPss => pss
|
||||
return signatureAlgo.value
|
||||
}
|
||||
}
|
||||
130
app/ios/NFCPassportReader/DataGroups/SecurityInfo.swift
Normal file
130
app/ios/NFCPassportReader/DataGroups/SecurityInfo.swift
Normal file
@@ -0,0 +1,130 @@
|
||||
|
||||
import Foundation
|
||||
import OpenSSL
|
||||
|
||||
@available(iOS 13, macOS 10.15,*)
|
||||
public class SecurityInfo {
|
||||
// Active Authentication OID
|
||||
static let ID_AA_OID = "2.23.136.1.1.5"
|
||||
|
||||
// Active Authentication Signature Algorithm OIDS
|
||||
// Specified in BSI TR 03111 Section 5.2.1.
|
||||
static let ECDSA_PLAIN_SIGNATURES = "0.4.0.127.0.7.1.1.4.1";
|
||||
static let ECDSA_PLAIN_SHA1_OID = ECDSA_PLAIN_SIGNATURES + ".1"; // 0.4.0.127.0.7.1.1.4.1.1, ecdsa-plain-SHA1
|
||||
static let ECDSA_PLAIN_SHA224_OID = ECDSA_PLAIN_SIGNATURES + ".2"; // 0.4.0.127.0.7.1.1.4.1.2, ecdsa-plain-SHA224
|
||||
static let ECDSA_PLAIN_SHA256_OID = ECDSA_PLAIN_SIGNATURES + ".3"; // 0.4.0.127.0.7.1.1.4.1.3, ecdsa-plain-SHA256
|
||||
static let ECDSA_PLAIN_SHA384_OID = ECDSA_PLAIN_SIGNATURES + ".4"; // 0.4.0.127.0.7.1.1.4.1.4, ecdsa-plain-SHA384
|
||||
static let ECDSA_PLAIN_SHA512_OID = ECDSA_PLAIN_SIGNATURES + ".5"; // 0.4.0.127.0.7.1.1.4.1.5, ecdsa-plain-SHA512
|
||||
static let ECDSA_PLAIN_RIPEMD160_OID = ECDSA_PLAIN_SIGNATURES + ".6"; // 0.4.0.127.0.7.1.1.4.1.6, ecdsa-plain-RIPEMD160
|
||||
|
||||
// Chip Authentication Public Key OIDS
|
||||
static let ID_PK_DH_OID = "0.4.0.127.0.7.2.2.1.1"
|
||||
static let ID_PK_ECDH_OID = "0.4.0.127.0.7.2.2.1.2"
|
||||
|
||||
// Chip Authentication OIDS
|
||||
static let ID_CA_DH_3DES_CBC_CBC_OID = "0.4.0.127.0.7.2.2.3.1.1"
|
||||
static let ID_CA_ECDH_3DES_CBC_CBC_OID = "0.4.0.127.0.7.2.2.3.2.1"
|
||||
static let ID_CA_DH_AES_CBC_CMAC_128_OID = "0.4.0.127.0.7.2.2.3.1.2"
|
||||
static let ID_CA_DH_AES_CBC_CMAC_192_OID = "0.4.0.127.0.7.2.2.3.1.3"
|
||||
static let ID_CA_DH_AES_CBC_CMAC_256_OID = "0.4.0.127.0.7.2.2.3.1.4"
|
||||
static let ID_CA_ECDH_AES_CBC_CMAC_128_OID = "0.4.0.127.0.7.2.2.3.2.2"
|
||||
static let ID_CA_ECDH_AES_CBC_CMAC_192_OID = "0.4.0.127.0.7.2.2.3.2.3"
|
||||
static let ID_CA_ECDH_AES_CBC_CMAC_256_OID = "0.4.0.127.0.7.2.2.3.2.4"
|
||||
|
||||
|
||||
// PACE OIDS
|
||||
static let ID_BSI = "0.4.0.127.0.7"
|
||||
static let ID_PACE = ID_BSI + ".2.2.4"
|
||||
static let ID_PACE_DH_GM = ID_PACE + ".1"
|
||||
static let ID_PACE_DH_GM_3DES_CBC_CBC = ID_PACE_DH_GM + ".1"; // 0.4.0.127.0.7.2.2.4.1.1, id-PACE-DH-GM-3DES-CBC-CBC
|
||||
static let ID_PACE_DH_GM_AES_CBC_CMAC_128 = ID_PACE_DH_GM + ".2"; // 0.4.0.127.0.7.2.2.4.1.2, id-PACE-DH-GM-AES-CBC-CMAC-128
|
||||
static let ID_PACE_DH_GM_AES_CBC_CMAC_192 = ID_PACE_DH_GM + ".3"; // 0.4.0.127.0.7.2.2.4.1.3, id-PACE-DH-GM-AES-CBC-CMAC-192
|
||||
static let ID_PACE_DH_GM_AES_CBC_CMAC_256 = ID_PACE_DH_GM + ".4"; // 0.4.0.127.0.7.2.2.4.1.4, id-PACE-DH-GM-AES-CBC-CMAC-256
|
||||
|
||||
static let ID_PACE_ECDH_GM = ID_PACE + ".2"
|
||||
static let ID_PACE_ECDH_GM_3DES_CBC_CBC = ID_PACE_ECDH_GM + ".1"; // 0.4.0.127.0.7.2.2.4.2.1, id-PACE-ECDH-GM-3DES-CBC-CBC
|
||||
static let ID_PACE_ECDH_GM_AES_CBC_CMAC_128 = ID_PACE_ECDH_GM + ".2"; // 0.4.0.127.0.7.2.2.4.2.2, id-PACE-ECDH-GM-AES-CBC-CMAC-128
|
||||
static let ID_PACE_ECDH_GM_AES_CBC_CMAC_192 = ID_PACE_ECDH_GM + ".3"; // 0.4.0.127.0.7.2.2.4.2.3, id-PACE-ECDH-GM-AES-CBC-CMAC-192
|
||||
static let ID_PACE_ECDH_GM_AES_CBC_CMAC_256 = ID_PACE_ECDH_GM + ".4"; // 0.4.0.127.0.7.2.2.4.2.4, id-PACE-ECDH-GM-AES-CBC-CMAC-256
|
||||
|
||||
static let ID_PACE_DH_IM = ID_PACE + ".3"
|
||||
static let ID_PACE_DH_IM_3DES_CBC_CBC = ID_PACE_DH_IM + ".1"; // 0.4.0.127.0.7.2.2.4.3.1, id-PACE-DH-IM-3DES-CBC-CBC
|
||||
static let ID_PACE_DH_IM_AES_CBC_CMAC_128 = ID_PACE_DH_IM + ".2"; // 0.4.0.127.0.7.2.2.4.3.2, id-PACE-DH-IM-AES-CBC-CMAC-128
|
||||
static let ID_PACE_DH_IM_AES_CBC_CMAC_192 = ID_PACE_DH_IM + ".3"; // 0.4.0.127.0.7.2.2.4.3.3, id-PACE-DH-IM-AES-CBC-CMAC-192
|
||||
static let ID_PACE_DH_IM_AES_CBC_CMAC_256 = ID_PACE_DH_IM + ".4"; // 0.4.0.127.0.7.2.2.4.3.4, id-PACE-DH-IM-AES-CBC-CMAC-256
|
||||
|
||||
static let ID_PACE_ECDH_IM = ID_PACE + ".4"
|
||||
static let ID_PACE_ECDH_IM_3DES_CBC_CBC = ID_PACE_ECDH_IM + ".1"; // 0.4.0.127.0.7.2.2.4.4.1, id-PACE-ECDH-IM-3DES-CBC-CBC
|
||||
static let ID_PACE_ECDH_IM_AES_CBC_CMAC_128 = ID_PACE_ECDH_IM + ".2"; // 0.4.0.127.0.7.2.2.4.4.2, id-PACE-ECDH-IM-AES-CBC-CMAC-128
|
||||
static let ID_PACE_ECDH_IM_AES_CBC_CMAC_192 = ID_PACE_ECDH_IM + ".3"; // 0.4.0.127.0.7.2.2.4.4.3, id-PACE-ECDH-IM-AES-CBC-CMAC-192
|
||||
static let ID_PACE_ECDH_IM_AES_CBC_CMAC_256 = ID_PACE_ECDH_IM + ".4"; // 0.4.0.127.0.7.2.2.4.4.4, id-PACE-ECDH-IM-AES-CBC-CMAC-256
|
||||
|
||||
static let ID_PACE_ECDH_CAM = ID_PACE + ".6"
|
||||
static let ID_PACE_ECDH_CAM_AES_CBC_CMAC_128 = ID_PACE_ECDH_CAM + ".2"; // 0.4.0.127.0.7.2.2.4.6.2, id-PACE-ECDH-CAM-AES-CBC-CMAC-128
|
||||
static let ID_PACE_ECDH_CAM_AES_CBC_CMAC_192 = ID_PACE_ECDH_CAM + ".3"; // 0.4.0.127.0.7.2.2.4.6.3, id-PACE-ECDH-CAM-AES-CBC-CMAC-192
|
||||
static let ID_PACE_ECDH_CAM_AES_CBC_CMAC_256 = ID_PACE_ECDH_CAM + ".4"; // 0.4.0.127.0.7.2.2.4.6.4, id-PACE-ECDH-CAM-AES-CBC-CMAC-256
|
||||
|
||||
public func getObjectIdentifier() -> String {
|
||||
preconditionFailure("This method must be overridden")
|
||||
}
|
||||
|
||||
public func getProtocolOIDString() -> String {
|
||||
preconditionFailure("This method must be overridden")
|
||||
}
|
||||
|
||||
static func getInstance( object : ASN1Item, body: [UInt8] ) -> SecurityInfo? {
|
||||
let oid = object.getChild(0)?.value ?? ""
|
||||
let requiredData = object.getChild(1)!
|
||||
var optionalData : ASN1Item? = nil
|
||||
if (object.getNumberOfChildren() == 3) {
|
||||
optionalData = object.getChild(2)
|
||||
}
|
||||
|
||||
if ChipAuthenticationPublicKeyInfo.checkRequiredIdentifier(oid) {
|
||||
|
||||
let keyData : [UInt8] = [UInt8](body[requiredData.pos ..< requiredData.pos+requiredData.headerLen+requiredData.length])
|
||||
|
||||
var subjectPublicKeyInfo : OpaquePointer? = nil
|
||||
let _ = keyData.withUnsafeBytes { (ptr) in
|
||||
var newPtr = ptr.baseAddress?.assumingMemoryBound(to: UInt8.self)
|
||||
|
||||
subjectPublicKeyInfo = d2i_PUBKEY(nil, &newPtr, keyData.count)
|
||||
}
|
||||
|
||||
if let subjectPublicKeyInfo = subjectPublicKeyInfo {
|
||||
|
||||
if optionalData == nil {
|
||||
return ChipAuthenticationPublicKeyInfo(oid:oid, pubKey:subjectPublicKeyInfo);
|
||||
} else {
|
||||
let keyId = Int(optionalData!.value, radix: 16)
|
||||
return ChipAuthenticationPublicKeyInfo(oid:oid, pubKey:subjectPublicKeyInfo, keyId: keyId);
|
||||
}
|
||||
|
||||
}
|
||||
} else if ChipAuthenticationInfo.checkRequiredIdentifier(oid) {
|
||||
let version = Int(requiredData.value) ?? -1
|
||||
if let optionalData = optionalData {
|
||||
let keyId = Int(optionalData.value, radix: 16)
|
||||
return ChipAuthenticationInfo(oid: oid, version: version, keyId: keyId);
|
||||
} else {
|
||||
return ChipAuthenticationInfo(oid: oid, version: version);
|
||||
}
|
||||
} else if PACEInfo.checkRequiredIdentifier(oid) {
|
||||
let version = Int(requiredData.value) ?? -1
|
||||
var parameterId : Int? = nil
|
||||
|
||||
if let optionalData = optionalData {
|
||||
parameterId = Int(optionalData.value, radix:16)
|
||||
}
|
||||
return PACEInfo(oid: oid, version: version, parameterId: parameterId);
|
||||
} else if ActiveAuthenticationInfo.checkRequiredIdentifier(oid) {
|
||||
let version = Int(requiredData.value) ?? -1
|
||||
if let optionalData = optionalData {
|
||||
return ActiveAuthenticationInfo(oid: oid, version: version, signatureAlgorithmOID: optionalData.value)
|
||||
} else {
|
||||
return ActiveAuthenticationInfo(oid: oid, version: version)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,3 @@
|
||||
//
|
||||
// Errors.swift
|
||||
// NFCPassportReader
|
||||
//
|
||||
// Created by Andy Qua on 09/02/2021.
|
||||
// Copyright © 2021 Andy Qua. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: TagError
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="AwesomeProject" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="GJd-Yh-RWb">
|
||||
<rect key="frame" x="0.0" y="202" width="375" height="43"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Powered by React Native" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="MN2-I3-ftu">
|
||||
<rect key="frame" x="0.0" y="626" width="375" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Bcu-3y-fUS" firstAttribute="bottom" secondItem="MN2-I3-ftu" secondAttribute="bottom" constant="20" id="OZV-Vh-mqD"/>
|
||||
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="GJd-Yh-RWb" secondAttribute="centerX" id="Q3B-4B-g5h"/>
|
||||
<constraint firstItem="MN2-I3-ftu" firstAttribute="centerX" secondItem="Bcu-3y-fUS" secondAttribute="centerX" id="akx-eg-2ui"/>
|
||||
<constraint firstItem="MN2-I3-ftu" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" id="i1E-0Y-4RG"/>
|
||||
<constraint firstItem="GJd-Yh-RWb" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="bottom" multiplier="1/3" constant="1" id="moa-c2-u7t"/>
|
||||
<constraint firstItem="GJd-Yh-RWb" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" symbolic="YES" id="x7j-FC-K8j"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="52.173913043478265" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -1,11 +1,3 @@
|
||||
//
|
||||
// Logging.swift
|
||||
// NFCTest
|
||||
//
|
||||
// Created by Andy Qua on 11/06/2019.
|
||||
// Copyright © 2019 Andy Qua. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// TODO: Quick log functions - will move this to something better
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
//
|
||||
// NFCPassportModel.swift
|
||||
// NFCPassportReader
|
||||
//
|
||||
// Created by Andy Qua on 29/10/2019.
|
||||
//
|
||||
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(iOS)
|
||||
|
||||
50
app/ios/NFCPassportReader/NFCViewDisplayMessage.swift
Normal file
50
app/ios/NFCPassportReader/NFCViewDisplayMessage.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
import Foundation
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public enum NFCViewDisplayMessage {
|
||||
case requestPresentPassport
|
||||
case authenticatingWithPassport(Int)
|
||||
case readingDataGroupProgress(DataGroupId, Int)
|
||||
case error(NFCPassportReaderError)
|
||||
case successfulRead
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
extension NFCViewDisplayMessage {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .requestPresentPassport:
|
||||
return "Hold your iPhone near an NFC enabled passport."
|
||||
case .authenticatingWithPassport(let progress):
|
||||
let progressString = handleProgress(percentualProgress: progress)
|
||||
return "Authenticating with passport.....\n\n\(progressString)"
|
||||
case .readingDataGroupProgress(let dataGroup, let progress):
|
||||
let progressString = handleProgress(percentualProgress: progress)
|
||||
return "Reading \(dataGroup).....\n\n\(progressString)"
|
||||
case .error(let tagError):
|
||||
switch tagError {
|
||||
case NFCPassportReaderError.TagNotValid:
|
||||
return "Tag not valid."
|
||||
case NFCPassportReaderError.MoreThanOneTagFound:
|
||||
return "More than 1 tags was found. Please present only 1 tag."
|
||||
case NFCPassportReaderError.ConnectionError:
|
||||
return "Connection error. Please try again."
|
||||
case NFCPassportReaderError.InvalidMRZKey:
|
||||
return "MRZ Key not valid for this document."
|
||||
case NFCPassportReaderError.ResponseError(let description, let sw1, let sw2):
|
||||
return "Sorry, there was a problem reading the passport. \(description) - (0x\(sw1), 0x\(sw2)"
|
||||
default:
|
||||
return "Sorry, there was a problem reading the passport. Please try again"
|
||||
}
|
||||
case .successfulRead:
|
||||
return "Passport read successfully"
|
||||
}
|
||||
}
|
||||
|
||||
func handleProgress(percentualProgress: Int) -> String {
|
||||
let p = (percentualProgress/20)
|
||||
let full = String(repeating: "🟢 ", count: p)
|
||||
let empty = String(repeating: "⚪️ ", count: 5-p)
|
||||
return "\(full)\(empty)"
|
||||
}
|
||||
}
|
||||
697
app/ios/NFCPassportReader/OpenSSLUtils.swift
Normal file
697
app/ios/NFCPassportReader/OpenSSLUtils.swift
Normal file
@@ -0,0 +1,697 @@
|
||||
import Foundation
|
||||
import OpenSSL
|
||||
import CryptoTokenKit
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class OpenSSLUtils {
|
||||
private static var loaded = false
|
||||
|
||||
/// Returns any OpenSSL Error as a String
|
||||
public static func getOpenSSLError() -> String {
|
||||
|
||||
guard let out = BIO_new(BIO_s_mem()) else { return "Unknown" }
|
||||
defer { BIO_free(out) }
|
||||
|
||||
ERR_print_errors( out )
|
||||
let str = OpenSSLUtils.bioToString( bio:out )
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
/// Extracts the contents of a BIO object and returns it as a String
|
||||
/// - Parameter bio: a Pointer to a BIO buffer
|
||||
/// - Returns: A string containing the contents of the BIO buffer
|
||||
static func bioToString( bio : OpaquePointer ) -> String {
|
||||
|
||||
let len = BIO_ctrl(bio, BIO_CTRL_PENDING, 0, nil)
|
||||
var buffer = [CChar](repeating: 0, count: len+1)
|
||||
BIO_read(bio, &buffer, Int32(len))
|
||||
|
||||
// Ensure last value is 0 (null terminated) otherwise we get buffer overflow!
|
||||
buffer[len] = 0
|
||||
let ret = String(cString:buffer)
|
||||
return ret
|
||||
}
|
||||
|
||||
static func X509ToPEM( x509: OpaquePointer ) -> String {
|
||||
|
||||
let out = BIO_new(BIO_s_mem())!
|
||||
defer { BIO_free( out) }
|
||||
|
||||
PEM_write_bio_X509(out, x509);
|
||||
let str = OpenSSLUtils.bioToString( bio:out )
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
static func pubKeyToPEM( pubKey: OpaquePointer ) -> String {
|
||||
|
||||
let out = BIO_new(BIO_s_mem())!
|
||||
defer { BIO_free( out) }
|
||||
|
||||
PEM_write_bio_PUBKEY(out, pubKey);
|
||||
let str = OpenSSLUtils.bioToString( bio:out )
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
static func privKeyToPEM( privKey: OpaquePointer ) -> String {
|
||||
|
||||
let out = BIO_new(BIO_s_mem())!
|
||||
defer { BIO_free( out) }
|
||||
|
||||
PEM_write_bio_PrivateKey(out, privKey, nil, nil, 0, nil, nil)
|
||||
let str = OpenSSLUtils.bioToString( bio:out )
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
static func pkcs7DataToPEM( pkcs7: Data ) -> String {
|
||||
|
||||
let inf = BIO_new(BIO_s_mem())!
|
||||
defer { BIO_free( inf) }
|
||||
let out = BIO_new(BIO_s_mem())!
|
||||
defer { BIO_free( out) }
|
||||
|
||||
let _ = pkcs7.withUnsafeBytes { (ptr) in
|
||||
BIO_write(inf, ptr.baseAddress?.assumingMemoryBound(to: Int8.self), Int32(pkcs7.count))
|
||||
}
|
||||
guard let p7 = d2i_PKCS7_bio(inf, nil) else { return "" }
|
||||
defer { PKCS7_free(p7) }
|
||||
|
||||
PEM_write_bio_PKCS7(out, p7)
|
||||
let str = OpenSSLUtils.bioToString( bio:out )
|
||||
return str
|
||||
}
|
||||
|
||||
|
||||
/// Extracts a X509 certificate in PEM format from a PKCS7 container
|
||||
/// - Parameter pkcs7Der: The PKCS7 container in DER format
|
||||
/// - Returns: The PEM formatted X509 certificate
|
||||
/// - Throws: A OpenSSLError.UnableToGetX509CertificateFromPKCS7 are thrown for any error
|
||||
static func getX509CertificatesFromPKCS7( pkcs7Der : Data ) throws -> [X509Wrapper] {
|
||||
|
||||
guard let inf = BIO_new(BIO_s_mem()) else { throw OpenSSLError.UnableToGetX509CertificateFromPKCS7("Unable to allocate input buffer") }
|
||||
defer { BIO_free(inf) }
|
||||
let _ = pkcs7Der.withUnsafeBytes { (ptr) in
|
||||
BIO_write(inf, ptr.baseAddress?.assumingMemoryBound(to: Int8.self), Int32(pkcs7Der.count))
|
||||
}
|
||||
guard let p7 = d2i_PKCS7_bio(inf, nil) else { throw OpenSSLError.UnableToGetX509CertificateFromPKCS7("Unable to read PKCS7 DER data") }
|
||||
defer { PKCS7_free(p7) }
|
||||
|
||||
var certs : OpaquePointer? = nil
|
||||
let i = OBJ_obj2nid(p7.pointee.type);
|
||||
switch (i) {
|
||||
case NID_pkcs7_signed:
|
||||
if let sign = p7.pointee.d.sign {
|
||||
certs = sign.pointee.cert
|
||||
}
|
||||
break;
|
||||
case NID_pkcs7_signedAndEnveloped:
|
||||
if let signed_and_enveloped = p7.pointee.d.signed_and_enveloped {
|
||||
certs = signed_and_enveloped.pointee.cert
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
var ret = [X509Wrapper]()
|
||||
if let certs = certs {
|
||||
let certCount = sk_X509_num(certs)
|
||||
for i in 0 ..< certCount {
|
||||
let x = sk_X509_value(certs, i);
|
||||
if let x509 = X509Wrapper(with:x) {
|
||||
ret.append( x509 )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
/// Checks whether a trust chain can be built up to verify a X509 certificate. A CAFile containing a list of trusted certificates (each in PEM format)
|
||||
/// is used to build the trust chain.
|
||||
/// The trusted certificates in this use case are typically from a Countries master list (see the scripts for more informaton on how to prepare this)
|
||||
/// - Parameter x509Cert: The X509 certificate (in PEM format) to verify
|
||||
/// - Parameter CAFile: The URL path of a file containing the list of certificates used to try to discover and build a trust chain
|
||||
/// - Returns: either the X509 issue signing certificate that was used to sign the passed in X509 certificate or an error
|
||||
static func verifyTrustAndGetIssuerCertificate( x509 : X509Wrapper, CAFile : URL ) -> Result<X509Wrapper, OpenSSLError> {
|
||||
|
||||
guard let cert_ctx = X509_STORE_new() else { return .failure(OpenSSLError.UnableToVerifyX509CertificateForSOD("Unable to create certificate store")) }
|
||||
defer { X509_STORE_free(cert_ctx) }
|
||||
|
||||
X509_STORE_set_verify_cb(cert_ctx) { (ok, ctx) -> Int32 in
|
||||
let cert_error = X509_STORE_CTX_get_error(ctx)
|
||||
|
||||
if ok == 0 {
|
||||
let errVal = X509_verify_cert_error_string(Int(cert_error))
|
||||
let val = errVal!.withMemoryRebound(to: CChar.self, capacity: 1000) { (ptr) in
|
||||
return String(cString: ptr)
|
||||
}
|
||||
|
||||
Log.error("error \(cert_error) at \(X509_STORE_CTX_get_error_depth(ctx)) depth lookup:\(val)" )
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
guard let lookup = X509_STORE_add_lookup(cert_ctx, X509_LOOKUP_file()) else { return .failure(OpenSSLError.UnableToVerifyX509CertificateForSOD("Unable to add lookup to store")) }
|
||||
|
||||
// Load masterList.pem file
|
||||
var rc = X509_LOOKUP_ctrl(lookup, X509_L_FILE_LOAD, CAFile.path, Int(X509_FILETYPE_PEM), nil)
|
||||
|
||||
guard let store = X509_STORE_CTX_new() else {
|
||||
return .failure(OpenSSLError.UnableToVerifyX509CertificateForSOD("Unable to create new X509_STORE_CTX"))
|
||||
}
|
||||
defer { X509_STORE_CTX_free(store) }
|
||||
|
||||
X509_STORE_set_flags(cert_ctx, 0)
|
||||
rc = X509_STORE_CTX_init(store, cert_ctx, x509.cert, nil)
|
||||
if rc == 0 {
|
||||
return .failure(OpenSSLError.UnableToVerifyX509CertificateForSOD("Unable to initialise X509_STORE_CTX"))
|
||||
}
|
||||
|
||||
// discover and verify X509 certificte chain
|
||||
let i = X509_verify_cert(store);
|
||||
if i != 1 {
|
||||
let err = X509_STORE_CTX_get_error(store)
|
||||
|
||||
return .failure(OpenSSLError.UnableToVerifyX509CertificateForSOD("Verification of certificate failed - errorCode \(err)"))
|
||||
}
|
||||
|
||||
// Get chain and issue certificate is the last cert in the chain
|
||||
let chain = X509_STORE_CTX_get1_chain(store);
|
||||
let nrCertsInChain = sk_X509_num(chain)
|
||||
if nrCertsInChain > 1 {
|
||||
let cert = sk_X509_value(chain, nrCertsInChain-1)
|
||||
if let certWrapper = X509Wrapper(with: cert) {
|
||||
return .success( certWrapper )
|
||||
}
|
||||
}
|
||||
|
||||
return .failure(OpenSSLError.UnableToVerifyX509CertificateForSOD("Unable to get issuer certificate - not found"))
|
||||
}
|
||||
|
||||
|
||||
/// Verifies the signed data section against the stored certificate and extracts the signed data section from a PKCS7 container (if present and valid)
|
||||
/// - Parameter pkcs7Der: The PKCS7 container in DER format
|
||||
/// - Returns: The signed data from a PKCS7 container if we could read it
|
||||
///
|
||||
/// - Note: To test from the command line using openssl (NOTE NOT THE default mac version as it doesn't currently support CMS):
|
||||
/// extract the SOD Base64 from an exported passport (you will need to unescape slashes!) - save this to ppt.b64
|
||||
/// convert to binary (cat ppt.b64 | base64 -D > ppt.bin
|
||||
/// extract the der file from the SOD (which includes header) - tail -c+5 ppt.bin > aq.der (blindy discards header)
|
||||
/// convert der to PEM - openssl pkcs7 -in ppt.der --inform der -out ppt.pem -outform pem
|
||||
/// verify signature data against included document signing cert - openssl cms -verify -in ppt.pem -inform pem -noverify
|
||||
/// the -noverify is don't verify against the signers certifcate (as we don' thave that!)
|
||||
///
|
||||
/// This should return Verification Successful and the signed data
|
||||
static func verifyAndReturnSODEncapsulatedDataUsingCMS( sod : SOD ) throws -> Data {
|
||||
|
||||
guard let inf = BIO_new(BIO_s_mem()) else { throw OpenSSLError.VerifyAndReturnSODEncapsulatedData("CMS - Unable to allocate input buffer") }
|
||||
defer { BIO_free(inf) }
|
||||
|
||||
guard let out = BIO_new(BIO_s_mem()) else { throw OpenSSLError.VerifyAndReturnSODEncapsulatedData("CMS - Unable to allocate output buffer") }
|
||||
defer { BIO_free(out) }
|
||||
|
||||
let _ = sod.body.withUnsafeBytes { (ptr) in
|
||||
BIO_write(inf, ptr.baseAddress?.assumingMemoryBound(to: UInt8.self), Int32(sod.body.count))
|
||||
}
|
||||
|
||||
guard let cms = d2i_CMS_bio(inf, nil) else {
|
||||
throw OpenSSLError.VerifyAndReturnSODEncapsulatedData("CMS - Verification of P7 failed - unable to create CMS")
|
||||
}
|
||||
defer { CMS_ContentInfo_free(cms) }
|
||||
|
||||
let flags : UInt32 = UInt32(CMS_NO_SIGNER_CERT_VERIFY)
|
||||
|
||||
if CMS_verify(cms, nil, nil, nil, out, flags) == 0 {
|
||||
throw OpenSSLError.VerifyAndReturnSODEncapsulatedData("CMS - Verification of P7 failed - unable to verify signature")
|
||||
}
|
||||
|
||||
Log.debug("Verification successful\n");
|
||||
let len = BIO_ctrl(out, BIO_CTRL_PENDING, 0, nil)
|
||||
var buffer = [UInt8](repeating: 0, count: len)
|
||||
BIO_read(out, &buffer, Int32(len))
|
||||
let sigData = Data(buffer)
|
||||
|
||||
return sigData
|
||||
}
|
||||
|
||||
|
||||
static func verifyAndReturnSODEncapsulatedData( sod : SOD ) throws -> Data {
|
||||
|
||||
let encapsulatedContent = try sod.getEncapsulatedContent()
|
||||
let signedAttribsHashAlgo = try sod.getEncapsulatedContentDigestAlgorithm()
|
||||
let signedAttributes = try sod.getSignedAttributes()
|
||||
let messageDigest = try sod.getMessageDigestFromSignedAttributes()
|
||||
let signature = try sod.getSignature()
|
||||
let sigType = try sod.getSignatureAlgorithm()
|
||||
|
||||
let pubKey = try sod.getPublicKey()
|
||||
|
||||
let mdHash : Data = try Data(calcHash(data: [UInt8](encapsulatedContent), hashAlgorithm: signedAttribsHashAlgo))
|
||||
|
||||
// Make sure that hash equals the messageDigest
|
||||
if messageDigest != mdHash {
|
||||
// Invalid - signed data hash doesn't match message digest hash
|
||||
throw OpenSSLError.VerifyAndReturnSODEncapsulatedData("messageDigest Hash doesn't hatch that of the signed attributes")
|
||||
}
|
||||
|
||||
// Verify signed attributes
|
||||
if !verifySignature( data : [UInt8](signedAttributes), signature : [UInt8](signature), pubKey : pubKey, digestType: sigType ) {
|
||||
|
||||
throw OpenSSLError.VerifyAndReturnSODEncapsulatedData("Unable to verify signature for signed attributes")
|
||||
}
|
||||
|
||||
return encapsulatedContent
|
||||
}
|
||||
|
||||
/// Parses a signed data structures encoded in ASN1 format and returns the structure in text format
|
||||
/// - Parameter data: The data to be parsed in ASN1 format
|
||||
/// - Returns: The parsed data as A String
|
||||
static func ASN1Parse( data: Data ) throws -> String {
|
||||
|
||||
guard let out = BIO_new(BIO_s_mem()) else { throw OpenSSLError.UnableToParseASN1("Unable to allocate output buffer") }
|
||||
defer { BIO_free(out) }
|
||||
|
||||
var parsed : String = ""
|
||||
let _ = try data.withUnsafeBytes { (ptr) in
|
||||
let rc = ASN1_parse_dump(out, ptr.baseAddress?.assumingMemoryBound(to: UInt8.self), data.count, 0, 0)
|
||||
if rc == 0 {
|
||||
let str = OpenSSLUtils.getOpenSSLError()
|
||||
Log.debug( "Failed to parse ASN1 Data - \(str)" )
|
||||
throw OpenSSLError.UnableToParseASN1("Failed to parse ASN1 Data - \(str)")
|
||||
}
|
||||
|
||||
parsed = bioToString(bio: out)
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Reads an RSA Public Key in DER format and converts it to an OpenSSL EVP_PKEY value for use whilst decrypting or verifying an RSA signature
|
||||
/// - Parameter data: The RSA key in DER format
|
||||
/// - Returns: The EVP_PKEY value
|
||||
/// NOTE THE CALLER IS RESPONSIBLE FOR FREEING THE RETURNED KEY USING
|
||||
/// EVP_PKEY_free(pemKey);
|
||||
static func readRSAPublicKey( data : [UInt8] ) throws -> OpaquePointer? {
|
||||
|
||||
guard let inf = BIO_new(BIO_s_mem()) else { throw OpenSSLError.UnableToReadECPublicKey("Unable to allocate output buffer") }
|
||||
defer { BIO_free(inf) }
|
||||
|
||||
let _ = data.withUnsafeBytes { (ptr) in
|
||||
BIO_write(inf, ptr.baseAddress?.assumingMemoryBound(to: UInt8.self), Int32(data.count))
|
||||
}
|
||||
|
||||
guard let rsakey = d2i_RSA_PUBKEY_bio(inf, nil) else { throw OpenSSLError.UnableToReadECPublicKey("Failed to load") }
|
||||
defer{ RSA_free(rsakey) }
|
||||
|
||||
let key = EVP_PKEY_new()
|
||||
if EVP_PKEY_set1_RSA(key, rsakey) != 1 {
|
||||
EVP_PKEY_free(key)
|
||||
throw OpenSSLError.UnableToReadECPublicKey("Failed to load")
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
/// This code is taken pretty much from rsautl.c - to decrypt a signature with a public key
|
||||
/// NOTE: Current no padding is used! - This seems to be the default for Active Authentication RSA signatures (guess)
|
||||
/// - Parameter signature: The RSA encrypted signature to decrypt
|
||||
/// - Parameter pubKey: The RSA Public Key
|
||||
/// - Returns: The decrypted signature data
|
||||
static func decryptRSASignature( signature : Data, pubKey : OpaquePointer ) throws -> [UInt8] {
|
||||
|
||||
let pad = RSA_NO_PADDING
|
||||
let rsa = EVP_PKEY_get1_RSA( pubKey )
|
||||
|
||||
let keysize = RSA_size(rsa);
|
||||
var outputBuf = [UInt8](repeating: 0, count: Int(keysize))
|
||||
|
||||
// Decrypt signature
|
||||
var outlen : Int32 = 0
|
||||
let _ = signature.withUnsafeBytes { (sigPtr) in
|
||||
let _ = outputBuf.withUnsafeMutableBytes { (outPtr) in
|
||||
outlen = RSA_public_decrypt(Int32(signature.count), sigPtr.baseAddress?.assumingMemoryBound(to: UInt8.self), outPtr.baseAddress?.assumingMemoryBound(to: UInt8.self), rsa, pad)
|
||||
}
|
||||
}
|
||||
|
||||
if outlen == 0 {
|
||||
let error = OpenSSLUtils.getOpenSSLError()
|
||||
throw OpenSSLError.UnableToDecryptRSASignature( "RSA_public_decrypt failed - \(error)" )
|
||||
}
|
||||
|
||||
return outputBuf
|
||||
}
|
||||
|
||||
/// Reads an ECDSA Public Key in DER format and converts it to an OpenSSL EVP_PKEY value for use whilst verifying a ECDSA signature
|
||||
/// - Parameter data: The ECDSA key in DER forma
|
||||
/// - Returns: The EVP_PKEY value
|
||||
/// NOTE THE CALLER IS RESPONSIBLE FOR FREEING THE RETURNED KEY USING
|
||||
/// EVP_PKEY_free(pemKey);
|
||||
static func readECPublicKey( data : [UInt8] ) throws -> OpaquePointer? {
|
||||
|
||||
guard let inf = BIO_new(BIO_s_mem()) else { throw OpenSSLError.UnableToReadECPublicKey("Unable to allocate output buffer") }
|
||||
defer { BIO_free(inf) }
|
||||
|
||||
let _ = data.withUnsafeBytes { (ptr) in
|
||||
BIO_write(inf, ptr.baseAddress?.assumingMemoryBound(to: UInt8.self), Int32(data.count))
|
||||
}
|
||||
|
||||
guard let eckey = d2i_EC_PUBKEY_bio(inf, nil) else { throw OpenSSLError.UnableToReadECPublicKey("Failed to load") }
|
||||
defer{ EC_KEY_free(eckey) }
|
||||
|
||||
guard let outf = BIO_new(BIO_s_mem()) else { throw OpenSSLError.UnableToReadECPublicKey("Unable to allocate output buffer") }
|
||||
defer { BIO_free(outf) }
|
||||
let _ = PEM_write_bio_EC_PUBKEY(outf, eckey);
|
||||
let pemKey = PEM_read_bio_PUBKEY(outf, nil, nil, nil)
|
||||
|
||||
return pemKey
|
||||
}
|
||||
|
||||
|
||||
/// Verifies Active Authentication data valid against an ECDSA signature and ECDSA Public Key - used in Active Authentication
|
||||
/// - Parameter publicKey: The OpenSSL EVP_PKEY ECDSA key
|
||||
/// - Parameter signature: The ECDSA signature to verify
|
||||
/// - Parameter data: The data used to generate the signature
|
||||
/// - Returns: True if the signature was verified
|
||||
static func verifyECDSASignature( publicKey:OpaquePointer, signature: [UInt8], data: [UInt8], digestType: String = "" ) -> Bool {
|
||||
|
||||
// We first need to convert the signature from PLAIN ECDSA to ASN1 DER encoded
|
||||
let ecsig = ECDSA_SIG_new()
|
||||
defer { ECDSA_SIG_free(ecsig) }
|
||||
let sigData = signature
|
||||
let l = sigData.count / 2
|
||||
sigData.withUnsafeBufferPointer { (unsafeBufPtr) in
|
||||
let unsafePointer = unsafeBufPtr.baseAddress!
|
||||
let r = BN_bin2bn(unsafePointer, Int32(l), nil)
|
||||
let s = BN_bin2bn((unsafePointer + l), Int32(l), nil)
|
||||
ECDSA_SIG_set0(ecsig, r, s)
|
||||
}
|
||||
let sigSize = i2d_ECDSA_SIG(ecsig, nil)
|
||||
var derBytes = [UInt8](repeating: 0, count: Int(sigSize))
|
||||
derBytes.withUnsafeMutableBufferPointer { (unsafeBufPtr) in
|
||||
var unsafePointer = unsafeBufPtr.baseAddress
|
||||
let _ = i2d_ECDSA_SIG(ecsig, &unsafePointer)
|
||||
}
|
||||
|
||||
let rc = verifySignature(data: data, signature: derBytes, pubKey: publicKey, digestType: digestType)
|
||||
return rc
|
||||
}
|
||||
|
||||
/// Verifies that a signature is valid for some data and a Public Key
|
||||
/// - Parameter data: The data used to generate the signature
|
||||
/// - Parameter signature: The signature to verify
|
||||
/// - Parameter publicKey: The OpenSSL EVP_PKEY key
|
||||
/// - Parameter digestType: the type of hash to use (empty string to use no digest type)
|
||||
/// - Returns: True if the signature was verified
|
||||
static func verifySignature( data : [UInt8], signature : [UInt8], pubKey : OpaquePointer, digestType: String ) -> Bool {
|
||||
|
||||
var digest = "sha256"
|
||||
let digestType = digestType.lowercased()
|
||||
if digestType.contains( "sha1" ) {
|
||||
digest = "sha1"
|
||||
} else if digestType.contains( "sha224" ) {
|
||||
digest = "sha224"
|
||||
} else if digestType.contains( "sha256" ) || digestType.contains( "rsassapss" ) {
|
||||
digest = "sha256"
|
||||
} else if digestType.contains( "sha384" ) {
|
||||
digest = "sha384"
|
||||
} else if digestType.contains( "sha512" ) {
|
||||
digest = "sha512"
|
||||
}
|
||||
|
||||
// Fix for some invalid ECDSA based signatures
|
||||
// An ECDSA signature comprises of a Sequence of 2 big integers (R & S) and the verification
|
||||
// is a linear equation of these two integers, the data hash and the public key
|
||||
// However, in some passports the encoding of the integers is incorrect and has a leading 00
|
||||
// causing the verification to fail.
|
||||
// So in this case, we check to see if it is actually a valid BigInteger, and if not, we remove the
|
||||
// leading prefix, check again and if a valid big integer this time then we use this otherwise
|
||||
// we keep the original value
|
||||
// If we change any values then we re-generate the signature and use this
|
||||
var fixedSignature = signature
|
||||
if digestType.contains( "ecdsa" ) {
|
||||
// Decode signature
|
||||
if let sequence = TKBERTLVRecord(from:Data(signature)),
|
||||
sequence.tag == 0x30,
|
||||
var intRecords = TKBERTLVRecord.sequenceOfRecords(from: sequence.value),
|
||||
intRecords.count == 2 {
|
||||
|
||||
var didFix = false
|
||||
for (idx, rec) in intRecords.enumerated() {
|
||||
// Only process if the first byte is a 0
|
||||
if rec.value[0] != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// There is a feature in TKBERTLVRecord.sequenceOfRecords where the 2nd record.data call
|
||||
// contains the data for the whole data not the actual record
|
||||
// (reported as FB9077037)
|
||||
// So for the moment, work aroud this and create a new record
|
||||
let fixedRec = TKBERTLVRecord( tag: rec.tag, value: rec.value)
|
||||
let data = [UInt8](fixedRec.data)
|
||||
|
||||
// Check to see if a valid Big Integer (we need the whole record including tag and length for the d2i_ASN1_INTEGER call)
|
||||
data.withUnsafeBufferPointer { (ptr) in
|
||||
var address = ptr.baseAddress
|
||||
let v = d2i_ASN1_INTEGER(nil, &address, data.count)
|
||||
defer { ASN1_INTEGER_free(v) }
|
||||
if v == nil {
|
||||
// Not a valid BigInteger, so remove the first value and try again
|
||||
let newRec = TKBERTLVRecord( tag: rec.tag, value: rec.value[1...])
|
||||
|
||||
let data2 = [UInt8](newRec.data)
|
||||
data2.withUnsafeBufferPointer { (ptr) in
|
||||
var address = ptr.baseAddress
|
||||
let v2 = d2i_ASN1_INTEGER(nil, &address, data2.count)
|
||||
defer { ASN1_INTEGER_free(v2) }
|
||||
if v2 != nil {
|
||||
// OK, we have a valid BigInteger this time so replace the original
|
||||
// record with the new one
|
||||
intRecords[idx] = newRec
|
||||
didFix = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We only reencode if we changed any of the integers, otherwise assume they were actually
|
||||
// correctly encoded
|
||||
if didFix {
|
||||
// re-encode
|
||||
let newSequence = TKBERTLVRecord( tag: sequence.tag, records: intRecords)
|
||||
fixedSignature = [UInt8](newSequence.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let md = EVP_get_digestbyname(digest)
|
||||
|
||||
let ctx = EVP_MD_CTX_new()
|
||||
var pkey_ctx : OpaquePointer?
|
||||
|
||||
defer{ EVP_MD_CTX_free( ctx) }
|
||||
|
||||
var nRes = EVP_DigestVerifyInit(ctx, &pkey_ctx, md, nil, pubKey)
|
||||
if ( nRes != 1 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if digestType.contains( "rsassapss" ) {
|
||||
EVP_PKEY_CTX_ctrl_str(pkey_ctx, "rsa_padding_mode", "pss" )
|
||||
EVP_PKEY_CTX_ctrl_str(pkey_ctx, "rsa_pss_saltlen", "auto" )
|
||||
}
|
||||
|
||||
nRes = EVP_DigestUpdate(ctx, data, data.count);
|
||||
if ( nRes != 1 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nRes = EVP_DigestVerifyFinal(ctx, fixedSignature, fixedSignature.count);
|
||||
if (nRes != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
static func generateAESCMAC( key: [UInt8], message : [UInt8] ) -> [UInt8] {
|
||||
let ctx = CMAC_CTX_new();
|
||||
defer { CMAC_CTX_free(ctx) }
|
||||
var key = key
|
||||
|
||||
var mac = [UInt8](repeating: 0, count: 32)
|
||||
var maclen : Int = 0
|
||||
|
||||
if key.count == 16 {
|
||||
CMAC_Init(ctx, &key, key.count, EVP_aes_128_cbc(), nil)
|
||||
} else if key.count == 24 {
|
||||
CMAC_Init(ctx, &key, key.count, EVP_aes_192_cbc(), nil)
|
||||
} else if key.count == 32 {
|
||||
CMAC_Init(ctx, &key, key.count, EVP_aes_256_cbc(), nil)
|
||||
}
|
||||
CMAC_Update(ctx, message, message.count);
|
||||
CMAC_Final(ctx, &mac, &maclen);
|
||||
|
||||
Log.verbose( "aesMac - mac - \(binToHexRep(mac))" )
|
||||
|
||||
return [UInt8](mac[0..<maclen])
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
static func asn1EncodeOID (oid : String) -> [UInt8] {
|
||||
|
||||
let obj = OBJ_txt2obj( oid.cString(using: .utf8), 1)
|
||||
let payloadLen = i2d_ASN1_OBJECT(obj, nil)
|
||||
|
||||
var data = [UInt8](repeating: 0, count: Int(payloadLen))
|
||||
|
||||
let _ = data.withUnsafeMutableBytes { (ptr) in
|
||||
var newPtr = ptr.baseAddress?.assumingMemoryBound(to: UInt8.self)
|
||||
_ = i2d_ASN1_OBJECT(obj, &newPtr)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public static func getPublicKeyData(from key:OpaquePointer) -> [UInt8]? {
|
||||
var data : [UInt8] = []
|
||||
// Get Key type
|
||||
let v = EVP_PKEY_base_id( key )
|
||||
if v == EVP_PKEY_DH || v == EVP_PKEY_DHX {
|
||||
guard let dh = EVP_PKEY_get0_DH(key) else {
|
||||
return nil
|
||||
}
|
||||
var dhPubKey : OpaquePointer?
|
||||
DH_get0_key(dh, &dhPubKey, nil)
|
||||
|
||||
let nrBytes = (BN_num_bits(dhPubKey)+7)/8
|
||||
data = [UInt8](repeating: 0, count: Int(nrBytes))
|
||||
_ = BN_bn2bin(dhPubKey, &data)
|
||||
} else if v == EVP_PKEY_EC {
|
||||
|
||||
guard let ec = EVP_PKEY_get0_EC_KEY(key),
|
||||
let ec_pub = EC_KEY_get0_public_key(ec),
|
||||
let ec_group = EC_KEY_get0_group(ec) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let form = EC_KEY_get_conv_form(ec)
|
||||
let len = EC_POINT_point2oct(ec_group, ec_pub, form, nil, 0, nil)
|
||||
data = [UInt8](repeating: 0, count: Int(len))
|
||||
if len == 0 {
|
||||
return nil
|
||||
}
|
||||
_ = EC_POINT_point2oct(ec_group, ec_pub, form, &data, len, nil)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// Caller is responsible for freeing the key
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public static func decodePublicKeyFromBytes(pubKeyData: [UInt8], params: OpaquePointer) -> OpaquePointer? {
|
||||
var pubKey : OpaquePointer?
|
||||
|
||||
let keyType = EVP_PKEY_base_id( params )
|
||||
if keyType == EVP_PKEY_DH || keyType == EVP_PKEY_DHX {
|
||||
|
||||
let dhKey = DH_new()
|
||||
defer{ DH_free(dhKey) }
|
||||
|
||||
// We don't free this as its part of the key!
|
||||
let bn = BN_bin2bn(pubKeyData, Int32(pubKeyData.count), nil)
|
||||
DH_set0_key(dhKey, bn, nil)
|
||||
|
||||
pubKey = EVP_PKEY_new()
|
||||
guard EVP_PKEY_set1_DH(pubKey, dhKey) == 1 else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
let ec = EVP_PKEY_get1_EC_KEY(params)
|
||||
let group = EC_KEY_get0_group(ec);
|
||||
let ecp = EC_POINT_new(group);
|
||||
let key = EC_KEY_new();
|
||||
defer {
|
||||
EC_KEY_free(ec)
|
||||
EC_POINT_free(ecp)
|
||||
EC_KEY_free(key)
|
||||
}
|
||||
|
||||
// Read EC_Point from public key data
|
||||
guard EC_POINT_oct2point(group, ecp, pubKeyData, pubKeyData.count, nil) == 1,
|
||||
EC_KEY_set_group(key, group) == 1,
|
||||
EC_KEY_set_public_key(key, ecp) == 1 else {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
pubKey = EVP_PKEY_new()
|
||||
guard EVP_PKEY_set1_EC_KEY(pubKey, key) == 1 else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return pubKey
|
||||
}
|
||||
|
||||
|
||||
public static func computeSharedSecret( privateKeyPair: OpaquePointer, publicKey: OpaquePointer ) -> [UInt8] {
|
||||
|
||||
// Oddly it seems that we cant use EVP_PKEY stuff for DH as it uses DTX keys which OpenSSL doesn't quite handle right
|
||||
// OR I'm misunderstanding something (which is more possible)
|
||||
// Works fine though for ECDH keys
|
||||
var secret : [UInt8]
|
||||
let keyType = EVP_PKEY_base_id( privateKeyPair )
|
||||
if keyType == EVP_PKEY_DH || keyType == EVP_PKEY_DHX {
|
||||
// Get bn for public key
|
||||
let dh = EVP_PKEY_get1_DH(privateKeyPair);
|
||||
|
||||
let dh_pub = EVP_PKEY_get1_DH(publicKey)
|
||||
var bn = BN_new()
|
||||
DH_get0_key( dh_pub, &bn, nil )
|
||||
|
||||
secret = [UInt8](repeating: 0, count: Int(DH_size(dh)))
|
||||
let len = DH_compute_key(&secret, bn, dh);
|
||||
|
||||
Log.verbose( "OpenSSLUtils.computeSharedSecret - DH secret len - \(len)" )
|
||||
} else {
|
||||
let ctx = EVP_PKEY_CTX_new(privateKeyPair, nil)
|
||||
defer{ EVP_PKEY_CTX_free(ctx) }
|
||||
|
||||
if EVP_PKEY_derive_init(ctx) != 1 {
|
||||
// error
|
||||
Log.error( "ERROR - \(OpenSSLUtils.getOpenSSLError())" )
|
||||
}
|
||||
|
||||
// Set the public key
|
||||
if EVP_PKEY_derive_set_peer( ctx, publicKey ) != 1 {
|
||||
// error
|
||||
Log.error( "ERROR - \(OpenSSLUtils.getOpenSSLError())" )
|
||||
}
|
||||
|
||||
// get buffer length needed for shared secret
|
||||
var keyLen = 0
|
||||
if EVP_PKEY_derive(ctx, nil, &keyLen) != 1 {
|
||||
// Error
|
||||
Log.error( "ERROR - \(OpenSSLUtils.getOpenSSLError())" )
|
||||
}
|
||||
|
||||
// Derive the shared secret
|
||||
secret = [UInt8](repeating: 0, count: keyLen)
|
||||
if EVP_PKEY_derive(ctx, &secret, &keyLen) != 1 {
|
||||
// Error
|
||||
Log.error( "ERROR - \(OpenSSLUtils.getOpenSSLError())" )
|
||||
}
|
||||
}
|
||||
return secret
|
||||
}
|
||||
|
||||
}
|
||||
614
app/ios/NFCPassportReader/PACEHandler.swift
Normal file
614
app/ios/NFCPassportReader/PACEHandler.swift
Normal file
@@ -0,0 +1,614 @@
|
||||
import Foundation
|
||||
import OpenSSL
|
||||
import CryptoTokenKit
|
||||
|
||||
#if !os(macOS)
|
||||
import CoreNFC
|
||||
import CryptoKit
|
||||
|
||||
@available(iOS 15, *)
|
||||
private enum PACEHandlerError {
|
||||
case DHKeyAgreementError(String)
|
||||
case ECDHKeyAgreementError(String)
|
||||
|
||||
var value: String {
|
||||
switch self {
|
||||
case .DHKeyAgreementError(let errMsg): return errMsg
|
||||
case .ECDHKeyAgreementError(let errMsg): return errMsg
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15, *)
|
||||
extension PACEHandlerError: LocalizedError {
|
||||
public var errorDescription: String? {
|
||||
return NSLocalizedString(value, comment: "PACEHandlerError")
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15, *)
|
||||
public class PACEHandler {
|
||||
|
||||
|
||||
private static let MRZ_PACE_KEY_REFERENCE : UInt8 = 0x01
|
||||
private static let CAN_PACE_KEY_REFERENCE : UInt8 = 0x02 // Not currently supported
|
||||
private static let PIN_PACE_KEY_REFERENCE : UInt8 = 0x03 // Not currently supported
|
||||
private static let CUK_PACE_KEY_REFERENCE : UInt8 = 0x04 // Not currently supported
|
||||
|
||||
var tagReader : TagReader
|
||||
var paceInfo : PACEInfo
|
||||
|
||||
var isPACESupported : Bool = false
|
||||
var paceError : String = ""
|
||||
|
||||
// Params used
|
||||
private var paceKey : [UInt8] = []
|
||||
private var paceKeyType : UInt8 = 0
|
||||
private var paceOID : String = ""
|
||||
private var parameterSpec : Int32 = -1
|
||||
private var mappingType : PACEMappingType!
|
||||
private var agreementAlg : String = ""
|
||||
private var cipherAlg : String = ""
|
||||
private var digestAlg : String = ""
|
||||
private var keyLength : Int = -1
|
||||
|
||||
public init(cardAccess : CardAccess, tagReader: TagReader) throws {
|
||||
self.tagReader = tagReader
|
||||
|
||||
guard let pi = cardAccess.paceInfo else {
|
||||
throw NFCPassportReaderError.NotYetSupported( "PACE not supported" )
|
||||
}
|
||||
|
||||
self.paceInfo = pi
|
||||
isPACESupported = true
|
||||
}
|
||||
|
||||
public func doPACE( mrzKey : String ) async throws {
|
||||
guard isPACESupported else {
|
||||
throw NFCPassportReaderError.NotYetSupported( "PACE not supported" )
|
||||
}
|
||||
|
||||
Log.info( "Performing PACE with \(paceInfo.getProtocolOIDString())" )
|
||||
|
||||
paceOID = paceInfo.getObjectIdentifier()
|
||||
parameterSpec = try paceInfo.getParameterSpec()
|
||||
|
||||
mappingType = try paceInfo.getMappingType() // Either GM, CAM, or IM.
|
||||
agreementAlg = try paceInfo.getKeyAgreementAlgorithm() // Either DH or ECDH.
|
||||
cipherAlg = try paceInfo.getCipherAlgorithm() // Either DESede or AES.
|
||||
digestAlg = try paceInfo.getDigestAlgorithm() // Either SHA-1 or SHA-256.
|
||||
keyLength = try paceInfo.getKeyLength() // Get key length the enc cipher. Either 128, 192, or 256.
|
||||
|
||||
paceKeyType = PACEHandler.MRZ_PACE_KEY_REFERENCE
|
||||
paceKey = try createPaceKey( from: mrzKey )
|
||||
|
||||
// Temporary logging
|
||||
Log.verbose("doPace - inpit parameters" )
|
||||
Log.verbose("paceOID - \(paceOID)" )
|
||||
Log.verbose("parameterSpec - \(parameterSpec)" )
|
||||
Log.verbose("mappingType - \(mappingType!)" )
|
||||
Log.verbose("agreementAlg - \(agreementAlg)" )
|
||||
Log.verbose("cipherAlg - \(cipherAlg)" )
|
||||
Log.verbose("digestAlg - \(digestAlg)" )
|
||||
Log.verbose("keyLength - \(keyLength)" )
|
||||
Log.verbose("keyLength - \(mrzKey)" )
|
||||
Log.verbose("paceKey - \(binToHexRep(paceKey, asArray:true))" )
|
||||
|
||||
// First start the initial auth call
|
||||
_ = try await tagReader.sendMSESetATMutualAuth(oid: paceOID, keyType: paceKeyType)
|
||||
|
||||
let decryptedNonce = try await self.doStep1()
|
||||
let ephemeralParams = try await self.doStep2(passportNonce: decryptedNonce)
|
||||
let (ephemeralKeyPair, passportPublicKey) = try await self.doStep3KeyExchange(ephemeralParams: ephemeralParams)
|
||||
let (encKey, macKey) = try await self.doStep4KeyAgreement( pcdKeyPair: ephemeralKeyPair, passportPublicKey: passportPublicKey)
|
||||
try self.paceCompleted( ksEnc: encKey, ksMac: macKey )
|
||||
Log.debug("PACE SUCCESSFUL" )
|
||||
}
|
||||
|
||||
/// Handles an error during the PACE process
|
||||
/// Logs and stoes the error and returns false to the caller
|
||||
/// - Parameters:
|
||||
/// - stage: Where in the PACE process the error occurred
|
||||
/// - error: The error message
|
||||
func handleError( _ stage: String, _ error: String, needToTerminateGA: Bool = false ) {
|
||||
Log.error( "PACEHandler: \(stage) - \(error)" )
|
||||
Log.error( " OpenSSLError: \(OpenSSLUtils.getOpenSSLError())" )
|
||||
self.paceError = "\(stage) - \(error)"
|
||||
//self.completedHandler?( false )
|
||||
|
||||
/*
|
||||
if needToTerminateGA {
|
||||
// This is to fix some passports that don't automatically terminate command chaining!
|
||||
// No idea if this is the correct way to do it but testing.....
|
||||
let terminateGA = wrapDO(b:0x83, arr:[0x00])
|
||||
tagReader.sendGeneralAuthenticate(data:terminateGA, isLast:true, completed: { [weak self] response, error in
|
||||
self?.completedHandler?( false )
|
||||
})
|
||||
} else {
|
||||
self.completedHandler?( false )
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/// Performs PACE Step 1- receives an encrypted nonce from the passport and decypts it with the PACE key - derived from MRZ, CAN (not yet supported)
|
||||
func doStep1() async throws -> [UInt8] {
|
||||
Log.debug("Doing PACE Step1...")
|
||||
let response = try await tagReader.sendGeneralAuthenticate(data: [], isLast: false)
|
||||
|
||||
let data = response.data
|
||||
let encryptedNonce = try unwrapDO(tag: 0x80, wrappedData: data)
|
||||
Log.verbose( "Encrypted nonce - \(binToHexRep(encryptedNonce, asArray:true))" )
|
||||
|
||||
let decryptedNonce: [UInt8]
|
||||
if self.cipherAlg == "DESede" {
|
||||
let iv = [UInt8](repeating:0, count: 8)
|
||||
decryptedNonce = tripleDESDecrypt(key: self.paceKey, message: encryptedNonce, iv: iv)
|
||||
} else if self.cipherAlg == "AES" {
|
||||
let iv = [UInt8](repeating:0, count: 16)
|
||||
decryptedNonce = AESDecrypt(key: self.paceKey, message: encryptedNonce, iv: iv)
|
||||
} else {
|
||||
throw NFCPassportReaderError.UnsupportedCipherAlgorithm
|
||||
}
|
||||
|
||||
Log.verbose( "Decrypted nonce - \(binToHexRep(decryptedNonce, asArray:true) )" )
|
||||
return decryptedNonce
|
||||
}
|
||||
|
||||
|
||||
/// Performs PACE Step 2 - computes ephemeral parameters by mapping the nonce received from the passport
|
||||
/// (and if IM used the nonce generated by us)
|
||||
///
|
||||
/// Using the supported
|
||||
/// - Parameters:
|
||||
/// - passportNonce: The decrypted nonce received from the passport
|
||||
func doStep2( passportNonce: [UInt8]) async throws -> OpaquePointer {
|
||||
Log.debug( "Doing PACE Step2...")
|
||||
switch(mappingType) {
|
||||
case .CAM, .GM:
|
||||
Log.debug( " Using General Mapping (GM)...")
|
||||
return try await doPACEStep2GM(passportNonce: passportNonce)
|
||||
case .IM:
|
||||
Log.debug( " Using Integrated Mapping (IM)...")
|
||||
return try await doPACEStep2IM(passportNonce: passportNonce)
|
||||
default:
|
||||
throw NFCPassportReaderError.PACEError( "Step2GM", "Unsupported Mapping Type" )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Performs PACEStep 2 using Generic Mapping
|
||||
///
|
||||
/// Using the supported
|
||||
/// - Parameters:
|
||||
/// - passportNonce: The decrypted nonce received from the passport
|
||||
func doPACEStep2GM(passportNonce : [UInt8]) async throws -> OpaquePointer {
|
||||
|
||||
let mappingKey : OpaquePointer
|
||||
mappingKey = try self.paceInfo.createMappingKey( )
|
||||
|
||||
guard let pcdMappingEncodedPublicKey = OpenSSLUtils.getPublicKeyData(from: mappingKey) else {
|
||||
throw NFCPassportReaderError.PACEError( "Step2GM", "Unable to get public key from mapping key")
|
||||
}
|
||||
Log.verbose( "public mapping key - \(binToHexRep(pcdMappingEncodedPublicKey, asArray:true))")
|
||||
|
||||
Log.debug( "Sending public mapping key to passport..")
|
||||
let step2Data = wrapDO(b:0x81, arr:pcdMappingEncodedPublicKey)
|
||||
let response = try await tagReader.sendGeneralAuthenticate(data:step2Data, isLast:false)
|
||||
|
||||
let piccMappingEncodedPublicKey = try unwrapDO(tag: 0x82, wrappedData: response.data)
|
||||
|
||||
Log.debug( "Received passports public mapping key")
|
||||
Log.verbose( " public mapping key - \(binToHexRep(piccMappingEncodedPublicKey, asArray: true))")
|
||||
|
||||
// Do mapping agreement
|
||||
|
||||
// First, Convert nonce to BIGNUM
|
||||
guard let bn_nonce = BN_bin2bn(passportNonce, Int32(passportNonce.count), nil) else {
|
||||
throw NFCPassportReaderError.PACEError( "Step2GM", "Unable to convert picc nonce to bignum" )
|
||||
}
|
||||
defer { BN_free(bn_nonce) }
|
||||
|
||||
// ephmeralParams are free'd in stage 3
|
||||
let ephemeralParams : OpaquePointer
|
||||
if self.agreementAlg == "DH" {
|
||||
Log.debug( "Doing DH Mapping agreement")
|
||||
ephemeralParams = try self.doDHMappingAgreement(mappingKey: mappingKey, passportPublicKeyData: piccMappingEncodedPublicKey, nonce: bn_nonce )
|
||||
} else if self.agreementAlg == "ECDH" {
|
||||
Log.debug( "Doing ECDH Mapping agreement")
|
||||
ephemeralParams = try self.doECDHMappingAgreement(mappingKey: mappingKey, passportPublicKeyData: piccMappingEncodedPublicKey, nonce: bn_nonce )
|
||||
} else {
|
||||
throw NFCPassportReaderError.PACEError( "Step2GM", "Unsupported agreement algorithm" )
|
||||
}
|
||||
|
||||
// Need to free the mapping key we created now
|
||||
EVP_PKEY_free(mappingKey)
|
||||
return ephemeralParams
|
||||
}
|
||||
|
||||
func doPACEStep2IM( passportNonce: [UInt8] ) async throws -> OpaquePointer {
|
||||
// Not implemented yet
|
||||
throw NFCPassportReaderError.PACEError( "Step2IM", "IM not yet implemented" )
|
||||
}
|
||||
|
||||
/// Generates an ephemeral public/private key pair based on mapping parameters from step 2, and then sends
|
||||
/// the public key to the passport and receives its ephmeral public key in exchange
|
||||
/// - Parameters:
|
||||
/// - ephemeralParams: The ehpemeral mapping keys generated by step2
|
||||
/// - Returns:
|
||||
/// - Tuple of Generated Ephemeral KeyPair and the Passport's public key
|
||||
func doStep3KeyExchange(ephemeralParams: OpaquePointer) async throws -> (OpaquePointer, OpaquePointer) {
|
||||
Log.debug( "Doing PACE Step3 - Key Exchange")
|
||||
|
||||
// Generate ephemeral keypair from ephemeralParams
|
||||
var ephKeyPair : OpaquePointer? = nil
|
||||
let pctx = EVP_PKEY_CTX_new(ephemeralParams, nil)
|
||||
EVP_PKEY_keygen_init(pctx)
|
||||
EVP_PKEY_keygen(pctx, &ephKeyPair)
|
||||
EVP_PKEY_CTX_free(pctx)
|
||||
|
||||
guard let ephemeralKeyPair = ephKeyPair else {
|
||||
throw NFCPassportReaderError.PACEError( "Step3 KeyEx", "Unable to get create ephermeral key pair" )
|
||||
}
|
||||
|
||||
Log.debug( "Generated Ephemeral key pair")
|
||||
|
||||
// We've finished with the ephemeralParams now - we can now free it
|
||||
EVP_PKEY_free( ephemeralParams )
|
||||
|
||||
guard let publicKey = OpenSSLUtils.getPublicKeyData( from: ephemeralKeyPair ) else {
|
||||
throw NFCPassportReaderError.PACEError( "Step3 KeyEx", "Unable to get public key from ephermeral key pair" )
|
||||
}
|
||||
Log.verbose( "Ephemeral public key - \(binToHexRep(publicKey, asArray: true))")
|
||||
|
||||
// exchange public keys
|
||||
Log.debug( "Sending ephemeral public key to passport")
|
||||
let step3Data = wrapDO(b:0x83, arr:publicKey)
|
||||
let response = try await tagReader.sendGeneralAuthenticate(data:step3Data, isLast:false)
|
||||
let passportEncodedPublicKey = try? unwrapDO(tag: 0x84, wrappedData: response.data)
|
||||
guard let passportPublicKey = OpenSSLUtils.decodePublicKeyFromBytes(pubKeyData: passportEncodedPublicKey!, params: ephemeralKeyPair) else {
|
||||
throw NFCPassportReaderError.PACEError( "Step3 KeyEx", "Unable to decode passports ephemeral key" )
|
||||
}
|
||||
|
||||
Log.verbose( "Received passports ephemeral public key - \(binToHexRep(passportEncodedPublicKey!, asArray: true))" )
|
||||
return (ephemeralKeyPair, passportPublicKey)
|
||||
}
|
||||
|
||||
/// This performs PACE Step 4 - Key Agreement.
|
||||
/// Here the shared secret is computed from our ephemeral private key and the passports ephemeral public key
|
||||
/// The new secure messaging (ksEnc and ksMac) keys are computed from the shared secret
|
||||
/// An authentication token is generated from the passports public key and the computed ksMac key
|
||||
/// Then, the authetication token is send to the passport, it returns its own computed authentication token
|
||||
/// We then compute an expected authentication token from the ksMac key and our ephemeral public key
|
||||
/// Finally we compare the recieved auth token to the expected token and if they are the same then PACE has succeeded!
|
||||
/// - Parameters:
|
||||
/// - pcdKeyPair: our ephemeral key pair
|
||||
/// - passportPublicKey: passports ephemeral public key
|
||||
/// - Returns:
|
||||
/// - Tuple of KSEnc KSMac
|
||||
func doStep4KeyAgreement( pcdKeyPair: OpaquePointer, passportPublicKey: OpaquePointer) async throws -> ([UInt8], [UInt8]) {
|
||||
Log.debug( "Doing PACE Step4 Key Agreement...")
|
||||
|
||||
Log.debug( "Computing shared secret...")
|
||||
let sharedSecret = OpenSSLUtils.computeSharedSecret(privateKeyPair: pcdKeyPair, publicKey: passportPublicKey)
|
||||
Log.verbose( "Shared secret - \(binToHexRep(sharedSecret, asArray:true))")
|
||||
|
||||
Log.debug( "Deriving ksEnc and ksMac keys from shared secret")
|
||||
let gen = SecureMessagingSessionKeyGenerator()
|
||||
let encKey = try! gen.deriveKey(keySeed: sharedSecret, cipherAlgName: cipherAlg, keyLength: keyLength, mode: .ENC_MODE)
|
||||
let macKey = try! gen.deriveKey(keySeed: sharedSecret, cipherAlgName: cipherAlg, keyLength: keyLength, mode: .MAC_MODE)
|
||||
Log.verbose( "encKey - \(binToHexRep(encKey, asArray:true))")
|
||||
Log.verbose( "macKey - \(binToHexRep(macKey, asArray:true))")
|
||||
|
||||
// Step 4 - generate authentication token
|
||||
Log.debug( "Generating authentication token")
|
||||
guard let pcdAuthToken = try? generateAuthenticationToken( publicKey: passportPublicKey, macKey: macKey) else {
|
||||
throw NFCPassportReaderError.PACEError( "Step3 KeyAgreement", "Unable to generate authentication token using passports public key" )
|
||||
}
|
||||
Log.verbose( "authentication token - \(pcdAuthToken)")
|
||||
|
||||
Log.debug( "Sending auth token to passport")
|
||||
let step4Data = wrapDO(b:0x85, arr:pcdAuthToken)
|
||||
let response = try await tagReader.sendGeneralAuthenticate(data:step4Data, isLast:true)
|
||||
|
||||
let tvlResp = TKBERTLVRecord.sequenceOfRecords(from: Data(response.data))!
|
||||
if tvlResp[0].tag != 0x86 {
|
||||
Log.warning("Was expecting tag 0x86, found: \(binToHex(UInt8(tvlResp[0].tag)))")
|
||||
}
|
||||
// Calculate expected authentication token
|
||||
let expectedPICCToken = try self.generateAuthenticationToken( publicKey: pcdKeyPair, macKey: macKey)
|
||||
|
||||
Log.verbose( "Expecting authentication token from passport - \(expectedPICCToken)")
|
||||
|
||||
let piccToken = [UInt8](tvlResp[0].value)
|
||||
Log.verbose( "Received authentication token from passport - \(piccToken)")
|
||||
|
||||
guard piccToken == expectedPICCToken else {
|
||||
Log.error( "Error PICC Token mismatch!\npicToken - \(piccToken)\nexpectedPICCToken - \(expectedPICCToken)" )
|
||||
throw NFCPassportReaderError.PACEError( "Step3 KeyAgreement", "Error PICC Token mismatch!\npicToken - \(piccToken)\nexpectedPICCToken - \(expectedPICCToken)" )
|
||||
}
|
||||
|
||||
Log.debug( "Auth token from passport matches expected token!" )
|
||||
|
||||
// This will be added for CAM when supported
|
||||
// var encryptedChipAuthenticationData : [UInt8]? = nil
|
||||
// if (sself.mappingType == PACEMappingType.CAM) {
|
||||
// if tvlResp[1].tag != 0x8A {
|
||||
// Log.warning("CAM: Was expecting tag 0x86, found: \(binToHex(UInt8(tvlResp[1].tag)))")
|
||||
// }
|
||||
// encryptedChipAuthenticationData = [UInt8](tvlResp[1].value)
|
||||
// }
|
||||
|
||||
// We're done!
|
||||
return (encKey, macKey)
|
||||
}
|
||||
|
||||
/// Called once PACE has completed with the newly generated ksEnc and ksMac keys for restarting secure messaging
|
||||
/// - Parameters:
|
||||
/// - ksEnc: the computed encryption key derived from the key agreement
|
||||
/// - ksMac: the computed mac key derived from the key agreement
|
||||
func paceCompleted( ksEnc: [UInt8], ksMac: [UInt8] ) throws {
|
||||
// Restart secure messaging
|
||||
let ssc = withUnsafeBytes(of: 0.bigEndian, Array.init)
|
||||
if (cipherAlg.hasPrefix("DESede")) {
|
||||
Log.info( "Restarting secure messaging using DESede encryption")
|
||||
let sm = SecureMessaging(encryptionAlgorithm: .DES, ksenc: ksEnc, ksmac: ksMac, ssc: ssc)
|
||||
tagReader.secureMessaging = sm
|
||||
} else if (cipherAlg.hasPrefix("AES")) {
|
||||
Log.info( "Restarting secure messaging using AES encryption")
|
||||
let sm = SecureMessaging(encryptionAlgorithm: .AES, ksenc: ksEnc, ksmac: ksMac, ssc: ssc)
|
||||
tagReader.secureMessaging = sm
|
||||
} else {
|
||||
throw NFCPassportReaderError.PACEError( "PACECompleted", "Not restarting secure messaging as unsupported cipher algorithm requested - \(cipherAlg)" )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK - PACEHandler Utility functions
|
||||
@available(iOS 15, *)
|
||||
extension PACEHandler {
|
||||
|
||||
/// Does the DH key Mapping agreement
|
||||
/// - Parameter mappingKey - Pointer to an EVP_PKEY structure containing the mapping key
|
||||
/// - Parameter passportPublicKeyData - byte array containing the publick key read from the passport
|
||||
/// - Parameter nonce - Pointer to an BIGNUM structure containing the unencrypted nonce
|
||||
/// - Returns the EVP_PKEY containing the mapped ephemeral parameters
|
||||
func doDHMappingAgreement( mappingKey : OpaquePointer, passportPublicKeyData: [UInt8], nonce: OpaquePointer ) throws -> OpaquePointer {
|
||||
guard let dh_mapping_key = EVP_PKEY_get1_DH(mappingKey) else {
|
||||
// Error
|
||||
throw PACEHandlerError.DHKeyAgreementError( "Unable to get DH mapping key" )
|
||||
}
|
||||
|
||||
// Compute the shared secret using the mapping key and the passports public mapping key
|
||||
let bn = BN_bin2bn(passportPublicKeyData, Int32(passportPublicKeyData.count), nil)
|
||||
defer { BN_free( bn ) }
|
||||
|
||||
var secret = [UInt8](repeating: 0, count: Int(DH_size(dh_mapping_key)))
|
||||
DH_compute_key( &secret, bn, dh_mapping_key)
|
||||
|
||||
// Convert the secret to a bignum
|
||||
let bn_h = BN_bin2bn(secret, Int32(secret.count), nil)
|
||||
defer { BN_clear_free(bn_h) }
|
||||
|
||||
// Initialize ephemeral parameters with parameters from the mapping key
|
||||
guard let ephemeral_key = DHparams_dup(dh_mapping_key) else {
|
||||
// Error
|
||||
throw PACEHandlerError.DHKeyAgreementError("Unable to get initialise ephemeral parameters from DH mapping key")
|
||||
}
|
||||
defer{ DH_free(ephemeral_key) }
|
||||
|
||||
var p : OpaquePointer? = nil
|
||||
var q : OpaquePointer? = nil
|
||||
var g : OpaquePointer? = nil
|
||||
DH_get0_pqg(dh_mapping_key, &p, &q, &g)
|
||||
|
||||
// map to new generator
|
||||
guard let bn_g = BN_new() else {
|
||||
throw PACEHandlerError.DHKeyAgreementError( "Unable to create bn_g" )
|
||||
}
|
||||
defer{ BN_free(bn_g) }
|
||||
guard let new_g = BN_new() else {
|
||||
throw PACEHandlerError.DHKeyAgreementError( "Unable to create new_g" )
|
||||
}
|
||||
defer{ BN_free(new_g) }
|
||||
|
||||
// bn_g = g^nonce mod p
|
||||
// ephemeral_key->g = bn_g mod p * h => (g^nonce mod p) * h mod p
|
||||
let bn_ctx = BN_CTX_new()
|
||||
guard BN_mod_exp(bn_g, g, nonce, p, bn_ctx) == 1,
|
||||
BN_mod_mul(new_g, bn_g, bn_h, p, bn_ctx) == 1 else {
|
||||
// Error
|
||||
throw PACEHandlerError.DHKeyAgreementError( "Failed to generate new parameters" )
|
||||
}
|
||||
|
||||
guard DH_set0_pqg(ephemeral_key, BN_dup(p), BN_dup(q), BN_dup(new_g)) == 1 else {
|
||||
// Error
|
||||
throw PACEHandlerError.DHKeyAgreementError( "Unable to set DH pqg paramerters" )
|
||||
}
|
||||
|
||||
// Set the ephemeral params
|
||||
guard let ephemeralParams = EVP_PKEY_new() else {
|
||||
throw PACEHandlerError.ECDHKeyAgreementError( "Unable to create ephemeral params" )
|
||||
}
|
||||
|
||||
guard EVP_PKEY_set1_DH(ephemeralParams, ephemeral_key) == 1 else {
|
||||
// Error
|
||||
EVP_PKEY_free( ephemeralParams )
|
||||
throw PACEHandlerError.DHKeyAgreementError( "Unable to set ephemeral parameters" )
|
||||
}
|
||||
return ephemeralParams
|
||||
}
|
||||
|
||||
/// Does the ECDH key Mapping agreement
|
||||
/// - Parameter mappingKey - Pointer to an EVP_PKEY structure containing the mapping key
|
||||
/// - Parameter passportPublicKeyData - byte array containing the publick key read from the passport
|
||||
/// - Parameter nonce - Pointer to an BIGNUM structure containing the unencrypted nonce
|
||||
/// - Returns the EVP_PKEY containing the mapped ephemeral parameters
|
||||
func doECDHMappingAgreement( mappingKey : OpaquePointer, passportPublicKeyData: [UInt8], nonce: OpaquePointer ) throws -> OpaquePointer {
|
||||
|
||||
let ec_mapping_key = EVP_PKEY_get1_EC_KEY(mappingKey)
|
||||
|
||||
guard let group = EC_GROUP_dup(EC_KEY_get0_group(ec_mapping_key)) else {
|
||||
// Error
|
||||
throw PACEHandlerError.ECDHKeyAgreementError( "Unable to get EC group" )
|
||||
}
|
||||
defer { EC_GROUP_free(group) }
|
||||
|
||||
guard let order = BN_new() else {
|
||||
// Error
|
||||
throw PACEHandlerError.ECDHKeyAgreementError( "Unable to create order bignum" )
|
||||
}
|
||||
defer { BN_free( order ) }
|
||||
|
||||
guard let cofactor = BN_new() else {
|
||||
// error
|
||||
throw PACEHandlerError.ECDHKeyAgreementError( "Unable to create cofactor bignum" )
|
||||
}
|
||||
defer { BN_free( cofactor ) }
|
||||
|
||||
guard EC_GROUP_get_order(group, order, nil) == 1 ||
|
||||
EC_GROUP_get_cofactor(group, cofactor, nil) == 1 else {
|
||||
// Handle error
|
||||
throw PACEHandlerError.ECDHKeyAgreementError( "Unable to get order or cofactor from group" )
|
||||
}
|
||||
|
||||
// Create the shared secret in the form of a ECPoint
|
||||
|
||||
// Ideally I'd use OpenSSLUtls.computeSharedSecret for this but for reasons as yet unknown, it only returns the first 32 bytes
|
||||
// NOT the full 64 bytes (would then convert to 65 with e header of 4 for uncompressed)
|
||||
guard let sharedSecretMappingPoint = self.computeECDHMappingKeyPoint(privateKey: mappingKey, inputKey: passportPublicKeyData) else {
|
||||
// Error
|
||||
throw PACEHandlerError.ECDHKeyAgreementError( "Failed to compute new shared secret mapping point from mapping key and passport public mapping key" )
|
||||
}
|
||||
defer { EC_POINT_free( sharedSecretMappingPoint ) }
|
||||
|
||||
// Map the nonce using Generic mapping to get the new parameters (inc a new generator)
|
||||
guard let newGenerater = EC_POINT_new(group) else {
|
||||
throw PACEHandlerError.ECDHKeyAgreementError( "Unable to create new mapping generator point" )
|
||||
}
|
||||
defer{ EC_POINT_free(newGenerater) }
|
||||
|
||||
// g = (generator * nonce) + (sharedSecretMappingPoint * 1)
|
||||
guard EC_POINT_mul(group, newGenerater, nonce, sharedSecretMappingPoint, BN_value_one(), nil) == 1 else {
|
||||
throw PACEHandlerError.ECDHKeyAgreementError( "Failed to map nonce to get new generator params" )
|
||||
}
|
||||
|
||||
// Initialize ephemeral parameters with parameters from the mapping key
|
||||
guard let ephemeralParams = EVP_PKEY_new() else {
|
||||
throw PACEHandlerError.ECDHKeyAgreementError( "Unable to create ephemeral params" )
|
||||
}
|
||||
|
||||
let ephemeral_key = EC_KEY_dup(ec_mapping_key)
|
||||
defer{ EC_KEY_free(ephemeral_key) }
|
||||
|
||||
// configure the new EC_KEY
|
||||
guard EVP_PKEY_set1_EC_KEY(ephemeralParams, ephemeral_key) == 1,
|
||||
EC_GROUP_set_generator(group, newGenerater, order, cofactor) == 1,
|
||||
EC_GROUP_check(group, nil) == 1,
|
||||
EC_KEY_set_group(ephemeral_key, group) == 1 else {
|
||||
// Error
|
||||
|
||||
EVP_PKEY_free( ephemeralParams )
|
||||
throw PACEHandlerError.ECDHKeyAgreementError( "Unable to configure new ephemeral params" )
|
||||
}
|
||||
return ephemeralParams
|
||||
}
|
||||
|
||||
/// Generate Authentication token from a publicKey and and a mac key
|
||||
/// - Parameters:
|
||||
/// - publicKey: An EVP_PKEY structure containing a public key data which will be used to generate the auth code
|
||||
/// - macKey: The mac key derived from the key agreement
|
||||
/// - Throws: An error if we are unable to encode the public key data
|
||||
/// - Returns: The authentication token (8 bytes)
|
||||
func generateAuthenticationToken( publicKey: OpaquePointer, macKey: [UInt8] ) throws -> [UInt8] {
|
||||
var encodedPublicKeyData = try encodePublicKey(oid:self.paceOID, key:publicKey)
|
||||
|
||||
if cipherAlg == "DESede" {
|
||||
// If DESede (3DES), we need to pad the data
|
||||
encodedPublicKeyData = pad(encodedPublicKeyData, blockSize: 8)
|
||||
}
|
||||
|
||||
Log.verbose( "Generating Authentication Token" )
|
||||
Log.verbose( "EncodedPubKey = \(binToHexRep(encodedPublicKeyData, asArray: true))" )
|
||||
Log.verbose( "macKey = \(binToHexRep(macKey, asArray: true))" )
|
||||
|
||||
let maccedPublicKeyDataObject = mac(algoName: cipherAlg == "DESede" ? .DES : .AES, key: macKey, msg: encodedPublicKeyData)
|
||||
|
||||
// Take 8 bytes for auth token
|
||||
let authToken = [UInt8](maccedPublicKeyDataObject[0..<8])
|
||||
Log.verbose( "Generated authToken = \(binToHexRep(authToken, asArray: true))" )
|
||||
return authToken
|
||||
}
|
||||
|
||||
/// Encodes a PublicKey as an TLV strucuture based on TR-SAC 1.01 4.5.1 and 4.5.2
|
||||
/// - Parameters:
|
||||
/// - oid: The object identifier specifying the key type
|
||||
/// - key: The ECP_PKEY public key to encode
|
||||
/// - Throws: Error if unable to encode
|
||||
/// - Returns: the encoded public key in tlv format
|
||||
func encodePublicKey( oid : String, key : OpaquePointer ) throws -> [UInt8] {
|
||||
let encodedOid = oidToBytes(oid:oid, replaceTag: false)
|
||||
guard let pubKeyData = OpenSSLUtils.getPublicKeyData(from: key) else {
|
||||
Log.error( "PACEHandler: encodePublicKey() - Unable to get public key data" )
|
||||
throw NFCPassportReaderError.InvalidDataPassed("Unable to get public key data")
|
||||
}
|
||||
|
||||
let keyType = EVP_PKEY_base_id( key )
|
||||
let tag : TKTLVTag
|
||||
if keyType == EVP_PKEY_DH || keyType == EVP_PKEY_DHX {
|
||||
tag = 0x84
|
||||
} else {
|
||||
tag = 0x86
|
||||
}
|
||||
|
||||
guard let encOid = TKBERTLVRecord(from: Data(encodedOid)) else {
|
||||
throw NFCPassportReaderError.InvalidASN1Value
|
||||
}
|
||||
let encPub = TKBERTLVRecord(tag:tag, value: Data(pubKeyData))
|
||||
let record = TKBERTLVRecord(tag: 0x7F49, records:[encOid, encPub])
|
||||
let data = record.data
|
||||
|
||||
return [UInt8](data)
|
||||
}
|
||||
|
||||
/// Computes a key seed based on an MRZ key
|
||||
/// - Parameter the mrz key
|
||||
/// - Returns a encoded key based on the mrz key that can be used for PACE
|
||||
func createPaceKey( from mrzKey: String ) throws -> [UInt8] {
|
||||
let buf: [UInt8] = Array(mrzKey.utf8)
|
||||
let hash = calcSHA1Hash(buf)
|
||||
|
||||
let smskg = SecureMessagingSessionKeyGenerator()
|
||||
let key = try smskg.deriveKey(keySeed: hash, cipherAlgName: cipherAlg, keyLength: keyLength, nonce: nil, mode: .PACE_MODE, paceKeyReference: paceKeyType)
|
||||
return key
|
||||
}
|
||||
|
||||
/// Performs the ECDH PACE GM key agreement protocol by multiplying a private key with a public key
|
||||
/// - Parameters:
|
||||
/// - key: an EVP_PKEY structure containng a ECDH private key
|
||||
/// - inputKey: a public key
|
||||
/// - Returns: a new EC_POINT
|
||||
func computeECDHMappingKeyPoint( privateKey : OpaquePointer, inputKey : [UInt8] ) -> OpaquePointer? {
|
||||
|
||||
let ecdh = EVP_PKEY_get1_EC_KEY(privateKey)
|
||||
defer { EC_KEY_free(ecdh) }
|
||||
|
||||
let privateECKey = EC_KEY_get0_private_key(ecdh) // BIGNUM
|
||||
|
||||
// decode public key
|
||||
guard let group = EC_KEY_get0_group(ecdh) else{ return nil }
|
||||
guard let ecp = EC_POINT_new(group) else { return nil }
|
||||
defer { EC_POINT_free(ecp) }
|
||||
guard EC_POINT_oct2point(group, ecp, inputKey, inputKey.count,nil) != 0 else { return nil }
|
||||
|
||||
// create our output point
|
||||
let output = EC_POINT_new(group)
|
||||
|
||||
// Multiply our private key with the passports public key to get a new point
|
||||
EC_POINT_mul(group, output, nil, ecp, privateECKey, nil)
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
405
app/ios/NFCPassportReader/PassportReader.swift
Normal file
405
app/ios/NFCPassportReader/PassportReader.swift
Normal file
@@ -0,0 +1,405 @@
|
||||
import Foundation
|
||||
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
import CoreNFC
|
||||
|
||||
@available(iOS 15, *)
|
||||
public class PassportReader : NSObject {
|
||||
private typealias NFCCheckedContinuation = CheckedContinuation<NFCPassportModel, Error>
|
||||
private var nfcContinuation: NFCCheckedContinuation?
|
||||
|
||||
private var passport : NFCPassportModel = NFCPassportModel()
|
||||
|
||||
private var readerSession: NFCTagReaderSession?
|
||||
private var currentlyReadingDataGroup : DataGroupId?
|
||||
|
||||
private var dataGroupsToRead : [DataGroupId] = []
|
||||
private var readAllDatagroups = false
|
||||
private var skipSecureElements = true
|
||||
private var skipCA = false
|
||||
private var skipPACE = false
|
||||
|
||||
private var bacHandler : BACHandler?
|
||||
private var caHandler : ChipAuthenticationHandler?
|
||||
private var paceHandler : PACEHandler?
|
||||
private var mrzKey : String = ""
|
||||
private var dataAmountToReadOverride : Int? = nil
|
||||
|
||||
private var scanCompletedHandler: ((NFCPassportModel?, NFCPassportReaderError?)->())!
|
||||
private var nfcViewDisplayMessageHandler: ((NFCViewDisplayMessage) -> String?)?
|
||||
private var masterListURL : URL?
|
||||
private var shouldNotReportNextReaderSessionInvalidationErrorUserCanceled : Bool = false
|
||||
|
||||
// By default, Passive Authentication uses the new RFS5652 method to verify the SOD, but can be switched to use
|
||||
// the previous OpenSSL CMS verification if necessary
|
||||
public var passiveAuthenticationUsesOpenSSL : Bool = false
|
||||
|
||||
public init( logLevel: LogLevel = .info, masterListURL: URL? = nil ) {
|
||||
super.init()
|
||||
|
||||
Log.logLevel = logLevel
|
||||
self.masterListURL = masterListURL
|
||||
}
|
||||
|
||||
public func setMasterListURL( _ masterListURL : URL ) {
|
||||
self.masterListURL = masterListURL
|
||||
}
|
||||
|
||||
// This function allows you to override the amount of data the TagReader tries to read from the NFC
|
||||
// chip. NOTE - this really shouldn't be used for production but is useful for testing as different
|
||||
// passports support different data amounts.
|
||||
// It appears that the most reliable is 0xA0 (160 chars) but some will support arbitary reads (0xFF or 256)
|
||||
public func overrideNFCDataAmountToRead( amount: Int ) {
|
||||
dataAmountToReadOverride = amount
|
||||
}
|
||||
|
||||
public func readPassport( mrzKey : String, tags : [DataGroupId] = [], skipSecureElements : Bool = true, skipCA : Bool = false, skipPACE : Bool = false, customDisplayMessage : ((NFCViewDisplayMessage) -> String?)? = nil) async throws -> NFCPassportModel {
|
||||
|
||||
self.passport = NFCPassportModel()
|
||||
self.mrzKey = mrzKey
|
||||
self.skipCA = skipCA
|
||||
self.skipPACE = skipPACE
|
||||
|
||||
self.dataGroupsToRead.removeAll()
|
||||
self.dataGroupsToRead.append( contentsOf:tags)
|
||||
self.nfcViewDisplayMessageHandler = customDisplayMessage
|
||||
self.skipSecureElements = skipSecureElements
|
||||
self.currentlyReadingDataGroup = nil
|
||||
self.bacHandler = nil
|
||||
self.caHandler = nil
|
||||
self.paceHandler = nil
|
||||
|
||||
// If no tags specified, read all
|
||||
if self.dataGroupsToRead.count == 0 {
|
||||
// Start off with .COM, will always read (and .SOD but we'll add that after), and then add the others from the COM
|
||||
self.dataGroupsToRead.append(contentsOf:[.COM, .SOD] )
|
||||
self.readAllDatagroups = true
|
||||
} else {
|
||||
// We are reading specific datagroups
|
||||
self.readAllDatagroups = false
|
||||
}
|
||||
|
||||
guard NFCNDEFReaderSession.readingAvailable else {
|
||||
throw NFCPassportReaderError.NFCNotSupported
|
||||
}
|
||||
|
||||
if NFCTagReaderSession.readingAvailable {
|
||||
readerSession = NFCTagReaderSession(pollingOption: [.iso14443], delegate: self, queue: nil)
|
||||
|
||||
self.updateReaderSessionMessage( alertMessage: NFCViewDisplayMessage.requestPresentPassport )
|
||||
readerSession?.begin()
|
||||
}
|
||||
|
||||
return try await withCheckedThrowingContinuation({ (continuation: NFCCheckedContinuation) in
|
||||
self.nfcContinuation = continuation
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15, *)
|
||||
extension PassportReader : NFCTagReaderSessionDelegate {
|
||||
// MARK: - NFCTagReaderSessionDelegate
|
||||
public func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
|
||||
// If necessary, you may perform additional operations on session start.
|
||||
// At this point RF polling is enabled.
|
||||
Log.debug( "tagReaderSessionDidBecomeActive" )
|
||||
}
|
||||
|
||||
public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
|
||||
// If necessary, you may handle the error. Note session is no longer valid.
|
||||
// You must create a new session to restart RF polling.
|
||||
Log.debug( "tagReaderSession:didInvalidateWithError - \(error.localizedDescription)" )
|
||||
self.readerSession?.invalidate()
|
||||
self.readerSession = nil
|
||||
|
||||
if let readerError = error as? NFCReaderError, readerError.code == NFCReaderError.readerSessionInvalidationErrorUserCanceled
|
||||
&& self.shouldNotReportNextReaderSessionInvalidationErrorUserCanceled {
|
||||
|
||||
self.shouldNotReportNextReaderSessionInvalidationErrorUserCanceled = false
|
||||
} else {
|
||||
var userError = NFCPassportReaderError.UnexpectedError
|
||||
if let readerError = error as? NFCReaderError {
|
||||
Log.error( "tagReaderSession:didInvalidateWithError - Got NFCReaderError - \(readerError.localizedDescription)" )
|
||||
switch (readerError.code) {
|
||||
case NFCReaderError.readerSessionInvalidationErrorUserCanceled:
|
||||
Log.error( " - User cancelled session" )
|
||||
userError = NFCPassportReaderError.UserCanceled
|
||||
default:
|
||||
Log.error( " - some other error - \(readerError.localizedDescription)" )
|
||||
userError = NFCPassportReaderError.UnexpectedError
|
||||
}
|
||||
} else {
|
||||
Log.error( "tagReaderSession:didInvalidateWithError - Received error - \(error.localizedDescription)" )
|
||||
}
|
||||
nfcContinuation?.resume(throwing: userError)
|
||||
nfcContinuation = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
|
||||
Log.debug( "tagReaderSession:didDetect - \(tags[0])" )
|
||||
if tags.count > 1 {
|
||||
Log.debug( "tagReaderSession:more than 1 tag detected! - \(tags)" )
|
||||
|
||||
let errorMessage = NFCViewDisplayMessage.error(.MoreThanOneTagFound)
|
||||
self.invalidateSession(errorMessage: errorMessage, error: NFCPassportReaderError.MoreThanOneTagFound)
|
||||
return
|
||||
}
|
||||
|
||||
let tag = tags.first!
|
||||
var passportTag: NFCISO7816Tag
|
||||
switch tags.first! {
|
||||
case let .iso7816(tag):
|
||||
passportTag = tag
|
||||
default:
|
||||
Log.debug( "tagReaderSession:invalid tag detected!!!" )
|
||||
|
||||
let errorMessage = NFCViewDisplayMessage.error(NFCPassportReaderError.TagNotValid)
|
||||
self.invalidateSession(errorMessage:errorMessage, error: NFCPassportReaderError.TagNotValid)
|
||||
return
|
||||
}
|
||||
|
||||
Task { [passportTag] in
|
||||
do {
|
||||
try await session.connect(to: tag)
|
||||
|
||||
Log.debug( "tagReaderSession:connected to tag - starting authentication" )
|
||||
self.updateReaderSessionMessage( alertMessage: NFCViewDisplayMessage.authenticatingWithPassport(0) )
|
||||
|
||||
let tagReader = TagReader(tag:passportTag)
|
||||
|
||||
if let newAmount = self.dataAmountToReadOverride {
|
||||
tagReader.overrideDataAmountToRead(newAmount: newAmount)
|
||||
}
|
||||
|
||||
tagReader.progress = { [unowned self] (progress) in
|
||||
if let dgId = self.currentlyReadingDataGroup {
|
||||
self.updateReaderSessionMessage( alertMessage: NFCViewDisplayMessage.readingDataGroupProgress(dgId, progress) )
|
||||
} else {
|
||||
self.updateReaderSessionMessage( alertMessage: NFCViewDisplayMessage.authenticatingWithPassport(progress) )
|
||||
}
|
||||
}
|
||||
|
||||
let passportModel = try await self.startReading( tagReader : tagReader)
|
||||
nfcContinuation?.resume(returning: passportModel)
|
||||
nfcContinuation = nil
|
||||
|
||||
|
||||
} catch let error as NFCPassportReaderError {
|
||||
let errorMessage = NFCViewDisplayMessage.error(error)
|
||||
self.invalidateSession(errorMessage: errorMessage, error: error)
|
||||
} catch let error {
|
||||
|
||||
nfcContinuation?.resume(throwing: error)
|
||||
nfcContinuation = nil
|
||||
Log.debug( "tagReaderSession:failed to connect to tag - \(error.localizedDescription)" )
|
||||
let errorMessage = NFCViewDisplayMessage.error(NFCPassportReaderError.ConnectionError)
|
||||
self.invalidateSession(errorMessage: errorMessage, error: NFCPassportReaderError.ConnectionError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateReaderSessionMessage(alertMessage: NFCViewDisplayMessage ) {
|
||||
self.readerSession?.alertMessage = self.nfcViewDisplayMessageHandler?(alertMessage) ?? alertMessage.description
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15, *)
|
||||
extension PassportReader {
|
||||
|
||||
func startReading(tagReader : TagReader) async throws -> NFCPassportModel {
|
||||
|
||||
if !skipPACE {
|
||||
do {
|
||||
let data = try await tagReader.readCardAccess()
|
||||
Log.verbose( "Read CardAccess - data \(binToHexRep(data))" )
|
||||
let cardAccess = try CardAccess(data)
|
||||
passport.cardAccess = cardAccess
|
||||
|
||||
Log.info( "Starting Password Authenticated Connection Establishment (PACE)" )
|
||||
|
||||
let paceHandler = try PACEHandler( cardAccess: cardAccess, tagReader: tagReader )
|
||||
try await paceHandler.doPACE(mrzKey: mrzKey )
|
||||
passport.PACEStatus = .success
|
||||
Log.debug( "PACE Succeeded" )
|
||||
} catch {
|
||||
passport.PACEStatus = .failed
|
||||
Log.error( "PACE Failed - falling back to BAC" )
|
||||
}
|
||||
|
||||
_ = try await tagReader.selectPassportApplication()
|
||||
}
|
||||
|
||||
// If either PACE isn't supported, we failed whilst doing PACE or we didn't even attempt it, then fall back to BAC
|
||||
if passport.PACEStatus != .success {
|
||||
try await doBACAuthentication(tagReader : tagReader)
|
||||
}
|
||||
|
||||
// Now to read the datagroups
|
||||
try await readDataGroups(tagReader: tagReader)
|
||||
|
||||
self.updateReaderSessionMessage(alertMessage: NFCViewDisplayMessage.successfulRead)
|
||||
|
||||
try await doActiveAuthenticationIfNeccessary(tagReader : tagReader)
|
||||
self.shouldNotReportNextReaderSessionInvalidationErrorUserCanceled = true
|
||||
self.readerSession?.invalidate()
|
||||
|
||||
// If we have a masterlist url set then use that and verify the passport now
|
||||
self.passport.verifyPassport(masterListURL: self.masterListURL, useCMSVerification: self.passiveAuthenticationUsesOpenSSL)
|
||||
|
||||
return self.passport
|
||||
}
|
||||
|
||||
|
||||
func doActiveAuthenticationIfNeccessary( tagReader : TagReader) async throws {
|
||||
guard self.passport.activeAuthenticationSupported else {
|
||||
return
|
||||
}
|
||||
|
||||
Log.info( "Performing Active Authentication" )
|
||||
|
||||
let challenge = generateRandomUInt8Array(8)
|
||||
Log.verbose( "Generated Active Authentication challange - \(binToHexRep(challenge))")
|
||||
let response = try await tagReader.doInternalAuthentication(challenge: challenge)
|
||||
self.passport.verifyActiveAuthentication( challenge:challenge, signature:response.data )
|
||||
}
|
||||
|
||||
|
||||
func doBACAuthentication(tagReader : TagReader) async throws {
|
||||
self.currentlyReadingDataGroup = nil
|
||||
|
||||
Log.info( "Starting Basic Access Control (BAC)" )
|
||||
|
||||
self.passport.BACStatus = .failed
|
||||
|
||||
self.bacHandler = BACHandler( tagReader: tagReader )
|
||||
try await bacHandler?.performBACAndGetSessionKeys( mrzKey: mrzKey )
|
||||
Log.info( "Basic Access Control (BAC) - SUCCESS!" )
|
||||
|
||||
self.passport.BACStatus = .success
|
||||
}
|
||||
|
||||
func readDataGroups( tagReader: TagReader ) async throws {
|
||||
|
||||
// Read COM
|
||||
var DGsToRead = [DataGroupId]()
|
||||
|
||||
self.updateReaderSessionMessage( alertMessage: NFCViewDisplayMessage.readingDataGroupProgress(.COM, 0) )
|
||||
if let com = try await readDataGroup(tagReader:tagReader, dgId:.COM) as? COM {
|
||||
self.passport.addDataGroup( .COM, dataGroup:com )
|
||||
|
||||
// SOD and COM shouldn't be present in the DG list but just in case (worst case here we read the sod twice)
|
||||
DGsToRead = [.SOD] + com.dataGroupsPresent.map { DataGroupId.getIDFromName(name:$0) }
|
||||
DGsToRead.removeAll { $0 == .COM }
|
||||
}
|
||||
|
||||
if DGsToRead.contains( .DG14 ) {
|
||||
DGsToRead.removeAll { $0 == .DG14 }
|
||||
|
||||
if !skipCA {
|
||||
// Do Chip Authentication
|
||||
if let dg14 = try await readDataGroup(tagReader:tagReader, dgId:.DG14) as? DataGroup14 {
|
||||
self.passport.addDataGroup( .DG14, dataGroup:dg14 )
|
||||
let caHandler = ChipAuthenticationHandler(dg14: dg14, tagReader: tagReader)
|
||||
|
||||
if caHandler.isChipAuthenticationSupported {
|
||||
do {
|
||||
// Do Chip authentication and then continue reading datagroups
|
||||
try await caHandler.doChipAuthentication()
|
||||
self.passport.chipAuthenticationStatus = .success
|
||||
} catch {
|
||||
Log.info( "Chip Authentication failed - re-establishing BAC")
|
||||
self.passport.chipAuthenticationStatus = .failed
|
||||
|
||||
// Failed Chip Auth, need to re-establish BAC
|
||||
try await doBACAuthentication(tagReader: tagReader)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we are skipping secure elements then remove .DG3 and .DG4
|
||||
if self.skipSecureElements {
|
||||
DGsToRead = DGsToRead.filter { $0 != .DG3 && $0 != .DG4 }
|
||||
}
|
||||
|
||||
if self.readAllDatagroups != true {
|
||||
DGsToRead = DGsToRead.filter { dataGroupsToRead.contains($0) }
|
||||
}
|
||||
for dgId in DGsToRead {
|
||||
self.updateReaderSessionMessage( alertMessage: NFCViewDisplayMessage.readingDataGroupProgress(dgId, 0) )
|
||||
if let dg = try await readDataGroup(tagReader:tagReader, dgId:dgId) {
|
||||
self.passport.addDataGroup( dgId, dataGroup:dg )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readDataGroup( tagReader : TagReader, dgId : DataGroupId ) async throws -> DataGroup? {
|
||||
|
||||
self.currentlyReadingDataGroup = dgId
|
||||
Log.info( "Reading tag - \(dgId)" )
|
||||
var readAttempts = 0
|
||||
|
||||
self.updateReaderSessionMessage( alertMessage: NFCViewDisplayMessage.readingDataGroupProgress(dgId, 0) )
|
||||
|
||||
repeat {
|
||||
do {
|
||||
let response = try await tagReader.readDataGroup(dataGroup:dgId)
|
||||
let dg = try DataGroupParser().parseDG(data: response)
|
||||
return dg
|
||||
} catch let error as NFCPassportReaderError {
|
||||
Log.error( "TagError reading tag - \(error)" )
|
||||
|
||||
// OK we had an error - depending on what happened, we may want to try to re-read this
|
||||
// E.g. we failed to read the last Datagroup because its protected and we can't
|
||||
let errMsg = error.value
|
||||
Log.error( "ERROR - \(errMsg)" )
|
||||
|
||||
var redoBAC = false
|
||||
if errMsg == "Session invalidated" || errMsg == "Class not supported" || errMsg == "Tag connection lost" {
|
||||
// Check if we have done Chip Authentication, if so, set it to nil and try to redo BAC
|
||||
if self.caHandler != nil {
|
||||
self.caHandler = nil
|
||||
redoBAC = true
|
||||
} else {
|
||||
// Can't go any more!
|
||||
throw error
|
||||
}
|
||||
} else if errMsg == "Security status not satisfied" || errMsg == "File not found" {
|
||||
// Can't read this element as we aren't allowed - remove it and return out so we re-do BAC
|
||||
self.dataGroupsToRead.removeFirst()
|
||||
redoBAC = true
|
||||
} else if errMsg == "SM data objects incorrect" || errMsg == "Class not supported" {
|
||||
// Can't read this element security objects now invalid - and return out so we re-do BAC
|
||||
redoBAC = true
|
||||
} else if errMsg.hasPrefix( "Wrong length" ) || errMsg.hasPrefix( "End of file" ) { // Should now handle errors 0x6C xx, and 0x67 0x00
|
||||
// OK passport can't handle max length so drop it down
|
||||
tagReader.reduceDataReadingAmount()
|
||||
redoBAC = true
|
||||
}
|
||||
|
||||
if redoBAC {
|
||||
// Redo BAC and try again
|
||||
try await doBACAuthentication(tagReader : tagReader)
|
||||
} else {
|
||||
// Some other error lets have another try
|
||||
}
|
||||
}
|
||||
readAttempts += 1
|
||||
} while ( readAttempts < 2 )
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func invalidateSession(errorMessage: NFCViewDisplayMessage, error: NFCPassportReaderError) {
|
||||
// Mark the next 'invalid session' error as not reportable (we're about to cause it by invalidating the
|
||||
// session). The real error is reported back with the call to the completed handler
|
||||
self.shouldNotReportNextReaderSessionInvalidationErrorUserCanceled = true
|
||||
self.readerSession?.invalidate(errorMessage: self.nfcViewDisplayMessageHandler?(errorMessage) ?? errorMessage.description)
|
||||
nfcContinuation?.resume(throwing: error)
|
||||
nfcContinuation = nil
|
||||
}
|
||||
}
|
||||
#endif
|
||||
17
app/ios/NFCPassportReader/ResponseAPDU.swift
Normal file
17
app/ios/NFCPassportReader/ResponseAPDU.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
#if !os(macOS)
|
||||
|
||||
@available(iOS 13, *)
|
||||
public struct ResponseAPDU {
|
||||
|
||||
public var data : [UInt8]
|
||||
public var sw1 : UInt8
|
||||
public var sw2 : UInt8
|
||||
|
||||
public init(data: [UInt8], sw1: UInt8, sw2: UInt8) {
|
||||
self.data = data
|
||||
self.sw1 = sw1
|
||||
self.sw2 = sw2
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
292
app/ios/NFCPassportReader/SecureMessaging.swift
Normal file
292
app/ios/NFCPassportReader/SecureMessaging.swift
Normal file
@@ -0,0 +1,292 @@
|
||||
import Foundation
|
||||
|
||||
public enum SecureMessagingSupportedAlgorithms {
|
||||
case DES
|
||||
case AES
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
import CoreNFC
|
||||
|
||||
|
||||
/// This class implements the secure messaging protocol.
|
||||
/// The class is a new layer that comes between the reader and the iso7816.
|
||||
/// It gives a new transmit method that takes an APDU object formed by the iso7816 layer,
|
||||
/// ciphers it following the doc9303 specification, sends the ciphered APDU to the reader
|
||||
/// layer and returns the unciphered APDU.
|
||||
@available(iOS 13, *)
|
||||
public class SecureMessaging {
|
||||
private var ksenc : [UInt8]
|
||||
private var ksmac : [UInt8]
|
||||
private var ssc : [UInt8]
|
||||
private let algoName : SecureMessagingSupportedAlgorithms
|
||||
private let padLength : Int
|
||||
|
||||
public init( encryptionAlgorithm : SecureMessagingSupportedAlgorithms = .DES, ksenc : [UInt8], ksmac : [UInt8], ssc : [UInt8]) {
|
||||
self.ksenc = ksenc
|
||||
self.ksmac = ksmac
|
||||
self.ssc = ssc
|
||||
self.algoName = encryptionAlgorithm
|
||||
self.padLength = algoName == .DES ? 8 : 16
|
||||
}
|
||||
|
||||
/// Protect the apdu following the doc9303 specification
|
||||
func protect(apdu : NFCISO7816APDU ) throws -> NFCISO7816APDU {
|
||||
|
||||
Log.verbose("\t\tSSC: " + binToHexRep(self.ssc))
|
||||
self.ssc = self.incSSC()
|
||||
let paddedSSC = algoName == .DES ? self.ssc : [UInt8](repeating: 0, count: 8) + ssc
|
||||
Log.verbose("\tIncrement SSC with 1")
|
||||
Log.verbose("\t\tSSC: " + binToHexRep(self.ssc))
|
||||
|
||||
|
||||
let cmdHeader = self.maskClassAndPad(apdu: apdu)
|
||||
var do87 : [UInt8] = []
|
||||
var do97 : [UInt8] = []
|
||||
|
||||
var tmp = "Concatenate CmdHeader"
|
||||
if apdu.data != nil {
|
||||
tmp += " and DO87"
|
||||
do87 = try self.buildD087(apdu: apdu)
|
||||
}
|
||||
|
||||
let isMSE = apdu.instructionCode == 0x22
|
||||
if apdu.expectedResponseLength > 0 && (isMSE ? apdu.expectedResponseLength < 256 : true) {
|
||||
tmp += " and DO97"
|
||||
do97 = try self.buildD097(apdu: apdu)
|
||||
}
|
||||
|
||||
let M = cmdHeader + do87 + do97
|
||||
Log.verbose(tmp)
|
||||
Log.verbose("\tM: " + binToHexRep(M))
|
||||
|
||||
Log.verbose("Compute MAC of M")
|
||||
|
||||
let N = pad(paddedSSC + M, blockSize:padLength)
|
||||
Log.verbose("\tConcatenate SSC and M and add padding")
|
||||
Log.verbose("\t\tN: " + binToHexRep(N))
|
||||
|
||||
var CC = mac(algoName: algoName, key: self.ksmac, msg: N)
|
||||
if CC.count > 8 {
|
||||
CC = [UInt8](CC[0..<8])
|
||||
}
|
||||
Log.verbose("\tCompute MAC over N with KSmac")
|
||||
Log.verbose("\t\tCC: " + binToHexRep(CC))
|
||||
|
||||
let do8e = self.buildD08E(mac: CC)
|
||||
|
||||
// If dataSize > 255 then it will be encoded in 3 bytes with the first byte being 0x00
|
||||
// otherwise its a single byte of size
|
||||
let size = do87.count + do97.count + do8e.count
|
||||
var dataSize: [UInt8]
|
||||
if size > 255 {
|
||||
dataSize = [0x00] + intToBin(size, pad: 4)
|
||||
} else {
|
||||
dataSize = intToBin(size)
|
||||
}
|
||||
var protectedAPDU = [UInt8](cmdHeader[0..<4]) + dataSize
|
||||
protectedAPDU += do87 + do97 + do8e
|
||||
|
||||
// If the data is more that 255, specify the we are using extended length (0x00, 0x00)
|
||||
// Thanks to @filom for the fix!
|
||||
if size > 255 {
|
||||
protectedAPDU += [0x00,0x00]
|
||||
} else {
|
||||
protectedAPDU += [0x00]
|
||||
}
|
||||
Log.verbose("Construct and send protected APDU")
|
||||
Log.verbose("\tProtectedAPDU: " + binToHexRep(protectedAPDU))
|
||||
|
||||
let newAPDU = NFCISO7816APDU(data:Data(protectedAPDU))!
|
||||
return newAPDU
|
||||
}
|
||||
|
||||
/// Unprotect the APDU following the iso7816 specification
|
||||
func unprotect(rapdu : ResponseAPDU ) throws -> ResponseAPDU {
|
||||
var needCC = false
|
||||
var do87 : [UInt8] = []
|
||||
var do87Data : [UInt8] = []
|
||||
var do99 : [UInt8] = []
|
||||
//var do8e : [UInt8] = []
|
||||
var offset = 0
|
||||
|
||||
self.ssc = self.incSSC()
|
||||
let paddedSSC = algoName == .DES ? self.ssc : [UInt8](repeating: 0, count: 8) + ssc
|
||||
Log.verbose("\tIncrement SSC with 1")
|
||||
Log.verbose("\t\tSSC: " + binToHexRep(self.ssc))
|
||||
|
||||
// Check for a SM error
|
||||
if(rapdu.sw1 != 0x90 || rapdu.sw2 != 0x00) {
|
||||
return rapdu
|
||||
}
|
||||
|
||||
let rapduBin = rapdu.data + [rapdu.sw1, rapdu.sw2]
|
||||
Log.verbose("Receive response APDU of MRTD's chip")
|
||||
Log.verbose("\tRAPDU: " + binToHexRep(rapduBin))
|
||||
|
||||
// DO'87'
|
||||
// Mandatory if data is returned, otherwise absent
|
||||
if rapduBin[0] == 0x87 {
|
||||
let (encDataLength, o) = try asn1Length([UInt8](rapduBin[1...]))
|
||||
offset = 1 + o
|
||||
|
||||
if rapduBin[offset] != 0x1 {
|
||||
throw NFCPassportReaderError.D087Malformed
|
||||
}
|
||||
|
||||
do87 = [UInt8](rapduBin[0 ..< offset + Int(encDataLength)])
|
||||
do87Data = [UInt8](rapduBin[offset+1 ..< offset + Int(encDataLength)])
|
||||
offset += Int(encDataLength)
|
||||
needCC = true
|
||||
}
|
||||
|
||||
//DO'99'
|
||||
// Mandatory, only absent if SM error occurs
|
||||
guard rapduBin.count >= offset + 5 else {
|
||||
print("size error")
|
||||
let returnSw1 = (rapduBin.count >= offset+3) ? rapduBin[offset+2] : 0;
|
||||
let returnSw2 = (rapduBin.count >= offset+4) ? rapduBin[offset+3] : 0;
|
||||
return ResponseAPDU(data: [], sw1: returnSw1, sw2: returnSw2);
|
||||
}
|
||||
|
||||
do99 = [UInt8](rapduBin[offset..<offset+4])
|
||||
let sw1 = rapduBin[offset+2]
|
||||
let sw2 = rapduBin[offset+3]
|
||||
offset += 4
|
||||
needCC = true
|
||||
|
||||
if do99[0] != 0x99 && do99[1] != 0x02 {
|
||||
//SM error, return the error code
|
||||
return ResponseAPDU(data: [], sw1: sw1, sw2: sw2)
|
||||
}
|
||||
|
||||
// DO'8E'
|
||||
//Mandatory if DO'87' and/or DO'99' is present
|
||||
if rapduBin[offset] == 0x8E {
|
||||
let ccLength : Int = Int(binToHex(rapduBin[offset+1]))
|
||||
let CC = [UInt8](rapduBin[offset+2 ..< offset+2+ccLength])
|
||||
// do8e = [UInt8](rapduBin[offset ..< offset+2+ccLength])
|
||||
|
||||
// CheckCC
|
||||
var tmp = ""
|
||||
if do87.count > 0 {
|
||||
tmp += " DO'87"
|
||||
}
|
||||
if do99.count > 0 {
|
||||
tmp += " DO'99"
|
||||
}
|
||||
Log.verbose("Verify RAPDU CC by computing MAC of" + tmp)
|
||||
|
||||
let K = pad(paddedSSC + do87 + do99, blockSize:padLength)
|
||||
Log.verbose("\tConcatenate SSC and" + tmp + " and add padding")
|
||||
Log.verbose("\t\tK: " + binToHexRep(K))
|
||||
|
||||
Log.verbose("\tCompute MAC with KSmac")
|
||||
var CCb = mac(algoName: algoName, key: self.ksmac, msg: K)
|
||||
if CCb.count > 8 {
|
||||
CCb = [UInt8](CC[0..<8])
|
||||
}
|
||||
Log.verbose("\t\tCC: " + binToHexRep(CCb))
|
||||
|
||||
let res = (CC == CCb)
|
||||
Log.verbose("\tCompare CC with data of DO'8E of RAPDU")
|
||||
Log.verbose("\t\t\(binToHexRep(CC)) == \(binToHexRep(CCb)) ? \(res)")
|
||||
|
||||
if !res {
|
||||
throw NFCPassportReaderError.InvalidResponseChecksum
|
||||
}
|
||||
}
|
||||
else if needCC {
|
||||
throw NFCPassportReaderError.MissingMandatoryFields
|
||||
}
|
||||
|
||||
var data : [UInt8] = []
|
||||
if do87Data.count > 0 {
|
||||
|
||||
let dec : [UInt8]
|
||||
if algoName == .DES {
|
||||
dec = tripleDESDecrypt(key: self.ksenc, message: do87Data, iv: [0,0,0,0,0,0,0,0])
|
||||
} else {
|
||||
// for AES the IV is the ssc with AES/EBC/NOPADDING
|
||||
let paddedssc = [UInt8](repeating: 0, count: 8) + ssc
|
||||
let iv = AESECBEncrypt(key: ksenc, message: paddedssc)
|
||||
dec = AESDecrypt(key: self.ksenc, message: do87Data, iv: iv)
|
||||
}
|
||||
|
||||
// There is a payload
|
||||
data = unpad(dec)
|
||||
Log.verbose("Decrypt data of DO'87 with KSenc")
|
||||
Log.verbose("\tDecryptedData: " + binToHexRep(data))
|
||||
}
|
||||
|
||||
Log.verbose("Unprotected APDU: [\(binToHexRep(data))] \(binToHexRep(sw1)) \(binToHexRep(sw2))" )
|
||||
return ResponseAPDU(data: data, sw1: sw1, sw2: sw2)
|
||||
}
|
||||
|
||||
func maskClassAndPad(apdu : NFCISO7816APDU ) -> [UInt8] {
|
||||
Log.verbose("Mask class byte and pad command header")
|
||||
let res = pad([0x0c, apdu.instructionCode, apdu.p1Parameter, apdu.p2Parameter], blockSize: padLength)
|
||||
Log.verbose("\tCmdHeader: " + binToHexRep(res))
|
||||
return res
|
||||
}
|
||||
|
||||
func buildD087(apdu : NFCISO7816APDU) throws -> [UInt8] {
|
||||
let cipher = [0x01] + self.padAndEncryptData(apdu)
|
||||
let res = try [0x87] + toAsn1Length(cipher.count) + cipher
|
||||
Log.verbose("Build DO'87")
|
||||
Log.verbose("\tDO87: " + binToHexRep(res))
|
||||
return res
|
||||
}
|
||||
|
||||
func padAndEncryptData(_ apdu : NFCISO7816APDU) -> [UInt8] {
|
||||
// Pad the data, encrypt data with KSenc and build DO'87
|
||||
let data = [UInt8](apdu.data!)
|
||||
let paddedData = pad( data, blockSize: padLength )
|
||||
|
||||
let enc : [UInt8]
|
||||
if algoName == .DES {
|
||||
enc = tripleDESEncrypt(key: self.ksenc, message: paddedData, iv: [0,0,0,0,0,0,0,0])
|
||||
} else {
|
||||
// for AES the IV is the ssc with AES/EBC/NOPADDING
|
||||
let paddedssc = [UInt8](repeating: 0, count: 8) + ssc
|
||||
let iv = AESECBEncrypt(key: ksenc, message: paddedssc)
|
||||
enc = AESEncrypt(key: self.ksenc, message: paddedData, iv: iv)
|
||||
}
|
||||
|
||||
Log.verbose("Pad data")
|
||||
Log.verbose("\tData: " + binToHexRep(paddedData))
|
||||
Log.verbose("Encrypt data with KSenc")
|
||||
Log.verbose("\tEncryptedData: " + binToHexRep(enc))
|
||||
return enc
|
||||
}
|
||||
|
||||
func incSSC() -> [UInt8] {
|
||||
let val = binToHex(self.ssc) + 1
|
||||
|
||||
// This needs to be fully zero padded - to 8 bytes = i.e. if SSC is 1 it should return [0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x1]
|
||||
// NOT [0x1]
|
||||
return withUnsafeBytes(of: val.bigEndian, Array.init)
|
||||
}
|
||||
|
||||
func buildD08E(mac : [UInt8]) -> [UInt8] {
|
||||
let res : [UInt8] = [0x8E, UInt8(mac.count)] + mac
|
||||
Log.verbose("Build DO'8E")
|
||||
Log.verbose("\tDO8E: \(binToHexRep(res))" )
|
||||
return res
|
||||
}
|
||||
|
||||
func buildD097(apdu : NFCISO7816APDU) throws -> [UInt8] {
|
||||
let le = apdu.expectedResponseLength
|
||||
var binLe = intToBin(le)
|
||||
if (le == 256 || le == 65536) {
|
||||
binLe = [0x00] + (le > 256 ? [0x00] : [])
|
||||
}
|
||||
|
||||
let res : [UInt8] = try [0x97] + toAsn1Length(binLe.count) + binLe
|
||||
Log.verbose("Build DO'97")
|
||||
Log.verbose("\tDO97: \(res)")
|
||||
return res
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,149 @@
|
||||
import Foundation
|
||||
|
||||
import CryptoKit
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
class SecureMessagingSessionKeyGenerator {
|
||||
static let NO_PACE_KEY_REFERENCE : UInt8 = 0x00
|
||||
enum SMSMode : UInt8 {
|
||||
case ENC_MODE = 0x1;
|
||||
case MAC_MODE = 0x2;
|
||||
case PACE_MODE = 0x3;
|
||||
}
|
||||
|
||||
/// Derives the ENC or MAC key for BAC from the keySeed.
|
||||
/// - Parameter keySeed the key seed.
|
||||
/// - Parameter mode either <code>ENC_MODE</code> or <code>MAC_MODE</code>
|
||||
/// - Returns the key.
|
||||
/// - Throws InvalidDataPassed on data error
|
||||
func deriveKey( keySeed : [UInt8], mode : SMSMode) throws -> [UInt8] {
|
||||
return try deriveKey(keySeed: keySeed, cipherAlgName: "DESede", keyLength: 128, mode: mode);
|
||||
}
|
||||
|
||||
/// Derives the ENC or MAC key for BAC or PACE or CA.
|
||||
/// - Parameter keySeed the key seed.
|
||||
/// - Parameter cipherAlgName either AES or DESede
|
||||
/// - Parameter keyLength key length in bits
|
||||
/// - Parameter mode either {@code ENC_MODE}, {@code MAC_MODE}, or {@code PACE_MODE}
|
||||
/// - Returns the key.
|
||||
/// - Throws InvalidDataPassed on data error
|
||||
func deriveKey(keySeed : [UInt8], cipherAlgName :String, keyLength : Int, mode : SMSMode) throws -> [UInt8] {
|
||||
return try deriveKey(keySeed: keySeed, cipherAlgName: cipherAlgName, keyLength: keyLength, nonce: nil, mode: mode);
|
||||
}
|
||||
|
||||
/// Derives the ENC or MAC key for BAC or PACE or CA.
|
||||
/// - Parameter keySeed the shared secret, as octets
|
||||
/// - Parameter cipherAlg in Java mnemonic notation (for example "DESede", "AES")
|
||||
/// - Parameter keyLength length in bits
|
||||
/// - Parameter nonce optional nonce or <code>nil</code>
|
||||
/// - Parameter mode the mode either {@code ENC}, {@code MAC}, or {@code PACE} mode
|
||||
/// - Returns the key.
|
||||
/// - Throws InvalidDataPassed on data error
|
||||
func deriveKey(keySeed : [UInt8], cipherAlgName :String, keyLength : Int, nonce : [UInt8]? = nil, mode : SMSMode) throws -> [UInt8] {
|
||||
return try deriveKey(keySeed: keySeed, cipherAlgName: cipherAlgName, keyLength: keyLength, nonce: nonce, mode: mode, paceKeyReference: SecureMessagingSessionKeyGenerator.NO_PACE_KEY_REFERENCE);
|
||||
}
|
||||
|
||||
/// Derives the ENC or MAC key for BAC or PACE or CA.
|
||||
/// - Parameter keySeed the shared secret, as octets
|
||||
/// - Parameter cipherAlg in Java mnemonic notation (for example "DESede", "AES")
|
||||
/// - Parameter keyLength length in bits
|
||||
/// - Parameter nonce optional nonce or <code>null</code>
|
||||
/// - Parameter mode the mode either {@code ENC}, {@code MAC}, or {@code PACE} mode
|
||||
/// - Parameter paceKeyReference Key Reference For Pace Protocol
|
||||
/// - Returns the key.
|
||||
/// - Throws InvalidDataPassed on data error
|
||||
func deriveKey(keySeed : [UInt8], cipherAlgName :String, keyLength : Int, nonce : [UInt8]?, mode : SMSMode, paceKeyReference : UInt8) throws -> [UInt8] {
|
||||
let digestAlgo = try inferDigestAlgorithmFromCipherAlgorithmForKeyDerivation(cipherAlg: cipherAlgName, keyLength: keyLength);
|
||||
|
||||
let modeArr : [UInt8] = [0x00, 0x00, 0x00, mode.rawValue]
|
||||
var dataEls = [Data(keySeed)]
|
||||
if let nonce = nonce {
|
||||
dataEls.append( Data(nonce) )
|
||||
}
|
||||
dataEls.append( Data(modeArr) )
|
||||
let hashResult = try getHash(algo: digestAlgo, dataElements: dataEls)
|
||||
|
||||
var keyBytes : [UInt8]
|
||||
if cipherAlgName == "DESede" || cipherAlgName == "3DES" {
|
||||
// TR-SAC 1.01, 4.2.1.
|
||||
switch(keyLength) {
|
||||
case 112, 128:
|
||||
// Copy E (Octects 1 to 8), D (Octects 9 to 16), E (again Octects 1 to 8), 112-bit 3DES key
|
||||
keyBytes = [UInt8](hashResult[0..<16] + hashResult[0..<8])
|
||||
break;
|
||||
default:
|
||||
throw NFCPassportReaderError.InvalidDataPassed("Can only use DESede with 128-but key length")
|
||||
}
|
||||
} else if cipherAlgName.lowercased() == "aes" || cipherAlgName.lowercased().hasPrefix("aes") {
|
||||
// TR-SAC 1.01, 4.2.2.
|
||||
switch(keyLength) {
|
||||
case 128:
|
||||
keyBytes = [UInt8](hashResult[0..<16]) // NOTE: 128 = 16 * 8
|
||||
case 192:
|
||||
keyBytes = [UInt8](hashResult[0..<24]) // NOTE: 192 = 24 * 8
|
||||
case 256:
|
||||
keyBytes = [UInt8](hashResult[0..<32]) // NOTE: 256 = 32 * 8
|
||||
default:
|
||||
throw NFCPassportReaderError.InvalidDataPassed("Can only use AES with 128-bit, 192-bit key or 256-bit length")
|
||||
}
|
||||
} else {
|
||||
throw NFCPassportReaderError.InvalidDataPassed( "Unsupported cipher algorithm used" )
|
||||
}
|
||||
|
||||
return keyBytes
|
||||
}
|
||||
|
||||
func inferDigestAlgorithmFromCipherAlgorithmForKeyDerivation( cipherAlg : String, keyLength : Int) throws -> String {
|
||||
if cipherAlg == "DESede" || cipherAlg == "AES-128" {
|
||||
return "SHA1";
|
||||
}
|
||||
if cipherAlg == "AES" && keyLength == 128 {
|
||||
return "SHA1";
|
||||
}
|
||||
if cipherAlg == "AES-256" || cipherAlg == "AES-192" {
|
||||
return "SHA256";
|
||||
}
|
||||
if cipherAlg == "AES" && (keyLength == 192 || keyLength == 256) {
|
||||
return "SHA256";
|
||||
}
|
||||
|
||||
throw NFCPassportReaderError.InvalidDataPassed("Unsupported cipher algorithm or key length")
|
||||
}
|
||||
|
||||
/// This generates a SHA-X hash based on the passed in algo.
|
||||
/// There must be a more generic way to do this?
|
||||
func getHash(algo: String, dataElements:[Data] ) throws -> [UInt8] {
|
||||
var hash : [UInt8]
|
||||
|
||||
let algo = algo.lowercased()
|
||||
if algo == "sha1" {
|
||||
var hasher = Insecure.SHA1()
|
||||
for d in dataElements {
|
||||
hasher.update( data:d )
|
||||
}
|
||||
hash = Array(hasher.finalize())
|
||||
} else if algo == "sha256" {
|
||||
var hasher = SHA256()
|
||||
for d in dataElements {
|
||||
hasher.update( data:d )
|
||||
}
|
||||
hash = Array(hasher.finalize())
|
||||
} else if algo == "sha384" {
|
||||
var hasher = SHA384()
|
||||
for d in dataElements {
|
||||
hasher.update( data:d )
|
||||
}
|
||||
hash = Array(hasher.finalize())
|
||||
} else if algo == "sha512" {
|
||||
var hasher = SHA512()
|
||||
for d in dataElements {
|
||||
hasher.update( data:d )
|
||||
}
|
||||
hash = Array(hasher.finalize())
|
||||
} else {
|
||||
throw NFCPassportReaderError.InvalidHashAlgorithmSpecified
|
||||
}
|
||||
|
||||
return hash
|
||||
}
|
||||
}
|
||||
166
app/ios/NFCPassportReader/SimpleASN1DumpParser.swift
Normal file
166
app/ios/NFCPassportReader/SimpleASN1DumpParser.swift
Normal file
@@ -0,0 +1,166 @@
|
||||
import Foundation
|
||||
import OpenSSL
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class ASN1Item : CustomDebugStringConvertible {
|
||||
var pos : Int = -1
|
||||
var depth : Int = -1
|
||||
var headerLen : Int = -1
|
||||
var length : Int = -1
|
||||
var itemType : String = "" // Primative or Constructed (prim or cons)
|
||||
var type : String = "" // Actual type of the value ( object, set, etc)
|
||||
var value : String = ""
|
||||
var line : String = ""
|
||||
var parent : ASN1Item? = nil
|
||||
|
||||
private var children = [ASN1Item] ()
|
||||
|
||||
public init( line : String) {
|
||||
self.line = line
|
||||
|
||||
let scanner = Scanner(string: line)
|
||||
|
||||
let space = CharacterSet(charactersIn: " =:")
|
||||
let equals = CharacterSet(charactersIn: "= ")
|
||||
let colon = CharacterSet(charactersIn: ":")
|
||||
let end = CharacterSet(charactersIn: "\n")
|
||||
|
||||
scanner.charactersToBeSkipped = space
|
||||
self.pos = scanner.scanInt() ?? -1
|
||||
_ = scanner.scanUpToCharacters(from: equals)
|
||||
self.depth = scanner.scanInt() ?? -1
|
||||
_ = scanner.scanUpToCharacters(from: equals)
|
||||
self.headerLen = scanner.scanInt() ?? -1
|
||||
_ = scanner.scanUpToCharacters(from: equals)
|
||||
self.length = scanner.scanInt() ?? -1
|
||||
self.itemType = scanner.scanUpToCharacters(from: colon) ?? ""
|
||||
let rest = scanner.scanUpToCharacters(from: end)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
|
||||
if itemType == "cons" {
|
||||
type = rest
|
||||
|
||||
} else {
|
||||
let items = rest.components(separatedBy: ":" ).filter{ !$0.isEmpty }
|
||||
self.type = items[0].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if ( items.count > 1 ) {
|
||||
self.value = items[1].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addChild( _ child : ASN1Item ) {
|
||||
child.parent = self
|
||||
self.children.append( child )
|
||||
}
|
||||
|
||||
public func getChild( _ child : Int ) -> ASN1Item? {
|
||||
if ( child < children.count ) {
|
||||
return children[child]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func getNumberOfChildren() -> Int {
|
||||
return children.count
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
var ret = "pos:\(pos), d=\(depth), hl=\(headerLen), l=\(length): \(itemType): \(type) \(value)\n"
|
||||
children.forEach { ret += $0.debugDescription }
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
/// Very very basic ASN1 parser class - uses OpenSSL to dump an ASN1 structure to a string, and then parses that out into
|
||||
/// a tree based hieracy of ASN1Item structures - depth based
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class SimpleASN1DumpParser {
|
||||
public init() {
|
||||
|
||||
}
|
||||
|
||||
public func parse( data: Data ) throws -> ASN1Item {
|
||||
var parsed : String = ""
|
||||
|
||||
|
||||
let _ = try data.withUnsafeBytes { (ptr) in
|
||||
guard let out = BIO_new(BIO_s_mem()) else { throw OpenSSLError.UnableToParseASN1("Unable to allocate output buffer") }
|
||||
defer { BIO_free(out) }
|
||||
|
||||
let rc = ASN1_parse_dump(out, ptr.baseAddress?.assumingMemoryBound(to: UInt8.self), data.count, 0, 0)
|
||||
if rc == 0 {
|
||||
throw OpenSSLError.UnableToParseASN1("Failed to parse ASN1 Data")
|
||||
}
|
||||
|
||||
parsed = OpenSSLUtils.bioToString(bio: out)
|
||||
}
|
||||
|
||||
let lines = parsed.components(separatedBy: "\n")
|
||||
let topItem : ASN1Item? = parseLines( lines:lines)
|
||||
|
||||
guard let ret = topItem else {
|
||||
throw OpenSSLError.UnableToParseASN1("Failed to format ASN1 Data")
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func parseLines( lines : [String] ) -> ASN1Item? {
|
||||
var topItem : ASN1Item?
|
||||
|
||||
var currentParent : ASN1Item?
|
||||
for line in lines {
|
||||
if line.trimmingCharacters(in: .whitespacesAndNewlines) == "" {
|
||||
continue
|
||||
}
|
||||
let item = ASN1Item(line: line)
|
||||
if item.depth == 0 {
|
||||
topItem = item
|
||||
} else if item.depth == currentParent!.depth {
|
||||
currentParent!.parent!.addChild( item )
|
||||
} else if item.depth > currentParent!.depth {
|
||||
currentParent!.addChild( item )
|
||||
} else {
|
||||
repeat {
|
||||
currentParent = currentParent!.parent
|
||||
} while currentParent!.depth > item.depth-1 && currentParent!.depth != 0
|
||||
if currentParent!.depth == item.depth-1 {
|
||||
currentParent!.addChild( item )
|
||||
}
|
||||
}
|
||||
currentParent = item
|
||||
}
|
||||
|
||||
return topItem
|
||||
}
|
||||
|
||||
public func test() {
|
||||
let lines = [
|
||||
" 0:d=0 hl=4 l= 758 cons: SET ",
|
||||
" 662:d=1 hl=2 l= 18 cons: SEQUENCE ",
|
||||
" 664:d=2 hl=2 l= 10 prim: OBJECT :0.4.0.127.0.7.2.2.3.2.4",
|
||||
" 676:d=2 hl=2 l= 1 prim: INTEGER :01",
|
||||
" 679:d=2 hl=2 l= 1 prim: INTEGER :01",
|
||||
" 682:d=1 hl=2 l= 18 cons: SEQUENCE ",
|
||||
" 684:d=2 hl=2 l= 10 prim: OBJECT :0.4.0.127.0.7.2.2.3.2.1",
|
||||
" 696:d=2 hl=2 l= 1 prim: INTEGER :01",
|
||||
" 699:d=2 hl=2 l= 1 prim: INTEGER :02",
|
||||
" 702:d=1 hl=2 l= 13 cons: SEQUENCE ",
|
||||
" 704:d=2 hl=2 l= 8 prim: OBJECT :0.4.0.127.0.7.2.2.2",
|
||||
" 714:d=2 hl=2 l= 1 prim: INTEGER :01",
|
||||
" 717:d=1 hl=2 l= 18 cons: SEQUENCE ",
|
||||
" 719:d=2 hl=2 l= 10 prim: OBJECT :0.4.0.127.0.7.2.2.4.2.4",
|
||||
" 731:d=2 hl=2 l= 1 prim: INTEGER :02",
|
||||
" 734:d=2 hl=2 l= 1 prim: INTEGER :0D",
|
||||
" 737:d=1 hl=2 l= 23 cons: SEQUENCE ",
|
||||
" 739:d=2 hl=2 l= 6 prim: OBJECT :2.23.136.1.1.5",
|
||||
" 747:d=2 hl=2 l= 1 prim: INTEGER :01",
|
||||
" 750:d=2 hl=2 l= 10 prim: OBJECT :0.4.0.127.0.7.1.1.4.1.3",
|
||||
""
|
||||
]
|
||||
|
||||
let topItem = parseLines( lines:lines )
|
||||
print( topItem?.debugDescription ?? "" )
|
||||
}
|
||||
}
|
||||
349
app/ios/NFCPassportReader/TagReader.swift
Normal file
349
app/ios/NFCPassportReader/TagReader.swift
Normal file
@@ -0,0 +1,349 @@
|
||||
import Foundation
|
||||
|
||||
#if !os(macOS)
|
||||
import CoreNFC
|
||||
|
||||
@available(iOS 15, *)
|
||||
public class TagReader {
|
||||
var tag : NFCISO7816Tag
|
||||
var secureMessaging : SecureMessaging?
|
||||
var maxDataLengthToRead : Int = 0xA0 // Should be able to use 256 to read arbitrary amounts of data at full speed BUT this isn't supported across all passports so for reliability just use the smaller amount.
|
||||
|
||||
var progress : ((Int)->())?
|
||||
|
||||
init( tag: NFCISO7816Tag ) {
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
func overrideDataAmountToRead( newAmount : Int ) {
|
||||
maxDataLengthToRead = newAmount
|
||||
}
|
||||
|
||||
func reduceDataReadingAmount() {
|
||||
if maxDataLengthToRead > 0xA0 {
|
||||
maxDataLengthToRead = 0xA0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func readDataGroup( dataGroup: DataGroupId ) async throws -> [UInt8] {
|
||||
guard let tag = dataGroup.getFileIDTag() else {
|
||||
throw NFCPassportReaderError.UnsupportedDataGroup
|
||||
}
|
||||
|
||||
return try await selectFileAndRead(tag: tag )
|
||||
}
|
||||
|
||||
func getChallenge() async throws -> ResponseAPDU{
|
||||
let cmd : NFCISO7816APDU = NFCISO7816APDU(instructionClass: 00, instructionCode: 0x84, p1Parameter: 0, p2Parameter: 0, data: Data(), expectedResponseLength: 8)
|
||||
|
||||
return try await send( cmd: cmd )
|
||||
}
|
||||
|
||||
func doInternalAuthentication( challenge: [UInt8] ) async throws -> ResponseAPDU {
|
||||
let randNonce = Data(challenge)
|
||||
|
||||
let cmd = NFCISO7816APDU(instructionClass: 00, instructionCode: 0x88, p1Parameter: 0, p2Parameter: 0, data: randNonce, expectedResponseLength: 256)
|
||||
|
||||
return try await send( cmd: cmd )
|
||||
}
|
||||
|
||||
func doMutualAuthentication( cmdData : Data ) async throws -> ResponseAPDU{
|
||||
let cmd : NFCISO7816APDU = NFCISO7816APDU(instructionClass: 00, instructionCode: 0x82, p1Parameter: 0, p2Parameter: 0, data: cmdData, expectedResponseLength: 256)
|
||||
|
||||
return try await send( cmd: cmd )
|
||||
}
|
||||
|
||||
/// The MSE KAT APDU, see EAC 1.11 spec, Section B.1.
|
||||
/// This command is sent in the "DESede" case.
|
||||
/// - Parameter keyData key data object (tag 0x91)
|
||||
/// - Parameter idData key id data object (tag 0x84), can be null
|
||||
/// - Parameter completed the complete handler - returns the success response or an error
|
||||
func sendMSEKAT( keyData : Data, idData: Data? ) async throws -> ResponseAPDU {
|
||||
|
||||
var data = keyData
|
||||
if let idData = idData {
|
||||
data += idData
|
||||
}
|
||||
|
||||
let cmd : NFCISO7816APDU = NFCISO7816APDU(instructionClass: 00, instructionCode: 0x22, p1Parameter: 0x41, p2Parameter: 0xA6, data: data, expectedResponseLength: 256)
|
||||
|
||||
return try await send( cmd: cmd )
|
||||
}
|
||||
|
||||
/// The MSE Set AT for Chip Authentication.
|
||||
/// This command is the first command that is sent in the "AES" case.
|
||||
/// For Chip Authentication. We prefix 0x80 for OID and 0x84 for keyId.
|
||||
///
|
||||
/// NOTE THIS IS CURRENTLY UNTESTED
|
||||
/// - Parameter oid the OID
|
||||
/// - Parameter keyId the keyId or {@code null}
|
||||
/// - Parameter completed the complete handler - returns the success response or an error
|
||||
func sendMSESetATIntAuth( oid: String, keyId: Int? ) async throws -> ResponseAPDU {
|
||||
|
||||
let cmd : NFCISO7816APDU
|
||||
let oidBytes = oidToBytes(oid: oid, replaceTag: true)
|
||||
|
||||
if let keyId = keyId, keyId != 0 {
|
||||
let keyIdBytes = wrapDO(b:0x84, arr:intToBytes(val:keyId, removePadding: true))
|
||||
let data = oidBytes + keyIdBytes
|
||||
|
||||
cmd = NFCISO7816APDU(instructionClass: 00, instructionCode: 0x22, p1Parameter: 0x41, p2Parameter: 0xA4, data: Data(data), expectedResponseLength: 256)
|
||||
|
||||
} else {
|
||||
cmd = NFCISO7816APDU(instructionClass: 00, instructionCode: 0x22, p1Parameter: 0x41, p2Parameter: 0xA4, data: Data(oidBytes), expectedResponseLength: 256)
|
||||
}
|
||||
|
||||
return try await send( cmd: cmd )
|
||||
}
|
||||
|
||||
func sendMSESetATMutualAuth( oid: String, keyType: UInt8 ) async throws -> ResponseAPDU {
|
||||
|
||||
let oidBytes = oidToBytes(oid: oid, replaceTag: true)
|
||||
let keyTypeBytes = wrapDO( b: 0x83, arr:[keyType])
|
||||
|
||||
let data = oidBytes + keyTypeBytes
|
||||
|
||||
let cmd = NFCISO7816APDU(instructionClass: 00, instructionCode: 0x22, p1Parameter: 0xC1, p2Parameter: 0xA4, data: Data(data), expectedResponseLength: -1)
|
||||
|
||||
return try await send( cmd: cmd )
|
||||
}
|
||||
|
||||
|
||||
/// Sends a General Authenticate command.
|
||||
/// This command is the second command that is sent in the "AES" case.
|
||||
/// - Parameter data data to be sent, without the {@code 0x7C} prefix (this method will add it)
|
||||
/// - Parameter lengthExpected the expected length defaults to 256
|
||||
/// - Parameter isLast indicates whether this is the last command in the chain
|
||||
/// - Parameter completed the complete handler - returns the dynamic authentication data without the {@code 0x7C} prefix (this method will remove it) or an error
|
||||
func sendGeneralAuthenticate( data : [UInt8], lengthExpected : Int = 256, isLast: Bool) async throws -> ResponseAPDU {
|
||||
|
||||
let wrappedData = wrapDO(b:0x7C, arr:data)
|
||||
let commandData = Data(wrappedData)
|
||||
|
||||
// NOTE: Support of Protocol Response Data is CONDITIONAL:
|
||||
// It MUST be provided for version 2 but MUST NOT be provided for version 1.
|
||||
// So, we are expecting 0x7C (= tag), 0x00 (= length) here.
|
||||
|
||||
// 0x10 is class command chaining
|
||||
let instructionClass : UInt8 = isLast ? 0x00 : 0x10
|
||||
let INS_BSI_GENERAL_AUTHENTICATE : UInt8 = 0x86
|
||||
|
||||
let cmd : NFCISO7816APDU = NFCISO7816APDU(instructionClass: instructionClass, instructionCode: INS_BSI_GENERAL_AUTHENTICATE, p1Parameter: 0x00, p2Parameter: 0x00, data: commandData, expectedResponseLength: lengthExpected)
|
||||
var response : ResponseAPDU
|
||||
do {
|
||||
response = try await send( cmd: cmd )
|
||||
response.data = try unwrapDO( tag:0x7c, wrappedData:response.data)
|
||||
} catch {
|
||||
// If wrong length error
|
||||
if case NFCPassportReaderError.ResponseError(_, let sw1, let sw2) = error,
|
||||
sw1 == 0x67, sw2 == 0x00 {
|
||||
|
||||
// Resend
|
||||
let cmd : NFCISO7816APDU = NFCISO7816APDU(instructionClass: instructionClass, instructionCode: INS_BSI_GENERAL_AUTHENTICATE, p1Parameter: 0x00, p2Parameter: 0x00, data: commandData, expectedResponseLength: 256)
|
||||
response = try await send( cmd: cmd )
|
||||
response.data = try unwrapDO( tag:0x7c, wrappedData:response.data)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
func selectFileAndRead( tag: [UInt8]) async throws -> [UInt8] {
|
||||
var resp = try await selectFile(tag: tag )
|
||||
|
||||
// Read first 4 bytes of header to see how big the data structure is
|
||||
guard let readHeaderCmd = NFCISO7816APDU(data:Data([0x00, 0xB0, 0x00, 0x00, 0x00, 0x00,0x04])) else {
|
||||
throw NFCPassportReaderError.UnexpectedError
|
||||
}
|
||||
resp = try await self.send( cmd: readHeaderCmd )
|
||||
|
||||
// Header looks like: <tag><length of data><nextTag> e.g.60145F01 -
|
||||
// the total length is the 2nd value plus the two header 2 bytes
|
||||
// We've read 4 bytes so we now need to read the remaining bytes from offset 4
|
||||
let (len, o) = try! asn1Length([UInt8](resp.data[1..<4]))
|
||||
var remaining = Int(len)
|
||||
var amountRead = o + 1
|
||||
|
||||
var data = [UInt8](resp.data[..<amountRead])
|
||||
|
||||
Log.debug( "TagReader - Number of data bytes to read - \(remaining)" )
|
||||
|
||||
var readAmount : Int = maxDataLengthToRead
|
||||
while remaining > 0 {
|
||||
if maxDataLengthToRead != 256 && remaining < maxDataLengthToRead {
|
||||
readAmount = remaining
|
||||
}
|
||||
|
||||
self.progress?( Int(Float(amountRead) / Float(remaining+amountRead ) * 100))
|
||||
let offset = intToBin(amountRead, pad:4)
|
||||
|
||||
Log.verbose( "TagReader - data bytes remaining: \(remaining), will read : \(readAmount)" )
|
||||
let cmd = NFCISO7816APDU(
|
||||
instructionClass: 00,
|
||||
instructionCode: 0xB0,
|
||||
p1Parameter: offset[0],
|
||||
p2Parameter: offset[1],
|
||||
data: Data(),
|
||||
expectedResponseLength: readAmount
|
||||
)
|
||||
resp = try await self.send( cmd: cmd )
|
||||
|
||||
Log.verbose( "TagReader - got resp - \(binToHexRep(resp.data, asArray: true)), sw1 : \(resp.sw1), sw2 : \(resp.sw2)" )
|
||||
data += resp.data
|
||||
|
||||
remaining -= resp.data.count
|
||||
amountRead += resp.data.count
|
||||
Log.verbose( "TagReader - Amount of data left to read - \(remaining)" )
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
func readCardAccess() async throws -> [UInt8]{
|
||||
// Info provided by @smulu
|
||||
// By default NFCISO7816Tag requirers a list of ISO/IEC 7816 applets (AIDs). Upon discovery of NFC tag the first found applet from this list is automatically selected (and you have no way of changing this).
|
||||
// This is a problem for PACE protocol becaues it requires reading parameters from file EF.CardAccess which lies outside of eMRTD applet (AID: A0000002471001) in the master file.
|
||||
|
||||
// Now, the ICAO 9303 standard does specify command for selecting master file by sending SELECT APDU with P1=0x00, P2=0x0C and empty data field (see part 10 page 8). But after some testing I found out this command doesn't work on some passports (European passports) and although receiving success (sw=9000) from passport the master file is not selected.
|
||||
|
||||
// After a bit of researching standard ISO/IEC 7816 I found there is an alternative SELECT command for selecting master file. The command doesn't differ much from the command specified in ICAO 9303 doc with only difference that data field is set to: 0x3F00. See section 6.11.3 of ISO/IEC 7816-4.
|
||||
// By executing above SELECT command (with data=0x3F00) master file should be selected and you should be able to read EF.CardAccess from passport.
|
||||
|
||||
// First select master file
|
||||
let cmd : NFCISO7816APDU = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0xA4, p1Parameter: 0x00, p2Parameter: 0x0C, data: Data([0x3f,0x00]), expectedResponseLength: -1)
|
||||
|
||||
_ = try await send( cmd: cmd)
|
||||
|
||||
// Now read EC.CardAccess
|
||||
let data = try await self.selectFileAndRead(tag: [0x01,0x1C])
|
||||
return data
|
||||
}
|
||||
|
||||
func selectPassportApplication() async throws -> ResponseAPDU {
|
||||
// Finally reselect the eMRTD application so the rest of the reading works as normal
|
||||
Log.debug( "Re-selecting eMRTD Application" )
|
||||
let cmd : NFCISO7816APDU = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0xA4, p1Parameter: 0x04, p2Parameter: 0x0C, data: Data([0xA0, 0x00, 0x00, 0x02, 0x47, 0x10, 0x01]), expectedResponseLength: -1)
|
||||
|
||||
let response = try await self.send( cmd: cmd)
|
||||
return response
|
||||
}
|
||||
|
||||
func selectFile( tag: [UInt8] ) async throws -> ResponseAPDU {
|
||||
|
||||
let data : [UInt8] = [0x00, 0xA4, 0x02, 0x0C, 0x02] + tag
|
||||
let cmd = NFCISO7816APDU(data:Data(data))!
|
||||
|
||||
return try await send( cmd: cmd )
|
||||
}
|
||||
|
||||
func send( cmd: NFCISO7816APDU ) async throws -> ResponseAPDU {
|
||||
Log.verbose( "TagReader - sending \(cmd)" )
|
||||
var toSend = cmd
|
||||
if let sm = secureMessaging {
|
||||
toSend = try sm.protect(apdu:cmd)
|
||||
Log.verbose("TagReader - [SM] \(toSend)" )
|
||||
}
|
||||
|
||||
let (data, sw1, sw2) = try await tag.sendCommand(apdu: toSend)
|
||||
Log.verbose( "TagReader - Received response" )
|
||||
var rep = ResponseAPDU(data: [UInt8](data), sw1: sw1, sw2: sw2)
|
||||
|
||||
if let sm = self.secureMessaging {
|
||||
rep = try sm.unprotect(rapdu:rep)
|
||||
Log.verbose(String(format:"TagReader [SM - unprotected] \(binToHexRep(rep.data, asArray:true)), sw1:0x%02x sw2:0x%02x", rep.sw1, rep.sw2) )
|
||||
} else {
|
||||
Log.verbose(String(format:"TagReader [unprotected] \(binToHexRep(rep.data, asArray:true)), sw1:0x%02x sw2:0x%02x", rep.sw1, rep.sw2) )
|
||||
|
||||
}
|
||||
|
||||
if rep.sw1 != 0x90 && rep.sw2 != 0x00 {
|
||||
Log.error( "Error reading tag: sw1 - 0x\(binToHexRep(sw1)), sw2 - 0x\(binToHexRep(sw2))" )
|
||||
let tagError: NFCPassportReaderError
|
||||
if (rep.sw1 == 0x63 && rep.sw2 == 0x00) {
|
||||
tagError = NFCPassportReaderError.InvalidMRZKey
|
||||
} else {
|
||||
let errorMsg = self.decodeError(sw1: rep.sw1, sw2: rep.sw2)
|
||||
Log.error( "reason: \(errorMsg)" )
|
||||
tagError = NFCPassportReaderError.ResponseError( errorMsg, sw1, sw2 )
|
||||
}
|
||||
throw tagError
|
||||
}
|
||||
|
||||
return rep
|
||||
}
|
||||
|
||||
private func decodeError( sw1: UInt8, sw2:UInt8 ) -> String {
|
||||
|
||||
let errors : [UInt8 : [UInt8:String]] = [
|
||||
0x62: [0x00:"No information given",
|
||||
0x81:"Part of returned data may be corrupted",
|
||||
0x82:"End of file/record reached before reading Le bytes",
|
||||
0x83:"Selected file invalidated",
|
||||
0x84:"FCI not formatted according to ISO7816-4 section 5.1.5"],
|
||||
|
||||
0x63: [0x81:"File filled up by the last write",
|
||||
0x82:"Card Key not supported",
|
||||
0x83:"Reader Key not supported",
|
||||
0x84:"Plain transmission not supported",
|
||||
0x85:"Secured Transmission not supported",
|
||||
0x86:"Volatile memory not available",
|
||||
0x87:"Non Volatile memory not available",
|
||||
0x88:"Key number not valid",
|
||||
0x89:"Key length is not correct",
|
||||
0xC:"Counter provided by X (valued from 0 to 15) (exact meaning depending on the command)"],
|
||||
0x65: [0x00:"No information given",
|
||||
0x81:"Memory failure"],
|
||||
0x67: [0x00:"Wrong length"],
|
||||
0x68: [0x00:"No information given",
|
||||
0x81:"Logical channel not supported",
|
||||
0x82:"Secure messaging not supported",
|
||||
0x83:"Last command of the chain expected",
|
||||
0x84:"Command chaining not supported"],
|
||||
0x69: [0x00:"No information given",
|
||||
0x81:"Command incompatible with file structure",
|
||||
0x82:"Security status not satisfied",
|
||||
0x83:"Authentication method blocked",
|
||||
0x84:"Referenced data invalidated",
|
||||
0x85:"Conditions of use not satisfied",
|
||||
0x86:"Command not allowed (no current EF)",
|
||||
0x87:"Expected SM data objects missing",
|
||||
0x88:"SM data objects incorrect"],
|
||||
0x6A: [0x00:"No information given",
|
||||
0x80:"Incorrect parameters in the data field",
|
||||
0x81:"Function not supported",
|
||||
0x82:"File not found",
|
||||
0x83:"Record not found",
|
||||
0x84:"Not enough memory space in the file",
|
||||
0x85:"Lc inconsistent with TLV structure",
|
||||
0x86:"Incorrect parameters P1-P2",
|
||||
0x87:"Lc inconsistent with P1-P2",
|
||||
0x88:"Referenced data not found"],
|
||||
0x6B: [0x00:"Wrong parameter(s) P1-P2]"],
|
||||
0x6D: [0x00:"Instruction code not supported or invalid"],
|
||||
0x6E: [0x00:"Class not supported"],
|
||||
0x6F: [0x00:"No precise diagnosis"],
|
||||
0x90: [0x00:"Success"] //No further qualification
|
||||
]
|
||||
|
||||
// Special cases - where sw2 isn't an error but contains a value
|
||||
if sw1 == 0x61 {
|
||||
return "SW2 indicates the number of response bytes still available - (\(sw2) bytes still available)"
|
||||
} else if sw1 == 0x64 {
|
||||
return "State of non-volatile memory unchanged (SW2=00, other values are RFU)"
|
||||
} else if sw1 == 0x6C {
|
||||
return "Wrong length Le: SW2 indicates the exact length - (exact length :\(sw2))"
|
||||
}
|
||||
|
||||
if let dict = errors[sw1], let errorMsg = dict[sw2] {
|
||||
return errorMsg
|
||||
}
|
||||
|
||||
return "Unknown error - sw1: 0x\(binToHexRep(sw1)), sw2 - 0x\(binToHexRep(sw2)) "
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
415
app/ios/NFCPassportReader/Utils.swift
Normal file
415
app/ios/NFCPassportReader/Utils.swift
Normal file
@@ -0,0 +1,415 @@
|
||||
import Foundation
|
||||
import CommonCrypto
|
||||
import CryptoTokenKit
|
||||
|
||||
#if canImport(CryptoKit)
|
||||
import CryptoKit
|
||||
#endif
|
||||
|
||||
private extension UInt8 {
|
||||
var hexString: String {
|
||||
let string = String(self, radix: 16)
|
||||
return (self < 16 ? "0" + string : string)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension FileManager {
|
||||
static var documentDir : URL {
|
||||
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
}
|
||||
}
|
||||
|
||||
extension StringProtocol {
|
||||
subscript(bounds: CountableClosedRange<Int>) -> SubSequence {
|
||||
let start = index(startIndex, offsetBy: bounds.lowerBound)
|
||||
let end = index(start, offsetBy: bounds.count)
|
||||
return self[start..<end]
|
||||
}
|
||||
|
||||
subscript(bounds: CountableRange<Int>) -> SubSequence {
|
||||
let start = index(startIndex, offsetBy: bounds.lowerBound)
|
||||
let end = index(start, offsetBy: bounds.count)
|
||||
return self[start..<end]
|
||||
}
|
||||
|
||||
func index(of string: Self, options: String.CompareOptions = []) -> Index? {
|
||||
return range(of: string, options: options)?.lowerBound
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public func binToHexRep( _ val : [UInt8], asArray : Bool = false ) -> String {
|
||||
var string = asArray ? "[" : ""
|
||||
for x in val {
|
||||
if asArray {
|
||||
string += String(format:"0x%02x, ", x )
|
||||
|
||||
} else {
|
||||
string += String(format:"%02x", x )
|
||||
}
|
||||
}
|
||||
string += asArray ? "]" : ""
|
||||
return asArray ? string : string.uppercased()
|
||||
}
|
||||
|
||||
public func binToHexRep( _ val : UInt8 ) -> String {
|
||||
let string = String(format:"%02x", val ).uppercased()
|
||||
return string
|
||||
}
|
||||
|
||||
public func binToHex( _ val: UInt8 ) -> Int {
|
||||
let hexRep = String(format:"%02X", val)
|
||||
return Int(hexRep, radix:16)!
|
||||
}
|
||||
|
||||
public func binToHex( _ val: [UInt8] ) -> UInt64 {
|
||||
let hexVal = UInt64(binToHexRep(val), radix:16)!
|
||||
return hexVal
|
||||
}
|
||||
|
||||
public func binToHex( _ val: ArraySlice<UInt8> ) -> UInt64 {
|
||||
return binToHex( [UInt8](val) )
|
||||
}
|
||||
|
||||
|
||||
public func hexToBin( _ val : UInt64 ) -> [UInt8] {
|
||||
let hexRep = String(format:"%lx", val)
|
||||
return hexRepToBin( hexRep)
|
||||
}
|
||||
|
||||
public func binToInt( _ val: ArraySlice<UInt8> ) -> Int {
|
||||
let hexVal = binToInt( [UInt8](val) )
|
||||
return hexVal
|
||||
}
|
||||
|
||||
public func binToInt( _ val: [UInt8] ) -> Int {
|
||||
let hexVal = Int(binToHexRep(val), radix:16)!
|
||||
return hexVal
|
||||
}
|
||||
|
||||
public func intToBin(_ data : Int, pad : Int = 2) -> [UInt8] {
|
||||
if pad == 2 {
|
||||
let hex = String(format:"%02x", data)
|
||||
return hexRepToBin(hex)
|
||||
} else {
|
||||
let hex = String(format:"%04x", data)
|
||||
return hexRepToBin(hex)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// 'AABB' --> \xaa\xbb'"""
|
||||
public func hexRepToBin(_ val : String) -> [UInt8] {
|
||||
var output : [UInt8] = []
|
||||
var x = 0
|
||||
while x < val.count {
|
||||
if x+2 <= val.count {
|
||||
output.append( UInt8(val[x ..< x + 2], radix:16)! )
|
||||
} else {
|
||||
output.append( UInt8(val[x ..< x+1], radix:16)! )
|
||||
|
||||
}
|
||||
x += 2
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
public func xor(_ kifd : [UInt8], _ response_kicc : [UInt8] ) -> [UInt8] {
|
||||
var kseed = [UInt8]()
|
||||
for i in 0 ..< kifd.count {
|
||||
kseed.append( kifd[i] ^ response_kicc[i] )
|
||||
}
|
||||
return kseed
|
||||
}
|
||||
|
||||
public func generateRandomUInt8Array( _ size: Int ) -> [UInt8] {
|
||||
|
||||
var ret : [UInt8] = []
|
||||
for _ in 0 ..< size {
|
||||
ret.append( UInt8(arc4random_uniform(UInt32(UInt8.max) + 1)) )
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
public func pad(_ toPad : [UInt8], blockSize : Int) -> [UInt8] {
|
||||
|
||||
var ret = toPad + [0x80]
|
||||
while ret.count % blockSize != 0 {
|
||||
ret.append(0x00)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
public func unpad( _ tounpad : [UInt8]) -> [UInt8] {
|
||||
var i = tounpad.count-1
|
||||
while tounpad[i] == 0x00 {
|
||||
i -= 1
|
||||
}
|
||||
|
||||
if tounpad[i] == 0x80 {
|
||||
return [UInt8](tounpad[0..<i])
|
||||
} else {
|
||||
// no padding
|
||||
return tounpad
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func mac(algoName: SecureMessagingSupportedAlgorithms, key : [UInt8], msg : [UInt8]) -> [UInt8] {
|
||||
if algoName == .DES {
|
||||
return desMAC(key: key, msg: msg)
|
||||
} else {
|
||||
return aesMAC(key: key, msg: msg)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func desMAC(key : [UInt8], msg : [UInt8]) -> [UInt8]{
|
||||
|
||||
let size = msg.count / 8
|
||||
var y : [UInt8] = [0,0,0,0,0,0,0,0]
|
||||
|
||||
Log.verbose("Calc mac" )
|
||||
for i in 0 ..< size {
|
||||
let tmp = [UInt8](msg[i*8 ..< i*8+8])
|
||||
Log.verbose("x\(i): \(binToHexRep(tmp))" )
|
||||
y = DESEncrypt(key: [UInt8](key[0..<8]), message: tmp, iv: y)
|
||||
Log.verbose("y\(i): \(binToHexRep(y))" )
|
||||
}
|
||||
|
||||
Log.verbose("y: \(binToHexRep(y))" )
|
||||
Log.verbose("bkey: \(binToHexRep([UInt8](key[8..<16])))" )
|
||||
Log.verbose("akey: \(binToHexRep([UInt8](key[0..<8])))" )
|
||||
let iv : [UInt8] = [0,0,0,0,0,0,0,0]
|
||||
let b = DESDecrypt(key: [UInt8](key[8..<16]), message: y, iv: iv, options:UInt32(kCCOptionECBMode))
|
||||
Log.verbose( "b: \(binToHexRep(b))" )
|
||||
let a = DESEncrypt(key: [UInt8](key[0..<8]), message: b, iv: iv, options:UInt32(kCCOptionECBMode))
|
||||
Log.verbose( "a: \(binToHexRep(a))" )
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func aesMAC( key: [UInt8], msg : [UInt8] ) -> [UInt8] {
|
||||
let mac = OpenSSLUtils.generateAESCMAC( key: key, message:msg )
|
||||
return mac
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func wrapDO( b : UInt8, arr : [UInt8] ) -> [UInt8] {
|
||||
let tag = TKBERTLVRecord(tag: TKTLVTag(b), value: Data(arr))
|
||||
let result = [UInt8](tag.data)
|
||||
return result;
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func unwrapDO( tag : UInt8, wrappedData : [UInt8]) throws -> [UInt8] {
|
||||
guard let rec = TKBERTLVRecord(from: Data(wrappedData)),
|
||||
rec.tag == tag else {
|
||||
throw NFCPassportReaderError.InvalidASN1Value
|
||||
}
|
||||
return [UInt8](rec.value);
|
||||
}
|
||||
|
||||
|
||||
public func intToBytes( val: Int, removePadding:Bool) -> [UInt8] {
|
||||
if val == 0 {
|
||||
return [0]
|
||||
}
|
||||
var data = withUnsafeBytes(of: val.bigEndian, Array.init)
|
||||
|
||||
if removePadding {
|
||||
// Remove initial 0 bytes
|
||||
for i in 0 ..< data.count {
|
||||
if data[i] != 0 {
|
||||
data = [UInt8](data[i...])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func oidToBytes(oid : String, replaceTag : Bool) -> [UInt8] {
|
||||
var encOID = OpenSSLUtils.asn1EncodeOID(oid: oid)
|
||||
|
||||
if replaceTag {
|
||||
// Replace tag (0x06) with 0x80
|
||||
encOID[0] = 0x80
|
||||
}
|
||||
return encOID
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Take an asn.1 length, and return a couple with the decoded length in hexa and the total length of the encoding (1,2 or 3 bytes)
|
||||
///
|
||||
/// Using Basic Encoding Rules (BER):
|
||||
/// If the first byte is <= 0x7F (0-127), then this is the total length of the data
|
||||
/// If the first byte is 0x81 then the length is the value of the next byte
|
||||
/// If the first byte is 0x82 then the length is the value of the next two bytes
|
||||
/// If the first byte is 0x80 then the length is indefinite (never seen this and not sure exactle what it means)
|
||||
/// e.g.
|
||||
/// if the data was 0x02, 0x11, 0x12, then the amount of data we have to read is two bytes, and the actual data is [0x11, 0x12]
|
||||
/// If the length was 0x81,0x80,....... then we know that the data length is contained in the next byte - 0x80 (128), so the amount of data to read is 128 bytes
|
||||
/// If the length was 0x82,0x01,0x01,....... then we know that the data length is contained in the next 2 bytes - 0x01, 0x01 (257) so the amount of data to read is 257 bytes
|
||||
///
|
||||
/// @param data: A length value encoded in the asn.1 format.
|
||||
/// @type data: A binary string.
|
||||
/// @return: A tuple with the decoded hexa length and the length of the asn.1 encoded value.
|
||||
/// @raise asn1Exception: If the parameter does not follow the asn.1 notation.
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func asn1Length( _ data: ArraySlice<UInt8> ) throws -> (Int, Int) {
|
||||
return try asn1Length( Array(data) )
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func asn1Length(_ data : [UInt8]) throws -> (Int, Int) {
|
||||
if data[0] < 0x80 {
|
||||
return (Int(binToHex(data[0])), 1)
|
||||
}
|
||||
if data[0] == 0x81 {
|
||||
return (Int(binToHex(data[1])), 2)
|
||||
}
|
||||
if data[0] == 0x82 {
|
||||
let val = binToHex([UInt8](data[1..<3]))
|
||||
return (Int(val), 3)
|
||||
}
|
||||
|
||||
throw NFCPassportReaderError.CannotDecodeASN1Length
|
||||
|
||||
}
|
||||
|
||||
/// Convert a length to asn.1 format
|
||||
/// @param data: The value to encode in asn.1
|
||||
/// @type data: An integer (hexa)
|
||||
/// @return: The asn.1 encoded value
|
||||
/// @rtype: A binary string
|
||||
/// @raise asn1Exception: If the parameter is too big, must be >= 0 and <= FFFF
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func toAsn1Length(_ data : Int) throws -> [UInt8] {
|
||||
if data < 0x80 {
|
||||
return hexRepToBin(String(format:"%02x", data))
|
||||
}
|
||||
if data >= 0x80 && data <= 0xFF {
|
||||
return [0x81] + hexRepToBin( String(format:"%02x",data))
|
||||
}
|
||||
if data >= 0x0100 && data <= 0xFFFF {
|
||||
return [0x82] + hexRepToBin( String(format:"%04x",data))
|
||||
}
|
||||
|
||||
throw NFCPassportReaderError.InvalidASN1Value
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// This function calculates a Hash of the input data based on the input algorithm
|
||||
/// @param data: a byte array of data
|
||||
/// @param hashAlgorithm: the hash algorithm to be used - supported ones are SHA1, SHA224, SHA256, SHA384 and SHA512
|
||||
/// Currently specifying any others return empty array
|
||||
/// @return: A hash of the data
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func calcHash( data: [UInt8], hashAlgorithm: String ) throws -> [UInt8] {
|
||||
var ret : [UInt8] = []
|
||||
|
||||
let hashAlgorithm = hashAlgorithm.lowercased()
|
||||
if hashAlgorithm == "sha1" {
|
||||
ret = calcSHA1Hash(data)
|
||||
} else if hashAlgorithm == "sha224" {
|
||||
ret = calcSHA224Hash(data)
|
||||
} else if hashAlgorithm == "sha256" {
|
||||
ret = calcSHA256Hash(data)
|
||||
} else if hashAlgorithm == "sha384" {
|
||||
ret = calcSHA384Hash(data)
|
||||
} else if hashAlgorithm == "sha512" {
|
||||
ret = calcSHA512Hash(data)
|
||||
} else {
|
||||
throw NFCPassportReaderError.InvalidHashAlgorithmSpecified
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
|
||||
/// This function calculates a SHA1 Hash of the input data
|
||||
/// @param data: a byte array of data
|
||||
/// @return: A SHA1 hash of the data
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func calcSHA1Hash( _ data: [UInt8] ) -> [UInt8] {
|
||||
#if canImport(CryptoKit)
|
||||
var sha1 = Insecure.SHA1()
|
||||
sha1.update(data: data)
|
||||
let hash = sha1.finalize()
|
||||
|
||||
return Array(hash)
|
||||
#else
|
||||
fatalError("Couldn't import CryptoKit")
|
||||
#endif
|
||||
}
|
||||
|
||||
/// This function calculates a SHA224 Hash of the input data
|
||||
/// @param data: a byte array of data
|
||||
/// @return: A SHA224 hash of the data
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func calcSHA224Hash( _ data: [UInt8] ) -> [UInt8] {
|
||||
|
||||
var digest = [UInt8](repeating: 0, count:Int(CC_SHA224_DIGEST_LENGTH))
|
||||
|
||||
data.withUnsafeBytes {
|
||||
_ = CC_SHA224($0.baseAddress, CC_LONG(data.count), &digest)
|
||||
}
|
||||
return digest
|
||||
}
|
||||
|
||||
/// This function calculates a SHA256 Hash of the input data
|
||||
/// @param data: a byte array of data
|
||||
/// @return: A SHA256 hash of the data
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func calcSHA256Hash( _ data: [UInt8] ) -> [UInt8] {
|
||||
#if canImport(CryptoKit)
|
||||
var sha256 = SHA256()
|
||||
sha256.update(data: data)
|
||||
let hash = sha256.finalize()
|
||||
|
||||
return Array(hash)
|
||||
#else
|
||||
fatalError("Couldn't import CryptoKit")
|
||||
#endif
|
||||
}
|
||||
|
||||
/// This function calculates a SHA512 Hash of the input data
|
||||
/// @param data: a byte array of data
|
||||
/// @return: A SHA512 hash of the data
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func calcSHA512Hash( _ data: [UInt8] ) -> [UInt8] {
|
||||
#if canImport(CryptoKit)
|
||||
var sha512 = SHA512()
|
||||
sha512.update(data: data)
|
||||
let hash = sha512.finalize()
|
||||
|
||||
return Array(hash)
|
||||
#else
|
||||
fatalError("Couldn't import CryptoKit")
|
||||
#endif
|
||||
}
|
||||
|
||||
/// This function calculates a SHA384 Hash of the input data
|
||||
/// @param data: a byte array of data
|
||||
/// @return: A SHA384 hash of the data
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public func calcSHA384Hash( _ data: [UInt8] ) -> [UInt8] {
|
||||
#if canImport(CryptoKit)
|
||||
var sha384 = SHA384()
|
||||
sha384.update(data: data)
|
||||
let hash = sha384.finalize()
|
||||
|
||||
return Array(hash)
|
||||
#else
|
||||
fatalError("Couldn't import CryptoKit")
|
||||
#endif
|
||||
}
|
||||
|
||||
161
app/ios/NFCPassportReader/X509Wrapper.swift
Normal file
161
app/ios/NFCPassportReader/X509Wrapper.swift
Normal file
@@ -0,0 +1,161 @@
|
||||
import OpenSSL
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public enum CertificateType {
|
||||
case documentSigningCertificate
|
||||
case issuerSigningCertificate
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public enum CertificateItem : String {
|
||||
case fingerprint = "Certificate fingerprint"
|
||||
case issuerName = "Issuer"
|
||||
case subjectName = "Subject"
|
||||
case serialNumber = "Serial number"
|
||||
case signatureAlgorithm = "Signature algorithm"
|
||||
case publicKeyAlgorithm = "Public key algorithm"
|
||||
case notBefore = "Valid from"
|
||||
case notAfter = "Valid to"
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
public class X509Wrapper {
|
||||
public let cert : OpaquePointer
|
||||
|
||||
public init?( with cert: OpaquePointer? ) {
|
||||
guard let cert = cert else { return nil }
|
||||
|
||||
self.cert = X509_dup(cert)
|
||||
}
|
||||
|
||||
public func getItemsAsDict() -> [CertificateItem:String] {
|
||||
var item = [CertificateItem:String]()
|
||||
if let fingerprint = self.getFingerprint() {
|
||||
item[.fingerprint] = fingerprint
|
||||
}
|
||||
if let issuerName = self.getIssuerName() {
|
||||
item[.issuerName] = issuerName
|
||||
|
||||
}
|
||||
if let subjectName = self.getSubjectName() {
|
||||
item[.subjectName] = subjectName
|
||||
}
|
||||
if let serialNr = self.getSerialNumber() {
|
||||
item[.serialNumber] = serialNr
|
||||
}
|
||||
if let signatureAlgorithm = self.getSignatureAlgorithm() {
|
||||
item[.signatureAlgorithm] = signatureAlgorithm
|
||||
}
|
||||
if let publicKeyAlgorithm = self.getPublicKeyAlgorithm() {
|
||||
item[.publicKeyAlgorithm] = publicKeyAlgorithm
|
||||
}
|
||||
if let notBefore = self.getNotBeforeDate() {
|
||||
item[.notBefore] = notBefore
|
||||
}
|
||||
if let notAfter = self.getNotAfterDate() {
|
||||
item[.notAfter] = notAfter
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
public func certToPEM() -> String {
|
||||
return OpenSSLUtils.X509ToPEM( x509:cert )
|
||||
}
|
||||
|
||||
public func getFingerprint( ) -> String? {
|
||||
let fdig = EVP_sha1();
|
||||
|
||||
var n : UInt32 = 0
|
||||
let md = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(EVP_MAX_MD_SIZE))
|
||||
defer { md.deinitialize(count: Int(EVP_MAX_MD_SIZE)); md.deallocate() }
|
||||
|
||||
X509_digest(cert, fdig, md, &n)
|
||||
let arr = UnsafeMutableBufferPointer(start: md, count: Int(n)).map({ binToHexRep($0) }).joined(separator: ":")
|
||||
return arr
|
||||
}
|
||||
|
||||
public func getNotBeforeDate() -> String? {
|
||||
var notBefore : String?
|
||||
if let val = X509_get0_notBefore(cert) {
|
||||
notBefore = ASN1TimeToString( val )
|
||||
}
|
||||
return notBefore
|
||||
|
||||
}
|
||||
|
||||
public func getNotAfterDate() -> String? {
|
||||
var notAfter : String?
|
||||
if let val = X509_get0_notAfter(cert) {
|
||||
notAfter = ASN1TimeToString( val )
|
||||
}
|
||||
return notAfter
|
||||
}
|
||||
|
||||
public func getSerialNumber() -> String? {
|
||||
let serialNr = String( ASN1_INTEGER_get(X509_get_serialNumber(cert)), radix:16, uppercase: true )
|
||||
return serialNr
|
||||
}
|
||||
|
||||
public func getSignatureAlgorithm() -> String? {
|
||||
let algor = X509_get0_tbs_sigalg(cert);
|
||||
let algo = getAlgorithm( algor?.pointee.algorithm )
|
||||
return algo
|
||||
}
|
||||
|
||||
public func getPublicKeyAlgorithm() -> String? {
|
||||
let pubKey = X509_get_X509_PUBKEY(cert)
|
||||
var ptr : OpaquePointer?
|
||||
X509_PUBKEY_get0_param(&ptr, nil, nil, nil, pubKey)
|
||||
let algo = getAlgorithm(ptr)
|
||||
return algo
|
||||
}
|
||||
|
||||
public func getIssuerName() -> String? {
|
||||
return getName(for: X509_get_issuer_name(cert))
|
||||
}
|
||||
|
||||
public func getSubjectName() -> String? {
|
||||
return getName(for: X509_get_subject_name(cert))
|
||||
}
|
||||
|
||||
private func getName( for name: OpaquePointer? ) -> String? {
|
||||
guard let name = name else { return nil }
|
||||
|
||||
var issuer: String = ""
|
||||
|
||||
guard let out = BIO_new( BIO_s_mem()) else { return nil }
|
||||
defer { BIO_free(out) }
|
||||
|
||||
X509_NAME_print_ex(out, name, 0, UInt(ASN1_STRFLGS_ESC_2253 |
|
||||
ASN1_STRFLGS_ESC_CTRL |
|
||||
ASN1_STRFLGS_ESC_MSB |
|
||||
ASN1_STRFLGS_UTF8_CONVERT |
|
||||
ASN1_STRFLGS_DUMP_UNKNOWN |
|
||||
ASN1_STRFLGS_DUMP_DER | XN_FLAG_SEP_COMMA_PLUS |
|
||||
XN_FLAG_DN_REV |
|
||||
XN_FLAG_FN_SN |
|
||||
XN_FLAG_DUMP_UNKNOWN_FIELDS))
|
||||
issuer = OpenSSLUtils.bioToString(bio: out)
|
||||
|
||||
return issuer
|
||||
}
|
||||
|
||||
private func getAlgorithm( _ algo: OpaquePointer? ) -> String? {
|
||||
guard let algo = algo else { return nil }
|
||||
let len = OBJ_obj2nid(algo)
|
||||
var algoString : String? = nil
|
||||
if let sa = OBJ_nid2ln(len) {
|
||||
algoString = String(cString: sa )
|
||||
}
|
||||
return algoString
|
||||
}
|
||||
|
||||
private func ASN1TimeToString( _ date: UnsafePointer<ASN1_TIME> ) -> String? {
|
||||
guard let b = BIO_new(BIO_s_mem()) else { return nil }
|
||||
defer { BIO_free(b) }
|
||||
|
||||
ASN1_TIME_print(b, date)
|
||||
return OpenSSLUtils.bioToString(bio: b)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user