# Tauri Plugin Secure Element
A Tauri plugin for secure element functionality on macOS & iOS (Secure Enclave) and Android (StrongBox and TEE).
## Features
- Generate secure keys using hardware-backed secure storage
- Sign data with keys stored in secure elements
- List and manage secure keys
- Check secure element support on the device
- Support for biometric and PIN authentication modes
- Cross-platform support for macOS, Windows, iOS, and Android
## Installation
### npm
```bash
npm install tauri-plugin-secure-element-api
# or
pnpm add tauri-plugin-secure-element-api
# or
yarn add tauri-plugin-secure-element-api
```
### Cargo
```toml
[dependencies]
tauri-plugin-secure-element = "4"
```
## Setup
Add the plugin to your Rust code in `src-tauri/src/lib.rs`:
```rust
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_secure_element::init())
// ... other plugins
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
```
Add the plugin permissions to `src-tauri/capabilities/default.json`:
```json
{
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": ["core:default", "secure-element:default"]
}
```
### Android Biometrics
In order to use biometric protected keys, add this to `src-tauri/gen/android/app/build.gradle.kts`:
```kotlin
dependencies {
implementation("androidx.biometric:biometric:1.1.0")
}
```
**Note**: The `src-tauri/gen/android` folder is generated by Tauri but should be **committed to version control** and customized as needed. Once you add the biometric dependency, it will persist across builds (you only need to add it again if you completely regenerate the Android project with `tauri android init`).
### iOS Face ID Permission
**Important**: For authentication-required keys to work on iOS with Face ID, you must add the Face ID usage description to your iOS Info.plist.
Add to `src-tauri/gen/apple/tauri-app_iOS/Info.plist` (replace `tauri-app_iOS` with your app name):
```xml
<key>NSFaceIDUsageDescription</key>
<string>This app uses Face ID to authenticate access to your secure keys.</string>
```
Add this entry anywhere within the `<dict>` section of the Info.plist file.
**Note**: Like the Android configuration, the `src-tauri/gen/apple` folder should be **committed to version control**. The Face ID permission will persist across builds unless you regenerate the iOS project with `tauri ios init`.
Touch ID does not require a separate permission entry - it works automatically when Face ID permission is granted or when no biometric hardware is available.
## Usage
```typescript
import {
checkSecureElementSupport,
generateSecureKey,
listKeys,
signWithKey,
deleteKey,
type AuthenticationMode,
type SecureElementBacking,
type SecureElementCapabilities,
} from "tauri-plugin-secure-element-api";
// Check device secure element capabilities
const capabilities = await checkSecureElementSupport();
console.log("Strongest backing:", capabilities.strongest);
console.log(
"Can enforce biometric-only:",
capabilities.canEnforceBiometricOnly
);
// Generate a new secure key
const { publicKey, keyName } = await generateSecureKey(
"my-key-name",
"pinOrBiometric" // or 'none' or 'biometricOnly'
);
// List all keys
const keys = await listKeys();
// Sign data with a key
const data = new Uint8Array([1, 2, 3, 4]);
const signature = await signWithKey("my-key-name", data);
// Delete a key
await deleteKey("my-key-name");
```
## API Reference
### `checkSecureElementSupport()`
Returns detailed information about secure element hardware capabilities on the device.
**Returns:** `Promise<SecureElementCapabilities>`
```typescript
/**
* Hardware backing tiers, ordered weakest → strongest:
* "none" < "firmware" < "integrated" < "discrete"
*/
type SecureElementBacking = "none" | "firmware" | "integrated" | "discrete";
interface SecureElementCapabilities {
/** A discrete physical security chip is available (e.g. discrete TPM 2.0, macOS T2, Android StrongBox) */
discrete: boolean;
/** An on-die isolated security core is available (e.g. Apple Silicon Secure Enclave, ARM TrustZone/TEE) */
integrated: boolean;
/** Firmware-backed security is available but no dedicated secure processor (e.g. Windows fTPM via Intel PTT or AMD PSP) */
firmware: boolean;
/** The security is emulated/virtual (e.g. vTPM in a VM, iOS Simulator, Android Emulator) */
emulated: boolean;
/** The strongest hardware backing tier available on this device */
strongest: SecureElementBacking;
/** Whether biometric-only authentication can be enforced at the key level (Android API 30+ only) */
canEnforceBiometricOnly: boolean;
}
```
**Hardware Backing Tiers:**
| Tier | Description | Examples |
| ------------ | ---------------------------------------------- | -------------------------------------------------- |
| `none` | No secure element available (software-only) | Unsupported devices, some VMs |
| `firmware` | Firmware-backed, no dedicated secure processor | Windows fTPM (Intel PTT, AMD PSP) |
| `integrated` | On-die isolated security core | Apple Silicon Secure Enclave, ARM TrustZone/TEE |
| `discrete` | Physically separate security processor | Discrete TPM 2.0, macOS T2 chip, Android StrongBox |
**Usage Example:**
```typescript
const caps = await checkSecureElementSupport();
// Check if any hardware backing is available
if (caps.strongest === "none") {
console.warn("No secure element available - keys will be software-only");
}
// Check for high-security backing (discrete or integrated)
if (caps.strongest === "discrete" || caps.strongest === "integrated") {
console.log("High-security hardware backing available");
}
// Warn if running in emulated environment
if (caps.emulated) {
console.warn("Running in emulator/VM - security may be reduced");
}
// Check before creating biometric-only keys
if (caps.canEnforceBiometricOnly) {
await generateSecureKey("my-key", "biometricOnly");
}
```
### `generateSecureKey(keyName: string, authMode?: AuthenticationMode)`
Generates a new secure key in the device's secure element.
**Parameters:**
- `keyName`: Unique name for the key
- `authMode`: Authentication mode (`'none'`, `'pinOrBiometric'`, or `'biometricOnly'`)
**Returns:** `Promise<GenerateSecureKeyResult>`
```typescript
interface GenerateSecureKeyResult {
publicKey: string;
keyName: string;
}
```
**Note:** The `biometricOnly` mode requires Android 11 (API 30) or higher. On older Android versions, this mode will be rejected with an error. Use `checkSecureElementSupport().canEnforceBiometricOnly` to check support before creating biometric-only keys.
### `listKeys(keyName?: string, publicKey?: string)`
Lists keys stored in the secure element. Can filter by key name or public key.
**Returns:** `Promise<KeyInfo[]>`
```typescript
interface KeyInfo {
keyName: string;
publicKey: string;
}
```
### `signWithKey(keyName: string, data: Uint8Array)`
Signs data using a key stored in the secure element.
**Parameters:**
- `keyName`: Name of the key to use
- `data`: Raw data to sign as `Uint8Array` (do not pre-hash)
**Returns:** `Promise<Uint8Array>` - DER-encoded ECDSA signature
**Important:** The plugin automatically hashes your data with SHA-256 before signing. Pass raw data, not a pre-computed hash. See [Signature Format](#signature-format) for details.
### `deleteKey(keyName?: string, publicKey?: string)`
Deletes a key from the secure element. At least one parameter must be provided.
**Returns:** `Promise<boolean>` - Success status
## Public Key Format
Public keys are returned as base64-encoded strings in **X9.62 uncompressed point format** (65 bytes), consistent across all platforms:
| 0 | `0x04` (uncompressed) |
| 1-32 | X coordinate (32 bytes) |
| 33-64 | Y coordinate (32 bytes) |
All keys use the **secp256r1 (P-256)** elliptic curve.
## Signature Format
### Hashing Convention
**The plugin automatically hashes your data with SHA-256 before signing.** This is handled internally on all platforms:
- **iOS/macOS**: Hashes data, then signs with `.ecdsaSignatureDigestX962SHA256`
- **Android**: Uses `SHA256withECDSA` which hashes internally
- **Windows**: Explicitly hashes with SHA-256 before calling `NCryptSignHash`
**Do not pre-hash your data.** Pass the raw data to `signWithKey()` and the plugin handles hashing.
### Signature Output
Signatures are returned as **DER-encoded ECDSA signatures** (ASN.1 format), consistent across all platforms. Typical size is 70-72 bytes for P-256.
```
SEQUENCE {
INTEGER r, -- 32-33 bytes (may have leading 0x00 for sign bit)
INTEGER s -- 32-33 bytes (may have leading 0x00 for sign bit)
}
```
### Verifying Signatures
To verify a signature produced by this plugin:
1. Use the **raw original data** (not hashed)
2. Use an **ECDSA-SHA256** verification algorithm (the verifier will hash internally)
3. Use the **secp256r1 (P-256)** curve
4. The signature is **DER-encoded**
**Example (Node.js):**
```typescript
import { createVerify } from "crypto";
function verifySignature(
publicKeyBase64: string,
data: Uint8Array,
signatureDer: Uint8Array
): boolean {
// Convert X9.62 public key to PEM format
const publicKeyBuffer = Buffer.from(publicKeyBase64, "base64");
const publicKey = {
key: publicKeyBuffer,
format: "raw",
type: "spki",
namedCurve: "P-256",
};
const verify = createVerify("SHA256");
verify.update(data); // Pass raw data - verify() hashes internally
return verify.verify(
{ key: publicKey, dsaEncoding: "der" },
Buffer.from(signatureDer)
);
}
```
**Example (Web Crypto API):**
```typescript
async function verifySignature(
publicKeyBase64: string,
data: Uint8Array,
signatureDer: Uint8Array
): Promise<boolean> {
const publicKeyBytes = Uint8Array.from(atob(publicKeyBase64), (c) =>
c.charCodeAt(0)
);
// Import the raw public key
const publicKey = await crypto.subtle.importKey(
"raw",
publicKeyBytes,
{ name: "ECDSA", namedCurve: "P-256" },
false,
["verify"]
);
const signatureRaw = derToRaw(signatureDer);
return crypto.subtle.verify(
{ name: "ECDSA", hash: "SHA-256" },
publicKey,
signatureRaw,
data // Pass raw data - verify() hashes internally
);
}
```
## Platform Support
- **iOS**: Uses Secure Enclave for key generation and signing
- **Android**: Uses StrongBox and TEE (Trusted Execution Environment) when available
- **Windows**: Uses TPM 2.0 for key generation and signing
- **macOS**: Uses Secure Enclave for key generation and signing
## Platform Limitations
### Windows
- Windows 11 (build 22000 or higher) requires TPM 2.0
- TPM 2.0 is supported on Windows 10 (since version 1507)
### macOS
- Secure Enclave is available on Macs with Apple Silicon (M1/M2/M3/M4) or T2 chip
### Android
| Hardware-backed keys | API 23+ | TEE or StrongBox required |
| StrongBox | API 28+ | Falls back to TEE if unavailable |
| `biometricOnly` auth mode | API 30+ | Rejected on older versions |
### iOS
- Secure Enclave is available on all devices with A7 chip or later (iPhone 5s+)
- Simulator does not support Secure Enclave - test on physical devices
### Authentication Modes
| `none` | ✅ No auth required | ✅ No auth required | ✅ No auth required |
| `pinOrBiometric` | ✅ Face ID, Touch ID, or passcode | ✅ Biometric or PIN/pattern/password | ✅ Windows Hello |
| `biometricOnly` | ❌ Not supported | ✅ API 30+ only, biometric only | ❌ Not supported |
## License
Apache-2.0
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## Links
- [Repository](https://github.com/dkackman/tauri-plugin-secure-element)
- [Issues](https://github.com/dkackman/tauri-plugin-secure-element/issues)