summaryrefslogtreecommitdiff
path: root/src/main/java/com/google/security/cryptauth
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/security/cryptauth')
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContext.java274
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextV0.java118
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextV1.java146
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securegcm/D2DCryptoOps.java239
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securegcm/D2DDiffieHellmanKeyExchangeHandshake.java307
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securegcm/D2DHandshakeContext.java158
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securegcm/Ed25519.java270
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securegcm/EnrollmentCryptoOps.java233
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securegcm/HandshakeException.java32
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securegcm/KeyEncoding.java180
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securegcm/SecureGcmConstants.java49
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securegcm/TransportCryptoOps.java268
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securegcm/Ukey2Handshake.java1041
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securegcm/device_to_device_messages_config.asciipb3
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securegcm/securegcm_config.asciipb4
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securemessage/CryptoOps.java564
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securemessage/PublicKeyProtoUtil.java675
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securemessage/SecureMessageBuilder.java277
-rw-r--r--src/main/java/com/google/security/cryptauth/lib/securemessage/SecureMessageParser.java270
19 files changed, 0 insertions, 5108 deletions
diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContext.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContext.java
deleted file mode 100644
index fb4af63..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContext.java
+++ /dev/null
@@ -1,274 +0,0 @@
-// Copyright 2020 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;
- }
-}
diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextV0.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextV0.java
deleted file mode 100644
index d0efa44..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextV0.java
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright 2020 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 java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import javax.crypto.SecretKey;
-
-/**
- * Implementation of {@link D2DConnectionContext} for version 0 of the D2D protocol. In this
- * version, communication is half-duplex, as there is a shared key and a shared sequence number
- * between the two sides.
- */
-public class D2DConnectionContextV0 extends D2DConnectionContext {
- public static final int PROTOCOL_VERSION = 0;
-
- private final SecretKey sharedKey;
- private int sequenceNumber;
-
- /**
- * Package private constructor. Should never be called directly except by the
- * {@link D2DHandshakeContext}
- *
- * @param sharedKey
- * @param initialSequenceNumber
- */
- D2DConnectionContextV0(SecretKey sharedKey, int initialSequenceNumber) {
- super(PROTOCOL_VERSION);
- this.sharedKey = sharedKey;
- this.sequenceNumber = initialSequenceNumber;
- }
-
- @Override
- public byte[] getSessionUnique() throws NoSuchAlgorithmException {
- if (sharedKey == null) {
- throw new IllegalStateException(
- "Connection has not been correctly initialized; shared key is null");
- }
-
- MessageDigest md = MessageDigest.getInstance("SHA-256");
- md.update(D2DCryptoOps.SALT);
- return md.digest(sharedKey.getEncoded());
- }
-
- @Override
- protected void incrementSequenceNumberForEncoding() {
- sequenceNumber++;
- }
-
- @Override
- protected void incrementSequenceNumberForDecoding() {
- sequenceNumber++;
- }
-
- @Override
- int getSequenceNumberForEncoding() {
- return sequenceNumber;
- }
-
- @Override
- int getSequenceNumberForDecoding() {
- return sequenceNumber;
- }
-
- @Override
- SecretKey getEncodeKey() {
- return sharedKey;
- }
-
- @Override
- SecretKey getDecodeKey() {
- return sharedKey;
- }
-
- /**
- * Structure of saved session is:
- * +-----------------------------------------------------+
- * | 1 Byte | 4 Bytes (big endian) | 32 Bytes |
- * +-----------------------------------------------------+
- * | Protocol Version | sequence number | key |
- * +-----------------------------------------------------+
- */
- @Override
- public byte[] saveSession() {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- try {
- // Protocol version
- bytes.write(0);
-
- // Sequence number
- bytes.write(signedIntToBytes(sequenceNumber));
-
- // Key
- bytes.write(sharedKey.getEncoded());
- } catch (IOException e) {
- // should not happen
- e.printStackTrace();
- return null;
- }
-
- return bytes.toByteArray();
- }
-}
diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextV1.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextV1.java
deleted file mode 100644
index 1566849..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextV1.java
+++ /dev/null
@@ -1,146 +0,0 @@
-// Copyright 2020 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 java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-import javax.crypto.SecretKey;
-
-/**
- * Implementation of {@link D2DConnectionContext} for version 1 of the D2D protocol. In this
- * version, communication is fully duplex, as separate keys and sequence nubmers are used for
- * encoding and decoding.
- */
-public class D2DConnectionContextV1 extends D2DConnectionContext {
- public static final int PROTOCOL_VERSION = 1;
-
- private final SecretKey encodeKey;
- private final SecretKey decodeKey;
- private int encodeSequenceNumber;
- private int decodeSequenceNumber;
-
- /**
- * Package private constructor. Should never be called directly except by the
- * {@link D2DHandshakeContext}
- *
- * @param encodeKey
- * @param decodeKey
- * @param initialEncodeSequenceNumber
- * @param initialDecodeSequenceNumber
- */
- D2DConnectionContextV1(
- SecretKey encodeKey,
- SecretKey decodeKey,
- int initialEncodeSequenceNumber,
- int initialDecodeSequenceNumber) {
- super(PROTOCOL_VERSION);
- this.encodeKey = encodeKey;
- this.decodeKey = decodeKey;
- this.encodeSequenceNumber = initialEncodeSequenceNumber;
- this.decodeSequenceNumber = initialDecodeSequenceNumber;
- }
-
- @Override
- public byte[] getSessionUnique() throws NoSuchAlgorithmException {
- if (encodeKey == null || decodeKey == null) {
- throw new IllegalStateException(
- "Connection has not been correctly initialized; encode key or decode key is null");
- }
-
- // Ensure that the initator and responder keys are hashed in a deterministic order, so they have
- // the same session unique code.
- byte[] encodeKeyBytes = encodeKey.getEncoded();
- byte[] decodeKeyBytes = decodeKey.getEncoded();
- int encodeKeyHash = Arrays.hashCode(encodeKeyBytes);
- int decodeKeyHash = Arrays.hashCode(decodeKeyBytes);
- byte[] firstKeyBytes = encodeKeyHash < decodeKeyHash ? encodeKeyBytes : decodeKeyBytes;
- byte[] secondKeyBytes = firstKeyBytes == encodeKeyBytes ? decodeKeyBytes : encodeKeyBytes;
-
- MessageDigest md = MessageDigest.getInstance("SHA-256");
- md.update(D2DCryptoOps.SALT);
- md.update(firstKeyBytes);
- md.update(secondKeyBytes);
- return md.digest();
- }
-
- @Override
- protected void incrementSequenceNumberForEncoding() {
- encodeSequenceNumber++;
- }
-
- @Override
- protected void incrementSequenceNumberForDecoding() {
- decodeSequenceNumber++;
- }
-
- @Override
- int getSequenceNumberForEncoding() {
- return encodeSequenceNumber;
- }
-
- @Override
- int getSequenceNumberForDecoding() {
- return decodeSequenceNumber;
- }
-
- @Override
- SecretKey getEncodeKey() {
- return encodeKey;
- }
-
- @Override
- SecretKey getDecodeKey() {
- return decodeKey;
- }
-
- /**
- * Structure of saved session is:
- * +------------------------------------------------------------------------------------------+
- * | 1 Byte | 4 Bytes (big endian) | 4 Bytes (big endian) | 32 Bytes | 32 Bytes |
- * +------------------------------------------------------------------------------------------+
- * | Protocol Version | encode seq number | decode seq number | encode key | decode key |
- * +------------------------------------------------------------------------------------------+
- */
- @Override
- public byte[] saveSession() {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- try {
- // Protocol version
- bytes.write(1);
-
- // Encode sequence number
- bytes.write(signedIntToBytes(encodeSequenceNumber));
-
- // Decode sequence number
- bytes.write(signedIntToBytes(decodeSequenceNumber));
-
- // Encode Key
- bytes.write(encodeKey.getEncoded());
-
- // Decode Key
- bytes.write(decodeKey.getEncoded());
- } catch (IOException e) {
- // should not happen
- e.printStackTrace();
- return null;
- }
-
- return bytes.toByteArray();
- }
-}
diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DCryptoOps.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DCryptoOps.java
deleted file mode 100644
index a7203d1..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DCryptoOps.java
+++ /dev/null
@@ -1,239 +0,0 @@
-// Copyright 2020 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.InvalidProtocolBufferException;
-import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.DeviceToDeviceMessage;
-import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.ResponderHello;
-import com.google.security.cryptauth.lib.securegcm.SecureGcmProto.GcmMetadata;
-import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.Payload;
-import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.PayloadType;
-import com.google.security.cryptauth.lib.securemessage.CryptoOps;
-import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType;
-import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType;
-import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageBuilder;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageParser;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.GenericPublicKey;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.Header;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBody;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.SignatureException;
-import java.security.spec.InvalidKeySpecException;
-import javax.annotation.Nullable;
-import javax.crypto.SecretKey;
-
-/**
- * A collection of static utility methods used by {@link D2DHandshakeContext} for the Device to
- * Device communication (D2D) library.
- */
-class D2DCryptoOps {
- // SHA256 of "D2D"
- // package-private
- static final byte[] SALT = new byte[] {
- (byte) 0x82, (byte) 0xAA, (byte) 0x55, (byte) 0xA0, (byte) 0xD3, (byte) 0x97, (byte) 0xF8,
- (byte) 0x83, (byte) 0x46, (byte) 0xCA, (byte) 0x1C, (byte) 0xEE, (byte) 0x8D, (byte) 0x39,
- (byte) 0x09, (byte) 0xB9, (byte) 0x5F, (byte) 0x13, (byte) 0xFA, (byte) 0x7D, (byte) 0xEB,
- (byte) 0x1D, (byte) 0x4A, (byte) 0xB3, (byte) 0x83, (byte) 0x76, (byte) 0xB8, (byte) 0x25,
- (byte) 0x6D, (byte) 0xA8, (byte) 0x55, (byte) 0x10
- };
-
- // Data passed to hkdf to create the key used by the initiator to encode messages.
- static final String INITIATOR_PURPOSE = "initiator";
- // Data passed to hkdf to create the key used by the responder to encode messages.
- static final String RESPONDER_PURPOSE = "responder";
-
- // Don't instantiate
- private D2DCryptoOps() { }
-
- /**
- * Used by the responder device to create a signcrypted message that contains
- * a payload and a {@link ResponderHello}.
- *
- * @param sharedKey used to signcrypt the {@link Payload}
- * @param publicDhKey the key the recipient will need to derive the shared DH secret.
- * This key will be added to the {@link ResponderHello} in the header.
- * @param protocolVersion the protocol version to include in the proto
- */
- static byte[] signcryptMessageAndResponderHello(
- Payload payload, SecretKey sharedKey, PublicKey publicDhKey, int protocolVersion)
- throws InvalidKeyException, NoSuchAlgorithmException {
- ResponderHello.Builder responderHello = ResponderHello.newBuilder();
- responderHello.setPublicDhKey(PublicKeyProtoUtil.encodePublicKey(publicDhKey));
- responderHello.setProtocolVersion(protocolVersion);
- return signcryptPayload(payload, sharedKey, responderHello.build().toByteArray());
- }
-
- /**
- * Used by a device to send a secure {@link Payload} to another device.
- */
- static byte[] signcryptPayload(
- Payload payload, SecretKey masterKey)
- throws InvalidKeyException, NoSuchAlgorithmException {
- return signcryptPayload(payload, masterKey, null);
- }
-
- /**
- * Used by a device to send a secure {@link Payload} to another device.
- *
- * @param responderHello is an optional public value to attach in the header of
- * the {@link SecureMessage} (in the DecryptionKeyId).
- */
- @VisibleForTesting
- static byte[] signcryptPayload(
- Payload payload, SecretKey masterKey, @Nullable byte[] responderHello)
- throws InvalidKeyException, NoSuchAlgorithmException {
- if ((payload == null) || (masterKey == null)) {
- throw new NullPointerException();
- }
-
- SecureMessageBuilder secureMessageBuilder = new SecureMessageBuilder()
- .setPublicMetadata(GcmMetadata.newBuilder()
- .setType(payload.getPayloadType().getType())
- .setVersion(SecureGcmConstants.SECURE_GCM_VERSION)
- .build()
- .toByteArray());
-
- if (responderHello != null) {
- secureMessageBuilder.setDecryptionKeyId(responderHello);
- }
-
- return secureMessageBuilder.buildSignCryptedMessage(
- masterKey,
- SigType.HMAC_SHA256,
- masterKey,
- EncType.AES_256_CBC,
- payload.getMessage())
- .toByteArray();
- }
-
- /**
- * Extracts a ResponderHello proto from the header of a signcrypted message so that we
- * can derive the shared secret that was used to sign/encrypt the message.
- *
- * @return the {@link ResponderHello} embedded in the signcrypted message.
- */
- static ResponderHello parseAndValidateResponderHello(
- byte[] signcryptedMessageFromResponder) throws InvalidProtocolBufferException {
- if (signcryptedMessageFromResponder == null) {
- throw new NullPointerException();
- }
- SecureMessage secmsg = SecureMessage.parseFrom(signcryptedMessageFromResponder);
- Header messageHeader = SecureMessageParser.getUnverifiedHeader(secmsg);
- if (!messageHeader.hasDecryptionKeyId()) {
- // Maybe this should be a different exception type, because in general, it's legal for the
- // SecureMessage proto to not have the decryption key id, but it's illegal in this protocol.
- throw new InvalidProtocolBufferException("Missing decryption key id");
- }
- byte[] encodedResponderHello = messageHeader.getDecryptionKeyId().toByteArray();
- ResponderHello responderHello = ResponderHello.parseFrom(encodedResponderHello);
- if (!responderHello.hasPublicDhKey()) {
- throw new InvalidProtocolBufferException("Missing public key in responder hello");
- }
- return responderHello;
- }
-
- /**
- * Used by a device to recover a secure {@link Payload} sent by another device.
- */
- static Payload verifydecryptPayload(
- byte[] signcryptedMessage, SecretKey masterKey)
- throws SignatureException, InvalidKeyException, NoSuchAlgorithmException {
- if ((signcryptedMessage == null) || (masterKey == null)) {
- throw new NullPointerException();
- }
- try {
- SecureMessage secmsg = SecureMessage.parseFrom(signcryptedMessage);
- HeaderAndBody parsed = SecureMessageParser.parseSignCryptedMessage(
- secmsg,
- masterKey,
- SigType.HMAC_SHA256,
- masterKey,
- EncType.AES_256_CBC);
- if (!parsed.getHeader().hasPublicMetadata()) {
- throw new SignatureException("missing metadata");
- }
- GcmMetadata metadata = GcmMetadata.parseFrom(parsed.getHeader().getPublicMetadata());
- if (metadata.getVersion() > SecureGcmConstants.SECURE_GCM_VERSION) {
- throw new SignatureException("Unsupported protocol version");
- }
- return new Payload(PayloadType.valueOf(metadata.getType()), parsed.getBody().toByteArray());
- } catch (InvalidProtocolBufferException e) {
- throw new SignatureException(e);
- } catch (IllegalArgumentException e) {
- throw new SignatureException(e);
- }
- }
-
- /**
- * Used by the initiator device to derive the shared key from the {@link PrivateKey} in the
- * {@link D2DHandshakeContext} and the responder's {@link GenericPublicKey} (contained in the
- * {@link ResponderHello} proto).
- */
- static SecretKey deriveSharedKeyFromGenericPublicKey(
- PrivateKey ourPrivateKey, GenericPublicKey theirGenericPublicKey) throws SignatureException {
- try {
- PublicKey theirPublicKey = PublicKeyProtoUtil.parsePublicKey(theirGenericPublicKey);
- return EnrollmentCryptoOps.doKeyAgreement(ourPrivateKey, theirPublicKey);
- } catch (InvalidKeySpecException e) {
- throw new SignatureException(e);
- } catch (InvalidKeyException e) {
- throw new SignatureException(e);
- }
- }
-
- /**
- * Used to derive a distinct key for each initiator and responder.
- *
- * @param masterKey the source key used to derive the new key.
- * @param purpose a string to make the new key different for each purpose.
- * @return the derived {@link SecretKey}.
- */
- static SecretKey deriveNewKeyForPurpose(SecretKey masterKey, String purpose)
- throws NoSuchAlgorithmException, InvalidKeyException {
- byte[] info = purpose.getBytes();
- return KeyEncoding.parseMasterKey(CryptoOps.hkdf(masterKey, SALT, info));
- }
-
- /**
- * Used by the initiator device to decrypt the first payload portion that was sent in the
- * {@code responderHelloAndPayload}, and extract the {@link DeviceToDeviceMessage} contained
- * within it. In order to decrypt, the {@code sharedKey} must first be derived.
- *
- * @see #deriveSharedKeyFromGenericPublicKey(PrivateKey, GenericPublicKey)
- */
- static DeviceToDeviceMessage decryptResponderHelloMessage(
- SecretKey sharedKey, byte[] responderHelloAndPayload) throws SignatureException {
- try {
- Payload payload = verifydecryptPayload(responderHelloAndPayload, sharedKey);
- if (!PayloadType.DEVICE_TO_DEVICE_RESPONDER_HELLO_PAYLOAD.equals(
- payload.getPayloadType())) {
- throw new SignatureException("wrong message type in responder hello");
- }
- return DeviceToDeviceMessage.parseFrom(payload.getMessage());
- } catch (InvalidProtocolBufferException e) {
- throw new SignatureException(e);
- } catch (InvalidKeyException e) {
- throw new SignatureException(e);
- } catch (NoSuchAlgorithmException e) {
- throw new SignatureException(e);
- }
- }
-}
diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DDiffieHellmanKeyExchangeHandshake.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DDiffieHellmanKeyExchangeHandshake.java
deleted file mode 100644
index f929a3a..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DDiffieHellmanKeyExchangeHandshake.java
+++ /dev/null
@@ -1,307 +0,0 @@
-// Copyright 2020 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.InvalidProtocolBufferException;
-import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.DeviceToDeviceMessage;
-import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.InitiatorHello;
-import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.ResponderHello;
-import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.Payload;
-import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.PayloadType;
-import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil;
-import java.security.InvalidKeyException;
-import java.security.KeyPair;
-import java.security.NoSuchAlgorithmException;
-import java.security.PublicKey;
-import java.security.SignatureException;
-import java.security.spec.InvalidKeySpecException;
-import javax.crypto.SecretKey;
-
-/**
- * Implements an unauthenticated EC Diffie Hellman Key Exchange Handshake
- * <p>
- * Initiator sends an InitiatorHello, which is a protobuf that contains a public key. Responder
- * sends a responder hello, which a signed and encrypted message containing a payload, and a public
- * key in the unencrypted header (payload is encrypted with the derived DH key).
- * <p>
- * Example Usage:
- * <pre>
- * // initiator:
- * D2DHandshakeContext initiatorHandshakeContext =
- * D2DDiffieHellmanKeyExchangeHandshake.forInitiator();
- * byte[] initiatorHello = initiatorHandshakeContext.getNextHandshakeMessage();
- * // (send initiatorHello to responder)
- *
- * // responder:
- * D2DHandshakeContext responderHandshakeContext =
- * D2DDiffieHellmanKeyExchangeHandshake.forResponder();
- * responderHandshakeContext.parseHandshakeMessage(initiatorHello);
- * byte[] responderHelloAndPayload = responderHandshakeContext.getNextHandshakeMessage(
- * toBytes(RESPONDER_HELLO_MESSAGE));
- * D2DConnectionContext responderCtx = responderHandshakeContext.toConnectionContext();
- * // (send responderHelloAndPayload to initiator)
- *
- * // initiator
- * byte[] messageFromPayload =
- * initiatorHandshakeContext.parseHandshakeMessage(responderHelloAndPayload);
- * if (messageFromPayload.length > 0) {
- * handle(messageFromPayload);
- * }
- *
- * D2DConnectionContext initiatorCtx = initiatorHandshakeContext.toConnectionContext();
- * </pre>
- */
-public class D2DDiffieHellmanKeyExchangeHandshake implements D2DHandshakeContext {
- private KeyPair ourKeyPair;
- private PublicKey theirPublicKey;
- private SecretKey initiatorEncodeKey;
- private SecretKey responderEncodeKey;
- private State handshakeState;
- private boolean isInitiator;
- private int protocolVersionToUse;
-
- private enum State {
- // Initiator state
- INITIATOR_START,
- INITIATOR_WAITING_FOR_RESPONDER_HELLO,
-
- // Responder state
- RESPONDER_START,
- RESPONDER_AFTER_INITIATOR_HELLO,
-
- // Common completion state
- HANDSHAKE_FINISHED,
- HANDSHAKE_ALREADY_USED
- }
-
- private D2DDiffieHellmanKeyExchangeHandshake(State state) {
- ourKeyPair = PublicKeyProtoUtil.generateEcP256KeyPair();
- theirPublicKey = null;
- initiatorEncodeKey = null;
- responderEncodeKey = null;
- handshakeState = state;
- isInitiator = state == State.INITIATOR_START;
- protocolVersionToUse = D2DConnectionContextV1.PROTOCOL_VERSION;
- }
-
- /**
- * Creates a new Diffie Hellman handshake context for the handshake initiator
- */
- public static D2DDiffieHellmanKeyExchangeHandshake forInitiator() {
- return new D2DDiffieHellmanKeyExchangeHandshake(State.INITIATOR_START);
- }
-
- /**
- * Creates a new Diffie Hellman handshake context for the handshake responder
- */
- public static D2DDiffieHellmanKeyExchangeHandshake forResponder() {
- return new D2DDiffieHellmanKeyExchangeHandshake(State.RESPONDER_START);
- }
-
- @Override
- public boolean isHandshakeComplete() {
- return handshakeState == State.HANDSHAKE_FINISHED
- || handshakeState == State.HANDSHAKE_ALREADY_USED;
- }
-
- @Override
- public byte[] getNextHandshakeMessage() throws HandshakeException {
- switch(handshakeState) {
- case INITIATOR_START:
- handshakeState = State.INITIATOR_WAITING_FOR_RESPONDER_HELLO;
- return InitiatorHello.newBuilder()
- .setPublicDhKey(PublicKeyProtoUtil.encodePublicKey(ourKeyPair.getPublic()))
- .setProtocolVersion(protocolVersionToUse)
- .build()
- .toByteArray();
-
- case RESPONDER_AFTER_INITIATOR_HELLO:
- byte[] responderHello = makeResponderHelloWithPayload(new byte[0]);
- handshakeState = State.HANDSHAKE_FINISHED;
- return responderHello;
-
- default:
- throw new HandshakeException("Cannot get next message in state: " + handshakeState);
- }
- }
-
- @Override
- public boolean canSendPayloadInHandshakeMessage() {
- return handshakeState == State.RESPONDER_AFTER_INITIATOR_HELLO;
- }
-
- @Override
- public byte[] getNextHandshakeMessage(byte[] payload) throws HandshakeException {
- if (handshakeState != State.RESPONDER_AFTER_INITIATOR_HELLO) {
- throw new HandshakeException(
- "Cannot get next message with payload in state: " + handshakeState);
- }
-
- byte[] responderHello = makeResponderHelloWithPayload(payload);
- handshakeState = State.HANDSHAKE_FINISHED;
-
- return responderHello;
- }
-
- private byte[] makeResponderHelloWithPayload(byte[] payload) throws HandshakeException {
- if (payload == null) {
- throw new HandshakeException("Not expecting null payload");
- }
-
- try {
- SecretKey masterKey =
- EnrollmentCryptoOps.doKeyAgreement(ourKeyPair.getPrivate(), theirPublicKey);
-
- // V0 uses the same key for encoding and decoding, but V1 uses separate keys.
- switch (protocolVersionToUse) {
- case D2DConnectionContextV0.PROTOCOL_VERSION:
- initiatorEncodeKey = masterKey;
- responderEncodeKey = masterKey;
- break;
- case D2DConnectionContextV1.PROTOCOL_VERSION:
- initiatorEncodeKey = D2DCryptoOps.deriveNewKeyForPurpose(masterKey,
- D2DCryptoOps.INITIATOR_PURPOSE);
- responderEncodeKey = D2DCryptoOps.deriveNewKeyForPurpose(masterKey,
- D2DCryptoOps.RESPONDER_PURPOSE);
- break;
- default:
- throw new IllegalStateException("Unexpected protocol version: " + protocolVersionToUse);
- }
-
- DeviceToDeviceMessage deviceToDeviceMessage =
- D2DConnectionContext.createDeviceToDeviceMessage(payload, 1 /* sequence number */);
-
- return D2DCryptoOps.signcryptMessageAndResponderHello(
- new Payload(PayloadType.DEVICE_TO_DEVICE_RESPONDER_HELLO_PAYLOAD,
- deviceToDeviceMessage.toByteArray()),
- responderEncodeKey,
- ourKeyPair.getPublic(),
- protocolVersionToUse);
- } catch (InvalidKeyException|NoSuchAlgorithmException e) {
- throw new HandshakeException(e);
- }
- }
-
- @Override
- public byte[] parseHandshakeMessage(byte[] handshakeMessage) throws HandshakeException {
- if (handshakeMessage == null || handshakeMessage.length == 0) {
- throw new HandshakeException("Handshake message too short");
- }
-
- switch(handshakeState) {
- case INITIATOR_WAITING_FOR_RESPONDER_HELLO:
- byte[] payload = parseResponderHello(handshakeMessage);
- handshakeState = State.HANDSHAKE_FINISHED;
- return payload;
-
- case RESPONDER_START:
- parseInitiatorHello(handshakeMessage);
- handshakeState = State.RESPONDER_AFTER_INITIATOR_HELLO;
- return new byte[0];
-
- default:
- throw new HandshakeException("Cannot parse message in state: " + handshakeState);
- }
- }
-
- private byte[] parseResponderHello(byte[] responderHello) throws HandshakeException {
- try {
- ResponderHello responderHelloProto =
- D2DCryptoOps.parseAndValidateResponderHello(responderHello);
-
- // Downgrade to protocol version 0 if needed for backwards compatibility.
- int protocolVersion = responderHelloProto.getProtocolVersion();
- if (protocolVersion == D2DConnectionContextV0.PROTOCOL_VERSION) {
- protocolVersionToUse = D2DConnectionContextV0.PROTOCOL_VERSION;
- }
-
- SecretKey masterKey = D2DCryptoOps.deriveSharedKeyFromGenericPublicKey(
- ourKeyPair.getPrivate(), responderHelloProto.getPublicDhKey());
-
- // V0 uses the same key for encoding and decoding, but V1 uses separate keys.
- if (protocolVersionToUse == D2DConnectionContextV0.PROTOCOL_VERSION) {
- initiatorEncodeKey = masterKey;
- responderEncodeKey = masterKey;
- } else {
- initiatorEncodeKey = D2DCryptoOps.deriveNewKeyForPurpose(masterKey,
- D2DCryptoOps.INITIATOR_PURPOSE);
- responderEncodeKey = D2DCryptoOps.deriveNewKeyForPurpose(masterKey,
- D2DCryptoOps.RESPONDER_PURPOSE);
- }
-
- DeviceToDeviceMessage message =
- D2DCryptoOps.decryptResponderHelloMessage(responderEncodeKey, responderHello);
-
- if (message.getSequenceNumber() != 1) {
- throw new HandshakeException("Incorrect sequence number in responder hello");
- }
-
- return message.getMessage().toByteArray();
- } catch (SignatureException | InvalidProtocolBufferException
- | NoSuchAlgorithmException | InvalidKeyException e) {
- throw new HandshakeException(e);
- }
- }
-
- private void parseInitiatorHello(byte[] initiatorHello) throws HandshakeException {
- try {
- InitiatorHello initiatorHelloProto = InitiatorHello.parseFrom(initiatorHello);
-
- if (!initiatorHelloProto.hasPublicDhKey()) {
- throw new HandshakeException("Missing public key in initiator hello");
- }
-
- theirPublicKey = PublicKeyProtoUtil.parsePublicKey(initiatorHelloProto.getPublicDhKey());
-
- // Downgrade to protocol version 0 if needed for backwards compatibility.
- int protocolVersion = initiatorHelloProto.getProtocolVersion();
- if (protocolVersion == D2DConnectionContextV0.PROTOCOL_VERSION) {
- protocolVersionToUse = D2DConnectionContextV0.PROTOCOL_VERSION;
- }
- } catch (InvalidKeySpecException | InvalidProtocolBufferException e) {
- throw new HandshakeException(e);
- }
- }
-
- @Override
- public D2DConnectionContext toConnectionContext() throws HandshakeException {
- if (handshakeState == State.HANDSHAKE_ALREADY_USED) {
- throw new HandshakeException("Cannot reuse handshake context; is has already been used");
- }
-
- if (!isHandshakeComplete()) {
- throw new HandshakeException("Handshake is not complete; cannot create connection context");
- }
-
- handshakeState = State.HANDSHAKE_ALREADY_USED;
-
- if (protocolVersionToUse == D2DConnectionContextV0.PROTOCOL_VERSION) {
- // Both sides start with an initial sequence number of 1 because the last message of the
- // handshake had an optional payload with sequence number 1. D2DConnectionContext remembers
- // the last sequence number used by each side.
- // Note: initiatorEncodeKey == responderEncodeKey
- return new D2DConnectionContextV0(initiatorEncodeKey, 1 /** initialSequenceNumber */);
- } else {
- SecretKey encodeKey = isInitiator ? initiatorEncodeKey : responderEncodeKey;
- SecretKey decodeKey = isInitiator ? responderEncodeKey : initiatorEncodeKey;
- // Only the responder sends a DeviceToDeviceMessage during the handshake, so it has an initial
- // sequence number of 1. The initiator will therefore have an initial sequence number of 0.
- int initialEncodeSequenceNumber = isInitiator ? 0 : 1;
- int initialDecodeSequenceNumber = isInitiator ? 1 : 0;
- return new D2DConnectionContextV1(
- encodeKey, decodeKey, initialEncodeSequenceNumber, initialDecodeSequenceNumber);
- }
- }
-}
diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DHandshakeContext.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DHandshakeContext.java
deleted file mode 100644
index 5fc1d7b..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DHandshakeContext.java
+++ /dev/null
@@ -1,158 +0,0 @@
-// Copyright 2020 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;
-
-/**
- * Describes a cryptographic handshake with arbitrary number of round trips. Some handshake
- * messages may also send a payload.
- *
- * <p>Generic usage for handshake Initiator:
- * {@code
- * // Handshake Initiator
- * D2DHandshakeContext handshake = <specific handshake>.forInitiator();
- * while (!handshake.isHandshakeComplete()) {
- * try {
- * // Get the next handshake message to send
- * byte[] initiatorMessage = handshake.getNextHandshakeMessage();
- *
- * // Send the message out and get the response
- * socket.send(initiatorMessage);
- * byte[] responderMessage = socket.read();
- *
- * // Handle the response and obtain the optional payload
- * byte[] payload = handshake.parseHandshakeMessage(responderMessage);
- *
- * // Handle the payload if one was sent
- * if (payload.length > 0) {
- * handlePayload(payload);
- * }
- * } catch (HandshakeException e) {
- * // Handshake has failed, bail
- * Log("Handshake failed!", e);
- * return;
- * }
- * }
- *
- * ConnectionContext connectionContext;
- * try {
- * // Upgrade handshake context to a full connection context
- * connectionContext = handshake.toConnectionContext();
- * } catch (HandshakeException e) {
- * Log("Cannot convert handshake to connection context", e);
- * }
- * }
- *
- * <p>Generic usage for handshake Responder:
- * {@code
- * // Handshake Responder
- * D2DHandshakeContext handshake = <specific handshake>.forResponder();
- *
- * while (!handshake.isHandshakeComplete()) {
- * try {
- * // Get the message from the initiator
- * byte[] initiatorMessage = socket.read();
- *
- * // Handle the message and get the payload if it exists
- * byte[] payload = handshake.parseHandshakeMessage(initiatorMessage);
- *
- * // Handle the payload if one was sent
- * if (payload.length > 0) {
- * handlePayload(payload);
- * }
- *
- * // Make sure that wasn't the last message
- * if (handshake.isHandshakeComplete()) {
- * break;
- * }
- *
- * // Get next message to send and send it
- * byte[] responderMessage = handshake.getNextHandshakeMessage();
- * socket.send(responderMessage);
- * } catch (HandshakeException e) {
- * // Handshake has failed, bail
- * Log("Handshake failed!", e);
- * return;
- * }
- * }
- *
- * ConnectionContext connectionContext;
- * try {
- * // Upgrade handshake context to a full connection context
- * connectionContext = handshake.toConnectionContext();
- * } catch (HandshakeException e) {
- * Log("Cannot convert handshake to connection context", e);
- * }
- * }
- */
-public interface D2DHandshakeContext {
-
- /**
- * Tells the caller whether the handshake has completed or not. If the handshake is complete, the
- * caller may call {@link #toConnectionContext()} to obtain a connection context.
- *
- * @return true if the handshake is complete, false otherwise
- */
- boolean isHandshakeComplete();
-
- /**
- * Constructs the next message that should be sent in the handshake.
- *
- * @return the next message
- * @throws HandshakeException if the handshake is over or if the next handshake message can't be
- * obtained (e.g., there is an internal error)
- */
- byte[] getNextHandshakeMessage() throws HandshakeException;
-
- /**
- * Tells the caller whether the next handshake message may carry a payload. If true, caller may
- * call {@link #getNextHandshakeMessage(byte[])} instead of the regular
- * {@link #getNextHandshakeMessage()}. If false, calling {@link #getNextHandshakeMessage(byte[])}
- * will result in a {@link HandshakeException}.
- *
- * @return true if the next handshake message can carry a payload, false otherwise
- */
- boolean canSendPayloadInHandshakeMessage();
-
- /**
- * Constructs the next message that should be sent in the handshake along with a payload. Caller
- * should verify that this method can be called by calling
- * {@link #canSendPayloadInHandshakeMessage()}.
- *
- * @param payload the payload to include in the handshake message
- * @return the next message
- * @throws HandshakeException if the handshake is over or if the next handshake message can't be
- * obtained (e.g., there is an internal error) or if the payload may not be included in this
- * message
- */
- byte[] getNextHandshakeMessage(byte[] payload) throws HandshakeException;
-
- /**
- * Parses a handshake message and returns the included payload (if any).
- *
- * @param handshakeMessage message received in the handshake
- * @return payload or empty byte[] if no payload was in the handshake message
- * @throws HandshakeException if an error occurs in parsing the handshake message
- */
- byte[] parseHandshakeMessage(byte[] handshakeMessage) throws HandshakeException;
-
- /**
- * Creates a full {@link D2DConnectionContext}. May only be called if
- * {@link #isHandshakeComplete()} returns true.
- *
- * @return a full {@link D2DConnectionContext}
- * @throws HandshakeException if a connection context cannot be created
- */
- D2DConnectionContext toConnectionContext() throws HandshakeException;
-}
diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/Ed25519.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/Ed25519.java
deleted file mode 100644
index 454b942..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securegcm/Ed25519.java
+++ /dev/null
@@ -1,270 +0,0 @@
-// Copyright 2020 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 static java.math.BigInteger.ONE;
-import static java.math.BigInteger.ZERO;
-
-import com.google.common.annotations.VisibleForTesting;
-import java.math.BigInteger;
-
-/**
- * Implements the Ed25519 twisted Edwards curve. See http://ed25519.cr.yp.to/ for more details.
- */
-public class Ed25519 {
-
- // Don't instantiate
- private Ed25519() { }
-
- // Curve parameters (http://ed25519.cr.yp.to/)
- private static final int HEX_RADIX = 16;
- private static final BigInteger Ed25519_P =
- new BigInteger("7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED", HEX_RADIX);
- private static final BigInteger Ed25519_D =
- new BigInteger("52036CEE2B6FFE738CC740797779E89800700A4D4141D8AB75EB4DCA135978A3", HEX_RADIX);
-
- // Helps to do fast addition k = 2*d
- private static final BigInteger Ed25519_K =
- new BigInteger("2406D9DC56DFFCE7198E80F2EEF3D13000E0149A8283B156EBD69B9426B2F159", HEX_RADIX);
-
- // Identity point in extended representation (0, 1, 1, 0)
- static final BigInteger[] IDENTITY_POINT = new BigInteger[] {ZERO, ONE, ONE, ZERO};
-
- // Helps for reading coordinate type in point representation
- private static final int X = 0;
- private static final int Y = 1;
- private static final int Z = 2;
- private static final int T = 3;
-
- // Number of bits that we need to represent a point. Realistically, we only need 255, but using
- // 256 doesn't hurt.
- private static final int POINT_SIZE_BITS = 256;
-
- /**
- * Returns the result of multiplying point p by scalar k. A point is represented as a BigInteger
- * array of length 2 where the first element (at index 0) is the X coordinate and the second
- * element (at index 1) is the Y coordinate.
- */
- public static BigInteger[] scalarMultiplyAffinePoint(BigInteger[] p, BigInteger k)
- throws Ed25519Exception {
- return toAffine(scalarMultiplyExtendedPoint(toExtended(p), k));
- }
-
- /**
- * Returns the sum of two points in affine representation. A point is represented as a BigInteger
- * array of length 2 where the first element (at index 0) is the X coordinate and the second
- * element (at index 1) is the Y coordinate.
- */
- public static BigInteger[] addAffinePoints(BigInteger[] p1, BigInteger[] p2)
- throws Ed25519Exception {
- return toAffine(addExtendedPoints(toExtended(p1), toExtended(p2)));
- }
-
- /**
- * Returns the result of subtracting p2 from p1 (i.e., p1 - p2) in affine representation. A point
- * is represented as a BigInteger array of length 2 where the first element (at index 0) is the X
- * coordinate and the second element (at index 1) is the Y coordinate.
- */
- public static BigInteger[] subtractAffinePoints(BigInteger[] p1, BigInteger[] p2)
- throws Ed25519Exception {
- return toAffine(subtractExtendedPoints(toExtended(p1), toExtended(p2)));
- }
-
- /**
- * Validates that a given point in affine representation is on the curve and is positive.
- * @throws Ed25519Exception if the point does not validate
- */
- public static void validateAffinePoint(BigInteger[] p) throws Ed25519Exception {
- checkPointIsInAffineRepresentation(p);
-
- BigInteger x = p[X];
- BigInteger y = p[Y];
-
- if (x.signum() != 1 || y.signum() != 1) {
- throw new Ed25519Exception("Point encoding must use only positive integers");
- }
-
- if ((x.compareTo(Ed25519_P) >= 0) || (y.compareTo(Ed25519_P) >= 0)) {
- throw new Ed25519Exception("Point lies outside of the expected field");
- }
-
- BigInteger xx = x.multiply(x);
- BigInteger yy = y.multiply(y);
- BigInteger lhs = xx.negate().add(yy).mod(Ed25519_P); // -x*x + y*y
- BigInteger rhs = ONE.add(Ed25519_D.multiply(xx).multiply(yy)).mod(Ed25519_P); // 1 + d*x*x*y*y
-
- if (!lhs.equals(rhs)) {
- throw new Ed25519Exception("Point does not lie on the expected curve");
- }
- }
-
- /**
- * Returns the result of multiplying point p by scalar k
- */
- static BigInteger[] scalarMultiplyExtendedPoint(BigInteger[] p, BigInteger k)
- throws Ed25519Exception {
- checkPointIsInExtendedRepresentation(p);
- if (k == null) {
- throw new Ed25519Exception("Can't multiply point by null");
- }
-
- if (k.bitLength() > POINT_SIZE_BITS) {
- throw new Ed25519Exception(
- "Refuse to multiply point by scalar with more than " + POINT_SIZE_BITS + " bits");
- }
-
- // Perform best effort time-constant accumulation
- BigInteger[] q = IDENTITY_POINT;
- BigInteger[] r = IDENTITY_POINT;
- BigInteger[] doubleAccumulator = p;
- for (int i = 0; i < POINT_SIZE_BITS; i++) {
- if (k.testBit(i)) {
- q = addExtendedPoints(q, doubleAccumulator);
- } else {
- r = addExtendedPoints(q, doubleAccumulator);
- }
- if (i < POINT_SIZE_BITS - 1) {
- doubleAccumulator = doubleExtendedPoint(doubleAccumulator);
- }
- }
-
- // Not needed, but we're just trying to fool the compiler into not optimizing away r
- r = subtractExtendedPoints(r, r);
- q = addExtendedPoints(q, r);
- return q;
- }
-
- /**
- * Returns the doubling of a point in extended representation
- */
- private static BigInteger[] doubleExtendedPoint(BigInteger[] p) throws Ed25519Exception {
- // The Edwards curve is complete, so we can just add a point to itself.
- // Note that the currently best known algorithms for doubling have the same order as addition.
- // https://hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html
- checkPointIsInExtendedRepresentation(p);
-
- BigInteger c = p[T].pow(2).multiply(Ed25519_K);
- BigInteger d = p[Z].pow(2).shiftLeft(1);
- BigInteger e = p[Y].multiply(p[X]).shiftLeft(2);
- BigInteger f = d.subtract(c);
- BigInteger g = d.add(c);
- BigInteger h = p[Y].pow(2).add(p[X].pow(2)).shiftLeft(1);
-
- return new BigInteger[] {
- e.multiply(f).mod(Ed25519_P),
- g.multiply(h).mod(Ed25519_P),
- f.multiply(g).mod(Ed25519_P),
- e.multiply(h).mod(Ed25519_P)
- };
- }
-
- /**
- * Returns the result of subtracting p2 from p1 (p1 - p2)
- */
- static BigInteger[] subtractExtendedPoints(BigInteger[] p1, BigInteger[] p2)
- throws Ed25519Exception {
- checkPointIsInExtendedRepresentation(p1);
- checkPointIsInExtendedRepresentation(p2);
-
- return addExtendedPoints(p1, new BigInteger[] {p2[X].negate(), p2[Y], p2[Z], p2[T].negate()});
- }
-
- /**
- * Returns the sum of two points in extended representation
- * Uses: https://hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html#addition-add-2008-hwcd-3
- */
- static BigInteger[] addExtendedPoints(BigInteger[] p1, BigInteger[] p2)
- throws Ed25519Exception {
- checkPointIsInExtendedRepresentation(p1);
- checkPointIsInExtendedRepresentation(p2);
-
- BigInteger a = p1[Y].subtract(p1[X]).multiply(p2[Y].subtract(p2[X]));
- BigInteger b = p1[Y].add(p1[X]).multiply(p2[Y].add(p2[X]));
- BigInteger c = p1[T].multiply(Ed25519_K).multiply(p2[T]);
- BigInteger d = p1[Z].add(p1[Z]).multiply(p2[Z]);
- BigInteger e = b.subtract(a);
- BigInteger f = d.subtract(c);
- BigInteger g = d.add(c);
- BigInteger h = b.add(a);
-
- return new BigInteger[] {
- e.multiply(f).mod(Ed25519_P),
- g.multiply(h).mod(Ed25519_P),
- f.multiply(g).mod(Ed25519_P),
- e.multiply(h).mod(Ed25519_P)
- };
- }
-
- /** Converts a point in affine representation to extended representation */
- // TODO(b/120887495): This @VisibleForTesting annotation was being ignored by prod code.
- // Please check that removing it is correct, and remove this comment along with it.
- // @VisibleForTesting
- static BigInteger[] toExtended(BigInteger[] p) throws Ed25519Exception {
- checkPointIsInAffineRepresentation(p);
-
- return new BigInteger[] {p[X], p[Y], ONE, p[X].multiply(p[Y]).mod(Ed25519_P)}; // x, y, 1, x*y
- }
-
- /** Converts a point in extended representation to affine representation */
- // TODO(b/120887495): This @VisibleForTesting annotation was being ignored by prod code.
- // Please check that removing it is correct, and remove this comment along with it.
- // @VisibleForTesting
- static BigInteger[] toAffine(BigInteger[] p) throws Ed25519Exception {
- checkPointIsInExtendedRepresentation(p);
-
- return new BigInteger[] {p[X].multiply(p[Z].modInverse(Ed25519_P)).mod(Ed25519_P), // x = X / Z
- p[Y].multiply(p[Z].modInverse(Ed25519_P)).mod(Ed25519_P)}; // y = Y / Z
- }
-
- /**
- * Checks that a given point is in the extended representation
- * @throws Ed25519Exception if the point is not in the extended representation
- */
- @VisibleForTesting
- static void checkPointIsInExtendedRepresentation(BigInteger[] p) throws Ed25519Exception {
- if (p == null || p.length != 4 || p[X] == null || p[Y] == null || p[Z] == null
- || p[T] == null) {
- throw new Ed25519Exception("Point is not in extended representation");
- }
- }
-
- /**
- * Checks that a given point is in the affine representation
- * @throws Ed25519Exception if the point is not in the affine representation
- */
- @VisibleForTesting
- static void checkPointIsInAffineRepresentation(BigInteger[] p) throws Ed25519Exception {
- if (p == null || p.length != 2 || p[X] == null || p[Y] == null) {
- throw new Ed25519Exception("Point is not in affine representation");
- }
- }
-
- /**
- * Represents an unrecoverable error that has occurred while performing a curve operation.
- */
- public static class Ed25519Exception extends Exception {
- public Ed25519Exception(String message) {
- super(message);
- }
-
- public Ed25519Exception(Exception e) {
- super(e);
- }
-
- public Ed25519Exception(String message, Exception e) {
- super(message, e);
- }
- }
-}
diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/EnrollmentCryptoOps.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/EnrollmentCryptoOps.java
deleted file mode 100644
index 450c806..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securegcm/EnrollmentCryptoOps.java
+++ /dev/null
@@ -1,233 +0,0 @@
-// Copyright 2020 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.InvalidProtocolBufferException;
-import com.google.security.annotations.SuppressInsecureCipherModeCheckerPendingReview;
-import com.google.security.cryptauth.lib.securegcm.SecureGcmProto.GcmDeviceInfo;
-import com.google.security.cryptauth.lib.securegcm.SecureGcmProto.GcmMetadata;
-import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.PayloadType;
-import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType;
-import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType;
-import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageBuilder;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageParser;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBody;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage;
-import java.security.InvalidKeyException;
-import java.security.KeyPair;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.SignatureException;
-import java.security.spec.InvalidKeySpecException;
-import java.util.Arrays;
-import javax.crypto.KeyAgreement;
-import javax.crypto.SecretKey;
-
-/**
- * Utility class for implementing Secure GCM enrollment flows.
- */
-public class EnrollmentCryptoOps {
-
- private EnrollmentCryptoOps() { } // Do not instantiate
-
- /**
- * Type of symmetric key signature to use for the signcrypted "outer layer" message.
- */
- private static final SigType OUTER_SIG_TYPE = SigType.HMAC_SHA256;
-
- /**
- * Type of symmetric key encryption to use for the signcrypted "outer layer" message.
- */
- private static final EncType OUTER_ENC_TYPE = EncType.AES_256_CBC;
-
- /**
- * Type of public key signature to use for the (cleartext) "inner layer" message.
- */
- private static final SigType INNER_SIG_TYPE = SigType.ECDSA_P256_SHA256;
-
- /**
- * Type of public key signature to use for the (cleartext) "inner layer" message on platforms that
- * don't support Elliptic Curve operations (such as old Android versions).
- */
- private static final SigType LEGACY_INNER_SIG_TYPE = SigType.RSA2048_SHA256;
-
- /**
- * Which {@link KeyAgreement} algorithm to use.
- */
- private static final String KA_ALG = "ECDH";
-
- /**
- * Which {@link KeyAgreement} algorithm to use on platforms that don't support Elliptic Curve.
- */
- private static final String LEGACY_KA_ALG = "DH";
-
- /**
- * Used by both the client and server to perform a key exchange.
- *
- * @return a {@link SecretKey} derived from the key exchange
- * @throws InvalidKeyException if either of the input keys is of the wrong type
- */
- @SuppressInsecureCipherModeCheckerPendingReview // b/32143855
- public static SecretKey doKeyAgreement(PrivateKey myKey, PublicKey peerKey)
- throws InvalidKeyException {
- String alg = KA_ALG;
- if (KeyEncoding.isLegacyPrivateKey(myKey)) {
- alg = LEGACY_KA_ALG;
- }
- KeyAgreement agreement;
- try {
- agreement = KeyAgreement.getInstance(alg);
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(e);
- }
-
- agreement.init(myKey);
- agreement.doPhase(peerKey, true);
- byte[] agreedKey = agreement.generateSecret();
-
- // Derive a 256-bit AES key by using sha256 on the Diffie-Hellman output
- return KeyEncoding.parseMasterKey(sha256(agreedKey));
- }
-
- public static KeyPair generateEnrollmentKeyAgreementKeyPair(boolean isLegacy) {
- if (isLegacy) {
- return PublicKeyProtoUtil.generateDh2048KeyPair();
- }
- return PublicKeyProtoUtil.generateEcP256KeyPair();
- }
-
- /**
- * @return SHA-256 hash of {@code masterKey}
- */
- public static byte[] getMasterKeyHash(SecretKey masterKey) {
- return sha256(masterKey.getEncoded());
- }
-
- /**
- * Used by the client to signcrypt an enrollment request before sending it to the server.
- *
- * <p>Note: You <em>MUST</em> correctly set the value of the {@code device_master_key_hash} on
- * {@code enrollmentInfo} from {@link #getMasterKeyHash(SecretKey)} before calling this method.
- *
- * @param enrollmentInfo the enrollment request to send to the server. You must correctly set
- * the {@code device_master_key_hash} field.
- * @param masterKey the shared key derived from the key agreement
- * @param signingKey the signing key corresponding to the user's {@link PublicKey} being enrolled
- * @return the encrypted enrollment message
- * @throws IllegalArgumentException if {@code enrollmentInfo} doesn't have a valid
- * {@code device_master_key_hash}
- * @throws InvalidKeyException if {@code masterKey} or {@code signingKey} is the wrong type
- */
- public static byte[] encryptEnrollmentMessage(
- GcmDeviceInfo enrollmentInfo, SecretKey masterKey, PrivateKey signingKey)
- throws InvalidKeyException, NoSuchAlgorithmException {
- if ((enrollmentInfo == null) || (masterKey == null) || (signingKey == null)) {
- throw new NullPointerException();
- }
-
- if (!Arrays.equals(enrollmentInfo.getDeviceMasterKeyHash().toByteArray(),
- getMasterKeyHash(masterKey))) {
- throw new IllegalArgumentException("DeviceMasterKeyHash not set correctly");
- }
-
- // First create the inner message, which is basically a self-signed certificate
- SigType sigType =
- KeyEncoding.isLegacyPrivateKey(signingKey) ? LEGACY_INNER_SIG_TYPE : INNER_SIG_TYPE;
- SecureMessage innerMsg = new SecureMessageBuilder()
- .setVerificationKeyId(enrollmentInfo.getUserPublicKey().toByteArray())
- .buildSignedCleartextMessage(signingKey, sigType, enrollmentInfo.toByteArray());
-
- // Next create the outer message, which uses the newly exchanged master key to signcrypt
- SecureMessage outerMsg = new SecureMessageBuilder()
- .setVerificationKeyId(new byte[] {}) // Empty
- .setPublicMetadata(GcmMetadata.newBuilder()
- .setType(PayloadType.ENROLLMENT.getType())
- .setVersion(SecureGcmConstants.SECURE_GCM_VERSION)
- .build()
- .toByteArray())
- .buildSignCryptedMessage(
- masterKey, OUTER_SIG_TYPE, masterKey, OUTER_ENC_TYPE, innerMsg.toByteArray());
- return outerMsg.toByteArray();
- }
-
- /**
- * Used by the server to decrypt the client's enrollment request.
- * @param enrollmentMessage generated by the client's call to
- * {@link #encryptEnrollmentMessage(GcmDeviceInfo, SecretKey, PrivateKey)}
- * @param masterKey the shared key derived from the key agreement
- * @return the client's enrollment request data
- * @throws SignatureException if {@code enrollmentMessage} is malformed or has been tampered with
- * @throws InvalidKeyException if {@code masterKey} is the wrong type
- */
- public static GcmDeviceInfo decryptEnrollmentMessage(
- byte[] enrollmentMessage, SecretKey masterKey, boolean isLegacy)
- throws SignatureException, InvalidKeyException, NoSuchAlgorithmException {
- if ((enrollmentMessage == null) || (masterKey == null)) {
- throw new NullPointerException();
- }
-
- HeaderAndBody outerHeaderAndBody;
- GcmMetadata outerMetadata;
- HeaderAndBody innerHeaderAndBody;
- byte[] encodedUserPublicKey;
- GcmDeviceInfo enrollmentInfo;
- try {
- SecureMessage outerMsg = SecureMessage.parseFrom(enrollmentMessage);
- outerHeaderAndBody = SecureMessageParser.parseSignCryptedMessage(
- outerMsg, masterKey, OUTER_SIG_TYPE, masterKey, OUTER_ENC_TYPE);
- outerMetadata = GcmMetadata.parseFrom(outerHeaderAndBody.getHeader().getPublicMetadata());
-
- SecureMessage innerMsg = SecureMessage.parseFrom(outerHeaderAndBody.getBody());
- encodedUserPublicKey = SecureMessageParser.getUnverifiedHeader(innerMsg)
- .getVerificationKeyId().toByteArray();
- PublicKey userPublicKey = KeyEncoding.parseUserPublicKey(encodedUserPublicKey);
- SigType sigType = isLegacy ? LEGACY_INNER_SIG_TYPE : INNER_SIG_TYPE;
- innerHeaderAndBody = SecureMessageParser.parseSignedCleartextMessage(
- innerMsg, userPublicKey, sigType);
- enrollmentInfo = GcmDeviceInfo.parseFrom(innerHeaderAndBody.getBody());
- } catch (InvalidProtocolBufferException e) {
- throw new SignatureException(e);
- } catch (InvalidKeySpecException e) {
- throw new SignatureException(e);
- }
-
- boolean verified =
- (outerMetadata.getType() == PayloadType.ENROLLMENT.getType())
- && (outerMetadata.getVersion() <= SecureGcmConstants.SECURE_GCM_VERSION)
- && outerHeaderAndBody.getHeader().getVerificationKeyId().isEmpty()
- && innerHeaderAndBody.getHeader().getPublicMetadata().isEmpty()
- // Verify the encoded public key we used matches the encoded public key key being enrolled
- && Arrays.equals(encodedUserPublicKey, enrollmentInfo.getUserPublicKey().toByteArray())
- && Arrays.equals(getMasterKeyHash(masterKey),
- enrollmentInfo.getDeviceMasterKeyHash().toByteArray());
-
- if (verified) {
- return enrollmentInfo;
- }
- throw new SignatureException();
- }
-
- static byte[] sha256(byte[] input) {
- try {
- MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
- return sha256.digest(input);
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(e); // Shouldn't happen
- }
- }
-}
diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/HandshakeException.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/HandshakeException.java
deleted file mode 100644
index b717eb6..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securegcm/HandshakeException.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright 2020 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;
-
-/**
- * Represents an unrecoverable error that has occurred during the handshake procedure.
- */
-public class HandshakeException extends Exception {
- public HandshakeException(String message) {
- super(message);
- }
-
- public HandshakeException(Exception e) {
- super(e);
- }
-
- public HandshakeException(String message, Exception e) {
- super(message, e);
- }
-}
diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/KeyEncoding.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/KeyEncoding.java
deleted file mode 100644
index 67e4ace..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securegcm/KeyEncoding.java
+++ /dev/null
@@ -1,180 +0,0 @@
-// Copyright 2020 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.InvalidProtocolBufferException;
-import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.GenericPublicKey;
-import java.security.KeyFactory;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.PKCS8EncodedKeySpec;
-import javax.crypto.SecretKey;
-import javax.crypto.interfaces.DHPrivateKey;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * Utility class for encoding and parsing keys used by SecureGcm.
- */
-public class KeyEncoding {
- private KeyEncoding() {} // Do not instantiate
-
- private static boolean simulateLegacyCryptoRequired = false;
-
- /**
- * The JCA algorithm name to use when encoding/decoding symmetric keys.
- */
- static final String SYMMETRIC_KEY_ENCODING_ALG = "AES";
-
- public static byte[] encodeMasterKey(SecretKey masterKey) {
- return masterKey.getEncoded();
- }
-
- public static SecretKey parseMasterKey(byte[] encodedMasterKey) {
- return new SecretKeySpec(encodedMasterKey, SYMMETRIC_KEY_ENCODING_ALG);
- }
-
- public static byte[] encodeUserPublicKey(PublicKey pk) {
- return encodePublicKey(pk);
- }
-
- public static byte[] encodeUserPrivateKey(PrivateKey sk) {
- return sk.getEncoded();
- }
-
- public static byte[] encodeDeviceSyncGroupPublicKey(PublicKey pk) {
- return PublicKeyProtoUtil.encodePaddedEcPublicKey(pk).toByteArray();
- }
-
- public static PrivateKey parseUserPrivateKey(byte[] encodedPrivateKey, boolean isLegacy)
- throws InvalidKeySpecException {
- PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encodedPrivateKey);
- if (isLegacy) {
- return getRsaKeyFactory().generatePrivate(keySpec);
- }
- return getEcKeyFactory().generatePrivate(keySpec);
- }
-
- public static PublicKey parseUserPublicKey(byte[] keyBytes) throws InvalidKeySpecException {
- return parsePublicKey(keyBytes);
- }
-
- public static PublicKey parseDeviceSyncGroupPublicKey(byte[] keyBytes)
- throws InvalidKeySpecException {
- return parsePublicKey(keyBytes);
- }
-
- public static byte[] encodeKeyAgreementPublicKey(PublicKey pk) {
- return encodePublicKey(pk);
- }
-
- public static PublicKey parseKeyAgreementPublicKey(byte[] keyBytes)
- throws InvalidKeySpecException {
- return parsePublicKey(keyBytes);
- }
-
- public static byte[] encodeKeyAgreementPrivateKey(PrivateKey sk) {
- if (isLegacyPrivateKey(sk)) {
- return PublicKeyProtoUtil.encodeDh2048PrivateKey((DHPrivateKey) sk);
- }
- return sk.getEncoded();
- }
-
- public static PrivateKey parseKeyAgreementPrivateKey(byte[] keyBytes, boolean isLegacy)
- throws InvalidKeySpecException {
- if (isLegacy) {
- return PublicKeyProtoUtil.parseDh2048PrivateKey(keyBytes);
- }
- PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
- return getEcKeyFactory().generatePrivate(keySpec);
- }
-
- public static byte[] encodeSigningPublicKey(PublicKey pk) {
- return encodePublicKey(pk);
- }
-
- public static PublicKey parseSigningPublicKey(byte[] keyBytes) throws InvalidKeySpecException {
- return parsePublicKey(keyBytes);
- }
-
- public static byte[] encodeSigningPrivateKey(PrivateKey sk) {
- return sk.getEncoded();
- }
-
- public static PrivateKey parseSigningPrivateKey(byte[] keyBytes) throws InvalidKeySpecException {
- PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
- return getEcKeyFactory().generatePrivate(keySpec);
- }
-
- public static boolean isLegacyPublicKey(PublicKey pk) {
- if (pk instanceof ECPublicKey) {
- return false;
- }
- return true;
- }
-
- public static boolean isLegacyPrivateKey(PrivateKey sk) {
- if (sk instanceof ECPrivateKey) {
- return false;
- }
- return true;
- }
-
- public static boolean isLegacyCryptoRequired() {
- return PublicKeyProtoUtil.isLegacyCryptoRequired() || simulateLegacyCryptoRequired;
- }
-
- /**
- * When testing, use this to force {@link #isLegacyCryptoRequired()} to return {@code true}
- */
- // @VisibleForTesting
- public static void setSimulateLegacyCrypto(boolean forceLegacy) {
- simulateLegacyCryptoRequired = forceLegacy;
- }
-
- private static byte[] encodePublicKey(PublicKey pk) {
- return PublicKeyProtoUtil.encodePublicKey(pk).toByteArray();
- }
-
- private static PublicKey parsePublicKey(byte[] keyBytes) throws InvalidKeySpecException {
- try {
- return PublicKeyProtoUtil.parsePublicKey(GenericPublicKey.parseFrom(keyBytes));
- } catch (InvalidProtocolBufferException e) {
- throw new InvalidKeySpecException("Unable to parse GenericPublicKey", e);
- } catch (IllegalArgumentException e) {
- throw new InvalidKeySpecException("Unable to parse GenericPublicKey", e);
- }
- }
-
- static KeyFactory getEcKeyFactory() {
- try {
- return KeyFactory.getInstance("EC");
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(e); // No ECDH provider available
- }
- }
-
- static KeyFactory getRsaKeyFactory() {
- try {
- return KeyFactory.getInstance("RSA");
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(e); // No RSA provider available
- }
- }
-}
diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/SecureGcmConstants.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/SecureGcmConstants.java
deleted file mode 100644
index a69431f..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securegcm/SecureGcmConstants.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2020 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;
-
-/**
- * A container for GCM related constants used by SecureGcm channels.
- */
-public final class SecureGcmConstants {
- private SecureGcmConstants() {} // Do not instantiate
-
- public static final int SECURE_GCM_VERSION = 1;
-
- /**
- * The GCM sender identity used by this library (GMSCore).
- */
- public static final String SENDER_ID = "745476177629";
-
- /**
- * The key used for indexing the GCM {@link TransportCryptoOps.Payload} within {@code AppData}.
- */
- public static final String MESSAGE_KEY = "P";
-
- /**
- * The origin that should be use for GCM device enrollments.
- */
- public static final String GOOGLE_ORIGIN = "google.com";
-
- /**
- * The origin that should be use for GCM Legacy android device enrollments.
- */
- public static final String LEGACY_ANDROID_ORIGIN = "c.g.a.gms";
-
- /**
- * The name of the protocol this library speaks.
- */
- public static final String PROTOCOL_TYPE_NAME = "gcmV1";
-}
diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/TransportCryptoOps.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/TransportCryptoOps.java
deleted file mode 100644
index b053bf4..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securegcm/TransportCryptoOps.java
+++ /dev/null
@@ -1,268 +0,0 @@
-// Copyright 2020 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.InvalidProtocolBufferException;
-import com.google.security.cryptauth.lib.securegcm.SecureGcmProto.GcmMetadata;
-import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType;
-import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageBuilder;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageParser;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBody;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage;
-import java.security.InvalidKeyException;
-import java.security.KeyPair;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.SignatureException;
-import java.security.interfaces.ECPublicKey;
-import java.security.interfaces.RSAPublicKey;
-import javax.crypto.SecretKey;
-
-/**
- * Utility class for implementing a secure transport for GCM messages.
- */
-public class TransportCryptoOps {
- private TransportCryptoOps() {} // Do not instantiate
-
- /**
- * A type safe version of the {@link SecureGcmProto} {@code Type} codes.
- */
- public enum PayloadType {
- ENROLLMENT(SecureGcmProto.Type.ENROLLMENT),
- TICKLE(SecureGcmProto.Type.TICKLE),
- TX_REQUEST(SecureGcmProto.Type.TX_REQUEST),
- TX_REPLY(SecureGcmProto.Type.TX_REPLY),
- TX_SYNC_REQUEST(SecureGcmProto.Type.TX_SYNC_REQUEST),
- TX_SYNC_RESPONSE(SecureGcmProto.Type.TX_SYNC_RESPONSE),
- TX_PING(SecureGcmProto.Type.TX_PING),
- DEVICE_INFO_UPDATE(SecureGcmProto.Type.DEVICE_INFO_UPDATE),
- TX_CANCEL_REQUEST(SecureGcmProto.Type.TX_CANCEL_REQUEST),
- LOGIN_NOTIFICATION(SecureGcmProto.Type.LOGIN_NOTIFICATION),
- PROXIMITYAUTH_PAIRING(SecureGcmProto.Type.PROXIMITYAUTH_PAIRING),
- GCMV1_IDENTITY_ASSERTION(SecureGcmProto.Type.GCMV1_IDENTITY_ASSERTION),
- DEVICE_TO_DEVICE_RESPONDER_HELLO_PAYLOAD(
- SecureGcmProto.Type.DEVICE_TO_DEVICE_RESPONDER_HELLO_PAYLOAD),
- DEVICE_TO_DEVICE_MESSAGE(SecureGcmProto.Type.DEVICE_TO_DEVICE_MESSAGE),
- DEVICE_PROXIMITY_CALLBACK(SecureGcmProto.Type.DEVICE_PROXIMITY_CALLBACK),
- UNLOCK_KEY_SIGNED_CHALLENGE(SecureGcmProto.Type.UNLOCK_KEY_SIGNED_CHALLENGE);
-
- private final SecureGcmProto.Type type;
- PayloadType(SecureGcmProto.Type type) {
- this.type = type;
- }
-
- public SecureGcmProto.Type getType() {
- return this.type;
- }
-
- public static PayloadType valueOf(SecureGcmProto.Type type) {
- return PayloadType.valueOf(type.getNumber());
- }
-
- public static PayloadType valueOf(int type) {
- for (PayloadType payloadType : PayloadType.values()) {
- if (payloadType.getType().getNumber() == type) {
- return payloadType;
- }
- }
- throw new IllegalArgumentException("Unsupported payload type: " + type);
- }
- }
-
- /**
- * Encapsulates a {@link PayloadType} specifier, and a corresponding raw {@code message} payload.
- */
- public static class Payload {
- private final PayloadType payloadType;
- private final byte[] message;
-
- public Payload(PayloadType payloadType, byte[] message) {
- if ((payloadType == null) || (message == null)) {
- throw new NullPointerException();
- }
- this.payloadType = payloadType;
- this.message = message;
- }
-
- public PayloadType getPayloadType() {
- return payloadType;
- }
-
- public byte[] getMessage() {
- return message;
- }
- }
-
- /**
- * Used by the the server-side to send a secure {@link Payload} to the client.
- *
- * @param masterKey used to signcrypt the {@link Payload}
- * @param keyHandle the name by which the client refers to the specified {@code masterKey}
- */
- public static byte[] signcryptServerMessage(
- Payload payload, SecretKey masterKey, byte[] keyHandle)
- throws InvalidKeyException, NoSuchAlgorithmException {
- if ((payload == null) || (masterKey == null) || (keyHandle == null)) {
- throw new NullPointerException();
- }
- return new SecureMessageBuilder()
- .setVerificationKeyId(keyHandle)
- .setPublicMetadata(GcmMetadata.newBuilder()
- .setType(payload.getPayloadType().getType())
- .setVersion(SecureGcmConstants.SECURE_GCM_VERSION)
- .build()
- .toByteArray())
- .buildSignCryptedMessage(
- masterKey,
- SigType.HMAC_SHA256,
- masterKey,
- EncType.AES_256_CBC,
- payload.getMessage())
- .toByteArray();
- }
-
- /**
- * Extracts the {@code keyHandle} from a {@code signcryptedMessage}.
- *
- * @see #signcryptServerMessage(Payload, SecretKey, byte[])
- */
- public static byte[] getKeyHandleFor(byte[] signcryptedServerMessage)
- throws InvalidProtocolBufferException {
- if (signcryptedServerMessage == null) {
- throw new NullPointerException();
- }
- SecureMessage secmsg = SecureMessage.parseFrom(signcryptedServerMessage);
- return SecureMessageParser.getUnverifiedHeader(secmsg).getVerificationKeyId().toByteArray();
- }
-
- /**
- * Used by a client to recover a secure {@link Payload} sent by the server-side.
- *
- * @see #getKeyHandleFor(byte[])
- * @see #signcryptServerMessage(Payload, SecretKey, byte[])
- */
- public static Payload verifydecryptServerMessage(
- byte[] signcryptedServerMessage, SecretKey masterKey)
- throws SignatureException, InvalidKeyException, NoSuchAlgorithmException {
- if ((signcryptedServerMessage == null) || (masterKey == null)) {
- throw new NullPointerException();
- }
- try {
- SecureMessage secmsg = SecureMessage.parseFrom(signcryptedServerMessage);
- HeaderAndBody parsed = SecureMessageParser.parseSignCryptedMessage(
- secmsg,
- masterKey,
- SigType.HMAC_SHA256,
- masterKey,
- EncType.AES_256_CBC);
- GcmMetadata metadata = GcmMetadata.parseFrom(parsed.getHeader().getPublicMetadata());
- if (metadata.getVersion() > SecureGcmConstants.SECURE_GCM_VERSION) {
- throw new SignatureException("Unsupported protocol version");
- }
- return new Payload(PayloadType.valueOf(metadata.getType()), parsed.getBody().toByteArray());
- } catch (InvalidProtocolBufferException | IllegalArgumentException e) {
- throw new SignatureException(e);
- }
- }
-
- /**
- * Used by the the client-side to send a secure {@link Payload} to the client.
- *
- * @param userKeyPair used to sign the {@link Payload}. In particular, the {@link PrivateKey}
- * portion is used for signing, and (the {@link PublicKey} portion is sent to the server.
- * @param masterKey used to encrypt the {@link Payload}
- */
- public static byte[] signcryptClientMessage(
- Payload payload, KeyPair userKeyPair, SecretKey masterKey)
- throws InvalidKeyException, NoSuchAlgorithmException {
- if ((payload == null) || (masterKey == null)) {
- throw new NullPointerException();
- }
-
- PublicKey userPublicKey = userKeyPair.getPublic();
- PrivateKey userPrivateKey = userKeyPair.getPrivate();
-
- return new SecureMessageBuilder()
- .setVerificationKeyId(KeyEncoding.encodeUserPublicKey(userPublicKey))
- .setPublicMetadata(GcmMetadata.newBuilder()
- .setType(payload.getPayloadType().getType())
- .setVersion(SecureGcmConstants.SECURE_GCM_VERSION)
- .build()
- .toByteArray())
- .buildSignCryptedMessage(
- userPrivateKey,
- getSigTypeFor(userPublicKey),
- masterKey,
- EncType.AES_256_CBC,
- payload.getMessage())
- .toByteArray();
- }
-
- /**
- * Used by the server-side to recover a secure {@link Payload} sent by a client.
- *
- * @see #getEncodedUserPublicKeyFor(byte[])
- * @see #signcryptClientMessage(Payload, KeyPair, SecretKey)
- */
- public static Payload verifydecryptClientMessage(
- byte[] signcryptedClientMessage, PublicKey userPublicKey, SecretKey masterKey)
- throws SignatureException, InvalidKeyException, NoSuchAlgorithmException {
- if ((signcryptedClientMessage == null) || (masterKey == null)) {
- throw new NullPointerException();
- }
- try {
- SecureMessage secmsg = SecureMessage.parseFrom(signcryptedClientMessage);
- HeaderAndBody parsed = SecureMessageParser.parseSignCryptedMessage(
- secmsg,
- userPublicKey,
- getSigTypeFor(userPublicKey),
- masterKey,
- EncType.AES_256_CBC);
- GcmMetadata metadata = GcmMetadata.parseFrom(parsed.getHeader().getPublicMetadata());
- if (metadata.getVersion() > SecureGcmConstants.SECURE_GCM_VERSION) {
- throw new SignatureException("Unsupported protocol version");
- }
- return new Payload(PayloadType.valueOf(metadata.getType()), parsed.getBody().toByteArray());
- } catch (InvalidProtocolBufferException | IllegalArgumentException e) {
- throw new SignatureException(e);
- }
- }
-
- /**
- * Extracts an encoded {@code userPublicKey} from a {@code signcryptedClientMessage}.
- *
- * @see #signcryptClientMessage(Payload, KeyPair, SecretKey)
- */
- public static byte[] getEncodedUserPublicKeyFor(byte[] signcryptedClientMessage)
- throws InvalidProtocolBufferException {
- if (signcryptedClientMessage == null) {
- throw new NullPointerException();
- }
- SecureMessage secmsg = SecureMessage.parseFrom(signcryptedClientMessage);
- return SecureMessageParser.getUnverifiedHeader(secmsg).getVerificationKeyId().toByteArray();
- }
-
- private static SigType getSigTypeFor(PublicKey userPublicKey) throws InvalidKeyException {
- if (userPublicKey instanceof ECPublicKey) {
- return SigType.ECDSA_P256_SHA256;
- } else if (userPublicKey instanceof RSAPublicKey) {
- return SigType.RSA2048_SHA256;
- } else {
- throw new InvalidKeyException("Unsupported key type");
- }
- }
-}
diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/Ukey2Handshake.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/Ukey2Handshake.java
deleted file mode 100644
index 8e00ea9..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securegcm/Ukey2Handshake.java
+++ /dev/null
@@ -1,1041 +0,0 @@
-// Copyright 2020 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);
- }
-}
diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/device_to_device_messages_config.asciipb b/src/main/java/com/google/security/cryptauth/lib/securegcm/device_to_device_messages_config.asciipb
deleted file mode 100644
index 0e2952c..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securegcm/device_to_device_messages_config.asciipb
+++ /dev/null
@@ -1,3 +0,0 @@
-optimize_mode: LITE_RUNTIME
-
-allowed_message: "securegcm.DeviceToDeviceMessage"
diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/securegcm_config.asciipb b/src/main/java/com/google/security/cryptauth/lib/securegcm/securegcm_config.asciipb
deleted file mode 100644
index d838bd3..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securegcm/securegcm_config.asciipb
+++ /dev/null
@@ -1,4 +0,0 @@
-optimize_mode: LITE_RUNTIME
-
-allowed_enum: "securegcm.Type"
-allowed_message: "securegcm.GcmMetadata"
diff --git a/src/main/java/com/google/security/cryptauth/lib/securemessage/CryptoOps.java b/src/main/java/com/google/security/cryptauth/lib/securemessage/CryptoOps.java
deleted file mode 100644
index 876bd93..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securemessage/CryptoOps.java
+++ /dev/null
@@ -1,564 +0,0 @@
-// Copyright 2020 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.securemessage;
-
-import com.google.security.annotations.SuppressInsecureCipherModeCheckerReviewed;
-import java.io.UnsupportedEncodingException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.Key;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.SecureRandom;
-import java.security.Signature;
-import java.security.SignatureException;
-import javax.annotation.Nullable;
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.Mac;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * Encapsulates the cryptographic operations used by the {@code SecureMessage*} classes.
- */
-public class CryptoOps {
-
- private CryptoOps() {} // Do not instantiate
-
- /**
- * Enum of supported signature types, with additional mappings to indicate the name of the
- * underlying JCA algorithm used to create the signature.
- * @see <a href=
- * "http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html">
- * Java Cryptography Architecture, Standard Algorithm Name Documentation</a>
- */
- public enum SigType {
- HMAC_SHA256(SecureMessageProto.SigScheme.HMAC_SHA256, "HmacSHA256", false),
- ECDSA_P256_SHA256(SecureMessageProto.SigScheme.ECDSA_P256_SHA256, "SHA256withECDSA", true),
- RSA2048_SHA256(SecureMessageProto.SigScheme.RSA2048_SHA256, "SHA256withRSA", true);
-
- public SecureMessageProto.SigScheme getSigScheme() {
- return sigScheme;
- }
-
- public String getJcaName() {
- return jcaName;
- }
-
- public boolean isPublicKeyScheme() {
- return publicKeyScheme;
- }
-
- public static SigType valueOf(SecureMessageProto.SigScheme sigScheme) {
- for (SigType value : values()) {
- if (value.sigScheme.equals(sigScheme)) {
- return value;
- }
- }
- throw new IllegalArgumentException("Unsupported SigType: " + sigScheme);
- }
-
- private final SecureMessageProto.SigScheme sigScheme;
- private final String jcaName;
- private final boolean publicKeyScheme;
-
- SigType(SecureMessageProto.SigScheme sigType, String jcaName, boolean publicKeyScheme) {
- this.sigScheme = sigType;
- this.jcaName = jcaName;
- this.publicKeyScheme = publicKeyScheme;
- }
- }
-
- /**
- * Enum of supported encryption types, with additional mappings to indicate the name of the
- * underlying JCA algorithm used to perform the encryption.
- * @see <a href=
- * "http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html">
- * Java Cryptography Architecture, Standard Algorithm Name Documentation</a>
- */
- public enum EncType {
- NONE(SecureMessageProto.EncScheme.NONE, "InvalidDoNotUseForJCA"),
- AES_256_CBC(SecureMessageProto.EncScheme.AES_256_CBC, "AES/CBC/PKCS5Padding");
-
- public SecureMessageProto.EncScheme getEncScheme() {
- return encScheme;
- }
-
- public String getJcaName() {
- return jcaName;
- }
-
- public static EncType valueOf(SecureMessageProto.EncScheme encScheme) {
- for (EncType value : values()) {
- if (value.encScheme.equals(encScheme)) {
- return value;
- }
- }
- throw new IllegalArgumentException("Unsupported EncType: " + encScheme);
- }
-
- private final SecureMessageProto.EncScheme encScheme;
- private final String jcaName;
-
- EncType(SecureMessageProto.EncScheme encScheme, String jcaName) {
- this.encScheme = encScheme;
- this.jcaName = jcaName;
- }
- }
-
- /**
- * Truncated hash output length, in bytes.
- */
- static final int DIGEST_LENGTH = 20;
- /**
- * A salt value specific to this library, generated as SHA-256("SecureMessage")
- */
- private static final byte[] SALT = sha256("SecureMessage");
-
- /**
- * Signs {@code data} using the algorithm specified by {@code sigType} with {@code signingKey}.
- *
- * @param rng is required for public key signature schemes
- * @return raw signature
- * @throws InvalidKeyException if {@code signingKey} is incompatible with {@code sigType}
- * @throws NoSuchAlgorithmException if the security provider is inadequate for {@code sigType}
- */
- static byte[] sign(
- SigType sigType, Key signingKey, @Nullable SecureRandom rng, byte[] data)
- throws InvalidKeyException, NoSuchAlgorithmException {
- if ((signingKey == null) || (data == null)) {
- throw new NullPointerException();
- }
- if (sigType.isPublicKeyScheme()) {
- if (rng == null) {
- throw new NullPointerException();
- }
- if (!(signingKey instanceof PrivateKey)) {
- throw new InvalidKeyException("Expected a PrivateKey");
- }
- Signature sigScheme = Signature.getInstance(sigType.getJcaName());
- sigScheme.initSign((PrivateKey) signingKey, rng);
- try {
- // We include a fixed magic value (salt) in the signature so that if the signing key is
- // reused in another context we can't be confused -- provided that the other user of the
- // signing key only signs statements that do not begin with this salt.
- sigScheme.update(SALT);
- sigScheme.update(data);
- return sigScheme.sign();
- } catch (SignatureException e) {
- throw new IllegalStateException(e); // Consistent with failures in Mac.doFinal
- }
- } else {
- Mac macScheme = Mac.getInstance(sigType.getJcaName());
- // Note that an AES-256 SecretKey should work with most Mac schemes
- SecretKey derivedKey = deriveAes256KeyFor(getSecretKey(signingKey), getPurpose(sigType));
- macScheme.init(derivedKey);
- return macScheme.doFinal(data);
- }
- }
-
- /**
- * Verifies the {@code signature} on {@code data} using the algorithm specified by
- * {@code sigType} with {@code verificationKey}.
- *
- * @return true iff the signature is verified
- * @throws NoSuchAlgorithmException if the security provider is inadequate for {@code sigType}
- * @throws InvalidKeyException if {@code verificationKey} is incompatible with {@code sigType}
- * @throws SignatureException
- */
- static boolean verify(Key verificationKey, SigType sigType, byte[] signature, byte[] data)
- throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
- if ((verificationKey == null) || (signature == null) || (data == null)) {
- throw new NullPointerException();
- }
- if (sigType.isPublicKeyScheme()) {
- if (!(verificationKey instanceof PublicKey)) {
- throw new InvalidKeyException("Expected a PublicKey");
- }
- Signature sigScheme = Signature.getInstance(sigType.getJcaName());
- sigScheme.initVerify((PublicKey) verificationKey);
- sigScheme.update(SALT); // See the comments in sign() for more on this
- sigScheme.update(data);
- return sigScheme.verify(signature);
- } else {
- Mac macScheme = Mac.getInstance(sigType.getJcaName());
- SecretKey derivedKey =
- deriveAes256KeyFor(getSecretKey(verificationKey), getPurpose(sigType));
- macScheme.init(derivedKey);
- return constantTimeArrayEquals(signature, macScheme.doFinal(data));
- }
- }
-
- /**
- * Generate a random IV appropriate for use with the algorithm specified in {@code encType}.
- *
- * @return a freshly generated IV (a random byte sequence of appropriate length)
- * @throws NoSuchAlgorithmException if the security provider is inadequate for {@code encType}
- */
- @SuppressInsecureCipherModeCheckerReviewed
- // See b/26525455 for security review.
- static byte[] generateIv(EncType encType, SecureRandom rng) throws NoSuchAlgorithmException {
- if (rng == null) {
- throw new NullPointerException();
- }
- try {
- Cipher encrypter = Cipher.getInstance(encType.getJcaName());
- byte[] iv = new byte[encrypter.getBlockSize()];
- rng.nextBytes(iv);
- return iv;
- } catch (NoSuchPaddingException e) {
- throw new NoSuchAlgorithmException(e); // Consolidate into NoSuchAlgorithmException
- }
- }
-
- /**
- * Encrypts {@code plaintext} using the algorithm specified in {@code encType}, with the specified
- * {@code iv} and {@code encryptionKey}.
- *
- * @param rng source of randomness to be used with the specified cipher, if necessary
- * @return encrypted data
- * @throws NoSuchAlgorithmException if the security provider is inadequate for {@code encType}
- * @throws InvalidKeyException if {@code encryptionKey} is incompatible with {@code encType}
- */
- @SuppressInsecureCipherModeCheckerReviewed
- // See b/26525455 for security review.
- static byte[] encrypt(
- Key encryptionKey, EncType encType, @Nullable SecureRandom rng, byte[] iv, byte[] plaintext)
- throws NoSuchAlgorithmException, InvalidKeyException {
- if ((encryptionKey == null) || (iv == null) || (plaintext == null)) {
- throw new NullPointerException();
- }
- if (encType == EncType.NONE) {
- throw new NoSuchAlgorithmException("Cannot use NONE type here");
- }
- try {
- Cipher encrypter = Cipher.getInstance(encType.getJcaName());
- SecretKey derivedKey =
- deriveAes256KeyFor(getSecretKey(encryptionKey), getPurpose(encType));
- encrypter.init(Cipher.ENCRYPT_MODE, derivedKey, new IvParameterSpec(iv), rng);
- return encrypter.doFinal(plaintext);
- } catch (InvalidAlgorithmParameterException e) {
- throw new AssertionError(e); // Should never happen
- } catch (IllegalBlockSizeException e) {
- throw new AssertionError(e); // Should never happen
- } catch (BadPaddingException e) {
- throw new AssertionError(e); // Should never happen
- } catch (NoSuchPaddingException e) {
- throw new NoSuchAlgorithmException(e); // Consolidate into NoSuchAlgorithmException
- }
- }
-
- /**
- * Decrypts {@code ciphertext} using the algorithm specified in {@code encType}, with the
- * specified {@code iv} and {@code decryptionKey}.
- *
- * @return the plaintext (decrypted) data
- * @throws NoSuchAlgorithmException if the security provider is inadequate for {@code encType}
- * @throws InvalidKeyException if {@code decryptionKey} is incompatible with {@code encType}
- * @throws InvalidAlgorithmParameterException if {@code encType} exceeds legal cryptographic
- * strength limits in this jurisdiction
- * @throws IllegalBlockSizeException if {@code ciphertext} contains an illegal block
- * @throws BadPaddingException if {@code ciphertext} contains an illegal padding
- */
- @SuppressInsecureCipherModeCheckerReviewed
- // See b/26525455 for security review
- static byte[] decrypt(Key decryptionKey, EncType encType, byte[] iv, byte[] ciphertext)
- throws NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException,
- IllegalBlockSizeException, BadPaddingException {
- if ((decryptionKey == null) || (iv == null) || (ciphertext == null)) {
- throw new NullPointerException();
- }
- if (encType == EncType.NONE) {
- throw new NoSuchAlgorithmException("Cannot use NONE type here");
- }
- try {
- Cipher decrypter = Cipher.getInstance(encType.getJcaName());
- SecretKey derivedKey =
- deriveAes256KeyFor(getSecretKey(decryptionKey), getPurpose(encType));
- decrypter.init(Cipher.DECRYPT_MODE, derivedKey, new IvParameterSpec(iv));
- return decrypter.doFinal(ciphertext);
- } catch (NoSuchPaddingException e) {
- throw new AssertionError(e); // Should never happen
- }
- }
-
- /**
- * Computes a collision-resistant hash of {@link #DIGEST_LENGTH} bytes
- * (using a truncated SHA-256 output).
- */
- static byte[] digest(byte[] data) throws NoSuchAlgorithmException {
- MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
- byte[] truncatedHash = new byte[DIGEST_LENGTH];
- System.arraycopy(sha256.digest(data), 0, truncatedHash, 0, DIGEST_LENGTH);
- return truncatedHash;
- }
-
- /**
- * Returns {@code true} if the two arrays are equal to one another.
- * When the two arrays differ in length, trivially returns {@code false}.
- * When the two arrays are equal in length, does a constant-time comparison
- * of the two, i.e. does not abort the comparison when the first differing
- * element is found.
- *
- * <p>NOTE: This is a copy of {@code java/com/google/math/crypto/ConstantTime#arrayEquals}.
- *
- * @param a An array to compare
- * @param b Another array to compare
- * @return {@code true} if these arrays are both null or if they have equal
- * length and equal bytes in all elements
- */
- static boolean constantTimeArrayEquals(@Nullable byte[] a, @Nullable byte[] b) {
- if (a == null || b == null) {
- return (a == b);
- }
- if (a.length != b.length) {
- return false;
- }
- byte result = 0;
- for (int i = 0; i < b.length; i++) {
- result = (byte) (result | a[i] ^ b[i]);
- }
- return (result == 0);
- }
-
- // @VisibleForTesting
- static String getPurpose(SigType sigType) {
- return "SIG:" + sigType.getSigScheme().getNumber();
- }
-
- // @VisibleForTesting
- static String getPurpose(EncType encType) {
- return "ENC:" + encType.getEncScheme().getNumber();
- }
-
- private static SecretKey getSecretKey(Key key) throws InvalidKeyException {
- if (!(key instanceof SecretKey)) {
- throw new InvalidKeyException("Expected a SecretKey");
- }
- return (SecretKey) key;
- }
-
- /**
- * @return the UTF-8 encoding of the given string
- * @throws RuntimeException if the UTF-8 charset is not present.
- */
- public static byte[] utf8StringToBytes(String input) {
- try {
- return input.getBytes("UTF-8");
- } catch (UnsupportedEncodingException e) {
- throw new RuntimeException(e); // Shouldn't happen, UTF-8 is universal
- }
- }
-
- /**
- * @return SHA-256(UTF-8 encoded input)
- */
- public static byte[] sha256(String input) {
- MessageDigest sha256;
- try {
- sha256 = MessageDigest.getInstance("SHA-256");
- return sha256.digest(utf8StringToBytes(input));
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException("No security provider initialized yet?", e);
- }
- }
-
- /**
- * A key derivation function specific to this library, which accepts a {@code masterKey} and an
- * arbitrary {@code purpose} describing the intended application of the derived sub-key,
- * and produces a derived AES-256 key safe to use as if it were independent of any other
- * derived key which used a different {@code purpose}.
- *
- * @param masterKey any key suitable for use with HmacSHA256
- * @param purpose a UTF-8 encoded string describing the intended purpose of derived key
- * @return a derived SecretKey suitable for use with AES-256
- * @throws InvalidKeyException if the encoded form of {@code masterKey} cannot be accessed
- */
- static SecretKey deriveAes256KeyFor(SecretKey masterKey, String purpose)
- throws NoSuchAlgorithmException, InvalidKeyException {
- return new SecretKeySpec(hkdf(masterKey, SALT, utf8StringToBytes(purpose)), "AES");
- }
-
- /**
- * Implements HKDF (RFC 5869) with the SHA-256 hash and a 256-bit output key length.
- *
- * Please make sure to select a salt that is fixed and unique for your codebase, and use the
- * {@code info} parameter to specify any additional bits that should influence the derived key.
- *
- * @param inputKeyMaterial master key from which to derive sub-keys
- * @param salt a (public) randomly generated 256-bit input that can be re-used
- * @param info arbitrary information that is bound to the derived key (i.e., used in its creation)
- * @return raw derived key bytes = HKDF-SHA256(inputKeyMaterial, salt, info)
- * @throws InvalidKeyException if the encoded form of {@code inputKeyMaterial} cannot be accessed
- */
- public static byte[] hkdf(SecretKey inputKeyMaterial, byte[] salt, byte[] info)
- throws NoSuchAlgorithmException, InvalidKeyException {
- return hkdf(inputKeyMaterial, salt, info, /* length= */ 32);
- }
-
- /**
- * Implements HKDF (RFC 5869) with the SHA-256 hash.
- *
- * <p>Please make sure to select a salt that is fixed and unique for your codebase, and use the
- * {@code info} parameter to specify any additional bits that should influence the derived key.
- *
- * @param inputKeyMaterial master key from which to derive sub-keys
- * @param salt a (public) randomly generated 256-bit input that can be re-used
- * @param info arbitrary information that is bound to the derived key (i.e., used in its creation)
- * @param length length of returned key material
- * @return raw derived key bytes = HKDF-SHA256(inputKeyMaterial, salt, info)
- * @throws InvalidKeyException if the encoded form of {@code inputKeyMaterial} cannot be accessed
- */
- public static byte[] hkdf(SecretKey inputKeyMaterial, byte[] salt, byte[] info, int length)
- throws NoSuchAlgorithmException, InvalidKeyException {
- if ((inputKeyMaterial == null) || (salt == null) || (info == null)) {
- throw new NullPointerException();
- }
- if (length < 0) {
- throw new IllegalArgumentException("Length must be positive");
- }
- return hkdfSha256Expand(hkdfSha256Extract(inputKeyMaterial, salt), info, length);
- }
-
- /**
- * @return the concatenation of {@code a} and {@code b}, treating {@code null} as the empty array.
- */
- static byte[] concat(@Nullable byte[] a, @Nullable byte[] b) {
- if ((a == null) && (b == null)) {
- return new byte[] { };
- }
- if (a == null) {
- return b;
- }
- if (b == null) {
- return a;
- }
- byte[] result = new byte[a.length + b.length];
- System.arraycopy(a, 0, result, 0, a.length);
- System.arraycopy(b, 0, result, a.length, b.length);
- return result;
- }
-
- /**
- * Since {@code Arrays.copyOfRange(...)} is not available on older Android platforms,
- * a custom method for computing a subarray is provided here.
- *
- * @return the substring of {@code in} from {@code beginIndex} (inclusive)
- * up to {@code endIndex} (exclusive)
- */
- static byte[] subarray(byte[] in, int beginIndex, int endIndex) {
- if (in == null) {
- throw new NullPointerException();
- }
- int length = endIndex - beginIndex;
- if ((length < 0)
- || (beginIndex < 0)
- || (endIndex < 0)
- || (beginIndex >= in.length)
- || (endIndex > in.length)) {
- throw new IndexOutOfBoundsException();
- }
- byte[] result = new byte[length];
- if (length > 0) {
- System.arraycopy(in, beginIndex, result, 0, length);
- }
- return result;
- }
-
- /**
- * The HKDF (RFC 5869) extraction function, using the SHA-256 hash function. This function is used
- * to pre-process the inputKeyMaterial and mix it with the salt, producing output suitable for use
- * with HKDF expansion function (which produces the actual derived key).
- *
- * @see #hkdfSha256Expand(byte[], byte[], int)
- * @return HMAC-SHA256(salt, inputKeyMaterial) (salt is the "key" for the HMAC)
- * @throws InvalidKeyException if the encoded form of {@code inputKeyMaterial} cannot be accessed
- * @throws NoSuchAlgorithmException if the HmacSHA256 or AES algorithms are unavailable
- */
- private static byte[] hkdfSha256Extract(SecretKey inputKeyMaterial, byte[] salt)
- throws NoSuchAlgorithmException, InvalidKeyException {
- Mac macScheme = Mac.getInstance("HmacSHA256");
- try {
- macScheme.init(new SecretKeySpec(salt, "AES"));
- } catch (InvalidKeyException e) {
- throw new AssertionError(e); // This should never happen
- }
- // Note that the SecretKey encoding format is defined to be RAW, so the encoded form should be
- // consistent across implementations.
- byte[] encodedKeyMaterial = inputKeyMaterial.getEncoded();
- if (encodedKeyMaterial == null) {
- throw new InvalidKeyException("Cannot get encoded form of SecretKey");
- }
- return macScheme.doFinal(encodedKeyMaterial);
- }
-
- /**
- * HKDF (RFC 5869) expansion function, using the SHA-256 hash function.
- *
- * @param pseudoRandomKey should be generated by {@link #hkdfSha256Extract(SecretKey, byte[])}
- * @param info arbitrary information the derived key should be bound to
- * @param length length of the output key material in bytes
- * @return raw derived key bytes = HMAC-SHA256(pseudoRandomKey, info | 0x01)
- * @throws NoSuchAlgorithmException if the HmacSHA256 or AES algorithms are unavailable
- */
- private static byte[] hkdfSha256Expand(byte[] pseudoRandomKey, byte[] info, int length)
- throws NoSuchAlgorithmException {
- Mac macScheme = Mac.getInstance("HmacSHA256");
- try {
- macScheme.init(new SecretKeySpec(pseudoRandomKey, "AES"));
- } catch (InvalidKeyException e) {
- throw new AssertionError(e); // This should never happen
- }
-
- // Number of blocks N = ceil(hash length / output length).
- int blocks = length / 32;
- if (length % 32 > 0) {
- blocks += 1;
- }
-
- // The counter used to generate the blocks according to the RFC is only one byte long,
- // which puts a limit on the number of blocks possible.
- if (blocks > 0xFF) {
- throw new IllegalArgumentException("Maximum HKDF output length exceeded.");
- }
-
- byte[] outputBlock = new byte[32];
- byte[] counter = new byte[1];
- byte[] output = new byte[32 * blocks];
- for (int i = 0; i < blocks; ++i) {
- macScheme.reset();
- if (i > 0) {
- // Previous block
- macScheme.update(outputBlock);
- }
- // Arbitrary info
- macScheme.update(info);
- // Counter
- counter[0] = (byte) (i + 1);
- outputBlock = macScheme.doFinal(counter);
-
- System.arraycopy(outputBlock, 0, output, 32 * i, 32);
- }
-
- return subarray(output, 0, length);
- }
-
-}
diff --git a/src/main/java/com/google/security/cryptauth/lib/securemessage/PublicKeyProtoUtil.java b/src/main/java/com/google/security/cryptauth/lib/securemessage/PublicKeyProtoUtil.java
deleted file mode 100644
index 0c593fe..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securemessage/PublicKeyProtoUtil.java
+++ /dev/null
@@ -1,675 +0,0 @@
-// Copyright 2020 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.securemessage;
-
-import com.google.common.collect.Lists;
-import com.google.protobuf.ByteString;
-import com.google.security.annotations.SuppressInsecureCipherModeCheckerPendingReview;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.DhPublicKey;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.EcP256PublicKey;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.GenericPublicKey;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SimpleRsaPublicKey;
-import java.math.BigInteger;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.KeyFactory;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.NoSuchAlgorithmException;
-import java.security.PublicKey;
-import java.security.SecureRandom;
-import java.security.interfaces.ECPublicKey;
-import java.security.interfaces.RSAPublicKey;
-import java.security.spec.ECFieldFp;
-import java.security.spec.ECGenParameterSpec;
-import java.security.spec.ECParameterSpec;
-import java.security.spec.ECPoint;
-import java.security.spec.ECPublicKeySpec;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.RSAPublicKeySpec;
-import javax.crypto.interfaces.DHPrivateKey;
-import javax.crypto.interfaces.DHPublicKey;
-import javax.crypto.spec.DHParameterSpec;
-import javax.crypto.spec.DHPublicKeySpec;
-
-/**
- * Utility class containing static factory methods for a simple protobuf based representation of
- * EC public keys that is intended for use with the SecureMessage library.
- *
- * N.B.: Requires the availability of an EC security provider supporting the NIST P-256 curve.
- *
- */
-public class PublicKeyProtoUtil {
-
- private PublicKeyProtoUtil() {} // Do not instantiate
-
- /**
- * Caches state about whether the current platform supports Elliptic Curve algorithms.
- */
- private static final Boolean IS_LEGACY_CRYPTO_REQUIRED = determineIfLegacyCryptoRequired();
-
- private static final BigInteger ONE = new BigInteger("1");
- private static final BigInteger TWO = new BigInteger("2");
-
- /**
- * Name for Elliptic Curve cryptography algorithm suite, used by the security provider. If the
- * security provider does not implement the specified algorithm, runtime errors will ensue.
- */
- private static final String EC_ALG = "EC";
-
- /**
- * A common name for the NIST P-256 curve, used by most Java security providers.
- */
- private static final String EC_P256_COMMON_NAME = "secp256r1";
-
- /**
- * A name the NIST P-256 curve, used by the OpenSSL Java security provider (e.g,. on Android).
- */
- private static final String EC_P256_OPENSSL_NAME = "prime256v1";
-
- /**
- * The {@link ECParameterSpec} for the NIST P-256 Elliptic Curve.
- */
- private static final ECParameterSpec EC_P256_PARAMS = isLegacyCryptoRequired() ? null :
- ((ECPublicKey) generateEcP256KeyPair().getPublic()).getParams();
-
- /**
- * The prime {@code p} describing the field for the NIST P-256 curve.
- */
- private static final BigInteger EC_P256_P = isLegacyCryptoRequired() ? null :
- ((ECFieldFp) EC_P256_PARAMS.getCurve().getField()).getP();
-
- /**
- * The coefficient {@code a} for the NIST P-256 curve.
- */
- private static final BigInteger EC_P256_A = isLegacyCryptoRequired() ? null :
- EC_P256_PARAMS.getCurve().getA();
-
- /**
- * The coefficient {@code b} for the NIST P-256 curve.
- */
- private static final BigInteger EC_P256_B = isLegacyCryptoRequired() ? null :
- EC_P256_PARAMS.getCurve().getB();
-
- /**
- * Maximum number of bytes in a 2's complement encoding of a NIST P-256 elliptic curve point.
- */
- private static final int MAX_P256_ENCODING_BYTES = 33;
-
- /**
- * The JCA name for the RSA cryptography suite.
- */
- private static final String RSA_ALG = "RSA";
-
- private static final int RSA2048_MODULUS_BITS = 2048;
-
- /**
- * Maximum number of bytes in a 2's complement encoding of a 2048-bit RSA key.
- */
- private static final int MAX_RSA2048_ENCODING_BYTES = 257;
-
- /**
- * The JCA name for the Diffie-Hellman cryptography suite.
- */
- private static final String DH_ALG = "DH";
-
- /**
- * The prime from the 2048-bit MODP Group (group 14) described in RFC 3526, to be used for
- * Diffie-Hellman computations. Use only if Elliptic Curve ciphers are unavailable.
- */
- public static final BigInteger DH_P = new BigInteger(
- "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" +
- "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" +
- "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" +
- "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" +
- "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" +
- "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" +
- "83655D23DCA3AD961C62F356208552BB9ED529077096966D" +
- "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" +
- "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" +
- "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" +
- "15728E5A8AACAA68FFFFFFFFFFFFFFFF", 16);
-
- /**
- * The generator for the 2048-bit MODP Group (group 14) described in RFC 3526, to be used for
- * Diffie-Hellman computations. Use only if Elliptic Curve ciphers are unavailable.
- */
- public static final BigInteger DH_G = TWO;
-
- /**
- * The size of the Diffie-Hellman exponent to use, in bits.
- */
- public static final int DH_LEN = 512;
-
- /**
- * Maximum number of bytes in a 2's complement encoding of a
- * Diffie-Hellman key using {@link #DH_G}.
- */
- private static final int MAX_DH2048_ENCODING_BYTES = 257;
-
- /**
- * Version code for the Honeycomb release of Android, which is the first release supporting
- * Elliptic Curve.
- */
- public static final int ANDROID_HONEYCOMB_SDK_INT = 11;
-
- /**
- * Encodes any supported {@link PublicKey} type as a {@link GenericPublicKey} proto message.
- *
- * @see SecureMessageProto constants (defined in the .proto file) for supported types
- */
- public static GenericPublicKey encodePublicKey(PublicKey pk) {
- if (pk == null) {
- throw new NullPointerException();
- }
- if (pk instanceof ECPublicKey) {
- return GenericPublicKey.newBuilder()
- .setType(SecureMessageProto.PublicKeyType.EC_P256)
- .setEcP256PublicKey(encodeEcPublicKey(pk))
- .build();
- }
- if (pk instanceof RSAPublicKey) {
- return GenericPublicKey.newBuilder()
- .setType(SecureMessageProto.PublicKeyType.RSA2048)
- .setRsa2048PublicKey(encodeRsa2048PublicKey(pk))
- .build();
- }
- if (pk instanceof DHPublicKey) {
- return GenericPublicKey.newBuilder()
- .setType(SecureMessageProto.PublicKeyType.DH2048_MODP)
- .setDh2048PublicKey(encodeDh2048PublicKey(pk))
- .build();
- }
- throw new IllegalArgumentException("Unsupported PublicKey type");
- }
-
- /**
- * Encodes an {@link ECPublicKey} to an {@link GenericPublicKey} proto message. The returned key
- * has a null-byte padded to the front in order to match the C++ implementation.
- */
- public static GenericPublicKey encodePaddedEcPublicKey(PublicKey pk) {
- if (pk == null) {
- throw new NullPointerException();
- }
- if (!(pk instanceof ECPublicKey)) {
- throw new IllegalArgumentException("Expected ECPublicKey PublicKey type");
- }
-
- ECPublicKey epk = pkToECPublicKey(pk);
- ByteString nullByteString = ByteString.copyFrom(new byte[] {0});
- ByteString xByteString = extractX(epk);
- if (xByteString.size() < MAX_P256_ENCODING_BYTES) {
- xByteString = ByteString.copyFrom(Lists.newArrayList(nullByteString, xByteString));
- }
- ByteString yByteString = extractY(epk);
- if (yByteString.size() < MAX_P256_ENCODING_BYTES) {
- yByteString = ByteString.copyFrom(Lists.newArrayList(nullByteString, yByteString));
- }
- EcP256PublicKey newKey =
- EcP256PublicKey.newBuilder().setX(xByteString).setY(yByteString).build();
-
- return GenericPublicKey.newBuilder()
- .setType(SecureMessageProto.PublicKeyType.EC_P256)
- .setEcP256PublicKey(newKey)
- .build();
- }
-
- /**
- * Encodes an {@link ECPublicKey} to an {@link EcP256PublicKey} proto message.
- */
- public static EcP256PublicKey encodeEcPublicKey(PublicKey pk) {
- ECPublicKey epk = pkToECPublicKey(pk);
- return EcP256PublicKey.newBuilder()
- .setX(extractX(epk))
- .setY(extractY(epk))
- .build();
- }
-
- /**
- * Encodes a 2048-bit {@link RSAPublicKey} to an {@link SimpleRsaPublicKey} proto message.
- */
- public static SimpleRsaPublicKey encodeRsa2048PublicKey(PublicKey pk) {
- RSAPublicKey rpk = pkToRSAPublicKey(pk);
- return SimpleRsaPublicKey.newBuilder()
- .setN(ByteString.copyFrom(rpk.getModulus().toByteArray()))
- .setE(rpk.getPublicExponent().intValue())
- .build();
- }
-
- /**
- * Encodes a 2048-bit {@link DhPublicKey} using the {@link #DH_G} group to a
- * {@link DhPublicKey} proto message.
- */
- public static DhPublicKey encodeDh2048PublicKey(PublicKey pk) {
- DHPublicKey dhpk = pkToDHPublicKey(pk);
- return DhPublicKey.newBuilder()
- .setY(ByteString.copyFrom(dhpk.getY().toByteArray()))
- .build();
- }
-
- /**
- * Extracts a {@link PublicKey} from an {@link GenericPublicKey} proto message.
- *
- * @throws InvalidKeySpecException if the input is not a valid and/or supported public key type
- */
- public static PublicKey parsePublicKey(GenericPublicKey gpk) throws InvalidKeySpecException {
- if (!gpk.hasType()) {
- // "required" means nothing in micro proto land. We have to check this ourselves.
- throw new InvalidKeySpecException("GenericPublicKey.type is a required field");
- }
- switch (gpk.getType()) {
- case EC_P256:
- if (!gpk.hasEcP256PublicKey()) {
- break;
- }
- return parseEcPublicKey(gpk.getEcP256PublicKey());
- case RSA2048:
- if (!gpk.hasRsa2048PublicKey()) {
- break;
- }
- return parseRsa2048PublicKey(gpk.getRsa2048PublicKey());
- case DH2048_MODP:
- if (!gpk.hasDh2048PublicKey()) {
- break;
- }
- return parseDh2048PublicKey(gpk.getDh2048PublicKey());
- default:
- throw new InvalidKeySpecException("Unsupported GenericPublicKey type: " + gpk.getType());
- }
- throw new InvalidKeySpecException("key object is missing for key type: " + gpk.getType());
- }
-
- /**
- * Extracts a {@link ECPublicKey} from an {@link EcP256PublicKey} proto message.
- *
- * @throws InvalidKeySpecException if the input is not a valid NIST P-256 public key or if
- * this platform does not support Elliptic Curve keys
- */
- public static ECPublicKey parseEcPublicKey(EcP256PublicKey p256pk)
- throws InvalidKeySpecException {
- if (!p256pk.hasX() || !p256pk.hasY()) {
- throw new InvalidKeySpecException("Key is missing a required coordinate");
- }
- if (isLegacyCryptoRequired()) {
- throw new InvalidKeySpecException("Elliptic Curve keys not supported on this platform");
- }
- byte[] encodedX = p256pk.getX().toByteArray();
- byte[] encodedY = p256pk.getY().toByteArray();
- try {
- validateEcP256CoordinateEncoding(encodedX);
- validateEcP256CoordinateEncoding(encodedY);
- BigInteger wX = new BigInteger(encodedX);
- BigInteger wY = new BigInteger(encodedY);
- validateEcP256CurvePoint(wX, wY);
- return (ECPublicKey) KeyFactory.getInstance(EC_ALG).generatePublic(
- new ECPublicKeySpec(new ECPoint(wX, wY), EC_P256_PARAMS));
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * Extracts a {@link RSAPublicKey} from an {@link SimpleRsaPublicKey} proto message.
- *
- * @throws InvalidKeySpecException when the input RSA public key is invalid
- */
- public static RSAPublicKey parseRsa2048PublicKey(SimpleRsaPublicKey pk)
- throws InvalidKeySpecException {
- if (!pk.hasN()) {
- throw new InvalidKeySpecException("required field is missing");
- }
- byte[] encodedN = pk.getN().toByteArray();
- validateSimpleRsaEncoding(encodedN);
- BigInteger n = new BigInteger(encodedN);
- if (n.bitLength() != RSA2048_MODULUS_BITS) {
- throw new InvalidKeySpecException();
- }
- BigInteger e = BigInteger.valueOf(pk.getE());
- try {
- return (RSAPublicKey) KeyFactory.getInstance(RSA_ALG).generatePublic(
- new RSAPublicKeySpec(n, e));
- } catch (NoSuchAlgorithmException e1) {
- throw new AssertionError(e1); // Should never happen
- }
- }
-
- /**
- * Extracts a {@link DHPublicKey} from an {@link DhPublicKey} proto message.
- *
- * @throws InvalidKeySpecException when the input DH public key is invalid
- */
- @SuppressInsecureCipherModeCheckerPendingReview // b/32143855
- public static DHPublicKey parseDh2048PublicKey(DhPublicKey pk) throws InvalidKeySpecException {
- if (!pk.hasY()) {
- throw new InvalidKeySpecException("required field is missing");
- }
- byte[] encodedY = pk.getY().toByteArray();
- validateDhEncoding(encodedY);
- BigInteger y;
- try {
- y = new BigInteger(encodedY);
- } catch (NumberFormatException e) {
- throw new InvalidKeySpecException();
- }
- validateDhGroupElement(y);
- try {
- return (DHPublicKey) KeyFactory.getInstance(DH_ALG).generatePublic(
- new DHPublicKeySpec(y, DH_P, DH_G));
- } catch (NoSuchAlgorithmException e) {
- throw new AssertionError(e); // Should never happen
- }
- }
-
- /**
- * @return a freshly generated NIST P-256 Elliptic Curve key pair.
- */
- public static KeyPair generateEcP256KeyPair() {
- return getEcKeyGen().generateKeyPair();
- }
-
- /**
- * @return a freshly generated 2048-bit RSA key pair.
- */
- public static KeyPair generateRSA2048KeyPair() {
- return getRsaKeyGen().generateKeyPair();
- }
-
- /**
- * @return a freshly generated Diffie-Hellman key pair for the 2048-bit group
- * described by {@link #DH_G}
- */
- public static KeyPair generateDh2048KeyPair() {
- try {
- return getDhKeyGen().generateKeyPair();
- } catch (InvalidAlgorithmParameterException e) {
- // Construct an appropriate KeyPair manually, since this platform refuses to do it for us
- DHParameterSpec spec = new DHParameterSpec(DH_P, DH_G);
- BigInteger x = new BigInteger(DH_LEN, new SecureRandom());
- DHPrivateKey privateKey = new DHPrivateKeyShim(x, spec);
- DHPublicKey publicKey = new DHPublicKeyShim(DH_G.modPow(x, DH_P), spec);
- return new KeyPair(publicKey, privateKey);
- }
- }
-
- /**
- * A lightweight encoding for a {@link DHPrivateKey}. Strongly recommended over attempting to use
- * {@link DHPrivateKey#getEncoded()}, but not compatible with the standard encoding.
- *
- * @see #parseDh2048PrivateKey(byte[])
- */
- public static byte[] encodeDh2048PrivateKey(DHPrivateKey sk) {
- return sk.getX().toByteArray();
- }
-
- /**
- * Parses a {@link DHPrivateKey} encoded with {@link #encodeDh2048PrivateKey(DHPrivateKey)}.
- */
- public static DHPrivateKey parseDh2048PrivateKey(byte[] encodedX)
- throws InvalidKeySpecException {
- validateDhEncoding(encodedX); // Could be stricter for x, but should be fine to use this
- BigInteger x;
- try {
- x = new BigInteger(encodedX);
- } catch (NumberFormatException e) {
- throw new InvalidKeySpecException();
- }
- validateDhGroupElement(x); // Again, this validation should be good enough
- return new DHPrivateKeyShim(x, new DHParameterSpec(DH_P, DH_G));
- }
-
- /**
- * @throws InvalidKeySpecException if point ({@code x},{@code y}) isn't on the NIST P-256 curve
- */
- private static void validateEcP256CurvePoint(BigInteger x, BigInteger y)
- throws InvalidKeySpecException {
- if ((x.signum() == -1) || (y.signum() == -1)) {
- throw new InvalidKeySpecException("Point encoding must use only non-negative integers");
- }
-
- BigInteger p = EC_P256_P;
- if ((x.compareTo(p) >= 0) || (y.compareTo(p) >= 0)) {
- throw new InvalidKeySpecException("Point lies outside of the expected field");
- }
-
- // Points on the curve satisfy y^2 = x^3 + ax + b (mod p)
- BigInteger lhs = squareMod(y, p);
- BigInteger rhs = squareMod(x, p).add(EC_P256_A) // = (x^2 + a)
- .multiply(x).mod(p) // = x(x^2 + a) = x^3 + ax
- .add(EC_P256_B) // = x^3 + ax + b
- .mod(p);
- if (!lhs.equals(rhs)) {
- throw new InvalidKeySpecException("Point does not lie on the expected curve");
- }
- }
-
- /**
- * @return value of {@code x}^2 (mod {@code p})
- */
- private static BigInteger squareMod(BigInteger x, BigInteger p) {
- return x.multiply(x).mod(p);
- }
-
- /**
- * @throws InvalidKeySpecException if the coordinate is too large for a 256-bit curve
- */
- private static void validateEcP256CoordinateEncoding(byte[] p) throws InvalidKeySpecException {
- if ((p.length == 0)
- || (p.length > MAX_P256_ENCODING_BYTES)
- || (p.length == MAX_P256_ENCODING_BYTES && p[0] != 0)) {
- throw new InvalidKeySpecException(); // Intentionally vague for security reasons
- }
- }
-
- /**
- * @throws InvalidKeySpecException if the input is too large for a 2048-bit RSA modulus
- */
- private static void validateSimpleRsaEncoding(byte[] n) throws InvalidKeySpecException {
- if (n.length == 0 || n.length > MAX_RSA2048_ENCODING_BYTES) {
- throw new InvalidKeySpecException();
- }
- }
-
- /**
- * @throws InvalidKeySpecException if the public key is too large for a 2048-bit DH group
- */
- private static void validateDhEncoding(byte[] y) throws InvalidKeySpecException {
- if (y.length == 0 || y.length > MAX_DH2048_ENCODING_BYTES) {
- throw new InvalidKeySpecException();
- }
- }
-
- /**
- * @throws InvalidKeySpecException if {@code y} is not a valid Diffie-Hellman public key
- */
- private static void validateDhGroupElement(BigInteger y) throws InvalidKeySpecException {
- // Check that 1 < y < p -1
- if ((y.compareTo(ONE) < 1) || (y.compareTo(DH_P.subtract(ONE)) > -1)) {
- throw new InvalidKeySpecException();
- }
- }
-
- private static ByteString extractY(ECPublicKey epk) {
- return ByteString.copyFrom(epk.getW().getAffineY().toByteArray());
- }
-
- private static ByteString extractX(ECPublicKey epk) {
- return ByteString.copyFrom(epk.getW().getAffineX().toByteArray());
- }
-
- private static ECPublicKey pkToECPublicKey(PublicKey pk) {
- if (pk == null) {
- throw new NullPointerException();
- }
- if (!(pk instanceof ECPublicKey)) {
- throw new IllegalArgumentException("Not an EC Public Key");
- }
- return (ECPublicKey) pk;
- }
-
- private static RSAPublicKey pkToRSAPublicKey(PublicKey pk) {
- if (pk == null) {
- throw new NullPointerException();
- }
- if (!(pk instanceof RSAPublicKey)) {
- throw new IllegalArgumentException("Not an RSA Public Key");
- }
- return (RSAPublicKey) pk;
- }
-
- private static DHPublicKey pkToDHPublicKey(PublicKey pk) {
- if (pk == null) {
- throw new NullPointerException();
- }
- if (!(pk instanceof DHPublicKey)) {
- throw new IllegalArgumentException("Not a DH Public Key");
- }
- return (DHPublicKey) pk;
- }
-
- /**
- * @return an EC {@link KeyPairGenerator} object initialized for NIST P-256.
- */
- private static KeyPairGenerator getEcKeyGen() {
- KeyPairGenerator keygen;
- try {
- keygen = KeyPairGenerator.getInstance(EC_ALG);
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(e);
- }
- try {
- // Try using the OpenSSL provider first, since we prefer it over BouncyCastle
- keygen.initialize(new ECGenParameterSpec(EC_P256_OPENSSL_NAME));
- return keygen;
- } catch (InvalidAlgorithmParameterException e) {
- // Try another name for NIST P-256
- }
- try {
- keygen.initialize(new ECGenParameterSpec(EC_P256_COMMON_NAME));
- return keygen;
- } catch (InvalidAlgorithmParameterException e) {
- throw new RuntimeException("Unable to find the NIST P-256 curve");
- }
- }
-
- /**
- * @return an RSA {@link KeyPairGenerator} object initialized for 2048-bit keys.
- */
- private static KeyPairGenerator getRsaKeyGen() {
- try {
- KeyPairGenerator keygen = KeyPairGenerator.getInstance(RSA_ALG);
- keygen.initialize(RSA2048_MODULUS_BITS);
- return keygen;
- } catch (NoSuchAlgorithmException e) {
- throw new AssertionError(e); // This should never happen
- }
- }
-
- /**
- * @return a DH {@link KeyPairGenerator} object initialized for the group described by {@link
- * #DH_G}.
- * @throws InvalidAlgorithmParameterException on some platforms that don't support large DH groups
- */
- @SuppressInsecureCipherModeCheckerPendingReview // b/32143855
- private static KeyPairGenerator getDhKeyGen() throws InvalidAlgorithmParameterException {
- try {
- KeyPairGenerator keygen = KeyPairGenerator.getInstance(DH_ALG);
- keygen.initialize(new DHParameterSpec(DH_P, DH_G, DH_LEN));
- return keygen;
- } catch (NoSuchAlgorithmException e) {
- throw new AssertionError(e); // This should never happen
- }
- }
-
- /**
- * A lightweight shim class to enable the creation of {@link DHPublicKey} and {@link DHPrivateKey}
- * objects that accept arbitrary {@link DHParameterSpec}s -- unfortunately, many platforms do
- * not support using reasonably sized Diffie-Hellman groups any other way. For instance, see
- * <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6521495">Java bug 6521495</a>.
- */
- public abstract static class DHKeyShim {
-
- private BigInteger eitherXorY;
- private DHParameterSpec params;
-
- public DHKeyShim(BigInteger eitherXorY, DHParameterSpec params) {
- this.eitherXorY = eitherXorY;
- this.params = params;
- }
-
- public DHParameterSpec getParams() {
- return params;
- }
-
- public String getAlgorithm() {
- return "DH";
- }
-
- public String getFormat() {
- return null;
- }
-
- public byte[] getEncoded() {
- return null;
- }
-
- public BigInteger getX() {
- return eitherXorY;
- }
-
- public BigInteger getY() {
- return eitherXorY;
- }
- }
-
- /**
- * A simple {@link DHPublicKey} implementation.
- *
- * @see DHKeyShim
- */
- public static class DHPublicKeyShim extends DHKeyShim implements DHPublicKey {
- public DHPublicKeyShim(BigInteger y, DHParameterSpec params) {
- super(y, params);
- }
- }
-
- /**
- * A simple {@link DHPrivateKey} implementation.
- *
- * @see DHKeyShim
- */
- public static class DHPrivateKeyShim extends DHKeyShim implements DHPrivateKey {
- public DHPrivateKeyShim(BigInteger x, DHParameterSpec params) {
- super(x, params);
- }
- }
-
- /**
- * @return true if this platform does not support Elliptic Curve algorithms
- */
- public static boolean isLegacyCryptoRequired() {
- return IS_LEGACY_CRYPTO_REQUIRED;
- }
-
- /**
- * @return true if using the Elliptic Curve key generator fails on this platform
- */
- private static boolean determineIfLegacyCryptoRequired() {
- try {
- getEcKeyGen();
- } catch (Exception e) {
- return true;
- }
- return false;
- }
-}
diff --git a/src/main/java/com/google/security/cryptauth/lib/securemessage/SecureMessageBuilder.java b/src/main/java/com/google/security/cryptauth/lib/securemessage/SecureMessageBuilder.java
deleted file mode 100644
index f1a9464..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securemessage/SecureMessageBuilder.java
+++ /dev/null
@@ -1,277 +0,0 @@
-// Copyright 2020 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.securemessage;
-
-import com.google.protobuf.ByteString;
-import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType;
-import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.Header;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBodyInternal;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage;
-import java.security.InvalidKeyException;
-import java.security.Key;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Arrays;
-import javax.annotation.Nullable;
-
-/**
- * Builder for {@link SecureMessage} protos. Can be used to create either signed messages,
- * or "signcrypted" (encrypted then signed) messages that include a tight binding between the
- * ciphertext portion and a verification key identity.
- *
- * @see SecureMessageParser
- */
-public class SecureMessageBuilder {
- private ByteString publicMetadata;
- private ByteString verificationKeyId;
- private ByteString decryptionKeyId;
- /**
- * This data is never sent inside the protobufs, so the builder just saves it as a byte[].
- */
- private byte[] associatedData;
-
- private SecureRandom rng;
-
- public SecureMessageBuilder() {
- reset();
- this.rng = new SecureRandom();
- }
-
- /**
- * Resets this {@link SecureMessageBuilder} instance to a blank configuration (and returns it).
- */
- public SecureMessageBuilder reset() {
- this.publicMetadata = null;
- this.verificationKeyId = null;
- this.decryptionKeyId = null;
- this.associatedData = null;
- return this;
- }
-
- /**
- * Optional metadata to be sent along with the header information in this {@link SecureMessage}.
- * <p>
- * Note that this value will be sent <em>UNENCRYPTED</em> in all cases.
- * <p>
- * Can be used with either cleartext or signcrypted messages, but is intended primarily for use
- * with signcrypted messages.
- */
- public SecureMessageBuilder setPublicMetadata(byte[] publicMetadata) {
- this.publicMetadata = ByteString.copyFrom(publicMetadata);
- return this;
- }
-
- /**
- * The recipient of the {@link SecureMessage} should be able to uniquely determine the correct
- * verification key, given only this value.
- * <p>
- * Can be used with either cleartext or signcrypted messages. Setting this is mandatory for
- * signcrypted messages using a public key {@link SigType}, in order to bind the encrypted
- * body to a specific verification key.
- * <p>
- * Note that this value is sent <em>UNENCRYPTED</em> in all cases.
- */
- public SecureMessageBuilder setVerificationKeyId(byte[] verificationKeyId) {
- this.verificationKeyId = ByteString.copyFrom(verificationKeyId);
- return this;
- }
-
- /**
- * To be used only with {@link #buildSignCryptedMessage(Key, SigType, Key, EncType, byte[])},
- * this value is sent <em>UNENCRYPTED</em> as part of the header. It should be used by the
- * recipient of the {@link SecureMessage} to identify an appropriate key to use for decrypting
- * the message body.
- */
- public SecureMessageBuilder setDecryptionKeyId(byte[] decryptionKeyId) {
- this.decryptionKeyId = ByteString.copyFrom(decryptionKeyId);
- return this;
- }
-
- /**
- * Additional data is "associated" with this {@link SecureMessage}, but will not be sent as
- * part of it. The recipient of the {@link SecureMessage} will need to provide the same data in
- * order to verify the message body. Setting this to {@code null} is equivalent to using an
- * empty array (unlike the behavior of {@code VerificationKeyId} and {@code DecryptionKeyId}).
- * <p>
- * Note that the <em>size</em> (length in bytes) of the associated data will be sent in the
- * <em>UNENCRYPTED</em> header information, even if you are using encryption.
- * <p>
- * If you will be using {@link #buildSignedCleartextMessage(Key, SigType, byte[])}, then anyone
- * observing the {@link SecureMessage} may be able to infer this associated data via an
- * "offline dictionary attack". That is, when no encryption is used, you will not be hiding this
- * data simply because it is not being sent over the wire.
- */
- public SecureMessageBuilder setAssociatedData(@Nullable byte[] associatedData) {
- this.associatedData = associatedData;
- return this;
- }
-
- // @VisibleForTesting
- SecureMessageBuilder setRng(SecureRandom rng) {
- this.rng = rng;
- return this;
- }
-
- /**
- * Generates a signed {@link SecureMessage} with the payload {@code body} left
- * <em>UNENCRYPTED</em>.
- *
- * <p>Note that if you have used {@link #setAssociatedData(byte[])}, the associated data will
- * be subject to offline dictionary attacks if you use a public key {@link SigType}.
- *
- * <p>Doesn't currently support symmetric keys stored in a TPM (since we access the raw key).
- *
- * @see SecureMessageParser#parseSignedCleartextMessage(SecureMessage, Key, SigType)
- */
- public SecureMessage buildSignedCleartextMessage(Key signingKey, SigType sigType, byte[] body)
- throws NoSuchAlgorithmException, InvalidKeyException {
- if ((signingKey == null) || (sigType == null) || (body == null)) {
- throw new NullPointerException();
- }
- if (decryptionKeyId != null) {
- throw new IllegalStateException("Cannot set decryptionKeyId for a cleartext message");
- }
-
- byte[] headerAndBody = serializeHeaderAndBody(
- buildHeader(sigType, EncType.NONE, null).toByteArray(), body);
- return createSignedResult(signingKey, sigType, headerAndBody, associatedData);
- }
-
- /**
- * Generates a signed and encrypted {@link SecureMessage}. If the signature type requires a public
- * key, such as with ECDSA_P256_SHA256, then the caller <em>must</em> set a verification id using
- * the {@link #setVerificationKeyId(byte[])} method. The verification key id will be bound to the
- * encrypted {@code body}, preventing attacks that involve stripping the signature and then
- * re-signing the encrypted {@code body} as if it was originally sent by the attacker.
- *
- * <p>
- * It is safe to re-use one {@link javax.crypto.SecretKey} as both {@code signingKey} and
- * {@code encryptionKey}, even if that key is also used for
- * {@link #buildSignedCleartextMessage(Key, SigType, byte[])}. In fact, the resulting output
- * encoding will be more compact when the same symmetric key is used for both.
- *
- * <p>
- * Note that PublicMetadata and other header fields are left <em>UNENCRYPTED</em>.
- *
- * <p>
- * Doesn't currently support symmetric keys stored in a TPM (since we access the raw key).
- *
- * @param encType <em>must not</em> be set to {@link EncType#NONE}
- * @see SecureMessageParser#parseSignCryptedMessage(SecureMessage, Key, SigType, Key, EncType)
- */
- public SecureMessage buildSignCryptedMessage(
- Key signingKey, SigType sigType, Key encryptionKey, EncType encType, byte[] body)
- throws NoSuchAlgorithmException, InvalidKeyException {
- if ((signingKey == null)
- || (sigType == null)
- || (encryptionKey == null)
- || (encType == null)
- || (body == null)) {
- throw new NullPointerException();
- }
- if (encType == EncType.NONE) {
- throw new IllegalArgumentException(encType + " not supported for encrypted messages");
- }
- if (sigType.isPublicKeyScheme() && (verificationKeyId == null)) {
- throw new IllegalStateException(
- "Must set a verificationKeyId when using public key signature with encryption");
- }
-
- byte[] iv = CryptoOps.generateIv(encType, rng);
- byte[] header = buildHeader(sigType, encType, iv).toByteArray();
-
- // We may or may not need an extra tag in front of the plaintext body
- byte[] taggedBody;
- // We will only sign the associated data when we don't tag the plaintext body
- byte[] associatedDataToBeSigned;
- if (taggedPlaintextRequired(signingKey, sigType, encryptionKey)) {
- // Place a "tag" in front of the the plaintext message containing a digest of the header
- taggedBody = CryptoOps.concat(
- // Digest the header + any associated data, yielding a tag to be encrypted with the body.
- CryptoOps.digest(CryptoOps.concat(header, associatedData)),
- body);
- associatedDataToBeSigned = null; // We already handled any associatedData via the tag
- } else {
- taggedBody = body;
- associatedDataToBeSigned = associatedData;
- }
-
- // Compute the encrypted body, which binds the tag to the message inside the ciphertext
- byte[] encryptedBody = CryptoOps.encrypt(encryptionKey, encType, rng, iv, taggedBody);
-
- byte[] headerAndBody = serializeHeaderAndBody(header, encryptedBody);
- return createSignedResult(signingKey, sigType, headerAndBody, associatedDataToBeSigned);
- }
-
- /**
- * Indicates whether a "tag" is needed next to the plaintext body inside the ciphertext, to
- * prevent the same ciphertext from being reused with someone else's signature on it.
- */
- static boolean taggedPlaintextRequired(Key signingKey, SigType sigType, Key encryptionKey) {
- // We need a tag if different keys are being used to "sign" vs. encrypt
- return sigType.isPublicKeyScheme()
- || !Arrays.equals(signingKey.getEncoded(), encryptionKey.getEncoded());
- }
-
- /**
- * @param iv IV or {@code null} if IV to be left unset in the Header
- */
- private Header buildHeader(SigType sigType, EncType encType, byte[] iv) {
- Header.Builder result = Header.newBuilder()
- .setSignatureScheme(sigType.getSigScheme())
- .setEncryptionScheme(encType.getEncScheme());
- if (verificationKeyId != null) {
- result.setVerificationKeyId(verificationKeyId);
- }
- if (decryptionKeyId != null) {
- result.setDecryptionKeyId(decryptionKeyId);
- }
- if (publicMetadata != null) {
- result.setPublicMetadata(publicMetadata);
- }
- if (associatedData != null) {
- result.setAssociatedDataLength(associatedData.length);
- }
- if (iv != null) {
- result.setIv(ByteString.copyFrom(iv));
- }
- return result.build();
- }
-
- /**
- * @param header a serialized representation of a {@link Header}
- * @param body arbitrary payload data
- * @return a serialized representation of a {@link SecureMessageProto.HeaderAndBody}
- */
- private byte[] serializeHeaderAndBody(byte[] header, byte[] body) {
- return HeaderAndBodyInternal.newBuilder()
- .setHeader(ByteString.copyFrom(header))
- .setBody(ByteString.copyFrom(body))
- .build()
- .toByteArray();
- }
-
- private SecureMessage createSignedResult(
- Key signingKey, SigType sigType, byte[] headerAndBody, @Nullable byte[] associatedData)
- throws NoSuchAlgorithmException, InvalidKeyException {
- byte[] sig =
- CryptoOps.sign(sigType, signingKey, rng, CryptoOps.concat(headerAndBody, associatedData));
- return SecureMessage.newBuilder()
- .setHeaderAndBody(ByteString.copyFrom(headerAndBody))
- .setSignature(ByteString.copyFrom(sig))
- .build();
- }
-}
diff --git a/src/main/java/com/google/security/cryptauth/lib/securemessage/SecureMessageParser.java b/src/main/java/com/google/security/cryptauth/lib/securemessage/SecureMessageParser.java
deleted file mode 100644
index d634d40..0000000
--- a/src/main/java/com/google/security/cryptauth/lib/securemessage/SecureMessageParser.java
+++ /dev/null
@@ -1,270 +0,0 @@
-// Copyright 2020 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.securemessage;
-
-import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
-import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType;
-import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.Header;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBody;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBodyInternal;
-import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.Key;
-import java.security.NoSuchAlgorithmException;
-import java.security.SignatureException;
-import javax.annotation.Nullable;
-import javax.crypto.BadPaddingException;
-import javax.crypto.IllegalBlockSizeException;
-
-/**
- * Utility class to parse and verify {@link SecureMessage} protos. Verifies the signature on the
- * message, and decrypts "signcrypted" messages (while simultaneously verifying the signature).
- *
- * @see SecureMessageBuilder
- */
-public class SecureMessageParser {
-
- private SecureMessageParser() {} // Do not instantiate
-
- /**
- * Extracts the {@link Header} component from a {@link SecureMessage} but <em>DOES NOT VERIFY</em>
- * the signature when doing so. Callers should not trust the resulting output until after a
- * subsequent {@code parse*()} call has succeeded.
- *
- * <p>The intention is to allow the caller to determine the type of the protocol message and which
- * keys are in use, prior to attempting to verify (and possibly decrypt) the payload body.
- */
- public static Header getUnverifiedHeader(SecureMessage secmsg)
- throws InvalidProtocolBufferException {
- if (!secmsg.hasHeaderAndBody()) {
- throw new InvalidProtocolBufferException("Missing header and body");
- }
- if (!HeaderAndBody.parseFrom(secmsg.getHeaderAndBody()).hasHeader()) {
- throw new InvalidProtocolBufferException("Missing header");
- }
- Header result = HeaderAndBody.parseFrom(secmsg.getHeaderAndBody()).getHeader();
- // Check that at least a signature scheme was set
- if (!result.hasSignatureScheme()) {
- throw new InvalidProtocolBufferException("Missing header field(s)");
- }
- // Check signature scheme is legal
- try {
- SigType.valueOf(result.getSignatureScheme());
- } catch (IllegalArgumentException e) {
- throw new InvalidProtocolBufferException("Corrupt/unsupported SignatureScheme");
- }
- // Check encryption scheme is legal
- if (result.hasEncryptionScheme()) {
- try {
- EncType.valueOf(result.getEncryptionScheme());
- } catch (IllegalArgumentException e) {
- throw new InvalidProtocolBufferException("Corrupt/unsupported EncryptionScheme");
- }
- }
- return result;
- }
-
- /**
- * Parses a {@link SecureMessage} containing a cleartext payload body, and verifies the signature.
- *
- * @return the parsed {@link HeaderAndBody} pair (which is fully verified)
- * @throws SignatureException if signature verification fails
- * @see SecureMessageBuilder#buildSignedCleartextMessage(Key, SigType, byte[])
- */
- public static HeaderAndBody parseSignedCleartextMessage(
- SecureMessage secmsg, Key verificationKey, SigType sigType)
- throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
- return parseSignedCleartextMessage(secmsg, verificationKey, sigType, null);
- }
-
- /**
- * Parses a {@link SecureMessage} containing a cleartext payload body, and verifies the signature.
- *
- * @param associatedData optional associated data bound to the signature (but not in the message)
- * @return the parsed {@link HeaderAndBody} pair (which is fully verified)
- * @throws SignatureException if signature verification fails
- * @see SecureMessageBuilder#buildSignedCleartextMessage(Key, SigType, byte[])
- */
- public static HeaderAndBody parseSignedCleartextMessage(
- SecureMessage secmsg, Key verificationKey, SigType sigType, @Nullable byte[] associatedData)
- throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
- if ((secmsg == null) || (verificationKey == null) || (sigType == null)) {
- throw new NullPointerException();
- }
- return verifyHeaderAndBody(
- secmsg,
- verificationKey,
- sigType,
- EncType.NONE,
- associatedData,
- false /* suppressAssociatedData is always false for signed cleartext */);
- }
-
- /**
- * Parses a {@link SecureMessage} containing an encrypted payload body, extracting a decryption of
- * the payload body and verifying the signature.
- *
- * @return the parsed {@link HeaderAndBody} pair (which is fully verified and decrypted)
- * @throws SignatureException if signature verification fails
- * @see SecureMessageBuilder#buildSignCryptedMessage(Key, SigType, Key, EncType, byte[])
- */
- public static HeaderAndBody parseSignCryptedMessage(
- SecureMessage secmsg,
- Key verificationKey,
- SigType sigType,
- Key decryptionKey,
- EncType encType)
- throws InvalidKeyException, NoSuchAlgorithmException, SignatureException {
- return parseSignCryptedMessage(secmsg, verificationKey, sigType, decryptionKey, encType, null);
- }
-
- /**
- * Parses a {@link SecureMessage} containing an encrypted payload body, extracting a decryption of
- * the payload body and verifying the signature.
- *
- * @param associatedData optional associated data bound to the signature (but not in the message)
- * @return the parsed {@link HeaderAndBody} pair (which is fully verified and decrypted)
- * @throws SignatureException if signature verification fails
- * @see SecureMessageBuilder#buildSignCryptedMessage(Key, SigType, Key, EncType, byte[])
- */
- public static HeaderAndBody parseSignCryptedMessage(
- SecureMessage secmsg,
- Key verificationKey,
- SigType sigType,
- Key decryptionKey,
- EncType encType,
- @Nullable byte[] associatedData)
- throws InvalidKeyException, NoSuchAlgorithmException, SignatureException {
- if ((secmsg == null)
- || (verificationKey == null)
- || (sigType == null)
- || (decryptionKey == null)
- || (encType == null)) {
- throw new NullPointerException();
- }
- if (encType == EncType.NONE) {
- throw new SignatureException("Not a signcrypted message");
- }
-
- boolean tagRequired =
- SecureMessageBuilder.taggedPlaintextRequired(verificationKey, sigType, decryptionKey);
- HeaderAndBody headerAndEncryptedBody;
- headerAndEncryptedBody = verifyHeaderAndBody(
- secmsg,
- verificationKey,
- sigType,
- encType,
- associatedData,
- tagRequired /* suppressAssociatedData if it is handled by the tag instead */);
-
- byte[] rawDecryptedBody;
- Header header = headerAndEncryptedBody.getHeader();
- if (!header.hasIv()) {
- throw new SignatureException();
- }
- try {
- rawDecryptedBody = CryptoOps.decrypt(
- decryptionKey, encType, header.getIv().toByteArray(),
- headerAndEncryptedBody.getBody().toByteArray());
- } catch (InvalidAlgorithmParameterException e) {
- throw new SignatureException();
- } catch (IllegalBlockSizeException e) {
- throw new SignatureException();
- } catch (BadPaddingException e) {
- throw new SignatureException();
- }
-
- if (!tagRequired) {
- // No tag expected, so we're all done
- return HeaderAndBody.newBuilder(headerAndEncryptedBody)
- .setBody(ByteString.copyFrom(rawDecryptedBody))
- .build();
- }
-
- // Verify the tag that binds the ciphertext to the header, and remove it
- byte[] headerBytes;
- try {
- headerBytes =
- HeaderAndBodyInternal.parseFrom(secmsg.getHeaderAndBody()).getHeader().toByteArray();
- } catch (InvalidProtocolBufferException e) {
- // This shouldn't happen, but throw it up just in case
- throw new SignatureException(e);
- }
- boolean verifiedBinding = false;
- byte[] expectedTag = CryptoOps.digest(CryptoOps.concat(headerBytes, associatedData));
- if (rawDecryptedBody.length >= CryptoOps.DIGEST_LENGTH) {
- byte[] actualTag = CryptoOps.subarray(rawDecryptedBody, 0, CryptoOps.DIGEST_LENGTH);
- if (CryptoOps.constantTimeArrayEquals(actualTag, expectedTag)) {
- verifiedBinding = true;
- }
- }
- if (!verifiedBinding) {
- throw new SignatureException();
- }
-
- int bodyLen = rawDecryptedBody.length - CryptoOps.DIGEST_LENGTH;
- return HeaderAndBody.newBuilder(headerAndEncryptedBody)
- // Remove the tag and set the plaintext body
- .setBody(ByteString.copyFrom(rawDecryptedBody, CryptoOps.DIGEST_LENGTH, bodyLen))
- .build();
- }
-
- private static HeaderAndBody verifyHeaderAndBody(
- SecureMessage secmsg,
- Key verificationKey,
- SigType sigType,
- EncType encType,
- @Nullable byte[] associatedData,
- boolean suppressAssociatedData /* in case it is in the tag instead */)
- throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
- if (!secmsg.hasHeaderAndBody() || !secmsg.hasSignature()) {
- throw new SignatureException("Signature failed verification");
- }
- byte[] signature = secmsg.getSignature().toByteArray();
- byte[] data = secmsg.getHeaderAndBody().toByteArray();
- byte[] signedData = suppressAssociatedData ? data : CryptoOps.concat(data, associatedData);
-
- // Try not to leak the specific reason for verification failures, due to security concerns.
- boolean verified = CryptoOps.verify(verificationKey, sigType, signature, signedData);
- HeaderAndBody result = null;
- try {
- result = HeaderAndBody.parseFrom(secmsg.getHeaderAndBody());
- // Even if declared required, micro proto doesn't throw an exception if fields are not present
- if (!result.hasHeader() || !result.hasBody()) {
- throw new SignatureException("Signature failed verification");
- }
- verified &= (result.getHeader().getSignatureScheme() == sigType.getSigScheme());
- verified &= (result.getHeader().getEncryptionScheme() == encType.getEncScheme());
- // Check that either a decryption operation is expected, or no DecryptionKeyId is set.
- verified &= (encType != EncType.NONE) || !result.getHeader().hasDecryptionKeyId();
- // If encryption was used, check that either we are not using a public key signature or a
- // VerificationKeyId was set (as is required for public key based signature + encryption).
- verified &= (encType == EncType.NONE) || !sigType.isPublicKeyScheme() ||
- result.getHeader().hasVerificationKeyId();
- int associatedDataLength = associatedData == null ? 0 : associatedData.length;
- verified &= (result.getHeader().getAssociatedDataLength() == associatedDataLength);
- } catch (InvalidProtocolBufferException e) {
- verified = false;
- }
-
- if (verified) {
- return result;
- }
- throw new SignatureException("Signature failed verification");
- }
-}