package ch.ge.ve.commons.crypto; /*- * #%L * Common crypto utilities * %% * Copyright (C) 2015 - 2016 République et Canton de Genève * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * #L% */ import ch.ge.ve.commons.crypto.exceptions.CryptoOperationRuntimeException; import ch.ge.ve.commons.crypto.utils.SaltUtils; import ch.ge.ve.commons.crypto.utils.SecureRandomFactory; import com.google.common.base.Preconditions; import com.google.common.primitives.Bytes; import javax.crypto.Cipher; import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import javax.xml.bind.DatatypeConverter; import java.io.Serializable; import java.math.BigInteger; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.util.Arrays; import java.util.Base64; /** * Crypto primitives for creating and verifying MAC, and for symmetric encryption and decryption. * Used only for securing sensitive data stored in the database, such as voting card numbers, municipality of origin, .. */ public class SensitiveDataCryptoUtils { public static final Charset ENCRYPTION_CHARSET = StandardCharsets.UTF_8; /** * Default size of the salts for the salted MACs, in bytes. */ private static final int SALT_SIZE_BYTES = 16; private static final Base64.Decoder base64decoder = Base64.getDecoder(); private static final Base64.Encoder base64encoder = Base64.getEncoder(); /** * the configuration required for the encryption, decryption and hashing operations */ private static SensitiveDataCryptoUtilsConfiguration config; /* * Due to the usage of this class in very varied places in the code both in the web application, * and in the administration console, it has been decided to keep its methods static and to * load a configuration when the application starts. * This way, the configuration of ciphers and authenticated digests can be aligned with the * way it is managed for the ballot encryption, while not needing a complete rewrite of various * sensitive parts of the code. * * In this case, using static methods is not an issue, most elements are retrieved through the * configuration. * The main impact is that tests cannot be run in parallel with different configurations, which * simply means that they take longer to run. */ /** * Static methods only */ private SensitiveDataCryptoUtils() { } /** * Loads the configurations. * <p> * The method is static, because the configuration needs to be, and synchronized, to avoid multiple simultaneous writes. * </p> * * @param configuration * @see #config */ public static synchronized void configure(SensitiveDataCryptoUtilsConfiguration configuration) { config = configuration; } /** * Instantiates a secret key based on its encoded representation and requested algorithm * * @param keyBytes the encoded representation of the key * @param algo the requested algorithm (e.g. AES) * @return a new secret key matching the encoded representation and compatible with the requested algorithm */ public static SecretKey buildSecretKey(byte[] keyBytes, String algo) { return new SecretKeySpec(keyBytes, algo); } /** * Generates a new random key * * @param lengthInBits the length of the requested key, in bits * @param algo the requested algorithm (e.g. AES) * @return a new secret key compatible with the requested algorithm, of the requested bitLength */ public static SecretKey buildSecretKey(int lengthInBits, String algo) { Preconditions.checkArgument(lengthInBits % 8 == 0, String.format("Invalid length, not a multiple of 8: %d", lengthInBits)); SecureRandom sr = SecureRandomFactory.createPRNG(); byte[] keyBytes = new byte[lengthInBits / 8]; sr.nextBytes(keyBytes); return new SecretKeySpec(keyBytes, algo); } /** * Builds the mac of the input string and returns it as a string * * @param input message to be MACed. * @return the MAC in base64 */ public static String buildMACAsBase64String(String input) { return base64encoder.encodeToString(buildMAC(input)); } /** * Builds the mac of the input string and returns it as a string, * applying a generated {@link #SALT_SIZE_BYTES} bytes salt on the message. * * @param input message to be MACed. * @return the concatenation of the {@link #SALT_SIZE_BYTES} bytes salt and the MAC in base64 */ public static String buildSaltedMACAsBase64String(String input) { final byte[] salt = SaltUtils.generateSalt(SALT_SIZE_BYTES * 8); final byte[] mac = buildMAC(input, salt); return base64encoder.encodeToString(mac); } /** * Computes the unsalted MAC of the input * * @param input any string * @return the MAC (using algorithm defined in the {@link #config}) of the input string */ public static byte[] buildMAC(String input) { return buildMAC(input, null); } /** * Computes a salted MAC of the input * * @param input any string * @param salt the salt to be used by the MAC * @return the MAC (using algorithm defined in the {@link #config}) of the input string, using the provided salt */ public static byte[] buildMAC(String input, byte[] salt) { return buildMAC(input.getBytes(), salt); } /** * Computes a salted MAC of the input * * @param input any byte array * @param salt the salt to be used by the MAC * @return the MAC (using algorithm defined in the {@link #config}) of the input byte array, using the provided salt */ public static byte[] buildMAC(byte[] input, byte[] salt) { try { Mac mac = config.getMac(); mac.init(config.getSecretKey()); if (salt != null) { mac.update(salt); final byte[] macText = mac.doFinal(input); return Bytes.concat(salt, macText); } else { return mac.doFinal(input); } } catch (GeneralSecurityException e) { throw new CryptoOperationRuntimeException(e); } } /** * Checks the authentication of a message. * * @param message message to be authenticated * @param macAsBase64 MAC against which the message is to be authenticated * @return true if the computed MAC of the message is equal to the provided MAC */ public static boolean verifyMAC(String message, String macAsBase64) { final byte[] knownMac = base64decoder.decode(macAsBase64); final byte[] calculatedMac = buildMAC(message); return Arrays.equals(knownMac, calculatedMac); } /** * Checks the authentication of a message using a salted MAC. * * @param message message to be authenticated * @param macAndSaltAsBase64 16 byte salt and MAC against which the message is to be authenticated. * @return true if the computed MAC of the message is equal to the provided MAC */ public static boolean verifySaltedMAC(String message, String macAndSaltAsBase64) { final byte[] knownMacAndSalt = base64decoder.decode(macAndSaltAsBase64); final byte[] salt = Arrays.copyOfRange(knownMacAndSalt, 0, SALT_SIZE_BYTES); final byte[] calculatedMac = buildMAC(message, salt); return Arrays.equals(knownMacAndSalt, calculatedMac); } /** * Encrypts the given Integer and encodes the resulting byte array into a Base64 String * * @param input any integer * @return the Base64 representation of the encrypted input (using the algorithm and key provided by the {@link #config}) */ public static String encryptAsBase64String(Integer input) { return encryptAsBase64String(input.longValue()); } /** * Encrypts the given Long and encodes the resulting byte array into a Base64 String * * @param input any long * @return the Base64 representation of the encrypted input (using the algorithm and key provided by the {@link #config}) * @see #decryptAsLong(String) the reverse operation */ public static String encryptAsBase64String(Long input) { byte[] bytes = BigInteger.valueOf(input).toByteArray(); return Base64.getEncoder().encodeToString(encrypt(bytes)); } /** * Encrypts the given String and encodes the resulting byte array into a Base64 String * * @param input any String * @return the Base64 representation of the encrypted input (using the algorithm and key provided by the {@link #config}) * @see #decryptAsString(String) the reverse operation */ public static String encryptAsBase64String(String input) { return base64encoder.encodeToString(encrypt(input)); } /** * Encrypts the given String * * @param input any String * @return the encrypted input as a byte array (using the algorithm and key provided by the {@link #config}) * @see #decryptAsString(byte[]) the reverse operation */ public static byte[] encrypt(String input) { return encrypt(input.getBytes(ENCRYPTION_CHARSET)); } /** * @param input an byte array to encrypt * @return the concatenation of the IV followed by the cipher text * @see #decrypt(byte[]) the reverse operation */ public static byte[] encrypt(byte[] input) { try { Cipher cipher = config.getCipher(); SecretKey secretKey = config.getSecretKey(); cipher.init(Cipher.ENCRYPT_MODE, secretKey, SecureRandomFactory.createPRNG()); // init generates the IV byte[] iv = cipher.getIV(); byte[] cipherText = cipher.doFinal(input); return Bytes.concat(iv, cipherText); } catch (GeneralSecurityException e) { throw new CryptoOperationRuntimeException(e); } } /** * Takes an encrypted Long, encoded as a Base64 String and decrypts it back into a Long * * @param base64Input a Base64 String representing an encrypted Long * @return the decrypted Long (using the algorithm and key defined in the {@link #config}) * @see #encryptAsBase64String(Long) the reverse operation */ public static Long decryptAsLong(String base64Input) { return decryptAsLong(base64decoder.decode(base64Input)); } /** * Takes an encrypted Long and decrypts it back into a Long * * @param input a byte array containing an encrypted Long * @return the decrypted Long (using the algorithm and key defined in the {@link #config}) */ public static Long decryptAsLong(byte[] input) { byte[] bytes = decrypt(input); return new BigInteger(bytes).longValue(); } /** * Takes an encrypted String, encoded as a Base64 String and decrypts it back into a String * * @param base64Input a Base64 String representing an encrypted String * @return the original plaintext String * @see #encryptAsBase64String(String) the reverse operation */ public static String decryptAsString(String base64Input) { return decryptAsString(base64decoder.decode(base64Input)); } /** * Takes a byte array representing an encrypted String and decrypts it back into a String * * @param input a byte array containing an encrypted String * @return the plaintext String * @see #encrypt(String) the reverse operation */ public static String decryptAsString(byte[] input) { return new String(decrypt(input), ENCRYPTION_CHARSET); } /** * Takes an encrypted byte array and returns the corresponding decrypted byte array * * @param input the concatenation of the IV followed by the cipher text * @return the decrypted byte array * @see #encrypt(byte[]) the reverse operation */ public static byte[] decrypt(byte[] input) { try { Cipher cipher = config.getCipher(); int blockSize = cipher.getBlockSize(); byte[] iv = Arrays.copyOfRange(input, 0, blockSize); byte[] cipherText = Arrays.copyOfRange(input, blockSize, input.length); SecretKey secretKey = config.getSecretKey(); cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv)); return cipher.doFinal(cipherText); } catch (GeneralSecurityException e) { throw new CryptoOperationRuntimeException(e); } } /** * Wraps any serializable object into a SealedObject and returns the corresponding byte array * * @param object the object to seal * @return the byte array representing the SealedObject (locked with the algorithm and key provided in the {@link #config} * @throws CryptoOperationRuntimeException * @see #unsealObject(byte[]) the matching unwrapping method */ public static byte[] sealObject(Serializable object) { ObjectSealer objectSealer = new ObjectSealer(config.getCipher(), config.getSecretKey()); return objectSealer.sealObject(object); } /** * Parses a SealedObject from the given byte array and retrieves the original wrapped object * * @param encryptedObject a byte array representing a SealedObject * @return the original Serializable object * @throws CryptoOperationRuntimeException * @see #sealObject(java.io.Serializable) the matching wrapping operation */ public static Object unsealObject(byte[] encryptedObject) { ObjectSealer objectSealer = new ObjectSealer(config.getCipher(), config.getSecretKey()); return objectSealer.unsealObject(encryptedObject, config.getSealMaxBytes()); } /** * Generates a strong hash from a given clear text password * * @param password the password * @return a String of 3 blocks: the number of iterations, * then the hexadecimal representation of the salt, * and then the hexadecimal representation of the password hash */ public static String generateStrongPasswordHash(char[] password) { int iterations = config.getIterations(); byte[] salt = SaltUtils.generateSalt(SALT_SIZE_BYTES * 8); try { SecretKeyFactory skf = SecretKeyFactory.getInstance(config.getPbkdf2Algorithm()); PBEKeySpec keySpec = new PBEKeySpec(password, salt, iterations, 64 * 8); SecretKey secretKey = skf.generateSecret(keySpec); return String.format("%d:%s:%s", iterations, DatatypeConverter.printHexBinary(salt), DatatypeConverter.printHexBinary(secretKey.getEncoded())); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { throw new CryptoOperationRuntimeException("cannot generate strong password hash", e); } } /** * Validates a given clear text password against its stored expected hashed value in a time constant manner. * * @param passwd the password to be tested * @param storedHash the stored expected hashed value * @return true if the password matches the stored hash */ public static boolean validateStrongPasswordHash(char[] passwd, String storedHash) { String[] parts = storedHash.split(":"); if (parts.length != 3) { return false; } int iterations = Integer.parseInt(parts[0]); byte[] salt = fromHex(parts[1]); byte[] hash = fromHex(parts[2]); try { PBEKeySpec keySpec = new PBEKeySpec(passwd, salt, iterations, hash.length * 8); SecretKeyFactory skf = SecretKeyFactory.getInstance(config.getPbkdf2Algorithm()); byte[] testHash = skf.generateSecret(keySpec).getEncoded(); return constantTimeArrayCompare(hash, testHash); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { throw new CryptoOperationRuntimeException("cannot validate strong password hash", e); } } private static boolean constantTimeArrayCompare(byte[] a1, byte[] a2) { int result = 0; for (int i = 0; i < a1.length; i++) { result |= a1[i] ^ a2[i]; } return result == 0; } private static byte[] fromHex(String hex) { byte[] bytes = new byte[hex.length() / 2]; for (int i = 0; i < bytes.length; i++) { bytes[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16); } return bytes; } }