Skip to content

Commit 3693df6

Browse files
authored
Merge pull request #27 from modothprav/part-2
Part 2 - CPA Security
2 parents e8d446a + 107e4ca commit 3693df6

File tree

2 files changed

+183
-79
lines changed

2 files changed

+183
-79
lines changed

src/FileEncryptor.java

Lines changed: 182 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import java.io.File;
2+
import java.io.FileOutputStream;
23
import java.io.IOException;
34
import java.io.InputStream;
45
import java.io.OutputStream;
@@ -16,6 +17,7 @@
1617
import javax.crypto.Cipher;
1718
import javax.crypto.CipherInputStream;
1819
import javax.crypto.CipherOutputStream;
20+
import javax.crypto.Mac;
1921
import javax.crypto.NoSuchPaddingException;
2022
import javax.crypto.spec.IvParameterSpec;
2123
import 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("\nSecret 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
}

src/resources/plaintext.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
This is part one of the assignment
1+
This is part two of the assignment

0 commit comments

Comments
 (0)