348 lines
13 KiB
Java
348 lines
13 KiB
Java
package cy.agorise.graphenej;
|
|
|
|
import com.google.common.primitives.Bytes;
|
|
import com.google.gson.*;
|
|
import cy.agorise.graphenej.errors.ChecksumException;
|
|
import cy.agorise.graphenej.errors.MalformedAddressException;
|
|
import cy.agorise.graphenej.interfaces.ByteSerializable;
|
|
import cy.agorise.graphenej.interfaces.JsonSerializable;
|
|
import org.bitcoinj.core.ECKey;
|
|
import org.spongycastle.math.ec.ECPoint;
|
|
|
|
import java.lang.reflect.Type;
|
|
import java.math.BigInteger;
|
|
import java.security.MessageDigest;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.util.Arrays;
|
|
|
|
/**
|
|
* Class used to represent a memo data structure
|
|
* {@url https://bitshares.org/doxygen/structgraphene_1_1chain_1_1memo__data.html}
|
|
*/
|
|
public class Memo implements ByteSerializable, JsonSerializable {
|
|
public static final String KEY_FROM = "from";
|
|
public static final String KEY_TO = "to";
|
|
public static final String KEY_NONCE = "nonce";
|
|
public static final String KEY_MESSAGE = "message";
|
|
|
|
private Address from;
|
|
private Address to;
|
|
private BigInteger nonce;
|
|
private byte[] message;
|
|
private String plaintextMessage;
|
|
|
|
public String getPlaintextMessage() {
|
|
if(plaintextMessage == null)
|
|
return "";
|
|
else
|
|
return plaintextMessage;
|
|
}
|
|
|
|
public void setPlaintextMessage(String plaintextMessage) {
|
|
this.plaintextMessage = plaintextMessage;
|
|
}
|
|
|
|
/**
|
|
* Empty Constructor
|
|
*/
|
|
public Memo() {
|
|
this.from = null;
|
|
this.to = null;
|
|
this.message = null;
|
|
}
|
|
|
|
/**
|
|
* Constructor used for private memos.
|
|
* @param from: Address of sender
|
|
* @param to: Address of recipient.
|
|
* @param nonce: Nonce used in the encryption.
|
|
* @param message: Message in ciphertext.
|
|
*/
|
|
public Memo(Address from, Address to, BigInteger nonce, byte[] message){
|
|
this.from = from;
|
|
this.to = to;
|
|
this.nonce = nonce;
|
|
this.message = message;
|
|
}
|
|
|
|
/**
|
|
* Constructor intended to be used with public memos
|
|
* @param message: Message in plaintext.
|
|
*/
|
|
public Memo(String message){
|
|
this.message = message.getBytes();
|
|
}
|
|
|
|
public Address getSource(){
|
|
return this.from;
|
|
}
|
|
|
|
public Address getDestination(){
|
|
return this.to;
|
|
}
|
|
|
|
public BigInteger getNonce(){
|
|
return this.nonce;
|
|
}
|
|
|
|
public byte[] getByteMessage(){
|
|
return this.message;
|
|
}
|
|
|
|
public String getStringMessage(){
|
|
if(this.message != null)
|
|
return new String(this.message);
|
|
else
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* Method used to decrypt memo data.
|
|
* @param privateKey: Private key of the sender.
|
|
* @param publicKey: Public key of the recipient.
|
|
* @param nonce: The nonce.
|
|
* @param message: Plaintext message.
|
|
* @return: The encrypted version of the message.
|
|
*/
|
|
public static byte[] encryptMessage(ECKey privateKey, PublicKey publicKey, BigInteger nonce, String message){
|
|
byte[] encrypted = null;
|
|
try {
|
|
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
|
MessageDigest sha512 = MessageDigest.getInstance("SHA-512");
|
|
|
|
// Getting nonce bytes
|
|
String stringNonce = nonce.toString();
|
|
byte[] nonceBytes = Arrays.copyOfRange(Util.hexlify(stringNonce), 0, stringNonce.length());
|
|
|
|
// Getting shared secret
|
|
byte[] secret = publicKey.getKey().getPubKeyPoint().multiply(privateKey.getPrivKey()).normalize().getXCoord().getEncoded();
|
|
|
|
// SHA-512 of shared secret
|
|
byte[] ss = sha512.digest(secret);
|
|
|
|
byte[] seed = Bytes.concat(nonceBytes, Util.hexlify(Util.bytesToHex(ss)));
|
|
|
|
// Calculating checksum
|
|
byte[] sha256Msg = sha256.digest(message.getBytes());
|
|
byte[] checksum = Arrays.copyOfRange(sha256Msg, 0, 4);
|
|
|
|
// Concatenating checksum + message bytes
|
|
byte[] msgFinal = Bytes.concat(checksum, message.getBytes());
|
|
|
|
// Applying encryption
|
|
encrypted = Util.encryptAES(msgFinal, seed);
|
|
} catch (NoSuchAlgorithmException ex) {
|
|
System.out.println("NoSuchAlgotithmException. Msg:"+ ex.getMessage());
|
|
}
|
|
return encrypted;
|
|
}
|
|
|
|
/**
|
|
* Method used to encrypt memo data.
|
|
* @param privateKey: Private key of the sender.
|
|
* @param destinationAddress: Address of the recipient.
|
|
* @param nonce: The nonce.
|
|
* @param message: Plaintext message.
|
|
* @return: The encrypted version of the message.
|
|
*/
|
|
public static byte[] encryptMessage(ECKey privateKey, Address destinationAddress, BigInteger nonce, String message){
|
|
return encryptMessage(privateKey, destinationAddress.getPublicKey(), nonce, message);
|
|
}
|
|
|
|
|
|
/**
|
|
* Method used to decrypt memo data.
|
|
* @param privateKey: The private key of the recipient.
|
|
* @param publicKey: The public key of the sender.
|
|
* @param nonce: The nonce.
|
|
* @param message: The encrypted message.
|
|
* @return: The plaintext version of the enrcrypted message.
|
|
* @throws ChecksumException
|
|
*/
|
|
public static String decryptMessage(ECKey privateKey, PublicKey publicKey, BigInteger nonce, byte[] message) throws ChecksumException {
|
|
String plaintext = "";
|
|
try {
|
|
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
|
MessageDigest sha512 = MessageDigest.getInstance("SHA-512");
|
|
|
|
// Getting nonce bytes
|
|
String stringNonce = nonce.toString();
|
|
byte[] nonceBytes = Arrays.copyOfRange(Util.hexlify(stringNonce), 0, stringNonce.length());
|
|
|
|
// Getting shared secret
|
|
byte[] secret = publicKey.getKey().getPubKeyPoint().multiply(privateKey.getPrivKey()).normalize().getXCoord().getEncoded();
|
|
|
|
// SHA-512 of shared secret
|
|
byte[] ss = sha512.digest(secret);
|
|
|
|
byte[] seed = Bytes.concat(nonceBytes, Util.hexlify(Util.bytesToHex(ss)));
|
|
|
|
// Applying decryption
|
|
byte[] temp = Util.decryptAES(message, seed);
|
|
byte[] checksum = Arrays.copyOfRange(temp, 0, 4);
|
|
byte[] decrypted = Arrays.copyOfRange(temp, 4, temp.length);
|
|
plaintext = new String(decrypted);
|
|
byte[] checksumConfirmation = Arrays.copyOfRange(sha256.digest(decrypted), 0, 4);
|
|
boolean checksumVerification = Arrays.equals(checksum, checksumConfirmation);
|
|
if(!checksumVerification){
|
|
throw new ChecksumException("Invalid checksum found while performing decryption");
|
|
}
|
|
} catch (NoSuchAlgorithmException e) {
|
|
System.out.println("NoSuchAlgorithmException. Msg:"+ e.getMessage());
|
|
}
|
|
return plaintext;
|
|
}
|
|
|
|
/**
|
|
* Method used to decrypt memo data.
|
|
* @param privateKey: The private key of the recipient.
|
|
* @param sourceAddress: The public address key of the sender.
|
|
* @param nonce: The nonce.
|
|
* @param message: The encrypted message.
|
|
* @return: The plaintext version of the enrcrypted message.
|
|
* @throws ChecksumException
|
|
*/
|
|
public static String decryptMessage(ECKey privateKey, Address sourceAddress, BigInteger nonce, byte[] message) throws ChecksumException {
|
|
return decryptMessage(privateKey, sourceAddress.getPublicKey(), nonce, message);
|
|
}
|
|
|
|
|
|
/**
|
|
* Implement metod, serialized this Object
|
|
* @return the byte array of this object serialized
|
|
*/
|
|
@Override
|
|
public byte[] toBytes() {
|
|
if ((this.from == null) && (this.to == null) && (this.message == null)) {
|
|
return new byte[]{(byte) 0};
|
|
} else if(this.from == null && this.to == null & this.message != null){
|
|
return Bytes.concat(new byte[]{1},
|
|
new byte[]{(byte)0},
|
|
new byte[]{(byte)0},
|
|
new byte[]{(byte)0},
|
|
new byte[]{(byte) this.message.length},
|
|
this.message);
|
|
} else {
|
|
|
|
byte[] paddedNonceBytes = new byte[8];
|
|
byte[] originalNonceBytes = nonce.toByteArray();
|
|
System.arraycopy(originalNonceBytes, 0, paddedNonceBytes, 8 - originalNonceBytes.length, originalNonceBytes.length);
|
|
byte[] nonceBytes = Util.revertBytes(paddedNonceBytes);
|
|
// byte[] nonceBytes = Util.revertBytes(nonce.toByteArray());
|
|
|
|
ECPoint senderPoint = ECKey.compressPoint(from.getPublicKey().getKey().getPubKeyPoint());
|
|
PublicKey senderPublicKey = new PublicKey(ECKey.fromPublicOnly(senderPoint));
|
|
|
|
ECPoint recipientPoint = ECKey.compressPoint(to.getPublicKey().getKey().getPubKeyPoint());
|
|
PublicKey recipientPublicKey = new PublicKey(ECKey.fromPublicOnly(recipientPoint));
|
|
|
|
return Bytes.concat(new byte[]{1},
|
|
senderPublicKey.toBytes(),
|
|
recipientPublicKey.toBytes(),
|
|
nonceBytes,
|
|
new byte[]{(byte) this.message.length},
|
|
this.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts a memo instance to a String.
|
|
*
|
|
* The nonce is always encoded as a decimal number in a string field.
|
|
* @return
|
|
*/
|
|
@Override
|
|
public String toJsonString() {
|
|
Gson gson = new Gson();
|
|
return gson.toJson(toJson(true));
|
|
}
|
|
|
|
/**
|
|
* Converts a memo instance into a JsonElement.
|
|
*
|
|
* This method differs from the {@link #toJson(boolean)} in that here the nonce is encoded as an
|
|
* hexadecimal number, while in that one we offer the user to choose either the decimal or hexadecimal
|
|
* representations.
|
|
*
|
|
* @return JsonObject instance representing this memo
|
|
*/
|
|
@Override
|
|
public JsonElement toJsonObject() {
|
|
JsonObject memoObject = new JsonObject();
|
|
if ((this.from == null) && (this.to == null)) {
|
|
// TODO: Check if this public memo serialization is accepted
|
|
memoObject.addProperty(KEY_FROM, "");
|
|
memoObject.addProperty(KEY_TO, "");
|
|
memoObject.addProperty(KEY_NONCE, "");
|
|
if(this.message != null)
|
|
memoObject.addProperty(KEY_MESSAGE, Util.bytesToHex(this.message));
|
|
return null;
|
|
}else{
|
|
memoObject.addProperty(KEY_FROM, this.from.toString());
|
|
memoObject.addProperty(KEY_TO, this.to.toString());
|
|
memoObject.addProperty(KEY_NONCE, this.nonce.toString());
|
|
if(this.message != null)
|
|
memoObject.addProperty(KEY_MESSAGE, Util.bytesToHex(this.message));
|
|
}
|
|
return memoObject;
|
|
}
|
|
|
|
/**
|
|
* Method that converts the memo into a JsonObject.
|
|
*
|
|
* @param decimal If true, the nonce is saved as string containing a decimal number representation.
|
|
* @return JsonObject instance representing this memo
|
|
*/
|
|
public JsonElement toJson(boolean decimal){
|
|
JsonElement jsonElement = toJsonObject();
|
|
if(decimal && jsonElement != null){
|
|
JsonObject jsonObject = (JsonObject) jsonElement;
|
|
// The nonce is interpreted in base 16, but it is going to be written in base 10
|
|
BigInteger nonce = new BigInteger(jsonObject.get(KEY_NONCE).getAsString(), 16);
|
|
jsonObject.addProperty(KEY_NONCE, nonce.toString());
|
|
}
|
|
return jsonElement;
|
|
}
|
|
|
|
/**
|
|
* Class used to deserialize a memo
|
|
*/
|
|
public static class MemoDeserializer implements JsonDeserializer<Memo> {
|
|
|
|
@Override
|
|
public Memo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
|
JsonObject jsonObject = json.getAsJsonObject();
|
|
String fromAddress = jsonObject.get(KEY_FROM).getAsString();
|
|
String toAddress = jsonObject.get(KEY_TO).getAsString();
|
|
|
|
// Apparently the nonce is always coming from the full node as a string containing a
|
|
// decimal number. This is at odds with the result of the #toJsonObject method
|
|
// which encodes this data in hexadecimal.
|
|
BigInteger nonce = new BigInteger(jsonObject.get(KEY_NONCE).getAsString(), 10);
|
|
|
|
String msg = jsonObject.get(KEY_MESSAGE).getAsString();
|
|
Memo memo = null;
|
|
try{
|
|
Address from = new Address(fromAddress);
|
|
Address to = new Address(toAddress);
|
|
byte[] message = Util.hexToBytes(msg);
|
|
|
|
memo = new Memo(from, to, nonce, message);
|
|
}catch(MalformedAddressException e){
|
|
System.out.println("MalformedAddressException. Msg: "+e.getMessage());
|
|
}
|
|
return memo;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Class used to serialize a memo
|
|
*/
|
|
public static class MemoSerializer implements JsonSerializer<Memo> {
|
|
|
|
@Override
|
|
public JsonElement serialize(Memo memo, Type typeOfSrc, JsonSerializationContext context) {
|
|
return memo.toJson(true);
|
|
}
|
|
}
|
|
} |