blob: da5abf193bd22a03b41c19d7fa0745d5a245032a [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.common.annotations.VisibleForTesting;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.DeviceToDeviceMessage;
import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.EcPoint;
import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.SpakeHandshakeMessage;
import com.google.security.cryptauth.lib.securegcm.Ed25519.Ed25519Exception;
import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.Payload;
import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.PayloadType;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.SignatureException;
import javax.crypto.spec.SecretKeySpec;
/**
* Implements a {@link D2DHandshakeContext} by using SPAKE2 (Simple Password-Based Encrypted Key
* Exchange Protocol) on top of the Ed25519 curve.
* SPAKE2: http://www.di.ens.fr/~mabdalla/papers/AbPo05a-letter.pdf
* Ed25519: http://ed25519.cr.yp.to/
*
* <p>Usage:
* {@code
* // initiator:
* D2DHandshakeContext initiatorHandshakeContext =
* D2DSpakeEd25519Handshake.forInitiator(PASSWORD);
* byte[] initiatorMsg = initiatorHandshakeContext.getNextHandshakeMessage();
* // (send initiatorMsg to responder)
*
* // responder:
* D2DHandshakeContext responderHandshakeContext =
* D2DSpakeEd25519Handshake.forResponder(PASSWORD);
* responderHandshakeContext.parseHandshakeMessage(initiatorMsg);
* byte[] responderMsg = responderHandshakeContext.getNextHandshakeMessage();
* // (send responderMsg to initiator)
*
* // initiator:
* initiatorHandshakeContext.parseHandshakeMessage(responderMsg);
* initiatorMsg = initiatorHandshakeContext.getNextHandshakeMessage();
* // (send initiatorMsg to responder)
*
* // responder:
* responderHandshakeContext.parseHandshakeMessage(initiatorMsg);
* responderMsg = responderHandshakeContext.getNextHandshakeMessage();
* D2DConnectionContext responderCtx = responderHandshakeContext.toConnectionContext();
* // (send responderMsg to initiator)
*
* // initiator:
* initiatorHandshakeContext.parseHandshakeMessage(responderMsg);
* D2DConnectionContext initiatorCtx = initiatorHandshakeContext.toConnectionContext();
* }
*
* <p>The initial computation is:
* Initiator Responder
* has KM (pre-agreed point) has KM (pre-agreed point)
* has KN (pre-agreed point) has KN (pre-agreed point)
* has Password (pre-agreed) has Password (pre-agreed)
* picks random scalar Xi (private key) picks random scalar Xr (private key)
* computes the public key Pxi = G*Xi computes the public key Pxr = G*Xr
* computes commitment: computes commitment:
* Ci = KM * password + Pxi Cr = KN * password + Pxr
*
* <p>The flow is:
* Initiator Responder
* ----- Ci --------------------------------->
* <--------------------------------- Cr -----
* computes shared key K: computes shared key K:
* (Cr - KN*password) * Xi (Ci - KM*password) * Xr
* computes hash: computes hash:
* Hi = sha256(0|Cr|Ci|K) Hr = sha256(1|Ci|Cr|K)
* ----- Hi --------------------------------->
* Verify Hi
* <-------------- Hr (optional payload) -----
* Verify Hr
*/
public class D2DSpakeEd25519Handshake implements D2DHandshakeContext {
// Minimum length password that is acceptable for the handshake
public static final int MIN_PASSWORD_LENGTH = 4;
/**
* Creates a new SPAKE handshake object for the initiator.
*
* @param password the password that should be used in the handshake. Note that this should be
* at least {@value #MIN_PASSWORD_LENGTH} bytes long
*/
public static D2DSpakeEd25519Handshake forInitiator(byte[] password) throws HandshakeException {
return new D2DSpakeEd25519Handshake(State.INITIATOR_START, password);
}
/**
* Creates a new SPAKE handshake object for the responder.
*
* @param password the password that should be used in the handshake. Note that this should be
* at least {@value #MIN_PASSWORD_LENGTH} bytes long
*/
public static D2DSpakeEd25519Handshake forResponder(byte[] password) throws HandshakeException {
return new D2DSpakeEd25519Handshake(State.RESPONDER_START, password);
}
//
// The protocol requires two verifiable, randomly generated group point. They were generated
// using the python code below. The algorithm is to first pick a random y in the group and solve
// the elliptic curve equation for a value of x, if possible. We then use (x, y) as the random
// point.
// Source of ed25519 is here: http://ed25519.cr.yp.to/python/ed25519.py
// import ed25519
// import hashlib
//
// # Seeds
// seed1 = 'D2D Ed25519 point generation seed (M)'
// seed2 = 'D2D Ed25519 point generation seed (N)'
//
// def find_seed(seed):
// # generate a random scalar for the y coordinate
// y = hashlib.sha256(seed).hexdigest()
//
// P = ed25519.scalarmult(ed25519.B, int(y, 16) % ed25519.q)
// if (not ed25519.isoncurve(P)):
// print 'Wat? P should be on curve!'
//
// print ' x: ' + hex(P[0])
// print ' y: ' + hex(P[1])
// print
//
// find_seed(seed1)
// find_seed(seed2)
//
// Output is:
// x: 0x1981fb43f103290ecf9772022db8b19bfaf389057ed91e8486eb368763435925L
// y: 0xa714c34f3b588aac92fd2587884a20964fd351a1f147d5c4bbf5c2f37a77c36L
//
// x: 0x201a184f47d9a7973891d148e3d1c864d8084547131c2c1cefb7eebd26c63567L
// y: 0x6da2d3b18ec4f9aa3b08e39c997cd8bf6e9948ffd4feffecaf8dd0b3d648b7e8L
//
// To get extended representation X, Y, Z, T, do: Z = 1, T = X*Y mod P
@VisibleForTesting
static final BigInteger[] KM = new BigInteger[] {
new BigInteger(new byte[] {(byte) 0x19, (byte) 0x81, (byte) 0xFB, (byte) 0x43,
(byte) 0xF1, (byte) 0x03, (byte) 0x29, (byte) 0x0E, (byte) 0xCF, (byte) 0x97,
(byte) 0x72, (byte) 0x02, (byte) 0x2D, (byte) 0xB8, (byte) 0xB1, (byte) 0x9B,
(byte) 0xFA, (byte) 0xF3, (byte) 0x89, (byte) 0x05, (byte) 0x7E, (byte) 0xD9,
(byte) 0x1E, (byte) 0x84, (byte) 0x86, (byte) 0xEB, (byte) 0x36, (byte) 0x87,
(byte) 0x63, (byte) 0x43, (byte) 0x59, (byte) 0x25}),
new BigInteger(new byte[] {(byte) 0x0A, (byte) 0x71, (byte) 0x4C, (byte) 0x34,
(byte) 0xF3, (byte) 0xB5, (byte) 0x88, (byte) 0xAA, (byte) 0xC9, (byte) 0x2F,
(byte) 0xD2, (byte) 0x58, (byte) 0x78, (byte) 0x84, (byte) 0xA2, (byte) 0x09,
(byte) 0x64, (byte) 0xFD, (byte) 0x35, (byte) 0x1A, (byte) 0x1F, (byte) 0x14,
(byte) 0x7D, (byte) 0x5C, (byte) 0x4B, (byte) 0xBF, (byte) 0x5C, (byte) 0x2F,
(byte) 0x37, (byte) 0xA7, (byte) 0x7C, (byte) 0x36}),
BigInteger.ONE,
new BigInteger(new byte[] {(byte) 0x04, (byte) 0x8F, (byte) 0xC1, (byte) 0xCE,
(byte) 0xE5, (byte) 0x83, (byte) 0x99, (byte) 0x25, (byte) 0xE5, (byte) 0x9B,
(byte) 0x80, (byte) 0xEA, (byte) 0xAD, (byte) 0x82, (byte) 0xAC, (byte) 0x0A,
(byte) 0x3C, (byte) 0xFE, (byte) 0xC5, (byte) 0x60, (byte) 0x93, (byte) 0x59,
(byte) 0x8B, (byte) 0x48, (byte) 0x44, (byte) 0xDD, (byte) 0x2A, (byte) 0x3E,
(byte) 0x24, (byte) 0x5D, (byte) 0x88, (byte) 0x33})};
@VisibleForTesting
static final BigInteger[] KN = new BigInteger[] {
new BigInteger(new byte[] {(byte) 0x20, (byte) 0x1A, (byte) 0x18, (byte) 0x4F,
(byte) 0x47, (byte) 0xD9, (byte) 0xA7, (byte) 0x97, (byte) 0x38, (byte) 0x91,
(byte) 0xD1, (byte) 0x48, (byte) 0xE3, (byte) 0xD1, (byte) 0xC8, (byte) 0x64,
(byte) 0xD8, (byte) 0x08, (byte) 0x45, (byte) 0x47, (byte) 0x13, (byte) 0x1C,
(byte) 0x2C, (byte) 0x1C, (byte) 0xEF, (byte) 0xB7, (byte) 0xEE, (byte) 0xBD,
(byte) 0x26, (byte) 0xC6, (byte) 0x35, (byte) 0x67}),
new BigInteger(new byte[] {(byte) 0x6D, (byte) 0xA2, (byte) 0xD3, (byte) 0xB1,
(byte) 0x8E, (byte) 0xC4, (byte) 0xF9, (byte) 0xAA, (byte) 0x3B, (byte) 0x08,
(byte) 0xE3, (byte) 0x9C, (byte) 0x99, (byte) 0x7C, (byte) 0xD8, (byte) 0xBF,
(byte) 0x6E, (byte) 0x99, (byte) 0x48, (byte) 0xFF, (byte) 0xD4, (byte) 0xFE,
(byte) 0xFF, (byte) 0xEC, (byte) 0xAF, (byte) 0x8D, (byte) 0xD0, (byte) 0xB3,
(byte) 0xD6, (byte) 0x48, (byte) 0xB7, (byte) 0xE8}),
BigInteger.ONE,
new BigInteger(new byte[] {(byte) 0x16, (byte) 0x40, (byte) 0xED, (byte) 0x5A,
(byte) 0x54, (byte) 0xFA, (byte) 0x0B, (byte) 0x07, (byte) 0x22, (byte) 0x86,
(byte) 0xE9, (byte) 0xD2, (byte) 0x2F, (byte) 0x46, (byte) 0x47, (byte) 0x63,
(byte) 0xFB, (byte) 0xF6, (byte) 0x0D, (byte) 0x79, (byte) 0x1D, (byte) 0x37,
(byte) 0xB9, (byte) 0x09, (byte) 0x3B, (byte) 0x58, (byte) 0x4D, (byte) 0xF4,
(byte) 0xC9, (byte) 0x95, (byte) 0xF7, (byte) 0x81})};
// Base point B as per ed25519.cr.yp.to
@VisibleForTesting
/* package */ static final BigInteger[] B = new BigInteger[] {
new BigInteger(new byte[] {(byte) 0x21, (byte) 0x69, (byte) 0x36, (byte) 0xD3,
(byte) 0xCD, (byte) 0x6E, (byte) 0x53, (byte) 0xFE, (byte) 0xC0, (byte) 0xA4,
(byte) 0xE2, (byte) 0x31, (byte) 0xFD, (byte) 0xD6, (byte) 0xDC, (byte) 0x5C,
(byte) 0x69, (byte) 0x2C, (byte) 0xC7, (byte) 0x60, (byte) 0x95, (byte) 0x25,
(byte) 0xA7, (byte) 0xB2, (byte) 0xC9, (byte) 0x56, (byte) 0x2D, (byte) 0x60,
(byte) 0x8F, (byte) 0x25, (byte) 0xD5, (byte) 0x1A}),
new BigInteger(new byte[] {(byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66,
(byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66,
(byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66,
(byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66,
(byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66,
(byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x58}),
BigInteger.ONE,
new BigInteger(new byte[] {(byte) 0x67, (byte) 0x87, (byte) 0x5F, (byte) 0x0F,
(byte) 0xD7, (byte) 0x8B, (byte) 0x76, (byte) 0x65, (byte) 0x66, (byte) 0xEA,
(byte) 0x4E, (byte) 0x8E, (byte) 0x64, (byte) 0xAB, (byte) 0xE3, (byte) 0x7D,
(byte) 0x20, (byte) 0xF0, (byte) 0x9F, (byte) 0x80, (byte) 0x77, (byte) 0x51,
(byte) 0x52, (byte) 0xF5, (byte) 0x6D, (byte) 0xDE, (byte) 0x8A, (byte) 0xB3,
(byte) 0xA5, (byte) 0xB7, (byte) 0xDD, (byte) 0xA3})};
// Number of bits needed to represent a point
private static final int POINT_SIZE_BITS = 256;
// Java Message Digest name for SHA 256
private static final String SHA256 = "SHA-256";
// Pre-shared password hash represented as an integer
private BigInteger passwordHash;
// Current state of the handshake
private State handshakeState;
// Derived shared key
private byte[] sharedKey;
// Private key (random scalar)
private BigInteger valueX;
// Public key (random point, in extended notation, based on valueX)
private BigInteger[] pointX;
// Commitment we've received from the other party (their password-authenticated public key)
private BigInteger[] theirCommitmentPointAffine;
private BigInteger[] theirCommitmentPointExtended;
// Commitment we've sent to the other party (our password-authenticated public key)
private BigInteger[] ourCommitmentPointAffine;
private BigInteger[] ourCommitmentPointExtended;
private enum State {
// Initiator state
INITIATOR_START,
INITIATOR_WAITING_FOR_RESPONDER_COMMITMENT,
INITIATOR_AFTER_RESPONDER_COMMITMENT,
INITIATOR_WAITING_FOR_RESPONDER_HASH,
// Responder state
RESPONDER_START,
RESPONDER_AFTER_INITIATOR_COMMITMENT,
RESPONDER_WAITING_FOR_INITIATOR_HASH,
RESPONDER_AFTER_INITIATOR_HASH,
// Common completion state
HANDSHAKE_FINISHED,
HANDSHAKE_ALREADY_USED
}
@VisibleForTesting
D2DSpakeEd25519Handshake(State state, byte[] password) throws HandshakeException {
if (password == null || password.length < MIN_PASSWORD_LENGTH) {
throw new HandshakeException("Passwords must be at least " + MIN_PASSWORD_LENGTH + " bytes");
}
handshakeState = state;
passwordHash = new BigInteger(1 /* positive */, hash(password));
do {
valueX = new BigInteger(POINT_SIZE_BITS, new SecureRandom());
} while (valueX.equals(BigInteger.ZERO));
try {
pointX = Ed25519.scalarMultiplyExtendedPoint(B, valueX);
} catch (Ed25519Exception e) {
throw new HandshakeException("Could not make public key point", e);
}
}
@Override
public boolean isHandshakeComplete() {
switch (handshakeState) {
case HANDSHAKE_FINISHED:
// fall-through intentional
case HANDSHAKE_ALREADY_USED:
return true;
default:
return false;
}
}
@Override
public byte[] getNextHandshakeMessage() throws HandshakeException {
byte[] nextMessage;
switch(handshakeState) {
case INITIATOR_START:
nextMessage = makeCommitmentPointMessage(true /* is initiator */);
handshakeState = State.INITIATOR_WAITING_FOR_RESPONDER_COMMITMENT;
break;
case RESPONDER_AFTER_INITIATOR_COMMITMENT:
nextMessage = makeCommitmentPointMessage(false /* is initiator */);
handshakeState = State.RESPONDER_WAITING_FOR_INITIATOR_HASH;
break;
case INITIATOR_AFTER_RESPONDER_COMMITMENT:
nextMessage = makeSharedKeyHashMessage(true /* is initiator */, null /* no payload */);
handshakeState = State.INITIATOR_WAITING_FOR_RESPONDER_HASH;
break;
case RESPONDER_AFTER_INITIATOR_HASH:
nextMessage = makeSharedKeyHashMessage(false /* is initiator */, null /* no payload */);
handshakeState = State.HANDSHAKE_FINISHED;
break;
default:
throw new HandshakeException("Cannot get next message in state: " + handshakeState);
}
return nextMessage;
}
@Override
public byte[] getNextHandshakeMessage(byte[] payload) throws HandshakeException {
byte[] nextMessage;
switch (handshakeState) {
case RESPONDER_AFTER_INITIATOR_HASH:
nextMessage = makeSharedKeyHashMessage(false /* is initiator */, payload);
handshakeState = State.HANDSHAKE_FINISHED;
break;
default:
throw new HandshakeException(
"Cannot send handshake message with payload in state: " + handshakeState);
}
return nextMessage;
}
private byte[] makeCommitmentPointMessage(boolean isInitiator) throws HandshakeException {
try {
ourCommitmentPointExtended =
Ed25519.scalarMultiplyExtendedPoint(isInitiator ? KM : KN, passwordHash);
ourCommitmentPointExtended = Ed25519.addExtendedPoints(ourCommitmentPointExtended, pointX);
ourCommitmentPointAffine = Ed25519.toAffine(ourCommitmentPointExtended);
return SpakeHandshakeMessage.newBuilder()
.setEcPoint(
EcPoint.newBuilder()
.setCurve(DeviceToDeviceMessagesProto.Curve.ED_25519)
.setX(ByteString.copyFrom(ourCommitmentPointAffine[0].toByteArray()))
.setY(ByteString.copyFrom(ourCommitmentPointAffine[1].toByteArray()))
.build())
.setFlowNumber(isInitiator ? 1 : 2 /* first or second message */)
.build()
.toByteArray();
} catch (Ed25519Exception e) {
throw new HandshakeException("Could not make commitment point message", e);
}
}
private void makeSharedKey(boolean isInitiator) throws HandshakeException {
if (handshakeState != State.RESPONDER_START
&& handshakeState != State.INITIATOR_WAITING_FOR_RESPONDER_COMMITMENT) {
throw new HandshakeException("Cannot make shared key in state: " + handshakeState);
}
try {
BigInteger[] kNMP = Ed25519.scalarMultiplyExtendedPoint(isInitiator ? KN : KM, passwordHash);
// TheirPublicKey = TheirCommitment - kNMP = (TheirPublicKey + kNMP) - kNMP
BigInteger[] theirPublicKey =
Ed25519.subtractExtendedPoints(theirCommitmentPointExtended, kNMP);
BigInteger[] sharedKeyPoint = Ed25519.scalarMultiplyExtendedPoint(theirPublicKey, valueX);
sharedKey = hash(pointToByteArray(Ed25519.toAffine(sharedKeyPoint)));
} catch (Ed25519Exception e) {
throw new HandshakeException("Error computing shared key", e);
}
}
private byte[] makeSharedKeyHashMessage(boolean isInitiator, byte[] payload)
throws HandshakeException {
SpakeHandshakeMessage.Builder handshakeMessage = SpakeHandshakeMessage.newBuilder()
.setHashValue(ByteString.copyFrom(computeOurKeyHash(isInitiator)))
.setFlowNumber(isInitiator ? 3 : 4 /* third or fourth message */);
if (canSendPayloadInHandshakeMessage() && payload != null) {
DeviceToDeviceMessage deviceToDeviceMessage =
D2DConnectionContext.createDeviceToDeviceMessage(payload, 1 /* sequence number */);
try {
handshakeMessage.setPayload(ByteString.copyFrom(
D2DCryptoOps.signcryptPayload(
new Payload(PayloadType.DEVICE_TO_DEVICE_RESPONDER_HELLO_PAYLOAD,
deviceToDeviceMessage.toByteArray()),
new SecretKeySpec(sharedKey, "AES"))));
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new HandshakeException("Cannot set payload", e);
}
}
return handshakeMessage.build().toByteArray();
}
private byte[] computeOurKeyHash(boolean isInitiator) throws HandshakeException {
return hash(concat(
new byte[] { (byte) (isInitiator ? 0 : 1) },
pointToByteArray(theirCommitmentPointAffine),
pointToByteArray(ourCommitmentPointAffine),
sharedKey));
}
private byte[] computeTheirKeyHash(boolean isInitiator) throws HandshakeException {
return hash(concat(
new byte[] { (byte) (isInitiator ? 1 : 0) },
pointToByteArray(ourCommitmentPointAffine),
pointToByteArray(theirCommitmentPointAffine),
sharedKey));
}
private byte[] pointToByteArray(BigInteger[] p) {
return concat(p[0].toByteArray(), p[1].toByteArray());
}
@Override
public boolean canSendPayloadInHandshakeMessage() {
return handshakeState == State.RESPONDER_AFTER_INITIATOR_HASH;
}
@Override
public byte[] parseHandshakeMessage(byte[] handshakeMessage) throws HandshakeException {
if (handshakeMessage == null || handshakeMessage.length == 0) {
throw new HandshakeException("Handshake message too short");
}
byte[] payload = new byte[0];
switch(handshakeState) {
case RESPONDER_START:
// no payload can be sent in this message
parseCommitmentMessage(handshakeMessage, false /* is initiator */);
makeSharedKey(false /* is initiator */);
handshakeState = State.RESPONDER_AFTER_INITIATOR_COMMITMENT;
break;
case INITIATOR_WAITING_FOR_RESPONDER_COMMITMENT:
// no payload can be sent in this message
parseCommitmentMessage(handshakeMessage, true /* is initiator */);
makeSharedKey(true /* is initiator */);
handshakeState = State.INITIATOR_AFTER_RESPONDER_COMMITMENT;
break;
case RESPONDER_WAITING_FOR_INITIATOR_HASH:
// no payload can be sent in this message
parseHashMessage(handshakeMessage, false /* is initiator */);
handshakeState = State.RESPONDER_AFTER_INITIATOR_HASH;
break;
case INITIATOR_WAITING_FOR_RESPONDER_HASH:
payload = parseHashMessage(handshakeMessage, true /* is initiator */);
handshakeState = State.HANDSHAKE_FINISHED;
break;
default:
throw new HandshakeException("Cannot parse message in state: " + handshakeState);
}
return payload;
}
private byte[] parseHashMessage(byte[] handshakeMessage, boolean isInitiator)
throws HandshakeException {
SpakeHandshakeMessage hashMessage;
// Parse the message
try {
hashMessage = SpakeHandshakeMessage.parseFrom(handshakeMessage);
} catch (InvalidProtocolBufferException e) {
throw new HandshakeException("Could not parse hash message", e);
}
// Check flow number
if (!hashMessage.hasFlowNumber()) {
throw new HandshakeException("Hash message missing flow number");
}
int expectedFlowNumber = isInitiator ? 4 : 3;
int actualFlowNumber = hashMessage.getFlowNumber();
if (actualFlowNumber != expectedFlowNumber) {
throw new HandshakeException("Hash message has flow number " + actualFlowNumber
+ ", but expected flow number " + expectedFlowNumber);
}
// Check and extract hash
if (!hashMessage.hasHashValue()) {
throw new HandshakeException("Hash message missing hash value");
}
byte[] theirHash = hashMessage.getHashValue().toByteArray();
byte[] theirCorrectHash = computeTheirKeyHash(isInitiator);
if (!constantTimeArrayEquals(theirCorrectHash, theirHash)) {
throw new HandshakeException("Hash message had incorrect hash value");
}
if (isInitiator && hashMessage.hasPayload()) {
try {
DeviceToDeviceMessage message = D2DCryptoOps.decryptResponderHelloMessage(
new SecretKeySpec(sharedKey, "AES"),
hashMessage.getPayload().toByteArray());
if (message.getSequenceNumber() != 1) {
throw new HandshakeException("Incorrect sequence number in responder hello");
}
return message.getMessage().toByteArray();
} catch (SignatureException e) {
throw new HandshakeException("Error recovering payload from hash message", e);
}
}
// empty/no payload
return new byte[0];
}
private void parseCommitmentMessage(byte[] handshakeMessage, boolean isInitiator)
throws HandshakeException {
SpakeHandshakeMessage commitmentMessage;
// Parse the message
try {
commitmentMessage = SpakeHandshakeMessage.parseFrom(handshakeMessage);
} catch (InvalidProtocolBufferException e) {
throw new HandshakeException("Could not parse commitment message", e);
}
// Check flow number
if (!commitmentMessage.hasFlowNumber()) {
throw new HandshakeException("Commitment message missing flow number");
}
if (commitmentMessage.getFlowNumber() != (isInitiator ? 2 : 1)) {
throw new HandshakeException("Commitment message has wrong flow number");
}
// Check point and curve; and extract point
if (!commitmentMessage.hasEcPoint()) {
throw new HandshakeException("Commitment message missing point");
}
EcPoint commitmentPoint = commitmentMessage.getEcPoint();
if (!commitmentPoint.hasCurve()
|| commitmentPoint.getCurve() != DeviceToDeviceMessagesProto.Curve.ED_25519) {
throw new HandshakeException("Commitment message has wrong curve");
}
if (!commitmentPoint.hasX()) {
throw new HandshakeException("Commitment point missing x coordinate");
}
if (!commitmentPoint.hasY()) {
throw new HandshakeException("Commitment point missing y coordinate");
}
// Build the point
theirCommitmentPointAffine = new BigInteger[] {
new BigInteger(commitmentPoint.getX().toByteArray()),
new BigInteger(commitmentPoint.getY().toByteArray())
};
// Validate the point to be sure
try {
Ed25519.validateAffinePoint(theirCommitmentPointAffine);
theirCommitmentPointExtended = Ed25519.toExtended(theirCommitmentPointAffine);
} catch (Ed25519Exception e) {
throw new HandshakeException("Error validating their commitment point", 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;
// 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.
return new D2DConnectionContextV0(
new SecretKeySpec(sharedKey, "AES"), 1 /* initialSequenceNumber */);
}
/**
* Implementation of byte array concatenation copied from Guava.
*/
private static byte[] concat(byte[]... arrays) {
int length = 0;
for (byte[] array : arrays) {
length += array.length;
}
byte[] result = new byte[length];
int pos = 0;
for (byte[] array : arrays) {
System.arraycopy(array, 0, result, pos, array.length);
pos += array.length;
}
return result;
}
private static byte[] hash(byte[] message) throws HandshakeException {
try {
return MessageDigest.getInstance(SHA256).digest(message);
} catch (NoSuchAlgorithmException e) {
throw new HandshakeException("Error performing hash", e);
}
}
private static boolean constantTimeArrayEquals(byte[] a, byte[] b) {
if (a == null || b == null) {
return (a == b);
}
if (a.length != b.length) {
return false;
}
byte result = 0;
for (int i = 0; i < b.length; i++) {
result = (byte) (result | (a[i] ^ b[i]));
}
return (result == 0);
}
}