FMB/src/net/lingala/zip4j/io/CipherOutputStream.java
2016-11-11 20:10:36 +01:00

570 lines
20 KiB
Java

/*
* Copyright 2010 Srikanth Reddy Lingala
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.lingala.zip4j.io;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.zip.CRC32;
import net.lingala.zip4j.core.HeaderWriter;
import net.lingala.zip4j.crypto.AESEncrpyter;
import net.lingala.zip4j.crypto.IEncrypter;
import net.lingala.zip4j.crypto.StandardEncrypter;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.AESExtraDataRecord;
import net.lingala.zip4j.model.CentralDirectory;
import net.lingala.zip4j.model.EndCentralDirRecord;
import net.lingala.zip4j.model.FileHeader;
import net.lingala.zip4j.model.LocalFileHeader;
import net.lingala.zip4j.model.ZipModel;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.util.InternalZipConstants;
import net.lingala.zip4j.util.Raw;
import net.lingala.zip4j.util.Zip4jConstants;
import net.lingala.zip4j.util.Zip4jUtil;
public class CipherOutputStream extends BaseOutputStream {
protected OutputStream outputStream;
private File sourceFile;
protected FileHeader fileHeader;
protected LocalFileHeader localFileHeader;
private IEncrypter encrypter;
protected ZipParameters zipParameters;
protected ZipModel zipModel;
private long totalBytesWritten;
protected CRC32 crc;
private long bytesWrittenForThisFile;
private byte[] pendingBuffer;
private int pendingBufferLength;
private long totalBytesRead;
public CipherOutputStream(OutputStream outputStream, ZipModel zipModel) {
this.outputStream = outputStream;
initZipModel(zipModel);
crc = new CRC32();
this.totalBytesWritten = 0;
this.bytesWrittenForThisFile = 0;
this.pendingBuffer = new byte[InternalZipConstants.AES_BLOCK_SIZE];
this.pendingBufferLength = 0;
this.totalBytesRead = 0;
}
public void putNextEntry(File file, ZipParameters zipParameters) throws ZipException {
if (!zipParameters.isSourceExternalStream() && file == null) {
throw new ZipException("input file is null");
}
if (!zipParameters.isSourceExternalStream() && !Zip4jUtil.checkFileExists(file)) {
throw new ZipException("input file does not exist");
}
try {
sourceFile = file;
this.zipParameters = (ZipParameters)zipParameters.clone();
if (!zipParameters.isSourceExternalStream()) {
if (sourceFile.isDirectory()) {
this.zipParameters.setEncryptFiles(false);
this.zipParameters.setEncryptionMethod(-1);
this.zipParameters.setCompressionMethod(Zip4jConstants.COMP_STORE);
}
} else {
if (!Zip4jUtil.isStringNotNullAndNotEmpty(this.zipParameters.getFileNameInZip())) {
throw new ZipException("file name is empty for external stream");
}
if (this.zipParameters.getFileNameInZip().endsWith("/") ||
this.zipParameters.getFileNameInZip().endsWith("\\")) {
this.zipParameters.setEncryptFiles(false);
this.zipParameters.setEncryptionMethod(-1);
this.zipParameters.setCompressionMethod(Zip4jConstants.COMP_STORE);
}
}
createFileHeader();
createLocalFileHeader();
if (zipModel.isSplitArchive()) {
if (zipModel.getCentralDirectory() == null ||
zipModel.getCentralDirectory().getFileHeaders() == null ||
zipModel.getCentralDirectory().getFileHeaders().size() == 0) {
byte[] intByte = new byte[4];
Raw.writeIntLittleEndian(intByte, 0, (int)InternalZipConstants.SPLITSIG);
outputStream.write(intByte);
totalBytesWritten += 4;
}
}
if (this.outputStream instanceof SplitOutputStream) {
if (totalBytesWritten == 4) {
fileHeader.setOffsetLocalHeader(4);
} else {
fileHeader.setOffsetLocalHeader(((SplitOutputStream)outputStream).getFilePointer());
}
} else {
if (totalBytesWritten == 4) {
fileHeader.setOffsetLocalHeader(4);
} else {
fileHeader.setOffsetLocalHeader(totalBytesWritten);
}
}
HeaderWriter headerWriter = new HeaderWriter();
totalBytesWritten += headerWriter.writeLocalFileHeader(zipModel, localFileHeader, outputStream);
if (this.zipParameters.isEncryptFiles()) {
initEncrypter();
if (encrypter != null) {
if (zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_STANDARD) {
byte[] headerBytes = ((StandardEncrypter)encrypter).getHeaderBytes();
outputStream.write(headerBytes);
totalBytesWritten += headerBytes.length;
bytesWrittenForThisFile += headerBytes.length;
} else if (zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) {
byte[] saltBytes = ((AESEncrpyter)encrypter).getSaltBytes();
byte[] passwordVerifier = ((AESEncrpyter)encrypter).getDerivedPasswordVerifier();
outputStream.write(saltBytes);
outputStream.write(passwordVerifier);
totalBytesWritten += saltBytes.length + passwordVerifier.length;
bytesWrittenForThisFile += saltBytes.length + passwordVerifier.length;
}
}
}
crc.reset();
} catch (CloneNotSupportedException e) {
throw new ZipException(e);
} catch (ZipException e) {
throw e;
} catch (Exception e) {
throw new ZipException(e);
}
}
private void initEncrypter() throws ZipException {
if (!zipParameters.isEncryptFiles()) {
encrypter = null;
return;
}
switch (zipParameters.getEncryptionMethod()) {
case Zip4jConstants.ENC_METHOD_STANDARD:
// Since we do not know the crc here, we use the modification time for encrypting.
encrypter = new StandardEncrypter(zipParameters.getPassword(), (localFileHeader.getLastModFileTime() & 0x0000ffff) << 16);
break;
case Zip4jConstants.ENC_METHOD_AES:
encrypter = new AESEncrpyter(zipParameters.getPassword(), zipParameters.getAesKeyStrength());
break;
default:
throw new ZipException("invalid encprytion method");
}
}
private void initZipModel(ZipModel zipModel) {
if (zipModel == null) {
this.zipModel = new ZipModel();
} else {
this.zipModel = zipModel;
}
if (this.zipModel.getEndCentralDirRecord() == null)
this.zipModel.setEndCentralDirRecord(new EndCentralDirRecord());
if (this.zipModel.getCentralDirectory() == null)
this.zipModel.setCentralDirectory(new CentralDirectory());
if (this.zipModel.getCentralDirectory().getFileHeaders() == null)
this.zipModel.getCentralDirectory().setFileHeaders(new ArrayList());
if (this.zipModel.getLocalFileHeaderList() == null)
this.zipModel.setLocalFileHeaderList(new ArrayList());
if (this.outputStream instanceof SplitOutputStream) {
if (((SplitOutputStream)outputStream).isSplitZipFile()) {
this.zipModel.setSplitArchive(true);
this.zipModel.setSplitLength(((SplitOutputStream)outputStream).getSplitLength());
}
}
this.zipModel.getEndCentralDirRecord().setSignature(InternalZipConstants.ENDSIG);
}
public void write(int bval) throws IOException {
byte[] b = new byte[1];
b[0] = (byte) bval;
write(b, 0, 1);
}
public void write(byte[] b) throws IOException {
if (b == null)
throw new NullPointerException();
if (b.length == 0) return;
write(b, 0, b.length);
}
public void write(byte[] b, int off, int len) throws IOException {
if (len == 0) return;
if (zipParameters.isEncryptFiles() &&
zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) {
if (pendingBufferLength != 0) {
if (len >= (InternalZipConstants.AES_BLOCK_SIZE - pendingBufferLength)) {
System.arraycopy(b, off, pendingBuffer, pendingBufferLength,
(InternalZipConstants.AES_BLOCK_SIZE - pendingBufferLength));
encryptAndWrite(pendingBuffer, 0, pendingBuffer.length);
off = (InternalZipConstants.AES_BLOCK_SIZE - pendingBufferLength);
len = len - off;
pendingBufferLength = 0;
} else {
System.arraycopy(b, off, pendingBuffer, pendingBufferLength,
len);
pendingBufferLength += len;
return;
}
}
if (len != 0 && len % 16 != 0) {
System.arraycopy(b, (len + off) - (len % 16), pendingBuffer, 0, len % 16);
pendingBufferLength = len % 16;
len = len - pendingBufferLength;
}
}
if (len != 0)
encryptAndWrite(b, off, len);
}
private void encryptAndWrite(byte[] b, int off, int len) throws IOException {
if (encrypter != null) {
try {
encrypter.encryptData(b, off, len);
} catch (ZipException e) {
throw new IOException(e.getMessage());
}
}
outputStream.write(b, off, len);
totalBytesWritten += len;
bytesWrittenForThisFile += len;
}
public void closeEntry() throws IOException, ZipException {
if (this.pendingBufferLength != 0) {
encryptAndWrite(pendingBuffer, 0, pendingBufferLength);
pendingBufferLength = 0;
}
if (this.zipParameters.isEncryptFiles() &&
this.zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) {
if (encrypter instanceof AESEncrpyter) {
outputStream.write(((AESEncrpyter)encrypter).getFinalMac());
bytesWrittenForThisFile += 10;
totalBytesWritten += 10;
} else {
throw new ZipException("invalid encrypter for AES encrypted file");
}
}
fileHeader.setCompressedSize(bytesWrittenForThisFile);
localFileHeader.setCompressedSize(bytesWrittenForThisFile);
if (zipParameters.isSourceExternalStream()) {
fileHeader.setUncompressedSize(totalBytesRead);
if (localFileHeader.getUncompressedSize() != totalBytesRead) {
localFileHeader.setUncompressedSize(totalBytesRead);
}
}
long crc32 = crc.getValue();
if (fileHeader.isEncrypted()) {
if (fileHeader.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) {
crc32 = 0;
}
}
if (zipParameters.isEncryptFiles() &&
zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) {
fileHeader.setCrc32(0);
localFileHeader.setCrc32(0);
} else {
fileHeader.setCrc32(crc32);
localFileHeader.setCrc32(crc32);
}
zipModel.getLocalFileHeaderList().add(localFileHeader);
zipModel.getCentralDirectory().getFileHeaders().add(fileHeader);
HeaderWriter headerWriter = new HeaderWriter();
totalBytesWritten += headerWriter.writeExtendedLocalHeader(localFileHeader, outputStream);
crc.reset();
bytesWrittenForThisFile = 0;
encrypter = null;
totalBytesRead = 0;
}
public void finish() throws IOException, ZipException {
zipModel.getEndCentralDirRecord().setOffsetOfStartOfCentralDir(totalBytesWritten);
HeaderWriter headerWriter = new HeaderWriter();
headerWriter.finalizeZipFile(zipModel, outputStream);
}
public void close() throws IOException {
if (outputStream != null)
outputStream.close();
}
private void createFileHeader() throws ZipException {
this.fileHeader = new FileHeader();
fileHeader.setSignature((int)InternalZipConstants.CENSIG);
fileHeader.setVersionMadeBy(20);
fileHeader.setVersionNeededToExtract(20);
if (zipParameters.isEncryptFiles() &&
zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) {
fileHeader.setCompressionMethod(Zip4jConstants.ENC_METHOD_AES);
fileHeader.setAesExtraDataRecord(generateAESExtraDataRecord(zipParameters));
} else {
fileHeader.setCompressionMethod(zipParameters.getCompressionMethod());
}
if (zipParameters.isEncryptFiles()) {
fileHeader.setEncrypted(true);
fileHeader.setEncryptionMethod(zipParameters.getEncryptionMethod());
}
String fileName = null;
if (zipParameters.isSourceExternalStream()) {
fileHeader.setLastModFileTime((int) Zip4jUtil.javaToDosTime(System.currentTimeMillis()));
if (!Zip4jUtil.isStringNotNullAndNotEmpty(zipParameters.getFileNameInZip())) {
throw new ZipException("fileNameInZip is null or empty");
}
fileName = zipParameters.getFileNameInZip();
} else {
fileHeader.setLastModFileTime((int) Zip4jUtil.javaToDosTime((Zip4jUtil.getLastModifiedFileTime(
sourceFile, zipParameters.getTimeZone()))));
fileHeader.setUncompressedSize(sourceFile.length());
fileName = Zip4jUtil.getRelativeFileName(
sourceFile.getAbsolutePath(), zipParameters.getRootFolderInZip(), zipParameters.getDefaultFolderPath());
}
if (!Zip4jUtil.isStringNotNullAndNotEmpty(fileName)) {
throw new ZipException("fileName is null or empty. unable to create file header");
}
fileHeader.setFileName(fileName);
if (Zip4jUtil.isStringNotNullAndNotEmpty(zipModel.getFileNameCharset())) {
fileHeader.setFileNameLength(Zip4jUtil.getEncodedStringLength(fileName,
zipModel.getFileNameCharset()));
} else {
fileHeader.setFileNameLength(Zip4jUtil.getEncodedStringLength(fileName));
}
if (outputStream instanceof SplitOutputStream) {
fileHeader.setDiskNumberStart(((SplitOutputStream)outputStream).getCurrSplitFileCounter());
} else {
fileHeader.setDiskNumberStart(0);
}
int fileAttrs = 0;
if (!zipParameters.isSourceExternalStream())
fileAttrs = getFileAttributes(sourceFile);
byte[] externalFileAttrs = {(byte)fileAttrs, 0, 0, 0};
fileHeader.setExternalFileAttr(externalFileAttrs);
if (zipParameters.isSourceExternalStream()) {
fileHeader.setDirectory(fileName.endsWith("/") || fileName.endsWith("\\"));
} else {
fileHeader.setDirectory(this.sourceFile.isDirectory());
}
if (fileHeader.isDirectory()) {
fileHeader.setCompressedSize(0);
fileHeader.setUncompressedSize(0);
} else {
if (!zipParameters.isSourceExternalStream()) {
long fileSize = Zip4jUtil.getFileLengh(sourceFile);
if (zipParameters.getCompressionMethod() == Zip4jConstants.COMP_STORE) {
if (zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_STANDARD) {
fileHeader.setCompressedSize(fileSize
+ InternalZipConstants.STD_DEC_HDR_SIZE);
} else if (zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) {
int saltLength = 0;
switch (zipParameters.getAesKeyStrength()) {
case Zip4jConstants.AES_STRENGTH_128:
saltLength = 8;
break;
case Zip4jConstants.AES_STRENGTH_256:
saltLength = 16;
break;
default:
throw new ZipException("invalid aes key strength, cannot determine key sizes");
}
fileHeader.setCompressedSize(fileSize + saltLength
+ InternalZipConstants.AES_AUTH_LENGTH + 2); //2 is password verifier
} else {
fileHeader.setCompressedSize(0);
}
} else {
fileHeader.setCompressedSize(0);
}
fileHeader.setUncompressedSize(fileSize);
}
}
if (zipParameters.isEncryptFiles() &&
zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_STANDARD) {
fileHeader.setCrc32(zipParameters.getSourceFileCRC());
}
byte[] shortByte = new byte[2];
shortByte[0] = Raw.bitArrayToByte(generateGeneralPurposeBitArray(
fileHeader.isEncrypted(), zipParameters.getCompressionMethod()));
boolean isFileNameCharsetSet = Zip4jUtil.isStringNotNullAndNotEmpty(zipModel.getFileNameCharset());
if ((isFileNameCharsetSet &&
zipModel.getFileNameCharset().equalsIgnoreCase(InternalZipConstants.CHARSET_UTF8)) ||
(!isFileNameCharsetSet &&
Zip4jUtil.detectCharSet(fileHeader.getFileName()).equals(InternalZipConstants.CHARSET_UTF8))) {
shortByte[1] = 8;
} else {
shortByte[1] = 0;
}
fileHeader.setGeneralPurposeFlag(shortByte);
}
private void createLocalFileHeader() throws ZipException {
if (fileHeader == null) {
throw new ZipException("file header is null, cannot create local file header");
}
this.localFileHeader = new LocalFileHeader();
localFileHeader.setSignature((int)InternalZipConstants.LOCSIG);
localFileHeader.setVersionNeededToExtract(fileHeader.getVersionNeededToExtract());
localFileHeader.setCompressionMethod(fileHeader.getCompressionMethod());
localFileHeader.setLastModFileTime(fileHeader.getLastModFileTime());
localFileHeader.setUncompressedSize(fileHeader.getUncompressedSize());
localFileHeader.setFileNameLength(fileHeader.getFileNameLength());
localFileHeader.setFileName(fileHeader.getFileName());
localFileHeader.setEncrypted(fileHeader.isEncrypted());
localFileHeader.setEncryptionMethod(fileHeader.getEncryptionMethod());
localFileHeader.setAesExtraDataRecord(fileHeader.getAesExtraDataRecord());
localFileHeader.setCrc32(fileHeader.getCrc32());
localFileHeader.setCompressedSize(fileHeader.getCompressedSize());
localFileHeader.setGeneralPurposeFlag((byte[])fileHeader.getGeneralPurposeFlag().clone());
}
/**
* Checks the file attributes and returns an integer
* @param file
* @return
* @throws ZipException
*/
private int getFileAttributes(File file) throws ZipException {
if (file == null) {
throw new ZipException("input file is null, cannot get file attributes");
}
if (!file.exists()) {
return 0;
}
if (file.isDirectory()) {
if (file.isHidden()) {
return InternalZipConstants.FOLDER_MODE_HIDDEN;
} else {
return InternalZipConstants.FOLDER_MODE_NONE;
}
} else {
if (!file.canWrite() && file.isHidden()) {
return InternalZipConstants.FILE_MODE_READ_ONLY_HIDDEN;
} else if (!file.canWrite()) {
return InternalZipConstants.FILE_MODE_READ_ONLY;
} else if (file.isHidden()) {
return InternalZipConstants.FILE_MODE_HIDDEN;
} else {
return InternalZipConstants.FILE_MODE_NONE;
}
}
}
private int[] generateGeneralPurposeBitArray(boolean isEncrpyted, int compressionMethod) {
int[] generalPurposeBits = new int[8];
if (isEncrpyted) {
generalPurposeBits[0] = 1;
} else {
generalPurposeBits[0] = 0;
}
if (compressionMethod == Zip4jConstants.COMP_DEFLATE) {
// Have to set flags for deflate
} else {
generalPurposeBits[1] = 0;
generalPurposeBits[2] = 0;
}
generalPurposeBits[3] = 1;
return generalPurposeBits;
}
private AESExtraDataRecord generateAESExtraDataRecord(ZipParameters parameters) throws ZipException {
if (parameters == null) {
throw new ZipException("zip parameters are null, cannot generate AES Extra Data record");
}
AESExtraDataRecord aesDataRecord = new AESExtraDataRecord();
aesDataRecord.setSignature(InternalZipConstants.AESSIG);
aesDataRecord.setDataSize(7);
aesDataRecord.setVendorID("AE");
// Always set the version number to 2 as we do not store CRC for any AES encrypted files
// only MAC is stored and as per the specification, if version number is 2, then MAC is read
// and CRC is ignored
aesDataRecord.setVersionNumber(2);
if (parameters.getAesKeyStrength() == Zip4jConstants.AES_STRENGTH_128) {
aesDataRecord.setAesStrength(Zip4jConstants.AES_STRENGTH_128);
} else if (parameters.getAesKeyStrength() == Zip4jConstants.AES_STRENGTH_256) {
aesDataRecord.setAesStrength(Zip4jConstants.AES_STRENGTH_256);
} else {
throw new ZipException("invalid AES key strength, cannot generate AES Extra data record");
}
aesDataRecord.setCompressionMethod(parameters.getCompressionMethod());
return aesDataRecord;
}
public void decrementCompressedFileSize(int value) {
if (value <= 0) return;
if (value <= this.bytesWrittenForThisFile) {
this.bytesWrittenForThisFile -= value;
}
}
protected void updateTotalBytesRead(int toUpdate) {
if (toUpdate > 0) {
totalBytesRead += toUpdate;
}
}
public void setSourceFile(File sourceFile) {
this.sourceFile = sourceFile;
}
public File getSourceFile() {
return sourceFile;
}
}