package org.bitseal.core; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.SecureRandom; import java.util.ArrayList; import org.bitseal.R; import org.bitseal.crypt.CryptProcessor; import org.bitseal.crypt.KeyConverter; import org.bitseal.crypt.PubkeyGenerator; 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.UnencryptedMsg; import org.bitseal.database.AddressProvider; import org.bitseal.database.AddressesTable; import org.bitseal.database.MessageProvider; import org.bitseal.database.PayloadProvider; import org.bitseal.database.PubkeyProvider; import org.bitseal.database.PubkeysTable; import org.bitseal.pow.POWProcessor; import org.bitseal.services.MessageStatusHandler; import org.bitseal.util.ArrayCopier; import org.bitseal.util.ByteFormatter; import org.bitseal.util.ByteUtils; import org.bitseal.util.TimeUtils; import org.bitseal.util.VarintEncoder; import org.spongycastle.jce.interfaces.ECPublicKey; import android.util.Log; /** * This class processes outgoing messages (i.e. messages sent by the user of the app) * * @author Jonathan Coe */ public class OutgoingMessageProcessor { private static final String TAG = "OUTGOING_MESSAGE_PROCESSOR"; /** In the Bitmessage protocol this value corresponds to a normal, text-based message */ private static final int MESSAGE_ENCODING_TYPE = 2; /** The object type number for msgs, as defined by the Bitmessage protocol */ private static final int OBJECT_TYPE_MSG = 2; /** The current version number for msg objects that we generate */ private static final int OBJECT_VERSION_MSG = 1; /** * Takes a Message object and does all the work necessary to * transform it into an encrypted message that is ready to be sent * over the Bitmessage network. This includes encryption and proof * of work. * * @param message - The Message object to be processed * @param toPubkey - The Pubkey of the address that the message is * being sent to * @param doPOW - A boolean value indicating whether or not POW should * be done for this message AND for pubkeys generated during the message * sending process * @param timeToLive - The 'time to live' value (in seconds) to be used in * processing this message * * @return A Payload object containing the encrypted message data ready to * be sent over the Bitmessage network */ public Payload processOutgoingMessage (Message message, Pubkey toPubkey, boolean doPOW, long timeToLive) { // Convert the message into a new UnencryptedMsg object UnencryptedMsg unencMsg = constructUnencryptedMsg(message, toPubkey, doPOW, timeToLive); // Encrypt the message and, if enabled, do POW BMObject encMsg = constructMsg(message, unencMsg, toPubkey, doPOW, timeToLive); // Construct the msg payload that will be sent over the network Payload msgPayload = constructMsgPayloadForDissemination(encMsg, doPOW, toPubkey); return msgPayload; } /** * Constructs an UnencryptedMsg object from a given Message object. Used when sending a message. <br><br> * * <b>NOTE!</b> Calling this method results in proof of work calculations being done for the acknowledgement * data of the message. This can take a long time and lots of CPU power!<br><br> * * <b>NOTE!</b> Calling this method can result in requests to a Bitseal server to retrieve pubkey data. These * requests may take some time to complete! * * @param message - The Message object to convert into an UnencryptedMsg object * @param toPubkey - A Pubkey object containing the public keys of the address the message is being sent to * @param doPOW - A boolean indicating whether or not POW should be done for msgs generated during this process * @param timeToLive - The 'time to live' value (in seconds) to be used in * processing this message * * @return An UnencryptedMsg object based on the supplied Message object. */ private UnencryptedMsg constructUnencryptedMsg(Message message, Pubkey toPubkey, boolean doPOW, long timeToLive) { String messageSubject = message.getSubject(); String messageBody = message.getBody(); // First let us check that the to address and from address Strings taken from the Message object are in fact valid Bitmessage addresses String toAddressString = message.getToAddress(); String fromAddressString = message.getFromAddress(); AddressProcessor addProc = new AddressProcessor(); if (addProc.validateAddress(toAddressString) != true) { throw new RuntimeException("During the execution of constructUnencryptedMsg(), it was found that the 'to' address in the supplied Message was not a valid Bitmessage address"); } if (addProc.validateAddress(fromAddressString) != true) { throw new RuntimeException("During the execution of constructUnencryptedMsg(), it was found that the 'from' address in the supplied Message was not a valid Bitmessage address"); } // Now that we have validated the to address and the from address, let us retrieve or create their corresponding Address and Pubkey objects. Address fromAddress = null; AddressProvider addProv = AddressProvider.get(App.getContext()); ArrayList<Address> retrievedAddresses = addProv.searchAddresses(AddressesTable.COLUMN_ADDRESS, fromAddressString); if (retrievedAddresses.size() != 1) { Log.e(TAG, "There should be exactly 1 record found in this search. Instead " + retrievedAddresses.size() + " records were found"); } else { fromAddress = retrievedAddresses.get(0); } // Now we need to get the behaviour bitfield from the pubkey which corresponds to the from address, so let us retrieve that pubkey. PubkeyProvider pubProv = PubkeyProvider.get(App.getContext()); ArrayList<Pubkey> retrievedPubkeys = pubProv.searchPubkeys(PubkeysTable.COLUMN_CORRESPONDING_ADDRESS_ID, String.valueOf(fromAddress.getId())); Pubkey fromPubkey = null; if (retrievedPubkeys.size() == 1) { fromPubkey = retrievedPubkeys.get(0); } else if (retrievedPubkeys.size() > 1) // If there are duplicate pubkeys for this address { Log.e(TAG, "There should be exactly 1 record found in this search. Instead " + retrievedPubkeys.size() + " records were found"); // Delete all but the most recent of the duplicate pubkeys long firstPubkeyTime = retrievedPubkeys.get(0).getExpirationTime(); Pubkey pubkeyToKeep = retrievedPubkeys.get(0); for (Pubkey p : retrievedPubkeys) { if (p.getExpirationTime() > firstPubkeyTime) { pubkeyToKeep = p; } } for (Pubkey p : retrievedPubkeys) { if (p.equals(pubkeyToKeep) == false) { pubProv.deletePubkey(p); } } // Use the most recent of the duplicate pubkeys fromPubkey = pubkeyToKeep; } if (fromPubkey == null) { Log.e(TAG, "Could not find the Pubkey which corresponds to the from address, even though it should be one of our own. Something is wrong!"); Log.d(TAG, "Regenerating the Pubkey for the from address"); fromPubkey = new PubkeyGenerator().generateAndSaveNewPubkey(fromAddress); // If we can't find the pubkey we need then let us generate it again } // Now extract the public signing and public encryption keys from the "from" pubkey // If the public signing and encryption keys taken from the Pubkey object have an "\x04" byte at their beginning, we need to remove it now. byte[] publicSigningKey = fromPubkey.getPublicSigningKey(); byte[] publicEncryptionKey = fromPubkey.getPublicEncryptionKey(); if (publicSigningKey[0] == (byte) 4 && publicSigningKey.length == 65) { publicSigningKey = ArrayCopier.copyOfRange(publicSigningKey, 1, publicSigningKey.length); } if (publicEncryptionKey[0] == (byte) 4 && publicEncryptionKey.length == 65) { publicEncryptionKey = ArrayCopier.copyOfRange(publicEncryptionKey, 1, publicEncryptionKey.length); } // Generate the ack data (32 random bytes) byte[] ackData = new byte[32]; new SecureRandom().nextBytes(ackData); // Generate the full ack Message that will be included in this unencrypted msg. // NOTE: Calling generateFullAckMessage() results in Proof of Work calculations being done for the // acknowledgement Message. This can take a long time and lots of CPU power! byte[] fullAckMessage = generateFullAckMessage(message, ackData, fromPubkey.getStreamNumber(), doPOW, timeToLive); Log.d(TAG, "Full ack Message: " + ByteFormatter.byteArrayToHexString(fullAckMessage)); // Create the single "message" text String which contains both the subject and the body of the message // See https://bitmessage.org/wiki/Protocol_specification#Message_Encodings String messsageText = "Subject:" + messageSubject + "\n" + "Body:" + messageBody; // Now create the UnencryptedMsg object and populate its fields. UnencryptedMsg unencMsg = new UnencryptedMsg(); unencMsg.setBelongsToMe(true); unencMsg.setExpirationTime(TimeUtils.getFuzzedExpirationTime(timeToLive)); unencMsg.setObjectType(OBJECT_TYPE_MSG); unencMsg.setObjectVersion(OBJECT_VERSION_MSG); unencMsg.setStreamNumber(toPubkey.getStreamNumber()); unencMsg.setSenderAddressVersion(fromPubkey.getObjectVersion()); unencMsg.setSenderStreamNumber(fromPubkey.getStreamNumber()); unencMsg.setBehaviourBitfield(fromPubkey.getBehaviourBitfield()); unencMsg.setPublicSigningKey(publicSigningKey); unencMsg.setPublicEncryptionKey(publicEncryptionKey); unencMsg.setNonceTrialsPerByte(fromPubkey.getNonceTrialsPerByte()); unencMsg.setExtraBytes(fromPubkey.getExtraBytes()); unencMsg.setDestinationRipe(new KeyConverter().calculateRipeHashFromPubkey(toPubkey)); unencMsg.setEncoding(MESSAGE_ENCODING_TYPE); unencMsg.setMessageLength(messsageText.getBytes().length); // We have to use the byte length rather than the string length - some characters take more bytes than others unencMsg.setMessage(messsageText.getBytes()); // PyBitmessage also uses UTF-8 as its character set, so this ought to be adequate unencMsg.setAckLength(fullAckMessage.length); unencMsg.setAckMsg(fullAckMessage); // Save the acknowledgment data to the database so that when we receive the acknowledgment for this message we will recognise it Payload ackPayload = new Payload(); ackPayload.setBelongsToMe(true); // i.e. This is an acknowledgment created by me ackPayload.setPOWDone(true); ackPayload.setAck(true); // This payload is an acknowledgment 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()); long ackPayloadId = payProv.addPayload(ackPayload); // Set the "ackPayloadId" field of the original Message object so that we know which Message this ack data is for message.setAckPayloadId(ackPayloadId); MessageProvider msgProv = MessageProvider.get(App.getContext()); msgProv.updateMessage(message); // Now create the signature for this message SigProcessor sigProc = new SigProcessor(); byte[] signaturePayload = sigProc.createUnencryptedMsgSignaturePayload(unencMsg); byte[] signature = sigProc.signWithWIFKey(signaturePayload, fromAddress.getPrivateSigningKey()); unencMsg.setSignature(signature); unencMsg.setSignatureLength(signature.length); return unencMsg; } /** * Takes an UnencryptedMsg object and does all the work necessary to transform it into an EncyrptedMsg * object that is ready to be serialised and sent out to the Bitmessage network. The two major parts of this * process are encryption and proof of work. <br><br> * * <b>NOTE!</b> Calling this method results in proof of work calculations being done for the * message. This can take a long time and lots of CPU power!<br><br> * * @param message - The original plain text Message object, provided so that its status can be updated during the process * @param unencMsg - The UnencryptedMsg object to be encrypted * @param toPubkey - The Pubkey object containing the public encryption key of the intended message recipient * @param doPOW - A boolean value indicating whether or not POW should be done for this message * @param timeToLive - The 'time to live' value (in seconds) to be used in creating this msg * * @return A Msg object containing the encrypted message data */ private BMObject constructMsg (Message message, UnencryptedMsg unencMsg, Pubkey toPubkey, boolean doPOW, long timeToLive) { // Reconstruct the ECPublicKey object from the byte[] found the the relevant PubKey ECPublicKey publicEncryptionKey = new KeyConverter().reconstructPublicKey(toPubkey.getPublicEncryptionKey()); // Construct the payload to be encrypted byte[] msgDataForEncryption = constructMsgPayloadForEncryption(unencMsg); // Update the status of this message displayed in the UI String messageStatus = App.getContext().getString(R.string.message_status_encrypting_message); MessageStatusHandler.updateMessageStatus(message, messageStatus); // Encrypt the payload CryptProcessor cryptProc = new CryptProcessor(); byte[] encryptedPayload = cryptProc.encrypt(msgDataForEncryption, publicEncryptionKey); // Create a new Msg object and populate its fields BMObject msg = new BMObject(); msg.setBelongsToMe(true); // NOTE: This method assumes that any message I am encrypting 'belongs to me' (i.e. The user of the application is the author of the message) msg.setExpirationTime(unencMsg.getExpirationTime()); msg.setObjectType(unencMsg.getObjectType()); msg.setObjectVersion(unencMsg.getObjectVersion()); msg.setStreamNumber(toPubkey.getStreamNumber()); msg.setPayload(encryptedPayload); if (doPOW) { MessageStatusHandler.updateMessageStatus(message, App.getContext().getString(R.string.message_status_doing_pow)); // Do proof of work for the Msg object Log.i(TAG, "About to do POW calculations for a msg that we are sending"); byte[] powPayload = constructMsgPayloadForPOW(msg); long powNonce = new POWProcessor().doPOW(powPayload, unencMsg.getExpirationTime(), toPubkey.getNonceTrialsPerByte(), toPubkey.getExtraBytes()); msg.setPOWNonce(powNonce); } else { msg.setPOWNonce((long) 0); // If POW is not to be done for this message, set the powNonce as zero for now. } return msg; } /** * Takes a Msg and constructs the payload needed to do POW for it. * * @param msg - The msg Object to construct the POW payload for * * @return The POW payload */ private byte[] constructMsgPayloadForPOW (BMObject msg) { try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); outputStream.write(ByteUtils.longToBytes(msg.getExpirationTime())); // This conversion results in a byte[] of length 8, which is what we want outputStream.write(ByteUtils.intToBytes(OBJECT_TYPE_MSG)); outputStream.write(VarintEncoder.encode(OBJECT_VERSION_MSG)); outputStream.write(VarintEncoder.encode(msg.getStreamNumber())); outputStream.write(msg.getPayload()); return outputStream.toByteArray(); } catch (IOException e) { throw new RuntimeException("IOException occurred in OutgoingMessageProcessor.constructMsgPayloadForPOW()", e); } } /** * Takes an UnencryptedMsg object and extracts only the data needed to encrypt the message, discarding * data that is only used by Bitseal internally, such as the ID number. * * @param inputMsgData - The UnencryptedMsg object from which the data is to be extracted * * @return A byte[] containing the message data needed for encryption */ private byte[] constructMsgPayloadForEncryption (UnencryptedMsg unencMsg) { byte[] msgDataForEncryption = null; ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try { outputStream.write(VarintEncoder.encode(unencMsg.getSenderAddressVersion())); outputStream.write(VarintEncoder.encode(unencMsg.getSenderStreamNumber())); outputStream.write(ByteUtils.intToBytes(unencMsg.getBehaviourBitfield())); // If the public signing and public encryption keys have their leading 0x04 byte in place then we need to remove them byte[] publicSigningKey = unencMsg.getPublicSigningKey(); if (publicSigningKey[0] == (byte) 4 && publicSigningKey.length == 65) { publicSigningKey = ArrayCopier.copyOfRange(publicSigningKey, 1, publicSigningKey.length); } outputStream.write(publicSigningKey); byte[] publicEncryptionKey = unencMsg.getPublicEncryptionKey(); if (publicEncryptionKey[0] == (byte) 4 && publicEncryptionKey.length == 65) { publicEncryptionKey = ArrayCopier.copyOfRange(publicEncryptionKey, 1, publicEncryptionKey.length); } outputStream.write(publicEncryptionKey); if (unencMsg.getSenderAddressVersion() >= 3) // The nonceTrialsPerByte and extraBytes fields are only included when the address version is >= 3 { outputStream.write(VarintEncoder.encode(unencMsg.getNonceTrialsPerByte())); outputStream.write(VarintEncoder.encode(unencMsg.getExtraBytes())); } outputStream.write(unencMsg.getDestinationRipe()); outputStream.write(VarintEncoder.encode(unencMsg.getEncoding())); outputStream.write(VarintEncoder.encode(unencMsg.getMessageLength())); outputStream.write(unencMsg.getMessage()); outputStream.write(VarintEncoder.encode(unencMsg.getAckLength())); outputStream.write(unencMsg.getAckMsg()); outputStream.write(VarintEncoder.encode(unencMsg.getSignatureLength())); outputStream.write(unencMsg.getSignature()); msgDataForEncryption = outputStream.toByteArray(); outputStream.close(); } catch (IOException e) { throw new RuntimeException("IOException occurred in DataProcessor.constructMsgPayloadForEncryption()", e); } return msgDataForEncryption; } /** * Calculates the acknowledgement Message for a given message. <br><br> * * The process for this is as follows:<br><br> * 1) initialPayload = time || stream number || 32 bytes of random data<br><br> * 2) Do POW for the initialPayload<br><br> * 3) ackData = POWnonce || msgHeader || initialPayload<br><br> * * @param message - The original plain text Message object, provided so that its status can be updated during the process * @param ackData - A byte[] containing the 32 bytes of random data which is the acknowledgment data * @param toStreamNumber - An int representing the stream number of the destination address of the message to be sent * @param doPOW - A boolean indicating whether or not POW should be done for ack msgs generated during this process * @param timeToLive - The 'time to live' value (in seconds) to be used in * processing this message * * @return A byte[] containing the acknowledgement data for the message we wish to send */ private byte[] generateFullAckMessage (Message message, byte[] ackData, int toStreamNumber, boolean doPOW, long timeToLive) { // Get the fuzzed expiration time long expirationTime = TimeUtils.getFuzzedExpirationTime(timeToLive); // Encode the expiration time, object type, object version, and stream number values into byte form byte[] expirationTimeBytes = ByteUtils.longToBytes((expirationTime)); byte[] objectTypeBytes = ByteUtils.intToBytes(OBJECT_TYPE_MSG); byte[] objectVersionBytes = VarintEncoder.encode(OBJECT_VERSION_MSG); byte[] streamNumberBytes = VarintEncoder.encode((long) toStreamNumber); // Combine the time, object type, object version, stream number, and ack data values into a single byte[] byte[] initialPayload = ByteUtils.concatenateByteArrays(expirationTimeBytes, objectTypeBytes, objectVersionBytes, streamNumberBytes, ackData); // Create the payload for the ack msg byte[] payload = new byte[0]; if (doPOW) { // Update the status of this message displayed in the UI String messageStatus = App.getContext().getString(R.string.message_status_doing_ack_pow); MessageStatusHandler.updateMessageStatus(message, messageStatus); // Do proof of work for the acknowledgement payload Log.i(TAG, "About to do POW calculations for the acknowledgment payload of a msg that we are sending"); long powNonce = new POWProcessor().doPOW(initialPayload, expirationTime, POWProcessor.NETWORK_NONCE_TRIALS_PER_BYTE, POWProcessor.NETWORK_EXTRA_BYTES); byte[] powNonceBytes = ByteUtils.longToBytes(powNonce); payload = ByteUtils.concatenateByteArrays(powNonceBytes, initialPayload); } else { payload = initialPayload; } byte[] headerData = new MessageProcessor().generateObjectHeader(payload); byte[] fullAckMsg = ByteUtils.concatenateByteArrays(headerData, payload); return fullAckMsg; } /** * Takes a Msg and encodes it into a single byte[], in a way that is compatible * with the way that PyBitmessage does. This payload can then be sent to a server * to be disseminated across the network. The payload is stored as a Payload object. * * @param encMsg - A msg Object containing the message data used to create * the payload. * @param powDone - A boolean value indicating whether or not POW has been done for this message * @param toPubkey - A Pubkey object containing the data for the Pubkey of the address that this * message is being sent to * * @return A Payload object containing the message payload */ private Payload constructMsgPayloadForDissemination (BMObject encMsg, boolean powDone, Pubkey toPubkey) { // Create a new Payload object to hold the payload data Payload msgPayload = new Payload(); msgPayload.setBelongsToMe(true); msgPayload.setPOWDone(powDone); msgPayload.setType(Payload.OBJECT_TYPE_MSG); // Encode the POW nonce, expiration time, object type, object version, and stream number values into byte form byte[] powNonceBytes = ByteUtils.longToBytes(encMsg.getPOWNonce()); byte[] expirationTimeBytes = ByteUtils.longToBytes(encMsg.getExpirationTime()); byte[] objectTypeBytes = ByteUtils.intToBytes(OBJECT_TYPE_MSG); byte[] objectVersionBytes = VarintEncoder.encode(OBJECT_VERSION_MSG); byte[] streamNumberBytes = VarintEncoder.encode(encMsg.getStreamNumber()); byte[] payload = null; ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try { if (powDone) { outputStream.write(powNonceBytes); } outputStream.write(expirationTimeBytes); outputStream.write(objectTypeBytes); outputStream.write(objectVersionBytes); outputStream.write(streamNumberBytes); outputStream.write(encMsg.getPayload()); payload = outputStream.toByteArray(); outputStream.close(); } catch (IOException e) { throw new RuntimeException("IOException occurred in DataProcessor.constructMsgPayloadForDissemination()", e); } msgPayload.setPayload(payload); // Save the Payload object to the database PayloadProvider payProv = PayloadProvider.get(App.getContext()); long msgPayloadId = payProv.addPayload(msgPayload); // Finally, set the msg payload ID to the one generated by the SQLite database msgPayload.setId(msgPayloadId); return msgPayload; } }