Producer and Consumer – Part 4

We are quickly approaching the end of another work week. In the Twin Cities of Minneapolis and St. Paul it appears that most businesses are back to normal. The only exception is that many office workers are still working from home and people is required to wear a mask when in public spaces. Most people that want to be vaccinated against COVID-19 have done so. We need to get most (never generalize) people to comply in order to achieve herd immunity.

This is the fourth installment of the sequence of posts related to a producer and consumer connected via TCP/IP. In this post we will not add functionality to the producer or consumer. We will explore some functions and methods that we have at our disposal when encrypting and decrypting files using the Advanced Encryption Standard.

We have added the AES.java file which includes a test scaffold that allows us to explore the encryption and decryption mechanisms provided by Java. In the next couple posts in this topic we will create an encrypted file holding the objects we need to extract and will be able to decrypt the objects before writing them to individual files.

main <<< PASSWORD ==>Server Password!Server Password!<== bits: 256
main <<< SALT ==>Stress is the salt of life<== bits: 208
main <<< secretKey: javax.crypto.spec.SecretKeySpec@1485a
main <<< iv: [-90, 81, 101, -118, -80, 55, 84, -33, 25, 68, 58, -2, 16, 118, -52, -32]
main <<< rawText ==>Bond, James Bond 007<== length: 20
main <<< cypherText ==>aj9CEhMmEuxMR/Sj1wbqsT3v8OkiXwnQDQFyVAi8q8I=<== length: 44
main <<< output ==>Bond, James Bond 007<== length: 20

main <<< encrypting and decrypting a file
main <<< inputFile ==>C:\Temp\alice_in_wonderland.txt<==
main <<< encryptedFile ==>C:\Temp\alice_in_wonderland_encrypted.txt<==
main <<< decryptedFile ==>C:\Temp\alice_in_wonderland_decrypted.txt<==
main <<< inputFile.length: 148574
encryptFile <<< outputBytes.length: 16
main <<< encryptedFile.length: 148576
decryptFile <<< outputBytes.length: 14
main <<< contents of inputFile == decryptedFile

main <<< using ECB mode
main <<< rawText ==>Bond, James Bond 007<== length: 20
main <<< cipherText ==>QaB7MqRyg/PcyXdLcCOoyBo5u4kojmryL1VUzGziw8U=<== length: 44
main <<< output ==>Bond, James Bond 007<== length: 20

Our test code displays a PASSWORD and a SALT string. In AES there are six modes:

 

Acronym Name
ECB Electronic Code Book
CBC Cipher Block Chaining
CFB Cipher Feedback
OFB Output Feedback
CTR Counter
GCM Galois/Counter Mode

 

We will take a look at the first two modes.

In the AES algorithms, we need three parameters:

 

Input data (raw text)
Secret key
Input vector (not used in ECB mode)
Salt

 

Our test scaffold displays the password and the salt strings. The salt will be used to turn the password into a secret key.

The secret key is then generated and displayed. We actually do not display the contents of the secret password, just a reference to it.

Apparently we create an initialization vector (IV) and populate it with random values. The IV has a length of 16 bytes which matches the block size used by AES.

Our example provides a raw test string which we will encrypt. We could prompt each time for a different string.

Apparently we take the raw text and encrypt it into a cipher text string. The cipher text string is displayed. Note that it has been also encoded in Base 64.

The code seems to take the cipher text string and decrypt it returning the results into the output string which is then displayed. As expected, the raw text matches the contents of the output string.

We then encrypt an entire file. The name of the input file is displayed, the name for the encrypted file follows, and finally the name for the decrypted file is displayed. When all is set and done the contents of the input and the decrypted files should match.

The input file appears to have a length of 148574 bytes.

The message from the function encrypting the file shows that a block consists of 16 bytes.

After the encryption is completed, the size of the encrypted file has 148576 bytes. In this case the encrypted file has grown by 2 bytes. The reason for this is that the encryption algorithm takes 16 bytes at a time so the encrypted file has been padded with two additional bytes.

We then seem to decrypt the encrypted file. In the process the last block needs an additional 2 bytes to complete the decryption process. These are the two bytes that we have as a pad in the encrypted file.

It seems that our test code checks the content and sizes of the original and decrypted files. In this case they seem to match.

