Metadata-Version: 2.4
Name: helvetisafe-client
Version: 0.1.1
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.8
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
- OAuth 2.0 Client Credentials authentication with automatic token lifecycle management
- Client-side zero-knowledge encryption (AES-256-GCM) and decryption
- RSA-OAEP Org Key unwrapping
- Two authentication modes:
  - **RSA Key mode** — supply a PEM private key file or bytes
  - **Password mode** — supply a service 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

```python
from helvetisafe import HelvetisafeClient

# RSA Key mode (recommended for 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",
)
```

## 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 2.0 client ID |
| `client_secret` | `str` | ✓ | OAuth 2.0 client secret |
| `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 | Service account password (password mode) |
| `scopes` | `list[str]` | — | OAuth scopes (default: all) |
| `session` | `requests.Session` | — | Custom HTTP session |

### `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
  │
  ├─► POST /oauth/token
  │       ← access_token  (refreshed automatically 30 s before expiry)
  │
  ├─► 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
