Profile
Back to NewsBack
GitHub Trending 9 min
Reader Mode
alexruperez/SecurePropertyStorage: Helps you define secure storages for your properties using Swift property wrappers.

alexruperez/SecurePropertyStorage: Helps you define secure storages for your properties using Swift property wrappers.

14 hours ago

🔐 Secure Property Storage

Helps you define secure storages for your properties using Swift property wrappers.

Twitter</a> CI</a> Swift</a> License</a> Swift Package Manager</a> Carthage</a> Bitrise</a> Maintainability</a> codecov</a> Quality</a>

🌟 Features

All keys are hashed using SHA512 and all values are encrypted using AES-GCM to keep user information safe, automagically. Symmetric key is stored in Keychain in a totally secure way.

🐒 Basic usage

@UserDefault

This property wrapper will store your property in UserDefaults using StoreKey (any String but i recommend you a String typed enum). Optionally, you can assign a default value to the property that will be secure stored at initialization.

@UserDefault(<#StoreKey#>)
var yourProperty: YourType? = yourDefaultValueIfNeeded

UserDefaultsStorage is also available, a subclass of UserDefaults with all the security provided by this library, where you can customize suite name.

@Keychain

This property wrapper will store your property in Keychain using StoreKey.

@Keychain(<#StoreKey#>)
var yourProperty: YourType? = yourDefaultValueIfNeeded

As UserDefaultsStorage, KeychainStorage is also available, where you can customize access, group and synchronize it with iCloud.

@Singleton

This property wrapper will store your property in a memory singleton, every property with the same wrapper and key can access or modify the value from wherever it is.

@Singleton(<#StoreKey#>)
var yourProperty: YourType? = yourDefaultValueIfNeeded

As KeychainStorage, SingletonStorage is also available.

@Inject

This property wrapper is similar to @Singleton but, together with @Register, will inject your dependencies. More details in Dependency Injection usage guide.

@Inject
var yourDependency: YourProtocol?

As SingletonStorage, InjectStorage is also available.

@Store

This is a custom wrapper, you can define your own Storage protocol implementation.

@Store(<#YourStorage#>, <#StoreKey#>)
var yourProperty: YourType? = yourDefaultValueIfNeeded

As InjectStorage, DelegatedStorage is also available with all the magic of this library.

🧙‍♂️ Codable usage

If your property conforms Codable protocol, just add Codable keyword as prefix of your property wrapper.

  • @CodableUserDefault
  • @CodableKeychain
  • @CodableSingleton
  • @CodableStore

🥡 Unwrapped usage

To avoid continually unwrapping your property, just add Unwrapped keyword as prefix of your property wrapper, assign a default value (mandatory except for @UnwrappedInject), and it will return stored value or default value, but your property will always be there for you.

  • @UnwrappedUserDefault
  • @UnwrappedKeychain
  • @UnwrappedSingleton
  • @UnwrappedInject
  • @UnwrappedStore

🥡 + 🧙‍♂️ Combo usage

You can also combine previous cases in case you need it, unwrapped first please.

  • @UnwrappedCodableUserDefault
  • @UnwrappedCodableKeychain
  • @UnwrappedCodableSingleton
  • @UnwrappedCodableStore

💉 Dependency Injection usage

@Register (click to expand)

This property wrapper will register the implementations of your dependencies. Register them wherever you want before inject it, but be sure to do it only once (except if you use qualifiers), for example, in an Injector class. You can register through a protocol or directly using your class implementation.

@Register
var yourDependency: YourProtocol = YourImplementation()

@Register var yourDependency = YourImplementation()

You can also define a closure that builds your dependency. Just remember cast your dependency if you are going to inject it through a protocol.

@Register
var yourDependency = {
    YourImplementation() as YourProtocol
}

@Register var yourDependency = { YourImplementation() }

You can also register your dependencies only after the moment someone tries to inject them and you haven't registered them yet, for this you can use the error closure.

InjectStorage.standard.errorClosure = { error in
    if case InjectError.notFound = error {
        YourImplementation.register()
    }
}

You can get this syntactic sugar because you can now use property wrappers in function parameters.

static func register(@Register yourDependency: YourProtocol = YourImplementation()) {}

@Inject and @UnwrappedInject (click to expand)

These property wrappers injects your dependencies @Register implementations.

@Inject
var yourDependency: YourProtocol?

@Inject var yourDependency: YourImplementation?

@UnwrappedInject var yourUnwrappedDependency: YourProtocol

@UnwrappedInject var yourUnwrappedDependency: YourImplementation

Scope

Because these property wrappers works similarly to @Singleton, the default scope is .singleton, but if you use builder closures on @Register, you can modify them to inject a single instance.

@Inject(.instance)
var yourDependency: YourProtocol?

@UnwrappedInject(.instance) var yourUnwrappedDependency: YourProtocol

@InjectWith and @UnwrappedInjectWith (click to expand)

Your dependency may need parameters when injecting, you can pass them with these property wrappers. Simply define a model with your dependency parameters and pass it. It will inject a new instance built with these parameters.

@Register
var yourDependency = { parameters in
    YourImplementation(parameters) as YourProtocol
}

@Inject(YourParameters()) var yourDependency: YourProtocol?

@UnwrappedInject(YourParameters()) var yourUnwrappedDependency: YourProtocol

Qualifiers (click to expand)

You can use qualifiers to provide various implementations of a particular dependency. A qualifier is just a @objc protocol that you apply to a class.

For example, you could declare Dog and Cat qualifier protocols and apply it to another class that conforms Animal protocol. To declare this qualifier, use the following code:

protocol Animal {
  func sound()
}

@objc protocol Dog {}

@objc protocol Cat {}

You can then define multiple classes that conforms Animal protocol and uses this qualifiers:

class DogImplementation: Animal, Dog {
    func sound() { print("Woof!") }
}

class CatImplementation: Animal, Cat { func sound() { print("Meow!") } }

Both implementations of the class can now be @Register:

@Register
var registerDog: Animal = DogImplementation()

@Register var registerCat: Animal = CatImplementation()

To inject one or the other implementation, simply add the qualifier(s) to your @Inject:

@UnwrappedInject(Dog.self)
var dog: Animal

@UnwrappedInject(Cat.self) var cat: Animal

dog.sound() // prints Woof! cat.sound() // prints Meow!

Testing (click to expand)

One of the advantages of dependency injection is that the code can be easily testable with mock implementation. That is why there is a Mock qualifier that has priority over all, so you can have your dependencies defined in the app and create your mock in the test target simply by adding this qualifier.

// App target

class YourImplementation: YourProtocol {}

@Register var yourDependency: YourProtocol = YourImplementation()

@Inject var yourDependency: YourProtocol?

// Test target

class YourMock: YourProtocol, Mock {}

@Register var yourDependency: YourProtocol = YourMock()

Groups (click to expand)

When you have a lot of dependencies in your app, you may want to optimize dependency resolution.

You can group them using @Register(group:) and a DependencyGroupKey:

@Register(group: <#DependencyGroupKey#>)
var yourDependency: YourProtocol = YourImplementation()

@Inject(group:) will look for those dependencies only in that group:

@Inject(group: <#DependencyGroupKey#>)
var yourDependency: YourProtocol?

🔒 Concurrency and Sendable

This library is designed with Swift's modern concurrency features in mind. Access to the underlying storage mechanisms (UserDefaults, Keychain, etc.) is serialized through a global actor (StorageActor), preventing data races during read and write operations performed by the library itself.

However, for your application to be fully concurrency-safe when using this library, it is crucial that the types you store and retrieve are Sendable.

  • Generic Types: When using generic property wrappers or methods like @Store var item: MyType?, storage.value(forKey: "key") as? MyType, or storage.decodable(forKey: "key") as? MyDecodableType, ensure that MyType and MyDecodableType conform to the Sendable protocol.
  • Object Archiving: If you use methods that rely on NSKeyedArchiver (such as storage.set(object: myNSObject, forKey: "key")), the class myNSObject should conform to NSSecureCoding for security and also be Sendable for concurrency safety.
  • Why is Sendable important?: While the library ensures that the act of storing or retrieving data is safe, if the data itself (e.g., a class instance) is not Sendable, then passing it across different concurrent tasks or actors after retrieval can lead to data races if the data is mutable. Conforming to Sendable helps Swift enforce that such types can be safely shared.
Enabling strict concurrency checking in your project's build settings (e.g., "Strict Concurrency Checking" set to "Complete" in Xcode) is highly recommended to help identify potential concurrency issues at compile time.

👀 Examples

Talk is cheap. Show me the code.
// Securely stored in UserDefaults.
    @UserDefault("username")
    var username: String?

// Securely stored in Keychain. @Keychain("password") var password: String?

// Securely stored in a Singleton storage. @Singleton("sessionToken") var sessionToken: String?

// Securely stored in a Singleton storage. // Always has a value, the stored or the default. @UnwrappedSingleton("refreshToken") var refreshToken: String = "B0610306-A33F"

struct User: Codable { let username: String let password: String? let sessionToken: String? }

// Codable model securely stored in UserDefaults. @CodableUserDefault("user") var user: User?

🛠 Compatibility

| Platform | Minimum deployment target | |---|---| | iOS / iPadOS | 15.0 | | macOS | 12.0 | | tvOS | 15.0 | | watchOS | 9.0 | | visionOS | 1.0 |

  • Developed and tested with Xcode 27.1 and Swift 6.4 (swift-tools-version:6.0, Swift 6 language mode).
  • Mac Catalyst builds are supported as well.
  • Continuous integration (see .github/workflows/ci.yml) runs the full test suite on an iPhone 17 Simulator (iOS 27.0) and on macOS (swift test),
and verifies that the package builds for macOS, Mac Catalyst, iOS, tvOS, watchOS and visionOS and that it can be consumed as an external Swift Package.
  • tvOS, watchOS and visionOS are compile-verified only: the test suite is not executed on those platforms.
  • If you need to support older OS versions, use the previous releases (0.8.2 supports iOS 13, macOS 11.5, tvOS 13 and watchOS 6).

⚙️ Installation

You can use the Swift Package Manager by declaring SecurePropertyStorage as a dependency in your Package.swift file:

.package(url: "https://github.com/alexruperez/SecurePropertyStorage", from: "0.8.2")

By default, all property wrappers are installed and you can import them, but if you want, you can install only some of them:

  • UserDefault: @*UserDefault property wrappers.
  • Keychain: @*Keychain property wrappers.
  • Singleton: @*Singleton property wrappers.
  • Storage: @*Store property wrappers.
  • Inject: @*Inject property wrappers.
Then add the products you need to your target dependencies:
.target(
    name: "YourTarget",
    dependencies: [
        .product(name: "SecurePropertyStorage", package: "SecurePropertyStorage")
        // or only some of them: "UserDefault", "Keychain", "Singleton", "Inject", "Storage"
    ]
)

In Xcode: File ▸ Add Package Dependencies… and enter https://github.com/alexruperez/SecurePropertyStorage.

For more information, see the Swift Package Manager documentation.

Or you can use Carthage:

github "alexruperez/SecurePropertyStorage"

🧪 Development

swift build                      # Build all targets
swift test                       # Run the test suite on macOS

Run the test suite on the iPhone 17 Simulator (iOS 27.0), as the CI does

xcodebuild test \ -project SecurePropertyStorage.xcodeproj \ -scheme SecurePropertyStorage-Package \ -destination 'platform=iOS Simulator,name=iPhone 17,OS=27.0'

Compile-check another platform

xcodebuild build \ -project SecurePropertyStorage.xcodeproj \ -scheme SecurePropertyStorage-Package \ -destination 'generic/platform=watchOS Simulator'

Notes for contributors:

  • Test classes are isolated to @StorageActor and their test methods are async, so XCTest runs them on that actor.
  • The Keychain needs entitlements that are not available to swift test on macOS; the Keychain tests therefore assert the error path there.
StorageTests use an in-memory delegate and cover the encryption logic deterministically.
  • Please keep the public API source compatible and add a CHANGELOG entry for every user-facing change.

🍻 Etc.

👨‍💻 Author

Alex Rupérez – @alexruperez

👮‍♂️ License

SecurePropertyStorage is available under the MIT license. See the LICENSE file for more info.

Chat with me