| /* 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.ByteString; |
| import com.google.protobuf.InvalidProtocolBufferException; |
| import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2Alert; |
| import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2ClientFinished; |
| import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2ClientInit; |
| import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2ClientInit.CipherCommitment; |
| import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2Message; |
| import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2ServerInit; |
| import com.google.security.cryptauth.lib.securemessage.CryptoOps; |
| import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil; |
| import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.GenericPublicKey; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.UnsupportedEncodingException; |
| import java.security.InvalidKeyException; |
| import java.security.KeyPair; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.PublicKey; |
| import java.security.SecureRandom; |
| import java.security.spec.InvalidKeySpecException; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| import javax.annotation.Nullable; |
| import javax.crypto.SecretKey; |
| import javax.crypto.spec.SecretKeySpec; |
| |
| /** |
| * Implements UKEY2 and produces a {@link D2DConnectionContext}. |
| * |
| * <p>Client Usage: |
| * <code> |
| * try { |
| * Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); |
| * byte[] handshakeMessage; |
| * |
| * // Message 1 (Client Init) |
| * handshakeMessage = client.getNextHandshakeMessage(); |
| * sendMessageToServer(handshakeMessage); |
| * |
| * // Message 2 (Server Init) |
| * handshakeMessage = receiveMessageFromServer(); |
| * client.parseHandshakeMessage(handshakeMessage); |
| * |
| * // Message 3 (Client Finish) |
| * handshakeMessage = client.getNextHandshakeMessage(); |
| * sendMessageToServer(handshakeMessage); |
| * |
| * // Get the auth string |
| * byte[] clientAuthString = client.getVerificationString(STRING_LENGTH); |
| * showStringToUser(clientAuthString); |
| * |
| * // Using out-of-band channel, verify auth string, then call: |
| * client.verifyHandshake(); |
| * |
| * // Make a connection context |
| * D2DConnectionContext clientContext = client.toConnectionContext(); |
| * } catch (AlertException e) { |
| * log(e.getMessage); |
| * sendMessageToServer(e.getAlertMessageToSend()); |
| * } catch (HandshakeException e) { |
| * log(e); |
| * // terminate handshake |
| * } |
| * </code> |
| * |
| * <p>Server Usage: |
| * <code> |
| * try { |
| * Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); |
| * byte[] handshakeMessage; |
| * |
| * // Message 1 (Client Init) |
| * handshakeMessage = receiveMessageFromClient(); |
| * server.parseHandshakeMessage(handshakeMessage); |
| * |
| * // Message 2 (Server Init) |
| * handshakeMessage = server.getNextHandshakeMessage(); |
| * sendMessageToServer(handshakeMessage); |
| * |
| * // Message 3 (Client Finish) |
| * handshakeMessage = receiveMessageFromClient(); |
| * server.parseHandshakeMessage(handshakeMessage); |
| * |
| * // Get the auth string |
| * byte[] serverAuthString = server.getVerificationString(STRING_LENGTH); |
| * showStringToUser(serverAuthString); |
| * |
| * // Using out-of-band channel, verify auth string, then call: |
| * server.verifyHandshake(); |
| * |
| * // Make a connection context |
| * D2DConnectionContext serverContext = server.toConnectionContext(); |
| * } catch (AlertException e) { |
| * log(e.getMessage); |
| * sendMessageToClient(e.getAlertMessageToSend()); |
| * } catch (HandshakeException e) { |
| * log(e); |
| * // terminate handshake |
| * } |
| * </code> |
| */ |
| public class Ukey2Handshake { |
| |
| /** |
| * Creates a {@link Ukey2Handshake} with a particular cipher that can be used by an initiator / |
| * client. |
| * |
| * @throws HandshakeException |
| */ |
| public static Ukey2Handshake forInitiator(HandshakeCipher cipher) throws HandshakeException { |
| return new Ukey2Handshake(InternalState.CLIENT_START, cipher); |
| } |
| |
| /** |
| * Creates a {@link Ukey2Handshake} with a particular cipher that can be used by an responder / |
| * server. |
| * |
| * @throws HandshakeException |
| */ |
| public static Ukey2Handshake forResponder(HandshakeCipher cipher) throws HandshakeException { |
| return new Ukey2Handshake(InternalState.SERVER_START, cipher); |
| } |
| |
| /** |
| * Handshake States. Meaning of states: |
| * <ul> |
| * <li>IN_PROGRESS: The handshake is in progress, caller should use |
| * {@link Ukey2Handshake#getNextHandshakeMessage()} and |
| * {@link Ukey2Handshake#parseHandshakeMessage(byte[])} to continue the handshake. |
| * <li>VERIFICATION_NEEDED: The handshake is complete, but pending verification of the |
| * authentication string. Clients should use {@link Ukey2Handshake#getVerificationString(int)} to |
| * get the verification string and use out-of-band methods to authenticate the handshake. |
| * <li>VERIFICATION_IN_PROGRESS: The handshake is complete, verification string has been |
| * generated, but has not been confirmed. After authenticating the handshake out-of-band, use |
| * {@link Ukey2Handshake#verifyHandshake()} to mark the handshake as verified. |
| * <li>FINISHED: The handshake is finished, and caller can use |
| * {@link Ukey2Handshake#toConnectionContext()} to produce a {@link D2DConnectionContext}. |
| * <li>ALREADY_USED: The handshake has already been used and should be discarded / garbage |
| * collected. |
| * <li>ERROR: The handshake produced an error and should be destroyed. |
| * </ul> |
| */ |
| public enum State { |
| IN_PROGRESS, |
| VERIFICATION_NEEDED, |
| VERIFICATION_IN_PROGRESS, |
| FINISHED, |
| ALREADY_USED, |
| ERROR, |
| } |
| |
| /** |
| * Currently implemented UKEY2 handshake ciphers. Each cipher is a tuple consisting of a key |
| * negotiation cipher and a hash function used for a commitment. Currently the ciphers are: |
| * <code> |
| * +-----------------------------------------------------+ |
| * | Enum | Key negotiation | Hash function | |
| * +-------------+-----------------------+---------------+ |
| * | P256_SHA512 | ECDH using NIST P-256 | SHA512 | |
| * +-----------------------------------------------------+ |
| * </code> |
| * |
| * <p>Note that these should correspond to values in device_to_device_messages.proto. |
| */ |
| public enum HandshakeCipher { |
| P256_SHA512(UkeyProto.Ukey2HandshakeCipher.P256_SHA512); |
| // TODO(aczeskis): add CURVE25519_SHA512 |
| |
| private final UkeyProto.Ukey2HandshakeCipher value; |
| |
| HandshakeCipher(UkeyProto.Ukey2HandshakeCipher value) { |
| // Make sure we only accept values that are valid as per the ukey protobuf. |
| // NOTE: Don't use switch statement on value, as that will trigger a bug. b/30682989. |
| if (value == UkeyProto.Ukey2HandshakeCipher.P256_SHA512) { |
| this.value = value; |
| } else { |
| throw new IllegalArgumentException("Unknown cipher value: " + value); |
| } |
| } |
| |
| public UkeyProto.Ukey2HandshakeCipher getValue() { |
| return value; |
| } |
| } |
| |
| /** |
| * If thrown, this exception contains information that should be sent on the wire. Specifically, |
| * the {@link #getAlertMessageToSend()} method returns a <code>byte[]</code> that communicates the |
| * error to the other party in the handshake. Meanwhile, the {@link #getMessage()} method can be |
| * used to get a log-able error message. |
| */ |
| public static class AlertException extends Exception { |
| private final Ukey2Alert alertMessageToSend; |
| |
| public AlertException(String alertMessageToLog, Ukey2Alert alertMessageToSend) { |
| super(alertMessageToLog); |
| this.alertMessageToSend = alertMessageToSend; |
| } |
| |
| /** |
| * @return a message suitable for sending to other member of handshake. |
| */ |
| public byte[] getAlertMessageToSend() { |
| return alertMessageToSend.toByteArray(); |
| } |
| } |
| |
| // Maximum version of the handshake supported by this class. |
| public static final int VERSION = 1; |
| |
| // Random nonce is fixed at 32 bytes (as per go/ukey2). |
| private static final int NONCE_LENGTH_IN_BYTES = 32; |
| |
| private static final String UTF_8 = "UTF-8"; |
| |
| // Currently, we only support one next protocol. |
| private static final String NEXT_PROTOCOL = "AES_256_CBC-HMAC_SHA256"; |
| |
| // Clients need to store a map of message 3's (client finishes) for each commitment. |
| private final HashMap<HandshakeCipher, byte[]> rawMessage3Map = new HashMap<>(); |
| |
| private final HandshakeCipher handshakeCipher; |
| private final HandshakeRole handshakeRole; |
| private InternalState handshakeState; |
| private final KeyPair ourKeyPair; |
| private PublicKey theirPublicKey; |
| private SecretKey derivedSecretKey; |
| |
| // Servers need to store client commitments. |
| private byte[] theirCommitment; |
| |
| // We store the raw messages sent for computing the authentication strings and next key. |
| private byte[] rawMessage1; |
| private byte[] rawMessage2; |
| |
| // Enums for internal state machinery |
| private enum InternalState { |
| // Initiator/client state |
| CLIENT_START, |
| CLIENT_WAITING_FOR_SERVER_INIT, |
| CLIENT_AFTER_SERVER_INIT, |
| |
| // Responder/server state |
| SERVER_START, |
| SERVER_AFTER_CLIENT_INIT, |
| SERVER_WAITING_FOR_CLIENT_FINISHED, |
| |
| // Common completion state |
| HANDSHAKE_VERIFICATION_NEEDED, |
| HANDSHAKE_VERIFICATION_IN_PROGRESS, |
| HANDSHAKE_FINISHED, |
| HANDSHAKE_ALREADY_USED, |
| HANDSHAKE_ERROR, |
| } |
| |
| // Helps us remember our role in the handshake |
| private enum HandshakeRole { |
| CLIENT, |
| SERVER |
| } |
| |
| /** |
| * Never invoked directly. Caller should use {@link #forInitiator(HandshakeCipher)} or |
| * {@link #forResponder(HandshakeCipher)} instead. |
| * |
| * @throws HandshakeException if an unrecoverable error occurs and the connection should be shut |
| * down. |
| */ |
| private Ukey2Handshake(InternalState state, HandshakeCipher cipher) throws HandshakeException { |
| if (cipher == null) { |
| throwIllegalArgumentException("Invalid handshake cipher"); |
| } |
| this.handshakeCipher = cipher; |
| |
| switch (state) { |
| case CLIENT_START: |
| handshakeRole = HandshakeRole.CLIENT; |
| break; |
| case SERVER_START: |
| handshakeRole = HandshakeRole.SERVER; |
| break; |
| default: |
| throwIllegalStateException("Invalid handshake state"); |
| handshakeRole = null; // unreachable, but makes compiler happy |
| } |
| this.handshakeState = state; |
| |
| this.ourKeyPair = genKeyPair(cipher); |
| } |
| |
| /** |
| * Get the next handshake message suitable for sending on the wire. |
| * |
| * @throws HandshakeException if an unrecoverable error occurs and the connection should be shut |
| * down. |
| */ |
| public byte[] getNextHandshakeMessage() throws HandshakeException { |
| switch (handshakeState) { |
| case CLIENT_START: |
| rawMessage1 = makeUkey2Message(Ukey2Message.Type.CLIENT_INIT, makeClientInitMessage()); |
| handshakeState = InternalState.CLIENT_WAITING_FOR_SERVER_INIT; |
| return rawMessage1; |
| |
| case SERVER_AFTER_CLIENT_INIT: |
| rawMessage2 = makeUkey2Message(Ukey2Message.Type.SERVER_INIT, makeServerInitMessage()); |
| handshakeState = InternalState.SERVER_WAITING_FOR_CLIENT_FINISHED; |
| return rawMessage2; |
| |
| case CLIENT_AFTER_SERVER_INIT: |
| // Make sure we have a message 3 for the chosen cipher. |
| if (!rawMessage3Map.containsKey(handshakeCipher)) { |
| throwIllegalStateException( |
| "Client state is CLIENT_AFTER_SERVER_INIT, and cipher is " |
| + handshakeCipher |
| + ", but no corresponding raw client finished message has been generated"); |
| } |
| handshakeState = InternalState.HANDSHAKE_VERIFICATION_NEEDED; |
| return rawMessage3Map.get(handshakeCipher); |
| |
| default: |
| throwIllegalStateException("Cannot get next message in state: " + handshakeState); |
| return null; // unreachable, but makes compiler happy |
| } |
| } |
| |
| /** |
| * Returns an authentication string suitable for authenticating the handshake out-of-band. Note |
| * that the authentication string can be short (e.g., a 6 digit visual confirmation code). Note: |
| * this should only be called when the state returned byte {@link #getHandshakeState()} is |
| * {@link State#VERIFICATION_NEEDED}, which means this can only be called once. |
| * |
| * @param byteLength length of output in bytes. Min length is 1; max length is 32. |
| */ |
| public byte[] getVerificationString(int byteLength) throws HandshakeException { |
| if (byteLength < 1 || byteLength > 32) { |
| throwIllegalArgumentException("Minimum length is 1 byte, max is 32 bytes"); |
| } |
| |
| if (handshakeState != InternalState.HANDSHAKE_VERIFICATION_NEEDED) { |
| throwIllegalStateException("Unexpected state: " + handshakeState); |
| } |
| |
| try { |
| derivedSecretKey = |
| EnrollmentCryptoOps.doKeyAgreement(ourKeyPair.getPrivate(), theirPublicKey); |
| } catch (InvalidKeyException e) { |
| // unreachable in practice |
| throwHandshakeException(e); |
| } |
| |
| ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); |
| try { |
| byteStream.write(rawMessage1); |
| byteStream.write(rawMessage2); |
| } catch (IOException e) { |
| // unreachable in practice |
| throwHandshakeException(e); |
| } |
| byte[] info = byteStream.toByteArray(); |
| |
| byte[] salt = null; |
| |
| try { |
| salt = "UKEY2 v1 auth".getBytes(UTF_8); |
| } catch (UnsupportedEncodingException e) { |
| // unreachable in practice |
| throwHandshakeException(e); |
| } |
| |
| byte[] authString = null; |
| try { |
| authString = CryptoOps.hkdf(derivedSecretKey, salt, info); |
| } catch (InvalidKeyException | NoSuchAlgorithmException e) { |
| // unreachable in practice |
| throwHandshakeException(e); |
| } |
| |
| handshakeState = InternalState.HANDSHAKE_VERIFICATION_IN_PROGRESS; |
| return Arrays.copyOf(authString, byteLength); |
| } |
| |
| /** |
| * Invoked to let handshake state machine know that caller has validated the authentication |
| * string obtained via {@link #getVerificationString(int)}; Note: this should only be called when |
| * the state returned byte {@link #getHandshakeState()} is {@link State#VERIFICATION_IN_PROGRESS}. |
| */ |
| public void verifyHandshake() { |
| if (handshakeState != InternalState.HANDSHAKE_VERIFICATION_IN_PROGRESS) { |
| throwIllegalStateException("Unexpected state: " + handshakeState); |
| } |
| handshakeState = InternalState.HANDSHAKE_FINISHED; |
| } |
| |
| /** |
| * Parses the given handshake message. |
| * @throws AlertException if an error occurs that should be sent to other party. |
| * @throws HandshakeException in an error occurs and the connection should be torn down. |
| */ |
| public void parseHandshakeMessage(byte[] handshakeMessage) |
| throws AlertException, HandshakeException { |
| switch (handshakeState) { |
| case SERVER_START: |
| parseMessage1(handshakeMessage); |
| handshakeState = InternalState.SERVER_AFTER_CLIENT_INIT; |
| break; |
| |
| case CLIENT_WAITING_FOR_SERVER_INIT: |
| parseMessage2(handshakeMessage); |
| handshakeState = InternalState.CLIENT_AFTER_SERVER_INIT; |
| break; |
| |
| case SERVER_WAITING_FOR_CLIENT_FINISHED: |
| parseMessage3(handshakeMessage); |
| handshakeState = InternalState.HANDSHAKE_VERIFICATION_NEEDED; |
| break; |
| |
| default: |
| throwIllegalStateException("Cannot parse message in state " + handshakeState); |
| } |
| } |
| |
| /** |
| * Returns the current state of the handshake. See {@link State}. |
| */ |
| public State getHandshakeState() { |
| switch (handshakeState) { |
| case CLIENT_START: |
| case CLIENT_WAITING_FOR_SERVER_INIT: |
| case CLIENT_AFTER_SERVER_INIT: |
| case SERVER_START: |
| case SERVER_WAITING_FOR_CLIENT_FINISHED: |
| case SERVER_AFTER_CLIENT_INIT: |
| // fallback intended -- these are all in-progress states |
| return State.IN_PROGRESS; |
| |
| case HANDSHAKE_ERROR: |
| return State.ERROR; |
| |
| case HANDSHAKE_VERIFICATION_NEEDED: |
| return State.VERIFICATION_NEEDED; |
| |
| case HANDSHAKE_VERIFICATION_IN_PROGRESS: |
| return State.VERIFICATION_IN_PROGRESS; |
| |
| case HANDSHAKE_FINISHED: |
| return State.FINISHED; |
| |
| case HANDSHAKE_ALREADY_USED: |
| return State.ALREADY_USED; |
| |
| default: |
| // unreachable in practice |
| throwIllegalStateException("Unknown state"); |
| return null; // really unreachable, but makes compiler happy |
| } |
| } |
| |
| /** |
| * Can be called to generate a {@link D2DConnectionContext}. Note: this should only be called |
| * when the state returned byte {@link #getHandshakeState()} is {@link State#FINISHED}. |
| * |
| * @throws HandshakeException |
| */ |
| public D2DConnectionContext toConnectionContext() throws HandshakeException { |
| switch (handshakeState) { |
| case HANDSHAKE_ERROR: |
| throwIllegalStateException("Cannot make context; handshake had error"); |
| return null; // makes linter happy |
| case HANDSHAKE_ALREADY_USED: |
| throwIllegalStateException("Cannot reuse handshake context; is has already been used"); |
| return null; // makes linter happy |
| case HANDSHAKE_VERIFICATION_NEEDED: |
| throwIllegalStateException("Handshake not verified, cannot create context"); |
| return null; // makes linter happy |
| case HANDSHAKE_FINISHED: |
| // We're done, okay to return a context |
| break; |
| default: |
| // unreachable in practice |
| throwIllegalStateException("Handshake is not complete; cannot create connection context"); |
| } |
| |
| if (derivedSecretKey == null) { |
| throwIllegalStateException("Unexpected state error: derived key is null"); |
| } |
| |
| ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); |
| try { |
| byteStream.write(rawMessage1); |
| byteStream.write(rawMessage2); |
| } catch (IOException e) { |
| // unreachable in practice |
| throwHandshakeException(e); |
| } |
| byte[] info = byteStream.toByteArray(); |
| |
| byte[] salt = null; |
| try { |
| salt = "UKEY2 v1 next".getBytes(UTF_8); |
| } catch (UnsupportedEncodingException e) { |
| // unreachable |
| throwHandshakeException(e); |
| } |
| |
| SecretKey nextProtocolKey = null; |
| try { |
| nextProtocolKey = new SecretKeySpec(CryptoOps.hkdf(derivedSecretKey, salt, info), "AES"); |
| } catch (InvalidKeyException | NoSuchAlgorithmException e) { |
| // unreachable in practice |
| throwHandshakeException(e); |
| } |
| |
| SecretKey clientKey = null; |
| SecretKey serverKey = null; |
| try { |
| clientKey = D2DCryptoOps.deriveNewKeyForPurpose(nextProtocolKey, "client"); |
| serverKey = D2DCryptoOps.deriveNewKeyForPurpose(nextProtocolKey, "server"); |
| } catch (InvalidKeyException | NoSuchAlgorithmException e) { |
| // unreachable in practice |
| throwHandshakeException(e); |
| } |
| |
| handshakeState = InternalState.HANDSHAKE_ALREADY_USED; |
| |
| return new D2DConnectionContextV1( |
| handshakeRole == HandshakeRole.CLIENT ? clientKey : serverKey, |
| handshakeRole == HandshakeRole.CLIENT ? serverKey : clientKey, |
| 0 /* initial encode sequence number */, |
| 0 /* initial decode sequence number */); |
| } |
| |
| /** |
| * Generates the byte[] encoding of a {@link Ukey2ClientInit} message. |
| * |
| * @throws HandshakeException |
| */ |
| private byte[] makeClientInitMessage() throws HandshakeException { |
| Ukey2ClientInit.Builder clientInit = Ukey2ClientInit.newBuilder(); |
| clientInit.setVersion(VERSION); |
| clientInit.setRandom(ByteString.copyFrom(generateRandomNonce())); |
| clientInit.setNextProtocol(NEXT_PROTOCOL); |
| |
| // At the moment, we only support one cipher |
| clientInit.addCipherCommitments(generateP256SHA512Commitment()); |
| |
| return clientInit.build().toByteArray(); |
| } |
| |
| /** |
| * Generates the byte[] encoding of a {@link Ukey2ServerInit} message. |
| */ |
| private byte[] makeServerInitMessage() { |
| Ukey2ServerInit.Builder serverInit = Ukey2ServerInit.newBuilder(); |
| serverInit.setVersion(VERSION); |
| serverInit.setRandom(ByteString.copyFrom(generateRandomNonce())); |
| serverInit.setHandshakeCipher(handshakeCipher.getValue()); |
| serverInit.setPublicKey( |
| PublicKeyProtoUtil.encodePublicKey(ourKeyPair.getPublic()).toByteString()); |
| |
| return serverInit.build().toByteArray(); |
| } |
| |
| /** |
| * Generates a keypair for the provided handshake cipher. Currently only P256_SHA512 is |
| * supported. |
| * |
| * @throws HandshakeException |
| */ |
| private KeyPair genKeyPair(HandshakeCipher cipher) throws HandshakeException { |
| switch (cipher) { |
| case P256_SHA512: |
| return PublicKeyProtoUtil.generateEcP256KeyPair(); |
| default: |
| // Should never happen |
| throwHandshakeException("unknown cipher: " + cipher); |
| } |
| return null; // unreachable, but makes compiler happy |
| } |
| |
| /** |
| * Attempts to parse message 1 (which is a wrapped {@link Ukey2ClientInit}). See go/ukey2 for |
| * details. |
| * |
| * @throws AlertException if an error occurs |
| */ |
| private void parseMessage1(byte[] handshakeMessage) throws AlertException, HandshakeException { |
| // Deserialize the protobuf; send a BAD_MESSAGE message if deserialization fails |
| Ukey2Message message = null; |
| try { |
| message = Ukey2Message.parseFrom(handshakeMessage); |
| } catch (InvalidProtocolBufferException e) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_MESSAGE, |
| "Can't parse message 1 " + e.getMessage()); |
| } |
| |
| // Verify that message_type == Type.CLIENT_INIT; send a BAD_MESSAGE_TYPE message if mismatch |
| if (!message.hasMessageType() || message.getMessageType() != Ukey2Message.Type.CLIENT_INIT) { |
| throwAlertException( |
| Ukey2Alert.AlertType.BAD_MESSAGE_TYPE, |
| "Expected, but did not find ClientInit message type"); |
| } |
| |
| // Deserialize message_data as a ClientInit message; send a BAD_MESSAGE_DATA message if |
| // deserialization fails |
| if (!message.hasMessageData()) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_MESSAGE_DATA, |
| "Expected message data, but didn't find it"); |
| } |
| Ukey2ClientInit clientInit = null; |
| try { |
| clientInit = Ukey2ClientInit.parseFrom(message.getMessageData()); |
| } catch (InvalidProtocolBufferException e) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_MESSAGE_DATA, |
| "Can't parse message data into ClientInit"); |
| } |
| |
| // Check that version == VERSION; send BAD_VERSION message if mismatch |
| if (!clientInit.hasVersion()) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_VERSION, "ClientInit missing version"); |
| } |
| if (clientInit.getVersion() != VERSION) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_VERSION, "ClientInit version mismatch"); |
| } |
| |
| // Check that random is exactly NONCE_LENGTH_IN_BYTES bytes; send Alert.BAD_RANDOM message if |
| // not. |
| if (!clientInit.hasRandom()) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_RANDOM, "ClientInit missing random"); |
| } |
| if (clientInit.getRandom().toByteArray().length != NONCE_LENGTH_IN_BYTES) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_RANDOM, "ClientInit has incorrect nonce length"); |
| } |
| |
| // Check to see if any of the handshake_cipher in cipher_commitment are acceptable. Servers |
| // should select the first handshake_cipher that it finds acceptable to support clients |
| // signaling deprecated but supported HandshakeCiphers. If no handshake_cipher is acceptable |
| // (or there are no HandshakeCiphers in the message), the server sends a BAD_HANDSHAKE_CIPHER |
| // message |
| List<Ukey2ClientInit.CipherCommitment> commitments = clientInit.getCipherCommitmentsList(); |
| if (commitments.isEmpty()) { |
| throwAlertException( |
| Ukey2Alert.AlertType.BAD_HANDSHAKE_CIPHER, "ClientInit is missing cipher commitments"); |
| } |
| for (Ukey2ClientInit.CipherCommitment commitment : commitments) { |
| if (!commitment.hasHandshakeCipher() |
| || !commitment.hasCommitment()) { |
| throwAlertException( |
| Ukey2Alert.AlertType.BAD_HANDSHAKE_CIPHER, |
| "ClientInit has improperly formatted cipher commitment"); |
| } |
| |
| // TODO(aczeskis): for now we only support one cipher, eventually support more |
| if (commitment.getHandshakeCipher() == handshakeCipher.getValue()) { |
| theirCommitment = commitment.getCommitment().toByteArray(); |
| } |
| } |
| if (theirCommitment == null) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_HANDSHAKE_CIPHER, |
| "No acceptable commitments found"); |
| } |
| |
| // Checks that next_protocol contains a protocol that the server supports. Send a |
| // BAD_NEXT_PROTOCOL message if not. We currently only support one protocol |
| if (!clientInit.hasNextProtocol() || !NEXT_PROTOCOL.equals(clientInit.getNextProtocol())) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_NEXT_PROTOCOL, "Incorrect next protocol"); |
| } |
| |
| // Store raw message for AUTH_STRING computation |
| rawMessage1 = handshakeMessage; |
| } |
| |
| /** |
| * Attempts to parse message 2 (which is a wrapped {@link Ukey2ServerInit}). See go/ukey2 for |
| * details. |
| */ |
| private void parseMessage2(final byte[] handshakeMessage) |
| throws AlertException, HandshakeException { |
| // Deserialize the protobuf; send a BAD_MESSAGE message if deserialization fails |
| Ukey2Message message = null; |
| try { |
| message = Ukey2Message.parseFrom(handshakeMessage); |
| } catch (InvalidProtocolBufferException e) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_MESSAGE, |
| "Can't parse message 2 " + e.getMessage()); |
| } |
| |
| // Verify that message_type == Type.SERVER_INIT; send a BAD_MESSAGE_TYPE message if mismatch |
| if (!message.hasMessageType()) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_MESSAGE_TYPE, |
| "Expected, but did not find message type"); |
| } |
| if (message.getMessageType() == Ukey2Message.Type.ALERT) { |
| handshakeState = InternalState.HANDSHAKE_ERROR; |
| throwHandshakeMessageFromAlertMessage(message); |
| } |
| if (message.getMessageType() != Ukey2Message.Type.SERVER_INIT) { |
| throwAlertException( |
| Ukey2Alert.AlertType.BAD_MESSAGE_TYPE, |
| "Expected, but did not find SERVER_INIT message type"); |
| } |
| |
| // Deserialize message_data as a ServerInit message; send a BAD_MESSAGE_DATA message if |
| // deserialization fails |
| if (!message.hasMessageData()) { |
| |
| throwAlertException(Ukey2Alert.AlertType.BAD_MESSAGE_DATA, |
| "Expected message data, but didn't find it"); |
| } |
| Ukey2ServerInit serverInit = null; |
| try { |
| serverInit = Ukey2ServerInit.parseFrom(message.getMessageData()); |
| } catch (InvalidProtocolBufferException e) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_MESSAGE_DATA, |
| "Can't parse message data into ServerInit"); |
| } |
| |
| // Check that version == VERSION; send BAD_VERSION message if mismatch |
| if (!serverInit.hasVersion()) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_VERSION, "ServerInit missing version"); |
| } |
| if (serverInit.getVersion() != VERSION) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_VERSION, "ServerInit version mismatch"); |
| } |
| |
| // Check that random is exactly NONCE_LENGTH_IN_BYTES bytes; send Alert.BAD_RANDOM message if |
| // not. |
| if (!serverInit.hasRandom()) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_RANDOM, "ServerInit missing random"); |
| } |
| if (serverInit.getRandom().toByteArray().length != NONCE_LENGTH_IN_BYTES) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_RANDOM, "ServerInit has incorrect nonce length"); |
| } |
| |
| // Check that handshake_cipher matches a handshake cipher that was sent in |
| // ClientInit.cipher_commitments. If not, send a BAD_HANDSHAKECIPHER message |
| if (!serverInit.hasHandshakeCipher()) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_HANDSHAKE_CIPHER, "No handshake cipher found"); |
| } |
| HandshakeCipher serverCipher = null; |
| for (HandshakeCipher cipher : HandshakeCipher.values()) { |
| if (cipher.getValue() == serverInit.getHandshakeCipher()) { |
| serverCipher = cipher; |
| break; |
| } |
| } |
| if (serverCipher == null || serverCipher != handshakeCipher) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_HANDSHAKE_CIPHER, |
| "No acceptable handshake cipher found"); |
| } |
| |
| // Check that public_key parses into a correct public key structure. If not, send a |
| // BAD_PUBLIC_KEY message. |
| if (!serverInit.hasPublicKey()) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_PUBLIC_KEY, "No public key found in ServerInit"); |
| } |
| theirPublicKey = parseP256PublicKey(serverInit.getPublicKey().toByteArray()); |
| |
| // Store raw message for AUTH_STRING computation |
| rawMessage2 = handshakeMessage; |
| } |
| |
| /** |
| * Attempts to parse message 3 (which is a wrapped {@link Ukey2ClientFinished}). See go/ukey2 for |
| * details. |
| */ |
| private void parseMessage3(final byte[] handshakeMessage) throws HandshakeException { |
| // Deserialize the protobuf; terminate the connection if deserialization fails. |
| Ukey2Message message = null; |
| try { |
| message = Ukey2Message.parseFrom(handshakeMessage); |
| } catch (InvalidProtocolBufferException e) { |
| throwHandshakeException("Can't parse message 3", e); |
| } |
| |
| // Verify that message_type == Type.CLIENT_FINISH; terminate connection if mismatch occurs |
| if (!message.hasMessageType()) { |
| throw new HandshakeException("Expected, but did not find message type"); |
| } |
| if (message.getMessageType() == Ukey2Message.Type.ALERT) { |
| throwHandshakeMessageFromAlertMessage(message); |
| } |
| if (message.getMessageType() != Ukey2Message.Type.CLIENT_FINISH) { |
| throwHandshakeException("Expected, but did not find CLIENT_FINISH message type"); |
| } |
| |
| // Verify that the hash of the ClientFinished matches the expected commitment from ClientInit. |
| // Terminate the connection if the expected match fails. |
| verifyCommitment(handshakeMessage); |
| |
| // Deserialize message_data as a ClientFinished message; terminate the connection if |
| // deserialization fails. |
| if (!message.hasMessageData()) { |
| throwHandshakeException("Expected message data, but didn't find it"); |
| } |
| Ukey2ClientFinished clientFinished = null; |
| try { |
| clientFinished = Ukey2ClientFinished.parseFrom(message.getMessageData()); |
| } catch (InvalidProtocolBufferException e) { |
| throwHandshakeException(e); |
| } |
| |
| // Check that public_key parses into a correct public key structure. If not, terminate the |
| // connection. |
| if (!clientFinished.hasPublicKey()) { |
| throwHandshakeException("No public key found in ClientFinished"); |
| } |
| try { |
| theirPublicKey = parseP256PublicKey(clientFinished.getPublicKey().toByteArray()); |
| } catch (AlertException e) { |
| // Wrap in a HandshakeException because error should not be sent on the wire. |
| throwHandshakeException(e); |
| } |
| } |
| |
| private void verifyCommitment(byte[] handshakeMessage) throws HandshakeException { |
| byte[] actualClientFinishHash = null; |
| switch (handshakeCipher) { |
| case P256_SHA512: |
| actualClientFinishHash = sha512(handshakeMessage); |
| break; |
| default: |
| // should be unreachable |
| throwIllegalStateException("Unexpected handshakeCipher"); |
| } |
| |
| // Time constant after Java SE 6 Update 17 |
| // See http://www.oracle.com/technetwork/java/javase/6u17-141447.html |
| if (!MessageDigest.isEqual(actualClientFinishHash, theirCommitment)) { |
| throwHandshakeException("Commitment does not match"); |
| } |
| } |
| |
| private void throwHandshakeMessageFromAlertMessage(Ukey2Message message) |
| throws HandshakeException { |
| if (message.hasMessageData()) { |
| Ukey2Alert alert = null; |
| try { |
| alert = Ukey2Alert.parseFrom(message.getMessageData()); |
| } catch (InvalidProtocolBufferException e) { |
| throwHandshakeException("Cannot parse alert message", e); |
| } |
| |
| if (alert.hasType() && alert.hasErrorMessage()) { |
| throwHandshakeException( |
| "Received Alert message. Type: " |
| + alert.getType() |
| + " Error Message: " |
| + alert.getErrorMessage()); |
| } else if (alert.hasType()) { |
| throwHandshakeException("Received Alert message. Type: " + alert.getType()); |
| } |
| } |
| |
| throwHandshakeException("Received empty Alert Message"); |
| } |
| |
| /** |
| * Parses an encoded public P256 key. |
| */ |
| private PublicKey parseP256PublicKey(byte[] encodedPublicKey) |
| throws AlertException, HandshakeException { |
| try { |
| return PublicKeyProtoUtil.parsePublicKey(GenericPublicKey.parseFrom(encodedPublicKey)); |
| } catch (InvalidProtocolBufferException | InvalidKeySpecException e) { |
| throwAlertException(Ukey2Alert.AlertType.BAD_PUBLIC_KEY, |
| "Cannot parse public key: " + e.getMessage()); |
| return null; // unreachable, but makes compiler happy |
| } |
| } |
| |
| /** |
| * Generates a {@link CipherCommitment} for the P256_SHA512 cipher. |
| */ |
| private CipherCommitment generateP256SHA512Commitment() throws HandshakeException { |
| // Generate the corresponding finished message if it's not done yet |
| if (!rawMessage3Map.containsKey(HandshakeCipher.P256_SHA512)) { |
| generateP256SHA512ClientFinished(ourKeyPair); |
| } |
| |
| CipherCommitment.Builder cipherCommitment = CipherCommitment.newBuilder(); |
| cipherCommitment.setHandshakeCipher(UkeyProto.Ukey2HandshakeCipher.P256_SHA512); |
| cipherCommitment.setCommitment( |
| ByteString.copyFrom(sha512(rawMessage3Map.get(HandshakeCipher.P256_SHA512)))); |
| |
| return cipherCommitment.build(); |
| } |
| |
| /** |
| * Generates and records a {@link Ukey2ClientFinished} message for the P256_SHA512 cipher. |
| */ |
| private Ukey2ClientFinished generateP256SHA512ClientFinished(KeyPair p256KeyPair) { |
| byte[] encodedKey = PublicKeyProtoUtil.encodePublicKey(p256KeyPair.getPublic()).toByteArray(); |
| |
| Ukey2ClientFinished.Builder clientFinished = Ukey2ClientFinished.newBuilder(); |
| clientFinished.setPublicKey(ByteString.copyFrom(encodedKey)); |
| |
| rawMessage3Map.put( |
| HandshakeCipher.P256_SHA512, |
| makeUkey2Message(Ukey2Message.Type.CLIENT_FINISH, clientFinished.build().toByteArray())); |
| |
| return clientFinished.build(); |
| } |
| |
| /** |
| * Generates the serialized representation of a {@link Ukey2Message} based on the provided type |
| * and data. |
| */ |
| private byte[] makeUkey2Message(Ukey2Message.Type messageType, byte[] messageData) { |
| Ukey2Message.Builder message = Ukey2Message.newBuilder(); |
| |
| switch (messageType) { |
| case ALERT: |
| case CLIENT_INIT: |
| case SERVER_INIT: |
| case CLIENT_FINISH: |
| // fall through intentional; valid message types |
| break; |
| default: |
| throwIllegalArgumentException("Invalid message type: " + messageType); |
| } |
| message.setMessageType(messageType); |
| |
| // Alerts a blank message data field |
| if (messageType != Ukey2Message.Type.ALERT) { |
| if (messageData == null || messageData.length == 0) { |
| throwIllegalArgumentException("Cannot send empty message data for non-alert messages"); |
| } |
| message.setMessageData(ByteString.copyFrom(messageData)); |
| } |
| |
| return message.build().toByteArray(); |
| } |
| |
| /** |
| * Returns a {@link Ukey2Alert} message of given type and having the loggable additional data if |
| * present. |
| */ |
| private Ukey2Alert makeAlertMessage(Ukey2Alert.AlertType alertType, |
| @Nullable String loggableAdditionalData) throws HandshakeException { |
| switch (alertType) { |
| case BAD_MESSAGE: |
| case BAD_MESSAGE_TYPE: |
| case INCORRECT_MESSAGE: |
| case BAD_MESSAGE_DATA: |
| case BAD_VERSION: |
| case BAD_RANDOM: |
| case BAD_HANDSHAKE_CIPHER: |
| case BAD_NEXT_PROTOCOL: |
| case BAD_PUBLIC_KEY: |
| case INTERNAL_ERROR: |
| // fall through intentional; valid alert types |
| break; |
| default: |
| throwHandshakeException("Unknown alert type: " + alertType); |
| } |
| |
| Ukey2Alert.Builder alert = Ukey2Alert.newBuilder(); |
| alert.setType(alertType); |
| |
| if (loggableAdditionalData != null) { |
| alert.setErrorMessage(loggableAdditionalData); |
| } |
| |
| return alert.build(); |
| } |
| |
| /** |
| * Generates a cryptoraphically random nonce of NONCE_LENGTH_IN_BYTES bytes. |
| */ |
| private static byte[] generateRandomNonce() { |
| SecureRandom rng = new SecureRandom(); |
| byte[] randomNonce = new byte[NONCE_LENGTH_IN_BYTES]; |
| rng.nextBytes(randomNonce); |
| return randomNonce; |
| } |
| |
| /** |
| * Handy wrapper to do SHA512. |
| */ |
| private byte[] sha512(byte[] input) throws HandshakeException { |
| MessageDigest sha512; |
| try { |
| sha512 = MessageDigest.getInstance("SHA-512"); |
| return sha512.digest(input); |
| } catch (NoSuchAlgorithmException e) { |
| throwHandshakeException("No security provider initialized yet?", e); |
| return null; // unreachable in practice, but makes compiler happy |
| } |
| } |
| |
| // Exception wrappers that remember to set the handshake state to ERROR |
| |
| private void throwAlertException(Ukey2Alert.AlertType alertType, String alertLogStatement) |
| throws AlertException, HandshakeException { |
| handshakeState = InternalState.HANDSHAKE_ERROR; |
| throw new AlertException(alertLogStatement, makeAlertMessage(alertType, alertLogStatement)); |
| } |
| |
| private void throwHandshakeException(String logMessage) throws HandshakeException { |
| handshakeState = InternalState.HANDSHAKE_ERROR; |
| throw new HandshakeException(logMessage); |
| } |
| |
| private void throwHandshakeException(Exception e) throws HandshakeException { |
| handshakeState = InternalState.HANDSHAKE_ERROR; |
| throw new HandshakeException(e); |
| } |
| |
| private void throwHandshakeException(String logMessage, Exception e) throws HandshakeException { |
| handshakeState = InternalState.HANDSHAKE_ERROR; |
| throw new HandshakeException(logMessage, e); |
| } |
| |
| private void throwIllegalStateException(String logMessage) { |
| handshakeState = InternalState.HANDSHAKE_ERROR; |
| throw new IllegalStateException(logMessage); |
| } |
| |
| private void throwIllegalArgumentException(String logMessage) { |
| handshakeState = InternalState.HANDSHAKE_ERROR; |
| throw new IllegalArgumentException(logMessage); |
| } |
| } |