blob: 20c59cfd06da86afa97e52c89db42641c2cdeb7a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
* file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
* to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.apache.tuweni.rlpx;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import org.bouncycastle.crypto.*;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.generators.EphemeralKeyPairGenerator;
import org.bouncycastle.crypto.params.*;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.BigIntegers;
import org.bouncycastle.util.Pack;
/**
* Support class for constructing integrated encryption ciphers for doing basic message exchanges on top of key
* agreement ciphers. Follows the description given in IEEE Std 1363a.
*/
public class EthereumIESEncryptionEngine {
BasicAgreement agree;
DerivationFunction kdf;
Mac mac;
BufferedBlockCipher cipher;
byte[] macBuf;
// Ethereum addition: commonMac added when performing the MAC encryption.
byte[] commonMac;
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 commonMac the common MAC bytes to append to the mac
*/
public EthereumIESEncryptionEngine(BasicAgreement agree, DerivationFunction kdf, Mac mac, byte[] commonMac) {
this.agree = agree;
this.kdf = kdf;
this.mac = mac;
this.macBuf = new byte[mac.getMacSize()];
this.commonMac = commonMac;
this.cipher = null;
}
/**
* Set up for use in conjunction with a block cipher to handle the message. It is <b>strongly</b> recommended that the
* cipher is not in ECB mode.
*
* @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 commonMac the common MAC bytes to append to the mac
* @param cipher the cipher to used for encrypting the message
*/
public EthereumIESEncryptionEngine(
BasicAgreement agree,
DerivationFunction kdf,
Mac mac,
byte[] commonMac,
BufferedBlockCipher cipher) {
this.agree = agree;
this.kdf = kdf;
this.mac = mac;
this.macBuf = new byte[mac.getMacSize()];
this.commonMac = commonMac;
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 decryptor.
*
* @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 = null;
if (V.length != 0) {
L2 = getLengthTag(P2);
}
// Apply the MAC.
byte[] T = new byte[mac.getMacSize()];
// Ethereum change:
// Instead of initializing the mac with the bytes, we initialize with the hash of the bytes.
// Old code: mac.init(new KeyParameter(K2));
Digest hash = new SHA256Digest();
byte[] K2hash = new byte[hash.getDigestSize()];
hash.reset();
hash.update(K2, 0, K2.length);
hash.doFinal(K2hash, 0);
mac.init(new KeyParameter(K2hash));
// we also update the mac with the IV:
mac.update(IV, 0, IV.length);
// end of Ethereum change.
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.update(commonMac, 0, commonMac.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, K, K1, K2;
int len = 0;
// Ensure that the length of the input is greater than the MAC in bytes
if (inLen < V.length + mac.getMacSize()) {
throw new InvalidCipherTextException("Length of input must be greater than the MAC and V combined");
}
// note order is important: set up keys, do simple encryptions, check mac, do final encryption.
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);
}
// process the message
M = new byte[K1.length];
for (int i = 0; i != K1.length; i++) {
M[i] = (byte) (in_enc[inOff + V.length + i] ^ K1[i]);
}
} 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);
CipherParameters cp = new KeyParameter(K1);
// If IV provide use it to initialize the cipher
if (IV != null) {
cp = new ParametersWithIV(cp, IV);
}
cipher.init(false, cp);
M = new byte[cipher.getOutputSize(inLen - V.length - mac.getMacSize())];
// do initial processing
len = cipher.processBytes(in_enc, inOff + V.length, inLen - V.length - mac.getMacSize(), M, 0);
}
// Convert the length of the encoding vector into a byte array.
byte[] P2 = param.getEncodingV();
byte[] L2 = null;
if (V.length != 0) {
L2 = getLengthTag(P2);
}
// Verify the MAC.
int end = inOff + inLen;
byte[] T1 = Arrays.copyOfRange(in_enc, end - mac.getMacSize(), end);
byte[] T2 = new byte[T1.length];
// Ethereum change:
// Instead of initializing the mac with the bytes, we initialize with the hash of the bytes.
// Old code: mac.init(new KeyParameter(K2));
Digest hash = new SHA256Digest();
byte[] K2hash = new byte[hash.getDigestSize()];
hash.reset();
hash.update(K2, 0, K2.length);
hash.doFinal(K2hash, 0);
mac.init(new KeyParameter(K2hash));
// we also update the mac with the IV:
mac.update(IV, 0, IV.length);
// end of Ethereum change.
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.update(commonMac, 0, commonMac.length);
mac.doFinal(T2, 0);
if (!Arrays.constantTimeAreEqual(T1, T2)) {
throw new InvalidCipherTextException("invalid MAC");
}
if (cipher == null) {
return M;
} else {
len += cipher.doFinal(M, len);
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);
} catch (IllegalArgumentException 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.
if (V.length != 0) {
byte[] VZ = Arrays.concatenate(V, Z);
Arrays.fill(Z, (byte) 0);
Z = VZ;
}
try {
// Initialise the KDF.
KDFParameters kdfParam = new KDFParameters(Z, param.getDerivationV());
kdf.init(kdfParam);
return forEncryption ? encryptBlock(in, inOff, inLen) : decryptBlock(in, inOff, inLen);
} finally {
Arrays.fill(Z, (byte) 0);
}
}
// as described in Shroup's paper and P1363a
protected byte[] getLengthTag(byte[] p2) {
byte[] L2 = new byte[8];
if (p2 != null) {
Pack.longToBigEndian(p2.length * 8L, L2, 0);
}
return L2;
}
/**
* Basic KDF generator for derived keys and ivs as defined by IEEE P1363a/ISO 18033 <br>
* This implementation is based on ISO 18033/P1363a.
* <p>
* This class has been adapted from the <tt>BaseKDFBytesGenerator</tt> implementation of Bouncy Castle. Only one
* change is present specifically for Ethereum.
*/
static class ECIESHandshakeKDFFunction implements DigestDerivationFunction {
private int counterStart;
private Digest digest;
private byte[] shared;
private byte[] iv;
/**
* Construct a KDF Parameters generator.
* <p>
*
* @param counterStart value of counter.
* @param digest the digest to be used as the source of derived keys.
*/
protected ECIESHandshakeKDFFunction(int counterStart, Digest digest) {
this.counterStart = counterStart;
this.digest = digest;
}
@Override
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 generator");
}
}
/**
* return the underlying digest.
*/
@Override
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.
*/
@Override
public int generateBytes(byte[] out, int outOff, int len) throws DataLengthException, IllegalArgumentException {
if ((out.length - len) < outOff) {
throw new OutputLengthException("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++) {
// only change for Ethereum: invert those 2 lines.
digest.update(C, 0, C.length);
digest.update(shared, 0, shared.length);
// End of change for Ethereum.
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;
}
}
}