blob: 8bc9e467ee0310f7ec0903a70cdbb3c9f86f90cc [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.scuttlebutt.handshake;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.apache.tuweni.crypto.sodium.Allocated;
import org.apache.tuweni.crypto.sodium.Box;
import org.apache.tuweni.crypto.sodium.Concatenate;
import org.apache.tuweni.crypto.sodium.DiffieHelman;
import org.apache.tuweni.crypto.sodium.HMACSHA512256;
import org.apache.tuweni.crypto.sodium.SHA256Hash;
import org.apache.tuweni.crypto.sodium.SecretBox;
import org.apache.tuweni.crypto.sodium.Signature;
import org.apache.tuweni.scuttlebutt.Identity;
import org.apache.tuweni.scuttlebutt.Invite;
/**
* Class responsible for performing a Secure Scuttlebutt handshake with a remote peer, as defined in the
* <a href="https://ssbc.github.io/scuttlebutt-protocol-guide/">Secure Scuttlebutt protocol guide</a>
* <p>
* Please note that only handshakes over the Ed25519 curve are supported.
* <p>
* This class manages the state of one handshake. It should not be reused across handshakes.
*
* If the handshake fails, a HandshakeException will be thrown.
*/
public final class SecureScuttlebuttHandshakeClient {
private final Signature.KeyPair longTermKeyPair;
private final Box.KeyPair ephemeralKeyPair;
private final HMACSHA512256.Key networkIdentifier;
private final Signature.PublicKey serverLongTermPublicKey;
private Box.PublicKey serverEphemeralPublicKey;
private DiffieHelman.Secret sharedSecret;
private DiffieHelman.Secret sharedSecret2;
private DiffieHelman.Secret sharedSecret3;
private Allocated detachedSignature;
/**
* Creates a new handshake client to connect to a specific remote peer.
*
* @param ourKeyPair our long term key pair
* @param networkIdentifier the network identifier
* @param serverLongTermPublicKey the server long term public key
* @return a new Secure Scuttlebutt handshake client
*/
public static SecureScuttlebuttHandshakeClient create(
Signature.KeyPair ourKeyPair,
Bytes32 networkIdentifier,
Signature.PublicKey serverLongTermPublicKey) {
return new SecureScuttlebuttHandshakeClient(ourKeyPair, networkIdentifier, serverLongTermPublicKey);
}
/**
* Create a new handshake client to connect to the server specified in the invite
*
* @param networkIdentifier the networkIdentifier
* @param invite the invite
* @return a new Secure Scuttlebutt handshake client
*/
public static SecureScuttlebuttHandshakeClient fromInvite(Bytes32 networkIdentifier, Invite invite) {
if (!Identity.Curve.Ed25519.equals(invite.identity().curve())) {
throw new IllegalArgumentException("Only ed25519 keys are supported");
}
return new SecureScuttlebuttHandshakeClient(
Signature.KeyPair.forSecretKey(invite.secretKey()),
networkIdentifier,
invite.identity().ed25519PublicKey());
}
private SecureScuttlebuttHandshakeClient(
Signature.KeyPair longTermKeyPair,
Bytes32 networkIdentifier,
Signature.PublicKey serverLongTermPublicKey) {
this.longTermKeyPair = longTermKeyPair;
this.ephemeralKeyPair = Box.KeyPair.random();
this.networkIdentifier = HMACSHA512256.Key.fromBytes(networkIdentifier);
this.serverLongTermPublicKey = serverLongTermPublicKey;
}
/**
* Creates a hello message to be sent to the other party, comprised of our ephemeral public key and an authenticator
* against our network identifier.
*
* @return the hello message, ready to be sent to the remote server
*/
public Bytes createHello() {
Bytes hmac = HMACSHA512256.authenticate(ephemeralKeyPair.publicKey().bytes(), networkIdentifier);
return Bytes.concatenate(hmac, ephemeralKeyPair.publicKey().bytes());
}
/**
* Validates the initial message's MAC with our network identifier, and returns the peer ephemeral public key.
*
* @param message initial handshake message
*/
public void readHello(Bytes message) {
if (message.size() != 64) {
throw new HandshakeException("Invalid handshake message length: " + message.size());
}
Bytes hmac = message.slice(0, 32);
Bytes key = message.slice(32, 32);
if (!HMACSHA512256.verify(hmac, key, networkIdentifier)) {
throw new HandshakeException("MAC does not match our network identifier");
}
this.serverEphemeralPublicKey = Box.PublicKey.fromBytes(key);
this.sharedSecret = DiffieHelman.Secret.forKeys(
DiffieHelman.SecretKey.forBoxSecretKey(ephemeralKeyPair.secretKey()),
DiffieHelman.PublicKey.forBoxPublicKey(serverEphemeralPublicKey));
this.sharedSecret2 = DiffieHelman.Secret.forKeys(
DiffieHelman.SecretKey.forBoxSecretKey(ephemeralKeyPair.secretKey()),
DiffieHelman.PublicKey.forSignaturePublicKey(serverLongTermPublicKey));
this.sharedSecret3 = DiffieHelman.Secret.forKeys(
DiffieHelman.SecretKey.forSignatureSecretKey(longTermKeyPair.secretKey()),
DiffieHelman.PublicKey.forBoxPublicKey(serverEphemeralPublicKey));
}
DiffieHelman.Secret sharedSecret() {
return sharedSecret;
}
DiffieHelman.Secret sharedSecret2() {
return sharedSecret2;
}
DiffieHelman.Secret sharedSecret3() {
return sharedSecret3;
}
/**
* Creates a message containing the identity of the client
*
* @return a message containing the identity of the client
*/
public Bytes createIdentityMessage() {
Concatenate concatenate = new Concatenate();
concatenate.add(networkIdentifier);
concatenate.add(serverLongTermPublicKey);
concatenate.add(SHA256Hash.hash(SHA256Hash.Input.fromSecret(sharedSecret)));
this.detachedSignature = Signature.signDetached(concatenate.concatenate(), longTermKeyPair.secretKey());
return SecretBox
.encrypt(
new Concatenate().add(detachedSignature).add(longTermKeyPair.publicKey()).concatenate(),
SecretBox.Key.fromHash(
SHA256Hash.hash(
SHA256Hash.Input.fromPointer(
new Concatenate().add(networkIdentifier).add(sharedSecret).add(sharedSecret2).concatenate()))),
SecretBox.Nonce.fromBytes(new byte[24]))
.bytes();
}
/**
* Reads the handshake acceptance message from the server
*
* @param message the message of acceptance of the handshake from the server
*/
public void readAcceptMessage(Bytes message) {
Allocated serverSignature = SecretBox.decrypt(
Allocated.fromBytes(message),
SecretBox.Key.fromHash(
SHA256Hash.hash(
SHA256Hash.Input.fromPointer(
new Concatenate()
.add(networkIdentifier)
.add(sharedSecret)
.add(sharedSecret2)
.add(sharedSecret3)
.concatenate()))),
SecretBox.Nonce.fromBytes(new byte[24]));
if (serverSignature == null) {
throw new HandshakeException("Could not decrypt accept message with our shared secrets");
}
boolean verified = serverLongTermPublicKey.verify(
new Concatenate()
.add(networkIdentifier)
.add(detachedSignature)
.add(longTermKeyPair.publicKey())
.add(SHA256Hash.hash(SHA256Hash.Input.fromSecret(sharedSecret)))
.concatenate(),
serverSignature);
if (!verified) {
throw new HandshakeException("Accept message signature does not match");
}
}
/**
* If the handshake completed successfully, this provides the secret box key to use to send messages to the server
* going forward.
*
* @return a new secret box key for use with encrypting messages to the server.
*/
SHA256Hash.Hash clientToServerSecretBoxKey() {
return SHA256Hash.hash(
SHA256Hash.Input.fromPointer(
new Concatenate()
.add(
SHA256Hash.hash(
SHA256Hash.Input.fromHash(
SHA256Hash.hash(
SHA256Hash.Input.fromPointer(
new Concatenate()
.add(networkIdentifier)
.add(sharedSecret)
.add(sharedSecret2)
.add(sharedSecret3)
.concatenate())))))
.add(serverLongTermPublicKey)
.concatenate()));
}
/**
* If the handshake completed successfully, this provides the clientToServerNonce to use to send messages to the
* server going forward.
*
* @return a clientToServerNonce for use with encrypting messages to the server.
*/
Bytes clientToServerNonce() {
return HMACSHA512256.authenticate(serverEphemeralPublicKey.bytes(), networkIdentifier).slice(0, 24);
}
/**
* If the handshake completed successfully, this provides the secret box key to use to receive messages from the
* server going forward.
*
* @return a new secret box key for use with decrypting messages from the server.
*/
SHA256Hash.Hash serverToClientSecretBoxKey() {
return SHA256Hash.hash(
SHA256Hash.Input.fromPointer(
new Concatenate()
.add(
SHA256Hash.hash(
SHA256Hash.Input.fromHash(
SHA256Hash.hash(
SHA256Hash.Input.fromPointer(
new Concatenate()
.add(networkIdentifier)
.add(sharedSecret)
.add(sharedSecret2)
.add(sharedSecret3)
.concatenate())))))
.add(longTermKeyPair.publicKey())
.concatenate()));
}
/**
* If the handshake completed successfully, this provides the clientToServerNonce to use to receive messages from the
* server going forward.
*
* @return a clientToServerNonce for use with decrypting messages from the server.
*/
Bytes serverToClientNonce() {
return HMACSHA512256.authenticate(ephemeralKeyPair.publicKey().bytes(), networkIdentifier).slice(0, 24);
}
/**
* Creates a stream to allow communication with the other peer after the handshake has completed
*
* @return a new stream for encrypted communications with the peer
*/
public SecureScuttlebuttStreamClient createStream() {
return new SecureScuttlebuttStream(
clientToServerSecretBoxKey(),
clientToServerNonce(),
serverToClientSecretBoxKey(),
serverToClientNonce());
}
}