diff --git a/DGCAVerifier.xcodeproj/project.pbxproj b/DGCAVerifier.xcodeproj/project.pbxproj index 8fbc1b0..61ef716 100644 --- a/DGCAVerifier.xcodeproj/project.pbxproj +++ b/DGCAVerifier.xcodeproj/project.pbxproj @@ -48,6 +48,9 @@ CEC2C4C32625ED030056E406 /* JWK.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC2C4C02625ED030056E406 /* JWK.swift */; }; CEC2C4C42625ED030056E406 /* Base45.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC2C4C12625ED030056E406 /* Base45.swift */; }; CED2726026398683003D47A9 /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED2725F26398683003D47A9 /* UIFont.swift */; }; + CED272652639A1DF003D47A9 /* VaccinationEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED272642639A1DF003D47A9 /* VaccinationEntry.swift */; }; + CED2726D2639A403003D47A9 /* TestEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED2726C2639A403003D47A9 /* TestEntry.swift */; }; + CED2727B2639A990003D47A9 /* RecoveryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED2727A2639A990003D47A9 /* RecoveryEntry.swift */; }; CEFAD86D2625F164009AFEF9 /* Signature.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFAD86C2625F164009AFEF9 /* Signature.swift */; }; CEFAD8722625F29E009AFEF9 /* String+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFAD8712625F29E009AFEF9 /* String+JSON.swift */; }; CEFAD87A26271414009AFEF9 /* COSE.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFAD87926271414009AFEF9 /* COSE.swift */; }; @@ -115,6 +118,9 @@ CEC2C4C02625ED030056E406 /* JWK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWK.swift; sourceTree = ""; }; CEC2C4C12625ED030056E406 /* Base45.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Base45.swift; sourceTree = ""; }; CED2725F26398683003D47A9 /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = ""; }; + CED272642639A1DF003D47A9 /* VaccinationEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaccinationEntry.swift; sourceTree = ""; }; + CED2726C2639A403003D47A9 /* TestEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestEntry.swift; sourceTree = ""; }; + CED2727A2639A990003D47A9 /* RecoveryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryEntry.swift; sourceTree = ""; }; CEFAD86C2625F164009AFEF9 /* Signature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signature.swift; sourceTree = ""; }; CEFAD8712625F29E009AFEF9 /* String+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+JSON.swift"; sourceTree = ""; }; CEFAD87926271414009AFEF9 /* COSE.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = COSE.swift; sourceTree = ""; }; @@ -235,6 +241,9 @@ children = ( CE157F8C262E24F900FE4821 /* HCert.swift */, CE1D1EF5263597A2004C8919 /* LocalData.swift */, + CED272642639A1DF003D47A9 /* VaccinationEntry.swift */, + CED2726C2639A403003D47A9 /* TestEntry.swift */, + CED2727A2639A990003D47A9 /* RecoveryEntry.swift */, ); path = Models; sourceTree = ""; @@ -451,10 +460,12 @@ CE13CF0A262DCDDA0070C80E /* CertificateViewer.swift in Sources */, CEC2C4C32625ED030056E406 /* JWK.swift in Sources */, CEC2C4C42625ED030056E406 /* Base45.swift in Sources */, + CED272652639A1DF003D47A9 /* VaccinationEntry.swift in Sources */, CE891300263570CF00CB92AF /* Enclave.swift in Sources */, CE8912EA26321DAA00CB92AF /* SHA256.swift in Sources */, CE3CC9442628C2130079FB78 /* CBOR.swift in Sources */, CE44799226306C86009A836B /* String.swift in Sources */, + CED2726D2639A403003D47A9 /* TestEntry.swift in Sources */, CE37B643263867D700DEE13D /* SecureBackground.swift in Sources */, CE8912F52634C60E00CB92AF /* GatewayConnection.swift in Sources */, CE582DC12635AE5F008F35D7 /* SecureStorage.swift in Sources */, @@ -473,6 +484,7 @@ CE13CF23262DDF810070C80E /* RoundedButton.swift in Sources */, CEA6D6EC261F8D2700715333 /* AppDelegate.swift in Sources */, CE891305263581D900CB92AF /* Home.swift in Sources */, + CED2727B2639A990003D47A9 /* RecoveryEntry.swift in Sources */, CEA15563262F6DAB0024B7AC /* ChildDismissedDelegate.swift in Sources */, CEFAD8722625F29E009AFEF9 /* String+JSON.swift in Sources */, CEC2C4C22625ED030056E406 /* ZLib.swift in Sources */, diff --git a/DGCAVerifier/Extensions/Date.swift b/DGCAVerifier/Extensions/Date.swift index 95953a8..229a607 100644 --- a/DGCAVerifier/Extensions/Date.swift +++ b/DGCAVerifier/Extensions/Date.swift @@ -43,6 +43,7 @@ extension Date { static let isoFormatter = formatter(for: "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") static let dateFormatter = formatter(for: "yyyy-MM-dd") + static let dateTimeFormatter = formatter(for: "yyyy-MM-dd HH:mm '(UTC)'") var isoString: String { Date.isoFormatter.string(from: self) @@ -50,6 +51,9 @@ extension Date { var dateString: String { Date.dateFormatter.string(from: self) } + var dateTimeString: String { + Date.dateTimeFormatter.string(from: self) + } init?(isoString: String) { guard let date = Date.isoFormatter.date(from: isoString) else { diff --git a/DGCAVerifier/Models/HCert.swift b/DGCAVerifier/Models/HCert.swift index 4d34f25..1144c0b 100644 --- a/DGCAVerifier/Models/HCert.swift +++ b/DGCAVerifier/Models/HCert.swift @@ -43,8 +43,6 @@ enum AttributeKey: String { case testStatements case vaccineStatements case recoveryStatements - case vaccineShotNo = "seq" - case vaccineShotTotal = "tot" } enum HCertType: String { @@ -59,19 +57,11 @@ enum HCertValidity { case invalid } -let identifierNames: [String: String] = [ - "PP": "Passport Number", - "NN": "National Person Identifier", - "CZ": "Citizenship Card Number", - "HC": "Health Card Number", -] - let attributeKeys: [AttributeKey: [String]] = [ .firstName: ["nam", "gn"], .lastName: ["nam", "fn"], .firstNameStandardized: ["nam", "gnt"], .lastNameStandardized: ["nam", "fnt"], - .gender: ["nam", "gen"], .dateOfBirth: ["dob"], .testStatements: ["t"], .vaccineStatements: ["v"], @@ -192,7 +182,7 @@ struct HCert { ), ] } - return info + return info + statement.info } var rawData: Data @@ -218,41 +208,60 @@ struct HCert { [] } - var testStatements: [JSON] { - return get(.testStatements).array ?? [] + var testStatements: [TestEntry] { + return get(.testStatements) + .array? + .compactMap { + TestEntry(body: $0) + } ?? [] } - var vaccineStatements: [JSON] { - return get(.vaccineStatements).array ?? [] + var vaccineStatements: [VaccinationEntry] { + return get(.vaccineStatements) + .array? + .compactMap { + VaccinationEntry(body: $0) + } ?? [] } - var recoveryStatements: [JSON] { - return get(.recoveryStatements).array ?? [] + var recoveryStatements: [RecoveryEntry] { + return get(.recoveryStatements) + .array? + .compactMap { + RecoveryEntry(body: $0) + } ?? [] } - var hasLastShot: Bool { - for statement in vaccineStatements { - let no = statement[AttributeKey.vaccineShotNo.rawValue].int ?? 1 - let total = statement[AttributeKey.vaccineShotTotal.rawValue].int ?? 2 - if no == total { - return true - } - } - return false + var statements: [HCertEntry] { + testStatements + vaccineStatements + recoveryStatements + } + var statement: HCertEntry! { + statements.last } var type: HCertType { - if hasLastShot { - return .vaccineTwo - } - if !vaccineStatements.isEmpty { + if let vaccine = statement as? VaccinationEntry { + if vaccine.doseNumber == vaccine.dosesTotal { + return .vaccineTwo + } return .vaccineOne } - if !recoveryStatements.isEmpty { + if statement is RecoveryEntry { return .recovery } return .test } var isValid: Bool { - return COSE.verify(rawData, with: LocalData.sharedInstance.encodedPublicKeys[kidStr] ?? "") + cryptographicallyValid && semanticallyValid + } + var cryptographicallyValid: Bool { + COSE.verify(rawData, with: LocalData.sharedInstance.encodedPublicKeys[kidStr] ?? "") + } + var semanticallyValid: Bool { + statement.isValid } var validity: HCertValidity { return isValid ? .valid : .invalid } } + +protocol HCertEntry { + var info: [InfoSection] { get } + var isValid: Bool { get } +} diff --git a/DGCAVerifier/Models/RecoveryEntry.swift b/DGCAVerifier/Models/RecoveryEntry.swift new file mode 100644 index 0000000..b4675eb --- /dev/null +++ b/DGCAVerifier/Models/RecoveryEntry.swift @@ -0,0 +1,83 @@ +// +/*- + * ---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 + */ +// +// TestResult.swift +// DGCAVerifier +// +// Created by Yannick Spreen on 4/28/21. +// + + +import Foundation +import SwiftyJSON + +struct RecoveryEntry: HCertEntry { + var info: [InfoSection] { + [ + InfoSection(header: "Valid Until", content: validUntil.dateString), + ] + } + + var isValid: Bool { + validFrom <= Date() && Date() <= validUntil + } + + enum Fields: String { + case diseaseTargeted = "tg" + case firstPositiveDate = "fr" + case country = "co" + case issuer = "is" + case validFrom = "df" + case validUntil = "du" + case uvci = "ci" + } + + init?(body: JSON) { + guard + let diseaseTargeted = body[Fields.diseaseTargeted.rawValue].string, + let firstPositiveDate = body[Fields.firstPositiveDate.rawValue].string, + let country = body[Fields.country.rawValue].string, + let issuer = body[Fields.issuer.rawValue].string, + let validFromStr = body[Fields.validFrom.rawValue].string, + let validUntilStr = body[Fields.validUntil.rawValue].string, + let validFrom = Date(dateString: validFromStr), + let validUntil = Date(dateString: validUntilStr), + let uvci = body[Fields.uvci.rawValue].string + else { + return nil + } + self.diseaseTargeted = diseaseTargeted + self.firstPositiveDate = firstPositiveDate + self.country = country + self.issuer = issuer + self.validFrom = validFrom + self.validUntil = validUntil + self.uvci = uvci + } + + var diseaseTargeted: String + var firstPositiveDate: String + var country: String + var issuer: String + var validFrom: Date + var validUntil: Date + var uvci: String +} diff --git a/DGCAVerifier/Models/TestEntry.swift b/DGCAVerifier/Models/TestEntry.swift new file mode 100644 index 0000000..ebecc7f --- /dev/null +++ b/DGCAVerifier/Models/TestEntry.swift @@ -0,0 +1,92 @@ +// +/*- + * ---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 + */ +// +// TestResult.swift +// DGCAVerifier +// +// Created by Yannick Spreen on 4/28/21. +// + + +import Foundation +import SwiftyJSON + +enum TestResult: String { + case detected = "260373001" + case notDetected = "260415000" +} + +struct TestEntry: HCertEntry { + var info: [InfoSection] { + [ + InfoSection(header: "Time of Sampling", content: sampleTime.dateTimeString), + InfoSection(header: "Test Result", content: resultNegative ? "Not Detected" : "Detected ⚠️"), + ] + } + + var isValid: Bool { + resultNegative && sampleTime < Date() + } + + enum Fields: String { + case diseaseTargeted = "tg" + case type = "tt" + case sampleTime = "sc" + case result = "tr" + case testCenter = "tc" + case country = "co" + case issuer = "is" + case uvci = "ci" + } + + init?(body: JSON) { + guard + let diseaseTargeted = body[Fields.diseaseTargeted.rawValue].string, + let type = body[Fields.type.rawValue].string, + let sampleTimeStr = body[Fields.sampleTime.rawValue].string, + let sampleTime = Date(rfc3339DateTimeString: sampleTimeStr), + let result = body[Fields.result.rawValue].string, + let testCenter = body[Fields.testCenter.rawValue].string, + let country = body[Fields.country.rawValue].string, + let issuer = body[Fields.issuer.rawValue].string, + let uvci = body[Fields.uvci.rawValue].string + else { + return nil + } + self.diseaseTargeted = diseaseTargeted + self.type = type + self.sampleTime = sampleTime + self.resultNegative = (TestResult(rawValue: result) == .notDetected) + self.testCenter = testCenter + self.country = country + self.issuer = issuer + self.uvci = uvci + } + + var diseaseTargeted: String + var type: String + var sampleTime: Date + var resultNegative: Bool + var testCenter: String + var country: String + var issuer: String + var uvci: String +} diff --git a/DGCAVerifier/Models/VaccinationEntry.swift b/DGCAVerifier/Models/VaccinationEntry.swift new file mode 100644 index 0000000..310bd94 --- /dev/null +++ b/DGCAVerifier/Models/VaccinationEntry.swift @@ -0,0 +1,73 @@ +// +/*- + * ---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 + */ +// +// TestResult.swift +// DGCAVerifier +// +// Created by Yannick Spreen on 4/28/21. +// + + +import Foundation +import SwiftyJSON + +struct VaccinationEntry: HCertEntry { + var info: [InfoSection] { + [ + InfoSection(header: "Date of Vaccination", content: date.dateString) + ] + } + + var isValid: Bool { + date < Date() + } + + enum Fields: String { + case diseaseTargeted = "tg" + case vaccineOrProphylaxis = "vp" + case medicalProduct = "mp" + case manufacturer = "ma" + case doseNumber = "dn" + case dosesTotal = "sd" + case date = "dt" + case country = "co" + case issuer = "is" + case uvci = "ci" + } + + init?(body: JSON) { + guard + let doseNumber = body[Fields.doseNumber.rawValue].int, + let dosesTotal = body[Fields.dosesTotal.rawValue].int, + let dateStr = body[Fields.date.rawValue].string, + let date = Date(dateString: dateStr) + else { + return nil + } + self.doseNumber = doseNumber + self.dosesTotal = dosesTotal + self.date = date + } + + var doseNumber: Int + var dosesTotal: Int + var date: Date +}