Browse code

2007-05-27 Michal Ludvig <michal@logix.cz>

* Support for on-the-fly GPG encryption.



git-svn-id: https://s3tools.svn.sourceforge.net/svnroot/s3tools/s3cmd/trunk@121 830e0280-6d2a-0410-9c65-932aecc39d9d

Michal Ludvig authored on 2007/05/28 00:59:31
Showing 5 changed files
... ...
@@ -1,3 +1,7 @@
1
+2007-05-27  Michal Ludvig  <michal@logix.cz>
2
+
3
+	* Support for on-the-fly GPG encryption.
4
+
1 5
 2007-05-26  Michal Ludvig  <michal@logix.cz>
2 6
 
3 7
 	* s3cmd.1: Add info about "s3cmd du" command.
... ...
@@ -19,6 +19,11 @@ class Config(object):
19 19
 	human_readable_sizes = False
20 20
 	force = False
21 21
 	acl_public = False
22
+	encrypt = False
23
+	gpg_passphrase = ""
24
+	gpg_command = "/usr/bin/gpg"
25
+	gpg_encrypt = "%(gpg_command)s -c --verbose --no-use-agent --batch --yes --passphrase-fd %(passphrase_fd)s -o %(output_file)s %(input_file)s"
26
+	gpg_decrypt = "%(gpg_command)s -d --verbose --no-use-agent --batch --yes --passphrase-fd %(passphrase_fd)s -o %(output_file)s %(input_file)s"
22 27
 
23 28
 	## Creating a singleton
24 29
 	def __new__(self, configfile = None):
... ...
@@ -109,8 +114,8 @@ class ConfigParser(object):
109 109
 				if r_quotes.match(data["value"]):
110 110
 					data["value"] = data["value"][1:-1]
111 111
 				self.__setitem__(data["key"], data["value"])
112
-				if data["key"] in ("access_key", "secret_key"):
113
-					print_value = (data["value"][:3]+"...%d_chars..."+data["value"][-2:]) % (len(data["value"]) - 4)
112
+				if data["key"] in ("access_key", "secret_key", "gpg_passphrase"):
113
+					print_value = (data["value"][:2]+"...%d_chars..."+data["value"][-1:]) % (len(data["value"]) - 3)
114 114
 				else:
115 115
 					print_value = data["value"]
116 116
 				debug("ConfigParser: %s->%s" % (data["key"], print_value))
... ...
@@ -109,7 +109,7 @@ class S3(object):
109 109
 		response = self.send_request(request)
110 110
 		return response
111 111
 
112
-	def object_put(self, filename, bucket, object):
112
+	def object_put(self, filename, bucket, object, extra_headers = None):
113 113
 		if not os.path.isfile(filename):
114 114
 			raise ParameterError("%s is not a regular file" % filename)
115 115
 		try:
... ...
@@ -118,6 +118,8 @@ class S3(object):
118 118
 		except IOError, e:
119 119
 			raise ParameterError("%s: %s" % (filename, e.strerror))
120 120
 		headers = SortedDict()
121
+		if extra_headers:
122
+			headers.update(extra_headers)
121 123
 		headers["content-length"] = size
122 124
 		if self.config.acl_public:
123 125
 			headers["x-amz-acl"] = "public-read"
... ...
@@ -143,10 +145,10 @@ class S3(object):
143 143
 		response = self.send_request(request)
144 144
 		return response
145 145
 
146
-	def object_put_uri(self, filename, uri):
146
+	def object_put_uri(self, filename, uri, extra_headers = None):
147 147
 		if uri.type != "s3":
148 148
 			raise ValueError("Expected URI type 's3', got '%s'" % uri.type)
149
-		return self.object_put(filename, uri.bucket(), uri.object())
149
+		return self.object_put(filename, uri.bucket(), uri.object(), extra_headers)
150 150
 
151 151
 	def object_get_uri(self, uri, filename):
152 152
 		if uri.type != "s3":
