0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ZIPファイルのAES暗号化をPHPで実装する

Posted at

ZIPファイルのAES暗号化をPHPで実装する

AES暗号化について

ZIPには元々Traditional PKWARE Encryption(通称:ZipCrypto)とよばれる暗号化機能があり、WindowsやMacOSXの標準で現在でも使われている。PKZIPのAPPNOTE.txtにはより強力な暗号化の仕様(Strong Encryption Specification)が載っているが、これをサポートしているアーカイバは少ない。開発元のWinZipやPHP7.2以降のZipArchive・7-zipでは、AESを利用したAE-x encryptionとよばれる暗号化処理を実装している。以降でその仕様をまとめ、PHPによる実装例を紹介する。

AE-x encriptionの概要

  • AESのカウンターモードを使う(aes-256-ctr等)
  • 暗号化キーと検証キーにPBKDF2を使う
  • ファイル検証にHMAC_SHA1を使う
  • ファイル名やファイルサイズが隠蔽されるわけではない
  • PKZIPのStrong Encryption Specificationと違って仕様が公開されおり再実装が可能
  • とはいえサポートするアーカイバは限られる

実装の詳細は、WinZipのサイトDr. Brian Gladmanのサイトにほとんど書いてある。
1点、aes-256-ctrに渡すnonce(IV)の情報がわかりにくかった。調べた結果、nonceには1から始まるブロック番号を渡せば良い。ブロック番号は16byte毎に1繰り上がり、nonceは16byteのリトルエンディアンバイトオーダーなので "01000000...", "02000000..." のようになる。詳しくは以下の実装例をみればわかるはず。

PHPでの実装例

aeszipsample.php
<?php
/**
 * ZIP圧縮・AES暗号化のサンプル
 */
function compress($data, $password) {
	$data = gzdeflate($data);
	$salt = openssl_random_pseudo_bytes(16);
	$dk = openssl_pbkdf2($password, $salt, 66, 1000);
	$enckey = substr($dk, 0, 32);  // 先頭32Bは、AES暗号化のキー
	$authkey = substr($dk, 32, 32); // 次の32Bは、HMAC-SHA1検証のキー
	$pwd_verify = substr($dk, 64, 2); // 末尾2Bは、パスワード検査
	$encrypted = '';
	for($i=$ctr=0,$len=strlen($data); $i<$len; $i+=16) {
		// 16byteずつ処理
		$nonce = pack('VVVV', ++$ctr, 0, 0, 0); //nonceは16バイト毎に1増加
		$encrypted .= openssl_encrypt(substr($data, $i, 16), 'aes-256-ctr', $enckey, OPENSSL_RAW_DATA, $nonce);
	}
	$authcode = substr(hash_hmac('sha1', $encrypted, $authkey, true), 0, 10); //暗号化データから検証コードを生成
	return $salt . $pwd_verify . $encrypted . $authcode;
}

function zipdatetime($time) {
	list($y,$m,$d,$h,$i,$s) = explode('/', date('Y/m/d/H/i/s', $time));
	$date = ($y - 1980) << 9 | ($m - 0) << 5 | ($d - 0);
	$time = (($h - 0) << 11) | ($i - 0) << 5 | ceil(($s-0)/2.0);
	return array($date, $time);
}

function local_file_header($opt) {
	$dt = zipdatetime($opt["mtime"]);
	$exfield = pack('vvvvCv', 0x9901, 7, 2, 0x4541, 3, 8); //AE-2 strength=3(256bit) deflated
	return pack('VvvvvvVVVvv',
			0x04034b50,               //file header signaure
			51,                       //version needed extract(minimum): 5.1
			2049,                     //general purpose bit flag: encrypted|utf-8
			99,                       //compression method: aes
			$dt[1],                   //last modified time
			$dt[0],                   //last modified date
			0,                        //CRC-32
			$opt['compsize'],         //compressed size
			$opt['filesize'],         //uncompressed size
			strlen($opt['filename']), //file name length(n)
			strlen($exfield)) .       //extra field length(m)
			$opt['filename'] .        //file name
			$exfield;                 //extra field
}

