Skip to content

feat(impl_jni): add JCA PBKDF2 implementation - #357

Open
mfazrinizar wants to merge 2 commits into
google:android-jca-branchfrom
mfazrinizar:feat/impl-jni-pbkdf2
Open

mfazrinizar wants to merge 2 commits into
google:android-jca-branchfrom
mfazrinizar:feat/impl-jni-pbkdf2

Conversation

@mfazrinizar

@mfazrinizar mfazrinizar commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the JNI/JCA PBKDF2 implementation on top of the Android JCA backend.

This PR implements:

  • raw key import with a defensive copy of the password bytes
  • RFC 8018 PBKDF2 using JCA Mac as the pseudorandom function
  • SHA-1, SHA-256, SHA-384, and SHA-512
  • arbitrary raw password bytes without PBEKeySpec character conversion
  • empty password/salt handling and zero-length output
  • byte-aligned output-length and positive-iteration validation
  • worker-isolate execution so derivation does not block the caller isolate
  • focused RFC/FFI interoperability tests and an Android public-API smoke test

Generated JCA bindings are not changed in this PR. The required Mac and
SecretKeySpec bindings are already present on android-jca-branch.

Experimental performance limitation

The byte-oriented implementation preserves arbitrary raw password bytes, but the RFC 8018 iteration/XOR loop currently runs in Dart. Each iteration invokes JCA through JNI and copies the intermediate U value into Dart for XOR. Benchmarks confirm that this boundary cost is significant at realistic iteration counts.

This is accepted for the experimental backend, but batching remains required before it can be considered production-ready. The design is tracked separately because the native-hook package currently has no supported way to distribute a custom Java helper to consumer Android applications.

Reproducible benchmark commands:

dart run benchmark/impl_jni_pbkdf2_benchmark.dart
cd example/webcrypto_demo_flutter_app
flutter test integration_test/jni_pbkdf2_benchmark.dart -d emulator-name
cd ../..

The desktop harness verifies JNI/FFI output equivalence before reporting median times at 1,000, 10,000, and 100,000 iterations. Android results are timing evidence only and contain no performance assertion.

Testing

Desktop JNI setup, if it has not been run already:

dart run jni:setup

Verified with:

dart format --output=none --set-exit-if-changed .
dart analyze benchmark/impl_jni_pbkdf2_benchmark.dart \
  lib/src/impl_jni/impl_jni.pbkdf2.dart \
  test/impl_jni_pbkdf2_test.dart
dart test test/impl_jni_pbkdf2_test.dart
dart test test/webcrypto_test.dart -p vm -n PBKDF2
cd example/webcrypto_demo_flutter_app
flutter test integration_test/jni_pbkdf2_test.dart -d emulator-name
cd ../..
git diff --check

@mfazrinizar
mfazrinizar marked this pull request as ready for review August 8, 2026 19:07
@mfazrinizar
mfazrinizar force-pushed the feat/impl-jni-pbkdf2 branch from d011c5d to b14c41c Compare August 8, 2026 19:17
mac.doFinal$1(u, 0);
u.copyRangeToDart(block, 0, hashLength);

for (var iteration = 1; iteration < iterations; iteration++) {

@HamdaanAliQuatil HamdaanAliQuatil Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: pbkdf2 is deliberately iteration-heavy, but this implementation crosses dart/jni on every round. Each round calls Mac.update, calls Mac.doFinal, and copies U into dart so we can xor it. At 100,000 iterations, that is about 200,000 JNI calls and 100,000 copies for one output block.

U stays as a Java byte array between rounds, so we are not copying it back into Java. But this is still far too much boundary traffic for normal PBKDF2 usage. We need to eventually run the complete HMAC/XOR loop on the JVM and return only the final bytes. PBEKeySpec is not a drop-in alternative because this API accepts raw password bytes, not Java characters.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed with that. I’ll move the complete byte-oriented HMAC/XOR loop into a JCA helper so Dart crosses JNI only for the inputs and final derived bytes. PBEKeySpec is not suitable because our API accepts arbitrary raw password bytes.

Before implementing it, I want to confirm the packaging direction. Since #264 removed the Android plugin module in favor of native hooks, a custom Java helper currently has no package-level path into consumer Android apps; putting it only in the demo app would not be sufficient. Desktop JNI tests would also need the helper compiled into a JAR and added to the JVM classpath.

Would you be okay with a small helper-support PR covering Android packaging and desktop test setup, followed by updating #357 to call it? I would keep generated bindings separate as usual. Or do you prefer another packaging mechanism?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are right about the packaging constraint. i was too quick to prescribe a java helper without accounting for the fact that the current native-hook setup has no path to package java bytecode into consumer android apps.

please do not add a demo-only helper, a desktop-only jar path, or a new plugin/aar packaging mechanism in this pr. keep the byte-oriented implementation for the experimental backend so we preserve raw-password semantics, but add a benchmark at realistic iteration counts such as 1k, 10k, and 100k so we quantify the boundary cost.

batching pbkdf2 remains a required design problem before this backend could be treated as production-ready, but it needs a deliberate package-level packaging decision rather than an ad hoc helper here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed. I kept the byte-oriented implementation unchanged and added reproducible benchmarks at 1k, 10k, and 100k iterations. The desktop harness verifies JNI/FFI output equivalence before timing. A fresh run measured JNI at 5.52 ms, 20.65 ms, and 219.92 ms, compared with FFI at 0.77 ms, 2.45 ms, and 19.87 ms. I also updated the TODO to track batching as a package-level production-readiness issue. I’ll link the tracking issue after filing it.

@mfazrinizar
mfazrinizar force-pushed the feat/impl-jni-pbkdf2 branch from b14c41c to f3ad985 Compare August 9, 2026 05:35
@jonasfj

jonasfj commented Sep 8, 2026

Copy link
Copy Markdown
Member

I think this is an awesome exploration, but that perhaps we should decide that even on Android with JCA enabled, we still fallback to boringssl/ffi for PBKDF2.

We don't want package:webcrypto to actually implement cryptographic primitives or be part of driving iterations. I may be perfectly fine to do, but this is where we start having risks that we might be doing something wrong.

Perhaps, better that we accept that we need boringssl/ffi when PBKDF2 is used.
Instead of trying to do things JCA can't do, we should accept the limitations and focus on making the ffi backend able to tree-shake symbols it doesn't need.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants