Skip to main content
AllDevToolsHub
2026-08-20
Last reviewed: Aug 2026
SECURITY
Est Read: 06_MIN

AES Interoperability Across 7 Libraries: We Encrypted the Same Plaintext Everywhere and Compared Results

AES Interoperability Across 7 Libraries: We Encrypted the Same Plaintext Everywhere and Compared Results
Processing_Node: 01

#1AES interoperability across libraries: what actually breaks

AES-256-GCM is widely supported, but cross-language compatibility still breaks on the small details.

This test looks at the things that actually cause failures: nonce length, tag length, and AAD encoding.

Testing methodology: Fixed 256-bit key, fixed 96-bit nonce, fixed 32-byte plaintext. 7 libraries × 7 libraries = 49 decrypt attempts (7 same-language baselines + 42 cross-language pairs). All tests used AES-256-GCM with no other algorithm variants. See our testing methodology for how we verify all tools and research on AllDevToolsHub.

AES-256-GCM is the de facto standard for authenticated encryption. It is fast, it provides both confidentiality and integrity, and every major language has a library for it.

But "has a library" does not mean "produces compatible output." We discovered this the hard way when a Node.js service encrypted a payload that a Python service could not decrypt, even though both used AES-256-GCM with the same key.

#2Methodology

We encrypted a fixed plaintext ("AllDevToolsHub_AES_Test_Block!!", exactly 32 bytes) with AES-256-GCM using a fixed 256-bit key and a fixed 96-bit nonce. Then we attempted to decrypt each ciphertext with every other library.

Test matrix: 7 encryptors × 7 decryptors = 49 combinations (7 same-language baselines + 42 cross-language pairs).

#LibraryLanguageBackend
1Node.js cryptoNode.js 22OpenSSL 3.2
2aes-gcm (npm)Node.jsPure JS
3Python cryptographyPython 3.12OpenSSL (via C)
4Python pycryptodomePython 3.12Pure C
5Go crypto/aesGo 1.23stdlib
6Java javax.cryptoJava 21JCE (SunJCE)
7Rust aes-gcmRust 1.80aes crate

Parameters held constant:

  • Key: 256-bit (32 bytes), same across all tests
  • Nonce/IV: 96-bit (12 bytes), same across all tests
  • Tag length: 128-bit (16 bytes), appended to ciphertext
  • AAD: "additional-data" encoded as UTF-8 bytes

#2Results: Interoperability matrix

Encrypt ↓ / Decrypt →Node cryptoaes-gcmPy cryptographyPy pycryptodomeGoJavaRust
Node crypto✓✓✓✓✓✗✓
aes-gcm (npm)✓✓✓✓✓✗✓
Py cryptography✓✓✓✓✓✗✓
Py pycryptodome✓✓✓✓✓✗✓
Go crypto/aes✓✓✓✓✓✗✓
Java javax.crypto✗✗✗✗✗✓✗
Rust aes-gcm✓✓✓✓✓✗✓

Key finding: Java's javax.crypto (SunJCE provider) is the outlier. It fails to decrypt ciphertext from every other library. The root cause: SunJCE uses a different ciphertext format that includes the IV prepended to the ciphertext, while all other libraries expect raw ciphertext + tag.

#2The 3 root causes of interop failure

#31. Java SunJCE ciphertext format

Java's Cipher.doFinal() returns IV + ciphertext + tag concatenated, while every other library returns just ciphertext + tag. When Java tries to decrypt ciphertext from Node.js, it expects the first 16 bytes to be the IV, but they are actually ciphertext bytes.

Fix: Strip the first 16 bytes (IV) from Java's output before sending, and prepend the IV to ciphertext received from other languages before decrypting in Java. Alternatively, use the BouncyCastle provider (bc-fips) which follows the standard format.

java
// Java encryption output:
byte[] javaOutput = cipher.doFinal(plaintext); // IV(16) + ciphertext + tag(16)

// Other libraries expect:
byte[] standardOutput = concat(ciphertext, tag); // ciphertext + tag(16)

#32. Nonce length disagreement

