| /* Copyright 2018 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(); |
| } |
| } |