blob: ae43d90103825d9d209a898cb075df29109d6555 [file] [log] [blame]
/* Copyright 2018 Google LLC
*
* 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
*
* https://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 com.google.security.cryptauth.lib.securegcm;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.DeviceToDeviceMessage;
import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.InitiatorHello;
import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.ResponderHello;
import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.Payload;
import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.PayloadType;
import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.SecretKey;
/**
* Implements an unauthenticated EC Diffie Hellman Key Exchange Handshake
* <p>
* Initiator sends an InitiatorHello, which is a protobuf that contains a public key. Responder
* sends a responder hello, which a signed and encrypted message containing a payload, and a public
* key in the unencrypted header (payload is encrypted with the derived DH key).
* <p>
* Example Usage:
* <pre>
* // initiator:
* D2DHandshakeContext initiatorHandshakeContext =
* D2DDiffieHellmanKeyExchangeHandshake.forInitiator();
* byte[] initiatorHello = initiatorHandshakeContext.getNextHandshakeMessage();
* // (send initiatorHello to responder)
*
* // responder:
* D2DHandshakeContext responderHandshakeContext =
* D2DDiffieHellmanKeyExchangeHandshake.forResponder();
* responderHandshakeContext.parseHandshakeMessage(initiatorHello);
* byte[] responderHelloAndPayload = responderHandshakeContext.getNextHandshakeMessage(
* toBytes(RESPONDER_HELLO_MESSAGE));
* D2DConnectionContext responderCtx = responderHandshakeContext.toConnectionContext();
* // (send responderHelloAndPayload to initiator)
*
* // initiator
* byte[] messageFromPayload =
* initiatorHandshakeContext.parseHandshakeMessage(responderHelloAndPayload);
* if (messageFromPayload.length > 0) {
* handle(messageFromPayload);
* }
*
* D2DConnectionContext initiatorCtx = initiatorHandshakeContext.toConnectionContext();
* </pre>
*/
public class D2DDiffieHellmanKeyExchangeHandshake implements D2DHandshakeContext {
// Data passed to hkdf to create the key used by the initiator to encode messages.
private static final String INITIATOR_PURPOSE = "initiator";
// Data passed to hkdf to create the key used by the responder to encode messages.
private static final String RESPONDER_PURPOSE = "responder";
private KeyPair ourKeyPair;
private PublicKey theirPublicKey;
private SecretKey initiatorEncodeKey;
private SecretKey responderEncodeKey;
private State handshakeState;
private boolean isInitiator;
private int protocolVersionToUse;
private enum State {
// Initiator state
INITIATOR_START,
INITIATOR_WAITING_FOR_RESPONDER_HELLO,
// Responder state
RESPONDER_START,
RESPONDER_AFTER_INITIATOR_HELLO,
// Common completion state
HANDSHAKE_FINISHED,
HANDSHAKE_ALREADY_USED
}
private D2DDiffieHellmanKeyExchangeHandshake(State state) {
ourKeyPair = PublicKeyProtoUtil.generateEcP256KeyPair();
theirPublicKey = null;
initiatorEncodeKey = null;
responderEncodeKey = null;
handshakeState = state;
isInitiator = state == State.INITIATOR_START;
protocolVersionToUse = D2DConnectionContextV1.PROTOCOL_VERSION;
}
/**
* Creates a new Diffie Hellman handshake context for the handshake initiator
*/
public static D2DDiffieHellmanKeyExchangeHandshake forInitiator() {
return new D2DDiffieHellmanKeyExchangeHandshake(State.INITIATOR_START);
}
/**
* Creates a new Diffie Hellman handshake context for the handshake responder
*/
public static D2DDiffieHellmanKeyExchangeHandshake forResponder() {
return new D2DDiffieHellmanKeyExchangeHandshake(State.RESPONDER_START);
}
@Override
public boolean isHandshakeComplete() {
return handshakeState == State.HANDSHAKE_FINISHED
|| handshakeState == State.HANDSHAKE_ALREADY_USED;
}
@Override
public byte[] getNextHandshakeMessage() throws HandshakeException {
switch(handshakeState) {
case INITIATOR_START:
handshakeState = State.INITIATOR_WAITING_FOR_RESPONDER_HELLO;
return InitiatorHello.newBuilder()
.setPublicDhKey(PublicKeyProtoUtil.encodePublicKey(ourKeyPair.getPublic()))
.setProtocolVersion(protocolVersionToUse)
.build()
.toByteArray();
case RESPONDER_AFTER_INITIATOR_HELLO:
byte[] responderHello = makeResponderHelloWithPayload(new byte[0]);
handshakeState = State.HANDSHAKE_FINISHED;
return responderHello;
default:
throw new HandshakeException("Cannot get next message in state: " + handshakeState);
}
}
@Override
public boolean canSendPayloadInHandshakeMessage() {
return handshakeState == State.RESPONDER_AFTER_INITIATOR_HELLO;
}
@Override
public byte[] getNextHandshakeMessage(byte[] payload) throws HandshakeException {
if (handshakeState != State.RESPONDER_AFTER_INITIATOR_HELLO) {
throw new HandshakeException(
"Cannot get next message with payload in state: " + handshakeState);
}
byte[] responderHello = makeResponderHelloWithPayload(payload);
handshakeState = State.HANDSHAKE_FINISHED;
return responderHello;
}
private byte[] makeResponderHelloWithPayload(byte[] payload) throws HandshakeException {
if (payload == null) {
throw new HandshakeException("Not expecting null payload");
}
try {
SecretKey masterKey =
EnrollmentCryptoOps.doKeyAgreement(ourKeyPair.getPrivate(), theirPublicKey);
// V0 uses the same key for encoding and decoding, but V1 uses separate keys.
switch (protocolVersionToUse) {
case D2DConnectionContextV0.PROTOCOL_VERSION:
initiatorEncodeKey = masterKey;
responderEncodeKey = masterKey;
break;
case D2DConnectionContextV1.PROTOCOL_VERSION:
initiatorEncodeKey = D2DCryptoOps.deriveNewKeyForPurpose(masterKey, INITIATOR_PURPOSE);
responderEncodeKey = D2DCryptoOps.deriveNewKeyForPurpose(masterKey, RESPONDER_PURPOSE);
break;
default:
throw new IllegalStateException("Unexpected protocol version: " + protocolVersionToUse);
}
DeviceToDeviceMessage deviceToDeviceMessage =
D2DConnectionContext.createDeviceToDeviceMessage(payload, 1 /* sequence number */);
return D2DCryptoOps.signcryptMessageAndResponderHello(
new Payload(PayloadType.DEVICE_TO_DEVICE_RESPONDER_HELLO_PAYLOAD,
deviceToDeviceMessage.toByteArray()),
responderEncodeKey,
ourKeyPair.getPublic(),
protocolVersionToUse);
} catch (InvalidKeyException|NoSuchAlgorithmException e) {
throw new HandshakeException(e);
}
}
@Override
public byte[] parseHandshakeMessage(byte[] handshakeMessage) throws HandshakeException {
if (handshakeMessage == null || handshakeMessage.length == 0) {
throw new HandshakeException("Handshake message too short");
}
switch(handshakeState) {
case INITIATOR_WAITING_FOR_RESPONDER_HELLO:
byte[] payload = parseResponderHello(handshakeMessage);
handshakeState = State.HANDSHAKE_FINISHED;
return payload;
case RESPONDER_START:
parseInitiatorHello(handshakeMessage);
handshakeState = State.RESPONDER_AFTER_INITIATOR_HELLO;
return new byte[0];
default:
throw new HandshakeException("Cannot parse message in state: " + handshakeState);
}
}
private byte[] parseResponderHello(byte[] responderHello) throws HandshakeException {
try {
ResponderHello responderHelloProto =
D2DCryptoOps.parseAndValidateResponderHello(responderHello);
// Downgrade to protocol version 0 if needed for backwards compatibility.
int protocolVersion = responderHelloProto.getProtocolVersion();
if (protocolVersion == D2DConnectionContextV0.PROTOCOL_VERSION) {
protocolVersionToUse = D2DConnectionContextV0.PROTOCOL_VERSION;
}
SecretKey masterKey = D2DCryptoOps.deriveSharedKeyFromGenericPublicKey(
ourKeyPair.getPrivate(), responderHelloProto.getPublicDhKey());
// V0 uses the same key for encoding and decoding, but V1 uses separate keys.
if (protocolVersionToUse == D2DConnectionContextV0.PROTOCOL_VERSION) {
initiatorEncodeKey = masterKey;
responderEncodeKey = masterKey;
} else {
initiatorEncodeKey = D2DCryptoOps.deriveNewKeyForPurpose(masterKey, INITIATOR_PURPOSE);
responderEncodeKey = D2DCryptoOps.deriveNewKeyForPurpose(masterKey, RESPONDER_PURPOSE);
}
DeviceToDeviceMessage message =
D2DCryptoOps.decryptResponderHelloMessage(responderEncodeKey, responderHello);
if (message.getSequenceNumber() != 1) {
throw new HandshakeException("Incorrect sequence number in responder hello");
}
return message.getMessage().toByteArray();
} catch (SignatureException | InvalidProtocolBufferException
| NoSuchAlgorithmException | InvalidKeyException e) {
throw new HandshakeException(e);
}
}
private void parseInitiatorHello(byte[] initiatorHello) throws HandshakeException {
try {
InitiatorHello initiatorHelloProto = InitiatorHello.parseFrom(initiatorHello);
if (!initiatorHelloProto.hasPublicDhKey()) {
throw new HandshakeException("Missing public key in initiator hello");
}
theirPublicKey = PublicKeyProtoUtil.parsePublicKey(initiatorHelloProto.getPublicDhKey());
// Downgrade to protocol version 0 if needed for backwards compatibility.
int protocolVersion = initiatorHelloProto.getProtocolVersion();
if (protocolVersion == D2DConnectionContextV0.PROTOCOL_VERSION) {
protocolVersionToUse = D2DConnectionContextV0.PROTOCOL_VERSION;
}
} catch (InvalidKeySpecException | InvalidProtocolBufferException e) {
throw new HandshakeException(e);
}
}
@Override
public D2DConnectionContext toConnectionContext() throws HandshakeException {
if (handshakeState == State.HANDSHAKE_ALREADY_USED) {
throw new HandshakeException("Cannot reuse handshake context; is has already been used");
}
if (!isHandshakeComplete()) {
throw new HandshakeException("Handshake is not complete; cannot create connection context");
}
handshakeState = State.HANDSHAKE_ALREADY_USED;
if (protocolVersionToUse == D2DConnectionContextV0.PROTOCOL_VERSION) {
// Both sides start with an initial sequence number of 1 because the last message of the
// handshake had an optional payload with sequence number 1. D2DConnectionContext remembers
// the last sequence number used by each side.
// Note: initiatorEncodeKey == responderEncodeKey
return new D2DConnectionContextV0(initiatorEncodeKey, 1 /** initialSequenceNumber */);
} else {
SecretKey encodeKey = isInitiator ? initiatorEncodeKey : responderEncodeKey;
SecretKey decodeKey = isInitiator ? responderEncodeKey : initiatorEncodeKey;
// Only the responder sends a DeviceToDeviceMessage during the handshake, so it has an initial
// sequence number of 1. The initiator will therefore have an initial sequence number of 0.
int initialEncodeSequenceNumber = isInitiator ? 0 : 1;
int initialDecodeSequenceNumber = isInitiator ? 1 : 0;
return new D2DConnectionContextV1(
encodeKey, decodeKey, initialEncodeSequenceNumber, initialDecodeSequenceNumber);
}
}
}