fuka’s diary

A blog that shares my knowledge.

PHPにおけるAES暗号化アルゴリズムGCM/CBCモードの使用

PHPではopenssl_encrypt、openssl_decryptを用いて対称暗号化アルゴリズムを使用できます。
クセモノなのが、$options引数と、$iv引数です。
ここを誤ると脆弱性を書き込んでしまうので、知識を共有します。

$options引数

公式マニュアルにある通り、以下の定数が使用できます。

OPENSSL_RAW_DATA = 1

暗号化データを生データとして処理します。
なお、指定がない場合は、暗号化データをBASE64として処理します。

OPENSSL_ZERO_PADDING = 2

パディングされません。GCMモードで使用します。

時折、OPENSSL_NO_PADDINGを使用した例を見ることがありますが、使用可能な定数は上記の2つだけです。
OPENSSL_NO_PADDING = 3なので、OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDINGと等価ではあります。
しかし、OPENSSL_NO_PADDINGは非対称暗号で用いる定数ですから、対称暗号化では使用してはいけません。

$iv引数

初期ベクトルはnonceであることが求められます。
nonceとは一意値(重複しない値)であり、単純にランダム値だけで構成することは誤りです。
また、UNIXタイムでは完全同時アクセスに脆弱であり、UUIDは確率的に衝突はゼロではありません。
PHPにはopenssl_random_pseudo_bytes関数が用意されていますので、これを使いましょう。
ちなみに、インクリメント値もnonceとして妥当ですが、攻撃を受けやすい弱点がありますから注意してください。

実装

実装は次のようになります。
ライセンスはMITライセンスね。

<?php
function enc_aes_gcm($plain, string $passwd, bool $base64_mode = true){
	$key = hash('sha256', $passwd, true);
	$options = OPENSSL_ZERO_PADDING;
	if(! $base64_mode){
		$options = $options|OPENSSL_RAW_DATA;
	}
	$ivleng = openssl_cipher_iv_length('aes-256-gcm');
	$iv = openssl_random_pseudo_bytes($ivleng);
	$tag = null;
	$cipherdata = false;
	try{
		$cipherdata = openssl_encrypt(
			$plain,
			'aes-256-gcm',
			$key,
			$options,
			$iv,
			$tag
		);
	}catch(\Throwable $e){
		;
	}
	return ['data'=>$cipherdata, 'iv'=>$iv, 'tag'=>$tag];
}

function dec_aes_gcm($cipherdata, string $passwd, string $iv, string $tag, bool $base64_mode = true){
	$key = hash('sha256', $passwd, true);
	$options = OPENSSL_ZERO_PADDING;
	if(! $base64_mode){
		$options = $options|OPENSSL_RAW_DATA;
	}
	$plain = false;
	try{
		$plain = openssl_decrypt(
			$cipherdata,
			'aes-256-gcm',
			$key,
			$options,
			$iv,
			$tag
		);
	}catch(\Throwable $e){
		;
	}
	return $plain;
}

function enc_aes_cbc($plain, string $passwd, bool $base64_mode = true){
	$key = hash('sha256', $passwd, true);
	$options = 0;
	if(! $base64_mode){
		$options = OPENSSL_RAW_DATA;
	}
	$ivleng = openssl_cipher_iv_length('aes-256-cbc');
	$iv = openssl_random_pseudo_bytes($ivleng);
	$cipherdata = false;
	try{
		$cipherdata = openssl_encrypt(
			$plain,
			'aes-256-cbc',
			$key,
			$options,
			$iv
		);
	}catch(\Throwable $e){
		;
	}
	return ['data'=>$cipherdata, 'iv'=>$iv];
}

function dec_aes_cbc($cipherdata, string $passwd, string $iv, bool $base64_mode = true){
	$key = hash('sha256', $passwd, true);
	$options = 0;
	if(! $base64_mode){
		$options = OPENSSL_RAW_DATA;
	}
	$plain = false;
	try{
		$plain = openssl_decrypt(
			$cipherdata,
			'aes-256-cbc',
			$key,
			$options,
			$iv
		);
	}catch(\Throwable $e){
		;
	}
	return $plain;
}

実際に使う時はこんな感じです。

<?php
$password = 'aaaaa';
$plaintext = 'テスト';
$enc = enc_aes_gcm($plaintext, $password, false);
$dec = dec_aes_gcm($enc['data'], $password, $enc['iv'], $enc['tag']);
echo $dec;
$enc = enc_aes_cbc($plaintext, $password, false);
$dec = dec_aes_cbc($enc['data'], $password, $enc['iv']);
echo $dec;

exit();