1 /* Copyright 2018 Google LLC 2 * 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * https://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 package com.google.security.cryptauth.lib.securegcm; 16 17 import com.google.common.annotations.VisibleForTesting; 18 import com.google.protobuf.ByteString; 19 import com.google.protobuf.InvalidProtocolBufferException; 20 import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.DeviceToDeviceMessage; 21 import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.EcPoint; 22 import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.SpakeHandshakeMessage; 23 import com.google.security.cryptauth.lib.securegcm.Ed25519.Ed25519Exception; 24 import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.Payload; 25 import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.PayloadType; 26 import java.math.BigInteger; 27 import java.security.InvalidKeyException; 28 import java.security.MessageDigest; 29 import java.security.NoSuchAlgorithmException; 30 import java.security.SecureRandom; 31 import java.security.SignatureException; 32 import javax.crypto.spec.SecretKeySpec; 33 34 /** 35 * Implements a {@link D2DHandshakeContext} by using SPAKE2 (Simple Password-Based Encrypted Key 36 * Exchange Protocol) on top of the Ed25519 curve. 37 * SPAKE2: http://www.di.ens.fr/~mabdalla/papers/AbPo05a-letter.pdf 38 * Ed25519: http://ed25519.cr.yp.to/ 39 * 40 * <p>Usage: 41 * {@code 42 * // initiator: 43 * D2DHandshakeContext initiatorHandshakeContext = 44 * D2DSpakeEd25519Handshake.forInitiator(PASSWORD); 45 * byte[] initiatorMsg = initiatorHandshakeContext.getNextHandshakeMessage(); 46 * // (send initiatorMsg to responder) 47 * 48 * // responder: 49 * D2DHandshakeContext responderHandshakeContext = 50 * D2DSpakeEd25519Handshake.forResponder(PASSWORD); 51 * responderHandshakeContext.parseHandshakeMessage(initiatorMsg); 52 * byte[] responderMsg = responderHandshakeContext.getNextHandshakeMessage(); 53 * // (send responderMsg to initiator) 54 * 55 * // initiator: 56 * initiatorHandshakeContext.parseHandshakeMessage(responderMsg); 57 * initiatorMsg = initiatorHandshakeContext.getNextHandshakeMessage(); 58 * // (send initiatorMsg to responder) 59 * 60 * // responder: 61 * responderHandshakeContext.parseHandshakeMessage(initiatorMsg); 62 * responderMsg = responderHandshakeContext.getNextHandshakeMessage(); 63 * D2DConnectionContext responderCtx = responderHandshakeContext.toConnectionContext(); 64 * // (send responderMsg to initiator) 65 * 66 * // initiator: 67 * initiatorHandshakeContext.parseHandshakeMessage(responderMsg); 68 * D2DConnectionContext initiatorCtx = initiatorHandshakeContext.toConnectionContext(); 69 * } 70 * 71 * <p>The initial computation is: 72 * Initiator Responder 73 * has KM (pre-agreed point) has KM (pre-agreed point) 74 * has KN (pre-agreed point) has KN (pre-agreed point) 75 * has Password (pre-agreed) has Password (pre-agreed) 76 * picks random scalar Xi (private key) picks random scalar Xr (private key) 77 * computes the public key Pxi = G*Xi computes the public key Pxr = G*Xr 78 * computes commitment: computes commitment: 79 * Ci = KM * password + Pxi Cr = KN * password + Pxr 80 * 81 * <p>The flow is: 82 * Initiator Responder 83 * ----- Ci ---------------------------------> 84 * <--------------------------------- Cr ----- 85 * computes shared key K: computes shared key K: 86 * (Cr - KN*password) * Xi (Ci - KM*password) * Xr 87 * computes hash: computes hash: 88 * Hi = sha256(0|Cr|Ci|K) Hr = sha256(1|Ci|Cr|K) 89 * ----- Hi ---------------------------------> 90 * Verify Hi 91 * <-------------- Hr (optional payload) ----- 92 * Verify Hr 93 */ 94 public class D2DSpakeEd25519Handshake implements D2DHandshakeContext { 95 // Minimum length password that is acceptable for the handshake 96 public static final int MIN_PASSWORD_LENGTH = 4; 97 /** 98 * Creates a new SPAKE handshake object for the initiator. 99 * 100 * @param password the password that should be used in the handshake. Note that this should be 101 * at least {@value #MIN_PASSWORD_LENGTH} bytes long 102 */ 103 public static D2DSpakeEd25519Handshake forInitiator(byte[] password) throws HandshakeException { 104 return new D2DSpakeEd25519Handshake(State.INITIATOR_START, password); 105 } 106 107 /** 108 * Creates a new SPAKE handshake object for the responder. 109 * 110 * @param password the password that should be used in the handshake. Note that this should be 111 * at least {@value #MIN_PASSWORD_LENGTH} bytes long 112 */ 113 public static D2DSpakeEd25519Handshake forResponder(byte[] password) throws HandshakeException { 114 return new D2DSpakeEd25519Handshake(State.RESPONDER_START, password); 115 } 116 117 // 118 // The protocol requires two verifiable, randomly generated group point. They were generated 119 // using the python code below. The algorithm is to first pick a random y in the group and solve 120 // the elliptic curve equation for a value of x, if possible. We then use (x, y) as the random 121 // point. 122 // Source of ed25519 is here: http://ed25519.cr.yp.to/python/ed25519.py 123 // import ed25519 124 // import hashlib 125 // 126 // # Seeds 127 // seed1 = 'D2D Ed25519 point generation seed (M)' 128 // seed2 = 'D2D Ed25519 point generation seed (N)' 129 // 130 // def find_seed(seed): 131 // # generate a random scalar for the y coordinate 132 // y = hashlib.sha256(seed).hexdigest() 133 // 134 // P = ed25519.scalarmult(ed25519.B, int(y, 16) % ed25519.q) 135 // if (not ed25519.isoncurve(P)): 136 // print 'Wat? P should be on curve!' 137 // 138 // print ' x: ' + hex(P[0]) 139 // print ' y: ' + hex(P[1]) 140 // print 141 // 142 // find_seed(seed1) 143 // find_seed(seed2) 144 // 145 // Output is: 146 // x: 0x1981fb43f103290ecf9772022db8b19bfaf389057ed91e8486eb368763435925L 147 // y: 0xa714c34f3b588aac92fd2587884a20964fd351a1f147d5c4bbf5c2f37a77c36L 148 // 149 // x: 0x201a184f47d9a7973891d148e3d1c864d8084547131c2c1cefb7eebd26c63567L 150 // y: 0x6da2d3b18ec4f9aa3b08e39c997cd8bf6e9948ffd4feffecaf8dd0b3d648b7e8L 151 // 152 // To get extended representation X, Y, Z, T, do: Z = 1, T = X*Y mod P 153 @VisibleForTesting 154 static final BigInteger[] KM = new BigInteger[] { 155 new BigInteger(new byte[] {(byte) 0x19, (byte) 0x81, (byte) 0xFB, (byte) 0x43, 156 (byte) 0xF1, (byte) 0x03, (byte) 0x29, (byte) 0x0E, (byte) 0xCF, (byte) 0x97, 157 (byte) 0x72, (byte) 0x02, (byte) 0x2D, (byte) 0xB8, (byte) 0xB1, (byte) 0x9B, 158 (byte) 0xFA, (byte) 0xF3, (byte) 0x89, (byte) 0x05, (byte) 0x7E, (byte) 0xD9, 159 (byte) 0x1E, (byte) 0x84, (byte) 0x86, (byte) 0xEB, (byte) 0x36, (byte) 0x87, 160 (byte) 0x63, (byte) 0x43, (byte) 0x59, (byte) 0x25}), 161 new BigInteger(new byte[] {(byte) 0x0A, (byte) 0x71, (byte) 0x4C, (byte) 0x34, 162 (byte) 0xF3, (byte) 0xB5, (byte) 0x88, (byte) 0xAA, (byte) 0xC9, (byte) 0x2F, 163 (byte) 0xD2, (byte) 0x58, (byte) 0x78, (byte) 0x84, (byte) 0xA2, (byte) 0x09, 164 (byte) 0x64, (byte) 0xFD, (byte) 0x35, (byte) 0x1A, (byte) 0x1F, (byte) 0x14, 165 (byte) 0x7D, (byte) 0x5C, (byte) 0x4B, (byte) 0xBF, (byte) 0x5C, (byte) 0x2F, 166 (byte) 0x37, (byte) 0xA7, (byte) 0x7C, (byte) 0x36}), 167 BigInteger.ONE, 168 new BigInteger(new byte[] {(byte) 0x04, (byte) 0x8F, (byte) 0xC1, (byte) 0xCE, 169 (byte) 0xE5, (byte) 0x83, (byte) 0x99, (byte) 0x25, (byte) 0xE5, (byte) 0x9B, 170 (byte) 0x80, (byte) 0xEA, (byte) 0xAD, (byte) 0x82, (byte) 0xAC, (byte) 0x0A, 171 (byte) 0x3C, (byte) 0xFE, (byte) 0xC5, (byte) 0x60, (byte) 0x93, (byte) 0x59, 172 (byte) 0x8B, (byte) 0x48, (byte) 0x44, (byte) 0xDD, (byte) 0x2A, (byte) 0x3E, 173 (byte) 0x24, (byte) 0x5D, (byte) 0x88, (byte) 0x33})}; 174 175 @VisibleForTesting 176 static final BigInteger[] KN = new BigInteger[] { 177 new BigInteger(new byte[] {(byte) 0x20, (byte) 0x1A, (byte) 0x18, (byte) 0x4F, 178 (byte) 0x47, (byte) 0xD9, (byte) 0xA7, (byte) 0x97, (byte) 0x38, (byte) 0x91, 179 (byte) 0xD1, (byte) 0x48, (byte) 0xE3, (byte) 0xD1, (byte) 0xC8, (byte) 0x64, 180 (byte) 0xD8, (byte) 0x08, (byte) 0x45, (byte) 0x47, (byte) 0x13, (byte) 0x1C, 181 (byte) 0x2C, (byte) 0x1C, (byte) 0xEF, (byte) 0xB7, (byte) 0xEE, (byte) 0xBD, 182 (byte) 0x26, (byte) 0xC6, (byte) 0x35, (byte) 0x67}), 183 new BigInteger(new byte[] {(byte) 0x6D, (byte) 0xA2, (byte) 0xD3, (byte) 0xB1, 184 (byte) 0x8E, (byte) 0xC4, (byte) 0xF9, (byte) 0xAA, (byte) 0x3B, (byte) 0x08, 185 (byte) 0xE3, (byte) 0x9C, (byte) 0x99, (byte) 0x7C, (byte) 0xD8, (byte) 0xBF, 186 (byte) 0x6E, (byte) 0x99, (byte) 0x48, (byte) 0xFF, (byte) 0xD4, (byte) 0xFE, 187 (byte) 0xFF, (byte) 0xEC, (byte) 0xAF, (byte) 0x8D, (byte) 0xD0, (byte) 0xB3, 188 (byte) 0xD6, (byte) 0x48, (byte) 0xB7, (byte) 0xE8}), 189 BigInteger.ONE, 190 new BigInteger(new byte[] {(byte) 0x16, (byte) 0x40, (byte) 0xED, (byte) 0x5A, 191 (byte) 0x54, (byte) 0xFA, (byte) 0x0B, (byte) 0x07, (byte) 0x22, (byte) 0x86, 192 (byte) 0xE9, (byte) 0xD2, (byte) 0x2F, (byte) 0x46, (byte) 0x47, (byte) 0x63, 193 (byte) 0xFB, (byte) 0xF6, (byte) 0x0D, (byte) 0x79, (byte) 0x1D, (byte) 0x37, 194 (byte) 0xB9, (byte) 0x09, (byte) 0x3B, (byte) 0x58, (byte) 0x4D, (byte) 0xF4, 195 (byte) 0xC9, (byte) 0x95, (byte) 0xF7, (byte) 0x81})}; 196 197 // Base point B as per ed25519.cr.yp.to 198 @VisibleForTesting 199 /* package */ static final BigInteger[] B = new BigInteger[] { 200 new BigInteger(new byte[] {(byte) 0x21, (byte) 0x69, (byte) 0x36, (byte) 0xD3, 201 (byte) 0xCD, (byte) 0x6E, (byte) 0x53, (byte) 0xFE, (byte) 0xC0, (byte) 0xA4, 202 (byte) 0xE2, (byte) 0x31, (byte) 0xFD, (byte) 0xD6, (byte) 0xDC, (byte) 0x5C, 203 (byte) 0x69, (byte) 0x2C, (byte) 0xC7, (byte) 0x60, (byte) 0x95, (byte) 0x25, 204 (byte) 0xA7, (byte) 0xB2, (byte) 0xC9, (byte) 0x56, (byte) 0x2D, (byte) 0x60, 205 (byte) 0x8F, (byte) 0x25, (byte) 0xD5, (byte) 0x1A}), 206 new BigInteger(new byte[] {(byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, 207 (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, 208 (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, 209 (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, 210 (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x66, 211 (byte) 0x66, (byte) 0x66, (byte) 0x66, (byte) 0x58}), 212 BigInteger.ONE, 213 new BigInteger(new byte[] {(byte) 0x67, (byte) 0x87, (byte) 0x5F, (byte) 0x0F, 214 (byte) 0xD7, (byte) 0x8B, (byte) 0x76, (byte) 0x65, (byte) 0x66, (byte) 0xEA, 215 (byte) 0x4E, (byte) 0x8E, (byte) 0x64, (byte) 0xAB, (byte) 0xE3, (byte) 0x7D, 216 (byte) 0x20, (byte) 0xF0, (byte) 0x9F, (byte) 0x80, (byte) 0x77, (byte) 0x51, 217 (byte) 0x52, (byte) 0xF5, (byte) 0x6D, (byte) 0xDE, (byte) 0x8A, (byte) 0xB3, 218 (byte) 0xA5, (byte) 0xB7, (byte) 0xDD, (byte) 0xA3})}; 219 220 // Number of bits needed to represent a point 221 private static final int POINT_SIZE_BITS = 256; 222 223 // Java Message Digest name for SHA 256 224 private static final String SHA256 = "SHA-256"; 225 226 // Pre-shared password hash represented as an integer 227 private BigInteger passwordHash; 228 229 // Current state of the handshake 230 private State handshakeState; 231 232 // Derived shared key 233 private byte[] sharedKey; 234 235 // Private key (random scalar) 236 private BigInteger valueX; 237 238 // Public key (random point, in extended notation, based on valueX) 239 private BigInteger[] pointX; 240 241 // Commitment we've received from the other party (their password-authenticated public key) 242 private BigInteger[] theirCommitmentPointAffine; 243 private BigInteger[] theirCommitmentPointExtended; 244 245 // Commitment we've sent to the other party (our password-authenticated public key) 246 private BigInteger[] ourCommitmentPointAffine; 247 private BigInteger[] ourCommitmentPointExtended; 248 249 private enum State { 250 // Initiator state 251 INITIATOR_START, 252 INITIATOR_WAITING_FOR_RESPONDER_COMMITMENT, 253 INITIATOR_AFTER_RESPONDER_COMMITMENT, 254 INITIATOR_WAITING_FOR_RESPONDER_HASH, 255 256 // Responder state 257 RESPONDER_START, 258 RESPONDER_AFTER_INITIATOR_COMMITMENT, 259 RESPONDER_WAITING_FOR_INITIATOR_HASH, 260 RESPONDER_AFTER_INITIATOR_HASH, 261 262 // Common completion state 263 HANDSHAKE_FINISHED, 264 HANDSHAKE_ALREADY_USED 265 } 266 267 @VisibleForTesting 268 D2DSpakeEd25519Handshake(State state, byte[] password) throws HandshakeException { 269 if (password == null || password.length < MIN_PASSWORD_LENGTH) { 270 throw new HandshakeException("Passwords must be at least " + MIN_PASSWORD_LENGTH + " bytes"); 271 } 272 273 handshakeState = state; 274 passwordHash = new BigInteger(1 /* positive */, hash(password)); 275 276 do { 277 valueX = new BigInteger(POINT_SIZE_BITS, new SecureRandom()); 278 } while (valueX.equals(BigInteger.ZERO)); 279 280 try { 281 pointX = Ed25519.scalarMultiplyExtendedPoint(B, valueX); 282 } catch (Ed25519Exception e) { 283 throw new HandshakeException("Could not make public key point", e); 284 } 285 } 286 287 @Override 288 public boolean isHandshakeComplete() { 289 switch (handshakeState) { 290 case HANDSHAKE_FINISHED: 291 // fall-through intentional 292 case HANDSHAKE_ALREADY_USED: 293 return true; 294 295 default: 296 return false; 297 } 298 } 299 300 @Override 301 public byte[] getNextHandshakeMessage() throws HandshakeException { 302 byte[] nextMessage; 303 304 switch(handshakeState) { 305 case INITIATOR_START: 306 nextMessage = makeCommitmentPointMessage(true /* is initiator */); 307 handshakeState = State.INITIATOR_WAITING_FOR_RESPONDER_COMMITMENT; 308 break; 309 310 case RESPONDER_AFTER_INITIATOR_COMMITMENT: 311 nextMessage = makeCommitmentPointMessage(false /* is initiator */); 312 handshakeState = State.RESPONDER_WAITING_FOR_INITIATOR_HASH; 313 break; 314 315 case INITIATOR_AFTER_RESPONDER_COMMITMENT: 316 nextMessage = makeSharedKeyHashMessage(true /* is initiator */, null /* no payload */); 317 handshakeState = State.INITIATOR_WAITING_FOR_RESPONDER_HASH; 318 break; 319 320 case RESPONDER_AFTER_INITIATOR_HASH: 321 nextMessage = makeSharedKeyHashMessage(false /* is initiator */, null /* no payload */); 322 handshakeState = State.HANDSHAKE_FINISHED; 323 break; 324 325 default: 326 throw new HandshakeException("Cannot get next message in state: " + handshakeState); 327 } 328 329 return nextMessage; 330 } 331 332 @Override 333 public byte[] getNextHandshakeMessage(byte[] payload) throws HandshakeException { 334 byte[] nextMessage; 335 336 switch (handshakeState) { 337 case RESPONDER_AFTER_INITIATOR_HASH: 338 nextMessage = makeSharedKeyHashMessage(false /* is initiator */, payload); 339 handshakeState = State.HANDSHAKE_FINISHED; 340 break; 341 342 default: 343 throw new HandshakeException( 344 "Cannot send handshake message with payload in state: " + handshakeState); 345 } 346 347 return nextMessage; 348 } 349 350 private byte[] makeCommitmentPointMessage(boolean isInitiator) throws HandshakeException { 351 try { 352 ourCommitmentPointExtended = 353 Ed25519.scalarMultiplyExtendedPoint(isInitiator ? KM : KN, passwordHash); 354 ourCommitmentPointExtended = Ed25519.addExtendedPoints(ourCommitmentPointExtended, pointX); 355 ourCommitmentPointAffine = Ed25519.toAffine(ourCommitmentPointExtended); 356 357 return SpakeHandshakeMessage.newBuilder() 358 .setEcPoint( 359 EcPoint.newBuilder() 360 .setCurve(DeviceToDeviceMessagesProto.Curve.ED_25519) 361 .setX(ByteString.copyFrom(ourCommitmentPointAffine[0].toByteArray())) 362 .setY(ByteString.copyFrom(ourCommitmentPointAffine[1].toByteArray())) 363 .build()) 364 .setFlowNumber(isInitiator ? 1 : 2 /* first or second message */) 365 .build() 366 .toByteArray(); 367 } catch (Ed25519Exception e) { 368 throw new HandshakeException("Could not make commitment point message", e); 369 } 370 } 371 372 private void makeSharedKey(boolean isInitiator) throws HandshakeException { 373 374 if (handshakeState != State.RESPONDER_START 375 && handshakeState != State.INITIATOR_WAITING_FOR_RESPONDER_COMMITMENT) { 376 throw new HandshakeException("Cannot make shared key in state: " + handshakeState); 377 } 378 379 try { 380 BigInteger[] kNMP = Ed25519.scalarMultiplyExtendedPoint(isInitiator ? KN : KM, passwordHash); 381 382 // TheirPublicKey = TheirCommitment - kNMP = (TheirPublicKey + kNMP) - kNMP 383 BigInteger[] theirPublicKey = 384 Ed25519.subtractExtendedPoints(theirCommitmentPointExtended, kNMP); 385 386 BigInteger[] sharedKeyPoint = Ed25519.scalarMultiplyExtendedPoint(theirPublicKey, valueX); 387 sharedKey = hash(pointToByteArray(Ed25519.toAffine(sharedKeyPoint))); 388 } catch (Ed25519Exception e) { 389 throw new HandshakeException("Error computing shared key", e); 390 } 391 } 392 393 private byte[] makeSharedKeyHashMessage(boolean isInitiator, byte[] payload) 394 throws HandshakeException { 395 SpakeHandshakeMessage.Builder handshakeMessage = SpakeHandshakeMessage.newBuilder() 396 .setHashValue(ByteString.copyFrom(computeOurKeyHash(isInitiator))) 397 .setFlowNumber(isInitiator ? 3 : 4 /* third or fourth message */); 398 399 if (canSendPayloadInHandshakeMessage() && payload != null) { 400 DeviceToDeviceMessage deviceToDeviceMessage = 401 D2DConnectionContext.createDeviceToDeviceMessage(payload, 1 /* sequence number */); 402 try { 403 handshakeMessage.setPayload(ByteString.copyFrom( 404 D2DCryptoOps.signcryptPayload( 405 new Payload(PayloadType.DEVICE_TO_DEVICE_RESPONDER_HELLO_PAYLOAD, 406 deviceToDeviceMessage.toByteArray()), 407 new SecretKeySpec(sharedKey, "AES")))); 408 } catch (InvalidKeyException | NoSuchAlgorithmException e) { 409 throw new HandshakeException("Cannot set payload", e); 410 } 411 } 412 413 return handshakeMessage.build().toByteArray(); 414 } 415 416 private byte[] computeOurKeyHash(boolean isInitiator) throws HandshakeException { 417 return hash(concat( 418 new byte[] { (byte) (isInitiator ? 0 : 1) }, 419 pointToByteArray(theirCommitmentPointAffine), 420 pointToByteArray(ourCommitmentPointAffine), 421 sharedKey)); 422 } 423 424 private byte[] computeTheirKeyHash(boolean isInitiator) throws HandshakeException { 425 return hash(concat( 426 new byte[] { (byte) (isInitiator ? 1 : 0) }, 427 pointToByteArray(ourCommitmentPointAffine), 428 pointToByteArray(theirCommitmentPointAffine), 429 sharedKey)); 430 } 431 432 private byte[] pointToByteArray(BigInteger[] p) { 433 return concat(p[0].toByteArray(), p[1].toByteArray()); 434 } 435 436 @Override 437 public boolean canSendPayloadInHandshakeMessage() { 438 return handshakeState == State.RESPONDER_AFTER_INITIATOR_HASH; 439 } 440 441 @Override 442 public byte[] parseHandshakeMessage(byte[] handshakeMessage) throws HandshakeException { 443 if (handshakeMessage == null || handshakeMessage.length == 0) { 444 throw new HandshakeException("Handshake message too short"); 445 } 446 447 byte[] payload = new byte[0]; 448 449 switch(handshakeState) { 450 case RESPONDER_START: 451 // no payload can be sent in this message 452 parseCommitmentMessage(handshakeMessage, false /* is initiator */); 453 makeSharedKey(false /* is initiator */); 454 handshakeState = State.RESPONDER_AFTER_INITIATOR_COMMITMENT; 455 break; 456 457 case INITIATOR_WAITING_FOR_RESPONDER_COMMITMENT: 458 // no payload can be sent in this message 459 parseCommitmentMessage(handshakeMessage, true /* is initiator */); 460 makeSharedKey(true /* is initiator */); 461 handshakeState = State.INITIATOR_AFTER_RESPONDER_COMMITMENT; 462 break; 463 464 case RESPONDER_WAITING_FOR_INITIATOR_HASH: 465 // no payload can be sent in this message 466 parseHashMessage(handshakeMessage, false /* is initiator */); 467 handshakeState = State.RESPONDER_AFTER_INITIATOR_HASH; 468 break; 469 470 case INITIATOR_WAITING_FOR_RESPONDER_HASH: 471 payload = parseHashMessage(handshakeMessage, true /* is initiator */); 472 handshakeState = State.HANDSHAKE_FINISHED; 473 break; 474 475 default: 476 throw new HandshakeException("Cannot parse message in state: " + handshakeState); 477 } 478 479 return payload; 480 } 481 482 private byte[] parseHashMessage(byte[] handshakeMessage, boolean isInitiator) 483 throws HandshakeException { 484 SpakeHandshakeMessage hashMessage; 485 486 // Parse the message 487 try { 488 hashMessage = SpakeHandshakeMessage.parseFrom(handshakeMessage); 489 } catch (InvalidProtocolBufferException e) { 490 throw new HandshakeException("Could not parse hash message", e); 491 } 492 493 // Check flow number 494 if (!hashMessage.hasFlowNumber()) { 495 throw new HandshakeException("Hash message missing flow number"); 496 } 497 int expectedFlowNumber = isInitiator ? 4 : 3; 498 int actualFlowNumber = hashMessage.getFlowNumber(); 499 if (actualFlowNumber != expectedFlowNumber) { 500 throw new HandshakeException("Hash message has flow number " + actualFlowNumber 501 + ", but expected flow number " + expectedFlowNumber); 502 } 503 504 // Check and extract hash 505 if (!hashMessage.hasHashValue()) { 506 throw new HandshakeException("Hash message missing hash value"); 507 } 508 509 byte[] theirHash = hashMessage.getHashValue().toByteArray(); 510 byte[] theirCorrectHash = computeTheirKeyHash(isInitiator); 511 512 if (!constantTimeArrayEquals(theirCorrectHash, theirHash)) { 513 throw new HandshakeException("Hash message had incorrect hash value"); 514 } 515 516 if (isInitiator && hashMessage.hasPayload()) { 517 try { 518 DeviceToDeviceMessage message = D2DCryptoOps.decryptResponderHelloMessage( 519 new SecretKeySpec(sharedKey, "AES"), 520 hashMessage.getPayload().toByteArray()); 521 522 if (message.getSequenceNumber() != 1) { 523 throw new HandshakeException("Incorrect sequence number in responder hello"); 524 } 525 526 return message.getMessage().toByteArray(); 527 528 } catch (SignatureException e) { 529 throw new HandshakeException("Error recovering payload from hash message", e); 530 } 531 } 532 533 // empty/no payload 534 return new byte[0]; 535 } 536 537 private void parseCommitmentMessage(byte[] handshakeMessage, boolean isInitiator) 538 throws HandshakeException { 539 SpakeHandshakeMessage commitmentMessage; 540 541 // Parse the message 542 try { 543 commitmentMessage = SpakeHandshakeMessage.parseFrom(handshakeMessage); 544 } catch (InvalidProtocolBufferException e) { 545 throw new HandshakeException("Could not parse commitment message", e); 546 } 547 548 // Check flow number 549 if (!commitmentMessage.hasFlowNumber()) { 550 throw new HandshakeException("Commitment message missing flow number"); 551 } 552 if (commitmentMessage.getFlowNumber() != (isInitiator ? 2 : 1)) { 553 throw new HandshakeException("Commitment message has wrong flow number"); 554 } 555 556 // Check point and curve; and extract point 557 if (!commitmentMessage.hasEcPoint()) { 558 throw new HandshakeException("Commitment message missing point"); 559 } 560 EcPoint commitmentPoint = commitmentMessage.getEcPoint(); 561 if (!commitmentPoint.hasCurve() 562 || commitmentPoint.getCurve() != DeviceToDeviceMessagesProto.Curve.ED_25519) { 563 throw new HandshakeException("Commitment message has wrong curve"); 564 } 565 566 if (!commitmentPoint.hasX()) { 567 throw new HandshakeException("Commitment point missing x coordinate"); 568 } 569 570 if (!commitmentPoint.hasY()) { 571 throw new HandshakeException("Commitment point missing y coordinate"); 572 } 573 574 // Build the point 575 theirCommitmentPointAffine = new BigInteger[] { 576 new BigInteger(commitmentPoint.getX().toByteArray()), 577 new BigInteger(commitmentPoint.getY().toByteArray()) 578 }; 579 580 // Validate the point to be sure 581 try { 582 Ed25519.validateAffinePoint(theirCommitmentPointAffine); 583 theirCommitmentPointExtended = Ed25519.toExtended(theirCommitmentPointAffine); 584 } catch (Ed25519Exception e) { 585 throw new HandshakeException("Error validating their commitment point", e); 586 } 587 } 588 589 @Override 590 public D2DConnectionContext toConnectionContext() throws HandshakeException { 591 if (handshakeState == State.HANDSHAKE_ALREADY_USED) { 592 throw new HandshakeException("Cannot reuse handshake context; is has already been used"); 593 } 594 595 if (!isHandshakeComplete()) { 596 throw new HandshakeException("Handshake is not complete; cannot create connection context"); 597 } 598 599 handshakeState = State.HANDSHAKE_ALREADY_USED; 600 601 // Both sides start with an initial sequence number of 1 because the last message of the 602 // handshake had an optional payload with sequence number 1. D2DConnectionContext remembers 603 // the last sequence number used. 604 return new D2DConnectionContextV0( 605 new SecretKeySpec(sharedKey, "AES"), 1 /* initialSequenceNumber */); 606 } 607 608 /** 609 * Implementation of byte array concatenation copied from Guava. 610 */ 611 private static byte[] concat(byte[]... arrays) { 612 int length = 0; 613 for (byte[] array : arrays) { 614 length += array.length; 615 } 616 617 byte[] result = new byte[length]; 618 int pos = 0; 619 for (byte[] array : arrays) { 620 System.arraycopy(array, 0, result, pos, array.length); 621 pos += array.length; 622 } 623 624 return result; 625 } 626 627 private static byte[] hash(byte[] message) throws HandshakeException { 628 try { 629 return MessageDigest.getInstance(SHA256).digest(message); 630 } catch (NoSuchAlgorithmException e) { 631 throw new HandshakeException("Error performing hash", e); 632 } 633 } 634 635 private static boolean constantTimeArrayEquals(byte[] a, byte[] b) { 636 if (a == null || b == null) { 637 return (a == b); 638 } 639 if (a.length != b.length) { 640 return false; 641 } 642 byte result = 0; 643 for (int i = 0; i < b.length; i++) { 644 result = (byte) (result | (a[i] ^ b[i])); 645 } 646 return (result == 0); 647 } 648 } 649