blob: 45ad68ca6e2856e696a7eeefcf85658f77ad9792 [file] [log] [blame]
James E. Blairc49e5e72017-03-16 14:56:32 -07001#!/usr/bin/env python
2
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15import argparse
Tobias Henkel39d6dcd2017-06-24 21:26:57 +020016import base64
James E. Blair9118c012017-08-03 11:19:16 -070017import math
James E. Blairc49e5e72017-03-16 14:56:32 -070018import os
James E. Blair9118c012017-08-03 11:19:16 -070019import re
James E. Blairc49e5e72017-03-16 14:56:32 -070020import subprocess
21import sys
22import tempfile
James E. Blair9118c012017-08-03 11:19:16 -070023import textwrap
Tobias Henkel39d6dcd2017-06-24 21:26:57 +020024
25# we to import Request and urlopen differently for python 2 and 3
26try:
27 from urllib.request import Request
28 from urllib.request import urlopen
Tobias Henkel0f3f6052018-02-11 09:27:43 +010029 from urllib.parse import urlparse
Tobias Henkel39d6dcd2017-06-24 21:26:57 +020030except ImportError:
31 from urllib2 import Request
32 from urllib2 import urlopen
Tobias Henkel0f3f6052018-02-11 09:27:43 +010033 from urlparse import urlparse
James E. Blairc49e5e72017-03-16 14:56:32 -070034
35DESCRIPTION = """Encrypt a secret for Zuul.
36
37This program fetches a project-specific public key from a Zuul server and
38uses that to encrypt a secret. The only pre-requisite is an installed
39OpenSSL binary.
40"""
41
42
43def main():
44 parser = argparse.ArgumentParser(description=DESCRIPTION)
45 parser.add_argument('url',
46 help="The base URL of the zuul server and tenant. "
47 "E.g., https://zuul.example.com/tenant-name")
James E. Blairc49e5e72017-03-16 14:56:32 -070048 parser.add_argument('project',
49 help="The name of the project.")
Clint Byrum04bcbe12017-12-30 06:20:39 -080050 parser.add_argument('--strip', action='store_true', default=False,
51 help="Strip whitespace from beginning/end of input.")
James E. Blairc49e5e72017-03-16 14:56:32 -070052 parser.add_argument('--infile',
53 default=None,
54 help="A filename whose contents will be encrypted. "
55 "If not supplied, the value will be read from "
56 "standard input.")
57 parser.add_argument('--outfile',
58 default=None,
59 help="A filename to which the encrypted value will be "
60 "written. If not supplied, the value will be written "
61 "to standard output.")
62 args = parser.parse_args()
63
Tobias Henkel0f3f6052018-02-11 09:27:43 +010064 # We should not use unencrypted connections for retrieving the public key.
65 # Otherwise our secret can be compromised. The schemes file and https are
66 # considered safe.
67 url = urlparse(args.url)
68 if url.scheme not in ('file', 'https'):
69 sys.stderr.write("WARNING: Retrieving encryption key via an "
70 "unencrypted connection. Your secret may get "
71 "compromised.\n")
72
Fabien Boucherbf331d42017-09-21 17:40:26 +020073 req = Request("%s/%s.pub" % (args.url.rstrip('/'), args.project))
Tobias Henkel39d6dcd2017-06-24 21:26:57 +020074 pubkey = urlopen(req)
James E. Blairc49e5e72017-03-16 14:56:32 -070075
76 if args.infile:
77 with open(args.infile) as f:
78 plaintext = f.read()
79 else:
80 plaintext = sys.stdin.read()
81
James E. Blair9118c012017-08-03 11:19:16 -070082 plaintext = plaintext.encode("utf-8")
Clint Byrum04bcbe12017-12-30 06:20:39 -080083 if args.strip:
84 plaintext = plaintext.strip()
James E. Blair9118c012017-08-03 11:19:16 -070085
James E. Blairc49e5e72017-03-16 14:56:32 -070086 pubkey_file = tempfile.NamedTemporaryFile(delete=False)
87 try:
88 pubkey_file.write(pubkey.read())
89 pubkey_file.close()
90
James E. Blair9118c012017-08-03 11:19:16 -070091 p = subprocess.Popen(['openssl', 'rsa', '-text',
92 '-pubin', '-in',
James E. Blairc49e5e72017-03-16 14:56:32 -070093 pubkey_file.name],
James E. Blairc49e5e72017-03-16 14:56:32 -070094 stdout=subprocess.PIPE)
James E. Blair9118c012017-08-03 11:19:16 -070095 (stdout, stderr) = p.communicate()
James E. Blairc49e5e72017-03-16 14:56:32 -070096 if p.returncode != 0:
97 raise Exception("Return code %s from openssl" % p.returncode)
James E. Blair9118c012017-08-03 11:19:16 -070098 output = stdout.decode('utf-8')
Clint Byrum2dc31dd2017-11-01 16:49:28 -070099 openssl_version = subprocess.check_output(
100 ['openssl', 'version']).split()[1]
101 if openssl_version.startswith(b'0.'):
102 m = re.match(r'^Modulus \((\d+) bit\):$', output, re.MULTILINE)
103 else:
104 m = re.match(r'^Public-Key: \((\d+) bit\)$', output, re.MULTILINE)
James E. Blair9118c012017-08-03 11:19:16 -0700105 nbits = int(m.group(1))
106 nbytes = int(nbits / 8)
107 max_bytes = nbytes - 42 # PKCS1-OAEP overhead
108 chunks = int(math.ceil(float(len(plaintext)) / max_bytes))
109
110 ciphertext_chunks = []
111
112 print("Public key length: {} bits ({} bytes)".format(nbits, nbytes))
113 print("Max plaintext length per chunk: {} bytes".format(max_bytes))
114 print("Input plaintext length: {} bytes".format(len(plaintext)))
115 print("Number of chunks: {}".format(chunks))
116
117 for count in range(chunks):
118 chunk = plaintext[int(count * max_bytes):
119 int((count + 1) * max_bytes)]
120 p = subprocess.Popen(['openssl', 'rsautl', '-encrypt',
121 '-oaep', '-pubin', '-inkey',
122 pubkey_file.name],
123 stdin=subprocess.PIPE,
124 stdout=subprocess.PIPE)
125 (stdout, stderr) = p.communicate(chunk)
126 if p.returncode != 0:
127 raise Exception("Return code %s from openssl" % p.returncode)
128 ciphertext_chunks.append(base64.b64encode(stdout).decode('utf-8'))
129
James E. Blairc49e5e72017-03-16 14:56:32 -0700130 finally:
131 os.unlink(pubkey_file.name)
132
James E. Blair9118c012017-08-03 11:19:16 -0700133 output = textwrap.dedent(
134 '''
135 - secret:
136 name: <name>
137 data:
138 <fieldname>: !encrypted/pkcs1-oaep
139 ''')
140
141 twrap = textwrap.TextWrapper(width=79,
142 initial_indent=' ' * 8,
143 subsequent_indent=' ' * 10)
144 for chunk in ciphertext_chunks:
145 chunk = twrap.fill('- ' + chunk)
146 output += chunk + '\n'
147
James E. Blairc49e5e72017-03-16 14:56:32 -0700148 if args.outfile:
James E. Blair9118c012017-08-03 11:19:16 -0700149 with open(args.outfile, "w") as f:
150 f.write(output)
James E. Blairc49e5e72017-03-16 14:56:32 -0700151 else:
James E. Blair9118c012017-08-03 11:19:16 -0700152 print(output)
James E. Blairc49e5e72017-03-16 14:56:32 -0700153
154
155if __name__ == '__main__':
156 main()