Skip to content

Commit f37c98c

Browse files
authored
Merge pull request #44 from AzureAD/linux-libsecret
Encryption on Linux based on libsecret
2 parents 5d22932 + 88d37b7 commit f37c98c

File tree

13 files changed

+582
-166
lines changed

13 files changed

+582
-166
lines changed

.pylintrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[MESSAGES CONTROL]
2+
good-names=
3+
logger
24
disable=
5+
trailing-newlines,
36
useless-object-inheritance

.travis.yml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,35 @@ matrix:
66
- python: "2.7"
77
env: TOXENV=py27 PYPI=true
88
os: linux
9+
before_install:
10+
- sudo apt update
11+
- sudo apt install python-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1
912
- python: "3.5"
1013
env: TOXENV=py35
1114
os: linux
15+
before_install:
16+
- sudo apt update
17+
- sudo apt install python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1
1218
- python: "3.6"
1319
env: TOXENV=py36
1420
os: linux
21+
before_install:
22+
- sudo apt update
23+
- sudo apt install python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1
1524
- python: "3.7"
1625
env: TOXENV=py37
1726
os: linux
1827
dist: xenial
28+
before_install:
29+
- sudo apt update
30+
- sudo apt install python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1
1931
- python: "3.8"
2032
env: TOXENV=py38
2133
os: linux
2234
dist: xenial
35+
before_install:
36+
- sudo apt update
37+
- sudo apt install python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1
2338
- name: "Python 3.7 on macOS"
2439
env: TOXENV=py37
2540
os: osx
@@ -46,7 +61,8 @@ install:
4661
- pip install .
4762

4863
script:
49-
- pylint msal_extensions
64+
- # Difficult to get .pylintrc working on both Python 2 & 3, and we don't have to
65+
- if [ "$TOXENV" = "py37"]; then pylint msal_extensions; fi
5066
- tox
5167

5268
deploy:

msal_extensions/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,18 @@
33

44
import sys
55

6+
from .persistence import (
7+
FilePersistence,
8+
FilePersistenceWithDataProtection,
9+
KeychainPersistence,
10+
LibsecretPersistence,
11+
)
12+
from .cache_lock import CrossPlatLock
13+
from .token_cache import PersistedTokenCache
14+
615
if sys.platform.startswith('win'):
716
from .token_cache import WindowsTokenCache as TokenCache
817
elif sys.platform.startswith('darwin'):
918
from .token_cache import OSXTokenCache as TokenCache
1019
else:
11-
from .token_cache import UnencryptedTokenCache as TokenCache
20+
from .token_cache import FileTokenCache as TokenCache

msal_extensions/cache_lock.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ def __exit__(self, *args):
3434
# likely that another process has raced this one and ended up clearing or locking the
3535
# file for itself.
3636
os.remove(self._lockpath)
37-
except OSError as ex:
37+
except OSError as ex: # pylint: disable=invalid-name
3838
if ex.errno != errno.ENOENT and ex.errno != errno.EACCES:
3939
raise

msal_extensions/libsecret.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""Implements a Linux specific TokenCache, and provides auxiliary helper types.
2+
3+
This module depends on PyGObject. But `pip install pygobject` would typically fail,
4+
until you install its dependencies first. For example, on a Debian Linux, you need::
5+
6+
sudo apt install libgirepository1.0-dev libcairo2-dev python3-dev gir1.2-secret-1
7+
pip install pygobject
8+
9+
Alternatively, you could skip Cairo & PyCairo, but you still need to do all these
10+
(derived from https://gitlab.gnome.org/GNOME/pygobject/-/issues/395)::
11+
12+
sudo apt install libgirepository1.0-dev python3-dev gir1.2-secret-1
13+
pip install wheel
14+
PYGOBJECT_WITHOUT_PYCAIRO=1 pip install --no-build-isolation pygobject
15+
"""
16+
import logging
17+
18+
import gi # https://pygobject.readthedocs.io/en/latest/getting_started.html
19+
20+
# pylint: disable=no-name-in-module
21+
gi.require_version("Secret", "1") # Would require a package gir1.2-secret-1
22+
# pylint: disable=wrong-import-position
23+
from gi.repository import Secret # Would require a package gir1.2-secret-1
24+
25+
26+
logger = logging.getLogger(__name__)
27+
28+
class LibSecretAgent(object):
29+
"""A loader/saver built on top of low-level libsecret"""
30+
# Inspired by https://developer.gnome.org/libsecret/unstable/py-examples.html
31+
def __init__( # pylint: disable=too-many-arguments
32+
self,
33+
schema_name,
34+
attributes, # {"name": "value", ...}
35+
label="", # Helpful when visualizing secrets by other viewers
36+
attribute_types=None, # {name: SchemaAttributeType, ...}
37+
collection=None, # None means default collection
38+
): # pylint: disable=bad-continuation
39+
"""This agent is built on top of lower level libsecret API.
40+
41+
Content stored via libsecret is associated with a bunch of attributes.
42+
43+
:param string schema_name:
44+
Attributes would conceptually follow an existing schema.
45+
But this class will do it in the other way around,
46+
by automatically deriving a schema based on your attributes.
47+
However, you will still need to provide a schema_name.
48+
load() and save() will only operate on data with matching schema_name.
49+
50+
:param dict attributes:
51+
Attributes are key-value pairs, represented as a Python dict here.
52+
They will be used to filter content during load() and save().
53+
Their arbitrary keys are strings.
54+
Their arbitrary values can MEAN strings, integers and booleans,
55+
but are always represented as strings, according to upstream sample:
56+
https://developer.gnome.org/libsecret/0.18/py-store-example.html
57+
58+
:param string label:
59+
It will not be used during data lookup and filtering.
60+
It is only helpful when/if you visualize secrets by other viewers.
61+
62+
:param dict attribute_types:
63+
Each key is the name of your each attribute.
64+
The corresponding value will be one of the following three:
65+
66+
* Secret.SchemaAttributeType.STRING
67+
* Secret.SchemaAttributeType.INTEGER
68+
* Secret.SchemaAttributeType.BOOLEAN
69+
70+
But if all your attributes are Secret.SchemaAttributeType.STRING,
71+
you do not need to provide this types definition at all.
72+
73+
:param collection:
74+
The default value `None` means default collection.
75+
"""
76+
self._collection = collection
77+
self._attributes = attributes or {}
78+
self._label = label
79+
self._schema = Secret.Schema.new(schema_name, Secret.SchemaFlags.NONE, {
80+
k: (attribute_types or {}).get(k, Secret.SchemaAttributeType.STRING)
81+
for k in self._attributes})
82+
83+
def save(self, data):
84+
"""Store data. Returns a boolean of whether operation was successful."""
85+
return Secret.password_store_sync(
86+
self._schema, self._attributes, self._collection, self._label,
87+
data, None)
88+
89+
def load(self):
90+
"""Load a password in the secret service, return None when found nothing"""
91+
return Secret.password_lookup_sync(self._schema, self._attributes, None)
92+
93+
def clear(self):
94+
"""Returns a boolean of whether any passwords were removed"""
95+
return Secret.password_clear_sync(self._schema, self._attributes, None)
96+
97+
98+
def trial_run():
99+
"""This trial run will raise an exception if libsecret is not functioning.
100+
101+
Even after you installed all the dependencies so that your script can start,
102+
or even if your previous run was successful, your script could fail next time,
103+
for example when it will be running inside a headless SSH session.
104+
105+
You do not have to do trial_run. The exception would also be raised by save().
106+
"""
107+
try:
108+
agent = LibSecretAgent("Test Schema", {"attr1": "foo", "attr2": "bar"})
109+
payload = "Test Data"
110+
agent.save(payload) # It would fail when running inside an SSH session
111+
assert agent.load() == payload # This line is probably not reachable
112+
agent.clear()
113+
except (gi.repository.GLib.Error, AssertionError):
114+
message = (
115+
"libsecret did not perform properly. Please refer to "
116+
"https://github.com/AzureAD/microsoft-authentication-extensions-for-python/wiki/Encryption-on-Linux") # pylint: disable=line-too-long
117+
logger.exception(message) # This log contains trace stack for debugging
118+
logger.warning(message) # This is visible by default
119+
raise
120+

0 commit comments

Comments
 (0)