アプリ開発ときどきアウトドア

主にJavaを使ったアプリ開発やトラブルシューティング等のノウハウ、キャンプや登山の紹介や体験談など。

1. システムエンジニアリング Java 実装技術

Javaでのパスワード付きzipファイルの圧縮/解凍方法(ZipCrypto/AES)

投稿日:2019年10月27日 更新日:

先日、JavaでのZIP暗号化の考察という記事を書きましたが、zip4jのメンテナンスが再開されており、バージョン2系が公開されていましたので、これを使って通常のzip圧縮/解凍、パスワード付きzip圧縮/解凍(ZipCrypto, AES256)のサンプルを作成してみました。

概要

  • zip4jはSrikanth Reddy Lingalaさんが開発しましたが2013年にメンテナンスが止まっていました。その後、JavaでのZIP圧縮ライブラリとしてzip4jが有名になったため、メンテナンスの再開を決意されたそうです。メンテナンスを再開してからのリリースは、バージョン2系として公開されています。
  • zip4jの公式サイトはGitHubのzip4jです。
  • バージョン2系では、より短いコードで簡単にファイル/ストリーム操作を行えるようになっています。単純な圧縮/解凍であればワンライナーでプログラミングが終わります。その他、処理状況を把握するためのプログレスモニターが追加されています。
  • バージョン2系の実行には、JRE8以上が必要となります。
  • 下記の調査結果の通り、Windows圧縮/解凍ではAES256を扱えませんが、その他のWindows、7-zip、Zip4j間での圧縮・暗号化の互換性は問題ありません。ファイル名に日本語が含まれる場合は文字化けする場合があるので、可能であれば日本語ファイル名を使用しないような設計を推奨します。
  • 解凍すると1GBになるようなファイルの解凍を試しましたが、特に大きくメモリを消費することもありませんでした。

サンプルコード

ソースコード

  • zip4jを使って、通常のZIP圧縮/解凍、パスワード付き圧縮/解凍(ZipCrypt, AES256)を行うサンプルです。JUnitで実行できます。
  • create(), extract()はWebサーバ上で使う想定のサンプルになっています。Webサーバにアップロードしたzipファイルを、サーバ上のファイルシステムに展開し、何らかの業務処理を行った後に、zipファイルのストリームに戻す、という想定です。
  • Webサーバにアップロードされたファイルは、サーバ上のプログラムからはInputStreamで参照することになります。そのため、解凍テスト用のメソッドextractTestでは、zipファイルの読み取り元としてInputStreamを使用します。
  • Webサーバ上に展開されたフォルダ内容をzipファイルとしてダウンロード、DBに登録する場合を想定します。このような場合、zipファイルの内容をOutputStreamに出力することになります。そのため、圧縮テスト用のcreateTestでは、zipファイルの出力先としてOutputStreamを使用します。
  • 内部的にメモリを大量に消費していないかを確認できるよう、debugメソッドでメモリ使用状況を出力するようにしています。
  • mavenを使う前提のサンプルになっています。
  • 分かりやすさを優先した関係で、一部でリソース解放やセキュリティの考慮がありませんので、業務で使用する際には検討してください。
package example.zip4j;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.junit.BeforeClass;
import org.junit.Test;

// java.util.zipにいくつか同名クラスがあることに注意
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.io.outputstream.ZipOutputStream;
import net.lingala.zip4j.model.LocalFileHeader;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.model.enums.AesKeyStrength;
import net.lingala.zip4j.model.enums.EncryptionMethod;

public class Zip4jExampleTest {

	private static final int BUF_SIZE = 4 * 1024 * 10;

	private static final String IN_TOP_DIR = ".\\indata\\"; // 入力フォルダ

	private static final String OUT_TOP_DIR = ".\\outdata\\"; // 出力先フォルダ(削除されます)

	private static final char[] PASSWORD = "password".toCharArray();

	private byte[] buf = new byte[BUF_SIZE];

	// 本来であればtry-with-resources等でストリームを閉じる必要があります!

	@BeforeClass
	public static void prepare() throws IOException {
		// 出力先ディレクトリを一旦削除
		Path out = Paths.get(OUT_TOP_DIR).toAbsolutePath().normalize();
		if (Files.exists(out)) {
			System.out.println("deleting: " + out);
			Files.walk(out).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
		}
		Files.createDirectories(out);
	}

