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 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ """
2
+ sspwd - A local password manager with a web UI.
3
+ Passwords are stored encrypted in ~/.sspwd/
4
+ """
5
+
6
+ from .__version__ import __version__
7
+
8
+ __all__ = ["__version__"]
@@ -0,0 +1,5 @@
1
+ """Version information for finamt package."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("sspwd")
@@ -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,3 @@
1
+ from .sqlite import SQLiteStorage
2
+
3
+ __all__ = ["SQLiteStorage"]
@@ -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."""