The third part of our test scaffold appears to take a single raw text string and encrypt it using the ECB mode. Note that the resulting encrypted string is encoded in Base 64.

The cipher text string is then decrypted and the output string holds the same value as the one in the raw text string.

/**
 * American Encryption Standard (AES)
 */
public class AES {
    

    // ***** class private variables ****
    private static final String SALT        = "Stress is the salt of life";
    private static final String PASSWORD    = "Server Password!Server Password!";
    //                                         01234567890123456789012345678901
	
	:::: :::: ::::
	
	}

The AES class declares the SALT and PASSWORD strings that we will use to encrypt and decrypt strings and files. Note that in general passwords and strings need to be stored outside of our code. Password managers and password vaults are commonly used to keep them safe. A password vault refers to a hardware implementation of password manager.

    /**
     * Test scaffold for this class.
     * 
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {

        // **** initialization ****
        int n = 256;
        
        // ???? ????
        System.out.println("main <<< PASSWORD ==>" + PASSWORD + "<== bits: " + PASSWORD.length() * 8);
        System.out.println("main <<< SALT ==>" + SALT + "<== bits: " + SALT.length() * 8);


        // **** experiment with raw text ... ****
        String rawText = "Bond, James Bond 007";
        //                01234567890123456789012345678901

        // **** generate a secret key from the password and salt ****
        SecretKey secretKey = getKeyFromPassword(PASSWORD, SALT);

        // ???? ????
        System.out.println("main <<< secretKey: " + secretKey.toString());

        // **** generate an iv ****
        IvParameterSpec iv = generateIv();

        // ???? ????
        System.out.println("main <<< iv: " + Arrays.toString(iv.getIV()));

        // **** encrypt the raw text string ****
        String algorithm = "AES/CBC/PKCS5Padding";
        String cipherText = encrypt(algorithm, rawText, secretKey, iv);

        // **** decrypt the cipher text string ****
        String output = decrypt(algorithm, cipherText, secretKey, iv);

        // ???? display strings of interest ????
        System.out.println("main <<< rawText ==>" + rawText + "<== length: " + rawText.length());
        System.out.println("main <<< cypherText ==>" + cipherText + "<== length: " + cipherText.length());
        System.out.println("main <<< output ==>" + output + "<== length: " + output.length());

        // **** check we get the raw text back ****
        if (!rawText.equals(output)) {
            System.err.println("main <<< rawText != output !!!");
            throw new Exception("EXCEPTION output ==>" + output + "<== != rawText ==>" + rawText + "<==");
        }


        // ???? ... now experiment with a file ????
        System.out.println("\nmain <<< encrypting and decrypting a file");

        // **** set parameters ****
        SecretKey key = generateKey(n);
        algorithm = "AES/CBC/PKCS5Padding";
        IvParameterSpec ivParameterSpec = generateIv();

        // **** create file objects ****
        File inputFile = new File("C:\\Temp\\alice_in_wonderland.txt");
        File encryptedFile = new File("C:\\Temp\\alice_in_wonderland_encrypted.txt");
        File decryptedFile = new File("C:\\Temp\\alice_in_wonderland_decrypted.txt");

        // ???? ????
        System.out.println("main <<< inputFile ==>" + inputFile.getAbsolutePath() + "<==");
        System.out.println("main <<< encryptedFile ==>" + encryptedFile.getAbsolutePath() + "<==");
        System.out.println("main <<< decryptedFile ==>" + decryptedFile.getAbsolutePath() + "<==");

        // ???? display the size of the input file ????
        System.out.println("main <<< inputFile.length: " + inputFile.length());

        // **** encrypt the file ****
        encryptFile(algorithm, key, ivParameterSpec, inputFile, encryptedFile);

        // ???? display the size of the input file ????
        System.out.println("main <<< encryptedFile.length: " + encryptedFile.length());

        // **** decrypt the file ****
        decryptFile(algorithm, key, ivParameterSpec, encryptedFile, decryptedFile);

        // ***** check if the contents of these files are NOT equal ****
        Boolean equal = isEqual(inputFile.toPath(), decryptedFile.toPath());
        if (!equal) {
            System.err.println("main <<< UNEXPECTED equal: " + equal);
            throw new Exception("EXCEPTION contents equal: " + equal + " inputFile ==>" + inputFile.getAbsolutePath() + 
                "<== decryptedFile ==>" + decryptedFile.getAbsolutePath() + "<==");
        }
        System.out.println("main <<< contents of inputFile == decryptedFile");


        // ???? using ECB mode ????
        System.out.println("\nmain <<< using ECB mode");

        // ???? ????
        System.out.println("main <<< rawText ==>" + rawText + "<== length: " + rawText.length());

        // **** encrypt raw text using ECB mode  ****
        cipherText = encryptData(rawText);

        // ???? ????
        System.out.println("main <<< cipherText ==>" + cipherText + "<== length: " + cipherText.length());
        // System.out.println("main <<< cipherText.length: " + cipherText.length());

        // **** decrypt cipher text using ECB mode ****
        output = decryptData(cipherText);

        // ???? ????
        System.out.println("main <<< output ==>" + output + "<== length: " + output.length());
    }

This code implements the code used to test encryption and decryption classes used by AES.

The code matches our previous description. If you have questions please visit the ORACLE web site associated with the different classes and methods used in this code.

The AES is based on the algorithm presented to a contest sponsored in October 2000 by the US National Institute of Standards and Technology won by a team of two cryptographers. If interested in the history and the internals of the algorithm I suggest reading the book “The Design of Rijndael” by Joan Daemen and Vincent Rijmen published by Springer. I pulled out the copy from a shelf. I bought and read the book in 2010. I also downloaded and experimented with the code. I have used it in a few implementations using the C / C++ languages.

The test code is divided into three parts. In the first, after some initialization steps we encrypt a raw text string. We then decrypt the cipher text into an output string.

The contents of the strings involved are displayed. The input string has a length of 20 bytes. After encrypted and encoded we end up with a string of 44 bytes. The encryption process generates binary code. So we can easily display it we encode the resulting encrypted string using base 64. Finally we decrypt the cipher text and get back the original raw text in the output string.

Since we are interested in our project to decrypt files this was just an opportunity to experiment with the functionality and parameters used by the AES class.

In the second section we will encrypt and then decrypt the contents of a text file. We start by setting some parameters which we will use to perform these tasks.

We then create File objects to be able to access the input file, the encrypted file and later the decrypted file. We will compare the contents of the input with the decrypted files to make sure that the encryption and decryption processes worked as expected.

We encrypt the file. Note that the size of the input and output files are slightly different. The reason for this is that the AES encryption engine uses 16-byte blocks. In this case we needed to pad the encrypted file. You can take a look at the encrypted file using a text editor to verify that the contents have been encrypted. I use Notepad++.

We then decrypt the encrypted file. As we mentioned previously, we then call a method to verify that the input and the decrypted files contain the same characters and are of the same size.

The last section in our test code is not used in our target project. This shows how to encrypt a raw text string using the ECB mode of AES. Note that the length of the encoded string is the same as before, but the actual cipher text, as expected, is different.

Once again, the raw and decrypted strings match.

    /**
     * The AES secret key can be derived from a given password 
     * using a password-based key derivation function like PBKDF2.
     * 
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    public static SecretKey getKeyFromPassword( String password, 
                                                String salt) 
                    throws NoSuchAlgorithmException, InvalidKeySpecException {

        // **** ****
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");

        // **** ****
        KeySpec spec = new PBEKeySpec(  password.toCharArray(), 
                                        salt.getBytes(),
                                        100000,
                                        256);

        // **** ****
        SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");

        // **** return secret key ****
        return secret;
    }

This function (in the next incarnation we will make it a method in a class) is used to initialize the secret key that we will use to encrypt some raw text.

We specify the factory we wish to use. We then create a specification using the password and salt we declared in our code. We should always try to keep passwords and secrets in some type of vault and not in plain view in the source code.

Note that we specify a couple additional arguments to the PBEKeySpec constructor. We will cycle 100000 times and a key length of 256 bits. This seems to be a rather short key. In production code, if possible / /allowed I like to use 2048 or 4096 bits for keys.

The AES standard recommends a salt length of at least 64 bits. The US National Institute of Standards and Technology recommends a salt length of 128 bits.

    /**
     * Method for generating an Initialization Vector (IV).
     */
    public static IvParameterSpec generateIv() {
        byte[] iv = new byte[16];
        new SecureRandom().nextBytes(iv);
        return new IvParameterSpec(iv);
    }

