tauri-plugin-keystore 2.1.0-alpha.2

Interact with the device-native key storage (Android Keystore, iOS Keychain).
import SwiftRs
import Tauri
import UIKit
import WebKit

import LocalAuthentication

class Prompt: Decodable {
  var title: String?
  var subtitle: String?
  var cancelLabel: String?
  var reason: String?
}

class StoreRequest: Decodable {
  let key: String
  let value: String
  var prompt: Prompt?
}

class RetrieveRequest: Decodable {
  let key: String
  var prompt: Prompt?
}

class RemoveRequest: Decodable {
  let key: String
}

let DEFAULT_REASON = "Authenticate to access your secret"

class KeystorePlugin: Plugin {
  @objc public func store(_ invoke: Invoke) throws {
    let args = try invoke.parseArgs(StoreRequest.self)

    guard let secretData = args.value.data(using: .utf8) else {
      throw NSError(
        domain: "StoreErrorDomain", code: -1,
        userInfo: [NSLocalizedDescriptionKey: "Invalid secret string"])
    }

    // Require user presence (biometrics or device passcode) and keep the item
    // on this device only, readable while it is unlocked.
    var error: Unmanaged<CFError>?
    guard
      let accessControl = SecAccessControlCreateWithFlags(
        nil,
        kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
        .userPresence,
        &error
      )
    else {
      throw error!.takeRetainedValue() as Error
    }

    // The caller's key is the keychain account, so each key addresses its own item.
    let query: [String: Any] = [
      kSecClass as String: kSecClassGenericPassword,
      kSecAttrAccount as String: args.key,
      kSecAttrAccessControl as String: accessControl,
      kSecValueData as String: secretData,
    ]

    // Replace any item already stored under this key.
    SecItemDelete([
      kSecClass as String: kSecClassGenericPassword,
      kSecAttrAccount as String: args.key,
    ] as CFDictionary)

    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else {
      throw NSError(domain: NSOSStatusErrorDomain, code: Int(status), userInfo: nil)
    }

    invoke.resolve()
  }

  @objc public func retrieve(_ invoke: Invoke) throws {
    let args = try invoke.parseArgs(RetrieveRequest.self)

    let context = LAContext()
    context.localizedReason = args.prompt?.reason ?? DEFAULT_REASON
    if let cancelLabel = args.prompt?.cancelLabel {
      context.localizedCancelTitle = cancelLabel
    }

    let query: [String: Any] = [
      kSecClass as String: kSecClassGenericPassword,
      kSecAttrAccount as String: args.key,
      kSecReturnData as String: true,
      kSecUseAuthenticationContext as String: context,
    ]

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

    // Nothing stored under this key is not an error.
    if status == errSecItemNotFound {
      invoke.resolve(["value": nil])
      return
    }

    guard status == errSecSuccess else {
      throw NSError(domain: NSOSStatusErrorDomain, code: Int(status), userInfo: nil)
    }

    guard let data = item as? Data,
      let secret = String(data: data, encoding: .utf8)
    else {
      throw NSError(
        domain: "com.impierce.identity-wallet", code: -1,
        userInfo: [NSLocalizedDescriptionKey: "Unable to decode secret"])
    }

    invoke.resolve(["value": secret])
  }

  @objc public func remove(_ invoke: Invoke) throws {
    let args = try invoke.parseArgs(RemoveRequest.self)

    let query: [String: Any] = [
      kSecClass as String: kSecClassGenericPassword,
      kSecAttrAccount as String: args.key,
    ]

    let status = SecItemDelete(query as CFDictionary)

    guard status == errSecSuccess || status == errSecItemNotFound else {
      throw NSError(domain: NSOSStatusErrorDomain, code: Int(status), userInfo: nil)
    }

    invoke.resolve()
  }
}

@_cdecl("init_plugin_keystore")
func initPlugin() -> Plugin {
  return KeystorePlugin()
}