tauri-plugin-crypto-hw 0.1.0

Android/iOS StrongBox/Secure Enclave Bindings+Wrapper for Tauri
Documentation
package com.plugin.crypto

import android.app.Activity
import android.content.pm.PackageManager
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import app.tauri.BuildConfig

import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin
import app.tauri.plugin.Invoke

import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.Signature
import java.security.spec.ECGenParameterSpec


//@InvokeArg
//class PingArgs {
//    var value: String? = null
//}

@InvokeArg class IncludesIdentifier {
    var identifier: String? = null
}

@InvokeArg class SignRequest {
    var identifier: String? = null
    var payload: String? = null
}

@InvokeArg class VerifySignatureRequest {
    var identifier: String? = null
    var payload: String? = null
    var signature: String? = null
}

@TauriPlugin
class CryptoPlugin(private val activity: Activity): Plugin(activity) {
    private fun alias(id: String) = "${BuildConfig.LIBRARY_PACKAGE_NAME}.$id"

    /** Generate EC keypair in StrongBox; fail if unavailable or exists */
    private fun generateKeyPair(alias: String) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
            !activity.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
            throw Exception("StrongBox is not available on this device.")
        }
        val ks = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
        if (ks.containsAlias(alias)) throw Exception("Key already exists")
        val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
        val spec = KeyGenParameterSpec.Builder(
            alias,
            KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
        )
            .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
            .setDigests(KeyProperties.DIGEST_SHA256)
            .setIsStrongBoxBacked(true)              // enforce StrongBox :contentReference[oaicite:5]{index=5}
            .build()
        try {
            kpg.initialize(spec)
            kpg.generateKeyPair()
        } catch (e: Exception) {
            throw Exception("StrongBox key generation failed: ${e.message}")
        }
    }

    /** Retrieve KeyPair from AndroidKeyStore */
    private fun getKeyPair(alias: String): KeyPair {
        val ks = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
        val priv = ks.getKey(alias, null) ?: throw Exception("Private key missing")
        val pub = ks.getCertificate(alias)?.publicKey ?: throw Exception("Public key missing")
        return KeyPair(pub, priv as java.security.PrivateKey)
    }

    @Command
    fun generate(invoke: Invoke) {
        val id = invoke.parseArgs(IncludesIdentifier::class.java).identifier
            ?: return invoke.reject("Missing identifier")
        try {
            generateKeyPair(alias(id))
            invoke.resolve(JSObject().apply { put("message","Key generated successfully") })
        } catch (e: Exception) {
            if (e.message?.contains("exists") == true) {
                invoke.resolve(JSObject().apply { put("message","Key already exists") })
            } else {
                invoke.reject(e.message ?: "Generation failed")
            }
        }
    }

    @Command
    fun exists(invoke: Invoke) {
        val id = invoke.parseArgs(IncludesIdentifier::class.java).identifier
            ?: return invoke.reject("Missing identifier")
        val ks = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
        invoke.resolve(JSObject().apply { put("exists", ks.containsAlias(alias(id))) })
    }

    @Command
    fun getPublicKey(invoke: Invoke) {
        val id = invoke.parseArgs(IncludesIdentifier::class.java).identifier
            ?: return invoke.reject("Missing identifier")
        try {
            val pub = getKeyPair(alias(id)).public.encoded
            val hex = HexUtils.toMultibaseHex(pub)     // 0x… hex output
            invoke.resolve(JSObject().apply { put("publicKey", hex) })
        } catch (e: Exception) {
            invoke.reject("Couldn't retrieve public key")
        }
    }

    @Command
    fun signPayload(invoke: Invoke) {
        val req = invoke.parseArgs(SignRequest::class.java)
        val id = req.identifier ?: return invoke.reject("Missing identifier")
        val payload = req.payload ?: return invoke.reject("Missing payload")
        try {
            val sig = Signature.getInstance("SHA256withECDSA").apply {
                initSign(getKeyPair(alias(id)).private)
                update(payload.toByteArray())
            }.sign()
            val base58Sig = Base58BTC.encode(sig)
            invoke.resolve(JSObject().apply { put("signature", base58Sig) })
        } catch (e: Exception) {
            invoke.reject("Couldn't create signature")
        }
    }

    @Command
    fun verifySignature(invoke: Invoke) {
        val req = invoke.parseArgs(VerifySignatureRequest::class.java)
        val id = req.identifier ?: return invoke.reject("Missing identifier")
        val payload = req.payload ?: return invoke.reject("Missing payload")
        val sig = req.signature ?: return invoke.reject("Missing signature")
        try {
            val sigBytes = Base58BTC.decode(sig) ?: throw Exception("Invalid signature format")
            val verified = Signature.getInstance("SHA256withECDSA").apply {
                initVerify(getKeyPair(alias(id)).public)
                update(payload.toByteArray())
            }.verify(sigBytes)
            invoke.resolve(JSObject().apply { put("valid", verified) })
        } catch (e: Exception) {
            invoke.reject("Couldn't verify signature")
        }
    }
}