208 lines
8.6 KiB
Python
208 lines
8.6 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2017 Google LLC. All Rights Reserved.
|
|
#
|
|
# 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
|
|
#
|
|
# http://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.
|
|
"""Decrypt a ciphertext file using a key."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
from apitools.base.py import exceptions as apitools_exceptions
|
|
from googlecloudsdk.api_lib.cloudkms import base as cloudkms_base
|
|
from googlecloudsdk.calliope import base
|
|
from googlecloudsdk.calliope import exceptions
|
|
from googlecloudsdk.command_lib.kms import crc32c
|
|
from googlecloudsdk.command_lib.kms import e2e_integrity
|
|
from googlecloudsdk.command_lib.kms import flags
|
|
from googlecloudsdk.core import log
|
|
from googlecloudsdk.core.console import console_io
|
|
from googlecloudsdk.core.util import files
|
|
|
|
|
|
class Decrypt(base.Command):
|
|
r"""Decrypt a ciphertext file using a Cloud KMS key.
|
|
|
|
`{command}` decrypts the given ciphertext file using the given Cloud KMS key
|
|
and writes the result to the named plaintext file. Note that to permit users
|
|
to decrypt using a key, they must be have at least one of the following IAM
|
|
roles for that key: `roles/cloudkms.cryptoKeyDecrypter`,
|
|
`roles/cloudkms.cryptoKeyEncrypterDecrypter`.
|
|
|
|
Additional authenticated data (AAD) is used as an additional check by Cloud
|
|
KMS to authenticate a decryption request. If an additional authenticated data
|
|
file is provided, its contents must match the additional authenticated data
|
|
provided during encryption and must not be larger than 64KiB. If you don't
|
|
provide a value for `--additional-authenticated-data-file`, an empty string is
|
|
used. For a thorough explanation of AAD, refer to this
|
|
guide: https://cloud.google.com/kms/docs/additional-authenticated-data
|
|
|
|
If `--ciphertext-file` or `--additional-authenticated-data-file` is set to
|
|
'-', that file is read from stdin. Note that both files cannot be read from
|
|
stdin. Similarly, if `--plaintext-file` is set to '-', the decrypted plaintext
|
|
is written to stdout.
|
|
|
|
By default, the command performs integrity verification on data sent to and
|
|
received from Cloud KMS. Use `--skip-integrity-verification` to disable
|
|
integrity verification.
|
|
|
|
## EXAMPLES
|
|
|
|
To decrypt the file 'path/to/ciphertext' using the key `frodo` with key
|
|
ring `fellowship` and location `global` and write the plaintext
|
|
to 'path/to/plaintext.dec', run:
|
|
|
|
$ {command} \
|
|
--key=frodo \
|
|
--keyring=fellowship \
|
|
--location=global \
|
|
--ciphertext-file=path/to/input/ciphertext \
|
|
--plaintext-file=path/to/output/plaintext.dec
|
|
|
|
To decrypt the file 'path/to/ciphertext' using the key `frodo` and the
|
|
additional authenticated data that was used to encrypt the ciphertext, and
|
|
write the decrypted plaintext to stdout, run:
|
|
|
|
$ {command} \
|
|
--key=frodo \
|
|
--keyring=fellowship \
|
|
--location=global \
|
|
--additional-authenticated-data-file=path/to/aad \
|
|
--ciphertext-file=path/to/input/ciphertext \
|
|
--plaintext-file='-'
|
|
"""
|
|
|
|
@staticmethod
|
|
def Args(parser):
|
|
flags.AddKeyResourceFlags(
|
|
parser, 'Cloud KMS key to use for decryption.\n'
|
|
'* For symmetric keys, Cloud KMS detects the decryption key version '
|
|
'from the ciphertext. If you specify a key version as part of a '
|
|
'symmetric decryption request, an error is logged and decryption '
|
|
'fails.\n'
|
|
'* For asymmetric keys, the encryption key version can\'t be detected '
|
|
'automatically. You must keep track of this information and provide '
|
|
'the key version in the decryption request. The key version itself '
|
|
'is not sensitive data and does not need to be encrypted.')
|
|
flags.AddCiphertextFileFlag(
|
|
parser, 'to decrypt. This file should contain the result of encrypting '
|
|
'a file with `gcloud kms encrypt`')
|
|
flags.AddPlaintextFileFlag(parser, 'to output')
|
|
flags.AddAadFileFlag(parser)
|
|
flags.AddSkipIntegrityVerification(parser)
|
|
|
|
def _ReadFileOrStdin(self, path, max_bytes):
|
|
data = console_io.ReadFromFileOrStdin(path, binary=True)
|
|
if len(data) > max_bytes:
|
|
raise exceptions.BadFileException(
|
|
'The file [{0}] is larger than the maximum size of {1} bytes.'.format(
|
|
path, max_bytes))
|
|
return data
|
|
|
|
def _PerformIntegrityVerification(self, args):
|
|
return not args.skip_integrity_verification
|
|
|
|
def _CreateDecryptRequest(self, args):
|
|
if (args.ciphertext_file == '-' and
|
|
args.additional_authenticated_data_file == '-'):
|
|
raise exceptions.InvalidArgumentException(
|
|
'--ciphertext-file',
|
|
'--ciphertext-file and --additional-authenticated-data-file cannot '
|
|
'both read from stdin.')
|
|
|
|
try:
|
|
# The Encrypt API has a limit of 64K; the output ciphertext files will be
|
|
# slightly larger. Check proactively (but generously) to avoid attempting
|
|
# to buffer and send obviously oversized files to KMS.
|
|
ciphertext = self._ReadFileOrStdin(
|
|
args.ciphertext_file, max_bytes=2 * 65536)
|
|
except files.Error as e:
|
|
raise exceptions.BadFileException(
|
|
'Failed to read ciphertext file [{0}]: {1}'.format(
|
|
args.ciphertext_file, e))
|
|
|
|
aad = None
|
|
if args.additional_authenticated_data_file:
|
|
try:
|
|
# The Encrypt API limits the AAD to 64KiB.
|
|
aad = self._ReadFileOrStdin(
|
|
args.additional_authenticated_data_file, max_bytes=65536)
|
|
except files.Error as e:
|
|
raise exceptions.BadFileException(
|
|
'Failed to read additional authenticated data file [{0}]: {1}'
|
|
.format(args.additional_authenticated_data_file, e))
|
|
|
|
crypto_key_ref = flags.ParseCryptoKeyName(args)
|
|
|
|
# Check that the key id does not include /cryptoKeyVersion/ which may occur
|
|
# as encrypt command does allow version, so it is easy for user to make a
|
|
# mistake here.
|
|
if '/cryptoKeyVersions/' in crypto_key_ref.cryptoKeysId:
|
|
raise exceptions.InvalidArgumentException(
|
|
'--key', '{} includes cryptoKeyVersion which is not valid for '
|
|
'decrypt.'.format(crypto_key_ref.cryptoKeysId))
|
|
|
|
messages = cloudkms_base.GetMessagesModule()
|
|
|
|
req = messages.CloudkmsProjectsLocationsKeyRingsCryptoKeysDecryptRequest(
|
|
name=crypto_key_ref.RelativeName())
|
|
|
|
# Populate request integrity fields.
|
|
if self._PerformIntegrityVerification(args):
|
|
ciphertext_crc32c = crc32c.Crc32c(ciphertext)
|
|
# Set checksum if AAD is not provided for consistency.
|
|
aad_crc32c = crc32c.Crc32c(aad) if aad is not None else crc32c.Crc32c(b'')
|
|
req.decryptRequest = messages.DecryptRequest(
|
|
ciphertext=ciphertext,
|
|
additionalAuthenticatedData=aad,
|
|
ciphertextCrc32c=ciphertext_crc32c,
|
|
additionalAuthenticatedDataCrc32c=aad_crc32c)
|
|
else:
|
|
req.decryptRequest = messages.DecryptRequest(
|
|
ciphertext=ciphertext, additionalAuthenticatedData=aad)
|
|
|
|
return req
|
|
|
|
def _VerifyResponseIntegrityFields(self, req, resp):
|
|
"""Verifies integrity fields in response."""
|
|
|
|
# Verify plaintext checksum.
|
|
if not crc32c.Crc32cMatches(resp.plaintext, resp.plaintextCrc32c):
|
|
raise e2e_integrity.ClientSideIntegrityVerificationError(
|
|
e2e_integrity.GetResponseFromServerCorruptedErrorMessage())
|
|
|
|
def Run(self, args):
|
|
req = self._CreateDecryptRequest(args)
|
|
client = cloudkms_base.GetClientInstance()
|
|
try:
|
|
resp = client.projects_locations_keyRings_cryptoKeys.Decrypt(req)
|
|
# Intercept INVALID_ARGUMENT errors related to checksum verification to
|
|
# present a user-friendly message. All other errors are surfaced as-is.
|
|
except apitools_exceptions.HttpBadRequestError as error:
|
|
e2e_integrity.ProcessHttpBadRequestError(error)
|
|
|
|
if self._PerformIntegrityVerification(args):
|
|
self._VerifyResponseIntegrityFields(req, resp)
|
|
|
|
try:
|
|
if resp.plaintext is None:
|
|
with files.FileWriter(args.plaintext_file):
|
|
# to create an empty file
|
|
pass
|
|
log.Print('Decrypted file is empty')
|
|
else:
|
|
log.WriteToFileOrStdout(
|
|
args.plaintext_file, resp.plaintext, binary=True, overwrite=True)
|
|
except files.Error as e:
|
|
raise exceptions.BadFileException(e)
|