先日、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 |