1# -*- coding: utf-8 -*- #
2# Copyright 2019 Google LLC. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#    http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""Import a provided key from file into KMS using an Import Job."""
16
17from __future__ import absolute_import
18from __future__ import division
19from __future__ import unicode_literals
20
21import os
22import sys
23
24from googlecloudsdk.api_lib.cloudkms import base as cloudkms_base
25from googlecloudsdk.calliope import base
26from googlecloudsdk.calliope import exceptions
27from googlecloudsdk.command_lib.kms import flags
28from googlecloudsdk.command_lib.kms import maps
29from googlecloudsdk.core import log
30from googlecloudsdk.core.util import files
31
32
33class Import(base.Command):
34  r"""Import a version into an existing crypto key.
35
36  Imports wrapped key material into a new version within an existing crypto key
37  following the import procedure documented at
38  https://cloud.google.com/kms/docs/importing-a-key.
39
40  ## EXAMPLES
41
42  The following command will read the files 'path/to/ephemeral/key' and
43  'path/to/target/key' and use them to create a new version with algorithm
44  'google-symmetric-encryption'  within the 'frodo' crypto key, 'fellowship'
45  keyring, and 'us-central1' location using import job 'strider' to unwrap the
46  provided key material.
47
48    $ {command} --location=global \
49         --keyring=fellowship \
50         --key=frodo \
51         --import-job=strider \
52         --rsa-aes-wrapped-key-file=path/to/target/key \
53         --algorithm=google-symmetric-encryption
54  """
55
56  @staticmethod
57  def Args(parser):
58    flags.AddKeyResourceFlags(parser, 'The containing key to import into.')
59    flags.AddRsaAesWrappedKeyFileFlag(parser, 'to import')
60    flags.AddImportedVersionAlgorithmFlag(parser)
61    flags.AddRequiredImportJobArgument(parser, 'to import from')
62    flags.AddOptionalPublicKeyFileArgument(parser)
63    flags.AddOptionalTargetKeyFileArgument(parser)
64
65  def _ReadFile(self, path, max_bytes):
66    data = files.ReadBinaryFileContents(path)
67    if len(data) > max_bytes:
68      raise exceptions.BadFileException(
69          'The file is larger than the maximum size of {0} bytes.'.format(
70              max_bytes))
71    return data
72
73  def _ReadOrFetchPublicKeyBytes(self, args, import_job_name):
74    client = cloudkms_base.GetClientInstance()
75    messages = cloudkms_base.GetMessagesModule()
76    # If the public key was provided, read it off disk. Otherwise, fetch it from
77    # KMS.
78    public_key_bytes = None
79    if args.public_key_file:
80      try:
81        public_key_bytes = self._ReadFile(
82            args.public_key_file, max_bytes=65536)
83      except files.Error as e:
84        raise exceptions.BadFileException(
85            'Failed to read public key file [{0}]: {1}'.format(
86                args.public_key_file, e))
87    else:
88      import_job = client.projects_locations_keyRings_importJobs.Get(  # pylint: disable=line-too-long
89          messages.CloudkmsProjectsLocationsKeyRingsImportJobsGetRequest(
90              name=import_job_name))
91      if import_job.state != messages.ImportJob.StateValueValuesEnum.ACTIVE:
92        raise exceptions.BadArgumentException(
93            'import-job',
94            'Import job [{0}] is not active (state is {1}).'.format(
95                import_job_name, import_job.state))
96      public_key_bytes = import_job.publicKey.pem.encode('ascii')
97    return public_key_bytes
98
99  def _CkmRsaAesKeyWrap(self, public_key_bytes, target_key_bytes):
100    try:
101      # TODO(b/141249289): Move imports to the top of the file. In the
102      # meantime, until we're sure that all Cloud SDK users have the
103      # cryptography module available, let's not error out if we can't load the
104      # module unless we're actually going down this code path.
105      # pylint: disable=g-import-not-at-top
106      from cryptography.hazmat.primitives import serialization
107      from cryptography.hazmat.backends import default_backend
108      from cryptography.hazmat.primitives import keywrap
109      from cryptography.hazmat.primitives.asymmetric import padding
110      from cryptography.hazmat.primitives import hashes
111    except ImportError:
112      log.err.Print('Cannot load the Pyca cryptography library. Either the '
113                    'library is not installed, or site packages are not '
114                    'enabled for the Google Cloud SDK. Please consult '
115                    'https://cloud.google.com/kms/docs/crypto for further '
116                    'instructions.')
117      sys.exit(1)
118
119    public_key = serialization.load_pem_public_key(
120        public_key_bytes, backend=default_backend())
121    ephem_key = os.urandom(32)
122    wrapped_ephem_key = public_key.encrypt(ephem_key,
123                                           padding.OAEP(
124                                               mgf=padding.MGF1(
125                                                   algorithm=hashes.SHA1()),
126                                               algorithm=hashes.SHA1(),
127                                               label=None))
128    wrapped_target_key = keywrap.aes_key_wrap_with_padding(ephem_key,
129                                                           target_key_bytes,
130                                                           default_backend())
131    return wrapped_ephem_key + wrapped_target_key
132
133  def Run(self, args):
134    client = cloudkms_base.GetClientInstance()
135    messages = cloudkms_base.GetMessagesModule()
136    import_job_name = flags.ParseImportJobName(args).RelativeName()
137
138    if bool(args.rsa_aes_wrapped_key_file) == bool(args.target_key_file):
139      raise exceptions.OneOfArgumentsRequiredException(
140          ('--target-key-file', '--rsa-aes-wrapped-key-file'),
141          'Either a pre-wrapped key or a key to be wrapped must be provided.')
142
143    rsa_aes_wrapped_key_bytes = None
144    if args.rsa_aes_wrapped_key_file:
145      try:
146        # This should be less than 64KiB.
147        rsa_aes_wrapped_key_bytes = self._ReadFile(
148            args.rsa_aes_wrapped_key_file, max_bytes=65536)
149      except files.Error as e:
150        raise exceptions.BadFileException(
151            'Failed to read rsa_aes_wrapped_key_file [{0}]: {1}'.format(
152                args.wrapped_target_key_file, e))
153
154    if args.target_key_file:
155      public_key_bytes = self._ReadOrFetchPublicKeyBytes(args, import_job_name)
156      target_key_bytes = None
157      try:
158        # This should be less than 64KiB.
159        target_key_bytes = self._ReadFile(
160            args.target_key_file, max_bytes=8192)
161      except files.Error as e:
162        raise exceptions.BadFileException(
163            'Failed to read target key file [{0}]: {1}'.format(
164                args.target_key_file, e))
165      rsa_aes_wrapped_key_bytes = self._CkmRsaAesKeyWrap(public_key_bytes,
166                                                         target_key_bytes)
167
168    # Send the request to KMS.
169    req = messages.CloudkmsProjectsLocationsKeyRingsCryptoKeysCryptoKeyVersionsImportRequest(  # pylint: disable=line-too-long
170        parent=flags.ParseCryptoKeyName(args).RelativeName())
171    req.importCryptoKeyVersionRequest = messages.ImportCryptoKeyVersionRequest(
172        algorithm=maps.ALGORITHM_MAPPER_FOR_IMPORT.GetEnumForChoice(
173            args.algorithm),
174        importJob=import_job_name,
175        rsaAesWrappedKey=rsa_aes_wrapped_key_bytes)
176
177    return client.projects_locations_keyRings_cryptoKeys_cryptoKeyVersions.Import(
178        req)
179