Skip to content

Commit 6f7174c

Browse files
author
Piotr Kaleta
committed
First shot at GPG encryption/decryption of transferred messages.
1 parent ec13d93 commit 6f7174c

File tree

2 files changed

+113
-29
lines changed

2 files changed

+113
-29
lines changed

glacier.py

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,27 @@
2424
from __future__ import print_function
2525
from __future__ import unicode_literals
2626

27+
from functools import partial
2728
import argparse
2829
import calendar
2930
import errno
3031
import itertools
3132
import os
3233
import os.path
3334
import sys
35+
import tempfile
3436
import time
3537

36-
from boto.glacier.utils import DEFAULT_NUM_THREADS
3738
from boto.glacier.concurrent import ConcurrentUploader
39+
from boto.glacier.utils import DEFAULT_NUM_THREADS
3840
import boto.glacier
3941
import iso8601
4042
import sqlalchemy
4143
import sqlalchemy.ext.declarative
4244
import sqlalchemy.orm
4345

46+
from gpg import Encryptor
47+
4448

4549
# There is a lag between an archive being created and the archive
4650
# appearing on an inventory. Even if the inventory has an InventoryDate
@@ -54,6 +58,8 @@
5458

5559
DEFAULT_PART_SIZE = 4194304
5660

61+
DEFAULT_REGION = 'us-east-1'
62+
5763

5864
class ConsoleError(RuntimeError):
5965
def __init__(self, m):
@@ -469,9 +475,13 @@ def archive_list(self, args):
469475
if archive_list:
470476
print(*archive_list, sep="\n")
471477

472-
def archive_upload(self, args, multipart=False,
478+
def archive_upload(self,
479+
args,
480+
multipart=False,
481+
encryptor=None,
473482
part_size=DEFAULT_PART_SIZE,
474483
num_threads=DEFAULT_NUM_THREADS):
484+
475485
# XXX: "Leading whitespace in archive descriptions is removed."
476486
# XXX: "The description must be less than or equal to 1024 bytes. The
477487
# allowable characters are 7 bit ASCII without control codes,
@@ -486,39 +496,53 @@ def archive_upload(self, args, multipart=False,
486496
raise RuntimeError("Archive name not specified. Use --name.")
487497
name = os.path.basename(full_name)
488498

499+
if encryptor:
500+
tmpfile = tempfile.NamedTemporaryFile()
501+
encryptor.encrypt_file(args.file, tmpfile.name)
502+
filename = tmpfile.name
503+
else:
504+
filename = args.file
505+
489506
vault = self.connection.get_vault(args.vault)
490507

491508
if not multipart:
492509
archive_id = vault.create_archive_from_file(
493-
filename=args.file, description=name)
510+
filename=filename, description=name)
494511
else:
495512
uploader = ConcurrentUploader(self.connection.layer1,
496513
vault.name,
497514
part_size=part_size,
498515
num_threads=num_threads)
499-
archive_id = uploader.upload(args.file, description=name)
516+
archive_id = uploader.upload(filename, description=name)
500517

501518
self.cache.add_archive(args.vault, name, archive_id)
502519

503-
def multipart_archive_upload(self, args):
504-
return self.archive_upload(args, multipart=True)
520+
if encryptor:
521+
tmpfile.close()
522+
523+
def multipart_archive_upload(self, args, encryptor=None):
524+
return self.archive_upload(args, multipart=True, encryptor=encryptor)
505525

506526
@staticmethod
507-
def _write_archive_retrieval_job(f, job, multipart_size):
527+
def _write_archive_retrieval_job(f, job, multipart_size,
528+
encryptor=None):
508529
if job.archive_size > multipart_size:
509530
def fetch(start, end):
510-
byte_range = start, end-1
531+
byte_range = start, end - 1
511532
f.write(job.get_output(byte_range).read())
512533

513534
whole_parts = job.archive_size // multipart_size
514535
for first_byte in xrange(0, whole_parts * multipart_size,
515-
multipart_size):
536+
multipart_size):
516537
fetch(first_byte, first_byte + multipart_size)
517538
remainder = job.archive_size % multipart_size
518539
if remainder:
519540
fetch(job.archive_size - remainder, job.archive_size)
520541
else:
521-
f.write(job.get_output().read())
542+
data = job.get_output().read()
543+
if encryptor:
544+
data = encryptor.decrypt(data)
545+
f.write(data)
522546