... ...
@@ -3,9 +3,12 @@
3 3
 ##         http://www.logix.cz/michal
4 4
 ## License: GPL Version 2
5 5
 
6
+import os
6 7
 import time
7 8
 import re
8 9
 import elementtree.ElementTree as ET
10
+import string
11
+import random
9 12
 
10 13
 def parseNodes(nodes, xmlns = ""):
11 14
 	retval = []
... ...
@@ -68,3 +71,36 @@ def convertTupleListToDict(list):
68 68
 		retval[tuple[0]] = tuple[1]
69 69
 	return retval
70 70
 
71
+
72
+_rnd_chars = string.ascii_letters+string.digits
73
+_rnd_chars_len = len(_rnd_chars)
74
+def rndstr(len):
75
+	retval = ""
76
+	while len > 0:
77
+		retval += _rnd_chars[random.randint(0, _rnd_chars_len-1)]
78
+		len -= 1
79
+	return retval
80
+
81
+def mktmpsomething(prefix, randchars, createfunc):
82
+	old_umask = os.umask(0077)
83
+	tries = 5
84
+	while tries > 0:
85
+		dirname = prefix + rndstr(randchars)
86
+		try:
87
+			createfunc(dirname)
88
+			break
89
+		except OSError, e:
90
+			if e.errno != errno.EEXIST:
91
+				os.umask(old_umask)
92
+				raise
93
+		tries -= 1
94
+
95
+	os.umask(old_umask)
96
+	return dirname
97
+
98
+def mktmpdir(prefix = "/tmp/tmpdir-", randchars = 10):
99
+	return mktmpsomething(prefix, randchars, os.mkdir)
100
+
101
+def mktmpfile(prefix = "/tmp/tmpfile-", randchars = 20):
102
+	createfunc = lambda filename : os.close(os.open(filename, os.O_CREAT | os.O_EXCL))
103
+	return mktmpsomething(prefix, randchars, createfunc)
... ...
@@ -19,6 +19,7 @@ from S3 import PkgInfo
19 19
 from S3.S3 import *
20 20
 from S3.Config import Config
21 21
 from S3.S3Uri import *
22
+from S3 import Utils
22 23
 
23 24
 
24 25
 def output(message):
... ...
@@ -172,14 +173,21 @@ def cmd_object_put(args):
172 172
 		uri_arg_final = str(uri)
173 173
 		if len(files) > 1 or uri.object() == "":
174 174
 			uri_arg_final += os.path.basename(file)
175
-
175
+		
176 176
 		uri_final = S3Uri(uri_arg_final)
177
-		response = s3.object_put_uri(file, uri_final)
177
+		extra_headers = {}
178
+		real_filename = file
179
+		if Config().encrypt:
180
+			exitcode, real_filename, extra_headers["x-amz-meta-s3tools-gpgenc"] = gpg_encrypt(file)
181
+		response = s3.object_put_uri(real_filename, uri_final, extra_headers)
178 182
 		output("File '%s' stored as %s (%d bytes)" %
179 183
 			(file, uri_final, response["size"]))
180 184
 		if Config().acl_public:
181 185
 			output("Public URL of the object is: %s" %
182 186
 				(uri.public_url()))
187
+		if Config().encrypt and real_filename != file:
188
+			debug("Removing temporary encrypted file: %s" % real_filename)
189
+			os.remove(real_filename)
183 190
 
184 191
 def cmd_object_get(args):
185 192
 	s3 = S3(Config())
... ...
@@ -195,6 +203,9 @@ def cmd_object_get(args):
195 195
 	if not Config().force and os.path.exists(destination):
196 196
 		raise ParameterError("File %s already exists. Use --force to overwrite it" % destination)
197 197
 	response = s3.object_get_uri(uri, destination)
198
+	if response["headers"].has_key("x-amz-meta-s3tools-gpgenc"):
199
+		gpg_decrypt(destination, response["headers"]["x-amz-meta-s3tools-gpgenc"])
200
+		response["size"] = os.stat(destination)[6]
198 201
 	if destination != "-":
