読者です 読者をやめる 読者になる 読者になる

Shin x Blog

PHPをメインにWebシステムを開発してます。Webシステム開発チームの技術サポートも行っています。

openssl_encrypt() による PKCS#7 パディング

PHP の openssl_encrypt() にて、ブロック暗号による暗号化を行うと PKCS#7 パディングが行われます。この動きを確認してみます。

http://php.net/manual/ja/function.openssl-encrypt.php

PKCS#7 パディング

ブロック暗号では、決められたブロック長を単位として暗号化を行います。対象となる平文がブロック長の倍数の長さであれば良いのですが、そうではない場合、不足分を補う必要があります。これがパディングです。*1

PKCS#7 パディングは RFC 5652 で定義されたパディング方式です。補ったバイト数を 1 バイトの値として埋めます。例えば、3 バイトを補うのであれば、0xXXXX030303 のように 3 が 3 つ並びます。PKCS#7 では、1 バイトで不足バイト数を示すので、1 - 255 バイトまでを補うことができます。

01     <--- 1 バイト分
0202   <--- 2 バイト分
030303 <--- 3 バイト分

https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7

これと似た動きをするのが、PKCS#5 パディングで、こちらは 8 バイトを倍数とした場合のパディングを行います。つまり、取り得る値は 1 - 8 となります。

余談ですが、Java の javax.crypto.Cipher にある PKCS5Padding は、名前は PKCS5 ですが、実質は PKCS#7 相当の動きをするようです。

他のパディングには、0 で埋める ゼロパディングなどがあります。

openssl_encrypt() によるパディング

openssl_encrypt() のデフォルトでは、PKCS#7 パディングが行われます。

この動きを見ていくためのサンプルが以下です。このサンプルでは、openssl_encrypt() で暗号化、openssl_decrypt()で復号を行っています。

ここでは、AES(Rijndael)を利用しているのでブロック長は 128bit(16バイト)です。平文は plain の 5 バイトなので、不足分の 11 バイトがパディングで埋められます。

<?php
$plaintext = 'plain';
$key = hash('sha256', 'this is a secret key.');
$method = 'aes-256-cbc';
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($method));

$encrypted = openssl_encrypt($plaintext, $method, $key, 0, $iv);
printf("encrypted: %s\n", $encrypted);

$decrypted = openssl_decrypt($encrypted, $method, $key, 0, $iv);
printf("decrypted: %s\n", $decrypted);

これを実行すると下記のような出力になります。暗号文はデフォルトで base64 エンコードされています。復号した結果を見ると元の平分が出力されています。ここではパディングが見えないですが、これは openssl_decrypt() にて復号時にパディングを除去するためです。

encrypted: vGZiiLXzuiGnOBkJbWAiHw==
decrypted: plain

暗号文にパディングが含まれているかを確認するために復号時に OPENSSL_NO_PADDING を指定して、パディング除去が行われないようにします。

<?php
$plaintext = 'plain';
$key = hash('sha256', 'this is a secret key.');
$method = 'aes-256-cbc';
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($method));

$encrypted = openssl_encrypt($plaintext, $method, $key, 0, $iv);
printf("size: %d bytes\n", strlen(base64_decode($encrypted)));

$decrypted = openssl_decrypt(base64_decode($encrypted), $method, $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $iv);
printf("decrypted: %s\n", bin2hex($decrypted));
printf("size: %d\n", strlen($decrypted));

これを実行したのが下記です。

パディングが見えるように復号した結果は bin2hex() で出力しています。前半の 706c61696e の部分は平文の plain です。それにつづく 0b が 11 回連続している箇所がパディングです。ブロック長 16 バイトから平文の 5 バイトを引いた 11 バイトがパディングとなるので 0x0b が 11 回連速しています。

復号した結果は 16 バイトとなっており、これもブロック長と一致します。

encrypted: iwBv6P3euwwdct8DHutBNA==
decrypted: 706c61696e0b0b0b0b0b0b0b0b0b0b0b
size: 16

平文サイズがブロック長と一致している場合

では、平文サイズがブロック長と一致している場合はどうなるでしょう。

平文を plainplainplaina の 16 バイトにして実行したのが下記です。

復号結果は、なんと 32 バイトになりました。内容を見ると 10 が 16 回連続しています。これは、ブロック長分のパディングです。つまり、1 ブロック分が追加されているわけです。

平文の後にはパディングが付いていることを前提とするため(そうでないと平文なのかパディングなのか判断できない)にこのような動きになっているのでしょう。

encrypted: PFZ8G+XYhs9x22aP/a7Jy88eQJv6GVaKMXrBfq5/bGU=
decrypted: 706c61696e706c61696e706c61696e6110101010101010101010101010101010
size: 32

さいごに

openssl_encrypt() によるパディングを見てきました。

デフォルトのまま、openssl_encrypt() で暗号化し、openssl_decrypt() で復号するならパディングを意識する必要はあまりありません。

しかし、別システムで暗号化されたものを復号したり、その逆の場合は、どのような形式でパディングを行うのかというのは共有しておく必要があります。

参照

*1:実際には、後述するとおりブロック長倍数であってもパディングが追加されます。