Skip to content

Commit

Permalink
Added more control on password handling; Improved docs
Browse files Browse the repository at this point in the history
Details:

* Added more user control for the handling of passwords: The 'EasyVault' class
  now accepts that the password is not provided and in that case is restricted
  to operate on unencrypted vault files. The 'get_password()' function got an
  additional 'use_prompting' parameter that can be used to disable the
  interactive prompting for passwords. (issue #20)

* Improved the documentation in many places.

Signed-off-by: Andreas Maier <andreas.r.maier@gmx.de>
  • Loading branch information
andy-maier committed Mar 31, 2021
1 parent 1a4329a commit 49cd2c6
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 90 deletions.
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ can stay encrypted in the file system but can still be used by the program in
clear text.

At first use on a particular vault file, the encryption command prompts for a
vault password and stores that in the keyring facility of your local system
vault password and stores that in the keyring service of your local system
using the `keyring package`_. Subsequent encryption and decryption of the vault
file will then use the password from the keyring, avoiding any further password
prompts. Programmatic access can also be done with the password from the
Expand Down
12 changes: 12 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,24 @@ Released: not yet

**Incompatible changes:**

* The new optional 'use_prompting' parameter of 'easy_vault.get_password()' was
not added at the end of the parameter list. This is incompatible for users
who called the function with positional arguments. (related to issue #20)

**Deprecations:**

**Bug fixes:**

**Enhancements:**

* Added more user control for the handling of passwords: The 'EasyVault' class
now accepts that the password is not provided and in that case is restricted
to operate on unencrypted vault files. The 'get_password()' function got an
additional 'use_prompting' parameter that can be used to disable the
interactive prompting for passwords. (issue #20)

* Docs: Improved the documentation in many places.

**Cleanup:**

**Known issues:**
Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ def get_version(version_file):
'py': ('https://docs.python.org/2/', None), # agnostic to Python version
'py2': ('https://docs.python.org/2', None), # specific to Python 2
'py3': ('https://docs.python.org/3', None), # specific to Python 3
'keyring': ('https://keyring.readthedocs.io/en/stable', None),
}

intersphinx_cache_limit = 5
Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ can stay encrypted in the file system but can still be used by the program in
clear text.

At first use on a particular vault file, the encryption command prompts for a
vault password and stores that in the keyring facility of your local system
vault password and stores that in the keyring service of your local system
using the `keyring package`_. Subsequent encryption and decryption of the vault
file will then use the password from the keyring, avoiding any further password
prompts. Programmatic access can also be done with the password from the
Expand Down
20 changes: 20 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,23 @@ code:
The vault file does not need to be in YAML format; there are access functions
for accessing its raw content as a Byte string and as a Unicode string, too.


.. _`Keyring service`:

Keyring service
----------------

The **easy-vault** package accesses the keyring service of the local system
via the `keyring package`_. That package supports a number of different
keyring services and can be configured to use alternate keyring services.

By default, the following keyring services are active and will be used by
the keyring package:

* On macOS: `Keychain <https://en.wikipedia.org/wiki/Keychain_%28software%29>`_
* On Linux: depends
* On Windows: `Credential Locker <https://docs.microsoft.com/en-us/windows/uwp/security/credential-locker>`_

.. # Links:
.. _`keyring package`: https://github.com/jaraco/keyring/blob/main/README.rst
95 changes: 68 additions & 27 deletions easy_vault/_easy_vault.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ class EasyVaultException(Exception):

class EasyVaultFileError(EasyVaultException):
"""
Exception indicating file I/O errors with a vault file.
Exception indicating file I/O errors with a vault file or with a temporary
file (that is used when writing the vault file).
Derived from :exc:`EasyVaultException`.
"""
Expand Down Expand Up @@ -106,8 +107,13 @@ class EasyVault(object):
well suited for handling huge files. It is really meant for vault files:
Files that keep secrets, but not huge data.
The password is converted to a symmetric key which is then used for
encryption and decryption of the vault file.
The password may be provided or not (`None`). If not provided, encryption
and decryption of the vault file is going to be rejected and access to the
data requires that the vault file is unencrypted. If the password is
provided, it is converted to a symmetric key which is then used for
encryption and decryption of the vault file and for decrypting the vault
file content upon access (the vault file in the file system remains
encrypted).
The encryption package used by this class is pluggable. The default
implementation uses the symmetric key support from the
Expand All @@ -130,20 +136,24 @@ def __init__(self, filepath, password):
Path name of the vault file.
password (:term:`unicode string`):
Password for encrypting and decrypting the vault file.
Password for encrypting and decrypting the vault file, or `None`
if not provided.
"""
self._filepath = filepath
if not isinstance(password, six.string_types):
raise TypeError(
"The password argument is not a unicode string, but: {t}".
format(t=type(password)))
key = self.generate_key(password)
if not isinstance(key, six.binary_type):
raise TypeError(
"The return value of generate_key() is not a byte string, "
"but: {t}".
format(t=type(key)))
self._key = key
if password is None:
self._key = None
else:
if not isinstance(password, six.string_types):
raise TypeError(
"The password argument is not a unicode string, but: {t}".
format(t=type(password)))
key = self.generate_key(password)
if not isinstance(key, six.binary_type):
raise TypeError(
"The return value of generate_key() is not a byte string, "
"but: {t}".
format(t=type(key)))
self._key = key

@property
def filepath(self):
Expand All @@ -152,21 +162,29 @@ def filepath(self):
"""
return self._filepath

@property
def password_provided(self):
"""
bool: Indicates whether a vault password was provided.
"""
return self._key is not None

def is_encrypted(self):
"""
Test whether the vault file is encrypted by easy-vault.
This is done by checking for the unique easy-vault header in the first
line of the vault file.
This method does not require a vault password to be provided.
Returns:
bool: Boolean indicating whether the vault file is encrypted by
easy-vault.
Raises:
EasyVaultFileError: Error with the vault file
EasyVaultFileError: I/O error with the vault file
"""

try:
with open(self._filepath, 'rb') as fp:
first_bline = fp.readline() # Including trailing newline
Expand All @@ -175,7 +193,7 @@ def is_encrypted(self):
"Cannot open vault file {fn} for reading: {exc}".
format(fn=self._filepath, exc=exc))
new_exc.__cause__ = None
raise new_exc # VaultFileOpenError
raise new_exc # EasyVaultFileError

m = HEADER_PATTERN.match(first_bline)
if m is None:
Expand All @@ -190,10 +208,17 @@ def encrypt(self):
The vault file must be unencrypted (i.e. not encrypted by easy-vault).
This method requires a vault password to be provided.
Raises:
EasyVaultFileError: Error with the vault file or a temporary file
EasyVaultFileError: I/O error with the vault file or a temporary file
EasyVaultEncryptError: Error encrypting the vault file
"""
if not self.password_provided:
raise EasyVaultEncryptError(
"Cannot encrypt vault file {fn}: "
"No password was provided".
format(fn=self._filepath))

try:
with open(self._filepath, 'rb', encoding=None) as fp:
Expand Down Expand Up @@ -231,24 +256,31 @@ def decrypt(self):
The vault file must be encrypted by easy-vault.
This method requires a vault password to be provided.
Raises:
EasyVaultFileError: Error with the vault file or a temporary file
EasyVaultFileError: I/O error with the vault file or a temporary file
EasyVaultDecryptError: Error decrypting the vault file
"""
clear_bdata = self._get_bytes_from_encrypted()
write_file(self._filepath, clear_bdata)

def _get_bytes_from_encrypted(self):
"""
Get the unencrypted data from an encrypted vault file.
Get the data from an encrypted vault file.
Returns:
:term:`byte string`: Unencrypted data from the vault file.
Raises:
EasyVaultFileError: Error with the vault file
EasyVaultFileError: I/O error with the vault file
EasyVaultDecryptError: Error decrypting the vault file
"""
if not self.password_provided:
raise EasyVaultDecryptError(
"Cannot decrypt vault file {fn}: "
"No password was provided".
format(fn=self._filepath))

try:
with open(self._filepath, 'rb') as fp:
Expand Down Expand Up @@ -285,7 +317,7 @@ def _get_bytes_from_clear(self):
:term:`byte string`: Unencrypted data from the vault file.
Raises:
EasyVaultFileError: Error with the vault file
EasyVaultFileError: I/O error with the vault file
"""
try:
with open(self._filepath, 'rb') as fp:
Expand All @@ -304,12 +336,15 @@ def get_bytes(self):
The vault file may be encrypted or unencrypted.
If the vault file is encrypted, this method requires a vault password
to be provided, otherwise it does not.
Returns:
:term:`byte string`: Unencrypted content of the vault file, as a Byte
sequence.
Raises:
EasyVaultFileError: Error with the vault file
EasyVaultFileError: I/O error with the vault file
EasyVaultDecryptError: Error decrypting the vault file
"""
if self.is_encrypted():
Expand All @@ -322,12 +357,15 @@ def get_text(self):
The vault file may be encrypted or unencrypted.
If the vault file is encrypted, this method requires a vault password
to be provided, otherwise it does not.
Returns:
:term:`unicode string`: Unencrypted content of the vault file, as a
Unicode string.
Raises:
EasyVaultFileError: Error with the vault file
EasyVaultFileError: I/O error with the vault file
EasyVaultDecryptError: Error decrypting the vault file
"""
bdata = self.get_bytes()
Expand All @@ -341,11 +379,14 @@ def get_yaml(self):
The vault file may be encrypted or unencrypted.
If the vault file is encrypted, this method requires a vault password
to be provided, otherwise it does not.
Returns:
dict or list: Top-level object of the YAML-formatted vault file.
Raises:
EasyVaultFileError: Error with the vault file or a temporary file
EasyVaultFileError: I/O error with the vault file or a temporary file
EasyVaultYamlError: YAML syntax error in the vault file
EasyVaultDecryptError: Error decrypting the vault file
"""
Expand Down Expand Up @@ -470,7 +511,7 @@ def write_file(filepath, data):
The data to be written.
Raises:
EasyVaultFileError: Error with the vault file or a temporary file
EasyVaultFileError: I/O error with the file
"""
assert isinstance(data, six.binary_type), type(data)
try:
Expand Down

0 comments on commit 49cd2c6

Please sign in to comment.