Last active
November 26, 2025 06:57
-
-
Save righettod/2f45fdb3d3cbda557d7b2cab5556b46c to your computer and use it in GitHub Desktop.
Example of combination of an Argon2id derivated key with an AEAD cipherer.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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