0
0
iOS Swiftmobile~20 mins

Keychain for secure storage in iOS Swift - Mini App: Build & Ship

Choose your learning style9 modes available
Build: Secure Storage
This screen lets the user save a secret text securely using the iOS Keychain and retrieve it later.
Target UI
-------------------------
| Secure Storage         |
|-----------------------|
| Secret: [__________]   |
|                       |
| [Save Secret] [Load]   |
|                       |
| Loaded Secret: _______ |
-------------------------
A TextField to enter a secret string.
A 'Save Secret' button that saves the entered text securely in the Keychain.
A 'Load' button that retrieves the saved secret from the Keychain and displays it below.
Use Apple's Keychain Services API for secure storage.
Handle errors gracefully and show alerts if saving or loading fails.
Starter Code
iOS Swift
import SwiftUI
import Security

struct SecureStorageView: View {
    @State private var secretText = ""
    @State private var loadedSecret = ""
    @State private var showingAlert = false
    @State private var alertMessage = ""

    var body: some View {
        VStack(spacing: 20) {
            TextField("Enter secret", text: $secretText)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()

            HStack {
                Button("Save Secret") {
                    // TODO: Save secretText to Keychain
                }
                Button("Load") {
                    // TODO: Load secret from Keychain
                }
            }
            .padding()

            Text("Loaded Secret: \(loadedSecret)")
                .padding()
        }
        .alert(isPresented: $showingAlert) {
            Alert(title: Text("Error"), message: Text(alertMessage), dismissButton: .default(Text("OK")))
        }
        .navigationTitle("Secure Storage")
    }
}

struct SecureStorageView_Previews: PreviewProvider {
    static var previews: some View {
        SecureStorageView()
    }
}
Task 1
Task 2
Task 3
Solution
iOS Swift
import SwiftUI
import Security

struct SecureStorageView: View {
    @State private var secretText = ""
    @State private var loadedSecret = ""
    @State private var showingAlert = false
    @State private var alertMessage = ""

    let service = "com.example.SecureStorage"
    let account = "userSecret"

    var body: some View {
        VStack(spacing: 20) {
            TextField("Enter secret", text: $secretText)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()

            HStack {
                Button("Save Secret") {
                    do {
                        try saveSecret(secretText)
                        loadedSecret = ""
                    } catch {
                        alertMessage = "Failed to save secret: \(error.localizedDescription)"
                        showingAlert = true
                    }
                }
                Button("Load") {
                    do {
                        loadedSecret = try loadSecret() ?? "(No secret found)"
                    } catch {
                        alertMessage = "Failed to load secret: \(error.localizedDescription)"
                        showingAlert = true
                    }
                }
            }
            .padding()

            Text("Loaded Secret: \(loadedSecret)")
                .padding()
        }
        .alert(isPresented: $showingAlert) {
            Alert(title: Text("Error"), message: Text(alertMessage), dismissButton: .default(Text("OK")))
        }
        .navigationTitle("Secure Storage")
    }

    func saveSecret(_ secret: String) throws {
        guard let data = secret.data(using: .utf8) else {
            throw NSError(domain: "InvalidString", code: -1, userInfo: nil)
        }

        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: account
        ]

        let attributes: [String: Any] = [
            kSecValueData as String: data
        ]

        let status = SecItemCopyMatching(query as CFDictionary, nil)
        if status == errSecSuccess {
            // Item exists, update it
            let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
            if updateStatus != errSecSuccess {
                throw NSError(domain: "KeychainError", code: Int(updateStatus), userInfo: nil)
            }
        } else if status == errSecItemNotFound {
            // Item does not exist, add it
            var newItem = query
            newItem[kSecValueData as String] = data
            let addStatus = SecItemAdd(newItem as CFDictionary, nil)
            if addStatus != errSecSuccess {
                throw NSError(domain: "KeychainError", code: Int(addStatus), userInfo: nil)
            }
        } else {
            throw NSError(domain: "KeychainError", code: Int(status), userInfo: nil)
        }
    }

    func loadSecret() throws -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: account,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]

        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)

        if status == errSecSuccess {
            guard let data = item as? Data,
                  let secret = String(data: data, encoding: .utf8) else {
                throw NSError(domain: "KeychainError", code: -2, userInfo: nil)
            }
            return secret
        } else if status == errSecItemNotFound {
            return nil
        } else {
            throw NSError(domain: "KeychainError", code: Int(status), userInfo: nil)
        }
    }
}

This solution uses SwiftUI for the user interface and Apple's Keychain Services API to securely save and load a secret string.

The saveSecret function first checks if the secret already exists in the Keychain. If it does, it updates the stored data. If not, it adds a new item. Errors during these operations throw exceptions that are caught and shown as alerts.

The loadSecret function tries to find the secret in the Keychain and returns it as a string. If not found, it returns nil.

The UI has a TextField for input, two buttons to save and load, and a Text view to display the loaded secret. Alerts inform the user if something goes wrong.

Final Result
Completed Screen
-------------------------
| Secure Storage         |
|-----------------------|
| Secret: [mySecret123]  |
|                       |
| [Save Secret] [Load]   |
|                       |
| Loaded Secret: mySecret123 |
-------------------------
User types a secret in the text field.
Tapping 'Save Secret' stores the secret securely in the Keychain.
Tapping 'Load' retrieves the secret from the Keychain and shows it below.
If saving or loading fails, an alert pops up with an error message.
Stretch Goal
Add biometric authentication (Face ID / Touch ID) before loading the secret.
💡 Hint
Use the LocalAuthentication framework to request biometric authentication before calling loadSecret().