SecureStorageSample/SecureStorageSample/Views/KeychainDemo.swift

184 lines
6.2 KiB
Swift

//
// KeychainDemo.swift
// SecureStorageSample
//
// Demonstrates Keychain storage with LocalData package.
//
import SwiftUI
import LocalData
@MainActor
struct KeychainDemo: View {
@State private var username = ""
@State private var password = ""
@State private var storedCredential = ""
@State private var statusMessage = ""
@State private var isLoading = false
@State private var selectedAccessibility: KeychainAccessibility = .afterFirstUnlock
@State private var selectedAccessControl: KeychainAccessControl? = nil
@FocusState private var focusedField: Bool
var body: some View {
Form {
Section {
Text("Keychain provides hardware-backed secure storage for sensitive data like passwords, tokens, and keys.")
.font(.caption)
.foregroundStyle(.secondary)
}
Section("Credentials") {
TextField("Username", text: $username)
.focused($focusedField)
SecureField("Password", text: $password)
.focused($focusedField)
}
Section("Accessibility") {
Picker("When Accessible", selection: $selectedAccessibility) {
ForEach(KeychainAccessibility.allCases, id: \.self) { option in
Text(option.displayName).tag(option)
}
}
.pickerStyle(.menu)
}
Section("Access Control (Optional)") {
Picker("Require Authentication", selection: $selectedAccessControl) {
Text("None").tag(nil as KeychainAccessControl?)
ForEach(KeychainAccessControl.allCases, id: \.self) { option in
Text(option.displayName).tag(option as KeychainAccessControl?)
}
}
.pickerStyle(.menu)
}
Section("Actions") {
Button(action: saveCredentials) {
HStack {
Image(systemName: "lock.fill")
Text("Save to Keychain")
}
}
.disabled(username.isEmpty || password.isEmpty || isLoading)
Button(action: loadCredentials) {
HStack {
Image(systemName: "key.fill")
Text("Retrieve from Keychain")
}
}
.disabled(isLoading)
Button(action: deleteCredentials) {
HStack {
Image(systemName: "trash")
Text("Delete from Keychain")
}
}
.foregroundStyle(.red)
.disabled(isLoading)
}
if !storedCredential.isEmpty {
Section("Retrieved Data") {
Text(storedCredential)
.font(.system(.body, design: .monospaced))
}
}
if !statusMessage.isEmpty {
Section {
Text(statusMessage)
.font(.caption)
.foregroundStyle(statusMessage.contains("Error") ? .red : .green)
}
}
Section("Example Key Configuration") {
LabeledContent("Domain", value: "Keychain")
LabeledContent("Security", value: "Keychain Policy")
LabeledContent("Serializer", value: "JSON")
LabeledContent("Platform", value: "Phone Only")
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") {
focusedField = false
}
}
}
}
private func saveCredentials() {
isLoading = true
Task {
do {
let key = StorageKey.credentialsKey(
accessibility: selectedAccessibility,
accessControl: selectedAccessControl
)
let credential = Credential(username: username, password: password)
try await StorageRouter.shared.set(credential, for: key)
statusMessage = "✓ Saved to Keychain securely"
} catch {
statusMessage = "Error: \(error.localizedDescription)"
}
isLoading = false
}
}
private func loadCredentials() {
isLoading = true
Task {
do {
let key = StorageKey.credentialsKey(
accessibility: selectedAccessibility,
accessControl: selectedAccessControl
)
let credential = try await StorageRouter.shared.get(key)
storedCredential = "Username: \(credential.username)\nPassword: ****"
// Sync to fields
username = credential.username
password = credential.password
statusMessage = "✓ Retrieved from Keychain"
} catch StorageError.notFound {
storedCredential = ""
statusMessage = "No credentials stored"
} catch {
statusMessage = "Error: \(error.localizedDescription)"
}
isLoading = false
}
}
private func deleteCredentials() {
isLoading = true
Task {
do {
let key = StorageKey.credentialsKey(
accessibility: selectedAccessibility,
accessControl: selectedAccessControl
)
try await StorageRouter.shared.remove(key)
storedCredential = ""
statusMessage = "✓ Deleted from Keychain"
} catch {
statusMessage = "Error: \(error.localizedDescription)"
}
isLoading = false
}
}
}
#Preview {
NavigationStack {
KeychainDemo()
}
}