Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for loading and deleting documents by status #16

Merged
merged 2 commits into from
Jul 16, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add support for loading and deleting documents by status
The `DataStorageService` protocol now includes new methods to load and delete documents based on their status. This allows for loading issued / deferred documents separately. The `loadDocument` and `loadDocuments` methods now accept a `status` parameter of type `DocumentStatus`, and the `deleteDocument` and `deleteDocuments` methods also accept the same `status` parameter.

This change improves the flexibility and functionality of the `DataStorageService` protocol, enabling developers to work with documents based on their status.
  • Loading branch information
phisakel committed Jul 16, 2024
commit c3779fb824153d358f44979f57550bb36073f399
9 changes: 4 additions & 5 deletions Sources/WalletStorage/DataStorageService.swift
Original file line number Diff line number Diff line change
@@ -20,10 +20,9 @@ import Foundation
public protocol DataStorageService {
var serviceName: String { get set }
var accessGroup: String? { get set }
func loadDocument(id: String) throws -> Document?
func loadDocuments() throws -> [Document]?
func loadDocument(id: String, status: DocumentStatus) throws -> Document?
func loadDocuments(status: DocumentStatus) throws -> [Document]?
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
func deleteDocument(id: String, status: DocumentStatus) throws
func deleteDocuments(status: DocumentStatus) throws
}
7 changes: 4 additions & 3 deletions Sources/WalletStorage/Document.swift
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ import MdocDataModel18013

/// wallet document structure
public struct Document {
public init(id: String = UUID().uuidString, docType: String, docDataType: DocDataType, data: Data, privateKeyType: PrivateKeyType?, privateKey: 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, status: DocumentStatus) {
self.id = id
self.docType = docType
self.docDataType = docDataType
@@ -28,6 +28,7 @@ public struct Document {
self.privateKey = privateKey
self.createdAt = createdAt ?? Date()
self.modifiedAt = modifiedAt
self.status = status
}

public var id: String = UUID().uuidString
@@ -38,6 +39,8 @@ public struct Document {
public let privateKey: Data?
public let createdAt: Date
public let modifiedAt: Date?
public let status: DocumentStatus
public var isDeferred: Bool { status == .deferred }

/// get CBOR data and private key from document
public func getCborData() -> (iss: (String, IssuerSigned), dpk: (String, CoseKeyPrivate))? {
@@ -51,8 +54,6 @@ public struct Document {
return ((id, iss), (id, dpk))
case .sjwt:
fatalError("Format \(docDataType) not implemented")
case .deferred:
return nil
}
}
}
18 changes: 12 additions & 6 deletions Sources/WalletStorage/Enumerations.swift
Original file line number Diff line number Diff line change
@@ -17,21 +17,22 @@ limitations under the License.
import Foundation

/// type of data to save in storage
public enum SavedKeyChainDataType{
case doc
case key
/// ``doc``: Document data
/// ``key``: Private-key
public enum SavedKeyChainDataType: String {
case doc = "sdoc"
case key = "skey"
}

/// Format of document data
/// ``cbor``: DeviceResponse cbor encoded
/// ``sjwt``: sd-jwt ** not yet supported **
/// ``signupResponseJson``: DeviceResponse and PrivateKey json serialized
/// ``deferred``: Deferred issuance
public enum DocDataType: String {
/// ``deferred``: Deferred issuance data
public enum DocDataType: String {
case cbor = "cbor"
case sjwt = "sjwt"
case signupResponseJson = "srjs"
case deferred = "defr"
}

/// Format of private key
@@ -47,3 +48,8 @@ public enum PrivateKeyType: String {
}


/// document status
public enum DocumentStatus: String {
case issued
case deferred
}
8 changes: 4 additions & 4 deletions Sources/WalletStorage/IssueRequest.swift
Original file line number Diff line number Diff line change
@@ -53,14 +53,14 @@ public struct IssueRequest {
}
}

public func saveToStorage(_ storageService: any DataStorageService) throws {
public func saveToStorage(_ storageService: any DataStorageService, status: DocumentStatus) 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())
let docKey = Document(id: id, docType: docType ?? "P256", docDataType: .cbor, data: Data(), privateKeyType: privateKeyType, privateKey: keyData, createdAt: Date(), status: status)
try storageService.saveDocument(docKey, allowOverwrite: true)
}

public init?(_ storageService: any DataStorageService, id: String) throws {
guard let doc = try storageService.loadDocument(id: id), let pk = doc.privateKey, let pkt = doc.privateKeyType else { return nil }
public init?(_ storageService: any DataStorageService, id: String, status: DocumentStatus) throws {
guard let doc = try storageService.loadDocument(id: id, status: status), let pk = doc.privateKey, let pkt = doc.privateKeyType else { return nil }
self.id = id
keyData = pk
privateKeyType = pkt
141 changes: 69 additions & 72 deletions Sources/WalletStorage/KeyChainStorageService.swift
Original file line number Diff line number Diff line change
@@ -16,57 +16,62 @@

import Foundation
/// Implements key-chain storage
/// Documents are saved as a pair of generic password items (document data and private key)
/// For implementation details see [Apple documentation](https://developer.apple.com/documentation/security/ksecclassgenericpassword)
public class KeyChainStorageService: DataStorageService {

public init(serviceName: String, accessGroup: String? = nil) {
self.serviceName = serviceName
self.accessGroup = accessGroup
}

public var serviceName: String
public var accessGroup: String?

/// Gets the secret document by id passed in parameter
/// - Parameter id: Document identifier
/// - Returns: The document if exists
public func loadDocument(id: String) throws -> Document? {
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)
public func loadDocument(id: String, status: DocumentStatus) throws -> Document? {
try loadDocuments(id: id, status: status)?.first
}

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))
}
return (result as! NSDictionary)
public func loadDocuments(status: DocumentStatus) throws -> [Document]? {
try loadDocuments(id: nil, status: status)
}
// use is-negative to denote type of data
static func isDocumentDataRow(_ d: [String: Any]) -> Bool { if let b = d[kSecAttrIsNegative as String] as? Bool { !b } else { true } }
static func isPrivateKeyRow(_ d: [String: Any]) -> Bool { if let b = d[kSecAttrIsNegative as String] as? Bool { b } else { false } }

/// Gets all documents
/// - Parameters:
/// - Returns: The documents stored in keychain under the serviceName
public func loadDocuments() throws -> [Document]? {
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})) }
func loadDocuments(id: String?, status: DocumentStatus) throws -> [Document]? {
guard var dicts1 = try loadDocumentsData(id: id, docStatus: status) else { return nil }
let dicts2 = dicts1.filter(Self.isPrivateKeyRow)
dicts1 = dicts1.filter(Self.isDocumentDataRow)
let documents = dicts1.compactMap { d1 in Self.makeDocument(dict1: d1, dict2: dicts2.first(where: { d2 in d1[kSecAttrAccount as String] as! String == d2[kSecAttrAccount as String] as! String}), status: status) }
return documents
}

