179 lines
6.1 KiB
Swift
179 lines
6.1 KiB
Swift
//
|
|
// KeychainDemo.swift
|
|
// SecureStorgageSample
|
|
//
|
|
// Demonstrates Keychain storage with LocalData package.
|
|
//
|
|
|
|
import SwiftUI
|
|
import LocalData
|
|
|
|
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")
|
|
}
|
|
}
|
|
.navigationTitle("Keychain")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItemGroup(placement: .keyboard) {
|
|
Spacer()
|
|
Button("Done") {
|
|
focusedField = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func saveCredentials() {
|
|
isLoading = true
|
|
Task {
|
|
do {
|
|
let key = StorageKeys.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 = StorageKeys.CredentialsKey(
|
|
accessibility: selectedAccessibility,
|
|
accessControl: selectedAccessControl
|
|
)
|
|
let credential = try await StorageRouter.shared.get(key)
|
|
storedCredential = "Username: \(credential.username)\nPassword: ****"
|
|
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 = StorageKeys.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()
|
|
}
|
|
}
|