Home | History | Annotate | Download | only in securegcm
      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