package ch.ge.ve.commons.crypto.ballot;
/*-
* #%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.AuthenticationTagMismatchException;
import ch.ge.ve.commons.crypto.exceptions.CryptoConfigurationRuntimeException;
import ch.ge.ve.commons.crypto.exceptions.CryptoOperationRuntimeException;
import ch.ge.ve.commons.crypto.exceptions.PrivateKeyPasswordMismatchException;
import ch.ge.ve.commons.crypto.utils.SecureRandomFactory;
import ch.ge.ve.commons.properties.PropertyConfigurationService;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.modes.AEADBlockCipher;
import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;
import javax.crypto.*;
import javax.crypto.spec.GCMParameterSpec;
import java.io.*;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
/**
* This class is responsible for managing the encryption and decryption of ballot related elements:
* <ul>
* <li>the ballot contents themselves, see {@link #encryptBallotThenWrapForAuthentication(String, int)}, {@link #verifyAuthenticationThenUnwrap(AuthenticatedBallot)} and {@link #decryptBallot(EncryptedBallotAndWrappedKey)} </li>
* </ul>
*/
public class BallotCipherService {
public static final int AEAD_TAG_SIZE = 128;
private final BallotCiphersProvider ciphersProvider;
private final PropertyConfigurationService propertyConfigurationService;
/**
* The default constructor.
*
* @param ciphersProvider the {@link BallotCiphersProvider} to use
* @param propertyConfigurationService the {@link PropertyConfigurationService} defining the algorithms to use and other parameters.
*/
public BallotCipherService(BallotCiphersProvider ciphersProvider, PropertyConfigurationService propertyConfigurationService) {
this.ciphersProvider = ciphersProvider;
this.propertyConfigurationService = propertyConfigurationService;
ciphersProvider.setPropertyConfigurationService(propertyConfigurationService);
}
private static byte[] toByteArray(SealedObject sealedBallot) throws IOException {
byte[] cipheredBallot;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutput out = null;
try {
out = new ObjectOutputStream(bos);
out.writeObject(sealedBallot);
cipheredBallot = bos.toByteArray();
} finally {
if (out != null) {
out.close();
}
bos.close();
}
return cipheredBallot;
}
private static SealedObject toSealedObject(byte[] ballotContentBytes) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(ballotContentBytes));
return (SealedObject) ois.readObject();
}
/**
* Retrieves the plain name of the algorithm from the transformation description.
*
* @param transformation expected input examples: "AES/CBC/PKCS5PADDING", "AES", ...
* @return the name of the underlying algorithm (<i>e.g.</i> "AES", "RSA", ...)
*/
private static String getAlgoPlainName(String transformation) {
return transformation.split("/")[0];
}
/**
* Encrypts the ballot contents (supports any String), to an AuthenticatedBallot
* <p>The process is two-fold</p>
* <ul>
* <li>First, the ballot is encrypted using the Election officials public key and standard mixed encryption</li>
* <li>Second, the resulting encrypted ballot and wrapped key are encrypted a second time, using an AEAD cipher,
* using the ballot index as associated data.</li>
* </ul>
* <p>The result is an AuthenticatedBallot</p>
*
* @param plainText the plainText to be encrypted
* @param ballotIndex the index of the ballot (since it is authenticated, it prevents the copy of one vote to another)
* @return an AuthenticatedBallot ready for storage
* @throws CryptoConfigurationRuntimeException
* @throws CryptoOperationRuntimeException
*/
public AuthenticatedBallot encryptBallotThenWrapForAuthentication(String plainText, int ballotIndex) {
Cipher ballotKeyCipher = ciphersProvider.getBallotKeyCipher();
Cipher ballotCipher = ciphersProvider.getBallotCipher();
// Generate a random symmetric key, renewed for each ballot
Key plainSymmetricKey = createNewRandomSymmetricKey(ballotCipher);
// Initialise the first layer symmetric cipher and perform the first layer of symmetric encryption
byte[] encryptedBallot = doFirstLayerEncryption(plainText, ballotCipher, plainSymmetricKey);
Key integrityKey = ciphersProvider.getIntegrityCheckSecretKey();
Cipher integrityCipher = ciphersProvider.getIntegrityCipher(propertyConfigurationService);
byte[] authenticatedBallot = aeadEncrypt(integrityCipher, integrityKey, ballotIndex, encryptedBallot);
// Due to the lack of an API to retrieve the tag from the cipher, it must be extracted from the resulting ciphertext
// BouncyCastle simply appends the tag to the ciphertext and verifies it upon decryption
byte[] tag = Arrays.copyOfRange(authenticatedBallot, authenticatedBallot.length - (AEAD_TAG_SIZE / Byte.SIZE), authenticatedBallot.length);
// Wrapping of the random symmetric key using the Electoral Officers' public key
byte[] wrappedKey = wrapKey(ballotKeyCipher, plainSymmetricKey);
return new AuthenticatedBallot(wrappedKey, authenticatedBallot, ballotIndex, tag);
}
private Key createNewRandomSymmetricKey(Cipher ballotCipher) {
KeyGenerator generator;
try {
generator = KeyGenerator.getInstance(getAlgoPlainName(ballotCipher.getAlgorithm()));
} catch (NoSuchAlgorithmException e) {
throw new CryptoConfigurationRuntimeException("first layer symmetric cipher key algorithm is invalid", e);
}
generator.init(ciphersProvider.getBallotCipherSize(), SecureRandomFactory.createPRNG());
return generator.generateKey();
}
private byte[] doFirstLayerEncryption(String plainText, Cipher ballotCipher, Key plainSymmetricKey) {
try {
ballotCipher.init(Cipher.ENCRYPT_MODE, plainSymmetricKey, SecureRandomFactory.createPRNG());
} catch (InvalidKeyException e) {
throw new CryptoConfigurationRuntimeException("first layer symmetric cipher key is invalid", e);
}
byte[] encryptedBallot;
try {
SealedObject sealedObject = new SealedObject(plainText, ballotCipher);
encryptedBallot = toByteArray(sealedObject);
} catch (IOException | IllegalBlockSizeException e) {
throw new CryptoOperationRuntimeException("cannot seal message", e);
}
return encryptedBallot;
}
private byte[] aeadEncrypt(Cipher cipher, Key integrityKey, int ballotIndex, byte[] encryptedBallot) {
byte[] ballotIndexBytes = BigInteger.valueOf(ballotIndex).toByteArray();
GCMParameterSpec spec = new GCMParameterSpec(AEAD_TAG_SIZE, ballotIndexBytes);
byte[] result;
try {
cipher.init(Cipher.ENCRYPT_MODE, integrityKey, spec);
cipher.updateAAD(ballotIndexBytes);
result = cipher.doFinal(encryptedBallot);
} catch (BadPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
throw new CryptoOperationRuntimeException("error while encrypting the cipher text", e);
}
return result;
}
private byte[] wrapKey(Cipher ballotKeyCipher, Key plainSymmetricKey) {
byte[] wrappedKey;
try {
ballotKeyCipher.init(Cipher.WRAP_MODE, ciphersProvider.getBallotKeyCipherPublicKey(), SecureRandomFactory.createPRNG());
wrappedKey = ballotKeyCipher.wrap(plainSymmetricKey);
} catch (InvalidKeyException e) {
throw new CryptoConfigurationRuntimeException("wrapping public key is invalid", e);
} catch (IllegalBlockSizeException e) {
throw new CryptoOperationRuntimeException("cannot wrap key", e);
}
return wrappedKey;
}
/**
* Performs the first layer of decryption of a ballot, verifying its authenticity
*
* @param authenticatedBallot an AuthenticatedBallot, as previously built by {@link #encryptBallotThenWrapForAuthentication(String, int)} and stored in the database
* @return an EncryptedBallotAndWrappedKey
* @throws AuthenticationTagMismatchException
* @throws CryptoOperationRuntimeException
*/
public EncryptedBallotAndWrappedKey verifyAuthenticationThenUnwrap(AuthenticatedBallot authenticatedBallot) throws AuthenticationTagMismatchException {
Key integrityKey = ciphersProvider.getIntegrityCheckSecretKey();
Cipher integrityCipher = ciphersProvider.getIntegrityCipher(propertyConfigurationService);
byte[] bytes;
try {
bytes = aeadDecrypt(integrityCipher, integrityKey, authenticatedBallot.getBallotIndex(), authenticatedBallot.getAuthenticatedEncryptedBallot());
} catch (AEADBadTagException e) {
throw new AuthenticationTagMismatchException(e.getMessage());
}
try {
return new EncryptedBallotAndWrappedKey(toSealedObject(bytes), authenticatedBallot.getWrappedKey());
} catch (IOException | ClassNotFoundException e) {
throw new CryptoOperationRuntimeException("second layer decryption error", e);
}
}
private byte[] aeadDecrypt(Cipher cipher, Key integrityKey, int ballotIndex, byte[] authenticatedEncryptedBallot) throws AEADBadTagException {
byte[] ballotIndexBytes = BigInteger.valueOf(ballotIndex).toByteArray();
GCMParameterSpec spec = new GCMParameterSpec(AEAD_TAG_SIZE, ballotIndexBytes);
byte[] result;
try {
cipher.init(Cipher.DECRYPT_MODE, integrityKey, spec);
cipher.updateAAD(ballotIndexBytes);
result = cipher.doFinal(authenticatedEncryptedBallot);
} catch (AEADBadTagException e) {
// In case of a tag mismatch, we want the exception to be handled by the caller
throw e;
} catch (BadPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
throw new CryptoOperationRuntimeException("error while decrypting the cipher text", e);
}
return result;
}
/**
* Performs the second layer of decryption of the ballot, using the Election Officers' private key and standard
* mixed encryption
*
* @param encryptedBallotAndWrappedKey an EncryptedBallotAndWrappedKey, as provided per a previous call to {@link #verifyAuthenticationThenUnwrap(AuthenticatedBallot)}
* @return the ballot's original contents
* @throws CryptoConfigurationRuntimeException
* @throws CryptoOperationRuntimeException
*/
public String decryptBallot(EncryptedBallotAndWrappedKey encryptedBallotAndWrappedKey) {
Cipher ballotKeyCipher = ciphersProvider.getBallotKeyCipher();
Cipher ballotCipher = ciphersProvider.getBallotCipher();
// Unwrap the random key k_i, using the Election Officers' private key
try {
ballotKeyCipher.init(Cipher.UNWRAP_MODE, ciphersProvider.getBallotKeyCipherPrivateKey());
} catch (InvalidKeyException e) {
throw new CryptoConfigurationRuntimeException("decryption key is invalid", e);
}
Key plainSymmetricKey;
try {
plainSymmetricKey = ballotKeyCipher.unwrap(encryptedBallotAndWrappedKey.getWrappedKey(), getAlgoPlainName(ballotCipher.getAlgorithm()), Cipher.SECRET_KEY);
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new CryptoOperationRuntimeException("key unwrapping error", e);
}
// Decrypt the ballot using the unwrapped key k_i
SealedObject sealedBallot = encryptedBallotAndWrappedKey.getEncryptedBallot();
try {
return (String) sealedBallot.getObject(plainSymmetricKey);
} catch (IOException | ClassNotFoundException | NoSuchAlgorithmException | InvalidKeyException e) {
throw new CryptoOperationRuntimeException("ballot decryption error", e);
}
}
/**
* Unlocks the private key for decryption.
*
* @param password the password of the key file
* @throws PrivateKeyPasswordMismatchException
*/
public void loadBallotKeyCipherPrivateKey(String password) throws PrivateKeyPasswordMismatchException {
ciphersProvider.loadBallotKeyCipherPrivateKey(password);
}
/**
* Sets the path to the private key file.
*
* @param privateKeyFileName the path to the private key
*/
public void setPrivateKeyFileName(String privateKeyFileName) {
ciphersProvider.invalidatePrivateKeyCache();
propertyConfigurationService.addConfigValue(BallotCiphersProvider.PRIVATE_KEY_FILE_NAME, privateKeyFileName);
}
/**
* Sets the path to the public key file
*
* @param publicKeyFileName the path to the public key
*/
public void setPublicKeyFileName(String publicKeyFileName) {
ciphersProvider.invalidatePublicKeyCache();
propertyConfigurationService.addConfigValue(BallotCiphersProvider.PUBLIC_KEY_FILE_NAME, publicKeyFileName);
}
/**
* Sets the path to the integrity key file
*
* @param integrityKeyFileName the path to the integrity key
*/
public void setIntegrityKeyFileName(String integrityKeyFileName) {
ciphersProvider.invalidateIntegrityKeyCache();
propertyConfigurationService.addConfigValue(BallotCiphersProvider.INTEGRITY_KEY_FILE_NAME, integrityKeyFileName);
}
}