function central_directory($opt) {
	$dt = zipdatetime($opt["mtime"]);
	$exfield = pack('vvvvCv', 0x9901, 7, 2, 0x4541, 3, 8); //AE-2 strength=3(256bit) deflated
	return pack('VvvvvvvVVVvvvvvVV',
			0x02014b50,               //cd signature
			51,                       //version made by: 5.1
			51,                       //version needed extract(minimum): 5.1
			2049,                     //general purpose bit flag: encrypted|utf-8
			99,                       //compression emthod: aes
			$dt[1],                   //last modified time
			$dt[0],                   //last modified date
			0,                        //CRC-32
			$opt['compsize'],         //compressed size
			$opt['filesize'],         //uncompressed size
			strlen($opt['filename']), //file name length(n)
			strlen($exfield),         //extra field length(m)
			0,                        //file comment length(k)
			0,                        //disk where cd starts
			0,                        //internal file attributes
			0,                        //external file attributes
			$opt['offset']) .         //offset of local file header
			$opt['filename'] .        //file name
			$exfield;                 //extra field
}

function end_of_central_directory($opt) {
	return pack('VvvvvVVv', 
			0x06054b50,     //eocd signature
			0,              //number of the disk", "2B"),
			0,              //disk where cd starts", "2B"),
			$opt['total'],  //number of cd on the disk", "2B"),
			$opt['total'],  //total number of cd", "2B"),
			$opt['cdsize'], //size of cd(bytes)
			$opt['offset'], //offset of start of cd
			0);             //comment length (n)
}

function save($output, $files, $password) {
	$offset = 0;
	$cd_all = '';
	$fh = fopen($output, 'wb');
	foreach($files as $filepath) {
		$filesize = filesize($filepath);
		$encrypted = compress(file_get_contents($filepath), $password);
		$opt = array(
			"filename" => $filepath,
			"filesize" => $filesize,
			"compsize" => strlen($encrypted),
			"mtime" => filemtime($filepath)
		);
		fwrite($fh, ($hdr = local_file_header($opt)));
		fwrite($fh, $encrypted);
		$cd = central_directory($opt + array("offset" => $offset));
		$offset += strlen($hdr) + strlen($encrypted);
		$cd_all .= $cd;
	}
	fwrite($fh, $cd_all);
	fwrite($fh, end_of_central_directory(array(
			"cdsize" => strlen($cd_all),
			"offset" => $offset,
			"total" => count($files))));
	fclose($fh);
	return $offset + strlen($cd_all) + 22;
}

if (isset($argv) && realpath($argv[0]) === __FILE__) {
	$output = 'output.zip';
	$files = array('sample.txt');
	$password = 'mypassword';
	$written = save($output, $files, $password);
	echo "saved: " . $output . " (" . $written . "bytes)";
}

適当な sample.txt を用意して php aeszipsample.php を実行すれば、AES暗号化された output.zip が作成される。

自作の[github - Rezipe] (https://github.com/wealandwoe/Rezipe) を利用すれば少し簡単に記述できる。

require_once 'rezipe.php';

$output = 'output.zip';
$files = array('sample.txt');
$password = 'mypassword';

$zip = new \Rezipe\Zip();
$zip->compress = true;
$zip->is_utf8 = true;
$zip->aescrypto = $password;
foreach($files as $file) {
	$zip->add_file($file, $file);
}
$zip->save($output);
echo "saved: " . $output. " (" . $zip->bytes() . "bytes)";

PHP7.2以降(libzip1.2以上)ならZipArchiveがAES暗号化に対応しているので

<?php
$zip = new ZipArchive();
if ($zip->open('output.zip', ZipArchive::CREATE) === TRUE) {
    $zip->setPassword('mypassword');
    $zip->addFile('sample.txt');
    $zip->setEncryptionName('sample.txt', ZipArchive::EM_AES_256);
    $zip->close();
    echo "Ok\n";
} else {
    echo "KO\n";
}
?>

で良い。ただ、ZipArchiveの作るAES暗号化ファイルはLocalFileHeaderにCRC-32が残っている。実はこれは間違いで、AE-x encryption(AE-2)ならば0にしないといけない。開けなくなるとか壊れるとかそういうことは無いのだが。

参考:

0
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?