vw-cli 0.2.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.
vw_cli-0.2.0/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright © 2026 robertanrbrandao[at]gmail[dot]com
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
vw_cli-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,199 @@
1
+ Metadata-Version: 2.4
2
+ Name: vw-cli
3
+ Version: 0.2.0
4
+ Summary: Lightweight Vaultwarden (Bitwarden-compatible) CLI in Python
5
+ Author-email: Roberta Brandao <roberta@betabrandao.com.br>
6
+ License: MIT
7
+ Project-URL: Homepage, https://gitlab.com/betabrandao/vaultvarden-cli
8
+ Project-URL: Repository, https://gitlab.com/betabrandao/vaultvarden-cli
9
+ Keywords: bitwarden,vaultwarden,cli,password-manager,alpine
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Security :: Cryptography
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: requests>=2.31
25
+ Requires-Dist: cryptography>=41
26
+ Requires-Dist: argon2-cffi>=23
27
+ Dynamic: license-file
28
+
29
+ # vw-cli
30
+
31
+ Lightweight Vaultwarden (Bitwarden-compatible) CLI written in Python.
32
+ Authenticates via API key, syncs the vault, derives the encryption key
33
+ locally, and prints decrypted passwords or items — all in a single command.
34
+
35
+ Designed for use in Alpine‑based Docker containers where the official
36
+ `bw` Node.js CLI is impractical.
37
+
38
+ ## Quick start
39
+
40
+ ```sh
41
+ pip install vw-cli
42
+
43
+ export VW_SERVER=https://vault.example.com
44
+ export VW_CLIENTID=user.aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
45
+ export VW_CLIENTSECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
46
+ export VW_PASSWORD="your master password"
47
+
48
+ vw-cli get password "My Website"
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ ```
54
+ usage: vw-cli <command> [args]
55
+
56
+ commands:
57
+ login authenticate with API key
58
+ sync sync vault and print raw JSON
59
+ unlock unlock vault (derive encryption key)
60
+ list list all item names
61
+ get password <item> print password for <item>
62
+ get item <item> print full decrypted <item> as JSON
63
+ ```
64
+
65
+ ### Commands
66
+
67
+ | command | description |
68
+ |---------|-------------|
69
+ | `login` | Authenticate with the API key. Verifies credentials work. |
70
+ | `sync` | Pull the full vault (profile + all ciphers) and print raw JSON. |
71
+ | `unlock` | Derive the master key locally and decrypt the stored symmetric key. |
72
+ | `list` | Print every item name (lowercased), one per line. |
73
+ | `get password <item>` | Print the password for the named item (empty string if none). |
74
+ | `get item <item>` | Print the full decrypted item as JSON. |
75
+
76
+ Item lookup is case‑insensitive and supports substring matching.
77
+
78
+ ## Environment variables
79
+
80
+ | variable | required | default | description |
81
+ |----------|----------|---------|-------------|
82
+ | `VW_SERVER` | no | `https://vault.bitwarden.com` | Vaultwarden or Bitwarden server URL |
83
+ | `VW_CLIENTID` | yes | — | API client ID (`user.xxx` or `org.xxx`) |
84
+ | `VW_CLIENTSECRET` | yes | — | API client secret |
85
+ | `VW_PASSWORD` | yes | — | Master password |
86
+
87
+ When `VW_SERVER` contains `bitwarden` the official Bitwarden identity
88
+ and API endpoints are used automatically; otherwise the same URL is used
89
+ for both identity and API paths.
90
+
91
+ ## Docker example
92
+
93
+ ```Dockerfile
94
+ FROM alpine:3.19
95
+ RUN apk add --no-cache python3 py3-pip
96
+ RUN pip install vw-cli
97
+ ```
98
+
99
+ ```sh
100
+ docker run --rm \
101
+ -e VW_SERVER=https://vault.example.com \
102
+ -e VW_CLIENTID=user.xxx \
103
+ -e VW_CLIENTSECRET=xxx \
104
+ -e VW_PASSWORD="..." \
105
+ my-image vw-cli get password "Database"
106
+ ```
107
+
108
+ ## How it works
109
+
110
+ 1. **Login** — sends a `client_credentials` grant with the API key to
111
+ `{identity_url}/connect/token` and receives a bearer token.
112
+ 2. **Sync** — fetches `GET /api/sync` which returns the user profile
113
+ (email, KDF parameters, encrypted symmetric key) and all ciphers.
114
+ 3. **Unlock** — derives the 32‑byte master key using the password and
115
+ email (PBKDF2‑SHA256 or Argon2id, depending on the profile KDF), then
116
+ decrypts the 64‑byte symmetric key stored in the profile.
117
+ 4. **Decrypt** — splits the symmetric key into an AES‑256‑CBC encryption
118
+ key (first 32 bytes) and an HMAC‑SHA256 MAC key (last 32 bytes), then
119
+ decrypts individual cipher fields.
120
+
121
+ All key derivation happens **locally** — no unlock endpoint is called.
122
+
123
+ ## Architecture
124
+
125
+ ```
126
+ vw_cli/
127
+ ├── __init__.py # package entry, re‑exports
128
+ ├── __main__.py # python -m vw_cli support
129
+ ├── error.py # VwError exception class
130
+ ├── crypto.py # VwCryptoKey, key derivation, AES-CBC, HMAC
131
+ ├── auth.py # VwAuth – API key login, session management
132
+ ├── vault.py # VwVault – sync, unlock, cipher operations
133
+ └── cli.py # CLI argument parsing, orchestration
134
+ ```
135
+
136
+ Separation of concerns:
137
+
138
+ | module | responsibility |
139
+ |--------|---------------|
140
+ | `crypto.py` | Pure functions: key derivation, encryption/decryption, data types. No I/O. |
141
+ | `auth.py` | `VwAuth` class: HTTP session, token acquisition, request helper. |
142
+ | `vault.py` | `VwVault` class: sync, unlock, name cache, find/get/list operations. |
143
+ | `cli.py` | Environment variable reading, argument parsing, command dispatch. |
144
+
145
+ ## Development
146
+
147
+ ### Virtualenv (recommended)
148
+
149
+ ```sh
150
+ python -m venv venv
151
+ source venv/bin/activate
152
+ pip install -e .
153
+ ```
154
+
155
+ The package is installed in **editable mode** (`-e`), so source changes take
156
+ effect immediately — no need to reinstall.
157
+
158
+ ### Run without pip
159
+
160
+ You don't need to `pip install` at all. The `__main__.py` entry point lets you
161
+ run the CLI directly from the checkout:
162
+
163
+ ```sh
164
+ python -m vw_cli get password "My Website"
165
+ ```
166
+
167
+ Or set up a shell alias for convenience:
168
+
169
+ ```sh
170
+ alias vw-cli='python -m vw_cli'
171
+ ```
172
+
173
+ ### Test
174
+
175
+ ```sh
176
+ python -m pytest tests/ -v
177
+ ```
178
+
179
+ ### Testing
180
+
181
+ Three test files covering all layers:
182
+
183
+ | test file | coverage |
184
+ |-----------|----------|
185
+ | `tests/test_crypto.py` | `VwCryptoKey`, `_safe_int`, key derivation, AES-CBC, HMAC, `decrypt`, `decrypt_bytes`, `decode_encrypted` |
186
+ | `tests/test_client.py` | `VwAuth` (login, errors), `VwVault` (sync, unlock, find, get, list), `_setup_urls`, network errors, timeout |
187
+ | `tests/test_cli.py` | CLI argument parsing, help/version output, error handling, exit codes |
188
+
189
+ Tests use mocks and never touch the network.
190
+
191
+ ### Dependencies
192
+
193
+ - `requests` — HTTP client
194
+ - `cryptography` — AES‑256‑CBC via OpenSSL bindings
195
+ - `argon2-cffi` — Argon2id KDF (optional; falls back gracefully)
196
+
197
+ ## License
198
+
199
+ MIT
vw_cli-0.2.0/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # vw-cli
2
+
3
+ Lightweight Vaultwarden (Bitwarden-compatible) CLI written in Python.
4
+ Authenticates via API key, syncs the vault, derives the encryption key
5
+ locally, and prints decrypted passwords or items — all in a single command.
6
+
7
+ Designed for use in Alpine‑based Docker containers where the official
8
+ `bw` Node.js CLI is impractical.
9
+
10
+ ## Quick start
11
+
12
+ ```sh
13
+ pip install vw-cli
14
+
15
+ export VW_SERVER=https://vault.example.com
16
+ export VW_CLIENTID=user.aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
17
+ export VW_CLIENTSECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
18
+ export VW_PASSWORD="your master password"
19
+
20
+ vw-cli get password "My Website"
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```
26
+ usage: vw-cli <command> [args]
27
+
28
+ commands:
29
+ login authenticate with API key
30
+ sync sync vault and print raw JSON
31
+ unlock unlock vault (derive encryption key)
32
+ list list all item names
33
+ get password <item> print password for <item>
34
+ get item <item> print full decrypted <item> as JSON
35
+ ```
36
+
37
+ ### Commands
38
+
39
+ | command | description |
40
+ |---------|-------------|
41
+ | `login` | Authenticate with the API key. Verifies credentials work. |
42
+ | `sync` | Pull the full vault (profile + all ciphers) and print raw JSON. |
43
+ | `unlock` | Derive the master key locally and decrypt the stored symmetric key. |
44
+ | `list` | Print every item name (lowercased), one per line. |
45
+ | `get password <item>` | Print the password for the named item (empty string if none). |
46
+ | `get item <item>` | Print the full decrypted item as JSON. |
47
+
48
+ Item lookup is case‑insensitive and supports substring matching.
49
+
50
+ ## Environment variables
51
+
52
+ | variable | required | default | description |
53
+ |----------|----------|---------|-------------|
54
+ | `VW_SERVER` | no | `https://vault.bitwarden.com` | Vaultwarden or Bitwarden server URL |
55
+ | `VW_CLIENTID` | yes | — | API client ID (`user.xxx` or `org.xxx`) |
56
+ | `VW_CLIENTSECRET` | yes | — | API client secret |
57
+ | `VW_PASSWORD` | yes | — | Master password |
58
+
59
+ When `VW_SERVER` contains `bitwarden` the official Bitwarden identity
60
+ and API endpoints are used automatically; otherwise the same URL is used
61
+ for both identity and API paths.
62
+
63
+ ## Docker example
64
+
65
+ ```Dockerfile
66
+ FROM alpine:3.19
67
+ RUN apk add --no-cache python3 py3-pip
68
+ RUN pip install vw-cli
69
+ ```
70
+
71
+ ```sh
72
+ docker run --rm \
73
+ -e VW_SERVER=https://vault.example.com \
74
+ -e VW_CLIENTID=user.xxx \
75
+ -e VW_CLIENTSECRET=xxx \
76
+ -e VW_PASSWORD="..." \
77
+ my-image vw-cli get password "Database"
78
+ ```
79
+
80
+ ## How it works
81
+
82
+ 1. **Login** — sends a `client_credentials` grant with the API key to
83
+ `{identity_url}/connect/token` and receives a bearer token.
84
+ 2. **Sync** — fetches `GET /api/sync` which returns the user profile
85
+ (email, KDF parameters, encrypted symmetric key) and all ciphers.
86
+ 3. **Unlock** — derives the 32‑byte master key using the password and
87
+ email (PBKDF2‑SHA256 or Argon2id, depending on the profile KDF), then
88
+ decrypts the 64‑byte symmetric key stored in the profile.
89
+ 4. **Decrypt** — splits the symmetric key into an AES‑256‑CBC encryption
90
+ key (first 32 bytes) and an HMAC‑SHA256 MAC key (last 32 bytes), then
91
+ decrypts individual cipher fields.
92
+
93
+ All key derivation happens **locally** — no unlock endpoint is called.
94
+
95
+ ## Architecture
96
+
97
+ ```
98
+ vw_cli/
99
+ ├── __init__.py # package entry, re‑exports
100
+ ├── __main__.py # python -m vw_cli support
101
+ ├── error.py # VwError exception class
102
+ ├── crypto.py # VwCryptoKey, key derivation, AES-CBC, HMAC
103
+ ├── auth.py # VwAuth – API key login, session management
104
+ ├── vault.py # VwVault – sync, unlock, cipher operations
105
+ └── cli.py # CLI argument parsing, orchestration
106
+ ```
107
+
108
+ Separation of concerns:
109
+
110
+ | module | responsibility |
111
+ |--------|---------------|
112
+ | `crypto.py` | Pure functions: key derivation, encryption/decryption, data types. No I/O. |
113
+ | `auth.py` | `VwAuth` class: HTTP session, token acquisition, request helper. |
114
+ | `vault.py` | `VwVault` class: sync, unlock, name cache, find/get/list operations. |
115
+ | `cli.py` | Environment variable reading, argument parsing, command dispatch. |
116
+
117
+ ## Development
118
+
119
+ ### Virtualenv (recommended)
120
+
121
+ ```sh
122
+ python -m venv venv
123
+ source venv/bin/activate
124
+ pip install -e .
125
+ ```
126
+
127
+ The package is installed in **editable mode** (`-e`), so source changes take
128
+ effect immediately — no need to reinstall.
129
+
130
+ ### Run without pip
131
+
132
+ You don't need to `pip install` at all. The `__main__.py` entry point lets you
133
+ run the CLI directly from the checkout:
134
+
135
+ ```sh
136
+ python -m vw_cli get password "My Website"
137
+ ```
138
+
139
+ Or set up a shell alias for convenience:
140
+
141
+ ```sh
142
+ alias vw-cli='python -m vw_cli'
143
+ ```
144
+
145
+ ### Test
146
+
147
+ ```sh
148
+ python -m pytest tests/ -v
149
+ ```
150
+
151
+ ### Testing
152
+
153
+ Three test files covering all layers:
154
+
155
+ | test file | coverage |
156
+ |-----------|----------|
157
+ | `tests/test_crypto.py` | `VwCryptoKey`, `_safe_int`, key derivation, AES-CBC, HMAC, `decrypt`, `decrypt_bytes`, `decode_encrypted` |
158
+ | `tests/test_client.py` | `VwAuth` (login, errors), `VwVault` (sync, unlock, find, get, list), `_setup_urls`, network errors, timeout |
159
+ | `tests/test_cli.py` | CLI argument parsing, help/version output, error handling, exit codes |
160
+
161
+ Tests use mocks and never touch the network.
162
+
163
+ ### Dependencies
164
+
165
+ - `requests` — HTTP client
166
+ - `cryptography` — AES‑256‑CBC via OpenSSL bindings
167
+ - `argon2-cffi` — Argon2id KDF (optional; falls back gracefully)
168
+
169
+ ## License
170
+
171
+ MIT
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vw-cli"
7
+ version = "0.2.0"
8
+ description = "Lightweight Vaultwarden (Bitwarden-compatible) CLI in Python"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ authors = [
12
+ {name = "Roberta Brandao", email = "roberta@betabrandao.com.br"},
13
+ ]
14
+ keywords = ["bitwarden", "vaultwarden", "cli", "password-manager", "alpine"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Intended Audience :: System Administrators",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Security :: Cryptography",
26
+ "Topic :: Utilities",
27
+ ]
28
+ requires-python = ">=3.10"
29
+ dependencies = ["requests>=2.31", "cryptography>=41", "argon2-cffi>=23"]
30
+
31
+ [project.urls]
32
+ Homepage = "https://gitlab.com/betabrandao/vaultvarden-cli"
33
+ Repository = "https://gitlab.com/betabrandao/vaultvarden-cli"
34
+
35
+ [project.scripts]
36
+ vw-cli = "vw_cli.cli:main"
37
+
38
+ [tool.setuptools.packages.find]
39
+ include = ["vw_cli*"]
vw_cli-0.2.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,83 @@
1
+ import sys
2
+
3
+ import pytest
4
+ import requests
5
+
6
+ from vw_cli.cli import VERSION, main
7
+ from vw_cli.error import VwError
8
+
9
+
10
+ class TestHelp:
11
+ def test_help_exits_zero(self):
12
+ sys.argv = ["vw-cli", "--help"]
13
+ with pytest.raises(SystemExit) as exc:
14
+ main()
15
+ assert exc.value.code == 0
16
+
17
+ def test_help_with_no_args(self):
18
+ sys.argv = ["vw-cli"]
19
+ with pytest.raises(SystemExit) as exc:
20
+ main()
21
+ assert exc.value.code == 0
22
+
23
+ def test_help_with_h_flag(self):
24
+ sys.argv = ["vw-cli", "-h"]
25
+ with pytest.raises(SystemExit) as exc:
26
+ main()
27
+ assert exc.value.code == 0
28
+
29
+
30
+ class TestVersion:
31
+ def test_version_output(self, capsys):
32
+ sys.argv = ["vw-cli", "--version"]
33
+ with pytest.raises(SystemExit):
34
+ main()
35
+ out, _ = capsys.readouterr()
36
+ assert "vw-cli" in out
37
+ assert VERSION in out
38
+
39
+
40
+ class TestUnknownCommand:
41
+ def test_unknown_command_exits_1(self):
42
+ sys.argv = ["vw-cli", "nonsense"]
43
+ with pytest.raises(SystemExit) as exc:
44
+ main()
45
+ assert exc.value.code == 1
46
+
47
+
48
+ class TestVwErrorCaught:
49
+ def test_bwerror_prints_and_exits_1(self, monkeypatch):
50
+ def fail(*a):
51
+ raise VwError("boom")
52
+
53
+ monkeypatch.setattr("vw_cli.auth.VwAuth.login", fail)
54
+ sys.argv = ["vw-cli", "login"]
55
+ with pytest.raises(SystemExit) as exc:
56
+ main()
57
+ assert exc.value.code == 1
58
+
59
+
60
+ class TestRequestExceptionCaught:
61
+ def test_network_error_prints_and_exits_1(self, monkeypatch):
62
+ def fail(*a):
63
+ raise requests.ConnectionError("timeout")
64
+
65
+ monkeypatch.setattr("vw_cli.auth.VwAuth.login", fail)
66
+ sys.argv = ["vw-cli", "login"]
67
+ with pytest.raises(SystemExit) as exc:
68
+ main()
69
+ assert exc.value.code == 1
70
+
71
+
72
+ class TestGetMissingArgs:
73
+ def test_get_without_subcommand(self):
74
+ sys.argv = ["vw-cli", "get"]
75
+ with pytest.raises(SystemExit) as exc:
76
+ main()
77
+ assert exc.value.code == 1
78
+
79
+ def test_get_without_name(self):
80
+ sys.argv = ["vw-cli", "get", "password"]
81
+ with pytest.raises(SystemExit) as exc:
82
+ main()
83
+ assert exc.value.code == 1