diff --git a/Sources/Auxilary/Helper.swift b/Sources/Auxilary/Helper.swift new file mode 100644 index 0000000..6e9a339 --- /dev/null +++ b/Sources/Auxilary/Helper.swift @@ -0,0 +1,29 @@ +// +// Helper.swift +// DCCRevocation +// +// Created by Igor Khomiak on 24.01.2022. +// + +import Foundation + +public class Helper { + + public static func convertToBase64url(base64: String) -> String { + let base64url = base64 + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + return base64url + } + + public static func convertToBase64(base64url: String) -> String { + var base64 = base64url + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + if base64.count % 4 != 0 { + base64.append(String(repeating: "=", count: 4 - base64.count % 4)) + } + return base64 + } +} diff --git a/Sources/Auxilary/SecureKeyChain.swift b/Sources/Auxilary/SecureKeyChain.swift new file mode 100644 index 0000000..885b162 --- /dev/null +++ b/Sources/Auxilary/SecureKeyChain.swift @@ -0,0 +1,71 @@ +// +/*- + * ---license-start + * eu-digital-green-certificates / dgca-wallet-app-ios + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ +// +// SecureKeyChain.swift +// DGCAWallet +// +// Created by Igor Khomiak on 18.11.2021. +// +// see https://stackoverflow.com/a/37539998/1694526 + + +import Foundation +import Security + +public class SecureKeyChain { + @discardableResult + public class func save(key: String, data: Data) -> OSStatus { + let query = [ + kSecClass as String : kSecClassGenericPassword as String, + kSecAttrAccount as String : key, + kSecValueData as String : data ] as [String : Any] + + SecItemDelete(query as CFDictionary) + + return SecItemAdd(query as CFDictionary, nil) + } + + public class func load(key: String) -> Data? { + let query = [ + kSecClass as String : kSecClassGenericPassword, + kSecAttrAccount as String : key, + kSecReturnData as String : kCFBooleanTrue!, + kSecMatchLimit as String : kSecMatchLimitOne ] as [String : Any] + + var dataTypeRef: AnyObject? = nil + + let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) + + if status == noErr { + return dataTypeRef as! Data? + } else { + return nil + } + } + + public class func createUniqueID() -> String { + let uuid: CFUUID = CFUUIDCreate(nil) + let cfStr: CFString = CFUUIDCreateString(nil, uuid) + + let swiftString: String = cfStr as String + return swiftString + } +} diff --git a/Sources/Auxilary/SyncArray.swift b/Sources/Auxilary/SyncArray.swift new file mode 100644 index 0000000..cc3e5fd --- /dev/null +++ b/Sources/Auxilary/SyncArray.swift @@ -0,0 +1,282 @@ +// +/*- + * ---license-start + * eu-digital-green-certificates / dgca-wallet-app-ios + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ +// +// SyncArray.swift +// DGCAWallet +// +// Created by Igor Khomiak on 19.11.2021. +// + + +import Foundation + +/// A thread-safe array. +public class SyncArray { + + fileprivate let queue = DispatchQueue(label: "Wallet.Element.SyncArray", attributes: .concurrent) + fileprivate var array = [Element]() + + public init() { + } +} + +// MARK: - Properties +public extension SyncArray { + + var resultArray: [Element] { + return array + } + + /// The first element of the collection. + var first: Element? { + var result: Element? + queue.sync { result = self.array.first } + return result + } + + /// The last element of the collection. + var last: Element? { + var result: Element? + queue.sync { result = self.array.last } + return result + } + + /// The number of elements in the array. + var count: Int { + var result = 0 + queue.sync { result = self.array.count } + return result + } + + /// A Boolean value indicating whether the collection is empty. + var isEmpty: Bool { + var result = false + queue.sync { result = self.array.isEmpty } + return result + } + + /// A textual representation of the array and its elements. + var description: String { + var result = "" + queue.sync { result = self.array.description } + return result + } +} + +// MARK: - Immutable +public extension SyncArray { + /// Returns the first element of the sequence that satisfies the given predicate or nil if no such element is found. + /// + /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. + /// - Returns: The first match or nil if there was no match. + func first(where predicate: (Element) -> Bool) -> Element? { + var result: Element? + queue.sync { result = self.array.first(where: predicate) } + return result + } + + /// Returns an array containing, in order, the elements of the sequence that satisfy the given predicate. + /// - Returns: An array of the elements that includeElement allowed. + func filter(_ isIncluded: (Element) -> Bool) -> [Element] { + var result = [Element]() + queue.sync { result = self.array.filter(isIncluded) } + return result + } + + /// Returns the first index in which an element of the collection satisfies the given predicate. + /// + /// - Parameter predicate: A closure that takes an element as its argument and returns a Boolean value that indicates whether the passed element represents a match. + /// - Returns: The index of the first element for which predicate returns true. If no elements in the collection satisfy the given predicate, returns nil. + func index(where predicate: (Element) -> Bool) -> Int? { + var result: Int? + queue.sync { result = self.array.firstIndex(where: predicate) } + return result + } + + /// Returns the elements of the collection, sorted using the given predicate as the comparison between elements. + /// + /// - Parameter areInIncreasingOrder: A predicate that returns true if its first argument should be ordered before its second argument; otherwise, false. + /// - Returns: A sorted array of the collection’s elements. + func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element] { + var result = [Element]() + queue.sync { result = self.array.sorted(by: areInIncreasingOrder) } + return result + } + + /// Returns an array containing the non-nil results of calling the given transformation with each element of this sequence. + /// + /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value. + /// - Returns: An array of the non-nil results of calling transform with each element of the sequence. + func flatMap(_ transform: (Element) -> ElementOfResult?) -> [ElementOfResult] { + var result = [ElementOfResult]() + queue.sync { result = self.array.compactMap(transform) } + return result + } + + /// Calls the given closure on each element in the sequence in the same order as a for-in loop. + /// + /// - Parameter body: A closure that takes an element of the sequence as a parameter. + func forEach(_ body: (Element) -> Void) { + queue.sync { self.array.forEach(body) } + } + + /// Returns a Boolean value indicating whether the sequence contains an element that satisfies the given predicate. + /// + /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element represents a match. + /// - Returns: true if the sequence contains an element that satisfies predicate; otherwise, false. + func contains(where predicate: (Element) -> Bool) -> Bool { + var result = false + queue.sync { result = self.array.contains(where: predicate) } + return result + } +} + +// MARK: - Mutable +public extension SyncArray { + + /// Adds a new element at the end of the array. + /// + /// - Parameter element: The element to append to the array. + func append( _ element: Element) { + queue.async(flags: .barrier) { + self.array.append(element) + } + } + + /// Adds a new element at the end of the array. + /// + /// - Parameter element: The element to append to the array. + func append( _ elements: [Element]) { + queue.async(flags: .barrier) { + self.array += elements + } + } + + /// Inserts a new element at the specified position. + /// + /// - Parameters: + /// - element: The new element to insert into the array. + /// - index: The position at which to insert the new element. + func insert( _ element: Element, at index: Int) { + queue.async(flags: .barrier) { + self.array.insert(element, at: index) + } + } + + /// Removes and returns the element at the specified position. + /// + /// - Parameters: + /// - index: The position of the element to remove. + /// - completion: The handler with the removed element. + func remove(at index: Int, completion: ((Element) -> Void)? = nil) { + queue.async(flags: .barrier) { + let element = self.array.remove(at: index) + + DispatchQueue.main.async { + completion?(element) + } + } + } + + /// Removes and returns the element at the specified position. + /// + /// - Parameters: + /// - predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. + /// - completion: The handler with the removed element. + func remove(where predicate: @escaping (Element) -> Bool, completion: ((Element) -> Void)? = nil) { + queue.async(flags: .barrier) { + guard let index = self.array.firstIndex(where: predicate) else { return } + let element = self.array.remove(at: index) + + DispatchQueue.main.async { + completion?(element) + } + } + } + + /// Removes all elements from the array. + /// + /// - Parameter completion: The handler with the removed elements. + func removeAll(completion: (([Element]) -> Void)? = nil) { + queue.async(flags: .barrier) { + let elements = self.array + self.array.removeAll() + + DispatchQueue.main.async { + completion?(elements) + } + } + } +} + +public extension SyncArray { + + /// Accesses the element at the specified position if it exists. + /// + /// - Parameter index: The position of the element to access. + /// - Returns: optional element if it exists. + subscript(index: Int) -> Element? { + get { + var result: Element? + + queue.sync { + guard self.array.startIndex.. Bool { + var result = false + queue.sync { result = self.array.contains(element) } + return result + } +} + +// MARK: - Infix operators +public extension SyncArray { + + static func +=(left: inout SyncArray, right: Element) { + left.append(right) + } + + static func +=(left: inout SyncArray, right: [Element]) { + left.append(right) + } +} diff --git a/Sources/Auxilary/SyncDict.swift b/Sources/Auxilary/SyncDict.swift new file mode 100644 index 0000000..31b7942 --- /dev/null +++ b/Sources/Auxilary/SyncDict.swift @@ -0,0 +1,86 @@ +// +// File.swift +// +// +// Created by Igor Khomiak on 19.01.2022. +// + +import Foundation + +/// A thread-safe array. +public class SyncDict { + + fileprivate let queue = DispatchQueue(label: "Wallet.Element.SyncDict", attributes: .concurrent) + fileprivate var dict = Dictionary() + + public init() { + } +} + +// MARK: - Properties +public extension SyncDict { + + var resultDict: Dictionary { + return dict + } + + /// The number of elements in the array. + var count: Int { + var result = 0 + queue.sync { result = self.dict.count } + return result + } + + /// A Boolean value indicating whether the collection is empty. + var isEmpty: Bool { + var result = false + queue.sync { result = self.dict.isEmpty } + return result + } + + /// A textual representation of the array and its elements. + var description: String { + var result = "" + queue.sync { result = self.dict.description } + return result + } +} + +// MARK: - Mutable +public extension SyncDict { + + /// Adds a new element at the end of the array. + /// - Parameter element: The element to append to the array. + func append( _ element: [String : Element]) { + queue.async(flags: .barrier) { + for (key, value) in element { + self.dict.updateValue(value, forKey: key) + } + } + } + + func update(value: Element, forKey key: String) { + queue.async(flags: .barrier) { + self.dict.updateValue(value, forKey: key) + } + } + + func value(forKey key: String) -> Element? { + var result: Element? + queue.sync { result = dict[key] } + return result + } + + /// Removes all elements from the array. + /// - Parameter completion: The handler with the removed elements. + func removeAll(completion: (([String: Element]) -> Void)? = nil) { + queue.async(flags: .barrier) { + let elements = self.dict + self.dict.removeAll() + + DispatchQueue.main.async { + completion?(elements) + } + } + } +} diff --git a/Sources/Services/ZLib.swift b/Sources/Auxilary/ZLib.swift similarity index 100% rename from Sources/Services/ZLib.swift rename to Sources/Auxilary/ZLib.swift diff --git a/Sources/Components/SquareViewFinder.swift b/Sources/Components/SquareViewFinder.swift deleted file mode 100644 index 634bbe8..0000000 --- a/Sources/Components/SquareViewFinder.swift +++ /dev/null @@ -1,80 +0,0 @@ -/*- - * ---license-start - * eu-digital-green-certificates / dgca-app-core-ios - * --- - * Copyright (C) 2021 T-Systems International GmbH and all other contributors - * --- - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ---license-end - */ -// -// SquareViewFinder.swift -// -// -// Created by Yannick Spreen on 4/29/21. -// - -#if os(iOS) -import UIKit - -public class SquareViewFinder { - public static func newView(from view: UIView? = nil) -> UIView { - let view = view ?? UIView(frame: .zero) - view.frame = .zero - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - view.isUserInteractionEnabled = false - return view - } - - public static func create(from controller: UIViewController) { - guard let view = controller.view else { return } - - let guide = newView() - let square = newView() - let imgTopRight = newView(from: UIImageView(image: UIImage(named: "cam_top_right"))) - let imgTopLeft = newView(from: UIImageView(image: UIImage(named: "cam_top_left"))) - let imgBottomRight = newView(from: UIImageView(image: UIImage(named: "cam_bottom_right"))) - let imgBottomLeft = newView(from: UIImageView(image: UIImage(named: "cam_bottom_left"))) - let constraints = [ - guide.leadingAnchor.constraint(equalTo: view.leadingAnchor), - guide.trailingAnchor.constraint(equalTo: view.trailingAnchor), - guide.topAnchor.constraint(equalTo: view.topAnchor), - guide.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.53), - square.bottomAnchor.constraint(equalTo: guide.bottomAnchor), - square.centerXAnchor.constraint(equalTo: guide.centerXAnchor), - square.widthAnchor.constraint(equalToConstant: 250), - square.heightAnchor.constraint(equalToConstant: 250), - imgTopRight.topAnchor.constraint(equalTo: square.topAnchor), - imgTopRight.rightAnchor.constraint(equalTo: square.rightAnchor), - imgBottomRight.bottomAnchor.constraint(equalTo: square.bottomAnchor), - imgBottomRight.rightAnchor.constraint(equalTo: square.rightAnchor), - imgBottomLeft.bottomAnchor.constraint(equalTo: square.bottomAnchor), - imgBottomLeft.leftAnchor.constraint(equalTo: square.leftAnchor), - imgTopLeft.topAnchor.constraint(equalTo: square.topAnchor), - imgTopLeft.leftAnchor.constraint(equalTo: square.leftAnchor) - ] - for child in [ - guide, - square, - imgTopRight, - imgTopLeft, - imgBottomRight, - imgBottomLeft - ] { - view.addSubview(child) - } - NSLayoutConstraint.activate(constraints) - } -} -#endif diff --git a/Sources/Extensions/Date.swift b/Sources/Extensions/Date.swift index bb2003f..f3d0ef3 100644 --- a/Sources/Extensions/Date.swift +++ b/Sources/Extensions/Date.swift @@ -62,31 +62,29 @@ extension Date { Date.dateTimeFormatter.string(from: self) } - init?(isoString: String) { - guard let date = Date.isoFormatter.date(from: isoString) else { - return nil - } - self = date - } - - init?(dateString: String) { - if let date = Date.dateFormatter.date(from: dateString) { - self = date - } else if let date = Date.dateFormatterYM.date(from: dateString) { - self = date - } else if let date = Date.dateFormatterY.date(from: dateString) { - self = date - } else if let date = Date.dateFormatterFull.date(from: dateString) { - self = date - } else if let date = Date.dateFormatterOffset.date(from: dateString) { - self = date - } else if let date = Date.dateFormatterFractional.date(from: dateString) { + public init?(isoString: String) { + guard let date = Date.isoFormatter.date(from: isoString) else { return nil } self = date - } else { - return nil } - } + public init?(dateString: String) { + if let date = Date.dateFormatter.date(from: dateString) { + self = date + } else if let date = Date.dateFormatterYM.date(from: dateString) { + self = date + } else if let date = Date.dateFormatterY.date(from: dateString) { + self = date + } else if let date = Date.dateFormatterFull.date(from: dateString) { + self = date + } else if let date = Date.dateFormatterOffset.date(from: dateString) { + self = date + } else if let date = Date.dateFormatterFractional.date(from: dateString) { + self = date + } else { + return nil + } + } + public init?(rfc3339DateTimeString str: String) { var str = str let rfc3339DateTimeFormatter = DateFormatter() diff --git a/Sources/Extensions/String.swift b/Sources/Extensions/String.swift index f65b3de..109d18f 100644 --- a/Sources/Extensions/String.swift +++ b/Sources/Extensions/String.swift @@ -67,5 +67,4 @@ public extension String { func padStart(length maxLength: Int, pad: String = " ") -> String { return generatePadString(length: maxLength, pad: pad).map { $0 + self } ?? self } - } diff --git a/Sources/Models/CoreManager.swift b/Sources/Models/CoreManager.swift index a7c27ad..04a2661 100644 --- a/Sources/Models/CoreManager.swift +++ b/Sources/Models/CoreManager.swift @@ -32,14 +32,12 @@ import AppKit public class CoreManager { public static var shared = CoreManager() - + lazy public var config = HCertConfig.default + public static var publicKeyEncoder: PublicKeyStorageDelegate? + #if os(iOS) - public static var cachedQrCodes = [String: UIImage]() + public static var cachedQrCodes = SyncDict() #else - public static var cachedQrCodes = [String: NSImage]() + public static var cachedQrCodes = SyncDict() #endif - - public static var publicKeyEncoder: PublicKeyStorageDelegate? - - lazy public var config = HCertConfig.default } diff --git a/Sources/Models/Enums.swift b/Sources/Models/Enums.swift index b4df5d8..6d11f66 100644 --- a/Sources/Models/Enums.swift +++ b/Sources/Models/Enums.swift @@ -56,9 +56,10 @@ public enum HCertType: String { } public enum HCertValidity { - case valid - case invalid - case ruleInvalid + case valid + case invalid + case ruleInvalid + case revocated } public let attributeKeys: [AttributeKey: [String]] = [ @@ -78,7 +79,7 @@ public enum InfoSectionStyle { } public enum RuleValidationResult: Int { - case error = 0 + case failed = 0 case passed case open } @@ -86,7 +87,7 @@ public enum RuleValidationResult: Int { public class ParseErrors { var errors: [ParseError] = [] } - + public enum ParseError { case base45 case prefix @@ -100,3 +101,9 @@ public enum CertificateParsingError: Error { case unknown case parsing(errors: [ParseError]) } + +public enum RevocationMode: String { + case point = "POINT" + case vector = "VECTOR" + case coordinate = "COORDINATE" +} diff --git a/Sources/Models/HCert+UIKit.swift b/Sources/Models/HCert+UIKit.swift index 14d7d35..1d2ae70 100644 --- a/Sources/Models/HCert+UIKit.swift +++ b/Sources/Models/HCert+UIKit.swift @@ -28,50 +28,25 @@ import UIKit extension HCert { - var qrCodeRendered: UIImage? { - CoreManager.cachedQrCodes[uvci] - } - public var qrCode: UIImage? { - return qrCodeRendered ?? renderQrCode() - } - - @discardableResult - func renderQrCode() -> UIImage? { - if let rendered = qrCodeRendered { - return rendered - } - let code = makeQrCode() - let lock = NSLock() - if let value = code { - lock.lock() - CoreManager.cachedQrCodes[uvci] = value - lock.unlock() + public var qrCode: UIImage? { + let codeRemdered = CoreManager.cachedQrCodes.value(forKey: uvci) + return codeRemdered ?? makeQrCode() } - return code - } - - func makeQrCode() -> UIImage? { - let data = fullPayloadString.data(using: String.Encoding.ascii) - - if let filter = CIFilter(name: "CIQRCodeGenerator") { - filter.setValue(data, forKey: "inputMessage") - let transform = CGAffineTransform(scaleX: 3, y: 3) - if let output = filter.outputImage?.transformed(by: transform) { - return UIImage(ciImage: output) + private func makeQrCode() -> UIImage? { + let data = fullPayloadString.data(using: String.Encoding.ascii) + if let filter = CIFilter(name: "CIQRCodeGenerator") { + filter.setValue(data, forKey: "inputMessage") + let transform = CGAffineTransform(scaleX: 3, y: 3) + + if let output = filter.outputImage?.transformed(by: transform) { + let codeImage = UIImage(ciImage: output) + CoreManager.cachedQrCodes.update(value: codeImage, forKey: uvci) + return codeImage + } } + return nil } - - return nil - } - - func prefetchCode() { - guard qrCodeRendered == nil else { return } - DispatchQueue.global(qos: .background).async { - self.renderQrCode() - } - } } - #endif diff --git a/Sources/Models/HCert.swift b/Sources/Models/HCert.swift index 52aaf71..6bbcff6 100644 --- a/Sources/Models/HCert.swift +++ b/Sources/Models/HCert.swift @@ -27,6 +27,7 @@ import Foundation import SwiftyJSON import JSONSchema +import SwiftCBOR public class HCert: Codable { public let fullPayloadString: String @@ -191,11 +192,6 @@ public class HCert: Codable { parsingErrors.append(.version(error: "Wrong EU_DGC Version!")) throw CertificateParsingError.parsing(errors: parsingErrors) } - #if os(iOS) - if CoreManager.shared.config.prefetchAllCodes { - prefetchCode() - } - #endif } private func get(_ attribute: AttributeKey) -> JSON { @@ -216,7 +212,7 @@ extension HCert { var bodyErrors = [ParseError]() let validation = try validate(bodyDict, schema: schema) validation.errors?.forEach { bodyErrors.append(.json(error: $0.description)) } - + #if DEBUG if CoreManager.shared.config.debugPrintJsonErrors { validation.errors?.forEach { print($0.description) } @@ -229,3 +225,58 @@ extension HCert { } } } + +// MARK: - Hashes for revocation search +extension HCert { + public var uvciHash: Data? { + if statement?.uvci != nil, + let uvciData = uvci.data(using: .utf8) { + return SHA256.sha256(data: uvciData) //.hexString + } else { + return nil + } + } + + public var countryCodeUvciHash: Data? { + if statement?.uvci != nil, + let countryCodeUvciData = (issCode + uvci).data(using: .utf8) { + return SHA256.sha256(data: countryCodeUvciData) //.hexString + } else { + return nil + } + } + + public var signatureHash: Data? { + guard var signatureBytesToHash = CBOR.unwrap(data: cborData)?.signatureBytes else { return nil } + + if isECDSASigned { + signatureBytesToHash = Array(signatureBytesToHash.prefix(32)) + } + + return SHA256.sha256(data: Data(signatureBytesToHash)) //.hexString + } + + private var isECDSASigned: Bool { + guard let cborHeader = CBOR.header(from: cborData), + let algorithmField = cborHeader[1] else { + return false + } + + let coseES256Algorithm = -7 + + return algorithmField == SwiftCBOR.CBOR(integerLiteral: coseES256Algorithm) + } +} + +public extension HCert { + func lookUp(mode: RevocationMode) -> CertLookUp { + switch mode { + case .point: + return CertLookUp(kid: kidStr, section: payloadString[0], x: "null", y: "null") + case .vector: + return CertLookUp(kid: kidStr, section: payloadString[1], x: payloadString[0], y: "null") + case .coordinate: + return CertLookUp(kid: kidStr, section: payloadString[2], x: payloadString[0], y: payloadString[1]) + } + } +} diff --git a/Sources/Models/SectionBuilder.swift b/Sources/Models/SectionBuilder.swift index e64a5d6..0f15543 100644 --- a/Sources/Models/SectionBuilder.swift +++ b/Sources/Models/SectionBuilder.swift @@ -18,12 +18,12 @@ * limitations under the License. * ---license-end */ -// +// // SectionBuilder.swift // DGCAVerifier -// +// // Created by Igor Khomiak on 18.10.2021. -// +// import Foundation @@ -33,154 +33,179 @@ public class SectionBuilder { private let validityState: ValidityState private let certificate: HCert - + public init(with cert: HCert, validity: ValidityState) { - self.certificate = cert - self.validityState = validity + self.certificate = cert + self.validityState = validity } - + public func makeSections(for appType: AppType) { - infoSection.removeAll() - switch appType { - case .verifier: - makeSectionsForVerifier() - - case .wallet: - switch certificate.certificateType { - case .vaccine: - makeSectionsForVaccine() - case .test: - makeSectionsForTest() - case .recovery: - makeSectionsForRecovery() - default: - makeSectionsForVerifier() + infoSection.removeAll() + switch appType { + case .verifier: + makeSectionsForVerifier() + + case .wallet: + switch certificate.certificateType { + case .vaccine: + makeSectionsForVaccine() + case .test: + makeSectionsForTest() + case .recovery: + makeSectionsForRecovery() + default: + makeSectionsForVerifier() + } } - } } - + public func makeSectionForRuleError(ruleSection: InfoSection, for appType: AppType) { let hSection = InfoSection(header: "Certificate Type".localized, content: certificate.certTypeString ) - infoSection += [hSection] + infoSection += [hSection] - guard validityState.isValid else { - let vSection = InfoSection(header: "Reason for Invalidity".localized, - content: validityState.validityFailures.joined(separator: " ")) - infoSection += [vSection] - return - } - - infoSection += [ruleSection] - switch appType { - case .verifier: - makeSectionsForVerifier(includeInvalidSection: false) - case .wallet: - switch certificate.certificateType { - case .vaccine: - makeSectionsForVaccine(includeInvalidSection: false) - case .test: - makeSectionsForTest() - case .recovery: - makeSectionsForRecovery(includeInvalidSection: false) - default: - makeSectionsForVerifier(includeInvalidSection: false) + guard validityState.revocationValidity != .revocated else { + let rSection = InfoSection(header: "Reason for Invalidity".localized, content: "Certificate was revoked".localized) + infoSection += [rSection] + return + } + + guard validityState.isValid else { + let vSection = InfoSection(header: "Reason for Invalidity".localized, + content: validityState.validityFailures.joined(separator: " ")) + infoSection += [vSection] + return + } + + infoSection += [ruleSection] + switch appType { + case .verifier: + makeSectionsForVerifier(includeInvalidSection: false) + case .wallet: + switch certificate.certificateType { + case .vaccine: + makeSectionsForVaccine(includeInvalidSection: false) + case .test: + makeSectionsForTest() + case .recovery: + makeSectionsForRecovery(includeInvalidSection: false) + default: + makeSectionsForVerifier(includeInvalidSection: false) + } } - } } // MARK: private section private func makeSectionsForVerifier(includeInvalidSection: Bool = true) { - if includeInvalidSection { - let hSection = InfoSection( header: "Certificate Type".localized, content: certificate.certTypeString ) - infoSection += [hSection] - if !validityState.isValid { - let vSection = InfoSection(header: "Reason for Invalidity".localized, - content: validityState.validityFailures.joined(separator: " ")) - infoSection += [vSection] - return + if includeInvalidSection { + let hSection = InfoSection( header: "Certificate Type".localized, content: certificate.certTypeString ) + infoSection += [hSection] + if validityState.revocationValidity == .revocated { + let rSection = InfoSection(header: "Reason for Invalidity".localized, content: "Certificate was revoked".localized) + infoSection += [rSection] + return + } + + if !validityState.isValid { + let vSection = InfoSection(header: "Reason for Invalidity".localized, + content: validityState.validityFailures.joined(separator: " ")) + infoSection += [vSection] + return + } } - } let hSection = InfoSection( header: "Standardised Family Name".localized, - content: certificate.lastNameStandardized.replacingOccurrences( of: "<", with: String.zeroWidthSpace + "<" + String.zeroWidthSpace), style: .fixedWidthFont) - infoSection += [hSection] + content: certificate.lastNameStandardized.replacingOccurrences( of: "<", + with: String.zeroWidthSpace + "<" + String.zeroWidthSpace), style: .fixedWidthFont) + infoSection += [hSection] infoSection += [InfoSection( header: "Standardised Given Name".localized, - content: certificate.firstNameStandardized.replacingOccurrences( of: "<", - with: String.zeroWidthSpace + "<" + String.zeroWidthSpace), style: .fixedWidthFont)] + content: certificate.firstNameStandardized.replacingOccurrences( of: "<", + with: String.zeroWidthSpace + "<" + String.zeroWidthSpace), style: .fixedWidthFont)] let sSection = InfoSection( header: "Date of birth".localized, content: certificate.dateOfBirth) - infoSection += [sSection] - infoSection += certificate.statement == nil ? [] : certificate.statement.info - let uSection = InfoSection(header: "Unique Certificate Identifier".localized, content: certificate.uvci,style: .fixedWidthFont,isPrivate: true) - infoSection += [uSection] - if !certificate.issCode.isEmpty { - let cSection = InfoSection(header: "Issuer Country".localized, content: l10n("country.\(certificate.issCode.uppercased())")) - infoSection += [cSection] - } + infoSection += [sSection] + infoSection += certificate.statement == nil ? [] : certificate.statement.info + let uSection = InfoSection(header: "Unique Certificate Identifier".localized, + content: certificate.uvci,style: .fixedWidthFont,isPrivate: true) + infoSection += [uSection] + if !certificate.issCode.isEmpty { + let cSection = InfoSection(header: "Issuer Country".localized, content: l10n("country.\(certificate.issCode.uppercased())")) + infoSection += [cSection] + } } private func makeSectionsForVaccine(includeInvalidSection: Bool = true) { - if includeInvalidSection { - let cSection = InfoSection( header: "Certificate Type".localized, content: certificate.certTypeString) - infoSection += [cSection] - if !validityState.isValid { - let hSection = InfoSection(header: "Reason for Invalidity".localized, - content: validityState.validityFailures.joined(separator: " ")) - infoSection += [hSection] + if includeInvalidSection { + let cSection = InfoSection( header: "Certificate Type".localized, content: certificate.certTypeString) + infoSection += [cSection] + + if validityState.revocationValidity == .revocated { + let rSection = InfoSection(header: "Reason for Invalidity".localized, content: "Certificate was revoked".localized) + infoSection += [rSection] + } else if !validityState.isValid { + let hSection = InfoSection(header: "Reason for Invalidity".localized, + content: validityState.validityFailures.joined(separator: " ")) + infoSection += [hSection] + } + } + + let fullName = certificate.fullName + if !fullName.isEmpty { + let sSection = InfoSection( header: "Name".localized, content: fullName, style: .fixedWidthFont ) + infoSection += [sSection] + } + infoSection += certificate.statement == nil ? [] : certificate.statement.walletInfo + if certificate.issCode.count > 0 { + let cSection = InfoSection( header: "Issuer Country".localized, content: l10n("country.\(certificate.issCode.uppercased())")) + infoSection += [cSection] } - } - let fullName = certificate.fullName - if !fullName.isEmpty { - let sSection = InfoSection( header: "Name".localized, content: fullName, style: .fixedWidthFont ) - infoSection += [sSection] - } - infoSection += certificate.statement == nil ? [] : certificate.statement.walletInfo - if certificate.issCode.count > 0 { - let cSection = InfoSection( header: "Issuer Country".localized, content: l10n("country.\(certificate.issCode.uppercased())")) - infoSection += [cSection] - } } private func makeSectionsForTest(includeInvalidSection: Bool = true) { if includeInvalidSection { let cSection = InfoSection(header: "Certificate Type".localized, content: certificate.certTypeString) - infoSection += [cSection] - if !validityState.isValid { - let hSection = InfoSection(header: "Reason for Invalidity".localized, - content: validityState.validityFailures.joined(separator: " ")) - infoSection += [hSection] - } + infoSection += [cSection] + if validityState.revocationValidity == .revocated { + let rSection = InfoSection(header: "Reason for Invalidity".localized, content: "Certificate was revoked".localized) + infoSection += [rSection] + } else if !validityState.isValid { + let hSection = InfoSection(header: "Reason for Invalidity".localized, + content: validityState.validityFailures.joined(separator: " ")) + infoSection += [hSection] + } } let fullName = certificate.fullName if !fullName.isEmpty { let section = InfoSection(header: "Name".localized, content: fullName, style: .fixedWidthFont) - infoSection += [section] + infoSection += [section] } infoSection += certificate.statement == nil ? [] : certificate.statement.walletInfo let section = InfoSection( header: "Issuer Country".localized, content: l10n("country.\(certificate.issCode.uppercased())")) if !certificate.issCode.isEmpty { - infoSection += [section] + infoSection += [section] } } private func makeSectionsForRecovery(includeInvalidSection: Bool = true) { if includeInvalidSection { let hSection = InfoSection(header: "Certificate Type".localized, content: certificate.certTypeString) - infoSection += [hSection] - if !validityState.isValid { + infoSection += [hSection] + if validityState.revocationValidity == .revocated { + let rSection = InfoSection(header: "Reason for Invalidity".localized, content: "Certificate was revoked".localized) + infoSection += [rSection] + } else if !validityState.isValid { let vSection = InfoSection(header: "Reason for Invalidity".localized, - content: validityState.validityFailures.joined(separator: " ")) - infoSection += [vSection] - } + content: validityState.validityFailures.joined(separator: " ")) + infoSection += [vSection] + } } let fullName = certificate.fullName if !fullName.isEmpty { let nSection = InfoSection( header: "Name".localized, content: fullName, style: .fixedWidthFont) - infoSection += [nSection] + infoSection += [nSection] } infoSection += certificate.statement == nil ? [] : certificate.statement.walletInfo if !certificate.issCode.isEmpty { let iSection = InfoSection(header: "Issuer Country".localized, content: l10n("country.\(certificate.issCode.uppercased())")) - infoSection += [iSection] + infoSection += [iSection] } } } diff --git a/Sources/Models/ValidityState.swift b/Sources/Models/ValidityState.swift index f3c019b..25b6fe9 100644 --- a/Sources/Models/ValidityState.swift +++ b/Sources/Models/ValidityState.swift @@ -29,73 +29,100 @@ import Foundation public struct ValidityState { - public static var invalid = ValidityState() - + public static var validState = ValidityState() + public static var invalidState = ValidityState(isValid: false) + public static var revocatedState = ValidityState(isRevocated: true) + public let technicalValidity: HCertValidity - public let issuerValidity: HCertValidity - public let destinationValidity: HCertValidity - public let travalerValidity: HCertValidity - public let allRulesValidity: HCertValidity + public var issuerValidity: HCertValidity + public var destinationValidity: HCertValidity + public var travalerValidity: HCertValidity + public var allRulesValidity: HCertValidity + public var revocationValidity: HCertValidity + public let validityFailures: [String] public var infoRulesSection: InfoSection? + + public var isNotPassed: Bool { + return technicalValidity != .valid || + issuerInvalidation != .passed || destinationAcceptence != .passed || travalerAcceptence != .passed + } - public init() { - self.technicalValidity = .invalid - self.issuerValidity = .invalid - self.destinationValidity = .invalid - self.travalerValidity = .invalid - self.allRulesValidity = .invalid + public init(isValid: Bool = true) { + self.technicalValidity = isValid ? .valid : .invalid + self.issuerValidity = isValid ? .valid : .invalid + self.destinationValidity = isValid ? .valid : .invalid + self.travalerValidity = isValid ? .valid : .invalid + self.allRulesValidity = isValid ? .valid : .invalid + self.revocationValidity = isValid ? .valid : .invalid self.validityFailures = [] self.infoRulesSection = nil } - + + public init(isRevocated: Bool) { + self.technicalValidity = .revocated + self.issuerValidity = .revocated + self.destinationValidity = .revocated + self.travalerValidity = .revocated + self.allRulesValidity = .revocated + self.revocationValidity = .revocated + self.validityFailures = [] + self.infoRulesSection = nil + } + public init( technicalValidity: HCertValidity, issuerValidity: HCertValidity, destinationValidity: HCertValidity, travalerValidity: HCertValidity, allRulesValidity: HCertValidity, + revocationValidity: HCertValidity, validityFailures: [String], infoRulesSection: InfoSection?) { self.technicalValidity = technicalValidity self.issuerValidity = issuerValidity self.destinationValidity = destinationValidity self.travalerValidity = travalerValidity + self.revocationValidity = revocationValidity self.allRulesValidity = allRulesValidity self.validityFailures = validityFailures self.infoRulesSection = infoRulesSection } private var validity: HCertValidity { - return validityFailures.isEmpty ? .valid : .invalid + return validityFailures.isEmpty ? .valid : .invalid } public var isValid: Bool { - return validityFailures.isEmpty + return validityFailures.isEmpty } - + public var issuerInvalidation: RuleValidationResult { let ruleResult: RuleValidationResult switch issuerValidity { - case .valid: - ruleResult = .passed - case .invalid: - ruleResult = .error - case .ruleInvalid: - ruleResult = .open - } + case .valid: + ruleResult = .passed + case .invalid: + ruleResult = .failed + case .ruleInvalid: + ruleResult = .open + case .revocated: + ruleResult = .failed + } return ruleResult } public var destinationAcceptence: RuleValidationResult { let ruleResult: RuleValidationResult switch destinationValidity { - case .valid: - ruleResult = .passed - case .invalid: - ruleResult = .error - case .ruleInvalid: - ruleResult = .open + case .valid: + ruleResult = .passed + case .invalid: + ruleResult = .failed + case .ruleInvalid: + ruleResult = .open + case .revocated: + ruleResult = .failed } return ruleResult } @@ -103,12 +130,14 @@ public struct ValidityState { public var travalerAcceptence: RuleValidationResult { let ruleResult: RuleValidationResult switch travalerValidity { - case .valid: - ruleResult = .passed - case .invalid: - ruleResult = .error - case .ruleInvalid: - ruleResult = .open + case .valid: + ruleResult = .passed + case .invalid: + ruleResult = .failed + case .ruleInvalid: + ruleResult = .open + case .revocated: + ruleResult = .failed } return ruleResult } diff --git a/Sources/Networking/CertLookUp.swift b/Sources/Networking/CertLookUp.swift new file mode 100644 index 0000000..8dfaf7a --- /dev/null +++ b/Sources/Networking/CertLookUp.swift @@ -0,0 +1,15 @@ +// +// CertLookUP.swift +// +// +// Created by Igor Khomiak on 20.02.2022. +// + +import Foundation + +public struct CertLookUp { + public let kid: String + public let section: String + public let x: String + public let y: String +} diff --git a/Sources/Services/ContextConnection.swift b/Sources/Networking/ContextConnection.swift similarity index 96% rename from Sources/Services/ContextConnection.swift rename to Sources/Networking/ContextConnection.swift index 1aa1cb6..77c3676 100644 --- a/Sources/Services/ContextConnection.swift +++ b/Sources/Networking/ContextConnection.swift @@ -43,8 +43,7 @@ public extension ContextConnection { encoding: ParameterEncoding = URLEncoding.default, headers: HTTPHeaders? = nil, interceptor: RequestInterceptor? = nil, - requestModifier: Alamofire.Session.RequestModifier? = nil - ) -> DataRequest { + requestModifier: Alamofire.Session.RequestModifier? = nil) -> DataRequest { var json = config for key in path { json = json[key] diff --git a/Sources/Networking/RequestFactory.swift b/Sources/Networking/RequestFactory.swift new file mode 100644 index 0000000..96eb0c2 --- /dev/null +++ b/Sources/Networking/RequestFactory.swift @@ -0,0 +1,76 @@ +// +// RequestFactory.swift +// DCCRevocation +// +// Created by Igor Khomiak on 20.01.2022. +// + +import Foundation + +internal enum ServiceConfig: String { + case test = "/" + case linkForAllRevocations = "/lists" + case linkForPartitions = "/lists/%@/partitions" + case linkForPartitionsWithID = "/lists/%@/partitions/%@" + case linkForPartitionChunks = "/lists/%@/partitions/%@/slices" + case linkForChunkSlices = "/lists/%@/partitions/%@/chunks/%@/slices" + case linkForSingleSlice = "/lists/%@/partitions/%@/chunks/%@/slices/%@" +} + +internal class RequestFactory { + typealias StringDictionary = [String : String] + + // MARK: - Private methods + fileprivate static func postRequest(url: URL, HTTPBody body: Data?, headerFields: StringDictionary?) -> URLRequest { + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.httpBody = body + headerFields?.forEach { request.setValue($0.1, forHTTPHeaderField: $0.0) } + return request + } + + fileprivate static func request(url: URL, query: StringDictionary?, headerFields: StringDictionary?) -> URLRequest { + var resultURL: URL = url + if let query = query { + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) + let queryItems = query.map { URLQueryItem(name:$0.0, value: $0.1) } + components?.queryItems = queryItems + if let urlComponents = components?.url { + resultURL = urlComponents + } + } + + var request = URLRequest(url: resultURL) + request.httpMethod = "GET" + + headerFields?.forEach { + request.setValue($0.1, forHTTPHeaderField: $0.0) + } + return request + } +} + +// TODO add protocol +extension RequestFactory { + + static func serviceGetRequest(path: String, etag: String? = nil) -> URLRequest? { + guard let url = URL(string: path) else { return nil } + + let headers = etag == nil ? ["If-None-Match" : "", "Content-Type" : "application/json"] : + ["If-Match" : etag!, "Content-Type" : "application/json"] + + let result = request(url: url, query: nil, headerFields: headers) + return result + } + + + static func servicePostRequest(path: String, body: Data?, etag: String? = nil) -> URLRequest? { + guard let url = URL(string: path) else { return nil } + + let headers = etag == nil ? ["If-None-Match" : "", "Content-Type" : "application/json"] : + ["If-Match" : etag!, "Content-Type" : "application/json"] + + let result = postRequest(url: url, HTTPBody: body, headerFields: headers) + return result + } +} diff --git a/Sources/Networking/RevocationModels.swift b/Sources/Networking/RevocationModels.swift new file mode 100644 index 0000000..759a053 --- /dev/null +++ b/Sources/Networking/RevocationModels.swift @@ -0,0 +1,35 @@ +// +// RevocationModels.swift +// DCCRevocation +// +// Created by Igor Khomiak on 21.01.2022. +// + +import Foundation + +public typealias SliceDict = [String : SliceModel] + +public struct RevocationModel: Hashable, Codable { + public let kid: String + public let mode: String + public let hashTypes: [String] + public let expires: String + public let lastUpdated: String +} + +public struct PartitionModel: Hashable, Codable { + public let kid: String + public var id: String? + public var x: String? + public var y: String? + public let lastUpdated: String + public let expired: String + public let chunks: [String : SliceDict] +} + +public struct SliceModel: Hashable, Codable { + public let type: String + public let version: String + public let hash: String +} + diff --git a/Sources/Networking/RevocationService.swift b/Sources/Networking/RevocationService.swift new file mode 100644 index 0000000..59f8dd6 --- /dev/null +++ b/Sources/Networking/RevocationService.swift @@ -0,0 +1,232 @@ +// +// RevocationService.swift +// DCCRevocation +// +// Created by Igor Khomiak on 21.01.2022. +// + +import Foundation + + +public enum RevocationError: Error { + case unauthorized // TODO - add unauthorized(error: NSError?) + case invalidID + case badRequest(path: String) + case nodata + case failedLoading(reason: String) + case failedValidation(status: Int) + case network(reason: String) +} + +public typealias RevocationListCompletion = ([RevocationModel]?, String?, RevocationError?) -> Void +public typealias PartitionListCompletion = ([PartitionModel]?, String?, RevocationError?) -> Void +public typealias JSONDataTaskCompletion = (T?, String?, RevocationError?) -> Void +public typealias ZIPDataTaskCompletion = (Data?, RevocationError?) -> Void + +public final class RevocationService { + + var baseServiceURLPath: String + var allChunks = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"] + + public init(baseServicePath path: String) { + self.baseServiceURLPath = path + } + + lazy var session: URLSession = { + return URLSession(configuration: .default) + }() + + // MARK: - Revocation Lists + // summary: Returns an overview about all available revocation lists. + // description: This method returns an over about available revocation lists for each KID. The response contains for all available KIDs the last modification date, the used hash types etc. + // paths: /lists: (get) + public func getRevocationLists(completion: @escaping RevocationListCompletion) { + let path = baseServiceURLPath + ServiceConfig.linkForAllRevocations.rawValue + guard let request = RequestFactory.serviceGetRequest(path: path) else { + completion(nil, nil, .badRequest(path: path)) + return + } + self.startJSONDataTask(for: request, completion: completion) + } + + // MARK: - Partitions Lists + // summary: Returns for the selected kid all Partitions + // description: Returns a list of all available partitions. + // paths: /lists/{kid}/partitions (get) + + public func getRevocationPartitions(for kid: String, completion: @escaping PartitionListCompletion) { + let partitionComponent = String(format: ServiceConfig.linkForPartitions.rawValue, kid) + let path = baseServiceURLPath + partitionComponent + guard let etagData = SecureKeyChain.load(key: "verifierETag") else { return } + let eTag = String(decoding: etagData, as: UTF8.self) + guard let request = RequestFactory.serviceGetRequest(path: path, etag: eTag) else { + completion(nil, nil, .badRequest(path: path)) + return + } + self.startJSONDataTask(for: request, completion: completion) + } + + // MARK: - Partitions Lists with ID + // summary: Returns for the selected kid a Partition + // description: Returns a Partition by Id + // paths: /lists/{kid}/partitions/{id}: (get) + + public func getRevocationPartitions(for kid: String, id: String, completion: @escaping PartitionListCompletion) { + let partitionIDComponent = String(format: ServiceConfig.linkForPartitionsWithID.rawValue, kid, id) + let path = baseServiceURLPath + partitionIDComponent + guard let etagData = SecureKeyChain.load(key: "verifierETag") else { return } + let eTag = String(decoding: etagData, as: UTF8.self) + guard let request = RequestFactory.serviceGetRequest(path: path, etag: eTag) else { + completion(nil, nil, .badRequest(path: path)) + return + } + self.startJSONDataTask(for: request, completion: completion) + } + + // MARK: - All chunks Lists + // summary: Returns for the selected partition all chunks. + // description: Returns a Partition by Id + // paths: /lists/{kid}/partitions/{id}/chunks (post) + + public func getRevocationPartitionChunks(for kid: String, id: String, cids: [String]? = nil, completion: @escaping ZIPDataTaskCompletion) { + let partitionIDComponent = String(format: ServiceConfig.linkForPartitionChunks.rawValue, kid, id) + let path = baseServiceURLPath + partitionIDComponent + guard let etagData = SecureKeyChain.load(key: "verifierETag") else { return } + let eTag = String(decoding: etagData, as: UTF8.self) + + let encoder = JSONEncoder() + let postData = cids == nil ? try? encoder.encode(allChunks) : try? encoder.encode(cids!) + + guard let request = RequestFactory.servicePostRequest(path: path, body: postData, etag: eTag) else { + completion(nil, .badRequest(path: path)) + return + } + self.startZIPDataTask(for: request, completion: completion) + } + + // MARK: - Chunk all content + //summary: Returns for the selected chunk all content. + //description: Returns a Partition by Id + // paths: /lists/{kid}/partitions/{id}/chunks/{cid} (get) + + public func getRevocationPartitionChunk(for kid: String, id: String, cid: String, completion: @escaping ZIPDataTaskCompletion) { + let partitionIDComponent = String(format: ServiceConfig.linkForChunkSlices.rawValue, kid, id, cid) + let path = baseServiceURLPath + partitionIDComponent + guard let etagData = SecureKeyChain.load(key: "verifierETag") else { return } + let eTag = String(decoding: etagData, as: UTF8.self) + guard let request = RequestFactory.serviceGetRequest(path: path, etag: eTag) else { + completion(nil, .badRequest(path: path)) + return + } + self.startZIPDataTask(for: request, completion: completion) + } + + // MARK: - Chunk's all slices Lists + // summary: Returns for the selected partition all chunks. + // description: Returns a Partition by Id + // paths: /lists/{kid}/partitions/{id}/chunks/{cid}/slice (post) + + public func getRevocationPartitionChunkSlice(for kid: String, id: String, cid: String, sids: [String]?, + completion: @escaping ZIPDataTaskCompletion) { + let partitionIDComponent = String(format: ServiceConfig.linkForChunkSlices.rawValue, kid, id, cid) + let path = baseServiceURLPath + partitionIDComponent + guard let etagData = SecureKeyChain.load(key: "verifierETag") else { return } + let eTag = String(decoding: etagData, as: UTF8.self) + + let encoder = JSONEncoder() + let postData = try? encoder.encode(sids) + + guard let request = RequestFactory.servicePostRequest(path: path, body: postData, etag: eTag) else { + completion(nil, .badRequest(path: path)) + return + } + self.startZIPDataTask(for: request, completion: completion) + } + + // MARK: - Single Slice content + //summary: Returns for the selected chunk all content. + //description: Returns a Partition by Id + // paths: /lists/{kid}/partitions/{id}/chunks/{cid}/slice/{sid} (get) + + public func getRevocationPartitionChunkSliceSingle(for kid: String, id: String, cid: String, sid: String, + completion: @escaping ZIPDataTaskCompletion) { + let partitionIDComponent = String(format: ServiceConfig.linkForSingleSlice.rawValue, kid, id, cid, sid) + let path = baseServiceURLPath + partitionIDComponent + guard let etagData = SecureKeyChain.load(key: "verifierETag") else { return } + let eTag = String(decoding: etagData, as: UTF8.self) + guard let request = RequestFactory.serviceGetRequest(path: path, etag: eTag) else { + completion(nil, .badRequest(path: path)) + return + } + self.startZIPDataTask(for: request, completion: completion) + } + + // private methods + fileprivate func startJSONDataTask(for request: URLRequest, completion: @escaping JSONDataTaskCompletion) { + let dataTask = session.dataTask(with: request) {[unowned self] (data, response, error) in + let httpResponse = response as! HTTPURLResponse + guard defaultResponseValidation(statusCode: httpResponse.statusCode) else { + completion(nil, nil, .failedValidation(status: httpResponse.statusCode)) + return + } + guard error == nil else { + completion(nil, nil, .network(reason: error!.localizedDescription)) + return + } + guard let data = data else { + completion(nil, nil, .nodata) + return + } + + do { + var eTag: String = "" + let decodedData: T = try JSONDecoder().decode(T.self, from: data) + if let eTagString = httpResponse.allHeaderFields["Etag"] as? String { + let str = eTagString.replacingOccurrences(of: "\"", with: "") + eTag = str + } + completion(decodedData, eTag, nil) + + } catch { + completion(nil, nil, RevocationError.failedLoading(reason: "Revocation list parsing error")) + } + } + dataTask.resume() + } + + fileprivate func startZIPDataTask(for request: URLRequest, completion: @escaping ZIPDataTaskCompletion) { + let dataTask = session.dataTask(with: request) {[unowned self] (zipData, response, error) in + guard let httpResponse = response as? HTTPURLResponse, self.defaultResponseValidation(statusCode: httpResponse.statusCode) else { + completion(nil, .failedValidation(status: (response as? HTTPURLResponse)?.statusCode ?? 0)) + return + } + guard error == nil else { + completion(nil, RevocationError.network(reason: error!.localizedDescription)) + return + } + guard let zipData = zipData else { + completion(nil, .nodata) + return + } + completion(zipData, nil) + } + dataTask.resume() + } + + fileprivate func defaultResponseValidation(statusCode: Int) -> Bool { + switch statusCode { + case 200: + return true + case 304: + return false //.network(reason: "Not-Modified.") + case 400, 404: + return false //RevocationError.invalidID + case 401: + return false //RevocationError.unauthorized + case 412: + return false //RevocationError.network(reason: "Pre-Condition Failed.") + default: + return false //RevocationError.network(reason: "Failed with statusCode \(statusCode)") + } + } +} diff --git a/Sources/Networking/SliceMetaData.swift b/Sources/Networking/SliceMetaData.swift new file mode 100644 index 0000000..c7c663c --- /dev/null +++ b/Sources/Networking/SliceMetaData.swift @@ -0,0 +1,24 @@ +// +// SliceMetaData.swift +// +// +// Created by Igor Khomiak on 20.02.2022. +// + +import Foundation + +public struct SliceMetaData { + public let kid: String + public let id: String + public let cid: String + public let hashID: String + public let contentData: Data + + public init(kid: String, id: String, cid: String, hashID: String, contentData: Data) { + self.kid = kid + self.id = id + self.cid = cid + self.hashID = hashID + self.contentData = contentData + } +} diff --git a/Sources/Services/SHA256.swift b/Sources/Services/SHA256.swift index 4482873..17cb731 100644 --- a/Sources/Services/SHA256.swift +++ b/Sources/Services/SHA256.swift @@ -31,13 +31,20 @@ import Foundation import CommonCrypto public class SHA256 { - public static func digest(input: NSData) -> Data { - if #available(iOS 13.0, *) { - return iOS13Digest(input: input) + public static func digest(input: NSData) -> Data { + if #available(iOS 13.0, *) { + return iOS13Digest(input: input) + } + return iOS12Digest(input: input) } - return iOS12Digest(input: input) - } + public static func sha256(data : Data) -> Data { + var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + data.withUnsafeBytes { + _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash) + } + return Data(hash) + } public static func iOS12Digest(input: NSData) -> Data { let input = input as Data var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) diff --git a/Tests/SwiftDGCTests/COSETests.swift b/Tests/SwiftDGCTests/COSETests.swift index 2d0eb2b..08df3fe 100644 --- a/Tests/SwiftDGCTests/COSETests.swift +++ b/Tests/SwiftDGCTests/COSETests.swift @@ -71,12 +71,12 @@ final class COSETests: XCTestCase { var body = ""; extractParameter(hex: hexCOSE, header: &header, kid: &kid, body: &body) - var jsonHeader = header.asJSONDict; + let jsonHeader = header.asJSONDict; XCTAssert(jsonHeader["1"] as! Int8 == -7); - XCTAssert(Data(bytes:(jsonHeader["4"] as! [UInt8])).hexString == Data(bytes:kid).hexString); - XCTAssert( Data(bytes:kid).base64EncodedString() == "X3SRAZXFzss="); - XCTAssert( Data(bytes:(jsonHeader["4"] as! [UInt8])).base64EncodedString() == "X3SRAZXFzss=") + XCTAssert(Data(jsonHeader["4"] as! [UInt8]).hexString == Data(kid).hexString); + XCTAssert( Data(kid).base64EncodedString() == "X3SRAZXFzss="); + XCTAssert( Data(jsonHeader["4"] as! [UInt8]).base64EncodedString() == "X3SRAZXFzss=") let jsonPayload = body.asJSONDict; @@ -124,10 +124,10 @@ final class COSETests: XCTestCase { var body = ""; extractParameter(hex: hexCOSE, header: &header, kid: &kid, body: &body) - var jsonHeader = header.asJSONDict; + let jsonHeader = header.asJSONDict; XCTAssert(jsonHeader["1"] as! Int8 == -7); XCTAssert(jsonHeader.count==1); - XCTAssert( Data(bytes:kid).base64EncodedString() == "DEsVUSvpFAE="); + XCTAssert( Data(kid).base64EncodedString() == "DEsVUSvpFAE="); XCTAssert(jsonHeader.index(forKey: "4") == nil) let jsonPayload = body.asJSONDict; diff --git a/templates/file-header.txt b/templates/file-header.txt deleted file mode 100644 index 56c0017..0000000 --- a/templates/file-header.txt +++ /dev/null @@ -1,25 +0,0 @@ -/*- - * ---license-start - * eu-digital-green-certificates / dgca-app-core-ios - * --- - * Copyright (C) 2021 T-Systems International GmbH and all other contributors - * --- - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ---license-end - */ -// -// ___FILENAME___ -// ___PACKAGENAME___ -// -// Created by ___FULLUSERNAME___ on ___DATE___. -// diff --git a/test.sh b/test.sh deleted file mode 100755 index 8b6ec2e..0000000 --- a/test.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -cd "$(dirname "$0")/Tests/SwiftDGCTests" -[ -d dgc-testdata ] && rm -rf dgc-testdata -git clone https://github.com/eu-digital-green-certificates/dgc-testdata -swift test