Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.git
**/target
**/node_modules
**/.venv
**/__pycache__
e2e/test-results
e2e/playwright-report
e2e/screenshots
deploy/.env
**/.env
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,30 @@ jobs:
- name: License fields are Apache-2.0
run: python3 scripts/check-license-metadata.py

e2e:
name: e2e
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v5

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- uses: actions/setup-node@v4
with:
node-version: "22"

- name: API dependencies
working-directory: api
run: python -m pip install -r requirements.txt

- name: Playwright
working-directory: e2e
run: npm install && npx playwright install --with-deps chromium

- name: Console flows
working-directory: e2e
run: npx playwright test

22 changes: 22 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: release

on:
push:
tags: ["v*"]

permissions:
contents: read

jobs:
version:
name: tag matches VERSION
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v5

- name: Tag matches VERSION
run: |
tag="${GITHUB_REF_NAME#v}"
current="$(tr -d '[:space:]' < VERSION)"
test "$tag" = "$current"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ target/
bindings/ts/pkg/
bindings/ts/node_modules/
bindings/ts/dist/
e2e/node_modules/
e2e/test-results/
e2e/playwright-report/
e2e/screenshots/
55 changes: 55 additions & 0 deletions INTEGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Adopt Encryptoglyph

Encryptoglyph 0.2.0 exposes two surfaces that share one envelope: the in-process library, and the versioned HTTP sidecar (`/v1`, header `X-Encryptoglyph-Api: 1`). Keys created through the sidecar stay in process memory. Responses carry fingerprints, never key bytes. The assistant endpoint refuses plaintext and key fields.

## In process

```python
from eg_crypto import Keyring, RecordContext, encrypt, decrypt

kr = Keyring({kid: key}, kid)
ctx = RecordContext("acme", "field", "customers", "42", "email")
blob = encrypt(kr, ctx, b"ada@example.com")
assert decrypt(kr, ctx, blob) == b"ada@example.com"
```

`api/eg_crypto` is the Python package. `crates/encryptoglyph` is the Rust source of truth. Both check `testvectors/envelope.json`.

## Sidecar

Run the API (it also serves the console):

```bash
cd api
python -m pip install -r requirements.txt
BUILD_SHA="$(git rev-parse --short=12 HEAD)" python -m uvicorn main:app --host 127.0.0.1 --port 8000
```

```python
import os
from encryptoglyph_client import Encryptoglyph

eg = Encryptoglyph(os.environ["EG_URL"])
eg.session()
sealed = eg.seal(
{"tenant": "acme", "purpose": "field", "table": "customers", "record_id": "42", "field": "email"},
b"ada@example.com",
)
```

The client is `sdk/python/encryptoglyph_client.py`. The browser equivalent is `sdk/js/encryptoglyph.js`. Set `EG_URL` to the sidecar origin. No secret belongs in the repo or in that environment variable.

A GoDesk app (GitGoat, RailYard, Portkyl, Komboe, Cerebro, or another service) vendors one client and calls `session` then `seal`. The same record context must be presented to open the blob.

## Updates

`VERSION` is the product version. `GET /v1/update` compares it with the public GitHub release for `GoDeskio/Encryptoglyph` and returns `update_available` without downloading anything.

```bash
python scripts/encryptoglyph-update.py --check
python scripts/encryptoglyph-update.py --download /tmp/encryptoglyph-next
```

The downloader accepts only `https://api.github.com/repos/GoDeskio/Encryptoglyph/tarball/<tag>`, checks the gzip header, and extracts into a new directory. It does not run the archive.

Tagged releases (`v*`) are checked by `.github/workflows/release.yml`, which fails if the tag disagrees with `VERSION`. Publishing the GitHub release is a separate maintainer step. This tree does not deploy.
24 changes: 13 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ Encryptoglyph is an open-source encryption-at-rest library and self-hosted servi
## Features