523547
# Make sure that the file now exactly matches the downloaded archive,
524548
# even if the file existed before and was longer.
@@ -531,19 +555,21 @@ def fetch(start, end):
531555
raise
532556

533557
@classmethod
534-
def _archive_retrieve_completed(cls, args, job, name):
558+
def _archive_retrieve_completed(cls, args, job, name, encryptor=None):
535559
if args.output_filename == '-':
536560
cls._write_archive_retrieval_job(
537-
sys.stdout, job, args.multipart_size)
561+
sys.stdout, job, args.multipart_size,
562+
encryptor=encryptor)
538563
else:
539564
if args.output_filename:
540565
filename = args.output_filename
541566
else:
542567
filename = os.path.basename(name)
543568
with open(filename, 'wb') as f:
544-
cls._write_archive_retrieval_job(f, job, args.multipart_size)
569+
cls._write_archive_retrieval_job(f, job, args.multipart_size,
570+
encryptor=encryptor)
545571

546-
def archive_retrieve_one(self, args, name):
572+
def archive_retrieve_one(self, args, name, encryptor=None):
547573
try:
548574
archive_id = self.cache.get_archive_id(args.vault, name)
549575
except KeyError:
@@ -554,30 +580,35 @@ def archive_retrieve_one(self, args, name):
554580

555581
complete_job = find_complete_job(retrieval_jobs)
556582
if complete_job:
557-
self._archive_retrieve_completed(args, complete_job, name)
583+
self._archive_retrieve_completed(args, complete_job, name,
584+
encryptor=encryptor)
558585
elif has_pending_job(retrieval_jobs):
559586
if args.wait:
560587
complete_job = wait_until_job_completed(retrieval_jobs)
561-
self._archive_retrieve_completed(args, complete_job, name)
588+
self._archive_retrieve_completed(args, complete_job, name,
589+
encryptor=encryptor)
562590
else:
563-
raise RetryConsoleError('job still pending for archive %r' % name)
591+
raise RetryConsoleError('job still pending for archive %r'
592+
% name)
564593
else:
565594
# create an archive retrieval job
566595
job = vault.retrieve_archive(archive_id)
567596
if args.wait:
568597
wait_until_job_completed([job])
569598
self._archive_retrieve_completed(args, job, name)
570599
else:
571-
raise RetryConsoleError('queued retrieval job for archive %r' % name)
600+
raise RetryConsoleError('queued retrieval job for archive %r'
601+
% name)
572602

573-
def archive_retrieve(self, args):
603+
def archive_retrieve(self, args, encryptor=None):
574604
if len(args.names) > 1 and args.output_filename:
575-
raise ConsoleError('cannot specify output filename with multi-archive retrieval')
605+
raise ConsoleError("cannot specify output filename with "
606+
"multi-archive retrieval")
576607
success_list = []
577608
retry_list = []
578609
for name in args.names:
579610
try:
580-
self.archive_retrieve_one(args, name)
611+
self.archive_retrieve_one(args, name, encryptor=encryptor)
581612
except RetryConsoleError, e:
582613
retry_list.append(e.message)
583614
else:
@@ -642,7 +673,7 @@ def too_old(last_seen):
642673

643674
def main(self):
644675
parser = argparse.ArgumentParser()
645-
parser.add_argument('--region', default='us-east-1')
676+
parser.add_argument('--region', default=DEFAULT_REGION)
646677
subparsers = parser.add_subparsers()
647678
vault_subparser = subparsers.add_parser('vault').add_subparsers()
648679
vault_subparser.add_parser('list').set_defaults(func=self.vault_list)
@@ -661,18 +692,23 @@ def main(self):
661692
archive_list_subparser.set_defaults(func=self.archive_list)
662693
archive_list_subparser.add_argument('vault')
663694

695+
encryptor = Encryptor()
696+
664697
# Upload command
698+
archive_upload_func = partial(self.archive_upload, encryptor=encryptor)
665699
archive_upload_subparser = archive_subparser.add_parser('upload')
666-
archive_upload_subparser.set_defaults(func=self.archive_upload)
700+
archive_upload_subparser.set_defaults(func=archive_upload_func)
667701
archive_upload_subparser.add_argument('vault')
668702
archive_upload_subparser.add_argument('file')
669703
archive_upload_subparser.add_argument('--name')
670704

