GordianCoreZipWriteFile.java
/*
* GordianKnot: Security Suite
* Copyright 2012-2026. Tony Washer
*
* 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 io.github.tonywasher.joceanus.gordianknot.impl.core.zip;
import io.github.tonywasher.joceanus.gordianknot.api.base.GordianException;
import io.github.tonywasher.joceanus.gordianknot.api.factory.GordianFactory;
import io.github.tonywasher.joceanus.gordianknot.api.keyset.GordianKeySet;
import io.github.tonywasher.joceanus.gordianknot.api.zip.GordianZipFileEntry;
import io.github.tonywasher.joceanus.gordianknot.api.zip.GordianZipWriteFile;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianDataConverter;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianDataException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianIOException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianLogicException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.keyset.GordianCoreKeySet;
import io.github.tonywasher.joceanus.gordianknot.impl.core.stream.GordianStreamDefinition;
import io.github.tonywasher.joceanus.gordianknot.impl.core.stream.GordianStreamManager;
import org.w3c.dom.Document;
import javax.xml.XMLConstants;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* Class used to build a ZipFile.
*/
public class GordianCoreZipWriteFile
implements GordianZipWriteFile {
/**
* The FileName prefix.
*/
private static final String FILE_PREFIX = "File";
/**
* Security Lock for this zip file.
*/
private final GordianCoreZipLock theLock;
/**
* securedKeySet for this zip file.
*/
private final byte[] theSecuredKeySet;
/**
* KeySet for this zip file.
*/
private final GordianCoreKeySet theKeySet;
/**
* The StreamFactory.
*/
private final GordianStreamManager theStreamFactory;
/**
* The underlying Zip output stream.
*/
private ZipOutputStream theStream;
/**
* The list of contents.
*/
private final GordianCoreZipFileContents theContents;
/**
* The active zipEntry.
*/
private ZipEntry theEntry;
/**
* The active zipFileEntry.
*/
private GordianCoreZipFileEntry theFileEntry;
/**
* The active filename.
*/
private String theFileName;
/**
* The active output stream.
*/
private OutputStream theOutput;
/**
* The fileNumber.
*/
private int theFileNo;
/**
* Constructor for new output zip file with security.
*
* @param pLock the lock to use
* @param pOutputStream the output stream to write to
* @throws GordianException on error
*/
GordianCoreZipWriteFile(final GordianCoreZipLock pLock,
final OutputStream pOutputStream) throws GordianException {
/* Check that the lock is usable */
if (pLock == null || !pLock.isFresh()) {
throw new GordianDataException("Invalid lock");
}
/* Record lock and mark as used */
theLock = pLock;
pLock.markAsUsed();
/* Create a child hash and record details */
final GordianCoreKeySet myKeySet = (GordianCoreKeySet) theLock.getKeySet();
final GordianFactory myFactory = myKeySet.getFactory();
theKeySet = (GordianCoreKeySet) myFactory.getKeySetFactory().generateKeySet(myKeySet.getKeySetSpec());
theSecuredKeySet = myKeySet.secureKeySet(theKeySet);
/* Create the Stream Manager */
theStreamFactory = new GordianStreamManager(theKeySet);
/* reSeed the random number generator */
theKeySet.getFactory().reSeedRandom();
/* Create the output streams */
final BufferedOutputStream myOutBuffer = new BufferedOutputStream(pOutputStream);
theStream = new ZipOutputStream(myOutBuffer);
/*
* Set compression level to zero to speed things up. It would be nice to use the STORED
* method, but this requires calculating the CRC and file size prior to writing data to
* the Zip file which will badly affect performance.
*/
theStream.setLevel(ZipOutputStream.STORED);
/* Create the file contents */
theContents = new GordianCoreZipFileContents();
}
/**
* Constructor for new output zip file with no security.
*
* @param pOutputStream the output stream to write to
*/
GordianCoreZipWriteFile(final OutputStream pOutputStream) {
/* record null security */
theLock = null;
theSecuredKeySet = null;
theKeySet = null;
theStreamFactory = null;
/* Create the output streams */
final BufferedOutputStream myOutBuffer = new BufferedOutputStream(pOutputStream);
theStream = new ZipOutputStream(myOutBuffer);
/* Create the file contents */
theContents = new GordianCoreZipFileContents();
}
/**
* Is the ZipFile encrypted.
*
* @return is the Zip File encrypted
*/
private boolean isEncrypted() {
return theLock != null;
}
@Override
public GordianCoreZipFileContents getContents() {
return theContents;
}
@Override
public GordianZipFileEntry getCurrentEntry() {
return theFileEntry;
}
@Override
public void writeXMLDocument(final File pFile,
final Document pDocument) throws GordianException {
/* Access the entry as an input stream */
try (OutputStream myOutputStream = createOutputStream(pFile, true)) {
/* Create the transformer */
final TransformerFactory myXformFactory = TransformerFactory.newInstance();
myXformFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
myXformFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
myXformFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
final Transformer myXformer = myXformFactory.newTransformer();
/* Format the XML and write to stream */
myXformer.transform(new DOMSource(pDocument), new StreamResult(myOutputStream));
/* Catch exceptions */
} catch (IOException
| TransformerException e) {
throw new GordianIOException("Failed to write Document", e);
}
}
@Override
public OutputStream createOutputStream(final File pFile,
final boolean pCompress) throws GordianException {
/* Reject call if we have closed the stream */
if (theStream == null) {
throw new GordianLogicException("ZipFile is closed");
}
/* Reject call if we have an open stream */
if (theOutput != null) {
throw new GordianLogicException("Output stream already open");
}
/* Increment file number */
theFileNo++;
/* Protect against exceptions */
try {
/* Start the new entry */
theFileName = pFile.getPath();
theEntry = new ZipEntry(isEncrypted()
? FILE_PREFIX
+ theFileNo
: theFileName);
theStream.putNextEntry(theEntry);
/* Create a new zipFileEntry */
theFileEntry = theContents.addZipFileEntry(theFileName);
/* Simply create a wrapper on the output stream */
theOutput = new GordianWrapOutputStream(theStream);
/* If we are encrypting */
if (isEncrypted()) {
/* Create an the output stream */
theOutput = theStreamFactory.buildOutputStream(theOutput, pCompress);
}
/* Catch exceptions */
} catch (IOException e) {
throw new GordianIOException("Exception creating new Output stream", e);
}
/* return the new stream */
return theOutput;
}
/**
* Close any active output stream and record digest values.
*
* @throws IOException on error
*/
private void closeOutputStream() throws IOException {
/* Protect against exceptions */
try {
/* If we have an output stream */
if (theOutput != null) {
/* Close the active entry */
theStream.closeEntry();
/* Add the details of the entry */
theFileEntry.setZipEntry(theEntry);
/* If we have encryption */
if (isEncrypted()) {
/* Analyse the output stream */
final List<GordianStreamDefinition> myStreams = theStreamFactory.analyseStreams(theOutput);
/* Analyse the stream */
theFileEntry.buildProperties(myStreams);
}
/* Release the entry */
theEntry = null;
theFileName = null;
theFileEntry = null;
}
/* Reset streams */
theOutput = null;
/* Catch exceptions */
} catch (GordianException e) {
throw new IOException(e);
}
}
@Override
public void close() throws IOException {
/* Close any open output stream */
closeOutputStream();
/* If the stream is open */
if (theStream != null) {
/* Protect against exceptions */
try {
/* If we have stored files and are encrypted */
if (theFileNo > 0
&& isEncrypted()) {
/* Create a new zipFileEntry */
final GordianCoreZipFileEntry myEntry = theContents.addZipFileHeader();
myEntry.setHash(theSecuredKeySet);
/* Create the header entry */
++theFileNo;
theEntry = new ZipEntry(FILE_PREFIX
+ theFileNo);
/* Declare the lock and encrypt the header */
theEntry.setExtra(theLock.getEncodedBytes());
/* Start the new entry */
theStream.putNextEntry(theEntry);
/* Declare the details */
myEntry.setZipEntry(theEntry);
/* Access the encoded file string */
final String myHeader = theContents.encodeContents();
/* Write the bytes to the Zip file and close the entry */
final byte[] myBytes = GordianDataConverter.stringToByteArray(myHeader);
final GordianKeySet myKeySet = theLock.getKeySet();
theStream.write(myKeySet.encryptBytes(myBytes));
theStream.closeEntry();
}
/* close the stream */
theStream.flush();
theStream.close();
theStream = null;
/* Catch exceptions */
} catch (GordianException e) {
throw new IOException(e);
}
}
}
/**
* Wrapper class to catch close of output stream and prevent it from closing the ZipFile.
*/
private final class GordianWrapOutputStream
extends OutputStream {
/**
* The underlying Zip output stream.
*/
private final ZipOutputStream theStream;
/**
* Constructor.
*
* @param pStream the ZipStream
*/
GordianWrapOutputStream(final ZipOutputStream pStream) {
theStream = pStream;
}
@Override
public void flush() throws IOException {
theStream.flush();
}
@Override
public void write(final int b) throws IOException {
theStream.write(b);
}
@Override
public void write(final byte[] b) throws IOException {
theStream.write(b);
}
@Override
public void write(final byte[] b,
final int offset,
final int length) throws IOException {
theStream.write(b, offset, length);
}
@Override
public void close() throws IOException {
closeOutputStream();
}
}
}