暗号zipを作ってみる

 zipファイルを解凍する際にパスワードを要求されることがありますが、それはzipファイルのアーカイブデータが暗号化されている証拠です。今回はzipデータの暗号化を行う方法について解説します。

 zip形式で用いられている暗号化アルゴリズムは「ZipCrypto」といわれる、独自の方法です。zipファイルに対しては、AESやBlowfishといったより強力な暗号化アルゴリズムが仕様上は使えるのですが、これに関してはPKWARE社が特許を取っているため、zip標準として実装するにはPKWAREとライセンスを結ぶ必要があり、こういった高度な暗号化ができるzipアーカイバの大半が有料のソフトウェアです(ちなみにzipファイルを圧縮ファイル単位ではなく、まるごと暗号化するのであれば、仕様の範囲外となるためライセンスは不要です)。

 zip形式の暗号化の主な手順はこのようになります。

  1. パスワードより3つの32ビットキーを生成する
  2. 32ビットキーより12バイトの暗号ヘッダを作成して、アーカイブデータの前に追記する
  3. これらをもとにアーカイブ対象のデータを暗号化する
 まずは32ビットキーの生成の仕方から。32ビットキーは、それぞれ異なる初期値を持つ3つのunsigned int型に対し、パスワードの文字数だけ、決められた符号化を施します。

static unsigned int key[3];

inline unsigned int GetCRC32(unsigned int n1, unsigned int n2)
{
	// tableはInitCRC32で生成された256個からなる配列
	return table[(n1 ^ n2) & 0xFF] ^ (n1 >> 8);
}

inline void update_keys(unsigned char n)
{
	key[0] = GetCRC32(key[0], n);
	key[1] += (key[0] & 0xFF);
	key[1] = key[1] * 134775813 + 1;
	key[2] = GetCRC32(key[2], key[1] >> 24);
}

void InitZipCrypt(char *password, unsigned char cryptheader[])
{
	// 3つの32ビットキーの初期値は決められている
	key[0] = 305419896;
	key[1] = 591751049;
	key[2] = 878082192;

	// パスワードを元にキーを符号化する
	char *p = password;
	while(*p != '\0') update_keys(*(p++));
}

 この符号化プログラムを見るとおわかりいただけると思いますが、パスワードにはひらがなや漢字などの英数字以外も理論上は使うこともできます(もちろん、文字列が一致していても、シフトJISなどの文字コードが一致していないと復号はできません)。

 続いて暗号ヘッダを作成します。この暗号ヘッダを仲介させることで、アーカイブデータの暗号化ルーチンにおける法則性を見かけ上なくさせます。暗号ヘッダは12バイトで、内部のバイトは完全なランダムにするようにしてください。C言語標準のrand関数は、乱数生成としては程度の低い部類に当たるため、より高度な乱数の作成を行いたいのであれば、Boost C++ Librariesの乱数発生クラスなどを使うとよいでしょう。

 暗号ヘッダの作成は通常これだけでよいのですが、厳密なチェックを行うzipアーカイバの場合、この暗号ヘッダに識別子が入っているかどうかも調べます。この識別子は、暗号ヘッダ12バイト目に圧縮元のデータのCRC32値の上位8ビット(つまり「zipheader.crc32 >> 24」)を代入することで対応可能です。

 暗号ヘッダの識別子の初期化とその補助処理を追加した、暗号化初期化関数「InitZipCrypt」のプログラムコード例は以下のようになります。

// この定義はメイン関数でも使うので、ヘッダファイルに設置すること
#define CRYPTHEADLEN 12

inline unsigned char decrypt_byte()
{
	unsigned short temp = (unsigned short)((key[2] & 0xFFFF) | 2);
	return (unsigned char)(((temp * (temp ^ 1)) >> 8) & 0xFF);
}

inline unsigned char zencode(unsigned char n)
{
	unsigned char t = decrypt_byte();
	update_keys(n);
	return (t ^ n);
}

