sspwd 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sspwd-0.1.0/LICENSE +21 -0
- sspwd-0.1.0/PKG-INFO +150 -0
- sspwd-0.1.0/README.md +102 -0
- sspwd-0.1.0/pyproject.toml +103 -0
- sspwd-0.1.0/setup.cfg +4 -0
- sspwd-0.1.0/src/sspwd/__init__.py +8 -0
- sspwd-0.1.0/src/sspwd/__version__.py +5 -0
- sspwd-0.1.0/src/sspwd/cli.py +166 -0
- sspwd-0.1.0/src/sspwd/storage/__init__.py +3 -0
- sspwd-0.1.0/src/sspwd/storage/base.py +56 -0
- sspwd-0.1.0/src/sspwd/storage/sqlite.py +200 -0
- sspwd-0.1.0/src/sspwd/ui/__init__.py +3 -0
- sspwd-0.1.0/src/sspwd/ui/api.py +119 -0
- sspwd-0.1.0/src/sspwd/ui/server.py +128 -0
- sspwd-0.1.0/src/sspwd/ui/static/assets/index-BJnatssi.css +1 -0
- sspwd-0.1.0/src/sspwd/ui/static/assets/index-BYeGpcdS.js +9 -0
- sspwd-0.1.0/src/sspwd/ui/static/index.html +14 -0
- sspwd-0.1.0/src/sspwd/ui/static/vite.svg +1 -0
- sspwd-0.1.0/src/sspwd.egg-info/PKG-INFO +150 -0
- sspwd-0.1.0/src/sspwd.egg-info/SOURCES.txt +24 -0
- sspwd-0.1.0/src/sspwd.egg-info/dependency_links.txt +1 -0
- sspwd-0.1.0/src/sspwd.egg-info/entry_points.txt +2 -0
- sspwd-0.1.0/src/sspwd.egg-info/requires.txt +20 -0
- sspwd-0.1.0/src/sspwd.egg-info/top_level.txt +1 -0
- sspwd-0.1.0/tests/test_api.py +85 -0
- sspwd-0.1.0/tests/test_storage.py +92 -0
sspwd-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Yauheniya Varabyova
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sspwd-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sspwd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A local, encrypted password manager with a web UI.
|
|
5
|
+
Author: Yauheniya Varabyova
|
|
6
|
+
Maintainer: Yauheniya Varabyova
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Repository, https://github.com/yauheniya-ai/sspwd
|
|
9
|
+
Project-URL: Changelog, https://github.com/yauheniya-ai/sspwd/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: password,manager,security,encryption,cli,vault
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Natural Language :: English
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Security :: Cryptography
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: fastapi>=0.110
|
|
29
|
+
Requires-Dist: uvicorn[standard]>=0.29
|
|
30
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
31
|
+
Requires-Dist: pydantic>=2.0
|
|
32
|
+
Requires-Dist: click>=8.1
|
|
33
|
+
Requires-Dist: cryptography>=42.0
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: twine; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-mock; extra == "dev"
|
|
39
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
40
|
+
Requires-Dist: black>=22.0; extra == "dev"
|
|
41
|
+
Requires-Dist: flake8>=5.0; extra == "dev"
|
|
42
|
+
Requires-Dist: mypy>=0.991; extra == "dev"
|
|
43
|
+
Requires-Dist: pre-commit>=2.20.0; extra == "dev"
|
|
44
|
+
Requires-Dist: sphinx>=5.0; extra == "dev"
|
|
45
|
+
Requires-Dist: sphinx-rtd-theme>=1.0; extra == "dev"
|
|
46
|
+
Requires-Dist: myst-parser>=0.18; extra == "dev"
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
|
|
49
|
+
# sspwd – super secret password
|
|
50
|
+
|
|
51
|
+
A local, encrypted password manager with a built-in web UI.
|
|
52
|
+
|
|
53
|
+
Passwords are stored in `~/.sspwd/vault.db` — fully encrypted with a key
|
|
54
|
+
derived from your master password. Nothing leaves your machine.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install sspwd
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
> Requires Python ≥ 3.10.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Quick start
|
|
69
|
+
|
|
70
|
+
### Web UI
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
sspwd serve
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Opens `http://127.0.0.1:7523` in your default browser. Enter your master
|
|
77
|
+
password when prompted (a new vault is created automatically on first run).
|
|
78
|
+
|
|
79
|
+
### CLI
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Add an entry
|
|
83
|
+
sspwd add
|
|
84
|
+
|
|
85
|
+
# List all entries
|
|
86
|
+
sspwd list
|
|
87
|
+
|
|
88
|
+
# Search
|
|
89
|
+
sspwd list --search github
|
|
90
|
+
|
|
91
|
+
# Show a single entry (reveals password)
|
|
92
|
+
sspwd get 3
|
|
93
|
+
|
|
94
|
+
# Delete an entry
|
|
95
|
+
sspwd delete 3
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Custom vault location
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
sspwd serve --vault-dir /path/to/my/vault
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Security
|
|
107
|
+
|
|
108
|
+
| Detail | Value |
|
|
109
|
+
|---|---|
|
|
110
|
+
| Encryption | AES-128-CBC via [Fernet](https://cryptography.io/en/latest/fernet/) |
|
|
111
|
+
| Key derivation | PBKDF2-HMAC-SHA256, 390 000 iterations |
|
|
112
|
+
| Storage | SQLite (`~/.sspwd/vault.db`) |
|
|
113
|
+
| Key never stored | Only a random 32-byte salt is persisted |
|
|
114
|
+
|
|
115
|
+
The master password is required every time you start the app. There is no
|
|
116
|
+
"remember me" — the derived key lives only in process memory.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Development
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
git clone https://github.com/yauheniya-ai/sspwd
|
|
124
|
+
cd sspwd/pypi
|
|
125
|
+
|
|
126
|
+
# Install in editable mode with dev extras
|
|
127
|
+
pip install -e ".[dev]"
|
|
128
|
+
|
|
129
|
+
# Run tests
|
|
130
|
+
pytest
|
|
131
|
+
|
|
132
|
+
# Lint
|
|
133
|
+
ruff check src tests
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Building the React UI
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
cd ../frontend
|
|
140
|
+
npm install
|
|
141
|
+
npm run build
|
|
142
|
+
# Copy dist/ into pypi/src/sspwd/ui/static/
|
|
143
|
+
cp -r dist/* ../pypi/src/sspwd/ui/static/
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT — see [LICENSE](LICENSE).
|
sspwd-0.1.0/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# sspwd – super secret password
|
|
2
|
+
|
|
3
|
+
A local, encrypted password manager with a built-in web UI.
|
|
4
|
+
|
|
5
|
+
Passwords are stored in `~/.sspwd/vault.db` — fully encrypted with a key
|
|
6
|
+
derived from your master password. Nothing leaves your machine.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install sspwd
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
> Requires Python ≥ 3.10.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
### Web UI
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
sspwd serve
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Opens `http://127.0.0.1:7523` in your default browser. Enter your master
|
|
29
|
+
password when prompted (a new vault is created automatically on first run).
|
|
30
|
+
|
|
31
|
+
### CLI
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Add an entry
|
|
35
|
+
sspwd add
|
|
36
|
+
|
|
37
|
+
# List all entries
|
|
38
|
+
sspwd list
|
|
39
|
+
|
|
40
|
+
# Search
|
|
41
|
+
sspwd list --search github
|
|
42
|
+
|
|
43
|
+
# Show a single entry (reveals password)
|
|
44
|
+
sspwd get 3
|
|
45
|
+
|
|
46
|
+
# Delete an entry
|
|
47
|
+
sspwd delete 3
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Custom vault location
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
sspwd serve --vault-dir /path/to/my/vault
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Security
|
|
59
|
+
|
|
60
|
+
| Detail | Value |
|
|
61
|
+
|---|---|
|
|
62
|
+
| Encryption | AES-128-CBC via [Fernet](https://cryptography.io/en/latest/fernet/) |
|
|
63
|
+
| Key derivation | PBKDF2-HMAC-SHA256, 390 000 iterations |
|
|
64
|
+
| Storage | SQLite (`~/.sspwd/vault.db`) |
|
|
65
|
+
| Key never stored | Only a random 32-byte salt is persisted |
|
|
66
|
+
|
|
67
|
+
The master password is required every time you start the app. There is no
|
|
68
|
+
"remember me" — the derived key lives only in process memory.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Development
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
git clone https://github.com/yauheniya-ai/sspwd
|
|
76
|
+
cd sspwd/pypi
|
|
77
|
+
|
|
78
|
+
# Install in editable mode with dev extras
|
|
79
|
+
pip install -e ".[dev]"
|
|
80
|
+
|
|
81
|
+
# Run tests
|
|
82
|
+
pytest
|
|
83
|
+
|
|
84
|
+
# Lint
|
|
85
|
+
ruff check src tests
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Building the React UI
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
cd ../frontend
|
|
92
|
+
npm install
|
|
93
|
+
npm run build
|
|
94
|
+
# Copy dist/ into pypi/src/sspwd/ui/static/
|
|
95
|
+
cp -r dist/* ../pypi/src/sspwd/ui/static/
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
# ---------------------------------------------------------------------------
|
|
6
|
+
# Project metadata
|
|
7
|
+
# ---------------------------------------------------------------------------
|
|
8
|
+
[project]
|
|
9
|
+
name = "sspwd"
|
|
10
|
+
version = "0.1.0"
|
|
11
|
+
description = "A local, encrypted password manager with a web UI."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = {text = "MIT"}
|
|
14
|
+
authors = [
|
|
15
|
+
{name = "Yauheniya Varabyova"}
|
|
16
|
+
]
|
|
17
|
+
maintainers = [
|
|
18
|
+
{name = "Yauheniya Varabyova"}
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 3 - Alpha",
|
|
22
|
+
"Environment :: Console",
|
|
23
|
+
"Environment :: Web Environment",
|
|
24
|
+
"Intended Audience :: End Users/Desktop",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"Natural Language :: English",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Topic :: Security :: Cryptography",
|
|
34
|
+
"Topic :: Utilities",
|
|
35
|
+
]
|
|
36
|
+
keywords = ["password", "manager", "security", "encryption", "cli", "vault"]
|
|
37
|
+
requires-python = ">=3.10"
|
|
38
|
+
|
|
39
|
+
dependencies = [
|
|
40
|
+
# Web server
|
|
41
|
+
"fastapi>=0.110",
|
|
42
|
+
"uvicorn[standard]>=0.29",
|
|
43
|
+
"python-multipart>=0.0.9",
|
|
44
|
+
"pydantic>=2.0", # FastAPI uses Pydantic v2 for request/response models
|
|
45
|
+
# CLI
|
|
46
|
+
"click>=8.1",
|
|
47
|
+
# Encryption (PBKDF2 key derivation + Fernet AES)
|
|
48
|
+
"cryptography>=42.0",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[project.optional-dependencies]
|
|
52
|
+
dev = [
|
|
53
|
+
"twine",
|
|
54
|
+
"pytest>=7.0",
|
|
55
|
+
"pytest-cov>=4.0",
|
|
56
|
+
"pytest-mock",
|
|
57
|
+
"httpx>=0.27", # needed by FastAPI TestClient
|
|
58
|
+
"black>=22.0",
|
|
59
|
+
"flake8>=5.0",
|
|
60
|
+
"mypy>=0.991",
|
|
61
|
+
"pre-commit>=2.20.0",
|
|
62
|
+
"sphinx>=5.0",
|
|
63
|
+
"sphinx-rtd-theme>=1.0",
|
|
64
|
+
"myst-parser>=0.18",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[project.urls]
|
|
68
|
+
"Repository" = "https://github.com/yauheniya-ai/sspwd"
|
|
69
|
+
"Changelog" = "https://github.com/yauheniya-ai/sspwd/blob/main/CHANGELOG.md"
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Entry point — `sspwd` command becomes available after `pip install`
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
[project.scripts]
|
|
75
|
+
sspwd = "sspwd.cli:main"
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Package discovery
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
[tool.setuptools.packages.find]
|
|
81
|
+
where = ["src"]
|
|
82
|
+
|
|
83
|
+
# Bundle the pre-built React/Vite static files shipped inside the package.
|
|
84
|
+
[tool.setuptools.package-data]
|
|
85
|
+
"sspwd.ui" = ["static/**/*"]
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Test / coverage config
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
[tool.pytest.ini_options]
|
|
91
|
+
testpaths = ["tests"]
|
|
92
|
+
pythonpath = ["src"]
|
|
93
|
+
|
|
94
|
+
[tool.coverage.run]
|
|
95
|
+
source = ["sspwd"]
|
|
96
|
+
omit = ["*/tests/*", "*/__pycache__/*", "*/site-packages/*"]
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# Mypy
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
[tool.mypy]
|
|
102
|
+
python_version = "3.10"
|
|
103
|
+
ignore_missing_imports = true
|
sspwd-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for sspwd.
|
|
3
|
+
|
|
4
|
+
Usage
|
|
5
|
+
-----
|
|
6
|
+
sspwd serve # start the web UI (prompts for master password)
|
|
7
|
+
sspwd add # add a new entry interactively
|
|
8
|
+
sspwd list # list all entries
|
|
9
|
+
sspwd get <id> # show a single entry (password revealed)
|
|
10
|
+
sspwd delete <id> # delete an entry
|
|
11
|
+
sspwd version # print version and exit
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
|
|
20
|
+
from .__version__ import __version__
|
|
21
|
+
from .storage.sqlite import SQLiteStorage
|
|
22
|
+
from .storage.base import PasswordEntry
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Shared helpers
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_storage(master_password: str, vault_dir: Optional[Path] = None) -> SQLiteStorage:
|
|
31
|
+
return SQLiteStorage(master_password=master_password, vault_dir=vault_dir)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _prompt_master() -> str:
|
|
35
|
+
return click.prompt("Master password", hide_input=True)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Root group
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@click.group()
|
|
44
|
+
@click.version_option(__version__, prog_name="sspwd")
|
|
45
|
+
def cli() -> None:
|
|
46
|
+
"""sspwd — a local, encrypted password manager."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Commands
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@cli.command()
|
|
55
|
+
@click.option("--host", default="127.0.0.1", show_default=True, help="Bind address.")
|
|
56
|
+
@click.option("--port", default=7523, show_default=True, help="TCP port.")
|
|
57
|
+
@click.option("--no-browser", is_flag=True, default=False, help="Do not open browser.")
|
|
58
|
+
@click.option("--vault-dir", default=None, type=click.Path(), help="Custom vault directory.")
|
|
59
|
+
def serve(host: str, port: int, no_browser: bool, vault_dir: Optional[str]) -> None:
|
|
60
|
+
"""Start the web UI."""
|
|
61
|
+
from .ui.server import UIServer
|
|
62
|
+
|
|
63
|
+
master = _prompt_master()
|
|
64
|
+
storage = _get_storage(master, Path(vault_dir) if vault_dir else None)
|
|
65
|
+
server = UIServer(
|
|
66
|
+
storage=storage,
|
|
67
|
+
host=host,
|
|
68
|
+
port=port,
|
|
69
|
+
open_browser=not no_browser,
|
|
70
|
+
)
|
|
71
|
+
server.start(block=True)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@cli.command("add")
|
|
75
|
+
@click.option("--vault-dir", default=None, type=click.Path())
|
|
76
|
+
def add_entry(vault_dir: Optional[str]) -> None:
|
|
77
|
+
"""Add a new password entry interactively."""
|
|
78
|
+
master = _prompt_master()
|
|
79
|
+
storage = _get_storage(master, Path(vault_dir) if vault_dir else None)
|
|
80
|
+
|
|
81
|
+
title = click.prompt("Title")
|
|
82
|
+
username = click.prompt("Username")
|
|
83
|
+
password = click.prompt("Password", hide_input=True, confirmation_prompt=True)
|
|
84
|
+
url = click.prompt("URL (optional)", default="")
|
|
85
|
+
notes = click.prompt("Notes (optional)", default="")
|
|
86
|
+
|
|
87
|
+
entry = PasswordEntry(
|
|
88
|
+
id=None,
|
|
89
|
+
title=title,
|
|
90
|
+
username=username,
|
|
91
|
+
password=password,
|
|
92
|
+
url=url or None,
|
|
93
|
+
notes=notes or None,
|
|
94
|
+
)
|
|
95
|
+
created = storage.add(entry)
|
|
96
|
+
click.echo(click.style(f"✓ Saved (id={created.id})", fg="green"))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@cli.command("list")
|
|
100
|
+
@click.option("--search", "-s", default=None, help="Filter by title / username / URL.")
|
|
101
|
+
@click.option("--vault-dir", default=None, type=click.Path())
|
|
102
|
+
def list_entries(search: Optional[str], vault_dir: Optional[str]) -> None:
|
|
103
|
+
"""List all stored entries."""
|
|
104
|
+
master = _prompt_master()
|
|
105
|
+
storage = _get_storage(master, Path(vault_dir) if vault_dir else None)
|
|
106
|
+
|
|
107
|
+
entries = storage.list(search=search)
|
|
108
|
+
if not entries:
|
|
109
|
+
click.echo("No entries found.")
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
click.echo(f"\n{'ID':>4} {'Title':<30} {'Username':<25} URL")
|
|
113
|
+
click.echo("-" * 80)
|
|
114
|
+
for e in entries:
|
|
115
|
+
click.echo(f"{e.id:>4} {e.title:<30} {e.username:<25} {e.url or ''}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@cli.command("get")
|
|
119
|
+
@click.argument("entry_id", type=int)
|
|
120
|
+
@click.option("--vault-dir", default=None, type=click.Path())
|
|
121
|
+
def get_entry(entry_id: int, vault_dir: Optional[str]) -> None:
|
|
122
|
+
"""Show a single entry including the password."""
|
|
123
|
+
master = _prompt_master()
|
|
124
|
+
storage = _get_storage(master, Path(vault_dir) if vault_dir else None)
|
|
125
|
+
|
|
126
|
+
entry = storage.get(entry_id)
|
|
127
|
+
if entry is None:
|
|
128
|
+
click.echo(click.style(f"Entry {entry_id} not found.", fg="red"), err=True)
|
|
129
|
+
sys.exit(1)
|
|
130
|
+
|
|
131
|
+
click.echo(f"\nTitle : {entry.title}")
|
|
132
|
+
click.echo(f"Username : {entry.username}")
|
|
133
|
+
click.echo(f"Password : {entry.password}")
|
|
134
|
+
click.echo(f"URL : {entry.url or '-'}")
|
|
135
|
+
click.echo(f"Notes : {entry.notes or '-'}")
|
|
136
|
+
click.echo(f"Updated : {entry.updated_at}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@cli.command("delete")
|
|
140
|
+
@click.argument("entry_id", type=int)
|
|
141
|
+
@click.option("--yes", "-y", is_flag=True, default=False, help="Skip confirmation.")
|
|
142
|
+
@click.option("--vault-dir", default=None, type=click.Path())
|
|
143
|
+
def delete_entry(entry_id: int, yes: bool, vault_dir: Optional[str]) -> None:
|
|
144
|
+
"""Delete an entry by ID."""
|
|
145
|
+
master = _prompt_master()
|
|
146
|
+
storage = _get_storage(master, Path(vault_dir) if vault_dir else None)
|
|
147
|
+
|
|
148
|
+
if not yes:
|
|
149
|
+
click.confirm(f"Delete entry {entry_id}?", abort=True)
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
storage.delete(entry_id)
|
|
153
|
+
click.echo(click.style(f"✓ Deleted entry {entry_id}.", fg="green"))
|
|
154
|
+
except KeyError:
|
|
155
|
+
click.echo(click.style(f"Entry {entry_id} not found.", fg="red"), err=True)
|
|
156
|
+
sys.exit(1)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@cli.command("version")
|
|
160
|
+
def version() -> None:
|
|
161
|
+
"""Print version."""
|
|
162
|
+
click.echo(f"sspwd {__version__}")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def main() -> None:
|
|
166
|
+
cli()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class PasswordEntry:
|
|
9
|
+
id: Optional[int]
|
|
10
|
+
title: str
|
|
11
|
+
username: str
|
|
12
|
+
password: str # stored encrypted, returned decrypted
|
|
13
|
+
url: Optional[str] = None
|
|
14
|
+
notes: Optional[str] = None
|
|
15
|
+
created_at: datetime = field(default_factory=datetime.utcnow)
|
|
16
|
+
updated_at: datetime = field(default_factory=datetime.utcnow)
|
|
17
|
+
|
|
18
|
+
def to_dict(self) -> dict:
|
|
19
|
+
return {
|
|
20
|
+
"id": self.id,
|
|
21
|
+
"title": self.title,
|
|
22
|
+
"username": self.username,
|
|
23
|
+
"password": self.password,
|
|
24
|
+
"url": self.url,
|
|
25
|
+
"notes": self.notes,
|
|
26
|
+
"created_at": self.created_at.isoformat(),
|
|
27
|
+
"updated_at": self.updated_at.isoformat(),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BaseStorage(ABC):
|
|
32
|
+
"""Abstract interface that every storage backend must implement."""
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def initialize(self) -> None:
|
|
36
|
+
"""Create tables / files if they do not exist yet."""
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def add(self, entry: PasswordEntry) -> PasswordEntry:
|
|
40
|
+
"""Persist a new entry and return it with its assigned id."""
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def get(self, entry_id: int) -> Optional[PasswordEntry]:
|
|
44
|
+
"""Return a single entry by id, or None if not found."""
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def list(self, search: Optional[str] = None) -> list[PasswordEntry]:
|
|
48
|
+
"""Return all entries, optionally filtered by a search string."""
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def update(self, entry: PasswordEntry) -> PasswordEntry:
|
|
52
|
+
"""Overwrite an existing entry. Raises KeyError if id not found."""
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def delete(self, entry_id: int) -> None:
|
|
56
|
+
"""Remove an entry by id. Raises KeyError if not found."""
|