Skip to content

Commit

Permalink
Merge pull request #16 from eu-digital-identity-wallet/develop
Browse files Browse the repository at this point in the history
Add support for loading and deleting documents by status
  • Loading branch information
phisakel authored Jul 16, 2024
2 parents 999410d + 0cd89f3 commit 8ebf1f0
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 102 deletions.
22 changes: 10 additions & 12 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
# This workflow will build a Swift project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift
name: Swift Build

name: Swift

on: [push]
on:
push:
branches-ignore:
- 'dependabot/*'
pull_request_target:
workflow_dispatch:

jobs:
build:

runs-on: macos-latest-xlarge

steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest
- uses: swift-actions/setup-swift@v1
xcode-version: latest-stable
- uses: swift-actions/setup-swift@v2
- name: Get swift version
run: swift --version
- uses: actions/checkout@v3
- name: Fix Up Private GitHub URLs
# Add personal access token to all private repo URLs
run: find . -type f \( -name 'Package.swift' -o -name 'Package.resolved' \) -exec sed -i '' "s/https:\/\/github.com\/eu-digital-identity-wallet/https:\/\/${{ secrets.USER_NAME }}:${{ secrets.USER_GITHUB_TOKEN }}@github.com\/eu-digital-identity-wallet/g" {} \;
- uses: actions/checkout@v4
- name: Build
run: swift build
- name: Run tests
Expand Down
9 changes: 4 additions & 5 deletions Sources/WalletStorage/DataStorageService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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))? {
Expand All @@ -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
Expand Up @@ -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
Expand All @@ -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
Expand Up @@ -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
Expand Down
141 changes: 69 additions & 72 deletions Sources/WalletStorage/KeyChainStorageService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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)
}
}

0 comments on commit 8ebf1f0

Please sign in to comment.