func loadDocumentsData(for type: SavedKeyChainDataType) throws -> [NSDictionary]? {
let query = makeQuery(id: nil, for: type, bAll: true)
func loadDocumentsData(id: String?, docStatus: DocumentStatus, dataToLoadType: SavedKeyChainDataType = .doc, bCompatOldVersion: Bool = false) throws -> [[String: Any]]? {
var query = makeQuery(id: id, bForSave: false, status: docStatus, dataType: dataToLoadType)
if bCompatOldVersion { query[kSecAttrService as String] = if dataToLoadType == .doc { serviceName } else { serviceName + "_key" } } // to be removed in version 1
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))
}
return (result as! [NSDictionary])
var res = result as! [[String: Any]]
if !bCompatOldVersion, dataToLoadType == .doc {
if let dicts2 = try loadDocumentsData(id: id, docStatus: docStatus, dataToLoadType: .key, bCompatOldVersion: bCompatOldVersion) { res.append(contentsOf: dicts2) }
}
// following lines to be removed in version 1
if !bCompatOldVersion, dataToLoadType == .doc { if let dicts1 = try loadDocumentsData(id: id, docStatus: docStatus, dataToLoadType: .doc, bCompatOldVersion: true) { res.append(contentsOf: dicts1) } }
if !bCompatOldVersion, dataToLoadType == .key { if let dicts2 = try loadDocumentsData(id: id, docStatus: docStatus, dataToLoadType: .key, bCompatOldVersion: true) {dicts2.forEach { d in var d2 = d; d2[kSecAttrIsNegative as String] = true; res.append(d2) } } }
return res
}

/// Save the secret to keychain
/// Note: the value passed in will be zeroed out after the secret is saved
/// - Parameters:
@@ -78,26 +83,45 @@ public class KeyChainStorageService: DataStorageService {
}
}

