You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@labs.apache.org by ad...@apache.org on 2013/10/14 20:17:07 UTC
svn commit: r1532012 - in /labs/panopticon: requirements.txt
src/asf/utils/safe.py tests/test_safe.py
Author: adc
Date: Mon Oct 14 18:17:06 2013
New Revision: 1532012
URL: http://svn.apache.org/r1532012
Log:
Safe store for passwords
Added:
labs/panopticon/src/asf/utils/safe.py
labs/panopticon/tests/test_safe.py
Modified:
labs/panopticon/requirements.txt
Modified: labs/panopticon/requirements.txt
URL: http://svn.apache.org/viewvc/labs/panopticon/requirements.txt?rev=1532012&r1=1532011&r2=1532012&view=diff
==============================================================================
--- labs/panopticon/requirements.txt (original)
+++ labs/panopticon/requirements.txt Mon Oct 14 18:17:06 2013
@@ -5,5 +5,6 @@ Flask-Principal
keyring==1.6.1
mock
nose
+PyCrypto
python-ldap
restkit
Added: labs/panopticon/src/asf/utils/safe.py
URL: http://svn.apache.org/viewvc/labs/panopticon/src/asf/utils/safe.py?rev=1532012&view=auto
==============================================================================
--- labs/panopticon/src/asf/utils/safe.py (added)
+++ labs/panopticon/src/asf/utils/safe.py Mon Oct 14 18:17:06 2013
@@ -0,0 +1,119 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+"""
+A secure in-memory store
+"""
+from base64 import b64encode, b64decode
+import hashlib
+from threading import Thread, Lock, Event
+import datetime
+from time import sleep
+
+from Crypto.Cipher import AES
+from Crypto import Random
+
+
+class DefaultStore(object):
+ def __init__(self, lifetime=5 * 60, period=10):
+ self.store = {}
+ self.lifetimes = {}
+ self.lifetime = lifetime
+ self.period = period
+ self.lock = Lock()
+ self.stop = Event()
+
+ def clean():
+ while not self.stop.is_set():
+ keys_to_delete = set()
+ with self.lock:
+ for key, enddate in self.lifetimes.iteritems():
+ if enddate < datetime.datetime.now():
+ keys_to_delete.add(key)
+
+ for key in keys_to_delete:
+ del self.store[key]
+ del self.lifetimes[key]
+
+ sleep(self.period)
+
+ cleaner = Thread(target=clean)
+ cleaner.daemon = True
+ cleaner.start()
+
+ def __del__(self):
+ self.stop.set()
+
+ def set(self, key, value):
+ with self.lock:
+ self.store[key] = value
+ self.lifetimes[key] = datetime.datetime.now() + datetime.timedelta(seconds=self.lifetime)
+
+ def get(self, key):
+ with self.lock:
+ self.lifetimes[key] = datetime.datetime.now() + datetime.timedelta(seconds=self.lifetime)
+ return self.store[key]
+
+
+class DefaultCodec(object):
+ SEPARATOR = ':'
+
+ def encrypt(self, data, key):
+ iv = Random.new().read(AES.block_size)
+ data = b64encode(data)
+ padded_data = data + ((16 - len(data) % 16) * b' ')
+
+ return b64encode(iv) + DefaultCodec.SEPARATOR + b64encode(AES.new(key, AES.MODE_CBC, iv).encrypt(padded_data))
+
+ def decrypt(self, data, key):
+ iv, data = data.split(DefaultCodec.SEPARATOR)
+ iv = b64decode(iv)
+ data = b64decode(data)
+
+ try:
+ return b64decode(AES.new(key, AES.MODE_CBC, iv).decrypt(data))
+ except TypeError:
+ raise ValueError('Invalid encryption key')
+
+
+def default_key_generator(key_size=32):
+ return Random.new().read(key_size)
+
+
+class Safe(object):
+ def __init__(self, store=None, codec=None, key_generator=default_key_generator):
+ self.store = store or DefaultStore()
+ self.codec = codec or DefaultCodec()
+ self.key_generator = key_generator
+
+ def set(self, key, value):
+ encryption_key = self.key_generator()
+ encrypted_value = self.codec.encrypt(value, encryption_key)
+
+ self.store.set(key, (encrypted_value, hashlib.sha224(value).hexdigest()))
+
+ return encryption_key
+
+ def get(self, key, encryption_key):
+ encrypted_value, sha224 = self.store.get(key)
+ decrypted_value = self.codec.decrypt(encrypted_value, encryption_key)
+
+ if not hashlib.sha224(decrypted_value).hexdigest() == sha224:
+ raise ValueError('Invalid encryption key')
+
+ return decrypted_value
Added: labs/panopticon/tests/test_safe.py
URL: http://svn.apache.org/viewvc/labs/panopticon/tests/test_safe.py?rev=1532012&view=auto
==============================================================================
--- labs/panopticon/tests/test_safe.py (added)
+++ labs/panopticon/tests/test_safe.py Mon Oct 14 18:17:06 2013
@@ -0,0 +1,69 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+from time import sleep
+
+from nose.tools import assert_equals, assert_raises
+
+from asf.utils.safe import DefaultStore, DefaultCodec, default_key_generator, Safe
+
+
+def test_default_store():
+ store = DefaultStore()
+
+ store.set('foo', 'bar')
+
+ assert_equals('bar', store.get('foo'))
+
+
+def test_default_store():
+ store = DefaultStore(lifetime=1, period=1)
+
+ store.set('foo', 'bar')
+
+ sleep(3)
+
+ try:
+ store.get('foo')
+ assert False, 'KeyError should have been thrown for expired entry'
+ except KeyError:
+ pass
+
+
+def test_default_codec():
+ codec = DefaultCodec()
+
+ key = default_key_generator()
+ data = 'How now brown cow!'
+
+ assert_equals(data, codec.decrypt(codec.encrypt('How now brown cow!', key), key))
+
+
+def test_safe():
+ safe = Safe()
+ data = 'How now brown cow!'
+
+ encryption_key = safe.set('foo', data)
+
+ assert_equals(data, safe.get('foo', encryption_key))
+
+ try:
+ safe.get('foo', default_key_generator())
+ assert False, 'ValueError should have been thrown for invalid encryption_key'
+ except ValueError:
+ pass
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@labs.apache.org
For additional commands, e-mail: commits-help@labs.apache.org