Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-14 10:24:05 -06:00
parent 7a459daa14
commit 0f208488a5
4 changed files with 192 additions and 0 deletions

View File

@ -151,5 +151,31 @@ The app owns WCSession activation and handling incoming updates.
## Testing
- Unit tests use Swift Testing (`Testing` package)
## Storage Audit
LocalData can generate a catalog of all configured storage keys, even if no data has been written yet. This is useful for security reviews and compliance.
1) Define a catalog in your app that lists all keys:
```swift
struct AppStorageCatalog: StorageKeyCatalog {
static var allKeys: [StorageKeyDescriptor] {
[
.from(StorageKeys.AppVersionKey(), serializer: "json"),
.from(StorageKeys.UserPreferencesKey(), serializer: "json")
]
}
}
```
2) Generate a report:
```swift
let report = StorageAuditReport.renderText(for: AppStorageCatalog.self)
print(report)
```
For dynamic key names, use a placeholder name and a note to describe how it is generated.
## Sample App
See `SecureStorgageSample` for working examples of all storage domains and security options.

View File

@ -0,0 +1,53 @@
import Foundation
public struct StorageKeyDescriptor: Sendable {
public let name: String
public let domain: StorageDomain
public let security: SecurityPolicy
public let serializer: String
public let valueType: String
public let owner: String
public let availability: PlatformAvailability
public let syncPolicy: SyncPolicy
public let notes: String?
public init(
name: String,
domain: StorageDomain,
security: SecurityPolicy,
serializer: String,
valueType: String,
owner: String,
availability: PlatformAvailability,
syncPolicy: SyncPolicy,
notes: String? = nil
) {
self.name = name
self.domain = domain
self.security = security
self.serializer = serializer
self.valueType = valueType
self.owner = owner
self.availability = availability
self.syncPolicy = syncPolicy
self.notes = notes
}
public static func from<Key: StorageKey>(
_ key: Key,
serializer: String,
notes: String? = nil
) -> StorageKeyDescriptor {
StorageKeyDescriptor(
name: key.name,
domain: key.domain,
security: key.security,
serializer: serializer,
valueType: String(describing: Key.Value.self),
owner: key.owner,
availability: key.availability,
syncPolicy: key.syncPolicy,
notes: notes
)
}
}

View File

@ -0,0 +1,3 @@
public protocol StorageKeyCatalog {
static var allKeys: [StorageKeyDescriptor] { get }
}

View File

@ -0,0 +1,110 @@
import Foundation
public struct StorageAuditReport: Sendable {
public static func items<C: StorageKeyCatalog>(for catalog: C.Type) -> [StorageKeyDescriptor] {
catalog.allKeys
}
public static func renderText<C: StorageKeyCatalog>(for catalog: C.Type) -> String {
renderText(items(for: catalog))
}
public static func renderText(_ items: [StorageKeyDescriptor]) -> String {
let lines = items.map { item in
var parts: [String] = []
parts.append("name=\(item.name)")
parts.append("domain=\(string(for: item.domain))")
parts.append("security=\(string(for: item.security))")
parts.append("serializer=\(item.serializer)")
parts.append("value=\(item.valueType)")
parts.append("owner=\(item.owner)")
parts.append("availability=\(string(for: item.availability))")
parts.append("sync=\(string(for: item.syncPolicy))")
if let notes = item.notes, !notes.isEmpty {
parts.append("notes=\(notes)")
}
return parts.joined(separator: " | ")
}
return lines.joined(separator: "\n")
}
private static func string(for domain: StorageDomain) -> String {
switch domain {
case .userDefaults(let suite):
return "userDefaults(\(suite ?? "standard"))"
case .keychain(let service):
return "keychain(\(service))"
case .fileSystem(let directory):
return "fileSystem(\(string(for: directory)))"
case .encryptedFileSystem(let directory):
return "encryptedFileSystem(\(string(for: directory)))"
}
}
private static func string(for directory: FileDirectory) -> String {
switch directory {
case .documents:
return "documents"
case .caches:
return "caches"
case .custom(let url):
return "custom(\(url.path))"
}
}
private static func string(for availability: PlatformAvailability) -> String {
switch availability {
case .all:
return "all"
case .phoneOnly:
return "phoneOnly"
case .watchOnly:
return "watchOnly"
case .phoneWithWatchSync:
return "phoneWithWatchSync"
}
}
private static func string(for syncPolicy: SyncPolicy) -> String {
switch syncPolicy {
case .never:
return "never"
case .manual:
return "manual"
case .automaticSmall:
return "automaticSmall"
}
}
private static func string(for security: SecurityPolicy) -> String {
switch security {
case .none:
return "none"
case .encrypted(let policy):
return "encrypted(\(string(for: policy)))"
case .keychain(let accessibility, let accessControl):
let accessControlValue = accessControl?.displayName ?? "none"
return "keychain(\(accessibility.displayName), \(accessControlValue))"
}
}
private static func string(for policy: SecurityPolicy.EncryptionPolicy) -> String {
switch policy {
case .aes256(let derivation):
return "aes256(\(string(for: derivation)))"
case .chacha20Poly1305(let derivation):
return "chacha20Poly1305(\(string(for: derivation)))"
case .external(let source, let derivation):
return "external(\(source.id), \(string(for: derivation)))"
}
}
private static func string(for derivation: SecurityPolicy.KeyDerivation) -> String {
switch derivation {
case .pbkdf2(let iterations, _):
return "pbkdf2(\(iterations))"
case .hkdf:
return "hkdf"
}
}
}