add ecies encrypt and decrypt

This commit is contained in:
Craig Raw 2020-04-21 16:58:00 +02:00
parent 7d69fafc3e
commit 282628e455
2 changed files with 158 additions and 5 deletions

View file

@ -7,11 +7,15 @@ import org.bouncycastle.asn1.*;
import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair; import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.ec.CustomNamedCurves; import org.bouncycastle.crypto.ec.CustomNamedCurves;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator; import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.*; import org.bouncycastle.crypto.params.*;
import org.bouncycastle.crypto.signers.ECDSASigner; import org.bouncycastle.crypto.signers.ECDSASigner;
import org.bouncycastle.crypto.signers.HMacDSAKCalculator; import org.bouncycastle.crypto.signers.HMacDSAKCalculator;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.math.ec.ECPoint; import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.math.ec.FixedPointCombMultiplier; import org.bouncycastle.math.ec.FixedPointCombMultiplier;
import org.bouncycastle.math.ec.FixedPointUtil; import org.bouncycastle.math.ec.FixedPointUtil;
@ -20,11 +24,17 @@ import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.security.SecureRandom; import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64;
import java.util.Objects; import java.util.Objects;
/** /**
@ -79,6 +89,8 @@ public class ECKey {
CURVE_PARAMS.getH()); CURVE_PARAMS.getH());
HALF_CURVE_ORDER = CURVE_PARAMS.getN().shiftRight(1); HALF_CURVE_ORDER = CURVE_PARAMS.getN().shiftRight(1);
secureRandom = new SecureRandom(); secureRandom = new SecureRandom();
Security.addProvider(new BouncyCastleProvider());
} }
// The two parts of the key. If "pub" is set but not "priv", we can only verify signatures, not make them. // The two parts of the key. If "pub" is set but not "priv", we can only verify signatures, not make them.
@ -120,9 +132,6 @@ public class ECKey {
protected ECKey(BigInteger priv, LazyECPoint pub) { protected ECKey(BigInteger priv, LazyECPoint pub) {
if(priv != null) { if(priv != null) {
if(priv.bitLength() > 32 * 8) {
throw new IllegalArgumentException("Private key exceeds 32 bytes: " + priv.bitLength() + " bits");
}
if(priv.equals(BigInteger.ZERO) || priv.equals(BigInteger.ONE)) { if(priv.equals(BigInteger.ZERO) || priv.equals(BigInteger.ONE)) {
throw new IllegalArgumentException("Private key is illegal: " + priv); throw new IllegalArgumentException("Private key is illegal: " + priv);
} }
@ -607,10 +616,131 @@ public class ECKey {
return Utils.bigIntegerToBytes(getPrivKey(), 32); return Utils.bigIntegerToBytes(getPrivKey(), 32);
} }
public static ECKey createKeyPbkdf2HmacSha512(String password) {
return createKeyPbkdf2HmacSha512(password, new byte[0], 1024);
}
public static ECKey createKeyPbkdf2HmacSha512(String password, byte[] salt, int iterationCount) {
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA512Digest());
gen.init(password.getBytes(StandardCharsets.UTF_8), salt, iterationCount);
byte[] secret = ((KeyParameter) gen.generateDerivedParameters(512)).getKey();
return ECKey.fromPrivate(secret);
}
public byte[] encryptEcies(byte[] message, byte[] magic) {
ECKey ephemeral = new ECKey();
byte[] ecdh_key = this.getPubKeyPoint().multiply(ephemeral.getPrivKey()).getEncoded(true);
byte[] hash = sha512(ecdh_key);
byte[] iv = new byte[16];
System.arraycopy(hash, 0, iv, 0, 16);
byte[] key_e = new byte[16];
System.arraycopy(hash, 16, key_e, 0, 16);
byte[] key_m = new byte[hash.length-32];
System.arraycopy(hash, 32, key_m, 0, hash.length-32);
byte[] ciphertext = encryptAesCbcPkcs7(message, iv, key_e);
byte[] encrypted = concat(magic, ephemeral.getPubKey(), ciphertext);
byte[] result = hmac256(key_m, encrypted);
return Base64.getEncoder().encode(concat(encrypted, result));
}
private byte[] encryptAesCbcPkcs7(byte[] message, byte[] iv, byte[] key_e) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
SecretKeySpec secretKeySpec = new SecretKeySpec(key_e, "AES");
AlgorithmParameterSpec paramSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, paramSpec);
return cipher.doFinal(message);
} catch(Exception e) {
throw new RuntimeException(e);
}
}
public byte[] decryptEcies(byte[] message, byte[] magic) {
byte[] decoded = Base64.getDecoder().decode(message);
if(decoded.length < 85) {
throw new IllegalArgumentException("Ciphertext is too short at " + decoded.length + " bytes");
}
byte[] magicFound = new byte[4];
System.arraycopy(decoded, 0, magicFound, 0, 4);
byte[] ephemeralPubKeyBytes = new byte[33];
System.arraycopy(decoded, 4, ephemeralPubKeyBytes, 0, 33);
int ciphertextlength = decoded.length - 37 - 32;
byte[] ciphertext = new byte[ciphertextlength];
System.arraycopy(decoded, 37, ciphertext, 0, ciphertextlength);
byte[] mac = new byte[32];
System.arraycopy(decoded, decoded.length - 32, mac, 0, 32);
if(!Arrays.equals(magic, magicFound)) {
throw new IllegalArgumentException("Invalid ciphertext: invalid magic bytes");
}
ECKey ephemeralPubKey = ECKey.fromPublicOnly(ephemeralPubKeyBytes);
byte[] ecdh_key = ephemeralPubKey.getPubKeyPoint().multiply(this.getPrivKey()).getEncoded(true);
byte[] hash = sha512(ecdh_key);
byte[] iv = new byte[16];
System.arraycopy(hash, 0, iv, 0, 16);
byte[] key_e = new byte[16];
System.arraycopy(hash, 16, key_e, 0, 16);
byte[] key_m = new byte[hash.length-32];
System.arraycopy(hash, 32, key_m, 0, hash.length-32);
byte[] hmacInput = new byte[decoded.length-32];
System.arraycopy(decoded, 0, hmacInput, 0, decoded.length - 32);
if(!Arrays.equals(mac, hmac256(key_m, hmacInput))) {
throw new InvalidPasswordException();
}
return decryptAesCbcPkcs7(ciphertext, iv, key_e);
}
private byte[] decryptAesCbcPkcs7(byte[] ciphertext, byte[] iv, byte[] key_e) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
SecretKeySpec secretKeySpec = new SecretKeySpec(key_e, "AES");
AlgorithmParameterSpec paramSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, paramSpec);
return cipher.doFinal(ciphertext);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private byte[] sha512(byte[] input) {
SHA512Digest digest = new SHA512Digest();
byte[] hash = new byte[digest.getDigestSize()];
digest.update(input, 0, input.length);
digest.doFinal(hash, 0);
return hash;
}
private byte[] hmac256(byte[] key, byte[] input) {
HMac hmac = new HMac(new SHA256Digest());
hmac.init(new KeyParameter(key));
byte[] result = new byte[hmac.getMacSize()];
hmac.update(input, 0, input.length);
hmac.doFinal(result, 0);
return result;
}
private byte[] concat(byte[] ...bytes) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
for(byte[] byteArray : bytes) {
out.write(byteArray);
}
} catch (IOException e) {
//can't happen
}
return out.toByteArray();
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || !(o instanceof ECKey)) return false; if (!(o instanceof ECKey)) return false;
ECKey other = (ECKey) o; ECKey other = (ECKey) o;
return Objects.equals(this.priv, other.priv) return Objects.equals(this.priv, other.priv)
&& Objects.equals(this.pub, other.pub); && Objects.equals(this.pub, other.pub);
@ -628,4 +758,7 @@ public class ECKey {
public static class MissingPrivateKeyException extends RuntimeException { public static class MissingPrivateKeyException extends RuntimeException {
} }
public static class InvalidPasswordException extends RuntimeException {
}
} }

View file

@ -0,0 +1,20 @@
package com.sparrowwallet.drongo.protocol;
import com.sparrowwallet.drongo.crypto.ECKey;
import org.junit.Assert;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
public class ECKeyTest {
@Test
public void encryptDecrypt() {
String testMessage = "thisisatestmessage";
ECKey pubKey = ECKey.createKeyPbkdf2HmacSha512("iampassword");
byte[] encrypted = pubKey.encryptEcies(testMessage.getBytes(StandardCharsets.UTF_8), "BIE1".getBytes(StandardCharsets.UTF_8));
byte[] decrypted = pubKey.decryptEcies(encrypted, "BIE1".getBytes(StandardCharsets.UTF_8));
String decStr = new String(decrypted, StandardCharsets.UTF_8);
Assert.assertEquals(testMessage, decStr);
}
}