blob: f59ce4e59b0e3dea662ba8b1c6ad030f6faccf98 [file] [log] [blame]
/* 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();
}
}