• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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