package de.idyl.winzipaes.impl;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipException;
import de.idyl.winzipaes.AesZipFileDecrypter;
/**
* Wrapper for the central directory entry (CDE) of one file.
* At the end of the ZIP file one CDE can be found for each
* entry (file) within the zip file.
*
central file header signature 4 bytes (0x02014b50)
version made by 2 bytes // 4
version needed to extract 2 bytes // 6
general purpose bit flag 2 bytes // 8
compression method 2 bytes // 10
last mod file time 2 bytes // 12
last mod file date 2 bytes // 14
crc-32 4 bytes // 16
compressed size 4 bytes // 20
uncompressed size 4 bytes // 24
file name length 2 bytes // 28
extra field length 2 bytes // 30
file comment length 2 bytes // 32
disk number start 2 bytes // 34
internal file attributes 2 bytes // 36
external file attributes 4 bytes // 38
relative offset of local header 4 bytes // 42
file name (variable size) // 46
extra field (variable size) // 46 + fileNameLength
file comment (variable size) // 46 + fileNameLength + extraFieldLength
*/
public class CentralDirectoryEntry implements ZipConstants {
private static final Logger LOG = Logger.getLogger( CentralDirectoryEntry.class.getName() );
// ------------------------------------------------------------------------
protected ExtRandomAccessFile raFile;
protected long fileOffset;
protected boolean isEncrypted;
protected boolean isAesEncrypted;
protected short fileNameLength;
protected long extraFieldOffset;
protected String fileName;
protected int localHeaderSize;
protected short actualCompressionMethod;
protected short extraFieldLength;
protected long localHeaderOffset;
protected int compressedSize;
protected int uncompressedSize;
// ------------------------------------------------------------------------
public CentralDirectoryEntry( ExtRandomAccessFile raFile, long fileOffset ) throws IOException {
this.raFile = raFile;
this.fileOffset = fileOffset;
initFromRaFile();
}
protected void initFromRaFile() throws IOException {
// Central directory structure / central file header signature
int censig = raFile.readInt( fileOffset );
if( censig!=CENSIG ) {
throw new ZipException("expected CENSIC not found in central directory (at end of zip file)");
} else if( LOG.isLoggable(Level.FINE) ) {
LOG.fine( "found censigOffset=" + fileOffset );
}
short flag = raFile.readShort( fileOffset + 8 );
this.isEncrypted = (flag&1)>0;
this.fileNameLength = raFile.readShort( fileOffset + 28 );
byte[] fileNameBytes = raFile.readByteArray( fileOffset + 46, fileNameLength );
this.fileName = new String( fileNameBytes, AesZipFileDecrypter.charset );
if( LOG.isLoggable(Level.FINE) ) {
LOG.fine( "fileName = " + this.fileName );
}
this.extraFieldOffset = this.fileOffset + 46 + this.fileNameLength;
this.extraFieldLength = raFile.readShort( fileOffset + 30 );
this.localHeaderOffset = raFile.readInt( fileOffset + 28 + 14 );
if( LOG.isLoggable(Level.FINE) ) {
LOG.fine( "CDS - extraFieldOffset =" + Long.toHexString(this.extraFieldOffset) );
LOG.fine( "CDS - extraFieldLength =" + this.extraFieldLength );
LOG.fine( "CDS - localHeaderOffset=" + Long.toHexString(this.localHeaderOffset) );
}
// zODO - check, why we have to use the local header instead of the CDS sometimes...
if( this.isEncrypted ) {
byte[] efhid = raFile.readByteArray( this.extraFieldOffset, 2 );
if( efhid[0]!=0x01 || efhid[1]!=(byte)0x99 ) {
this.extraFieldOffset = localHeaderOffset+30+fileNameLength;
this.extraFieldLength = raFile.readShort( localHeaderOffset+28 );
if( LOG.isLoggable(Level.FINE) ) {
LOG.fine( "local header - extraFieldOffset=" + Long.toHexString(this.extraFieldOffset) );
LOG.fine( "local header - extraFieldLength=" + Long.toHexString(this.extraFieldLength) );
}
if( 0==extraFieldLength ) {
throw new ZipException("extra field is of length 0 - this is probably not a WinZip AES encrypted entry");
}
efhid = raFile.readByteArray( extraFieldOffset, 2);
if( efhid[0]==0x01 && efhid[1]==(byte)0x99 ) {
this.isAesEncrypted = true;
}
} else {
this.isAesEncrypted = true;
}
if( this.isAesEncrypted ) {
this.actualCompressionMethod = raFile.readShort( getExtraFieldOffset() + 9 );
this.localHeaderSize = 30 + getExtraFieldLength() + getFileNameLength();
}
}
this.compressedSize = (int)raFile.readLong( fileOffset + 20 );
this.uncompressedSize = (int)raFile.readLong( fileOffset + 24 );
}
// ------------------------------------------------------------------------
public int getCompressedSize() {
return this.compressedSize;
}
public int getUncompressedSize() {
return this.uncompressedSize;
}
public boolean isEncrypted() {
return isEncrypted;
}
public String getFileName() {
return this.fileName;
}
public short getFileNameLength() {
return fileNameLength;
}
public short getExtraFieldLength() {
return extraFieldLength;
}
public long getLocalHeaderOffset() {
return localHeaderOffset;
}
protected long getExtraFieldOffset() {
return extraFieldOffset;
}
/**
* @return position within zip file where the actual data of the entry (file) starts
* (after encryption salt + pw data)
*/
public int getOffset() {
return (int)(getLocalHeaderOffset() + getLocalHeaderSize() + getCryptoHeaderLength());
}
/**
* (for encrypted files) stored in extra field
* java zip only supports STORED and DEFLATED
*
* @return 0=stored (no compression) | 8=deflated
*/
public short getActualCompressionMethod() {
return actualCompressionMethod;
}
/**
* this library currently only supports 256bit keys
*
* @return keySize - 1=128bit | 2=192bit | 3=256bit
*/
public byte getEncryptionStrength() throws IOException {
return raFile.readByte( getExtraFieldOffset() + 8 );
}
public int getLocalHeaderSize() {
return localHeaderSize;
}
/** fixed 256 key size, 16 bytes salt + 2 bytes pwVerification */
public short getCryptoHeaderLength() {
// TODO support 128+192 byte keys reduces the salt byte size to 8+2 or 12+2
return 18;
}
public boolean isAesEncrypted() {
return isAesEncrypted;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append( "fileName\t\t = " ).append( this.fileName ).append('\n');
try {
sb.append( "uncompressedSize\t = " ).append( this.getUncompressedSize() ).append('\n');
sb.append( "compressedSize\t\t = " ).append( this.getCompressedSize() ).append('\n');
sb.append( "encryptionStrength\t = ").append( this.getEncryptionStrength() ).append('\n');
sb.append( "extraFieldOffset\t = " ).append( this.getExtraFieldOffset() ).append('\n');
sb.append( "extraFieldLength\t = " ).append( this.getExtraFieldLength() ).append('\n');
sb.append( "localHeaderOffset\t = " ).append( this.getLocalHeaderOffset() ).append('\n');
sb.append( "localHeaderSize\t\t = " ).append( this.getLocalHeaderSize() ).append('\n');
sb.append( "offset\t\t\t = ").append( this.getOffset() ).append('\n');
//sb.append().append().append('\n');
} catch( IOException ioEx ) {
LOG.log(Level.WARNING, ioEx.getMessage(), ioEx);
}
return sb.toString();
}
}