void InitZipCrypt(char *password, unsigned char cryptheader[], unsigned int crc32)
{
	key[0] = 305419896;
	key[1] = 591751049;
	key[2] = 878082192;

	char *p = password;
	while(*p != '\0') update_keys(*(p++));

	for(int i = 0; i < CRYPTHEADLEN; i++){
		if(i == CRYPTHEADLEN - 1){
			cryptheader[i] = (unsigned char)(crc32 & 0xFF);
		}else{
			cryptheader[i] = rand();
		}
		cryptheader[i] = zencode(cryptheader[i]);
	}
}

 InitZipCryptで返されたcryptheader配列は、暗号化されるアーカイブデータの前に付加します。ここで注意しておきたいことは、このヘッダはアーカイブデータの一部とみなされるため、compsizeにはアーカイブデータのサイズ+ヘッダサイズ(12)を代入しなくてはならないことです。

 アーカイブデータに対する暗号化アルゴリズムであるZipCryptoは、1バイトごとに暗号処理を行うストリーム暗号形式となっているため、暗号化前と暗号化後のデータサイズが変わることはありません。

void ZipCrypt(unsigned char *buffer, int buflen)
{
	for(int i = 0; i < buflen; i++){
		buffer[i] = zencode(buffer[i]);
	}
}

 それでは、以上の暗号化関数を含めた暗号化zipを作成するサンプルプログラムをどうぞ。

void _tmain()
{
	/* 省略 */
	
	for(int i = 0; i < ARCHIVECOUNT; i++){
		zipheader[i].signature = ZIPSIG_CENTDIR;	// PK0304
		zipheader[i].needver = 20;					// Deflate圧縮を利用できるVer
		zipheader[i].comptype = 8;					// Deflate圧縮
		zipheader[i].option |= 0x1;					// 暗号化の宣言

		/* 省略 */
	}

	/* 省略 */

	unsigned char cryptheader[CRYPTHEADLEN];

	/* 省略 */

	InitCRC32();
	for(int i = 0; i < ARCHIVECOUNT; i++){
		// データの位置を格納
		archeader[i].headerpos = ftell(fp);

		// zipヘッダの書き込み
		WriteZipHeader(fp, &zipheader[i]);

		// 現在のファイル位置を一時的に取得
		current = ftell(fp);
	
		// zlib構造体をメモリの開放を行わず再初期化する
		deflateReset(&zs);

		// crc32の初期値は0xFFFFFFFFにする
		zipheader[i].crc32 = 0xFFFFFFFF;


		// データの圧縮と書き込み
		compsize = 0;
		zs.avail_in = zipheader[i].uncompsize;
		zs.next_in = (Bytef*)teststrs[i];

		// CRC32値の更新
		zipheader[i].crc32 = GetCRC32(zs.next_in, zs.avail_in, zipheader[i].crc32);
		InitZipCrypt("zip", cryptheader, zipheader[i].crc32 >> 24);
		fwrite(cryptheader, 1, CRYPTHEADLEN, fp);

		do{
			zs.avail_out = tempsize;
			zs.next_out = outbuf;

			deflate(&zs, Z_FINISH);

			// 暗号化
			ZipCrypt(outbuf, tempsize - zs.avail_out);

			fwrite(outbuf, 1, tempsize - zs.avail_out, fp);
		}while(zs.avail_out == 0);


		// 圧縮サイズデータの更新
		zipheader[i].compsize = ftell(fp) - current;

		current = ftell(fp);

		// ファイルデータの書き換え
		fseek(fp, archeader[i].headerpos + 14, SEEK_SET);
		fwrite(&zipheader[i].crc32, 1, sizeof(unsigned int), fp);
		fwrite(&zipheader[i].compsize, 1, sizeof(unsigned int), fp);
		fseek(fp, 0, SEEK_END);


		// ヘッダ内容のコピー
		CopyToCentralDirHeader(&archeader[i], &zipheader[i]);
	}
	
	/* 省略 */
}

暗号化zipテスト
7-zipによる展開例

 ちなみに、上記サンプルプログラムの場合、「zip」が暗号化を解除するためのパスワードとなります。コードファイル一式が含まれているプロジェクトファイルのダウンロードはこちらからどうぞ。