Skip to content

Commit 7708606

Browse files
committed
Proof-of-Concept: an encryption wrapper based on libsecret
1 parent 5d22932 commit 7708606

File tree

4 files changed

+139
-1
lines changed

4 files changed

+139
-1
lines changed

.pylintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
[MESSAGES CONTROL]
22
disable=
3+
trailing-newlines,
34
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/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+

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
install_requires=[
2020
'msal>=0.4.1,<2.0.0',
2121
'portalocker~=1.6',
22+
"pygobject>=3,<4;platform_system=='Linux'",
2223
],
2324
tests_require=['pytest'],
2425
)

0 commit comments

Comments
 (0)