Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
17a43f3
Modernize SnapPass: client-side encryption, remove legacy UI dependen…
neuhaus Jul 27, 2026
98dd156
Fix tests: inject SECRET_KEY into tox environment
neuhaus Jul 27, 2026
fe1c173
Tests: add coverage for routing edge cases and missing lines
neuhaus Jul 27, 2026
85e381a
Tests: Fix flake8 linting errors in tests and main.py
neuhaus Jul 27, 2026
c2f5308
fix for E402
neuhaus Jul 27, 2026
be03fea
remove old paths from ignore list
neuhaus Jul 28, 2026
6bfff61
Modernize SSDLC, add Python 3.11-3.15 to CI, and add mypy typing
neuhaus Jul 28, 2026
d0c80ac
Allow prereleases in CI for Python 3.15 beta
neuhaus Jul 28, 2026
b9ccfa5
Fix flake8 line length issues introduced by typing
neuhaus Jul 28, 2026
82bf36c
Modernize CI/CD, Makefile, and Docker configs, and remove dead code
neuhaus Jul 28, 2026
61be288
Integrate SSDLC tooling (Ruff, pip-audit, Trivy, Hadolint), update CO…
neuhaus Jul 28, 2026
bb9f69c
Refactor Dockerfile to satisfy Hadolint best practices
neuhaus Jul 28, 2026
77370fd
Refactor: Extract database layer into storage.py and fix confirm.html…
neuhaus Jul 28, 2026
87a1de6
build: consolidate packaging in pyproject.toml and bump dependencies
neuhaus Jul 28, 2026
fd5d18d
chore: finalize 1.7.0 modernization and documentation
neuhaus Jul 28, 2026
132f5b3
fix: satisfy strict mypy union types for redis-py 5.x
neuhaus Jul 28, 2026
ec191a9
Restrict Redis commands using ACLs
neuhaus Jul 30, 2026
84ddf5f
Enhance Redis container security and persistence
neuhaus Jul 30, 2026
7ac5db5
Revert manual privilege drop to fix Redis volume permissions
neuhaus Jul 30, 2026
d7a0f4e
Enforce security and size limits
neuhaus Jul 30, 2026
3dd417e
fix for line too long
neuhaus Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[bumpversion]
current_version = 1.7.0
commit = True
tag = True

[bumpversion:file:pyproject.toml]
search = version = "{current_version}"
replace = version = "{new_version}"

[bumpversion:file:snappass/__init__.py]
search = __version__ = '{current_version}'
replace = __version__ = '{new_version}'
4 changes: 0 additions & 4 deletions .github/codeql-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,3 @@ name: "CodeQL config"

paths-ignore:
- tests.py
- 'snappass/static/bootstrap/**'
- 'snappass/static/clipboardjs/**'
- 'snappass/static/fontawesome/**'
- 'snappass/static/jquery/**'
30 changes: 27 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10']
python-version: ['3.10', '3.14', '3.15']

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
- uses: actions/cache@v4
with:
path: ~/.cache/pip
Expand All @@ -30,8 +31,31 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install tox tox-gh-actions
- name: Lint
- name: Security & Linting
if: matrix.python-version == '3.10'
run: tox -e flake8
run: |
tox -e ruff
tox -e pip-audit
tox -e mypy
- name: Tests
run: tox

docker-security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Hadolint
uses: hadolint/hadolint-action@v3.1.0
with:
dockerfile: Dockerfile
- name: Build Docker Image
run: docker build -t snappass:local .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'snappass:local'
format: 'table'
exit-code: '1'
ignore-unfixed: true
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'
4 changes: 0 additions & 4 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ on:
- tests.py
- '**/*.md'
- '**/*.rst'
- 'snappass/static/bootstrap/**'
- 'snappass/static/clipboardjs/**'
- 'snappass/static/fontawesome/**'
- 'snappass/static/jquery/**'

jobs:
analyze:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ htmlcov/
.coverage.*
.cache
.pytest_cache/
.ruff_cache/
.mypy_cache/

