Ticket #684: new2-684.diff
File new2-684.diff, 19.2 KB (added by warner, at 2009-05-16T23:29:10Z) |
---|
-
docs/frontends/webapi.txt
diff --git a/docs/frontends/webapi.txt b/docs/frontends/webapi.txt index 6abbd42..092a829 100644
a b PUT /uri 340 340 mutable file, and return its write-cap in the HTTP respose. The default is 341 341 to create an immutable file, returning the read-cap as a response. 342 342 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 343 367 === Creating A New Directory === 344 368 345 369 POST /uri?t=mkdir … … POST /uri?t=upload 747 771 the upload results page. The default is to create an immutable file, 748 772 returning the upload results page as a response. 749 773 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. 750 797 751 798 POST /uri/$DIRCAP/[SUBDIRS../]?t=upload 752 799 -
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: 1100 1100 class FileHandle(BaseUploadable): 1101 1101 implements(IUploadable) 1102 1102 1103 def __init__(self, filehandle, convergence ):1103 def __init__(self, filehandle, convergence, key=None): 1104 1104 """ 1105 1105 Upload the data from the filehandle. If convergence is None then a 1106 1106 random encryption key will be used, else the plaintext will be hashed, … … class FileHandle(BaseUploadable): 1112 1112 self._key = None 1113 1113 self.convergence = convergence 1114 1114 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 1115 1119 1116 1120 def _get_encryption_key_convergent(self): 1117 1121 if self._key is not None: … … class FileHandle(BaseUploadable): 1156 1160 def get_encryption_key(self): 1157 1161 if self.convergence is not None: 1158 1162 return self._get_encryption_key_convergent() 1163 elif self.chosen_key is not None: 1164 return defer.succeed(self.chosen_key) 1159 1165 else: 1160 1166 return self._get_encryption_key_random() 1161 1167 … … class FileHandle(BaseUploadable): 1176 1182 pass 1177 1183 1178 1184 class FileName(FileHandle): 1179 def __init__(self, filename, convergence ):1185 def __init__(self, filename, convergence, key=None): 1180 1186 """ 1181 1187 Upload the data from the filename. If convergence is None then a 1182 1188 random encryption key will be used, else the plaintext will be hashed, … … class FileName(FileHandle): 1184 1190 "convergence" argument to form the encryption key. 1185 1191 """ 1186 1192 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) 1188 1194 def close(self): 1189 1195 FileHandle.close(self) 1190 1196 self._filehandle.close() 1191 1197 1192 1198 class Data(FileHandle): 1193 def __init__(self, data, convergence ):1199 def __init__(self, data, convergence, key=None): 1194 1200 """ 1195 1201 Upload the data from the data argument. If convergence is None then a 1196 1202 random encryption key will be used, else the plaintext will be hashed, … … class Data(FileHandle): 1198 1204 "convergence" argument to form the encryption key. 1199 1205 """ 1200 1206 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) 1202 1208 1203 1209 class Uploader(service.MultiService, log.PrefixingLogMixin): 1204 1210 """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 1 from base64 import b32encode, b16encode 2 2 import os, sys, time, re, simplejson 3 3 from cStringIO import StringIO 4 4 from zope.interface import implements … … class SystemTest(SystemTestMixin, unittest.TestCase): 1183 1183 d.addCallback(lambda res: self.GET(public + "/subdir3/new.txt")) 1184 1184 d.addCallback(self.failUnlessEqual, "NEWER contents") 1185 1185 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 1186 1204 # test unlinked POST 1187 1205 d.addCallback(lambda res: self.POST("uri", t="upload", 1188 1206 file=("new.txt", "data" * 10000))) … … class SystemTest(SystemTestMixin, unittest.TestCase): 1287 1305 d.addCallback(lambda res: self.GET("statistics?t=json")) 1288 1306 def _got_stats_json(res): 1289 1307 data = simplejson.loads(res) 1290 self.failUnlessEqual(data["counters"]["uploader.files_uploaded"], 5)1308 self.failUnlessEqual(data["counters"]["uploader.files_uploaded"], 7) 1291 1309 self.failUnlessEqual(data["stats"]["chk_upload_helper.upload_need_upload"], 1) 1292 1310 d.addCallback(_got_stats_json) 1293 1311 -
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): 30 30 self.failUnlessEqual(s, expected) 31 31 32 32 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) 34 37 35 38 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) 37 40 38 def _test_filehandle(self, convergence ):41 def _test_filehandle(self, convergence, key): 39 42 s = StringIO("a"*41) 40 u = upload.FileHandle(s, convergence=convergence )43 u = upload.FileHandle(s, convergence=convergence, key=key) 41 44 d = u.get_size() 42 45 d.addCallback(self.failUnlessEqual, 41) 43 46 d.addCallback(lambda res: u.read(1)) … … SIZE_ZERO = 0 217 220 SIZE_SMALL = 16 218 221 SIZE_LARGE = len(DATA) 219 222 220 def upload_data(uploader, data ):221 u = upload.Data(data, convergence=None )223 def upload_data(uploader, data, key=None): 224 u = upload.Data(data, convergence=None, key=key) 222 225 return uploader.upload(u) 223 226 def upload_filename(uploader, filename): 224 227 u = upload.FileName(filename, convergence=None) … … class GoodServer(unittest.TestCase, ShouldFailMixin): 256 259 self.failUnlessEqual(len(u.key), 16) 257 260 self.failUnlessEqual(u.size, size) 258 261 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 259 267 def get_data(self, size): 260 268 return DATA[:size] 261 269 … … class GoodServer(unittest.TestCase, ShouldFailMixin): 301 309 d.addCallback(self._check_large, SIZE_LARGE) 302 310 return d 303 311 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 304 319 def test_data_large_odd_segments(self): 305 320 data = self.get_data(SIZE_LARGE) 306 321 segsize = int(SIZE_LARGE / 2.5) … … class StorageIndex(unittest.TestCase): 566 581 eu = upload.EncryptAnUploadable(u) 567 582 d1salt1a = eu.get_storage_index() 568 583 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 569 590 # and if we change the encoding parameters, it should be different (from the same convergence string with different encoding parameters) 570 591 u = upload.Data(DATA, convergence="") 571 592 u.encoding_param_k = u.default_encoding_param_k + 1 … … class StorageIndex(unittest.TestCase): 581 602 eu = upload.EncryptAnUploadable(u) 582 603 d4 = eu.get_storage_index() 583 604 584 d = DeferredListShouldSucceed([d1,d1a,d1salt1,d1salt2,d1salt1a, d2,d3,d4])605 d = DeferredListShouldSucceed([d1,d1a,d1salt1,d1salt2,d1salt1a,k1,d2,d3,d4]) 585 606 def _done(res): 586 si1, si1a, si1salt1, si1salt2, si1salt1a, si 2, si3, si4 = res607 si1, si1a, si1salt1, si1salt2, si1salt1a, sik1, si2, si3, si4 = res 587 608 self.failUnlessEqual(si1, si1a) 588 609 self.failIfEqual(si1, si2) 589 610 self.failIfEqual(si1, si3) … … class StorageIndex(unittest.TestCase): 593 614 self.failIfEqual(si1salt1, si1salt2) 594 615 self.failIfEqual(si1salt2, si1) 595 616 self.failUnlessEqual(si1salt1, si1salt1a) 617 self.failIfEqual(sik1, si1) 618 self.failIfEqual(sik1, si1a) 596 619 d.addCallback(_done) 597 620 return d 598 621 -
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 1 import os.path, re, urllib, base64 2 2 import simplejson 3 3 from StringIO import StringIO 4 4 from twisted.application import service … … class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): 717 717 self.NEWFILE_CONTENTS)) 718 718 return d 719 719 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 720 767 def test_PUT_NEWFILEURL_not_mutable(self): 721 768 d = self.PUT(self.public_url + "/foo/new.txt?mutable=false", 722 769 self.NEWFILE_CONTENTS) … … class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): 1250 1297 self.NEWFILE_CONTENTS)) 1251 1298 return d 1252 1299 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 1253 1314 def test_POST_upload_unicode(self): 1254 1315 filename = u"n\u00e9wer.txt" # n e-acute w e r . t x t 1255 1316 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 7 7 from allmydata.interfaces import ExistingChildError, NoSuchChildError, \ 8 8 FileTooLargeError, NotEnoughSharesError 9 9 from allmydata.mutable.common import UnrecoverableFileError 10 from allmydata.util import abbreviate # TODO: consolidate 10 from allmydata.util import abbreviate, base32 # TODO: consolidate 11 import base64 11 12 12 13 class IOpHandleTable(Interface): 13 14 pass … … def get_arg(ctx_or_req, argname, default=None, multiple=False): 46 47 return results[0] 47 48 return default 48 49 50 class RandomKey: 51 """Marker to indicate that the user wants a randomly-generated key""" 52 53 def 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 49 85 def abbreviate_time(data): 50 86 # 1.23s, 790ms, 132us 51 87 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 4 4 from twisted.internet import defer 5 5 from nevow import rend, url, tags as T 6 6 from allmydata.immutable.upload import FileHandle 7 from allmydata.web.common import getxmlfile, get_arg, boolean_of_arg 7 from allmydata.web.common import getxmlfile, get_arg, boolean_of_arg, \ 8 get_key_arg, RandomKey 8 9 from allmydata.web import status 9 10 10 11 def PUTUnlinkedCHK(req, client): 11 12 # "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) 13 22 d = client.upload(uploadable) 14 23 d.addCallback(lambda results: results.uri) 15 24 # that fires with the URI of the new file … … def PUTUnlinkedCreateDirectory(req, client): 33 42 34 43 def POSTUnlinkedCHK(req, client): 35 44 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) 37 54 d = client.upload(uploadable) 38 55 when_done = get_arg(req, "when_done", None) 39 56 if when_done: