Skip to content

Commit

Permalink
key chain storage changes
Browse files Browse the repository at this point in the history
Save CBOR as raw bytes in keychain, save key in different record
  • Loading branch information
phisakel committed Nov 23, 2023
1 parent 6db8461 commit 1954f62
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 64 deletions.
1 change: 1 addition & 0 deletions Documentation/change_notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
## Save key in keychain
18 changes: 18 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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
Expand Down
8 changes: 5 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
Expand Down
3 changes: 2 additions & 1 deletion Sources/eudi-lib-ios-wallet-storage/DataStorageService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
24 changes: 22 additions & 2 deletions Sources/eudi-lib-ios-wallet-storage/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}
34 changes: 34 additions & 0 deletions Sources/eudi-lib-ios-wallet-storage/Enumerations.swift
Original file line number Diff line number Diff line change
@@ -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"
}
73 changes: 40 additions & 33 deletions Sources/eudi-lib-ios-wallet-storage/IssueRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}


87 changes: 62 additions & 25 deletions Sources/eudi-lib-ios-wallet-storage/KeyChainStorageService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,52 +29,75 @@ 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 }
let statusMessage = SecCopyErrorMessageString(status, nil) as? String
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 }
let statusMessage = SecCopyErrorMessageString(status, nil) as? String
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
Expand All @@ -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 {
Expand All @@ -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 }
Expand All @@ -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)
}
}

0 comments on commit 1954f62

Please sign in to comment.