package org.bitseal.core; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; 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.database.AddressProvider; import org.bitseal.database.PayloadProvider; import org.bitseal.database.PubkeyProvider; import org.bitseal.database.PubkeysTable; import org.bitseal.network.NetworkHelper; import org.bitseal.network.ServerCommunicator; import org.bitseal.pow.POWProcessor; import org.bitseal.services.MessageStatusHandler; import org.bitseal.util.ArrayCopier; 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; /** * A class which provides various methods used for processing pubkeys within Bitseal. * * @author Jonathan Coe */ public class PubkeyProcessor { /** 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 EMPTY_SIGNATURE_LENGTH = 0; // Pubkeys of version 2 and below do not have signatures private static final byte[] EMPTY_SIGNATURE = new byte[]{0}; private static final String TAG = "PUBKEY_PROCESSOR"; /** * Checks whether a given Pubkey and Bitmessage address are valid for * each other. * * @param pubkey - A Pubkey object to be validated * @param addressString - A String containing the Bitmessage address to * validate the Pubkey against * * @return A boolean indicating whether or not the Pubkey and address String * are valid for each other */ public boolean validatePubkey (Pubkey pubkey, String addressString) { // First check that the given address string is a valid Bitmessage address. AddressProcessor addProc = new AddressProcessor(); boolean addressStringValid = addProc.validateAddress(addressString); if (addressStringValid == false) { Log.i(TAG, "While running PubkeyProcessor.validatePubkey(), it was found that the supplied \n" + "address String was NOT a valid Bitmessage address"); return false; } // Check that the pubkey is valid by using its public signing key, public encryption key, // address version number, and stream number to recreate the address string that it corresponds to. // This should match the address string that we started with. AddressGenerator addGen = new AddressGenerator(); String recreatedAddress = addGen.recreateAddressString(pubkey.getObjectVersion(), pubkey.getStreamNumber(), pubkey.getPublicSigningKey(), pubkey.getPublicEncryptionKey()); Log.i(TAG, "Recreated address String: " + recreatedAddress); boolean recreatedAddressValid = recreatedAddress.equals(addressString); if (recreatedAddressValid == false) { Log.i(TAG, "While running PubkeyProcessor.validatePubkey(), it was found that the recreated address String \n" + "generated using data from the pubkey did not match the original address String. \n" + "The original address String was : " + addressString + "\n" + "The recreated address String was: " + recreatedAddress); return false; } // If this pubkey is of version 2 or above, also check that the signature of the pubkey is valid int[] addressNumbers = addProc.decodeAddressNumbers(addressString); int addressVersion = addressNumbers[0]; if (addressVersion > 2) { // To verify the signature we first have to convert the public signing key from the retrieved pubkey into an ECPublicKey object KeyConverter keyConv = new KeyConverter(); ECPublicKey publicSigningKey = keyConv.reconstructPublicKey(pubkey.getPublicSigningKey()); SigProcessor sigProc = new SigProcessor(); byte[] signaturePayload = sigProc.createPubkeySignaturePayload(pubkey); boolean sigValid = (sigProc.verifySignature(signaturePayload, pubkey.getSignature(), publicSigningKey)); if (sigValid == false) { Log.i(TAG, "While running PubkeyProcessor.validatePubkey(), it was found that the pubkey's signature was invalid"); return false; } } // If the recreated address String and signature were both valid return true; } /** * Takes a Message and attempts to retrieve the Pubkey of the Message's 'to address'<br><br> * * Note: If the pubkey has to be retrieved from a server and the attempt to do so fails, * this method will throw a RuntimeException. * * @param addressString - The Message we are attempting to send * * @return A Pubkey object that represents the pubkey for the supplied Message's 'to address' */ public Pubkey retrievePubkeyForMessage (Message message) { String addressString = message.getToAddress(); // Extract the ripe hash from the address String byte[] ripeHash = new AddressProcessor().extractRipeHashFromAddress(addressString); Pubkey pubkey = retrievePubkeyFromDatabase(ripeHash); if (pubkey != null) { return pubkey; } else { Log.i(TAG, "Unable to find the requested pubkey in the application database. The pubkey will now be requested from a server."); // Update the status of this message displayed in the UI String messageStatus = App.getContext().getString(R.string.message_status_requesting_pubkey); MessageStatusHandler.updateMessageStatus(message, messageStatus); // Check whether an Internet connection is available. if (NetworkHelper.checkInternetAvailability() == true) { return retrievePubkeyFromServer(addressString, ripeHash); } else { MessageStatusHandler.updateMessageStatus(message, App.getContext().getString(R.string.message_status_waiting_for_connection)); throw new RuntimeException("Unable to retrieve the pubkey because no internet connection is available"); } } } /** * Takes a String representing a Bitmessage address and uses it to retrieve the Pubkey that * corresponds to that address. <br><br> * * This method is intended to be used to retrieve the Pubkey of another person * when we have their address and wish to send them a message.<br><br> * * Note: If the pubkey has to be retrieved from a server and the attempt to do so fails, * this method will throw a RuntimeException. * * @param addressString - A String containing the Bitmessage address that we wish to retrieve * the pubkey for - e.g. "BM-NBpe4wbtC59sWFKxwaiGGNCb715D6xvY" * * @return A Pubkey object that represents the pubkey for the supplied address */ public Pubkey retrievePubkeyByAddressString (String addressString) { // Extract the ripe hash from the address String byte[] ripeHash = new AddressProcessor().extractRipeHashFromAddress(addressString); Pubkey pubkey = retrievePubkeyFromDatabase(ripeHash); if (pubkey != null) { return pubkey; } else { Log.i(TAG, "Unable to find the requested pubkey in the application database. The pubkey will now be requested from a server."); return retrievePubkeyFromServer(addressString, ripeHash); } } /** * Attempts to retrieve the Pubkey with a given ripe hash from the database.<br><br> * * Note! If the Pubkey cannot be found, this method will return null * * @param ripeHash A byte[] containing the ripe hash of the Pubkey to be retrieved * * @return A Pubkey, or null if the Pubkey cannot be found */ private Pubkey retrievePubkeyFromDatabase(byte[] ripeHash) { // Search the application's database to see if the pubkey we need is stored there // Note that ripe hashes in the database have their leading zeros removed PubkeyProvider pubProv = PubkeyProvider.get(App.getContext()); ArrayList<Pubkey> retrievedPubkeys = pubProv.searchPubkeys(PubkeysTable.COLUMN_RIPE_HASH, Base64.encodeToString(ByteUtils.stripLeadingZeros(ripeHash), Base64.DEFAULT)); if (retrievedPubkeys.size() > 1) { Log.i(TAG, "We seem to have found duplicate pubkeys during the database search. We will use the first one and delete the duplicates."); for (Pubkey p : retrievedPubkeys) { if (retrievedPubkeys.indexOf(p) != 0) // Keep the first record and delete all the others { pubProv.deletePubkey(p); } } Pubkey pubkey = retrievedPubkeys.get(0); return pubkey; } else if (retrievedPubkeys.size() == 1) { Pubkey pubkey = retrievedPubkeys.get(0); return pubkey; } else { return null; } } /** * Attempts to retrieve the pubkey with a given address string and ripe hash from a server.<br><br> * * Note! If the pubkey cannot be found, this method will throw a RuntimeException * * @param addressString - A String containing the address of pubkey to be retrieved * @param ripeHash - A byte[] containing the ripe hash of the pubkey to be retrieved * * @return A Pubkey, or null if the Pubkey cannot be found */ private Pubkey retrievePubkeyFromServer(String addressString, byte[] ripeHash) { // Extract the address version from the address string in order to determine whether the pubkey will // be encrypted (version 4 and above) AddressProcessor addProc = new AddressProcessor(); int[] decodedAddressValues = addProc.decodeAddressNumbers(addressString); int addressVersion = decodedAddressValues[0]; // Retrieve the pubkey from a server ServerCommunicator servCom = new ServerCommunicator(); Pubkey pubkey = null; if (addressVersion >= 4) // The pubkey will be encrypted { // Calculate the tag that will be used to request the encrypted pubkey byte[] tag = addProc.calculateAddressTag(addressString); // Retrieve the encrypted pubkey from a server pubkey = servCom.requestPubkeyFromServer(addressString, tag, addressVersion); } else // The pubkey is of version 3 or below, and will therefore not be encrypted { pubkey = servCom.requestPubkeyFromServer(addressString, ripeHash, addressVersion); } // Save the pubkey to the database and set its ID with the one generated by the database PubkeyProvider pubProv = PubkeyProvider.get(App.getContext()); long id = pubProv.addPubkey(pubkey); pubkey.setId(id); return pubkey; // If the ServerCommunicator fails to retrieve the Pubkey then it will throw a RuntimeException. This will be passed // up the method call hierarchy and handled. } /** * Reconstructs a pubkey from its encoded byte[] form, typically * the data received from a server after requesting a pubkey. * * @param pubkeyData - A byte[] containing the encoded data for a pubkey * @param addressString - If the pubkey is to be reconstructed is of address * version 4 or above, then a String representing the Bitmessage address * corresponding to the pubkey must be supplied, in order for the encrypted * part of the pubkey to be decrypted. Otherwise, the addressString parameter * will not be used. * * @return A Pubkey object constructed from the data provided */ public Pubkey reconstructPubkey (byte[] pubkeyData, String addressString) { // First parse the standard Bitmessage object data BMObject pubkeyObject = new ObjectProcessor().parseObject(pubkeyData); // Now parse the pubkey-specific data byte[] pubkeyPayload = pubkeyObject.getPayload(); int readPosition = 0; // Pubkeys of version 4 and above have most of their data encrypted. if (pubkeyObject.getObjectVersion() >= 4) { byte[] encryptedData = ArrayCopier.copyOfRange(pubkeyPayload, readPosition + 32, pubkeyPayload.length); // Skip over the tag // Create the ECPrivateKey object that we will use to decrypt encrypted the pubkey data AddressProcessor addProc = new AddressProcessor(); byte[] encryptionKey = addProc.calculateAddressEncryptionKey(addressString); KeyConverter keyConv = new KeyConverter(); ECPrivateKey k = keyConv.calculatePrivateKeyFromDoubleHashKey(encryptionKey); // Attempt to decrypt the encrypted pubkey data CryptProcessor cryptProc = new CryptProcessor(); pubkeyPayload = cryptProc.decrypt(encryptedData, k); readPosition = 0; // Reset the read position so that we start from the beginning of the decrypted data } int behaviourBitfield = ByteUtils.bytesToInt((ArrayCopier.copyOfRange(pubkeyPayload, readPosition, readPosition + 4))); readPosition += 4; //The behaviour bitfield should always be 4 bytes in length byte[] publicSigningKey = ArrayCopier.copyOfRange(pubkeyPayload, readPosition, readPosition + 64); readPosition += 64; // Both the public signing and public encryption keys need to have the 0x04 byte which was stripped off for transmission // over the wire added back on to them byte[] fourByte = new byte[]{4}; publicSigningKey = ByteUtils.concatenateByteArrays(fourByte, publicSigningKey); byte[] publicEncryptionKey = ArrayCopier.copyOfRange(pubkeyPayload, readPosition, readPosition + 64); readPosition += 64; publicEncryptionKey = ByteUtils.concatenateByteArrays(fourByte, publicEncryptionKey); // Set the nonceTrialsPerByte and extraBytes values to the network standard values. If the pubkey address version is // 3 or greater, we will then set these two values to those specified in the pubkey. Otherwise they remain at // their default values. int nonceTrialsPerByte = NETWORK_NONCE_TRIALS_PER_BYTE; int extraBytes = NETWORK_EXTRA_BYTES; // Set the signature and signature length to some default blank values. Pubkeys of address version 2 and below // do not have signatures. int signatureLength = EMPTY_SIGNATURE_LENGTH; byte[] signature = EMPTY_SIGNATURE; // Only unencrypted msgs of address version 3 or greater contain // values for nonceTrialsPerByte, extraBytes, signatureLength, and // signature if (pubkeyObject.getObjectVersion() >= 3) { long[] decoded = VarintEncoder.decode(ArrayCopier.copyOfRange(pubkeyPayload, 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(pubkeyPayload, 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 decoded = VarintEncoder.decode(ArrayCopier.copyOfRange(pubkeyPayload, readPosition, readPosition + 9)); // Take 9 bytes, the maximum length for an encoded var_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 signature = (ArrayCopier.copyOfRange(pubkeyPayload, readPosition, readPosition + signatureLength)); } // Recalculate the ripe hash of this pubkey so that it can be stored in the database byte[] ripeHash = new AddressGenerator().calculateRipeHash(publicSigningKey, publicEncryptionKey); Pubkey pubkey = new Pubkey(); pubkey.setBelongsToMe(false); pubkey.setPOWNonce(pubkeyObject.getPOWNonce()); pubkey.setExpirationTime(pubkeyObject.getExpirationTime()); pubkey.setObjectType(pubkeyObject.getObjectType()); pubkey.setObjectVersion(pubkeyObject.getObjectVersion()); pubkey.setStreamNumber(pubkeyObject.getStreamNumber()); pubkey.setRipeHash(ripeHash); pubkey.setBehaviourBitfield(behaviourBitfield); pubkey.setPublicSigningKey(publicSigningKey); pubkey.setPublicEncryptionKey(publicEncryptionKey); pubkey.setNonceTrialsPerByte(nonceTrialsPerByte); pubkey.setExtraBytes(extraBytes); pubkey.setSignatureLength(signatureLength); pubkey.setSignature(signature); return pubkey; } /** * Takes a Pubkey and encodes it into a single byte[] (in a way that is compatible * with the way that PyBitmessage does), and does POW for this payload. This payload * can then be sent to a server to be disseminated across the network. <br><br> * * Note: This method is currently only valid for version 4 pubkeys * * @param pubkey - An Pubkey object containing the pubkey data used to create * the payload. * @param doPOW - A boolean value indicating whether or not to do POW for this pubkey * * @return A Payload object containing the pubkey payload */ public Payload constructPubkeyPayload (Pubkey pubkey, boolean doPOW) { // Construct the pubkey payload byte[] payload = null; ByteArrayOutputStream payloadStream = new ByteArrayOutputStream(); try { payloadStream.write(ByteUtils.longToBytes(pubkey.getExpirationTime())); payloadStream.write(ByteUtils.intToBytes(pubkey.getObjectType())); payloadStream.write(VarintEncoder.encode(pubkey.getObjectVersion())); payloadStream.write(VarintEncoder.encode(pubkey.getStreamNumber())); // Assemble the pubkey data that will be encrypted ByteArrayOutputStream dataToEncryptStream = new ByteArrayOutputStream(); dataToEncryptStream.write(ByteUtils.intToBytes(pubkey.getBehaviourBitfield())); // If the public signing and public encryption keys have their leading 0x04 byte in place then we need to remove them byte[] publicSigningKey = pubkey.getPublicSigningKey(); if (publicSigningKey[0] == (byte) 4 && publicSigningKey.length == 65) { publicSigningKey = ArrayCopier.copyOfRange(publicSigningKey, 1, publicSigningKey.length); } dataToEncryptStream.write(publicSigningKey); byte[] publicEncryptionKey = pubkey.getPublicEncryptionKey(); if (publicEncryptionKey[0] == (byte) 4 && publicEncryptionKey.length == 65) { publicEncryptionKey = ArrayCopier.copyOfRange(publicEncryptionKey, 1, publicEncryptionKey.length); } dataToEncryptStream.write(publicEncryptionKey); dataToEncryptStream.write(VarintEncoder.encode(pubkey.getNonceTrialsPerByte())); dataToEncryptStream.write(VarintEncoder.encode(pubkey.getExtraBytes())); dataToEncryptStream.write(VarintEncoder.encode(pubkey.getSignatureLength())); dataToEncryptStream.write(pubkey.getSignature()); // Create the ECPublicKey object that we will use to encrypt the data. First we will // retrieve the Address corresponding to this pubkey, so that we can calculate the encryption // key derived from the double hash of the address data. Address address = AddressProvider.get(App.getContext()).searchForSingleRecord(pubkey.getCorrespondingAddressId()); String addressString = address.getAddress(); byte[] encryptionKey = new AddressProcessor().calculateAddressEncryptionKey(addressString); ECPublicKey K = new KeyConverter().calculatePublicKeyFromDoubleHashKey(encryptionKey); // Encrypt the pubkey data byte[] dataToEncrypt = dataToEncryptStream.toByteArray(); byte[] encryptedPayload = new CryptProcessor().encrypt(dataToEncrypt, K); // Get the tag used to identify the pubkey payload byte[] tag = address.getTag(); // Add the tag and the encrypted data to the rest of the pubkey payload payloadStream.write(tag); payloadStream.write(encryptedPayload); payload = payloadStream.toByteArray(); } catch (IOException e) { throw new RuntimeException("IOException occurred in PubkeyProcessor.constructPubkeyPayloadForDissemination()", e); } if (doPOW) { long powNonce = new POWProcessor().doPOW(payload, pubkey.getExpirationTime(), POWProcessor.NETWORK_NONCE_TRIALS_PER_BYTE, POWProcessor.NETWORK_EXTRA_BYTES); payload = ByteUtils.concatenateByteArrays(ByteUtils.longToBytes(powNonce), payload); } // Create a new Payload object to hold the payload data Payload pubkeyPayload = new Payload(); pubkeyPayload.setRelatedAddressId(pubkey.getCorrespondingAddressId()); pubkeyPayload.setBelongsToMe(true); pubkeyPayload.setPOWDone(doPOW); pubkeyPayload.setType(Payload.OBJECT_TYPE_PUBKEY); pubkeyPayload.setPayload(payload); // Save the Payload object to the database PayloadProvider payProv = PayloadProvider.get(App.getContext()); long pubkeyPayloadID = payProv.addPayload(pubkeyPayload); // Finally, set the pubkey payload's ID to the one generated by the database pubkeyPayload.setId(pubkeyPayloadID); return pubkeyPayload; } }