Skip to content

Commit

Permalink
Support CodableWithConfiguration when storing and retrieving from Loc…
Browse files Browse the repository at this point in the history
…alStorage (#24)

# Support CodableWithConfiguration when storing and retrieving from
LocalStorage

## ♻️ Current situation & Problem
As of know, we only support storage and retrieval using `Encodable` and
`Decodable` types. iOS 17 introduced a new set of protocols:
`EncodableWithConfiguration` and `DecodableWithConfiguration`. Instead
of relying on the `userInfo` dictionary and runtime checks to ensure
configuration is supplied on the call site, type can now strictly define
a certain configuration type that has to be supplied when encoding or
decoding. There is also a new
[`@CodableConfiguration`](https://developer.apple.com/documentation/foundation/codableconfiguration)
property wrapper that can be used to statically define configurations
for nested types.

To embrace these strongly typed configurations, SpeziStorage add support
to accept types of these new protocols. We rely on the newly introduced
`TopLevelEncoder` and `TopLevelDecoder` protocols introduced with
StanfordSpezi/SpeziFoundation#15 to stay generic
over the encoder and decoder.

## ⚙️ Release Notes 
* Support storage and retrieval of `CodableWithConfiguration` types.
* Improved documentation.


## 📚 Documentation
This PR reworks some of the documentation structure of SpeziStorage.
Reducing some of the fluff and reorganizing the catalog a bit.

## ✅ Testing
--

## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
Supereg authored Aug 28, 2024
1 parent 099e059 commit 6096237
Show file tree
Hide file tree
Showing 12 changed files with 181 additions and 93 deletions.
8 changes: 5 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,16 @@ let package = Package(
.library(name: "SpeziSecureStorage", targets: ["SpeziSecureStorage"])
],
dependencies: [
.package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.2.1"),
.package(url: "https://github.com/StanfordBDHG/XCTRuntimeAssertions", from: "1.0.1")
.package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.7.3"),
.package(url: "https://github.com/StanfordSpezi/SpeziFoundation", from: "2.0.0-beta.2"),
.package(url: "https://github.com/StanfordBDHG/XCTRuntimeAssertions", from: "1.1.1")
] + swiftLintPackage(),
targets: [
.target(
name: "SpeziLocalStorage",
dependencies: [
.product(name: "Spezi", package: "Spezi"),
.product(name: "SpeziFoundation", package: "SpeziFoundation"),
.target(name: "SpeziSecureStorage")
],
swiftSettings: [
Expand Down Expand Up @@ -83,7 +85,7 @@ func swiftLintPlugin() -> [Target.PluginUsage] {

func swiftLintPackage() -> [PackageDescription.Package.Dependency] {
if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil {
[.package(url: "https://github.com/realm/SwiftLint.git", .upToNextMinor(from: "0.55.1"))]
[.package(url: "https://github.com/realm/SwiftLint.git", from: "0.55.1")]
} else {
[]
}
Expand Down
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ You need to add the Spezi Storage Swift package to
> [!IMPORTANT]
> If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) to set up the core Spezi infrastructure.
You can configure the [`LocalStorage`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezilocalstorage/localstorage) or [`SecureStorage`](https://swiftpackageindex.com/StanfordSpezi/SpeziStorage/documentation/spezisecurestorage) module in the [`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate).

> [!IMPORTANT]
> If you use SpeziStorage on the macOS platform, ensure to add the [`Keychain Access Groups` entitlement](https://developer.apple.com/documentation/bundleresources/entitlements/keychain-access-groups) to the enclosing Xcode project via *PROJECT_NAME > Signing&Capabilities > + Capability*. The array of keychain groups can be left empty, only the base entitlement is required.
You can configure the [`LocalStorage`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezilocalstorage/localstorage) or [`SecureStorage`](https://swiftpackageindex.com/StanfordSpezi/SpeziStorage/documentation/spezisecurestorage) module in the [`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate).

```swift
import Spezi
import SpeziLocalStorage
Expand All @@ -52,7 +52,7 @@ class ExampleDelegate: SpeziAppDelegate {
}
```

You can then use the [`LocalStorage`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezilocalstorage/localstorage) or [`SecureStorage`](https://swiftpackageindex.com/StanfordSpezi/SpeziStorage/documentation/spezisecurestorage) class in any SwiftUI view.
You can then use the `LocalStorage` or `SecureStorage` class in any SwiftUI view.

```swift
struct ExampleStorageView: View {
Expand All @@ -66,15 +66,15 @@ struct ExampleStorageView: View {
}
```

Alternatively, it is common to use the [`LocalStorage`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezilocalstorage/localstorage) or [`SecureStorage`](https://swiftpackageindex.com/StanfordSpezi/SpeziStorage/documentation/spezisecurestorage) module in other modules as a dependency: [Spezi Module dependencies](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module-dependency).
Alternatively, it is common to use the `LocalStorage` or `SecureStorage` module in other modules as a dependency: [Spezi Module dependencies](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module-dependency).


## Local Storage

The [`LocalStorage`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezilocalstorage/localstorage) module enables the on-disk storage of data in mobile applications.
The `LocalStorage` module enables the on-disk storage of data in mobile applications.

The [`LocalStorage`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezilocalstorage/localstorage) module defaults to storing data encrypted supported by the [`SecureStorage`](https://swiftpackageindex.com/StanfordSpezi/SpeziStorage/documentation/spezisecurestorage) module.
The [`LocalStorageSetting`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezilocalstorage/localstoragesetting) enables configuring how data in the [`LocalStorage`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezilocalstorage/localstorage) module can be stored and retrieved.
The `LocalStorage` module defaults to storing data encrypted supported by the `SecureStorage` module.
The [`LocalStorageSetting`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezilocalstorage/localstoragesetting) enables configuring how data in the `LocalStorage` module can be stored and retrieved.

- Store or update new elements: [`store(_:encoder:storageKey:settings:)`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezilocalstorage/localstorage/store(_:encoder:storagekey:settings:))
- Retrieve existing elements: [`read(_:decoder:storageKey:settings:)`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezilocalstorage/localstorage/read(_:decoder:storagekey:settings:))
Expand All @@ -83,13 +83,13 @@ The [`LocalStorageSetting`](https://swiftpackageindex.com/stanfordspezi/spezisto

## Secure Storage

The [`SecureStorage`](https://swiftpackageindex.com/StanfordSpezi/SpeziStorage/documentation/spezisecurestorage) module allows for the encrypted storage of small chunks of sensitive user data, such as usernames and passwords for internet services, using Apple's [Keychain documentation](https://developer.apple.com/documentation/security/keychain_services/keychain_items/using_the_keychain_to_manage_user_secrets).
The `SecureStorage` module allows for the encrypted storage of small chunks of sensitive user data, such as usernames and passwords for internet services, using Apple's [Keychain documentation](https://developer.apple.com/documentation/security/keychain_services/keychain_items/using_the_keychain_to_manage_user_secrets).

Credentials can be stored in the Secure Enclave (if available) or the Keychain. Credentials stored in the Keychain can be made synchronizable between different instances of user devices.

### Handling Credentials

Use the [`SecureStorage`](https://swiftpackageindex.com/StanfordSpezi/SpeziStorage/documentation/spezisecurestorage) module to store a set of [`Credentials`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezisecurestorage/credentials) instances in the Keychain associated with a server that is synchronizable between different devices.
Use the `SecureStorage` module to store a set of [`Credentials`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezisecurestorage/credentials) instances in the Keychain associated with a server that is synchronizable between different devices.

- Store new credentials: [`store(credentials:server:removeDuplicate:storageScope:)`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezisecurestorage/securestorage/store(credentials:server:removeduplicate:storagescope:))
- Retrieve existing credentials: [`retrieveCredentials(_:server:accessGroup:)`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezisecurestorage/securestorage/retrievecredentials(_:server:accessgroup:))
Expand All @@ -101,7 +101,7 @@ Use the [`SecureStorage`](https://swiftpackageindex.com/StanfordSpezi/SpeziStora

### Handling Keys

Similar to [`Credentials`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezisecurestorage/credentials) instances, you can also use the [`SecureStorage`](https://swiftpackageindex.com/StanfordSpezi/SpeziStorage/documentation/spezisecurestorage) module to interact with keys.
Similar to `Credentials` instances, you can also use the `SecureStorage` module to interact with keys.

- Create new keys: [`createKey(_:size:storageScope:)`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezisecurestorage/securestorage/createkey(_:size:storagescope:))
- Retrieve existing public keys: [`retrievePublicKey(forTag:)`](https://swiftpackageindex.com/stanfordspezi/spezistorage/documentation/spezisecurestorage/securestorage/retrievepublickey(fortag:))
Expand Down
117 changes: 97 additions & 20 deletions Sources/SpeziLocalStorage/LocalStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,36 @@
// SPDX-License-Identifier: MIT
//

import Combine
import Foundation
import Security
import Spezi
import SpeziFoundation
import SpeziSecureStorage


/// On-disk storage of data in mobile applications.
///
/// The module relies on the [`SecureStorage`](https://swiftpackageindex.com/StanfordSpezi/SpeziStorage/documentation/spezisecurestorage)
/// module to enable an encrypted on-disk storage as defined by the ``LocalStorageSetting`` configuration.
/// module to enable an encrypted on-disk storage. You configuration encryption using the ``LocalStorageSetting`` type.
///
/// Use ``store(_:encoder:storageKey:settings:)`` to store elements on disk and define the settings using a `LocalStorageSetting` instance.
/// ## Topics
///
/// Use ``read(_:decoder:storageKey:settings:)`` to read elements on disk which are decoded as define by passed in `LocalStorageSetting` instance.
/// ### Configuration
/// - ``init()``
///
/// ### Storing Elements
/// - ``store(_:encoder:storageKey:settings:)``
/// - ``store(_:configuration:encoder:storageKey:settings:)``
///
/// ### Loading Elements
///
/// - ``read(_:decoder:storageKey:settings:)``
/// - ``read(_:configuration:decoder:storageKey:settings:)``
///
/// ### Deleting Entries
///
/// - ``delete(_:)``
/// - ``delete(storageKey:)``
public final class LocalStorage: Module, DefaultInitializable, EnvironmentAccessible, @unchecked Sendable {
private let encryptionAlgorithm: SecKeyAlgorithm = .eciesEncryptionCofactorX963SHA256AESGCM
@Dependency private var secureStorage = SecureStorage()
Expand All @@ -42,11 +57,11 @@ public final class LocalStorage: Module, DefaultInitializable, EnvironmentAccess
}


/// The ``LocalStorage`` initializer.
/// Configure the `LocalStorage` module.
public required init() {}


/// Store elements on disk and define the settings using a ``LocalStorageSetting`` instance.
/// Store elements on disk.
///
/// ```swift
/// struct Note: Codable, Equatable {
Expand Down Expand Up @@ -74,9 +89,40 @@ public final class LocalStorage: Module, DefaultInitializable, EnvironmentAccess
storageKey: String? = nil,
settings: LocalStorageSetting = .encryptedUsingKeyChain()
) throws where D.Output == Data {
try store(element, storageKey: storageKey, settings: settings) { element in
try encoder.encode(element)
}
}

/// Store elements on disk that require additional configuration for encoding.
///
/// - Parameters:
/// - element: The element that should be stored conforming to `Encodable`
/// - configuration: A configuration that provides additional information for encoding.
/// - encoder: The `Encoder` to use for encoding the `element`.
/// - storageKey: An optional storage key to identify the file.
/// - settings: The ``LocalStorageSetting``s applied to the file on disk.
public func store<C: EncodableWithConfiguration, D: TopLevelEncoder>(
_ element: C,
configuration: C.EncodingConfiguration,
encoder: D = JSONEncoder(),
storageKey: String? = nil,
settings: LocalStorageSetting = .encryptedUsingKeyChain()
) throws where D.Output == Data {
try store(element, storageKey: storageKey, settings: settings) { element in
try encoder.encode(element, configuration: configuration)
}
}

private func store<C>(
_ element: C,
storageKey: String?,
settings: LocalStorageSetting,
encoding: (C) throws -> Data
) throws {
var fileURL = fileURL(from: storageKey, type: C.self)
let fileExistsAlready = FileManager.default.fileExists(atPath: fileURL.path)

// Called at the end of each execution path
// We can not use defer as the function can potentially throw an error.
func setResourceValues() throws {
Expand All @@ -95,17 +141,17 @@ public final class LocalStorage: Module, DefaultInitializable, EnvironmentAccess
}
}

let data = try encoder.encode(element)
let data = try encoding(element)



// Determine if the data should be encrypted or not:
guard let keys = try settings.keys(from: secureStorage) else {
// No encryption:
try data.write(to: fileURL)
try setResourceValues()
return
}

// Encryption enabled:
guard SecKeyIsAlgorithmSupported(keys.publicKey, .encrypt, encryptionAlgorithm) else {
throw LocalStorageError.encryptionNotPossible
Expand All @@ -115,13 +161,13 @@ public final class LocalStorage: Module, DefaultInitializable, EnvironmentAccess
guard let encryptedData = SecKeyCreateEncryptedData(keys.publicKey, encryptionAlgorithm, data as CFData, &encryptError) as Data? else {
throw LocalStorageError.encryptionNotPossible
}

try encryptedData.write(to: fileURL)
try setResourceValues()
}



/// Read elements on disk which are decoded as defined by passed in ``LocalStorageSetting`` instance.
/// Read elements from disk.
///
/// ```swift
/// do {
Expand All @@ -144,14 +190,45 @@ public final class LocalStorage: Module, DefaultInitializable, EnvironmentAccess
storageKey: String? = nil,
settings: LocalStorageSetting = .encryptedUsingKeyChain()
) throws -> C where D.Input == Data {
try read(storageKey: storageKey, settings: settings) { data in
try decoder.decode(type, from: data)
}
}

/// Read elements from disk that require additional configuration for decoding.
///
/// - Parameters:
/// - type: The `Decodable` type that is used to decode the data from disk.
/// - configuration: A configuration that provides additional information for decoding.
/// - decoder: The `Decoder` to use to decode the stored data into the provided `type`.
/// - storageKey: An optional storage key to identify the file.
/// - settings: The ``LocalStorageSetting``s used to retrieve the file on disk.
/// - Returns: The element conforming to `Decodable`.
public func read<C: DecodableWithConfiguration, D: TopLevelDecoder>( // swiftlint:disable:this function_default_parameter_at_end
_ type: C.Type = C.self,
configuration: C.DecodingConfiguration,
decoder: D = JSONDecoder(),
storageKey: String? = nil,
settings: LocalStorageSetting = .encryptedUsingKeyChain()
) throws -> C where D.Input == Data {
try read(storageKey: storageKey, settings: settings) { data in
try decoder.decode(type, from: data, configuration: configuration)
}
}

private func read<C>(
storageKey: String?,
settings: LocalStorageSetting,
decoding: (Data) throws -> C
) throws -> C {
let fileURL = fileURL(from: storageKey, type: C.self)
let data = try Data(contentsOf: fileURL)

// Determine if the data should be decrypted or not:
guard let keys = try settings.keys(from: secureStorage) else {
return try decoder.decode(C.self, from: data)
return try decoding(data)
}

guard SecKeyIsAlgorithmSupported(keys.privateKey, .decrypt, encryptionAlgorithm) else {
throw LocalStorageError.decryptionNotPossible
}
Expand All @@ -160,10 +237,10 @@ public final class LocalStorage: Module, DefaultInitializable, EnvironmentAccess
guard let decryptedData = SecKeyCreateDecryptedData(keys.privateKey, encryptionAlgorithm, data as CFData, &decryptError) as Data? else {
throw LocalStorageError.decryptionNotPossible
}
return try decoder.decode(C.self, from: decryptedData)

return try decoding(decryptedData)
}


/// Deletes a file stored on disk identified by the `storageKey`.
///
Expand All @@ -185,7 +262,7 @@ public final class LocalStorage: Module, DefaultInitializable, EnvironmentAccess

/// Deletes a file stored on disk defined by a `Decodable` type that is used to derive the storage key.
///
/// Use ``delete(storageKey:)`` to manually define the storage key.
/// - Note: Use ``delete(storageKey:)`` to manually define the storage key.
///
/// - Parameters:
/// - type: The `Encodable` type that is used to store the type originally.
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziLocalStorage/LocalStorageError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//


/// An error thrown by the ``LocalStorage`` module.
/// An error thrown by the `LocalStorage` module.
enum LocalStorageError: Error {
/// Encryption of the file was not possible, did not store the data on disk.
case encryptionNotPossible
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziLocalStorage/LocalStorageSetting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Spezi
import SpeziSecureStorage


/// Configure how data in the ``LocalStorage`` module can be stored and retrieved.
/// Configure how data can be stored and retrieved.
public enum LocalStorageSetting {
/// Unencrypted
case unencrypted(excludedFromBackup: Bool = true)
Expand Down
Loading

0 comments on commit 6096237

Please sign in to comment.