- **XChaCha20-Poly1305 AEAD.** New ciphertext uses a 192-bit nonce and a 256-bit key. The header carries a version, an algorithm id, and a key id. Legacy AES-256-GCM envelopes still decrypt, and `rewrap` upgrades them. Rust (`crates/encryptoglyph`) and Python (`api/eg_crypto`) share the wire format and the vectors in `testvectors/envelope.json`.
- **Key custody.** The compose file runs OpenBao on an internal network for Transit keys and KV. The library itself has no escrow key and no master override. Root keys are 32 bytes, derived with HKDF-SHA256 per tenant, purpose, and algorithm, or from a password with Argon2id. The health-only API does not call OpenBao yet. The custody design is in [docs/plan.md](docs/plan.md) and [docs/SECURITY-HARDENING.md](docs/SECURITY-HARDENING.md).
- **Key custody.** The compose file runs OpenBao on an internal network for Transit keys and KV. The library itself has no escrow key and no master override. Root keys are 32 bytes, derived with HKDF-SHA256 per tenant, purpose, and algorithm, or from a password with Argon2id. The console and `/v1` sidecar do not call OpenBao. The custody design is in [docs/plan.md](docs/plan.md) and [docs/SECURITY-HARDENING.md](docs/SECURITY-HARDENING.md).
- **Encrypted backup.** `restic/rest-server` sits on the same internal network as the backup target. Host ports are not published. Backup scheduling and restore evidence are described in the plan and are not implemented in this tree.
- **MSP console.** Operators are meant to reach `eg-console` through Traefik at `eg.home.arpa`. The compose service is present. Its image is `encryptoglyph-console:placeholder`, and this repository does not contain a console UI.
- **Console.** `console/` is the operator UI, served by the API at `/`. It seals fields, rotates and shreds keys, and runs a deterministic assistant. OpenBao and restic stay unwired until a deployment supplies them. The compose service builds the same image as the API.

## What this tree runs

Expand All @@ -23,21 +23,21 @@ Encryptoglyph is an open-source encryption-at-rest library and self-hosted servi
| Rust envelope, HKDF, Argon2id, key rotation | Implemented and tested |
| Python module, wire-compatible with Rust | Implemented and tested |
| TypeScript wrapper over the WASM build | Source in `bindings/ts`; `wasm-pack` output is not committed |
| `eg-api` container | `GET /healthz` only. Swagger, ReDoc, and `/openapi.json` are not served |
| OpenBao, Postgres 16, restic rest-server | Images and config in `deploy/docker-compose.yml` |
| MSP console | Service name and Traefik labels only |
| `eg-api` container | `/healthz`, the `/v1` sidecar, and the console. Swagger, ReDoc, and `/openapi.json` are not served |
| OpenBao, Postgres 16, restic rest-server | Images and config in `deploy/docker-compose.yml`. The console reports them as not connected |
| Console | `console/`, served by the API. See [INTEGRATION.md](INTEGRATION.md) |

## Architecture

Traefik is external. Only `eg-console` and `eg-api` join `proxy-net`. OpenBao, Postgres, and the restic rest-server stay on the `internal` network (`internal: true`), which has no route off the Docker host. The API container is the only app process that joins both networks. The crypto libraries are not containers. The API image copies `main.py` only, so the running container does not load `eg_crypto`.
Traefik is external. Only `eg-console` and `eg-api` join `proxy-net`. OpenBao, Postgres, and the restic rest-server stay on the `internal` network (`internal: true`), which has no route off the Docker host. The API container is the only app process that joins both networks. The crypto libraries are not containers. The API image copies the console, `eg_crypto`, and the `/v1` service.

