package org.bitseal.crypt;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import org.bitseal.data.EncryptedPayload;
import org.bitseal.util.ArrayCopier;
import org.bitseal.util.ByteUtils;
import org.spongycastle.crypto.BufferedBlockCipher;
import org.spongycastle.crypto.CipherParameters;
import org.spongycastle.crypto.DataLengthException;
import org.spongycastle.crypto.InvalidCipherTextException;
import org.spongycastle.crypto.engines.AESEngine;
import org.spongycastle.crypto.modes.CBCBlockCipher;
import org.spongycastle.crypto.paddings.BlockCipherPadding;
import org.spongycastle.crypto.paddings.PKCS7Padding;
import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.crypto.params.ParametersWithIV;
import org.spongycastle.jce.ECNamedCurveTable;
import org.spongycastle.jce.interfaces.ECPrivateKey;
import org.spongycastle.jce.interfaces.ECPublicKey;
import org.spongycastle.jce.provider.asymmetric.ec.EC5Util;
import org.spongycastle.jce.spec.ECNamedCurveParameterSpec;
import org.spongycastle.math.ec.ECCurve;
import org.spongycastle.math.ec.ECPoint;
/**
* Offers methods for encryption and decryption.
*
* @author Sebastian Schmidt, modified by Jonathan Coe
*/
public class CryptProcessor
{
private static final String ALGORITHM_ECDSA = "ECDSA";
private static final String ALGORITHM_ECIES = "ECIES";
private static final String PROVIDER = "SC"; // Spongy Castle
private static final String CURVE = "secp256k1";
private static final int CURVE_TYPE = 714;
private KeyPairGenerator kpg;
private KeyPairGenerator skpg;
public CryptProcessor()
{
Security.addProvider(new org.spongycastle.jce.provider.BouncyCastleProvider());
try
{
kpg = KeyPairGenerator.getInstance(ALGORITHM_ECIES, PROVIDER);
kpg.initialize(ECNamedCurveTable.getParameterSpec(CURVE), new SecureRandom());
skpg = KeyPairGenerator.getInstance(ALGORITHM_ECDSA, PROVIDER);
skpg.initialize(ECNamedCurveTable.getParameterSpec(CURVE), new SecureRandom());
}
catch (NoSuchAlgorithmException e)
{
throw new RuntimeException("NoSuchAlgorithmException occurred in CryptProcessor constructor", e);
}
catch (NoSuchProviderException e)
{
throw new RuntimeException("NoSuchProviderException occurred in CryptProcessor constructor", e);
}
catch (InvalidAlgorithmParameterException e)
{
throw new RuntimeException("InvalidAlgorithmParameterException occurred in CryptProcessor constructor", e);
}
}
/**
* Encrypts the given data using the supplied public key.<br><br>
*
* See https://bitmessage.org/wiki/Encryption and https://bitmessage.org/forum/index.php?topic=2848.0
*
* @param plain - A byte[] containing the data to be encrypted.
* @param K - An ECPublicKey object containing the public key 'K' to encrypt the data with.
*
* @return A byte[] containing the encrypted payload.
*/
public byte[] encrypt (byte[] plain, ECPublicKey K)
{
KeyPair random = generateEncryptionKeyPair();
ECPublicKey R = (ECPublicKey) random.getPublic();
BigInteger r = ((ECPrivateKey)random.getPrivate()).getD();
ECPoint P = K.getQ().multiply(r);
byte[] tmpKey = deriveKey(P);
byte[] key_e = ArrayCopier.copyOfRange(tmpKey, 0, 32);
byte[] key_m = ArrayCopier.copyOfRange(tmpKey, 32, 64);
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
byte[] cipherText = doAES(key_e, iv, plain, true);
byte[] x = ByteUtils.getUnsignedBytes(R.getQ().getX().toBigInteger(), 32);
byte[] y = ByteUtils.getUnsignedBytes(R.getQ().getY().toBigInteger(), 32);
int xLength = x.length;
int yLength = y.length;
byte[] encodedR = ByteUtils.concatenateByteArrays(ByteUtils.shortToBytes((short) 714), ByteUtils.shortToBytes((short) xLength), x, ByteUtils.shortToBytes((short) yLength), y);
byte[] dataForMac = ByteUtils.concatenateByteArrays(iv, encodedR, cipherText);
byte[] mac = SHA256.hmacSHA256(dataForMac, key_m);
byte[] encryptedPayload = ByteUtils.concatenateByteArrays(iv, encodedR, cipherText, mac);
return encryptedPayload;
}
/**
* Decrypts an encrypted msg.<br><br>
*
* <b>NOTE! If decryption fails, this method will throw a RuntimeException</b>
*
* @param encryptedPayload - A byte[] containing the data to be decrypted
* @param k - The ECPrivateKey object used to decrypt the data
*
* @return A byte[] containing the decrypted plain text
*/
public byte[] decrypt (byte[] encryptedPayload, ECPrivateKey k)
{
// Parse the data from the encrypted payload
EncryptedPayload encPay = parseEncryptedPayload(encryptedPayload);
byte[] iv = encPay.getIV();
BigInteger x = encPay.getX();
BigInteger y = encPay.getY();
byte[] cipherText = encPay.getCipherText();
byte[] mac = encPay.getMac();
// Reconstruct public key R
ECPublicKey R = createPublicEncryptionKey(x, y);
// Now that we have parsed all the data from the encrypted payload, we can begin the decryption process.
// First, do an EC point multiply with private key k and public key R. This gives you public key P.
ECPoint P = R.getQ().multiply(k.getD());
byte[] tmpKey = deriveKey(P);
byte[] key_e = ArrayCopier.copyOf(tmpKey, 32);
byte[] key_m = ArrayCopier.copyOfRange(tmpKey, 32, 64);
// Check whether the mac is valid
byte[] dataForMac = ArrayCopier.copyOfRange(encryptedPayload, 0, encryptedPayload.length - 32); // The mac now covers everything except itself
byte[] expectedMAC = SHA256.hmacSHA256(dataForMac, key_m);
if (Arrays.equals(mac, expectedMAC) == false)
{
// The mac is invalid
throw new RuntimeException("While attempting to decrypt an encrypted payload in CryptProcessor.decryptMsg(), the mac was found to be invalid");
}
else
{
// The mac is valid. Decrypt the parsed data
return doAES(key_e, iv, cipherText, false);
}
}
/**
* Parses an encrypted payload, for example from a msg or broadcast,
* and uses it to create a new EncryptedPayload object.
*
* @param encryptedPayload - A byte[] containing the encrypted payload data
*
* @return An EncryptedPayload object containing the parsed data.
*/
private EncryptedPayload parseEncryptedPayload(byte[] encryptedPayload)
{
// Parse the data from the payload
int readPosition = 0;
byte[] iv = ArrayCopier.copyOfRange(encryptedPayload, readPosition, readPosition + 16);
readPosition += 16;
int curveType = ByteUtils.bytesToShort(ArrayCopier.copyOfRange(encryptedPayload, readPosition, readPosition + 2));
readPosition += 2;
if (curveType != CURVE_TYPE)
{
throw new RuntimeException("While running CryptProcessor.parseEncryptedPayload(), the curve type was not 714. Something is wrong!\n"
+ "The curve type read was " + curveType);
}
int xLength = ByteUtils.bytesToShort(ArrayCopier.copyOfRange(encryptedPayload, readPosition, readPosition + 2));
readPosition += 2;
if (xLength > 32 || xLength < 0)
{
throw new RuntimeException("While running CryptProcessor.parseEncryptedPayload(), the xLength value was found to not be between 0 and 32. Something is wrong!\n"
+ "The xLength read was " + xLength);
}
BigInteger x = ByteUtils.getUnsignedBigInteger(ArrayCopier.copyOfRange(encryptedPayload, readPosition, readPosition + xLength), 0, xLength);
readPosition += xLength;
int yLength = ByteUtils.bytesToShort(ArrayCopier.copyOfRange(encryptedPayload, readPosition, readPosition + 2));
readPosition += 2;
if (yLength > 32 || yLength < 0)
{
throw new RuntimeException("While running CryptProcessor.parseEncryptedPayload(), the yLength value was found to not be between 0 and 32. Something is wrong!\n"
+ "The yLength read was " + yLength);
}
BigInteger y = ByteUtils.getUnsignedBigInteger(ArrayCopier.copyOfRange(encryptedPayload, readPosition, readPosition + yLength), 0, yLength);
readPosition += yLength;
byte[] cipherText = ArrayCopier.copyOfRange(encryptedPayload, readPosition, encryptedPayload.length - 32);
byte[] mac = ArrayCopier.copyOfRange(encryptedPayload, encryptedPayload.length - 32, encryptedPayload.length);
// Now use the parsed data to create a new EncryptedPayload object
EncryptedPayload encPay = new EncryptedPayload();
encPay.setIV(iv);
encPay.setCurveType(curveType);
encPay.setxLength(xLength);
encPay.setX(x);
encPay.setyLength(yLength);
encPay.setY(y);
encPay.setCipherText(cipherText);
encPay.setMac(mac);
return encPay;
}
/**
* Creates an ECPublicKey with the given coordinates. The key will have valid parameters.
*
* @param x - A BigInteger object denoting the x coordinate on the curve.
* @param y -A BigInteger object denoting the y coordinate on the curve.
*
* @return An ECPublicKey object with the given coordinates.
*/
private ECPublicKey createPublicEncryptionKey (BigInteger x, BigInteger y)
{
try
{
java.security.spec.ECPoint w = new java.security.spec.ECPoint(x, y);
ECNamedCurveParameterSpec params = ECNamedCurveTable.getParameterSpec(CURVE);
KeyFactory fact = KeyFactory.getInstance(ALGORITHM_ECDSA, PROVIDER);
ECCurve curve = params.getCurve();
java.security.spec.EllipticCurve ellipticCurve = EC5Util.convertCurve(curve, params.getSeed());
java.security.spec.ECParameterSpec params2 = EC5Util.convertSpec(ellipticCurve, params);
java.security.spec.ECPublicKeySpec keySpec = new java.security.spec.ECPublicKeySpec(w, params2);
return (ECPublicKey) fact.generatePublic(keySpec);
}
catch (InvalidKeySpecException e)
{
throw new RuntimeException("InvalidKeySpecException occurred in CryptProcessor.createPublicEncryptionKey()", e);
}
catch (NoSuchAlgorithmException e)
{
throw new RuntimeException("NoSuchAlgorithmException occurred in CryptProcessor.createPublicEncryptionKey()", e);
}
catch (NoSuchProviderException e)
{
throw new RuntimeException("NoSuchProviderException occurred in CryptProcessor.createPublicEncryptionKey()", e);
}
}
/**
* Generates a new random ECIES key pair.
*
* @return A KeyPair object containing the new random ECIES key pair.
*/
private KeyPair generateEncryptionKeyPair()
{
synchronized (kpg)
{
return kpg.generateKeyPair();
}
}
/**
* Derives a 64 byte key from the given ECPoint.
*
* @param p - An ECPoint object corresponding to the given point.
*
* @return A byte[] containing the 64 byte key.
*/
private byte[] deriveKey (ECPoint p)
{
return SHA512.sha512(ByteUtils.getUnsignedBytes(p.getX().toBigInteger(), 32));
}
/**
* Encrypts or decrypts the given data with the given key.
*
* @param keyBytes - A byte[] containing the AES key.
* @param iv - A byte[] containing the initialization vector to be used.
* @param data - A byte[] containing the data to process.
* @param encrypt - A boolean value: true if the data should be encrypted, false if it should be decrypted.
*
* @return A byte[] containing the encrypted or decrypted data.
*/
private byte[] doAES (byte[] keyBytes, byte[] iv, byte[] data, boolean encrypt)
{
BlockCipherPadding padding = new PKCS7Padding();
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()), padding);
KeyParameter key = new KeyParameter(keyBytes);
CipherParameters params = new ParametersWithIV(key, iv);
cipher.init(encrypt, params);
byte[] buffer = new byte[cipher.getOutputSize(data.length)];
int length = cipher.processBytes(data, 0, data.length, buffer, 0);
try
{
length += cipher.doFinal(buffer, length);
}
catch (DataLengthException e)
{
throw new RuntimeException("DataLengthException occurred in CryptProcessor.doAES()", e);
}
catch (IllegalStateException e)
{
throw new RuntimeException("IllegalStateException occurred in CryptProcessor.doAES()", e);
}
catch (InvalidCipherTextException e)
{
throw new RuntimeException("InvalidCipherTextException occurred in CryptProcessor.doAES()", e);
}
return ArrayCopier.copyOf(buffer, length);
}
}