* 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
... | ... |
@@ -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) |