blob: 09023a5f4c369d2f5ea59d7c26a4b5dcf1f55752 [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.TransportCryptoOps.Payload;
import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.PayloadType;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.util.Arrays;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
/**
* The full context of a secure connection. This object has methods to encode and decode messages
* that are to be sent to another device.
*
* Subclasses keep track of the keys shared with the other device, and of the sequence in which the
* messages are expected.
*/
public abstract class D2DConnectionContext {
private static final String UTF8 = "UTF-8";
private final int protocolVersion;
protected D2DConnectionContext(int protocolVersion) {
this.protocolVersion = protocolVersion;
}
/**
* @return the version of the D2D protocol.
*/
public int getProtocolVersion() {
return protocolVersion;
}
/**
* Once initiator and responder have exchanged public keys, use this method to encrypt and
* sign a payload. Both initiator and responder devices can use this message.
*
* @param payload the payload that should be encrypted.
*/
public byte[] encodeMessageToPeer(byte[] payload) {
incrementSequenceNumberForEncoding();
DeviceToDeviceMessage message = createDeviceToDeviceMessage(
payload, getSequenceNumberForEncoding());
try {
return D2DCryptoOps.signcryptPayload(
new Payload(PayloadType.DEVICE_TO_DEVICE_MESSAGE,
message.toByteArray()),
getEncodeKey());
} catch (InvalidKeyException e) {
// should never happen, since we agreed on the key earlier
throw new RuntimeException(e);
} catch (NoSuchAlgorithmException e) {
// should never happen
throw new RuntimeException(e);
}
}
/**
* Encrypting/signing a string for transmission to another device.
*
* @see #encodeMessageToPeer(byte[])
*
* @param payload the payload that should be encrypted.
*/
public byte[] encodeMessageToPeer(String payload) {
try {
return encodeMessageToPeer(payload.getBytes(UTF8));
} catch (UnsupportedEncodingException e) {
// Should never happen - we should always be able to UTF-8-encode a string
throw new RuntimeException(e);
}
}
/**
* Once InitiatorHello and ResponderHello(AndPayload) are exchanged, use this method
* to decrypt and verify a message received from the other device. Both initiator and
* responder device can use this message.
*
* @param message the message that should be encrypted.
* @throws SignatureException if the message from the remote peer did not pass verification
*/
public byte[] decodeMessageFromPeer(byte[] message) throws SignatureException {
try {
Payload payload = D2DCryptoOps.verifydecryptPayload(message, getDecodeKey());
if (!PayloadType.DEVICE_TO_DEVICE_MESSAGE.equals(payload.getPayloadType())) {
throw new SignatureException("wrong message type in device-to-device message");
}
DeviceToDeviceMessage messageProto = DeviceToDeviceMessage.parseFrom(payload.getMessage());
incrementSequenceNumberForDecoding();
if (messageProto.getSequenceNumber() != getSequenceNumberForDecoding()) {
throw new SignatureException("Incorrect sequence number");
}
return messageProto.getMessage().toByteArray();
} catch (InvalidKeyException e) {
throw new SignatureException(e);
} catch (NoSuchAlgorithmException e) {
// this shouldn't happen - the algorithms are hard-coded.
throw new RuntimeException(e);
} catch (InvalidProtocolBufferException e) {
throw new SignatureException(e);
}
}
/**
* Once InitiatorHello and ResponderHello(AndPayload) are exchanged, use this method
* to decrypt and verify a message received from the other device. Both initiator and
* responder device can use this message.
*
* @param message the message that should be encrypted.
*/
public String decodeMessageFromPeerAsString(byte[] message) throws SignatureException {
try {
return new String(decodeMessageFromPeer(message), UTF8);
} catch (UnsupportedEncodingException e) {
// Should never happen - we should always be able to UTF-8-encode a string
throw new RuntimeException(e);
}
}
// package-private
static DeviceToDeviceMessage createDeviceToDeviceMessage(byte[] message, int sequenceNumber) {
DeviceToDeviceMessage.Builder deviceToDeviceMessage = DeviceToDeviceMessage.newBuilder();
deviceToDeviceMessage.setSequenceNumber(sequenceNumber);
deviceToDeviceMessage.setMessage(ByteString.copyFrom(message));
return deviceToDeviceMessage.build();
}
/**
* Returns a cryptographic digest (SHA256) of the session keys prepended by the SHA256 hash
* of the ASCII string "D2D"
* @throws NoSuchAlgorithmException if SHA 256 doesn't exist on this platform
*/
public abstract byte[] getSessionUnique() throws NoSuchAlgorithmException;
/**
* Increments the sequence number used for encoding messages.
*/
protected abstract void incrementSequenceNumberForEncoding();
/**
* Increments the sequence number used for decoding messages.
*/
protected abstract void incrementSequenceNumberForDecoding();
/**
* @return the last sequence number used to encode a message.
*/
@VisibleForTesting
abstract int getSequenceNumberForEncoding();
/**
* @return the last sequence number used to decode a message.
*/
@VisibleForTesting
abstract int getSequenceNumberForDecoding();
/**
* @return the {@link SecretKey} used for encoding messages.
*/
@VisibleForTesting
abstract SecretKey getEncodeKey();
/**
* @return the {@link SecretKey} used for decoding messages.
*/
@VisibleForTesting
abstract SecretKey getDecodeKey();
/**
* Creates a saved session that can later be used for resumption. Note, this must be stored in a
* secure location.
*
* @return the saved session, suitable for resumption.
*/
public abstract byte[] saveSession();
/**
* Parse a saved session info and attempt to construct a resumed context.
* The first byte in a saved session info must always be the protocol version.
* Note that an {@link IllegalArgumentException} will be thrown if the savedSessionInfo is not
* properly formatted.
*
* @return a resumed context from a saved session.
*/
public static D2DConnectionContext fromSavedSession(byte[] savedSessionInfo) {
if (savedSessionInfo == null || savedSessionInfo.length == 0) {
throw new IllegalArgumentException("savedSessionInfo null or too short");
}
int protocolVersion = savedSessionInfo[0] & 0xff;
switch (protocolVersion) {
case 0:
// Version 0 has a 1 byte protocol version, a 4 byte sequence number,
// and 32 bytes of AES key (1 + 4 + 32 = 37)
if (savedSessionInfo.length != 37) {
throw new IllegalArgumentException("Incorrect data length (" + savedSessionInfo.length
+ ") for v0 protocol");
}
int sequenceNumber = bytesToSignedInt(Arrays.copyOfRange(savedSessionInfo, 1, 5));
SecretKey sharedKey = new SecretKeySpec(Arrays.copyOfRange(savedSessionInfo, 5, 37), "AES");
return new D2DConnectionContextV0(sharedKey, sequenceNumber);
case 1:
// Version 1 has a 1 byte protocol version, two 4 byte sequence numbers,
// and two 32 byte AES keys (1 + 4 + 4 + 32 + 32 = 73)
if (savedSessionInfo.length != 73) {
throw new IllegalArgumentException("Incorrect data length for v1 protocol");
}
int encodeSequenceNumber = bytesToSignedInt(Arrays.copyOfRange(savedSessionInfo, 1, 5));
int decodeSequenceNumber = bytesToSignedInt(Arrays.copyOfRange(savedSessionInfo, 5, 9));
SecretKey encodeKey =
new SecretKeySpec(Arrays.copyOfRange(savedSessionInfo, 9, 41), "AES");
SecretKey decodeKey =
new SecretKeySpec(Arrays.copyOfRange(savedSessionInfo, 41, 73), "AES");
return new D2DConnectionContextV1(encodeKey, decodeKey, encodeSequenceNumber,
decodeSequenceNumber);
default:
throw new IllegalArgumentException("Cannot rebuild context, unkown protocol version: "
+ protocolVersion);
}
}
/**
* Convert 4 bytes in big-endian representation into a signed int.
*/
static int bytesToSignedInt(byte[] bytes) {
if (bytes.length != 4) {
throw new IllegalArgumentException("Expected 4 bytes to encode int, but got: "
+ bytes.length + " bytes");
}
return ((bytes[0] << 24) & 0xff000000)
| ((bytes[1] << 16) & 0x00ff0000)
| ((bytes[2] << 8) & 0x0000ff00)
| (bytes[3] & 0x000000ff);
}
/**
* Convert a signed int into a 4 byte big-endian representation
*/
static byte[] signedIntToBytes(int val) {
byte[] bytes = new byte[4];
bytes[0] = (byte) ((val >> 24) & 0xff);
bytes[1] = (byte) ((val >> 16) & 0xff);
bytes[2] = (byte) ((val >> 8) & 0xff);
bytes[3] = (byte) (val & 0xff);
return bytes;
}
}