summaryrefslogtreecommitdiff
path: root/tools/encrypt_secret.py
blob: 40a264cde05d5e36d3ed1e18a5cd4057a3dd8f81 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
#!/usr/bin/env python3

# 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.

import argparse
import base64
import json
import math
import os
import re
import subprocess
import sys
import tempfile
import textwrap
import ssl

# we to import Request and urlopen differently for python 2 and 3
try:
    from urllib.request import Request
    from urllib.request import urlopen
    from urllib.parse import urlparse
except ImportError:
    from urllib2 import Request
    from urllib2 import urlopen
    from urlparse import urlparse

DESCRIPTION = """Encrypt a secret for Zuul.

This program fetches a project-specific public key from a Zuul server and
uses that to encrypt a secret.  The only pre-requisite is an installed
OpenSSL binary.
"""


def main():
    parser = argparse.ArgumentParser(description=DESCRIPTION)
    parser.add_argument('url',
                        help="The base URL of the zuul server.  "
                        "E.g., https://zuul.example.com/ or path"
                        " to project public key file. E.g.,"
                        " file:///path/to/key.pub")
    parser.add_argument('project', default=None, nargs="?",
                        help="The name of the project. Required when using"
                        " the Zuul API to fetch the public key.")
    parser.add_argument('--tenant',
                        default=None,
                        help="The name of the Zuul tenant.  This may be "
                        "required in a multi-tenant environment.")
    parser.add_argument('--strip', default=None,
                        help='Unused, kept for backward compatibility.')
    parser.add_argument('--no-strip', action='store_true', default=False,
                        help="Do not strip whitespace from beginning or "
                        "end of input.")
    parser.add_argument('--infile',
                        default=None,
                        help="A filename whose contents will be encrypted.  "
                        "If not supplied, the value will be read from "
                        "standard input.")
    parser.add_argument('--outfile',
                        default=None,
                        help="A filename to which the encrypted value will be "
                        "written.  If not supplied, the value will be written "
                        "to standard output.")
    parser.add_argument('--insecure', action='store_true', default=False,
                        help="Do not verify remote certificate")
    args = parser.parse_args()

    # We should not use unencrypted connections for retrieving the public key.
    # Otherwise our secret can be compromised. The schemes file and https are
    # considered safe.
    url = urlparse(args.url)
    if url.scheme not in ('file', 'https'):
        sys.stderr.write("WARNING: Retrieving encryption key via an "
                         "unencrypted connection. Your secret may get "
                         "compromised.\n")

    ssl_ctx = None
    if url.scheme == 'file':
        req = Request(args.url)
    else:
        if args.insecure:
            ssl_ctx = ssl.create_default_context()
            ssl_ctx.check_hostname = False
            ssl_ctx.verify_mode = ssl.CERT_NONE

        # Check if tenant is white label
        req = Request("%s/api/info" % (args.url.rstrip('/'),))
        info = json.loads(urlopen(req, context=ssl_ctx).read().decode('utf8'))

        api_tenant = info.get('info', {}).get('tenant')
        if not api_tenant and not args.tenant:
            print("Error: the --tenant argument is required")
            exit(1)

        if api_tenant:
            req = Request("%s/api/key/%s.pub" % (
                args.url.rstrip('/'), args.project))
        else:
            req = Request("%s/api/tenant/%s/key/%s.pub" % (
                args.url.rstrip('/'), args.tenant, args.project))
    try:
        pubkey = urlopen(req, context=ssl_ctx)
    except Exception:
        sys.stderr.write(
            "ERROR: Couldn't retrieve project key via %s\n" % req.full_url)
        raise

    if args.infile:
        with open(args.infile) as f:
            plaintext = f.read()
    else:
        plaintext = sys.stdin.read()

    plaintext = plaintext.encode("utf-8")
    if not args.no_strip:
        plaintext = plaintext.strip()

    pubkey_file = tempfile.NamedTemporaryFile(delete=False)
    try:
        pubkey_file.write(pubkey.read())
        pubkey_file.close()

        p = subprocess.Popen(['openssl', 'rsa', '-text',
                              '-pubin', '-in',
                              pubkey_file.name],
                             stdout=subprocess.PIPE)
        (stdout, stderr) = p.communicate()
        if p.returncode != 0:
            raise Exception("Return code %s from openssl" % p.returncode)
        output = stdout.decode('utf-8')
        openssl_version = subprocess.check_output(
            ['openssl', 'version']).split()[1]
        if openssl_version.startswith(b'0.'):
            key_length_re = r'^Modulus \((?P<key_length>\d+) bit\):$'
        else:
            key_length_re = r'^(|RSA )Public-Key: \((?P<key_length>\d+) bit\)$'
        m = re.match(key_length_re, output, re.MULTILINE)
        nbits = int(m.group('key_length'))
        nbytes = int(nbits / 8)
        max_bytes = nbytes - 42  # PKCS1-OAEP overhead
        chunks = int(math.ceil(float(len(plaintext)) / max_bytes))

        ciphertext_chunks = []

        print("Public key length: {} bits ({} bytes)".format(nbits, nbytes))
        print("Max plaintext length per chunk: {} bytes".format(max_bytes))
        print("Input plaintext length: {} bytes".format(len(plaintext)))
        print("Number of chunks: {}".format(chunks))

        for count in range(chunks):
            chunk = plaintext[int(count * max_bytes):
                              int((count + 1) * max_bytes)]
            p = subprocess.Popen(['openssl', 'rsautl', '-encrypt',
                                  '-oaep', '-pubin', '-inkey',
                                  pubkey_file.name],
                                 stdin=subprocess.PIPE,
                                 stdout=subprocess.PIPE)
            (stdout, stderr) = p.communicate(chunk)
            if p.returncode != 0:
                raise Exception("Return code %s from openssl" % p.returncode)
            ciphertext_chunks.append(base64.b64encode(stdout).decode('utf-8'))

    finally:
        os.unlink(pubkey_file.name)

    output = textwrap.dedent(
        '''
        - secret:
            name: <name>
            data:
              <fieldname>: !encrypted/pkcs1-oaep
        ''')

    twrap = textwrap.TextWrapper(width=79,
                                 initial_indent=' ' * 8,
                                 subsequent_indent=' ' * 10)
    for chunk in ciphertext_chunks:
        chunk = twrap.fill('- ' + chunk)
        output += chunk + '\n'

    if args.outfile:
        with open(args.outfile, "w") as f:
            f.write(output)
    else:
        print(output)


if __name__ == '__main__':
    print(
        "This script is deprecated. Use `zuul-client encrypt` instead. "
        "Please refer to https://zuul-ci.org/docs/zuul-client/ "
        "for more details on how to use zuul-client."
    )
    main()