Metadata-Version: 2.4
Name: vw-cli
Version: 0.2.0
Summary: Lightweight Vaultwarden (Bitwarden-compatible) CLI in Python
Author-email: Roberta Brandao <roberta@betabrandao.com.br>
License: MIT
Project-URL: Homepage, https://gitlab.com/betabrandao/vaultvarden-cli
Project-URL: Repository, https://gitlab.com/betabrandao/vaultvarden-cli
Keywords: bitwarden,vaultwarden,cli,password-manager,alpine
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.31
Requires-Dist: cryptography>=41
Requires-Dist: argon2-cffi>=23
Dynamic: license-file

# vw-cli

Lightweight Vaultwarden (Bitwarden-compatible) CLI written in Python.  
Authenticates via API key, syncs the vault, derives the encryption key
locally, and prints decrypted passwords or items — all in a single command.

Designed for use in Alpine‑based Docker containers where the official
`bw` Node.js CLI is impractical.

## Quick start

```sh
pip install vw-cli

export VW_SERVER=https://vault.example.com
export VW_CLIENTID=user.aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
export VW_CLIENTSECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
export VW_PASSWORD="your master password"

vw-cli get password "My Website"
```

## Usage

```
usage: vw-cli <command> [args]

commands:
  login                     authenticate with API key
  sync                      sync vault and print raw JSON
  unlock                    unlock vault (derive encryption key)
  list                      list all item names
  get password <item>       print password for <item>
  get item <item>           print full decrypted <item> as JSON
```

### Commands

| command | description |
|---------|-------------|
| `login` | Authenticate with the API key. Verifies credentials work. |
| `sync`  | Pull the full vault (profile + all ciphers) and print raw JSON. |
| `unlock` | Derive the master key locally and decrypt the stored symmetric key. |
| `list` | Print every item name (lowercased), one per line. |
| `get password <item>` | Print the password for the named item (empty string if none). |
| `get item <item>` | Print the full decrypted item as JSON. |

Item lookup is case‑insensitive and supports substring matching.

## Environment variables

| variable | required | default | description |
|----------|----------|---------|-------------|
| `VW_SERVER` | no | `https://vault.bitwarden.com` | Vaultwarden or Bitwarden server URL |
| `VW_CLIENTID` | yes | — | API client ID (`user.xxx` or `org.xxx`) |
| `VW_CLIENTSECRET` | yes | — | API client secret |
| `VW_PASSWORD` | yes | — | Master password |

When `VW_SERVER` contains `bitwarden` the official Bitwarden identity
and API endpoints are used automatically; otherwise the same URL is used
for both identity and API paths.

## Docker example

```Dockerfile
FROM alpine:3.19
RUN apk add --no-cache python3 py3-pip
RUN pip install vw-cli
```

```sh
docker run --rm \
  -e VW_SERVER=https://vault.example.com \
  -e VW_CLIENTID=user.xxx \
  -e VW_CLIENTSECRET=xxx \
  -e VW_PASSWORD="..." \
  my-image vw-cli get password "Database"
```

## How it works

1. **Login** — sends a `client_credentials` grant with the API key to
   `{identity_url}/connect/token` and receives a bearer token.
2. **Sync** — fetches `GET /api/sync` which returns the user profile
   (email, KDF parameters, encrypted symmetric key) and all ciphers.
3. **Unlock** — derives the 32‑byte master key using the password and
   email (PBKDF2‑SHA256 or Argon2id, depending on the profile KDF), then
   decrypts the 64‑byte symmetric key stored in the profile.
4. **Decrypt** — splits the symmetric key into an AES‑256‑CBC encryption
   key (first 32 bytes) and an HMAC‑SHA256 MAC key (last 32 bytes), then
   decrypts individual cipher fields.

All key derivation happens **locally** — no unlock endpoint is called.

## Architecture

```
vw_cli/
├── __init__.py     # package entry, re‑exports
├── __main__.py     # python -m vw_cli support
├── error.py        # VwError exception class
├── crypto.py       # VwCryptoKey, key derivation, AES-CBC, HMAC
├── auth.py         # VwAuth – API key login, session management
├── vault.py        # VwVault – sync, unlock, cipher operations
└── cli.py          # CLI argument parsing, orchestration
```

Separation of concerns:

| module | responsibility |
|--------|---------------|
| `crypto.py` | Pure functions: key derivation, encryption/decryption, data types. No I/O. |
| `auth.py` | `VwAuth` class: HTTP session, token acquisition, request helper. |
| `vault.py` | `VwVault` class: sync, unlock, name cache, find/get/list operations. |
| `cli.py` | Environment variable reading, argument parsing, command dispatch. |

## Development

### Virtualenv (recommended)

```sh
python -m venv venv
source venv/bin/activate
pip install -e .
```

The package is installed in **editable mode** (`-e`), so source changes take
effect immediately — no need to reinstall.

### Run without pip

You don't need to `pip install` at all.  The `__main__.py` entry point lets you
run the CLI directly from the checkout:

```sh
python -m vw_cli get password "My Website"
```

Or set up a shell alias for convenience:

```sh
alias vw-cli='python -m vw_cli'
```

### Test

```sh
python -m pytest tests/ -v
```

### Testing

Three test files covering all layers:

| test file | coverage |
|-----------|----------|
| `tests/test_crypto.py` | `VwCryptoKey`, `_safe_int`, key derivation, AES-CBC, HMAC, `decrypt`, `decrypt_bytes`, `decode_encrypted` |
| `tests/test_client.py` | `VwAuth` (login, errors), `VwVault` (sync, unlock, find, get, list), `_setup_urls`, network errors, timeout |
| `tests/test_cli.py` | CLI argument parsing, help/version output, error handling, exit codes |

Tests use mocks and never touch the network.

### Dependencies

- `requests` — HTTP client
- `cryptography` — AES‑256‑CBC via OpenSSL bindings
- `argon2-cffi` — Argon2id KDF (optional; falls back gracefully)

## License

MIT