	@Test
	public void createZip() throws IOException {
		String[] files = { IN_TOP_DIR + "testdir" };
		FileOutputStream fos = new FileOutputStream(OUT_TOP_DIR + "testdir_zip4j.zip");
		ZipOutputStream zos = new ZipOutputStream(fos);
		ZipParameters baseParams = new ZipParameters();
		baseParams.setEncryptFiles(false);
		create(files, baseParams, zos);
		zos.close();
	}

	@Test
	public void createPasswordZip_ZipCrypto() throws IOException {
		String[] files = { IN_TOP_DIR + "testdir" };
		FileOutputStream fos = new FileOutputStream(OUT_TOP_DIR + "testdir_zip4j_zipcrypto.zip");
		ZipOutputStream zos = new ZipOutputStream(fos, PASSWORD);
		ZipParameters baseParams = new ZipParameters();
		baseParams.setEncryptFiles(true);
		baseParams.setEncryptionMethod(EncryptionMethod.ZIP_STANDARD);
		create(files, baseParams, zos);
		zos.close();
	}

	@Test
	public void createPasswordZip_AES256() throws IOException {
		String[] files = { IN_TOP_DIR + "testdir" };
		FileOutputStream fos = new FileOutputStream(OUT_TOP_DIR + "testdir_zip4j_aes256.zip");
		ZipOutputStream zos = new ZipOutputStream(fos, PASSWORD);
		ZipParameters baseParams = new ZipParameters();
		baseParams.setEncryptFiles(true);
		baseParams.setEncryptionMethod(EncryptionMethod.AES);
		baseParams.setAesKeyStrength(AesKeyStrength.KEY_STRENGTH_256); // 128/192/256
		create(files, baseParams, zos);
		zos.close();
	}

	@Test
	public void extractZip() throws IOException {
		FileInputStream fis = new FileInputStream(IN_TOP_DIR + "testdir_win.zip");
		ZipInputStream zis = new ZipInputStream(fis);
		ZipParameters baseParams = new ZipParameters();
		baseParams.setEncryptFiles(false);
		extract(zis, OUT_TOP_DIR + "extracted_zip");
		zis.close();
	}

	@Test
	public void extractPasswordZip_ZipCrypto() throws IOException {
		FileInputStream fis = new FileInputStream(IN_TOP_DIR + "testdir_7zip_zipcrypto.zip");
		ZipInputStream zis = new ZipInputStream(fis, PASSWORD);
		extract(zis, OUT_TOP_DIR + "extracted_zipcrypto");
		zis.close();
	}

	@Test
	public void extractPasswordZip_AES256() throws IOException {
		FileInputStream fis = new FileInputStream(IN_TOP_DIR + "testdir_7zip_aes256.zip");
		ZipInputStream zis = new ZipInputStream(fis, PASSWORD);
		extract(zis, OUT_TOP_DIR + "extracted_aes256");
		zis.close();
	}

	public void create(String[] srcFiles, ZipParameters baseParams, ZipOutputStream zos) throws IOException {

		// 圧縮対象の元となるソースパスリスト
		List<Path> srcPathList = new ArrayList<>();
		try (Stream<String> st = Arrays.stream(srcFiles)) {
			srcPathList = st.map(Paths::get).collect(Collectors.toList());
		}

		debug("圧縮開始");

		// ソースパス上のファイルを再帰的に圧縮
		for (Path srcPath : srcPathList) {

			// ソースファイルのパスを正規化済の絶対パスに変換
			srcPath = srcPath.toAbsolutePath().normalize();
			Path parentPath = srcPath.getParent();

			// 圧縮対象ファイルリスト
			List<Path> targetPathList = new ArrayList<>();

			// パスがディレクトリの場合、ディレクトリ配下のファイルを再帰的に
			// 圧縮対象ファイルリストへ追加
			if (Files.isDirectory(srcPath)) {
				// 解凍時にディレクトリは自動で作成されるのでディレクトリは除外
				try (Stream<Path> st = Files.walk(srcPath)) {
					st.filter(Files::isRegularFile).forEach(targetPathList::add);
				}
			} else {
				targetPathList.add(srcPath);
			}

			// 圧縮対象ファイルリストのファイルをZIPストリームに出力
			for (Path targetPath : targetPathList) {

				// ソースファイルのパスから、圧縮対象ファイルの相対パスを生成
				Path relPath = parentPath.relativize(targetPath);

				// 対象ファイル用のパラメータ定義
				ZipParameters targetParams = new ZipParameters(baseParams);
				targetParams.setFileNameInZip(relPath.toString());

				// ZIPストリームにファイルを出力
				zos.putNextEntry(targetParams);
				try (InputStream is = new FileInputStream(targetPath.toFile())) {
					int readSize;
					while ((readSize = is.read(buf)) > 0) {
						zos.write(buf, 0, readSize);
					}
				}
				zos.closeEntry();

				debug("圧縮済: " + relPath + " <- " + targetPath);
			}
		}
		// close()が確実に実行される実装にしないと不正なzipファイルになる場合がある
	}

