-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #52 from niscy-eudiw/main
Refactor document model and enhance claim management
- Loading branch information
Showing
15 changed files
with
983 additions
and
983 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
/* | ||
Copyright (c) 2023 European Commission | ||
|
||
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. | ||
*/ | ||
|
||
|
||
import Foundation | ||
|
||
/// This structure is used to store and manage pairs of names and their corresponding values. | ||
/// It provides functionality for comparing instances, generating string representations for | ||
/// debugging and display purposes, and ensuring safe concurrent access. | ||
@DebugDescription | ||
public struct DocClaim: Equatable, CustomStringConvertible, CustomDebugStringConvertible, Sendable { | ||
public init(name: String, displayName: String? = nil, docDataValue: DocDataValue? = nil, valueType: String? = nil, stringValue: String, isOptional: Bool = false, order: Int = 0, namespace: String? = nil, children: [DocClaim]? = nil) { | ||
self.name = name | ||
self.displayName = displayName | ||
self.docDataValue = docDataValue | ||
self.valueType = valueType | ||
self.stringValue = stringValue | ||
self.isOptional = isOptional | ||
self.order = order | ||
self.namespace = namespace | ||
self.children = children | ||
} | ||
/// The namespace of the claim (if document is a mso-mdoc) | ||
public let namespace: String? | ||
/// The name of the claim. | ||
public let name: String | ||
/// The display name of the claim, originated from VCI metadata/claims. | ||
public let displayName: String? | ||
/// The value of the claim as a string. | ||
public let stringValue: String | ||
/// The value of the claim as a `DocDataValue` (enum with associated values) | ||
public let docDataValue: DocDataValue? | ||
/// The type of the value of the claim, originated from VCI metadata/claims. | ||
public let valueType: String? | ||
/// A flag indicating whether the claim is optional, originated from VCI metadata/claims. | ||
public var isOptional: Bool = false | ||
/// The order of the claim in the document. | ||
public var order: Int = 0 | ||
/// A string for Wallet UI usage to define the style of the claim. | ||
public var style: String? | ||
/// The children of the claim. | ||
public var children: [DocClaim]? | ||
/// Description of the claim. | ||
public var description: String { "\(name): \(stringValue)" } | ||
/// Debug description of the claim. | ||
public var debugDescription: String { "\(order). \t\(name): \(stringValue)" } | ||
|
||
/// Adds a child to the claim. | ||
public mutating func add(child: DocClaim) { | ||
if children == nil { children = [] } | ||
children!.append(child) | ||
} | ||
} |
326 changes: 156 additions & 170 deletions
326
...013/MdocKnownDocTypes/MdocDecodable.swift → ...3/DocumentClaims/DocClaimsDecodable.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,170 +1,156 @@ | ||
/* | ||
Copyright (c) 2023 European Commission | ||
|
||
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. | ||
*/ | ||
|
||
// MdocDecodable.swift | ||
|
||
import Foundation | ||
import SwiftCBOR | ||
#if canImport(UIKit) | ||
import UIKit | ||
#endif | ||
|
||
/// A conforming type represents mdoc data. | ||
/// | ||
/// Can be decoded by a CBOR device response | ||
public protocol MdocDecodable: Sendable, DocumentProtocol, AgeAttesting { | ||
var issuerSigned: IssuerSigned? { get set} | ||
var devicePrivateKey: CoseKeyPrivate? { get set} | ||
var nameSpaces: [NameSpace]? { get set} | ||
var mandatoryElementKeys: [DataElementIdentifier] { get} | ||
var displayStrings: [NameValue] { get } | ||
var displayImages: [NameImage] { get } | ||
func toJson(base64: Bool) -> [String: Any] | ||
} // end protocol | ||
|
||
extension MdocDecodable { | ||
|
||
public func getItemValue<T>(_ s: String) -> T? { | ||
guard let issuerSigned else { return nil } | ||
let nameSpaceItems = Self.getSignedItems(issuerSigned, docType) | ||
guard let nameSpaceItems else { return nil } | ||
return Self.getItemValue(nameSpaceItems, string: s) | ||
} | ||
|
||
static func getItemValue<T>(_ nameSpaceItems: [NameSpace: [IssuerSignedItem]], string s: String) -> T? { | ||
for (_,v) in nameSpaceItems { | ||
if let item = v.first(where: { s == $0.elementIdentifier }) { return item.getTypedValue() } | ||
} | ||
return nil | ||
} | ||
|
||
public static func getSignedItems(_ issuerSigned: IssuerSigned, _ docType: String, _ ns: [NameSpace]? = nil) -> [String: [IssuerSignedItem]]? { | ||
guard var nameSpaces = issuerSigned.issuerNameSpaces?.nameSpaces else { return nil } | ||
if let ns { nameSpaces = nameSpaces.filter { ns.contains($0.key) } } | ||
return nameSpaces | ||
} | ||
|
||
public func toJson(base64: Bool = false) -> [String: Any] { | ||
guard let issuerSigned, let nameSpaceItems = Self.getSignedItems(issuerSigned, docType) else { return [:] } | ||
return nameSpaceItems.mapValues { $0.toJson(base64: base64) } | ||
} | ||
|
||
/// Extracts age-over values from the provided namespaces and updates the given dictionary with the results. | ||
/// | ||
/// - Parameters: | ||
/// - nameSpaces: A dictionary where the key is a `NameSpace` and the value is an array of `IssuerSignedItem`. | ||
/// - ageOverXX: An inout parameter that is a dictionary where the key is an integer representing the age and the value is a boolean indicating whether the age condition is met. | ||
public static func extractAgeOverValues(_ nameSpaces: [NameSpace: [IssuerSignedItem]], _ ageOverXX: inout [Int: Bool]) { | ||
for (_, items) in nameSpaces { | ||
for item in items { | ||
let k = item.elementIdentifier | ||
if !k.hasPrefix("age_over_") { continue } | ||
if let age = Int(k.suffix(k.count - 9)) { | ||
let b: Bool? = item.getTypedValue() | ||
if let b { ageOverXX[age] = b } | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// Determines if there are more than two age-over element identifiers in the provided list. | ||
/// | ||
/// - Parameters: | ||
/// - reqDocType: The required document type. | ||
/// - reqNamespace: The required namespace.pa | ||
/// - ageAttest: An instance conforming to the `AgeAttesting` protocol.s | ||
/// - reqElementIdentifiers: A list of data element identifiers. | ||
/// - Returns: A set of strings representing the identifiers that meet the criteria. } | ||
|
||
public static func moreThan2AgeOverElementIdentifiers(_ reqDocType: DocType, _ reqNamespace: NameSpace, _ ageAttest: any AgeAttesting, _ reqElementIdentifiers: [DataElementIdentifier]) -> Set<String> { | ||
// special case for maximum two age_over_NN data elements shall be returned | ||
guard reqDocType == IsoMdlModel.isoDocType, reqNamespace == IsoMdlModel.isoNamespace else { return Set() } | ||
let ages = reqElementIdentifiers.filter { $0.hasPrefix("age_over_")}.compactMap { k in Int(k.suffix(k.count - 9)) } | ||
let agesDict = ageAttest.max2AgesOver(ages: ages) | ||
return Set( agesDict.filter { $1 == false }.keys.map { "age_over_\($0)" }) | ||
} | ||
|
||
/// Extracts a display string or image from a CBOR value. | ||
/// | ||
/// - Parameters: | ||
/// - name: The name associated with the CBOR value. | ||
/// - cborValue: The CBOR value to be processed.pa | ||
/// - bDebugDisplay: A boolean flag indicating whether to enable debug display.s | ||
/// - displayImages: An inout array of `NameImage` to store the extracted images. | ||
/// - ns: The namespace associated with the CBOR value. | ||
/// - order: The order in which the value should be processed. | ||
/// - labels: A dictionary where the key is the elementIdentifier and the value is a string representing the label. | ||
/// - Returns: A `NameValue` object containing the extracted display string or image. | ||
public static func extractDisplayStringOrImage(_ name: String, _ cborValue: CBOR, _ bDebugDisplay: Bool, _ displayImages: inout [NameImage], _ ns: NameSpace, _ order: Int, _ labels: [String: String]? = nil) -> NameValue { | ||
var value = bDebugDisplay ? cborValue.debugDescription : cborValue.description | ||
var dt = cborValue.mdocDataType | ||
if name == "sex", let isex = Int(value), isex <= 2 { | ||
value = NSLocalizedString(isex == 1 ? "male" : "female", comment: ""); dt = .string | ||
} | ||
if case let .byteString(bs) = cborValue { | ||
var encodeAsBase64 = name == "user_pseudonym" | ||
#if os(iOS) | ||
if UIImage(data: Data(bs)) == nil { encodeAsBase64 = true } | ||
#endif | ||
if encodeAsBase64 { | ||
value = Data(bs).base64EncodedString() | ||
} else { | ||
displayImages.append(NameImage(name: labels?[name] ?? name, image: Data(bs), ns: ns)) | ||
} | ||
} | ||
var node = NameValue(name: labels?[name] ?? name, value: value, ns: ns, mdocDataType: dt, order: order) | ||
if case let .map(m) = cborValue { | ||
let innerJsonMap = CBOR.decodeDictionary(m, unwrap: false) | ||
for (o2,(k,v)) in innerJsonMap.enumerated() { | ||
guard let cv = v as? CBOR else { continue } | ||
node.add(child: extractDisplayStringOrImage(k, cv, bDebugDisplay, &displayImages, ns, o2, labels)) | ||
} | ||
} else if case let .array(a) = cborValue { | ||
let innerJsonArray = CBOR.decodeList(a, unwrap: false) | ||
for (o2,v) in innerJsonArray.enumerated() { | ||
guard let cv = v as? CBOR else { continue } | ||
let k = "\(name)[\(o2)]" | ||
node.add(child: extractDisplayStringOrImage(k, cv, bDebugDisplay, &displayImages, ns, o2, labels)) | ||
} | ||
} | ||
return node | ||
} | ||
|
||
/// Extracts display strings and images from the provided namespaces and populates the given arrays. | ||
/// | ||
/// - Parameters: | ||
/// - nameSpaces: A dictionary where the key is a `NameSpace` and the value is an array of `IssuerSignedItem`. | ||
/// - displayStrings: An inout parameter that will be populated with `NameValue` items extracted from the namespaces. | ||
/// - displayImages: An inout parameter that will be populated with `NameImage` items extracted from the namespaces. | ||
/// - labels: A dictionary where the key is the elementIdentifier and the value is a string representing the label. | ||
/// - nsFilter: An optional array of `NameSpace` to filter/sort the extraction. Defaults to `nil`. | ||
public static func extractDisplayStrings(_ nameSpaces: [NameSpace: [IssuerSignedItem]], _ displayStrings: inout [NameValue], _ displayImages: inout [NameImage], _ labels: [String: String]? = nil, _ nsFilter: [NameSpace]? = nil) { | ||
let bDebugDisplay = UserDefaults.standard.bool(forKey: "DebugDisplay") | ||
var order = 0 | ||
let nsFilterUsed = nsFilter ?? Array(nameSpaces.keys) | ||
for ns in nsFilterUsed { | ||
let items = nameSpaces[ns] ?? [] | ||
for item in items { | ||
let n = extractDisplayStringOrImage(item.elementIdentifier, item.elementValue, bDebugDisplay, &displayImages, ns, order, labels) | ||
displayStrings.append(n) | ||
order = order + 1 | ||
} | ||
} | ||
} | ||
|
||
|
||
} // end extension | ||
|
||
/* | ||
Copyright (c) 2023 European Commission | ||
|
||
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. | ||
*/ | ||
|
||
// ClaimsDecodable.swift | ||
|
||
import Foundation | ||
import SwiftCBOR | ||
#if canImport(UIKit) | ||
import UIKit | ||
#endif | ||
|
||
/// A conforming type represents claims data. | ||
/// | ||
/// Can be decoded by CBOR or SD-JWT data | ||
public protocol DocClaimsDecodable: Sendable, AgeAttesting { | ||
/// The unique identifier of the document. | ||
var id: String { get } | ||
/// The date and time the document was created. | ||
var createdAt: Date { get } | ||
/// The date and time the document was last modified. | ||
var modifiedAt: Date? { get } | ||
/// The display name of the document. | ||
var displayName: String? { get } | ||
// The document type. It is not null for CBOR (mso-mdoc) documents | ||
var docType: String? { get } | ||
// document claims in a format agnostic way | ||
var docClaims: [DocClaim] { get } | ||
} // end protocol | ||
|
||
extension DocClaimsDecodable { | ||
|
||
public static func getCborItemValue<T>(_ issuerSigned: IssuerSigned, _ s: String) -> T? { | ||
let nameSpaceItems = Self.getCborSignedItems(issuerSigned) | ||
guard let nameSpaceItems else { return nil } | ||
return Self.getCborItemValue(nameSpaceItems, string: s) | ||
} | ||
|
||
static func getCborItemValue<T>(_ nameSpaceItems: [NameSpace: [IssuerSignedItem]], string name: String) -> T? { | ||
for (_,v) in nameSpaceItems { | ||
if let item = v.first(where: { name == $0.elementIdentifier }) { return item.getTypedValue() } | ||
} | ||
return nil | ||
} | ||
|
||
public static func getCborSignedItems(_ issuerSigned: IssuerSigned, _ ns: [NameSpace]? = nil) -> [String: [IssuerSignedItem]]? { | ||
guard var nameSpaces = issuerSigned.issuerNameSpaces?.nameSpaces else { return nil } | ||
if let ns { nameSpaces = nameSpaces.filter { ns.contains($0.key) } } | ||
return nameSpaces | ||
} | ||
|
||
|
||
/// Extracts age-over values from the provided namespaces and updates the given dictionary with the results. | ||
/// | ||
/// - Parameters: | ||
/// - nameSpaces: A dictionary where the key is a `NameSpace` and the value is an array of `IssuerSignedItem`. | ||
/// - ageOverXX: An inout parameter that is a dictionary where the key is an integer representing the age and the value is a boolean indicating whether the age condition is met. | ||
public static func extractAgeOverValues(_ nameSpaces: [NameSpace: [IssuerSignedItem]], _ ageOverXX: inout [Int: Bool]) { | ||
for (_, items) in nameSpaces { | ||
for item in items { | ||
let k = item.elementIdentifier | ||
if !k.hasPrefix("age_over_") { continue } | ||
if let age = Int(k.suffix(k.count - 9)) { | ||
let b: Bool? = item.getTypedValue() | ||
if let b { ageOverXX[age] = b } | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// Determines if there are more than two age-over element identifiers in the provided list. | ||
/// | ||
/// - Parameters: | ||
/// - reqDocType: The required document type. | ||
/// - reqNamespace: The required namespace.pa | ||
/// - ageAttest: An instance conforming to the `AgeAttesting` protocol.s | ||
/// - reqElementIdentifiers: A list of data element identifiers. | ||
/// - Returns: A set of strings representing the identifiers that meet the criteria. } | ||
|
||
public static func moreThan2AgeOverElementIdentifiers(_ reqDocType: DocType, _ reqNamespace: NameSpace, _ ageAttest: any AgeAttesting, _ reqElementIdentifiers: [DataElementIdentifier]) -> Set<String> { | ||
// special case for maximum two age_over_NN data elements shall be returned | ||
guard reqDocType == IsoMdlModel.isoDocType, reqNamespace == IsoMdlModel.isoNamespace else { return Set() } | ||
let ages = reqElementIdentifiers.filter { $0.hasPrefix("age_over_")}.compactMap { k in Int(k.suffix(k.count - 9)) } | ||
let agesDict = ageAttest.max2AgesOver(ages: ages) | ||
return Set( agesDict.filter { $1 == false }.keys.map { "age_over_\($0)" }) | ||
} | ||
|
||
/// Extracts a display string or image from a CBOR value. | ||
/// | ||
/// - Parameters: | ||
/// - name: The name associated with the CBOR value. | ||
/// - cborValue: The CBOR value to be processed.pa | ||
/// - bDebugDisplay: A boolean flag indicating whether to enable debug display.s | ||
/// - ns: The namespace associated with the CBOR value. | ||
/// - order: The order in which the value should be processed. | ||
/// - labels: A dictionary where the key is the elementIdentifier and the value is a string representing the label. | ||
/// - Returns: A `DocClaim` object containing the extracted display string or image. | ||
public static func extractCborClaim(_ name: String, _ cborValue: CBOR, _ bDebugDisplay: Bool, _ namespace: NameSpace, _ order: Int, _ displayNames: [String:String]? = nil, _ mandatory: [String:Bool]? = nil, _ valueTypes: [String:String]? = nil) -> DocClaim { | ||
var stringValue = bDebugDisplay ? cborValue.debugDescription : cborValue.description | ||
let dt = cborValue.mdocDataValue | ||
if name == "sex", let isex = Int(stringValue), isex <= 2 { stringValue = NSLocalizedString(isex == 1 ? "male" : "female", comment: "") } | ||
let isMandatory = mandatory?[name] ?? true | ||
var node = DocClaim(name: name, displayName: displayNames?[name], docDataValue: dt, valueType: valueTypes?[name], stringValue: stringValue, isOptional: !isMandatory, order: order, namespace: namespace) | ||
if case let .map(m) = cborValue { | ||
let innerJsonMap = CBOR.decodeDictionary(m, unwrap: false) | ||
for (o2,(k,v)) in innerJsonMap.enumerated() { | ||
guard let cv = v as? CBOR else { continue } | ||
node.add(child: extractCborClaim(k, cv, bDebugDisplay, namespace, o2, displayNames, mandatory, valueTypes)) | ||
} | ||
} else if case let .array(a) = cborValue { | ||
let innerJsonArray = CBOR.decodeList(a, unwrap: false) | ||
for (o2,v) in innerJsonArray.enumerated() { | ||
guard let cv = v as? CBOR else { continue } | ||
let k = "\(name)[\(o2)]" | ||
node.add(child: extractCborClaim(k, cv, bDebugDisplay, namespace, o2, displayNames, mandatory, valueTypes)) | ||
} | ||
} | ||
return node | ||
} | ||
|
||
/// Extracts display strings and images from the provided namespaces and populates the given arrays. | ||
/// | ||
/// - Parameters: | ||
/// - nameSpaces: A dictionary where the key is a `NameSpace` and the value is an array of `IssuerSignedItem`. | ||
/// - docClaims: An inout parameter that will be populated with `DocClaim` items extracted from the namespaces. | ||
/// - labels: A dictionary where the key is the elementIdentifier and the value is a string representing the label. | ||
/// - nsFilter: An optional array of `NameSpace` to filter/sort the extraction. Defaults to `nil`. | ||
public static func extractCborClaims(_ nameSpaces: [NameSpace: [IssuerSignedItem]], _ docClaims: inout [DocClaim], _ claimDisplayNames: [NameSpace: [String: String]]? = nil, _ mandatoryClaims: [NameSpace: [String: Bool]]? = nil, _ claimValueTypes: [NameSpace: [String: String]]? = nil, nsFilter: [NameSpace]? = nil) { | ||
let bDebugDisplay = UserDefaults.standard.bool(forKey: "DebugDisplay") | ||
var order = 0 | ||
let nsFilterUsed = nsFilter ?? Array(nameSpaces.keys) | ||
for ns in nsFilterUsed { | ||
let items = nameSpaces[ns] ?? [] | ||
for item in items { | ||
let n = extractCborClaim(item.elementIdentifier, item.elementValue, bDebugDisplay, ns, order, claimDisplayNames?[ns], mandatoryClaims?[ns], claimValueTypes?[ns]) | ||
docClaims.append(n) | ||
order = order + 1 | ||
} | ||
} | ||
} | ||
|
||
|
||
} // end extension | ||
|
Oops, something went wrong.