Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
7a459daa14
commit
0f208488a5
26
README.md
26
README.md
@ -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.
|
||||
|
||||
53
Sources/LocalData/Models/StorageKeyDescriptor.swift
Normal file
53
Sources/LocalData/Models/StorageKeyDescriptor.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
3
Sources/LocalData/Protocols/StorageKeyCatalog.swift
Normal file
3
Sources/LocalData/Protocols/StorageKeyCatalog.swift
Normal file
@ -0,0 +1,3 @@
|
||||
public protocol StorageKeyCatalog {
|
||||
static var allKeys: [StorageKeyDescriptor] { get }
|
||||
}
|
||||
110
Sources/LocalData/Services/StorageAuditReport.swift
Normal file
110
Sources/LocalData/Services/StorageAuditReport.swift
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user