The generateIv() method is used to initialize the input vector. Some AES implementations may allow not initialized vectors while other not.

    /**
     * Encrypt the specified raw text string.
     */
    public static String encrypt(   String algorithm, 
                                    String rawText, 
                                    SecretKey key, 
                                    IvParameterSpec iv)
        throws NoSuchPaddingException, NoSuchAlgorithmException,
        InvalidAlgorithmParameterException, InvalidKeyException,
        BadPaddingException, IllegalBlockSizeException {
    
        // **** initialize cipher ****
        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.ENCRYPT_MODE, key, iv);

        // **** ****
        byte[] cipherText = cipher.doFinal(rawText.getBytes());

        // **** return th estring representation ****
        return Base64.getEncoder().encodeToString(cipherText);
    }

This method is used to encrypt a raw text string. To get better acquainted with this code, try omitting some of the arguments of changing their values. You could also read the description of the Cipher class in the documentation.

    /**
     * Decrypt the specified cipher text string.
     */
    public static String decrypt(   String algorithm, 
                                    String cipherText,
                                    SecretKey key,
                                    IvParameterSpec iv) 
        throws NoSuchPaddingException, NoSuchAlgorithmException,
        InvalidAlgorithmParameterException, InvalidKeyException,
        BadPaddingException, IllegalBlockSizeException {
    
        // **** initialize cipher ****
        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.DECRYPT_MODE, key, iv);

        // **** ****
        byte[] plainText = cipher.doFinal(Base64.getDecoder().decode(cipherText));

        // **** return the string representation ****
        return new String(plainText);
    }

