I built a small Flutter Android app to explore using Android’s Java Cryptography Architecture (JCA) directly from Dart using package:jnigen.
The goal was to understand whether a JCA-based backend could be a viable alternative to the current BoringSSL-based implementation in package:webcrypto.
What this demonstratesDart (Uint8List) -> JNI bindings (jnigen) -> Java wrapper->Android JCA (MessageDigest) -> back to Dart (Uint8List)

Why the outputs match
SHA-256 is a standardized algorithm (FIPS 180-4).
As long as:
- input encoding is identical (UTF-8)
- no mutation happens across JNI
different implementations (JCA vs BoringSSL) will always produce the same result.
Benchmark (500 iterations)
- JCA via JNI: ~7 ms (~0.01 ms/op)
- webcrypto: ~2 ms (~0.00 ms/op)
Observation:
- JNI overhead is visible for small inputs
- For larger inputs, the gap becomes smaller
Key implementation
Java (JCA):
Dart (JNI wrapper):

What’s interesting
- JNI adds overhead, mainly from boundary crossing and data conversion
- JCA APIs are synchronous, which doesn’t align perfectly with Dart’s async model
- Wrapper/API design becomes important for usability and performance
Why this approach matters
Using platform crypto instead of bundling BoringSSL:
- Enables hardware-backed security (Keystore / StrongBox)
- Helps with compliance requirements (FIPS environments)
- Can reduce binary size
Questions
- Would a thin mapping over JCA APIs be preferable, or should the backend introduce batching to reduce JNI crossings?
- How should the sync nature of JCA APIs be exposed in Dart (sync wrapper vs async abstraction)?
- Are there known pitfalls with JNI overhead for crypto workloads at scale?