```mermaid
flowchart TB
operator[MSP operator]
subgraph proxy [proxy-net]
traefik[Traefik TLS, external]
console["eg-console MSP console"]
api["eg-api GET /healthz"]
api["eg-api console and /v1"]
end
subgraph internal [internal network, no host ports]
openbao[OpenBao key custody]
Expand Down Expand Up @@ -78,7 +78,7 @@ docker compose -f deploy/docker-compose.yml config

`proxy-net` must already exist because the compose file marks it external. `docker compose config` is the check CI runs. Do not commit `.env`.

`docker compose up` needs a local image named `encryptoglyph-console:placeholder`. That image is not published, and the console UI is not in this repository. OpenBao, Postgres, and rest-server also expect real values in `.env` before they will start.
`docker compose up` builds `eg-api` and `eg-console` from `api/Dockerfile`. OpenBao, Postgres, and rest-server still expect real values in `.env` before they will start. The console does not require them.

The API image is buildable on its own. It listens on port 8000 inside the container and, in the compose file, publishes no host port. This loopback publish is only for a local health check:

Expand All @@ -95,20 +95,22 @@ curl -sS http://127.0.0.1:8000/healthz

A healthy process returns `{"status":"ok","build":"<12 hex chars>"}`. `/docs`, `/redoc`, `/openapi.json`, and `/swagger` are not served. That is checked by `scripts/assert-exposure.sh`.

Without Docker, from `api/` with Python 3.12:
Without Docker, from `api/` with Python 3.12. This serves `/healthz`, `/v1`, and the console at `/`:

```bash
python -m pip install -r requirements.txt
BUILD_SHA="$(git rev-parse --short=12 HEAD)" python -m uvicorn main:app --host 127.0.0.1 --port 8000
```

Open `http://127.0.0.1:8000`. Adoption snippets are in [INTEGRATION.md](INTEGRATION.md). The assistant uses deterministic commands with no model. Set `OLLAMA_BASE_URL` when a local Ollama should choose those commands. It never receives plaintext or keys.

## API and SDKs

The HTTP API in this repository is the health check above. Envelope encryption is the library surface, with the same record context in each language: tenant, purpose, table, record id, field, and schema version. Those fields are authenticated as additional data, so a ciphertext cannot be moved to another row, field, or tenant.

### Rust

`crates/encryptoglyph` is the source of truth. `unsafe` is forbidden. Default writes are version 2, XChaCha20-Poly1305.
`crates/encryptoglyph` is the source of truth. `unsafe` is forbidden. Default writes are version 3, XChaCha20-Poly1305 with a key commitment.

```rust
use encryptoglyph::{decrypt, encrypt, Keyring, RecordContext, VERSION_V2};
Expand Down Expand Up @@ -192,7 +194,7 @@ The package is `private` and is not published to npm from this repository. `bind
- **Key separation.** A root key never encrypts plaintext directly. HKDF-SHA256 derives a data key from the root key, the purpose, the tenant, and the algorithm. Password root keys use Argon2id with a 16-byte salt.
- **Rotation.** A `Keyring` encrypts with the current key id and can decrypt older ids. `rewrap` writes a fresh version-2 blob. The current key cannot be retired.
- **Uniform failures.** Decrypt errors do not distinguish a bad tag from a malformed header.
- **No backdoor in the library.** There is no escrow or override key in the Rust or Python code. Custody of root keys is an OpenBao operation, still outside the health-only API.
- **No backdoor in the library.** There is no escrow or override key in the Rust or Python code. Custody of root keys is an OpenBao operation. This console keeps session keys in process memory and says so.
- **Network.** Compose does not publish host ports. OpenBao's own UI is disabled. Postgres passwords come from the gitignored env file.
- **Dependency licenses.** `cargo deny` allows Apache-2.0, MIT, BSD, ISC, MPL-2.0, Unicode, Zlib, and CC0. The check runs in CI.

Expand Down
14 changes: 14 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ Encryptoglyph runs the following third-party software unmodified. Attribute them
- License: BSD 2-Clause License
- Source: https://github.com/restic/rest-server

## fpe

- Project: fpe (FF1)
- Use: Rust dependency of the format-preserving encryption wrapper
- License: MIT OR Apache-2.0
- Source: https://github.com/str4d/fpe

## blahaj

- Project: blahaj
- Use: Rust dependency for Shamir secret sharing (fixed fork of sharks)
- License: MIT
- Source: https://git.distrust.co/public/blahaj

## FastAPI

- Project: FastAPI
Expand Down
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.2.0
8 changes: 6 additions & 2 deletions api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
COPY api/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY main.py .
COPY api/main.py /app/main.py
COPY api/eg_crypto /app/eg_crypto
COPY api/eg_api /app/eg_api
COPY console /app/console
COPY VERSION /app/VERSION

ENV BUILD_SHA=unknown \
PYTHONDONTWRITEBYTECODE=1
Expand Down
1 change: 1 addition & 0 deletions api/eg_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Encryptoglyph HTTP service helpers. Crypto stays in eg_crypto."""
Loading
Loading