This method is used to decrypt the cipher text string we encrypted with the previous method. Note that all the arguments to create and initialize the cipher must match the ones used for encryption. Once again I invite to experiment and see the results.

Once the cipher has been initialized, we get back the plain text. Note that since we encoded the cipher text in th encryption step, now we need to decode the cipher text before passing in to the decryption process.

    /**
     * The secret key should be generated from a 
     * Cryptographically Secure (Pseudo-)Random Number Generator 
     * like the SecureRandom class.
     * 
     * @throws NoSuchAlgorithmException
     */
    public static SecretKey generateKey(int n) throws NoSuchAlgorithmException {

        // **** sanity check(s) ****
        switch (n) {
            case 128:
            case 192:
            case 256:

                // **** valid values ****

            break;

            default:
                System.err.println("main <<< UNEXPECTED n: " + n);
                throw new InvalidParameterException();
            // break;
        }

        // **** ****
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(n);
        SecretKey key = keyGenerator.generateKey();
        
        // **** return the secret key ****
        return key;
    }

This function is used to generate a secret key. Note that there is a specific set of number we can used to generate the length of the secret key. Perhaps we could look of other implementations that allow for longer keys.

    /**
     * Encrypt the specified raw file.
     */
    public static void encryptFile( String algorithm, 
                                    SecretKey key,
                                    IvParameterSpec iv,
                                    File inputFile,
                                    File outputFile) 
        throws IOException, NoSuchPaddingException,
        NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException,
        BadPaddingException, IllegalBlockSizeException {
        
        // **** initialize cipher ****
        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.ENCRYPT_MODE, key, iv);

        // **** open the input and output streams ****
        FileInputStream inputStream = new FileInputStream(inputFile);
        FileOutputStream outputStream = new FileOutputStream(outputFile);

        // **** process the file one 64-byte block at a time ****
        byte[] buffer = new byte[64];
        int bytesRead;
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            byte[] output = cipher.update(buffer, 0, bytesRead);
            if (output != null) {
                outputStream.write(output);
            }
        }

        // **** ****
        byte[] outputBytes = cipher.doFinal();

        // ???? ????
        System.out.println("encryptFile <<< outputBytes.length: " + outputBytes.length);

        if (outputBytes != null) {
            outputStream.write(outputBytes);
        }

        // **** close the input and output streams ****
        inputStream.close();
        outputStream.close();
    }

This is the function we use to encrypt the text file. We need to specify a set of arguments.

We then initialize the cipher.

We then open an input and an output file streams.

We are ready to encrypt the file.

After the file is encrypted, we need to handle the padding. That is done with the doFinal() method.

