Sidebar

How can I use "AES/GCM/NoPadding" encryption/cipher mode in QIE?

0 votes
5.0K views
asked Dec 22, 2022 by rich-c-2789 (17,490 points)

I saw these two questions:

Why is ECB cipher mode not recommended for encryption?

What cipher modes are recommended for encryption?

Is there a QIE example to encrypt/decrypt using AES/GCM?

2 Answers

0 votes

Let's just create a channel with three published functions that will encrypt the incoming message.

In the first mapping node it will call the published functions needed to encrypt the message.  The second mapping node will likewise decrypt the message.  In this simple channel we pass the encrypted message to the next node by replacing the message content with the base64 encoding of the encrypted bytes. For additional details see the comments in the code below.

NOTE:  This example creates a propritary way to encrypt and decrypt that unless another system implements the same details they will not be read the data.  

Mapping Node 2 - Encrypt Message

var password = 'password';

var originalText = source.getNode('/');
qie.info('originalText: ' + originalText);

var encrypted = aes256GCMEncryptBytes(originalText.getBytes(), password);
var base64Encoded = java.util.Base64.getEncoder().encodeToString(encrypted);
qie.info("base64Encoded: " + base64Encoded);

message.setNode('/', base64Encoded);

Mapping Node 3 - Decrypt Message

var password = 'password';

var base64Encoded = message.getNode('/');

var base64Decoded = java.util.Base64.getDecoder().decode(base64Encoded);
qie.info('base64Decoded: ' + base64Decoded);

var decryptedText = aes256GCMDecryptBytes(base64Decoded, password);
qie.info('decryptedText: ' + decryptedText);

message.setNode('/', decryptedText);

Published Function - aes256GCMEncryptBytes(bytesToEncrypt, password)

function aes256GCMEncryptBytes(bytesToEncrypt, password) {
   //Prepare the nonce. Also used to salt the password hash.
   //  It is recommended to use a new iv/nonce for each byte[] to be encrypted.  The same password and iv/nonce
   //  needs to be used to decrypt.  Later we will store the iv/nonce as part of the encrypted bytes so that it
   //  doesn't have to be shared like the password, but is paired with the encrypted bytes without getting lost.
   //  This approach means that we need to use the same logic to recover the iv/nonce from the bytes to decrypt it.
   var iv = java.nio.ByteBuffer.allocate(12).array(); //Nonce should be 12 bytes (GCM_IV_LENGTH)   
   var secureRandom = new java.security.SecureRandom();
   secureRandom.nextBytes(iv);
   
   //Generate a secret key
   var secretKey = generateSecretKey(password, iv);
   
   // Get Cipher Instance using AES with the GCM mode
   var cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding");
   
   // Create GCMParameterSpec with nonce
   var gcmParameterSpec = new javax.crypto.spec.GCMParameterSpec(16 * 8, iv); //Tag length should be 16 (GCM_TAG_LENGTH)
   
   // Initialize Cipher for ENCRYPT_MODE
   cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec);
   
   // Perform Encryption
   var encryptedBytes = cipher.doFinal(bytesToEncrypt);
   
   //Prepend the length of the nonce, and the nonce to the front of the encryptedBytes
   var byteBuffer = java.nio.ByteBuffer.allocate(4 + iv.length + encryptedBytes.length);
   byteBuffer.putInt(iv.length);
   byteBuffer.put(iv);
   byteBuffer.put(encryptedBytes);
   
   //Return the combined byte[]
   var combinedBytes = byteBuffer.array();
   return combinedBytes; 
}

Continued...

answered Dec 22, 2022 by rich-c-2789 (17,490 points)
edited Dec 28, 2022 by rich-c-2789
0 votes

Published Function - aes256GCMDecryptBytes(bytesToDecrypt, password)

function aes256GCMDecryptBytes(bytesToDecrypt, password) {
   //Wrap the bytesToDecrypt into a byte buffer to ease the reading process
   var byteBuffer = java.nio.ByteBuffer.wrap(bytesToDecrypt);
   
   //First get the size of the nonce from the beginning of the bytesToDecrypt
   var nonceSize = byteBuffer.getInt();
   
   //Make sure that the bytesToDecrypt were encrypted properly
   if (nonceSize < 12 || nonceSize >= 16) {
      throw new java.lang.IllegalArgumentException("Nonce size is incorrect. Make sure that the bytesToDecrypt were properly encrypted.");
   }
   
   //Next get the nonce from the bytesToDecrypt
   var iv = java.nio.ByteBuffer.allocate(nonceSize).array();
   byteBuffer.get(iv);
   
   //Generate a secret key
   var secretKey = generateSecretKey(password, iv);
   
   //Now get the actual encrypted data from bytesToDecrypt
   var encryptedBytes = java.nio.ByteBuffer.allocate(byteBuffer.remaining()).array();
   byteBuffer.get(encryptedBytes);
   
   // Get Cipher Instance using AES with the GCM mode
   var cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding");
   
   // Create GCMParameterSpec
   var gcmParameterSpec = new javax.crypto.spec.GCMParameterSpec(16 * 8, iv); //Tag length should be 16 (GCM_TAG_LENGTH)
   
   // Initialize Cipher for DECRYPT_MODE
   cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec);
   
   // Perform Decryption
   var decryptedText = cipher.doFinal(encryptedBytes);
   
   return new java.lang.String(decryptedText);
}

Published Function - generateSecretKey(password, iv)

function generateSecretKey(password, iv) {
   //The iv/nonce is also used as the salt for the hashed password.
   var salt = iv;
   //Prepare a secret key
   var spec = new javax.crypto.spec.PBEKeySpec(new java.lang.String(password).toCharArray(), salt, 65536, 256);  //Key size should be 256 (AES_KEY_SIZE)
   var secretKeyFactory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
   var key = secretKeyFactory.generateSecret(spec).getEncoded();
   return new javax.crypto.spec.SecretKeySpec(key, "AES");   
}

NOTE: In the generateSecretKey method, this line may be modified to adjust how long it takes to generate a secret key from the password:

var spec = new javax.crypto.spec.PBEKeySpec(new java.lang.String(password).toCharArray(), salt, 65536, 256);

To do so change the third parameter (the iterationCount described here).  A lower number takes less time.  See the top of this question in which I compared this implementation to an ECB implementation that demonstrates how changing this number affects how long it takes to encrypt and decrypt using 65536 vs 1.  

As I prepared this example, here are some of the errors hit and what they mean:

NoSuchPaddingException - This exception is thrown when a particular padding mechanism is requested but is not available in the environment. Example provider could not find it. Could be something wrong with java configuration.

NoSuchAlgorithmException - This exception is thrown when a particular cryptographic algorithm is requested but is not available in the environment. Example: AES/NONE/NoPadding

InvalidKeyException - This is the exception for invalid Keys (invalid encoding, wrong length, uninitialized, etc). Example: "passw" is only 40 bits (5 bytes) and is not valid for AES as key. Key must be 128, 192, or 256 bits. Recommended that the password is used to generate a key, not as the key.

IllegalBlockSizeException - This exception is thrown when the length of data provided to a block cipher is incorrect, i.e., does not match the block size of the cipher. This can happen when decrypting if the encrypted bytes were base64 encoded without base64 decoding before decrypting.

BadPaddingException - Caused during decryption when the bytes being decrypted do not match the encrypted bytes. For example encrypt -> base64 encoded -> decrypt. Forgetting to base64 decode could cause it to error.

 

See also:

Why is ECB cipher mode not recommended for encryption?

What cipher modes are recommended for encryption?

answered Dec 22, 2022 by rich-c-2789 (17,490 points)
edited Dec 28, 2022 by rich-c-2789
...