671705
# Multipart upload command
706+
multipart_archive_upload_func = partial(
707+
self.archive_upload, encryptor=encryptor)
672708
archive_multipart_upload_subparser = archive_subparser.add_parser(
673709
'multipart_upload')
674710
archive_multipart_upload_subparser.set_defaults(
675-
func=self.multipart_archive_upload)
711+
func=multipart_archive_upload_func)
676712
archive_multipart_upload_subparser.add_argument('vault')
677713
archive_multipart_upload_subparser.add_argument('file')
678714
archive_multipart_upload_subparser.add_argument('--name')
@@ -687,32 +723,40 @@ def main(self):
687723
dest="num_threads"
688724
)
689725

726+
# Retrieve command
727+
archive_retrieve_func = partial(
728+
self.archive_retrieve, encryptor=encryptor)
690729
archive_retrieve_subparser = archive_subparser.add_parser('retrieve')
691-
archive_retrieve_subparser.set_defaults(func=self.archive_retrieve)
730+
archive_retrieve_subparser.set_defaults(func=archive_retrieve_func)
692731
archive_retrieve_subparser.add_argument('vault')
693732
archive_retrieve_subparser.add_argument('names', nargs='+',
694733
metavar='name')
695734
archive_retrieve_subparser.add_argument('--multipart-size', type=int,
696-
default=(8*1024*1024))
735+
default=(8 * 1024 * 1024))
697736
archive_retrieve_subparser.add_argument('-o', dest='output_filename',
698737
metavar='OUTPUT_FILENAME')
699738
archive_retrieve_subparser.add_argument('--wait', action='store_true')
739+
740+
# Delete command
700741
archive_delete_subparser = archive_subparser.add_parser('delete')
701742
archive_delete_subparser.set_defaults(func=self.archive_delete)
702743
archive_delete_subparser.add_argument('vault')
703744
archive_delete_subparser.add_argument('name')
745+
746+
# Checkpresent command
704747
archive_checkpresent_subparser = archive_subparser.add_parser(
705-
'checkpresent')
748+
'checkpresent')
706749
archive_checkpresent_subparser.set_defaults(
707-
func=self.archive_checkpresent)
750+
func=self.archive_checkpresent)
708751
archive_checkpresent_subparser.add_argument('vault')
709752
archive_checkpresent_subparser.add_argument('name')
710753
archive_checkpresent_subparser.add_argument('--wait',
711754
action='store_true')
712755
archive_checkpresent_subparser.add_argument('--quiet',
713756
action='store_true')
714757
archive_checkpresent_subparser.add_argument(
715-
'--max-age', type=int, default=80, dest='max_age_hours')
758+
'--max-age', type=int, default=80, dest='max_age_hours')
759+
716760
job_subparser = subparsers.add_parser('job').add_subparsers()
717761
job_subparser.add_parser('list').set_defaults(func=self.job_list)
718762
args = parser.parse_args()

gpg.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import gnupg
2+
import os
3+
4+
5+
GNUPG_DIRECTORY = os.path.join(os.path.abspath(os.path.dirname(__file__)),
6+
"gnupg")
7+
GNUPG_KEY_TYPE = "RSA"
8+
GNUPG_KEY_LENGTH = 2048
9+
10+
11+
class Encryptor(object):
12+
13+
def __init__(self):
14+
self.gpg = gnupg.GPG(gnupghome=GNUPG_DIRECTORY)
15+
self.gpg.encoding = "utf-8"
16+
if self.gpg.list_keys() == []:
17+
self.generate_keypair()
18+
19+
def generate_keypair(self):
20+
input_data = self.gpg.gen_key_input(key_type=GNUPG_KEY_TYPE,
21+
key_length=GNUPG_KEY_LENGTH)
22+
key = self.gpg.gen_key(input_data)
23+
24+
return key
25+
26+
def export_keypair(self, key):
27+
public_key = self.gpg.export_keys(key)
28+
private_key = self.gpg.export_keys(key, True)
29+
30+
return public_key, private_key
31+
32+
def encrypt_file(self, input_filename, output_filename):
33+
fingerprint = self.gpg.list_keys()[0]["fingerprint"]
34+
35+
with open(input_filename) as input_file:
36+
self.gpg.encrypt_file(input_file, fingerprint,
37+
output=output_filename)
38+
39+
def decrypt(self, data):
40+
return self.gpg.decrypt(data)

0 commit comments

Comments
 (0)