package de.idyl.winzipaes;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.CRC32;
import java.util.zip.DataFormatException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import com.rareventure.util.OutputStreamToInputStreamPipe;
import com.rareventure.util.OutputStreamToInputStreamPipe.PipeClosedException;
import de.idyl.winzipaes.impl.AESDecrypter;
import de.idyl.winzipaes.impl.ByteArrayHelper;
import de.idyl.winzipaes.impl.CentralDirectoryEntry;
import de.idyl.winzipaes.impl.ExtRandomAccessFile;
import de.idyl.winzipaes.impl.ExtZipEntry;
import de.idyl.winzipaes.impl.ExtZipOutputStream;
import de.idyl.winzipaes.impl.ZipConstants;
/**
* List/Extract data from AES encrypted WinZip file (readOnly).
*
* TODO - support 128 + 192 keys
* TODO - refactor this class to use an ExtZipInputStream and put all "offset handling" there
*
* @see http://www.winzip.com/aes_info.htm
*
* @author olaf@merkert.de
* @author jos.v.roosmalen@gmail.com
*/
public class AesZipFileDecrypter implements ZipConstants {
private static final Logger LOG = Logger.getLogger( AesZipFileDecrypter.class.getName() );
// --------------------------------------------------------------------------
/** charset to use for filename(s) - defaults to iso-8859-1 */
public static String charset = "iso-8859-1";
/** size of buffer to use for byte[] operations - defaults to 1024 */
protected static int bufferSize = 1024 * 10;
// --------------------------------------------------------------------------
protected AESDecrypter decrypter;
// --------------------------------------------------------------------------
/** random access file to access the archive data */
protected ExtRandomAccessFile raFile;
/** where does the directory (after file data) start? */
protected long dirOffsetPos;
protected File zipFile;
protected String comment;
private static final int LOOP_COUNT_BEFORE_UPDATE = 10;
public AesZipFileDecrypter( File zipFile, AESDecrypter decrypter ) throws IOException {
this.zipFile = zipFile;
this.decrypter = decrypter;
this.raFile = new ExtRandomAccessFile( zipFile );
initDirOffsetPosAndComment();
}
protected void initDirOffsetPosAndComment() throws IOException {
// zip files without a comment contain the offset/position of the central directory at this fixed position
this.dirOffsetPos = zipFile.length() - 6;
final int dirOffset = raFile.readInt( this.dirOffsetPos - 16 );
if( dirOffset!=ENDSIG ) {
// if a comment is present, search the ENDSIG constant, starting at the end of the zip file
byte[] endsig = ByteArrayHelper.toByteArray((int)ZipConstants.ENDSIG);
long endsigPos = raFile.lastPosOf(endsig);
if( endsigPos==-1 ) {
throw new ZipException("expected ENDSIC not found (marks the beginning of the central directory at end of the zip file)");
} else {
this.dirOffsetPos = endsigPos+16;
short commentLength = raFile.readShort( this.dirOffsetPos + 4 );
this.comment = new String( raFile.readByteArray( this.dirOffsetPos+6, commentLength ) );
}
}
}
public void close() throws IOException {
raFile.close();
}
// --------------------------------------------------------------------------
/**
* return list of entries from zip file - the list contains files as well as non-decryptable (!)
* directories, that can be determined by using the isDirectory() method
*/
public List<ExtZipEntry> getEntryList() throws IOException, ZipException {
List<ExtZipEntry> out = new ArrayList<ExtZipEntry>();
short totalNumberOfEntries = this.getNumberOfEntries();
final int dirOffset = raFile.readInt( this.dirOffsetPos );
long fileOffset = dirOffset;
for( int i=0; i<totalNumberOfEntries; i++ ) {
int censig = raFile.readInt( fileOffset );
if( censig!=CENSIG ) {
throw new ZipException("expected CENSIC not found at entry no " + (i+1) + " in central directory at end of zip file at " + fileOffset);
}
short fileNameLength = raFile.readShort( fileOffset + 28 );
short extraFieldLength = raFile.readShort( fileOffset + 30 );
long fileOffsetPos = fileOffset + 28 + 14;
long fileDataOffset = raFile.readInt( fileOffsetPos );
int locsig = raFile.readInt( fileDataOffset );
if( locsig!=LOCSIG ) {
throw new ZipException("expected LOCSIC not found at alleged position of data for file no " + (i+1));
}
byte[] fileNameBytes = raFile.readByteArray( fileOffsetPos+4, fileNameLength );
long nextFileOffset = raFile.getFilePointer();
String fileName = new String( fileNameBytes, charset );
CentralDirectoryEntry cde = new CentralDirectoryEntry( raFile, fileOffset );
ExtZipEntry zipEntry = new ExtZipEntry( fileName, cde );
zipEntry.setCompressedSize( cde.getCompressedSize() );
zipEntry.setSize( cde.getUncompressedSize() );
long dosTime = raFile.readInt( fileOffset + 12 );
zipEntry.setTime( ExtZipEntry.dosToJavaTime(dosTime) );
if( cde.isEncrypted() ) {
zipEntry.setMethod( cde.getActualCompressionMethod() );
zipEntry.setOffset( (int)(cde.getLocalHeaderOffset() + cde.getLocalHeaderSize()) + cde.getCryptoHeaderLength() );
zipEntry.initEncryptedEntry();
} else {
zipEntry.setMethod( ZipEntry.DEFLATED );
zipEntry.setPrimaryCompressionMethod( ZipEntry.DEFLATED );
}
nextFileOffset += extraFieldLength;
out.add(zipEntry);
fileOffset = nextFileOffset;
}
return out;
}
public ExtZipEntry getEntry( String name ) throws IOException, ZipException, DataFormatException {
for( ExtZipEntry zipEntry : getEntryList() ) {
if( name.equals(zipEntry.getName()) ) {
return zipEntry;
}
}
return null;
}
protected void checkZipEntry(ExtZipEntry zipEntry) throws ZipException {
if( zipEntry==null ) {
throw new ZipException("zipEntry must NOT be NULL");
}
if( zipEntry.isDirectory() ) {
throw new ZipException("directory entries cannot be decrypted");
}
if( !zipEntry.isEncrypted() ) {
throw new ZipException( "currently only extracts encrypted files - use java.util.zip to unzip" );
}
}
public void extractEntryWithTmpFile( ExtZipEntry zipEntry, File outFile, String password ) throws IOException, ZipException, DataFormatException {
checkZipEntry(zipEntry);
CentralDirectoryEntry cde = zipEntry.getCentralDirectoryEntry();
if( !cde.isAesEncrypted() ) {
throw new ZipException("only AES encrypted files are supported");
}
int cryptoHeaderOffset = zipEntry.getOffset() - cde.getCryptoHeaderLength();
byte[] salt = raFile.readByteArray( cryptoHeaderOffset, 16 );
byte[] pwVerification = raFile.readByteArray( cryptoHeaderOffset+16, 2 );
if( LOG.isLoggable(Level.FINEST) ) {
LOG.finest( "\n" + cde.toString() );
LOG.finest( "offset = " + zipEntry.getOffset() );
LOG.finest( "cryptoOff = " + cryptoHeaderOffset );
LOG.finest( "password = " + password + " - " + password.length() );
LOG.finest( "salt = " + ByteArrayHelper.toString(salt) + " - " + salt.length );
LOG.finest( "pwVerif = " + ByteArrayHelper.toString(pwVerification) + " - " + pwVerification.length );
}
// encrypter throws ZipException for wrong password
decrypter.init( password, 256, salt, pwVerification );
// create tmp file that contains the decrypted, but still compressed data
File tmpFile = new File( outFile.getPath() + "_TMP.zip" );
makeDir( tmpFile.getParent() );
ExtZipOutputStream zos = null;
ZipFile zf = null;
FileOutputStream fos = null;
InputStream is = null;
try {
zos = new ExtZipOutputStream( tmpFile );
ExtZipEntry tmpEntry = new ExtZipEntry( zipEntry );
tmpEntry.setPrimaryCompressionMethod( zipEntry.getMethod() );
zos.putNextEntry( tmpEntry );
raFile.seek( cde.getOffset() );
byte[] buffer = new byte[bufferSize];
int remaining = (int)zipEntry.getEncryptedDataSize();
while( remaining>0 ) {
int len = (remaining>buffer.length) ? buffer.length : remaining;
int read = raFile.readByteArray(buffer,len);
decrypter.decrypt( buffer, read );
zos.writeBytes( buffer, 0, read );
remaining -= len;
}
zos.finish();
zos = null;
byte[] storedMac = new byte[10];
raFile.readByteArray(storedMac,10);
byte[] calcMac = decrypter.getFinalAuthentication();
if( LOG.isLoggable(Level.FINE) ) {
LOG.fine( "storedMac=" + Arrays.toString(storedMac) );
LOG.fine( "calcMac=" + Arrays.toString(calcMac) );
}
if( !Arrays.equals(storedMac, calcMac ) ) {
throw new ZipException("stored authentication (mac) value does not match calculated one");
}
zf = new ZipFile( tmpFile );
ZipEntry ze = zf.entries().nextElement();
is = zf.getInputStream( ze );
fos = new FileOutputStream ( outFile.getPath() );
int read = is.read( buffer );
while( read>0 ) {
fos.write( buffer, 0, read );
read = is.read( buffer );
}
} finally {
if (zos != null) {
zos.close();
}
if (zf != null) {
zf.close();
}
if (fos != null) {
fos.close();
}
if (is != null) {
is.close();
}
}
tmpFile.delete();
}
/** number of entries in file (files AND directories) */
public short getNumberOfEntries() throws IOException {
return raFile.readShort( this.dirOffsetPos-6 );
}
protected static void makeDir(String dirStr) {
if(dirStr!=null) {
makeDir(new File(dirStr));
}
}
protected static void makeDir(File dir) {
if( dir!=null ) {
if( !dir.exists() ) {
if( dir.getParent()!=null ) {
File parentDir = new File(dir.getParent());
if( !parentDir.exists() ) {
makeDir(parentDir);
}
}
dir.mkdir();
}
}
}
/** return the zip file's comment (if defined) */
public String getComment() {
return comment;
}
// --------------------------------------------------------------------------
/**
* extract zipEntry - uses in-memory, so your file (stream contents) should not be too big
*/
public void extractEntry(ExtZipEntry zipEntry, OutputStream outStream, String password)
throws IOException, ZipException, DataFormatException {
checkZipEntry(zipEntry);
ZipInputStream zipInputStream = null;
ByteArrayOutputStream bos = null;
try {
CentralDirectoryEntry cde = zipEntry.getCentralDirectoryEntry();
if (!cde.isAesEncrypted()) {
throw new ZipException("only AES encrypted files are supported");
}
int cryptoHeaderOffset = zipEntry.getOffset() - cde.getCryptoHeaderLength();
byte[] salt = raFile.readByteArray(cryptoHeaderOffset, 16);
byte[] pwVerification = raFile.readByteArray(cryptoHeaderOffset + 16, 2);
if (LOG.isLoggable(Level.FINEST)) {
LOG.finest("\n" + cde.toString());
LOG.finest("offset = " + zipEntry.getOffset());
LOG.finest("cryptoOff = " + cryptoHeaderOffset);
LOG.finest("password = " + password + " - " + password.length());
LOG.finest("salt = " + ByteArrayHelper.toString(salt) + " - " + salt.length);
LOG.finest("pwVerif = " + ByteArrayHelper.toString(pwVerification) + " - " + pwVerification.length);
}
// encrypter throws ZipException for wrong password
decrypter.init(password, 256, salt, pwVerification);
bos = new ByteArrayOutputStream(bufferSize);
ExtZipOutputStream zos = new ExtZipOutputStream(bos);
ExtZipEntry tmpEntry = new ExtZipEntry(zipEntry);
tmpEntry.setPrimaryCompressionMethod(zipEntry.getMethod());
tmpEntry.setCompressedSize(zipEntry.getEncryptedDataSize());
zos.putNextEntry(tmpEntry);
raFile.seek(cde.getOffset());
byte[] buffer = new byte[bufferSize];
CRC32 crc32 = new CRC32();
int remaining = (int) zipEntry.getEncryptedDataSize();
while (remaining > 0) {
int len = (remaining > buffer.length) ? buffer.length : remaining;
int read = raFile.readByteArray(buffer, len);
decrypter.decrypt(buffer, read);
zos.writeBytes(buffer, 0, read);
remaining -= len;
crc32.update(buffer, 0, read);
}
tmpEntry.setCrc(crc32.getValue());
zos.finish();
byte[] storedMac = new byte[10];
raFile.readByteArray(storedMac, 10);
byte[] calcMac = decrypter.getFinalAuthentication();
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("storedMac=" + Arrays.toString(storedMac));
LOG.fine("calcMac=" + Arrays.toString(calcMac));
}
if (!Arrays.equals(storedMac, calcMac)) {
throw new ZipException("stored authentication (mac) value does not match calculated one");
}
zipInputStream = new ZipInputStream(new ByteArrayInputStream(bos.toByteArray()));
ZipEntry entry = zipInputStream.getNextEntry();
// At the end of the entry read-cycle a CRC check is performed.
// Because our entry doesn't have a CRC this will result in an Exception
// we solve this by updating a CRC and pass this to the entry.
entry.setCrc( crc32.getValue() );
if( entry.getSize()!=0 ) {
crc32 = new CRC32();
int read = zipInputStream.read(buffer);
while (read > 0) {
outStream.write(buffer, 0, read);
crc32.update(buffer, 0, read);
entry.setCrc(crc32.getValue());
read = zipInputStream.read(buffer);
}
}
} finally {
if (bos != null) {
bos.close();
}
if (zipInputStream != null) {
zipInputStream.close();
}
// not opened here, so we don't close it here
// if (outStream != null) {
// outStream.close();
// }
}
}
/**
* extract zipEntry - uses in-memory, so your file should not be too big
*/
public void extractEntry(ExtZipEntry zipEntry, File outFile, String password) throws IOException,
ZipException, DataFormatException {
ByteArrayOutputStream bos = null;
FileOutputStream fos = null;
try {
bos = new ByteArrayOutputStream(bufferSize);
fos = new FileOutputStream(outFile);
extractEntry(zipEntry, bos, password);
byte[] buffer = bos.toByteArray();
fos.write(buffer);
} finally {
if (bos != null) {
bos.close();
}
if (fos != null) {
fos.close();
}
}
}
public static interface ProgressListener
{
void notifyProgress(int count, int total);
/**
* If true, extractEnteryToZippedByteArray will stop
*/
boolean isCanceled();
}
/**
* extract zipEntry - uses in-memory, so your file (stream contents) should not be too big
* @param progressListener
* @return
*/
public byte[] extractEntryToZippedByteArray(ExtZipEntry zipEntry, String password, ProgressListener progressListener)
throws IOException, ZipException, DataFormatException {
checkZipEntry(zipEntry);
ZipInputStream zipInputStream = null;
BufAccessibleByteArrayOutputStream bos = null;
try {
CentralDirectoryEntry cde = zipEntry.getCentralDirectoryEntry();
if (!cde.isAesEncrypted()) {
throw new ZipException("only AES encrypted files are supported");
}
int cryptoHeaderOffset = zipEntry.getOffset() - cde.getCryptoHeaderLength();
byte[] salt = raFile.readByteArray(cryptoHeaderOffset, 16);
byte[] pwVerification = raFile.readByteArray(cryptoHeaderOffset + 16, 2);
if (LOG.isLoggable(Level.FINEST)) {
LOG.finest("\n" + cde.toString());
LOG.finest("offset = " + zipEntry.getOffset());
LOG.finest("cryptoOff = " + cryptoHeaderOffset);
LOG.finest("password = " + password + " - " + password.length());
LOG.finest("salt = " + ByteArrayHelper.toString(salt) + " - " + salt.length);
LOG.finest("pwVerif = " + ByteArrayHelper.toString(pwVerification) + " - " + pwVerification.length);
}
// encrypter throws ZipException for wrong password
decrypter.init(password, 256, salt, pwVerification);
bos = new BufAccessibleByteArrayOutputStream(bufferSize);
ExtZipOutputStream zos = new ExtZipOutputStream(bos);
ExtZipEntry tmpEntry = new ExtZipEntry(zipEntry);
tmpEntry.setPrimaryCompressionMethod(zipEntry.getMethod());
tmpEntry.setCompressedSize(zipEntry.getEncryptedDataSize());
zos.putNextEntry(tmpEntry);
raFile.seek(cde.getOffset());
byte[] buffer = new byte[bufferSize];
int total = (int) zipEntry.getEncryptedDataSize();
int count = 0;
int loops = 0;
while (count < total) {
int remaining = total - count;
int len = (remaining > buffer.length) ? buffer.length : remaining;
int read = raFile.readByteArray(buffer, len);
decrypter.decrypt(buffer, read);
zos.writeBytes(buffer, 0, read);
count += len;
if(progressListener != null && loops % LOOP_COUNT_BEFORE_UPDATE == 0)
{
progressListener.notifyProgress(count, total);
if(progressListener.isCanceled())
{
return null;
}
}
}
zos.finish();
byte[] storedMac = new byte[10];
raFile.readByteArrayFully(storedMac, 0, 10);
byte[] calcMac = decrypter.getFinalAuthentication();
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("storedMac=" + Arrays.toString(storedMac));
LOG.fine("calcMac=" + Arrays.toString(calcMac));
}
if (!Arrays.equals(storedMac, calcMac)) {
throw new ZipException("stored authentication (mac) value does not match calculated one");
}
byte [] data = bos.getBuf();
return data;
} finally {
if (zipInputStream != null) {
zipInputStream.close();
}
}
}
}