199 202
 		output("Object %s saved as '%s' (%d bytes)" %
200 203
 			(uri, destination, response["size"]))
... ...
@@ -210,6 +221,52 @@ def cmd_object_del(args):
210 210
 	response = s3.object_delete_uri(uri)
211 211
 	output("Object %s deleted" % uri)
212 212
 
213
+def resolve_list(lst, args):
214
+	retval = []
215
+	for item in lst:
216
+		retval.append(item % args)
217
+	return retval
218
+
219
+def gpg_command(command, passphrase = ""):
220
+	p_in, p_out = os.popen4(command)
221
+	if command.count("--passphrase-fd"):
222
+		p_in.write(passphrase+"\n")
223
+		p_in.flush()
224
+	for line in p_out:
225
+		info(line.strip())
226
+	p_pid, p_exitcode = os.wait()
227
+	return p_exitcode
228
+
229
+def gpg_encrypt(filename):
230
+	tmp_filename = Utils.mktmpfile()
231
+	args = {
232
+		"gpg_command" : cfg.gpg_command,
233
+		"passphrase_fd" : "0",
234
+		"input_file" : filename, 
235
+		"output_file" : tmp_filename,
236
+	}
237
+	info("Encrypting file %(input_file)s to %(output_file)s..." % args)
238
+	command = resolve_list(cfg.gpg_encrypt.split(" "), args)
239
+	code = gpg_command(command, cfg.gpg_passphrase)
240
+	return (code, tmp_filename, "gpg")
241
+
242
+def gpg_decrypt(filename, gpgenc_header = ""):
243
+	tmp_filename = Utils.mktmpfile(filename)
244
+	args = {
245
+		"gpg_command" : cfg.gpg_command,
246
+		"passphrase_fd" : "0",
247
+		"input_file" : filename, 
248
+		"output_file" : tmp_filename,
249
+	}
250
+	info("Decrypting file %(input_file)s to %(output_file)s..." % args)
251
+	command = resolve_list(cfg.gpg_decrypt.split(" "), args)
252
+	code = gpg_command(command, cfg.gpg_passphrase)
253
+	if code == 0:
254
+		debug("Renaming %s to %s" % (tmp_filename, filename))
255
+		os.unlink(filename)
256
+		os.rename(tmp_filename, filename)
257
+	return (code)
258
+
213 259
 def run_configure(config_file):
214 260
 	cfg = Config()
215 261
 	options = [
... ...
@@ -325,6 +382,7 @@ if __name__ == '__main__':
325 325
 	optparser.add_option("-c", "--config", dest="config", metavar="FILE", help="Config file name. Defaults to %default")
326 326
 	optparser.add_option(      "--dump-config", dest="dump_config", action="store_true", help="Dump current configuration after parsing config files and command line options and exit.")
327 327
 
328
+	optparser.add_option("-e", "--encrypt", dest="encrypt", action="store_true", help="Encrypt files before uploading to S3.")
328 329
 	optparser.add_option("-f", "--force", dest="force", action="store_true", help="Force overwrite and other dangerous operations.")
329 330
 	optparser.add_option("-P", "--acl-public", dest="acl_public", action="store_true", help="Store objects with ACL allowing read by anyone.")
330 331
 
... ...
@@ -382,6 +440,11 @@ if __name__ == '__main__':
382 382
 			## Some Config() options are not settable from command line
383 383
 			pass
384 384
 
385
+	if cfg.encrypt and cfg.gpg_passphrase == "":
386
+		error("Encryption requested but no passphrase set in config file.")
387
+		error("Please re-run 's3cmd --configure' and supply it.")
388
+		sys.exit(1)
389
+
385 390
 	if options.dump_config:
386 391
 		cfg.dump_config(sys.stdout)
387 392
 		sys.exit(0)