# virtualenv
venv/
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
Version 1.7.0
-------------
* **BREAKING:** Migrate encryption to client-side (browser) using AES-GCM and Web Crypto API.
The server only stores encrypted data and never sees the password.
* **BREAKING:** APIs now expect pre-encrypted secrets.
* Remove `cryptography` python dependency.
* Removed external JavaScript dependencies (jQuery, Bootstrap JS, Clipboard.js)
in favor of modern vanilla browser APIs.
* Security: Integrated Trivy and Hadolint into the Docker build pipeline.
* Security: Upgraded base Docker image to `python:3.14-slim-trixie` and added `pip-audit` to CI pipelines.
* Modernization: Consolidated all dependencies into `pyproject.toml` (removed legacy requirements.txt).
* Modernization: Enforced Python 3.10+ as the minimum supported version.
* Modernization: Bumped Flask to 3.1+, Flask-Babel to 4.0+, and redis-py to 5.0+.
* Modernization: Configured `bumpversion` to handle automated version releases.
* Fix: Extracted Redis database logic into a dedicated `storage.py` abstraction layer.

Version 1.6.0
-------------
* Drop support for officially unsupported Python versions (< Python 3.7)
Expand Down
47 changes: 28 additions & 19 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,12 @@ Here's how to set up ``snappass`` for local development.

$ git clone git@github.com:your_name_here/snappass.git

3. Install your local copy into a ``virtualenv``. Assuming you have
``virtualenvwrapper`` installed, this is how you set up your fork for local
development::
3. Install your local copy into a ``virtualenv``. It is recommended to use standard ``venv``::

$ mkvirtualenv snappass
$ python -m venv venv
$ source venv/bin/activate
$ cd snappass/
$ python setup.py develop
$ pip install -e .
$ make dev

4. Create a branch for local development::
Expand All @@ -92,29 +91,19 @@ Here's how to set up ``snappass`` for local development.

You now have a running instance on localhost:5000/

6. Please add some tests to tests.py and run tests::
6. Please add some tests to tests.py. When you're done making changes, check that your changes pass all tests and security linters (Ruff, Pip-Audit, etc.)::

$ make test

7. When you're done making changes, check that your changes pass the tests and
flake8::
(Tox will automatically run all linting, security scans, and tests with coverage output.)

$ flake8 snappass tests.py setup.py
$ tox

8. Check that the test coverage hasn't dropped::

$ coverage run --source snappass tests.py
$ coverage report -m
$ coverage html

9. Commit your changes and push your branch to GitHub::
7. Commit your changes and push your branch to GitHub::

$ git add .
$ git commit -m "Your detailed description of your changes."
$ git push origin name-of-your-bugfix-or-feature

10. Submit a pull request through the GitHub website.
8. Submit a pull request through the GitHub website.

Pull Request Guidelines
-----------------------
Expand All @@ -126,3 +115,23 @@ Before you submit a pull request, check that it meets these guidelines:
your new functionality into a function with a docstring, and add the
feature to the list in README.rst.
3. The pull request should work on all supported Python versions.

Releasing a New Version
-----------------------

This project uses `bumpversion <https://github.com/peritus/bumpversion>`_ to manage releases.
The `.bumpversion.cfg` file is configured to automatically update the version strings in both `pyproject.toml` and `snappass/__init__.py`, create a new git commit, and generate a git tag.

When you are ready to make a release, ensure you are on the main branch with a clean working directory, and run one of the following commands depending on the type of release (major, minor, or patch):

::

$ bumpversion patch # (e.g. 1.7.0 -> 1.7.1)
$ bumpversion minor # (e.g. 1.7.0 -> 1.8.0)
$ bumpversion major # (e.g. 1.7.0 -> 2.0.0)

Then, push the commit and the tags to GitHub:

::

$ git push origin master --tags
12 changes: 5 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.8-slim
FROM python:3.14-slim-trixie

ENV APP_DIR=/usr/src/snappass

Expand All @@ -8,14 +8,12 @@ RUN groupadd -r snappass && \

WORKDIR $APP_DIR