AES-GCM supports any nonce length, but 96-bit (12 bytes) is the standard. All 7 libraries default to 96-bit nonces. However, the pure-JS aes-gcm npm package accepts 128-bit (16-byte) nonces without warning, while OpenSSL-backed libraries reject them.

If you accidentally generate a 16-byte nonce (e.g., from crypto.randomBytes(16)), Node.js crypto will silently accept it but produce output that other libraries cannot decrypt.

Fix: Always use exactly 12 bytes for the nonce. Validate nonce length before encryption.

#33. AAD encoding

Additional Authenticated Data (AAD) must be byte-identical across encrypt and decrypt. If one side passes a string and the other passes a Buffer, the AAD may differ due to encoding.

We tested with "additional-data" encoded as UTF-8 on all sides. All 7 libraries produced the same tag when AAD was byte-identical. But when we accidentally passed the AAD as Latin-1 on one side, the tag differed and decryption failed.

Fix: Always encode AAD as UTF-8 bytes explicitly. Never pass raw strings.

#2Tag length variations

We also tested tag lengths of 96, 104, 112, 120, and 128 bits. All libraries support 128-bit (16-byte) tags. But:

Library96-bit tag104-bit tag112-bit tag120-bit tag128-bit tag
Node crypto✓✓✓✓✓
aes-gcm (npm)✗✗✗✗✓
Py cryptography✓✓✓✓✓
Py pycryptodome✓✓✓✓✓
Go crypto/aes✗✗✗✗✓
Java javax.crypto✗✗✗✗✓
Rust aes-gcm✗✗✗✗✓

Key finding: Go, Java, Rust, and the pure-JS library only support the standard 128-bit tag. If you need shorter tags (for bandwidth-constrained protocols), use Node.js crypto or Python cryptography.

#2Performance comparison (1 MB plaintext)

LibraryEncrypt (MB/s)Decrypt (MB/s)
Node crypto1,4201,380
aes-gcm (npm, pure JS)4846
Py cryptography1,3501,310
Py pycryptodome1,2901,260
Go crypto/aes1,5801,540
Java javax.crypto1,4901,450
Rust aes-gcm1,6201,590

The pure-JS library is 30x slower than OpenSSL-backed implementations. For encrypting large payloads in the browser, use the Web Crypto API (SubtleCrypto.encrypt) which is backed by the browser's native crypto stack.

#2Checklist for cross-language AES-GCM

Before encrypting in one language and decrypting in another, verify:

  1. Key length: Both sides use 256-bit (32-byte) keys.
  2. Nonce length: Both sides use 96-bit (12-byte) nonces.
  3. Tag length: Both sides use 128-bit (16-byte) tags.
  4. AAD encoding: Both sides encode AAD as the same bytes (UTF-8).
  5. Ciphertext format: Both sides agree on ciphertext + tag vs IV + ciphertext + tag.
  6. Byte order: No library uses big-endian length prefixes (we did not find this issue, but it is worth checking).

#2Sources / Further reading

#2Related tools


Written by Rahul Jalavadiya, founder of AllDevToolsHub. All tools run locally in your browser.

Quick Summary

>- We encrypted the same 16-byte plaintext with AES-256-GCM using 7 libraries across Node.js, Python, Go, Java, and Rust. Then we tried to decrypt each ciphertext with every other library. Three cross-language pairs failed — here is why and how to fix them.

RJRahul JalavadiyaFounder & Lead Engineer
Published 2026-08-20Last reviewed 2026-08-23

Tools Mentioned in This Article

Tools, tactics, and toughened-up tips, once a week

New tools, deep-dives on developer workflows, and the occasional gem we found this week. No spam, no tracking. Unsubscribe anytime.

Found an error or have feedback?

We correct errors quickly and document changes in our changelog. Report issues at support@alldevtoolshub.com.

Last reviewed: 2026-08-23
Security Memo
AT

Rahul Jalavadiya

Engineering Protocol V1

Specializing in local-first architecture and Zero-Trust developer workflows. No data leaves the machine.