• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2020 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 
D2DConnectionContext(int protocolVersion)42   protected D2DConnectionContext(int protocolVersion) {
43     this.protocolVersion = protocolVersion;
44   }
45 
46   /**
47    * @return the version of the D2D protocol.
48    */
getProtocolVersion()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    */
encodeMessageToPeer(byte[] payload)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    */
encodeMessageToPeer(String payload)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    */
decodeMessageFromPeer(byte[] message)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    */
decodeMessageFromPeerAsString(byte[] message)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
createDeviceToDeviceMessage(byte[] message, int sequenceNumber)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    */
getSessionUnique()154   public abstract byte[] getSessionUnique() throws NoSuchAlgorithmException;
155 
156   /**
157    * Increments the sequence number used for encoding messages.
158    */
incrementSequenceNumberForEncoding()159   protected abstract void incrementSequenceNumberForEncoding();
160 
161   /**
162    * Increments the sequence number used for decoding messages.
163    */
incrementSequenceNumberForDecoding()164   protected abstract void incrementSequenceNumberForDecoding();
165 
166   /**
167    * @return the last sequence number used to encode a message.
168    */
169   @VisibleForTesting
getSequenceNumberForEncoding()170   abstract int getSequenceNumberForEncoding();
171 
172   /**
173    * @return the last sequence number used to decode a message.
174    */
175   @VisibleForTesting
getSequenceNumberForDecoding()176   abstract int getSequenceNumberForDecoding();
177 
178   /**
179    * @return the {@link SecretKey} used for encoding messages.
180    */
181   @VisibleForTesting
getEncodeKey()182   abstract SecretKey getEncodeKey();
183 
184   /**
185    * @return the {@link SecretKey} used for decoding messages.
186    */
187   @VisibleForTesting
getDecodeKey()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    */
saveSession()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    */
fromSavedSession(byte[] savedSessionInfo)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    */
bytesToSignedInt(byte[] bytes)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    */
signedIntToBytes(int val)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