From 7d7173349551fdbe7afa0fceba9e1760035dbd4f Mon Sep 17 00:00:00 2001 From: Yannick Spreen Date: Wed, 28 Apr 2021 14:14:31 +0200 Subject: [PATCH] Add standardized names to cert viewer. --- DGCAVerifier.xcodeproj/project.pbxproj | 20 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- DGCAVerifier/Components/InfoCell.swift | 12 + DGCAVerifier/Extensions/String.swift | 4 + DGCAVerifier/Extensions/UIFont.swift | 65 ++ DGCAVerifier/Models/HCert.swift | 74 ++- DGCAVerifier/Services/GatewayConnection.swift | 9 +- .../SupportingFiles/EuDgcSchema.swift | 606 ++++++++++-------- DGCAVerifier/ViewControllers/Scan.swift | 2 +- 9 files changed, 466 insertions(+), 330 deletions(-) create mode 100644 DGCAVerifier/Extensions/UIFont.swift diff --git a/DGCAVerifier.xcodeproj/project.pbxproj b/DGCAVerifier.xcodeproj/project.pbxproj index 59b78c6..8fbc1b0 100644 --- a/DGCAVerifier.xcodeproj/project.pbxproj +++ b/DGCAVerifier.xcodeproj/project.pbxproj @@ -18,10 +18,10 @@ CE157F9B262E2A9F00FE4821 /* SwiftCBOR.CBOR.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE157F9A262E2A9F00FE4821 /* SwiftCBOR.CBOR.swift */; }; CE1BDF99262A4CD600766F97 /* X509.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1BDF98262A4CD600766F97 /* X509.swift */; }; CE1D1EF6263597A2004C8919 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1D1EF5263597A2004C8919 /* LocalData.swift */; }; + CE37B638263756BF00DEE13D /* JSONSchema in Frameworks */ = {isa = PBXBuildFile; productRef = CE37B637263756BF00DEE13D /* JSONSchema */; }; CE37B643263867D700DEE13D /* SecureBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE37B642263867D700DEE13D /* SecureBackground.swift */; }; CE3CC93C2628A7820079FB78 /* ASN1.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3CC93B2628A7820079FB78 /* ASN1.swift */; }; CE3CC9442628C2130079FB78 /* CBOR.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3CC9432628C2130079FB78 /* CBOR.swift */; }; - CE44798D26304D8F009A836B /* JSONSchema in Frameworks */ = {isa = PBXBuildFile; productRef = CE44798C26304D8F009A836B /* JSONSchema */; }; CE44799226306C86009A836B /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE44799126306C86009A836B /* String.swift */; }; CE44799726306C9B009A836B /* Data+Base45.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE44799626306C9B009A836B /* Data+Base45.swift */; }; CE582DC12635AE5F008F35D7 /* SecureStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE582DC02635AE5F008F35D7 /* SecureStorage.swift */; }; @@ -47,6 +47,7 @@ CEC2C4C22625ED030056E406 /* ZLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC2C4BF2625ED030056E406 /* ZLib.swift */; }; 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 */; }; 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 */; }; @@ -113,6 +114,7 @@ CEC2C4BF2625ED030056E406 /* ZLib.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZLib.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -126,8 +128,8 @@ buildActionMask = 2147483647; files = ( CE8912FB2634C6B900CB92AF /* Alamofire in Frameworks */, + CE37B638263756BF00DEE13D /* JSONSchema in Frameworks */, CE157F87262E24DE00FE4821 /* SwiftyJSON in Frameworks */, - CE44798D26304D8F009A836B /* JSONSchema in Frameworks */, CE7DE7FA2625EF18007E6694 /* SwiftCBOR in Frameworks */, CE13CF00262DCC180070C80E /* FloatingPanel in Frameworks */, ); @@ -190,6 +192,7 @@ CE157F9A262E2A9F00FE4821 /* SwiftCBOR.CBOR.swift */, CE44799126306C86009A836B /* String.swift */, CE44799626306C9B009A836B /* Data+Base45.swift */, + CED2725F26398683003D47A9 /* UIFont.swift */, ); path = Extensions; sourceTree = ""; @@ -318,8 +321,8 @@ CE7DE7F92625EF18007E6694 /* SwiftCBOR */, CE13CEFF262DCC180070C80E /* FloatingPanel */, CE157F86262E24DE00FE4821 /* SwiftyJSON */, - CE44798C26304D8F009A836B /* JSONSchema */, CE8912FA2634C6B900CB92AF /* Alamofire */, + CE37B637263756BF00DEE13D /* JSONSchema */, ); productName = DGCAVerifier; productReference = CEA6D6E8261F8D2700715333 /* DGCAVerifier.app */; @@ -396,8 +399,8 @@ CE7DE7F82625EF18007E6694 /* XCRemoteSwiftPackageReference "SwiftCBOR" */, CE13CEFE262DCC180070C80E /* XCRemoteSwiftPackageReference "FloatingPanel" */, CE157F85262E24DE00FE4821 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, - CE44798B26304D8F009A836B /* XCRemoteSwiftPackageReference "JSONSchema" */, CE8912F92634C6B900CB92AF /* XCRemoteSwiftPackageReference "Alamofire" */, + CE37B636263756BF00DEE13D /* XCRemoteSwiftPackageReference "JSONSchema" */, ); productRefGroup = CEA6D6E9261F8D2700715333 /* Products */; projectDirPath = ""; @@ -461,6 +464,7 @@ CE157F81262E1F7A00FE4821 /* Date.swift in Sources */, CE1D1EF6263597A2004C8919 /* LocalData.swift in Sources */, CE1BDF99262A4CD600766F97 /* X509.swift in Sources */, + CED2726026398683003D47A9 /* UIFont.swift in Sources */, CEA1555D262F63B30024B7AC /* EuDgcSchema.swift in Sources */, CEA6D6F0261F8D2700715333 /* Scan.swift in Sources */, CE3CC93C2628A7820079FB78 /* ASN1.swift in Sources */, @@ -829,9 +833,9 @@ minimumVersion = 5.0.1; }; }; - CE44798B26304D8F009A836B /* XCRemoteSwiftPackageReference "JSONSchema" */ = { + CE37B636263756BF00DEE13D /* XCRemoteSwiftPackageReference "JSONSchema" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/eu-digital-green-certificates/JSONSchema.swift"; + repositoryURL = "https://github.com/jnewc/JSONSchema.swift"; requirement = { branch = master; kind = branch; @@ -866,9 +870,9 @@ package = CE157F85262E24DE00FE4821 /* XCRemoteSwiftPackageReference "SwiftyJSON" */; productName = SwiftyJSON; }; - CE44798C26304D8F009A836B /* JSONSchema */ = { + CE37B637263756BF00DEE13D /* JSONSchema */ = { isa = XCSwiftPackageProductDependency; - package = CE44798B26304D8F009A836B /* XCRemoteSwiftPackageReference "JSONSchema" */; + package = CE37B636263756BF00DEE13D /* XCRemoteSwiftPackageReference "JSONSchema" */; productName = JSONSchema; }; CE7DE7F92625EF18007E6694 /* SwiftCBOR */ = { diff --git a/DGCAVerifier.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DGCAVerifier.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a3ac02e..0f69fab 100644 --- a/DGCAVerifier.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DGCAVerifier.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -21,10 +21,10 @@ }, { "package": "JSONSchema", - "repositoryURL": "https://github.com/eu-digital-green-certificates/JSONSchema.swift", + "repositoryURL": "https://github.com/jnewc/JSONSchema.swift", "state": { "branch": "master", - "revision": "adb480fe1c1e285c131bff465f641aebc986d736", + "revision": "4637ac1cf57745c29003738f36e1b6c232fbd1a6", "version": null } }, diff --git a/DGCAVerifier/Components/InfoCell.swift b/DGCAVerifier/Components/InfoCell.swift index 6f12a42..2477494 100644 --- a/DGCAVerifier/Components/InfoCell.swift +++ b/DGCAVerifier/Components/InfoCell.swift @@ -33,5 +33,17 @@ class InfoCell: UITableViewCell { func draw(_ info: InfoSection) { headerLabel?.text = info.header contentLabel?.text = info.content + let fontSize = contentLabel.font.pointSize + let fontWeight = contentLabel.font.weight + switch info.style { + case .fixedWidthFont: + if #available(iOS 13.0, *) { + contentLabel.font = .monospacedSystemFont(ofSize: fontSize, weight: fontWeight) + } else { + contentLabel.font = .monospacedDigitSystemFont(ofSize: fontSize, weight: fontWeight) + } + default: + contentLabel.font = .systemFont(ofSize: fontSize, weight: fontWeight) + } } } diff --git a/DGCAVerifier/Extensions/String.swift b/DGCAVerifier/Extensions/String.swift index e885178..b2c412a 100644 --- a/DGCAVerifier/Extensions/String.swift +++ b/DGCAVerifier/Extensions/String.swift @@ -30,4 +30,8 @@ extension String { subscript(i: Int) -> String { return String(self[index(startIndex, offsetBy: i)]) } + + static var zeroWidthSpace: String { + "\u{200B}" + } } diff --git a/DGCAVerifier/Extensions/UIFont.swift b/DGCAVerifier/Extensions/UIFont.swift new file mode 100644 index 0000000..3052885 --- /dev/null +++ b/DGCAVerifier/Extensions/UIFont.swift @@ -0,0 +1,65 @@ +// +/*- + * ---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 + */ +// +// UIFont.swift +// DGCAVerifier +// +// Created by Yannick Spreen on 4/28/21. +// +// https://stackoverflow.com/a/48688917/2585092 +// + +import UIKit + + +extension UIFont { + var weight: UIFont.Weight { + let fontAttributeKey = UIFontDescriptor.AttributeName.init(rawValue: "NSCTFontUIUsageAttribute") + + if let fontWeight = self.fontDescriptor.fontAttributes[fontAttributeKey] as? String { + switch fontWeight { + case "CTFontBoldUsage": + return UIFont.Weight.bold + case "CTFontBlackUsage": + return UIFont.Weight.black + case "CTFontHeavyUsage": + return UIFont.Weight.heavy + case "CTFontUltraLightUsage": + return UIFont.Weight.ultraLight + case "CTFontThinUsage": + return UIFont.Weight.thin + case "CTFontLightUsage": + return UIFont.Weight.light + case "CTFontMediumUsage": + return UIFont.Weight.medium + case "CTFontDemiUsage": + return UIFont.Weight.semibold + case "CTFontRegularUsage": + return UIFont.Weight.regular + + default: + return UIFont.Weight.regular + } + } + + return UIFont.Weight.regular + } +} diff --git a/DGCAVerifier/Models/HCert.swift b/DGCAVerifier/Models/HCert.swift index d89915f..4d34f25 100644 --- a/DGCAVerifier/Models/HCert.swift +++ b/DGCAVerifier/Models/HCert.swift @@ -36,15 +36,13 @@ enum ClaimKey: String { enum AttributeKey: String { case firstName case lastName + case firstNameStandardized + case lastNameStandardized case gender case dateOfBirth case testStatements case vaccineStatements case recoveryStatements - case personIdentifiers - case identifierType = "t" - case identifierCountry = "c" - case identifierValue = "i" case vaccineShotNo = "seq" case vaccineShotTotal = "tot" } @@ -69,19 +67,26 @@ let identifierNames: [String: String] = [ ] let attributeKeys: [AttributeKey: [String]] = [ - .firstName: ["sub", "gn"], - .lastName: ["sub", "fn"], - .gender: ["sub", "gen"], - .dateOfBirth: ["sub", "dob"], - .personIdentifiers: ["sub", "id"], - .testStatements: ["tst"], - .vaccineStatements: ["vac"], - .recoveryStatements: ["rec"], + .firstName: ["nam", "gn"], + .lastName: ["nam", "fn"], + .firstNameStandardized: ["nam", "gnt"], + .lastNameStandardized: ["nam", "fnt"], + .gender: ["nam", "gen"], + .dateOfBirth: ["dob"], + .testStatements: ["t"], + .vaccineStatements: ["v"], + .recoveryStatements: ["r"], ] +enum InfoSectionStyle { + case normal + case fixedWidthFont +} + struct InfoSection { var header: String var content: String + var style = InfoSectionStyle.normal } struct HCert { @@ -156,11 +161,35 @@ struct HCert { var info: [InfoSection] { var info = [ - InfoSection(header: "Certificate Type", content: type.rawValue), + InfoSection( + header: "Certificate Type", + content: type.rawValue + ), ] + personIdentifiers if let date = dateOfBirth { info += [ - InfoSection(header: "Date of Birth", content: date.localDateString), + InfoSection( + header: "Date of Birth", + content: date.localDateString + ), + ] + } + if let last = get(.lastNameStandardized).string { + info += [ + InfoSection( + header: "Standardised Family Name", + content: last.replacingOccurrences(of: "<", with: String.zeroWidthSpace + "<"), + style: .fixedWidthFont + ), + ] + } + if let first = get(.firstNameStandardized).string { + info += [ + InfoSection( + header: "Standardised Given Name", + content: first.replacingOccurrences(of: "<", with: String.zeroWidthSpace + "<"), + style: .fixedWidthFont + ), ] } return info @@ -185,21 +214,8 @@ struct HCert { } var personIdentifiers: [InfoSection] { - guard let identifiers = get(.personIdentifiers).array else { - return [] - } - return identifiers.map { - let type = $0[AttributeKey.identifierType.rawValue].string ?? "" - let country = $0[AttributeKey.identifierCountry.rawValue].string - let value = $0[AttributeKey.identifierValue.rawValue].string ?? "" - - var header = identifierNames[type] ?? "Unknown Identifier" - if let country = country { - header += " (\(country))" - } - - return InfoSection(header: header, content: value) - } + /// Note from author: Identifiers were previously planned, but got removed *for now*. + [] } var testStatements: [JSON] { diff --git a/DGCAVerifier/Services/GatewayConnection.swift b/DGCAVerifier/Services/GatewayConnection.swift index a05caa9..6c1179b 100644 --- a/DGCAVerifier/Services/GatewayConnection.swift +++ b/DGCAVerifier/Services/GatewayConnection.swift @@ -87,7 +87,7 @@ struct GatewayConnection { timer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { _ in trigger() } - timer?.tolerance = 1.0 + timer?.tolerance = 5.0 trigger() } @@ -113,11 +113,8 @@ struct GatewayConnection { static func status() { certStatus { validKids in - var invalid = [String]() - for key in LocalData.sharedInstance.encodedPublicKeys.keys { - if !validKids.contains(key) { - invalid.append(key) - } + let invalid = LocalData.sharedInstance.encodedPublicKeys.keys.filter { + !validKids.contains($0) } for key in invalid { LocalData.sharedInstance.encodedPublicKeys.removeValue(forKey: key) diff --git a/DGCAVerifier/SupportingFiles/EuDgcSchema.swift b/DGCAVerifier/SupportingFiles/EuDgcSchema.swift index 579ecbe..46f66b8 100644 --- a/DGCAVerifier/SupportingFiles/EuDgcSchema.swift +++ b/DGCAVerifier/SupportingFiles/EuDgcSchema.swift @@ -23,301 +23,339 @@ // // Created by Yannick Spreen on 4/20/21. // +// https://raw.githubusercontent.com/ehn-digital-green-development/ehn-dgc-schema/main/DGC.combined-schema.json +// import Foundation let EU_DGC_SCHEMA_V1 = """ { - "$schema": "http://json-schema.org/draft/2020-12/schema#", - "$id": "https://github.com/ehn-digital-green-development/hcert-schema/eu_dgc_v1", - "title": "Digital Green Certificate", - "description": "Proof of vaccination, test results or recovery according to EU eHN, version 1.0, including certificate metadata; According to 1) REGULATION OF THE EUROPEAN PARLIAMENT AND OF THE COUNCIL on a framework for the issuance, verification and acceptance of interoperable certificates on vaccination, testing and recovery to facilitate free movement during the COVID-19 pandemic (Digital Green Certificate) - https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A52021PC0130 2) Document \\"Value Sets for the digital green certificate as stated in the Annex ...\\", abbr. \\"VS-2021-04-14\\" - https://webgate.ec.europa.eu/fpfis/wikis/x/05PuKg 3) Guidelines on verifiable vaccination certificates - basic interoperability elements - Release 2 - 2021-03-12, abbr. \\"guidelines\\"", - "type": "object", - "required": [ - "v", - "dgcid", - "sub" - ], - "properties": { - "v": { - "title": "Schema version", - "description": "Version of the schema, according to Semantic versioning (ISO, https://semver.org/ version 2.0.0 or newer) (viz. guidelines)", - "type": "string", - "example": "1.0.0" + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://id.uvci.eu/DGC.combined-schema.json", + "title": "EU DGC", + "description": "EU Digital Green Certificate", + "required": [ + "ver", + "nam", + "dob" + ], + "type": "object", + "properties": { + "ver": { + "title": "Schema version", + "description": "Version of the schema, according to Semantic versioning (ISO, https://semver.org/ version 2.0.0 or newer)", + "type": "string", + "pattern": "^\\\\d+.\\\\d+.\\\\d+$", + "examples": [ + "1.0.0" + ] + }, + "nam": { + "description": "Surname(s), given name(s) - in that order", + "$ref": "#/$defs/person_name" + }, + "dob": { + "title": "Date of birth", + "description": "Date of Birth of the person addressed in the DGC. ISO 8601 date format restricted to range 1900-2099", + "type": "string", + "format": "date", + "pattern": "[19|20][0-9][0-9]-(0[1-9]|1[0-2])-([0-2][1-9]|3[0|1])", + "examples": [ + "1979-04-14" + ] + }, + "v": { + "description": "Vaccination Group", + "type": "array", + "items": { + "$ref": "#/$defs/vaccination_entry" + }, + "minItems": 1 + }, + "t": { + "description": "Test Group", + "type": "array", + "items": { + "$ref": "#/$defs/test_entry" + }, + "minItems": 1 + }, + "r": { + "description": "Recovery Group", + "type": "array", + "items": { + "$ref": "#/$defs/recovery_entry" + }, + "minItems": 1 + } + }, + "$defs": { + "dose_posint": { + "description": "Dose Number / Total doses in Series: positive integer, range: [1,9]", + "type": "integer", + "minimum": 1, + "maximum": 9 + }, + "country_vt": { + "description": "Country of Vaccination / Test, ISO 3166 where possible", + "type": "string", + "pattern": "[A-Z]{1,10}" + }, + "issuer": { + "description": "Certificate Issuer", + "type": "string", + "maxLength": 50 + }, + "person_name": { + "description": "Person name: Surname(s), given name(s) - in that order", + "required": [ + "fnt" + ], + "type": "object", + "properties": { + "fn": { + "title": "Family name", + "description": "The family or primary name(s) of the person addressed in the certificate", + "type": "string", + "maxLength": 50, + "examples": [ + "d'Červenková Panklová" + ] + }, + "fnt": { + "title": "Standardised family name", + "description": "The family name(s) of the person transliterated", + "type": "string", + "pattern": "^[A-Z<]*$", + "maxLength": 50, + "examples": [ + "DCERVENKOVA