	public void extract(ZipInputStream zis, String extractTopDir) throws IOException {

		byte[] buf = new byte[BUF_SIZE];

		// 展開先ディレクトリパス(存在しない場合は新規作成)
		Path extractTopPath = Paths.get(extractTopDir).toAbsolutePath().normalize();
		if (!Files.exists(extractTopPath)) {
			Files.createDirectories(extractTopPath);
		}

		debug("解凍開始");

		// ZIPストリームのエントリ毎にファイルを作成
		LocalFileHeader fh;
		while ((fh = zis.getNextEntry()) != null) {

			// 解凍先パスの決定
			String filename = fh.getFileName();
			File extractFile = new File(extractTopPath + File.separator + filename);

			// ディレクトリを解凍
			if (fh.isDirectory()) {
				extractFile.mkdirs(); // 2層以上作成する場合もあるのでmkdirsを使用
				continue;
			}

			// ファイルを解凍
			// ※zipファイルによってはディレクトリが含まれない場合があるので事前作成
			File parentFile = extractFile.getParentFile();
			if (!parentFile.exists()) {
				parentFile.mkdirs();
			}
			try (OutputStream os = new BufferedOutputStream(new FileOutputStream(extractFile));) {
				int readSize;
				while ((readSize = zis.read(buf)) > 0) {
					os.write(buf, 0, readSize);
				}
			}

			debug("解凍済: " + filename + " -> " + extractFile);
		}
	}

	// debug purpose only
	private static void debug(String msg) {
		Runtime r = Runtime.getRuntime();
		long base = 1024 * 1024;
		long total = r.totalMemory() / base;
		long free = r.freeMemory() / base;
		long used = total - free;
		String ts = new SimpleDateFormat("HH:mm:ss.SSS").format(new Date());
		System.out.printf("%s:(%3d/%3d[MB]): %s\n", ts, used, total, msg);
	}

}

mavenを使う前提であり、pom.xmlでzip4jライブラリを指定します。

<project ...
	<dependencies>
...
		<!-- for zip4j -->
		<dependency>
			<groupId>net.lingala.zip4j</groupId>
			<artifactId>zip4j</artifactId>
			<version>2.2.3</version>
		</dependency>
...
</project>

実行結果の例

※パスが長すぎるので一部を”…”で省略しています。