COPY ["setup.py", "requirements.txt", "MANIFEST.in", "README.rst", "AUTHORS.rst", "$APP_DIR/"]
COPY ["pyproject.toml", "README.rst", "AUTHORS.rst", "LICENSE", "$APP_DIR/"]
COPY ["./snappass", "$APP_DIR/snappass"]

RUN pip install -r requirements.txt

RUN pybabel compile -d snappass/translations

RUN python setup.py install && \
RUN pip install --no-cache-dir . && \
pybabel compile -d snappass/translations && \
pip install --no-cache-dir . && \
chown -R snappass $APP_DIR && \
chgrp -R snappass $APP_DIR

Expand Down
4 changes: 0 additions & 4 deletions MANIFEST.in

This file was deleted.

12 changes: 6 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
.PHONY: dev prod run test

dev: dev-requirements.txt
pip install -r dev-requirements.txt
dev:
pip install .[dev]

prod: requirements.txt
pip install -r requirements.txt
prod:
pip install .

run: prod
FLASK_DEBUG=1 FLASK_APP=snappass.main NO_SSL=True venv/bin/flask run
FLASK_DEBUG=1 FLASK_APP=snappass.main NO_SSL=True flask run

test:
PYTHONPATH=snappass venv/bin/nosetests -s tests
tox
45 changes: 22 additions & 23 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,16 @@ to do. Enjoy.
Security
--------

Passwords are encrypted using `Fernet`_ symmetric encryption, from the `cryptography`_ library.
A random unique key is generated for each password, and is never stored;
it is rather sent as part of the password link.
This means that even if someone has access to the Redis store, the passwords are still safe.

