HandshakeSecrets.java
/*
* Copyright ConsenSys AG.
*
* Licensed 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.p2p.rlpx.handshake;
import static com.google.common.base.Preconditions.checkArgument;
import java.util.Arrays;
import java.util.Objects;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.bouncycastle.crypto.digests.KeccakDigest;
/**
* Encapsulates the secrets generated during the RLPx crypto handshake, and offers a facility for
* updating some values as messages are exchanged during the lifetime of the connection.
*
* <p>The following secret materials are modelled:
*
* <ul>
* <li><strong>AES secret:</strong> shared secret used to cipher and decipher message payloads.
* <li><strong>MAC secret:</strong> shared secret used to update ingress and egress MACs as
* messages are exchanged.
* <li><strong>Token:</strong> identifies this session, currently unused.
* <li><strong>Ingress MAC:</strong> continuously-updating MAC for received bytes.
* <li><strong>Egress MAC:</strong> continuously-updating MAC for sent bytes.
* </ul>
*
* @see <a href="https://github.com/ethereum/devp2p/blob/master/rlpx.md#encrypted-handshake">RLPx
* Encrypted Handshake</a>
*/
public class HandshakeSecrets {
private final byte[] aesSecret;
private final byte[] macSecret;
private final byte[] token;
private final KeccakDigest egressMac = new KeccakDigest(Bytes32.SIZE * 8);
private final KeccakDigest ingressMac = new KeccakDigest(Bytes32.SIZE * 8);
/**
* Creates an instance with empty MACs.
*
* @param aesSecret The AES shared secret.
* @param macSecret The MAC shared secret.
* @param token The session token.
*/
public HandshakeSecrets(final byte[] aesSecret, final byte[] macSecret, final byte[] token) {
checkArgument(aesSecret.length == Bytes32.SIZE, "aes secret must be exactly 32 bytes long");
checkArgument(macSecret.length == Bytes32.SIZE, "mac secret must be exactly 32 bytes long");
checkArgument(token.length == Bytes32.SIZE, "token must be exactly 32 bytes long");
this.aesSecret = aesSecret;
this.macSecret = macSecret;
this.token = token;
}
/**
* Updates the egress mac with the provided bytes.
*
* @param bytes The bytes of the outgoing message.
* @return Returns this instance for fluent chaining.
*/
public HandshakeSecrets updateEgress(final byte[] bytes) {
egressMac.update(bytes, 0, bytes.length);
return this;
}
/**
* Updates the ingress mac with the provided bytes.
*
* @param bytes The bytes of the incoming message.
* @return Returns this instance for fluent chaining.
*/
public HandshakeSecrets updateIngress(final byte[] bytes) {
ingressMac.update(bytes, 0, bytes.length);
return this;
}
/**
* Returns the AES shared secret.
*
* @return The AES shared secret.
*/
public byte[] getAesSecret() {
return aesSecret;
}
/**
* Returns the MAC shared secret.
*
* @return The MAC shared secret.
*/
public byte[] getMacSecret() {
return macSecret;
}
/**
* Returns the token that identifies a session (unused).
*
* @return The token.
*/
public byte[] getToken() {
return token;
}
/**
* Returns a snapshot of the current egress MAC, without finalising the underlying digest.
*
* @return Snapshot of the current egress MAC.
*/
public byte[] getEgressMac() {
return snapshot(egressMac);
}
/**
* Returns a snapshot of the current ingress MAC, without finalising the underlying digest.
*
* @return Snapshot of the current ingress MAC.
*/
public byte[] getIngressMac() {
return snapshot(ingressMac);
}
/**
* TODO: It's not wise to print secrets. Maybe print only the first and last 8 bytes (ellipsize
* the middle). That might be enough for testing.
*/
@Override
public String toString() {
return "HandshakeSecrets{"
+ "aesSecret="
+ Bytes.wrap(aesSecret)
+ ", macSecret="
+ Bytes.wrap(macSecret)
+ ", token="
+ Bytes.wrap(token)
+ ", egressMac="
+ Bytes.wrap(snapshot(egressMac))
+ ", ingressMac="
+ Bytes.wrap(snapshot(ingressMac))
+ '}';
}
private static byte[] snapshot(final KeccakDigest digest) {
final byte[] out = new byte[Bytes32.SIZE];
new KeccakDigest(digest).doFinal(out, 0);
return out;
}
@SuppressWarnings("EqualsWhichDoesntCheckParameterClass") // checked in delegated method
@Override
public boolean equals(final Object obj) {
return equals(obj, false);
}
/**
* Performs an equals comparison with the ability to flip the MAC comparison, catering for
* scenarios where we want to compare the handshake secrets on opposing ends of a channel.
*
* @param o The object whose equality to test with this.
* @param flipMacs Whether the egress MAC should be compared against the ingress MAC, and
* viceversa.
* @return Whether both objects are equal or not.
*/
public boolean equals(final Object o, final boolean flipMacs) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final HandshakeSecrets that = (HandshakeSecrets) o;
final KeccakDigest vsEgress = flipMacs ? that.ingressMac : that.egressMac;
final KeccakDigest vsIngress = flipMacs ? that.egressMac : that.ingressMac;
return Arrays.equals(aesSecret, that.aesSecret)
&& Arrays.equals(macSecret, that.macSecret)
&& Arrays.equals(token, that.token)
&& Arrays.equals(snapshot(egressMac), snapshot(vsEgress))
&& Arrays.equals(snapshot(ingressMac), snapshot(vsIngress));
}
@Override
public int hashCode() {
return Objects.hash(
Arrays.hashCode(aesSecret),
Arrays.hashCode(macSecret),
Arrays.hashCode(token),
egressMac,
ingressMac);
}
}