Search code examples
javascriptencryptioncryptographywebcrypto-api

Disable crypto padding


If I use this code:

let enc = new TextEncoder();
let data = enc.encode('January February');

let algorithm = {
   name: 'AES-CBC', iv: enc.encode('0123456789ABCDEF')
};

crypto.subtle.importKey(
   'raw', enc.encode('GHIJKLMNOPQRSTUV'), 'AES-CBC', true, ['encrypt']
).then(
   key => crypto.subtle.encrypt(algorithm, key, data)
).then(
   ct => console.log(btoa(String.fromCharCode(...new Uint8Array(ct))))
);

I get this result:

q6BAetimbeLcdlSC7GoBbtrh/HM4xs3t1+BzEYxdEIk=

If I use this PHP code, I get the same result:

echo openssl_encrypt(
   'January February', 'aes-128-cbc', 'GHIJKLMNOPQRSTUV', iv: '0123456789ABCDEF'
);

However PHP has the option to disable padding:

echo openssl_encrypt(
   'January February',
   'aes-128-cbc',
   'GHIJKLMNOPQRSTUV',
   OPENSSL_ZERO_PADDING,
   '0123456789ABCDEF'
);

Result:

q6BAetimbeLcdlSC7GoBbg==

Can padding be disabled with JavaScript? If not, what is a good way to get the same shorter output?


Solution

  • WebCrypto uses in the context of AES-CBC PKCS7 padding by default (here), which as far as I know cannot be disabled (s. also the documentation of SubtleCrypto.encrypt()).

    Encryption with AES-CBC without padding is only possible if the plaintext is an integer multiple of the block size (16 bytes for AES), as in your example. In this case PKCS7 appends a complete block with padding bytes (which are all 0x10), i.e. in the ciphertext only this last block needs to be removed:

    //
    // Your code (implicit base64 encoding)
    //
    $enc = openssl_encrypt('January February', 'aes-128-cbc', 'GHIJKLMNOPQRSTUV', OPENSSL_ZERO_PADDING, '0123456789ABCDEF');
    print($enc . PHP_EOL);
    
    //
    // Remove last block (explicit Base64 encoding required, since the last block of the ACTUAL ciphertext must be removed)
    //
    $enc = base64_encode(substr(openssl_encrypt('January February', 'aes-128-cbc', 'GHIJKLMNOPQRSTUV', OPENSSL_RAW_DATA, '0123456789ABCDEF'), 0, -16));
    print($enc . PHP_EOL);
    

    and

    let enc = new TextEncoder();
    let data = enc.encode('January February');
    
    let algorithm = {
       name: 'AES-CBC', iv: enc.encode('0123456789ABCDEF')
    };
    
    crypto.subtle.importKey(
       'raw', enc.encode('GHIJKLMNOPQRSTUV'), 'AES-CBC', true, ['encrypt']
    ).then(
       key => crypto.subtle.encrypt(algorithm, key, data)
    ).then(     
       ct => console.log(btoa(String.fromCharCode(...new Uint8Array(ct).slice(0, -16)))) // reomve last block
    );

    Both code snippets provide q6BAetimbeLcdlSC7GoBbg== as output.

    It should be noted, that decryption is more expensive because the encrypted padding bytes must be added (if there is no valid PKCS7 padding after decryption, a DOMException is thrown).


    There are of course JavaScript libraries that are more comfortable than the low level WebCrypto API, and that also support different paddings as well as disabling padding, e.g. CryptoJS:

    var data = 'January February';
    var key = CryptoJS.enc.Utf8.parse('GHIJKLMNOPQRSTUV');
    var iv = CryptoJS.enc.Utf8.parse('0123456789ABCDEF');
    
    var encrypted = CryptoJS.AES.encrypt(
        data, 
        key,
        {
            iv: iv,
            padding: CryptoJS.pad.NoPadding
        }
    );
    
    console.log(encrypted.toString());
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>