| /* 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.protobuf.InvalidProtocolBufferException; |
| 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.HeaderAndBody; |
| import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBodyInternal; |
| import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage; |
| import java.security.InvalidAlgorithmParameterException; |
| import java.security.InvalidKeyException; |
| import java.security.Key; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.SignatureException; |
| import javax.annotation.Nullable; |
| import javax.crypto.BadPaddingException; |
| import javax.crypto.IllegalBlockSizeException; |
| |
| /** |
| * Utility class to parse and verify {@link SecureMessage} protos. Verifies the signature on the |
| * message, and decrypts "signcrypted" messages (while simultaneously verifying the signature). |
| * |
| * @see SecureMessageBuilder |
| */ |
| public class SecureMessageParser { |
| |
| private SecureMessageParser() {} // Do not instantiate |
| |
| /** |
| * Extracts the {@link Header} component from a {@link SecureMessage} but <em>DOES NOT VERIFY</em> |
| * the signature when doing so. Callers should not trust the resulting output until after a |
| * subsequent {@code parse*()} call has succeeded. |
| * |
| * <p>The intention is to allow the caller to determine the type of the protocol message and which |
| * keys are in use, prior to attempting to verify (and possibly decrypt) the payload body. |
| */ |
| public static Header getUnverifiedHeader(SecureMessage secmsg) |
| throws InvalidProtocolBufferException { |
| if (!secmsg.hasHeaderAndBody()) { |
| throw new InvalidProtocolBufferException("Missing header and body"); |
| } |
| if (!HeaderAndBody.parseFrom(secmsg.getHeaderAndBody()).hasHeader()) { |
| throw new InvalidProtocolBufferException("Missing header"); |
| } |
| Header result = HeaderAndBody.parseFrom(secmsg.getHeaderAndBody()).getHeader(); |
| // Check that at least a signature scheme was set |
| if (!result.hasSignatureScheme()) { |
| throw new InvalidProtocolBufferException("Missing header field(s)"); |
| } |
| // Check signature scheme is legal |
| try { |
| SigType.valueOf(result.getSignatureScheme()); |
| } catch (IllegalArgumentException e) { |
| throw new InvalidProtocolBufferException("Corrupt/unsupported SignatureScheme"); |
| } |
| // Check encryption scheme is legal |
| if (result.hasEncryptionScheme()) { |
| try { |
| EncType.valueOf(result.getEncryptionScheme()); |
| } catch (IllegalArgumentException e) { |
| throw new InvalidProtocolBufferException("Corrupt/unsupported EncryptionScheme"); |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Parses a {@link SecureMessage} containing a cleartext payload body, and verifies the signature. |
| * |
| * @return the parsed {@link HeaderAndBody} pair (which is fully verified) |
| * @throws SignatureException if signature verification fails |
| * @see SecureMessageBuilder#buildSignedCleartextMessage(Key, SigType, byte[]) |
| */ |
| public static HeaderAndBody parseSignedCleartextMessage( |
| SecureMessage secmsg, Key verificationKey, SigType sigType) |
| throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { |
| return parseSignedCleartextMessage(secmsg, verificationKey, sigType, null); |
| } |
| |
| /** |
| * Parses a {@link SecureMessage} containing a cleartext payload body, and verifies the signature. |
| * |
| * @param associatedData optional associated data bound to the signature (but not in the message) |
| * @return the parsed {@link HeaderAndBody} pair (which is fully verified) |
| * @throws SignatureException if signature verification fails |
| * @see SecureMessageBuilder#buildSignedCleartextMessage(Key, SigType, byte[]) |
| */ |
| public static HeaderAndBody parseSignedCleartextMessage( |
| SecureMessage secmsg, Key verificationKey, SigType sigType, @Nullable byte[] associatedData) |
| throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { |
| if ((secmsg == null) || (verificationKey == null) || (sigType == null)) { |
| throw new NullPointerException(); |
| } |
| return verifyHeaderAndBody( |
| secmsg, |
| verificationKey, |
| sigType, |
| EncType.NONE, |
| associatedData, |
| false /* suppressAssociatedData is always false for signed cleartext */); |
| } |
| |
| /** |
| * Parses a {@link SecureMessage} containing an encrypted payload body, extracting a decryption of |
| * the payload body and verifying the signature. |
| * |
| * @return the parsed {@link HeaderAndBody} pair (which is fully verified and decrypted) |
| * @throws SignatureException if signature verification fails |
| * @see SecureMessageBuilder#buildSignCryptedMessage(Key, SigType, Key, EncType, byte[]) |
| */ |
| public static HeaderAndBody parseSignCryptedMessage( |
| SecureMessage secmsg, |
| Key verificationKey, |
| SigType sigType, |
| Key decryptionKey, |
| EncType encType) |
| throws InvalidKeyException, NoSuchAlgorithmException, SignatureException { |
| return parseSignCryptedMessage(secmsg, verificationKey, sigType, decryptionKey, encType, null); |
| } |
| |
| /** |
| * Parses a {@link SecureMessage} containing an encrypted payload body, extracting a decryption of |
| * the payload body and verifying the signature. |
| * |
| * @param associatedData optional associated data bound to the signature (but not in the message) |
| * @return the parsed {@link HeaderAndBody} pair (which is fully verified and decrypted) |
| * @throws SignatureException if signature verification fails |
| * @see SecureMessageBuilder#buildSignCryptedMessage(Key, SigType, Key, EncType, byte[]) |
| */ |
| public static HeaderAndBody parseSignCryptedMessage( |
| SecureMessage secmsg, |
| Key verificationKey, |
| SigType sigType, |
| Key decryptionKey, |
| EncType encType, |
| @Nullable byte[] associatedData) |
| throws InvalidKeyException, NoSuchAlgorithmException, SignatureException { |
| if ((secmsg == null) |
| || (verificationKey == null) |
| || (sigType == null) |
| || (decryptionKey == null) |
| || (encType == null)) { |
| throw new NullPointerException(); |
| } |
| if (encType == EncType.NONE) { |
| throw new SignatureException("Not a signcrypted message"); |
| } |
| |
| boolean tagRequired = |
| SecureMessageBuilder.taggedPlaintextRequired(verificationKey, sigType, decryptionKey); |
| HeaderAndBody headerAndEncryptedBody; |
| headerAndEncryptedBody = verifyHeaderAndBody( |
| secmsg, |
| verificationKey, |
| sigType, |
| encType, |
| associatedData, |
| tagRequired /* suppressAssociatedData if it is handled by the tag instead */); |
| |
| byte[] rawDecryptedBody; |
| Header header = headerAndEncryptedBody.getHeader(); |
| if (!header.hasIv()) { |
| throw new SignatureException(); |
| } |
| try { |
| rawDecryptedBody = CryptoOps.decrypt( |
| decryptionKey, encType, header.getIv().toByteArray(), |
| headerAndEncryptedBody.getBody().toByteArray()); |
| } catch (InvalidAlgorithmParameterException e) { |
| throw new SignatureException(); |
| } catch (IllegalBlockSizeException e) { |
| throw new SignatureException(); |
| } catch (BadPaddingException e) { |
| throw new SignatureException(); |
| } |
| |
| if (!tagRequired) { |
| // No tag expected, so we're all done |
| return HeaderAndBody.newBuilder(headerAndEncryptedBody) |
| .setBody(ByteString.copyFrom(rawDecryptedBody)) |
| .build(); |
| } |
| |
| // Verify the tag that binds the ciphertext to the header, and remove it |
| byte[] headerBytes; |
| try { |
| headerBytes = |
| HeaderAndBodyInternal.parseFrom(secmsg.getHeaderAndBody()).getHeader().toByteArray(); |
| } catch (InvalidProtocolBufferException e) { |
| // This shouldn't happen, but throw it up just in case |
| throw new SignatureException(e); |
| } |
| boolean verifiedBinding = false; |
| byte[] expectedTag = CryptoOps.digest(CryptoOps.concat(headerBytes, associatedData)); |
| if (rawDecryptedBody.length >= CryptoOps.DIGEST_LENGTH) { |
| byte[] actualTag = CryptoOps.subarray(rawDecryptedBody, 0, CryptoOps.DIGEST_LENGTH); |
| if (CryptoOps.constantTimeArrayEquals(actualTag, expectedTag)) { |
| verifiedBinding = true; |
| } |
| } |
| if (!verifiedBinding) { |
| throw new SignatureException(); |
| } |
| |
| int bodyLen = rawDecryptedBody.length - CryptoOps.DIGEST_LENGTH; |
| return HeaderAndBody.newBuilder(headerAndEncryptedBody) |
| // Remove the tag and set the plaintext body |
| .setBody(ByteString.copyFrom(rawDecryptedBody, CryptoOps.DIGEST_LENGTH, bodyLen)) |
| .build(); |
| } |
| |
| private static HeaderAndBody verifyHeaderAndBody( |
| SecureMessage secmsg, |
| Key verificationKey, |
| SigType sigType, |
| EncType encType, |
| @Nullable byte[] associatedData, |
| boolean suppressAssociatedData /* in case it is in the tag instead */) |
| throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { |
| if (!secmsg.hasHeaderAndBody() || !secmsg.hasSignature()) { |
| throw new SignatureException("Signature failed verification"); |
| } |
| byte[] signature = secmsg.getSignature().toByteArray(); |
| byte[] data = secmsg.getHeaderAndBody().toByteArray(); |
| byte[] signedData = suppressAssociatedData ? data : CryptoOps.concat(data, associatedData); |
| |
| // Try not to leak the specific reason for verification failures, due to security concerns. |
| boolean verified = CryptoOps.verify(verificationKey, sigType, signature, signedData); |
| HeaderAndBody result = null; |
| try { |
| result = HeaderAndBody.parseFrom(secmsg.getHeaderAndBody()); |
| // Even if declared required, micro proto doesn't throw an exception if fields are not present |
| if (!result.hasHeader() || !result.hasBody()) { |
| throw new SignatureException("Signature failed verification"); |
| } |
| verified &= (result.getHeader().getSignatureScheme() == sigType.getSigScheme()); |
| verified &= (result.getHeader().getEncryptionScheme() == encType.getEncScheme()); |
| // Check that either a decryption operation is expected, or no DecryptionKeyId is set. |
| verified &= (encType != EncType.NONE) || !result.getHeader().hasDecryptionKeyId(); |
| // If encryption was used, check that either we are not using a public key signature or a |
| // VerificationKeyId was set (as is required for public key based signature + encryption). |
| verified &= (encType == EncType.NONE) || !sigType.isPublicKeyScheme() || |
| result.getHeader().hasVerificationKeyId(); |
| int associatedDataLength = associatedData == null ? 0 : associatedData.length; |
| verified &= (result.getHeader().getAssociatedDataLength() == associatedDataLength); |
| } catch (InvalidProtocolBufferException e) { |
| verified = false; |
| } |
| |
| if (verified) { |
| return result; |
| } |
| throw new SignatureException("Signature failed verification"); |
| } |
| } |