On a recent “fun” project, I needed my application to be able to access password-protected zip files of a particular format. It was one of these features I thought will take me no time to implement. Anyway, to my surprise, neither JDK supports password-protected ZIP files, nor I was able to find a suitable Java open source library I could use for that purpose. So, I ended up writing the utility class on my own. I wrote an implementation of java.io.InputStream
that filters the ZIP file data and turns a password-protected ZIP into an unprotected one on the fly – so the stream can be nicely chained with java.util.zip.ZipInputStream
. Although the class is specifically targeted at the particular type of ZIP files I had to deal with (see the limitations below), maybe other people have to deal with the same type of files, or this class can provide a good start for others to turn it into a utility that would work with any type of ZIP (maybe I will do it myself some day – for now I don’t have time).
To implement this class I used the ZIP File Format Specification as the source of information. I also used the 7-zip project (C++) as a reference during the debugging to verify my understanding of the ZIP spec. and the CRC algorithm.
So, here is the class:
import java.io.IOException;
import java.io.InputStream;
public class ZipDecryptInputStream extends InputStream {
private static final int[] CRC_TABLE = new int[256];
// compute the table
// (could also have it pre-computed - see http://snippets.dzone.com/tag/crc32)
static {
for (int i = 0; i < 256; i++) {
int r = i;
for (int j = 0; j < 8; j++) {
if ((r & 1) == 1) {
r = (r >>> 1) ^ 0xedb88320;
} else {
r >>>= 1;
}
}
CRC_TABLE[i] = r;
}
}
private static final int DECRYPT_HEADER_SIZE = 12;
private static final int[] LFH_SIGNATURE = {0x50, 0x4b, 0x03, 0x04};
private final InputStream delegate;
private final String password;
private final int keys[] = new int[3];
private State state = State.SIGNATURE;
private int skipBytes;
private int compressedSize;
private int value;
private int valuePos;
private int valueInc;
public ZipDecryptInputStream(InputStream stream, String password) {
this.delegate = stream;
this.password = password;
}
@Override
public int read() throws IOException {
int result = delegate.read();
if (skipBytes == 0) {
switch (state) {
case SIGNATURE:
if (result != LFH_SIGNATURE[valuePos]) {
state = State.TAIL;
} else {
valuePos++;
if (valuePos >= LFH_SIGNATURE.length) {
skipBytes = 2;
state = State.FLAGS;
}
}
break;
case FLAGS:
if ((result & 1) == 0) {
throw new IllegalStateException("ZIP not password protected.");
}
if ((result & 64) == 64) {
throw new IllegalStateException("Strong encryption used.");
}
if ((result & 8) == 8) {
throw new IllegalStateException("Unsupported ZIP format.");
}
result -= 1;
compressedSize = 0;
valuePos = 0;
valueInc = DECRYPT_HEADER_SIZE;
state = State.COMPRESSED_SIZE;
skipBytes = 11;
break;
case COMPRESSED_SIZE:
compressedSize += result << (8 * valuePos);
result -= valueInc;
if (result < 0) {
valueInc = 1;
result += 256;
} else {
valueInc = 0;
}
valuePos++;
if (valuePos > 3) {
valuePos = 0;
value = 0;
state = State.FN_LENGTH;
skipBytes = 4;
}
break;
case FN_LENGTH:
case EF_LENGTH:
value += result << 8 * valuePos;
if (valuePos == 1) {
valuePos = 0;
if (state == State.FN_LENGTH) {
state = State.EF_LENGTH;
} else {
state = State.HEADER;
skipBytes = value;
}
} else {
valuePos = 1;
}
break;
case HEADER:
initKeys(password);
for (int i = 0; i < DECRYPT_HEADER_SIZE; i++) {
updateKeys((byte) (result ^ decryptByte()));
result = delegate.read();
}
compressedSize -= DECRYPT_HEADER_SIZE;
state = State.DATA;
// intentionally no break
case DATA:
result = (result ^ decryptByte()) & 0xff;
updateKeys((byte) result);
compressedSize--;
if (compressedSize == 0) {
valuePos = 0;
state = State.SIGNATURE;
}
break;
case TAIL:
// do nothing
}
} else {
skipBytes--;
}
return result;
}
@Override
public void close() throws IOException {
delegate.close();
super.close();
}
private void initKeys(String password) {
keys[0] = 305419896;
keys[1] = 591751049;
keys[2] = 878082192;
for (int i = 0; i < password.length(); i++) {
updateKeys((byte) (password.charAt(i) & 0xff));
}
}
private void updateKeys(byte charAt) {
keys[0] = crc32(keys[0], charAt);
keys[1] += keys[0] & 0xff;
keys[1] = keys[1] * 134775813 + 1;
keys[2] = crc32(keys[2], (byte) (keys[1] >> 24));
}
private byte decryptByte() {
int temp = keys[2] | 2;
return (byte) ((temp * (temp ^ 1)) >>> 8);
}
private int crc32(int oldCrc, byte charAt) {
return ((oldCrc >>> 8) ^ CRC_TABLE[(oldCrc ^ charAt) & 0xff]);
}
private static enum State {
SIGNATURE, FLAGS, COMPRESSED_SIZE, FN_LENGTH, EF_LENGTH, HEADER, DATA, TAIL
}
}
These are the limitations:
- Only the “Traditional PKWARE Encryption” is supported (spec. section VII)
- Files that have the “compressed length” information at the end of the data section (rather than at the beginning) are not supported (see “general purpose bit flag”, bit 3 in section V, subsection J in the spec.)
And this is how you can use it in your code:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
// usage: java Main [filename] [password]
public class Main {
public static void main(String[] args) throws IOException {
// password-protected zip file I need to read
FileInputStream fis = new FileInputStream(args[0]);
// wrap it in the decrypt stream
ZipDecryptInputStream zdis = new ZipDecryptInputStream(fis, args[1]);
// wrap the decrypt stream by the ZIP input stream
ZipInputStream zis = new ZipInputStream(zdis);
// read all the zip entries and save them as files
ZipEntry ze;
while ((ze = zis.getNextEntry()) != null) {
FileOutputStream fos = new FileOutputStream(ze.getName());
int b;
while ((b = zis.read()) != -1) {
fos.write(b);
}
fos.close();
zis.closeEntry();
}
zis.close();
}
}