deleting: C:\...\outdata
12:41:32.832:( 11/123[MB]): 解凍開始
12:41:32.841:( 11/123[MB]): 解凍済: testdir/dir1/dir1-1/dir1-1_file1.txt -> C:\...\outdata\extracted_zipcrypto\testdir\dir1\dir1-1\dir1-1_file1.txt
12:41:32.842:( 11/123[MB]): 解凍済: testdir/dir1/dir1-1/dir1-1_file2.txt -> C:\...\outdata\extracted_zipcrypto\testdir\dir1\dir1-1\dir1-1_file2.txt
12:41:32.843:( 11/123[MB]): 解凍済: testdir/dir1/dir1_file1.txt -> C:\...\outdata\extracted_zipcrypto\testdir\dir1\dir1_file1.txt
12:41:32.844:( 11/123[MB]): 解凍済: testdir/dir1/dir1_file2.txt -> C:\...\outdata\extracted_zipcrypto\testdir\dir1\dir1_file2.txt
12:41:32.846:( 11/123[MB]): 解凍済: testdir/file1.xlsx -> C:\...\outdata\extracted_zipcrypto\testdir\file1.xlsx
12:41:32.861:( 12/123[MB]): 圧縮開始
12:41:32.866:( 12/123[MB]): 圧縮済: testdir\dir1\dir1-1\dir1-1_file1.txt <- C:\...\indata\testdir\dir1\dir1-1\dir1-1_file1.txt
12:41:32.867:( 12/123[MB]): 圧縮済: testdir\dir1\dir1-1\dir1-1_file2.txt <- C:\...\indata\testdir\dir1\dir1-1\dir1-1_file2.txt
12:41:32.868:( 12/123[MB]): 圧縮済: testdir\dir1\dir1_file1.txt <- C:\...\indata\testdir\dir1\dir1_file1.txt
12:41:32.868:( 12/123[MB]): 圧縮済: testdir\dir1\dir1_file2.txt <- C:\...\indata\testdir\dir1\dir1_file2.txt
12:41:32.869:( 12/123[MB]): 圧縮済: testdir\file1.xlsx <- C:\...\indata\testdir\file1.xlsx
12:41:32.871:( 12/123[MB]): 解凍開始
12:41:32.873:( 12/123[MB]): 解凍済: testdir/dir1/dir1-1/dir1-1_file1.txt -> C:\...\outdata\extracted_zip\testdir\dir1\dir1-1\dir1-1_file1.txt
12:41:32.874:( 12/123[MB]): 解凍済: testdir/dir1/dir1-1/dir1-1_file2.txt -> C:\...\outdata\extracted_zip\testdir\dir1\dir1-1\dir1-1_file2.txt
12:41:32.876:( 12/123[MB]): 解凍済: testdir/dir1/dir1_file1.txt -> C:\...\outdata\extracted_zip\testdir\dir1\dir1_file1.txt
12:41:32.877:( 12/123[MB]): 解凍済: testdir/dir1/dir1_file2.txt -> C:\...\outdata\extracted_zip\testdir\dir1\dir1_file2.txt
12:41:32.879:( 12/123[MB]): 解凍済: testdir/file1.xlsx -> C:\...\outdata\extracted_zip\testdir\file1.xlsx
12:41:32.880:( 12/123[MB]): 解凍開始
12:41:32.980:( 13/123[MB]): 解凍済: testdir/dir1/dir1-1/dir1-1_file1.txt -> C:\...\outdata\extracted_aes256\testdir\dir1\dir1-1\dir1-1_file1.txt
12:41:32.990:( 14/123[MB]): 解凍済: testdir/dir1/dir1-1/dir1-1_file2.txt -> C:\...\outdata\extracted_aes256\testdir\dir1\dir1-1\dir1-1_file2.txt
12:41:33.005:( 14/123[MB]): 解凍済: testdir/dir1/dir1_file1.txt -> C:\...\outdata\extracted_aes256\testdir\dir1\dir1_file1.txt
12:41:33.021:( 14/123[MB]): 解凍済: testdir/dir1/dir1_file2.txt -> C:\...\outdata\extracted_aes256\testdir\dir1\dir1_file2.txt
12:41:33.042:( 14/123[MB]): 解凍済: testdir/file1.xlsx -> C:\...\outdata\extracted_aes256\testdir\file1.xlsx
12:41:33.044:( 14/123[MB]): 圧縮開始
12:41:33.056:( 14/123[MB]): 圧縮済: testdir\dir1\dir1-1\dir1-1_file1.txt <- C:\...\indata\testdir\dir1\dir1-1\dir1-1_file1.txt
12:41:33.062:( 14/123[MB]): 圧縮済: testdir\dir1\dir1-1\dir1-1_file2.txt <- C:\...\indata\testdir\dir1\dir1-1\dir1-1_file2.txt
12:41:33.070:( 14/123[MB]): 圧縮済: testdir\dir1\dir1_file1.txt <- C:\...\indata\testdir\dir1\dir1_file1.txt
12:41:33.079:( 15/123[MB]): 圧縮済: testdir\dir1\dir1_file2.txt <- C:\...\indata\testdir\dir1\dir1_file2.txt
12:41:33.086:( 15/123[MB]): 圧縮済: testdir\file1.xlsx <- C:\...\indata\testdir\file1.xlsx
12:41:33.088:( 15/123[MB]): 圧縮開始
12:41:33.091:( 15/123[MB]): 圧縮済: testdir\dir1\dir1-1\dir1-1_file1.txt <- C:\...\indata\testdir\dir1\dir1-1\dir1-1_file1.txt
12:41:33.093:( 15/123[MB]): 圧縮済: testdir\dir1\dir1-1\dir1-1_file2.txt <- C:\...\indata\testdir\dir1\dir1-1\dir1-1_file2.txt
12:41:33.093:( 15/123[MB]): 圧縮済: testdir\dir1\dir1_file1.txt <- C:\...\indata\testdir\dir1\dir1_file1.txt
12:41:33.094:( 15/123[MB]): 圧縮済: testdir\dir1\dir1_file2.txt <- C:\...\indata\testdir\dir1\dir1_file2.txt
12:41:33.097:( 15/123[MB]): 圧縮済: testdir\file1.xlsx <- C:\...\indata\testdir\file1.xlsx