func serviceToSave(for dataToSaveType: SavedKeyChainDataType) -> String {
switch dataToSaveType { case .key: serviceName + "_key"; default: serviceName }
/// Make a query for a an item in keychain
/// - Parameters:
/// - id: id
/// - bAll: request all matching items
/// - Returns: The dictionary query
func makeQuery(id: String?, bForSave: Bool, status: DocumentStatus, dataType: SavedKeyChainDataType) -> [String: Any] {
let comps = [serviceName, dataType.rawValue, status.rawValue ]
let queryValue = comps.joined(separator: ":")
var query: [String: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrService: queryValue] as [String: Any]
if !bForSave {
query[kSecReturnData as String] = true
query[kSecReturnAttributes as String] = true
query[kSecMatchLimit as String] = kSecMatchLimitAll
}
if let id { query[kSecAttrAccount as String] = id}
if let accessGroup, !accessGroup.isEmpty { query[kSecAttrAccessGroup as String] = accessGroup }
return query
}


static func getIsNegativeValueToUse(_ dataToSaveType: SavedKeyChainDataType) -> Bool { switch dataToSaveType { case .key: true; default: false } }

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
// kSecAttrAccount is used to store the secret Id (we save the document ID)
// kSecAttrService is a key whose value is a string indicating the item's service.
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]
var query: [String: Any] = makeQuery(id: document.id, bForSave: true, status: document.status, dataType: dataToSaveType)
#if os(macOS)
query[kSecUseDataProtectionKeychain as String] = true
#endif
query[kSecValueData as String] = switch dataToSaveType { case .key: document.privateKey!; default: document.data }
// use this attribute to differentiate between document and key data
query[kSecAttrIsNegative as String] = Self.getIsNegativeValueToUse(dataToSaveType)
query[kSecAttrLabel as String] = document.docType
query[kSecAttrType as String] = dataType
var status = SecItemAdd(query as CFDictionary, nil)
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]
let updated: [String: Any] = [kSecValueData: query[kSecValueData as String] as! Data, kSecAttrIsNegative: Self.getIsNegativeValueToUse(dataToSaveType), kSecAttrLabel: document.docType, kSecAttrType: dataType] as [String: Any]
query = makeQuery(id: document.id, bForSave: true, status: document.status, dataType: dataToSaveType)
status = SecItemUpdate(query as CFDictionary, updated as CFDictionary)
}
let statusMessage = SecCopyErrorMessageString(status, nil) as? String
@@ -110,63 +134,36 @@ public class KeyChainStorageService: DataStorageService {
/// Note: the value passed in will be zeroed out after the secret is deleted
/// - 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 deleteDocument(id: String, status: DocumentStatus) throws {
try deleteDocumentData(id: id, docStatus: status)
}

public func deleteDocumentData(id: String, for saveType: SavedKeyChainDataType) throws {
let query: [String: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrService: serviceToSave(for: saveType), kSecAttrAccount: id] as [String: Any]
public func deleteDocumentData(id: String?, docStatus: DocumentStatus, dataType: SavedKeyChainDataType = .doc) throws {
let query: [String: Any] = makeQuery(id: id, bForSave: true, status: docStatus, dataType: dataType)
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)) }
if dataType == .doc { try deleteDocumentData(id: id, docStatus: docStatus, dataType: .key) }
}

/// 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: serviceToSave(for: saveType)] 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))
}
}

/// Make a query for a an item in keychain
/// - Parameters:
/// - id: id
/// - bAll: request all matching items
/// - Returns: The dictionary query
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 }
return query
public func deleteDocuments(status: DocumentStatus) throws {
try deleteDocumentData(id: nil, docStatus: status)
}

/// Make a document from a keychain item
/// - Parameter dict: keychain item returned as dictionary
/// - Returns: the document
func makeDocument(dict1: NSDictionary, dict2: NSDictionary?) -> Document {
var data = dict1[kSecValueData] as! Data
static func makeDocument(dict1: [String: Any], dict2: [String: Any]?, status: DocumentStatus) -> Document {
var data = dict1[kSecValueData as String] as! Data
defer { let c = data.count; data.withUnsafeMutableBytes { memset_s($0.baseAddress, c, 0, c); return } }
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)
keyType = PrivateKeyType(rawValue: dict2[kSecAttrType as String] as? String ?? PrivateKeyType.derEncodedP256.rawValue)!
privateKeyData = (dict2[kSecValueData as String] 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)
return Document(id: dict1[kSecAttrAccount as String] as! String, docType: dict1[kSecAttrLabel as String] as? String ?? "", docDataType: DocDataType(rawValue: dict1[kSecAttrType as String] as? String ?? DocDataType.cbor.rawValue) ?? DocDataType.cbor, data: data, privateKeyType: keyType, privateKey: privateKeyData, createdAt: (dict1[kSecAttrCreationDate as String] as! Date), modifiedAt: dict1[kSecAttrModificationDate as String] as? Date, status: status)
}
}
Loading