diff --git a/Documentation/change_notes.md b/Documentation/change_notes.md new file mode 100644 index 0000000..4924347 --- /dev/null +++ b/Documentation/change_notes.md @@ -0,0 +1 @@ +## Save key in keychain diff --git a/Package.resolved b/Package.resolved index b282015..3bc95e6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "eudi-lib-ios-iso18013-data-model", + "kind" : "remoteSourceControl", + "location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-model.git", + "state" : { + "branch" : "develop", + "revision" : "f789d682824183a5c648c186486caaef0b9a303a" + } + }, { "identity" : "swift-log", "kind" : "remoteSourceControl", @@ -8,6 +17,15 @@ "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", "version" : "1.5.3" } + }, + { + "identity" : "swiftcbor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/valpackett/SwiftCBOR.git", + "state" : { + "revision" : "edc01765cf6b3685bb622bb09242ef5964fb991b", + "version" : "0.4.6" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index d7c4ae0..1e61409 100644 --- a/Package.swift +++ b/Package.swift @@ -15,15 +15,17 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"), - ], + .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-model.git", branch: "develop"), + ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( name: "WalletStorage", dependencies: [ - .product(name: "Logging", package: "swift-log") - ]), + .product(name: "Logging", package: "swift-log"), + .product(name: "MdocDataModel18013", package: "eudi-lib-ios-iso18013-data-model"), + ]), .testTarget( name: "WalletStorageTests", dependencies: ["WalletStorage"]), diff --git a/Sources/eudi-lib-ios-wallet-storage/DataStorageService.swift b/Sources/eudi-lib-ios-wallet-storage/DataStorageService.swift index fa6d543..21c466b 100644 --- a/Sources/eudi-lib-ios-wallet-storage/DataStorageService.swift +++ b/Sources/eudi-lib-ios-wallet-storage/DataStorageService.swift @@ -22,7 +22,8 @@ public protocol DataStorageService { var accessGroup: String? { get set } func loadDocument(id: String) throws -> Document? func loadDocuments() throws -> [Document]? - func saveDocument(_ document: Document) throws + func saveDocument(_ document: Document, allowOverwrite: Bool) throws + func saveDocumentData(_ document: Document, dataToSaveType: SavedKeyChainDataType, dataType: String, allowOverwrite: Bool) throws func deleteDocument(id: String) throws func deleteDocuments() throws } diff --git a/Sources/eudi-lib-ios-wallet-storage/Document.swift b/Sources/eudi-lib-ios-wallet-storage/Document.swift index 17494ae..a08e36e 100644 --- a/Sources/eudi-lib-ios-wallet-storage/Document.swift +++ b/Sources/eudi-lib-ios-wallet-storage/Document.swift @@ -15,20 +15,40 @@ limitations under the License. */ import Foundation +import MdocDataModel18013 /// wallet document structure public struct Document { - public init(id: String = UUID().uuidString, docType: String, data: Data, createdAt: Date, modifiedAt: Date? = nil) { + public init(id: String = UUID().uuidString, docType: String, docDataType: DocDataType, data: Data, privateKeyType: PrivateKeyType?, privateKey: Data?, createdAt: Date?, modifiedAt: Date? = nil) { self.id = id self.docType = docType + self.docDataType = docDataType self.data = data - self.createdAt = createdAt + self.privateKeyType = privateKeyType + self.privateKey = privateKey + self.createdAt = createdAt ?? Date() self.modifiedAt = modifiedAt } public var id: String = UUID().uuidString public let docType: String public let data: Data + public let docDataType: DocDataType + public let privateKeyType: PrivateKeyType? + public let privateKey: Data? public let createdAt: Date public let modifiedAt: Date? + + public func getCborData() -> (dr: DeviceResponse, dpk: CoseKeyPrivate)? { + switch docDataType { + case .signupResponseJson: + guard let sr = data.decodeJSON(type: SignUpResponse.self), let dr = sr.deviceResponse, let dpk = sr.devicePrivateKey else { return nil } + return (dr,dpk) + case .cbor: + guard let dr = DeviceResponse(data: [UInt8](data)), let privateKeyType, let privateKey, let dpk = try? IssueRequest(id: id, privateKeyType: privateKeyType, keyData: privateKey).toCoseKeyPrivate() else { return nil } + return (dr,dpk) + case .sjwt: + fatalError("Format \(docDataType) not implemented") + } + } } diff --git a/Sources/eudi-lib-ios-wallet-storage/Enumerations.swift b/Sources/eudi-lib-ios-wallet-storage/Enumerations.swift new file mode 100644 index 0000000..f822cf4 --- /dev/null +++ b/Sources/eudi-lib-ios-wallet-storage/Enumerations.swift @@ -0,0 +1,34 @@ +/* +* Copyright (c) 2023 European Commission +* +* Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European +* Commission - subsequent versions of the EUPL (the "Licence"); You may not use this work +* except in compliance with the Licence. +* +* You may obtain a copy of the Licence at: +* https://joinup.ec.europa.eu/software/page/eupl +* +* Unless required by applicable law or agreed to in writing, software distributed under +* the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF +* ANY KIND, either express or implied. See the Licence for the specific language +* governing permissions and limitations under the Licence. +*/ + +import Foundation + +public enum SavedKeyChainDataType{ + case doc + case key +} +public enum DocDataType: String { + case cbor = "cbor" + case sjwt = "sjwt" + case signupResponseJson = "srjs" +} + +public enum PrivateKeyType: String { + case derEncodedP256 = "dep2" + case pemStringDataP256 = "pep2" + case x963EncodedP256 = "x9p2" + case secureEnclaveP256 = "sep2" +} diff --git a/Sources/eudi-lib-ios-wallet-storage/IssueRequest.swift b/Sources/eudi-lib-ios-wallet-storage/IssueRequest.swift index 9f25561..10efd0e 100644 --- a/Sources/eudi-lib-ios-wallet-storage/IssueRequest.swift +++ b/Sources/eudi-lib-ios-wallet-storage/IssueRequest.swift @@ -16,50 +16,57 @@ limitations under the License. import Foundation import CryptoKit +import MdocDataModel18013 /// Issue request structure public struct IssueRequest { - #if os(iOS) - let secureKey: SecureEnclave.P256.Signing.PrivateKey - /// DER representation of public key - public var publicKeyDer: Data { secureKey.publicKey.derRepresentation } - /// PEM representation of public key - public var publicKeyPEM: String { secureKey.publicKey.pemRepresentation } - /// X963 representation of public key - public var publicKeyX963: Data { secureKey.publicKey.x963Representation } - #endif - - /// Initialize issue request - /// - Parameters: - /// - savedKey: saved key representation (optional) - public init(savedKey: Data? = nil) throws { - #if os(iOS) - secureKey = if let savedKey { try SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: savedKey) } else { try SecureEnclave.P256.Signing.PrivateKey() } - #endif - } + public let id: String + public let docType: String? + public var keyData: Data? + public let privateKeyType: PrivateKeyType /// Initialize issue request with id /// /// - Parameters: /// - id: a key identifier (uuid) - public init(id: String, storageService: any DataStorageService) throws { - #if os(iOS) - secureKey = try SecureEnclave.P256.Signing.PrivateKey() - let docKey = Document(id: id, docType: "P256", data: secureKey.dataRepresentation, createdAt: Date()) - try storageService.saveDocument(docKey) - #endif + public init(id: String, docType: String? = nil, privateKeyType: PrivateKeyType = .x963EncodedP256, keyData: Data? = nil) throws { + self.id = id + self.docType = docType + self.privateKeyType = privateKeyType + if let keyData { + self.keyData = keyData + return + } + if privateKeyType == .derEncodedP256 || privateKeyType == .pemStringDataP256 || privateKeyType == .x963EncodedP256 { + let p256 = P256.Signing.PrivateKey() + self.keyData = switch privateKeyType { case .derEncodedP256: p256.derRepresentation; case .pemStringDataP256: p256.pemRepresentation.data(using: .utf8)!; case .x963EncodedP256: p256.x963Representation; default: Data() } + } else if privateKeyType == .secureEnclaveP256 { + let secureEnclaveKey = try SecureEnclave.P256.Signing.PrivateKey() + self.keyData = secureEnclaveKey.dataRepresentation + } + } + + public func saveToStorage(_ storageService: any DataStorageService) throws { + // save key data to storage with id + let docKey = Document(id: id, docType: docType ?? "P256", docDataType: .cbor, data: Data(), privateKeyType: privateKeyType, privateKey: keyData, createdAt: Date()) + try storageService.saveDocument(docKey, allowOverwrite: true) + } + + public mutating func loadFromStorage(_ storageService: any DataStorageService, id: String) throws { + guard let doc = try storageService.loadDocument(id: id) else { return } + keyData = doc.privateKey } - #if os(iOS) - /// Sign data with ``secureKey`` - /// - Parameter data: Data to be signed - /// - Returns: DER representation of signture for SHA256 hash - func signData(_ data: Data) throws -> Data { - let signature: P256.Signing.ECDSASignature = try secureKey.signature(for: SHA256.hash(data: data)) - return signature.derRepresentation + public func toCoseKeyPrivate() throws -> CoseKeyPrivate { + guard let keyData else { fatalError("Key data not loaded") } + if privateKeyType == .derEncodedP256 || privateKeyType == .pemStringDataP256 || privateKeyType == .x963EncodedP256 { + let p256 = switch privateKeyType { case .derEncodedP256: try P256.Signing.PrivateKey(derRepresentation: keyData); case .x963EncodedP256: try P256.Signing.PrivateKey(x963Representation: keyData); case .pemStringDataP256: try P256.Signing.PrivateKey(pemRepresentation: String(data: keyData, encoding: .utf8)!); default: P256.Signing.PrivateKey() } + return CoseKeyPrivate(privateKeyx963Data: p256.x963Representation, crv: .p256) + } else { + let se256 = try SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData) + return CoseKeyPrivate(publicKeyx963Data: se256.publicKey.x963Representation, secureEnclaveData: keyData) + } } - #endif - //func certificateTrust(certificate: SecCertificate) -> ver } diff --git a/Sources/eudi-lib-ios-wallet-storage/KeyChainStorageService.swift b/Sources/eudi-lib-ios-wallet-storage/KeyChainStorageService.swift index d1f2cea..305bfc1 100644 --- a/Sources/eudi-lib-ios-wallet-storage/KeyChainStorageService.swift +++ b/Sources/eudi-lib-ios-wallet-storage/KeyChainStorageService.swift @@ -29,7 +29,13 @@ public class KeyChainStorageService: DataStorageService { /// - Parameter id: Document identifier /// - Returns: The document if exists public func loadDocument(id: String) throws -> Document? { - let query = makeQuery(id: nil, bAll: false) + guard let dict1 = try loadDocumentData(id: id, for: .doc) else { return nil } + let dict2 = try loadDocumentData(id: id, for: .key) + return makeDocument(dict1: dict1, dict2: dict2) + } + + func loadDocumentData(id: String, for type: SavedKeyChainDataType) throws -> NSDictionary? { + let query = makeQuery(id: id, for: type, bAll: false) var result: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &result) if status == errSecItemNotFound { return nil } @@ -37,14 +43,20 @@ public class KeyChainStorageService: DataStorageService { guard status == errSecSuccess else { throw StorageError(description: statusMessage ?? "", code: Int(status)) } - let dict = result as! NSDictionary - return makeDocument(dict: dict) + return (result as! NSDictionary) } /// Gets all documents /// - Parameters: /// - Returns: The documents stored in keychain under the serviceName public func loadDocuments() throws -> [Document]? { - let query = makeQuery(id: nil, bAll: true) + guard let dicts1 = try loadDocumentsData(for: .doc) else { return nil } + let dicts2 = try loadDocumentsData(for: .key) + let documents = dicts1.compactMap { d1 in makeDocument(dict1: d1, dict2: dicts2?.first(where: { d2 in d1[kSecAttrAccount] as! String == d2[kSecAttrAccount] as! String})) } + return documents + } + + func loadDocumentsData(for type: SavedKeyChainDataType) throws -> [NSDictionary]? { + let query = makeQuery(id: nil, for: type, bAll: true) var result: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &result) if status == errSecItemNotFound { return nil } @@ -52,29 +64,40 @@ public class KeyChainStorageService: DataStorageService { guard status == errSecSuccess else { throw StorageError(description: statusMessage ?? "", code: Int(status)) } - let dicts = result as! [NSDictionary] - let documents = dicts.compactMap { makeDocument(dict: $0) } - return documents + return (result as! [NSDictionary]) } - + /// Save the secret to keychain /// Note: the value passed in will be zeroed out after the secret is saved /// - Parameters: /// - document: The document to save - public func saveDocument(_ document: Document) throws { + public func saveDocument(_ document: Document, allowOverwrite: Bool = true) throws { + try saveDocumentData(document, dataToSaveType: .doc, dataType: document.docDataType.rawValue, allowOverwrite: allowOverwrite) + if document.docDataType != .signupResponseJson { + try saveDocumentData(document, dataToSaveType: .key, dataType: document.privateKeyType!.rawValue, allowOverwrite: allowOverwrite) + } + } + + func serviceToSave(for dataToSaveType: SavedKeyChainDataType) -> String { + switch dataToSaveType { case .key: serviceName + "_key"; default: serviceName } + } + + public func saveDocumentData(_ document: Document, dataToSaveType: SavedKeyChainDataType, dataType: String, allowOverwrite: Bool = true) throws { // kSecAttrAccount is used to store the secret Id so that we can look it up later // kSecAttrService is always set to serviceName to enable us to lookup all our secrets later if needed - // kSecAttrType is used to store the secret type to allow us to cast it to the right Type on search - var query: [String: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrService: serviceName, kSecAttrAccount: document.id] as [String: Any] + guard dataType.count == 4 else { throw StorageError(description: "Invalid type") } + if dataToSaveType == .key && document.privateKey == nil { throw StorageError(description: "Private key not available") } + var query: [String: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrService: serviceToSave(for: dataToSaveType), kSecAttrAccount: document.id] as [String: Any] #if os(macOS) query[kSecUseDataProtectionKeychain as String] = true #endif - query[kSecValueData as String] = document.data + query[kSecValueData as String] = switch dataToSaveType { case .key: document.privateKey!; default: document.data } query[kSecAttrLabel as String] = document.docType + query[kSecAttrType as String] = dataType var status = SecItemAdd(query as CFDictionary, nil) - if status == errSecDuplicateItem { - let updated = [kSecValueData: document.data, kSecAttrLabel: document.docType] as [String: Any] - query = [kSecClass: kSecClassGenericPassword, kSecAttrService: serviceName, kSecAttrAccount: document.id] as [String: Any] + if allowOverwrite && status == errSecDuplicateItem { + let updated = [kSecValueData: query[kSecValueData as String] as! Data, kSecAttrLabel: document.docType, kSecAttrType: dataType] as [String: Any] + query = [kSecClass: kSecClassGenericPassword, kSecAttrService: serviceToSave(for: dataToSaveType), kSecAttrAccount: document.id] as [String: Any] status = SecItemUpdate(query as CFDictionary, updated as CFDictionary) } let statusMessage = SecCopyErrorMessageString(status, nil) as? String @@ -88,25 +111,33 @@ public class KeyChainStorageService: DataStorageService { /// - Parameters: /// - id: The Id of the secret public func deleteDocument(id: String) throws { + try deleteDocumentData(id: id, for: .doc) + try? deleteDocumentData(id: id, for: .key) + } + + public func deleteDocumentData(id: String, for saveType: SavedKeyChainDataType) throws { // kSecAttrAccount is used to store the secret Id so that we can look it up later // kSecAttrService is always set to serviceName to enable us to lookup all our secrets later if needed // kSecAttrType is used to store the secret type to allow us to cast it to the right Type on search - let query: [String: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrService: serviceName, kSecAttrAccount: id] as [String: Any] + let query: [String: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrService: serviceToSave(for: saveType), kSecAttrAccount: id] as [String: Any] let status = SecItemDelete(query as CFDictionary) let statusMessage = SecCopyErrorMessageString(status, nil) as? String - guard status == errSecSuccess else { - throw StorageError(description: statusMessage ?? "", code: Int(status)) - } + guard status == errSecSuccess else { throw StorageError(description: statusMessage ?? "", code: Int(status)) } } /// Delete all documents from keychain /// - Parameters: /// - id: The Id of the secret public func deleteDocuments() throws { + try deleteDocumentsData(for: .doc) + try? deleteDocumentsData(for: .key) + } + + public func deleteDocumentsData(for saveType: SavedKeyChainDataType) throws { // kSecAttrAccount is used to store the secret Id so that we can look it up later // kSecAttrService is always set to serviceName to enable us to lookup all our secrets later if needed // kSecAttrType is used to store the secret type to allow us to cast it to the right Type on search - let query: [String: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrService: serviceName] as [String: Any] + let query: [String: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrService: serviceToSave(for: saveType)] as [String: Any] let status = SecItemDelete(query as CFDictionary) let statusMessage = SecCopyErrorMessageString(status, nil) as? String guard status == errSecSuccess else { @@ -119,8 +150,9 @@ public class KeyChainStorageService: DataStorageService { /// - id: id /// - bAll: request all matching items /// - Returns: The dictionary query - func makeQuery(id: String?, bAll: Bool) -> [String: Any] { - var query: [String: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrService: serviceName, kSecReturnData: true, kSecReturnAttributes: true] as [String: Any] + func makeQuery(id: String?, for saveType: SavedKeyChainDataType, bAll: Bool) -> [String: Any] { + guard id != nil || bAll else { fatalError("Invalid call to makeQuery") } + var query: [String: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrService: serviceToSave(for: saveType), kSecReturnData: true, kSecReturnAttributes: true] as [String: Any] if bAll { query[kSecMatchLimit as String] = kSecMatchLimitAll} if let id { query[kSecAttrAccount as String] = id} if let accessGroup, !accessGroup.isEmpty { query[kSecAttrAccessGroup as String] = accessGroup } @@ -130,9 +162,14 @@ public class KeyChainStorageService: DataStorageService { /// Make a document from a keychain item /// - Parameter dict: keychain item returned as dictionary /// - Returns: the document - func makeDocument(dict: NSDictionary) -> Document { - var data = dict[kSecValueData] as! Data + func makeDocument(dict1: NSDictionary, dict2: NSDictionary?) -> Document { + var data = dict1[kSecValueData] as! Data defer { let c = data.count; data.withUnsafeMutableBytes { memset_s($0.baseAddress, c, 0, c); return } } - return Document(id: dict[kSecAttrAccount] as? String ?? "", docType: dict[kSecAttrLabel] as? String ?? "", data: data, createdAt: dict[kSecAttrCreationDate] as! Date, modifiedAt: dict[kSecAttrModificationDate] as? Date) + var keyType: PrivateKeyType? = nil; var privateKeyData: Data? = nil + if let dict2 { + keyType = PrivateKeyType(rawValue: dict2[kSecAttrType] as? String ?? PrivateKeyType.derEncodedP256.rawValue)! + privateKeyData = (dict2[kSecValueData] as! Data) + } + return Document(id: dict1[kSecAttrAccount] as! String, docType: dict1[kSecAttrLabel] as? String ?? "", docDataType: DocDataType(rawValue: dict1[kSecAttrType] as? String ?? DocDataType.cbor.rawValue)!, data: data, privateKeyType: keyType, privateKey: privateKeyData, createdAt: (dict1[kSecAttrCreationDate] as! Date), modifiedAt: dict1[kSecAttrModificationDate] as? Date) } }