After all is written to the output streams, the streams are closed.

    /**
     * Decrypt the specified encrypted file.
     */
    public static void decryptFile( String algorithm, 
                                    SecretKey key,
                                    IvParameterSpec iv,
                                    File inputFile,
                                    File outputFile) 
        throws IOException, NoSuchPaddingException,
        NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException,
        BadPaddingException, IllegalBlockSizeException {
        
        // **** initialize cipher ****
        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.DECRYPT_MODE, key, iv);

        // **** open input and output streams ****
        FileInputStream inputStream = new FileInputStream(inputFile);
        FileOutputStream outputStream = new FileOutputStream(outputFile);

        // **** ****
        byte[] buffer = new byte[64];
        int bytesRead;
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            byte[] output = cipher.update(buffer, 0, bytesRead);
            if (output != null) {
                outputStream.write(output);
            }
        }

        // **** ****
        byte[] outputBytes = cipher.doFinal();

        // ???? ????
        System.out.println("decryptFile <<< outputBytes.length: " + outputBytes.length);

        if (outputBytes != null) {
            outputStream.write(outputBytes);
        }

        // **** close input and output streams ****
        inputStream.close();
        outputStream.close();
    }

This method if used to decrypt the encrypted file.

Note that it performs similar operation in reverse order. The main difference is the first argument to the cipher.init() method.

   /**
     * Check if the contents of the two specified files are equal.
     */
    private static boolean isEqual(Path firstFile, Path secondFile)
    {
        try {
            if (Files.size(firstFile) != Files.size(secondFile)) {
                return false;
            }
 
            byte[] first = Files.readAllBytes(firstFile);
            byte[] second = Files.readAllBytes(secondFile);
            return Arrays.equals(first, second);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

This method is used to check if the input file matches the length and contents of the decrypted file.

    /**
     * Encrypt data.
     */
    private static String encryptData(String rawData) throws Exception, InvalidKeyException {

        // **** ****
        SecretKeySpec skeySpec = new SecretKeySpec(PASSWORD.getBytes(), "AES");

        // **** ****
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec);

        // // ???? ????
        // System.out.println("encryptData <<< Base64 encoded: " + Base64.getEncoder().encode(rawData.getBytes()).length);

        // **** ****
        byte[] original = Base64.getEncoder().encode(cipher.doFinal(rawData.getBytes()));
        // byte[] original = cipher.doFinal(rawData.getBytes());

        // ???? ????
        // System.out.println("encryptData <<< original.length: " + original.length);

        // **** return cipher data ****
        return new String(original);
    }

This method is used to encrypt raw text. Note that we use s different cipher. This cipher does not require as many arguments as the one used in the first section. Of course, the cipher text is not as secure as the one generated in the first section.

    /**
     * Decrypt data.
     */
    private static String decryptData(String cipherData)
        throws NoSuchAlgorithmException, NoSuchPaddingException,
        InvalidKeyException, IllegalBlockSizeException, BadPaddingException {

        // **** ****
        SecretKeySpec skeySpec = new SecretKeySpec(PASSWORD.getBytes(), "AES");

        // **** ****
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.DECRYPT_MODE, skeySpec);

        // // ???? ????
        // System.out.println("decryptData <<< Base64 decoded: " + Base64.getDecoder().decode(cipherData.getBytes()).length);

        // **** ****
        byte[] original = cipher.doFinal(Base64.getDecoder().decode(cipherData.getBytes()));
        // byte[] original = cipher.doFinal(cipherData.getBytes());

        // ???? ????
        // System.out.println("decryptData <<< original.length: " + original.length);

        // **** return raw data ****
        return new String(original).trim();
    }

This method decrypts the cipher text generated by the previous function.

Note that in the next pass of our project, we will use the file encryption and decryption functions from the second set of operations in this code. We will not be encrypting strings.

Hope you enjoyed solving this problem as much as I did. The entire code for this project can be found in my GitHub repository.

If you have comments or questions regarding this, or any other post in this blog, please do not hesitate and leave me a note below. I will reply as soon as possible.

Keep on reading and experimenting. It is one of the best ways to learn, become proficient, refresh your knowledge and enhance your developer toolset.

Thanks for reading this post, feel free to connect with me John Canessa at LinkedIn.

Regards;

John

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.