11import java .io .File ;
2+ import java .io .FileOutputStream ;
23import java .io .IOException ;
34import java .io .InputStream ;
45import java .io .OutputStream ;
1617import javax .crypto .Cipher ;
1718import javax .crypto .CipherInputStream ;
1819import javax .crypto .CipherOutputStream ;
20+ import javax .crypto .Mac ;
1921import javax .crypto .NoSuchPaddingException ;
2022import javax .crypto .spec .IvParameterSpec ;
2123import javax .crypto .spec .SecretKeySpec ;
@@ -30,54 +32,75 @@ public class FileEncryptor {
3032 private static final Logger LOG = Logger .getLogger (FileEncryptor .class .getSimpleName ());
3133
3234 private static final String ALGORITHM = "AES" ;
35+ private static final String HASH_AlGORITHM = "HmacSHA256" ;
3336 private static final String CIPHER = "AES/CBC/PKCS5PADDING" ;
3437
3538 public static void main (String [] args ) throws Exception {
3639 // Error Message
37- final String validCmdMsg = "Valid Encryption command: java FileEncryptor enc [inputFile] [outputFile]\n "
38- + "Valid Decryption command: java FileEncryptor dec [Key] [Vector] [inputFile] [outputFile]" ;
40+ final String validCmdMsg = "Valid Encryption command: java FileEncryptor enc [Key] [inputFile] [outputFile]\n "
41+ + "Valid Decryption command: java FileEncryptor dec [Key] [inputFile] [outputFile]\n "
42+ + "Valid Key generation command: java FileEncryptor key\n "
43+ + "NOTE: The key specified must be Base64 Encoded" ;
3944
40- if (args .length < 3 ) { throw new IllegalArgumentException ("Not Enough Argunments specified\n " + validCmdMsg ); }
45+ if (args .length < 1 ) { throw new IllegalArgumentException ("Not Enough Argunments specified\n " + validCmdMsg ); }
4146
4247 // Convert String arguments to char arrays
4348 char [][] charArgs = Util .getCharArgunments (args );
4449
4550 // Clear String argunments
4651 Arrays .fill (args , null );
4752
48- if (Arrays .equals (charArgs [0 ], "enc" .toCharArray ())) { // Encrypt
49- encrypt (new String (charArgs [1 ]), new String (charArgs [2 ]));
50-
51- } else if (Arrays .equals (charArgs [0 ], "dec" .toCharArray ())) { // Decrypt
53+ // Generate and display key, for testing and marking purposes
54+ if (Arrays .equals (charArgs [0 ], "key" .toCharArray ())) {
55+ generateKey ();
56+ return ;
57+ }
5258
53- if (charArgs .length < 5 ) { throw new IllegalArgumentException ("Not Enough Argunments Provided for Decryption\n " + validCmdMsg ); }
59+ if (charArgs .length < 4 ) {
60+ throw new IllegalArgumentException ("Not Enough Argunments Provided\n " + validCmdMsg );
61+ }
5462
55- // Decode the Base64 argunments
56- byte [] key = Base64 .getDecoder ().decode (Util .convertCharToByte (charArgs [1 ]));
57- byte [] initVector = Base64 .getDecoder ().decode (Util .convertCharToByte (charArgs [2 ]));
58-
59- decrypt (key , initVector , new String (charArgs [3 ]), new String (charArgs [4 ]));
63+ // Options Available
64+ char [] enc = "enc" .toCharArray ();
65+ char [] dec = "dec" .toCharArray ();
6066
61- // Tear Down, clear arrays
62- Arrays .fill (key , (byte ) 0 );
63- Arrays .fill (initVector , (byte ) 0 );
64- key = null ; initVector = null ;
67+ if (!Arrays .equals (charArgs [0 ], enc ) && !Arrays .equals (charArgs [0 ], dec )) {
68+ throw new IllegalArgumentException ("Neither enc (encrypt) or dec (decrypt) option specified\n " + validCmdMsg );
69+ }
6570
66- for (int i = 0 ; i < charArgs .length ; i ++) {
67- Arrays .fill (charArgs [i ], '\0' );
68- }
69- charArgs = null ;
71+ byte [] key ;
72+ try {
73+ key = Base64 .getDecoder ().decode (Util .convertCharToByte (charArgs [1 ]));
74+ } catch (IllegalArgumentException e ) {
75+ throw new IllegalArgumentException ("Key provided must be Base64 encoded\n " + validCmdMsg );
76+ }
7077
71- } else {
72- throw new IllegalArgumentException ("Neither enc (encrypt) or dec (decrypt) option specified\n " + validCmdMsg );
73- }
78+ if (Arrays .equals (charArgs [0 ], enc )) { // Encrypt
79+ encrypt (key , new String (charArgs [2 ]), new String (charArgs [3 ]));
80+
81+ } else if (Arrays .equals (charArgs [0 ], dec )) { // Decrypt
82+ decrypt (key , new String (charArgs [2 ]), new String (charArgs [3 ]));
83+
84+ }
85+
86+ // Tear Down, clear arrays
87+ Arrays .fill (key , (byte ) 0 );
88+ Arrays .fill (enc , '\0' ); Arrays .fill (dec , '\0' );
89+
90+ for (int i = 0 ; i < charArgs .length ; i ++) {
91+ Arrays .fill (charArgs [i ], '\0' );
92+ }
93+ charArgs = null ; key = null ; dec = null ; enc = null ;
7494 }
7595
7696 /**
7797 * Encrypts a plain text input file by outputing an encrypted version. It does this
7898 * generating a 128 bit secret key and initialisation vector which are used as the
79- * specifications during the file encryption process.
99+ * specifications during the file encryption process. A message aithentication code
100+ * is also computed with the intialisaton vector and plaintext values, hence these
101+ * values can be checked for tampering during decryption.
80102 *
103+ * @param key byte[] The secrect key which will be used to encrypt the file
81104 * @param inputPath - A String specifying the Input path of the plaintext file
82105 * @param outputPath - A String specifying the Ouput path of the ciphertext file
83106 * @throws NoSuchAlgorithmException
@@ -86,26 +109,23 @@ public static void main(String[] args) throws Exception {
86109 * @throws InvalidAlgorithmParameterException
87110 * @throws IOException
88111 */
89- public static void encrypt (String inputPath , String outputPath ) throws NoSuchAlgorithmException ,
112+ public static void encrypt (byte [] key , String inputPath , String outputPath ) throws NoSuchAlgorithmException ,
90113 NoSuchPaddingException , InvalidKeyException , InvalidAlgorithmParameterException , IOException {
91- //This snippet is literally copied from SymmetrixExample
114+ //Generate Initilisation Vector
92115 SecureRandom sr = new SecureRandom ();
93- byte [] key = new byte [16 ];
94- sr .nextBytes (key ); // 128 bit key
95116 byte [] initVector = new byte [16 ];
96117 sr .nextBytes (initVector ); // 16 bytes IV
97118
98- // Display the Base64 encoded versions of Key and Vector
99- System .out .print ("\n <---------------------------------------->\n " );
100- System .out .println ("Secret Key is: " + Base64 .getEncoder ().encodeToString (key ));
101- System .out .println ("IV is: " + Base64 .getEncoder ().encodeToString (initVector ));
102- System .out .print ("<---------------------------------------->\n \n " );
103-
104- // Initialize Key, Vector Specfications and the Cipher mode
119+ // Initialize Vector and Keys
105120 IvParameterSpec iv = new IvParameterSpec (initVector );
106121 SecretKeySpec skeySpec = new SecretKeySpec (key , ALGORITHM );
122+ SecretKeySpec macKey = new SecretKeySpec (key , HASH_AlGORITHM );
123+
124+ // Initialize cipher and Mac
107125 Cipher cipher = Cipher .getInstance (CIPHER );
108126 cipher .init (Cipher .ENCRYPT_MODE , skeySpec , iv );
127+ Mac hmac = Mac .getInstance (HASH_AlGORITHM );
128+ hmac .init (macKey );
109129
110130 File outputFile = new File (outputPath );
111131 // Create the output file if it doesn't exist
@@ -114,52 +134,104 @@ public static void encrypt(String inputPath, String outputPath) throws NoSuchAlg
114134 final Path plaintextFile = Paths .get (inputPath );
115135 final Path encryptedFile = Paths .get (outputPath );
116136
137+ // Compute Mac for authentication
138+ hmac .update (initVector );
139+ byte [] mac = computeMac (hmac , plaintextFile );
140+
141+ // Display the Base64 encoded versions of Vector and computed mac
142+ System .out .print ("\n <---------------------------------------->\n " );
143+ System .out .println ("IV is: " + Base64 .getEncoder ().encodeToString (initVector ));
144+ System .out .println ("Computed Mac: " + Base64 .getEncoder ().encodeToString (mac ));
145+ System .out .print ("<---------------------------------------->\n \n " );
146+
117147 // Write plaintext into ciphertext
118- if (writeEncryptedFile (plaintextFile , encryptedFile , cipher )) {
148+ if (writeEncryptedFile (plaintextFile , encryptedFile , cipher , mac )) {
119149 LOG .info ("Encryption finished, saved at " + encryptedFile );
120150 } else {
121- LOG .log (Level .WARNING , "Encryption Failed, Ensure Valid File Paths are specified " );
151+ LOG .log (Level .WARNING , "Encryption Failed" );
122152 }
123153 }
124154
125155 /**
126- * Writes an encrypted version of the input file, into the output file.
156+ * Writes an encrypted version of the input file, into a new output file.
127157 * Uses a FileInputStream to read the plaintext file and wraps the OutputStream
128- * with a CipherOutStream to write an encrypted version of the plaintext file.
129- * Returns True if the encryption writing was successfull, False otherwise.
158+ * with a CipherOutStream to write an encrypted version. Prior to writing the
159+ * encrypted data, IV and the computed mac is saved as metadata in the encrypted
160+ * file with the use of a FileOutputStream. Returns True if the encryption writing
161+ * was successfull, False otherwise.
130162 *
131163 * @param inputPath Path The file path of the input file (plaintext)
132164 * @param outputPath Path The file path of the output file (ciphertext)
133165 * @param cipher Cipher The cipher instance initialized with the appropriate
134166 * specifications in ENCRYPT mode
135167 * @return boolean True if encryption successful False otherwise
136168 */
137- private static boolean writeEncryptedFile (Path inputPath , Path outputPath , Cipher cipher ) {
138- try (InputStream fin = Files .newInputStream (inputPath );
139- OutputStream fout = Files .newOutputStream (outputPath );
140- CipherOutputStream cipherOut = new CipherOutputStream (fout , cipher ) {
141- }) {
142- final byte [] bytes = new byte [1024 ];
143- for (int length = fin .read (bytes ); length != -1 ; length = fin .read (bytes )){
144- cipherOut .write (bytes , 0 , length );
169+ private static boolean writeEncryptedFile (Path inputPath , Path outputPath , Cipher cipher , byte [] mac ) {
170+ try (InputStream fin = Files .newInputStream (inputPath );) {
171+
172+ try (FileOutputStream fout = new FileOutputStream (outputPath .toFile ());) {
173+ // Write Metadata
174+ fout .write (cipher .getIV ());
175+ fout .write (mac );
176+
177+ try (CipherOutputStream cipherOut = new CipherOutputStream (fout , cipher );) {
178+ final byte [] bytes = new byte [1024 ];
179+ for (int length = fin .read (bytes ); length != -1 ; length = fin .read (bytes )){
180+ cipherOut .write (bytes , 0 , length );
181+ }
182+ }
145183 }
146184 } catch (IOException e ) {
147- LOG .log (Level .INFO , "Unable to encrypt " );
185+ LOG .log (Level .WARNING , "Ensure Valid File paths are specified. " );
148186 return false ;
149187 }
150188
151189 return true ;
152190 }
153191
192+ /**
193+ * Computes a Message authencitaion code for a given inputfile
194+ * Takes in an initialised hmac which gets updated with the file's
195+ * contents line by line. Once completed the doFinal method will
196+ * return a byte array with the computed Mac.
197+ *
198+ * @param hmac Mac The initialised Mac object
199+ * @param filePath Path The file path
200+ * @return byte[] The file's computed Mac
201+ */
202+ private static byte [] computeMac (Mac hmac , Path filePath ) {
203+ try (InputStream fin = Files .newInputStream (filePath );) {
204+ final byte [] bytes = new byte [1024 ];
205+ for (int length = fin .read (bytes ); length != -1 ; length = fin .read (bytes )){
206+ hmac .update (bytes , 0 , length );
207+ }
208+ } catch (IOException e ) {
209+
210+ }
211+
212+ return hmac .doFinal ();
213+ }
214+
215+ /**
216+ * This function is invoked when the 'key' option is specified
217+ * in the command line. Generates a random 128 bit key which
218+ * gets printed out in Base64 encoded form to the command line.
219+ * For marking purposes and ease of testing for Part 2.
220+ */
221+ private static void generateKey () {
222+ SecureRandom sr = new SecureRandom ();
223+ byte [] key = new byte [16 ];
224+ sr .nextBytes (key );
225+ System .out .println ("\n Secret Key is: " + Base64 .getEncoder ().encodeToString (key ) + "\n " );
226+ }
227+
154228 /**
155229 * Decrypts a given cipertext file into its original plaintext form.
156- * A successful decryption occurs when provided with the right key and
157- * initialisation vector to create the Cipher specifications required
158- * for decryption. Will overwrite the resultant output file if it
159- * already exists.
230+ * A successful decryption occurs when provided with the right key
231+ * to create the Cipher specifications required for decryption.
232+ * Will overwrite the resultant output file if it already exists.
160233 *
161234 * @param key byte[] - The Key used to originally encrypt the input file
162- * @param initVector byte[] - The initialisation vector originally used for encryption
163235 * @param inputPath String - The input file path (encrypted document)
164236 * @param outputPath String - The file path of the resultant decrypted text
165237 * @throws NoSuchAlgorithmException
@@ -168,54 +240,86 @@ private static boolean writeEncryptedFile(Path inputPath, Path outputPath, Ciphe
168240 * @throws InvalidAlgorithmParameterException
169241 * @throws IOException
170242 */
171- public static void decrypt (byte [] key , byte [] initVector , String inputPath , String outputPath ) throws NoSuchAlgorithmException ,
172- NoSuchPaddingException , InvalidKeyException , InvalidAlgorithmParameterException , IOException {
173- // Initialize Key and Vector Specifications and the Cipher Mode
174- IvParameterSpec iv = new IvParameterSpec (initVector );
175- SecretKeySpec skeySpec = new SecretKeySpec (key , ALGORITHM );
176- Cipher cipher = Cipher .getInstance (CIPHER );
177- cipher .init (Cipher .DECRYPT_MODE , skeySpec , iv );
178-
243+ public static void decrypt (byte [] key , String inputPath , String outputPath ) throws IOException ,
244+ NoSuchAlgorithmException , NoSuchPaddingException , InvalidKeyException , InvalidAlgorithmParameterException {
245+
179246 File outputFile = new File (outputPath );
180247 // Create a new Output file if it doesn't exist
181248 if (!outputFile .exists ()) { outputFile .createNewFile (); }
182249
183250 final Path encryptedFile = Paths .get (inputPath );
184251 final Path decryptedFile = Paths .get (outputPath );
185252
186- if (writeDecryptedFile (encryptedFile , decryptedFile , cipher )) {
253+ if (writeDecryptedFile (encryptedFile , decryptedFile , key )) {
187254 LOG .info ("Decryption complete, open " + decryptedFile );
188255 } else {
189- LOG .log (Level .SEVERE , "Ensure the correct Key, Vector, and Files paths are specified" );
256+ LOG .log (Level .SEVERE , "Decryption failed: Ensure the correct Key and Files paths are specified" );
190257 }
191258 }
192259
193260 /**
194261 * Reads an encrypted file by wrapping an InputStream with a CipherInputStream
195262 * The encrypted files gets decrypted and written out to the output file.
196- * For a successful decryption the Cipher new to be initialized in DECRYPT mode
197- * with the correct key and vector specifications.
263+ * For a successful decryption the Cipher needs to be initialized in DECRYPT mode
264+ * with the correct key and vector specifications. The IV is read from the encrypted
265+ * file as it was saved unencrypted during the encryption process. Decryption will
266+ * also fail if the computed authentication code doesn't match with the given
267+ * authentication code, which it also reads from the encrpted file.
198268 *
199269 * @param inputPath Path The input file path (encrypted file)
200270 * @param outputPath Path The output file path (decrypted file)
201- * @param cipher Cipher The cipher instance initialized with the appropriate
202- * specifications in DECRYPT mode
271+ * @param key byte[] The secret key which will be used for decryption
203272 * @return boolean True if Decryption is successful False otherwise
273+ * @throws NoSuchPaddingException
274+ * @throws NoSuchAlgorithmException
275+ * @throws InvalidKeyException
276+ * @throws InvalidAlgorithmParameterException
204277 */
205- private static boolean writeDecryptedFile (Path inputPath , Path outputPath , Cipher cipher ) {
206- try (InputStream encryptedData = Files .newInputStream (inputPath );
207- CipherInputStream decryptStream = new CipherInputStream (encryptedData , cipher );
208- OutputStream decryptedOut = Files .newOutputStream (outputPath )) {
278+ private static boolean writeDecryptedFile (Path inputPath , Path outputPath , byte [] key ) throws NoSuchAlgorithmException , NoSuchPaddingException , InvalidKeyException , InvalidAlgorithmParameterException {
279+ try (InputStream encryptedData = Files .newInputStream (inputPath );){
280+
281+ // Read metadata from the input file
282+ byte [] initVector = new byte [16 ];
283+ byte [] givenMac = new byte [32 ];
284+
285+ encryptedData .read (initVector );
286+ encryptedData .read (givenMac );
287+
288+ // Create key specifications
289+ IvParameterSpec iv = new IvParameterSpec (initVector );
290+ SecretKeySpec skeySpec = new SecretKeySpec (key , ALGORITHM );
291+ SecretKeySpec macKey = new SecretKeySpec (key , HASH_AlGORITHM );
209292
210- final byte [] bytes = new byte [1024 ];
211- for (int length =decryptStream .read (bytes ); length !=-1 ; length = decryptStream .read (bytes )){
212- decryptedOut .write (bytes , 0 , length );
293+ // Initialise cipher and HMac
294+ Cipher cipher = Cipher .getInstance (CIPHER );
295+ cipher .init (Cipher .DECRYPT_MODE , skeySpec , iv );
296+
297+ // Read cipertext data and write plaintext data
298+ try (CipherInputStream decryptStream = new CipherInputStream (encryptedData , cipher );) {
299+ try (OutputStream decryptedOut = Files .newOutputStream (outputPath );) {
300+ final byte [] bytes = new byte [1024 ];
301+ for (int length =decryptStream .read (bytes ); length !=-1 ; length = decryptStream .read (bytes )){
302+ decryptedOut .write (bytes , 0 , length );
303+ }
213304 }
305+ }
306+
307+ // Check authentication and file integerity
308+ Mac hmac = Mac .getInstance (HASH_AlGORITHM );
309+ hmac .init (macKey );
214310
311+ hmac .update (initVector );
312+ byte [] computedMac = computeMac (hmac , outputPath );
313+ if (!Arrays .equals (givenMac , computedMac )) {
314+ throw new SecurityException ("Authentication failed, file may have been tampered with" );
315+ }
316+
317+ LOG .info ("Authentication passed, file integrity maintained" );
318+
215319 } catch (IOException ex ) {
216- Logger .getLogger (FileEncryptor .class .getName ()).log (Level .SEVERE , "Unable to decrypt " );
320+ Logger .getLogger (FileEncryptor .class .getName ()).log (Level .SEVERE , "IOException caught " );
217321 return false ;
218- }
322+ }
219323 return true ;
220324 }
221325}
0 commit comments