GCM is a stream cipher mode and therefore does not require padding. During encryption, an authentication tag is implicitly generated, which is used for authentication during decryption. Also, an IV/nonce of 12 bytes is recommended for GCM.
The posted Python code unnecessarily pads and doesn't take the authentication tag into account, unlike the JavaScript code, which may be the main reason for the different ciphertexts. Whether this is the only reason and whether the JavaScript code implements GCM correctly, is difficult to say, since the getMessageEncoding() method was not posted, so testing this was not possible.
Also, both codes apply a 16 bytes IV/nonce instead of the recommended 12 bytes IV/nonce.
Cryptography offers two possible implementations for GCM. One implementation uses the architecture of the non-authenticating modes like CBC. The posted Python code applies this design, but does not take authentication into account and therefore implements GCM incompletely. A correct example for this design can be found here.
Cryptography generally recommends the other approach for GCM (s. the Danger note), namely the AESGCM class, which performs implicit authentication so that this cannot be accidentally forgotten or incorrectly implemented.
The following implementation uses the AESGCM class (and also takes into account the optional additional authenticated data):
from cryptography.hazmat.primitives.ciphers.aead import AESGCM import base64 #import os #key = AESGCM.generate_key(bit_length=256) #nonce = os.urandom(12) key = base64.b64decode('MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=') # fix for testing, AES-256 nonce = base64.b64decode('MDEyMzQ1Njc4OTAx') # fix for testing, 12 bytes plaintext = b'The quick brown fox jumps over the lazy dog' aad = b'the aad' # aad = None without additional authenticated data aesgcm = AESGCM(key) ciphertext = aesgcm.encrypt(nonce, plaintext, aad) print('Ciphertext (B64): ' + base64.b64encode(ciphertext).decode('utf8')) decrypted = aesgcm.decrypt(nonce, ciphertext, aad) print('Decrypted: ' + decrypted.decode('utf8'))
with the output:
Output Ciphertext (B64): JOetStCANhPISvQ6G6IcRBauqbtC8fzRooblayHqkqSPKzLbidx/gBWfLNzBC+ZpcAGnGnHXaI7CB1U= Decrypted: The quick brown fox jumps over the lazy dog
The authentication tag is appended to the ciphertext, so the (Base64 decoded) result has the length of the plaintext (43 bytes) plus the length of the tag (16 bytes, default), giving a total of 59 bytes.
For testing, a predefined key and IV/nonce are used with regard to a comparison with the result of the JavaScript code. Note that in practice a key/IV pair may only be used once for security reasons, which is especially important for GCM mode, e.g. here. Therefore a random IV/nonce is typically generated for each encryption.
The WebCrypto API is a low level API for cryptography and does not provide methods for Base64 encoding/decoding. In the following, js-base64 is used for simplicity. Just like the Python code, the tag is appended to the ciphertext.
A possible implementation for AES-GCM using the key and IV/nonce of the Python code that is functionally essentially the same as the posted JavaScript code is:
(async () => { var key = Base64.toUint8Array('MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE='); // fix for testing, AES-256 var nonce = Base64.toUint8Array('MDEyMzQ1Njc4OTAx'); // fix for testing, 12 bytes var plaintext = new TextEncoder().encode("The quick brown fox jumps over the lazy dog"); var aad = new TextEncoder().encode('the aad'); var keyImported = await await crypto.subtle.importKey( "raw", key, { name: "AES-GCM" }, true, ["decrypt", "encrypt"] ); var ciphertext = await await crypto.subtle.encrypt( { name: "AES-GCM", iv: nonce, additionalData: aad }, // { name: "AES-GCM", iv: nonce } without additional authenticated data keyImported, plaintext ); console.log('Ciphertext (Base64):\n', Base64.fromUint8Array(new Uint8Array(ciphertext)).replace(/(.{48})/g,'$1\n')); var decrypted = await await crypto.subtle.decrypt( { name: "AES-GCM", iv: nonce, additionalData: aad }, // { name: "AES-GCM", iv: nonce } without additional authenticated data keyImported, ciphertext ); console.log('Plaintext:\n', new TextDecoder().decode(decrypted).replace(/(.{48})/g,'$1\n')); })();
<script src="https://cdn.jsdelivr.net/npm/[email protected]/base64.min.js"></script>
with the output:
Ciphertext (Base64): JOetStCANhPISvQ6G6IcRBauqbtC8fzRooblayHqkqSPKzLbidx/gBWfLNzBC+ZpcAGnGnHXaI7CB1U= Plaintext: The quick brown fox jumps over the lazy dog
where the ciphertext is the same as that of the Python code.