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
|
## Testing
|
||||||
- Unit tests use Swift Testing (`Testing` package)
|
- 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
|
## Sample App
|
||||||
See `SecureStorgageSample` for working examples of all storage domains and security options.
|
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