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.ProtocolNotRespectedException;
import ch.ge.ve.commons.crypto.utils.CertificateUtils;
import ch.ge.ve.commons.crypto.utils.CipherFactory;
import ch.ge.ve.commons.properties.PropertyConfigurationException;
import ch.ge.ve.commons.properties.PropertyConfigurationService;
import com.google.common.base.Strings;
import org.apache.log4j.Logger;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Before;
import org.junit.Test;
import javax.crypto.Cipher;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import static ch.ge.ve.commons.crypto.ballot.BallotCiphersProvider.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
/**
* Tests for the {@link BallotCipherService} class
*/
public class BallotCipherServiceTest {
private final Logger log = Logger.getLogger(this.getClass());
private BallotCiphersProvider ballotCiphersProvider;
private BallotCipherService ballotCipherService;
private PropertyConfigurationService propertyConfigurationService;
@Before
public void setUp() {
ballotCiphersProvider = mock(BallotCiphersProvider.class);
propertyConfigurationService = mock(PropertyConfigurationService.class);
ballotCipherService = new BallotCipherService(ballotCiphersProvider, propertyConfigurationService);
Security.addProvider(new BouncyCastleProvider());
}
/**
* plain ballot encryption should work
*/
@Test
public void testEncryptBallot() throws Exception {
initBallotCiphersProviderMock();
StringBuilder sb = new StringBuilder();
sb.append("GE;6699:9903");
sb.append("370001;0;37000101;p=DEP");
sb.append(Strings.repeat(";1001", 545));
AuthenticatedBallot authenticatedBallot = ballotCipherService.encryptBallotThenWrapForAuthentication(sb.toString(), 1);
assertThat("The encrypted ballot byte array length should be less than 4000 chars", authenticatedBallot.getAuthenticatedEncryptedBallot().length, lessThan(4000));
assertThat("The encrypted ballot key should be 256 bytes long", authenticatedBallot.getWrappedKey().length, is(256));
assertThat("The encryption authentication tag should be 128bit / 16 bytes long", authenticatedBallot.getTag().length, is(16));
assertThat("The authenticated ballot should have the proper index attached", authenticatedBallot.getBallotIndex(), is(1));
}
/**
* encryption followed by decryption of a ballot should return the same object
*/
@Test
public void testEncryptAndDecryptBallot() throws Exception {
initBallotCiphersProviderMock();
String plainText = "plainText";
AuthenticatedBallot authenticatedBallot = ballotCipherService.encryptBallotThenWrapForAuthentication(plainText, 1);
when(ballotCiphersProvider.getBallotKeyCipherPrivateKey()).thenReturn(null);
EncryptedBallotAndWrappedKey encryptedBallotAndWrappedKey = ballotCipherService.verifyAuthenticationThenUnwrap(authenticatedBallot);
initializePrivateKey();
String decryptedText = ballotCipherService.decryptBallot(encryptedBallotAndWrappedKey);
assertThat("decryptBallot(encryptBallotThenWrapForAuthentication(...)) should be identical", decryptedText, equalTo(plainText));
}
/**
* verification of an altered ballot cipher should fail
*/
@Test(expected = AuthenticationTagMismatchException.class)
public void testDecryptBallotWithTagMismatch() throws Exception {
initBallotCiphersProviderMock();
String plainText = "plainText";
AuthenticatedBallot authenticatedBallot = ballotCipherService.encryptBallotThenWrapForAuthentication(plainText, 1);
when(ballotCiphersProvider.getBallotKeyCipherPrivateKey()).thenReturn(null);
// alter the tag
// The tag doesn't need to be kept in the Authenticated ballot anymore, since it is included in the cipher text
// authenticatedBallot.getTag()[0] = (byte)(authenticatedBallot.getTag()[0] ^ (byte)1);
int length = authenticatedBallot.getAuthenticatedEncryptedBallot().length;
authenticatedBallot.getAuthenticatedEncryptedBallot()[length - 1] = (byte) (authenticatedBallot.getAuthenticatedEncryptedBallot()[length - 1] ^ (byte) 1);
// should raise an exception
ballotCipherService.verifyAuthenticationThenUnwrap(authenticatedBallot);
}
/**
* stress test the encrypt and decrypt methods; verification are done manually by analysing the provided logs
*/
@Test
public void testEncryptDecryptPerf() throws Exception {
initBallotCiphersProviderMock();
int nbIterations = 200;
List<String> inputs = new ArrayList<String>(3);
inputs.add("testString");
inputs.add("somewhat long string");
inputs.add(Strings.repeat("testString", nbIterations));
String result = String.format("| %-15s | %-15s | %-8s (%3dx) | %-15s | %-8s (%3dx) | %-15s | %-8s (%3dx) |", "input length", "encrypt mean", "encrypt", nbIterations, "verify mean", "verify", nbIterations, "decryptBallot mean", "decryptBallot", nbIterations);
log.info(result);
for (String input : inputs) {
iterateEncryptDecrypt(input, nbIterations);
}
}
private void iterateEncryptDecrypt(String input, int nbIterations) throws ClassNotFoundException, GeneralSecurityException, InvalidCipherTextException, IOException, ProtocolNotRespectedException, AuthenticationTagMismatchException {
List<AuthenticatedBallot> authenticatedBallots = new ArrayList<AuthenticatedBallot>();
// Run a first encryption to initialize the cipher, we want to discard the initialization time
AuthenticatedBallot initBallot = ballotCipherService.encryptBallotThenWrapForAuthentication("test", 0);
final long startEncrypt = System.currentTimeMillis();
for (int i = 0; i < nbIterations; i++) {
authenticatedBallots.add(ballotCipherService.encryptBallotThenWrapForAuthentication(input, i));
}
final long endEncrypt = System.currentTimeMillis();
when(ballotCiphersProvider.getBallotKeyCipherPrivateKey()).thenReturn(null);
// Again, a first run to initialize the cipher
EncryptedBallotAndWrappedKey initVerif = ballotCipherService.verifyAuthenticationThenUnwrap(initBallot);
List<EncryptedBallotAndWrappedKey> encryptedBallotAndWrappedKeys = new ArrayList<EncryptedBallotAndWrappedKey>();
final long startVerify = System.currentTimeMillis();
for (AuthenticatedBallot authenticatedBallot : authenticatedBallots) {
encryptedBallotAndWrappedKeys.add(ballotCipherService.verifyAuthenticationThenUnwrap(authenticatedBallot));
}
final long endVerify = System.currentTimeMillis();
initializePrivateKey();
String init = ballotCipherService.decryptBallot(initVerif);
assertThat(init, equalTo("test"));
List<String> retrievedPlaintexts = new ArrayList<String>();
final long startDecrypt = System.currentTimeMillis();
for (EncryptedBallotAndWrappedKey encryptedBallotAndWrappedKey : encryptedBallotAndWrappedKeys) {
retrievedPlaintexts.add(ballotCipherService.decryptBallot(encryptedBallotAndWrappedKey));
}
final long endDecrypt = System.currentTimeMillis();
assertThat("retrieved plaintexts should all be equal to the input", retrievedPlaintexts, everyItem(equalTo(input)));
final long totalEncryptionTime = endEncrypt - startEncrypt;
final long meanEncryptionTime = totalEncryptionTime / authenticatedBallots.size();
assertThat(meanEncryptionTime, lessThan(500l));
final long totalVerificationTime = endVerify - startVerify;
final long meanVerificationTime = totalVerificationTime / authenticatedBallots.size();
assertThat(meanVerificationTime, lessThan(500l));
final long totalDecryptionTime = endDecrypt - startDecrypt;
final long meanDecryptionTime = totalDecryptionTime / authenticatedBallots.size();
assertThat(meanDecryptionTime, lessThan(1000l));
String result = String.format("| %15d | %15d | %15d | %15d | %15d | %15d | %15d |", input.length(), meanEncryptionTime, totalEncryptionTime, meanVerificationTime, totalVerificationTime, meanDecryptionTime, totalDecryptionTime);
log.info(result);
}
private void initBallotCiphersProviderMock() throws GeneralSecurityException, IOException, ClassNotFoundException, PropertyConfigurationException {
// Instantiate ballotCipher
PropertyConfigurationService propertyConfigurationService1 = new PropertyConfigurationService();
String ballotCipherAlgo = propertyConfigurationService1.getConfigValue(BALLOT_CRYPTING_ALGORITHM);
String ballotBlockmode = propertyConfigurationService1.getConfigValue(BALLOT_CRYPTING_BLOCK_MODE);
Cipher ballotCipher = new CipherFactory(propertyConfigurationService1).getInstance(ballotCipherAlgo + ballotBlockmode);
when(ballotCiphersProvider.getBallotCipher()).thenReturn(ballotCipher);
// Define ballotCipher key size
int ballotCipherSize = 256;
when(ballotCiphersProvider.getBallotCipherSize()).thenReturn(ballotCipherSize);
// Instantiate ballotKeyCipher
String ballotKeyCipherAlgo = propertyConfigurationService1.getConfigValue(BALLOT_KEY_CRYPTING_ALGORITHM);
String ballotKeyCipherBlockmode = propertyConfigurationService1.getConfigValue(BALLOT_KEY_CRYPTING_BLOCKMODE);
Cipher ballotKeyCipher = new CipherFactory(propertyConfigurationService1).getInstance(ballotKeyCipherAlgo + ballotKeyCipherBlockmode);
when(ballotCiphersProvider.getBallotKeyCipher()).thenReturn(ballotKeyCipher);
String ballotIntegrityAlgo = propertyConfigurationService1.getConfigValue(BALLOT_INTEGRITY_CHECK_CRYPTING_ALGORITHM);
String ballotIntegrityBlockmode = propertyConfigurationService1.getConfigValue(BALLOT_INTEGRITY_CHECK_CRYPTING_BLOCK_MODE);
Cipher integrityCipher = new CipherFactory(propertyConfigurationService1).getInstance(ballotIntegrityAlgo + ballotIntegrityBlockmode);
when(ballotCiphersProvider.getIntegrityCipher(propertyConfigurationService)).thenReturn(integrityCipher);
when(ballotCiphersProvider.getMacLength()).thenReturn(128);
// Instantiate public key
final InputStream publicKeyStream = BallotCipherServiceTest.class.getResourceAsStream("/ctrl.der");
java.security.cert.CertificateFactory cf = java.security.cert.CertificateFactory.getInstance("X509");
X509Certificate certPublic = (X509Certificate) cf.generateCertificate(publicKeyStream);
Key ballotKeyCipherPublicKey = certPublic.getPublicKey();
when(ballotCiphersProvider.getBallotKeyCipherPublicKey()).thenReturn(ballotKeyCipherPublicKey);
// Instantiate secret key
final InputStream integrityKeyStream = BallotCipherServiceTest.class.getResourceAsStream("/integrity.key");
ObjectInputStream ois = new ObjectInputStream(integrityKeyStream);
Key integrityCheckSecretKey = (Key) ois.readObject();
ois.close();
when(ballotCiphersProvider.getIntegrityCheckSecretKey()).thenReturn(integrityCheckSecretKey);
}
private void initializePrivateKey() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException {
// Instantiate private key
final InputStream privateKeyStream = BallotCipherServiceTest.class.getResourceAsStream("/ctrl.p12");
KeyStore caKs = CertificateUtils.createPKCS12KeyStore();
caKs.load(privateKeyStream, "testtest".toCharArray());
Key ballotKeyCipherPrivateKey = caKs.getKey("ctrl", "testtest".toCharArray());
when(ballotCiphersProvider.getBallotKeyCipherPrivateKey()).thenReturn(ballotKeyCipherPrivateKey);
}
/**
* loadBallotKeyCipherPrivateKey should be delegated to BallotCiphersProvider
*/
@Test
public void testLoadBallotKeyCipherPrivateKey() throws Exception {
ballotCipherService.loadBallotKeyCipherPrivateKey("password");
verify(ballotCiphersProvider, times(1)).loadBallotKeyCipherPrivateKey("password");
}
/**
* setPrivateKeyFileName should be delegated to BallotCiphersProvider
*/
@Test
public void testSetPrivateKeyFileName() throws Exception {
ballotCipherService.setPrivateKeyFileName("priv_key");
verify(ballotCiphersProvider, times(1)).invalidatePrivateKeyCache();
}
/**
* setPublicKeyFileName should be delegated to BallotCiphersProvider
*/
@Test
public void testSetPublicKeyFileName() throws Exception {
ballotCipherService.setPublicKeyFileName("pub_key");
verify(ballotCiphersProvider, times(1)).invalidatePublicKeyCache();
}
/**
* setIntegrityKeyFileName should be delegated to BallotCiphersProvider
*/
@Test
public void testSetIntegrityKeyFileName() throws Exception {
ballotCipherService.setIntegrityKeyFileName("integrity_key");
verify(ballotCiphersProvider, times(1)).invalidateIntegrityKeyCache();
}
}