.. _Fernet: https://cryptography.io/en/latest/fernet/
.. _cryptography: https://cryptography.io/en/latest/
Passwords are encrypted purely on the client-side using the Web Crypto API (AES-GCM).
The server only ever receives and stores the encrypted ciphertext and a random IV.
The encryption key is generated in the browser and transported via the URL hash fragment (``#key``) which is **never** sent to the server.
This means that even if someone has access to the Redis store or the application logs, the passwords remain completely safe.

Requirements
------------

* `Redis`_
* Python 3.8+
* `Redis`_ 6.2.0 or newer
* Python 3.10+

.. _Redis: https://redis.io/

Expand All @@ -57,6 +54,7 @@ Installation
::

$ pip install snappass
$ export SECRET_KEY="<your-secret-key-here>"
$ snappass
* Running on http://0.0.0.0:5000/
* Restarting with reloader
Expand Down Expand Up @@ -120,26 +118,26 @@ Simple API

The advantage of using the simple API is that you can create a password and retrieve the link without having to open the web interface. This is useful if you want to embed it in a script or use it in a CI/CD pipeline.

To create a password, send a POST request to ``/api/set_password`` like so:
To create a password, send a POST request to ``/api/set_password`` like so. **Note:** The `password` field must be pre-encrypted and encoded in unpadded Base64URL.

::

$ curl -X POST -H "Content-Type: application/json" -d '{"password": "foobar"}' http://localhost:5000/api/set_password/
$ curl -X POST -H "Content-Type: application/json" -d '{"password": "<base64url-encoded-iv-and-ciphertext>"}' http://localhost:5000/api/set_password/

This will return a JSON response with the password link:

::

{
"link": "http://127.0.0.1:5000/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D",
"link": "http://127.0.0.1:5000/snappassd7x9aZ...",
"ttl":1209600
}

the default TTL is 2 weeks (1209600 seconds), but you can override it by adding a expiration parameter:

::

$ curl -X POST -H "Content-Type: application/json" -d '{"password": "foobar", "ttl": 3600 }' http://localhost:5000/api/set_password/
$ curl -X POST -H "Content-Type: application/json" -d '{"password": "<base64url-encoded-iv-and-ciphertext>", "ttl": 3600 }' http://localhost:5000/api/set_password/


REST API
Expand All @@ -152,24 +150,24 @@ This is useful if you want to embed it in a script, use it in a CI/CD pipeline
Create a password
"""""""""""""""""

To create a password, send a POST request to ``/api/v2/passwords`` like so:
To create a password, send a POST request to ``/api/v2/passwords`` like so. **Note:** As with the simple API, the payload must be pre-encrypted:

::

$ curl -X POST -H "Content-Type: application/json" -d '{"password": "foobar"}' http://localhost:5000/api/v2/passwords
$ curl -X POST -H "Content-Type: application/json" -d '{"password": "<base64url-encoded-iv-and-ciphertext>"}' http://localhost:5000/api/v2/passwords

This will return a JSON response with a token and the password link:

::

{
"token": "snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY=",
"token": "snappassd7x9aZ...",
"links": [{
"rel": "self",
"href": "http://127.0.0.1:5000/api/v2/passwords/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D",
"href": "http://127.0.0.1:5000/api/v2/passwords/snappassd7x9aZ..."
},{
"rel": "web-view",
"href": "http://127.0.0.1:5000/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D",
"href": "http://127.0.0.1:5000/snappassd7x9aZ..."
}],
"ttl":1209600
}
Expand All @@ -178,7 +176,7 @@ The default TTL is 2 weeks (1209600 seconds), but you can override it by adding

::

$ curl -X POST -H "Content-Type: application/json" -d '{"password": "foobar", "ttl": 3600 }' http://localhost:5000/api/v2/passwords
$ curl -X POST -H "Content-Type: application/json" -d '{"password": "<base64url-encoded-iv-and-ciphertext>", "ttl": 3600 }' http://localhost:5000/api/v2/passwords

If the password is null or empty, and the TTL is larger than the max TTL of the application, the API will return an error like this:

Expand Down Expand Up @@ -206,7 +204,7 @@ To check if a password exists, send a HEAD request to ``/api/v2/passwords/<token

::

$ curl --head http://localhost:5000/api/v2/passwords/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D
$ curl --head http://localhost:5000/api/v2/passwords/snappassd7x9aZ...

If :
- the passwork_key is valid
Expand Down Expand Up @@ -245,7 +243,7 @@ To read a password, send a GET request to ``/api/v2/passwords/<password_key>``,

::

$ curl -X GET http://localhost:5000/api/v2/passwords/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D
$ curl -X GET http://localhost:5000/api/v2/passwords/snappassd7x9aZ...

If :
- the token is valid
Expand All @@ -259,7 +257,7 @@ Then the API will return a 200 (OK) with a JSON response containing the password
::

{
"password": "foobar"
"password": "<base64url-encoded-iv-and-ciphertext>"
}

Otherwise, the API will return a 404 (Not Found) response like so:
Expand All @@ -279,6 +277,7 @@ Notes on APIs

Notes:

- **Breaking Change in 1.7.0:** The backend no longer handles encryption. The ``password`` field sent to any API endpoint MUST be pre-encrypted by the client using AES-128-GCM. The payload must consist of a 12-byte IV concatenated with the ciphertext, and then encoded using unpadded Base64URL. The encryption key should never be sent to the server, and should instead be appended to the generated link as a URL hash fragment (``#``).
- When using the APIs, you can specify any ttl, as long as it is lower than the default.
- The password is passed in the body of the request rather than in the URL. This is to prevent the password from being logged in the server logs.
- Depending on the environment you are running it, you might want to expose the ``/api`` endpoint to your internal network only, and put the web interface behind authentication.
Expand All @@ -296,7 +295,7 @@ Alternatively, you can use `Docker`_ and `Docker Compose`_ to install and run Sn

$ docker-compose up -d

This will pull all dependencies, i.e. Redis and appropriate Python version (3.7), then start up SnapPass and Redis server. SnapPass server is accessible at: http://localhost:5000
This will pull all dependencies, i.e. Redis and appropriate Python version (3.14), then start up SnapPass and Redis server. SnapPass server is accessible at: http://localhost:5000

Similar Tools
-------------
Expand Down
9 changes: 0 additions & 9 deletions dev-requirements.txt

This file was deleted.

Loading
Loading