From 3f79565b69f8cec198eb6f41f4d217dba98e7cc9 Mon Sep 17 00:00:00 2001 From: Devrandom Date: Thu, 12 Mar 2015 12:07:33 -0700 Subject: [PATCH] ECIES matching go-ethereum --- ethereumj-core/build.gradle | 1 + .../org/ethereum/ConcatKDFBytesGenerator.java | 145 ++++++ .../ethereum/crypto/EthereumIESEngine.java | 420 ++++++++++++++++++ .../java/test/ethereum/crypto/ECIESTest.java | 143 ++++++ 4 files changed, 709 insertions(+) create mode 100644 ethereumj-core/src/main/java/org/ethereum/ConcatKDFBytesGenerator.java create mode 100644 ethereumj-core/src/main/java/org/ethereum/crypto/EthereumIESEngine.java create mode 100644 ethereumj-core/src/test/java/test/ethereum/crypto/ECIESTest.java diff --git a/ethereumj-core/build.gradle b/ethereumj-core/build.gradle index 73878c02..186e44c8 100644 --- a/ethereumj-core/build.gradle +++ b/ethereumj-core/build.gradle @@ -58,6 +58,7 @@ ext { dependencies { compile "io.netty:netty-all:4.0.23.Final" compile "com.madgag.spongycastle:core:${scastleVersion}" // for SHA3 and SECP256K1 + compile "com.madgag.spongycastle:prov:${scastleVersion}" // for SHA3 and SECP256K1 compile "org.iq80.leveldb:leveldb:${leveldbVersion}" compile "com.cedarsoftware:java-util:1.8.0" // for deep equals compile "org.antlr:antlr4-runtime:4.5" // for serpent compilation diff --git a/ethereumj-core/src/main/java/org/ethereum/ConcatKDFBytesGenerator.java b/ethereumj-core/src/main/java/org/ethereum/ConcatKDFBytesGenerator.java new file mode 100644 index 00000000..362d1b1c --- /dev/null +++ b/ethereumj-core/src/main/java/org/ethereum/ConcatKDFBytesGenerator.java @@ -0,0 +1,145 @@ +package org.ethereum; + +import org.spongycastle.crypto.DataLengthException; +import org.spongycastle.crypto.DerivationParameters; +import org.spongycastle.crypto.Digest; +import org.spongycastle.crypto.DigestDerivationFunction; +import org.spongycastle.crypto.params.ISO18033KDFParameters; +import org.spongycastle.crypto.params.KDFParameters; +import org.spongycastle.util.Pack; + +/** + * Basic KDF generator for derived keys and ivs as defined by NIST SP 800-56A. + */ +public class ConcatKDFBytesGenerator + implements DigestDerivationFunction +{ + private int counterStart; + private Digest digest; + private byte[] shared; + private byte[] iv; + + /** + * Construct a KDF Parameters generator. + *

+ * + * @param counterStart + * value of counter. + * @param digest + * the digest to be used as the source of derived keys. + */ + protected ConcatKDFBytesGenerator(int counterStart, Digest digest) + { + this.counterStart = counterStart; + this.digest = digest; + } + + public ConcatKDFBytesGenerator(Digest digest) { + this(1, digest); + } + + public void init(DerivationParameters param) + { + if (param instanceof KDFParameters) + { + KDFParameters p = (KDFParameters)param; + + shared = p.getSharedSecret(); + iv = p.getIV(); + } + else if (param instanceof ISO18033KDFParameters) + { + ISO18033KDFParameters p = (ISO18033KDFParameters)param; + + shared = p.getSeed(); + iv = null; + } + else + { + throw new IllegalArgumentException("KDF parameters required for KDF2Generator"); + } + } + + /** + * return the underlying digest. + */ + public Digest getDigest() + { + return digest; + } + + /** + * fill len bytes of the output buffer with bytes generated from the + * derivation function. + * + * @throws IllegalArgumentException + * if the size of the request will cause an overflow. + * @throws DataLengthException + * if the out buffer is too small. + */ + public int generateBytes(byte[] out, int outOff, int len) throws DataLengthException, + IllegalArgumentException + { + if ((out.length - len) < outOff) + { + throw new DataLengthException("output buffer too small"); + } + + long oBytes = len; + int outLen = digest.getDigestSize(); + + // + // this is at odds with the standard implementation, the + // maximum value should be hBits * (2^32 - 1) where hBits + // is the digest output size in bits. We can't have an + // array with a long index at the moment... + // + if (oBytes > ((2L << 32) - 1)) + { + throw new IllegalArgumentException("Output length too large"); + } + + int cThreshold = (int)((oBytes + outLen - 1) / outLen); + + byte[] dig = new byte[digest.getDigestSize()]; + + byte[] C = new byte[4]; + Pack.intToBigEndian(counterStart, C, 0); + + int counterBase = counterStart & ~0xFF; + + for (int i = 0; i < cThreshold; i++) + { + digest.update(C, 0, C.length); + digest.update(shared, 0, shared.length); + + if (iv != null) + { + digest.update(iv, 0, iv.length); + } + + digest.doFinal(dig, 0); + + if (len > outLen) + { + System.arraycopy(dig, 0, out, outOff, outLen); + outOff += outLen; + len -= outLen; + } + else + { + System.arraycopy(dig, 0, out, outOff, len); + } + + if (++C[3] == 0) + { + counterBase += 0x100; + Pack.intToBigEndian(counterBase, C, 0); + } + } + + digest.reset(); + + return (int)oBytes; + } +} diff --git a/ethereumj-core/src/main/java/org/ethereum/crypto/EthereumIESEngine.java b/ethereumj-core/src/main/java/org/ethereum/crypto/EthereumIESEngine.java new file mode 100644 index 00000000..1789f746 --- /dev/null +++ b/ethereumj-core/src/main/java/org/ethereum/crypto/EthereumIESEngine.java @@ -0,0 +1,420 @@ +package org.ethereum.crypto; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.math.BigInteger; + +import org.spongycastle.crypto.*; +import org.spongycastle.crypto.generators.EphemeralKeyPairGenerator; +import org.spongycastle.crypto.params.AsymmetricKeyParameter; +import org.spongycastle.crypto.params.IESParameters; +import org.spongycastle.crypto.params.IESWithCipherParameters; +import org.spongycastle.crypto.params.KDFParameters; +import org.spongycastle.crypto.params.KeyParameter; +import org.spongycastle.crypto.params.ParametersWithIV; +import org.spongycastle.util.Arrays; +import org.spongycastle.util.BigIntegers; +import org.spongycastle.util.Pack; + +/** + * Support class for constructing integrated encryption cipher + * for doing basic message exchanges on top of key agreement ciphers. + * Follows the description given in IEEE Std 1363a with a couple of changes + * specific to Ethereum: + * - Hash the MAC key before use + * - Include the encryption IV in the MAC computation + */ +public class EthereumIESEngine +{ + private final Digest hash; + BasicAgreement agree; + DerivationFunction kdf; + Mac mac; + BufferedBlockCipher cipher; + byte[] macBuf; + + boolean forEncryption; + CipherParameters privParam, pubParam; + IESParameters param; + + byte[] V; + private EphemeralKeyPairGenerator keyPairGenerator; + private KeyParser keyParser; + private byte[] IV; + + /** + * set up for use with stream mode, where the key derivation function + * is used to provide a stream of bytes to xor with the message. + * @param agree the key agreement used as the basis for the encryption + * @param kdf the key derivation function used for byte generation + * @param mac the message authentication code generator for the message + * @param hash + * @param cipher + */ + public EthereumIESEngine( + BasicAgreement agree, + DerivationFunction kdf, + Mac mac, Digest hash, BufferedBlockCipher cipher) + { + this.agree = agree; + this.kdf = kdf; + this.mac = mac; + this.hash = hash; + this.macBuf = new byte[mac.getMacSize()]; + this.cipher = cipher; + } + + + /** + * Initialise the encryptor. + * + * @param forEncryption whether or not this is encryption/decryption. + * @param privParam our private key parameters + * @param pubParam the recipient's/sender's public key parameters + * @param params encoding and derivation parameters, may be wrapped to include an IV for an underlying block cipher. + */ + public void init( + boolean forEncryption, + CipherParameters privParam, + CipherParameters pubParam, + CipherParameters params) + { + this.forEncryption = forEncryption; + this.privParam = privParam; + this.pubParam = pubParam; + this.V = new byte[0]; + + extractParams(params); + } + + + /** + * Initialise the encryptor. + * + * @param publicKey the recipient's/sender's public key parameters + * @param params encoding and derivation parameters, may be wrapped to include an IV for an underlying block cipher. + * @param ephemeralKeyPairGenerator the ephemeral key pair generator to use. + */ + public void init(AsymmetricKeyParameter publicKey, CipherParameters params, EphemeralKeyPairGenerator ephemeralKeyPairGenerator) + { + this.forEncryption = true; + this.pubParam = publicKey; + this.keyPairGenerator = ephemeralKeyPairGenerator; + + extractParams(params); + } + + /** + * Initialise the encryptor. + * + * @param privateKey the recipient's private key. + * @param params encoding and derivation parameters, may be wrapped to include an IV for an underlying block cipher. + * @param publicKeyParser the parser for reading the ephemeral public key. + */ + public void init(AsymmetricKeyParameter privateKey, CipherParameters params, KeyParser publicKeyParser) + { + this.forEncryption = false; + this.privParam = privateKey; + this.keyParser = publicKeyParser; + + extractParams(params); + } + + private void extractParams(CipherParameters params) + { + if (params instanceof ParametersWithIV) + { + this.IV = ((ParametersWithIV)params).getIV(); + this.param = (IESParameters)((ParametersWithIV)params).getParameters(); + } + else + { + this.IV = null; + this.param = (IESParameters)params; + } + } + + public BufferedBlockCipher getCipher() + { + return cipher; + } + + public Mac getMac() + { + return mac; + } + + private byte[] encryptBlock( + byte[] in, + int inOff, + int inLen) + throws InvalidCipherTextException + { + byte[] C = null, K = null, K1 = null, K2 = null; + int len; + + if (cipher == null) + { + // Streaming mode. + K1 = new byte[inLen]; + K2 = new byte[param.getMacKeySize() / 8]; + K = new byte[K1.length + K2.length]; + + kdf.generateBytes(K, 0, K.length); + + if (V.length != 0) + { + System.arraycopy(K, 0, K2, 0, K2.length); + System.arraycopy(K, K2.length, K1, 0, K1.length); + } + else + { + System.arraycopy(K, 0, K1, 0, K1.length); + System.arraycopy(K, inLen, K2, 0, K2.length); + } + + C = new byte[inLen]; + + for (int i = 0; i != inLen; i++) + { + C[i] = (byte)(in[inOff + i] ^ K1[i]); + } + len = inLen; + } + else + { + // Block cipher mode. + K1 = new byte[((IESWithCipherParameters)param).getCipherKeySize() / 8]; + K2 = new byte[param.getMacKeySize() / 8]; + K = new byte[K1.length + K2.length]; + + kdf.generateBytes(K, 0, K.length); + System.arraycopy(K, 0, K1, 0, K1.length); + System.arraycopy(K, K1.length, K2, 0, K2.length); + + // If iv provided use it to initialise the cipher + if (IV != null) + { + cipher.init(true, new ParametersWithIV(new KeyParameter(K1), IV)); + } + else + { + cipher.init(true, new KeyParameter(K1)); + } + + C = new byte[cipher.getOutputSize(inLen)]; + len = cipher.processBytes(in, inOff, inLen, C, 0); + len += cipher.doFinal(C, len); + } + + + // Convert the length of the encoding vector into a byte array. + byte[] P2 = param.getEncodingV(); + byte[] L2 = new byte[4]; + if (V.length != 0 && P2 != null) + { + Pack.intToBigEndian(P2.length * 8, L2, 0); + } + + + // Apply the MAC. + byte[] T = new byte[mac.getMacSize()]; + + mac.init(new KeyParameter(K2)); + mac.update(C, 0, C.length); + if (P2 != null) + { + mac.update(P2, 0, P2.length); + } + if (V.length != 0) + { + mac.update(L2, 0, L2.length); + } + mac.doFinal(T, 0); + + + // Output the triple (V,C,T). + byte[] Output = new byte[V.length + len + T.length]; + System.arraycopy(V, 0, Output, 0, V.length); + System.arraycopy(C, 0, Output, V.length, len); + System.arraycopy(T, 0, Output, V.length + len, T.length); + return Output; + } + + private byte[] decryptBlock( + byte[] in_enc, + int inOff, + int inLen) + throws InvalidCipherTextException + { + byte[] M = null, K = null, K1 = null, K2 = null; + int len; + + // Ensure that the length of the input is greater than the MAC in bytes + if (inLen <= (param.getMacKeySize() / 8)) + { + throw new InvalidCipherTextException("Length of input must be greater than the MAC"); + } + + if (cipher == null) + { + // Streaming mode. + K1 = new byte[inLen - V.length - mac.getMacSize()]; + K2 = new byte[param.getMacKeySize() / 8]; + K = new byte[K1.length + K2.length]; + + kdf.generateBytes(K, 0, K.length); + + if (V.length != 0) + { + System.arraycopy(K, 0, K2, 0, K2.length); + System.arraycopy(K, K2.length, K1, 0, K1.length); + } + else + { + System.arraycopy(K, 0, K1, 0, K1.length); + System.arraycopy(K, K1.length, K2, 0, K2.length); + } + + M = new byte[K1.length]; + + for (int i = 0; i != K1.length; i++) + { + M[i] = (byte)(in_enc[inOff + V.length + i] ^ K1[i]); + } + + len = K1.length; + } + else + { + // Block cipher mode. + K1 = new byte[((IESWithCipherParameters)param).getCipherKeySize() / 8]; + K2 = new byte[param.getMacKeySize() / 8]; + K = new byte[K1.length + K2.length]; + + kdf.generateBytes(K, 0, K.length); + System.arraycopy(K, 0, K1, 0, K1.length); + System.arraycopy(K, K1.length, K2, 0, K2.length); + + // If IV provide use it to initialize the cipher + if (IV != null) + { + cipher.init(false, new ParametersWithIV(new KeyParameter(K1), IV)); + } + else + { + cipher.init(false, new KeyParameter(K1)); + } + + M = new byte[cipher.getOutputSize(inLen - V.length - mac.getMacSize())]; + len = cipher.processBytes(in_enc, inOff + V.length, inLen - V.length - mac.getMacSize(), M, 0); + len += cipher.doFinal(M, len); + } + + + // Convert the length of the encoding vector into a byte array. + byte[] P2 = param.getEncodingV(); + byte[] L2 = new byte[4]; + if (V.length != 0 && P2 != null) + { + Pack.intToBigEndian(P2.length * 8, L2, 0); + } + + + // Verify the MAC. + int end = inOff + inLen; + byte[] T1 = Arrays.copyOfRange(in_enc, end - mac.getMacSize(), end); + + byte[] T2 = new byte[T1.length]; + byte[] K2a = new byte[hash.getDigestSize()]; + hash.reset(); + hash.update(K2, 0, K2.length); + hash.doFinal(K2a, 0); + mac.init(new KeyParameter(K2a)); + mac.update(IV, 0, IV.length); + mac.update(in_enc, inOff + V.length, inLen - V.length - T2.length); + + if (P2 != null) + { + mac.update(P2, 0, P2.length); + } + if (V.length != 0) + { + mac.update(L2, 0, L2.length); + } + mac.doFinal(T2, 0); + + if (!Arrays.constantTimeAreEqual(T1, T2)) + { + throw new InvalidCipherTextException("Invalid MAC."); + } + + + // Output the message. + return Arrays.copyOfRange(M, 0, len); + } + + + public byte[] processBlock( + byte[] in, + int inOff, + int inLen) + throws InvalidCipherTextException + { + if (forEncryption) + { + if (keyPairGenerator != null) + { + EphemeralKeyPair ephKeyPair = keyPairGenerator.generate(); + + this.privParam = ephKeyPair.getKeyPair().getPrivate(); + this.V = ephKeyPair.getEncodedPublicKey(); + } + } + else + { + if (keyParser != null) + { + ByteArrayInputStream bIn = new ByteArrayInputStream(in, inOff, inLen); + + try + { + this.pubParam = keyParser.readKey(bIn); + } + catch (IOException e) + { + throw new InvalidCipherTextException("unable to recover ephemeral public key: " + e.getMessage(), e); + } + + int encLength = (inLen - bIn.available()); + this.V = Arrays.copyOfRange(in, inOff, inOff + encLength); + } + } + + // Compute the common value and convert to byte array. + agree.init(privParam); + BigInteger z = agree.calculateAgreement(pubParam); + byte[] Z = BigIntegers.asUnsignedByteArray(agree.getFieldSize(), z); + + // Create input to KDF. + byte[] VZ; +// if (V.length != 0) +// { +// VZ = new byte[V.length + Z.length]; +// System.arraycopy(V, 0, VZ, 0, V.length); +// System.arraycopy(Z, 0, VZ, V.length, Z.length); +// } +// else + { + VZ = Z; + } + + // Initialise the KDF. + KDFParameters kdfParam = new KDFParameters(VZ, param.getDerivationV()); + kdf.init(kdfParam); + + return forEncryption + ? encryptBlock(in, inOff, inLen) + : decryptBlock(in, inOff, inLen); + } +} diff --git a/ethereumj-core/src/test/java/test/ethereum/crypto/ECIESTest.java b/ethereumj-core/src/test/java/test/ethereum/crypto/ECIESTest.java new file mode 100644 index 00000000..fd03e67a --- /dev/null +++ b/ethereumj-core/src/test/java/test/ethereum/crypto/ECIESTest.java @@ -0,0 +1,143 @@ +package test.ethereum.crypto; + +import org.ethereum.ConcatKDFBytesGenerator; +import org.ethereum.crypto.ECKey; +import org.ethereum.crypto.EthereumIESEngine; +import org.junit.BeforeClass; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongycastle.asn1.sec.SECNamedCurves; +import org.spongycastle.asn1.x9.X9ECParameters; +import org.spongycastle.crypto.AsymmetricCipherKeyPair; +import org.spongycastle.crypto.BufferedBlockCipher; +import org.spongycastle.crypto.KeyGenerationParameters; +import org.spongycastle.crypto.agreement.ECDHBasicAgreement; +import org.spongycastle.crypto.digests.SHA256Digest; +import org.spongycastle.crypto.engines.AESFastEngine; +import org.spongycastle.crypto.generators.ECKeyPairGenerator; +import org.spongycastle.crypto.generators.KDF2BytesGenerator; +import org.spongycastle.crypto.macs.HMac; +import org.spongycastle.crypto.modes.SICBlockCipher; +import org.spongycastle.crypto.params.*; +import org.spongycastle.math.ec.ECPoint; +import org.spongycastle.util.encoders.Hex; + +import java.io.ByteArrayInputStream; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.security.Security; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertTrue; + +public class ECIESTest { + public static final int MAC_KEY_SIZE = 128; + static Logger log = LoggerFactory.getLogger("test"); + private static ECDomainParameters curve; + private static final String CIPHERTEXT1 = "042a851331790adacf6e64fcb19d0872fcdf1285a899a12cdc897da941816b0ea6485402aaf6c2e0a5d98ae3af1b05c68b307d1e0eb7a426a46f1617ba5b94f90b606eee3b5e9d2b527a9ee52cfa377bcd118b9390ed27ffe7d48e8155004375cae209012c3e057bb13a478a64a201d79ad4ae83"; + private static final X9ECParameters IES_CURVE_PARAM = SECNamedCurves.getByName("secp256r1"); + private static final BigInteger PRIVATE_KEY1 = new BigInteger("51134539186617376248226283012294527978458758538121566045626095875284492680246"); + + private static ECPoint pub(BigInteger d) throws Exception { + return curve.getG().multiply(d); + } + + @BeforeClass + public static void beforeAll() { + if (Security.getProvider("SC") == null) + Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1); + curve = new ECDomainParameters(IES_CURVE_PARAM.getCurve(), IES_CURVE_PARAM.getG(), IES_CURVE_PARAM.getN(), IES_CURVE_PARAM.getH()); + } + + @Test + public void testKDF() { + ConcatKDFBytesGenerator kdf = new ConcatKDFBytesGenerator(new SHA256Digest()); + kdf.init(new KDFParameters(new String("Hello").getBytes(), new byte[0])); + byte[] bytes = new byte[2]; + kdf.generateBytes(bytes, 0, bytes.length); + assertArrayEquals(new byte[]{-66, -89}, bytes); + } + + @Test + public void testDecryptTestVector() throws Throwable { + ECPoint pub1 = pub(PRIVATE_KEY1); + byte[] cipher = Hex.decode(CIPHERTEXT1); + ByteArrayInputStream is = new ByteArrayInputStream(cipher); + byte[] ephemBytes = new byte[2*((curve.getCurve().getFieldSize()+7)/8) + 1]; + is.read(ephemBytes); + ECPoint ephem = curve.getCurve().decodePoint(ephemBytes); + byte[] IV = new byte[MAC_KEY_SIZE/8]; + is.read(IV); + byte[] cipherBody = new byte[is.available()]; + is.read(cipherBody); + + byte[] plaintext = decrypt(ephem, PRIVATE_KEY1, IV, cipherBody); + assertArrayEquals(new byte[]{1,1,1}, plaintext); + } + + public static byte[] decrypt(ECPoint ephem, BigInteger prv, byte[] IV, byte[] cipher) throws Throwable { + AESFastEngine aesFastEngine = new AESFastEngine(); + + EthereumIESEngine iesEngine = new EthereumIESEngine( + new ECDHBasicAgreement(), + new ConcatKDFBytesGenerator(new SHA256Digest()), + new HMac(new SHA256Digest()), + new SHA256Digest(), + new BufferedBlockCipher(new SICBlockCipher(aesFastEngine))); + + + byte[] d = new byte[] {}; + byte[] e = new byte[] {}; + + IESParameters p = new IESWithCipherParameters(d, e, MAC_KEY_SIZE, MAC_KEY_SIZE); + ParametersWithIV parametersWithIV = + new ParametersWithIV(p, IV); + + iesEngine.init(false, new ECPrivateKeyParameters(prv, curve), new ECPublicKeyParameters(ephem, curve), parametersWithIV); + + byte[] message = iesEngine.processBlock(cipher, 0, cipher.length); + return message; + } + + public static byte[] encrypt(byte[] plaintext) throws Throwable { + AESFastEngine aesFastEngine = new AESFastEngine(); + + EthereumIESEngine iesEngine = new EthereumIESEngine( + new ECDHBasicAgreement(), + new KDF2BytesGenerator(new SHA256Digest()), + new HMac(new SHA256Digest()), + new SHA256Digest(), + new BufferedBlockCipher(new SICBlockCipher(aesFastEngine))); + + + byte[] d = new byte[] {}; + byte[] e = new byte[] {}; + + IESParameters p = new IESWithCipherParameters(d, e, 256, MAC_KEY_SIZE); + ParametersWithIV parametersWithIV = new ParametersWithIV(p, new byte[256/8]); + + ECKeyPairGenerator eGen = new ECKeyPairGenerator(); + SecureRandom random = new SecureRandom(); + KeyGenerationParameters gParam = new ECKeyGenerationParameters(ECKey.CURVE, random); + + eGen.init(gParam); + + + AsymmetricCipherKeyPair p1 = eGen.generateKeyPair(); + AsymmetricCipherKeyPair p2 = eGen.generateKeyPair(); + + + ECKeyGenerationParameters keygenParams = new ECKeyGenerationParameters(ECKey.CURVE, random); + ECKeyPairGenerator generator = new ECKeyPairGenerator(); + generator.init(keygenParams); + + ECKeyPairGenerator gen = new ECKeyPairGenerator(); + gen.init(new ECKeyGenerationParameters(ECKey.CURVE, random)); + + iesEngine.init(true, p1.getPrivate(), p2.getPublic(), parametersWithIV); + + byte[] cipher = iesEngine.processBlock(plaintext, 0, plaintext.length); + return cipher; + } +}