package org.bitseal.core;
import java.util.Arrays;
import org.bitseal.crypt.AddressGenerator;
import org.bitseal.crypt.SHA256;
import org.bitseal.crypt.SHA512;
import org.bitseal.data.Address;
import org.bitseal.services.BackgroundService;
import org.bitseal.util.ArrayCopier;
import org.bitseal.util.Base58;
import org.bitseal.util.ByteFormatter;
import org.bitseal.util.ByteUtils;
import org.bitseal.util.VarintEncoder;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
/**
* A class which provides various methods used for processing
* Bitmessage addresses within Bitseal.
*
* @author Jonathan Coe
*/
public final class AddressProcessor
{
private static final String BITMESSAGE_ADDRESS_PREFIX = "BM-";
private static final int BITMESSAGE_ADDRESS_MIN_LENGTH = 35;
private static final int BITMESSAGE_ADDRESS_MAX_LENGTH = 38;
private static final int SECONDS_IN_A_DAY = 86400;
private static final String TAG = "ADDRESS_PROCESSOR";
/**
* Checks whether or not a given String is a valid Bitmessage address.
*
* @param address - A String containing the Bitmessage address to be validated
*
* @return A boolean indicating whether or not the String is a valid Bitmessage address
*/
public boolean validateAddress (String address)
{
// Validation check 1: Check the length of the String. NOTE: This check assumes that all valid addresses are between 35 and 38 characters in length
if ((address.length() >= BITMESSAGE_ADDRESS_MIN_LENGTH) != true)
{
Log.i(TAG, "An address String supplied to AddressProcessor.validateAddress() was found to be shorter than the minimum length. \n" +
"The invalid address String was: " + address);
return false;
}
if ((address.length() <= BITMESSAGE_ADDRESS_MAX_LENGTH) != true)
{
Log.i(TAG, "An address String supplied to AddressProcessor.validateAddress() was found to be longer than the maximum length. \n" +
"The invalid address String was: " + address);
return false;
}
// Validation check 2: Check whether or not the first 3 characters of the String match the required Bitmessage address prefix
String addressPrefix = address.substring(0, 3);
if (addressPrefix.equals(BITMESSAGE_ADDRESS_PREFIX) != true)
{
Log.i(TAG, "An address String supplied to AddressProcessor.validateAddress() was found to have an invalid prefix. \n" +
"The invalid address String was: " + address);
return false;
}
// Validation check 3: Check whether or not the final 4 characters of the String are a valid checksum for all other characters after the "BM-" prefix
String addressData = address.substring(3, address.length());
byte[] addressDataBytes = null;
addressDataBytes = Base58.decode(addressData);
byte[] combinedChecksumData = ArrayCopier.copyOfRange(addressDataBytes, 0, (addressDataBytes.length - 4));
byte[] checksum = ArrayCopier.copyOfRange(addressDataBytes, (addressDataBytes.length - 4), addressDataBytes.length);
byte[] testChecksumFullHash = SHA512.doubleHash(combinedChecksumData);
byte[] testChecksum = ArrayCopier.copyOfRange(testChecksumFullHash, 0, 4);
if (Arrays.equals(checksum, testChecksum) != true)
{
Log.i(TAG, "An address String supplied to AddressProcessor.validateAddress() was found to have an invalid checksum. \n" +
"The invalid address String was: " + address);
return false;
}
return true;
}
/**
* Returns a boolean indicating whether a given String is a valid Bitmessage private key.
*/
public boolean validatePrivateKey(String privateKey)
{
byte[] privateKeyBytes = null;
try
{
privateKeyBytes = Base58.decode(privateKey);
}
catch (IllegalArgumentException e)
{
Log.i(TAG, "While validating a private key in AddressProcessor.validatePrivateKey(), the given String was found to contain an invalid character.");
return false;
}
byte[] privateKeyWithoutChecksum = ArrayCopier.copyOfRange(privateKeyBytes, 0, (privateKeyBytes.length - 4));
byte[] checksum = ArrayCopier.copyOfRange(privateKeyBytes, (privateKeyBytes.length - 4), privateKeyBytes.length);
byte[] hashOfPrivateKey = SHA256.doubleDigest(privateKeyWithoutChecksum);
byte[] testChecksum = ArrayCopier.copyOfRange(hashOfPrivateKey, 0, 4);
// Check the checksum
if (Arrays.equals(checksum, testChecksum) == false)
{
Log.i(TAG, "While validating a private key in AddressProcessor.validatePrivateKey(), the checksum was found to be invalid.");
return false;
}
// Check that the prepended 128 byte is in place
if (privateKeyWithoutChecksum[0] != (byte) 128)
{
Log.i(TAG, "While validating a private key in AddressProcessor.validatePrivateKey(), its prepended value was found to be invalid.");
return false;
}
// Drop the prepended 128 byte
byte[] privateKeyFinalBytes = ArrayCopier.copyOfRange(privateKeyWithoutChecksum, 1, privateKeyWithoutChecksum.length);
// Check the length of the key
if (privateKeyFinalBytes.length != 32)
{
Log.i(TAG, "While validating a private key in AddressProcessor.validatePrivateKey(), its length was found to be " + privateKeyFinalBytes.length + " instead of 32.");
return false;
}
return true;
}
/**
* Takes a String representing a Bitmessage address (e.g. "BM-NBniqBpDRZHLx7rVWyyrEf1XmPgSiSrr"
* and extracts the address version number and stream number encoded in the address.
*
* @param addressString - A String containing the Bitmessage address that we wish to extract the
* address version number and stream number from
*
* @return A int[] of exactly two elements. The first is an int representing the address version
* number and the second is an int representing the stream number
*/
public int[] decodeAddressNumbers (String addressString)
{
// First check that the String supplied is a valid Bitmessage address
if (validateAddress(addressString) == false)
{
throw new RuntimeException("Address String supplied to AddressProcessor.extractAddressVersionAndStreamNumberFromAddress() was found" +
"to be an invalid address by the AddressValidator.validateAddress() method. Throwing new RuntimeException.");
}
else
{
// Remove leading 3 characters ("BM-")
String addressDataString = addressString.substring(3, addressString.length());
// Do Base58 decode on the remaining string to get the "combinedAddressData" byte[]
byte[] combinedAddressData = Base58.decode(addressDataString);
// Do Varint check on first 9 bytes (for the address version)
long[] decoded = VarintEncoder.decode(ArrayCopier.copyOfRange(combinedAddressData, 0, 9));
int addressVersion = (int) decoded[0];
int bytesUsedForAddressVersion = (int) decoded[1]; // The number of bytes that were used to encode the address version
// Taking account of length result from first varint check, do second varint check (for the stream number)
decoded = VarintEncoder.decode(ArrayCopier.copyOfRange(combinedAddressData, bytesUsedForAddressVersion, bytesUsedForAddressVersion + 9));
int streamNumber = (int) decoded[0];
return new int[]{addressVersion, streamNumber};
}
}
/**
* Takes a String representing a Bitmessage address (e.g. "BM-NBniqBpDRZHLx7rVWyyrEf1XmPgSiSrr"
* and extracts the ripe hash encoded in the address.
*
* @param addressString - A String containing the Bitmessage address that we wish to extract the
* ripe hash from
*
* @return A byte[] containing the ripe hash extracted from the address
*/
public byte[] extractRipeHashFromAddress (String addressString)
{
// First check that the String supplied is a valid Bitmessage address
if (validateAddress(addressString) == false)
{
throw new RuntimeException("Address String supplied to AddressProcessor.extractRipeHashFromAddressString() was found" +
"to be an invalid address by the AddressValidator.validateAddress() method. Throwing new RuntimeException.");
}
else
{
// Remove leading 3 characters ("BM-")
String addressDataString = addressString.substring(3, addressString.length());
// Do Base58 decode on the remaining string to get the "combinedAddressData" byte[]
byte[] combinedAddressData = Base58.decode(addressDataString);
// Discard the final 4 bytes (the checksum)
byte[] combinedChecksumData = ArrayCopier.copyOfRange(combinedAddressData, 0, combinedAddressData.length - 4);
// Do varint check on first 9 bytes (for the address version)
long[] decoded = VarintEncoder.decode(ArrayCopier.copyOfRange(combinedChecksumData, 0, 9));
int bytesUsedForAddressVersion = (int) decoded[1]; // The number of bytes that were used to encode the address version
// Taking account of length result from first varint check, do second varint check (for the stream number)
decoded = VarintEncoder.decode(ArrayCopier.copyOfRange(combinedChecksumData, bytesUsedForAddressVersion, bytesUsedForAddressVersion + 9));
int bytesUsedForStreamNumber = (int) decoded[1]; // The number of bytes that were used to encode the stream number
// The remaining bytes are the ripe hash.
byte [] ripeHash = ArrayCopier.copyOfRange(combinedChecksumData, bytesUsedForAddressVersion + bytesUsedForStreamNumber, combinedChecksumData.length);
// If the ripe hash is less than 20 bytes in length, it needs to be padded with zero bytes until it is
while (ripeHash.length < 20)
{
byte[] zeroByte = new byte[]{0};
ripeHash = ByteUtils.concatenateByteArrays(zeroByte, ripeHash);
}
Log.i(TAG, "Ripe hash extracted from address string: " + ByteFormatter.byteArrayToHexString(ripeHash));
return ripeHash;
}
}
/**
* Calculates the encryption key of a given Bitmessage address. The encryption
* key is the first half of the double SHA-512 hash of the combined address data.
*
* @param addressString - A String containing the Bitmessage address to calculate
* the encryption key of
*
* @return A byte[] containing the encryption key
*/
public byte[] calculateAddressEncryptionKey (String addressString)
{
byte[] doubleHash = calculateDoubleHashOfAddressData(addressString);
byte[] encryptionKey = ArrayCopier.copyOfRange(doubleHash, 0, 32);
return encryptionKey;
}
/**
* Calculates the 'tag' of a given Bitmessage address. The 'tag' is the second
* half of the double SHA-512 hash of the combined address data.
*
* @param addressString - A String containing the Bitmessage address to calculate
* the tag of
*
* @return A byte[] containing the tag
*/
public byte[] calculateAddressTag (String addressString)
{
byte[] doubleHash = calculateDoubleHashOfAddressData(addressString);
byte[] tag = ArrayCopier.copyOfRange(doubleHash, 32, doubleHash.length);
return tag;
}
/**
* Calculates the double hash of the data encoded in a Bitmessage address.
* This 'double hash of address data' is used in the encryption of pubkeys.
*
* @param addressString - A String containing the Bitmessage address to calculate
* the double hash of
*
* @return A byte[] containing the 'double hash of address data'
*/
public byte[] calculateDoubleHashOfAddressData (String addressString)
{
// First check that the String supplied is a valid Bitmessage address
if (validateAddress(addressString) == false)
{
throw new RuntimeException("Address String supplied to AddressProcessor.calculateDoubleHashOfAddressData() was found" +
"to be an invalid address by the AddressValidator.validateAddress() method. Throwing new RuntimeException.");
}
else
{
// Calculate the 'encoded address data'. This is the data to be hashed.
byte[] dataToHash = extractEncodedAddressData(addressString);
// Double-hash the address data
byte[] doubleHashOfAddressData = SHA512.doubleHash(dataToHash);
return doubleHashOfAddressData;
}
}
/**
* Calculates the message tag for a given address at the current time.
*
* @param addressString - A String containing the Bitmessage address to calculate
* the message tag of
*
* @return A byte[] containing the message tag
*/
public byte[] calculateCurrentMessageTag (String addressString)
{
return calculateMessageTag(addressString, System.currentTimeMillis() / 1000);
}
/**
* Calculates the message tags for a given address since a given time.
*
* @param addressString - A String containing the Bitmessage address to calculate
* the message tags of
* @param pastTime - A long containing the time value to use to calculate the
* message tags
*
* @return A byte[] containing the message tags
*/
public byte[] calculateMessageTagsSince (String addressString, long pastTime)
{
long currentTime = System.currentTimeMillis() / 1000;
long timeElapsed = currentTime - pastTime;
long numberOfDaysSince = timeElapsed / SECONDS_IN_A_DAY;
byte[] messageTags = new byte[0];
for (int i = 0; i <= numberOfDaysSince; i++)
{
byte[] tag = calculateMessageTag(addressString, pastTime);
messageTags = ByteUtils.concatenateByteArrays(messageTags, tag);
pastTime += SECONDS_IN_A_DAY; // Advance to the next day
}
return messageTags;
}
/**
* Calculates the message tag for a given address and a given time value.
*
* @param addressString - A String containing the Bitmessage address to calculate
* the message tag of
* @param time - A long containing the time value to use to calculate the message tag
*
* @return A byte[] containing the message tag
*/
public byte[] calculateMessageTag (String addressString, long time)
{
// First check that the String supplied is a valid Bitmessage address
if (validateAddress(addressString) == false)
{
throw new RuntimeException("Address String supplied to AddressProcessor.calculateMessageTag() was found" +
"to be an invalid address by the AddressValidator.validateAddress() method. Throwing new RuntimeException.");
}
else
{
byte[] encodedAddressData = extractEncodedAddressData(addressString);
// Calculate the time value to use in the hash
long remainderSeconds = time % SECONDS_IN_A_DAY;
long timeValue = time - remainderSeconds;
// Get the byte form of the time value
byte[] timeValueBytes = ByteUtils.longToBytes(timeValue);
// Combine the bytes for the encoded address data and the time value into a single byte[]. This is the data to be hashed.
byte[] dataToHash = ByteUtils.concatenateByteArrays(encodedAddressData, timeValueBytes);
// Hash the input data to get the full tag
byte[] fullTag = SHA512.doubleHash(dataToHash);
// Get the first 32 bytes of the full tag. The result is the message tag.
byte[] messageTag = ArrayCopier.copyOf(fullTag, 32);
return messageTag;
}
}
/**
* Takes a pair of private keys and re-generates the Bitmessage address that
* they correspond to. The imported address is then saved to the database.
*
* @param privateSigningKey - The private signing key
* @param privateEncryptionKey - The private encryption key
*
* @return A boolean indicating whether or not the address was successfully imported
*/
public boolean importAddress (String privateSigningKey, String privateEncryptionKey)
{
try
{
// Re-create and save the Address
Address recreatedAddress = new AddressGenerator().importAddress(privateSigningKey, privateEncryptionKey);
// Make a BackgroundService request for the task, using the QueueRecord
Context appContext = App.getContext();
Intent intent = new Intent(appContext, BackgroundService.class);
intent.putExtra(BackgroundService.UI_REQUEST, BackgroundService.UI_REQUEST_CREATE_IDENTITY);
intent.putExtra(BackgroundService.ADDRESS_ID, recreatedAddress.getId());
BackgroundService.sendWakefulWork(appContext, intent);
}
catch (Exception e)
{
Log.e(TAG, "Exception occurred while running AddressProcessor.importAddress(). The exception message was: " + e.getMessage());
return false;
}
return true;
}
/**
* Extracts the 'encoded address data' of a given Bitmessage address.
* The 'encoded address data' is made up of the address version number,
* stream number, and ripe hash of the address, encoded into a single byte[].
*
* @param addressString - A String containing the Bitmessage address to calculate
* the 'encoded address data' of
*
* @return A byte[] containing the 'encoded address data'
*/
private byte[] extractEncodedAddressData (String addressString)
{
// Remove leading 3 characters ("BM-")
String addressDataString = addressString.substring(3, addressString.length());
// Do Base58 decode on the remaining string to get the "combinedAddressData" byte[]
byte[] combinedAddressData = Base58.decode(addressDataString);
// Discard the final 4 bytes (the checksum)
byte[] combinedChecksumData = ArrayCopier.copyOfRange(combinedAddressData, 0, combinedAddressData.length - 4);
// Do Varint check on first 9 bytes (for the address version)
long[] decoded = VarintEncoder.decode(ArrayCopier.copyOfRange(combinedChecksumData, 0, 9));
int addressVersion = (int) decoded[0];
int bytesUsedForAddressVersion = (int) decoded[1]; // The number of bytes that were used to encode the address version
// Taking account of length result from first varint check, do second varint check (for the stream number)
decoded = VarintEncoder.decode(ArrayCopier.copyOfRange(combinedChecksumData, bytesUsedForAddressVersion, bytesUsedForAddressVersion + 9));
int streamNumber = (int) decoded[0];
int bytesUsedForStreamNumber = (int) decoded[1]; // The number of bytes that were used to encode the stream number
// The remaining bytes are the ripe hash.
byte [] ripeHash = ArrayCopier.copyOfRange(combinedChecksumData, bytesUsedForAddressVersion + bytesUsedForStreamNumber, combinedChecksumData.length);
// If the ripe hash is less than 20 bytes in length, it needs to be padded with zero bytes until it is
while (ripeHash.length < 20)
{
byte[] zeroByte = new byte[]{0};
ripeHash = ByteUtils.concatenateByteArrays(zeroByte, ripeHash);
}
// Convert the address version and stream number into varint-encoded byte form
byte[] encodedAddressVersion = VarintEncoder.encode(addressVersion);
byte[] encodedStreamNumber = VarintEncoder.encode(streamNumber);
// Combine the bytes for the address version, stream number, and ripe hash into a single byte[]
byte[] encodedAddressData = ByteUtils.concatenateByteArrays(encodedAddressVersion, encodedStreamNumber);
encodedAddressData = ByteUtils.concatenateByteArrays(encodedAddressData, ripeHash);
return encodedAddressData;
}
}