--- old/src/java.base/share/classes/java/util/zip/DeflaterOutputStream.java 2015-11-26 09:40:26.902271622 +0900 +++ new/src/java.base/share/classes/java/util/zip/DeflaterOutputStream.java 2015-11-26 09:40:26.740938833 +0900 @@ -58,6 +58,8 @@ private final boolean syncFlush; + private ZipCryption zipCryption; + /** * Creates a new output stream with the specified compressor, * buffer size and flush mode. @@ -250,6 +252,11 @@ protected void deflate() throws IOException { int len = def.deflate(buf, 0, buf.length); if (len > 0) { + + if (zipCryption != null) { + zipCryption.encryptBytes(buf, 0, len); + } + out.write(buf, 0, len); } } @@ -274,6 +281,11 @@ int len = 0; while ((len = def.deflate(buf, 0, buf.length, Deflater.SYNC_FLUSH)) > 0) { + + if (zipCryption != null) { + zipCryption.encryptBytes(buf, 0, len); + } + out.write(buf, 0, len); if (len < buf.length) break; @@ -281,4 +293,13 @@ } out.flush(); } + + /** + * Set ZIP encryption/decryption engine to this deflater. + * @param zipCryption ZIP encrypt/decrypt engine. + */ + public void setZipCryption(ZipCryption zipCryption) { + this.zipCryption = zipCryption; + } + } --- old/src/java.base/share/classes/java/util/zip/InflaterInputStream.java 2015-11-26 09:40:27.470603039 +0900 +++ new/src/java.base/share/classes/java/util/zip/InflaterInputStream.java 2015-11-26 09:40:27.314770231 +0900 @@ -51,6 +51,11 @@ protected byte[] buf; /** + * Original input buffer. + */ + protected byte[] originBuf; + + /** * Length of input buffer. */ protected int len; @@ -59,6 +64,8 @@ // this flag is set to true after EOF has reached private boolean reachEOF = false; + private ZipCryption zipCryption; + /** * Check to make sure that this stream has not been closed */ @@ -86,6 +93,7 @@ } this.inf = inf; buf = new byte[size]; + originBuf = new byte[size]; } /** @@ -239,6 +247,12 @@ if (len == -1) { throw new EOFException("Unexpected end of ZLIB input stream"); } + + if (zipCryption != null) { + System.arraycopy(buf, 0, originBuf, 0, len); + zipCryption.decryptBytes(buf, 0, len); + } + inf.setInput(buf, 0, len); } @@ -285,4 +299,12 @@ public synchronized void reset() throws IOException { throw new IOException("mark/reset not supported"); } + + /** + * Set ZIP encryption/decryption engine to this inflater. + * @param zipCryption ZIP encrypt/decrypt engine. + */ + public void setZipCryption(ZipCryption zipCryption) { + this.zipCryption = zipCryption; + } } --- old/src/java.base/share/classes/java/util/zip/ZipFile.java 2015-11-26 09:40:28.009601221 +0900 +++ new/src/java.base/share/classes/java/util/zip/ZipFile.java 2015-11-26 09:40:27.851018423 +0900 @@ -344,9 +344,38 @@ * @throws IllegalStateException if the zip file has been closed */ public InputStream getInputStream(ZipEntry entry) throws IOException { + return getInputStream(entry, null); + } + + /** + * Returns an input stream for reading the contents of the specified + * zip file entry. + * + *

