Java で暗号化 ZIP を作成する

f:id:Naotsugu:20200630230424p:plain


はじめに

Java で暗号化ZIP扱うライブラリなんて腐るほどあるだろう。 なんなら標準ライブラリだけで完結できるだろう。

と高を括っていましたが、Commons Compress でも no support for encryption となっており、サポートしているライブラリは思った以上に少ないものでした。

簡単に利用できるライブラリとしては zip4j がほぼ唯一の選択肢になりそうです。


zip4j

現在の最新版は 2.6.1 で、ライセンスは Apache-2.0 License です。

GitHub - srikanth-lingala/zip4j: A Java library for zip files and streams

1.3.3 未満にはパストラバーサルの脆弱性があるようなので利用には注意してください。

機能としては以下としてされています。

  • 作成、追加、抽出、更新、ZIPからのファイル削除
  • ストリームのサポート (ZipInputStream と ZipOutputStream)
  • パスワードで保護されたZipファイルとストリームの読み書き
  • AES と Zip-Standard の暗号化方式サポート
  • ZIP64形式のサポート
  • ストア(圧縮なし)とデフレート圧縮方式
  • 分割されたZIPファイルの作成と抽出
  • ZIP内のUnicodeファイル名とコメントのサポート
  • 進捗モニター


zip4j の利用

Gradle の場合は以下のように依存を追加します。

implementation 'net.lingala.zip4j:zip4j:2.6.1'

Maven の場合は以下のようになります。

<dependency>
    <groupId>net.lingala.zip4j</groupId>
    <artifactId>zip4j</artifactId>
    <version>2.6.1</version>
</dependency>


ZIP ファイルの作成は以下のようにするだけで、非常に簡単です。

ZipFile zip = new ZipFile("example.zip");
// zip.setCharset(Charset.forName("windows-31j"));
zip.addFile("example.txt");
zip.addFolder(new File("folder"));

既存のファイルやフォルダが追加された example.zip が作成されます(Charset 指定もできます)。


Java 標準APIを使った場合は、ZipOutputStreamZipEntry を使って以下のような感じになります。

try (ZipOutputStream zipOut = new ZipOutputStream(
        new FileOutputStream("example.zip"),
        Charset.forName("windows-31J"))) {
    for (String srcFile : Arrays.asList("test1.txt", "test2.txt")) {
        File fileToZip = new File(srcFile);
        try (FileInputStream fis = new FileInputStream(fileToZip)) {
            ZipEntry entry = new ZipEntry(fileToZip.getPath().replace('\\', '/'));
            zipOut.putNextEntry(entry);

            byte[] bytes = new byte[1024];
            int length;
            while((length = fis.read(bytes)) >= 0) {
                zipOut.write(bytes, 0, length);
            }
        }
    }
}

Charset を指定しないと(デフォルトでUTF-8になり) Windows 環境で文字化けするケースがあります。

Zip File System Provider を使って以下のようにすることができます。

Map<String, String> env = new HashMap<>(); 
env.put("create", "true");

// locate file system by using the syntax 
// defined in java.net.JarURLConnection
URI uri = URI.create("jar:file:/codeSamples/zipfs/zipfstest.zip");
        
try (FileSystem zipfs = FileSystems.newFileSystem(uri, env)) {
    Path externalTxtFile = Paths.get("/codeSamples/zipfs/SomeTextFile.txt");
    Path pathInZipfile = zipfs.getPath("/SomeTextFile.txt");          
    // copy a file into the zip file
    Files.copy(externalTxtFile, pathInZipfile, 
            StandardCopyOption.REPLACE_EXISTING); 
} 

上記はいずれも単純なファイル格納ですが、ディレクトリごと圧縮したいケースなどあり意外と面倒です。

zip4j では非常に簡単に ZIP ファイルの作成ができます。


暗号化ZIPファイルの作成

ZipParameters で暗号化指定するだけです。

ZipParameters params = new ZipParameters();
params.setEncryptFiles(true);
params.setEncryptionMethod(EncryptionMethod.ZIP_STANDARD);

List<File> filesToAdd = Arrays.asList(
        new File("test1.txt"),
        new File("test2.txt"));
char[] pass = new char[] {'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};

ZipFile zipFile = new ZipFile("example.zip");
zipFile.setPassword(pass);
zipFile.addFiles(filesToAdd, params);

解凍時にはパスワードが求められます。

f:id:Naotsugu:20200701002728p:plain

パスワードは "password".toCharArray() のように指定することもできますが、文字列は不変であり文字列プールに保存されるため、メモリダンプで容易にパスワードにアクセスできてしまう他、不用意にログ出力されてしまう恐れもあるため、センシティブな内容に対しては文字配列を使うことが良いプラクティスとなっています。


まとめ

以上簡単ですが、zip4j を使った 暗号化 ZIP の作成方法について見てみました。

暗号化に限らず ZIP 操作に便利なライブラリなので覚えておくと良いと思います。