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.securemessage; 16 17 import com.google.protobuf.ByteString; 18 import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType; 19 import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType; 20 import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.Header; 21 import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBodyInternal; 22 import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage; 23 import java.security.InvalidKeyException; 24 import java.security.Key; 25 import java.security.NoSuchAlgorithmException; 26 import java.security.SecureRandom; 27 import java.util.Arrays; 28 import javax.annotation.Nullable; 29 30 /** 31 * Builder for {@link SecureMessage} protos. Can be used to create either signed messages, 32 * or "signcrypted" (encrypted then signed) messages that include a tight binding between the 33 * ciphertext portion and a verification key identity. 34 * 35 * @see SecureMessageParser 36 */ 37 public class SecureMessageBuilder { 38 private ByteString publicMetadata; 39 private ByteString verificationKeyId; 40 private ByteString decryptionKeyId; 41 /** 42 * This data is never sent inside the protobufs, so the builder just saves it as a byte[]. 43 */ 44 private byte[] associatedData; 45 46 private SecureRandom rng; 47 SecureMessageBuilder()48 public SecureMessageBuilder() { 49 reset(); 50 this.rng = new SecureRandom(); 51 } 52 53 /** 54 * Resets this {@link SecureMessageBuilder} instance to a blank configuration (and returns it). 55 */ reset()56 public SecureMessageBuilder reset() { 57 this.publicMetadata = null; 58 this.verificationKeyId = null; 59 this.decryptionKeyId = null; 60 this.associatedData = null; 61 return this; 62 } 63 64 /** 65 * Optional metadata to be sent along with the header information in this {@link SecureMessage}. 66 * <p> 67 * Note that this value will be sent <em>UNENCRYPTED</em> in all cases. 68 * <p> 69 * Can be used with either cleartext or signcrypted messages, but is intended primarily for use 70 * with signcrypted messages. 71 */ setPublicMetadata(byte[] publicMetadata)72 public SecureMessageBuilder setPublicMetadata(byte[] publicMetadata) { 73 this.publicMetadata = ByteString.copyFrom(publicMetadata); 74 return this; 75 } 76 77 /** 78 * The recipient of the {@link SecureMessage} should be able to uniquely determine the correct 79 * verification key, given only this value. 80 * <p> 81 * Can be used with either cleartext or signcrypted messages. Setting this is mandatory for 82 * signcrypted messages using a public key {@link SigType}, in order to bind the encrypted 83 * body to a specific verification key. 84 * <p> 85 * Note that this value is sent <em>UNENCRYPTED</em> in all cases. 86 */ setVerificationKeyId(byte[] verificationKeyId)87 public SecureMessageBuilder setVerificationKeyId(byte[] verificationKeyId) { 88 this.verificationKeyId = ByteString.copyFrom(verificationKeyId); 89 return this; 90 } 91 92 /** 93 * To be used only with {@link #buildSignCryptedMessage(Key, SigType, Key, EncType, byte[])}, 94 * this value is sent <em>UNENCRYPTED</em> as part of the header. It should be used by the 95 * recipient of the {@link SecureMessage} to identify an appropriate key to use for decrypting 96 * the message body. 97 */ setDecryptionKeyId(byte[] decryptionKeyId)98 public SecureMessageBuilder setDecryptionKeyId(byte[] decryptionKeyId) { 99 this.decryptionKeyId = ByteString.copyFrom(decryptionKeyId); 100 return this; 101 } 102 103 /** 104 * Additional data is "associated" with this {@link SecureMessage}, but will not be sent as 105 * part of it. The recipient of the {@link SecureMessage} will need to provide the same data in 106 * order to verify the message body. Setting this to {@code null} is equivalent to using an 107 * empty array (unlike the behavior of {@code VerificationKeyId} and {@code DecryptionKeyId}). 108 * <p> 109 * Note that the <em>size</em> (length in bytes) of the associated data will be sent in the 110 * <em>UNENCRYPTED</em> header information, even if you are using encryption. 111 * <p> 112 * If you will be using {@link #buildSignedCleartextMessage(Key, SigType, byte[])}, then anyone 113 * observing the {@link SecureMessage} may be able to infer this associated data via an 114 * "offline dictionary attack". That is, when no encryption is used, you will not be hiding this 115 * data simply because it is not being sent over the wire. 116 */ setAssociatedData(@ullable byte[] associatedData)117 public SecureMessageBuilder setAssociatedData(@Nullable byte[] associatedData) { 118 this.associatedData = associatedData; 119 return this; 120 } 121 122 // @VisibleForTesting setRng(SecureRandom rng)123 SecureMessageBuilder setRng(SecureRandom rng) { 124 this.rng = rng; 125 return this; 126 } 127 128 /** 129 * Generates a signed {@link SecureMessage} with the payload {@code body} left 130 * <em>UNENCRYPTED</em>. 131 * 132 * <p>Note that if you have used {@link #setAssociatedData(byte[])}, the associated data will 133 * be subject to offline dictionary attacks if you use a public key {@link SigType}. 134 * 135 * <p>Doesn't currently support symmetric keys stored in a TPM (since we access the raw key). 136 * 137 * @see SecureMessageParser#parseSignedCleartextMessage(SecureMessage, Key, SigType) 138 */ buildSignedCleartextMessage(Key signingKey, SigType sigType, byte[] body)139 public SecureMessage buildSignedCleartextMessage(Key signingKey, SigType sigType, byte[] body) 140 throws NoSuchAlgorithmException, InvalidKeyException { 141 if ((signingKey == null) || (sigType == null) || (body == null)) { 142 throw new NullPointerException(); 143 } 144 if (decryptionKeyId != null) { 145 throw new IllegalStateException("Cannot set decryptionKeyId for a cleartext message"); 146 } 147 148 byte[] headerAndBody = serializeHeaderAndBody( 149 buildHeader(sigType, EncType.NONE, null).toByteArray(), body); 150 return createSignedResult(signingKey, sigType, headerAndBody, associatedData); 151 } 152 153 /** 154 * Generates a signed and encrypted {@link SecureMessage}. If the signature type requires a public 155 * key, such as with ECDSA_P256_SHA256, then the caller <em>must</em> set a verification id using 156 * the {@link #setVerificationKeyId(byte[])} method. The verification key id will be bound to the 157 * encrypted {@code body}, preventing attacks that involve stripping the signature and then 158 * re-signing the encrypted {@code body} as if it was originally sent by the attacker. 159 * 160 * <p> 161 * It is safe to re-use one {@link javax.crypto.SecretKey} as both {@code signingKey} and 162 * {@code encryptionKey}, even if that key is also used for 163 * {@link #buildSignedCleartextMessage(Key, SigType, byte[])}. In fact, the resulting output 164 * encoding will be more compact when the same symmetric key is used for both. 165 * 166 * <p> 167 * Note that PublicMetadata and other header fields are left <em>UNENCRYPTED</em>. 168 * 169 * <p> 170 * Doesn't currently support symmetric keys stored in a TPM (since we access the raw key). 171 * 172 * @param encType <em>must not</em> be set to {@link EncType#NONE} 173 * @see SecureMessageParser#parseSignCryptedMessage(SecureMessage, Key, SigType, Key, EncType) 174 */ buildSignCryptedMessage( Key signingKey, SigType sigType, Key encryptionKey, EncType encType, byte[] body)175 public SecureMessage buildSignCryptedMessage( 176 Key signingKey, SigType sigType, Key encryptionKey, EncType encType, byte[] body) 177 throws NoSuchAlgorithmException, InvalidKeyException { 178 if ((signingKey == null) 179 || (sigType == null) 180 || (encryptionKey == null) 181 || (encType == null) 182 || (body == null)) { 183 throw new NullPointerException(); 184 } 185 if (encType == EncType.NONE) { 186 throw new IllegalArgumentException(encType + " not supported for encrypted messages"); 187 } 188 if (sigType.isPublicKeyScheme() && (verificationKeyId == null)) { 189 throw new IllegalStateException( 190 "Must set a verificationKeyId when using public key signature with encryption"); 191 } 192 193 byte[] iv = CryptoOps.generateIv(encType, rng); 194 byte[] header = buildHeader(sigType, encType, iv).toByteArray(); 195 196 // We may or may not need an extra tag in front of the plaintext body 197 byte[] taggedBody; 198 // We will only sign the associated data when we don't tag the plaintext body 199 byte[] associatedDataToBeSigned; 200 if (taggedPlaintextRequired(signingKey, sigType, encryptionKey)) { 201 // Place a "tag" in front of the the plaintext message containing a digest of the header 202 taggedBody = CryptoOps.concat( 203 // Digest the header + any associated data, yielding a tag to be encrypted with the body. 204 CryptoOps.digest(CryptoOps.concat(header, associatedData)), 205 body); 206 associatedDataToBeSigned = null; // We already handled any associatedData via the tag 207 } else { 208 taggedBody = body; 209 associatedDataToBeSigned = associatedData; 210 } 211 212 // Compute the encrypted body, which binds the tag to the message inside the ciphertext 213 byte[] encryptedBody = CryptoOps.encrypt(encryptionKey, encType, rng, iv, taggedBody); 214 215 byte[] headerAndBody = serializeHeaderAndBody(header, encryptedBody); 216 return createSignedResult(signingKey, sigType, headerAndBody, associatedDataToBeSigned); 217 } 218 219 /** 220 * Indicates whether a "tag" is needed next to the plaintext body inside the ciphertext, to 221 * prevent the same ciphertext from being reused with someone else's signature on it. 222 */ taggedPlaintextRequired(Key signingKey, SigType sigType, Key encryptionKey)223 static boolean taggedPlaintextRequired(Key signingKey, SigType sigType, Key encryptionKey) { 224 // We need a tag if different keys are being used to "sign" vs. encrypt 225 return sigType.isPublicKeyScheme() 226 || !Arrays.equals(signingKey.getEncoded(), encryptionKey.getEncoded()); 227 } 228 229 /** 230 * @param iv IV or {@code null} if IV to be left unset in the Header 231 */ buildHeader(SigType sigType, EncType encType, byte[] iv)232 private Header buildHeader(SigType sigType, EncType encType, byte[] iv) { 233 Header.Builder result = Header.newBuilder() 234 .setSignatureScheme(sigType.getSigScheme()) 235 .setEncryptionScheme(encType.getEncScheme()); 236 if (verificationKeyId != null) { 237 result.setVerificationKeyId(verificationKeyId); 238 } 239 if (decryptionKeyId != null) { 240 result.setDecryptionKeyId(decryptionKeyId); 241 } 242 if (publicMetadata != null) { 243 result.setPublicMetadata(publicMetadata); 244 } 245 if (associatedData != null) { 246 result.setAssociatedDataLength(associatedData.length); 247 } 248 if (iv != null) { 249 result.setIv(ByteString.copyFrom(iv)); 250 } 251 return result.build(); 252 } 253 254 /** 255 * @param header a serialized representation of a {@link Header} 256 * @param body arbitrary payload data 257 * @return a serialized representation of a {@link SecureMessageProto.HeaderAndBody} 258 */ serializeHeaderAndBody(byte[] header, byte[] body)259 private byte[] serializeHeaderAndBody(byte[] header, byte[] body) { 260 return HeaderAndBodyInternal.newBuilder() 261 .setHeader(ByteString.copyFrom(header)) 262 .setBody(ByteString.copyFrom(body)) 263 .build() 264 .toByteArray(); 265 } 266 createSignedResult( Key signingKey, SigType sigType, byte[] headerAndBody, @Nullable byte[] associatedData)267 private SecureMessage createSignedResult( 268 Key signingKey, SigType sigType, byte[] headerAndBody, @Nullable byte[] associatedData) 269 throws NoSuchAlgorithmException, InvalidKeyException { 270 byte[] sig = 271 CryptoOps.sign(sigType, signingKey, rng, CryptoOps.concat(headerAndBody, associatedData)); 272 return SecureMessage.newBuilder() 273 .setHeaderAndBody(ByteString.copyFrom(headerAndBody)) 274 .setSignature(ByteString.copyFrom(sig)) 275 .build(); 276 } 277 } 278