Tahlil
Tahlil

Reputation: 1091

Encrypt in JS front end and decrypt in python backend using AES GCM

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?

Upvotes: 1

Views: 3276

Answers (1)

Topaco
Topaco

Reputation: 49141

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.

Upvotes: 2

Related Questions