mobiler 0.11.0

Build mobile apps in Rust — one core, native UI on Android, iOS, and the web (CLI)
import Foundation
import Security
import SharedTypes

// Secure key/value storage (free, bundled). Backed by the iOS Keychain (encrypted, app-scoped).
// For secrets — auth tokens, API keys — NOT bulk data. `input` is JSON:
//   set:    {"key": "...", "value": "..."}  → ok:true
//   get:    {"key": "..."}                  → ok:true, output = value ("" if absent)
//   delete: {"key": "..."}                  → ok:true
// Pair with the `biometric` plugin (gate a get behind an authenticate in your Rust core).
enum SecureStorePlugin {
    static func handle(op: String, input: String) async -> PluginResponse {
        guard
            let data = input.data(using: .utf8),
            let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
            let key = obj["key"] as? String, !key.isEmpty
        else { return PluginResponse(ok: false, output: "missing key") }

        switch op {
        case "set":
            let value = (obj["value"] as? String) ?? ""
            return set(key: key, value: value)
        case "get":
            return get(key: key)
        case "delete":
            delete(key: key)
            return PluginResponse(ok: true, output: "")
        default:
            return PluginResponse(ok: false, output: "unknown op '\(op)'")
        }
    }

    private static func query(_ key: String) -> [String: Any] {
        [kSecClass as String: kSecClassGenericPassword,
         kSecAttrService as String: "mobiler.securestore",
         kSecAttrAccount as String: key]
    }

    private static func set(key: String, value: String) -> PluginResponse {
        SecItemDelete(query(key) as CFDictionary) // overwrite semantics
        var attrs = query(key)
        attrs[kSecValueData as String] = Data(value.utf8)
        attrs[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
        let status = SecItemAdd(attrs as CFDictionary, nil)
        return status == errSecSuccess
            ? PluginResponse(ok: true, output: "")
            : PluginResponse(ok: false, output: "keychain set failed (\(status))")
    }

    private static func get(key: String) -> PluginResponse {
        var q = query(key)
        q[kSecReturnData as String] = true
        q[kSecMatchLimit as String] = kSecMatchLimitOne
        var item: CFTypeRef?
        let status = SecItemCopyMatching(q as CFDictionary, &item)
        if status == errSecItemNotFound { return PluginResponse(ok: true, output: "") }
        guard status == errSecSuccess, let data = item as? Data, let value = String(data: data, encoding: .utf8)
        else { return PluginResponse(ok: false, output: "keychain get failed (\(status))") }
        return PluginResponse(ok: true, output: value)
    }

    private static func delete(key: String) {
        SecItemDelete(query(key) as CFDictionary)
    }
}