Skip to content

Instantly share code, notes, and snippets.

@righettod
Last active November 26, 2025 06:57
Show Gist options
  • Select an option

  • Save righettod/2f45fdb3d3cbda557d7b2cab5556b46c to your computer and use it in GitHub Desktop.

Select an option

Save righettod/2f45fdb3d3cbda557d7b2cab5556b46c to your computer and use it in GitHub Desktop.
Example of combination of an Argon2id derivated key with an AEAD cipherer.
package eu.righettod.sdb;
import com.google.crypto.tink.Aead;
import com.google.crypto.tink.InsecureSecretKeyAccess;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.aead.AeadConfig;
import com.google.crypto.tink.aead.AesGcmKey;
import com.google.crypto.tink.aead.AesGcmParameters;
import com.google.crypto.tink.util.SecretBytes;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.params.Argon2Parameters;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.encoders.Base64;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.Objects;
import java.util.UUID;
/**
* This class show a example of usage of the:
* <ul>
* <li>Bouncy Castle library to derivate a symmetric 32 bits key using the Argon2id algorithm.</li>
* <li>Bouncy Castle library to create the hash of a password using the Argon2id algorithm.</li>
* <li>Google Tink library to create an Authenticated Encryption with Associated Data (AEAD) symmetric cipherer from a 32 bits key generated using the Argon2id algorithm.</li>
* </ul>
*
* @see "https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk18on"
* @see "https://mvnrepository.com/artifact/com.google.crypto.tink/tink"
* @see "https://developers.google.com/tink"
* @see "https://www.bouncycastle.org/documentation/documentation-java/"
*/
public class Argon2idWithAEADCiphererCombination {
static {
//Initialize Tink
try {
AeadConfig.register();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
//Define Argon2 parameters using OWASP Password Storage Cheat Sheet recommendations
//See https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
private final int iterationRound = 1;
private final int memoryAsKB = 47104; //46 MiB
private final int degreeOfParallelism = 1;
private final int hashOutputSize = 32; // 32 bytes is 256-bit hash or key size for AES 256
private final int saltSize = 16;
private final int aesGcmTagSize = 16;
private final int aesGcmIVSize = 12;
private final SecureRandom secureRandom = new SecureRandom();
/**
* Prepare a Argon2 parameters object using configuration recommended by the OWASP Password Storage Cheat Sheet.
*
* @param salt Salt to use to associated to the parameters and that will be used to derivate key/password hash.
* @return The parameters object
*/
private Argon2Parameters getArgon2Parameters(byte[] salt) {
Objects.requireNonNull(salt);
Argon2Parameters.Builder builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
.withSalt(salt)
.withIterations(iterationRound)
.withMemoryAsKB(memoryAsKB)
.withParallelism(degreeOfParallelism);
return builder.build();
}
/**
* Prepare a Argon2 generator using the Argon2 parameters from the method "getArgon2Parameters()".
*
* @param salt Salt to use to associated to the parameters and that will be used to derivate key/password hash.
* @return The generator object
*/
private Argon2BytesGenerator getArgon2BytesGenerator(byte[] salt) {
Objects.requireNonNull(salt);
Argon2Parameters params = getArgon2Parameters(salt);
Argon2BytesGenerator generator = new Argon2BytesGenerator();
generator.init(params);
return generator;
}
/**
* Return random bytes to use for the salt
*
* @return The array of random bytes.
*/
private byte[] generateSalt() {
byte[] salt = new byte[saltSize];
secureRandom.nextBytes(salt);
return salt;
}
/**
* Generate the hash of a password using Argon2id algorithm.
*
* @param password Clear text password.
* @param saltToUse The salt to use to generate the hash. If null or invalid then its value is overridden.
* @return The hash of the password.
*/
public String generatePasswordHash(String password, byte[] saltToUse) {
Objects.requireNonNull(password);
byte[] salt = saltToUse;
if (saltToUse == null || saltToUse.length != saltSize) {
salt = generateSalt();
}
byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8);
byte[] hash = new byte[hashOutputSize];
Argon2BytesGenerator generator = getArgon2BytesGenerator(salt);
generator.generateBytes(passwordBytes, hash);
String hashBase64Encoded = Base64.toBase64String(hash);
String saltBase64Encoded = Base64.toBase64String(salt);
return String.format("$argon2id$v=%s$m=%s,t=%s,p=%s$%s$%s",
getArgon2Parameters(salt).getVersion(),
memoryAsKB,
iterationRound,
degreeOfParallelism,
saltBase64Encoded,
hashBase64Encoded);
}
/**
* Validate the hash of a password using Argon2id algorithm.
*
* @param password Clear text password.
* @param passwordHash The hash of the password with the format retuned by the method "generatePasswordHash()".
* @return True only the password matches the hash.
*/
public boolean validatePasswordHash(String passwordHash, String password) {
Objects.requireNonNull(passwordHash);
Objects.requireNonNull(password);
byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8);
String[] parts = passwordHash.split("\\$");
String saltBase64Encoded = parts[4].trim();
String hashBase64Encoded = parts[5].trim();
byte[] salt = Base64.decode(saltBase64Encoded);
byte[] hash = Base64.decode(hashBase64Encoded);
byte[] hashComputed = new byte[hashOutputSize];
Argon2BytesGenerator generator = getArgon2BytesGenerator(salt);
generator.generateBytes(passwordBytes, hashComputed);
return Arrays.constantTimeAreEqual(hashComputed, hash);
}
/**
* Create an Authenticated Encryption with Associated Data (AEAD) symmetric cipherer with a symmetric 32 bits keys derivated from the password and salt specified.
*
* @param password Clear text password.
* @param salt The salt to use to derivate the key.
* @return The Cipherer as an instance of an implementation of the interface "com.google.crypto.tink.Aead".
* @throws Exception If any error occurs.
*/
public Aead buildTinkCipherFromDerivatedKey(String password, byte[] salt) throws Exception {
Objects.requireNonNull(password);
Objects.requireNonNull(salt);
//ARGON2id part: Derivate the key from the password and the salt provided
String keyContent = generatePasswordHash(password, salt);
String[] parts = keyContent.split("\\$");
String keyBase64Encoded = parts[5].trim();
byte[] key = Base64.decode(keyBase64Encoded);
//TINK part: Create the cipher from the derived key
AesGcmParameters parameters = AesGcmParameters.builder()
.setKeySizeBytes(hashOutputSize)
.setIvSizeBytes(aesGcmIVSize)
.setTagSizeBytes(aesGcmTagSize)
.setVariant(AesGcmParameters.Variant.NO_PREFIX)
.build();
AesGcmKey aesGcmKey = AesGcmKey.builder()
.setParameters(parameters)
.setKeyBytes(SecretBytes.copyFrom(key, InsecureSecretKeyAccess.get()))
.build();
KeysetHandle keysetHandle = KeysetHandle.newBuilder()
.addEntry(KeysetHandle.importKey(aesGcmKey).withRandomId().makePrimary())
.build();
return keysetHandle.getPrimitive(Aead.class);
}
//Example of usage of methods for both cases
public static void main(String[] args) throws Exception {
//Case of password hash generation
String userPassword = "#gM~1sfy2?.dZ8?m2Phswe}4+,0k@mC!D";
Argon2idWithAEADCiphererCombination sdb = new Argon2idWithAEADCiphererCombination();
String hash = sdb.generatePasswordHash(userPassword, null);
boolean isValid = sdb.validatePasswordHash(hash, userPassword);
System.out.println(hash);
if(!isValid){
throw new Exception("Validation of the hash failed!");
}
//Case of data ciphering
String data = "Hello World!!!!!";
String additionalAuthenticatedData = UUID.randomUUID().toString();
byte[] saltAssociatedToPassword = sdb.generateSalt();
Aead cipher1 = sdb.buildTinkCipherFromDerivatedKey(userPassword, saltAssociatedToPassword);
byte[] cipheredData = cipher1.encrypt(data.getBytes(StandardCharsets.UTF_8), additionalAuthenticatedData.getBytes(StandardCharsets.UTF_8));
byte[] decipheredData = cipher1.decrypt(cipheredData, additionalAuthenticatedData.getBytes(StandardCharsets.UTF_8));
if(!new String(decipheredData).equals(data)){
throw new Exception("[ROUND 1] Validation of the ciphering/deciphering phases failed!");
}
Aead cipher2 = sdb.buildTinkCipherFromDerivatedKey(userPassword, saltAssociatedToPassword);
decipheredData = cipher2.decrypt(cipheredData, additionalAuthenticatedData.getBytes(StandardCharsets.UTF_8));
if(!new String(decipheredData).equals(data)){
throw new Exception("[ROUND 2] Validation of the ciphering/deciphering phases failed!");
}
System.out.println("All checks are OK.");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment