Skip to content

Instantly share code, notes, and snippets.

@patrickfav
Last active February 27, 2024 11:32
Show Gist options
  • Star 30 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save patrickfav/7e28d4eb4bf500f7ee8012c4a0cf7bbf to your computer and use it in GitHub Desktop.
Save patrickfav/7e28d4eb4bf500f7ee8012c4a0cf7bbf to your computer and use it in GitHub Desktop.
Java Authenticated Encryption with AES and GCM.
package at.favre.lib.bytes.otherPackage;
import org.junit.Test;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import static org.junit.Assert.assertEquals;
/**
* A simple showcase for encryption and decryption with AES + GCM in Java
*/
public class AesGcmTest {
private final SecureRandom secureRandom = new SecureRandom();
private final static int GCM_IV_LENGTH = 12;
@Test
public void testEncryption() throws Exception {
//create new random key
byte[] key = new byte[16];
secureRandom.nextBytes(key);
SecretKey secretKey = new SecretKeySpec(key, "AES");
byte[] associatedData = "ProtocolVersion1".getBytes(StandardCharsets.UTF_8); //meta data you want to verify with the secret message
String message = "the secret message";
byte[] cipherText = encrypt(message, secretKey, associatedData);
String decrypted = decrypt(cipherText, secretKey, associatedData);
assertEquals(message, decrypted);
}
/**
* Encrypt a plaintext with given key.
*
* @param plaintext to encrypt (utf-8 encoding will be used)
* @param secretKey to encrypt, must be AES type, see {@link SecretKeySpec}
* @param associatedData optional, additional (public) data to verify on decryption with GCM auth tag
* @return encrypted message
* @throws Exception if anything goes wrong
*/
private byte[] encrypt(String plaintext, SecretKey secretKey, byte[] associatedData) throws Exception {
byte[] iv = new byte[GCM_IV_LENGTH]; //NEVER REUSE THIS IV WITH SAME KEY
secureRandom.nextBytes(iv);
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); //128 bit auth tag length
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
if (associatedData != null) {
cipher.updateAAD(associatedData);
}
byte[] cipherText = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + cipherText.length);
byteBuffer.put(iv);
byteBuffer.put(cipherText);
return byteBuffer.array();
}
/**
* Decrypts encrypted message (see {@link #encrypt(String, SecretKey, byte[])}).
*
* @param cipherMessage iv with ciphertext
* @param secretKey used to decrypt
* @param associatedData optional, additional (public) data to verify on decryption with GCM auth tag
* @return original plaintext
* @throws Exception if anything goes wrong
*/
private String decrypt(byte[] cipherMessage, SecretKey secretKey, byte[] associatedData) throws Exception {
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
//use first 12 bytes for iv
AlgorithmParameterSpec gcmIv = new GCMParameterSpec(128, cipherMessage, 0, GCM_IV_LENGTH);
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmIv);
if (associatedData != null) {
cipher.updateAAD(associatedData);
}
//use everything from 12 bytes on as ciphertext
byte[] plainText = cipher.doFinal(cipherMessage, GCM_IV_LENGTH, cipherMessage.length - GCM_IV_LENGTH);
return new String(plainText, StandardCharsets.UTF_8);
}
}
@cezary-butler
Copy link

It is not needed, it's a JUnit test.

To see where to start look at @Test annotated method. It's being run by the test runner every time the test phase of the project runs.

@Amine-CHICHI
Copy link

@cezary-butler Thanks for sharing this piece of code i was struggling for 24H to debug Tag mismatch exception and your approach worked

@cezary-butler
Copy link

@Amine-CHICHI thanks for the credit but I am not the one who should receive it. The code was shared by @patrickfav

@vendessitorus
Copy link

Hi, if I want to separate encrypt and decrypt on different class, how can I call the decryptor method? (byte param, etc)?

@Animesh7734
Copy link

is there any way to get authentication tag ?

@patrickfav
Copy link
Author

patrickfav commented Mar 15, 2023

The tag is automatically appended to the cipher text (should be the last 16 byte of cipherText).

@sisimomo
Copy link

@patrickfav, in your opinion, is it safe to use this code in a Production environment to encrypt API keys in a database?

If we assume for sure that the SecretKey (in base64) is kept securely outside of the database.

@patrickfav
Copy link
Author

patrickfav commented Mar 22, 2023

@sisimomo yes, as long as you keep the secret key confidential; however if you are unsure about this I would recommend using a library with a higher abstraction, that manages these things for you. But whouldnt it suffice to hash the api keys with e.g. bcrypt?

@sisimomo
Copy link

sisimomo commented Mar 22, 2023

Sorry if that wasn't clear, but in my case hashing the API Key won't do the trick as I understand it. I need to save the API key in the database so that I can call an external service using the user's API key. So in my case I need to be able to retrieve the original supplied API key to send to the external API service.

I agree, without your well-documented example code to base my code on, I definitely would have used a library with higher abstraction. But I like to use as few external libraries as possible, especially if I can get a similar result with native Java.

Thanks for your example. It saved me a day or 2 of debugging and development. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment