package org.bitseal.core;
import java.util.ArrayList;
import java.util.Arrays;
import org.bitseal.R;
import org.bitseal.crypt.AddressGenerator;
import org.bitseal.crypt.CryptProcessor;
import org.bitseal.crypt.KeyConverter;
import org.bitseal.crypt.SigProcessor;
import org.bitseal.data.Address;
import org.bitseal.data.BMObject;
import org.bitseal.data.Message;
import org.bitseal.data.Payload;
import org.bitseal.data.Pubkey;
import org.bitseal.data.QueueRecord;
import org.bitseal.data.UnencryptedMsg;
import org.bitseal.database.AddressProvider;
import org.bitseal.database.AddressesTable;
import org.bitseal.database.MessageProvider;
import org.bitseal.database.MessagesTable;
import org.bitseal.database.PayloadProvider;
import org.bitseal.database.PayloadsTable;
import org.bitseal.database.PubkeyProvider;
import org.bitseal.database.PubkeysTable;
import org.bitseal.database.QueueRecordProvider;
import org.bitseal.database.QueueRecordsTable;
import org.bitseal.services.MessageStatusHandler;
import org.bitseal.util.ArrayCopier;
import org.bitseal.util.ByteFormatter;
import org.bitseal.util.ByteUtils;
import org.bitseal.util.VarintEncoder;
import org.spongycastle.jce.interfaces.ECPrivateKey;
import org.spongycastle.jce.interfaces.ECPublicKey;
import android.util.Base64;
import android.util.Log;
/**
* This class processes incoming messages (i.e. messages received by
* the user of the application)
*
* @author Jonathan Coe
*/
public class IncomingMessageProcessor
{
private static final int MIN_VALID_ADDRESS_VERSION = 1;
private static final int MAX_VALID_ADDRESS_VERSION = 4;
private static final int MIN_VALID_STREAM_NUMBER = 1;
private static final int MAX_VALID_STREAM_NUMBER = 1;
/** In Bitmessage protocol version 3, the network standard value for nonce trials per byte is 1000. */
public static final int NETWORK_NONCE_TRIALS_PER_BYTE = 1000;
/** In Bitmessage protocol version 3, the network standard value for extra bytes is 1000. */
public static final int NETWORK_EXTRA_BYTES = 1000;
private static final int ACK_DATA_LENGTH = 32;
/** Used when broadcasting Intents to the UI so that it can refresh the data it is displaying */
public static final String UI_NOTIFICATION = "uiNotification";
private static final String TAG = "INCOMING_MESSAGE_PROCESSOR";
/**
* Takes a Payload containing the data of a msg encrypted messages and
* processes it, returning a new Message object for each valid message
* found in the given data. <br><br>
*
* @param msgPayload - An Payload containing the payload a possible new msg
*
* @return An boolean indicating whether or not the given Payload contained a new message
* for us
*/
public Message processReceivedMsg(Payload msgPayload)
{
// Attempt to reconstruct the payload into a Msg object
BMObject msgObject = null;
try
{
msgObject = new ObjectProcessor().parseObject(msgPayload.getPayload());
}
catch (RuntimeException runEx)
{
Log.i(TAG, "RuntimeException occurred in IncomingMessageProcessor.processReceivedMsg().\n" +
"The exception message was: " + runEx.getMessage());
return null;
}
// Check whether this msg is an acknowledgement
if (msgObject.getPayload().length == ACK_DATA_LENGTH)
{
// If this msg is an acknowledgement, process it (checking whether it is one that I am awaiting)
processAck(msgObject);
return null;
}
else
{
// This msg is not an acknowledgement. Attempt to decrypt it using each of our addresses
ArrayList<Address> myAddresses = AddressProvider.get(App.getContext()).getAllAddresses();
UnencryptedMsg unencMsg = null;
for (Address a : myAddresses)
{
try
{
unencMsg = attemptMsgDecryption(msgObject, a);
if (unencMsg != null)
{
// Decryption was successful! Now use the reconstructed message to create a new Message object,
// containing the data that will be shown in the UI
Message message = extractMessageFromUnencryptedMsg(unencMsg);
// Check whether this message is a duplicate
MessageProvider msgProv = MessageProvider.get(App.getContext());
boolean messageIsADuplicate = msgProv.detectDuplicateMessage(message);
if (messageIsADuplicate)
{
Log.d(TAG, "Processed a msg which we decrypted successfully but then found to be a duplicate of a message we had already received.\n" +
"This message will therefore be ignored.\n" +
"Message to address: " + message.getToAddress() + "\n" +
"Message from address: " + message.getFromAddress() + "\n" +
"Message subject: " + message.getSubject() + "\n" +
"Message body: " + message.getBody());
return null;
}
else
{
checkPubkeyAndSaveIfNew(unencMsg);
Log.d(TAG, "We received a new message!\n" +
"Message subject: " + message.getSubject());
return message;
}
}
}
catch (RuntimeException e)
{
// If the attempt to decrypt the message fails, move on to the next address
Log.e(TAG, "Runtime exception occurred in IncomingMessageProccessor.processReceivedMsg(). The exception message was: \n"
+ e.getLocalizedMessage());
continue;
}
}
// If we were unable to decrypt the msg with any of our addresses
Log.i(TAG, "Processed a msg which we failed to decrypt with any of our addresses");
return null;
}
}
/**
* Takes the embedded pubkey data from a decrypted msg that we have received
* and checks whether or not we have that pubkey data already. If we do not,
* then we use that data to create a new Pubkey object and save it to the database.
*
* @param unencMsg - The unencrypted msg to check.
*/
private void checkPubkeyAndSaveIfNew(UnencryptedMsg unencMsg)
{
try
{
byte[] publicSigningKey = unencMsg.getPublicSigningKey();
byte[] publicEncryptionKey = unencMsg.getPublicEncryptionKey();
// We have to restore the leading 0x04 byte that is stripped off when public keys are transmitted
byte[] fourByte = new byte[]{4};
publicSigningKey = ByteUtils.concatenateByteArrays(fourByte, publicSigningKey);
publicEncryptionKey = ByteUtils.concatenateByteArrays(fourByte, publicEncryptionKey);
// Check whether or not we have the pubkey for the sender of this message stored in our database
byte[] ripeHash = new AddressGenerator().calculateRipeHash(publicSigningKey, publicEncryptionKey);
PubkeyProvider pubProv = PubkeyProvider.get(App.getContext());
ArrayList<Pubkey> retrievedPubkeys = pubProv.searchPubkeys(PubkeysTable.COLUMN_RIPE_HASH, Base64.encodeToString(ripeHash, Base64.DEFAULT));
if (retrievedPubkeys.size() == 0)
{
Log.i(TAG, "We received a message and found that we do not have the embedded pubkey data already. Therefore we will now save that pubkey data to our database");
// If we do not have it already, save the public key data to the database
Pubkey pubkey = new Pubkey();
pubkey.setBelongsToMe(false);
pubkey.setRipeHash(ripeHash);
pubkey.setObjectVersion(unencMsg.getSenderAddressVersion());
pubkey.setStreamNumber(unencMsg.getStreamNumber());
pubkey.setBehaviourBitfield(unencMsg.getBehaviourBitfield());
pubkey.setPublicSigningKey(publicSigningKey);
pubkey.setPublicEncryptionKey(publicEncryptionKey);
pubkey.setNonceTrialsPerByte(unencMsg.getNonceTrialsPerByte());
pubkey.setExtraBytes(unencMsg.getExtraBytes());
// We don't have all the data that we normally would for a pubkey, so we shall use dummy values as placeholders
pubkey.setPOWNonce(0);
pubkey.setSignatureLength(0);
pubkey.setSignature(new byte[0]);
pubProv.addPubkey(pubkey);
}
}
catch (Exception e)
{
throw new RuntimeException("Exception occurred in IncomingMessageProcessor.checkForPubkeyAndSaveIfNew().\n"
+ "The exception message was: " + e.getLocalizedMessage().toString());
}
}
/**
* Takes a msg that we have determined to be an acknowledgement and
* checks whether it is one which we are awaiting. If so, the status
* of the corresponding Message is updated.
*
* @param msg - A msg object containing the acknowledgement to be processed
*/
private void processAck(BMObject msg)
{
// Get the ack data from the msg
byte[] ackData = msg.getPayload();
// Get all acknowledgements that I am awaiting
PayloadProvider payProv = PayloadProvider.get(App.getContext());
String[] columnNames = new String[]{PayloadsTable.COLUMN_ACK, PayloadsTable.COLUMN_BELONGS_TO_ME};
String[] searchTerms = new String[]{"1", "1"}; // 1 stands for true in the database
ArrayList<Payload> expectedAckPayloads = payProv.searchPayloads(columnNames, searchTerms);
// Check if this is an acknowledgement bound for me
for (Payload p : expectedAckPayloads)
{
if (Arrays.equals(p.getPayload(), ackData))
{
// This is an acknowledgement that I am expecting!
// Update the status of the Message that this acknowledgement is for
MessageProvider msgProv = MessageProvider.get(App.getContext());
ArrayList<Message> retrievedMessages = msgProv.searchMessages(MessagesTable.COLUMN_ACK_PAYLOAD_ID, String.valueOf(p.getId()));
if (retrievedMessages.size() == 1)
{
// Retrieve the original message
Message originalMessage = retrievedMessages.get(0);
// Update the status of this message displayed in the UI
String messageStatus = App.getContext().getString(R.string.message_status_ack_received);
MessageStatusHandler.updateMessageStatus(originalMessage, messageStatus);
Log.d(TAG, "Acknowledgement received!\n" +
"Message subject: " + originalMessage.getSubject() + "\n" +
"Message to address: " + originalMessage.getToAddress());
// Delete any QueueRecords for sending this message
QueueRecordProvider queueProv = QueueRecordProvider.get(App.getContext());
ArrayList<QueueRecord> retrievedRecords = queueProv.searchQueueRecords(QueueRecordsTable.COLUMN_OBJECT_0_ID, String.valueOf(originalMessage.getId()));
for (QueueRecord q : retrievedRecords)
{
// If this is a QueueRecord for one of the three 'send message' tasks
if (q.getTask().equals(QueueRecordProcessor.TASK_SEND_MESSAGE) || q.getTask().equals(QueueRecordProcessor.TASK_PROCESS_OUTGOING_MESSAGE) || q.getTask().equals(QueueRecordProcessor.TASK_DISSEMINATE_MESSAGE))
{
queueProv.deleteQueueRecord(q);
}
}
}
else
{
Log.d(TAG, "We received an acknowledgement that we were awaiting, but the original message could not be found in the database.");
}
// We have now received this acknowledgement, so delete the 'awaiting' ack payload from the database
payProv.deletePayload(p);
return;
}
}
// If the acknowledgement was not one that we are awaiting
Log.i(TAG, "Processed a msg that was found to be an acknowledgement bound for someone else");
}
/**
* Attempts to decrypt a msg. If decryption is successful, the decrypted
* data is used to create a new UnencryptedMsg object. <br><br>
*
* <b>NOTE:</b>If decryption of the msg fails, this method will return null
*
* @param msgObject - A msg Object containing the msg to attempt to decrypt
*
* @return If decryption is successful, returns an UnencryptedMsg object
* containing the decrypted message data. Otherwise returns null.
*/
private UnencryptedMsg attemptMsgDecryption(BMObject msgObject, Address address)
{
try
{
// Create the ECPrivateKey object that we will use to decrypt the message data
ECPrivateKey k = new KeyConverter().decodePrivateKeyFromWIF(address.getPrivateEncryptionKey());
// Attempt to decrypt the encrypted message data
byte[] decryptedMsgData = null;
try
{
decryptedMsgData = new CryptProcessor().decrypt(msgObject.getPayload(), k);
}
catch (RuntimeException e)
{
// If decryption fails (as is to be expected when we processes msgs not bound for us)
return null;
}
// Use the decrypted message data to construct a new UnencryptedMsg object
UnencryptedMsg unencMsg = parseDecryptedMessage(msgObject, decryptedMsgData, address);
return unencMsg;
}
catch (Exception e)
{
throw new RuntimeException("Exception occurred in IncomingMessageProcessor.attemptMsgDecryption().\n"
+ "The exception message was: " + e.getLocalizedMessage().toString());
}
}
/**
* Parses the data of a decrypted msg, using it to construct a new
* UnencryptedMsg object.<br><br>
*
* @param msg - The msg Object which the decrypted data came from
* @param plainText - A byte[] containing the decrypted msg data
* @param toAddress - The Bitmessage address (presumably belonging to me) which the message was sent to
*
* @return An UnencryptedMsg object containing the decrypted message data
*/
private UnencryptedMsg parseDecryptedMessage(BMObject msg, byte[] plainText, Address toAddress)
{
// Parse the individual fields from the decrypted msg data
int readPosition = 0;
// Read and check the sender's address version number
long [] decoded = VarintEncoder.decode(ArrayCopier.copyOfRange(plainText, readPosition, readPosition + 9)); // Take 9 bytes, the maximum length for an encoded var_int
int senderAddressVersion = (int) decoded[0]; // Get the var_int encoded value
readPosition += (int) decoded[1]; // Find out how many bytes the var_int was in length and adjust the read position accordingly
if (senderAddressVersion < MIN_VALID_ADDRESS_VERSION || senderAddressVersion > MAX_VALID_ADDRESS_VERSION)
{
throw new RuntimeException("Decrypted address version number was invalid. Aborting message decryption. The invalid value was " + senderAddressVersion);
}
// Read and check the sender's stream number
decoded = VarintEncoder.decode(ArrayCopier.copyOfRange(plainText, readPosition, readPosition + 9)); // Take 9 bytes, the maximum length for an encoded var_int
int senderStreamNumber = (int) decoded[0]; // Get the var_int encoded value
readPosition += (int) decoded[1]; // Find out how many bytes the var_int was in length and adjust the read position accordingly
if (senderStreamNumber < MIN_VALID_STREAM_NUMBER || senderStreamNumber > MAX_VALID_STREAM_NUMBER)
{
throw new RuntimeException("Decrypted stream number was invalid. Aborting message decryption. The invalid value was " + senderStreamNumber);
}
// Read the behaviour bitfield
int behaviourBitfield = ByteUtils.bytesToInt((ArrayCopier.copyOfRange(plainText, readPosition, readPosition + 4)));
readPosition += 4; //The behaviour bitfield should always be 4 bytes in length
// Read the public signing key
byte[] publicSigningKey = ArrayCopier.copyOfRange(plainText, readPosition, readPosition + 64);
readPosition += 64;
// Read the public encryption key
byte[] publicEncryptionKey = ArrayCopier.copyOfRange(plainText, readPosition, readPosition + 64);
readPosition += 64;
// Set the nonceTrialsPerByte and extraBytes values to the network standard values. If the unencryptedMsg address version is
// 3 or greater, we will then set these two values to those specified in the message. Otherwise they remain at
// their default values.
int nonceTrialsPerByte = NETWORK_NONCE_TRIALS_PER_BYTE;
int extraBytes = NETWORK_EXTRA_BYTES;
if (senderAddressVersion >= 3) // Only unencrypted msgs of address version 3 or greater contain nonceTrialsPerByte and extraBytes values
{
decoded = VarintEncoder.decode(ArrayCopier.copyOfRange(plainText, readPosition, readPosition + 9)); // Take 9 bytes, the maximum length for an encoded var_int
nonceTrialsPerByte = (int) decoded[0]; // Get the var_int encoded value
readPosition += (int) decoded[1]; // Find out how many bytes the var_int was in length and adjust the read position accordingly
decoded = VarintEncoder.decode(ArrayCopier.copyOfRange(plainText, readPosition, readPosition + 9)); // Take 9 bytes, the maximum length for an encoded var_int
extraBytes = (int) decoded[0]; // Get the var_int encoded value
readPosition += (int) decoded[1]; // Find out how many bytes the var_int was in length and adjust the read position accordingly
}
byte[] destinationRipe = ArrayCopier.copyOfRange(plainText, readPosition, readPosition + 20);
readPosition += 20;
// Strip any leading zeros from the extraction destination ripe hash
destinationRipe = ByteUtils.stripLeadingZeros(destinationRipe);
if (Arrays.equals(destinationRipe, toAddress.getRipeHash()) == false)
{
throw new RuntimeException("The ripe hash read from the decrypted message text does not match the ripe hash of the address that " +
" the key we have used to decrypt the message belongs to. This may mean that the original sender of this message did not" +
" send it to you and that someone is attempting a Surreptitious Forwarding Attack. " +
"\n The expected ripe hash is: " + ByteFormatter.byteArrayToHexString(toAddress.getRipeHash()) +
"\n The ripe hash read from the decrypted message text is: " + ByteFormatter.byteArrayToHexString(destinationRipe));
}
// Read the message encoding type
decoded = VarintEncoder.decode(ArrayCopier.copyOfRange(plainText, readPosition, readPosition + 9)); // Take 9 bytes, the maximum length for an encoded var_int
int encoding = (int) decoded[0]; // Get the var_int encoded value
readPosition += (int) decoded[1]; // Find out how many bytes the var_int was in length and adjust the read position accordingly
// Read the message length
decoded = VarintEncoder.decode(ArrayCopier.copyOfRange(plainText, readPosition, readPosition + 9)); // Take 9 bytes, the maximum length for an encoded var_int
int messageLength = (int) decoded[0]; // Get the var_int encoded value
readPosition += (int) decoded[1]; // Find out how many bytes the var_int was in length and adjust the read position accordingly
// Read the message
byte[] message = (ArrayCopier.copyOfRange(plainText, readPosition, readPosition + messageLength));
readPosition += messageLength;
// Read the ack length
decoded = VarintEncoder.decode(ArrayCopier.copyOfRange(plainText, readPosition, readPosition + 9)); // Take 9 bytes, the maximum length for an encoded var_int
int ackLength = (int) decoded[0]; // Get the var_int encoded value
readPosition += (int) decoded[1]; // Find out how many bytes the var_int was in length and adjust the read position accordingly
// Read the ack data
byte[] ackData = (ArrayCopier.copyOfRange(plainText, readPosition, readPosition + ackLength));
readPosition += ackLength;
// Read the signature length
decoded = VarintEncoder.decode(ArrayCopier.copyOfRange(plainText, readPosition, readPosition + 9)); // Take 9 bytes, the maximum length for an encoded var_int
int signatureLength = (int) decoded[0]; // Get the var_int encoded value
readPosition += (int) decoded[1]; // Find out how many bytes the var_int was in length and adjust the read position accordingly
// Read the signature
byte[] signature = (ArrayCopier.copyOfRange(plainText, readPosition, readPosition + signatureLength));
// Create a new UnencryptedMsg object and populate its fields using the decrypted msg data
UnencryptedMsg unencMsg = new UnencryptedMsg();
unencMsg.setBelongsToMe(false);
unencMsg.setExpirationTime(msg.getExpirationTime());
unencMsg.setObjectType(msg.getObjectType());
unencMsg.setObjectVersion(msg.getObjectVersion());
unencMsg.setStreamNumber(msg.getStreamNumber());
unencMsg.setSenderAddressVersion(senderAddressVersion);
unencMsg.setSenderStreamNumber(senderStreamNumber);
unencMsg.setBehaviourBitfield(behaviourBitfield);
unencMsg.setPublicSigningKey(publicSigningKey);
unencMsg.setPublicEncryptionKey(publicEncryptionKey);
unencMsg.setNonceTrialsPerByte(nonceTrialsPerByte);
unencMsg.setExtraBytes(extraBytes);
unencMsg.setDestinationRipe(destinationRipe);
unencMsg.setEncoding(encoding);
unencMsg.setMessageLength(messageLength);
unencMsg.setMessage(message);
unencMsg.setAckLength(ackLength);
unencMsg.setAckMsg(ackData);
unencMsg.setSignatureLength(signatureLength);
unencMsg.setSignature(signature);
// Verify the signature of the decrypted message
ECPublicKey ecPublicSigningKey = new KeyConverter().reconstructPublicKey(publicSigningKey);
SigProcessor sigProc = new SigProcessor();
byte[] payloadToVerify = sigProc.createUnencryptedMsgSignaturePayload(unencMsg);
if (sigProc.verifySignature(payloadToVerify, signature, ecPublicSigningKey) == false)
{
// The signature of the message is invalid. Abort the process.
throw new RuntimeException("While attempting to parse a decrypted message in IncomingMessageProcessor.parseDecryptedMessage(), the signature was found to be invalid");
}
// In some rare instances, such as PyBitmessage sending a message to one of its own addresses, no ack data will be included
if (ackData.length != 0)
{
// Save the acknowledgement data of this msg as a Payload object and save it to
// the database so that we can send it later
Payload ackPayload = new Payload();
ackPayload.setBelongsToMe(false); // i.e This is the acknowledgement of a msg created by someone else
ackPayload.setProcessingComplete(true); // Set 'processing complete' to true so that we won't attempt to process this as a new incoming msg
ackPayload.setPOWDone(true);
ackPayload.setAck(true); // This payload is an acknowledgement
ackPayload.setType(Payload.OBJECT_TYPE_MSG); // Currently we treat all acks from other people as msgs. Strictly though they can be objects of any type, so this may change
ackPayload.setPayload(ackData);
PayloadProvider payProv = PayloadProvider.get(App.getContext());
payProv.addPayload(ackPayload);
}
return unencMsg;
}
/**
* Extracts the basic message data from an UnencryptedMsg object. Used when receiving a message.
*
* @param unencMsg - An UnencryptedMsg object containing the message we wish to extract.
*
* @return A Message object containing the extracted data.
*/
private Message extractMessageFromUnencryptedMsg (UnencryptedMsg unencMsg)
{
// Extract the message subject and body
// See https://bitmessage.org/wiki/Protocol_specification#Message_Encodings
String rawMessage = new String(unencMsg.getMessage()); // PyBitmessage also uses UTF-8, so this ought to be adequate.
String messageSubject = rawMessage.substring(rawMessage.indexOf("Subject:") + 8);
messageSubject = messageSubject.substring(0, messageSubject.indexOf("\n"));
String messageBody = rawMessage.substring(rawMessage.lastIndexOf("Body:") + 5);
// Get the String representation of the 'to' address
String toAddressString = null;
AddressProvider addProv = AddressProvider.get(App.getContext());
// Match the ripe hash from the UnencryptedMsg to the address of mine which shares that ripe hash
ArrayList<Address> retrievedAddresses = addProv.searchAddresses(
AddressesTable.COLUMN_RIPE_HASH, Base64.encodeToString(unencMsg.getDestinationRipe(), Base64.DEFAULT));
if (retrievedAddresses.size() != 1)
{
throw new RuntimeException("We successfully decrypted a msg sent to one of our addresses, but a database search for that address \n" +
"did not return exactly one result. Something is wrong! The ripe hash used to search for the address was " +
ByteFormatter.byteArrayToHexString(unencMsg.getDestinationRipe()));
}
else
{
Address toAddress = retrievedAddresses.get(0);
toAddressString = toAddress.getAddress();
}
// Recreate the String representation of the 'from' address. Before we do this we must be sure that
// the public keys from the UnencryptedMsg have their leading 0x04 byte in place. If it is not in place,
// we must restore it before the keys can be used.
byte[] publicSigningKey = unencMsg.getPublicSigningKey();
if (publicSigningKey[0] != (byte) 4 && publicSigningKey.length == 64)
{
byte[] fourByte = new byte[]{4};
publicSigningKey = ByteUtils.concatenateByteArrays(fourByte, publicSigningKey);
}
byte[] publicEncryptionKey = unencMsg.getPublicEncryptionKey();
if (publicEncryptionKey[0] != (byte) 4 && publicEncryptionKey.length == 64)
{
byte[] fourByte = new byte[]{4};
publicEncryptionKey = ByteUtils.concatenateByteArrays(fourByte, publicEncryptionKey);
}
String fromAddress = new AddressGenerator().recreateAddressString(unencMsg.getSenderAddressVersion(), unencMsg.getStreamNumber(),
publicSigningKey, publicEncryptionKey);
// Create a new Message object and populate its fields with the extracted data
Message message = new Message();
message.setBelongsToMe(unencMsg.belongsToMe());
message.setToAddress(toAddressString);
message.setFromAddress(fromAddress);
message.setSubject(messageSubject);
message.setBody(messageBody);
return message;
}
}