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