def test_GET_unencrypted_data(self): # testing case of an unencrypted object with encrypted metadata from # a later POST env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' obj_key = fetch_crypto_keys()['object'] hdrs = {'Etag': md5hex(body), 'content-type': 'text/plain', 'content-length': len(body), 'x-object-transient-sysmeta-crypto-meta-test': base64.b64encode(encrypt('encrypt me', obj_key, FAKE_IV)) + ';swift_meta=' + get_crypto_meta_header(), 'x-object-sysmeta-test': 'do not encrypt me'} self.app.register('GET', '/v1/a/c/o', HTTPOk, body=body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual(body, resp.body) self.assertEqual('200 OK', resp.status) self.assertEqual(md5hex(body), resp.headers['Etag']) self.assertEqual('text/plain', resp.headers['Content-Type']) # POSTed user meta was encrypted self.assertEqual('encrypt me', resp.headers['x-object-meta-test']) # PUT sysmeta was not encrypted self.assertEqual('do not encrypt me', resp.headers['x-object-sysmeta-test'])
def test_GET_unencrypted_data(self): # testing case of an unencrypted object with encrypted metadata from # a later POST env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' obj_key = fetch_crypto_keys()['object'] hdrs = { 'Etag': md5hex(body), 'content-type': 'text/plain', 'content-length': len(body), 'x-object-transient-sysmeta-crypto-meta-test': base64.b64encode(encrypt('encrypt me', obj_key, FAKE_IV)) + ';swift_meta=' + get_crypto_meta_header(), 'x-object-sysmeta-test': 'do not encrypt me' } self.app.register('GET', '/v1/a/c/o', HTTPOk, body=body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual(body, resp.body) self.assertEqual('200 OK', resp.status) self.assertEqual(md5hex(body), resp.headers['Etag']) self.assertEqual('text/plain', resp.headers['Content-Type']) # POSTed user meta was encrypted self.assertEqual('encrypt me', resp.headers['x-object-meta-test']) # PUT sysmeta was not encrypted self.assertEqual('do not encrypt me', resp.headers['x-object-sysmeta-test'])
def _test_override_etag_bad_meta(self, method, bad_crypto_meta): env = { 'REQUEST_METHOD': method, CRYPTO_KEY_CALLBACK: fetch_crypto_keys } req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' key = fetch_crypto_keys()['object'] enc_body = encrypt(body, key, FAKE_IV) hdrs = self._make_response_headers(len(body), md5hex(body), fetch_crypto_keys(), 'not used') # simulate missing crypto meta from encrypted override etag hdrs['X-Object-Sysmeta-Container-Update-Override-Etag'] = \ encrypt_and_append_meta( md5hex(body), key, crypto_meta=bad_crypto_meta) self.app.register(method, '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertIn( 'Error decrypting header ' 'X-Object-Sysmeta-Container-Update-Override-Etag', self.decrypter.logger.get_lines_for_level('error')[0]) return resp
def test_GET_decryption_override(self): # This covers the case of an old un-encrypted object env = { 'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys, 'swift.crypto.override': True } req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' hdrs = { 'Etag': md5hex(body), 'content-type': 'text/plain', 'content-length': len(body), 'x-object-meta-test': 'do not encrypt me', 'x-object-sysmeta-test': 'do not encrypt me' } self.app.register('GET', '/v1/a/c/o', HTTPOk, body=body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual(body, resp.body) self.assertEqual('200 OK', resp.status) self.assertEqual(md5hex(body), resp.headers['Etag']) self.assertEqual('text/plain', resp.headers['Content-Type']) self.assertEqual('do not encrypt me', resp.headers['x-object-meta-test']) self.assertEqual('do not encrypt me', resp.headers['x-object-sysmeta-test'])
def test_headers_case(self): body = 'fAkE ApP' req = Request.blank('/v1/a/c/o', body='FaKe') req.environ[CRYPTO_KEY_CALLBACK] = fetch_crypto_keys plaintext_etag = md5hex(body) body_key = os.urandom(32) enc_body = encrypt(body, body_key, FAKE_IV) hdrs = self._make_response_headers( len(enc_body), plaintext_etag, fetch_crypto_keys(), body_key) hdrs.update({ 'x-Object-mEta-ignoRes-caSe': 'thIs pArt WilL bE cOol', }) self.app.register( 'GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) status, headers, app_iter = req.call_application(self.decrypter) self.assertEqual(status, '200 OK') expected = { 'Etag': '7f7837924188f7b511a9e3881a9f77a8', 'X-Object-Sysmeta-Container-Update-Override-Etag': 'encrypt me, too', 'X-Object-Meta-Test': 'encrypt me', 'Content-Length': '8', 'X-Object-Meta-Ignores-Case': 'thIs pArt WilL bE cOol', 'X-Object-Sysmeta-Test': 'do not encrypt me', 'Content-Type': 'text/plain', } self.assertEqual(dict(headers), expected) self.assertEqual('fAkE ApP', ''.join(app_iter))
def test_GET_multiseg_with_range(self): env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) req.headers['Content-Range'] = 'bytes 3-10/17' chunks = ['0123', '45678', '9abcdef'] body = ''.join(chunks) plaintext_etag = md5hex(body) body_key = os.urandom(32) ctxt = Crypto().create_encryption_ctxt(body_key, FAKE_IV) enc_body = [encrypt(chunk, ctxt=ctxt) for chunk in chunks] enc_body = [enc_body[0][3:], enc_body[1], enc_body[2][:2]] hdrs = self._make_response_headers(sum(map(len, enc_body)), plaintext_etag, fetch_crypto_keys(), body_key) hdrs['content-range'] = req.headers['Content-Range'] self.app.register('GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('3456789a', resp.body) self.assertEqual('200 OK', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual('text/plain', resp.headers['Content-Type'])
def test_headers_case(self): body = 'fAkE ApP' req = Request.blank('/v1/a/c/o', body='FaKe') req.environ[CRYPTO_KEY_CALLBACK] = fetch_crypto_keys plaintext_etag = md5hex(body) body_key = os.urandom(32) enc_body = encrypt(body, body_key, FAKE_IV) hdrs = self._make_response_headers(len(enc_body), plaintext_etag, fetch_crypto_keys(), body_key) hdrs.update({ 'x-Object-mEta-ignoRes-caSe': 'thIs pArt WilL bE cOol', }) self.app.register('GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) status, headers, app_iter = req.call_application(self.decrypter) self.assertEqual(status, '200 OK') expected = { 'Etag': '7f7837924188f7b511a9e3881a9f77a8', 'X-Object-Sysmeta-Container-Update-Override-Etag': 'encrypt me, too', 'X-Object-Meta-Test': 'encrypt me', 'Content-Length': '8', 'X-Object-Meta-Ignores-Case': 'thIs pArt WilL bE cOol', 'X-Object-Sysmeta-Test': 'do not encrypt me', 'Content-Type': 'text/plain', } self.assertEqual(dict(headers), expected) self.assertEqual('fAkE ApP', ''.join(app_iter))
def test_PUT_with_bad_etag_in_other_footers(self): # verify that etag supplied in footers from other middleware overrides # header etag when validating inbound plaintext etags plaintext = 'FAKE APP' plaintext_etag = md5hex(plaintext) other_footers = { 'Etag': 'bad etag', 'X-Object-Sysmeta-Other': 'other sysmeta', 'X-Object-Sysmeta-Container-Update-Override-Etag': 'other override' } env = { 'REQUEST_METHOD': 'PUT', CRYPTO_KEY_CALLBACK: fetch_crypto_keys, 'swift.callback.update_footers': lambda footers: footers.update(other_footers) } hdrs = { 'content-type': 'text/plain', 'content-length': str(len(plaintext)), 'Etag': plaintext_etag } req = Request.blank('/v1/a/c/o', environ=env, body=plaintext, headers=hdrs) self.app.register('PUT', '/v1/a/c/o', HTTPCreated, {}) resp = req.get_response(self.encrypter) self.assertEqual('422 Unprocessable Entity', resp.status) self.assertNotIn('Etag', resp.headers)
def _test_req_metadata_not_encrypted(self, method): # check that metadata is not decrypted if it does not have crypto meta; # testing for case of an unencrypted POST to an object. env = { 'REQUEST_METHOD': method, CRYPTO_KEY_CALLBACK: fetch_crypto_keys } req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' plaintext_etag = md5hex(body) body_key = os.urandom(32) enc_body = encrypt(body, body_key, FAKE_IV) hdrs = self._make_response_headers(len(body), plaintext_etag, fetch_crypto_keys(), body_key) hdrs.pop('x-object-transient-sysmeta-crypto-meta-test') hdrs['x-object-meta-test'] = 'plaintext not encrypted' self.app.register(method, '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('200 OK', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual('text/plain', resp.headers['Content-Type']) self.assertEqual('plaintext not encrypted', resp.headers['x-object-meta-test'])
def test_GET_multipart_content_type(self): # *just* having multipart content type shouldn't trigger the mime doc # code path body_key = os.urandom(32) plaintext = 'Cwm fjord veg balks nth pyx quiz' plaintext_etag = md5hex(plaintext) ciphertext = encrypt(plaintext, body_key, FAKE_IV) # register request with fake swift hdrs = self._make_response_headers( len(ciphertext), plaintext_etag, fetch_crypto_keys(), body_key) hdrs['content-type'] = \ 'multipart/byteranges;boundary=multipartboundary' self.app.register('GET', '/v1/a/c/o', HTTPOk, body=ciphertext, headers=hdrs) # issue request env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) resp = req.get_response(self.decrypter) self.assertEqual('200 OK', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual(len(plaintext), int(resp.headers['Content-Length'])) self.assertEqual('multipart/byteranges;boundary=multipartboundary', resp.headers['Content-Type']) self.assertEqual(plaintext, resp.body)
def test_GET_multipart_content_type(self): # *just* having multipart content type shouldn't trigger the mime doc # code path body_key = os.urandom(32) plaintext = 'Cwm fjord veg balks nth pyx quiz' plaintext_etag = md5hex(plaintext) ciphertext = encrypt(plaintext, body_key, FAKE_IV) # register request with fake swift hdrs = self._make_response_headers(len(ciphertext), plaintext_etag, fetch_crypto_keys(), body_key) hdrs['content-type'] = \ 'multipart/byteranges;boundary=multipartboundary' self.app.register('GET', '/v1/a/c/o', HTTPOk, body=ciphertext, headers=hdrs) # issue request env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) resp = req.get_response(self.decrypter) self.assertEqual('200 OK', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual(len(plaintext), int(resp.headers['Content-Length'])) self.assertEqual('multipart/byteranges;boundary=multipartboundary', resp.headers['Content-Type']) self.assertEqual(plaintext, resp.body)
def _test_request_success(self, method, body): env = {'REQUEST_METHOD': method, CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) plaintext_etag = md5hex(body) body_key = os.urandom(32) enc_body = encrypt(body, body_key, FAKE_IV) hdrs = self._make_response_headers( len(enc_body), plaintext_etag, fetch_crypto_keys(), body_key) # there shouldn't be any x-object-meta- headers, but if there are # then the decrypted header will win where there is a name clash... hdrs.update({ 'x-object-meta-test': 'unexpected, overwritten by decrypted value', 'x-object-meta-distinct': 'unexpected but distinct from encrypted' }) self.app.register( method, '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('200 OK', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual('text/plain', resp.headers['Content-Type']) self.assertEqual('encrypt me', resp.headers['x-object-meta-test']) self.assertEqual('unexpected but distinct from encrypted', resp.headers['x-object-meta-distinct']) self.assertEqual('do not encrypt me', resp.headers['x-object-sysmeta-test']) self.assertEqual( 'encrypt me, too', resp.headers['X-Object-Sysmeta-Container-Update-Override-Etag']) self.assertNotIn('X-Object-Sysmeta-Crypto-Body-Meta', resp.headers) self.assertNotIn('X-Object-Sysmeta-Crypto-Etag', resp.headers) return resp
def _test_bad_crypto_meta_for_user_metadata(self, method, bad_crypto_meta): # use bad iv for metadata headers env = { 'REQUEST_METHOD': method, CRYPTO_KEY_CALLBACK: fetch_crypto_keys } req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' key = fetch_crypto_keys()['object'] enc_body = encrypt(body, key, FAKE_IV) hdrs = self._make_response_headers(len(body), md5hex(body), fetch_crypto_keys(), 'not used') enc_val = base64.b64encode(encrypt('encrypt me', key, FAKE_IV)) if bad_crypto_meta: enc_val += ';swift_meta=' + get_crypto_meta_header( crypto_meta=bad_crypto_meta) hdrs['x-object-transient-sysmeta-crypto-meta-test'] = enc_val self.app.register(method, '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertIn( 'Error decrypting header X-Object-Transient-Sysmeta-Crypto-Meta-' 'Test', self.decrypter.logger.get_lines_for_level('error')[0]) return resp
def _test_PUT_with_empty_etag_override_in_headers(self, plaintext): # verify that an override etag value of '' from other middleware is # passed through unencrypted plaintext_etag = md5hex(plaintext) override_etag = '' env = {'REQUEST_METHOD': 'PUT', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} hdrs = { 'content-type': 'text/plain', 'content-length': str(len(plaintext)), 'Etag': plaintext_etag, 'X-Object-Sysmeta-Container-Update-Override-Etag': override_etag } req = Request.blank('/v1/a/c/o', environ=env, body=plaintext, headers=hdrs) self.app.register('PUT', '/v1/a/c/o', HTTPCreated, {}) resp = req.get_response(self.encrypter) self.assertEqual('201 Created', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual(1, len(self.app.calls), self.app.calls) self.assertEqual(('PUT', '/v1/a/c/o'), self.app.calls[0]) req_hdrs = self.app.headers[0] self.assertIn('X-Object-Sysmeta-Container-Update-Override-Etag', req_hdrs) self.assertEqual( override_etag, req_hdrs['X-Object-Sysmeta-Container-Update-Override-Etag'])
def test_GET_cipher_mismatch_for_metadata(self): # Cipher does not match env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' key = fetch_crypto_keys()['object'] enc_body = encrypt(body, key, FAKE_IV) bad_crypto_meta = fake_get_crypto_meta() bad_crypto_meta['cipher'] = 'unknown_cipher' hdrs = self._make_response_headers(len(enc_body), md5hex(body), fetch_crypto_keys(), 'not used') hdrs.update({ 'x-object-transient-sysmeta-crypto-meta-test': base64.b64encode(encrypt('encrypt me', key, FAKE_IV)) + ';swift_meta=' + get_crypto_meta_header(crypto_meta=bad_crypto_meta) }) self.app.register('GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertEqual('Error decrypting header', resp.body) self.assertIn( 'Error decrypting header X-Object-Transient-Sysmeta-Crypto-Meta-' 'Test', self.decrypter.logger.get_lines_for_level('error')[0])
def test_PUT_multiseg_good_client_etag(self): body_key = os.urandom(32) chunks = ['some', 'chunks', 'of data'] body = ''.join(chunks) env = { 'REQUEST_METHOD': 'PUT', CRYPTO_KEY_CALLBACK: fetch_crypto_keys, 'wsgi.input': FileLikeIter(chunks) } hdrs = { 'content-type': 'text/plain', 'content-length': str(len(body)), 'Etag': md5hex(body) } req = Request.blank('/v1/a/c/o', environ=env, headers=hdrs) self.app.register('PUT', '/v1/a/c/o', HTTPCreated, {}) with mock.patch( 'swift.common.middleware.crypto.crypto_utils.' 'Crypto.create_random_key', lambda *args: body_key): resp = req.get_response(self.encrypter) self.assertEqual('201 Created', resp.status) # verify object is encrypted by getting direct from the app get_req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'GET'}) self.assertEqual(encrypt(body, body_key, FAKE_IV), get_req.get_response(self.app).body)
def _test_PUT_with_empty_etag_override_in_footers(self, plaintext): # verify that an override etag value of '' from other middleware is # passed through unencrypted plaintext_etag = md5hex(plaintext) override_etag = '' other_footers = { 'X-Object-Sysmeta-Container-Update-Override-Etag': override_etag} env = {'REQUEST_METHOD': 'PUT', CRYPTO_KEY_CALLBACK: fetch_crypto_keys, 'swift.callback.update_footers': lambda footers: footers.update(other_footers)} hdrs = {'content-type': 'text/plain', 'content-length': str(len(plaintext)), 'Etag': plaintext_etag} req = Request.blank( '/v1/a/c/o', environ=env, body=plaintext, headers=hdrs) self.app.register('PUT', '/v1/a/c/o', HTTPCreated, {}) resp = req.get_response(self.encrypter) self.assertEqual('201 Created', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual(1, len(self.app.calls), self.app.calls) self.assertEqual(('PUT', '/v1/a/c/o'), self.app.calls[0]) req_hdrs = self.app.headers[0] self.assertIn( 'X-Object-Sysmeta-Container-Update-Override-Etag', req_hdrs) self.assertEqual( override_etag, req_hdrs['X-Object-Sysmeta-Container-Update-Override-Etag'])
def test_GET_multipart_ciphertext(self): # build fake multipart response body body_key = os.urandom(32) plaintext = 'Cwm fjord veg balks nth pyx quiz' plaintext_etag = md5hex(plaintext) ciphertext = encrypt(plaintext, body_key, FAKE_IV) parts = ((0, 3, 'text/plain'), (4, 9, 'text/plain; charset=us-ascii'), (24, 32, 'text/plain')) length = len(ciphertext) body = '' for start, end, ctype in parts: body += '--multipartboundary\r\n' body += 'Content-Type: %s\r\n' % ctype body += 'Content-Range: bytes %s-%s/%s' % (start, end - 1, length) body += '\r\n\r\n' + ciphertext[start:end] + '\r\n' body += '--multipartboundary--' # register request with fake swift hdrs = self._make_response_headers(len(body), plaintext_etag, fetch_crypto_keys(), body_key) hdrs['content-type'] = \ 'multipart/byteranges;boundary=multipartboundary' self.app.register('GET', '/v1/a/c/o', HTTPPartialContent, body=body, headers=hdrs) # issue request env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) resp = req.get_response(self.decrypter) self.assertEqual('206 Partial Content', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual(len(body), int(resp.headers['Content-Length'])) self.assertEqual('multipart/byteranges;boundary=multipartboundary', resp.headers['Content-Type']) # the multipart headers could be re-ordered, so parse response body to # verify expected content resp_lines = resp.body.split('\r\n') resp_lines.reverse() for start, end, ctype in parts: self.assertEqual('--multipartboundary', resp_lines.pop()) expected_header_lines = { 'Content-Type: %s' % ctype, 'Content-Range: bytes %s-%s/%s' % (start, end - 1, length) } resp_header_lines = {resp_lines.pop(), resp_lines.pop()} self.assertEqual(expected_header_lines, resp_header_lines) self.assertEqual('', resp_lines.pop()) self.assertEqual(plaintext[start:end], resp_lines.pop()) self.assertEqual('--multipartboundary--', resp_lines.pop()) # we should have consumed the whole response body self.assertFalse(resp_lines)
def test_GET_missing_etag_crypto_meta(self): env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' key = fetch_crypto_keys()['object'] enc_body = encrypt(body, key, FAKE_IV) hdrs = self._make_response_headers( len(body), md5hex(body), fetch_crypto_keys(), 'not used') # simulate missing crypto meta from encrypted etag hdrs['X-Object-Sysmeta-Crypto-Etag'] = \ base64.b64encode(encrypt(md5hex(body), key, FAKE_IV)) self.app.register('GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertIn('Error decrypting header', resp.body) self.assertIn('Error decrypting header X-Object-Sysmeta-Crypto-Etag', self.decrypter.logger.get_lines_for_level('error')[0])
def test_GET_multipart_ciphertext(self): # build fake multipart response body body_key = os.urandom(32) plaintext = 'Cwm fjord veg balks nth pyx quiz' plaintext_etag = md5hex(plaintext) ciphertext = encrypt(plaintext, body_key, FAKE_IV) parts = ((0, 3, 'text/plain'), (4, 9, 'text/plain; charset=us-ascii'), (24, 32, 'text/plain')) length = len(ciphertext) body = '' for start, end, ctype in parts: body += '--multipartboundary\r\n' body += 'Content-Type: %s\r\n' % ctype body += 'Content-Range: bytes %s-%s/%s' % (start, end - 1, length) body += '\r\n\r\n' + ciphertext[start:end] + '\r\n' body += '--multipartboundary--' # register request with fake swift hdrs = self._make_response_headers( len(body), plaintext_etag, fetch_crypto_keys(), body_key) hdrs['content-type'] = \ 'multipart/byteranges;boundary=multipartboundary' self.app.register('GET', '/v1/a/c/o', HTTPPartialContent, body=body, headers=hdrs) # issue request env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) resp = req.get_response(self.decrypter) self.assertEqual('206 Partial Content', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual(len(body), int(resp.headers['Content-Length'])) self.assertEqual('multipart/byteranges;boundary=multipartboundary', resp.headers['Content-Type']) # the multipart headers could be re-ordered, so parse response body to # verify expected content resp_lines = resp.body.split('\r\n') resp_lines.reverse() for start, end, ctype in parts: self.assertEqual('--multipartboundary', resp_lines.pop()) expected_header_lines = { 'Content-Type: %s' % ctype, 'Content-Range: bytes %s-%s/%s' % (start, end - 1, length)} resp_header_lines = {resp_lines.pop(), resp_lines.pop()} self.assertEqual(expected_header_lines, resp_header_lines) self.assertEqual('', resp_lines.pop()) self.assertEqual(plaintext[start:end], resp_lines.pop()) self.assertEqual('--multipartboundary--', resp_lines.pop()) # we should have consumed the whole response body self.assertFalse(resp_lines)
def _test_412_response(self, method): # simulate a 412 response to a conditional GET which has an Etag header data = 'the object content' env = {CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env, method=method) resp_body = 'I am sorry, you have failed to meet a precondition' hdrs = self._make_response_headers( len(resp_body), md5hex(data), fetch_crypto_keys(), 'not used') self.app.register(method, '/v1/a/c/o', HTTPPreconditionFailed, body=resp_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('412 Precondition Failed', resp.status) # the response body should not be decrypted, it is already plaintext self.assertEqual(resp_body if method == 'GET' else '', resp.body) # whereas the Etag and other headers should be decrypted self.assertEqual(md5hex(data), resp.headers['Etag']) self.assertEqual('text/plain', resp.headers['Content-Type']) self.assertEqual('encrypt me', resp.headers['x-object-meta-test']) self.assertEqual('do not encrypt me', resp.headers['x-object-sysmeta-test'])
def _test_override_etag_bad_meta(self, method, bad_crypto_meta): env = {'REQUEST_METHOD': method, CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' key = fetch_crypto_keys()['object'] enc_body = encrypt(body, key, FAKE_IV) hdrs = self._make_response_headers( len(body), md5hex(body), fetch_crypto_keys(), 'not used') # simulate missing crypto meta from encrypted override etag hdrs['X-Object-Sysmeta-Container-Update-Override-Etag'] = \ encrypt_and_append_meta( md5hex(body), key, crypto_meta=bad_crypto_meta) self.app.register(method, '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertIn('Error decrypting header ' 'X-Object-Sysmeta-Container-Update-Override-Etag', self.decrypter.logger.get_lines_for_level('error')[0]) return resp
def test_GET_missing_etag_crypto_meta(self): env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' key = fetch_crypto_keys()['object'] enc_body = encrypt(body, key, FAKE_IV) hdrs = self._make_response_headers(len(body), md5hex(body), fetch_crypto_keys(), 'not used') # simulate missing crypto meta from encrypted etag hdrs['X-Object-Sysmeta-Crypto-Etag'] = \ base64.b64encode(encrypt(md5hex(body), key, FAKE_IV)) self.app.register('GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertIn('Error decrypting header', resp.body) self.assertIn('Error decrypting header X-Object-Sysmeta-Crypto-Etag', self.decrypter.logger.get_lines_for_level('error')[0])
def test_GET_decryption_override(self): # This covers the case of an old un-encrypted object env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys, 'swift.crypto.override': True} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' hdrs = {'Etag': md5hex(body), 'content-type': 'text/plain', 'content-length': len(body), 'x-object-meta-test': 'do not encrypt me', 'x-object-sysmeta-test': 'do not encrypt me'} self.app.register('GET', '/v1/a/c/o', HTTPOk, body=body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual(body, resp.body) self.assertEqual('200 OK', resp.status) self.assertEqual(md5hex(body), resp.headers['Etag']) self.assertEqual('text/plain', resp.headers['Content-Type']) self.assertEqual('do not encrypt me', resp.headers['x-object-meta-test']) self.assertEqual('do not encrypt me', resp.headers['x-object-sysmeta-test'])
def _test_PUT_with_etag_override_in_headers(self, override_etag): # verify handling of another middleware's # container-update-override-etag in headers plaintext = b'FAKE APP' plaintext_etag = md5hex(plaintext) env = {'REQUEST_METHOD': 'PUT', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} hdrs = { 'content-type': 'text/plain', 'content-length': str(len(plaintext)), 'Etag': plaintext_etag, 'X-Object-Sysmeta-Container-Update-Override-Etag': override_etag } req = Request.blank('/v1/a/c/o', environ=env, body=plaintext, headers=hdrs) self.app.register('PUT', '/v1/a/c/o', HTTPCreated, {}) resp = req.get_response(self.encrypter) self.assertEqual('201 Created', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) # verify metadata items self.assertEqual(1, len(self.app.calls), self.app.calls) self.assertEqual(('PUT', '/v1/a/c/o'), self.app.calls[0]) req_hdrs = self.app.headers[0] # verify encrypted etag for container update self.assertIn('X-Object-Sysmeta-Container-Update-Override-Etag', req_hdrs) parts = req_hdrs[ 'X-Object-Sysmeta-Container-Update-Override-Etag'].rsplit(';', 1) self.assertEqual(2, len(parts)) cont_key = fetch_crypto_keys()['container'] # extract crypto_meta from end of etag for container update param = parts[1].strip() crypto_meta_tag = 'swift_meta=' self.assertTrue(param.startswith(crypto_meta_tag), param) actual_meta = json.loads( urlparse.unquote_plus(param[len(crypto_meta_tag):])) self.assertEqual(Crypto().cipher, actual_meta['cipher']) self.assertEqual(fetch_crypto_keys()['id'], actual_meta['key_id']) cont_etag_iv = base64.b64decode(actual_meta['iv']) self.assertEqual(FAKE_IV, cont_etag_iv) exp_etag = encrypt(override_etag.encode('ascii'), cont_key, cont_etag_iv) self.assertEqual(exp_etag, base64.b64decode(parts[0]))
def _test_412_response(self, method): # simulate a 412 response to a conditional GET which has an Etag header data = 'the object content' env = {CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env, method=method) resp_body = 'I am sorry, you have failed to meet a precondition' hdrs = self._make_response_headers(len(resp_body), md5hex(data), fetch_crypto_keys(), 'not used') self.app.register(method, '/v1/a/c/o', HTTPPreconditionFailed, body=resp_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('412 Precondition Failed', resp.status) # the response body should not be decrypted, it is already plaintext self.assertEqual(resp_body if method == 'GET' else '', resp.body) # whereas the Etag and other headers should be decrypted self.assertEqual(md5hex(data), resp.headers['Etag']) self.assertEqual('text/plain', resp.headers['Content-Type']) self.assertEqual('encrypt me', resp.headers['x-object-meta-test']) self.assertEqual('do not encrypt me', resp.headers['x-object-sysmeta-test'])
def _test_PUT_with_etag_override_in_headers(self, override_etag): # verify handling of another middleware's # container-update-override-etag in headers plaintext = b'FAKE APP' plaintext_etag = md5hex(plaintext) env = {'REQUEST_METHOD': 'PUT', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} hdrs = {'content-type': 'text/plain', 'content-length': str(len(plaintext)), 'Etag': plaintext_etag, 'X-Object-Sysmeta-Container-Update-Override-Etag': override_etag} req = Request.blank( '/v1/a/c/o', environ=env, body=plaintext, headers=hdrs) self.app.register('PUT', '/v1/a/c/o', HTTPCreated, {}) resp = req.get_response(self.encrypter) self.assertEqual('201 Created', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) # verify metadata items self.assertEqual(1, len(self.app.calls), self.app.calls) self.assertEqual(('PUT', '/v1/a/c/o'), self.app.calls[0]) req_hdrs = self.app.headers[0] # verify encrypted etag for container update self.assertIn( 'X-Object-Sysmeta-Container-Update-Override-Etag', req_hdrs) parts = req_hdrs[ 'X-Object-Sysmeta-Container-Update-Override-Etag'].rsplit(';', 1) self.assertEqual(2, len(parts)) cont_key = fetch_crypto_keys()['container'] # extract crypto_meta from end of etag for container update param = parts[1].strip() crypto_meta_tag = 'swift_meta=' self.assertTrue(param.startswith(crypto_meta_tag), param) actual_meta = json.loads( urlparse.unquote_plus(param[len(crypto_meta_tag):])) self.assertEqual(Crypto().cipher, actual_meta['cipher']) self.assertEqual(fetch_crypto_keys()['id'], actual_meta['key_id']) cont_etag_iv = base64.b64decode(actual_meta['iv']) self.assertEqual(FAKE_IV, cont_etag_iv) exp_etag = encrypt(override_etag.encode('ascii'), cont_key, cont_etag_iv) self.assertEqual(exp_etag, base64.b64decode(parts[0]))
def test_GET_missing_key_callback(self): # Do not provide keys, and do not set override flag env = {'REQUEST_METHOD': 'GET'} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' enc_body = encrypt(body, fetch_crypto_keys()['object'], FAKE_IV) hdrs = self._make_response_headers( len(body), md5hex('not the body'), fetch_crypto_keys(), 'not used') self.app.register( 'GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertEqual('Unable to retrieve encryption keys.', resp.body) self.assertIn('missing callback', self.decrypter.logger.get_lines_for_level('error')[0])
def _test_bad_key(self, method): # use bad key def bad_fetch_crypto_keys(): keys = fetch_crypto_keys() keys['object'] = 'bad key' return keys env = {'REQUEST_METHOD': method, CRYPTO_KEY_CALLBACK: bad_fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' key = fetch_crypto_keys()['object'] enc_body = encrypt(body, key, FAKE_IV) hdrs = self._make_response_headers( len(body), md5hex(body), fetch_crypto_keys(), 'not used') self.app.register(method, '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) return req.get_response(self.decrypter)
def test_GET_missing_key_callback(self): # Do not provide keys, and do not set override flag env = {'REQUEST_METHOD': 'GET'} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' enc_body = encrypt(body, fetch_crypto_keys()['object'], FAKE_IV) hdrs = self._make_response_headers(len(body), md5hex('not the body'), fetch_crypto_keys(), 'not used') self.app.register('GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertEqual('Unable to retrieve encryption keys.', resp.body) self.assertIn('missing callback', self.decrypter.logger.get_lines_for_level('error')[0])
def test_GET_error_in_key_callback(self): def raise_exc(): raise Exception('Testing') env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: raise_exc} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' enc_body = encrypt(body, fetch_crypto_keys()['object'], FAKE_IV) hdrs = self._make_response_headers( len(body), md5hex(body), fetch_crypto_keys(), 'not used') self.app.register( 'GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertEqual('Unable to retrieve encryption keys.', resp.body) self.assertIn('from callback: Testing', self.decrypter.logger.get_lines_for_level('error')[0])
def _test_GET_with_bad_crypto_meta_for_object_body(self, bad_crypto_meta): # use bad iv for object body env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' key = fetch_crypto_keys()['object'] enc_body = encrypt(body, key, FAKE_IV) hdrs = self._make_response_headers( len(body), md5hex(body), fetch_crypto_keys(), 'not used') hdrs['X-Object-Sysmeta-Crypto-Body-Meta'] = \ get_crypto_meta_header(crypto_meta=bad_crypto_meta) self.app.register('GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertEqual('Error decrypting object', resp.body) self.assertIn('Error decrypting object', self.decrypter.logger.get_lines_for_level('error')[0])
def test_PUT_with_etag_override_in_headers(self): # verify handling of another middleware's # container-update-override-etag in headers plaintext = "FAKE APP" plaintext_etag = md5hex(plaintext) env = {"REQUEST_METHOD": "PUT", CRYPTO_KEY_CALLBACK: fetch_crypto_keys} hdrs = { "content-type": "text/plain", "content-length": str(len(plaintext)), "Etag": plaintext_etag, "X-Object-Sysmeta-Container-Update-Override-Etag": "final etag", } req = Request.blank("/v1/a/c/o", environ=env, body=plaintext, headers=hdrs) self.app.register("PUT", "/v1/a/c/o", HTTPCreated, {}) resp = req.get_response(self.encrypter) self.assertEqual("201 Created", resp.status) self.assertEqual(plaintext_etag, resp.headers["Etag"]) # verify metadata items self.assertEqual(1, len(self.app.calls), self.app.calls) self.assertEqual(("PUT", "/v1/a/c/o"), self.app.calls[0]) req_hdrs = self.app.headers[0] # verify encrypted etag for container update self.assertIn("X-Object-Sysmeta-Container-Update-Override-Etag", req_hdrs) parts = req_hdrs["X-Object-Sysmeta-Container-Update-Override-Etag"].rsplit(";", 1) self.assertEqual(2, len(parts)) cont_key = fetch_crypto_keys()["container"] # extract crypto_meta from end of etag for container update param = parts[1].strip() crypto_meta_tag = "swift_meta=" self.assertTrue(param.startswith(crypto_meta_tag), param) actual_meta = json.loads(urllib.unquote_plus(param[len(crypto_meta_tag) :])) self.assertEqual(Crypto().cipher, actual_meta["cipher"]) self.assertEqual(fetch_crypto_keys()["id"], actual_meta["key_id"]) cont_etag_iv = base64.b64decode(actual_meta["iv"]) self.assertEqual(FAKE_IV, cont_etag_iv) self.assertEqual(encrypt("final etag", cont_key, cont_etag_iv), base64.b64decode(parts[0]))
def test_GET_multiseg(self): env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) chunks = ['some', 'chunks', 'of data'] body = ''.join(chunks) plaintext_etag = md5hex(body) body_key = os.urandom(32) ctxt = Crypto().create_encryption_ctxt(body_key, FAKE_IV) enc_body = [encrypt(chunk, ctxt=ctxt) for chunk in chunks] hdrs = self._make_response_headers( sum(map(len, enc_body)), plaintext_etag, fetch_crypto_keys(), body_key) self.app.register( 'GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual(body, resp.body) self.assertEqual('200 OK', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual('text/plain', resp.headers['Content-Type'])
def test_GET_error_in_key_callback(self): def raise_exc(): raise Exception('Testing') env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: raise_exc} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' enc_body = encrypt(body, fetch_crypto_keys()['object'], FAKE_IV) hdrs = self._make_response_headers(len(body), md5hex(body), fetch_crypto_keys(), 'not used') self.app.register('GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertEqual('Unable to retrieve encryption keys.', resp.body) self.assertIn('from callback: Testing', self.decrypter.logger.get_lines_for_level('error')[0])
def _test_GET_with_bad_crypto_meta_for_object_body(self, bad_crypto_meta): # use bad iv for object body env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' key = fetch_crypto_keys()['object'] enc_body = encrypt(body, key, FAKE_IV) hdrs = self._make_response_headers(len(body), md5hex(body), fetch_crypto_keys(), 'not used') hdrs['X-Object-Sysmeta-Crypto-Body-Meta'] = \ get_crypto_meta_header(crypto_meta=bad_crypto_meta) self.app.register('GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertEqual('Error decrypting object', resp.body) self.assertIn('Error decrypting object', self.decrypter.logger.get_lines_for_level('error')[0])
def test_GET_multipart_no_body_crypto_meta(self): # build fake multipart response body plaintext = 'Cwm fjord veg balks nth pyx quiz' plaintext_etag = md5hex(plaintext) parts = ((0, 3, 'text/plain'), (4, 9, 'text/plain; charset=us-ascii'), (24, 32, 'text/plain')) length = len(plaintext) body = '' for start, end, ctype in parts: body += '--multipartboundary\r\n' body += 'Content-Type: %s\r\n' % ctype body += 'Content-Range: bytes %s-%s/%s' % (start, end - 1, length) body += '\r\n\r\n' + plaintext[start:end] + '\r\n' body += '--multipartboundary--' # register request with fake swift hdrs = { 'Etag': plaintext_etag, 'content-type': 'multipart/byteranges;boundary=multipartboundary', 'content-length': len(body) } self.app.register('GET', '/v1/a/c/o', HTTPPartialContent, body=body, headers=hdrs) # issue request env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) resp = req.get_response(self.decrypter) self.assertEqual('206 Partial Content', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual(len(body), int(resp.headers['Content-Length'])) self.assertEqual('multipart/byteranges;boundary=multipartboundary', resp.headers['Content-Type']) # the multipart response body should be unchanged self.assertEqual(body, resp.body)
def _test_request_success(self, method, body): env = { 'REQUEST_METHOD': method, CRYPTO_KEY_CALLBACK: fetch_crypto_keys } req = Request.blank('/v1/a/c/o', environ=env) plaintext_etag = md5hex(body) body_key = os.urandom(32) enc_body = encrypt(body, body_key, FAKE_IV) hdrs = self._make_response_headers(len(enc_body), plaintext_etag, fetch_crypto_keys(), body_key) # there shouldn't be any x-object-meta- headers, but if there are # then the decrypted header will win where there is a name clash... hdrs.update({ 'x-object-meta-test': 'unexpected, overwritten by decrypted value', 'x-object-meta-distinct': 'unexpected but distinct from encrypted' }) self.app.register(method, '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('200 OK', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual('text/plain', resp.headers['Content-Type']) self.assertEqual('encrypt me', resp.headers['x-object-meta-test']) self.assertEqual('unexpected but distinct from encrypted', resp.headers['x-object-meta-distinct']) self.assertEqual('do not encrypt me', resp.headers['x-object-sysmeta-test']) self.assertEqual( 'encrypt me, too', resp.headers['X-Object-Sysmeta-Container-Update-Override-Etag']) self.assertNotIn('X-Object-Sysmeta-Crypto-Body-Meta', resp.headers) self.assertNotIn('X-Object-Sysmeta-Crypto-Etag', resp.headers) return resp
def test_PUT_with_bad_etag_in_other_footers(self): # verify that etag supplied in footers from other middleware overrides # header etag when validating inbound plaintext etags plaintext = "FAKE APP" plaintext_etag = md5hex(plaintext) other_footers = { "Etag": "bad etag", "X-Object-Sysmeta-Other": "other sysmeta", "X-Object-Sysmeta-Container-Update-Override-Etag": "other override", } env = { "REQUEST_METHOD": "PUT", CRYPTO_KEY_CALLBACK: fetch_crypto_keys, "swift.callback.update_footers": lambda footers: footers.update(other_footers), } hdrs = {"content-type": "text/plain", "content-length": str(len(plaintext)), "Etag": plaintext_etag} req = Request.blank("/v1/a/c/o", environ=env, body=plaintext, headers=hdrs) self.app.register("PUT", "/v1/a/c/o", HTTPCreated, {}) resp = req.get_response(self.encrypter) self.assertEqual("422 Unprocessable Entity", resp.status) self.assertNotIn("Etag", resp.headers)
def test_GET_cipher_mismatch_for_body(self): # Cipher does not match env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' enc_body = encrypt(body, fetch_crypto_keys()['object'], FAKE_IV) bad_crypto_meta = fake_get_crypto_meta() bad_crypto_meta['cipher'] = 'unknown_cipher' hdrs = self._make_response_headers( len(enc_body), md5hex(body), fetch_crypto_keys(), 'not used') hdrs['X-Object-Sysmeta-Crypto-Body-Meta'] = \ get_crypto_meta_header(crypto_meta=bad_crypto_meta) self.app.register( 'GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertEqual('Error decrypting object', resp.body) self.assertIn('Error decrypting object', self.decrypter.logger.get_lines_for_level('error')[0]) self.assertIn('Bad crypto meta: Cipher', self.decrypter.logger.get_lines_for_level('error')[0])
def _test_req_metadata_not_encrypted(self, method): # check that metadata is not decrypted if it does not have crypto meta; # testing for case of an unencrypted POST to an object. env = {'REQUEST_METHOD': method, CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' plaintext_etag = md5hex(body) body_key = os.urandom(32) enc_body = encrypt(body, body_key, FAKE_IV) hdrs = self._make_response_headers( len(body), plaintext_etag, fetch_crypto_keys(), body_key) hdrs.pop('x-object-transient-sysmeta-crypto-meta-test') hdrs['x-object-meta-test'] = 'plaintext not encrypted' self.app.register( method, '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('200 OK', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual('text/plain', resp.headers['Content-Type']) self.assertEqual('plaintext not encrypted', resp.headers['x-object-meta-test'])
def test_GET_multiseg(self): env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) chunks = ['some', 'chunks', 'of data'] body = ''.join(chunks) plaintext_etag = md5hex(body) body_key = os.urandom(32) ctxt = Crypto().create_encryption_ctxt(body_key, FAKE_IV) enc_body = [encrypt(chunk, ctxt=ctxt) for chunk in chunks] hdrs = self._make_response_headers(sum(map(len, enc_body)), plaintext_etag, fetch_crypto_keys(), body_key) self.app.register('GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual(body, resp.body) self.assertEqual('200 OK', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual('text/plain', resp.headers['Content-Type'])
def test_GET_multipart_no_body_crypto_meta(self): # build fake multipart response body plaintext = 'Cwm fjord veg balks nth pyx quiz' plaintext_etag = md5hex(plaintext) parts = ((0, 3, 'text/plain'), (4, 9, 'text/plain; charset=us-ascii'), (24, 32, 'text/plain')) length = len(plaintext) body = '' for start, end, ctype in parts: body += '--multipartboundary\r\n' body += 'Content-Type: %s\r\n' % ctype body += 'Content-Range: bytes %s-%s/%s' % (start, end - 1, length) body += '\r\n\r\n' + plaintext[start:end] + '\r\n' body += '--multipartboundary--' # register request with fake swift hdrs = { 'Etag': plaintext_etag, 'content-type': 'multipart/byteranges;boundary=multipartboundary', 'content-length': len(body)} self.app.register('GET', '/v1/a/c/o', HTTPPartialContent, body=body, headers=hdrs) # issue request env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) resp = req.get_response(self.decrypter) self.assertEqual('206 Partial Content', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual(len(body), int(resp.headers['Content-Length'])) self.assertEqual('multipart/byteranges;boundary=multipartboundary', resp.headers['Content-Type']) # the multipart response body should be unchanged self.assertEqual(body, resp.body)
def _test_GET_multipart_bad_body_crypto_meta(self, bad_crypto_meta): # build fake multipart response body key = fetch_crypto_keys()['object'] ctxt = Crypto().create_encryption_ctxt(key, FAKE_IV) plaintext = 'Cwm fjord veg balks nth pyx quiz' plaintext_etag = md5hex(plaintext) ciphertext = encrypt(plaintext, ctxt=ctxt) parts = ((0, 3, 'text/plain'), (4, 9, 'text/plain; charset=us-ascii'), (24, 32, 'text/plain')) length = len(ciphertext) body = '' for start, end, ctype in parts: body += '--multipartboundary\r\n' body += 'Content-Type: %s\r\n' % ctype body += 'Content-Range: bytes %s-%s/%s' % (start, end - 1, length) body += '\r\n\r\n' + ciphertext[start:end] + '\r\n' body += '--multipartboundary--' # register request with fake swift hdrs = self._make_response_headers( len(body), plaintext_etag, fetch_crypto_keys(), 'not used') hdrs['content-type'] = \ 'multipart/byteranges;boundary=multipartboundary' hdrs['X-Object-Sysmeta-Crypto-Body-Meta'] = \ get_crypto_meta_header(bad_crypto_meta) self.app.register('GET', '/v1/a/c/o', HTTPOk, body=body, headers=hdrs) # issue request env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertEqual('Error decrypting object', resp.body) self.assertIn('Error decrypting object', self.decrypter.logger.get_lines_for_level('error')[0])
def test_GET_multiseg_with_range(self): env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) req.headers['Content-Range'] = 'bytes 3-10/17' chunks = ['0123', '45678', '9abcdef'] body = ''.join(chunks) plaintext_etag = md5hex(body) body_key = os.urandom(32) ctxt = Crypto().create_encryption_ctxt(body_key, FAKE_IV) enc_body = [encrypt(chunk, ctxt=ctxt) for chunk in chunks] enc_body = [enc_body[0][3:], enc_body[1], enc_body[2][:2]] hdrs = self._make_response_headers( sum(map(len, enc_body)), plaintext_etag, fetch_crypto_keys(), body_key) hdrs['content-range'] = req.headers['Content-Range'] self.app.register( 'GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('3456789a', resp.body) self.assertEqual('200 OK', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual('text/plain', resp.headers['Content-Type'])
def _test_bad_key(self, method): # use bad key def bad_fetch_crypto_keys(): keys = fetch_crypto_keys() keys['object'] = 'bad key' return keys env = { 'REQUEST_METHOD': method, CRYPTO_KEY_CALLBACK: bad_fetch_crypto_keys } req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' key = fetch_crypto_keys()['object'] enc_body = encrypt(body, key, FAKE_IV) hdrs = self._make_response_headers(len(body), md5hex(body), fetch_crypto_keys(), 'not used') self.app.register(method, '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) return req.get_response(self.decrypter)
def _test_bad_crypto_meta_for_user_metadata(self, method, bad_crypto_meta): # use bad iv for metadata headers env = {'REQUEST_METHOD': method, CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' key = fetch_crypto_keys()['object'] enc_body = encrypt(body, key, FAKE_IV) hdrs = self._make_response_headers( len(body), md5hex(body), fetch_crypto_keys(), 'not used') enc_val = base64.b64encode(encrypt('encrypt me', key, FAKE_IV)) if bad_crypto_meta: enc_val += ';swift_meta=' + get_crypto_meta_header( crypto_meta=bad_crypto_meta) hdrs['x-object-transient-sysmeta-crypto-meta-test'] = enc_val self.app.register(method, '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertIn( 'Error decrypting header X-Object-Transient-Sysmeta-Crypto-Meta-' 'Test', self.decrypter.logger.get_lines_for_level('error')[0]) return resp
def test_GET_cipher_mismatch_for_body(self): # Cipher does not match env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' enc_body = encrypt(body, fetch_crypto_keys()['object'], FAKE_IV) bad_crypto_meta = fake_get_crypto_meta() bad_crypto_meta['cipher'] = 'unknown_cipher' hdrs = self._make_response_headers(len(enc_body), md5hex(body), fetch_crypto_keys(), 'not used') hdrs['X-Object-Sysmeta-Crypto-Body-Meta'] = \ get_crypto_meta_header(crypto_meta=bad_crypto_meta) self.app.register('GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertEqual('Error decrypting object', resp.body) self.assertIn('Error decrypting object', self.decrypter.logger.get_lines_for_level('error')[0]) self.assertIn('Bad crypto meta: Cipher', self.decrypter.logger.get_lines_for_level('error')[0])
def test_PUT_multiseg_good_client_etag(self): body_key = os.urandom(32) chunks = ['some', 'chunks', 'of data'] body = ''.join(chunks) env = {'REQUEST_METHOD': 'PUT', CRYPTO_KEY_CALLBACK: fetch_crypto_keys, 'wsgi.input': FileLikeIter(chunks)} hdrs = {'content-type': 'text/plain', 'content-length': str(len(body)), 'Etag': md5hex(body)} req = Request.blank('/v1/a/c/o', environ=env, headers=hdrs) self.app.register('PUT', '/v1/a/c/o', HTTPCreated, {}) with mock.patch( 'swift.common.middleware.crypto.crypto_utils.' 'Crypto.create_random_key', lambda *args: body_key): resp = req.get_response(self.encrypter) self.assertEqual('201 Created', resp.status) # verify object is encrypted by getting direct from the app get_req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'GET'}) self.assertEqual(encrypt(body, body_key, FAKE_IV), get_req.get_response(self.app).body)
def _test_GET_multipart_bad_body_crypto_meta(self, bad_crypto_meta): # build fake multipart response body key = fetch_crypto_keys()['object'] ctxt = Crypto().create_encryption_ctxt(key, FAKE_IV) plaintext = 'Cwm fjord veg balks nth pyx quiz' plaintext_etag = md5hex(plaintext) ciphertext = encrypt(plaintext, ctxt=ctxt) parts = ((0, 3, 'text/plain'), (4, 9, 'text/plain; charset=us-ascii'), (24, 32, 'text/plain')) length = len(ciphertext) body = '' for start, end, ctype in parts: body += '--multipartboundary\r\n' body += 'Content-Type: %s\r\n' % ctype body += 'Content-Range: bytes %s-%s/%s' % (start, end - 1, length) body += '\r\n\r\n' + ciphertext[start:end] + '\r\n' body += '--multipartboundary--' # register request with fake swift hdrs = self._make_response_headers(len(body), plaintext_etag, fetch_crypto_keys(), 'not used') hdrs['content-type'] = \ 'multipart/byteranges;boundary=multipartboundary' hdrs['X-Object-Sysmeta-Crypto-Body-Meta'] = \ get_crypto_meta_header(bad_crypto_meta) self.app.register('GET', '/v1/a/c/o', HTTPOk, body=body, headers=hdrs) # issue request env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertEqual('Error decrypting object', resp.body) self.assertIn('Error decrypting object', self.decrypter.logger.get_lines_for_level('error')[0])
def test_PUT_with_bad_etag_in_other_footers(self): # verify that etag supplied in footers from other middleware overrides # header etag when validating inbound plaintext etags plaintext = 'FAKE APP' plaintext_etag = md5hex(plaintext) other_footers = { 'Etag': 'bad etag', 'X-Object-Sysmeta-Other': 'other sysmeta', 'X-Object-Sysmeta-Container-Update-Override-Etag': 'other override'} env = {'REQUEST_METHOD': 'PUT', CRYPTO_KEY_CALLBACK: fetch_crypto_keys, 'swift.callback.update_footers': lambda footers: footers.update(other_footers)} hdrs = {'content-type': 'text/plain', 'content-length': str(len(plaintext)), 'Etag': plaintext_etag} req = Request.blank( '/v1/a/c/o', environ=env, body=plaintext, headers=hdrs) self.app.register('PUT', '/v1/a/c/o', HTTPCreated, {}) resp = req.get_response(self.encrypter) self.assertEqual('422 Unprocessable Entity', resp.status) self.assertNotIn('Etag', resp.headers)
def test_GET_cipher_mismatch_for_metadata(self): # Cipher does not match env = {'REQUEST_METHOD': 'GET', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} req = Request.blank('/v1/a/c/o', environ=env) body = 'FAKE APP' key = fetch_crypto_keys()['object'] enc_body = encrypt(body, key, FAKE_IV) bad_crypto_meta = fake_get_crypto_meta() bad_crypto_meta['cipher'] = 'unknown_cipher' hdrs = self._make_response_headers( len(enc_body), md5hex(body), fetch_crypto_keys(), 'not used') hdrs.update({'x-object-transient-sysmeta-crypto-meta-test': base64.b64encode(encrypt('encrypt me', key, FAKE_IV)) + ';swift_meta=' + get_crypto_meta_header(crypto_meta=bad_crypto_meta)}) self.app.register( 'GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) resp = req.get_response(self.decrypter) self.assertEqual('500 Internal Error', resp.status) self.assertEqual('Error decrypting header', resp.body) self.assertIn( 'Error decrypting header X-Object-Transient-Sysmeta-Crypto-Meta-' 'Test', self.decrypter.logger.get_lines_for_level('error')[0])
def _test_PUT_with_other_footers(self, override_etag): # verify handling of another middleware's footer callback cont_key = fetch_crypto_keys()['container'] body_key = os.urandom(32) object_key = fetch_crypto_keys()['object'] plaintext = 'FAKE APP' plaintext_etag = md5hex(plaintext) ciphertext = encrypt(plaintext, body_key, FAKE_IV) ciphertext_etag = md5hex(ciphertext) other_footers = { 'Etag': plaintext_etag, 'X-Object-Sysmeta-Other': 'other sysmeta', 'X-Object-Sysmeta-Container-Update-Override-Size': 'other override', 'X-Object-Sysmeta-Container-Update-Override-Etag': override_etag } env = { 'REQUEST_METHOD': 'PUT', CRYPTO_KEY_CALLBACK: fetch_crypto_keys, 'swift.callback.update_footers': lambda footers: footers.update(other_footers) } hdrs = { 'content-type': 'text/plain', 'content-length': str(len(plaintext)), 'Etag': 'correct etag is in footers' } req = Request.blank('/v1/a/c/o', environ=env, body=plaintext, headers=hdrs) self.app.register('PUT', '/v1/a/c/o', HTTPCreated, {}) with mock.patch( 'swift.common.middleware.crypto.crypto_utils.' 'Crypto.create_random_key', lambda *args: body_key): resp = req.get_response(self.encrypter) self.assertEqual('201 Created', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) # verify metadata items self.assertEqual(1, len(self.app.calls), self.app.calls) self.assertEqual('PUT', self.app.calls[0][0]) req_hdrs = self.app.headers[0] # verify that other middleware's footers made it to app, including any # container update overrides but nothing Etag-related other_footers.pop('Etag') other_footers.pop('X-Object-Sysmeta-Container-Update-Override-Etag') for k, v in other_footers.items(): self.assertEqual(v, req_hdrs[k]) # verify encryption footers are ok encrypted_etag, _junk, etag_meta = \ req_hdrs['X-Object-Sysmeta-Crypto-Etag'].partition('; swift_meta=') self.assertTrue(etag_meta) actual_meta = json.loads(urllib.unquote_plus(etag_meta)) self.assertEqual(Crypto().cipher, actual_meta['cipher']) self.assertEqual(ciphertext_etag, req_hdrs['Etag']) actual = base64.b64decode(encrypted_etag) etag_iv = base64.b64decode(actual_meta['iv']) self.assertEqual(encrypt(plaintext_etag, object_key, etag_iv), actual) # verify encrypted etag for container update self.assertIn('X-Object-Sysmeta-Container-Update-Override-Etag', req_hdrs) parts = req_hdrs[ 'X-Object-Sysmeta-Container-Update-Override-Etag'].rsplit(';', 1) self.assertEqual(2, len(parts)) # extract crypto_meta from end of etag for container update param = parts[1].strip() crypto_meta_tag = 'swift_meta=' self.assertTrue(param.startswith(crypto_meta_tag), param) actual_meta = json.loads( urllib.unquote_plus(param[len(crypto_meta_tag):])) self.assertEqual(Crypto().cipher, actual_meta['cipher']) cont_key = fetch_crypto_keys()['container'] cont_etag_iv = base64.b64decode(actual_meta['iv']) self.assertEqual(FAKE_IV, cont_etag_iv) self.assertEqual(encrypt(override_etag, cont_key, cont_etag_iv), base64.b64decode(parts[0])) # verify body crypto meta actual = req_hdrs['X-Object-Sysmeta-Crypto-Body-Meta'] actual = json.loads(urllib.unquote_plus(actual)) self.assertEqual(Crypto().cipher, actual['cipher']) self.assertEqual(FAKE_IV, base64.b64decode(actual['iv'])) # verify wrapped body key expected_wrapped_key = encrypt(body_key, object_key, FAKE_IV) self.assertEqual(expected_wrapped_key, base64.b64decode(actual['body_key']['key'])) self.assertEqual(FAKE_IV, base64.b64decode(actual['body_key']['iv'])) self.assertEqual(fetch_crypto_keys()['id'], actual['key_id'])
def setUp(self): self.plaintext = 'unencrypted body content' self.plaintext_etag = md5hex(self.plaintext) self._setup_crypto_app()
def test_PUT_req(self): body_key = os.urandom(32) object_key = fetch_crypto_keys()['object'] plaintext = 'FAKE APP' plaintext_etag = md5hex(plaintext) ciphertext = encrypt(plaintext, body_key, FAKE_IV) ciphertext_etag = md5hex(ciphertext) env = {'REQUEST_METHOD': 'PUT', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} hdrs = {'etag': plaintext_etag, 'content-type': 'text/plain', 'content-length': str(len(plaintext)), 'x-object-meta-etag': 'not to be confused with the Etag!', 'x-object-meta-test': 'encrypt me', 'x-object-sysmeta-test': 'do not encrypt me'} req = Request.blank( '/v1/a/c/o', environ=env, body=plaintext, headers=hdrs) self.app.register('PUT', '/v1/a/c/o', HTTPCreated, {}) with mock.patch( 'swift.common.middleware.crypto.crypto_utils.' 'Crypto.create_random_key', return_value=body_key): resp = req.get_response(self.encrypter) self.assertEqual('201 Created', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) # verify metadata items self.assertEqual(1, len(self.app.calls), self.app.calls) self.assertEqual('PUT', self.app.calls[0][0]) req_hdrs = self.app.headers[0] # verify body crypto meta actual = req_hdrs['X-Object-Sysmeta-Crypto-Body-Meta'] actual = json.loads(urlparse.unquote_plus(actual)) self.assertEqual(Crypto().cipher, actual['cipher']) self.assertEqual(FAKE_IV, base64.b64decode(actual['iv'])) # verify wrapped body key expected_wrapped_key = encrypt(body_key, object_key, FAKE_IV) self.assertEqual(expected_wrapped_key, base64.b64decode(actual['body_key']['key'])) self.assertEqual(FAKE_IV, base64.b64decode(actual['body_key']['iv'])) self.assertEqual(fetch_crypto_keys()['id'], actual['key_id']) # verify etag self.assertEqual(ciphertext_etag, req_hdrs['Etag']) encrypted_etag, _junk, etag_meta = \ req_hdrs['X-Object-Sysmeta-Crypto-Etag'].partition('; swift_meta=') # verify crypto_meta was appended to this etag self.assertTrue(etag_meta) actual_meta = json.loads(urlparse.unquote_plus(etag_meta)) self.assertEqual(Crypto().cipher, actual_meta['cipher']) # verify encrypted version of plaintext etag actual = base64.b64decode(encrypted_etag) etag_iv = base64.b64decode(actual_meta['iv']) enc_etag = encrypt(plaintext_etag, object_key, etag_iv) self.assertEqual(enc_etag, actual) # verify etag MAC for conditional requests actual_hmac = base64.b64decode( req_hdrs['X-Object-Sysmeta-Crypto-Etag-Mac']) self.assertEqual(actual_hmac, hmac.new( object_key, plaintext_etag, hashlib.sha256).digest()) # verify encrypted etag for container update self.assertIn( 'X-Object-Sysmeta-Container-Update-Override-Etag', req_hdrs) parts = req_hdrs[ 'X-Object-Sysmeta-Container-Update-Override-Etag'].rsplit(';', 1) self.assertEqual(2, len(parts)) # extract crypto_meta from end of etag for container update param = parts[1].strip() crypto_meta_tag = 'swift_meta=' self.assertTrue(param.startswith(crypto_meta_tag), param) actual_meta = json.loads( urlparse.unquote_plus(param[len(crypto_meta_tag):])) self.assertEqual(Crypto().cipher, actual_meta['cipher']) self.assertEqual(fetch_crypto_keys()['id'], actual_meta['key_id']) cont_key = fetch_crypto_keys()['container'] cont_etag_iv = base64.b64decode(actual_meta['iv']) self.assertEqual(FAKE_IV, cont_etag_iv) self.assertEqual(encrypt(plaintext_etag, cont_key, cont_etag_iv), base64.b64decode(parts[0])) # content-type is not encrypted self.assertEqual('text/plain', req_hdrs['Content-Type']) # user meta is encrypted self._verify_user_metadata(req_hdrs, 'Test', 'encrypt me', object_key) self._verify_user_metadata( req_hdrs, 'Etag', 'not to be confused with the Etag!', object_key) # sysmeta is not encrypted self.assertEqual('do not encrypt me', req_hdrs['X-Object-Sysmeta-Test']) # verify object is encrypted by getting direct from the app get_req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'GET'}) resp = get_req.get_response(self.app) self.assertEqual(ciphertext, resp.body) self.assertEqual(ciphertext_etag, resp.headers['Etag'])
def setUp(self): skip_if_no_xattrs() self.plaintext = 'unencrypted body content' self.plaintext_etag = md5hex(self.plaintext) self._setup_crypto_app()
def test_PUT_req(self): body_key = os.urandom(32) object_key = fetch_crypto_keys()['object'] plaintext = 'FAKE APP' plaintext_etag = md5hex(plaintext) ciphertext = encrypt(plaintext, body_key, FAKE_IV) ciphertext_etag = md5hex(ciphertext) env = {'REQUEST_METHOD': 'PUT', CRYPTO_KEY_CALLBACK: fetch_crypto_keys} hdrs = { 'etag': plaintext_etag, 'content-type': 'text/plain', 'content-length': str(len(plaintext)), 'x-object-meta-etag': 'not to be confused with the Etag!', 'x-object-meta-test': 'encrypt me', 'x-object-sysmeta-test': 'do not encrypt me' } req = Request.blank('/v1/a/c/o', environ=env, body=plaintext, headers=hdrs) self.app.register('PUT', '/v1/a/c/o', HTTPCreated, {}) with mock.patch( 'swift.common.middleware.crypto.crypto_utils.' 'Crypto.create_random_key', return_value=body_key): resp = req.get_response(self.encrypter) self.assertEqual('201 Created', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) # verify metadata items self.assertEqual(1, len(self.app.calls), self.app.calls) self.assertEqual('PUT', self.app.calls[0][0]) req_hdrs = self.app.headers[0] # verify body crypto meta actual = req_hdrs['X-Object-Sysmeta-Crypto-Body-Meta'] actual = json.loads(urllib.unquote_plus(actual)) self.assertEqual(Crypto().cipher, actual['cipher']) self.assertEqual(FAKE_IV, base64.b64decode(actual['iv'])) # verify wrapped body key expected_wrapped_key = encrypt(body_key, object_key, FAKE_IV) self.assertEqual(expected_wrapped_key, base64.b64decode(actual['body_key']['key'])) self.assertEqual(FAKE_IV, base64.b64decode(actual['body_key']['iv'])) self.assertEqual(fetch_crypto_keys()['id'], actual['key_id']) # verify etag self.assertEqual(ciphertext_etag, req_hdrs['Etag']) encrypted_etag, _junk, etag_meta = \ req_hdrs['X-Object-Sysmeta-Crypto-Etag'].partition('; swift_meta=') # verify crypto_meta was appended to this etag self.assertTrue(etag_meta) actual_meta = json.loads(urllib.unquote_plus(etag_meta)) self.assertEqual(Crypto().cipher, actual_meta['cipher']) # verify encrypted version of plaintext etag actual = base64.b64decode(encrypted_etag) etag_iv = base64.b64decode(actual_meta['iv']) enc_etag = encrypt(plaintext_etag, object_key, etag_iv) self.assertEqual(enc_etag, actual) # verify etag MAC for conditional requests actual_hmac = base64.b64decode( req_hdrs['X-Object-Sysmeta-Crypto-Etag-Mac']) self.assertEqual( actual_hmac, hmac.new(object_key, plaintext_etag, hashlib.sha256).digest()) # verify encrypted etag for container update self.assertIn('X-Object-Sysmeta-Container-Update-Override-Etag', req_hdrs) parts = req_hdrs[ 'X-Object-Sysmeta-Container-Update-Override-Etag'].rsplit(';', 1) self.assertEqual(2, len(parts)) # extract crypto_meta from end of etag for container update param = parts[1].strip() crypto_meta_tag = 'swift_meta=' self.assertTrue(param.startswith(crypto_meta_tag), param) actual_meta = json.loads( urllib.unquote_plus(param[len(crypto_meta_tag):])) self.assertEqual(Crypto().cipher, actual_meta['cipher']) self.assertEqual(fetch_crypto_keys()['id'], actual_meta['key_id']) cont_key = fetch_crypto_keys()['container'] cont_etag_iv = base64.b64decode(actual_meta['iv']) self.assertEqual(FAKE_IV, cont_etag_iv) self.assertEqual(encrypt(plaintext_etag, cont_key, cont_etag_iv), base64.b64decode(parts[0])) # content-type is not encrypted self.assertEqual('text/plain', req_hdrs['Content-Type']) # user meta is encrypted self._verify_user_metadata(req_hdrs, 'Test', 'encrypt me', object_key) self._verify_user_metadata(req_hdrs, 'Etag', 'not to be confused with the Etag!', object_key) # sysmeta is not encrypted self.assertEqual('do not encrypt me', req_hdrs['X-Object-Sysmeta-Test']) # verify object is encrypted by getting direct from the app get_req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'GET'}) resp = get_req.get_response(self.app) self.assertEqual(ciphertext, resp.body) self.assertEqual(ciphertext_etag, resp.headers['Etag'])
def setUp(self): skip_if_no_xattrs() self.plaintext = b'unencrypted body content' self.plaintext_etag = md5hex(self.plaintext) self._setup_crypto_app()
def _test_PUT_with_other_footers(self, override_etag): # verify handling of another middleware's footer callback body_key = os.urandom(32) object_key = fetch_crypto_keys()['object'] plaintext = 'FAKE APP' plaintext_etag = md5hex(plaintext) ciphertext = encrypt(plaintext, body_key, FAKE_IV) ciphertext_etag = md5hex(ciphertext) other_footers = { 'Etag': plaintext_etag, 'X-Object-Sysmeta-Other': 'other sysmeta', 'X-Object-Sysmeta-Container-Update-Override-Size': 'other override', 'X-Object-Sysmeta-Container-Update-Override-Etag': override_etag} env = {'REQUEST_METHOD': 'PUT', CRYPTO_KEY_CALLBACK: fetch_crypto_keys, 'swift.callback.update_footers': lambda footers: footers.update(other_footers)} hdrs = {'content-type': 'text/plain', 'content-length': str(len(plaintext)), 'Etag': 'correct etag is in footers'} req = Request.blank( '/v1/a/c/o', environ=env, body=plaintext, headers=hdrs) self.app.register('PUT', '/v1/a/c/o', HTTPCreated, {}) with mock.patch( 'swift.common.middleware.crypto.crypto_utils.' 'Crypto.create_random_key', lambda *args: body_key): resp = req.get_response(self.encrypter) self.assertEqual('201 Created', resp.status) self.assertEqual(plaintext_etag, resp.headers['Etag']) # verify metadata items self.assertEqual(1, len(self.app.calls), self.app.calls) self.assertEqual('PUT', self.app.calls[0][0]) req_hdrs = self.app.headers[0] # verify that other middleware's footers made it to app, including any # container update overrides but nothing Etag-related other_footers.pop('Etag') other_footers.pop('X-Object-Sysmeta-Container-Update-Override-Etag') for k, v in other_footers.items(): self.assertEqual(v, req_hdrs[k]) # verify encryption footers are ok encrypted_etag, _junk, etag_meta = \ req_hdrs['X-Object-Sysmeta-Crypto-Etag'].partition('; swift_meta=') self.assertTrue(etag_meta) actual_meta = json.loads(urlparse.unquote_plus(etag_meta)) self.assertEqual(Crypto().cipher, actual_meta['cipher']) self.assertEqual(ciphertext_etag, req_hdrs['Etag']) actual = base64.b64decode(encrypted_etag) etag_iv = base64.b64decode(actual_meta['iv']) self.assertEqual(encrypt(plaintext_etag, object_key, etag_iv), actual) # verify encrypted etag for container update self.assertIn( 'X-Object-Sysmeta-Container-Update-Override-Etag', req_hdrs) parts = req_hdrs[ 'X-Object-Sysmeta-Container-Update-Override-Etag'].rsplit(';', 1) self.assertEqual(2, len(parts)) # extract crypto_meta from end of etag for container update param = parts[1].strip() crypto_meta_tag = 'swift_meta=' self.assertTrue(param.startswith(crypto_meta_tag), param) actual_meta = json.loads( urlparse.unquote_plus(param[len(crypto_meta_tag):])) self.assertEqual(Crypto().cipher, actual_meta['cipher']) cont_key = fetch_crypto_keys()['container'] cont_etag_iv = base64.b64decode(actual_meta['iv']) self.assertEqual(FAKE_IV, cont_etag_iv) self.assertEqual(encrypt(override_etag, cont_key, cont_etag_iv), base64.b64decode(parts[0])) # verify body crypto meta actual = req_hdrs['X-Object-Sysmeta-Crypto-Body-Meta'] actual = json.loads(urlparse.unquote_plus(actual)) self.assertEqual(Crypto().cipher, actual['cipher']) self.assertEqual(FAKE_IV, base64.b64decode(actual['iv'])) # verify wrapped body key expected_wrapped_key = encrypt(body_key, object_key, FAKE_IV) self.assertEqual(expected_wrapped_key, base64.b64decode(actual['body_key']['key'])) self.assertEqual(FAKE_IV, base64.b64decode(actual['body_key']['iv'])) self.assertEqual(fetch_crypto_keys()['id'], actual['key_id'])