[Crypto] PKCS#7 padding

As we know, plaintext may be of arbitrary length. But cryptographic primitives works only with fixed-size blocks. AES -- 16 bytes.

So a last block to be encrypted must be expanded somehow. One naive solution is to pad it with zeroes and to store plaintext length somewhere.

By the way, what is 'pad'? For non-English speakers (like me), 'pad' is a process of stuffing pillow or teddy bear, so to make it bigger, of desired form. Here we have padding of some block of data to make it as long as AES block size.

Let's use openssl to explore PKCS#7 padding. Last packet is simply padded with a byte, which indicate the length of padding.

 % xxd -g 1 test1.txt
00000000: 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21 0a        Hello, world!.

# encrypt:
openssl aes-256-cbc -pbkdf2 -in test1.txt -out test1.txt.enc -pass pass:1234

# decrypt using two modes:
openssl aes-256-cbc -d -pbkdf2 -in test1.txt.enc -out test1.txt.enc.dec -pass pass:1234
openssl aes-256-cbc -d -pbkdf2 -in test1.txt.enc -out test1.txt.enc.dec.nopad -nopad -pass pass:1234

First '-d' openssl command will produce the same plaintext file. But what's with '-nopad' option? It left padding as is:

 % xxd -g 1 test1.txt.enc.dec.nopad
00000000: 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21 0a 02 02  Hello, world!...

You see these two 0x02 bytes. They indicate that there are two padding bytes. Without '-nopad' option, openssl removes them or 'unpad'.

What if plaintext has 16 bytes and don't need to be padded?

 % xxd -g 1 test2.txt
00000000: 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21 21 21 0a  Hello, world!!!.

 % xxd -g 1 test2.txt.enc.dec.nopad
00000000: 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21 21 21 0a  Hello, world!!!.
00000010: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10  ................

Padding is added anyway, always. This padding indicates that there are 16 bytes.

Now something different:

 % xxd -g 1 test3.txt
00000000: 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21 20 48 6f  Hello, world! Ho
00000010: 6c 61 2c 20 4d 75 6e 64 6f 21 20 47 75 74 65 6e  la, Mundo! Guten
00000020: 20 54 61 67 21 0a                                 Tag!.

Got encrypted file:

 % xxd -g 1 test3.txt.enc
00000000: 53 61 6c 74 65 64 5f 5f d9 00 cc b1 a5 36 07 e3  Salted__.....6..
00000010: 3d 4b e0 de c2 cb f2 eb f4 d7 f1 0c ed db df 13  =K..............
00000020: d6 25 97 8f 28 09 e6 ed 00 88 3e 6c e0 23 65 02  .%..(.....>l.#e.
00000030: de 0d d7 84 09 15 f5 36 6c 78 0f 69 7e 83 8e bf  .......6lx.i~...

Let's vandalize that encrypted file. I modify the last 0xbf byte to 0x01:

 % xxd -g 1 test3.txt.enc.modified
...
00000030: fb d8 d6 b1 fe 90 94 45 d8 05 f0 2f b8 83 8a 01  .......E.../....

Let's try to decrypt that modified file.

 % openssl aes-256-cbc -d -pbkdf2 -in test3.txt.enc.modified -out test3.txt.enc.dec.modified.nopad -nopad -pass pass:1234

 % xxd -g 1 test3.txt.enc.dec.modified.nopad
00000000: 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21 20 48 6f  Hello, world! Ho
00000010: 6c 61 2c 20 4d 75 6e 64 6f 21 20 47 75 74 65 6e  la, Mundo! Guten
00000020: 91 be 5d bb 0f 81 f0 92 6e c9 23 8a e0 37 21 a8  ..].....n.#..7!.

OpenSSL is silent, because '-nopad' option supplied, so it doesn't check padding. You see, last 16 bytes are garbled. Indeed, AES algo can't decrypt last 16-byte blocks correctly. We see "Guten" without "Tag".

What if to decrypt this file without '-nopad' option?
 % openssl aes-256-cbc -d -pbkdf2 -in test3.txt.enc.modified -out test3.txt.enc.dec.modified -pass pass:1234
bad decrypt
139966322365632:error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt:crypto/evp/evp_enc.c:610:

 % xxd -g 1 test3.txt.enc.dec.modified
00000000: 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21 20 48 6f  Hello, world! Ho
00000010: 6c 61 2c 20 4d 75 6e 64 6f 21 20 47 75 74 65 6e  la, Mundo! Guten

Unpadding failed. That garbled noise isn't a valid padding. How OpenSSL checks it? Easily. It finds the last byte. It's N, let's say. OpenSSL then looks for N bytes with N value. If it is so, padding is correct and removed. If not, report the error.

Nevertheless, OpenSSL can decrypt that file partially, without "Tag" word, ignoring last 16 bytes.

Important: I ran this using OpenSSL 1.1.1k. What is in the crypto/evp/evp_enc.c file for that version?

...
        /*
         * The following assumes that the ciphertext has been authenticated.
         * Otherwise it provides a padding oracle.
         */
        n = ctx->final[b - 1];
        if (n == 0 || n > (int)b) {
            EVPerr(EVP_F_EVP_DECRYPTFINAL_EX, EVP_R_BAD_DECRYPT);
            return 0;
        }
        for (i = 0; i < n; i++) {
            if (ctx->final[--b] != n) {
                EVPerr(EVP_F_EVP_DECRYPTFINAL_EX, EVP_R_BAD_DECRYPT);
                return 0;
            }
        }
...

But OpenSSL 3.0.2 (on another computer I use) would report another error:

bad decrypt
40C7C3A30E7F0000:error:1C800064:Provider routines:ossl_cipher_unpadblock:bad decrypt:../providers/implementations/ciphers/ciphercommon_block.c:129:

What is in the providers/implementations/ciphers/ciphercommon_block.c file for that version?

int ossl_cipher_unpadblock(unsigned char *buf, size_t *buflen, size_t blocksize)
{
    size_t pad, i;
    size_t len = *buflen;

    if(len != blocksize) {
        ERR_raise(ERR_LIB_PROV, ERR_R_INTERNAL_ERROR);
        return 0;
    }

    /*
     * The following assumes that the ciphertext has been authenticated.
     * Otherwise it provides a padding oracle.
     */
    pad = buf[blocksize - 1];
    if (pad == 0 || pad > blocksize) {
        ERR_raise(ERR_LIB_PROV, PROV_R_BAD_DECRYPT);
        return 0;
    }
    for (i = 0; i < pad; i++) {
        if (buf[--len] != pad) {
            ERR_raise(ERR_LIB_PROV, PROV_R_BAD_DECRYPT);
            return 0;
        }
    }
    *buflen = len;
    return 1;
}

This is how padding is checked in OpenSSL.

Stay tuned for my next blog post about padding.

(the post first published at 20230104.)


List of my other blog posts.

Subscribe to my news feed

Yes, I know about these lousy Disqus ads. Please use adblocker. I would consider to subscribe to 'pro' version of Disqus if the signal/noise ratio in comments would be good enough.