Ticket #684: new2-684.diff

File new2-684.diff, 19.2 KB (added by warner, at 2009-05-16T23:29:10Z)

new version of the patch, with minor stylistic changes applied

  • docs/frontends/webapi.txt

    diff --git a/docs/frontends/webapi.txt b/docs/frontends/webapi.txt
    index 6abbd42..092a829 100644
    a b PUT /uri 
    340340 mutable file, and return its write-cap in the HTTP respose. The default is
    341341 to create an immutable file, returning the read-cap as a response.
    342342
     343 To use a randomly-generated key as the encryption key for the file, add
     344 the query argument "key=random".
     345
     346 To specify a key, add a query argument of the form "key=encoding-value",
     347 where 'encoding' is one of 'hex', 'base16', or 'base32' and value is the
     348 128-bit key encoded with the specified encoding.  For example, the following
     349 are all equivalent, and permitted:
     350
     351          key=hex-B6F39C58C25A501B6FDF4AF94E07BB5D
     352          key=base16-B6F39C58C25A501B6FDF4AF94E07BB5D
     353          key=base32-w3zzywgcljibw367jl4u4b53lu
     354
     355 Be VERY careful that you know what you're doing if you use this feature.
     356 Choosing bad keys could compromise the security of your files.  Also, make
     357 sure that every file is encrypted with a unique key because uploading
     358 different files with the same key will result in a storage index collision.
     359 Even uploading the same file encoded with different FEC parameters will cause
     360 a collision if you use the same key.  It's a good idea to hash the FEC
     361 parameters (k, N, segsize) into your key to be sure that doesn't happen.
     362
     363 Normally, you should omit the 'key' argument and let Tahoe construct a
     364 content hash key (CHK) which is secure, unique and will make your uploads
     365 idempotent.
     366
    343367=== Creating A New Directory ===
    344368
    345369POST /uri?t=mkdir
    POST /uri?t=upload 
    747771 the upload results page. The default is to create an immutable file,
    748772 returning the upload results page as a response.
    749773
     774 To use a randomly-generated key as the encryption key for the file, add
     775 the argument "key=random".
     776
     777 To specify a key, add an argument of the form "key=encoding-value", where
     778 'encoding' is one of 'hex', 'base16', or 'base32' and value is the 128-bit
     779 key encoded with the specified encoding.  For example, the following are all
     780 equivalent, and permitted:
     781
     782          key=hex-B6F39C58C25A501B6FDF4AF94E07BB5D
     783          key=base16-B6F39C58C25A501B6FDF4AF94E07BB5D
     784          key=base32-w3zzywgcljibw367jl4u4b53lu
     785
     786 Be VERY careful that you know what you're doing if you use this feature.
     787 Choosing bad keys could compromise the security of your files.  Also, make
     788 sure that every file is encrypted with a unique key because uploading
     789 different files with the same key will result in a storage index collision.
     790 Even uploading the same file encoded with different FEC parameters will cause
     791 a collision if you use the same key.  It's a good idea to hash the FEC
     792 parameters (k, N, segsize) into your key to be sure that doesn't happen.
     793
     794 Normally, you should omit the 'key' argument and let Tahoe construct a
     795 content hash key (CHK) which is secure, unique and will make your uploads
     796 idempotent.
    750797
    751798POST /uri/$DIRCAP/[SUBDIRS../]?t=upload
    752799
  • src/allmydata/immutable/upload.py

    diff --git a/src/allmydata/immutable/upload.py b/src/allmydata/immutable/upload.py
    index ec3619f..fbee0ae 100644
    a b class BaseUploadable: 
    11001100class FileHandle(BaseUploadable):
    11011101    implements(IUploadable)
    11021102
    1103     def __init__(self, filehandle, convergence):
     1103    def __init__(self, filehandle, convergence, key=None):
    11041104        """
    11051105        Upload the data from the filehandle.  If convergence is None then a
    11061106        random encryption key will be used, else the plaintext will be hashed,
    class FileHandle(BaseUploadable): 
    11121112        self._key = None
    11131113        self.convergence = convergence
    11141114        self._size = None
     1115        self.chosen_key = key
     1116        if key:
     1117            assert convergence is None # Can't specify both key and convergence
     1118            assert isinstance(key, str) and len(key) is 16
    11151119
    11161120    def _get_encryption_key_convergent(self):
    11171121        if self._key is not None:
    class FileHandle(BaseUploadable): 
    11561160    def get_encryption_key(self):
    11571161        if self.convergence is not None:
    11581162            return self._get_encryption_key_convergent()
     1163        elif self.chosen_key is not None:
     1164            return defer.succeed(self.chosen_key)
    11591165        else:
    11601166            return self._get_encryption_key_random()
    11611167
    class FileHandle(BaseUploadable): 
    11761182        pass
    11771183
    11781184class FileName(FileHandle):
    1179     def __init__(self, filename, convergence):
     1185    def __init__(self, filename, convergence, key=None):
    11801186        """
    11811187        Upload the data from the filename.  If convergence is None then a
    11821188        random encryption key will be used, else the plaintext will be hashed,
    class FileName(FileHandle): 
    11841190        "convergence" argument to form the encryption key.
    11851191        """
    11861192        assert convergence is None or isinstance(convergence, str), (convergence, type(convergence))
    1187         FileHandle.__init__(self, open(filename, "rb"), convergence=convergence)
     1193        FileHandle.__init__(self, open(filename, "rb"), convergence=convergence, key=key)
    11881194    def close(self):
    11891195        FileHandle.close(self)
    11901196        self._filehandle.close()
    11911197
    11921198class Data(FileHandle):
    1193     def __init__(self, data, convergence):
     1199    def __init__(self, data, convergence, key=None):
    11941200        """
    11951201        Upload the data from the data argument.  If convergence is None then a
    11961202        random encryption key will be used, else the plaintext will be hashed,
    class Data(FileHandle): 
    11981204        "convergence" argument to form the encryption key.
    11991205        """
    12001206        assert convergence is None or isinstance(convergence, str), (convergence, type(convergence))
    1201         FileHandle.__init__(self, StringIO(data), convergence=convergence)
     1207        FileHandle.__init__(self, StringIO(data), convergence=convergence, key=key)
    12021208
    12031209class Uploader(service.MultiService, log.PrefixingLogMixin):
    12041210    """I am a service that allows file uploading. I am a service-child of the
  • src/allmydata/test/test_system.py

    diff --git a/src/allmydata/test/test_system.py b/src/allmydata/test/test_system.py
    index f66c4e4..b6bc200 100644
    a b  
    1 from base64 import b32encode
     1from base64 import b32encode, b16encode
    22import os, sys, time, re, simplejson
    33from cStringIO import StringIO
    44from zope.interface import implements
    class SystemTest(SystemTestMixin, unittest.TestCase): 
    11831183        d.addCallback(lambda res: self.GET(public + "/subdir3/new.txt"))
    11841184        d.addCallback(self.failUnlessEqual, "NEWER contents")
    11851185
     1186        # test unlinked PUT with specified key
     1187        key = 'd'*16
     1188        d.addCallback(lambda res: self.PUT("uri?key=hex-" + b16encode(key),
     1189                                           "data" * 100))
     1190        def _check_specified_key_uri(res):
     1191            u = uri.from_string_filenode(res)
     1192            self.failUnlessEqual(u.key, key)
     1193            return res
     1194        d.addCallback(_check_specified_key_uri)
     1195
     1196        # test unlinked PUT with content hash key
     1197        d.addCallback(lambda res: self.PUT("uri", "data" * 100))
     1198        def _check_CHK_key_uri(res):
     1199            u = uri.from_string_filenode(res)
     1200            self.failIfEqual(u.key, key)
     1201            return res
     1202        d.addCallback(_check_CHK_key_uri)
     1203
    11861204        # test unlinked POST
    11871205        d.addCallback(lambda res: self.POST("uri", t="upload",
    11881206                                            file=("new.txt", "data" * 10000)))
    class SystemTest(SystemTestMixin, unittest.TestCase): 
    12871305        d.addCallback(lambda res: self.GET("statistics?t=json"))
    12881306        def _got_stats_json(res):
    12891307            data = simplejson.loads(res)
    1290             self.failUnlessEqual(data["counters"]["uploader.files_uploaded"], 5)
     1308            self.failUnlessEqual(data["counters"]["uploader.files_uploaded"], 7)
    12911309            self.failUnlessEqual(data["stats"]["chk_upload_helper.upload_need_upload"], 1)
    12921310        d.addCallback(_got_stats_json)
    12931311
  • src/allmydata/test/test_upload.py

    diff --git a/src/allmydata/test/test_upload.py b/src/allmydata/test/test_upload.py
    index a0bc798..5cda969 100644
    a b class Uploadable(unittest.TestCase): 
    3030        self.failUnlessEqual(s, expected)
    3131
    3232    def test_filehandle_random_key(self):
    33         return self._test_filehandle(convergence=None)
     33        return self._test_filehandle(convergence=None, key=None)
     34
     35    def test_filehandle_specified_key(self):
     36        return self._test_filehandle(convergence=None, key='a'*16)
    3437
    3538    def test_filehandle_convergent_encryption(self):
    36         return self._test_filehandle(convergence="some convergence string")
     39        return self._test_filehandle(convergence="some convergence string", key=None)
    3740
    38     def _test_filehandle(self, convergence):
     41    def _test_filehandle(self, convergence, key):
    3942        s = StringIO("a"*41)
    40         u = upload.FileHandle(s, convergence=convergence)
     43        u = upload.FileHandle(s, convergence=convergence, key=key)
    4144        d = u.get_size()
    4245        d.addCallback(self.failUnlessEqual, 41)
    4346        d.addCallback(lambda res: u.read(1))
    SIZE_ZERO = 0 
    217220SIZE_SMALL = 16
    218221SIZE_LARGE = len(DATA)
    219222
    220 def upload_data(uploader, data):
    221     u = upload.Data(data, convergence=None)
     223def upload_data(uploader, data, key=None):
     224    u = upload.Data(data, convergence=None, key=key)
    222225    return uploader.upload(u)
    223226def upload_filename(uploader, filename):
    224227    u = upload.FileName(filename, convergence=None)
    class GoodServer(unittest.TestCase, ShouldFailMixin): 
    256259        self.failUnlessEqual(len(u.key), 16)
    257260        self.failUnlessEqual(u.size, size)
    258261
     262    def _check_provided_key(self, newuri, size):
     263        self._check_large(newuri, size)
     264        u = IFileURI(newuri)
     265        self.failUnlessEqual(u.key, 'b'*16)
     266
    259267    def get_data(self, size):
    260268        return DATA[:size]
    261269
    class GoodServer(unittest.TestCase, ShouldFailMixin): 
    301309        d.addCallback(self._check_large, SIZE_LARGE)
    302310        return d
    303311
     312    def test_specified_key(self):
     313        data = self.get_data(SIZE_LARGE)
     314        d = upload_data(self.u, data, 'b'*16)
     315        d.addCallback(extract_uri)
     316        d.addCallback(self._check_provided_key, SIZE_LARGE)
     317        return d
     318
    304319    def test_data_large_odd_segments(self):
    305320        data = self.get_data(SIZE_LARGE)
    306321        segsize = int(SIZE_LARGE / 2.5)
    class StorageIndex(unittest.TestCase): 
    566581        eu = upload.EncryptAnUploadable(u)
    567582        d1salt1a = eu.get_storage_index()
    568583
     584        # and if we specify a custom encryption key it should be different again
     585        key = '\x01' * 16
     586        u = upload.Data(DATA, convergence=None, key=key)
     587        eu = upload.EncryptAnUploadable(u)
     588        k1 = eu.get_storage_index()
     589
    569590        # and if we change the encoding parameters, it should be different (from the same convergence string with different encoding parameters)
    570591        u = upload.Data(DATA, convergence="")
    571592        u.encoding_param_k = u.default_encoding_param_k + 1
    class StorageIndex(unittest.TestCase): 
    581602        eu = upload.EncryptAnUploadable(u)
    582603        d4 = eu.get_storage_index()
    583604
    584         d = DeferredListShouldSucceed([d1,d1a,d1salt1,d1salt2,d1salt1a,d2,d3,d4])
     605        d = DeferredListShouldSucceed([d1,d1a,d1salt1,d1salt2,d1salt1a,k1,d2,d3,d4])
    585606        def _done(res):
    586             si1, si1a, si1salt1, si1salt2, si1salt1a, si2, si3, si4 = res
     607            si1, si1a, si1salt1, si1salt2, si1salt1a, sik1, si2, si3, si4 = res
    587608            self.failUnlessEqual(si1, si1a)
    588609            self.failIfEqual(si1, si2)
    589610            self.failIfEqual(si1, si3)
    class StorageIndex(unittest.TestCase): 
    593614            self.failIfEqual(si1salt1, si1salt2)
    594615            self.failIfEqual(si1salt2, si1)
    595616            self.failUnlessEqual(si1salt1, si1salt1a)
     617            self.failIfEqual(sik1, si1)
     618            self.failIfEqual(sik1, si1a)
    596619        d.addCallback(_done)
    597620        return d
    598621
  • src/allmydata/test/test_web.py

    diff --git a/src/allmydata/test/test_web.py b/src/allmydata/test/test_web.py
    index ad3fa40..eacc189 100644
    a b  
    1 import os.path, re, urllib
     1import os.path, re, urllib, base64
    22import simplejson
    33from StringIO import StringIO
    44from twisted.application import service
    class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): 
    717717                                                      self.NEWFILE_CONTENTS))
    718718        return d
    719719
     720    def PUT_URI_specified_key(self, key, encoding, encoder, data):
     721        return self.PUT("/uri?key=" + encoding + '-' + encoder(key), data)
     722
     723    def test_PUT_URI_random_key(self):
     724        d = self.PUT("/uri?key=random", self.NEWFILE_CONTENTS)
     725        return d
     726
     727    def test_PUT_URI_specified_key_hex(self):
     728        return self.PUT_URI_specified_key('0'*16, 'hex', base64.b16encode,
     729                                          self.NEWFILE_CONTENTS)
     730
     731    def test_PUT_URI_specified_key_base16(self):
     732        return self.PUT_URI_specified_key('1'*16, 'base16', base64.b16encode,
     733                                          self.NEWFILE_CONTENTS)
     734
     735    def test_PUT_URI_specified_key_base32(self):
     736        return self.PUT_URI_specified_key('2'*16, 'base32', base32.b2a,
     737                                          self.NEWFILE_CONTENTS)
     738
     739    def test_PUT_URI_specified_key_invalid_format(self):
     740        key_str = base32.b2a('3'*16)
     741        d = self.PUT("/uri?key=" + key_str, self.NEWFILE_CONTENTS)
     742        return self.failUnlessFailure(d, error.Error)
     743
     744    def test_PUT_URI_specified_key_incorrect_encoding(self):
     745        d = self.PUT_URI_specified_key('4'*16, 'hex', base32.b2a,
     746                                       self.NEWFILE_CONTENTS)
     747        return self.failUnlessFailure(d, error.Error)
     748
     749    def test_PUT_URI_specified_key_incorrect_length(self):
     750        d = self.PUT_URI_specified_key('5'*16, 'base32', base64.b16encode,
     751                                       self.NEWFILE_CONTENTS)
     752        return self.failUnlessFailure(d, error.Error)
     753
     754    def test_PUT_NEWFILEURL_specified_key(self):
     755        key = '6' * 16
     756        key_str = 'base32-'+base32.b2a(key)
     757        d = self.PUT(self.public_url + "/foo/new.txt?key=" + key_str,
     758                     self.NEWFILE_CONTENTS)
     759        # TODO: we lose the response code, so we can't check this
     760        #self.failUnlessEqual(responsecode, 201)
     761        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt")
     762        d.addCallback(lambda res:
     763                      self.failUnlessChildContentsAre(self._foo_node, u"new.txt",
     764                                                      self.NEWFILE_CONTENTS))
     765        return d
     766
    720767    def test_PUT_NEWFILEURL_not_mutable(self):
    721768        d = self.PUT(self.public_url + "/foo/new.txt?mutable=false",
    722769                     self.NEWFILE_CONTENTS)
    class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): 
    12501297                                                      self.NEWFILE_CONTENTS))
    12511298        return d
    12521299
     1300    def test_POST_upload_specified_key(self):
     1301        key = '\x27' * 16
     1302        key_str = 'base32-' + base32.b2a(key)
     1303        d = self.POST(self.public_url + "/foo", t="upload",
     1304                      file=("new.txt", self.NEWFILE_CONTENTS),
     1305                      key=key_str)
     1306        fn = self._foo_node
     1307        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
     1308        d.addCallback(lambda res:
     1309                      self.failUnlessChildContentsAre(fn, u"new.txt",
     1310                                                      self.NEWFILE_CONTENTS))
     1311        return d
     1312       
     1313
    12531314    def test_POST_upload_unicode(self):
    12541315        filename = u"n\u00e9wer.txt" # n e-acute w e r . t x t
    12551316        d = self.POST(self.public_url + "/foo", t="upload",
  • src/allmydata/web/common.py

    diff --git a/src/allmydata/web/common.py b/src/allmydata/web/common.py
    index 5c33758..a64fe6a 100644
    a b from nevow.util import resource_filename 
    77from allmydata.interfaces import ExistingChildError, NoSuchChildError, \
    88     FileTooLargeError, NotEnoughSharesError
    99from allmydata.mutable.common import UnrecoverableFileError
    10 from allmydata.util import abbreviate # TODO: consolidate
     10from allmydata.util import abbreviate, base32 # TODO: consolidate
     11import base64
    1112
    1213class IOpHandleTable(Interface):
    1314    pass
    def get_arg(ctx_or_req, argname, default=None, multiple=False): 
    4647        return results[0]
    4748    return default
    4849
     50class RandomKey:
     51    """Marker to indicate that the user wants a randomly-generated key"""
     52
     53def get_key_arg(ctx_or_req):
     54    """
     55    Extract the 'key' argument from the query args.  If not found,
     56    return None.  If the argument is "random", return RandomKey.
     57    Otherwise, the argument should be of the form "encoding-value",
     58    where encoding is one of 'hex', 'base16', or 'base32'.  Parse it
     59    and return the value as a binary string, which must be 16 bytes in
     60    length.
     61    """
     62    req = IRequest(ctx_or_req)
     63    key_str = get_arg(req, "key", "").strip()
     64    if key_str == "":
     65        return None
     66    elif key_str == "random":
     67        return RandomKey
     68
     69    try:
     70        encoding, value = key_str.split('-', 1)
     71        if encoding == 'base32':
     72            key = base32.a2b(value)
     73        elif encoding == 'hex' or encoding == 'base16':
     74            key = base64.b16decode(value)
     75        else:
     76            raise WebError('Unknown key format ' + encoding)
     77    except:
     78        raise WebError('Invalid key format')
     79
     80    if len(key) != 16:
     81        raise WebError("Key must be 16 bytes in length")
     82
     83    return key
     84
    4985def abbreviate_time(data):
    5086    # 1.23s, 790ms, 132us
    5187    if data is None:
  • src/allmydata/web/unlinked.py

    diff --git a/src/allmydata/web/unlinked.py b/src/allmydata/web/unlinked.py
    index d3ef96f..686d5d9 100644
    a b from twisted.web import http 
    44from twisted.internet import defer
    55from nevow import rend, url, tags as T
    66from allmydata.immutable.upload import FileHandle
    7 from allmydata.web.common import getxmlfile, get_arg, boolean_of_arg
     7from allmydata.web.common import getxmlfile, get_arg, boolean_of_arg, \
     8    get_key_arg, RandomKey
    89from allmydata.web import status
    910
    1011def PUTUnlinkedCHK(req, client):
    1112    # "PUT /uri", to create an unlinked file.
    12     uploadable = FileHandle(req.content, client.convergence)
     13    key = get_key_arg(req)
     14    if key is not None:
     15        convergence = None
     16        if key is RandomKey:
     17            key = None
     18    else:
     19        convergence = client.convergence
     20
     21    uploadable = FileHandle(req.content, convergence=convergence, key=key)
    1322    d = client.upload(uploadable)
    1423    d.addCallback(lambda results: results.uri)
    1524    # that fires with the URI of the new file
    def PUTUnlinkedCreateDirectory(req, client): 
    3342
    3443def POSTUnlinkedCHK(req, client):
    3544    fileobj = req.fields["file"].file
    36     uploadable = FileHandle(fileobj, client.convergence)
     45    key = get_key_arg(req)
     46    if key is not None:
     47        convergence = None
     48        if key is RandomKey:
     49            key = None
     50    else:
     51        convergence = client.convergence
     52
     53    uploadable = FileHandle(fileobj, convergence, key)
    3754    d = client.upload(uploadable)
    3855    when_done = get_arg(req, "when_done", None)
    3956    if when_done: