Dev.to Security ๐Ÿ” Cybersecurity ๐Ÿ‘ 0 ๐Ÿ“– 4 min read

I Built a Flutter Plugin That Generates Crypto Keys Inside Your Phone's Hardware Chip

Flutter has no native way to generate a cryptographic key inside the device's hardware security chip and sign something with it. You can store keys in the Secure Enclave on iOS or StrongBox on Android, but you have to wr

Flutter has no native way to generate a cryptographic key inside the device's hardware security chip and sign something with it. You can store keys in the Secure Enclave on iOS or StrongBox on Android, but you have to write Swift and Kotlin yourself, bridge it through method channels, handle the three different signature formats each platform returns, and then figure out how to make the result usable for Ethereum's account abstraction standards. I spent 8 weeks doing exactly that, and the result is chainkey.

The actual problem

I was building a Web3 authentication flow in Flutter. The requirement: seedless wallets. No mnemonic phrase, no "write down these 12 words." The private key lives in the Secure Enclave (iOS) or StrongBox (Android) and never leaves the chip. Every signature requires biometric presence: Face ID, Touch ID, or fingerprint.

No Flutter package did this. The closest options handled key storage in software (Dart-side, exportable) or wrapped a single platform. None handled all three targets (iOS, Android, Web) behind one API, and none produced signatures in a format Ethereum smart contracts could verify.

What chainkey does

Five coordinated packages. One umbrella (chainkey), one interface (chainkey_platform_interface), three platform implementations.

const chainkey = Chainkey();

// Generate a P-256 key inside the hardware enclave
await chainkey.generateHardwareKey(
  keyAlias: 'my_wallet',
  requireUserPresence: true, // biometric on every sign
);

// Sign a 32-byte hash (triggers Face ID / fingerprint)
final signature = await chainkey.signWithHardwareKey(
  keyAlias: 'my_wallet',
  data: transactionHash,
);

// signature.r and signature.s are already low-S normalized
// Ready for EIP-7212 precompile or ERC-4337 UserOperation

The key never leaves the chip. requireUserPresence: true means the OS prompts for biometric auth on every sign call. You can customize the biometric prompt text on both platforms (iOS LAContext.localizedReason, Android BiometricPrompt.PromptInfo with title, subtitle, negative button).

The hard part: signature format hell

This is the thing that ate most of the 8 weeks. Each platform returns signatures in a different format:

Apple's Security framework gives you raw SEC1 point coordinates. Android's KeyStore gives you ASN.1 DER-encoded bytes. Ethereum expects 64 bytes: r concatenated with s, low-S normalized per BIP-62/EIP-2.

I built four converters to bridge these:

  • ASN.1 DER unpacker (parses the TLV structure Android returns)
  • SEC1 point formatter (handles compressed and uncompressed public keys)
  • SPKI parser (extracts the raw key from the SubjectPublicKeyInfo wrapper)
  • Low-S normalizer (if s > n/2, replace with n - s)

All four are validated against Wycheproof test vectors and the EIP-7212 reference. The Wycheproof vectors specifically caught an edge case in the DER parser where a leading zero byte in r was being included in the output, producing a 33-byte value instead of 32. That would silently fail on-chain with no error message.

WebAuthn passkeys (v0.4.0)

v0.4.0 added a full WebAuthn passkey engine across all three platforms: iOS AuthenticationServices, Android CredentialManager, and Web navigator.credentials via dart:js_interop. Registration and assertion, with a binary AuthenticatorData parser and a zero-copy UTF-8 ClientDataJSON scanner that extracts the challenge field at the byte offsets Solady and Safe contracts expect.

I also wrote a CLI tool (dart run chainkey:verify_domain) that validates your apple-app-site-association and assetlinks.json files for WebAuthn RP binding. Getting those wrong is silent on iOS (the passkey creation just fails with a generic error) and debugging it from the Dart side is painful.

The signature serializers (v0.5.0)

The latest release added three contract-specific encoders:

  • encodeSafePasskeySignature() for Safe 4337 WebAuthn Signer
  • encodeErc7579WebAuthnSignature() for ZeroDev Kernel v3 and Biconomy Nexus
  • encodeEip7212Payload() for the secp256r1 precompile (160-byte payload)

These exist because every smart account implementation expects a slightly different ABI encoding for the same underlying (r, s, authenticatorData, clientDataJSON) tuple. Getting the encoding wrong produces a signature that looks valid locally but reverts on-chain.

Performance

Signing latency matters because it's user-facing (the biometric prompt appears and the user waits). The integration test suite measures pure crypto latency with biometric disabled:

  • P50: 42ms
  • KPI target: under 350ms

42ms for a hardware enclave round-trip is fast enough that the user never perceives the signing step. The biometric prompt itself takes longer.

What I haven't solved

The package is pre-1.0. The roadmap has 14 open issues across two epics. Key rotation (what happens when the user gets a new phone and the old Secure Enclave key is gone) is the biggest unsolved problem. The current answer is "your smart contract wallet has a recovery mechanism, and that's outside the scope of this package." I'm not happy with that answer but it's honest.

I also chose P-256 over secp256k1. This was deliberate: P-256 aligns with WebAuthn and the EIP-7212 precompile, which is gaining L2 adoption. But it means chainkey does not work with legacy EOA wallets. Hardware enclaves on iOS and Android don't support secp256k1 at all, so this wasn't really a choice.

160/160 on pub.dev. 77 commits. 12,747 lines across Dart, Kotlin, and Swift. Apache-2.0.

pub.dev/packages/chainkey. Source at github.com/GhagSagar23/chainkey.

๐Ÿ“ฐ Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ€” full credit and traffic to the original publisher.