/**
* 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.security.Security;
import java.util.Arrays;
import java.util.zip.CRC32;
import org.strippedcastle.crypto.digests.SHA256Digest;
import org.strippedcastle.crypto.params.ECPublicKeyParameters;
import org.strippedcastle.jce.provider.BouncyCastleProvider;
import org.strippedcastle.util.encoders.Hex;
import android.util.Base64;
import android.util.Log;
import com.orwell.crypto.APrioriInfo;
import com.orwell.crypto.ECGKeyExchange;
import com.orwell.crypto.ECGKeyUtil;
import com.orwell.params.ECKeyParam;
import com.tinfoil.sms.dataStructures.Number;
import com.tinfoil.sms.dataStructures.User;
import com.tinfoil.sms.database.DBAccessor;
import com.tinfoil.sms.database.InvalidDatabaseStateException;
import com.tinfoil.sms.utility.SMSUtility;
/**
* A class which operates as a facade to greatly simplify the underlying complexity
* required to sign and verify keys needed to execute the public key exchange operations.
*
* TODO Add support to get the signature for the keys so it can be added to the DB
* TODO CLEAN THIS CLASS UP!
*/
public abstract class KeyExchange
{
/* The size in bytes, of the checksum used, CRC32 is 4 bytes (32 bits) */
private static final int CHECKSUM_SIZE = 4;
public static final int VALID_KEY_EXCHANGE = 0;
public static final int VALID_KEY_REVERSE = 1;
public static final int INVALID_KEY_EXCHANGE = 2;
/* Register spongycastle as the most preferred security provider */
static {
Security.insertProviderAt(new BouncyCastleProvider(), 1);
}
/**
* Attempts to identify if the message received is a key exchange by checking
* if the message is encoded as Base64, matches the expected length of a
* key exchange message, and has a valid CRC32 checksum.
*/
public static boolean isKeyExchange(String message)
{
byte[] decodedMsg = null;
try
{
/* Test if message is a key exchange, based on length and checksum */
decodedMsg = Base64.decode(message, Base64.DEFAULT);
Log.v("decoded message", new String(decodedMsg));
Log.v("decoded message length", Integer.toString(decodedMsg.length));
if (decodedMsg.length > 32 && validChecksum(decodedMsg))
{
Log.v("Message is key exchange", new String(Hex.encode(decodedMsg)));
return true;
}
}
catch (IllegalArgumentException e)
{
e.printStackTrace();
return false;
}
return false;
}
/**
* Gets the public key from the BASE64 encoded key exchange, which contains
* the key and signature, and returns the public key encoded as BASE64, for
* proper storage and transmission in textual form.
*
* @param signedPubKey The signed public key the user received from the number
*
* @return The public key received, encoded as BASE64 for storage
*/
public static byte[] encodedPubKey(String signedPubKey)
{
ECKeyParam param = new ECKeyParam();
/* Decode the message in order to remove the checksum, then get the pubkey */
byte[] decodedSignedPubKey = stripChecksum(Base64.decode(signedPubKey, Base64.DEFAULT));
ECPublicKeyParameters pubKey = ECGKeyUtil.decodeBase64SignedPubKey(
param,
new SHA256Digest(),
Base64.encode(decodedSignedPubKey, Base64.DEFAULT));
return ECGKeyUtil.encodeBase64PubKey(param, pubKey);
}
/**
* Gets the signature from the BASE64 encoded key exchange, which contains
* the key and signature, and returns the signature encoded as BASE64, for
* proper storage and transmission in textual form.
*
* @param signedPubKey The signed public key the user received from the number
*
* @return The public key received, encoded as BASE64 for storage
*/
public static byte[] encodedSignature(String signedPubKey)
{
byte[] decodedSignedPubKey = stripChecksum(Base64.decode(signedPubKey, Base64.DEFAULT));
SHA256Digest digest = new SHA256Digest();
byte[] signature = new byte[digest.getDigestSize()];
System.arraycopy(
decodedSignedPubKey,
decodedSignedPubKey.length - digest.getDigestSize(),
signature,
0,
digest.getDigestSize());
return Base64.encode(signature, Base64.DEFAULT);
}
/**
* Wrapper method to sign a key exchange with the user's public key. This is used
* to ensure that the user's information is in memory (and if not it is retrieved)
* and catches and handles possible errors involving illegal database states.
*
* Signs the current user's public key using the apriori information shared
* between the current user and the number provided. After signing the
* public key the CRC32 checksum is calculated and appended to the signed
* public key.
*
* -------------------------------------
* | public key | signature | checksum |
* -------------------------------------
*
* @param number The contact's Number.
* @param dba The database interface.
* @param user The user's information.
* @return The current user's signed public key based on shared information
* the user has with the number given.
*/
public static String sign(Number number, DBAccessor dba, User user)
{
try {
user = SMSUtility.getUser(dba, user);
return signWithUser(number, user);
} catch (InvalidDatabaseStateException e) {
//TODO create error message to tell user key exchange failed because of db issue
e.printStackTrace();
}
return null;
}
/**
* Signs the current user's public key using the apriori information shared
* between the current user and the number provided. After signing the
* public key the CRC32 checksum is calculated and appended to the signed
* public key.
*
* -------------------------------------
* | public key | signature | checksum |
* -------------------------------------
*
* @param number The number that the key will be exchanged with
* @param user The user's information containing the user's public key.
*
* @return The current user's signed public key based on shared information
* the user has with the number given.
*/
private static String signWithUser(Number number, User user)
{
/* Get the current user's public key and shared information */
ECKeyParam param = new ECKeyParam();
ECPublicKeyParameters pubKey = ECGKeyUtil.decodeBase64PubKey(param, user.getPublicKey());
APrioriInfo sharedInfo = new APrioriInfo(number.getSharedInfo1(), number.getSharedInfo2());
/* Sign the public key using the shared information based on whether the
* current user is the initiator of the key exchange with the number
*/
byte[] encodedPubKey = ECGKeyUtil.encodePubKey(param, pubKey);
byte[] encodedSignedPubKey = ECGKeyExchange.signPubKey(
new SHA256Digest(),
encodedPubKey,
sharedInfo,
number.isInitiator());
/* Calculate the checksum of the signed public key */
byte[] checksum = checksum(encodedSignedPubKey);
byte[] signedPubKeySum = new byte[encodedSignedPubKey.length + checksum.length];
Log.v("checksum", new String(checksum));
Log.v("checksum length", Integer.toString(checksum.length));
Log.v("publickey", Base64.encodeToString(encodedPubKey, Base64.DEFAULT));
Log.v("Shared Info1:", number.getSharedInfo1());
Log.v("Shared Info2:", number.getSharedInfo2());
Log.v("decoded signedpubkey", new String(Hex.encode(encodedSignedPubKey)));
System.arraycopy(encodedSignedPubKey, 0, signedPubKeySum, 0, encodedSignedPubKey.length);
System.arraycopy(checksum, 0, signedPubKeySum, encodedSignedPubKey.length, checksum.length);
/* Return the signed public key in a BASE64 encoded, transmissible form */
Log.v("encoded signedpubkey", Base64.encodeToString(signedPubKeySum, Base64.DEFAULT));
return Base64.encodeToString(signedPubKeySum, Base64.DEFAULT);
}
/**
* Facilitate the verification of the key exchange and allow for
* the user to decide if they wish to ignore the security vulnerability
* caused by accepting a key exchange initiation from the other contact
* after initiating a key exchange them self. (User A sends a key
* exchange to User B and then receives a key exchange from User B
* but the key exchange has User B as the initiator) This could lead to
* a man in the middle attack.
*
* @param number The contact's Number
* @param signedPubKey The signed public key received from the contact
* @return If it is a valid key exchange the return will be VALID_KEY_EXCHANGE,
* if the sender was set as the initiator (reversed) the return will be
* VALID_KEY_REVERSE, otherwise it will return INVALID_KEY_EXCHANGE
*/
public static int validateKeyExchange(Number number, String signedPubKey)
{
if(verify(number, signedPubKey))
{
return VALID_KEY_EXCHANGE;
}
if(number.isInitiator())
{
// Check if the key exchange is initiated by the other person
number.setInitiator(false);
if(verify(number, signedPubKey))
{
return VALID_KEY_REVERSE;
}
// Set the initiator back to previous state
number.setInitiator(true);
}
return INVALID_KEY_EXCHANGE;
}
/**
* Verifies the public key received and verifies that the signature is valid
* given the apriori information shared between the current user and the
* number the key has been received from.
*
* @param number The number that the key will be exchanged with
* @param signedPubKey The signed public key the user received from the number
*
* @return True if the signed public key received is valid
*
* TODO possibly throw an exception instead
*/
public static boolean verify(Number number, String signedPubKey)
{
APrioriInfo sharedInfo = new APrioriInfo(number.getSharedInfo1(), number.getSharedInfo2());
Log.v("signedpubkey received", signedPubKey);
Log.v("Shared Info1:", number.getSharedInfo1());
Log.v("Shared Info2:", number.getSharedInfo2());
/* Decode the key exchange, and strip the checksum */
byte[] decodedSignedPubKey = stripChecksum(Base64.decode(signedPubKey, Base64.DEFAULT));
Log.v("decoded signedpubkey received", new String(Hex.encode(decodedSignedPubKey)));
boolean result = ECGKeyExchange.verifyPubKey(
new SHA256Digest(),
decodedSignedPubKey,
sharedInfo,
number.isInitiator());
Log.v("valid key?", Boolean.toString(result));
return result;
}
/**
* Calculates the CRC32 checksum of the input provided.
*
* @param input The input to calculate the CRC32 checksum for
* @return The CRC32 checksum
*/
private static byte[] checksum(byte[] input)
{
ByteBuffer buffer = ByteBuffer.allocate(8);
CRC32 checksum = new CRC32();
checksum.update(input);
buffer.putLong(checksum.getValue());
Log.v("checksum as hex", Long.toHexString(checksum.getValue()));
/* Since CRC32 is 4 bytes , truncate the value to only be 4 bytes */
byte[] checksum32 = new byte[CHECKSUM_SIZE];
System.arraycopy(buffer.array(), 4, checksum32, 0, CHECKSUM_SIZE);
Log.v("checksum converted to 4 bytes", new String(Hex.encode(checksum32)));
return checksum32;
}
/**
* Strips the checksum from the byte data for the signed public key
* used in the key exchange, which includes the additional checksum.
*/
private static byte[] stripChecksum(byte[] input)
{
byte[] data = new byte[input.length - CHECKSUM_SIZE];
/* Remove the checksum from the input data */
System.arraycopy(input, 0, data, 0, data.length);
return data;
}
/**
* Calculates if the checksum the input has matches the calculated
* checksum, returns true if the checksum is valid.
*
* @param input Which includes the checksum to validate
* @return True if the input has a valid checksum
*/
private static boolean validChecksum(byte[] input)
{
/* Get the data and the checksum of the data */
byte[] data = new byte[input.length - CHECKSUM_SIZE];
byte[] checksum = new byte[CHECKSUM_SIZE];
System.arraycopy(input, 0, data, 0, data.length);
System.arraycopy(input, data.length, checksum, 0, checksum.length);
/* Calculate the checksum of the data */
byte[] calcChecksum = checksum(data);
Log.v("original checksum", new String(checksum));
Log.v("calculated checksum", new String(calcChecksum));
/* Check if the checksum of the data matches the calculated checksum */
return Arrays.equals(checksum, calcChecksum);
}
}