--- old/src/java.base/share/classes/java/util/zip/DeflaterOutputStream.java 2016-02-10 23:24:06.292791930 +0900 +++ new/src/java.base/share/classes/java/util/zip/DeflaterOutputStream.java 2016-02-10 23:24:06.046792657 +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,16 @@ } out.flush(); } + + /** + * Set ZIP encryption/decryption engine to this deflater. + * + * @param zipCryption ZIP encrypt/decrypt engine. zip encryption will not + * work if this value set to null. + * @since 1.9 + */ + void setZipCryption(ZipCryption zipCryption) { + this.zipCryption = zipCryption; + } + } --- old/src/java.base/share/classes/java/util/zip/InflaterInputStream.java 2016-02-10 23:24:06.960789956 +0900 +++ new/src/java.base/share/classes/java/util/zip/InflaterInputStream.java 2016-02-10 23:24:06.716790677 +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,15 @@ 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. zip decryption will not + * work if this value set to null. + * @since 1.9 + */ + void setZipCryption(ZipCryption zipCryption) { + this.zipCryption = zipCryption; + } } --- old/src/java.base/share/classes/java/util/zip/ZipEntry.java 2016-02-10 23:24:07.622787999 +0900 +++ new/src/java.base/share/classes/java/util/zip/ZipEntry.java 2016-02-10 23:24:07.376788726 +0900 @@ -57,6 +57,9 @@ int flag = 0; // general purpose flag byte[] extra; // optional extra field data for entry String comment; // optional comment string for entry + ZipCryption zipCryption = null; // encryption / decryption engine + byte[] encryptionHeader; // ZIP encryption header + boolean passphraseValidated = false; // passphrase validation is not yet. /** * Compression method for uncompressed entries. @@ -131,6 +134,8 @@ flag = e.flag; extra = e.extra; comment = e.comment; + zipCryption = e.zipCryption; + encryptionHeader = e.encryptionHeader; } /** @@ -673,4 +678,51 @@ throw new InternalError(e); } } + + /** + * Set passphrase for ZIP encryption and decryption. + * @param passphrase Passphrase for encryption and decryption. + */ + public void setPassphrase(String passphrase) { + zipCryption = new TraditionalZipCryption(passphrase); + } + + ZipCryption getZipCryption() { + return zipCryption; + } + + /** + * Get the state whether encrypted entry or not. + * @return true if passphrase is required. + */ + public boolean isPassphraseRequired() { + return (flag & 1) == 1; + } + + /** + * Validate passphrase for this entry. + * @return true if passphrase is valid, or this entry is not encrypted. + * @throws ZipException Passphrase is not set or Encryption header is not set. + */ + public boolean isValidPassphrase() throws ZipException{ + if (!isPassphraseRequired()) { + return true; + } + + if (zipCryption == null) { + throw new ZipException("Passphrase is not set."); + } + if (passphraseValidated) { + return true; + } + if (encryptionHeader == null) { + throw new ZipException("Encryption header is not set."); + } + + zipCryption.reset(); + byte[] tmp = encryptionHeader.clone(); + zipCryption.decryptBytes(tmp); + passphraseValidated = zipCryption.isValid(this, tmp); + return passphraseValidated; + } } --- old/src/java.base/share/classes/java/util/zip/ZipFile.java 2016-02-10 23:24:08.294786014 +0900 +++ new/src/java.base/share/classes/java/util/zip/ZipFile.java 2016-02-10 23:24:08.050786735 +0900 @@ -325,6 +325,13 @@ */ public InputStream getInputStream(ZipEntry entry) throws IOException { Objects.requireNonNull(entry, "entry"); + ZipCryption zipCryption = null; + + if (entry.isPassphraseRequired()) { + zipCryption = entry.zipCryption; + Objects.requireNonNull(zipCryption, "Passphrase is required."); + } + int pos = -1; ZipFileInputStream in = null; synchronized (this) { @@ -337,9 +344,19 @@ if (pos == -1) { return null; } - in = new ZipFileInputStream(zsrc.cen, pos); + in = new ZipFileInputStream(zsrc.cen, pos, zipCryption); switch (CENHOW(zsrc.cen, pos)) { case STORED: + if (entry.isPassphraseRequired()) { + entry.encryptionHeader = + new byte[TraditionalZipCryption.ENCRYPTION_HEADER_SIZE]; + in.readRaw(entry.encryptionHeader, 0, + entry.encryptionHeader.length); + if (!entry.isValidPassphrase()) { + throw new ZipException("possibly incorrect passphrase"); + } + } + synchronized (streams) { streams.put(in, null); } @@ -355,6 +372,17 @@ size = 4096; } Inflater inf = getInflater(); + + if (entry.isPassphraseRequired()) { + entry.encryptionHeader = + new byte[TraditionalZipCryption.ENCRYPTION_HEADER_SIZE]; + in.readRaw(entry.encryptionHeader, 0, + entry.encryptionHeader.length); + if (!entry.isValidPassphrase()) { + throw new ZipException("possibly incorrect passphrase"); + } + } + InputStream is = new ZipFileInflaterInputStream(in, inf, (int)size); synchronized (streams) { streams.put(is, inf); @@ -658,8 +686,10 @@ 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(byte[] cen, int cenpos) throws IOException { + ZipFileInputStream(byte[] cen, int cenpos, ZipCryption zipCryption) + throws IOException { rem = CENSIZ(cen, cenpos); size = CENLEN(cen, cenpos); pos = CENOFF(cen, cenpos); @@ -670,6 +700,7 @@ } // negative for lazy initialization, see getDataOffset(); pos = - (pos + ZipFile.this.zsrc.locpos); + this.zipCryption = zipCryption; } private void checkZIP64(byte[] cen, int cenpos) throws IOException { @@ -731,6 +762,16 @@ } 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) { ensureOpenOrZipException(); initDataOffset(); @@ -1180,8 +1221,6 @@ int nlen = CENNAM(cen, pos); int elen = CENEXT(cen, pos); int clen = CENCOM(cen, pos); - if ((CENFLG(cen, pos) & 1) != 0) - zerror("invalid CEN header (encrypted entry)"); if (method != STORED && method != DEFLATED) zerror("invalid CEN header (bad compression method: " + method + ")"); if (pos + CENHDR + nlen > limit) --- old/src/java.base/share/classes/java/util/zip/ZipInputStream.java 2016-02-10 23:24:09.004783915 +0900 +++ new/src/java.base/share/classes/java/util/zip/ZipInputStream.java 2016-02-10 23:24:08.752784660 +0900 @@ -31,6 +31,7 @@ import java.io.PushbackInputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.Objects; import static java.util.zip.ZipConstants64.*; import static java.util.zip.ZipUtils.*; @@ -59,6 +60,8 @@ private ZipCoder zc; + private ZipCryption zipCryption; + /** * Check to make sure that this stream has not been closed */ @@ -189,6 +192,18 @@ if (entry == null) { return -1; } + + if (entry.isPassphraseRequired()) { + if (!entry.isValidPassphrase()) { + throw new ZipException("possibly incorrect passphrase"); + } + + zipCryption = entry.zipCryption; + super.setZipCryption(zipCryption); + } else { + zipCryption = null; + } + switch (entry.method) { case DEFLATED: len = super.read(b, off, len); @@ -213,6 +228,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()) { @@ -282,7 +300,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,10 +316,8 @@ 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"); - } e.method = get16(tmpbuf, LOCHOW); e.xdostime = get32(tmpbuf, LOCTIM); if ((flag & 8) == 8) { @@ -322,6 +338,13 @@ e.setExtra0(extra, e.csize == ZIP64_MAGICVAL || e.size == ZIP64_MAGICVAL); } + + if (e.isPassphraseRequired()) { + e.encryptionHeader = + new byte[TraditionalZipCryption.ENCRYPTION_HEADER_SIZE]; + readFully(e.encryptionHeader, 0, e.encryptionHeader.length); + } + return e; } @@ -355,7 +378,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 +420,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 2016-02-10 23:24:09.707781838 +0900 +++ new/src/java.base/share/classes/java/util/zip/ZipOutputStream.java 2016-02-10 23:24:09.456782580 +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; } /** @@ -167,8 +189,8 @@ } /** - * Sets the compression level for subsequent entries which are DEFLATED. * The default setting is DEFAULT_COMPRESSION. + * Sets the compression level for subsequent entries which are DEFLATED. * @param level the compression level (0-9) * @exception IllegalArgumentException if the compression level is invalid */ @@ -191,6 +213,11 @@ if (current != null) { closeEntry(); // close previous entry } + zipCryption = e.zipCryption; + super.setZipCryption(zipCryption); + if (zipCryption != null) { + zipCryption.reset(); + } if (e.xdostime == -1) { // by default, do NOT use extended timestamps in extra // data, for now. @@ -224,6 +251,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 +263,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 +318,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 +376,7 @@ switch (entry.method) { case DEFLATED: super.write(b, off, len); + crc.update(b, off, len); break; case STORED: written += len; @@ -336,12 +384,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 +521,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/zip_util.c 2016-02-10 23:24:10.594779217 +0900 +++ new/src/java.base/share/native/libzip/zip_util.c 2016-02-10 23:24:10.256780215 +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 2016-02-10 23:24:11.397776844 +0900 +++ new/src/java.base/share/native/libzip/zip_util.h 2016-02-10 23:24:11.118777668 +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 2016-02-10 22:13:15.924572000 +0900 +++ new/src/java.base/share/classes/java/util/zip/TraditionalZipCryption.java 2016-02-10 23:24:11.825775579 +0900 @@ -0,0 +1,219 @@ +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. + * + * @since 1.9 + */ +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() { + + if (password == null) { + return; + } + + 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 2016-02-10 22:13:15.924572000 +0900 +++ new/src/java.base/share/classes/java/util/zip/ZipCryption.java 2016-02-10 23:24:12.711772960 +0900 @@ -0,0 +1,71 @@ +package java.util.zip; + + +/** + * Interface for ZIP encrypt/decrypt engine. + * + * @since 1.9 + */ +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); + +}