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.TransportCryptoOps.Payload; 22 import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.PayloadType; 23 import java.io.UnsupportedEncodingException; 24 import java.security.InvalidKeyException; 25 import java.security.NoSuchAlgorithmException; 26 import java.security.SignatureException; 27 import java.util.Arrays; 28 import javax.crypto.SecretKey; 29 import javax.crypto.spec.SecretKeySpec; 30 31 /** 32 * The full context of a secure connection. This object has methods to encode and decode messages 33 * that are to be sent to another device. 34 * 35 * Subclasses keep track of the keys shared with the other device, and of the sequence in which the 36 * messages are expected. 37 */ 38 public abstract class D2DConnectionContext { 39 private static final String UTF8 = "UTF-8"; 40 private final int protocolVersion; 41 42 protected D2DConnectionContext(int protocolVersion) { 43 this.protocolVersion = protocolVersion; 44 } 45 46 /** 47 * @return the version of the D2D protocol. 48 */ 49 public int getProtocolVersion() { 50 return protocolVersion; 51 } 52 53 /** 54 * Once initiator and responder have exchanged public keys, use this method to encrypt and 55 * sign a payload. Both initiator and responder devices can use this message. 56 * 57 * @param payload the payload that should be encrypted. 58 */ 59 public byte[] encodeMessageToPeer(byte[] payload) { 60 incrementSequenceNumberForEncoding(); 61 DeviceToDeviceMessage message = createDeviceToDeviceMessage( 62 payload, getSequenceNumberForEncoding()); 63 try { 64 return D2DCryptoOps.signcryptPayload( 65 new Payload(PayloadType.DEVICE_TO_DEVICE_MESSAGE, 66 message.toByteArray()), 67 getEncodeKey()); 68 } catch (InvalidKeyException e) { 69 // should never happen, since we agreed on the key earlier 70 throw new RuntimeException(e); 71 } catch (NoSuchAlgorithmException e) { 72 // should never happen 73 throw new RuntimeException(e); 74 } 75 } 76 77 /** 78 * Encrypting/signing a string for transmission to another device. 79 * 80 * @see #encodeMessageToPeer(byte[]) 81 * 82 * @param payload the payload that should be encrypted. 83 */ 84 public byte[] encodeMessageToPeer(String payload) { 85 try { 86 return encodeMessageToPeer(payload.getBytes(UTF8)); 87 } catch (UnsupportedEncodingException e) { 88 // Should never happen - we should always be able to UTF-8-encode a string 89 throw new RuntimeException(e); 90 } 91 } 92 93 /** 94 * Once InitiatorHello and ResponderHello(AndPayload) are exchanged, use this method 95 * to decrypt and verify a message received from the other device. Both initiator and 96 * responder device can use this message. 97 * 98 * @param message the message that should be encrypted. 99 * @throws SignatureException if the message from the remote peer did not pass verification 100 */ 101 public byte[] decodeMessageFromPeer(byte[] message) throws SignatureException { 102 try { 103 Payload payload = D2DCryptoOps.verifydecryptPayload(message, getDecodeKey()); 104 if (!PayloadType.DEVICE_TO_DEVICE_MESSAGE.equals(payload.getPayloadType())) { 105 throw new SignatureException("wrong message type in device-to-device message"); 106 } 107 108 DeviceToDeviceMessage messageProto = DeviceToDeviceMessage.parseFrom(payload.getMessage()); 109 incrementSequenceNumberForDecoding(); 110 if (messageProto.getSequenceNumber() != getSequenceNumberForDecoding()) { 111 throw new SignatureException("Incorrect sequence number"); 112 } 113 114 return messageProto.getMessage().toByteArray(); 115 } catch (InvalidKeyException e) { 116 throw new SignatureException(e); 117 } catch (NoSuchAlgorithmException e) { 118 // this shouldn't happen - the algorithms are hard-coded. 119 throw new RuntimeException(e); 120 } catch (InvalidProtocolBufferException e) { 121 throw new SignatureException(e); 122 } 123 } 124 125 /** 126 * Once InitiatorHello and ResponderHello(AndPayload) are exchanged, use this method 127 * to decrypt and verify a message received from the other device. Both initiator and 128 * responder device can use this message. 129 * 130 * @param message the message that should be encrypted. 131 */ 132 public String decodeMessageFromPeerAsString(byte[] message) throws SignatureException { 133 try { 134 return new String(decodeMessageFromPeer(message), UTF8); 135 } catch (UnsupportedEncodingException e) { 136 // Should never happen - we should always be able to UTF-8-encode a string 137 throw new RuntimeException(e); 138 } 139 } 140 141 // package-private 142 static DeviceToDeviceMessage createDeviceToDeviceMessage(byte[] message, int sequenceNumber) { 143 DeviceToDeviceMessage.Builder deviceToDeviceMessage = DeviceToDeviceMessage.newBuilder(); 144 deviceToDeviceMessage.setSequenceNumber(sequenceNumber); 145 deviceToDeviceMessage.setMessage(ByteString.copyFrom(message)); 146 return deviceToDeviceMessage.build(); 147 } 148 149 /** 150 * Returns a cryptographic digest (SHA256) of the session keys prepended by the SHA256 hash 151 * of the ASCII string "D2D" 152 * @throws NoSuchAlgorithmException if SHA 256 doesn't exist on this platform 153 */ 154 public abstract byte[] getSessionUnique() throws NoSuchAlgorithmException; 155 156 /** 157 * Increments the sequence number used for encoding messages. 158 */ 159 protected abstract void incrementSequenceNumberForEncoding(); 160 161 /** 162 * Increments the sequence number used for decoding messages. 163 */ 164 protected abstract void incrementSequenceNumberForDecoding(); 165 166 /** 167 * @return the last sequence number used to encode a message. 168 */ 169 @VisibleForTesting 170 abstract int getSequenceNumberForEncoding(); 171 172 /** 173 * @return the last sequence number used to decode a message. 174 */ 175 @VisibleForTesting 176 abstract int getSequenceNumberForDecoding(); 177 178 /** 179 * @return the {@link SecretKey} used for encoding messages. 180 */ 181 @VisibleForTesting 182 abstract SecretKey getEncodeKey(); 183 184 /** 185 * @return the {@link SecretKey} used for decoding messages. 186 */ 187 @VisibleForTesting 188 abstract SecretKey getDecodeKey(); 189 190 /** 191 * Creates a saved session that can later be used for resumption. Note, this must be stored in a 192 * secure location. 193 * 194 * @return the saved session, suitable for resumption. 195 */ 196 public abstract byte[] saveSession(); 197 198 /** 199 * Parse a saved session info and attempt to construct a resumed context. 200 * The first byte in a saved session info must always be the protocol version. 201 * Note that an {@link IllegalArgumentException} will be thrown if the savedSessionInfo is not 202 * properly formatted. 203 * 204 * @return a resumed context from a saved session. 205 */ 206 public static D2DConnectionContext fromSavedSession(byte[] savedSessionInfo) { 207 if (savedSessionInfo == null || savedSessionInfo.length == 0) { 208 throw new IllegalArgumentException("savedSessionInfo null or too short"); 209 } 210 211 int protocolVersion = savedSessionInfo[0] & 0xff; 212 213 switch (protocolVersion) { 214 case 0: 215 // Version 0 has a 1 byte protocol version, a 4 byte sequence number, 216 // and 32 bytes of AES key (1 + 4 + 32 = 37) 217 if (savedSessionInfo.length != 37) { 218 throw new IllegalArgumentException("Incorrect data length (" + savedSessionInfo.length 219 + ") for v0 protocol"); 220 } 221 int sequenceNumber = bytesToSignedInt(Arrays.copyOfRange(savedSessionInfo, 1, 5)); 222 SecretKey sharedKey = new SecretKeySpec(Arrays.copyOfRange(savedSessionInfo, 5, 37), "AES"); 223 return new D2DConnectionContextV0(sharedKey, sequenceNumber); 224 225 case 1: 226 // Version 1 has a 1 byte protocol version, two 4 byte sequence numbers, 227 // and two 32 byte AES keys (1 + 4 + 4 + 32 + 32 = 73) 228 if (savedSessionInfo.length != 73) { 229 throw new IllegalArgumentException("Incorrect data length for v1 protocol"); 230 } 231 int encodeSequenceNumber = bytesToSignedInt(Arrays.copyOfRange(savedSessionInfo, 1, 5)); 232 int decodeSequenceNumber = bytesToSignedInt(Arrays.copyOfRange(savedSessionInfo, 5, 9)); 233 SecretKey encodeKey = 234 new SecretKeySpec(Arrays.copyOfRange(savedSessionInfo, 9, 41), "AES"); 235 SecretKey decodeKey = 236 new SecretKeySpec(Arrays.copyOfRange(savedSessionInfo, 41, 73), "AES"); 237 return new D2DConnectionContextV1(encodeKey, decodeKey, encodeSequenceNumber, 238 decodeSequenceNumber); 239 240 default: 241 throw new IllegalArgumentException("Cannot rebuild context, unkown protocol version: " 242 + protocolVersion); 243 } 244 } 245 246 /** 247 * Convert 4 bytes in big-endian representation into a signed int. 248 */ 249 static int bytesToSignedInt(byte[] bytes) { 250 if (bytes.length != 4) { 251 throw new IllegalArgumentException("Expected 4 bytes to encode int, but got: " 252 + bytes.length + " bytes"); 253 } 254 255 return ((bytes[0] << 24) & 0xff000000) 256 | ((bytes[1] << 16) & 0x00ff0000) 257 | ((bytes[2] << 8) & 0x0000ff00) 258 | (bytes[3] & 0x000000ff); 259 } 260 261 /** 262 * Convert a signed int into a 4 byte big-endian representation 263 */ 264 static byte[] signedIntToBytes(int val) { 265 byte[] bytes = new byte[4]; 266 267 bytes[0] = (byte) ((val >> 24) & 0xff); 268 bytes[1] = (byte) ((val >> 16) & 0xff); 269 bytes[2] = (byte) ((val >> 8) & 0xff); 270 bytes[3] = (byte) (val & 0xff); 271 272 return bytes; 273 } 274 } 275