diff --git a/Package.resolved b/Package.resolved index 606edab..0670210 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/spruceid/mobile-sdk-rs.git", "state" : { - "revision" : "e3ed692d6b9530601eefb30c8bab125c764ae291", - "version" : "0.0.26" + "revision" : "9deb085da5d26630045505cad6f46f11460d739d", + "version" : "0.0.27" } }, { diff --git a/Package.swift b/Package.swift index de36259..bbe3e98 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,8 @@ let package = Package( targets: ["SpruceIDMobileSdk"]) ], dependencies: [ - .package(url: "https://github.com/spruceid/mobile-sdk-rs.git", from: "0.0.26"), + // .package(url: "https://github.com/spruceid/mobile-sdk-rs.git", .branch("main")), + .package(url: "https://github.com/spruceid/mobile-sdk-rs.git", from: "0.0.27"), // .package(path: "../mobile-sdk-rs"), .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0") ], diff --git a/Sources/MobileSdk/StorageManager.swift b/Sources/MobileSdk/StorageManager.swift index c9b8467..daeb404 100644 --- a/Sources/MobileSdk/StorageManager.swift +++ b/Sources/MobileSdk/StorageManager.swift @@ -1,145 +1,138 @@ -// File: Storage Manager -// -// Store and retrieve sensitive data. Data is stored in the Application Support directory of the app, encrypted in -// place via the .completeFileProtection option, and marked as excluded from backups so it will not be included in -// iCloud backps. - -// -// Imports -// +/// Storage Manager +/// +/// Store and retrieve sensitive data. Data is stored in the Application Support directory of the app, encrypted in +/// place via the .completeFileProtection option, and marked as excluded from backups so it will not be included in +/// iCloud backps. import Foundation -// -// Code -// - -// Class: StorageManager -// Store and retrieve sensitive data. - -class StorageManager: NSObject { - // Local-Method: path() - // Get the path to the application support dir, appending the given file name to it. We use the application - // support directory because its contents are not shared. - // - // Arguments: - // file - the name of the file - // - // Returns: - // An URL for the named file in the app's Application Support directory. - - private func path(file: String) -> URL? { - do { - // Get the applications support dir, and tack the name of the thing we're storing on the end of it. - // This does imply that `file` should be a valid filename. - - let asdir = try FileManager.default.url(for: .applicationSupportDirectory, - in: .userDomainMask, - appropriateFor: nil, // Ignored - create: true) // May not exist, make if necessary. - - return asdir.appendingPathComponent(file) - } catch { // Did the attempt to get the application support dir fail? - print("Failed to get/create the application support dir.") - return nil - } - } - - // Method: add() - // Store a value for a specified key, encrypted in place. - // - // Arguments: - // key - the name of the file - // value - the data to store - // - // Returns: - // A boolean indicating success. - - func add(key: String, value: Data) -> Bool { - guard let file = path(file: key) else { return false } - - do { - try value.write(to: file, options: .completeFileProtection) - } catch { - print("Failed to write the data for '\(key)'.") - return false - } - - return true - } - - // Method: get() - // Get a value for the specified key. - // - // Arguments: - // key - the name associated with the data - // - // Returns: - // Optional data potentially containing the value associated with the key; may be `nil`. - - func get(key: String) -> Data? { - guard let file = path(file: key) else { return nil } - - do { - let data = try Data(contentsOf: file) - return data - } catch { - print("Failed to read '\(file)'.") - } - - return nil - } - - // Method: remove() - // Remove a key/value pair. Removing a nonexistent key/value pair is not an error. - // - // Arguments: - // key - the name of the file - // - // Returns: - // A boolean indicating success; at present, there is no failure path, but this may change in the future. - - func remove(key: String) -> Bool { - guard let file = path(file: key) else { return true } - - do { - try FileManager.default.removeItem(at: file) - } catch { - // It's fine if the file isn't there. - } - - return true - } - - // Method: sys_test() - // Check to see if everything works. - - func sys_test() { - let key = "test_key" - let value = Data("Some random string of text. 😎".utf8) - - if !add(key: key, value: value) { - print("\(self.classForCoder):\(#function): Failed add() key/value pair.") - return - } - - guard let payload = get(key: key) else { - print("\(self.classForCoder):\(#function): Failed get() value for key.") - return - } - - if !(payload == value) { - print("\(self.classForCoder):\(#function): Mismatch between stored & retrieved value.") - return - } - - if !remove(key: key) { - print("\(self.classForCoder):\(#function): Failed to delete key/value pair.") - return - } - - print("\(self.classForCoder):\(#function): Completed successfully.") - } +import SpruceIDMobileSdkRs + +// The following is a stripped-down version of the protocol definition from mobile-sdk-rs against which the storage +// manager is intended to link. + +/// Store and retrieve sensitive data. +class StorageManager: NSObject, StorageManagerInterface { + /// Get the path to the application support dir, appending the given file name to it. + /// + /// We use the application support directory because its contents are not shared. + /// + /// - Parameters: + /// - file: the name of the file + /// + /// - Returns: An URL for the named file in the app's Application Support directory. + + private func path(file: String) -> URL? { + do { + // Get the applications support dir, and tack the name of the thing we're storing on the end of it. + // This does imply that `file` should be a valid filename. + + let fileman = FileManager.default + let bundle = Bundle.main + + let asdir = try fileman.url(for: .applicationSupportDirectory, + in: .userDomainMask, + appropriateFor: nil, // Ignored + create: true) // May not exist, make if necessary. + + // If we create subdirectories in the application support directory, we need to put them in a subdir + // named after the app; normally, that's `CFBundleDisplayName` from `info.plist`, but that key doesn't + // have to be set, in which case we need to use `CFBundleName`. + + guard let appname = bundle.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? + bundle.object(forInfoDictionaryKey: "CFBundleName") as? String + else { + return nil + } + + let datadir: URL + + if #available(iOS 16.0, *) { + datadir = asdir.appending(path: "\(appname)/sprucekit/datastore/", directoryHint: .isDirectory) + } else { + datadir = asdir.appendingPathComponent("\(appname)/sprucekit/datastore/") + } + + if !fileman.fileExists(atPath: datadir.path) { + try fileman.createDirectory(at: datadir, withIntermediateDirectories: true, attributes: nil) + } + + return datadir.appendingPathComponent(file) + } catch { + return nil + } + } + + /// Store a value for a specified key, encrypted in place. + /// + /// - Parameters: + /// - key: the name of the file + /// - value: the data to store + /// + /// - Returns: a boolean indicating success + + func add(key: Key, value: Value) throws { + guard let file = path(file: key) else { throw StorageManagerError.InternalError } + + do { + try value.write(to: file, options: .completeFileProtection) + } catch { + throw StorageManagerError.InternalError + } + } + + /// Get a value for the specified key. + /// + /// - Parameters: + /// - key: the name associated with the data + /// + /// - Returns: optional data potentially containing the value associated with the key; may be `nil` + + func get(key: Key) throws -> Value? { + guard let file = path(file: key) else { throw StorageManagerError.InternalError } + + do { + return try Data(contentsOf: file) + } catch { + throw StorageManagerError.InternalError + } + } + + /// List the the items in storage. + /// + /// Note that this will list all items in the `application support` directory, potentially including any files + /// created by other systems. + /// + /// - Returns: a list of items in storage + + func list() throws -> [Key] { + guard let asdir = path(file: "")?.path else { return [String]() } + + do { + return try FileManager.default.contentsOfDirectory(atPath: asdir) + } catch { + throw StorageManagerError.InternalError + } + } + + /// Remove a key/value pair. + /// + /// Removing a nonexistent key/value pair is not an error. + /// + /// - Parameters: + /// - key: the name of the file + /// + /// - Returns: a boolean indicating success; at present, there is no failure path, but this may change + + func remove(key: Key) throws { + guard let file = path(file: key) else { return } + + do { + try FileManager.default.removeItem(at: file) + } catch { + // It's fine if the file isn't there. + } + } } // diff --git a/Sources/MobileSdk/ui/QRCodeScanner.swift b/Sources/MobileSdk/ui/QRCodeScanner.swift index 69cb744..19de9e8 100644 --- a/Sources/MobileSdk/ui/QRCodeScanner.swift +++ b/Sources/MobileSdk/ui/QRCodeScanner.swift @@ -83,7 +83,7 @@ public struct QRCodeScanner: View { @State private var qrOutput: AVCaptureMetadataOutput = .init() /// Camera QR Output delegate - @StateObject private var qrDelegate = QRScannerDelegate() + @ObservedObject private var qrDelegate = QRScannerDelegate() // Was @StateObject, but that requires iOS 14. /// Scanned code @State private var scannedCode: String = "" diff --git a/SpruceIDMobileSdk.podspec b/SpruceIDMobileSdk.podspec index 5fd5ed8..b4eb251 100644 --- a/SpruceIDMobileSdk.podspec +++ b/SpruceIDMobileSdk.podspec @@ -20,7 +20,7 @@ Pod::Spec.new do |spec| spec.source_files = "Sources/MobileSdk/*.swift" spec.static_framework = true - spec.dependency 'SpruceIDMobileSdkRs', "~> 0.0.26" + spec.dependency 'SpruceIDMobileSdkRs', "~> 0.0.27" spec.dependency 'SwiftAlgorithms', "~> 1.0.0" spec.frameworks = 'Foundation', 'CoreBluetooth', 'CryptoKit' end diff --git a/Tests/MobileSdkTests/StorageManager.swift b/Tests/MobileSdkTests/StorageManager.swift new file mode 100644 index 0000000..8b49e8d --- /dev/null +++ b/Tests/MobileSdkTests/StorageManager.swift @@ -0,0 +1,18 @@ +import XCTest +@testable import SpruceIDMobileSdk + +final class StorageManagerTest: XCTestCase { + func testStorage() throws { + let storeman = StorageManager() + let key = "test_key" + let value = Data("Some random string of text. 😎".utf8) + + XCTAssertNoThrow(try storeman.add(key: key, value: value)) + + let payload = try storeman.get(key: key) + + XCTAssert(payload == value, "\(classForCoder):\(#function): Mismatch between stored & retrieved value.") + + XCTAssertNoThrow(try storeman.remove(key: key)) + } +} diff --git a/project.yml b/project.yml index 4517209..c856b5f 100644 --- a/project.yml +++ b/project.yml @@ -4,7 +4,7 @@ options: packages: SpruceIDMobileSdkRs: url: https://github.com/spruceid/mobile-sdk-rs - from: 0.0.4 + from: 0.0.27 # path: "../mobile-sdk-rs" SwiftAlgorithms: url: https://github.com/apple/swift-algorithms