/*
* Copyright (c) [2016] [ <ether.camp> ]
* This file is part of the ethereumJ library.
*
* The ethereumJ library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The ethereumJ library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>.
*/
package org.ethereum.net.rlpx;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import org.ethereum.crypto.ECIESCoder;
import org.ethereum.crypto.ECKey;
import org.ethereum.util.ByteUtil;
import org.spongycastle.crypto.InvalidCipherTextException;
import org.spongycastle.crypto.digests.KeccakDigest;
import org.spongycastle.math.ec.ECPoint;
import javax.annotation.Nullable;
import java.io.IOException;
import java.math.BigInteger;
import java.security.SecureRandom;
import static org.ethereum.crypto.HashUtil.sha3;
/**
* Created by devrandom on 2015-04-08.
*/
public class EncryptionHandshake {
public static final int NONCE_SIZE = 32;
public static final int MAC_SIZE = 256;
public static final int SECRET_SIZE = 32;
private SecureRandom random = new SecureRandom();
private boolean isInitiator;
private ECKey ephemeralKey;
private ECPoint remotePublicKey;
private ECPoint remoteEphemeralKey;
private byte[] initiatorNonce;
private byte[] responderNonce;
private Secrets secrets;
public EncryptionHandshake(ECPoint remotePublicKey) {
this.remotePublicKey = remotePublicKey;
ephemeralKey = new ECKey(random);
initiatorNonce = new byte[NONCE_SIZE];
random.nextBytes(initiatorNonce);
isInitiator = true;
}
EncryptionHandshake(ECPoint remotePublicKey, ECKey ephemeralKey, byte[] initiatorNonce, byte[] responderNonce, boolean isInitiator) {
this.remotePublicKey = remotePublicKey;
this.ephemeralKey = ephemeralKey;
this.initiatorNonce = initiatorNonce;
this.responderNonce = responderNonce;
this.isInitiator = isInitiator;
}
public EncryptionHandshake() {
ephemeralKey = new ECKey(random);
responderNonce = new byte[NONCE_SIZE];
random.nextBytes(responderNonce);
isInitiator = false;
}
/**
* Create a handshake auth message defined by EIP-8
*
* @param key our private key
*/
public AuthInitiateMessageV4 createAuthInitiateV4(ECKey key) {
AuthInitiateMessageV4 message = new AuthInitiateMessageV4();
BigInteger secretScalar = key.keyAgreement(remotePublicKey);
byte[] token = ByteUtil.bigIntegerToBytes(secretScalar, NONCE_SIZE);
byte[] nonce = initiatorNonce;
byte[] signed = xor(token, nonce);
message.signature = ephemeralKey.sign(signed);
message.publicKey = key.getPubKeyPoint();
message.nonce = initiatorNonce;
return message;
}
public byte[] encryptAuthInitiateV4(AuthInitiateMessageV4 message) {
byte[] msg = message.encode();
byte[] padded = padEip8(msg);
return encryptAuthEIP8(padded);
}
public AuthInitiateMessageV4 decryptAuthInitiateV4(byte[] in, ECKey myKey) throws InvalidCipherTextException {
try {
byte[] prefix = new byte[2];
System.arraycopy(in, 0, prefix, 0, 2);
short size = ByteUtil.bigEndianToShort(prefix, 0);
byte[] ciphertext = new byte[size];
System.arraycopy(in, 2, ciphertext, 0, size);
byte[] plaintext = ECIESCoder.decrypt(myKey.getPrivKey(), ciphertext, prefix);
return AuthInitiateMessageV4.decode(plaintext);
} catch (InvalidCipherTextException e) {
throw e;
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
public byte[] encryptAuthResponseV4(AuthResponseMessageV4 message) {
byte[] msg = message.encode();
byte[] padded = padEip8(msg);
return encryptAuthEIP8(padded);
}
public AuthResponseMessageV4 decryptAuthResponseV4(byte[] in, ECKey myKey) {
try {
byte[] prefix = new byte[2];
System.arraycopy(in, 0, prefix, 0, 2);
short size = ByteUtil.bigEndianToShort(prefix, 0);
byte[] ciphertext = new byte[size];
System.arraycopy(in, 2, ciphertext, 0, size);
byte[] plaintext = ECIESCoder.decrypt(myKey.getPrivKey(), ciphertext, prefix);
return AuthResponseMessageV4.decode(plaintext);
} catch (IOException | InvalidCipherTextException e) {
throw Throwables.propagate(e);
}
}
AuthResponseMessageV4 makeAuthInitiateV4(AuthInitiateMessageV4 initiate, ECKey key) {
initiatorNonce = initiate.nonce;
remotePublicKey = initiate.publicKey;
BigInteger secretScalar = key.keyAgreement(remotePublicKey);
byte[] token = ByteUtil.bigIntegerToBytes(secretScalar, NONCE_SIZE);
byte[] signed = xor(token, initiatorNonce);
ECKey ephemeral = ECKey.recoverFromSignature(recIdFromSignatureV(initiate.signature.v),
initiate.signature, signed);
if (ephemeral == null) {
throw new RuntimeException("failed to recover signatue from message");
}
remoteEphemeralKey = ephemeral.getPubKeyPoint();
AuthResponseMessageV4 response = new AuthResponseMessageV4();
response.ephemeralPublicKey = ephemeralKey.getPubKeyPoint();
response.nonce = responderNonce;
return response;
}
public AuthResponseMessageV4 handleAuthResponseV4(ECKey myKey, byte[] initiatePacket, byte[] responsePacket) {
AuthResponseMessageV4 response = decryptAuthResponseV4(responsePacket, myKey);
remoteEphemeralKey = response.ephemeralPublicKey;
responderNonce = response.nonce;
agreeSecret(initiatePacket, responsePacket);
return response;
}
byte[] encryptAuthEIP8(byte[] msg) {
short size = (short) (msg.length + ECIESCoder.getOverhead());
byte[] prefix = ByteUtil.shortToBytes(size);
byte[] encrypted = ECIESCoder.encrypt(remotePublicKey, msg, prefix);
byte[] out = new byte[prefix.length + encrypted.length];
int offset = 0;
System.arraycopy(prefix, 0, out, offset, prefix.length);
offset += prefix.length;
System.arraycopy(encrypted, 0, out, offset, encrypted.length);
return out;
}
/**
* Create a handshake auth message
*
* @param token previous token if we had a previous session
* @param key our private key
*/
public AuthInitiateMessage createAuthInitiate(@Nullable byte[] token, ECKey key) {
AuthInitiateMessage message = new AuthInitiateMessage();
boolean isToken;
if (token == null) {
isToken = false;
BigInteger secretScalar = key.keyAgreement(remotePublicKey);
token = ByteUtil.bigIntegerToBytes(secretScalar, NONCE_SIZE);
} else {
isToken = true;
}
byte[] nonce = initiatorNonce;
byte[] signed = xor(token, nonce);
message.signature = ephemeralKey.sign(signed);
message.isTokenUsed = isToken;
message.ephemeralPublicHash = sha3(ephemeralKey.getPubKey(), 1, 64);
message.publicKey = key.getPubKeyPoint();
message.nonce = initiatorNonce;
return message;
}
private static byte[] xor(byte[] b1, byte[] b2) {
Preconditions.checkArgument(b1.length == b2.length);
byte[] out = new byte[b1.length];
for (int i = 0; i < b1.length; i++) {
out[i] = (byte) (b1[i] ^ b2[i]);
}
return out;
}
public byte[] encryptAuthMessage(AuthInitiateMessage message) {
return ECIESCoder.encrypt(remotePublicKey, message.encode());
}
public byte[] encryptAuthResponse(AuthResponseMessage message) {
return ECIESCoder.encrypt(remotePublicKey, message.encode());
}
public AuthResponseMessage decryptAuthResponse(byte[] ciphertext, ECKey myKey) {
try {
byte[] plaintext = ECIESCoder.decrypt(myKey.getPrivKey(), ciphertext);
return AuthResponseMessage.decode(plaintext);
} catch (IOException | InvalidCipherTextException e) {
throw Throwables.propagate(e);
}
}
public AuthInitiateMessage decryptAuthInitiate(byte[] ciphertext, ECKey myKey) throws InvalidCipherTextException {
try {
byte[] plaintext = ECIESCoder.decrypt(myKey.getPrivKey(), ciphertext);
return AuthInitiateMessage.decode(plaintext);
} catch (InvalidCipherTextException e) {
throw e;
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
public AuthResponseMessage handleAuthResponse(ECKey myKey, byte[] initiatePacket, byte[] responsePacket) {
AuthResponseMessage response = decryptAuthResponse(responsePacket, myKey);
remoteEphemeralKey = response.ephemeralPublicKey;
responderNonce = response.nonce;
agreeSecret(initiatePacket, responsePacket);
return response;
}
void agreeSecret(byte[] initiatePacket, byte[] responsePacket) {
BigInteger secretScalar = ephemeralKey.keyAgreement(remoteEphemeralKey);
byte[] agreedSecret = ByteUtil.bigIntegerToBytes(secretScalar, SECRET_SIZE);
byte[] sharedSecret = sha3(agreedSecret, sha3(responderNonce, initiatorNonce));
byte[] aesSecret = sha3(agreedSecret, sharedSecret);
secrets = new Secrets();
secrets.aes = aesSecret;
secrets.mac = sha3(agreedSecret, aesSecret);
secrets.token = sha3(sharedSecret);
// System.out.println("mac " + Hex.toHexString(secrets.mac));
// System.out.println("aes " + Hex.toHexString(secrets.aes));
// System.out.println("shared " + Hex.toHexString(sharedSecret));
// System.out.println("ecdhe " + Hex.toHexString(agreedSecret));
KeccakDigest mac1 = new KeccakDigest(MAC_SIZE);
mac1.update(xor(secrets.mac, responderNonce), 0, secrets.mac.length);
byte[] buf = new byte[32];
new KeccakDigest(mac1).doFinal(buf, 0);
mac1.update(initiatePacket, 0, initiatePacket.length);
new KeccakDigest(mac1).doFinal(buf, 0);
KeccakDigest mac2 = new KeccakDigest(MAC_SIZE);
mac2.update(xor(secrets.mac, initiatorNonce), 0, secrets.mac.length);
new KeccakDigest(mac2).doFinal(buf, 0);
mac2.update(responsePacket, 0, responsePacket.length);
new KeccakDigest(mac2).doFinal(buf, 0);
if (isInitiator) {
secrets.egressMac = mac1;
secrets.ingressMac = mac2;
} else {
secrets.egressMac = mac2;
secrets.ingressMac = mac1;
}
}
public byte[] handleAuthInitiate(byte[] initiatePacket, ECKey key) throws InvalidCipherTextException {
AuthResponseMessage response = makeAuthInitiate(initiatePacket, key);
byte[] responsePacket = encryptAuthResponse(response);
agreeSecret(initiatePacket, responsePacket);
return responsePacket;
}
AuthResponseMessage makeAuthInitiate(byte[] initiatePacket, ECKey key) throws InvalidCipherTextException {
AuthInitiateMessage initiate = decryptAuthInitiate(initiatePacket, key);
return makeAuthInitiate(initiate, key);
}
AuthResponseMessage makeAuthInitiate(AuthInitiateMessage initiate, ECKey key) {
initiatorNonce = initiate.nonce;
remotePublicKey = initiate.publicKey;
BigInteger secretScalar = key.keyAgreement(remotePublicKey);
byte[] token = ByteUtil.bigIntegerToBytes(secretScalar, NONCE_SIZE);
byte[] signed = xor(token, initiatorNonce);
ECKey ephemeral = ECKey.recoverFromSignature(recIdFromSignatureV(initiate.signature.v),
initiate.signature, signed);
if (ephemeral == null) {
throw new RuntimeException("failed to recover signatue from message");
}
remoteEphemeralKey = ephemeral.getPubKeyPoint();
AuthResponseMessage response = new AuthResponseMessage();
response.isTokenUsed = initiate.isTokenUsed;
response.ephemeralPublicKey = ephemeralKey.getPubKeyPoint();
response.nonce = responderNonce;
return response;
}
/**
* Pads messages with junk data,
* pad data length is random value satisfying 100 < len < 300.
* It's necessary to make messages described by EIP-8 distinguishable from pre-EIP-8 msgs
*
* @param msg message to pad
* @return padded message
*/
private byte[] padEip8(byte[] msg) {
byte[] paddedMessage = new byte[msg.length + random.nextInt(200) + 100];
random.nextBytes(paddedMessage);
System.arraycopy(msg, 0, paddedMessage, 0, msg.length);
return paddedMessage;
}
static public byte recIdFromSignatureV(int v) {
if (v >= 31) {
// compressed
v -= 4;
}
return (byte)(v - 27);
}
public Secrets getSecrets() {
return secrets;
}
public ECPoint getRemotePublicKey() {
return remotePublicKey;
}
public static class Secrets {
byte[] aes;
byte[] mac;
byte[] token;
KeccakDigest egressMac;
KeccakDigest ingressMac;
public byte[] getAes() {
return aes;
}
public byte[] getMac() {
return mac;
}
public byte[] getToken() {
return token;
}
public KeccakDigest getIngressMac() {
return ingressMac;
}
public KeccakDigest getEgressMac() {
return egressMac;
}
}
public boolean isInitiator() {
return isInitiator;
}
}