Couldn't decrypt my Backups made on Android, found out lazysodium is encrypting with the nonce being all zero.
Summary
SecretStream.State extends JNA Structure. JNA's auto-sync mechanism writes zero-valued Java fields over native memory before the first push/pull call, silently corrupting the secretstream state initialized by init_push/init_pull. Ciphertext produced this way cannot be decrypted by any standard libsodium implementation.
Reproduction
val state = SecretStream.State()
val header = ByteArray(SecretStream.HEADERBYTES)
sodium.cryptoSecretStreamInitPush(state, header, key)
// State is correct in native memory at this point.
// But state's Java fields (k, nonce, _pad) are still zeros.
val cipher = ByteArray(plaintext.size + SecretStream.ABYTES)
sodium.cryptoSecretStreamPush(state, cipher, plaintext, plaintext.size.toLong(), SecretStream.TAG_MESSAGE)
// JNA auto-writes zero Java fields -> native memory BEFORE this call.
// The init_push state is destroyed. Encryption uses zeroed state.
// After the call, JNA auto-reads native -> Java, so subsequent calls work.
The first push encrypts with a zeroed state. Subsequent pushes work correctly because JNA auto-read syncs the (post-zero, evolved) native state back to Java fields after the first call.
Impact
- The first secretstream chunk in every stream is encrypted with corrupted state
- Standard
init_pull + pull cannot decrypt it
- The AEAD authentication tag is computed against the wrong state
- Data appears irrecoverably corrupted to the user
- No error is raised - encryption "succeeds" silently
- This affects anyone using the streaming API for its intended purpose (multiple push/pull calls)
Verified behavior
We confirmed this empirically on production data:
init_push -> first push uses zeroed state (JNA clobber)
- After first
push, JNA auto-reads native -> Java, subsequent calls evolve normally
- Data is recoverable by simulating the zeroed state on the pull side
Suggested fix
Option A (minimal - in documentation): Document that users must call state.setAutoWrite(false) and state.setAutoRead(false) after creating a State instance.
Option B (recommended - in library): Disable auto-sync in the State constructor:
class State extends Structure {
// ... fields ...
public State() {
super();
setAutoWrite(false);
setAutoRead(false);
}
}
Since the state is an opaque blob from the caller's perspective (the Java fields have no useful meaning to users), auto-sync provides no benefit and actively causes corruption.
Option C (safest): Replace State extends Structure with a wrapper around a raw Memory(statebytes) pointer, avoiding JNA Structure auto-sync entirely.
Environment
- lazysodium-java (also affects lazysodium-android - same codebase)
- JNA 5.x
- Any libsodium version
Couldn't decrypt my Backups made on Android, found out lazysodium is encrypting with the nonce being all zero.
Summary
SecretStream.Stateextends JNAStructure. JNA's auto-sync mechanism writes zero-valued Java fields over native memory before the firstpush/pullcall, silently corrupting the secretstream state initialized byinit_push/init_pull. Ciphertext produced this way cannot be decrypted by any standard libsodium implementation.Reproduction
The first
pushencrypts with a zeroed state. Subsequent pushes work correctly because JNA auto-read syncs the (post-zero, evolved) native state back to Java fields after the first call.Impact
init_pull+pullcannot decrypt itVerified behavior
We confirmed this empirically on production data:
init_push-> firstpushuses zeroed state (JNA clobber)push, JNA auto-reads native -> Java, subsequent calls evolve normallySuggested fix
Option A (minimal - in documentation): Document that users must call
state.setAutoWrite(false)andstate.setAutoRead(false)after creating aStateinstance.Option B (recommended - in library): Disable auto-sync in the
Stateconstructor:Since the state is an opaque blob from the caller's perspective (the Java fields have no useful meaning to users), auto-sync provides no benefit and actively causes corruption.
Option C (safest): Replace
State extends Structurewith a wrapper around a rawMemory(statebytes)pointer, avoiding JNA Structure auto-sync entirely.Environment