From f1bcb3b21532021692ff8b1358bcc8d807ed1c84 Mon Sep 17 00:00:00 2001 From: Paul B Date: Thu, 5 May 2022 11:20:18 +0200 Subject: [PATCH] Comp revocation development (#120) * Fix verification fields * Minor changes in responce * Fixed visibility of enums to public * Update Core Extensions * Added corrected localisation strings * Fixed Warnings * Fixed QR Code crash * minor fix * minor * Added Networking to core * Added BlumFilter functionality * Prepared DataCenter to adding Revocations, fixed Bloom filter SHA256 * Added revocation main URL * Added Revocation request * Updated request factory * Added chunk request * Added zip loading * UVCI and country code concat UVCI hashes accessors * Certificate signature hash calculation * Marked incorrect public key for the signature algorithm check * Added UI screens for loading * Signature hash finalized * Added revocation Functionality * Imported BloomFilter * Changed status of x and y * Added UI * Added minor * Updated validity * removed unnecessary file * Added new revocation section * fixed certificate hash * feat: prefix check revocation * RevocationServiceProtocol * minor * Fixed last commit * Added revoc enum * Added temp header * Added temp SliceType * Refactored API * Refactored changes * fix: core * Added field to Hcert * minor chages * fixed crash with no internet * Updated storage * Added new API to Verifier, removed old API * Added Headers * increased buildld num * minor fixes Co-authored-by: IgorKhomiak Co-authored-by: Denis Melenevsky Co-authored-by: Paul Ballmann --- Sources/Extensions/Date.swift | 29 +- Sources/Extensions/String+JSON.swift | 18 +- Sources/Extensions/SwiftCBOR.CBOR.swift | 56 +-- Sources/Extensions/UIViewController.swift | 88 ---- Sources/Extensions/UserDefaults+.swift | 66 --- Sources/Models/CoreManager.swift | 2 +- Sources/Models/Enums.swift | 25 +- Sources/Models/HCert.swift | 443 +++++++++--------- Sources/Models/InfoSection.swift | 4 +- Sources/Models/SectionBuilder.swift | 76 ++- Sources/Models/SecureStorage.swift | 176 ++++--- Sources/Models/ValidityState.swift | 98 +--- Sources/Networking/RequestFactory.swift | 41 +- Sources/Networking/RevocationService.swift | 46 +- .../RevocationServiceProtocol.swift | 18 + .../de.lproj/Localizable.strings | 3 +- .../en.lproj/Localizable.strings | 1 + 17 files changed, 523 insertions(+), 667 deletions(-) delete mode 100644 Sources/Extensions/UIViewController.swift delete mode 100644 Sources/Extensions/UserDefaults+.swift create mode 100644 Sources/Networking/RevocationServiceProtocol.swift diff --git a/Sources/Extensions/Date.swift b/Sources/Extensions/Date.swift index f3d0ef3..86c30ad 100644 --- a/Sources/Extensions/Date.swift +++ b/Sources/Extensions/Date.swift @@ -49,18 +49,23 @@ extension Date { static let dateTimeFormatter = formatter(for: "yyyy-MM-dd HH:mm", utcPosix: false) static let dateFormatterOffset = formatter(for: "yyyy-MM-dd'T'HH:mm:ssZZZZZ") static let dateFormatterFractional = formatter(for: "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX") + + // 2022-03-30T15:23:39Z + public var isoString: String { + Date.isoFormatter.string(from: self) + } - public var isoString: String { - Date.isoFormatter.string(from: self) - } - - public var dateString: String { - Date.dateFormatter.string(from: self) - } - - public var dateTimeString: String { - Date.dateTimeFormatter.string(from: self) - } + public var dateString: String { + Date.dateFormatter.string(from: self) + } + + public var dateOffsetString: String { + Date.dateFormatterOffset.string(from: self) + } + + public var dateTimeString: String { + Date.dateTimeFormatter.string(from: self) + } public init?(isoString: String) { guard let date = Date.isoFormatter.date(from: isoString) else { return nil } @@ -83,7 +88,7 @@ extension Date { } else { return nil } - } + } public init?(rfc3339DateTimeString str: String) { var str = str diff --git a/Sources/Extensions/String+JSON.swift b/Sources/Extensions/String+JSON.swift index ce599cf..39922f7 100644 --- a/Sources/Extensions/String+JSON.swift +++ b/Sources/Extensions/String+JSON.swift @@ -27,15 +27,15 @@ import Foundation extension String { - var asJSONDict: [String: AnyObject] { - if let data = data(using: .utf8) { - do { - let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: AnyObject] - return json ?? [:] - } catch { + var asJSONDict: [String: AnyObject] { + if let data = data(using: .utf8) { + do { + let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: AnyObject] + return json ?? [:] + } catch { + return [:] + } + } return [:] - } } - return [:] - } } diff --git a/Sources/Extensions/SwiftCBOR.CBOR.swift b/Sources/Extensions/SwiftCBOR.CBOR.swift index 4d68fd3..f46bcef 100644 --- a/Sources/Extensions/SwiftCBOR.CBOR.swift +++ b/Sources/Extensions/SwiftCBOR.CBOR.swift @@ -37,49 +37,43 @@ extension SwiftCBOR.CBOR { public func toString() -> String { switch self { case let .byteString(val): - let fallBack = "[" + val.map { "\($0)" }.joined(separator: ", ") + "]" - // if - // let child = try? SwiftCBOR.CBOR.decode(val), - // case .map(_) = child - // { - // return child.toString() - // } - return fallBack + let fallBack = "[" + val.map { "\($0)" }.joined(separator: ", ") + "]" + return fallBack case let .unsignedInt(val): - return "\(val)" + return "\(val)" case let .negativeInt(val): - return "-\(val + 1)" + return "-\(val + 1)" case let .utf8String(val): - return "\"\(sanitize(value: val))\"" + return "\"\(sanitize(value: val))\"" case let .array(vals): - var str = "" - for val in vals { - str += (str.isEmpty ? "" : ", ") + val.toString() - } - return "[\(str)]" + var str = "" + for val in vals { + str += (str.isEmpty ? "" : ", ") + val.toString() + } + return "[\(str)]" case let .map(vals): - var str = "" - for pair in vals { - let val = pair.value - if case .undefined = val { - continue + var str = "" + for pair in vals { + let val = pair.value + if case .undefined = val { + continue + } + let key = "\"\(pair.key.toString().trimmingCharacters(in: ["\""]))\"" + str += (str.isEmpty ? "" : ", ") + "\(key): \(val.toString())" } - let key = "\"\(pair.key.toString().trimmingCharacters(in: ["\""]))\"" - str += (str.isEmpty ? "" : ", ") + "\(key): \(val.toString())" - } - return "{\(str)}" + return "{\(str)}" case let .boolean(val): - return String(describing: val) + return String(describing: val) case .null, .undefined: - return "null" + return "null" case let .float(val): - return "\(val)" + return "\(val)" case let .double(val): - return "\(val)" + return "\(val)" case let .date(val): - return "\"\(val.isoString)\"" + return "\"\(val.isoString)\"" default: - return "\"unsupported data\"" + return "\"unsupported data\"" } } } diff --git a/Sources/Extensions/UIViewController.swift b/Sources/Extensions/UIViewController.swift deleted file mode 100644 index 47e6057..0000000 --- a/Sources/Extensions/UIViewController.swift +++ /dev/null @@ -1,88 +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 - */ -// -// UIViewController.swift -// -// -// Created by Yannick Spreen on 5/3/21. -// - -#if os(iOS) -import UIKit - -public extension UIViewController { - func showInputDialog( - title: String? = nil, - subtitle: String? = nil, - actionTitle: String? = "OK".localized, - cancelTitle: String? = "Cancel".localized, - inputPlaceholder: String? = nil, - inputKeyboardType: UIKeyboardType = UIKeyboardType.default, - capitalization: UITextAutocapitalizationType? = nil, - handler: ((_ text: String?) -> Void)? = nil - ) { - let alert = UIAlertController(title: title, message: subtitle, preferredStyle: .alert) - alert.addTextField { (textField: UITextField) in - textField.placeholder = inputPlaceholder - textField.keyboardType = inputKeyboardType - if let cap = capitalization { - textField.autocapitalizationType = cap - } - } - alert.addAction(UIAlertAction(title: actionTitle, style: .default) { _ in - guard let textField = alert.textFields?.first else { - handler?(nil) - return - } - handler?(textField.text) - }) - alert.addAction(UIAlertAction(title: cancelTitle, style: .cancel) { _ in - handler?(nil) - }) - present(alert, animated: true, completion: nil) - } - - func showAlert( - title: String? = nil, - subtitle: String? = nil, - actionTitle: String? = "OK".localized, - cancelTitle: String? = nil, - handler: ((Bool) -> Void)? = nil - ) { - let alert = UIAlertController(title: title, message: subtitle, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: actionTitle, style: .default) { _ in - handler?(true) - }) - if let cancelTitle = cancelTitle { - alert.addAction(UIAlertAction(title: cancelTitle, style: .cancel) { _ in - handler?(false) - }) - } - present(alert, animated: true, completion: nil) - } - - func showInfoAlert(withTitle title: String, message: String) { - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: "OK", style: .default)) - self.present(alertController, animated: true) - } -} -#endif diff --git a/Sources/Extensions/UserDefaults+.swift b/Sources/Extensions/UserDefaults+.swift deleted file mode 100644 index 2f67bf9..0000000 --- a/Sources/Extensions/UserDefaults+.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -/*- - * ---license-start - * eu-digital-green-certificates / dgca-verifier-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 - */ -// -// File.swift -// -// -// Created by Alexandr Chernyy on 22.06.2021. -// - -import Foundation - -public enum ObjectSavableError: String, LocalizedError { - case unableToEncode = "Unable to encode object into data" - case noValue = "No data object found for the given key" - case unableToDecode = "Unable to decode object into given type" - - public var errorDescription: String? { - return self.rawValue - } -} - -public protocol ObjectSavable { - func setObject(_ object: Object, forKey: String) throws where Object: Encodable - func getObject(forKey: String, castTo type: Object.Type) throws -> Object where Object: Decodable -} - -extension UserDefaults: ObjectSavable { - public func setObject(_ object: Object, forKey: String) throws where Object: Encodable { - let encoder = JSONEncoder() - do { - let data = try encoder.encode(object) - set(data, forKey: forKey) - } catch { - throw ObjectSavableError.unableToEncode - } - } - - public func getObject(forKey: String, castTo type: Object.Type) throws -> Object where Object: Decodable { - guard let data = data(forKey: forKey) else { throw ObjectSavableError.noValue } - let decoder = JSONDecoder() - do { - let object = try decoder.decode(type, from: data) - return object - } catch { - throw ObjectSavableError.unableToDecode - } - } -} diff --git a/Sources/Models/CoreManager.swift b/Sources/Models/CoreManager.swift index 04a2661..5c7c3d8 100644 --- a/Sources/Models/CoreManager.swift +++ b/Sources/Models/CoreManager.swift @@ -32,7 +32,7 @@ import AppKit public class CoreManager { public static var shared = CoreManager() - lazy public var config = HCertConfig.default + public static var config = HCertConfig.default public static var publicKeyEncoder: PublicKeyStorageDelegate? #if os(iOS) diff --git a/Sources/Models/Enums.swift b/Sources/Models/Enums.swift index 6d11f66..d14778d 100644 --- a/Sources/Models/Enums.swift +++ b/Sources/Models/Enums.swift @@ -59,7 +59,7 @@ public enum HCertValidity { case valid case invalid case ruleInvalid - case revocated + case revoked } public let attributeKeys: [AttributeKey: [String]] = [ @@ -78,12 +78,6 @@ public enum InfoSectionStyle { case fixedWidthFont } -public enum RuleValidationResult: Int { - case failed = 0 - case passed - case open -} - public class ParseErrors { var errors: [ParseError] = [] } @@ -106,4 +100,21 @@ public enum RevocationMode: String { case point = "POINT" case vector = "VECTOR" case coordinate = "COORDINATE" + + public func partitionOffset() -> UInt8 { + switch self { + case .point: + return 0 + case .vector: + return 8 + case .coordinate: + return 16 + } + } +} + +public var sliceType: SliceType = .VARHASHLIST + +public enum SliceType: String { + case BLOOMFILTER, VARHASHLIST } diff --git a/Sources/Models/HCert.swift b/Sources/Models/HCert.swift index 7c7e0f8..63f26fa 100644 --- a/Sources/Models/HCert.swift +++ b/Sources/Models/HCert.swift @@ -30,254 +30,261 @@ import JSONSchema import SwiftCBOR public class HCert: Codable { - public let fullPayloadString: String - public let payloadString: String - public let cborData: Data - public let kidStr: String - public let issCode: String - public let header: JSON - public let body: JSON - public let iat: Date - public let exp: Date - public var ruleCountryCode: String? - public var isRevoked: Bool? - - public var dateOfBirth: String { - return get(.dateOfBirth).string ?? "" - } - public var firstName: String { - return get(.firstName).string ?? "" - } - public var firstNameStandardized: String { - return get(.firstNameStandardized).string ?? "" - } - public var lastName: String { - return get(.lastName).string ?? "" - } - public var lastNameStandardized: String { - return get(.lastNameStandardized).string ?? "" - } - - public var fullName: String { - var fullName = "" - fullName = fullName + firstName.replacingOccurrences(of: "<", - with: String.zeroWidthSpace + "<" + String.zeroWidthSpace) - if !fullName.isEmpty { - fullName = fullName + " " + lastName.replacingOccurrences( of: "<", with: String.zeroWidthSpace + - "<" + String.zeroWidthSpace) - } else { - fullName = fullName + lastName.replacingOccurrences(of: "<", - with: String.zeroWidthSpace + "<" + String.zeroWidthSpace) + public let fullPayloadString: String + public let payloadString: String + public let cborData: Data + public let kidStr: String + public let issCode: String + public let header: JSON + public let body: JSON + public let iat: Date + public let exp: Date + public var ruleCountryCode: String? + public var isRevoked: Bool = false + + public var dateOfBirth: String { + return get(.dateOfBirth).string ?? "" } - if fullName.isEmpty { - fullName = fullName + firstNameStandardized.replacingOccurrences( of: "<", - with: String.zeroWidthSpace + "<" + String.zeroWidthSpace) - if !fullName.isEmpty { - fullName = fullName + " " + lastNameStandardized.replacingOccurrences( of: "<", - with: String.zeroWidthSpace + "<" + String.zeroWidthSpace) - } else { - fullName = fullName + lastNameStandardized.replacingOccurrences( of: "<", - with: String.zeroWidthSpace + "<" + String.zeroWidthSpace) - } + public var firstName: String { + return get(.firstName).string ?? "" } - return fullName - } - - public var certTypeString: String { - certificateType.l10n + (statement == nil ? "" : " \(statement.typeAddon)") - } - - public var uvci: String { - statement?.uvci ?? "empty" - } - - public var certificateType: HCertType { - switch statement { - case is VaccinationEntry: - return .vaccine - case is RecoveryEntry: - return .recovery - case is TestEntry: - return .test - default: - return .unknown - } - } - - var testStatements: [TestEntry] { - return get(.testStatements).array?.compactMap {TestEntry(body: $0)} ?? [] - } - var vaccineStatements: [VaccinationEntry] { - return get(.vaccineStatements).array?.compactMap { VaccinationEntry(body: $0) } ?? [] - } - var recoveryStatements: [RecoveryEntry] { - return get(.recoveryStatements).array?.compactMap {RecoveryEntry(body: $0)} ?? [] - } - var statements: [HCertEntry] { - return testStatements + vaccineStatements + recoveryStatements - } - public var statement: HCertEntry! { - return statements.last - } - - public var cryptographicallyValid: Bool { - if !CoreManager.shared.config.checkSignatures { - return true + public var firstNameStandardized: String { + return get(.firstNameStandardized).string ?? "" } - guard let delegate = CoreManager.publicKeyEncoder else { return false } - - for key in delegate.getEncodedPublicKeys(for: kidStr) { - if !X509.isCertificateValid(cert: key) { - return false - } - return X509.checkisSuitable(cert:key, certType: certificateType) + public var lastName: String { + return get(.lastName).string ?? "" } - return false - } - - public var certHash: String { - CBOR.hash(from: cborData) - } - public var keyPair: SecKey! { - Enclave.loadOrGenerateKey(with: uvci) - } - public static var clock: Date { - clockOverride ?? Date() - } - public static var clockOverride: Date? - - public init(from payload: String, ruleCountryCode: String? = nil) throws { - if HCertConfig.checkCH1PreffixExist(payload) { - fullPayloadString = payload - payloadString = HCertConfig.parsePrefix(payload) - } else { - let supportedPrefix = HCertConfig.supportedPrefixes.first ?? "" - fullPayloadString = supportedPrefix + payload - payloadString = payload + public var lastNameStandardized: String { + return get(.lastNameStandardized).string ?? "" } - - self.ruleCountryCode = ruleCountryCode - guard let compressed = try? payloadString.fromBase45() else { - throw CertificateParsingError.parsing(errors: [ParseError.base45]) + + public var fullName: String { + var fullName = "" + fullName = fullName + firstName.replacingOccurrences(of: "<", + with: String.zeroWidthSpace + "<" + String.zeroWidthSpace) + if !fullName.isEmpty { + fullName = fullName + " " + lastName.replacingOccurrences( of: "<", with: String.zeroWidthSpace + + "<" + String.zeroWidthSpace) + } else { + fullName = fullName + lastName.replacingOccurrences(of: "<", + with: String.zeroWidthSpace + "<" + String.zeroWidthSpace) + } + if fullName.isEmpty { + fullName = fullName + firstNameStandardized.replacingOccurrences( of: "<", + with: String.zeroWidthSpace + "<" + String.zeroWidthSpace) + if !fullName.isEmpty { + fullName = fullName + " " + lastNameStandardized.replacingOccurrences( of: "<", + with: String.zeroWidthSpace + "<" + String.zeroWidthSpace) + } else { + fullName = fullName + lastNameStandardized.replacingOccurrences( of: "<", + with: String.zeroWidthSpace + "<" + String.zeroWidthSpace) + } + } + return fullName } - - var parsingErrors = [ParseError]() - cborData = ZLib.decompress(compressed) - if cborData.isEmpty { - parsingErrors.append(ParseError.zlib) + + public var certTypeString: String { + certificateType.l10n + (statement == nil ? "" : " \(statement.typeAddon)") } - guard let headerStr = CBOR.header(from: cborData)?.toString(), - let bodyStr = CBOR.payload(from: cborData)?.toString(), - let kid = CBOR.kid(from: cborData) else { - parsingErrors.append(ParseError.cbor) - throw CertificateParsingError.parsing(errors: parsingErrors) + + public var uvci: String { + statement?.uvci ?? "empty" } - - kidStr = KID.string(from: kid) - header = JSON(parseJSON: headerStr) - var body = JSON(parseJSON: bodyStr) - iat = Date(timeIntervalSince1970: Double(body["6"].int ?? 0)) - exp = Date(timeIntervalSince1970: Double(body["4"].int ?? 0)) - issCode = body["1"].string ?? "" - if body[ClaimKey.hCert.rawValue].exists() { - body = body[ClaimKey.hCert.rawValue] + + public var certificateType: HCertType { + switch statement { + case is VaccinationEntry: + return .vaccine + case is RecoveryEntry: + return .recovery + case is TestEntry: + return .test + default: + return .unknown + } } - if body[ClaimKey.euDgcV1.rawValue].exists() { - self.body = body[ClaimKey.euDgcV1.rawValue] - let parseBodyErrors = parseBodyV1() - if !parseBodyErrors.isEmpty { - throw CertificateParsingError.parsing(errors: parsingErrors) - } - } else { - print("Wrong EU_DGC Version!") - parsingErrors.append(.version(error: "Wrong EU_DGC Version!")) - throw CertificateParsingError.parsing(errors: parsingErrors) + + var testStatements: [TestEntry] { + return get(.testStatements).array?.compactMap {TestEntry(body: $0)} ?? [] + } + var vaccineStatements: [VaccinationEntry] { + return get(.vaccineStatements).array?.compactMap { VaccinationEntry(body: $0) } ?? [] + } + var recoveryStatements: [RecoveryEntry] { + return get(.recoveryStatements).array?.compactMap {RecoveryEntry(body: $0)} ?? [] + } + var statements: [HCertEntry] { + return testStatements + vaccineStatements + recoveryStatements + } + public var statement: HCertEntry! { + return statements.last } - } - private func get(_ attribute: AttributeKey) -> JSON { - var object = body - for key in attributeKeys[attribute] ?? [] { - object = object[key] + public var cryptographicallyValid: Bool { + if !CoreManager.config.checkSignatures { + return true + } + guard let delegate = CoreManager.publicKeyEncoder else { return false } + + for key in delegate.getEncodedPublicKeys(for: kidStr) { + if !X509.isCertificateValid(cert: key) { + return false + } + return X509.checkisSuitable(cert:key, certType: certificateType) + } + return false } - return object - } -} -extension HCert { - private func parseBodyV1() -> [ParseError] { - guard let schema = JSON(parseJSON: euDgcSchemaV1).dictionaryObject, let bodyDict = body.dictionaryObject else { - return [.json(error: "Schema Validation failed")] + public var certHash: String { + CBOR.hash(from: cborData) + } + public var keyPair: SecKey! { + Enclave.loadOrGenerateKey(with: uvci) } - do { - var bodyErrors = [ParseError]() - let validation = try validate(bodyDict, schema: schema) - validation.errors?.forEach { bodyErrors.append(.json(error: $0.description)) } + public static var clock: Date { + clockOverride ?? Date() + } + public static var clockOverride: Date? + + public init(from payload: String, ruleCountryCode: String? = nil) throws { + var copyPayload = payload + self.isRevoked = false + if let firstChar = payload.first { + if firstChar == "x" { + self.isRevoked = true + copyPayload.removeFirst() + } + } + if HCertConfig.checkCH1PreffixExist(copyPayload) { + fullPayloadString = copyPayload + payloadString = HCertConfig.parsePrefix(copyPayload) + } else { + let supportedPrefix = HCertConfig.supportedPrefixes.first ?? "" + fullPayloadString = supportedPrefix + copyPayload + payloadString = copyPayload + } + + self.ruleCountryCode = ruleCountryCode + guard let compressed = try? payloadString.fromBase45() else { + throw CertificateParsingError.parsing(errors: [ParseError.base45]) + } + + var parsingErrors = [ParseError]() + cborData = ZLib.decompress(compressed) + if cborData.isEmpty { + parsingErrors.append(ParseError.zlib) + } + guard let headerStr = CBOR.header(from: cborData)?.toString(), + let bodyStr = CBOR.payload(from: cborData)?.toString(), + let kid = CBOR.kid(from: cborData) else { + parsingErrors.append(ParseError.cbor) + throw CertificateParsingError.parsing(errors: parsingErrors) + } + + kidStr = KID.string(from: kid) + header = JSON(parseJSON: headerStr) + var body = JSON(parseJSON: bodyStr) + iat = Date(timeIntervalSince1970: Double(body["6"].int ?? 0)) + exp = Date(timeIntervalSince1970: Double(body["4"].int ?? 0)) + issCode = body["1"].string ?? "" + if body[ClaimKey.hCert.rawValue].exists() { + body = body[ClaimKey.hCert.rawValue] + } - #if DEBUG - if CoreManager.shared.config.debugPrintJsonErrors { - validation.errors?.forEach { print($0.description) } + if body[ClaimKey.euDgcV1.rawValue].exists() { + self.body = body[ClaimKey.euDgcV1.rawValue] + let parseBodyErrors = parseBodyV1() + if !parseBodyErrors.isEmpty { + throw CertificateParsingError.parsing(errors: parsingErrors) + } + } else { + print("Wrong EU_DGC Version!") + parsingErrors.append(.version(error: "Wrong EU_DGC Version!")) + throw CertificateParsingError.parsing(errors: parsingErrors) } - #endif - return bodyErrors - - } catch { - return [.json(error: "Body Validation failed")] - } - } + } + + private func get(_ attribute: AttributeKey) -> JSON { + var object = body + for key in attributeKeys[attribute] ?? [] { + object = object[key] + } + return object + } +} + +extension HCert { + private func parseBodyV1() -> [ParseError] { + guard let schema = JSON(parseJSON: euDgcSchemaV1).dictionaryObject, + let bodyDict = body.dictionaryObject else { + return [.json(error: "Schema Validation failed")] + } + do { + var bodyErrors = [ParseError]() + let validation = try validate(bodyDict, schema: schema) + validation.errors?.forEach { bodyErrors.append(.json(error: $0.description)) } + + #if DEBUG + if CoreManager.config.debugPrintJsonErrors { + validation.errors?.forEach { print($0.description) } + } + #endif + return bodyErrors + } catch { + return [.json(error: "Body Validation failed")] + } + } } // 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 - } + public var uvciHash: Data? { + if statement?.uvci != nil, + let uvciData = uvci.data(using: .utf8) { + let data = SHA256.sha256(data: uvciData) + //return data.dropLast(16) + return data + } else { + return nil + } + } - private var isECDSASigned: Bool { - guard let cborHeader = CBOR.header(from: cborData), - let algorithmField = cborHeader[1] else { - return false - } - - let coseES256Algorithm = -7 + public var countryCodeUvciHash: Data? { + if statement?.uvci != nil, let countryCodeUvciData = (issCode + uvci).data(using: .utf8) { + let data = SHA256.sha256(data: countryCodeUvciData) + return data + } else { + return nil + } + } - return algorithmField == SwiftCBOR.CBOR(integerLiteral: coseES256Algorithm) + public var signatureHash: Data? { + guard var signatureBytesToHash = CBOR.unwrap(data: cborData)?.signatureBytes else { return nil } + + if isECDSASigned { + signatureBytesToHash = Array(signatureBytesToHash.prefix(32)) + } + let data = SHA256.sha256(data: Data(signatureBytesToHash)) + return data + } + + 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 { + func lookUp(mode: RevocationMode, hash: Data) -> CertLookUp { switch mode { case .point: - return CertLookUp(kid: kidStr, section: payloadString[0], x: "null", y: "null") + return CertLookUp(kid: kidStr, section: hash.hexString[0], x: "null", y: "null") case .vector: - return CertLookUp(kid: kidStr, section: payloadString[1], x: payloadString[0], y: "null") + return CertLookUp(kid: kidStr, section: hash.hexString[1], x: payloadString[0], y: "null") case .coordinate: - return CertLookUp(kid: kidStr, section: payloadString[2], x: payloadString[0], y: payloadString[1]) + return CertLookUp(kid: kidStr, section: hash.hexString[2], x: payloadString[0], y: payloadString[1]) } } } diff --git a/Sources/Models/InfoSection.swift b/Sources/Models/InfoSection.swift index 1afe5af..5aa6e64 100644 --- a/Sources/Models/InfoSection.swift +++ b/Sources/Models/InfoSection.swift @@ -34,10 +34,10 @@ public class InfoSection { public var sectionItems: [InfoSection] = [] public var isExpanded: Bool = false public var countryName: String? - public let ruleValidationResult: RuleValidationResult + public let ruleValidationResult: HCertValidity public init(header: String, content: String, style: InfoSectionStyle = .normal, - isPrivate: Bool = false, countryName: String? = nil, ruleValidationResult: RuleValidationResult = .open) { + isPrivate: Bool = false, countryName: String? = nil, ruleValidationResult: HCertValidity = .ruleInvalid) { self.header = header self.content = content self.countryName = countryName diff --git a/Sources/Models/SectionBuilder.swift b/Sources/Models/SectionBuilder.swift index 0f15543..9bf594d 100644 --- a/Sources/Models/SectionBuilder.swift +++ b/Sources/Models/SectionBuilder.swift @@ -34,12 +34,51 @@ public class SectionBuilder { private let validityState: ValidityState private let certificate: HCert - public init(with cert: HCert, validity: ValidityState) { + public init(with cert: HCert, validity: ValidityState, for appType: AppType) { self.certificate = cert self.validityState = validity + + if validity.revocationValidity == .revoked { + self.makeRevocationSections() + } else { + self.makeSections(for: appType) + if let section = validityState.infoRulesSection { + self.makeSectionForRuleError(ruleSection: section, for: appType) + } + } + } + + // MARK: private section + private func makeRevocationSections() { + infoSection.removeAll() + + let hSection = InfoSection(header: "Certificate Type".localized, content: certificate.certTypeString) + infoSection += [hSection] + + let rSection = InfoSection(header: "Reason for Invalidity".localized, content: "Certificate has been revoked".localized) + infoSection += [rSection] + + let famSection = InfoSection( header: "Standardised Family Name".localized, + content: certificate.lastNameStandardized.replacingOccurrences( of: "<", + with: String.zeroWidthSpace + "<" + String.zeroWidthSpace), style: .fixedWidthFont) + infoSection += [famSection] + + infoSection += [InfoSection( header: "Standardised Given Name".localized, + 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] + } } - public func makeSections(for appType: AppType) { + private func makeSections(for appType: AppType) { infoSection.removeAll() switch appType { case .verifier: @@ -63,13 +102,13 @@ public class SectionBuilder { let hSection = InfoSection(header: "Certificate Type".localized, content: certificate.certTypeString ) infoSection += [hSection] - guard validityState.revocationValidity != .revocated else { - let rSection = InfoSection(header: "Reason for Invalidity".localized, content: "Certificate was revoked".localized) + guard validityState.revocationValidity != .revoked else { + let rSection = InfoSection(header: "Reason for Invalidity".localized, content: "Certificate has been revoked".localized) infoSection += [rSection] return } - guard validityState.isValid else { + guard validityState.validityFailures.isEmpty else { let vSection = InfoSection(header: "Reason for Invalidity".localized, content: validityState.validityFailures.joined(separator: " ")) infoSection += [vSection] @@ -94,18 +133,17 @@ public class SectionBuilder { } } - // MARK: private section - private func makeSectionsForVerifier(includeInvalidSection: Bool = true) { + private func makeSectionsForVerifier(includeInvalidSection: Bool = true) { 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) + if validityState.revocationValidity == .revoked { + let rSection = InfoSection(header: "Reason for Invalidity".localized, content: "Certificate has been revoked".localized) infoSection += [rSection] return } - if !validityState.isValid { + if !validityState.validityFailures.isEmpty { let vSection = InfoSection(header: "Reason for Invalidity".localized, content: validityState.validityFailures.joined(separator: " ")) infoSection += [vSection] @@ -137,10 +175,10 @@ public class SectionBuilder { 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) + if validityState.revocationValidity == .revoked { + let rSection = InfoSection(header: "Reason for Invalidity".localized, content: "Certificate has been revoked".localized) infoSection += [rSection] - } else if !validityState.isValid { + } else if !validityState.validityFailures.isEmpty { let hSection = InfoSection(header: "Reason for Invalidity".localized, content: validityState.validityFailures.joined(separator: " ")) infoSection += [hSection] @@ -163,10 +201,10 @@ public class SectionBuilder { 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) + if validityState.revocationValidity == .revoked { + let rSection = InfoSection(header: "Reason for Invalidity".localized, content: "Certificate has been revoked".localized) infoSection += [rSection] - } else if !validityState.isValid { + } else if !validityState.validityFailures.isEmpty { let hSection = InfoSection(header: "Reason for Invalidity".localized, content: validityState.validityFailures.joined(separator: " ")) infoSection += [hSection] @@ -188,10 +226,10 @@ public class SectionBuilder { 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) + if validityState.revocationValidity == .revoked { + let rSection = InfoSection(header: "Reason for Invalidity".localized, content: "Certificate has been revoked".localized) infoSection += [rSection] - } else if !validityState.isValid { + } else if !validityState.validityFailures.isEmpty { let vSection = InfoSection(header: "Reason for Invalidity".localized, content: validityState.validityFailures.joined(separator: " ")) infoSection += [vSection] diff --git a/Sources/Models/SecureStorage.swift b/Sources/Models/SecureStorage.swift index 620c362..eb926e0 100644 --- a/Sources/Models/SecureStorage.swift +++ b/Sources/Models/SecureStorage.swift @@ -28,116 +28,110 @@ import Foundation public enum DataOperationError: Error { - case noInputData - case initializationError - case encodindFailure - case signingFailure - case dataError(description: String) + case noInputData + case initializationError + case encodindFailure + case signingFailure + case dataError(description: String) } + public enum DataOperationResult { - case success(Bool) - case failure(Error) + case noData + case success + case failure(DataOperationError) } public typealias DataCompletionHandler = (DataOperationResult) -> Void struct SecureDB: Codable { - let data: Data - let signature: Data + let data: Data + let signature: Data } public class SecureStorage { - - lazy var databaseURL: URL = { - let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) - let documentsDirectory = paths[0] - let urlPath = URL(fileURLWithPath: documentsDirectory) - let prodURL = urlPath.appendingPathComponent("\(fileName).db") - return prodURL - }() - - let fileName: String - lazy var secureStorageKey = Enclave.loadOrGenerateKey(with: "secureStorageKey") - - public init(fileName: String) { - self.fileName = fileName - } + lazy var databaseURL: URL = { + let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) + let documentsDirectory = paths[0] + let urlPath = URL(fileURLWithPath: documentsDirectory) + let prodURL = urlPath.appendingPathComponent("\(fileName).db") + return prodURL + }() + + let fileName: String + lazy var secureStorageKey = Enclave.loadOrGenerateKey(with: "secureStorageKey") - /** - Loads encrypted db and overrides it with an empty one if that fails. - */ - public func loadOverride(fallback: T, completion: @escaping ((T?) -> Void)) { - if !FileManager.default.fileExists(atPath: databaseURL.path) { - self.save(fallback) { _ in - self.load(completion: completion) - } - } else { - self.load(completion: completion) + public init(fileName: String) { + self.fileName = fileName } - } - public func loadStoredData(fallback: T, completion: @escaping ((T?) -> Void)) { - if !FileManager.default.fileExists(atPath: databaseURL.path) { - self.save(fallback) { _ in - self.load(completion: completion) - } + /** + Loads encrypted db and overrides it with an empty one if that fails. + */ + + public func loadStoredData(fallback: T, completion: @escaping ((T?) -> Void)) { + if !FileManager.default.fileExists(atPath: databaseURL.path) { + self.save(fallback) { _ in + self.load(completion: completion) + } - } else { - self.load(completion: completion) + } else { + self.load(completion: completion) + } } - } - - public func load(completion: @escaping ((T?) -> Void)) { - guard let (data, signature) = read(), - let key = secureStorageKey, - Enclave.verify(data: data, signature: signature, with: key).0 - else { - completion(nil) - return + + private func load(completion: @escaping ((T?) -> Void)) { + guard let (data, signature) = read(), + let key = secureStorageKey, + Enclave.verify(data: data, signature: signature, with: key).0 + else { + completion(nil) + return + } + + Enclave.decrypt(data: data, with: key) { decrypted, err in + guard let decrypted = decrypted, err == nil, + let data = try? JSONDecoder().decode(T.self, from: decrypted) + else { + completion(nil) + return + } + completion(data) + } } - Enclave.decrypt(data: data, with: key) { decrypted, err in - guard let decrypted = decrypted, err == nil, - let data = try? JSONDecoder().decode(T.self, from: decrypted) - else { - completion(nil) - return - } - completion(data) - } - } - public func save(_ instance: T, completion: @escaping DataCompletionHandler) { - guard let data = try? JSONEncoder().encode(instance), - let key = secureStorageKey, - let encrypted = Enclave.encrypt(data: data, with: key).0 - else { - completion(.failure(DataOperationError.encodindFailure)) - return + public func save(_ instance: T, completion: @escaping DataCompletionHandler) { + guard let data = try? JSONEncoder().encode(instance), + let key = secureStorageKey, + let encrypted = Enclave.encrypt(data: data, with: key).0 + else { + completion(.failure(DataOperationError.encodindFailure)) + return + } + + Enclave.sign(data: encrypted, with: key, completion: { [unowned self] result, error in + guard let signature = result, error == nil else { + completion(.failure(DataOperationError.dataError(description: error!))) + return + } + self.write(data: encrypted, signature: signature, completion: completion) + }) } - Enclave.sign(data: encrypted, with: key, completion: { [unowned self] result, error in - guard let signature = result, error == nil else { - completion(.failure(DataOperationError.dataError(description: error!))) - return - } - self.write(data: encrypted, signature: signature, completion: completion) - }) - } - func write(data: Data, signature: Data, completion: DataCompletionHandler) { - do { - let rawData = try JSONEncoder().encode(SecureDB(data: data, signature: signature)) - try rawData.write(to: databaseURL) - completion(.success(true)) - } catch { - completion(.failure(DataOperationError.encodindFailure)) - return + func write(data: Data, signature: Data, completion: DataCompletionHandler) { + do { + let rawData = try JSONEncoder().encode(SecureDB(data: data, signature: signature)) + try rawData.write(to: databaseURL) + completion(.success) + } catch { + completion(.failure(DataOperationError.encodindFailure)) + return + } } - } - func read() -> (Data, Data)? { - guard let rawData = try? Data(contentsOf: databaseURL, options: [.uncached]), - let result = try? JSONDecoder().decode(SecureDB.self, from: rawData) - else { return nil } - return (result.data, result.signature) - } + func read() -> (Data, Data)? { + guard let rawData = try? Data(contentsOf: databaseURL, options: [.uncached]), + let result = try? JSONDecoder().decode(SecureDB.self, from: rawData) + else { return nil } + return (result.data, result.signature) + } } diff --git a/Sources/Models/ValidityState.swift b/Sources/Models/ValidityState.swift index 25b6fe9..e990cf0 100644 --- a/Sources/Models/ValidityState.swift +++ b/Sources/Models/ValidityState.swift @@ -26,50 +26,25 @@ // -import Foundation - public struct 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 var issuerValidity: HCertValidity - public var destinationValidity: HCertValidity - public var travalerValidity: HCertValidity - public var allRulesValidity: HCertValidity - public var revocationValidity: HCertValidity - + public let issuerValidity: HCertValidity + public let destinationValidity: HCertValidity + public let travalerValidity: HCertValidity + public let allRulesValidity: HCertValidity + public let revocationValidity: HCertValidity public let validityFailures: [String] - public var infoRulesSection: InfoSection? + public let infoRulesSection: InfoSection? public var isNotPassed: Bool { return technicalValidity != .valid || - issuerInvalidation != .passed || destinationAcceptence != .passed || travalerAcceptence != .passed + issuerValidity != .valid || + destinationValidity != .valid || + revocationValidity != .valid || + travalerValidity != .valid } - 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, @@ -88,57 +63,4 @@ public struct ValidityState { self.validityFailures = validityFailures self.infoRulesSection = infoRulesSection } - - private var validity: HCertValidity { - return validityFailures.isEmpty ? .valid : .invalid - } - - public var isValid: Bool { - return validityFailures.isEmpty - } - - public var issuerInvalidation: RuleValidationResult { - let ruleResult: RuleValidationResult - switch issuerValidity { - 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 = .failed - case .ruleInvalid: - ruleResult = .open - case .revocated: - ruleResult = .failed - } - return ruleResult - } - - public var travalerAcceptence: RuleValidationResult { - let ruleResult: RuleValidationResult - switch travalerValidity { - case .valid: - ruleResult = .passed - case .invalid: - ruleResult = .failed - case .ruleInvalid: - ruleResult = .open - case .revocated: - ruleResult = .failed - } - return ruleResult - } } diff --git a/Sources/Networking/RequestFactory.swift b/Sources/Networking/RequestFactory.swift index 96eb0c2..4ce9a11 100644 --- a/Sources/Networking/RequestFactory.swift +++ b/Sources/Networking/RequestFactory.swift @@ -19,7 +19,7 @@ internal enum ServiceConfig: String { 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) @@ -50,27 +50,40 @@ internal class RequestFactory { } } -// TODO add protocol extension RequestFactory { - static func serviceGetRequest(path: String, etag: String? = nil) -> URLRequest? { + static func serviceGetRequest(path: String, etag: String? = nil, dateStr: String? = nil) -> URLRequest? { guard let url = URL(string: path) else { return nil } + var headerDict: [String : String] = ["Content-Type" : "application/json"] + if etag == nil { + headerDict["If-None-Match"] = "" + } else { + headerDict["If-Match"] = etag! + headerDict["X-SLICE-FILTER-TYPE"] = sliceType.rawValue + } + if dateStr != nil { + headerDict["If-Modified-Since"] = dateStr! + } - 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) + let result = request(url: url, query: nil, headerFields: headerDict) return result } - - - static func servicePostRequest(path: String, body: Data?, etag: String? = nil) -> URLRequest? { + + static func servicePostRequest(path: String, body: Data?, etag: String? = nil, dateStr: 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"] + + var headerDict: [String : String] = ["Content-Type" : "application/json"] + if etag == nil { + headerDict["If-None-Match"] = "" + } else { + headerDict["If-Match"] = etag! + headerDict["X-SLICE-FILTER-TYPE"] = sliceType.rawValue + } + if dateStr != nil { + headerDict["If-Modified-Since"] = dateStr! + } - let result = postRequest(url: url, HTTPBody: body, headerFields: headers) + let result = postRequest(url: url, HTTPBody: body, headerFields: headerDict) return result } } diff --git a/Sources/Networking/RevocationService.swift b/Sources/Networking/RevocationService.swift index 59f8dd6..71478da 100644 --- a/Sources/Networking/RevocationService.swift +++ b/Sources/Networking/RevocationService.swift @@ -23,7 +23,7 @@ public typealias PartitionListCompletion = ([PartitionModel]?, String?, Revocati public typealias JSONDataTaskCompletion = (T?, String?, RevocationError?) -> Void public typealias ZIPDataTaskCompletion = (Data?, RevocationError?) -> Void -public final class RevocationService { +public final class RevocationService: RevocationServiceProtocol { var baseServiceURLPath: String var allChunks = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"] @@ -54,29 +54,29 @@ public final class RevocationService { // description: Returns a list of all available partitions. // paths: /lists/{kid}/partitions (get) - public func getRevocationPartitions(for kid: String, completion: @escaping PartitionListCompletion) { + public func getRevocationPartitions(for kid: String, dateString dateStr: 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 { + guard let request = RequestFactory.serviceGetRequest(path: path, etag: eTag, dateStr: dateStr) 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) { + public func getRevocationPartitions(for kid: String, id: String, dateString dateStr: 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 { + guard let request = RequestFactory.serviceGetRequest(path: path, etag: eTag, dateStr: dateStr) else { completion(nil, nil, .badRequest(path: path)) return } @@ -87,8 +87,8 @@ public final class RevocationService { // 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) { + + public func getRevocationPartitionChunks(for kid: String, id: String, cids: [String]? = nil, dateString dateStr: String?, 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 } @@ -97,7 +97,7 @@ public final class RevocationService { 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 { + guard let request = RequestFactory.servicePostRequest(path: path, body: postData, etag: eTag, dateStr: dateStr) else { completion(nil, .badRequest(path: path)) return } @@ -109,12 +109,12 @@ public final class RevocationService { //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) { + public func getRevocationPartitionChunk(for kid: String, id: String, cid: String, dateString dateStr: 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 { + guard let request = RequestFactory.serviceGetRequest(path: path, etag: eTag, dateStr: dateStr) else { completion(nil, .badRequest(path: path)) return } @@ -126,7 +126,7 @@ public final class RevocationService { // 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]?, + public func getRevocationPartitionChunkSlice(for kid: String, id: String, cid: String, sids: [String]?, dateString dateStr: String?, completion: @escaping ZIPDataTaskCompletion) { let partitionIDComponent = String(format: ServiceConfig.linkForChunkSlices.rawValue, kid, id, cid) let path = baseServiceURLPath + partitionIDComponent @@ -136,7 +136,7 @@ public final class RevocationService { let encoder = JSONEncoder() let postData = try? encoder.encode(sids) - guard let request = RequestFactory.servicePostRequest(path: path, body: postData, etag: eTag) else { + guard let request = RequestFactory.servicePostRequest(path: path, body: postData, etag: eTag, dateStr: dateStr) else { completion(nil, .badRequest(path: path)) return } @@ -148,13 +148,13 @@ public final class RevocationService { //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, + public func getRevocationPartitionChunkSliceSingle(for kid: String, id: String, cid: String, sid: String, dateString dateStr: 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 { + guard let request = RequestFactory.serviceGetRequest(path: path, etag: eTag, dateStr: dateStr) else { completion(nil, .badRequest(path: path)) return } @@ -164,15 +164,21 @@ public final class RevocationService { // 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 httpResponse = response as? HTTPURLResponse else { + completion(nil, nil, .network(reason: "No HTTPURLResponse")) + return + } + + guard defaultResponseValidation(statusCode: httpResponse.statusCode) else { + completion(nil, nil, .failedValidation(status: httpResponse.statusCode)) + return + } + guard let data = data else { completion(nil, nil, .nodata) return diff --git a/Sources/Networking/RevocationServiceProtocol.swift b/Sources/Networking/RevocationServiceProtocol.swift new file mode 100644 index 0000000..3995e40 --- /dev/null +++ b/Sources/Networking/RevocationServiceProtocol.swift @@ -0,0 +1,18 @@ +// +// RevocationServiceProtocol.swift +// +// +// Created by Denis Melenevsky on 04.03.2022. +// + +import Foundation + +public protocol RevocationServiceProtocol { + func getRevocationLists(completion: @escaping RevocationListCompletion) + func getRevocationPartitions(for kid: String, dateString dateStr: String?, completion: @escaping PartitionListCompletion) + func getRevocationPartitions(for kid: String, id: String, dateString dateStr: String?, completion: @escaping PartitionListCompletion) + func getRevocationPartitionChunks(for kid: String, id: String, cids: [String]?, dateString dateStr: String?, completion: @escaping ZIPDataTaskCompletion) + func getRevocationPartitionChunk(for kid: String, id: String, cid: String, dateString dateStr: String?, completion: @escaping ZIPDataTaskCompletion) + func getRevocationPartitionChunkSlice(for kid: String, id: String, cid: String, sids: [String]?, dateString dateStr: String?, completion: @escaping ZIPDataTaskCompletion) + func getRevocationPartitionChunkSliceSingle(for kid: String, id: String, cid: String, sid: String, dateString dateStr: String?, completion: @escaping ZIPDataTaskCompletion) +} diff --git a/Sources/SupportingFiles/de.lproj/Localizable.strings b/Sources/SupportingFiles/de.lproj/Localizable.strings index daca000..3cb2f88 100644 --- a/Sources/SupportingFiles/de.lproj/Localizable.strings +++ b/Sources/SupportingFiles/de.lproj/Localizable.strings @@ -28,9 +28,10 @@ "enum.HCertType.test" = "Testergebnis"; "enum.HCertType.vaccine" = "Impfung"; "enum.HCertType.recovery" = "Recovery"; -"Valid" = "Genesung"; +"Valid" = "Gültig"; "Invalid" = "Ungültig"; "Limited validity" = "Begrenzte Gültigkeit"; +"Revoked" = "Revoked"; "Certificate Type" = "Zertifikatstyp"; "Date of birth" = "Geburtsdatum"; diff --git a/Sources/SupportingFiles/en.lproj/Localizable.strings b/Sources/SupportingFiles/en.lproj/Localizable.strings index e61b2e7..2397e54 100644 --- a/Sources/SupportingFiles/en.lproj/Localizable.strings +++ b/Sources/SupportingFiles/en.lproj/Localizable.strings @@ -31,6 +31,7 @@ "Valid" = "Valid"; "Invalid" = "Invalid"; "Limited validity" = "Limited validity"; +"Revoked" = "Revoked"; "Certificate Type" = "Certificate Type"; "Date of birth" = "Date of birth";