補足

Windows、7-zip、zip4jの圧縮・解凍の互換性を確認しました。
確認で使用したのは、Windows10、zip4jは2.2.3、7-zipは19.00(x64)です。

変換元 変換先 成否 備考
Windows圧縮 zip4j解凍 OK
7-zip圧縮 OK
7-zip圧縮(ZipCrypto) OK
7-zip圧縮(AES-256) OK
zip4j圧縮 Windows解凍 OK
7-zip解凍 OK
zip4j解凍 OK ディレクトリのエントリが含まれていないようで、ファイル作成時に親ディレクトリの存在確認(205~208行目)が必要
zip4j圧縮(ZipCrypto) Windows解凍 OK
7-zip解凍 OK
zip4j解凍 OK ディレクトリのエントリが含まれていないようで、ファイル作成時に親ディレクトリの存在確認(205~208行目)が必要
zip4j圧縮(AES-256) Windows解凍 NG Windows圧縮/解凍はAES未対応(こちらの記事を参考のこと)
7-zip解凍 OK
zip4j解凍 OK ディレクトリのエントリが含まれていないようで、ファイル作成時に親ディレクトリの存在確認(205~208行目)が必要

日本語を含んだファイルの圧縮/解凍の正常性も確認しました。

変換元 変換先 成否 備考
Windows圧縮 zip4j解凍 NG ファイル名が文字化けし、ファイルを作成できない場合がある。
7-zip圧縮
zip4j圧縮 Windows解凍 OK
7-zip解凍 OK
zip4j解凍 OK


(adsbygoogle = window.adsbygoogle || []).push({});


(adsbygoogle = window.adsbygoogle || []).push({});

-1. システムエンジニアリング, Java, 実装技術

執筆者:

関連記事

vbaでのエンコード/デコードのサンプル

Excel(vba)で、MD5/SHA-1/SHA-2(SHA-256)の出力、Hex/Base64エンコード/デコードを調べたので備忘録として残します。 動作検証した環境は、Windows10+Of …

リモートからのwarデプロイの自動化

JavaEEベースのツールを公開しているが、デプロイの都度、warファイルをサーバにコピーしてwildflyにデプロイするのが面倒なので、mavenで自動化しました。 前提 mavenのプラグインと後 …

開発環境のJBoss EAP7にリモートアクセス

開発中のものを他者に見せたり、問題が発生している開発者の開発物を参照するために、eclipse上で起動しているEAP7のWebアプリに別のPCからアクセスしたい場合があります。 このための手順を記載し …

JavaでのZIP暗号化の考察

法務系業務を行うシステムを設計するにあたり、次のような要件がありました。 CSVファイルの暗号化方式として、当初からパスワード付きZIPファイルの使用を検討していたため、ZIP圧縮を使用する前提で調査 …

Hyper-VでリモートのISOイメージをマウント

皆で使用するCD/DVDはISOイメージファイルとして、ファイルサーバ上の共有フォルダに配置する運用を想定しています。Hyper-V上の仮想マシンのCD/DVDドライブに、これらのISOイメージファイ …

プロフィール ゆっきーです。
都内でシステムエンジニアをやっています。
もっと詳細を見る