Closing this ZIP file will, in turn, close all input + * streams that have been returned by invocations of this method. + * + * @param entry the zip file entry + * @param zipCryption instance of ZipCryption + * @return the input stream for reading the contents of the specified + * zip file entry. + * @throws ZipException if a ZIP format error has occurred + * @throws IOException if an I/O error has occurred + * @throws IllegalStateException if the zip file has been closed + */ + public InputStream getInputStream(ZipEntry entry, ZipCryption zipCryption) + throws IOException { if (entry == null) { throw new NullPointerException("entry"); } + + if ((entry.flag & 1) == 1) { + if (zipCryption == null) { + throw new ZipException("Passphrase is required."); + } else { + zipCryption.reset(); + } + } + long jzentry = 0; ZipFileInputStream in = null; synchronized (this) { @@ -359,10 +388,22 @@ if (jzentry == 0) { return null; } - in = new ZipFileInputStream(jzentry); + in = new ZipFileInputStream(jzentry, zipCryption); switch (getEntryMethod(jzentry)) { case STORED: + + if ((entry.flag & 1) == 1) { + byte[] encryptionHeader = + new byte[zipCryption.getEncryptionHeaderSize()]; + in.readRaw(encryptionHeader, 0, encryptionHeader.length); + zipCryption.decryptBytes(encryptionHeader); + + if (!zipCryption.isValid(entry, encryptionHeader)) { + throw new ZipException("possibly incorrect passphrase"); + } + } + synchronized (streams) { streams.put(in, null); } @@ -373,6 +414,18 @@ if (size > 65536) size = 8192; if (size <= 0) size = 4096; Inflater inf = getInflater(); + + if ((entry.flag & 1) == 1) { + byte[] encryptionHeader = + new byte[zipCryption.getEncryptionHeaderSize()]; + in.readRaw(encryptionHeader, 0, encryptionHeader.length); + zipCryption.decryptBytes(encryptionHeader); + + if (!zipCryption.isValid(entry, encryptionHeader)) { + throw new ZipException("possibly incorrect passphrase"); + } + } + InputStream is = new ZipFileInflaterInputStream(in, inf, (int)size); synchronized (streams) { @@ -696,15 +749,27 @@ private long pos; // current position within entry data protected long rem; // number of remaining bytes within entry protected long size; // uncompressed size of this entry + private ZipCryption zipCryption; // ZIP encrypt/decrypt engine - ZipFileInputStream(long jzentry) { + ZipFileInputStream(long jzentry, ZipCryption zipCryption) { pos = 0; rem = getEntryCSize(jzentry); size = getEntrySize(jzentry); this.jzentry = jzentry; + this.zipCryption = zipCryption; } public int read(byte b[], int off, int len) throws IOException { + len = readRaw(b, off, len); + + if (zipCryption != null) { + zipCryption.decryptBytes(b, off, len); + } + + return len; + } + + public int readRaw(byte b[], int off, int len) throws IOException { synchronized (ZipFile.this) { long rem = this.rem; long pos = this.pos; --- old/src/java.base/share/classes/java/util/zip/ZipInputStream.java 2015-11-26 09:40:28.569682666 +0900 +++ new/src/java.base/share/classes/java/util/zip/ZipInputStream.java 2015-11-26 09:40:28.411099867 +0900 @@ -59,6 +59,8 @@ private ZipCoder zc; + private ZipCryption zipCryption; + /** * Check to make sure that this stream has not been closed */ @@ -113,13 +115,25 @@ * @exception IOException if an I/O error has occurred */ public ZipEntry getNextEntry() throws IOException { + return getNextEntry(null); + } + + /** + * Reads the next ZIP file entry and positions the stream at the + * beginning of the entry data. + * @param zipCryption instance of ZipCryption + * @return the next ZIP file entry, or null if there are no more entries + * @exception ZipException if a ZIP file error has occurred + * @exception IOException if an I/O error has occurred + */ + public ZipEntry getNextEntry(ZipCryption zipCryption) throws IOException { ensureOpen(); if (entry != null) { closeEntry(); } crc.reset(); inf.reset(); - if ((entry = readLOC()) == null) { + if ((entry = readLOC(zipCryption)) == null) { return null; } if (entry.method == STORED) { @@ -213,6 +227,9 @@ if (len == -1) { throw new ZipException("unexpected EOF"); } + if (zipCryption != null) { + zipCryption.decryptBytes(b, off, len); + } crc.update(b, off, len); remaining -= len; if (remaining == 0 && entry.crc != crc.getValue()) { @@ -273,7 +290,9 @@ /* * Reads local file (LOC) header for next entry. */ - private ZipEntry readLOC() throws IOException { + private ZipEntry readLOC(ZipCryption zipCryption) throws IOException { + this.zipCryption = zipCryption; + try { readFully(tmpbuf, 0, LOCHDR); } catch (EOFException e) { @@ -282,7 +301,7 @@ if (get32(tmpbuf, 0) != LOCSIG) { return null; } - // get flag first, we need check EFS. + // get flag first, we need check EFS and encryption. flag = get16(tmpbuf, LOCFLG); // get the entry name and create the ZipEntry first int len = get16(tmpbuf, LOCNAM); @@ -298,9 +317,10 @@ ZipEntry e = createZipEntry(((flag & EFS) != 0) ? zc.toStringUTF8(b, len) : zc.toString(b, len)); + e.flag = flag; // now get the remaining fields for the entry - if ((flag & 1) == 1) { - throw new ZipException("encrypted ZIP entry not supported"); + if (((flag & 1) == 1) && (zipCryption == null)) { + throw new ZipException("ZipCryption is required."); } e.method = get16(tmpbuf, LOCHOW); e.xdostime = get32(tmpbuf, LOCTIM); @@ -322,6 +342,22 @@ e.setExtra0(extra, e.csize == ZIP64_MAGICVAL || e.size == ZIP64_MAGICVAL); } + + if (zipCryption != null) { + zipCryption.reset(); + super.setZipCryption(zipCryption); + + byte[] encryptionHeader = + new byte[zipCryption.getEncryptionHeaderSize()]; + readFully(encryptionHeader, 0, encryptionHeader.length); + zipCryption.decryptBytes(encryptionHeader); + + if (!zipCryption.isValid(e, encryptionHeader)) { + throw new ZipException("possibly incorrect passphrase"); + } + + } + return e; } @@ -355,7 +391,8 @@ private void readEnd(ZipEntry e) throws IOException { int n = inf.getRemaining(); if (n > 0) { - ((PushbackInputStream)in).unread(buf, len - n, n); + ((PushbackInputStream)in).unread( + (zipCryption == null) ? buf : originBuf, len - n, n); } if ((flag & 8) == 8) { /* "Data Descriptor" present */ @@ -396,6 +433,9 @@ "invalid entry size (expected " + e.size + " but got " + inf.getBytesWritten() + " bytes)"); } + if (zipCryption != null) { + e.csize -= zipCryption.getEncryptionHeaderSize(); + } if (e.csize != inf.getBytesRead()) { throw new ZipException( "invalid entry compressed size (expected " + e.csize + --- old/src/java.base/share/classes/java/util/zip/ZipOutputStream.java 2015-11-26 09:40:29.109597512 +0900 +++ new/src/java.base/share/classes/java/util/zip/ZipOutputStream.java 2015-11-26 09:40:28.951014713 +0900 @@ -81,12 +81,34 @@ private final ZipCoder zc; - private static int version(ZipEntry e) throws ZipException { + private ZipCryption zipCryption; + + private int version(ZipEntry e) throws ZipException { + int result; + switch (e.method) { - case DEFLATED: return 20; - case STORED: return 10; - default: throw new ZipException("unsupported compression method"); + case DEFLATED: + result = 20; + break; + + case STORED: + result = 10; + break; + + default: + throw new ZipException("unsupported compression method"); + } + + /* + * Zip Crypto is defined version 2.0 or later. + * 4.4.3.2 Current minimum feature versions + * https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT + */ + if (zipCryption != null) { + result = 20; } + + return result; } /** @@ -116,7 +138,20 @@ * @param out the actual output stream */ public ZipOutputStream(OutputStream out) { - this(out, StandardCharsets.UTF_8); + this(out, StandardCharsets.UTF_8, null); + } + + /** + * Creates a new ZIP output stream. + * + *

The UTF-8 {@link java.nio.charset.Charset charset} is used + * to encode the entry names and comments. + * + * @param out the actual output stream + * @param zipCryption ZIP encrypt/decrypt engine + */ + public ZipOutputStream(OutputStream out, ZipCryption zipCryption) { + this(out, StandardCharsets.UTF_8, zipCryption); } /** @@ -130,11 +165,28 @@ * @since 1.7 */ public ZipOutputStream(OutputStream out, Charset charset) { + this(out, charset, null); + } + + /** + * Creates a new ZIP output stream. + * + * @param out the actual output stream + * + * @param charset the {@linkplain java.nio.charset.Charset charset} + * to be used to encode the entry names and comments + * + * @param zipCryption ZIP encrypt/decrypt engine + */ + public ZipOutputStream(OutputStream out, + Charset charset, ZipCryption zipCryption) { super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true)); if (charset == null) throw new NullPointerException("charset is null"); this.zc = ZipCoder.get(charset); usesDefaultDeflater = true; + this.zipCryption = zipCryption; + super.setZipCryption(zipCryption); } /** @@ -191,6 +243,9 @@ if (current != null) { closeEntry(); // close previous entry } + if (zipCryption != null) { + zipCryption.reset(); + } if (e.xdostime == -1) { // by default, do NOT use extended timestamps in extra // data, for now. @@ -224,6 +279,9 @@ throw new ZipException( "STORED entry missing size, compressed size, or crc-32"); } + if (zipCryption != null) { + e.csize += zipCryption.getEncryptionHeaderSize(); + } break; default: throw new ZipException("unsupported compression method"); @@ -233,9 +291,17 @@ } if (zc.isUTF8()) e.flag |= EFS; + if (zipCryption != null) + e.flag |= 1; // Bit 0: If set, indicates that the file is encrypted. current = new XEntry(e, written); xentries.add(current); writeLOC(current); + + if (zipCryption != null) { + byte[] encryptionHeader = zipCryption.getEncryptionHeader(e); + writeBytes(encryptionHeader, 0, encryptionHeader.length); + locoff += encryptionHeader.length; + } } /** @@ -280,6 +346,15 @@ } def.reset(); written += e.csize; + + if (zipCryption != null) { + /* Substruct sizeof encryption header. + * This value adds in writeBytes() when encryption header + * is written. + */ + written -= zipCryption.getEncryptionHeaderSize(); + } + break; case STORED: // we already know that both e.size and e.csize are the same @@ -329,6 +404,7 @@ switch (entry.method) { case DEFLATED: super.write(b, off, len); + crc.update(b, off, len); break; case STORED: written += len; @@ -336,12 +412,18 @@ throw new ZipException( "attempt to write past end of STORED entry"); } + + crc.update(b, off, len); + + if (zipCryption != null) { + zipCryption.encryptBytes(b, off, len); + } + out.write(b, off, len); break; default: throw new ZipException("invalid compression method"); } - crc.update(b, off, len); } /** @@ -467,6 +549,11 @@ private void writeEXT(ZipEntry e) throws IOException { writeInt(EXTSIG); // EXT header signature writeInt(e.crc); // crc-32 + + if (zipCryption != null) { + e.csize += zipCryption.getEncryptionHeaderSize(); + } + if (e.csize >= ZIP64_MAGICVAL || e.size >= ZIP64_MAGICVAL) { writeLong(e.csize); writeLong(e.size); --- old/src/java.base/share/native/libzip/ZipFile.c 2015-11-26 09:40:29.714595471 +0900 +++ new/src/java.base/share/native/libzip/ZipFile.c 2015-11-26 09:40:29.502846186 +0900 @@ -211,7 +211,7 @@ Java_java_util_zip_ZipFile_getEntryMethod(JNIEnv *env, jclass cls, jlong zentry) { jzentry *ze = jlong_to_ptr(zentry); - return ze->csize != 0 ? DEFLATED : STORED; + return ze->method; } JNIEXPORT jint JNICALL --- old/src/java.base/share/native/libzip/zip_util.c 2015-11-26 09:40:30.403926480 +0900 +++ new/src/java.base/share/native/libzip/zip_util.c 2015-11-26 09:40:30.223343756 +0900 @@ -676,8 +676,6 @@ if (!CENSIG_AT(cp)) ZIP_FORMAT_ERROR("invalid CEN header (bad signature)"); - if (CENFLG(cp) & 1) - ZIP_FORMAT_ERROR("invalid CEN header (encrypted entry)"); if (method != STORED && method != DEFLATED) ZIP_FORMAT_ERROR("invalid CEN header (bad compression method)"); if (cp + CENHDR + nlen > cenend) @@ -998,9 +996,10 @@ nlen = CENNAM(cen); elen = CENEXT(cen); clen = CENCOM(cen); + ze->method = CENHOW(cen); ze->time = CENTIM(cen); ze->size = CENLEN(cen); - ze->csize = (CENHOW(cen) == STORED) ? 0 : CENSIZ(cen); + ze->csize = CENSIZ(cen); ze->crc = CENCRC(cen); locoff = CENOFF(cen); ze->pos = -(zip->locpos + locoff); --- old/src/java.base/share/native/libzip/zip_util.h 2015-11-26 09:40:31.386630438 +0900 +++ new/src/java.base/share/native/libzip/zip_util.h 2015-11-26 09:40:31.222547658 +0900 @@ -160,6 +160,7 @@ typedef struct jzentry { /* Zip file entry */ char *name; /* entry name */ + jint method; /* compression method */ jlong time; /* modification time */ jlong size; /* size of uncompressed data */ jlong csize; /* size of compressed data (zero if uncompressed) */ --- /dev/null 2015-11-26 09:39:11.281708300 +0900 +++ new/src/java.base/share/classes/java/util/zip/TraditionalZipCryption.java 2015-11-26 09:40:31.789045748 +0900 @@ -0,0 +1,212 @@ +package java.util.zip; + +import java.util.Random; + + +/** + * This class implements a Traditional Zip Encryption / Decryption + * engine according to the ZIP file format specification to encrypt + * / decrypt a data after it has been compressed. + */ +public class TraditionalZipCryption implements ZipCryption { + + private static long[] crc32Table; + + private long[] keys; + + private String password; + + /** + * Encryption header size + */ + public static int ENCRYPTION_HEADER_SIZE = 12; + + private static long[] INITIAL_KEY = {305419896L, 591751049L, 878082192L}; + + private static int CRC_TABLE_SIZE = 256; + + static { + crc32Table = new long[CRC_TABLE_SIZE]; + + /* + * Calculate CRC-32 table + * make_crc_table() + * https://tools.ietf.org/html/rfc1952#section-8 + */ + for (int n = 0; n < CRC_TABLE_SIZE; n++) { + long c = n; + + for (int k = 0; k < 8; k++) { + c = ((c & 1) == 1) ? 0xedb88320L ^ (c >>> 1) : (c >>> 1); + c &= 0xffffffffL; + } + + crc32Table[n] = c; + } + + } + + /** + * Constructor of TraditionalZipCryption. + * @param password ZIP password + */ + public TraditionalZipCryption(String password) { + this.password = password; + this.keys = new long[ENCRYPTION_HEADER_SIZE]; + reset(); + } + + /** + * update_crc() + * https://tools.ietf.org/html/rfc1952#section-8 + */ + private long crc32(long crc, int buf) { + return + (crc32Table[(int)(crc ^ buf) & 0xff] ^ (crc >>> 8)) & 0xffffffffL; + } + + /** + * update_keys() + * 6.1.5 Initializing the encryption keys + * https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT + */ + private void updateKeys(int c){ + keys[0] = crc32(keys[0], c); + keys[1] += keys[0] & 0xffL; + keys[1] = ((keys[1] * 134775813L) + 1L) & 0xffffffffL; + keys[2] = crc32(keys[2], (int)(keys[1] >>> 24)); + } + + /** + * decrypt_byte() + * 6.1.6 Decrypting the encryption header + * https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT + * + * @return Decrypt byte + */ + private int decryptByte() { + int temp = (int)(keys[2] & 0xffffL) | 2; + return ((temp * (temp ^ 1)) >>> 8) & 0xff; + } + + private int encode(int unsignedByteData) { + int decByte = decryptByte(); + updateKeys(unsignedByteData); + return decByte ^ unsignedByteData; + } + + private int decode(int unsignedByteData) { + int decByte = (decryptByte() ^ unsignedByteData) & 0xff; + updateKeys(decByte); + return decByte; + } + + /** + * Calculate encription header + * + * @param e ZIP entry + * @return ZIP encryption header + */ + @Override + public byte[] getEncryptionHeader(ZipEntry e) { + /* + * 6.1.6 Decrypting the encryption header + * + * After the header is decrypted, the last 1 or 2 bytes in Buffer + * should be the high-order word/byte of the CRC for the file being + * decrypted, stored in Intel low-byte/high-byte order. Versions of + * PKZIP prior to 2.0 used a 2 byte CRC check; a 1 byte CRC check is + * used on versions after 2.0. This can be used to test if the password + * supplied is correct or not. + */ + Random rand = new Random(); + byte[] encryptionHeader = new byte[ENCRYPTION_HEADER_SIZE]; + rand.nextBytes(encryptionHeader); + + /* This code comes from testkey() at crypt.c in unzip 6.0 */ + encryptionHeader[ENCRYPTION_HEADER_SIZE - 1] = + (e.crc == -1) ? (byte)((e.xdostime >>> 8) & 0xffL) + : (byte)((e.crc >>> 24) & 0xffL); + encryptBytes(encryptionHeader); + return encryptionHeader; + } + + /** + * {@inheritDoc} + */ + @Override + public byte[] encryptBytes(byte[] data, int offset, int length) { + + for (int idx = offset; idx < length; idx++) { + data[idx] = (byte)(encode(Byte.toUnsignedInt(data[idx])) & 0xff); + } + + return data; + } + + /** + * {@inheritDoc} + */ + @Override + public byte[] encryptBytes(byte[] data) { + return encryptBytes(data, 0, data.length); + } + + /** + * {@inheritDoc} + */ + @Override + public void reset() { + keys[0] = INITIAL_KEY[0]; + keys[1] = INITIAL_KEY[1]; + keys[2] = INITIAL_KEY[2]; + + for (byte b : password.getBytes()) { + updateKeys(Byte.toUnsignedInt(b)); + } + + } + + /** + * {@inheritDoc} + */ + @Override + public int getEncryptionHeaderSize() { + return ENCRYPTION_HEADER_SIZE; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isValid(ZipEntry e, byte[] encryptionHeader) { + /* This code comes from testkey() at crypt.c in unzip 6.0 */ + byte checkDigit = encryptionHeader[ENCRYPTION_HEADER_SIZE - 1]; + + return (e.flag & 8) == 8 + ? (checkDigit == (byte)((e.xdostime >>> 8) & 0xffL)) + : (checkDigit == (byte)((e.crc >>> 24) & 0xffL)); + } + + /** + * {@inheritDoc} + */ + @Override + public byte[] decryptBytes(byte[] data, int offset, int length) { + + for (int idx = offset; idx < length; idx++) { + data[idx] = (byte)(decode(Byte.toUnsignedInt(data[idx])) & 0xff); + } + + return data; + } + + /** + * {@inheritDoc} + */ + @Override + public byte[] decryptBytes(byte[] data) { + return decryptBytes(data, 0, data.length); + } + +} --- /dev/null 2015-11-26 09:39:11.281708300 +0900 +++ new/src/java.base/share/classes/java/util/zip/ZipCryption.java 2015-11-26 09:40:32.376627100 +0900 @@ -0,0 +1,69 @@ +package java.util.zip; + + +/** + * Interface for ZIP encrypt/decrypt engine. + */ +public interface ZipCryption { + + /** + * Calculate encription header + * + * @param e ZIP entry + * @return ZIP encryption header + */ + public byte[] getEncryptionHeader(ZipEntry e); + + /** + * Encrypt byte array. + * @param data Byte array to encrypt + * @param offset offset to start + * @param length array length + * @return Encrypted array. This instance is same to argument. + */ + public byte[] encryptBytes(byte[] data, int offset, int length); + + /** + * Encrypt byte array. + * @param data Byte array to encrypt + * @return Encrypted array. This instance is same to argument. + */ + public byte[] encryptBytes(byte[] data); + + /** + * Reset encryption engine. + */ + public void reset(); + + /** + * Get ZIP encryption header. + * @return sizeof encryption header. + */ + public int getEncryptionHeaderSize(); + + /** + * Check encryption header + * + * @param e ZipEntry + * @param encryptionHeader encryption header to be check + * @return true if this encryption header is valid. + */ + public boolean isValid(ZipEntry e, byte[] encryptionHeader); + + /** + * Decrypt byte array. + * @param data Byte array to decrypt + * @param offset offset to start + * @param length array length + * @return Decrypted array. This instance is same to argument. + */ + public byte[] decryptBytes(byte[] data, int offset, int length); + + /** + * Decrypt byte array. + * @param data Byte array to decrypt + * @return Decrypted array. This instance is same to argument. + */ + public byte[] decryptBytes(byte[] data); + +}