1 /* Copyright 2018 Google LLC 2 * 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * https://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 package com.google.security.cryptauth.lib.securegcm; 16 17 import com.google.protobuf.InvalidProtocolBufferException; 18 import com.google.security.annotations.SuppressInsecureCipherModeCheckerPendingReview; 19 import com.google.security.cryptauth.lib.securegcm.SecureGcmProto.GcmDeviceInfo; 20 import com.google.security.cryptauth.lib.securegcm.SecureGcmProto.GcmMetadata; 21 import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.PayloadType; 22 import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType; 23 import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType; 24 import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil; 25 import com.google.security.cryptauth.lib.securemessage.SecureMessageBuilder; 26 import com.google.security.cryptauth.lib.securemessage.SecureMessageParser; 27 import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBody; 28 import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage; 29 import java.security.InvalidKeyException; 30 import java.security.KeyPair; 31 import java.security.MessageDigest; 32 import java.security.NoSuchAlgorithmException; 33 import java.security.PrivateKey; 34 import java.security.PublicKey; 35 import java.security.SignatureException; 36 import java.security.spec.InvalidKeySpecException; 37 import java.util.Arrays; 38 import javax.crypto.KeyAgreement; 39 import javax.crypto.SecretKey; 40 41 /** 42 * Utility class for implementing Secure GCM enrollment flows. 43 */ 44 public class EnrollmentCryptoOps { 45 EnrollmentCryptoOps()46 private EnrollmentCryptoOps() { } // Do not instantiate 47 48 /** 49 * Type of symmetric key signature to use for the signcrypted "outer layer" message. 50 */ 51 private static final SigType OUTER_SIG_TYPE = SigType.HMAC_SHA256; 52 53 /** 54 * Type of symmetric key encryption to use for the signcrypted "outer layer" message. 55 */ 56 private static final EncType OUTER_ENC_TYPE = EncType.AES_256_CBC; 57 58 /** 59 * Type of public key signature to use for the (cleartext) "inner layer" message. 60 */ 61 private static final SigType INNER_SIG_TYPE = SigType.ECDSA_P256_SHA256; 62 63 /** 64 * Type of public key signature to use for the (cleartext) "inner layer" message on platforms that 65 * don't support Elliptic Curve operations (such as old Android versions). 66 */ 67 private static final SigType LEGACY_INNER_SIG_TYPE = SigType.RSA2048_SHA256; 68 69 /** 70 * Which {@link KeyAgreement} algorithm to use. 71 */ 72 private static final String KA_ALG = "ECDH"; 73 74 /** 75 * Which {@link KeyAgreement} algorithm to use on platforms that don't support Elliptic Curve. 76 */ 77 private static final String LEGACY_KA_ALG = "DH"; 78 79 /** 80 * Used by both the client and server to perform a key exchange. 81 * 82 * @return a {@link SecretKey} derived from the key exchange 83 * @throws InvalidKeyException if either of the input keys is of the wrong type 84 */ 85 @SuppressInsecureCipherModeCheckerPendingReview // b/32143855 doKeyAgreement(PrivateKey myKey, PublicKey peerKey)86 public static SecretKey doKeyAgreement(PrivateKey myKey, PublicKey peerKey) 87 throws InvalidKeyException { 88 String alg = KA_ALG; 89 if (KeyEncoding.isLegacyPrivateKey(myKey)) { 90 alg = LEGACY_KA_ALG; 91 } 92 KeyAgreement agreement; 93 try { 94 agreement = KeyAgreement.getInstance(alg); 95 } catch (NoSuchAlgorithmException e) { 96 throw new RuntimeException(e); 97 } 98 99 agreement.init(myKey); 100 agreement.doPhase(peerKey, true); 101 byte[] agreedKey = agreement.generateSecret(); 102 103 // Derive a 256-bit AES key by using sha256 on the Diffie-Hellman output 104 return KeyEncoding.parseMasterKey(sha256(agreedKey)); 105 } 106 generateEnrollmentKeyAgreementKeyPair(boolean isLegacy)107 public static KeyPair generateEnrollmentKeyAgreementKeyPair(boolean isLegacy) { 108 if (isLegacy) { 109 return PublicKeyProtoUtil.generateDh2048KeyPair(); 110 } 111 return PublicKeyProtoUtil.generateEcP256KeyPair(); 112 } 113 114 /** 115 * @return SHA-256 hash of {@code masterKey} 116 */ getMasterKeyHash(SecretKey masterKey)117 public static byte[] getMasterKeyHash(SecretKey masterKey) { 118 return sha256(masterKey.getEncoded()); 119 } 120 121 /** 122 * Used by the client to signcrypt an enrollment request before sending it to the server. 123 * 124 * <p>Note: You <em>MUST</em> correctly set the value of the {@code device_master_key_hash} on 125 * {@code enrollmentInfo} from {@link #getMasterKeyHash(SecretKey)} before calling this method. 126 * 127 * @param enrollmentInfo the enrollment request to send to the server. You must correctly set 128 * the {@code device_master_key_hash} field. 129 * @param masterKey the shared key derived from the key agreement 130 * @param signingKey the signing key corresponding to the user's {@link PublicKey} being enrolled 131 * @return the encrypted enrollment message 132 * @throws IllegalArgumentException if {@code enrollmentInfo} doesn't have a valid 133 * {@code device_master_key_hash} 134 * @throws InvalidKeyException if {@code masterKey} or {@code signingKey} is the wrong type 135 */ encryptEnrollmentMessage( GcmDeviceInfo enrollmentInfo, SecretKey masterKey, PrivateKey signingKey)136 public static byte[] encryptEnrollmentMessage( 137 GcmDeviceInfo enrollmentInfo, SecretKey masterKey, PrivateKey signingKey) 138 throws InvalidKeyException, NoSuchAlgorithmException { 139 if ((enrollmentInfo == null) || (masterKey == null) || (signingKey == null)) { 140 throw new NullPointerException(); 141 } 142 143 if (!Arrays.equals(enrollmentInfo.getDeviceMasterKeyHash().toByteArray(), 144 getMasterKeyHash(masterKey))) { 145 throw new IllegalArgumentException("DeviceMasterKeyHash not set correctly"); 146 } 147 148 // First create the inner message, which is basically a self-signed certificate 149 SigType sigType = 150 KeyEncoding.isLegacyPrivateKey(signingKey) ? LEGACY_INNER_SIG_TYPE : INNER_SIG_TYPE; 151 SecureMessage innerMsg = new SecureMessageBuilder() 152 .setVerificationKeyId(enrollmentInfo.getUserPublicKey().toByteArray()) 153 .buildSignedCleartextMessage(signingKey, sigType, enrollmentInfo.toByteArray()); 154 155 // Next create the outer message, which uses the newly exchanged master key to signcrypt 156 SecureMessage outerMsg = new SecureMessageBuilder() 157 .setVerificationKeyId(new byte[] {}) // Empty 158 .setPublicMetadata(GcmMetadata.newBuilder() 159 .setType(PayloadType.ENROLLMENT.getType()) 160 .setVersion(SecureGcmConstants.SECURE_GCM_VERSION) 161 .build() 162 .toByteArray()) 163 .buildSignCryptedMessage( 164 masterKey, OUTER_SIG_TYPE, masterKey, OUTER_ENC_TYPE, innerMsg.toByteArray()); 165 return outerMsg.toByteArray(); 166 } 167 168 /** 169 * Used by the server to decrypt the client's enrollment request. 170 * @param enrollmentMessage generated by the client's call to 171 * {@link #encryptEnrollmentMessage(GcmDeviceInfo, SecretKey, PrivateKey)} 172 * @param masterKey the shared key derived from the key agreement 173 * @return the client's enrollment request data 174 * @throws SignatureException if {@code enrollmentMessage} is malformed or has been tampered with 175 * @throws InvalidKeyException if {@code masterKey} is the wrong type 176 */ decryptEnrollmentMessage( byte[] enrollmentMessage, SecretKey masterKey, boolean isLegacy)177 public static GcmDeviceInfo decryptEnrollmentMessage( 178 byte[] enrollmentMessage, SecretKey masterKey, boolean isLegacy) 179 throws SignatureException, InvalidKeyException, NoSuchAlgorithmException { 180 if ((enrollmentMessage == null) || (masterKey == null)) { 181 throw new NullPointerException(); 182 } 183 184 HeaderAndBody outerHeaderAndBody; 185 GcmMetadata outerMetadata; 186 HeaderAndBody innerHeaderAndBody; 187 byte[] encodedUserPublicKey; 188 GcmDeviceInfo enrollmentInfo; 189 try { 190 SecureMessage outerMsg = SecureMessage.parseFrom(enrollmentMessage); 191 outerHeaderAndBody = SecureMessageParser.parseSignCryptedMessage( 192 outerMsg, masterKey, OUTER_SIG_TYPE, masterKey, OUTER_ENC_TYPE); 193 outerMetadata = GcmMetadata.parseFrom(outerHeaderAndBody.getHeader().getPublicMetadata()); 194 195 SecureMessage innerMsg = SecureMessage.parseFrom(outerHeaderAndBody.getBody()); 196 encodedUserPublicKey = SecureMessageParser.getUnverifiedHeader(innerMsg) 197 .getVerificationKeyId().toByteArray(); 198 PublicKey userPublicKey = KeyEncoding.parseUserPublicKey(encodedUserPublicKey); 199 SigType sigType = isLegacy ? LEGACY_INNER_SIG_TYPE : INNER_SIG_TYPE; 200 innerHeaderAndBody = SecureMessageParser.parseSignedCleartextMessage( 201 innerMsg, userPublicKey, sigType); 202 enrollmentInfo = GcmDeviceInfo.parseFrom(innerHeaderAndBody.getBody()); 203 } catch (InvalidProtocolBufferException e) { 204 throw new SignatureException(e); 205 } catch (InvalidKeySpecException e) { 206 throw new SignatureException(e); 207 } 208 209 boolean verified = 210 (outerMetadata.getType() == PayloadType.ENROLLMENT.getType()) 211 && (outerMetadata.getVersion() <= SecureGcmConstants.SECURE_GCM_VERSION) 212 && outerHeaderAndBody.getHeader().getVerificationKeyId().isEmpty() 213 && innerHeaderAndBody.getHeader().getPublicMetadata().isEmpty() 214 // Verify the encoded public key we used matches the encoded public key key being enrolled 215 && Arrays.equals(encodedUserPublicKey, enrollmentInfo.getUserPublicKey().toByteArray()) 216 && Arrays.equals(getMasterKeyHash(masterKey), 217 enrollmentInfo.getDeviceMasterKeyHash().toByteArray()); 218 219 if (verified) { 220 return enrollmentInfo; 221 } 222 throw new SignatureException(); 223 } 224 sha256(byte[] input)225 static byte[] sha256(byte[] input) { 226 try { 227 MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); 228 return sha256.digest(input); 229 } catch (NoSuchAlgorithmException e) { 230 throw new RuntimeException(e); // Shouldn't happen 231 } 232 } 233 } 234