View Javadoc
1   package org.jastacry;
2   
3   import java.io.BufferedInputStream;
4   import java.io.BufferedOutputStream;
5   import java.io.BufferedReader;
6   import java.io.Console;
7   import java.io.File;
8   import java.io.FileInputStream;
9   import java.io.FileNotFoundException;
10  import java.io.FileOutputStream;
11  import java.io.IOException;
12  import java.io.InputStream;
13  import java.io.InputStreamReader;
14  import java.io.OutputStream;
15  import java.io.PipedInputStream;
16  import java.io.PipedOutputStream;
17  import java.nio.charset.StandardCharsets;
18  import java.util.ArrayList;
19  import java.util.List;
20  import java.util.Locale;
21  import java.util.concurrent.CountDownLatch;
22  import java.util.concurrent.Executors;
23  import java.util.concurrent.ThreadPoolExecutor;
24  
25  import org.apache.logging.log4j.LogManager;
26  import org.apache.logging.log4j.Logger;
27  import org.jastacry.GlobalData.Action;
28  import org.jastacry.GlobalData.Returncode;
29  import org.jastacry.layer.AbstractBasicLayer;
30  import org.jastacry.layer.AesCbcLayer;
31  import org.jastacry.layer.AesCtrLayer;
32  import org.jastacry.layer.AesEcbLayer;
33  import org.jastacry.layer.AsciiTransportLayer;
34  import org.jastacry.layer.FilemergeLayer;
35  import org.jastacry.layer.Md5DesLayer;
36  import org.jastacry.layer.RandomLayer;
37  import org.jastacry.layer.ReadWriteLayer;
38  import org.jastacry.layer.ReverseLayer;
39  import org.jastacry.layer.RotateLayer;
40  import org.jastacry.layer.TransparentLayer;
41  import org.jastacry.layer.XorLayer;
42  
43  /**
44   * Real working class.
45   *
46   * <p>SPDX-License-Identifier: MIT
47   *
48   * @author Kai Kretschmann
49   */
50  @SuppressWarnings("PMD.NcssCount")
51  public class Worker
52  {
53      /**
54       * log4j logger object.
55       */
56      private static final Logger LOGGER = LogManager.getLogger();
57  
58      /**
59       * Minimal number of threads needed. Better use all cores.
60       */
61      private static final int MINUMUM_THREADS = 2;
62  
63      /**
64       * Char to mark comments.
65       */
66      private static final char TOKEN_COMMENT = ';';
67  
68      /**
69       * Token count comparator, means no argument to command.
70       */
71      private static final int TOKENCOUNT_ONE = 1;
72  
73      /**
74       * boolean status: do we encode to text transport format.
75       */
76      private boolean doAsciitransport;
77  
78      /**
79       * Filename of configuration file.
80       */
81      private String confFilename;
82  
83      /**
84       * Some input filename.
85       */
86      private String inputFilename;
87  
88      /**
89       * Some output filename.
90       */
91      private String outputFilename;
92  
93      /**
94       * Be verbose about every step.
95       */
96      private boolean isVerbose;
97  
98      /**
99       * action variable.
100      */
101     private Action action;
102 
103     /**
104      * Thread pool object.
105      */
106     private final ThreadPoolExecutor executor;
107 
108     /**
109      * Layer thread factory.
110      */
111     private final LayerThreadFactory threadFactory;
112 
113     /**
114      * Constructor of Worker class.
115      */
116     public Worker()
117     {
118         LOGGER.traceEntry();
119         final int numCores = Runtime.getRuntime().availableProcessors();
120         LOGGER.trace("CPU cores available: {}", numCores);
121         final int numThreads = Math.max(numCores, MINUMUM_THREADS);
122         LOGGER.trace("Using {} threads in pool", numThreads);
123 
124         this.threadFactory = new LayerThreadFactory();
125         this.executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(numThreads);
126         this.executor.setThreadFactory(threadFactory);
127         LOGGER.traceExit();
128     }
129 
130     /**
131      * Main method for running a command line interface.
132      *
133      * @return int system return code to shell
134      */
135     @SuppressWarnings("squid:S4797") // Handling files is security-sensitive
136     public final int mainWork()
137     {
138         LOGGER.traceEntry();
139         final List<AbstractBasicLayer> layers = createLayers();
140 
141         if (layers.isEmpty())
142         {
143             LOGGER.error("No layers defined!");
144             return LOGGER.traceExit(Returncode.RC_ERROR.getNumVal());
145         } // if
146 
147         if (layers.size() == 1)
148         {
149             LOGGER.warn("Warning: Only one layer defined!");
150             return LOGGER.traceExit(Returncode.RC_ERROR.getNumVal());
151         }
152 
153         // In case of plain text, either encode after layers or decode before
154         // them.
155         if (doAsciitransport)
156         {
157             final AbstractBasicLayer layerEncode = new AsciiTransportLayer();
158             switch (action)
159             {
160                 case ENCODE:
161                     GlobalFunctions.logDebug(isVerbose, LOGGER, "add text encoding to end");
162                     layers.add(layerEncode);
163                     break;
164                 case DECODE: // reverse order
165                     GlobalFunctions.logDebug(isVerbose, LOGGER, "add text encoding to beginning");
166                     layers.add(0, layerEncode);
167                     break;
168                 case UNKOWN:
169                 default:
170                     // will never be reached if main setup function works
171                     // correctly
172                     LOGGER.error("unknown action '{}'", action);
173                     break;
174             } // switch
175         }
176 
177         final File fileIn = new File(inputFilename);
178         final File fileOut = new File(outputFilename);
179 
180         try
181         {
182             try (InputStream input = new BufferedInputStream(new FileInputStream(fileIn));
183                     OutputStream output = new BufferedOutputStream(new FileOutputStream(fileOut)))
184             {
185                 loopLayers(layers, input, output);
186             }
187         }
188         catch (final FileNotFoundException e)
189         {
190             LOGGER.catching(e);
191             return LOGGER.traceExit(Returncode.RC_ERROR.getNumVal());
192         }
193         catch (final IOException e)
194         {
195             LOGGER.catching(e);
196         }
197 
198         if (isVerbose)
199         {
200             LOGGER.info("JaStaCry finished");
201         }
202 
203         return LOGGER.traceExit(Returncode.RC_OK.getNumVal());
204     }
205 
206     /**
207      * Create Array of layer objects.
208      *
209      * @return List of abstract layer objects
210      */
211     @SuppressWarnings({"squid:S3776", "squid:S4797"}) // #2 Handling files is security-sensitive
212     private List<AbstractBasicLayer> createLayers()
213     {
214         LOGGER.traceEntry();
215         final List<AbstractBasicLayer> layers = new ArrayList<>();
216 
217         // try with resources
218         try (FileInputStream fstream = new FileInputStream(confFilename);
219                 InputStreamReader isr = new InputStreamReader(fstream, StandardCharsets.UTF_8);
220                 BufferedReader breader = new BufferedReader(isr))
221         {
222             String strLine;
223 
224             AbstractBasicLayer layer = null;
225 
226             // Read File Line By Line
227             while ((strLine = breader.readLine()) != null)
228             {
229                 strLine = strLine.trim();
230                 if (TOKEN_COMMENT == strLine.charAt(0))
231                 {
232                     GlobalFunctions.logDebug(isVerbose, LOGGER, "skip comment line '{}'", strLine);
233                 }
234                 else
235                 {
236 
237                     String sLayer;
238                     String sParams;
239 
240                     final String[] toks = strLine.split("\\s+");
241                     // no parameter?
242                     if (TOKENCOUNT_ONE == toks.length)
243                     {
244                         sLayer = strLine;
245                         sParams = "";
246                     }
247                     else
248                     {
249                         sLayer = toks[0];
250                         sParams = toks[1];
251                         GlobalFunctions.logDebug(isVerbose, LOGGER, "read config, layer={}, params={}", sLayer, sParams);
252 
253                         // Optional interactive password entry
254                         if (sParams.equalsIgnoreCase(org.jastacry.GlobalData.MACRO_PASSWORD))
255                         {
256                             sParams = readPassword(sLayer);
257                         }
258                     }
259 
260                     layer = createLayer(sLayer);
261                     if (null == layer)
262                     {
263                         continue;
264                     }
265                     GlobalFunctions.logDebug(isVerbose, LOGGER, "adding layer {}", sLayer);
266 
267                     layer.init(sParams);
268                     switch (action)
269                     {
270                         case ENCODE:
271                             layers.add(layer);
272                             break;
273                         case DECODE: // reverse order
274                             layers.add(0, layer);
275                             break;
276                         case UNKOWN:
277                         default:
278                             LOGGER.error("unkown action {}", action);
279                             break;
280                     } // switch
281                 } // if comment
282             } // while
283 
284         }
285         catch (final IOException e)
286         {
287             LOGGER.catching(e);
288         }
289 
290         return LOGGER.traceExit(layers);
291     }
292 
293     /**
294      * read secret password from console interactively.
295      *
296      * @param layername for labelling
297      * @return String for password
298      */
299     @SuppressWarnings("squid:S4829") // Reading the Standard Input is security-sensitive
300     private String readPassword(final String layername)
301     {
302         String passwordString = "";
303         final String prompt = "Layer " + layername + " Password: ";
304         final Console console = System.console();
305 
306         if (null == console)
307         {
308             LOGGER.error("No interactive console available for password entry!");
309         }
310         else
311         {
312             final char[] password = console.readPassword(prompt);
313             passwordString = new String(password);
314         } // if
315 
316         return passwordString;
317     }
318 
319     /**
320      * Create layer objects by given String name.
321      *
322      * @param sName name of layer
323      * @return Layer object
324      */
325     private AbstractBasicLayer createLayer(final String sName)
326     {
327         LOGGER.traceEntry(sName);
328         AbstractBasicLayer layer;
329 
330         switch (sName.toLowerCase(Locale.getDefault()))
331         {
332             case GlobalData.LAYER_TRANSPARENT:
333                 layer = new TransparentLayer();
334                 break;
335             case GlobalData.LAYER_XOR:
336                 layer = new XorLayer();
337                 break;
338             case GlobalData.LAYER_ROTATE:
339                 layer = new RotateLayer();
340                 break;
341             case GlobalData.LAYER_REVERSE:
342                 layer = new ReverseLayer();
343                 break;
344             case GlobalData.LAYER_RANDOM:
345                 layer = new RandomLayer();
346                 break;
347             case GlobalData.LAYER_FILEMERGE:
348                 layer = new FilemergeLayer();
349                 break;
350             case GlobalData.LAYER_MD5DES:
351                 layer = new Md5DesLayer();
352                 break;
353             case GlobalData.LAYER_AESCBC:
354                 layer = new AesCbcLayer();
355                 break;
356             case GlobalData.LAYER_AESECB:
357                 layer = new AesEcbLayer();
358                 break;
359             case GlobalData.LAYER_AESCTR:
360                 layer = new AesCtrLayer();
361                 break;
362             default:
363                 LOGGER.error("unknown layer '{}'", sName);
364                 layer = null;
365                 break;
366         } // switch
367 
368         return LOGGER.traceExit(layer);
369     } // function
370 
371     /**
372      * Loop through layers with data streams.
373      *
374      * @param layers Array of layers
375      * @param input input Stream
376      * @param output output Stream
377      * @throws IOException in case of error
378      */
379     @SuppressWarnings("squid:S2093")
380     private void loopLayers(final List<AbstractBasicLayer> layers, final InputStream input, final OutputStream output)
381     {
382         LOGGER.traceEntry();
383 
384         final CountDownLatch endController = new CountDownLatch(layers.size() + 2);
385         final List<AbstractBasicLayer> layersWithIo = new ArrayList<>();
386         final List<InputStream> inputStreams = new ArrayList<>();
387         final List<OutputStream> outputStreams = new ArrayList<>();
388 
389         AbstractBasicLayer l = null;
390         PipedOutputStream prevOutput = null;
391         PipedOutputStream pipedOutputFromFile = null;
392         PipedInputStream pipedInputStream = null;
393         PipedOutputStream pipedOutputStream = null;
394         PipedInputStream pipedInputStreamToFile = null;
395 
396         try
397         {
398             // Handle file input
399             pipedOutputFromFile = createOutputPipe();
400             outputStreams.add(pipedOutputFromFile);
401             l = new ReadWriteLayer();
402             l.setInputStream(input);
403             l.setOutputStream(pipedOutputFromFile);
404             l.setAction(action);
405             l.setEndController(endController);
406             layersWithIo.add(l);
407 
408             // Handle very first layer
409             l = layers.get(0);
410             GlobalFunctions.logDebug(isVerbose, LOGGER, "layer FIRST '{}'", l);
411             pipedInputStream = createInputPipe();
412             pipedOutputStream = createOutputPipe();
413             inputStreams.add(pipedInputStream);
414             outputStreams.add(pipedOutputStream);
415             pipedInputStream.connect(pipedOutputFromFile);
416             prevOutput = pipedOutputStream;
417             l.setInputStream(pipedInputStream);
418             l.setOutputStream(pipedOutputStream);
419             l.setAction(action);
420             l.setEndController(endController);
421             layersWithIo.add(l);
422 
423             // only second and further layers are looped through
424             for (int i = 1; i < layers.size(); i++)
425             {
426                 l = layers.get(i);
427 
428                 GlobalFunctions.logDebug(isVerbose, LOGGER, "layer {} '{}'", i, l);
429 
430                 pipedInputStream = createInputPipe();
431                 pipedOutputStream = createOutputPipe();
432                 inputStreams.add(pipedInputStream);
433                 outputStreams.add(pipedOutputStream);
434                 pipedInputStream.connect(prevOutput);
435                 prevOutput = pipedOutputStream;
436                 l.setInputStream(pipedInputStream);
437                 l.setOutputStream(pipedOutputStream);
438                 l.setAction(action);
439                 l.setEndController(endController);
440                 layersWithIo.add(l);
441             } // for
442 
443             // Handle file output as very last layer
444             pipedInputStreamToFile = createInputPipe();
445             inputStreams.add(pipedInputStreamToFile);
446             pipedInputStreamToFile.connect(prevOutput);
447             l = new ReadWriteLayer();
448             l.setInputStream(pipedInputStreamToFile);
449             l.setOutputStream(output);
450             l.setAction(action);
451             l.setEndController(endController);
452             layersWithIo.add(l);
453 
454             // Start all threads
455             for (int i = 0; i < layersWithIo.size(); i++)
456             {
457                 GlobalFunctions.logDebug(isVerbose, LOGGER, "start thread {}", i);
458                 final AbstractBasicLayer layer = layersWithIo.get(i);
459                 threadFactory.setNumber(i);
460                 executor.execute(layer);
461             } // for
462 
463             // wait for all threads
464             waitForThreads(endController);
465 
466         }
467         catch (final IOException e)
468         {
469             LOGGER.catching(e);
470         }
471         finally
472         {
473             try
474             {
475                 for (final InputStream inputStream : inputStreams)
476                 {
477                     inputStream.close();
478                 } // for
479                 for (final OutputStream outputStream : outputStreams)
480                 {
481                     outputStream.close();
482                 } // for
483                 inputStreams.clear();
484                 outputStreams.clear();
485             }
486             catch (final IOException e)
487             {
488                 LOGGER.catching(e);
489             }
490         }
491 
492         LOGGER.traceExit();
493     } // function
494 
495     /**
496      * Create object outside of a loop.
497      *
498      * @return created object
499      */
500     private PipedInputStream createInputPipe()
501     {
502         return new PipedInputStream();
503     }
504 
505     /**
506      * Create object outside of a loop.
507      *
508      * @return created object
509      */
510     private PipedOutputStream createOutputPipe()
511     {
512         return new PipedOutputStream();
513     }
514 
515     /**
516      * Wait for all threads to end.
517      *
518      * @param endController the controller which counts the threads
519      */
520     private void waitForThreads(final CountDownLatch endController)
521     {
522         try
523         {
524             endController.await();
525         }
526         catch (final InterruptedException e)
527         {
528             LOGGER.catching(e);
529             Thread.currentThread().interrupt();
530         }
531     }
532 
533     /**
534      * Destroy just like a inverted constructor function.
535      */
536     public final void destroy()
537     {
538         executor.shutdown();
539     }
540 
541     /**
542      * Setter method for ascii transport.
543      *
544      * @param bStatus the doEncode to set
545      */
546     public final void setDoAsciitransport(final boolean bStatus)
547     {
548         doAsciitransport = bStatus;
549     }
550 
551     /**
552      * Setter method for config file name.
553      *
554      * @param sFilename the confFilename to set
555      */
556     public final void setConfFilename(final String sFilename)
557     {
558         confFilename = sFilename;
559     }
560 
561     /**
562      * Setter method for input file name.
563      *
564      * @param sFilename the inputFilename to set
565      */
566     public final void setInputFilename(final String sFilename)
567     {
568         inputFilename = sFilename;
569     }
570 
571     /**
572      * Setter method for output file name.
573      *
574      * @param sFilename the outputFilename to set
575      */
576     public final void setOutputFilename(final String sFilename)
577     {
578         outputFilename = sFilename;
579     }
580 
581     /**
582      * Setter method for verbosity.
583      *
584      * @param bStatus the isVerbose to set
585      */
586     public final void setVerbose(final boolean bStatus)
587     {
588         isVerbose = bStatus;
589     }
590 
591     /**
592      * Setter method for action value.
593      *
594      * @param oAction the action to set
595      */
596     public final void setAction(final Action oAction)
597     {
598         this.action = oAction;
599     }
600 
601 }