Worker.java
package org.jastacry;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.Console;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jastacry.GlobalData.Action;
import org.jastacry.GlobalData.Returncode;
import org.jastacry.layer.AbstractBasicLayer;
import org.jastacry.layer.AesCbcLayer;
import org.jastacry.layer.AesCtrLayer;
import org.jastacry.layer.AesEcbLayer;
import org.jastacry.layer.AsciiTransportLayer;
import org.jastacry.layer.FilemergeLayer;
import org.jastacry.layer.Md5DesLayer;
import org.jastacry.layer.RandomLayer;
import org.jastacry.layer.ReadWriteLayer;
import org.jastacry.layer.ReverseLayer;
import org.jastacry.layer.RotateLayer;
import org.jastacry.layer.TransparentLayer;
import org.jastacry.layer.XorLayer;
/**
* Real working class.
*
* <p>SPDX-License-Identifier: MIT
*
* @author Kai Kretschmann
*/
@SuppressWarnings("PMD.NcssCount")
public class Worker
{
/**
* log4j logger object.
*/
private static final Logger LOGGER = LogManager.getLogger();
/**
* Minimal number of threads needed. Better use all cores.
*/
private static final int MINUMUM_THREADS = 2;
/**
* Char to mark comments.
*/
private static final char TOKEN_COMMENT = ';';
/**
* Token count comparator, means no argument to command.
*/
private static final int TOKENCOUNT_ONE = 1;
/**
* boolean status: do we encode to text transport format.
*/
private boolean doAsciitransport;
/**
* Filename of configuration file.
*/
private String confFilename;
/**
* Some input filename.
*/
private String inputFilename;
/**
* Some output filename.
*/
private String outputFilename;
/**
* Be verbose about every step.
*/
private boolean isVerbose;
/**
* action variable.
*/
private Action action;
/**
* Thread pool object.
*/
private final ThreadPoolExecutor executor;
/**
* Layer thread factory.
*/
private final LayerThreadFactory threadFactory;
/**
* Constructor of Worker class.
*/
public Worker()
{
LOGGER.traceEntry();
final int numCores = Runtime.getRuntime().availableProcessors();
LOGGER.trace("CPU cores available: {}", numCores);
final int numThreads = Math.max(numCores, MINUMUM_THREADS);
LOGGER.trace("Using {} threads in pool", numThreads);
this.threadFactory = new LayerThreadFactory();
this.executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(numThreads);
this.executor.setThreadFactory(threadFactory);
LOGGER.traceExit();
}
/**
* Main method for running a command line interface.
*
* @return int system return code to shell
*/
@SuppressWarnings("squid:S4797") // Handling files is security-sensitive
public final int mainWork()
{
LOGGER.traceEntry();
final List<AbstractBasicLayer> layers = createLayers();
if (layers.isEmpty())
{
LOGGER.error("No layers defined!");
return LOGGER.traceExit(Returncode.RC_ERROR.getNumVal());
} // if
if (layers.size() == 1)
{
LOGGER.warn("Warning: Only one layer defined!");
return LOGGER.traceExit(Returncode.RC_ERROR.getNumVal());
}
// In case of plain text, either encode after layers or decode before
// them.
if (doAsciitransport)
{
final AbstractBasicLayer layerEncode = new AsciiTransportLayer();
switch (action)
{
case ENCODE:
GlobalFunctions.logDebug(isVerbose, LOGGER, "add text encoding to end");
layers.add(layerEncode);
break;
case DECODE: // reverse order
GlobalFunctions.logDebug(isVerbose, LOGGER, "add text encoding to beginning");
layers.add(0, layerEncode);
break;
case UNKOWN:
default:
// will never be reached if main setup function works
// correctly
LOGGER.error("unknown action '{}'", action);
break;
} // switch
}
final File fileIn = new File(inputFilename);
final File fileOut = new File(outputFilename);
try
{
try (InputStream input = new BufferedInputStream(new FileInputStream(fileIn));
OutputStream output = new BufferedOutputStream(new FileOutputStream(fileOut)))
{
loopLayers(layers, input, output);
}
}
catch (final FileNotFoundException e)
{
LOGGER.catching(e);
return LOGGER.traceExit(Returncode.RC_ERROR.getNumVal());
}
catch (final IOException e)
{
LOGGER.catching(e);
}
if (isVerbose)
{
LOGGER.info("JaStaCry finished");
}
return LOGGER.traceExit(Returncode.RC_OK.getNumVal());
}
/**
* Create Array of layer objects.
*
* @return List of abstract layer objects
*/
@SuppressWarnings({"squid:S3776", "squid:S4797"}) // #2 Handling files is security-sensitive
private List<AbstractBasicLayer> createLayers()
{
LOGGER.traceEntry();
final List<AbstractBasicLayer> layers = new ArrayList<>();
// try with resources
try (FileInputStream fstream = new FileInputStream(confFilename);
InputStreamReader isr = new InputStreamReader(fstream, StandardCharsets.UTF_8);
BufferedReader breader = new BufferedReader(isr))
{
String strLine;
AbstractBasicLayer layer = null;
// Read File Line By Line
while ((strLine = breader.readLine()) != null)
{
strLine = strLine.trim();
if (TOKEN_COMMENT == strLine.charAt(0))
{
GlobalFunctions.logDebug(isVerbose, LOGGER, "skip comment line '{}'", strLine);
}
else
{
String sLayer;
String sParams;
final String[] toks = strLine.split("\\s+");
// no parameter?
if (TOKENCOUNT_ONE == toks.length)
{
sLayer = strLine;
sParams = "";
}
else
{
sLayer = toks[0];
sParams = toks[1];
GlobalFunctions.logDebug(isVerbose, LOGGER, "read config, layer={}, params={}", sLayer, sParams);
// Optional interactive password entry
if (sParams.equalsIgnoreCase(org.jastacry.GlobalData.MACRO_PASSWORD))
{
sParams = readPassword(sLayer);
}
}
layer = createLayer(sLayer);
if (null == layer)
{
continue;
}
GlobalFunctions.logDebug(isVerbose, LOGGER, "adding layer {}", sLayer);
layer.init(sParams);
switch (action)
{
case ENCODE:
layers.add(layer);
break;
case DECODE: // reverse order
layers.add(0, layer);
break;
case UNKOWN:
default:
LOGGER.error("unkown action {}", action);
break;
} // switch
} // if comment
} // while
}
catch (final IOException e)
{
LOGGER.catching(e);
}
return LOGGER.traceExit(layers);
}
/**
* read secret password from console interactively.
*
* @param layername for labelling
* @return String for password
*/
@SuppressWarnings("squid:S4829") // Reading the Standard Input is security-sensitive
private String readPassword(final String layername)
{
String passwordString = "";
final String prompt = "Layer " + layername + " Password: ";
final Console console = System.console();
if (null == console)
{
LOGGER.error("No interactive console available for password entry!");
}
else
{
final char[] password = console.readPassword(prompt);
passwordString = new String(password);
} // if
return passwordString;
}
/**
* Create layer objects by given String name.
*
* @param sName name of layer
* @return Layer object
*/
private AbstractBasicLayer createLayer(final String sName)
{
LOGGER.traceEntry(sName);
AbstractBasicLayer layer;
switch (sName.toLowerCase(Locale.getDefault()))
{
case GlobalData.LAYER_TRANSPARENT:
layer = new TransparentLayer();
break;
case GlobalData.LAYER_XOR:
layer = new XorLayer();
break;
case GlobalData.LAYER_ROTATE:
layer = new RotateLayer();
break;
case GlobalData.LAYER_REVERSE:
layer = new ReverseLayer();
break;
case GlobalData.LAYER_RANDOM:
layer = new RandomLayer();
break;
case GlobalData.LAYER_FILEMERGE:
layer = new FilemergeLayer();
break;
case GlobalData.LAYER_MD5DES:
layer = new Md5DesLayer();
break;
case GlobalData.LAYER_AESCBC:
layer = new AesCbcLayer();
break;
case GlobalData.LAYER_AESECB:
layer = new AesEcbLayer();
break;
case GlobalData.LAYER_AESCTR:
layer = new AesCtrLayer();
break;
default:
LOGGER.error("unknown layer '{}'", sName);
layer = null;
break;
} // switch
return LOGGER.traceExit(layer);
} // function
/**
* Loop through layers with data streams.
*
* @param layers Array of layers
* @param input input Stream
* @param output output Stream
* @throws IOException in case of error
*/
@SuppressWarnings("squid:S2093")
private void loopLayers(final List<AbstractBasicLayer> layers, final InputStream input, final OutputStream output)
{
LOGGER.traceEntry();
final CountDownLatch endController = new CountDownLatch(layers.size() + 2);
final List<AbstractBasicLayer> layersWithIo = new ArrayList<>();
final List<InputStream> inputStreams = new ArrayList<>();
final List<OutputStream> outputStreams = new ArrayList<>();
AbstractBasicLayer l = null;
PipedOutputStream prevOutput = null;
PipedOutputStream pipedOutputFromFile = null;
PipedInputStream pipedInputStream = null;
PipedOutputStream pipedOutputStream = null;
PipedInputStream pipedInputStreamToFile = null;
try
{
// Handle file input
pipedOutputFromFile = createOutputPipe();
outputStreams.add(pipedOutputFromFile);
l = new ReadWriteLayer();
l.setInputStream(input);
l.setOutputStream(pipedOutputFromFile);
l.setAction(action);
l.setEndController(endController);
layersWithIo.add(l);
// Handle very first layer
l = layers.get(0);
GlobalFunctions.logDebug(isVerbose, LOGGER, "layer FIRST '{}'", l);
pipedInputStream = createInputPipe();
pipedOutputStream = createOutputPipe();
inputStreams.add(pipedInputStream);
outputStreams.add(pipedOutputStream);
pipedInputStream.connect(pipedOutputFromFile);
prevOutput = pipedOutputStream;
l.setInputStream(pipedInputStream);
l.setOutputStream(pipedOutputStream);
l.setAction(action);
l.setEndController(endController);
layersWithIo.add(l);
// only second and further layers are looped through
for (int i = 1; i < layers.size(); i++)
{
l = layers.get(i);
GlobalFunctions.logDebug(isVerbose, LOGGER, "layer {} '{}'", i, l);
pipedInputStream = createInputPipe();
pipedOutputStream = createOutputPipe();
inputStreams.add(pipedInputStream);
outputStreams.add(pipedOutputStream);
pipedInputStream.connect(prevOutput);
prevOutput = pipedOutputStream;
l.setInputStream(pipedInputStream);
l.setOutputStream(pipedOutputStream);
l.setAction(action);
l.setEndController(endController);
layersWithIo.add(l);
} // for
// Handle file output as very last layer
pipedInputStreamToFile = createInputPipe();
inputStreams.add(pipedInputStreamToFile);
pipedInputStreamToFile.connect(prevOutput);
l = new ReadWriteLayer();
l.setInputStream(pipedInputStreamToFile);
l.setOutputStream(output);
l.setAction(action);
l.setEndController(endController);
layersWithIo.add(l);
// Start all threads
for (int i = 0; i < layersWithIo.size(); i++)
{
GlobalFunctions.logDebug(isVerbose, LOGGER, "start thread {}", i);
final AbstractBasicLayer layer = layersWithIo.get(i);
threadFactory.setNumber(i);
executor.execute(layer);
} // for
// wait for all threads
waitForThreads(endController);
}
catch (final IOException e)
{
LOGGER.catching(e);
}
finally
{
try
{
for (final InputStream inputStream : inputStreams)
{
inputStream.close();
} // for
for (final OutputStream outputStream : outputStreams)
{
outputStream.close();
} // for
inputStreams.clear();
outputStreams.clear();
}
catch (final IOException e)
{
LOGGER.catching(e);
}
}
LOGGER.traceExit();
} // function
/**
* Create object outside of a loop.
*
* @return created object
*/
private PipedInputStream createInputPipe()
{
return new PipedInputStream();
}
/**
* Create object outside of a loop.
*
* @return created object
*/
private PipedOutputStream createOutputPipe()
{
return new PipedOutputStream();
}
/**
* Wait for all threads to end.
*
* @param endController the controller which counts the threads
*/
private void waitForThreads(final CountDownLatch endController)
{
try
{
endController.await();
}
catch (final InterruptedException e)
{
LOGGER.catching(e);
Thread.currentThread().interrupt();
}
}
/**
* Destroy just like a inverted constructor function.
*/
public final void destroy()
{
executor.shutdown();
}
/**
* Setter method for ascii transport.
*
* @param bStatus the doEncode to set
*/
public final void setDoAsciitransport(final boolean bStatus)
{
doAsciitransport = bStatus;
}
/**
* Setter method for config file name.
*
* @param sFilename the confFilename to set
*/
public final void setConfFilename(final String sFilename)
{
confFilename = sFilename;
}
/**
* Setter method for input file name.
*
* @param sFilename the inputFilename to set
*/
public final void setInputFilename(final String sFilename)
{
inputFilename = sFilename;
}
/**
* Setter method for output file name.
*
* @param sFilename the outputFilename to set
*/
public final void setOutputFilename(final String sFilename)
{
outputFilename = sFilename;
}
/**
* Setter method for verbosity.
*
* @param bStatus the isVerbose to set
*/
public final void setVerbose(final boolean bStatus)
{
isVerbose = bStatus;
}
/**
* Setter method for action value.
*
* @param oAction the action to set
*/
public final void setAction(final Action oAction)
{
this.action = oAction;
}
}