1

I am trying encrypting in JS front end and decrypt in python backend using AES GCM cryptographic algorithm. I am using Web cryptography api for JS front end and python cryptography library for python backend as cryptographic library. I have fixed the IV for now in both side. I have implemented encryption-decryption code in both side, they work on each side. But I think the padding is done differently, can't seem to figure out how the padding is done in web cryptography api. Here is the encryption and decryption for the python backend:

def encrypt(derived_key, secret): IV = bytes("ddfbccae-b4c4-11", encoding="utf-8") aes = Cipher(algorithms.AES(derived_key), modes.GCM(IV)) encryptor = aes.encryptor() padder = padding.PKCS7(128).padder() padded_data = padder.update(secret.encode()) + padder.finalize() return encryptor.update(padded_data) + encryptor.finalize() def decrypt(derived_key, secret): IV = bytes("ddfbccae-b4c4-11", encoding="utf-8") aes = Cipher(algorithms.AES(derived_key), modes.GCM(IV)) decryptor = aes.decryptor() decrypted_data = decryptor.update(secret) unpadder = padding.PKCS7(128).unpadder() return unpadder.update(decrypted_data) + unpadder.finalize() 

Here's the JS code for encryption and decryption code:

async function encrypt(secretKey, message) { let iv = "ddfbccae-b4c4-11"; iv = Uint8Array.from(iv, x => x.charCodeAt(0)) let encoded = getMessageEncoding(message); ciphertext = await window.crypto.subtle.encrypt( { name: "AES-GCM", iv: iv }, secretKey, encoded ); return ciphertext; } async function decrypt(secretKey, cipherText) { iv = "ddfbccae-b4c4-11"; iv = Uint8Array.from(iv, x => x.charCodeAt(0)) try { let decrypted = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv: iv }, secretKey, cipherText ); let dec = new TextDecoder(); console.log("Decrypted message: "); console.log(dec.decode(decrypted)); } catch (e) { console.log("error"); } } 

I try to encrypt in the JS side and decrypt in the python side. But I got the following error: enter image description here

If I try to encrypt the same string in both side I got these outputs: In python the encrypted text: \x17O\xadn\x11*I\x94\x99\xc6\x90\x8a\xa9\x9cc=

In JS the encrypted text: \x17O\xadn\x11*I\xdf\xe3F\x81(\x15\xcc\x8c^z\xdf+\x1d\x91K\xbc

How to solve this padding issue?

2
  • 1
    GCM is a stream cipher mode and doesn't apply padding, also Cryptography implements GCM differently. In your implementation at least the tag is missing. Additionally, a 12 bytes nonce is recommended. Commented Jun 14, 2021 at 6:16
  • Right, my bad. Thanks. Commented Jun 15, 2021 at 3:07

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.