Skip to content

Commit 6af4b78

Browse files
authored
Merge pull request #4 from rbrich/windows
Windows support
2 parents 616f9ff + 81b79de commit 6af4b78

29 files changed

+885
-703
lines changed

.github/workflows/python-app.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
sudo apt-get install --no-install-recommends -y wamerican
3131
python -m pip install --upgrade pip
3232
pip install flake8 twine wheel tox
33-
pip install -r requirements.txt -r test-requirements.txt -r docs/requirements.txt
33+
pip install -r requirements.txt
3434
3535
- name: Lint with flake8
3636
run: |
@@ -42,8 +42,10 @@ jobs:
4242
- name: Build and check package
4343
run: make check
4444

45-
- name: Build zipapp
46-
run: make zipapp
45+
- name: Build and check zipapp
46+
run: |
47+
make zipapp
48+
build/keybox.pyz pwgen
4749
4850
- name: Build docs
4951
run: make -C docs html

Makefile

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ dist/keybox-$(VERSION).tar.gz:
1212

1313
$(BUILD)/keybox.pyz: keybox
1414
rm -rf $(ZIPAPP)
15-
mkdir -p $(ZIPAPP)/keybox
16-
cp keybox/*.py $(ZIPAPP)/keybox
15+
mkdir -p $(ZIPAPP)
16+
cp -r keybox $(ZIPAPP)
1717
python3 -m zipapp $(ZIPAPP) -m 'keybox.main:main' -p '/usr/bin/env python3' -o $@
1818

1919
cryptoref: cryptoref/cryptoref.pyx
@@ -23,8 +23,7 @@ test:
2323
python3 setup.py pytest
2424

2525
.coverage: keybox tests .coveragerc
26-
env COVERAGE=1 coverage run --parallel-mode setup.py pytest
27-
coverage combine
26+
coverage run setup.py pytest
2827

2928
cov: .coverage
3029
coverage report --show-missing --fail-under=70

README.rst

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,94 +17,100 @@ using some other tool.
1717
Features:
1818

1919
- Data encrypted using strong encryption (PyNaCl)
20-
- Simple tab-delimited file format
20+
- Inside encrypted envelope, it's a simple tab-delimited file format
2121
- Shell-like text user interface
2222

2323
Security:
2424

2525
- Master password is saved in memory for as long as the program runs.
26-
- Neither the password nor decrypted data are ever written to disk.
26+
- Neither the password nor decrypted data are written to the disk (unless explicitly exported).
2727

2828
Portability:
2929

30-
- The script should run on any system with Python3 installed.
30+
- The script should run on any system with Python3 installed (including Windows).
3131
- Requires no installation. You can bring your keybox with you anywhere.
3232
- Can be contained in a single Python file (see `Static Distribution`_ below)
3333

3434
Dependencies:
3535

36-
- POSIX OS
3736
- Python 3.7 or later
38-
- PyNaCl, blessed, pyperclip
37+
- PyNaCl, prompt_toolkit, blessed, pyperclip
3938

4039

4140
Installation
4241
------------
4342

44-
Install Python package together with the ``keybox`` wrapper script,
43+
Install Python package, together with the ``keybox`` wrapper script,
4544
from PyPI::
4645

4746
pip3 install keybox
4847

4948
That's it. PIP should pull in the required dependencies.
5049

50+
From source / Git repo
51+
``````````````````````
52+
5153
Alternatively, install from source::
5254

5355
python3 setup.py install
5456

55-
The package can also be run directly, without installation::
57+
The package can also run without installation, directly from source tree root::
5658

5759
python3 -m keybox
5860

59-
Dependencies:
61+
Dependencies
62+
````````````
6063

61-
* ``/usr/share/dict/words``
64+
* `pynacl <https://pynacl.readthedocs.io/en/latest/install/>`_ - the encryption
6265

63-
- required for pwgen
64-
- Debian: ``apt install wamerican``
66+
* **argon2-cffi** - optional, replaces argon2 from PyNaCl when available
6567

66-
* blessed, pyperclip - terminal utility
68+
* **prompt_toolkit, blessed, pyperclip** - command-line and shell
6769

68-
* `pynacl <https://pynacl.readthedocs.io/en/latest/install/>`_
70+
* ``/usr/share/dict/words``
6971

70-
* argon2-cffi - optional, replaces argon2 from PyNaCl when available
72+
* used for password generator
73+
* Debian: ``apt install wamerican``
74+
* when not available, a replacement ``words`` file is downloaded from Internet
75+
(This is the only option on Windows)
7176

72-
* pytest, pexpect - for tests
77+
* **pytest, coverage** - for tests
7378

7479
Getting Started
7580
---------------
7681

7782
Run the program, choose a master password. A new keybox file will be created.
7883

79-
You are now in the shell. The basic workflow is as follows:
84+
You are now in the shell. The basic workflow uses the following commands:
8085

8186
- **add** some passwords
8287
- **list** the records
8388
- **select** a record
8489
- **print** the password
8590
- **quit**
8691

87-
Type **help** for a list of all commands.
92+
Type **help** for a list of all commands, **help <cmd>** for description of each command and its parameters.
8893

8994

9095
Config file
9196
-----------
9297

93-
The default config file path is `~/.keybox/keybox.conf`.
98+
The default config file path is ``~/.keybox/keybox.conf``.
9499
It can be used to point to a different location for the keybox file::
95100

96101
[keybox]
97102
path = ~/vcs/keybox/keybox.safe
98103

99-
The default path is `~/.keybox/keybox.safe`.
104+
Without the config file, the default keybox path is ``~/.keybox/keybox.safe``.
100105

101106

102107
Password Generator
103108
------------------
104109

105110
A bundled password generator can be called from command line (``keybox pwgen``)
106111
or internally from the shell.
107-
In the shell, try ``<tab>`` when asked for a password (in the ``add`` command).
112+
In the shell, use ``<tab>`` when asked for a password (in the ``add``/``modify`` commands)
113+
to generate some random passwords.
108114

109115
Pwgen is based on the system word list that is usually found in ``/usr/share/dict/words``.
110116
By default, it generates a password from two concatenated words, altered by
@@ -118,10 +124,10 @@ Static Distribution
118124
-------------------
119125

120126
Call ``make zipapp`` to create a `zipapp file <https://docs.python.org/3.5/library/zipapp.html#the-python-zip-application-archive-format>`_ containing all sources.
121-
The zipapp file is written to ``dist`` directory and is directly executable
127+
The zipapp file is written to ``build`` directory and is directly executable
122128
by Python.
123129

124-
The make target uses ``zipapp`` module which is available since Python 3.5.
130+
The Makefile target uses ``zipapp`` module which is available since Python 3.5.
125131

126132

127133
Development

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.4.1
1+
0.5.0

docs/keybox.rst

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,6 @@ keybox.keybox module
2525
:undoc-members:
2626
:show-inheritance:
2727

28-
keybox.memory module
29-
---------------------
30-
31-
.. automodule:: keybox.memory
32-
:members:
33-
:undoc-members:
34-
:show-inheritance:
35-
3628
keybox.pwgen module
3729
-------------------
3830

docs/requirements.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

keybox/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__all__ = ('envelope', 'envelope_gpg', 'fileformat', 'keybox', 'main', 'memory', 'pwgen',
1+
__all__ = ('datasafe', 'envelope', 'envelope_gpg', 'fileformat', 'keybox', 'main', 'pwgen',
22
'record', 'shell', 'stringutil', 'ui')

keybox/backend/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414
'crc32',
1515
# utility
1616
'randombytes',
17+
'SecureMemory',
18+
'lock_file',
19+
'timeout',
1720
)
1821

1922
# ordered by priority, the first one providing a function will be picked
20-
all_backend_names = ('cryptoref', 'argon2_cffi', 'pynacl', 'standard')
23+
all_backend_names = ('cryptoref', 'argon2_cffi', 'pynacl', 'os_unix', 'os_windows', 'standard')
2124

2225
symbol_provided_by = {
2326
'Argon2Params': ('argon2_cffi', 'pynacl'),
@@ -29,6 +32,9 @@
2932
'deflate_decompress': ('standard',),
3033
'crc32': ('standard',),
3134
'randombytes': ('pynacl', 'standard'),
35+
'SecureMemory': ('os_unix', 'os_windows'),
36+
'lock_file': ('os_unix', 'os_windows'),
37+
'timeout': ('os_unix', 'standard'),
3238
}
3339

3440
available_backends = ()

keybox/memory.py renamed to keybox/backend/os_unix.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import ctypes
22
from ctypes.util import find_library
3+
from ctypes import c_void_p, c_size_t, c_int
34
import sys
45
import os
56
import errno
67
import resource
8+
import fcntl
9+
import signal
10+
from contextlib import contextmanager
711

812
libc = ctypes.CDLL(find_library("c"), use_errno=True)
913

@@ -14,12 +18,19 @@ def memory_lock(addr, len):
1418
Encountering error while locking memory is not considered fatal,
1519
no exception is raised.
1620
21+
The memory page is locked until the process terminates.
22+
We cannot pair mlock/munlock safely without additional page management.
23+
From linux' mlock(2):
24+
> Memory locks do not stack, that is, pages which have been locked several times
25+
> by calls to mlock() or mlockall() will be unlocked by a single call to munlock()
26+
> for the corresponding range.
27+
1728
"""
1829
# Set MEMLOCK soft limit to maximum
1930
limits = resource.getrlimit(resource.RLIMIT_MEMLOCK)
2031
resource.setrlimit(resource.RLIMIT_MEMLOCK, (limits[1], limits[1]))
2132
try:
22-
rc = libc.mlock(ctypes.c_void_p(addr), len)
33+
rc = libc.mlock(c_void_p(addr), c_size_t(len))
2334
except OSError as e:
2435
print("Warning: Unable to lock memory.", str(e))
2536
return
@@ -33,20 +44,9 @@ def memory_lock(addr, len):
3344
print("Error (mlock):", errno.errorcode[err], os.strerror(err))
3445

3546

36-
def memory_unlock(addr, len):
37-
try:
38-
rc = libc.munlock(ctypes.c_void_p(addr), len)
39-
except OSError as e:
40-
print("Warning: Unable to unlock memory.", str(e))
41-
return
42-
if rc == -1: # pragma: no cover
43-
err = ctypes.get_errno()
44-
print("Error (munlock):", errno.errorcode[err], os.strerror(err))
45-
46-
4747
def memory_clear(addr, len):
4848
try:
49-
rc = libc.memset(ctypes.c_void_p(addr), 0, len)
49+
rc = libc.memset(c_void_p(addr), c_int(0), c_size_t(len))
5050
except OSError as e:
5151
print("Warning: Unable to clear memory.", str(e))
5252
return
@@ -60,7 +60,7 @@ class SecureMemory:
6060
"""Memlock the memory (do not allow swap).
6161
6262
Zero the memory in deleter. This is a little hacky,
63-
it depends on CPython and it's bytes object implementation.
63+
it depends on CPython and its bytes object implementation.
6464
6565
"""
6666

@@ -77,7 +77,6 @@ def __del__(self):
7777
# CPython assumption:
7878
# bytes object has header, followed by data and 1 byte terminator
7979
memory_clear(addr + (brutto - netto - 1), netto)
80-
memory_unlock(addr, brutto)
8180

8281
def __bytes__(self):
8382
return self._data
@@ -86,6 +85,23 @@ def __eq__(self, other):
8685
return self._data == bytes(other)
8786

8887

88+
def lock_file(fileobj):
89+
fcntl.lockf(fileobj.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
90+
91+
92+
@contextmanager
93+
def timeout(secs: int, handler):
94+
def sigalrm_handler(_signum, _frame):
95+
handler()
96+
orig_handler = signal.signal(signal.SIGALRM, sigalrm_handler)
97+
signal.alarm(int(secs))
98+
try:
99+
yield
100+
finally:
101+
signal.alarm(0)
102+
signal.signal(signal.SIGALRM, orig_handler)
103+
104+
89105
if __name__ == '__main__':
90106
def self_test():
91107
b = b"sensitive data"

0 commit comments

Comments
 (0)