/**
* Copyright (C) 2013 Jonathan Gillett, Joseph Heron
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.tinfoil.sms.crypto;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.Security;
import java.util.HashMap;
import org.strippedcastle.crypto.InvalidCipherTextException;
import org.strippedcastle.crypto.digests.SHA256Digest;
import org.strippedcastle.crypto.engines.ISAACEngine;
import org.strippedcastle.crypto.params.ECPrivateKeyParameters;
import org.strippedcastle.crypto.params.ECPublicKeyParameters;
import org.strippedcastle.crypto.prng.RandomGenerator;
import org.strippedcastle.jce.provider.BouncyCastleProvider;
import android.util.Base64;
import android.util.Log;
import com.bugsense.trace.BugSenseHandler;
import com.orwell.crypto.APrioriInfo;
import com.orwell.crypto.ECEngine;
import com.orwell.crypto.ECGKeyUtil;
import com.orwell.csprng.ISAACRandomGenerator;
import com.orwell.csprng.SDFGenerator;
import com.orwell.params.ECKeyParam;
import com.orwell.params.Nonce;
import com.orwell.params.SDFParameters;
import com.tinfoil.sms.dataStructures.Number;
import com.tinfoil.sms.dataStructures.User;
/**
* A class which operates as a facade to greatly simplify the underlying
* complexity required to encrypt/decrypt messages using Elliptic Curve
* Cryptography.
*/
public class Encryption
{
private HashMap<Long, ECEngine> encryptMap;
private HashMap<Long, ECEngine> decryptMap;
private User user;
/* Size of the message counter in bytes, usually 2 bytes, based on Nonce.MAX_CYCLES */
private static final int COUNT_SIZE = 2;
private static final int NONCE_VARIANCE_THRESHOLD = 10;
/* Register spongycastle as the most preferred security provider */
static {
Security.insertProviderAt(new BouncyCastleProvider(), 1);
}
/**
* The basic constructor, initializes the encrypt/decrypt hash maps.
*/
public Encryption(User user)
{
this.encryptMap = new HashMap<Long, ECEngine>();
this.decryptMap = new HashMap<Long, ECEngine>();
this.user = user;
}
/**
* Encrypts the message provided using the public key belonging to the number
* and adds a 32 byte verification signature (HMAC) to the message.
*
* @param number The number that the message is to be encrypted for
* @param message The plaintext message to encrypt
*
* @return The ciphertext of the encrypted message
*
* @throws InvalidCipherTextException If an error occurs attempting to encrypt the message.
*/
public String encrypt(Number number, String message) throws InvalidCipherTextException
{
byte[] encMessage;
byte[] finalMessage;
Log.v("Original Message", message);
/* Initialize the encryption engine if the number is not in hash map */
if (! encryptMap.containsKey(number.getId()))
{
initECEngine(number, true);
}
/* Encrypt the message, add the counter to the message, which is used by the nonce */
encMessage = encryptMap.get(number.getId()).processBlock(message.getBytes());
finalMessage = addCounter(encMessage, number.getNonceEncrypt());
/* Log data for debugging, in the event of a crash */
Log.v("Nonce counter:", Integer.toString(number.getNonceEncrypt()));
BugSenseHandler.addCrashExtraData("Nonce", Integer.toString(number.getNonceEncrypt()));
/* Increment and save the counter used by the nonce for encryption */
number.setNonceEncrypt(number.getNonceEncrypt() + 1);
/* Log data for debugging, in the event of a crash */
Log.v("Encrypted Message", Base64.encodeToString(finalMessage, Base64.DEFAULT));
BugSenseHandler.addCrashExtraData("Encrypted", new String(finalMessage));
BugSenseHandler.addCrashExtraData("Encoded", Base64.encodeToString(finalMessage, Base64.DEFAULT));
return Base64.encodeToString(finalMessage, Base64.DEFAULT);
}
/**
* Decrypts the message provided using the public key belonging to the number
* and adds a 32 byte verification signature (HMAC) to the message.
*
* @param number The number that the message is to be decrypted for
* @param message The ciphertext message to decrypt
*
* @return The plaintext of the decrypted message
*
* @throws InvalidCipherTextException If an error occurs attempting to encrypt the message.
*/
public String decrypt(Number number, String message) throws InvalidCipherTextException
{
/* Log data, before it's decrypted, in the event of a crash */
BugSenseHandler.addCrashExtraData("Original", message);
byte[] decodedMessage = Base64.decode(message, Base64.DEFAULT);
byte[] encMessage = new byte[decodedMessage.length - COUNT_SIZE];
byte[] decMessage;
Log.v("Encypted Message Received", message);
/* Get the nonce counter from the message */
Integer counter = getCounter(decodedMessage);
Log.v("Nonce counter received:", Integer.toString(counter));
/* Log data, before it's decrypted, in the event of a crash */
BugSenseHandler.addCrashExtraData("Nonce Received", Integer.toString(counter));
BugSenseHandler.addCrashExtraData("Nonce", Integer.toString(number.getNonceDecrypt()));
/* Re-initialize the encryption engine if the nonce counter is incorrect
* due to message not being received in the correct order (benefit of the doubt)
*
* NOTE: In the event of a malicious attack (trying to exhaust the nonce counter)
* the engine will not be re-initialized, it will only be reinitialized if the counter
* is greater than the current counter (to avoid an attack of re-using IVs) and
* if it is greater by at most 10.
*/
if ((counter > number.getNonceDecrypt()) && (counter - number.getNonceDecrypt()) <= NONCE_VARIANCE_THRESHOLD)
{
Log.v("Re-initializing nonce counter to:", Integer.toString(counter));
number.setNonceDecrypt(counter);
initECEngine(number, false);
}
/* Otherwise, initialize the encryption engine if the number is not in hash map */
else if (! decryptMap.containsKey(number.getId()))
{
initECEngine(number, false);
}
/* Remove the nonce counter from the message received */
System.arraycopy(decodedMessage, COUNT_SIZE, encMessage, 0, encMessage.length);
/* Log data, before it's decrypted, in the event of a crash */
BugSenseHandler.addCrashExtraData("Decoded", new String(decodedMessage));
BugSenseHandler.addCrashExtraData("Encrypted", new String(encMessage));
/* decrypt the message, increment and save the nonce counter */
decMessage = decryptMap.get(number.getId()).processBlock(encMessage);
number.setNonceDecrypt(number.getNonceDecrypt() + 1);
Log.v("Decrypted Message", new String(decMessage));
return new String(decMessage);
}
/**
* Initializes the encryption engine given the number, which contains
* the public key, shared apriori info, and nonces, unique to that
* number, which is needed to initialize the encryption engine.
*
* @param number The number, which contains the cryptographic info
* @param mode The mode to initialize the engine as, true for encryption
* and false for decryption mode.
*/
private void initECEngine(Number number, boolean mode)
{
/* Initialize the seed derivative function, using SHA256, which is used
* to generate the unique seed used by the deterministic CSPRNG
*/
SDFGenerator generator = new SDFGenerator(new SHA256Digest());
/* Initialize the seed generator based on the state of who initialized
* the original key exchange, the initiator uses shared information S1 + S2
* for encrypt, S2 + S1 for decrypt, the recipient uses the inverse.
*/
String sharedInfo1, sharedInfo2;
APrioriInfo sharedInfo;
if (number.isInitiator())
{
sharedInfo1 = number.getSharedInfo1();
sharedInfo2 = number.getSharedInfo2();
}
/* Number was key exchange recipient, use the inverse */
else
{
sharedInfo1 = number.getSharedInfo2();
sharedInfo2 = number.getSharedInfo1();
}
Log.v("SharedInfo 1", sharedInfo1);
Log.v("SharedInfo 2", sharedInfo2);
/* Setup apriori info, generator, and nonce used by the block cipher to generate IVs */
RandomGenerator nonce;
ISAACRandomGenerator CSPRNG = new ISAACRandomGenerator(new ISAACEngine());
if (mode)
{
/* encryption mode */
sharedInfo = new APrioriInfo(sharedInfo1, sharedInfo2);
generator.init(new SDFParameters(sharedInfo1, sharedInfo2));
nonce = new Nonce(CSPRNG, number.getNonceEncrypt());
/* Log data for debugging, in the event of a crash */
Log.v("Nonce Encrypt", String.valueOf(number.getNonceEncrypt()));
BugSenseHandler.addCrashExtraData("Nonce Encrypt", String.valueOf(number.getNonceEncrypt()));
}
else
{
/* decryption mode */
sharedInfo = new APrioriInfo(sharedInfo2, sharedInfo1);
generator.init(new SDFParameters(sharedInfo2, sharedInfo1));
nonce = new Nonce(CSPRNG, number.getNonceDecrypt());
/* Log data for debugging, in the event of a crash */
Log.v("Nonce Decrypt", String.valueOf(number.getNonceDecrypt()));
BugSenseHandler.addCrashExtraData("Nonce Decrypt", String.valueOf(number.getNonceDecrypt()));
}
/* Generate the seed, initialize the nonce */
byte[] seed = new byte[generator.getDigest().getDigestSize()];
generator.generateBytes(seed, 0, 0);
((Nonce)nonce).init(seed, seed.length);
/* Initialize the keypair using the current user's private key and the number's public key */
ECKeyParam param = new ECKeyParam();
/* Log keys for debugging, in the event of a crash */
Log.v("My private key", new String(user.getPrivateKey()));
Log.v("Number's public key", new String(number.getPublicKey()));
BugSenseHandler.addCrashExtraData("Public Key", new String(number.getPublicKey()));
ECPrivateKeyParameters priKey = ECGKeyUtil.decodeBase64PriKey(param, user.getPrivateKey());
ECPublicKeyParameters pubKey = ECGKeyUtil.decodeBase64PubKey(param, number.getPublicKey());
/* Finally initialize the encryption engine */
ECEngine engine = new ECEngine(nonce, sharedInfo);
engine.init(mode,priKey,pubKey);
/* Add the engine to the hash map */
if (mode)
{
encryptMap.put(number.getId(), engine);
}
else
{
decryptMap.put(number.getId(), engine);
}
}
/**
* Prefixes the message count to the beginning of the message that is to be encrypted
*
* @param message The message to prefix the counter to
* @return The message with the counter value prefixed
*/
private byte[] addCounter(byte[] message, Integer counter)
{
byte[] output = new byte[message.length + COUNT_SIZE];
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.putInt(counter);
/* Truncate the integer to COUNTER_SIZE and prefix it to output */
System.arraycopy(buffer.array(), 2, output, 0, COUNT_SIZE);
System.arraycopy(message, 0, output, COUNT_SIZE, message.length);
return output;
}
/**
* Gets the prefixed message count from the message received
* @param message The message received to get the prefixed counter from
* @return The counter value
*/
private Integer getCounter(byte[] message)
{
byte[] counter = new byte[COUNT_SIZE];
System.arraycopy(message, 0, counter, 0, COUNT_SIZE);
/* Convert the nonce back to int */
ByteBuffer buffer = ByteBuffer.wrap(counter);
buffer.order(ByteOrder.BIG_ENDIAN);
/* Convert the nonce value to an unsigned short */
return buffer.getShort() & 0xFFFF;
}
}