Commits

dani84bs committed 7231605

Osx Keyring implemented

Comments (0)

Files changed (4)

conversa/passwords/__init__.py

+#-*- coding: utf-8 -*-
+# Copyright (c) 2012 Conversa and other contributors,
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+# 
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""Main password conversa package."""

conversa/passwords/osx.py

+#-*- coding: utf-8 -*-
+# Copyright (c) 2012 Conversa and other contributors,
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+# 
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""Conversa osx keyring module."""
+
+from ctypes import *
+from ctypes.util import find_library
+
+_security = cdll.LoadLibrary(find_library('Security'))
+_core = cdll.LoadLibrary(find_library('CoreService'))
+
+#Constants
+ErrSecItemNotFound = -25300
+
+
+def args_not_none(fn):
+    """Decorator that checks for not None parameters."""
+    def _fn_call(*args, **kwargs):
+        if any(arg is None for arg in args):
+            raise ValueError("Arguments can't be None")
+        return fn(*args, **kwargs)
+    return _fn_call
+
+
+class OsxKeychain(object):
+    """Represent a Mac OSX KeyChain."""
+    def __init__(self, name='login.keychain'):
+        """Arguments:
+        - name: The keychain name (it's a POSIX path)
+        """
+        self.kc = c_void_p()
+        if _security.SecKeychainOpen(name, byref(self.kc)):
+            raise OSError("Can't open %s keychain", name)
+
+    @args_not_none
+    def get_password(self, service, username):
+        """
+        Retrieve the password from the keychain for
+        the given service, username.
+        """
+        l = c_uint32()
+        tmp = c_void_p()
+        res = _security.SecKeychainFindGenericPassword(self.kc, len(service),
+                                                       service, len(username),
+                                                       username, byref(l),
+                                                       byref(tmp), None)
+        #No entry for service, username
+        if res == ErrSecItemNotFound:
+            return None
+        #Not implemented error
+        if res:
+            raise OSError()
+        #Password is present
+        res = create_string_buffer(l.value)
+        memmove(res, tmp.value, l.value)
+        _security.SecKeychainItemFreeContent(None, tmp)
+        return res.raw
+
+    @args_not_none
+    def set_password(self, service, username, password):
+        """
+        Set password for service and username in the keychain,
+        raise if the password is already set.
+        """
+        l = c_uint32()
+        tmp = c_void_p()
+        #Check if the password is present
+        res = _security.SecKeychainFindGenericPassword(self.kc, len(service),
+                                                       service, len(username),
+                                                       username, byref(l),
+                                                       byref(tmp), None)
+        #The password is already in the keychain
+        if res != ErrSecItemNotFound:
+            _security.SecKeychainItemFreeContent(None, tmp)
+            raise OSError()
+
+        res = _security.SecKeychainAddGenericPassword(self.kc,
+                                                      len(service), service,
+                                                      len(username), username,
+                                                      len(password), password,
+                                                      None)
+        if res:
+            raise OSError()
+
+    @args_not_none
+    def delete_password(self, service, username):
+        """
+        Delete the entry for service, username and raises if not present.
+        """
+        l = c_uint32()
+        tmp = c_void_p()
+        #Check if the password is present
+        res = _security.SecKeychainFindGenericPassword(self.kc, len(service),
+                                                       service, len(username),
+                                                       username, None,
+                                                       None,
+                                                       byref(tmp))
+        #Password not present in the KeyChain
+        if res == ErrSecItemNotFound:
+            raise OSError()
+        _security.SecKeychainItemDelete(tmp)
+        _core.CFRelease(tmp)

conversa/passwords/test/__init__.py

+#-*- coding: utf-8 -*-
+# Copyright (c) 2012 Conversa and other contributors,
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+# 
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""Conversa password tests."""

conversa/passwords/test/test_osx.py

+#-*- coding: utf-8 -*-
+# Copyright (c) 2012 Conversa and other contributors,
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+# 
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""Osx keyring tests."""
+import unittest
+import uuid
+from conversa.passwords import osx
+
+
+class OsxKeyChainTest(unittest.TestCase):
+    """Test case for conversa.passwords.osx."""
+    def setUp(self):
+        """Generate random (and uniques) service, username and password."""
+        self.service = str(uuid.uuid1())
+        self.name = str(uuid.uuid1())
+        self.password = str(uuid.uuid1())
+
+    def test_creation(self):
+        """Test case for conversa.passwords.osx OsxKeychain creation."""
+        oskc = osx.OsxKeychain()
+        self.assertIsNotNone(oskc.kc)
+
+    def test_get_pass_raises(self):
+        """Test case for get_password ValueError."""
+        name = str(uuid.uuid1())
+        oskc = osx.OsxKeychain()
+        with self.assertRaises(ValueError):
+            oskc.get_password(None, name)
+        with self.assertRaises(ValueError):
+            oskc.get_password(name, None)
+
+    def test_set_pass_raises(self):
+        """Test case for set_password ValueError."""
+        name = str(uuid.uuid1())
+        oskc = osx.OsxKeychain()
+        with self.assertRaises(ValueError):
+            oskc.set_password(None, name, name)
+        with self.assertRaises(ValueError):
+            oskc.get_password(name, None, name)
+        with self.assertRaises(ValueError):
+            oskc.get_password(name, name, None)
+
+    def test_delete_pass_raises(self):
+        """Test case for delete_password ValueError."""
+        name = str(uuid.uuid1())
+        oskc = osx.OsxKeychain()
+        with self.assertRaises(ValueError):
+            oskc.delete_password(None, name)
+        with self.assertRaises(ValueError):
+            oskc.delete_password(name, None)
+
+    def test_get_pass_fail(self):
+        """Test get_password with a non present password."""
+        name = str(uuid.uuid1())
+        service = str(uuid.uuid1())
+        oskc = osx.OsxKeychain()
+        p = oskc.get_password(service, name)
+        self.assertIsNone(p)
+
+    def test_set_get_del(self):
+        """
+        Integration test for set_password, get_password and
+        delete_password.
+        """
+        oskc = osx.OsxKeychain()
+        oskc.set_password(self.service, self.name, self.password)
+        p = oskc.get_password(self.service, self.name)
+        self.assertEqual(p, self.password)
+        oskc.delete_password(self.service, self.name)
+        p = oskc.get_password(self.service, self.name)
+        self.assertIsNone(p)