Metadata-Version: 2.4
Name: helvetisafe-client
Version: 0.1.2
Summary: Python client library for the HelvetiSafe secret management system with zero-knowledge encryption
Author: Helvetisafe contributors
License-Expression: MIT
Project-URL: Homepage, https://github.com/TheM0f/helvetisafe-python-client
Project-URL: Documentation, https://github.com/TheM0f/helvetisafe-python-client#readme
Project-URL: Repository, https://github.com/TheM0f/helvetisafe-python-client
Project-URL: Issue Tracker, https://github.com/TheM0f/helvetisafe-python-client/issues
Project-URL: Changelog, https://github.com/TheM0f/helvetisafe-python-client/releases
Keywords: secrets,vault,encryption,zero-knowledge,oauth,helvetisafe
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28.0
Requires-Dist: cryptography>=41.0.0
Requires-Dist: argon2-cffi>=23.1.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: responses>=0.23.0; extra == "dev"
Requires-Dist: build>=1.0.0; extra == "dev"
Requires-Dist: twine>=5.0.0; extra == "dev"
Dynamic: license-file

# helvetisafe-client

Python client library for the [Helvetisafe](https://vault.helvetisafe.ch) secret management system with zero-knowledge encryption.

## Status

✅ v0.1.0 — available

## Features

- Pythonic interface for the Helvetisafe API
- **Two authentication modes:**
  - **Service account mode** — OAuth 2.0 Client Credentials (`client_id` / `client_secret`)
  - **API token mode** — portal-generated token (obtained via *Profile → Generate API Token* in the web portal)
- Automatic OAuth token lifecycle management (service account mode only)
- Client-side zero-knowledge encryption (AES-256-GCM) and decryption
- RSA-OAEP Org Key unwrapping
- **Three key sources** (works with both authentication modes):
  - **RSA Key mode** — supply a PEM private key file or bytes
  - **Password mode** — supply the account password; the client derives the key automatically via Argon2id
- Full secret operations: `create`, `get`, `update`, `delete`, `list`
- Structured exceptions for all error conditions
- Type hints throughout

## Installation

```bash
pip install helvetisafe-client
```

## Quick Start

### Service account (OAuth 2.0)

```python
from helvetisafe import HelvetisafeClient

# RSA Key mode (recommended for service accounts in production)
client = HelvetisafeClient(
    base_url="https://vault.helvetisafe.ch",
    client_id="your-client-id",
    client_secret="your-client-secret",
    private_key_path="/path/to/service-account-private-key.pem",
)

# Password mode
client = HelvetisafeClient(
    base_url="https://vault.helvetisafe.ch",
    client_id="your-client-id",
    client_secret="your-client-secret",
    password="your-service-account-password",
)
```

### Portal API token

Generate a token in the HelvetiSafe web portal (*Profile → Generate API Token*),
then use it directly — no OAuth exchange is required.

```python
from helvetisafe import HelvetisafeClient

# RSA Key mode
client = HelvetisafeClient(
    base_url="https://vault.helvetisafe.ch",
    api_token="your-portal-api-token",
    private_key_path="/path/to/your-private-key.pem",
)

# Password mode
client = HelvetisafeClient(
    base_url="https://vault.helvetisafe.ch",
    api_token="your-portal-api-token",
    password="your-account-password",
)
```

## Usage

```python
# Create a secret (encrypted automatically on the client)
client.secrets.create("database_password", "s3cr3t-v@lue")

# Read a secret (decrypted automatically on the client)
secret = client.secrets.get("database_password")
print(secret.value)

# Update a secret
client.secrets.update("database_password", "n3w-v@lue")

# List secrets — metadata only, no plaintext values
for s in client.secrets.list():
    print(s.key, s.expires_at)

# Delete a secret
client.secrets.delete("database_password")
```

### Setting an expiry

```python
from datetime import datetime, timedelta, timezone

expires = datetime.now(tz=timezone.utc) + timedelta(days=30)
client.secrets.create("temp_token", "abc123", expires_at=expires)
```

### Error handling

```python
from helvetisafe import (
    HelvetisafeAuthError,
    HelvetisafeNotFoundError,
    HelvetisafeForbiddenError,
    HelvetisafeRateLimitError,
)

try:
    secret = client.secrets.get("my_key")
except HelvetisafeNotFoundError:
    print("Secret does not exist.")
except HelvetisafeForbiddenError:
    print("Insufficient permissions.")
except HelvetisafeRateLimitError:
    print("Rate limit exceeded.")
except HelvetisafeAuthError:
    print("Authentication failed.")
```

## API Reference

### `HelvetisafeClient`

| Parameter | Type | Required | Description |
|---|---|---|---|
| `base_url` | `str` | ✓ | Base URL of the Helvetisafe instance |
| `client_id` | `str` | OAuth mode | OAuth 2.0 client ID |
| `client_secret` | `str` | OAuth mode | OAuth 2.0 client secret |
| `api_token` | `str` | token mode | Portal-generated API token |
| `private_key_path` | `str` | one of | Path to RSA private key PEM file |
| `private_key_pem` | `bytes` | one of | PEM-encoded RSA private key bytes |
| `password` | `str` | one of | Account password (password mode) |
| `scopes` | `list[str]` | — | OAuth scopes (OAuth mode only; default: all) |
| `session` | `requests.Session` | — | Custom HTTP session |

> Either `api_token` **or** (`client_id` + `client_secret`) must be provided — not both.
> Exactly one of `private_key_path`, `private_key_pem`, or `password` must also be provided.

### `client.secrets`

| Method | Description |
|---|---|
| `get(key)` | Retrieve and decrypt a secret |
| `create(key, value, expires_at=None)` | Create a new encrypted secret |
| `update(key, value, expires_at=None)` | Update an existing encrypted secret |
| `delete(key)` | Delete a secret |
| `list()` | List all visible secret keys (metadata only) |

### Exceptions

| Exception | HTTP status | Description |
|---|---|---|
| `HelvetisafeError` | — | Base exception |
| `HelvetisafeAuthError` | 401 | Authentication / token failure |
| `HelvetisafeCryptoError` | — | Local cryptographic operation failed |
| `HelvetisafeAPIError` | any | Generic API error |
| `HelvetisafeNotFoundError` | 404 | Secret not found |
| `HelvetisafeConflictError` | 409 | Secret already exists |
| `HelvetisafeForbiddenError` | 403 | Insufficient permissions |
| `HelvetisafeRateLimitError` | 429 | Rate limit exceeded |

## Architecture

```
HelvetisafeClient
  │
  ├─ OAuth mode ──► POST /oauth/token
  │                     ← access_token  (refreshed automatically 30 s before expiry)
  │
  ├─ API token mode ──► token used directly as Bearer (no exchange)
  │
  ├─► GET  /api/v1/credentials/org-key
  │       ← encrypted Org Key (RSA-OAEP)
  │   RSA decrypt → Org Key  (held in memory for client lifetime)
  │
  ├─► secrets.create / update
  │   AES-256-GCM encrypt (random nonce) → POST /api/v1/secrets/{key}
  │
  └─► secrets.get
      GET /api/v1/secrets/{key}  ← AES-256-GCM ciphertext
      AES decrypt → plaintext
```

The server stores and transmits only ciphertext. Plaintext values never leave the process.

## Requirements

- Python 3.8+
- `cryptography >= 41.0`
- `requests >= 2.28`
- `argon2-cffi >= 23.1`

## Development

```bash
pip install -e ".[dev]"
pytest tests/ -v
```

## Examples

See the `examples/` directory:

- `basic_usage.py` — create, read, update, list, and delete a secret
- `env_config.py` — load all configuration from environment variables

## Contributing

Contributions are welcome. Please open an issue or pull request in this repository.

## License

MIT
