package org.bitseal.crypt;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import org.bitseal.core.App;
import org.bitseal.data.Address;
import org.bitseal.database.AddressProvider;
import org.bitseal.util.ArrayCopier;
import org.bitseal.util.Base58;
import org.bitseal.util.ByteUtils;
import org.bitseal.util.VarintEncoder;
import org.spongycastle.jce.interfaces.ECPrivateKey;
/**
This class offers methods to generate new Bitmessage addresses, calculate the ripe hash of
a set of public keys, and to calculate the String representation of Bitmessage addresses
from existing data. <br><br>
A pseudocode overview of the address generation process can be found on page 10 of the Bitmessage
technical paper. The paper is available at at https://bitmessage.org/Bitmessage%20Technical%20Paper.pdf <br><br>
The relevant parts of this pseudocode are copied below. <br><br>
To calculate an address: <br><br>
1) private_signing_key = random 32 byte string <br><br>
2) private_encryption_key = random 32 byte string <br><br>
3) public_signing_key = calculate public key from private_signing_key <br><br>
4) public_encryption_key = calculate public key from private_encryption_key <br><br>
5) hash = RIPEMD160(SHA512(public_signing_key || public_encryption_key) <br><br>
6) checksum = first four bytes of SHA512(SHA512(addressversion || streamnumber|| hash)) <br><br>
7) address = base58encode(addressversion || streamnumber || hash || checksum) <br><br>
@author Jonathan Coe
*/
public class AddressGenerator
{
// The values for address version and stream number are currently hard-coded.
private static final int MY_ADDRESS_VERSION = 4;
private static final int MY_STREAM_NUMBER = 1;
private static final String DEFAULT_ADDRESS_LABEL = "New address";
private static final String IMPORTED_ADDRESS_LABEL = "Imported address";
/**
* Generates a new Address object representing a Bitmessage address.
*
* @return - A new Address object
*/
public Address generateAndSaveNewAddress()
{
// Generate a new Address
Address address = generateNewAddress();
// Save the new Address to the database
AddressProvider addProv = AddressProvider.get(App.getContext());
long addressId = addProv.addAddress(address);
// Finally, set the Address's ID to the one generated by the database
address.setId(addressId);
return address;
}
/**
* Recreates the String representation of a Bitmessage address from a given address version, stream number,
* public signing key and encryption key. <br><br>
*
* @param addressVersion - An int representing the address version number of the address to be recreated
* @param streamNumber - An int representing the stream number of the address to be recreated
* @param publicSigningKey - A byte[] containing the private signing key for this address.
* @param publicEncryptionKey - A byte[] containing the private encryption key for this address.
*
* @return A String representing the recreated Bitmessage address.
*/
public String recreateAddressString(int addressVersion, int streamNumber, byte[] publicSigningKey, byte[] publicEncryptionKey)
{
byte[] ripeHash = calculateRipeHash(publicSigningKey, publicEncryptionKey);
// When creating addresses, any leading zeros should be stripped from the ripe hash
ripeHash = ByteUtils.stripLeadingZeros(ripeHash);
String addressString = calculateAddressString(addressVersion, streamNumber, ripeHash);
return addressString;
}
/**
* Calculates the ripe hash needed for address generation
*
* @param publicSigningKey - A byte[] containing the public signing key to be used
* @param publicEncryptionKey - A byte[] containing the public encryption key to be used
*
* @return A byte[] containing the generated ripe hash
*/
public byte[] calculateRipeHash(byte[] publicSigningKey, byte[] publicEncryptionKey)
{
byte[] concatenatedPublicKeys = ByteUtils.concatenateByteArrays(publicSigningKey, publicEncryptionKey);
byte[] ripeHash = SHA512.sha512hash160(concatenatedPublicKeys);
// Remove any leading zeros from the ripe hash
ripeHash = ByteUtils.stripLeadingZeros(ripeHash);
return ripeHash;
}
/**
* 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 Address importAddress (String privateSigningKey, String privateEncryptionKey)
{
KeyConverter keyConv = new KeyConverter();
ECPrivateKey signingKey = keyConv.decodePrivateKeyFromWIF(privateSigningKey);
ECPrivateKey encryptionKey = keyConv.decodePrivateKeyFromWIF(privateEncryptionKey);
BigInteger signingKeyDValue = signingKey.getD();
BigInteger encryptionKeyDValue = encryptionKey.getD();
byte[] signingKeyBytes = signingKeyDValue.toByteArray();
byte[] encryptionKeyBytes = encryptionKeyDValue.toByteArray();
byte[] publicSigningKeyBytes = ECKeyPair.publicKeyFromPrivate(signingKeyDValue);
byte[] publicEncryptionKeyBytes = ECKeyPair.publicKeyFromPrivate(encryptionKeyDValue);
Address recreatedAddress = createAddressFromKeys(signingKeyBytes, encryptionKeyBytes, publicSigningKeyBytes, publicEncryptionKeyBytes);
recreatedAddress.setLabel(IMPORTED_ADDRESS_LABEL);
// Save the new Address to the database
AddressProvider addProv = AddressProvider.get(App.getContext());
long addressId = addProv.addAddress(recreatedAddress);
// Finally, set the Address's ID to the one generated by the database
recreatedAddress.setId(addressId);
return recreatedAddress;
}
/**
* Generates a new Bitmessage address.
*
* @return An Address object representing the newly generated Bitmessage address
*/
private Address generateNewAddress()
{
ECKeyPair signingKeyPair = new ECKeyPair();
ECKeyPair encryptionKeyPair = new ECKeyPair();
byte[] privateSigningKey = signingKeyPair.getPrivKey().toByteArray();
byte[] privateEncryptionKey = encryptionKeyPair.getPrivKey().toByteArray();
byte[] publicSigningKey = signingKeyPair.getPubKey();
byte[] publicEncryptionKey = encryptionKeyPair.getPubKey();
Address newAddress = createAddressFromKeys(privateSigningKey, privateEncryptionKey, publicSigningKey, publicEncryptionKey);
newAddress.setLabel(DEFAULT_ADDRESS_LABEL);
return newAddress;
}
/**
* Takes a set of signing and encryption keys and uses them to create a Bitmessage address.
*
* @param privateSigningKey - The private signing key
* @param privateEncryptionKey - The private encryption key
* @param publicSigningKey - The public signing key
* @param publicEncryptionKey - The public encryption key
*
* @return The created Address
*/
private Address createAddressFromKeys(byte[] privateSigningKey, byte[] privateEncryptionKey, byte[] publicSigningKey, byte[] publicEncryptionKey)
{
byte[] ripeHash = calculateRipeHash(publicSigningKey, publicEncryptionKey);
byte[] tag = calculateAddressTag(MY_ADDRESS_VERSION, MY_STREAM_NUMBER, ripeHash);
String addressString = calculateAddressString(MY_ADDRESS_VERSION, MY_STREAM_NUMBER, ripeHash);
KeyConverter keyConv = new KeyConverter();
String wifPrivateSigningKey = keyConv.encodePrivateKeyToWIF(privateSigningKey);
String wifPrivateEncryptionKey = keyConv.encodePrivateKeyToWIF(privateEncryptionKey);
Address generatedAddress = new Address();
generatedAddress.setAddress(addressString);
generatedAddress.setPrivateSigningKey(wifPrivateSigningKey);
generatedAddress.setPrivateEncryptionKey(wifPrivateEncryptionKey);
generatedAddress.setRipeHash(ripeHash);
generatedAddress.setTag(tag);
return generatedAddress; // Note that the ID and correspondingPubkeyId fields of this Address object have not yet been set
}
/**
* Calculates the combined checksum data needed for generating an address
*
* @param addressVersion - An int representing the address version of address being generated
* @param streamNumber - An int representing the stream number of the address being generated
* @param ripeHash - A byte[] containing the ripe hash of the address being generated
*
* @return A byte[] containing the combined checksum data
*/
private byte[] calculateCombinedChecksumData(int addressVersion, int streamNumber, byte[] ripeHash)
{
byte[] addressVersionBytes = VarintEncoder.encode((long) addressVersion);
byte[] streamNumberBytes = VarintEncoder.encode((long) streamNumber);
byte[] combinedChecksumData = null;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try
{
outputStream.write(addressVersionBytes);
outputStream.write(streamNumberBytes);
outputStream.write(ripeHash);
combinedChecksumData = outputStream.toByteArray();
outputStream.close();
}
catch (IOException e)
{
throw new RuntimeException("IOException occurred in AddressGenerator.calculateCombinedChecksumData()", e);
}
return combinedChecksumData;
}
/**
* Calculates the checksum needed for generating a new address
*
* @param combinedChecksumData - The combined checksum data to make the checksum from
*
* @return A byte[] containing the calculated checksum
*/
private byte[] calculateChecksum(byte[] combinedChecksumData)
{
byte[] checksumFullHash = SHA512.doubleHash(combinedChecksumData);
byte[] checksum = ArrayCopier.copyOfRange(checksumFullHash, 0, 4);
return checksum;
}
/**
* Calculates the String representation of a Bitmessage address from the supplied data.
*
* @param addressVersion - An int representing the address version number
* @param streamNumber - An int representing the address stream number
* @param ripeHash - A byte[] containing the ripe hash of the address
*
* @return A String containing the calculated address
*/
private String calculateAddressString(int addressVersion, int streamNumber, byte[] ripeHash)
{
byte[] combinedChecksumData = calculateCombinedChecksumData(addressVersion, streamNumber, ripeHash);
byte[] checksum = calculateChecksum(combinedChecksumData);
ByteArrayOutputStream outputStream2 = new ByteArrayOutputStream();
byte[] combinedAddressData = null;
try
{
outputStream2.write(combinedChecksumData);
outputStream2.write(checksum);
combinedAddressData = outputStream2.toByteArray();
outputStream2.close();
}
catch (IOException e)
{
throw new RuntimeException("IOException occurred in AddressGenerator.calculateAddressString()", e);
}
String base58Address = Base58.encode(combinedAddressData);
String addressString = "BM-" + base58Address;
return addressString;
}
/**
* 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 addressVersion - An int representing the address version number
* @param streamNumber - An int representing the address stream number
* @param ripeHash - A byte[] containing the ripe hash of the address
*
* @return A byte[] containing the Address tag
*/
private byte[] calculateAddressTag(int addressVersion, int streamNumber, byte[] ripeHash)
{
byte[] addressVersionBytes = VarintEncoder.encode((long) addressVersion);
byte[] streamNumberBytes = VarintEncoder.encode((long) streamNumber);
byte[] combinedAddressData = null;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try
{
outputStream.write(addressVersionBytes);
outputStream.write(streamNumberBytes);
outputStream.write(ripeHash);
combinedAddressData = outputStream.toByteArray();
outputStream.close();
}
catch (IOException e)
{
throw new RuntimeException("IOException occurred in AddressGenerator.calculateAddressTag()", e);
}
byte[] doubleHashOfAddressData = SHA512.doubleHash(combinedAddressData);
byte[] tag = ArrayCopier.copyOfRange(doubleHashOfAddressData, 32, doubleHashOfAddressData.length);
return tag;
}
}