diff --git a/REMEDIATION.md b/REMEDIATION.md new file mode 100644 index 0000000..5b3e7a0 --- /dev/null +++ b/REMEDIATION.md @@ -0,0 +1,354 @@ +# Remediation Plan + +**Date**: 2026-03-16 +**Scope**: Audit findings #25–#38 from engine design review + +This document provides a concrete remediation plan for each open finding. Items +are grouped by priority and ordered for efficient implementation (dependencies +first). + +--- + +## Critical + +### #37 — `adminOnlyOperations` name collision blocks user `rotate-key` + +**Problem**: The `adminOnlyOperations` map in `handleEngineRequest` +(`internal/server/routes.go:265`) is a flat `map[string]bool` keyed by +operation name. The transit engine's `rotate-key` is admin-only, but the user +engine's `rotate-key` is user-self. Since the map is checked before engine +dispatch, non-admin users are blocked from calling `rotate-key` on any engine +mount — including user engine mounts where it should be allowed. + +**Fix**: Replace the flat map with an engine-type-qualified lookup. Two options: + +**Option A — Qualify the map key** (minimal change): + +Change the map type to include the engine type prefix: + +```go +var adminOnlyOperations = map[string]bool{ + "ca:import-root": true, + "ca:create-issuer": true, + "ca:delete-issuer": true, + "ca:revoke-cert": true, + "ca:delete-cert": true, + "transit:create-key": true, + "transit:delete-key": true, + "transit:rotate-key": true, + "transit:update-key-config": true, + "transit:trim-key": true, + "sshca:create-profile": true, + "sshca:update-profile": true, + "sshca:delete-profile": true, + "sshca:revoke-cert": true, + "sshca:delete-cert": true, + "user:provision": true, + "user:delete-user": true, +} +``` + +In `handleEngineRequest`, look up `engineType + ":" + operation` instead of +just `operation`. The `engineType` is already known from the mount registry +(the generic endpoint resolves the mount to an engine type). + +**Option B — Per-engine admin operations** (cleaner but more code): + +Each engine implements an `AdminOperations() []string` method. The server +queries the resolved engine for its admin operations instead of using a global +map. + +**Recommendation**: Option A. It requires a one-line change to the lookup and +a mechanical update to the map keys. The generic endpoint already resolves the +mount to get the engine type. + +**Files to change**: +- `internal/server/routes.go` — update map and lookup in `handleEngineRequest` +- `engines/sshca.md` — update `adminOnlyOperations` section +- `engines/transit.md` — update `adminOnlyOperations` section +- `engines/user.md` — update `adminOnlyOperations` section + +**Tests**: Add test case in `internal/server/server_test.go` — non-admin user +calling `rotate-key` via generic endpoint on a user engine mount should succeed +(policy permitting). Same call on a transit mount should return 403. + +--- + +## High + +### #28 — HMAC output not versioned + +**Problem**: HMAC output is raw base64 with no key version indicator. After key +rotation and `min_decryption_version` advancement, old HMACs are unverifiable +because the engine doesn't know which key version produced them. + +**Fix**: Use the same versioned prefix format as ciphertext and signatures: + +``` +metacrypt:v{version}:{base64(mac_bytes)} +``` + +Update the `hmac` operation to include `key_version` in the response. Update +internal HMAC verification to parse the version prefix and select the +corresponding key version (subject to `min_decryption_version` enforcement). + +**Files to change**: +- `engines/transit.md` — update HMAC section, add HMAC output format, update + Cryptographic Details section +- Implementation: `internal/engine/transit/sign.go` (when implemented) + +### #30 — `max_key_versions` vs `min_decryption_version` unclear + +**Problem**: The spec doesn't define when `max_key_versions` pruning happens or +whether it respects `min_decryption_version`. Auto-pruning on rotation could +destroy versions that still have unrewrapped ciphertext. + +**Fix**: Define the behavior explicitly in `engines/transit.md`: + +1. `max_key_versions` pruning happens during `rotate-key`, after the new + version is created. +2. Pruning **only** deletes versions **strictly less than** + `min_decryption_version`. If `max_key_versions` would require deleting a + version at or above `min_decryption_version`, the version is **retained** + and a warning is included in the response: + `"warning": "max_key_versions exceeded; advance min_decryption_version to enable pruning"`. +3. This means `max_key_versions` is a soft limit — it is only enforceable + after the operator completes the rotation cycle (rotate → rewrap → advance + min → prune happens automatically on next rotate). + +This resolves the original audit finding #16 as well. + +**Files to change**: +- `engines/transit.md` — add `max_key_versions` behavior to Key Rotation + section and `rotate-key` flow +- `AUDIT.md` — mark #16 as RESOLVED with reference to the new behavior + +### #33 — Auto-provision creates keys for arbitrary usernames + +**Problem**: The encrypt flow auto-provisions recipients without validating +that the username exists in MCIAS. Any authenticated user can create barrier +entries for non-existent users. + +**Fix**: Before auto-provisioning, validate the recipient username against +MCIAS. The engine has access to the auth system via `req.CallerInfo` context. +Add an MCIAS user lookup: + +1. Add a `ValidateUsername(username string) (bool, error)` method to the auth + client interface. This calls the MCIAS user info endpoint to check if the + username exists. +2. In the encrypt flow, before auto-provisioning a recipient, call + `ValidateUsername`. If the user doesn't exist in MCIAS, return an error: + `"recipient not found: {username}"`. +3. Document this validation in the encrypt flow and security considerations. + +**Alternative** (simpler, weaker): Skip MCIAS validation but add a +rate limit on auto-provisioning (e.g., max 10 new provisions per encrypt +request, max 100 total auto-provisions per hour per caller). This prevents +storage inflation but doesn't prevent phantom users. + +**Recommendation**: MCIAS validation. It's the correct security boundary — +only real MCIAS users should have keypairs. + +**Files to change**: +- `engines/user.md` — update encrypt flow step 2, add MCIAS validation +- `internal/auth/` — add `ValidateUsername` to auth client (when implemented) + +--- + +## Medium + +### #25 — Missing `list-certs` REST route (SSH CA) + +**Fix**: Add to the REST endpoints table: + +``` +| GET | `/v1/sshca/{mount}/certs` | List cert records | +``` + +Add to the route registration code block: + +```go +r.Get("/v1/sshca/{mount}/certs", s.requireAuth(s.handleSSHCAListCerts)) +``` + +**Files to change**: `engines/sshca.md` + +### #26 — KRL section type description error + +**Fix**: Change the description block from: + +``` +Section type: KRL_SECTION_CERT_SERIAL_LIST (0x21) +``` + +to: + +``` +Section type: KRL_SECTION_CERTIFICATES (0x01) + CA key blob: ssh.MarshalAuthorizedKey(caSigner.PublicKey()) + Subsection type: KRL_SECTION_CERT_SERIAL_LIST (0x20) +``` + +This matches the pseudocode comments and the OpenSSH `PROTOCOL.krl` spec. + +**Files to change**: `engines/sshca.md` + +### #27 — Policy check after cert construction (SSH CA) + +**Fix**: Reorder the sign-host flow steps: + +1. Authenticate caller. +2. Parse the supplied SSH public key. +3. Parse TTL. +4. **Policy check**: for each hostname, check policy on + `sshca/{mount}/id/{hostname}`, action `sign`. +5. Generate serial (only after policy passes). +6. Build `ssh.Certificate`. +7. Sign, store, return. + +Same reordering for sign-user. + +**Files to change**: `engines/sshca.md` + +### #29 — `rewrap` policy action not specified + +**Fix**: Add `rewrap` as an explicit action in the `operationAction` mapping. +`rewrap` maps to `decrypt` (since it requires internal access to plaintext). +Batch variants map to the same action. + +Add to the authorization section in `engines/transit.md`: + +> The `rewrap` and `batch-rewrap` operations require the `decrypt` action — +> rewrap internally decrypts with the old version and re-encrypts with the +> latest, so the caller must have decrypt permission. Alternatively, a +> dedicated `rewrap` action could be added for finer-grained control, but +> `decrypt` is the safer default (granting `rewrap` without `decrypt` would be +> odd since rewrap implies decrypt capability). + +**Recommendation**: Map to `decrypt`. Simpler, and anyone who should rewrap +should also be able to decrypt. + +**Files to change**: `engines/transit.md` + +### #31 — Missing `get-public-key` REST route (Transit) + +**Fix**: Add to the REST endpoints table: + +``` +| GET | `/v1/transit/{mount}/keys/{name}/public-key` | Get public key | +``` + +Add to the route registration code block: + +```go +r.Get("/v1/transit/{mount}/keys/{name}/public-key", s.requireAuth(s.handleTransitGetPublicKey)) +``` + +**Files to change**: `engines/transit.md` + +### #34 — No recipient limit on encrypt (User) + +**Fix**: Add a compile-time constant `maxRecipients = 100` to the user engine. +Reject requests exceeding this limit with `400 Bad Request` / `InvalidArgument` +before any ECDH computation. + +Add to the encrypt flow in `engines/user.md` after step 1: + +> Validate that `len(recipients) <= maxRecipients` (100). Reject with error if +> exceeded. + +Add to the security considerations section. + +**Files to change**: `engines/user.md` + +--- + +## Low + +### #32 — `exportable` flag with no export operation (Transit) + +**Fix**: Add an `export-key` operation to the transit engine: + +- Auth: User+Policy (action `read`). +- Only succeeds if the key's `exportable` flag is `true`. +- Returns raw key material (base64-encoded) for the current version only. +- Asymmetric keys: returns private key in PKCS8 PEM. +- Symmetric keys: returns raw key bytes, base64-encoded. +- Add to HandleRequest dispatch, gRPC service, REST endpoints. + +Alternatively, if key export is never intended, remove the `exportable` flag +from `create-key` to avoid dead code. Given that transit is meant to keep keys +server-side, **removing the flag** may be the better choice. Document the +decision either way. + +**Recommendation**: Remove `exportable`. Transit's entire value proposition is +that keys never leave the service. If export is needed for migration, a +dedicated admin-only `export-key` can be added later with appropriate audit +logging (#7). + +**Files to change**: `engines/transit.md` + +### #35 — No re-encryption support for user key rotation + +**Fix**: Add a `re-encrypt` operation: + +- Auth: User (self) — only the envelope recipient can re-encrypt. +- Input: old envelope. +- Flow: decrypt with current key, generate new DEK, re-encrypt, return new + envelope. +- The old key must still be valid at the time of re-encryption. Document the + workflow: re-encrypt all stored envelopes, then rotate-key. + +This is a quality-of-life improvement, not a security fix. The current design +(decrypt + encrypt separately) works but requires the caller to handle +plaintext. + +**Files to change**: `engines/user.md` + +### #36 — `UserKeyConfig` type undefined + +**Fix**: Add the type definition to the in-memory state section: + +```go +type UserKeyConfig struct { + Algorithm string `json:"algorithm"` // key exchange algorithm used + CreatedAt time.Time `json:"created_at"` + AutoProvisioned bool `json:"auto_provisioned"` // created via auto-provision +} +``` + +**Files to change**: `engines/user.md` + +### #38 — `ZeroizeKey` prerequisite not cross-referenced + +**Fix**: Add to the Implementation Steps section in both `engines/transit.md` +and `engines/user.md`: + +> **Prerequisite**: `engine.ZeroizeKey` must exist in +> `internal/engine/helpers.go` (created as part of the SSH CA engine +> implementation — see `engines/sshca.md` step 1). + +**Files to change**: `engines/transit.md`, `engines/user.md` + +--- + +## Implementation Order + +The remediation items should be implemented in this order to respect +dependencies: + +1. **#37** — `adminOnlyOperations` qualification (critical, blocks user engine + `rotate-key`). This is a code change to `internal/server/routes.go` plus + spec updates. Do first because it affects all engine implementations. + +2. **#28, #29, #30, #31, #32** — Transit spec fixes (can be done as a single + spec update pass). + +3. **#25, #26, #27** — SSH CA spec fixes (single spec update pass). + +4. **#33, #34, #35, #36** — User spec fixes (single spec update pass). + +5. **#38** — Cross-reference update (trivial, do with transit and user spec + fixes). + +Items within the same group are independent and can be done in parallel. diff --git a/cmd/metacrypt/migrate_barrier.go b/cmd/metacrypt/migrate_barrier.go new file mode 100644 index 0000000..e6d2a4f --- /dev/null +++ b/cmd/metacrypt/migrate_barrier.go @@ -0,0 +1,160 @@ +package main + +import ( + "context" + "database/sql" + "fmt" + "os" + "syscall" + + "github.com/spf13/cobra" + "golang.org/x/term" + + "git.wntrmute.dev/kyle/metacrypt/internal/barrier" + "git.wntrmute.dev/kyle/metacrypt/internal/config" + "git.wntrmute.dev/kyle/metacrypt/internal/crypto" + "git.wntrmute.dev/kyle/metacrypt/internal/db" +) + +var migrateBarrierCmd = &cobra.Command{ + Use: "migrate-barrier", + Short: "Migrate barrier entries from v1 (MEK) to v2 (per-engine DEKs)", + Long: `Converts all v1 barrier entries to v2 format with per-engine data +encryption keys (DEKs). Creates a "system" DEK for non-engine data and +per-engine DEKs for each engine mount found in the barrier. + +After migration, each engine's data is encrypted with its own DEK rather +than the MEK directly, limiting blast radius if a single key is compromised. +The MEK only wraps the DEKs. + +Entries already in v2 format are skipped. Run while the server is stopped +to avoid concurrent access. Requires the unseal password.`, + RunE: runMigrateBarrier, +} + +var migrateBarrierDryRun bool + +func init() { + migrateBarrierCmd.Flags().BoolVar(&migrateBarrierDryRun, "dry-run", false, "report what would be migrated without writing") + rootCmd.AddCommand(migrateBarrierCmd) +} + +func runMigrateBarrier(cmd *cobra.Command, args []string) error { + configPath := cfgFile + if configPath == "" { + configPath = "/srv/metacrypt/metacrypt.toml" + } + + cfg, err := config.Load(configPath) + if err != nil { + return err + } + + database, err := db.Open(cfg.Database.Path) + if err != nil { + return err + } + defer func() { _ = database.Close() }() + + // Apply any pending schema migrations (creates barrier_keys table). + if err := db.Migrate(database); err != nil { + return fmt.Errorf("schema migration: %w", err) + } + + // Read unseal password. + fmt.Fprint(os.Stderr, "Unseal password: ") + passwordBytes, err := term.ReadPassword(int(syscall.Stdin)) + fmt.Fprintln(os.Stderr) + if err != nil { + return fmt.Errorf("read password: %w", err) + } + defer crypto.Zeroize(passwordBytes) + + // Load seal config and derive MEK. + mek, err := deriveMEK(database, passwordBytes) + if err != nil { + return err + } + defer crypto.Zeroize(mek) + + if migrateBarrierDryRun { + return migrateBarrierDryRunReport(database, mek) + } + + // Unseal the barrier with the MEK (loads existing DEKs). + b := barrier.NewAESGCMBarrier(database) + if err := b.Unseal(mek); err != nil { + return fmt.Errorf("unseal barrier: %w", err) + } + defer func() { _ = b.Seal() }() + + ctx := context.Background() + migrated, err := b.MigrateToV2(ctx) + if err != nil { + return fmt.Errorf("migration failed: %w", err) + } + + if migrated == 0 { + fmt.Println("Nothing to migrate — all entries already use v2 format.") + } else { + fmt.Printf("Migrated %d entries to v2 format with per-engine DEKs.\n", migrated) + } + + // Show key summary. + keys, err := b.ListKeys(ctx) + if err != nil { + return fmt.Errorf("list keys: %w", err) + } + if len(keys) > 0 { + fmt.Printf("\nBarrier keys (%d):\n", len(keys)) + for _, k := range keys { + fmt.Printf(" %-30s version=%d created=%s\n", k.KeyID, k.Version, k.CreatedAt) + } + } + + return nil +} + +func migrateBarrierDryRunReport(database *sql.DB, mek []byte) error { + ctx := context.Background() + rows, err := database.QueryContext(ctx, "SELECT path, value FROM barrier_entries") + if err != nil { + return fmt.Errorf("query entries: %w", err) + } + defer func() { _ = rows.Close() }() + + var v1Count, v2Count, total int + var v1Paths []string + for rows.Next() { + var path string + var value []byte + if err := rows.Scan(&path, &value); err != nil { + return fmt.Errorf("scan: %w", err) + } + total++ + if len(value) > 0 && value[0] == crypto.BarrierVersionV2 { + v2Count++ + } else { + v1Count++ + v1Paths = append(v1Paths, path) + } + } + if err := rows.Err(); err != nil { + return fmt.Errorf("iterate: %w", err) + } + + fmt.Printf("Total entries: %d\n", total) + fmt.Printf("Already v2: %d\n", v2Count) + fmt.Printf("Need migration: %d\n", v1Count) + + if len(v1Paths) > 0 { + fmt.Println("\nEntries that would be migrated:") + for _, p := range v1Paths { + fmt.Printf(" %s\n", p) + } + } else { + fmt.Println("\nNothing to migrate.") + } + + return nil +} diff --git a/cmd/metacrypt/server.go b/cmd/metacrypt/server.go index 82a30e0..1c9627c 100644 --- a/cmd/metacrypt/server.go +++ b/cmd/metacrypt/server.go @@ -17,6 +17,7 @@ import ( "git.wntrmute.dev/kyle/metacrypt/internal/engine" "git.wntrmute.dev/kyle/metacrypt/internal/engine/ca" "git.wntrmute.dev/kyle/metacrypt/internal/engine/sshca" + "git.wntrmute.dev/kyle/metacrypt/internal/engine/transit" "git.wntrmute.dev/kyle/metacrypt/internal/grpcserver" "git.wntrmute.dev/kyle/metacrypt/internal/policy" "git.wntrmute.dev/kyle/metacrypt/internal/seal" @@ -76,6 +77,7 @@ func runServer(cmd *cobra.Command, args []string) error { engineRegistry := engine.NewRegistry(b, logger) engineRegistry.RegisterFactory(engine.EngineTypeCA, ca.NewCAEngine) engineRegistry.RegisterFactory(engine.EngineTypeSSHCA, sshca.NewSSHCAEngine) + engineRegistry.RegisterFactory(engine.EngineTypeTransit, transit.NewTransitEngine) srv := server.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger, version) grpcSrv := grpcserver.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger) diff --git a/docs/engineering-standards.md b/docs/engineering-standards.md new file mode 100644 index 0000000..6f97a86 --- /dev/null +++ b/docs/engineering-standards.md @@ -0,0 +1,713 @@ +# Metacircular Dynamics — Engineering Standards + +This document describes the standard repository layout, tooling, and software +development lifecycle (SDLC) for services built at Metacircular Dynamics. It is +derived from the conventions established in Metacrypt and codifies them as the +baseline for all new and existing services. + +## Table of Contents + +1. [Repository Layout](#repository-layout) +2. [Language & Toolchain](#language--toolchain) +3. [Build System](#build-system) +4. [API Design](#api-design) +5. [Authentication & Authorization](#authentication--authorization) +6. [Database Conventions](#database-conventions) +7. [Configuration](#configuration) +8. [Web UI](#web-ui) +9. [Testing](#testing) +10. [Linting & Static Analysis](#linting--static-analysis) +11. [Deployment](#deployment) +12. [Documentation](#documentation) +13. [Security](#security) +14. [Development Workflow](#development-workflow) + +--- + +## Repository Layout + +Every service follows a consistent directory structure. Adjust the +service-specific directories (e.g. `engines/` in Metacrypt) as appropriate, +but the top-level skeleton is fixed. + +``` +. +├── cmd/ +│ ├── / CLI entry point (server, subcommands) +│ └── -web/ Web UI entry point (if separate binary) +├── internal/ +│ ├── auth/ MCIAS integration (token validation, caching) +│ ├── config/ TOML configuration loading & validation +│ ├── db/ Database setup, schema migrations +│ ├── server/ REST API server, routes, middleware +│ ├── grpcserver/ gRPC server, interceptors, service handlers +│ ├── webserver/ Web UI server, template routes, HTMX handlers +│ └── / Service-specific packages +├── proto// +│ ├── v1/ Legacy proto definitions (if applicable) +│ └── v2/ Current proto definitions +├── gen// +│ ├── v1/ Generated Go gRPC/protobuf code +│ └── v2/ +├── web/ +│ ├── embed.go //go:embed directive for templates and static +│ ├── templates/ Go HTML templates +│ └── static/ CSS, JS (htmx) +├── deploy/ +│ ├── docker/ Docker Compose configuration +│ ├── examples/ Example config files +│ ├── scripts/ Install, backup, migration scripts +│ └── systemd/ systemd unit files and timers +├── docs/ Internal engineering documentation +├── Dockerfile.api API server container (if split binary) +├── Dockerfile.web Web UI container (if split binary) +├── Makefile +├── buf.yaml Protobuf linting & breaking-change config +├── .golangci.yaml Linter configuration +├── .gitignore +├── CLAUDE.md AI-assisted development instructions +├── ARCHITECTURE.md Full system specification +└── .toml.example Example configuration +``` + +### Key Principles + +- **`cmd/`** contains only CLI wiring (cobra commands, flag parsing). No + business logic. +- **`internal/`** contains all service logic. Nothing in `internal/` is + importable by other modules — this is enforced by Go's module system. +- **`proto/`** is the source of truth for gRPC definitions. Generated code + lives in `gen/`, never edited by hand. +- **`deploy/`** contains everything needed to run the service in production. + A new engineer should be able to deploy from this directory alone. +- **`web/`** is embedded into the binary via `//go:embed`. No external file + dependencies at runtime. + +### What Does Not Belong in the Repository + +- Runtime data (databases, certificates, logs) — these live in `/srv/` +- Real configuration files with secrets — only examples are committed +- IDE configuration (`.idea/`, `.vscode/`) — per-developer, not shared +- Vendored dependencies — Go module proxy handles this + +--- + +## Language & Toolchain + +| Tool | Version | Purpose | +|------|---------|---------| +| Go | 1.25+ | Primary language | +| protoc + protoc-gen-go | Latest | Protobuf/gRPC code generation | +| buf | Latest | Proto linting and breaking-change detection | +| golangci-lint | v2 | Static analysis and linting | +| Docker | Latest | Container builds | + +### Go Conventions + +- **Pure-Go dependencies** where possible. Avoid CGo — it complicates + cross-compilation and container builds. Use `modernc.org/sqlite` instead + of `mattn/go-sqlite3`. +- **`CGO_ENABLED=0`** for all production builds. Statically linked binaries + deploy cleanly to Alpine containers. +- **Stripped binaries**: Build with `-trimpath -ldflags="-s -w"` to remove + debug symbols and reduce image size. +- **Version injection**: Pass `git describe --tags --always --dirty` via + `-X main.version=...` at build time. Every binary must report its version. + +### Module Path + +Services hosted on `git.wntrmute.dev` use: + +``` +git.wntrmute.dev/kyle/ +``` + +--- + +## Build System + +Every repository has a Makefile with these standard targets: + +```makefile +.PHONY: build test vet lint proto-lint clean docker all + +LDFLAGS := -trimpath -ldflags="-s -w -X main.version=$(shell git describe --tags --always --dirty)" + +: + go build $(LDFLAGS) -o ./cmd/ + +build: + go build ./... + +test: + go test ./... + +vet: + go vet ./... + +lint: + golangci-lint run ./... + +proto: + protoc --go_out=. --go_opt=module= \ + --go-grpc_out=. --go-grpc_opt=module= \ + proto//v2/*.proto + +proto-lint: + buf lint + buf breaking --against '.git#branch=master,subdir=proto' + +clean: + rm -f + +docker: + docker build -t -f Dockerfile.api . + +all: vet lint test +``` + +### Target Semantics + +| Target | When to Run | CI Gate? | +|--------|-------------|----------| +| `vet` | Every change | Yes | +| `lint` | Every change | Yes | +| `test` | Every change | Yes | +| `proto-lint` | Any proto change | Yes | +| `proto` | After editing `.proto` files | No (manual) | +| `all` | Pre-push verification | Yes | + +The `all` target is the CI pipeline: `vet → lint → test → build`. If any +step fails, the pipeline stops. + +--- + +## API Design + +Services expose two synchronized API surfaces: + +### gRPC (Primary) + +- Proto definitions live in `proto//v2/`. +- Use strongly-typed, per-operation RPCs. Avoid generic "execute" patterns. +- Use `google.protobuf.Timestamp` for all time fields (not RFC 3339 strings). +- Run `buf lint` and `buf breaking` against master before merging proto + changes. + +### REST (Secondary) + +- JSON over HTTPS. Routes live in `internal/server/routes.go`. +- Use `chi` for routing (lightweight, stdlib-compatible). +- Standard error format: `{"error": "description"}`. +- Standard HTTP status codes: `401` (unauthenticated), `403` (unauthorized), + `412` (precondition failed), `503` (service unavailable). + +### API Sync Rule + +**Every REST endpoint must have a corresponding gRPC RPC, and vice versa.** +When adding, removing, or changing an endpoint in either surface, the other +must be updated in the same change. This is enforced in code review. + +### gRPC Interceptors + +Access control is enforced via interceptor maps, not per-handler checks: + +| Map | Effect | +|-----|--------| +| `sealRequiredMethods` | Returns `UNAVAILABLE` if the service is sealed/locked | +| `authRequiredMethods` | Validates MCIAS bearer token, populates caller info | +| `adminRequiredMethods` | Requires admin role on the caller | + +Adding a new RPC means adding it to the correct interceptor maps. Forgetting +this is a security defect. + +--- + +## Authentication & Authorization + +### Authentication + +All services delegate authentication to **MCIAS** (Metacircular Identity and +Access Service). No service maintains its own user database. + +- Client sends credentials to the service's `/v1/auth/login` endpoint. +- The service forwards them to MCIAS via the client library + (`git.wntrmute.dev/kyle/mcias/clients/go`). +- On success, MCIAS returns a bearer token. The service returns it to the + client and optionally sets it as a cookie for the web UI. +- Subsequent requests include the token via `Authorization: Bearer ` + header or cookie. +- Token validation calls MCIAS `ValidateToken()`. Results should be cached + (keyed by SHA-256 of the token) with a short TTL (30 seconds or less). + +### Authorization + +Three role levels: + +| Role | Meaning | +|------|---------| +| `admin` | Full access to everything. Policy bypass. | +| `user` | Access governed by policy rules. Default deny. | +| `guest` | Service-dependent restrictions. Default deny. | + +Admin detection is based solely on the MCIAS `admin` role. The service never +promotes users locally. + +Services that need fine-grained access control should implement a policy +engine (priority-based ACL rules stored in encrypted storage, default deny, +admin bypass). See Metacrypt's implementation as the reference. + +--- + +## Database Conventions + +### SQLite + +SQLite is the default database for Metacircular services. It is simple to +operate, requires no external processes, and backs up cleanly with +`VACUUM INTO`. + +Connection settings (applied at open time): + +```go +PRAGMA journal_mode = WAL; +PRAGMA foreign_keys = ON; +PRAGMA busy_timeout = 5000; +``` + +File permissions: `0600`. Created by the service on first run. + +### Migrations + +- Migrations are Go functions registered in `internal/db/` and run + sequentially at startup. +- Each migration is idempotent — `CREATE TABLE IF NOT EXISTS`, + `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`. +- Applied migrations are tracked in a `schema_migrations` table. +- Never modify a migration that has been deployed. Add a new one. + +### Backup + +Every service must provide a `snapshot` CLI command that creates a consistent +backup using `VACUUM INTO`. Automated backups run via a systemd timer +(daily, with retention pruning). + +--- + +## Configuration + +### Format + +TOML. Parsed with `go-toml/v2`. Environment variable overrides via +`SERVICENAME_*` (e.g. `METACRYPT_SERVER_LISTEN_ADDR`). + +### Standard Sections + +```toml +[server] +listen_addr = ":8443" # HTTPS API +grpc_addr = ":9443" # gRPC (optional; disabled if unset) +tls_cert = "/srv//certs/cert.pem" +tls_key = "/srv//certs/key.pem" + +[web] +listen_addr = "127.0.0.1:8080" # Web UI (optional; disabled if unset) +vault_grpc = "127.0.0.1:9443" # gRPC address of the API server +vault_ca_cert = "" # CA cert for verifying API server TLS + +[database] +path = "/srv//.db" + +[mcias] +server_url = "https://mcias.metacircular.net:8443" +ca_cert = "" # Custom CA for MCIAS TLS + +[log] +level = "info" # debug, info, warn, error +``` + +### Validation + +Required fields are validated at startup. The service refuses to start if +any are missing. Do not silently default required values. + +### Data Directory + +All runtime data lives in `/srv//`: + +``` +/srv// +├── .toml Configuration +├── .db SQLite database +├── certs/ TLS certificates +└── backups/ Database snapshots +``` + +This convention enables straightforward service migration between hosts: +copy `/srv//` and the binary. + +--- + +## Web UI + +### Technology + +- **Go `html/template`** for server-side rendering. No JavaScript frameworks. +- **htmx** for dynamic interactions (form submission, partial page updates) + without full page reloads. +- Templates and static files are embedded in the binary via `//go:embed`. + +### Structure + +- `web/templates/layout.html` — shared HTML skeleton, navigation, CSS/JS + includes. All page templates extend this. +- Page templates: one `.html` file per page/feature. +- `web/static/` — CSS, htmx. Keep this minimal. + +### Architecture + +The web UI runs as a separate binary (`-web`) that communicates +with the API server via its gRPC interface. This separation means: + +- The web UI has no direct database access. +- The API server enforces all authorization. +- The web UI can be deployed independently or omitted entirely. + +### Security + +- CSRF protection via signed double-submit cookies on all mutating requests + (POST/PUT/PATCH/DELETE). +- Session cookie: `HttpOnly`, `Secure`, `SameSite=Strict`. +- All user input is escaped by `html/template` (the default). + +--- + +## Testing + +### Philosophy + +Tests are written using the Go standard library `testing` package. No test +frameworks (testify, gomega, etc.) — the standard library is sufficient and +keeps dependencies minimal. + +### Patterns + +```go +func TestFeatureName(t *testing.T) { + // Setup: use t.TempDir() for isolated file system state. + dir := t.TempDir() + database, err := db.Open(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("open db: %v", err) + } + defer func() { _ = database.Close() }() + db.Migrate(database) + + // Exercise the code under test. + // ... + + // Assert with t.Fatal (not t.Error) for precondition failures. + if !bytes.Equal(got, want) { + t.Fatalf("got %q, want %q", got, want) + } +} +``` + +### Guidelines + +- **Use `t.TempDir()`** for all file-system state. Never write to fixed + paths. Cleanup is automatic. +- **Use `errors.Is`** for error assertions, not string comparison. +- **No mocks for databases.** Tests use real SQLite databases created in + temp directories. This catches migration bugs that mocks would hide. +- **Test files** live alongside the code they test: `barrier.go` and + `barrier_test.go` in the same package. +- **Test helpers** call `t.Helper()` so failures report the caller's line. + +### What to Test + +| Layer | Test Strategy | +|-------|---------------| +| Crypto primitives | Roundtrip encryption/decryption, wrong-key rejection, edge cases | +| Storage (barrier, DB) | CRUD operations, sealed-state rejection, concurrent access | +| API handlers | Request/response correctness, auth enforcement, error codes | +| Policy engine | Rule matching, priority ordering, default deny, admin bypass | +| CLI commands | Flag parsing, output format (lightweight) | + +--- + +## Linting & Static Analysis + +### Configuration + +Every repository includes a `.golangci.yaml` with this philosophy: +**fail loudly for security and correctness; everything else is a warning.** + +### Required Linters + +| Linter | Category | Purpose | +|--------|----------|---------| +| `errcheck` | Correctness | Unhandled errors are silent failures | +| `govet` | Correctness | Printf mismatches, unreachable code, suspicious constructs | +| `ineffassign` | Correctness | Dead writes hide logic bugs | +| `unused` | Correctness | Unused variables and functions | +| `errorlint` | Error handling | Proper `errors.Is`/`errors.As` usage | +| `gosec` | Security | Hardcoded secrets, weak RNG, insecure crypto, SQL injection | +| `staticcheck` | Security | Deprecated APIs, mutex misuse, deep analysis | +| `revive` | Style | Go naming conventions, error return ordering | +| `gofmt` | Formatting | Standard Go formatting | +| `goimports` | Formatting | Import grouping and ordering | + +### Settings + +- `errcheck`: `check-type-assertions: true` (catch `x.(*T)` without ok check). +- `govet`: all analyzers enabled except `shadow` (too noisy for idiomatic Go). +- `gosec`: severity and confidence set to `medium`. Exclude `G104` (overlaps + with errcheck). +- `max-issues-per-linter: 0` — report everything. No caps. +- Test files: allow `G101` (hardcoded credentials) for test fixtures. + +--- + +## Deployment + +### Container-First + +Services are designed for container deployment but must also run as native +systemd services. Both paths are first-class. + +### Docker + +Multi-stage builds: + +1. **Builder**: `golang:1.23-alpine`. Compile with `CGO_ENABLED=0`, strip + symbols. +2. **Runtime**: `alpine:3.21`. Non-root user (``), minimal attack + surface. + +If the service has separate API and web binaries, use separate Dockerfiles +(`Dockerfile.api`, `Dockerfile.web`) and a `docker-compose.yml` that wires +them together with a shared data volume. + +### systemd + +Every service ships with: + +| File | Purpose | +|------|---------| +| `.service` | Main service unit (API server) | +| `-web.service` | Web UI unit (if applicable) | +| `-backup.service` | Oneshot backup unit | +| `-backup.timer` | Daily backup timer (02:00 UTC, 5-minute jitter) | + +#### Security Hardening + +All service units must include these security directives: + +```ini +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true +PrivateDevices=true +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictSUIDSGID=true +RestrictNamespaces=true +LockPersonality=true +MemoryDenyWriteExecute=true +RestrictRealtime=true +ReadWritePaths=/srv/ +``` + +The web UI unit should use `ReadOnlyPaths=/srv/` instead of +`ReadWritePaths` — it has no reason to write to the data directory. + +### Install Script + +`deploy/scripts/install.sh` handles: + +1. Create system user/group (idempotent). +2. Install binary to `/usr/local/bin/`. +3. Create `/srv//` directory structure. +4. Install example config if none exists. +5. Install systemd units and reload the daemon. + +### TLS + +- **Minimum TLS version: 1.3.** No exceptions, no fallback cipher suites. + Go's TLS 1.3 implementation manages cipher selection automatically. +- **Timeouts**: read 30s, write 30s, idle 120s. +- Certificate and key paths are required configuration — the service refuses + to start without them. + +### Graceful Shutdown + +Services handle `SIGINT` and `SIGTERM`, shutting down cleanly: + +1. Stop accepting new connections. +2. Drain in-flight requests (with a timeout). +3. Clean up resources (close databases, zeroize secrets if applicable). +4. Exit. + +--- + +## Documentation + +### Required Files + +| File | Purpose | Audience | +|------|---------|----------| +| `CLAUDE.md` | AI-assisted development context | Claude Code | +| `ARCHITECTURE.md` | Full system specification | Engineers | +| `deploy/examples/.toml` | Example configuration | Operators | + +### CLAUDE.md + +This file provides context for AI-assisted development. It should contain: + +- Project overview (one paragraph). +- Build, test, and lint commands. +- High-level architecture summary. +- Project structure with directory descriptions. +- Ignored directories (runtime data, generated code). +- Critical rules (e.g. API sync requirements). + +Keep it concise. AI tools read this on every interaction. + +### ARCHITECTURE.md + +This is the canonical specification for the service. It should cover: + +1. System overview with a layered architecture diagram. +2. Cryptographic design (if applicable): algorithms, key hierarchy. +3. State machines and lifecycle (if applicable). +4. Storage design. +5. Authentication and authorization model. +6. API surface (REST and gRPC, with tables of every endpoint). +7. Web interface routes. +8. Database schema (every table, every column). +9. Configuration reference. +10. Deployment guide. +11. Security model: threat mitigations table and security invariants. +12. Future work. + +This document is the source of truth. When the code and the spec disagree, +one of them has a bug. + +### Engine/Feature Design Documents + +For services with a modular architecture, each module gets its own design +document (e.g. `engines/sshca.md`). These are detailed implementation plans +that include: + +- Overview and core concepts. +- Data model and storage layout. +- Lifecycle (initialization, teardown). +- Operations table with auth requirements. +- API definitions (gRPC and REST). +- Implementation steps (file-by-file). +- Security considerations. +- References to existing code patterns to follow. + +Write these before writing code. They are the blueprint, not the afterthought. + +--- + +## Security + +### General Principles + +- **Default deny.** Unauthenticated requests are rejected. Unauthorized + requests are rejected. If in doubt, deny. +- **Fail closed.** If the service cannot verify authorization, it denies the + request. If the database is unavailable, the service is unavailable. +- **Least privilege.** Service processes run as non-root. systemd units + restrict filesystem access, syscalls, and capabilities. +- **No local user databases.** Authentication is always delegated to MCIAS. + +### Cryptographic Standards + +| Purpose | Algorithm | Notes | +|---------|-----------|-------| +| Symmetric encryption | AES-256-GCM | 12-byte random nonce per operation | +| Symmetric alternative | XChaCha20-Poly1305 | For contexts needing nonce misuse resistance | +| Key derivation | Argon2id | Memory-hard; tune params to hardware | +| Asymmetric signing | Ed25519, ECDSA (P-256, P-384) | Prefer Ed25519 | +| CSPRNG | `crypto/rand` | All keys, nonces, salts, tokens | +| Constant-time comparison | `crypto/subtle` | All secret comparisons | + +- **Never use RSA for new designs.** Ed25519 and ECDSA are faster, produce + smaller keys, and have simpler security models. +- **Zeroize secrets** from memory when they are no longer needed. Overwrite + byte slices with zeros, nil out pointers. +- **Never log secrets.** Keys, passwords, tokens, and plaintext must never + appear in log output. + +### Web Security + +- CSRF tokens on all mutating requests. +- `SameSite=Strict` on all cookies. +- `html/template` for automatic escaping. +- Validate all input at system boundaries. + +--- + +## Development Workflow + +### Local Development + +```bash +# Build and run both servers locally: +make devserver + +# Or build everything and run the full pipeline: +make all +``` + +The `devserver` target builds both binaries and runs them against a local +config in `srv/`. The `srv/` directory is gitignored — it holds your local +database, certificates, and configuration. + +### Pre-Push Checklist + +Before pushing a branch: + +```bash +make all # vet → lint → test → build +make proto-lint # if proto files changed +``` + +### Proto Changes + +1. Edit `.proto` files in `proto//v2/`. +2. Run `make proto` to regenerate Go code. +3. Run `make proto-lint` to check for linting violations and breaking changes. +4. Update REST routes to match the new/changed RPCs. +5. Update gRPC interceptor maps for any new RPCs. +6. Update `ARCHITECTURE.md` API tables. + +### Adding a New Feature + +1. **Design first.** Write or update the relevant design document. For a new + engine or major subsystem, create a new doc in `docs/` or `engines/`. +2. **Implement.** Follow existing patterns — the design doc should reference + specific files and line numbers. +3. **Test.** Write tests alongside the implementation. +4. **Update docs.** Update `ARCHITECTURE.md`, `CLAUDE.md`, and route tables. +5. **Verify.** Run `make all`. + +### CLI Commands + +Every service uses cobra for CLI commands. Standard subcommands: + +| Command | Purpose | +|---------|---------| +| `server` | Start the service | +| `init` | First-time setup (if applicable) | +| `status` | Query a running instance's health | +| `snapshot` | Create a database backup | + +Add service-specific subcommands as needed (e.g. `migrate-aad`, `unseal`). +Each command lives in its own file in `cmd//`. diff --git a/gen/metacrypt/v2/transit.pb.go b/gen/metacrypt/v2/transit.pb.go new file mode 100644 index 0000000..8bfec8a --- /dev/null +++ b/gen/metacrypt/v2/transit.pb.go @@ -0,0 +1,2197 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.20.3 +// source: proto/metacrypt/v2/transit.proto + +package metacryptv2 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CreateTransitKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // type is the key algorithm: aes256-gcm, chacha20-poly, ed25519, + // ecdsa-p256, ecdsa-p384, hmac-sha256, hmac-sha512. + // Defaults to aes256-gcm if empty. + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateTransitKeyRequest) Reset() { + *x = CreateTransitKeyRequest{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateTransitKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateTransitKeyRequest) ProtoMessage() {} + +func (x *CreateTransitKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateTransitKeyRequest.ProtoReflect.Descriptor instead. +func (*CreateTransitKeyRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{0} +} + +func (x *CreateTransitKeyRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *CreateTransitKeyRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateTransitKeyRequest) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +type CreateTransitKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + Version int32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateTransitKeyResponse) Reset() { + *x = CreateTransitKeyResponse{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateTransitKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateTransitKeyResponse) ProtoMessage() {} + +func (x *CreateTransitKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateTransitKeyResponse.ProtoReflect.Descriptor instead. +func (*CreateTransitKeyResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{1} +} + +func (x *CreateTransitKeyResponse) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateTransitKeyResponse) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *CreateTransitKeyResponse) GetVersion() int32 { + if x != nil { + return x.Version + } + return 0 +} + +type DeleteTransitKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteTransitKeyRequest) Reset() { + *x = DeleteTransitKeyRequest{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteTransitKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteTransitKeyRequest) ProtoMessage() {} + +func (x *DeleteTransitKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteTransitKeyRequest.ProtoReflect.Descriptor instead. +func (*DeleteTransitKeyRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{2} +} + +func (x *DeleteTransitKeyRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *DeleteTransitKeyRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type DeleteTransitKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteTransitKeyResponse) Reset() { + *x = DeleteTransitKeyResponse{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteTransitKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteTransitKeyResponse) ProtoMessage() {} + +func (x *DeleteTransitKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteTransitKeyResponse.ProtoReflect.Descriptor instead. +func (*DeleteTransitKeyResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{3} +} + +type GetTransitKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetTransitKeyRequest) Reset() { + *x = GetTransitKeyRequest{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetTransitKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTransitKeyRequest) ProtoMessage() {} + +func (x *GetTransitKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetTransitKeyRequest.ProtoReflect.Descriptor instead. +func (*GetTransitKeyRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{4} +} + +func (x *GetTransitKeyRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *GetTransitKeyRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type GetTransitKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + CurrentVersion int32 `protobuf:"varint,3,opt,name=current_version,json=currentVersion,proto3" json:"current_version,omitempty"` + MinDecryptionVersion int32 `protobuf:"varint,4,opt,name=min_decryption_version,json=minDecryptionVersion,proto3" json:"min_decryption_version,omitempty"` + AllowDeletion bool `protobuf:"varint,5,opt,name=allow_deletion,json=allowDeletion,proto3" json:"allow_deletion,omitempty"` + Versions []int32 `protobuf:"varint,6,rep,packed,name=versions,proto3" json:"versions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetTransitKeyResponse) Reset() { + *x = GetTransitKeyResponse{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetTransitKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTransitKeyResponse) ProtoMessage() {} + +func (x *GetTransitKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetTransitKeyResponse.ProtoReflect.Descriptor instead. +func (*GetTransitKeyResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{5} +} + +func (x *GetTransitKeyResponse) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *GetTransitKeyResponse) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *GetTransitKeyResponse) GetCurrentVersion() int32 { + if x != nil { + return x.CurrentVersion + } + return 0 +} + +func (x *GetTransitKeyResponse) GetMinDecryptionVersion() int32 { + if x != nil { + return x.MinDecryptionVersion + } + return 0 +} + +func (x *GetTransitKeyResponse) GetAllowDeletion() bool { + if x != nil { + return x.AllowDeletion + } + return false +} + +func (x *GetTransitKeyResponse) GetVersions() []int32 { + if x != nil { + return x.Versions + } + return nil +} + +type ListTransitKeysRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTransitKeysRequest) Reset() { + *x = ListTransitKeysRequest{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTransitKeysRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTransitKeysRequest) ProtoMessage() {} + +func (x *ListTransitKeysRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTransitKeysRequest.ProtoReflect.Descriptor instead. +func (*ListTransitKeysRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{6} +} + +func (x *ListTransitKeysRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +type ListTransitKeysResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Keys []string `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTransitKeysResponse) Reset() { + *x = ListTransitKeysResponse{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTransitKeysResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTransitKeysResponse) ProtoMessage() {} + +func (x *ListTransitKeysResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTransitKeysResponse.ProtoReflect.Descriptor instead. +func (*ListTransitKeysResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{7} +} + +func (x *ListTransitKeysResponse) GetKeys() []string { + if x != nil { + return x.Keys + } + return nil +} + +type RotateTransitKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RotateTransitKeyRequest) Reset() { + *x = RotateTransitKeyRequest{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RotateTransitKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RotateTransitKeyRequest) ProtoMessage() {} + +func (x *RotateTransitKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RotateTransitKeyRequest.ProtoReflect.Descriptor instead. +func (*RotateTransitKeyRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{8} +} + +func (x *RotateTransitKeyRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *RotateTransitKeyRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type RotateTransitKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Version int32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RotateTransitKeyResponse) Reset() { + *x = RotateTransitKeyResponse{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RotateTransitKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RotateTransitKeyResponse) ProtoMessage() {} + +func (x *RotateTransitKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RotateTransitKeyResponse.ProtoReflect.Descriptor instead. +func (*RotateTransitKeyResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{9} +} + +func (x *RotateTransitKeyResponse) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *RotateTransitKeyResponse) GetVersion() int32 { + if x != nil { + return x.Version + } + return 0 +} + +type UpdateTransitKeyConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + MinDecryptionVersion int32 `protobuf:"varint,3,opt,name=min_decryption_version,json=minDecryptionVersion,proto3" json:"min_decryption_version,omitempty"` + AllowDeletion bool `protobuf:"varint,4,opt,name=allow_deletion,json=allowDeletion,proto3" json:"allow_deletion,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateTransitKeyConfigRequest) Reset() { + *x = UpdateTransitKeyConfigRequest{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateTransitKeyConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateTransitKeyConfigRequest) ProtoMessage() {} + +func (x *UpdateTransitKeyConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateTransitKeyConfigRequest.ProtoReflect.Descriptor instead. +func (*UpdateTransitKeyConfigRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{10} +} + +func (x *UpdateTransitKeyConfigRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *UpdateTransitKeyConfigRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UpdateTransitKeyConfigRequest) GetMinDecryptionVersion() int32 { + if x != nil { + return x.MinDecryptionVersion + } + return 0 +} + +func (x *UpdateTransitKeyConfigRequest) GetAllowDeletion() bool { + if x != nil { + return x.AllowDeletion + } + return false +} + +type UpdateTransitKeyConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateTransitKeyConfigResponse) Reset() { + *x = UpdateTransitKeyConfigResponse{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateTransitKeyConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateTransitKeyConfigResponse) ProtoMessage() {} + +func (x *UpdateTransitKeyConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateTransitKeyConfigResponse.ProtoReflect.Descriptor instead. +func (*UpdateTransitKeyConfigResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{11} +} + +type TrimTransitKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TrimTransitKeyRequest) Reset() { + *x = TrimTransitKeyRequest{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TrimTransitKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TrimTransitKeyRequest) ProtoMessage() {} + +func (x *TrimTransitKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TrimTransitKeyRequest.ProtoReflect.Descriptor instead. +func (*TrimTransitKeyRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{12} +} + +func (x *TrimTransitKeyRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *TrimTransitKeyRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type TrimTransitKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Trimmed int32 `protobuf:"varint,1,opt,name=trimmed,proto3" json:"trimmed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TrimTransitKeyResponse) Reset() { + *x = TrimTransitKeyResponse{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TrimTransitKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TrimTransitKeyResponse) ProtoMessage() {} + +func (x *TrimTransitKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TrimTransitKeyResponse.ProtoReflect.Descriptor instead. +func (*TrimTransitKeyResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{13} +} + +func (x *TrimTransitKeyResponse) GetTrimmed() int32 { + if x != nil { + return x.Trimmed + } + return 0 +} + +type TransitEncryptRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + // plaintext is base64-encoded data to encrypt. + Plaintext string `protobuf:"bytes,3,opt,name=plaintext,proto3" json:"plaintext,omitempty"` + // context is optional base64-encoded additional authenticated data (AAD). + Context string `protobuf:"bytes,4,opt,name=context,proto3" json:"context,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitEncryptRequest) Reset() { + *x = TransitEncryptRequest{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitEncryptRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitEncryptRequest) ProtoMessage() {} + +func (x *TransitEncryptRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitEncryptRequest.ProtoReflect.Descriptor instead. +func (*TransitEncryptRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{14} +} + +func (x *TransitEncryptRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *TransitEncryptRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *TransitEncryptRequest) GetPlaintext() string { + if x != nil { + return x.Plaintext + } + return "" +} + +func (x *TransitEncryptRequest) GetContext() string { + if x != nil { + return x.Context + } + return "" +} + +type TransitEncryptResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // ciphertext in format "metacrypt:v{version}:{base64(nonce+ciphertext+tag)}" + Ciphertext string `protobuf:"bytes,1,opt,name=ciphertext,proto3" json:"ciphertext,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitEncryptResponse) Reset() { + *x = TransitEncryptResponse{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitEncryptResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitEncryptResponse) ProtoMessage() {} + +func (x *TransitEncryptResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitEncryptResponse.ProtoReflect.Descriptor instead. +func (*TransitEncryptResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{15} +} + +func (x *TransitEncryptResponse) GetCiphertext() string { + if x != nil { + return x.Ciphertext + } + return "" +} + +type TransitDecryptRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + Ciphertext string `protobuf:"bytes,3,opt,name=ciphertext,proto3" json:"ciphertext,omitempty"` + Context string `protobuf:"bytes,4,opt,name=context,proto3" json:"context,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitDecryptRequest) Reset() { + *x = TransitDecryptRequest{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitDecryptRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitDecryptRequest) ProtoMessage() {} + +func (x *TransitDecryptRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitDecryptRequest.ProtoReflect.Descriptor instead. +func (*TransitDecryptRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{16} +} + +func (x *TransitDecryptRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *TransitDecryptRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *TransitDecryptRequest) GetCiphertext() string { + if x != nil { + return x.Ciphertext + } + return "" +} + +func (x *TransitDecryptRequest) GetContext() string { + if x != nil { + return x.Context + } + return "" +} + +type TransitDecryptResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // plaintext is base64-encoded decrypted data. + Plaintext string `protobuf:"bytes,1,opt,name=plaintext,proto3" json:"plaintext,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitDecryptResponse) Reset() { + *x = TransitDecryptResponse{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitDecryptResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitDecryptResponse) ProtoMessage() {} + +func (x *TransitDecryptResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitDecryptResponse.ProtoReflect.Descriptor instead. +func (*TransitDecryptResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{17} +} + +func (x *TransitDecryptResponse) GetPlaintext() string { + if x != nil { + return x.Plaintext + } + return "" +} + +type TransitRewrapRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + Ciphertext string `protobuf:"bytes,3,opt,name=ciphertext,proto3" json:"ciphertext,omitempty"` + Context string `protobuf:"bytes,4,opt,name=context,proto3" json:"context,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitRewrapRequest) Reset() { + *x = TransitRewrapRequest{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitRewrapRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitRewrapRequest) ProtoMessage() {} + +func (x *TransitRewrapRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitRewrapRequest.ProtoReflect.Descriptor instead. +func (*TransitRewrapRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{18} +} + +func (x *TransitRewrapRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *TransitRewrapRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *TransitRewrapRequest) GetCiphertext() string { + if x != nil { + return x.Ciphertext + } + return "" +} + +func (x *TransitRewrapRequest) GetContext() string { + if x != nil { + return x.Context + } + return "" +} + +type TransitRewrapResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ciphertext string `protobuf:"bytes,1,opt,name=ciphertext,proto3" json:"ciphertext,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitRewrapResponse) Reset() { + *x = TransitRewrapResponse{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitRewrapResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitRewrapResponse) ProtoMessage() {} + +func (x *TransitRewrapResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitRewrapResponse.ProtoReflect.Descriptor instead. +func (*TransitRewrapResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{19} +} + +func (x *TransitRewrapResponse) GetCiphertext() string { + if x != nil { + return x.Ciphertext + } + return "" +} + +type TransitBatchItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + Plaintext string `protobuf:"bytes,1,opt,name=plaintext,proto3" json:"plaintext,omitempty"` + Ciphertext string `protobuf:"bytes,2,opt,name=ciphertext,proto3" json:"ciphertext,omitempty"` + Context string `protobuf:"bytes,3,opt,name=context,proto3" json:"context,omitempty"` + Reference string `protobuf:"bytes,4,opt,name=reference,proto3" json:"reference,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitBatchItem) Reset() { + *x = TransitBatchItem{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitBatchItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitBatchItem) ProtoMessage() {} + +func (x *TransitBatchItem) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitBatchItem.ProtoReflect.Descriptor instead. +func (*TransitBatchItem) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{20} +} + +func (x *TransitBatchItem) GetPlaintext() string { + if x != nil { + return x.Plaintext + } + return "" +} + +func (x *TransitBatchItem) GetCiphertext() string { + if x != nil { + return x.Ciphertext + } + return "" +} + +func (x *TransitBatchItem) GetContext() string { + if x != nil { + return x.Context + } + return "" +} + +func (x *TransitBatchItem) GetReference() string { + if x != nil { + return x.Reference + } + return "" +} + +type TransitBatchResultItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + Plaintext string `protobuf:"bytes,1,opt,name=plaintext,proto3" json:"plaintext,omitempty"` + Ciphertext string `protobuf:"bytes,2,opt,name=ciphertext,proto3" json:"ciphertext,omitempty"` + Reference string `protobuf:"bytes,3,opt,name=reference,proto3" json:"reference,omitempty"` + Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitBatchResultItem) Reset() { + *x = TransitBatchResultItem{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitBatchResultItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitBatchResultItem) ProtoMessage() {} + +func (x *TransitBatchResultItem) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitBatchResultItem.ProtoReflect.Descriptor instead. +func (*TransitBatchResultItem) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{21} +} + +func (x *TransitBatchResultItem) GetPlaintext() string { + if x != nil { + return x.Plaintext + } + return "" +} + +func (x *TransitBatchResultItem) GetCiphertext() string { + if x != nil { + return x.Ciphertext + } + return "" +} + +func (x *TransitBatchResultItem) GetReference() string { + if x != nil { + return x.Reference + } + return "" +} + +func (x *TransitBatchResultItem) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type TransitBatchEncryptRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + Items []*TransitBatchItem `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitBatchEncryptRequest) Reset() { + *x = TransitBatchEncryptRequest{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitBatchEncryptRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitBatchEncryptRequest) ProtoMessage() {} + +func (x *TransitBatchEncryptRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitBatchEncryptRequest.ProtoReflect.Descriptor instead. +func (*TransitBatchEncryptRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{22} +} + +func (x *TransitBatchEncryptRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *TransitBatchEncryptRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *TransitBatchEncryptRequest) GetItems() []*TransitBatchItem { + if x != nil { + return x.Items + } + return nil +} + +type TransitBatchDecryptRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + Items []*TransitBatchItem `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitBatchDecryptRequest) Reset() { + *x = TransitBatchDecryptRequest{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitBatchDecryptRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitBatchDecryptRequest) ProtoMessage() {} + +func (x *TransitBatchDecryptRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitBatchDecryptRequest.ProtoReflect.Descriptor instead. +func (*TransitBatchDecryptRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{23} +} + +func (x *TransitBatchDecryptRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *TransitBatchDecryptRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *TransitBatchDecryptRequest) GetItems() []*TransitBatchItem { + if x != nil { + return x.Items + } + return nil +} + +type TransitBatchRewrapRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + Items []*TransitBatchItem `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitBatchRewrapRequest) Reset() { + *x = TransitBatchRewrapRequest{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitBatchRewrapRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitBatchRewrapRequest) ProtoMessage() {} + +func (x *TransitBatchRewrapRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitBatchRewrapRequest.ProtoReflect.Descriptor instead. +func (*TransitBatchRewrapRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{24} +} + +func (x *TransitBatchRewrapRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *TransitBatchRewrapRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *TransitBatchRewrapRequest) GetItems() []*TransitBatchItem { + if x != nil { + return x.Items + } + return nil +} + +type TransitBatchResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Results []*TransitBatchResultItem `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitBatchResponse) Reset() { + *x = TransitBatchResponse{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitBatchResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitBatchResponse) ProtoMessage() {} + +func (x *TransitBatchResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitBatchResponse.ProtoReflect.Descriptor instead. +func (*TransitBatchResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{25} +} + +func (x *TransitBatchResponse) GetResults() []*TransitBatchResultItem { + if x != nil { + return x.Results + } + return nil +} + +type TransitSignRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + // input is base64-encoded data to sign. + Input string `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitSignRequest) Reset() { + *x = TransitSignRequest{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitSignRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitSignRequest) ProtoMessage() {} + +func (x *TransitSignRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitSignRequest.ProtoReflect.Descriptor instead. +func (*TransitSignRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{26} +} + +func (x *TransitSignRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *TransitSignRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *TransitSignRequest) GetInput() string { + if x != nil { + return x.Input + } + return "" +} + +type TransitSignResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // signature in format "metacrypt:v{version}:{base64(signature_bytes)}" + Signature string `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitSignResponse) Reset() { + *x = TransitSignResponse{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitSignResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitSignResponse) ProtoMessage() {} + +func (x *TransitSignResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitSignResponse.ProtoReflect.Descriptor instead. +func (*TransitSignResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{27} +} + +func (x *TransitSignResponse) GetSignature() string { + if x != nil { + return x.Signature + } + return "" +} + +type TransitVerifyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + Input string `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` + Signature string `protobuf:"bytes,4,opt,name=signature,proto3" json:"signature,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitVerifyRequest) Reset() { + *x = TransitVerifyRequest{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitVerifyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitVerifyRequest) ProtoMessage() {} + +func (x *TransitVerifyRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitVerifyRequest.ProtoReflect.Descriptor instead. +func (*TransitVerifyRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{28} +} + +func (x *TransitVerifyRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *TransitVerifyRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *TransitVerifyRequest) GetInput() string { + if x != nil { + return x.Input + } + return "" +} + +func (x *TransitVerifyRequest) GetSignature() string { + if x != nil { + return x.Signature + } + return "" +} + +type TransitVerifyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Valid bool `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitVerifyResponse) Reset() { + *x = TransitVerifyResponse{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitVerifyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitVerifyResponse) ProtoMessage() {} + +func (x *TransitVerifyResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitVerifyResponse.ProtoReflect.Descriptor instead. +func (*TransitVerifyResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{29} +} + +func (x *TransitVerifyResponse) GetValid() bool { + if x != nil { + return x.Valid + } + return false +} + +type TransitHmacRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + // input is base64-encoded data to HMAC. + Input string `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` + // hmac, if set, switches to verify mode. + Hmac string `protobuf:"bytes,4,opt,name=hmac,proto3" json:"hmac,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitHmacRequest) Reset() { + *x = TransitHmacRequest{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitHmacRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitHmacRequest) ProtoMessage() {} + +func (x *TransitHmacRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitHmacRequest.ProtoReflect.Descriptor instead. +func (*TransitHmacRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{30} +} + +func (x *TransitHmacRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *TransitHmacRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *TransitHmacRequest) GetInput() string { + if x != nil { + return x.Input + } + return "" +} + +func (x *TransitHmacRequest) GetHmac() string { + if x != nil { + return x.Hmac + } + return "" +} + +type TransitHmacResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // hmac is set in compute mode. + Hmac string `protobuf:"bytes,1,opt,name=hmac,proto3" json:"hmac,omitempty"` + // valid is set in verify mode. + Valid bool `protobuf:"varint,2,opt,name=valid,proto3" json:"valid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransitHmacResponse) Reset() { + *x = TransitHmacResponse{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransitHmacResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransitHmacResponse) ProtoMessage() {} + +func (x *TransitHmacResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransitHmacResponse.ProtoReflect.Descriptor instead. +func (*TransitHmacResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{31} +} + +func (x *TransitHmacResponse) GetHmac() string { + if x != nil { + return x.Hmac + } + return "" +} + +func (x *TransitHmacResponse) GetValid() bool { + if x != nil { + return x.Valid + } + return false +} + +type GetTransitPublicKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Version int32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetTransitPublicKeyRequest) Reset() { + *x = GetTransitPublicKeyRequest{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetTransitPublicKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTransitPublicKeyRequest) ProtoMessage() {} + +func (x *GetTransitPublicKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetTransitPublicKeyRequest.ProtoReflect.Descriptor instead. +func (*GetTransitPublicKeyRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{32} +} + +func (x *GetTransitPublicKeyRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *GetTransitPublicKeyRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *GetTransitPublicKeyRequest) GetVersion() int32 { + if x != nil { + return x.Version + } + return 0 +} + +type GetTransitPublicKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // public_key is base64-encoded PKIX DER public key. + PublicKey string `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + Version int32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetTransitPublicKeyResponse) Reset() { + *x = GetTransitPublicKeyResponse{} + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetTransitPublicKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTransitPublicKeyResponse) ProtoMessage() {} + +func (x *GetTransitPublicKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_transit_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetTransitPublicKeyResponse.ProtoReflect.Descriptor instead. +func (*GetTransitPublicKeyResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_transit_proto_rawDescGZIP(), []int{33} +} + +func (x *GetTransitPublicKeyResponse) GetPublicKey() string { + if x != nil { + return x.PublicKey + } + return "" +} + +func (x *GetTransitPublicKeyResponse) GetVersion() int32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *GetTransitPublicKeyResponse) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +var File_proto_metacrypt_v2_transit_proto protoreflect.FileDescriptor + +const file_proto_metacrypt_v2_transit_proto_rawDesc = "" + + "\n" + + " proto/metacrypt/v2/transit.proto\x12\fmetacrypt.v2\"W\n" + + "\x17CreateTransitKeyRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" + + "\x04type\x18\x03 \x01(\tR\x04type\"\\\n" + + "\x18CreateTransitKeyResponse\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + + "\x04type\x18\x02 \x01(\tR\x04type\x12\x18\n" + + "\aversion\x18\x03 \x01(\x05R\aversion\"C\n" + + "\x17DeleteTransitKeyRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\"\x1a\n" + + "\x18DeleteTransitKeyResponse\"@\n" + + "\x14GetTransitKeyRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\"\xe1\x01\n" + + "\x15GetTransitKeyResponse\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + + "\x04type\x18\x02 \x01(\tR\x04type\x12'\n" + + "\x0fcurrent_version\x18\x03 \x01(\x05R\x0ecurrentVersion\x124\n" + + "\x16min_decryption_version\x18\x04 \x01(\x05R\x14minDecryptionVersion\x12%\n" + + "\x0eallow_deletion\x18\x05 \x01(\bR\rallowDeletion\x12\x1a\n" + + "\bversions\x18\x06 \x03(\x05R\bversions\".\n" + + "\x16ListTransitKeysRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\"-\n" + + "\x17ListTransitKeysResponse\x12\x12\n" + + "\x04keys\x18\x01 \x03(\tR\x04keys\"C\n" + + "\x17RotateTransitKeyRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\"H\n" + + "\x18RotateTransitKeyResponse\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" + + "\aversion\x18\x02 \x01(\x05R\aversion\"\xa6\x01\n" + + "\x1dUpdateTransitKeyConfigRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x124\n" + + "\x16min_decryption_version\x18\x03 \x01(\x05R\x14minDecryptionVersion\x12%\n" + + "\x0eallow_deletion\x18\x04 \x01(\bR\rallowDeletion\" \n" + + "\x1eUpdateTransitKeyConfigResponse\"A\n" + + "\x15TrimTransitKeyRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\"2\n" + + "\x16TrimTransitKeyResponse\x12\x18\n" + + "\atrimmed\x18\x01 \x01(\x05R\atrimmed\"w\n" + + "\x15TransitEncryptRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x10\n" + + "\x03key\x18\x02 \x01(\tR\x03key\x12\x1c\n" + + "\tplaintext\x18\x03 \x01(\tR\tplaintext\x12\x18\n" + + "\acontext\x18\x04 \x01(\tR\acontext\"8\n" + + "\x16TransitEncryptResponse\x12\x1e\n" + + "\n" + + "ciphertext\x18\x01 \x01(\tR\n" + + "ciphertext\"y\n" + + "\x15TransitDecryptRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x10\n" + + "\x03key\x18\x02 \x01(\tR\x03key\x12\x1e\n" + + "\n" + + "ciphertext\x18\x03 \x01(\tR\n" + + "ciphertext\x12\x18\n" + + "\acontext\x18\x04 \x01(\tR\acontext\"6\n" + + "\x16TransitDecryptResponse\x12\x1c\n" + + "\tplaintext\x18\x01 \x01(\tR\tplaintext\"x\n" + + "\x14TransitRewrapRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x10\n" + + "\x03key\x18\x02 \x01(\tR\x03key\x12\x1e\n" + + "\n" + + "ciphertext\x18\x03 \x01(\tR\n" + + "ciphertext\x12\x18\n" + + "\acontext\x18\x04 \x01(\tR\acontext\"7\n" + + "\x15TransitRewrapResponse\x12\x1e\n" + + "\n" + + "ciphertext\x18\x01 \x01(\tR\n" + + "ciphertext\"\x88\x01\n" + + "\x10TransitBatchItem\x12\x1c\n" + + "\tplaintext\x18\x01 \x01(\tR\tplaintext\x12\x1e\n" + + "\n" + + "ciphertext\x18\x02 \x01(\tR\n" + + "ciphertext\x12\x18\n" + + "\acontext\x18\x03 \x01(\tR\acontext\x12\x1c\n" + + "\treference\x18\x04 \x01(\tR\treference\"\x8a\x01\n" + + "\x16TransitBatchResultItem\x12\x1c\n" + + "\tplaintext\x18\x01 \x01(\tR\tplaintext\x12\x1e\n" + + "\n" + + "ciphertext\x18\x02 \x01(\tR\n" + + "ciphertext\x12\x1c\n" + + "\treference\x18\x03 \x01(\tR\treference\x12\x14\n" + + "\x05error\x18\x04 \x01(\tR\x05error\"z\n" + + "\x1aTransitBatchEncryptRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x10\n" + + "\x03key\x18\x02 \x01(\tR\x03key\x124\n" + + "\x05items\x18\x03 \x03(\v2\x1e.metacrypt.v2.TransitBatchItemR\x05items\"z\n" + + "\x1aTransitBatchDecryptRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x10\n" + + "\x03key\x18\x02 \x01(\tR\x03key\x124\n" + + "\x05items\x18\x03 \x03(\v2\x1e.metacrypt.v2.TransitBatchItemR\x05items\"y\n" + + "\x19TransitBatchRewrapRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x10\n" + + "\x03key\x18\x02 \x01(\tR\x03key\x124\n" + + "\x05items\x18\x03 \x03(\v2\x1e.metacrypt.v2.TransitBatchItemR\x05items\"V\n" + + "\x14TransitBatchResponse\x12>\n" + + "\aresults\x18\x01 \x03(\v2$.metacrypt.v2.TransitBatchResultItemR\aresults\"R\n" + + "\x12TransitSignRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x10\n" + + "\x03key\x18\x02 \x01(\tR\x03key\x12\x14\n" + + "\x05input\x18\x03 \x01(\tR\x05input\"3\n" + + "\x13TransitSignResponse\x12\x1c\n" + + "\tsignature\x18\x01 \x01(\tR\tsignature\"r\n" + + "\x14TransitVerifyRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x10\n" + + "\x03key\x18\x02 \x01(\tR\x03key\x12\x14\n" + + "\x05input\x18\x03 \x01(\tR\x05input\x12\x1c\n" + + "\tsignature\x18\x04 \x01(\tR\tsignature\"-\n" + + "\x15TransitVerifyResponse\x12\x14\n" + + "\x05valid\x18\x01 \x01(\bR\x05valid\"f\n" + + "\x12TransitHmacRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x10\n" + + "\x03key\x18\x02 \x01(\tR\x03key\x12\x14\n" + + "\x05input\x18\x03 \x01(\tR\x05input\x12\x12\n" + + "\x04hmac\x18\x04 \x01(\tR\x04hmac\"?\n" + + "\x13TransitHmacResponse\x12\x12\n" + + "\x04hmac\x18\x01 \x01(\tR\x04hmac\x12\x14\n" + + "\x05valid\x18\x02 \x01(\bR\x05valid\"`\n" + + "\x1aGetTransitPublicKeyRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x18\n" + + "\aversion\x18\x03 \x01(\x05R\aversion\"j\n" + + "\x1bGetTransitPublicKeyResponse\x12\x1d\n" + + "\n" + + "public_key\x18\x01 \x01(\tR\tpublicKey\x12\x18\n" + + "\aversion\x18\x02 \x01(\x05R\aversion\x12\x12\n" + + "\x04type\x18\x03 \x01(\tR\x04type2\xfd\v\n" + + "\x0eTransitService\x12Z\n" + + "\tCreateKey\x12%.metacrypt.v2.CreateTransitKeyRequest\x1a&.metacrypt.v2.CreateTransitKeyResponse\x12Z\n" + + "\tDeleteKey\x12%.metacrypt.v2.DeleteTransitKeyRequest\x1a&.metacrypt.v2.DeleteTransitKeyResponse\x12Q\n" + + "\x06GetKey\x12\".metacrypt.v2.GetTransitKeyRequest\x1a#.metacrypt.v2.GetTransitKeyResponse\x12W\n" + + "\bListKeys\x12$.metacrypt.v2.ListTransitKeysRequest\x1a%.metacrypt.v2.ListTransitKeysResponse\x12Z\n" + + "\tRotateKey\x12%.metacrypt.v2.RotateTransitKeyRequest\x1a&.metacrypt.v2.RotateTransitKeyResponse\x12l\n" + + "\x0fUpdateKeyConfig\x12+.metacrypt.v2.UpdateTransitKeyConfigRequest\x1a,.metacrypt.v2.UpdateTransitKeyConfigResponse\x12T\n" + + "\aTrimKey\x12#.metacrypt.v2.TrimTransitKeyRequest\x1a$.metacrypt.v2.TrimTransitKeyResponse\x12T\n" + + "\aEncrypt\x12#.metacrypt.v2.TransitEncryptRequest\x1a$.metacrypt.v2.TransitEncryptResponse\x12T\n" + + "\aDecrypt\x12#.metacrypt.v2.TransitDecryptRequest\x1a$.metacrypt.v2.TransitDecryptResponse\x12Q\n" + + "\x06Rewrap\x12\".metacrypt.v2.TransitRewrapRequest\x1a#.metacrypt.v2.TransitRewrapResponse\x12\\\n" + + "\fBatchEncrypt\x12(.metacrypt.v2.TransitBatchEncryptRequest\x1a\".metacrypt.v2.TransitBatchResponse\x12\\\n" + + "\fBatchDecrypt\x12(.metacrypt.v2.TransitBatchDecryptRequest\x1a\".metacrypt.v2.TransitBatchResponse\x12Z\n" + + "\vBatchRewrap\x12'.metacrypt.v2.TransitBatchRewrapRequest\x1a\".metacrypt.v2.TransitBatchResponse\x12K\n" + + "\x04Sign\x12 .metacrypt.v2.TransitSignRequest\x1a!.metacrypt.v2.TransitSignResponse\x12Q\n" + + "\x06Verify\x12\".metacrypt.v2.TransitVerifyRequest\x1a#.metacrypt.v2.TransitVerifyResponse\x12K\n" + + "\x04Hmac\x12 .metacrypt.v2.TransitHmacRequest\x1a!.metacrypt.v2.TransitHmacResponse\x12c\n" + + "\fGetPublicKey\x12(.metacrypt.v2.GetTransitPublicKeyRequest\x1a).metacrypt.v2.GetTransitPublicKeyResponseB>Z metacrypt.v2.TransitBatchItem + 20, // 1: metacrypt.v2.TransitBatchDecryptRequest.items:type_name -> metacrypt.v2.TransitBatchItem + 20, // 2: metacrypt.v2.TransitBatchRewrapRequest.items:type_name -> metacrypt.v2.TransitBatchItem + 21, // 3: metacrypt.v2.TransitBatchResponse.results:type_name -> metacrypt.v2.TransitBatchResultItem + 0, // 4: metacrypt.v2.TransitService.CreateKey:input_type -> metacrypt.v2.CreateTransitKeyRequest + 2, // 5: metacrypt.v2.TransitService.DeleteKey:input_type -> metacrypt.v2.DeleteTransitKeyRequest + 4, // 6: metacrypt.v2.TransitService.GetKey:input_type -> metacrypt.v2.GetTransitKeyRequest + 6, // 7: metacrypt.v2.TransitService.ListKeys:input_type -> metacrypt.v2.ListTransitKeysRequest + 8, // 8: metacrypt.v2.TransitService.RotateKey:input_type -> metacrypt.v2.RotateTransitKeyRequest + 10, // 9: metacrypt.v2.TransitService.UpdateKeyConfig:input_type -> metacrypt.v2.UpdateTransitKeyConfigRequest + 12, // 10: metacrypt.v2.TransitService.TrimKey:input_type -> metacrypt.v2.TrimTransitKeyRequest + 14, // 11: metacrypt.v2.TransitService.Encrypt:input_type -> metacrypt.v2.TransitEncryptRequest + 16, // 12: metacrypt.v2.TransitService.Decrypt:input_type -> metacrypt.v2.TransitDecryptRequest + 18, // 13: metacrypt.v2.TransitService.Rewrap:input_type -> metacrypt.v2.TransitRewrapRequest + 22, // 14: metacrypt.v2.TransitService.BatchEncrypt:input_type -> metacrypt.v2.TransitBatchEncryptRequest + 23, // 15: metacrypt.v2.TransitService.BatchDecrypt:input_type -> metacrypt.v2.TransitBatchDecryptRequest + 24, // 16: metacrypt.v2.TransitService.BatchRewrap:input_type -> metacrypt.v2.TransitBatchRewrapRequest + 26, // 17: metacrypt.v2.TransitService.Sign:input_type -> metacrypt.v2.TransitSignRequest + 28, // 18: metacrypt.v2.TransitService.Verify:input_type -> metacrypt.v2.TransitVerifyRequest + 30, // 19: metacrypt.v2.TransitService.Hmac:input_type -> metacrypt.v2.TransitHmacRequest + 32, // 20: metacrypt.v2.TransitService.GetPublicKey:input_type -> metacrypt.v2.GetTransitPublicKeyRequest + 1, // 21: metacrypt.v2.TransitService.CreateKey:output_type -> metacrypt.v2.CreateTransitKeyResponse + 3, // 22: metacrypt.v2.TransitService.DeleteKey:output_type -> metacrypt.v2.DeleteTransitKeyResponse + 5, // 23: metacrypt.v2.TransitService.GetKey:output_type -> metacrypt.v2.GetTransitKeyResponse + 7, // 24: metacrypt.v2.TransitService.ListKeys:output_type -> metacrypt.v2.ListTransitKeysResponse + 9, // 25: metacrypt.v2.TransitService.RotateKey:output_type -> metacrypt.v2.RotateTransitKeyResponse + 11, // 26: metacrypt.v2.TransitService.UpdateKeyConfig:output_type -> metacrypt.v2.UpdateTransitKeyConfigResponse + 13, // 27: metacrypt.v2.TransitService.TrimKey:output_type -> metacrypt.v2.TrimTransitKeyResponse + 15, // 28: metacrypt.v2.TransitService.Encrypt:output_type -> metacrypt.v2.TransitEncryptResponse + 17, // 29: metacrypt.v2.TransitService.Decrypt:output_type -> metacrypt.v2.TransitDecryptResponse + 19, // 30: metacrypt.v2.TransitService.Rewrap:output_type -> metacrypt.v2.TransitRewrapResponse + 25, // 31: metacrypt.v2.TransitService.BatchEncrypt:output_type -> metacrypt.v2.TransitBatchResponse + 25, // 32: metacrypt.v2.TransitService.BatchDecrypt:output_type -> metacrypt.v2.TransitBatchResponse + 25, // 33: metacrypt.v2.TransitService.BatchRewrap:output_type -> metacrypt.v2.TransitBatchResponse + 27, // 34: metacrypt.v2.TransitService.Sign:output_type -> metacrypt.v2.TransitSignResponse + 29, // 35: metacrypt.v2.TransitService.Verify:output_type -> metacrypt.v2.TransitVerifyResponse + 31, // 36: metacrypt.v2.TransitService.Hmac:output_type -> metacrypt.v2.TransitHmacResponse + 33, // 37: metacrypt.v2.TransitService.GetPublicKey:output_type -> metacrypt.v2.GetTransitPublicKeyResponse + 21, // [21:38] is the sub-list for method output_type + 4, // [4:21] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_proto_metacrypt_v2_transit_proto_init() } +func file_proto_metacrypt_v2_transit_proto_init() { + if File_proto_metacrypt_v2_transit_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_metacrypt_v2_transit_proto_rawDesc), len(file_proto_metacrypt_v2_transit_proto_rawDesc)), + NumEnums: 0, + NumMessages: 34, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_metacrypt_v2_transit_proto_goTypes, + DependencyIndexes: file_proto_metacrypt_v2_transit_proto_depIdxs, + MessageInfos: file_proto_metacrypt_v2_transit_proto_msgTypes, + }.Build() + File_proto_metacrypt_v2_transit_proto = out.File + file_proto_metacrypt_v2_transit_proto_goTypes = nil + file_proto_metacrypt_v2_transit_proto_depIdxs = nil +} diff --git a/gen/metacrypt/v2/transit_grpc.pb.go b/gen/metacrypt/v2/transit_grpc.pb.go new file mode 100644 index 0000000..27beafe --- /dev/null +++ b/gen/metacrypt/v2/transit_grpc.pb.go @@ -0,0 +1,777 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc v3.20.3 +// source: proto/metacrypt/v2/transit.proto + +package metacryptv2 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + TransitService_CreateKey_FullMethodName = "/metacrypt.v2.TransitService/CreateKey" + TransitService_DeleteKey_FullMethodName = "/metacrypt.v2.TransitService/DeleteKey" + TransitService_GetKey_FullMethodName = "/metacrypt.v2.TransitService/GetKey" + TransitService_ListKeys_FullMethodName = "/metacrypt.v2.TransitService/ListKeys" + TransitService_RotateKey_FullMethodName = "/metacrypt.v2.TransitService/RotateKey" + TransitService_UpdateKeyConfig_FullMethodName = "/metacrypt.v2.TransitService/UpdateKeyConfig" + TransitService_TrimKey_FullMethodName = "/metacrypt.v2.TransitService/TrimKey" + TransitService_Encrypt_FullMethodName = "/metacrypt.v2.TransitService/Encrypt" + TransitService_Decrypt_FullMethodName = "/metacrypt.v2.TransitService/Decrypt" + TransitService_Rewrap_FullMethodName = "/metacrypt.v2.TransitService/Rewrap" + TransitService_BatchEncrypt_FullMethodName = "/metacrypt.v2.TransitService/BatchEncrypt" + TransitService_BatchDecrypt_FullMethodName = "/metacrypt.v2.TransitService/BatchDecrypt" + TransitService_BatchRewrap_FullMethodName = "/metacrypt.v2.TransitService/BatchRewrap" + TransitService_Sign_FullMethodName = "/metacrypt.v2.TransitService/Sign" + TransitService_Verify_FullMethodName = "/metacrypt.v2.TransitService/Verify" + TransitService_Hmac_FullMethodName = "/metacrypt.v2.TransitService/Hmac" + TransitService_GetPublicKey_FullMethodName = "/metacrypt.v2.TransitService/GetPublicKey" +) + +// TransitServiceClient is the client API for TransitService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// TransitService provides typed, authenticated access to transit engine +// operations: symmetric encryption, signing, HMAC, and versioned key +// management. All RPCs require the service to be unsealed. +type TransitServiceClient interface { + // CreateKey creates a new named encryption key. Admin only. + CreateKey(ctx context.Context, in *CreateTransitKeyRequest, opts ...grpc.CallOption) (*CreateTransitKeyResponse, error) + // DeleteKey permanently removes a named key. Admin only. + // Only succeeds if allow_deletion is true on the key config. + DeleteKey(ctx context.Context, in *DeleteTransitKeyRequest, opts ...grpc.CallOption) (*DeleteTransitKeyResponse, error) + // GetKey returns metadata for a named key (no raw material). Auth required. + GetKey(ctx context.Context, in *GetTransitKeyRequest, opts ...grpc.CallOption) (*GetTransitKeyResponse, error) + // ListKeys returns the names of all configured keys. Auth required. + ListKeys(ctx context.Context, in *ListTransitKeysRequest, opts ...grpc.CallOption) (*ListTransitKeysResponse, error) + // RotateKey creates a new version of the named key. Admin only. + RotateKey(ctx context.Context, in *RotateTransitKeyRequest, opts ...grpc.CallOption) (*RotateTransitKeyResponse, error) + // UpdateKeyConfig updates key configuration (e.g. min_decryption_version). + // Admin only. + UpdateKeyConfig(ctx context.Context, in *UpdateTransitKeyConfigRequest, opts ...grpc.CallOption) (*UpdateTransitKeyConfigResponse, error) + // TrimKey deletes versions below min_decryption_version. Admin only. + TrimKey(ctx context.Context, in *TrimTransitKeyRequest, opts ...grpc.CallOption) (*TrimTransitKeyResponse, error) + // Encrypt encrypts plaintext with the latest key version. Auth required. + Encrypt(ctx context.Context, in *TransitEncryptRequest, opts ...grpc.CallOption) (*TransitEncryptResponse, error) + // Decrypt decrypts ciphertext. Auth required. + Decrypt(ctx context.Context, in *TransitDecryptRequest, opts ...grpc.CallOption) (*TransitDecryptResponse, error) + // Rewrap re-encrypts ciphertext with the latest key version without + // exposing plaintext. Auth required. + Rewrap(ctx context.Context, in *TransitRewrapRequest, opts ...grpc.CallOption) (*TransitRewrapResponse, error) + // BatchEncrypt encrypts multiple items in a single request. Auth required. + BatchEncrypt(ctx context.Context, in *TransitBatchEncryptRequest, opts ...grpc.CallOption) (*TransitBatchResponse, error) + // BatchDecrypt decrypts multiple items in a single request. Auth required. + BatchDecrypt(ctx context.Context, in *TransitBatchDecryptRequest, opts ...grpc.CallOption) (*TransitBatchResponse, error) + // BatchRewrap re-encrypts multiple items in a single request. Auth required. + BatchRewrap(ctx context.Context, in *TransitBatchRewrapRequest, opts ...grpc.CallOption) (*TransitBatchResponse, error) + // Sign signs input data with an asymmetric key. Auth required. + Sign(ctx context.Context, in *TransitSignRequest, opts ...grpc.CallOption) (*TransitSignResponse, error) + // Verify verifies a signature against input data. Auth required. + Verify(ctx context.Context, in *TransitVerifyRequest, opts ...grpc.CallOption) (*TransitVerifyResponse, error) + // Hmac computes or verifies an HMAC. Auth required. + Hmac(ctx context.Context, in *TransitHmacRequest, opts ...grpc.CallOption) (*TransitHmacResponse, error) + // GetPublicKey returns the public key for an asymmetric key. Auth required. + GetPublicKey(ctx context.Context, in *GetTransitPublicKeyRequest, opts ...grpc.CallOption) (*GetTransitPublicKeyResponse, error) +} + +type transitServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewTransitServiceClient(cc grpc.ClientConnInterface) TransitServiceClient { + return &transitServiceClient{cc} +} + +func (c *transitServiceClient) CreateKey(ctx context.Context, in *CreateTransitKeyRequest, opts ...grpc.CallOption) (*CreateTransitKeyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateTransitKeyResponse) + err := c.cc.Invoke(ctx, TransitService_CreateKey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *transitServiceClient) DeleteKey(ctx context.Context, in *DeleteTransitKeyRequest, opts ...grpc.CallOption) (*DeleteTransitKeyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteTransitKeyResponse) + err := c.cc.Invoke(ctx, TransitService_DeleteKey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *transitServiceClient) GetKey(ctx context.Context, in *GetTransitKeyRequest, opts ...grpc.CallOption) (*GetTransitKeyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetTransitKeyResponse) + err := c.cc.Invoke(ctx, TransitService_GetKey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *transitServiceClient) ListKeys(ctx context.Context, in *ListTransitKeysRequest, opts ...grpc.CallOption) (*ListTransitKeysResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListTransitKeysResponse) + err := c.cc.Invoke(ctx, TransitService_ListKeys_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *transitServiceClient) RotateKey(ctx context.Context, in *RotateTransitKeyRequest, opts ...grpc.CallOption) (*RotateTransitKeyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RotateTransitKeyResponse) + err := c.cc.Invoke(ctx, TransitService_RotateKey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *transitServiceClient) UpdateKeyConfig(ctx context.Context, in *UpdateTransitKeyConfigRequest, opts ...grpc.CallOption) (*UpdateTransitKeyConfigResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdateTransitKeyConfigResponse) + err := c.cc.Invoke(ctx, TransitService_UpdateKeyConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *transitServiceClient) TrimKey(ctx context.Context, in *TrimTransitKeyRequest, opts ...grpc.CallOption) (*TrimTransitKeyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TrimTransitKeyResponse) + err := c.cc.Invoke(ctx, TransitService_TrimKey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *transitServiceClient) Encrypt(ctx context.Context, in *TransitEncryptRequest, opts ...grpc.CallOption) (*TransitEncryptResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TransitEncryptResponse) + err := c.cc.Invoke(ctx, TransitService_Encrypt_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *transitServiceClient) Decrypt(ctx context.Context, in *TransitDecryptRequest, opts ...grpc.CallOption) (*TransitDecryptResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TransitDecryptResponse) + err := c.cc.Invoke(ctx, TransitService_Decrypt_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *transitServiceClient) Rewrap(ctx context.Context, in *TransitRewrapRequest, opts ...grpc.CallOption) (*TransitRewrapResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TransitRewrapResponse) + err := c.cc.Invoke(ctx, TransitService_Rewrap_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *transitServiceClient) BatchEncrypt(ctx context.Context, in *TransitBatchEncryptRequest, opts ...grpc.CallOption) (*TransitBatchResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TransitBatchResponse) + err := c.cc.Invoke(ctx, TransitService_BatchEncrypt_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *transitServiceClient) BatchDecrypt(ctx context.Context, in *TransitBatchDecryptRequest, opts ...grpc.CallOption) (*TransitBatchResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TransitBatchResponse) + err := c.cc.Invoke(ctx, TransitService_BatchDecrypt_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *transitServiceClient) BatchRewrap(ctx context.Context, in *TransitBatchRewrapRequest, opts ...grpc.CallOption) (*TransitBatchResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TransitBatchResponse) + err := c.cc.Invoke(ctx, TransitService_BatchRewrap_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *transitServiceClient) Sign(ctx context.Context, in *TransitSignRequest, opts ...grpc.CallOption) (*TransitSignResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TransitSignResponse) + err := c.cc.Invoke(ctx, TransitService_Sign_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *transitServiceClient) Verify(ctx context.Context, in *TransitVerifyRequest, opts ...grpc.CallOption) (*TransitVerifyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TransitVerifyResponse) + err := c.cc.Invoke(ctx, TransitService_Verify_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *transitServiceClient) Hmac(ctx context.Context, in *TransitHmacRequest, opts ...grpc.CallOption) (*TransitHmacResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TransitHmacResponse) + err := c.cc.Invoke(ctx, TransitService_Hmac_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *transitServiceClient) GetPublicKey(ctx context.Context, in *GetTransitPublicKeyRequest, opts ...grpc.CallOption) (*GetTransitPublicKeyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetTransitPublicKeyResponse) + err := c.cc.Invoke(ctx, TransitService_GetPublicKey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// TransitServiceServer is the server API for TransitService service. +// All implementations must embed UnimplementedTransitServiceServer +// for forward compatibility. +// +// TransitService provides typed, authenticated access to transit engine +// operations: symmetric encryption, signing, HMAC, and versioned key +// management. All RPCs require the service to be unsealed. +type TransitServiceServer interface { + // CreateKey creates a new named encryption key. Admin only. + CreateKey(context.Context, *CreateTransitKeyRequest) (*CreateTransitKeyResponse, error) + // DeleteKey permanently removes a named key. Admin only. + // Only succeeds if allow_deletion is true on the key config. + DeleteKey(context.Context, *DeleteTransitKeyRequest) (*DeleteTransitKeyResponse, error) + // GetKey returns metadata for a named key (no raw material). Auth required. + GetKey(context.Context, *GetTransitKeyRequest) (*GetTransitKeyResponse, error) + // ListKeys returns the names of all configured keys. Auth required. + ListKeys(context.Context, *ListTransitKeysRequest) (*ListTransitKeysResponse, error) + // RotateKey creates a new version of the named key. Admin only. + RotateKey(context.Context, *RotateTransitKeyRequest) (*RotateTransitKeyResponse, error) + // UpdateKeyConfig updates key configuration (e.g. min_decryption_version). + // Admin only. + UpdateKeyConfig(context.Context, *UpdateTransitKeyConfigRequest) (*UpdateTransitKeyConfigResponse, error) + // TrimKey deletes versions below min_decryption_version. Admin only. + TrimKey(context.Context, *TrimTransitKeyRequest) (*TrimTransitKeyResponse, error) + // Encrypt encrypts plaintext with the latest key version. Auth required. + Encrypt(context.Context, *TransitEncryptRequest) (*TransitEncryptResponse, error) + // Decrypt decrypts ciphertext. Auth required. + Decrypt(context.Context, *TransitDecryptRequest) (*TransitDecryptResponse, error) + // Rewrap re-encrypts ciphertext with the latest key version without + // exposing plaintext. Auth required. + Rewrap(context.Context, *TransitRewrapRequest) (*TransitRewrapResponse, error) + // BatchEncrypt encrypts multiple items in a single request. Auth required. + BatchEncrypt(context.Context, *TransitBatchEncryptRequest) (*TransitBatchResponse, error) + // BatchDecrypt decrypts multiple items in a single request. Auth required. + BatchDecrypt(context.Context, *TransitBatchDecryptRequest) (*TransitBatchResponse, error) + // BatchRewrap re-encrypts multiple items in a single request. Auth required. + BatchRewrap(context.Context, *TransitBatchRewrapRequest) (*TransitBatchResponse, error) + // Sign signs input data with an asymmetric key. Auth required. + Sign(context.Context, *TransitSignRequest) (*TransitSignResponse, error) + // Verify verifies a signature against input data. Auth required. + Verify(context.Context, *TransitVerifyRequest) (*TransitVerifyResponse, error) + // Hmac computes or verifies an HMAC. Auth required. + Hmac(context.Context, *TransitHmacRequest) (*TransitHmacResponse, error) + // GetPublicKey returns the public key for an asymmetric key. Auth required. + GetPublicKey(context.Context, *GetTransitPublicKeyRequest) (*GetTransitPublicKeyResponse, error) + mustEmbedUnimplementedTransitServiceServer() +} + +// UnimplementedTransitServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedTransitServiceServer struct{} + +func (UnimplementedTransitServiceServer) CreateKey(context.Context, *CreateTransitKeyRequest) (*CreateTransitKeyResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateKey not implemented") +} +func (UnimplementedTransitServiceServer) DeleteKey(context.Context, *DeleteTransitKeyRequest) (*DeleteTransitKeyResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteKey not implemented") +} +func (UnimplementedTransitServiceServer) GetKey(context.Context, *GetTransitKeyRequest) (*GetTransitKeyResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetKey not implemented") +} +func (UnimplementedTransitServiceServer) ListKeys(context.Context, *ListTransitKeysRequest) (*ListTransitKeysResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListKeys not implemented") +} +func (UnimplementedTransitServiceServer) RotateKey(context.Context, *RotateTransitKeyRequest) (*RotateTransitKeyResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RotateKey not implemented") +} +func (UnimplementedTransitServiceServer) UpdateKeyConfig(context.Context, *UpdateTransitKeyConfigRequest) (*UpdateTransitKeyConfigResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateKeyConfig not implemented") +} +func (UnimplementedTransitServiceServer) TrimKey(context.Context, *TrimTransitKeyRequest) (*TrimTransitKeyResponse, error) { + return nil, status.Error(codes.Unimplemented, "method TrimKey not implemented") +} +func (UnimplementedTransitServiceServer) Encrypt(context.Context, *TransitEncryptRequest) (*TransitEncryptResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Encrypt not implemented") +} +func (UnimplementedTransitServiceServer) Decrypt(context.Context, *TransitDecryptRequest) (*TransitDecryptResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Decrypt not implemented") +} +func (UnimplementedTransitServiceServer) Rewrap(context.Context, *TransitRewrapRequest) (*TransitRewrapResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Rewrap not implemented") +} +func (UnimplementedTransitServiceServer) BatchEncrypt(context.Context, *TransitBatchEncryptRequest) (*TransitBatchResponse, error) { + return nil, status.Error(codes.Unimplemented, "method BatchEncrypt not implemented") +} +func (UnimplementedTransitServiceServer) BatchDecrypt(context.Context, *TransitBatchDecryptRequest) (*TransitBatchResponse, error) { + return nil, status.Error(codes.Unimplemented, "method BatchDecrypt not implemented") +} +func (UnimplementedTransitServiceServer) BatchRewrap(context.Context, *TransitBatchRewrapRequest) (*TransitBatchResponse, error) { + return nil, status.Error(codes.Unimplemented, "method BatchRewrap not implemented") +} +func (UnimplementedTransitServiceServer) Sign(context.Context, *TransitSignRequest) (*TransitSignResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Sign not implemented") +} +func (UnimplementedTransitServiceServer) Verify(context.Context, *TransitVerifyRequest) (*TransitVerifyResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Verify not implemented") +} +func (UnimplementedTransitServiceServer) Hmac(context.Context, *TransitHmacRequest) (*TransitHmacResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Hmac not implemented") +} +func (UnimplementedTransitServiceServer) GetPublicKey(context.Context, *GetTransitPublicKeyRequest) (*GetTransitPublicKeyResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetPublicKey not implemented") +} +func (UnimplementedTransitServiceServer) mustEmbedUnimplementedTransitServiceServer() {} +func (UnimplementedTransitServiceServer) testEmbeddedByValue() {} + +// UnsafeTransitServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to TransitServiceServer will +// result in compilation errors. +type UnsafeTransitServiceServer interface { + mustEmbedUnimplementedTransitServiceServer() +} + +func RegisterTransitServiceServer(s grpc.ServiceRegistrar, srv TransitServiceServer) { + // If the following call panics, it indicates UnimplementedTransitServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&TransitService_ServiceDesc, srv) +} + +func _TransitService_CreateKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateTransitKeyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TransitServiceServer).CreateKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TransitService_CreateKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TransitServiceServer).CreateKey(ctx, req.(*CreateTransitKeyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TransitService_DeleteKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteTransitKeyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TransitServiceServer).DeleteKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TransitService_DeleteKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TransitServiceServer).DeleteKey(ctx, req.(*DeleteTransitKeyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TransitService_GetKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetTransitKeyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TransitServiceServer).GetKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TransitService_GetKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TransitServiceServer).GetKey(ctx, req.(*GetTransitKeyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TransitService_ListKeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListTransitKeysRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TransitServiceServer).ListKeys(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TransitService_ListKeys_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TransitServiceServer).ListKeys(ctx, req.(*ListTransitKeysRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TransitService_RotateKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RotateTransitKeyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TransitServiceServer).RotateKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TransitService_RotateKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TransitServiceServer).RotateKey(ctx, req.(*RotateTransitKeyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TransitService_UpdateKeyConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateTransitKeyConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TransitServiceServer).UpdateKeyConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TransitService_UpdateKeyConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TransitServiceServer).UpdateKeyConfig(ctx, req.(*UpdateTransitKeyConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TransitService_TrimKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TrimTransitKeyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TransitServiceServer).TrimKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TransitService_TrimKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TransitServiceServer).TrimKey(ctx, req.(*TrimTransitKeyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TransitService_Encrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TransitEncryptRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TransitServiceServer).Encrypt(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TransitService_Encrypt_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TransitServiceServer).Encrypt(ctx, req.(*TransitEncryptRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TransitService_Decrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TransitDecryptRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TransitServiceServer).Decrypt(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TransitService_Decrypt_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TransitServiceServer).Decrypt(ctx, req.(*TransitDecryptRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TransitService_Rewrap_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TransitRewrapRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TransitServiceServer).Rewrap(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TransitService_Rewrap_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TransitServiceServer).Rewrap(ctx, req.(*TransitRewrapRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TransitService_BatchEncrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TransitBatchEncryptRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TransitServiceServer).BatchEncrypt(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TransitService_BatchEncrypt_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TransitServiceServer).BatchEncrypt(ctx, req.(*TransitBatchEncryptRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TransitService_BatchDecrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TransitBatchDecryptRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TransitServiceServer).BatchDecrypt(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TransitService_BatchDecrypt_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TransitServiceServer).BatchDecrypt(ctx, req.(*TransitBatchDecryptRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TransitService_BatchRewrap_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TransitBatchRewrapRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TransitServiceServer).BatchRewrap(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TransitService_BatchRewrap_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TransitServiceServer).BatchRewrap(ctx, req.(*TransitBatchRewrapRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TransitService_Sign_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TransitSignRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TransitServiceServer).Sign(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TransitService_Sign_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TransitServiceServer).Sign(ctx, req.(*TransitSignRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TransitService_Verify_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TransitVerifyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TransitServiceServer).Verify(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TransitService_Verify_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TransitServiceServer).Verify(ctx, req.(*TransitVerifyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TransitService_Hmac_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TransitHmacRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TransitServiceServer).Hmac(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TransitService_Hmac_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TransitServiceServer).Hmac(ctx, req.(*TransitHmacRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TransitService_GetPublicKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetTransitPublicKeyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TransitServiceServer).GetPublicKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TransitService_GetPublicKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TransitServiceServer).GetPublicKey(ctx, req.(*GetTransitPublicKeyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// TransitService_ServiceDesc is the grpc.ServiceDesc for TransitService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var TransitService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "metacrypt.v2.TransitService", + HandlerType: (*TransitServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateKey", + Handler: _TransitService_CreateKey_Handler, + }, + { + MethodName: "DeleteKey", + Handler: _TransitService_DeleteKey_Handler, + }, + { + MethodName: "GetKey", + Handler: _TransitService_GetKey_Handler, + }, + { + MethodName: "ListKeys", + Handler: _TransitService_ListKeys_Handler, + }, + { + MethodName: "RotateKey", + Handler: _TransitService_RotateKey_Handler, + }, + { + MethodName: "UpdateKeyConfig", + Handler: _TransitService_UpdateKeyConfig_Handler, + }, + { + MethodName: "TrimKey", + Handler: _TransitService_TrimKey_Handler, + }, + { + MethodName: "Encrypt", + Handler: _TransitService_Encrypt_Handler, + }, + { + MethodName: "Decrypt", + Handler: _TransitService_Decrypt_Handler, + }, + { + MethodName: "Rewrap", + Handler: _TransitService_Rewrap_Handler, + }, + { + MethodName: "BatchEncrypt", + Handler: _TransitService_BatchEncrypt_Handler, + }, + { + MethodName: "BatchDecrypt", + Handler: _TransitService_BatchDecrypt_Handler, + }, + { + MethodName: "BatchRewrap", + Handler: _TransitService_BatchRewrap_Handler, + }, + { + MethodName: "Sign", + Handler: _TransitService_Sign_Handler, + }, + { + MethodName: "Verify", + Handler: _TransitService_Verify_Handler, + }, + { + MethodName: "Hmac", + Handler: _TransitService_Hmac_Handler, + }, + { + MethodName: "GetPublicKey", + Handler: _TransitService_GetPublicKey_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/metacrypt/v2/transit.proto", +} diff --git a/internal/engine/helpers.go b/internal/engine/helpers.go new file mode 100644 index 0000000..0b7e53d --- /dev/null +++ b/internal/engine/helpers.go @@ -0,0 +1,28 @@ +package engine + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" +) + +// ZeroizeKey overwrites an asymmetric private key's sensitive material. +func ZeroizeKey(key crypto.PrivateKey) { + if key == nil { + return + } + switch k := key.(type) { + case *ecdsa.PrivateKey: + k.D.SetInt64(0) + case *rsa.PrivateKey: + k.D.SetInt64(0) + for _, p := range k.Primes { + p.SetInt64(0) + } + case ed25519.PrivateKey: + for i := range k { + k[i] = 0 + } + } +} diff --git a/internal/engine/transit/transit.go b/internal/engine/transit/transit.go new file mode 100644 index 0000000..415d6ca --- /dev/null +++ b/internal/engine/transit/transit.go @@ -0,0 +1,1602 @@ +// Package transit implements the transit encryption engine for symmetric +// encryption, signing, and HMAC operations with versioned key management. +package transit + +import ( + "context" + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "crypto/sha512" + "crypto/x509" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "hash" + "sort" + "strconv" + "strings" + "sync" + + "golang.org/x/crypto/chacha20poly1305" + + "git.wntrmute.dev/kyle/metacrypt/internal/barrier" + mcrypto "git.wntrmute.dev/kyle/metacrypt/internal/crypto" + "git.wntrmute.dev/kyle/metacrypt/internal/engine" +) + +const maxBatchSize = 500 + +var ( + ErrSealed = errors.New("transit: engine is sealed") + ErrKeyNotFound = errors.New("transit: key not found") + ErrKeyExists = errors.New("transit: key already exists") + ErrForbidden = errors.New("transit: forbidden") + ErrUnauthorized = errors.New("transit: authentication required") + ErrDeletionDenied = errors.New("transit: deletion not allowed") + ErrInvalidKeyType = errors.New("transit: invalid key type") + ErrUnsupportedOp = errors.New("transit: unsupported operation for key type") + ErrDecryptVersion = errors.New("transit: ciphertext version below minimum decryption version") + ErrInvalidFormat = errors.New("transit: invalid ciphertext format") + ErrBatchTooLarge = errors.New("transit: batch size exceeds maximum") + ErrInvalidMinVer = errors.New("transit: min_decryption_version can only increase and cannot exceed current version") +) + +// keyVersion holds a single version of key material. +type keyVersion struct { + version int + key []byte // symmetric key material + privKey crypto.PrivateKey // asymmetric (nil for symmetric) + pubKey crypto.PublicKey // asymmetric (nil for symmetric) +} + +// keyState holds in-memory state for a loaded key. +type keyState struct { + config *KeyConfig + versions map[int]*keyVersion +} + +// TransitEngine implements the transit encryption engine. +type TransitEngine struct { + barrier barrier.Barrier + config *TransitConfig + keys map[string]*keyState + mountPath string + mu sync.RWMutex +} + +// NewTransitEngine creates a new transit engine instance. +func NewTransitEngine() engine.Engine { + return &TransitEngine{ + keys: make(map[string]*keyState), + } +} + +func (e *TransitEngine) Type() engine.EngineType { + return engine.EngineTypeTransit +} + +// Initialize sets up the transit engine for first use. +func (e *TransitEngine) Initialize(ctx context.Context, b barrier.Barrier, mountPath string, config map[string]interface{}) error { + e.mu.Lock() + defer e.mu.Unlock() + + e.barrier = b + e.mountPath = mountPath + + cfg := &TransitConfig{} + if config != nil { + if v, ok := config["max_key_versions"]; ok { + switch val := v.(type) { + case float64: + cfg.MaxKeyVersions = int(val) + case int: + cfg.MaxKeyVersions = val + } + } + } + e.config = cfg + + configData, err := json.Marshal(cfg) + if err != nil { + return fmt.Errorf("transit: marshal config: %w", err) + } + if err := b.Put(ctx, mountPath+"config.json", configData); err != nil { + return fmt.Errorf("transit: store config: %w", err) + } + + e.keys = make(map[string]*keyState) + return nil +} + +// Unseal loads the transit state from the barrier into memory. +func (e *TransitEngine) Unseal(ctx context.Context, b barrier.Barrier, mountPath string) error { + e.mu.Lock() + defer e.mu.Unlock() + + e.barrier = b + e.mountPath = mountPath + + // Load config. + configData, err := b.Get(ctx, mountPath+"config.json") + if err != nil { + return fmt.Errorf("transit: load config: %w", err) + } + var cfg TransitConfig + if err := json.Unmarshal(configData, &cfg); err != nil { + return fmt.Errorf("transit: parse config: %w", err) + } + e.config = &cfg + e.keys = make(map[string]*keyState) + + // Load all keys. + keyPaths, err := b.List(ctx, mountPath+"keys/") + if err != nil { + return nil // no keys yet + } + + // Collect unique key names from paths like "mykey/config.json", "mykey/v1.key". + keyNames := make(map[string]bool) + for _, p := range keyPaths { + parts := strings.SplitN(p, "/", 2) + if len(parts) > 0 && parts[0] != "" { + keyNames[parts[0]] = true + } + } + + for name := range keyNames { + ks, err := e.loadKey(ctx, b, mountPath, name) + if err != nil { + return fmt.Errorf("transit: load key %q: %w", name, err) + } + e.keys[name] = ks + } + + return nil +} + +func (e *TransitEngine) loadKey(ctx context.Context, b barrier.Barrier, mountPath, name string) (*keyState, error) { + prefix := mountPath + "keys/" + name + "/" + + configData, err := b.Get(ctx, prefix+"config.json") + if err != nil { + return nil, fmt.Errorf("load config: %w", err) + } + var cfg KeyConfig + if err := json.Unmarshal(configData, &cfg); err != nil { + return nil, fmt.Errorf("parse config: %w", err) + } + + ks := &keyState{ + config: &cfg, + versions: make(map[int]*keyVersion), + } + + // Load all versions. + for v := 1; v <= cfg.CurrentVersion; v++ { + kv, err := e.loadKeyVersion(ctx, b, prefix, &cfg, v) + if err != nil { + // Version may have been trimmed; skip. + continue + } + ks.versions[v] = kv + } + + return ks, nil +} + +func (e *TransitEngine) loadKeyVersion(ctx context.Context, b barrier.Barrier, prefix string, cfg *KeyConfig, version int) (*keyVersion, error) { + path := fmt.Sprintf("%sv%d.key", prefix, version) + data, err := b.Get(ctx, path) + if err != nil { + return nil, err + } + + kv := &keyVersion{version: version} + + switch cfg.Type { + case "aes256-gcm", "chacha20-poly", "hmac-sha256", "hmac-sha512": + kv.key = data + case "ed25519": + privKey := ed25519.PrivateKey(data) + kv.key = data + kv.privKey = privKey + kv.pubKey = privKey.Public() + case "ecdsa-p256", "ecdsa-p384": + privKey, err := x509.ParsePKCS8PrivateKey(data) + if err != nil { + return nil, fmt.Errorf("parse PKCS8 key: %w", err) + } + ecKey, ok := privKey.(*ecdsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("expected ECDSA key, got %T", privKey) + } + kv.privKey = ecKey + kv.pubKey = &ecKey.PublicKey + default: + return nil, fmt.Errorf("unknown key type: %s", cfg.Type) + } + + return kv, nil +} + +// Seal zeroizes all in-memory key material. +func (e *TransitEngine) Seal() error { + e.mu.Lock() + defer e.mu.Unlock() + + for name, ks := range e.keys { + for _, kv := range ks.versions { + if kv.key != nil { + mcrypto.Zeroize(kv.key) + } + zeroizeKey(kv.privKey) + } + delete(e.keys, name) + } + e.keys = nil + e.config = nil + + return nil +} + +// HandleRequest dispatches transit operations. +func (e *TransitEngine) HandleRequest(ctx context.Context, req *engine.Request) (*engine.Response, error) { + switch req.Operation { + case "create-key": + return e.handleCreateKey(ctx, req) + case "delete-key": + return e.handleDeleteKey(ctx, req) + case "get-key": + return e.handleGetKey(ctx, req) + case "list-keys": + return e.handleListKeys(ctx, req) + case "rotate-key": + return e.handleRotateKey(ctx, req) + case "update-key-config": + return e.handleUpdateKeyConfig(ctx, req) + case "trim-key": + return e.handleTrimKey(ctx, req) + case "encrypt": + return e.handleEncrypt(ctx, req) + case "decrypt": + return e.handleDecrypt(ctx, req) + case "rewrap": + return e.handleRewrap(ctx, req) + case "batch-encrypt": + return e.handleBatchEncrypt(ctx, req) + case "batch-decrypt": + return e.handleBatchDecrypt(ctx, req) + case "batch-rewrap": + return e.handleBatchRewrap(ctx, req) + case "sign": + return e.handleSign(ctx, req) + case "verify": + return e.handleVerify(ctx, req) + case "hmac": + return e.handleHMAC(ctx, req) + case "get-public-key": + return e.handleGetPublicKey(ctx, req) + default: + return nil, fmt.Errorf("transit: unknown operation: %s", req.Operation) + } +} + +// --- Authorization helpers --- + +func (e *TransitEngine) requireAdmin(req *engine.Request) error { + if req.CallerInfo == nil { + return ErrUnauthorized + } + if !req.CallerInfo.IsAdmin { + return ErrForbidden + } + return nil +} + +func (e *TransitEngine) requireUser(req *engine.Request) error { + if req.CallerInfo == nil { + return ErrUnauthorized + } + if !req.CallerInfo.IsUser() { + return ErrForbidden + } + return nil +} + +func (e *TransitEngine) requireUserWithPolicy(req *engine.Request, keyName string) error { + if req.CallerInfo == nil { + return ErrUnauthorized + } + if req.CallerInfo.IsAdmin { + return nil + } + if !req.CallerInfo.IsUser() { + return ErrForbidden + } + + // Check policy for the specific key. + if req.CheckPolicy != nil { + resource := fmt.Sprintf("transit/%s/key/%s", e.mountName(), keyName) + action := operationToAction(req.Operation) + effect, matched := req.CheckPolicy(resource, action) + if matched { + if effect == "allow" { + return nil + } + return ErrForbidden + } + } + + // Default: users can access transit operations without explicit policy. + return nil +} + +func operationToAction(op string) string { + switch op { + case "get-key", "list-keys", "get-public-key": + return "read" + case "decrypt", "rewrap", "batch-decrypt", "batch-rewrap": + return "decrypt" + default: + return "write" + } +} + +// mountName extracts the user-facing mount name from the mount path. +func (e *TransitEngine) mountName() string { + parts := strings.Split(strings.TrimSuffix(e.mountPath, "/"), "/") + if len(parts) >= 3 { + return parts[2] + } + return "" +} + +func (e *TransitEngine) sealed() bool { + return e.config == nil +} + +// --- Key Management Operations --- + +func (e *TransitEngine) handleCreateKey(ctx context.Context, req *engine.Request) (*engine.Response, error) { + if err := e.requireAdmin(req); err != nil { + return nil, err + } + + e.mu.Lock() + defer e.mu.Unlock() + + if e.sealed() { + return nil, ErrSealed + } + + name, _ := req.Data["name"].(string) + keyType, _ := req.Data["type"].(string) + if name == "" { + return nil, fmt.Errorf("transit: name is required") + } + if keyType == "" { + keyType = "aes256-gcm" + } + + if !isValidKeyType(keyType) { + return nil, ErrInvalidKeyType + } + + if _, exists := e.keys[name]; exists { + return nil, ErrKeyExists + } + + // Generate key version 1. + kv, err := generateKeyVersion(keyType, 1) + if err != nil { + return nil, fmt.Errorf("transit: generate key: %w", err) + } + + cfg := &KeyConfig{ + Name: name, + Type: keyType, + CurrentVersion: 1, + MinDecryptionVersion: 1, + AllowDeletion: false, + } + + // Store config and key. + prefix := e.mountPath + "keys/" + name + "/" + if err := e.storeKeyConfig(ctx, prefix, cfg); err != nil { + return nil, err + } + if err := e.storeKeyVersion(ctx, prefix, cfg, kv); err != nil { + return nil, err + } + + e.keys[name] = &keyState{ + config: cfg, + versions: map[int]*keyVersion{1: kv}, + } + + return &engine.Response{ + Data: map[string]interface{}{ + "name": name, + "type": keyType, + "version": 1, + }, + }, nil +} + +func (e *TransitEngine) handleDeleteKey(ctx context.Context, req *engine.Request) (*engine.Response, error) { + if err := e.requireAdmin(req); err != nil { + return nil, err + } + + e.mu.Lock() + defer e.mu.Unlock() + + if e.sealed() { + return nil, ErrSealed + } + + name, _ := req.Data["name"].(string) + if name == "" { + return nil, fmt.Errorf("transit: name is required") + } + + ks, ok := e.keys[name] + if !ok { + return nil, ErrKeyNotFound + } + + if !ks.config.AllowDeletion { + return nil, ErrDeletionDenied + } + + // Delete all versions and config from barrier. + prefix := e.mountPath + "keys/" + name + "/" + for v := range ks.versions { + path := fmt.Sprintf("%sv%d.key", prefix, v) + _ = e.barrier.Delete(ctx, path) + } + _ = e.barrier.Delete(ctx, prefix+"config.json") + + // Zeroize in-memory material. + for _, kv := range ks.versions { + if kv.key != nil { + mcrypto.Zeroize(kv.key) + } + zeroizeKey(kv.privKey) + } + delete(e.keys, name) + + return &engine.Response{ + Data: map[string]interface{}{"ok": true}, + }, nil +} + +func (e *TransitEngine) handleGetKey(_ context.Context, req *engine.Request) (*engine.Response, error) { + if err := e.requireUser(req); err != nil { + return nil, err + } + + e.mu.RLock() + defer e.mu.RUnlock() + + if e.sealed() { + return nil, ErrSealed + } + + name, _ := req.Data["name"].(string) + if name == "" { + return nil, fmt.Errorf("transit: name is required") + } + + ks, ok := e.keys[name] + if !ok { + return nil, ErrKeyNotFound + } + + versions := make([]int, 0, len(ks.versions)) + for v := range ks.versions { + versions = append(versions, v) + } + sort.Ints(versions) + + return &engine.Response{ + Data: map[string]interface{}{ + "name": ks.config.Name, + "type": ks.config.Type, + "current_version": ks.config.CurrentVersion, + "min_decryption_version": ks.config.MinDecryptionVersion, + "allow_deletion": ks.config.AllowDeletion, + "versions": versions, + }, + }, nil +} + +func (e *TransitEngine) handleListKeys(_ context.Context, req *engine.Request) (*engine.Response, error) { + if err := e.requireUser(req); err != nil { + return nil, err + } + + e.mu.RLock() + defer e.mu.RUnlock() + + if e.sealed() { + return nil, ErrSealed + } + + keys := make([]string, 0, len(e.keys)) + for name := range e.keys { + keys = append(keys, name) + } + sort.Strings(keys) + + return &engine.Response{ + Data: map[string]interface{}{"keys": keys}, + }, nil +} + +func (e *TransitEngine) handleRotateKey(ctx context.Context, req *engine.Request) (*engine.Response, error) { + if err := e.requireAdmin(req); err != nil { + return nil, err + } + + e.mu.Lock() + defer e.mu.Unlock() + + if e.sealed() { + return nil, ErrSealed + } + + name, _ := req.Data["name"].(string) + if name == "" { + return nil, fmt.Errorf("transit: name is required") + } + + ks, ok := e.keys[name] + if !ok { + return nil, ErrKeyNotFound + } + + newVersion := ks.config.CurrentVersion + 1 + kv, err := generateKeyVersion(ks.config.Type, newVersion) + if err != nil { + return nil, fmt.Errorf("transit: generate key version: %w", err) + } + + ks.config.CurrentVersion = newVersion + prefix := e.mountPath + "keys/" + name + "/" + if err := e.storeKeyConfig(ctx, prefix, ks.config); err != nil { + return nil, err + } + if err := e.storeKeyVersion(ctx, prefix, ks.config, kv); err != nil { + return nil, err + } + + ks.versions[newVersion] = kv + + // Prune old versions if max_key_versions is set. + if e.config.MaxKeyVersions > 0 && len(ks.versions) > e.config.MaxKeyVersions { + e.pruneVersions(ctx, ks, prefix) + } + + return &engine.Response{ + Data: map[string]interface{}{ + "name": name, + "version": newVersion, + }, + }, nil +} + +func (e *TransitEngine) pruneVersions(ctx context.Context, ks *keyState, prefix string) { + versions := make([]int, 0, len(ks.versions)) + for v := range ks.versions { + versions = append(versions, v) + } + sort.Ints(versions) + + for len(versions) > e.config.MaxKeyVersions { + v := versions[0] + if v >= ks.config.MinDecryptionVersion { + break + } + path := fmt.Sprintf("%sv%d.key", prefix, v) + _ = e.barrier.Delete(ctx, path) + if kv, ok := ks.versions[v]; ok { + if kv.key != nil { + mcrypto.Zeroize(kv.key) + } + zeroizeKey(kv.privKey) + } + delete(ks.versions, v) + versions = versions[1:] + } +} + +func (e *TransitEngine) handleUpdateKeyConfig(ctx context.Context, req *engine.Request) (*engine.Response, error) { + if err := e.requireAdmin(req); err != nil { + return nil, err + } + + e.mu.Lock() + defer e.mu.Unlock() + + if e.sealed() { + return nil, ErrSealed + } + + name, _ := req.Data["name"].(string) + if name == "" { + return nil, fmt.Errorf("transit: name is required") + } + + ks, ok := e.keys[name] + if !ok { + return nil, ErrKeyNotFound + } + + if v, ok := req.Data["min_decryption_version"]; ok { + newMin := toInt(v) + if newMin < ks.config.MinDecryptionVersion || newMin > ks.config.CurrentVersion { + return nil, ErrInvalidMinVer + } + ks.config.MinDecryptionVersion = newMin + } + + if v, ok := req.Data["allow_deletion"]; ok { + if b, ok := v.(bool); ok { + ks.config.AllowDeletion = b + } + } + + prefix := e.mountPath + "keys/" + name + "/" + if err := e.storeKeyConfig(ctx, prefix, ks.config); err != nil { + return nil, err + } + + return &engine.Response{ + Data: map[string]interface{}{"ok": true}, + }, nil +} + +func (e *TransitEngine) handleTrimKey(ctx context.Context, req *engine.Request) (*engine.Response, error) { + if err := e.requireAdmin(req); err != nil { + return nil, err + } + + e.mu.Lock() + defer e.mu.Unlock() + + if e.sealed() { + return nil, ErrSealed + } + + name, _ := req.Data["name"].(string) + if name == "" { + return nil, fmt.Errorf("transit: name is required") + } + + ks, ok := e.keys[name] + if !ok { + return nil, ErrKeyNotFound + } + + prefix := e.mountPath + "keys/" + name + "/" + trimmed := 0 + for v, kv := range ks.versions { + if v < ks.config.MinDecryptionVersion { + path := fmt.Sprintf("%sv%d.key", prefix, v) + _ = e.barrier.Delete(ctx, path) + if kv.key != nil { + mcrypto.Zeroize(kv.key) + } + zeroizeKey(kv.privKey) + delete(ks.versions, v) + trimmed++ + } + } + + return &engine.Response{ + Data: map[string]interface{}{ + "trimmed": trimmed, + }, + }, nil +} + +// --- Crypto Operations --- + +func (e *TransitEngine) handleEncrypt(_ context.Context, req *engine.Request) (*engine.Response, error) { + keyName, _ := req.Data["key"].(string) + if keyName == "" { + keyName, _ = req.Data["name"].(string) + } + if err := e.requireUserWithPolicy(req, keyName); err != nil { + return nil, err + } + + e.mu.RLock() + defer e.mu.RUnlock() + + if e.sealed() { + return nil, ErrSealed + } + + if keyName == "" { + return nil, fmt.Errorf("transit: key name is required") + } + + plaintextB64, _ := req.Data["plaintext"].(string) + contextB64, _ := req.Data["context"].(string) + + ciphertext, err := e.encryptWithKey(keyName, plaintextB64, contextB64) + if err != nil { + return nil, err + } + + return &engine.Response{ + Data: map[string]interface{}{"ciphertext": ciphertext}, + }, nil +} + +func (e *TransitEngine) encryptWithKey(keyName, plaintextB64, contextB64 string) (string, error) { + ks, ok := e.keys[keyName] + if !ok { + return "", ErrKeyNotFound + } + + if !isSymmetric(ks.config.Type) { + return "", ErrUnsupportedOp + } + + plaintext, err := base64.StdEncoding.DecodeString(plaintextB64) + if err != nil { + return "", fmt.Errorf("transit: invalid base64 plaintext: %w", err) + } + + var aad []byte + if contextB64 != "" { + aad, err = base64.StdEncoding.DecodeString(contextB64) + if err != nil { + return "", fmt.Errorf("transit: invalid base64 context: %w", err) + } + } + + currentVersion := ks.config.CurrentVersion + kv, ok := ks.versions[currentVersion] + if !ok { + return "", fmt.Errorf("transit: current key version %d not found", currentVersion) + } + + encrypted, err := encryptData(ks.config.Type, kv.key, plaintext, aad) + if err != nil { + return "", err + } + + return formatCiphertext(currentVersion, encrypted), nil +} + +func (e *TransitEngine) handleDecrypt(_ context.Context, req *engine.Request) (*engine.Response, error) { + keyName, _ := req.Data["key"].(string) + if keyName == "" { + keyName, _ = req.Data["name"].(string) + } + if err := e.requireUserWithPolicy(req, keyName); err != nil { + return nil, err + } + + e.mu.RLock() + defer e.mu.RUnlock() + + if e.sealed() { + return nil, ErrSealed + } + + if keyName == "" { + return nil, fmt.Errorf("transit: key name is required") + } + + ciphertextStr, _ := req.Data["ciphertext"].(string) + contextB64, _ := req.Data["context"].(string) + + plaintext, err := e.decryptWithKey(keyName, ciphertextStr, contextB64) + if err != nil { + return nil, err + } + + return &engine.Response{ + Data: map[string]interface{}{"plaintext": base64.StdEncoding.EncodeToString(plaintext)}, + }, nil +} + +func (e *TransitEngine) decryptWithKey(keyName, ciphertextStr, contextB64 string) ([]byte, error) { + ks, ok := e.keys[keyName] + if !ok { + return nil, ErrKeyNotFound + } + + if !isSymmetric(ks.config.Type) { + return nil, ErrUnsupportedOp + } + + version, data, err := parseCiphertext(ciphertextStr) + if err != nil { + return nil, err + } + + if version < ks.config.MinDecryptionVersion { + return nil, ErrDecryptVersion + } + + kv, ok := ks.versions[version] + if !ok { + return nil, fmt.Errorf("transit: key version %d not found", version) + } + + var aad []byte + if contextB64 != "" { + aad, err = base64.StdEncoding.DecodeString(contextB64) + if err != nil { + return nil, fmt.Errorf("transit: invalid base64 context: %w", err) + } + } + + return decryptData(ks.config.Type, kv.key, data, aad) +} + +func (e *TransitEngine) handleRewrap(_ context.Context, req *engine.Request) (*engine.Response, error) { + keyName, _ := req.Data["key"].(string) + if keyName == "" { + keyName, _ = req.Data["name"].(string) + } + if err := e.requireUserWithPolicy(req, keyName); err != nil { + return nil, err + } + + e.mu.RLock() + defer e.mu.RUnlock() + + if e.sealed() { + return nil, ErrSealed + } + + if keyName == "" { + return nil, fmt.Errorf("transit: key name is required") + } + + ciphertextStr, _ := req.Data["ciphertext"].(string) + contextB64, _ := req.Data["context"].(string) + + // Decrypt with old version. + plaintext, err := e.decryptWithKey(keyName, ciphertextStr, contextB64) + if err != nil { + return nil, err + } + + // Re-encrypt with latest version (reuse the decoded plaintext as raw bytes). + plaintextB64 := base64.StdEncoding.EncodeToString(plaintext) + newCiphertext, err := e.encryptWithKey(keyName, plaintextB64, contextB64) + if err != nil { + return nil, err + } + + return &engine.Response{ + Data: map[string]interface{}{"ciphertext": newCiphertext}, + }, nil +} + +// --- Batch Operations --- + +type batchItem struct { + Plaintext string `json:"plaintext"` + Ciphertext string `json:"ciphertext"` + Context string `json:"context"` + Reference string `json:"reference"` +} + +type batchResult struct { + Plaintext string `json:"plaintext,omitempty"` + Ciphertext string `json:"ciphertext,omitempty"` + Reference string `json:"reference,omitempty"` + Error string `json:"error,omitempty"` +} + +func (e *TransitEngine) handleBatchEncrypt(_ context.Context, req *engine.Request) (*engine.Response, error) { + keyName, _ := req.Data["key"].(string) + if keyName == "" { + keyName, _ = req.Data["name"].(string) + } + if err := e.requireUserWithPolicy(req, keyName); err != nil { + return nil, err + } + + e.mu.RLock() + defer e.mu.RUnlock() + + if e.sealed() { + return nil, ErrSealed + } + + items, err := extractBatchItems(req.Data["items"]) + if err != nil { + return nil, err + } + if len(items) > maxBatchSize { + return nil, ErrBatchTooLarge + } + + results := make([]interface{}, len(items)) + for i, item := range items { + ct, err := e.encryptWithKey(keyName, item.Plaintext, item.Context) + r := batchResult{Reference: item.Reference} + if err != nil { + r.Error = err.Error() + } else { + r.Ciphertext = ct + } + results[i] = r + } + + return &engine.Response{ + Data: map[string]interface{}{"results": results}, + }, nil +} + +func (e *TransitEngine) handleBatchDecrypt(_ context.Context, req *engine.Request) (*engine.Response, error) { + keyName, _ := req.Data["key"].(string) + if keyName == "" { + keyName, _ = req.Data["name"].(string) + } + if err := e.requireUserWithPolicy(req, keyName); err != nil { + return nil, err + } + + e.mu.RLock() + defer e.mu.RUnlock() + + if e.sealed() { + return nil, ErrSealed + } + + items, err := extractBatchItems(req.Data["items"]) + if err != nil { + return nil, err + } + if len(items) > maxBatchSize { + return nil, ErrBatchTooLarge + } + + results := make([]interface{}, len(items)) + for i, item := range items { + pt, err := e.decryptWithKey(keyName, item.Ciphertext, item.Context) + r := batchResult{Reference: item.Reference} + if err != nil { + r.Error = err.Error() + } else { + r.Plaintext = base64.StdEncoding.EncodeToString(pt) + } + results[i] = r + } + + return &engine.Response{ + Data: map[string]interface{}{"results": results}, + }, nil +} + +func (e *TransitEngine) handleBatchRewrap(_ context.Context, req *engine.Request) (*engine.Response, error) { + keyName, _ := req.Data["key"].(string) + if keyName == "" { + keyName, _ = req.Data["name"].(string) + } + if err := e.requireUserWithPolicy(req, keyName); err != nil { + return nil, err + } + + e.mu.RLock() + defer e.mu.RUnlock() + + if e.sealed() { + return nil, ErrSealed + } + + items, err := extractBatchItems(req.Data["items"]) + if err != nil { + return nil, err + } + if len(items) > maxBatchSize { + return nil, ErrBatchTooLarge + } + + results := make([]interface{}, len(items)) + for i, item := range items { + r := batchResult{Reference: item.Reference} + // Decrypt with old version. + pt, err := e.decryptWithKey(keyName, item.Ciphertext, item.Context) + if err != nil { + r.Error = err.Error() + results[i] = r + continue + } + // Re-encrypt with latest version. + ptB64 := base64.StdEncoding.EncodeToString(pt) + ct, err := e.encryptWithKey(keyName, ptB64, item.Context) + if err != nil { + r.Error = err.Error() + } else { + r.Ciphertext = ct + } + results[i] = r + } + + return &engine.Response{ + Data: map[string]interface{}{"results": results}, + }, nil +} + +// --- Sign/Verify Operations --- + +func (e *TransitEngine) handleSign(_ context.Context, req *engine.Request) (*engine.Response, error) { + keyName, _ := req.Data["key"].(string) + if keyName == "" { + keyName, _ = req.Data["name"].(string) + } + if err := e.requireUserWithPolicy(req, keyName); err != nil { + return nil, err + } + + e.mu.RLock() + defer e.mu.RUnlock() + + if e.sealed() { + return nil, ErrSealed + } + + if keyName == "" { + return nil, fmt.Errorf("transit: key name is required") + } + + ks, ok := e.keys[keyName] + if !ok { + return nil, ErrKeyNotFound + } + + if !isAsymmetric(ks.config.Type) { + return nil, ErrUnsupportedOp + } + + inputB64, _ := req.Data["input"].(string) + input, err := base64.StdEncoding.DecodeString(inputB64) + if err != nil { + return nil, fmt.Errorf("transit: invalid base64 input: %w", err) + } + + currentVersion := ks.config.CurrentVersion + kv, ok := ks.versions[currentVersion] + if !ok { + return nil, fmt.Errorf("transit: current key version %d not found", currentVersion) + } + + var sig []byte + switch ks.config.Type { + case "ed25519": + edKey, ok := kv.privKey.(ed25519.PrivateKey) + if !ok { + return nil, fmt.Errorf("transit: expected ed25519 key") + } + sig = ed25519.Sign(edKey, input) + case "ecdsa-p256": + ecKey, ok := kv.privKey.(*ecdsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("transit: expected ECDSA key") + } + h := sha256.Sum256(input) + sig, err = ecdsa.SignASN1(rand.Reader, ecKey, h[:]) + if err != nil { + return nil, fmt.Errorf("transit: sign: %w", err) + } + case "ecdsa-p384": + ecKey, ok := kv.privKey.(*ecdsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("transit: expected ECDSA key") + } + h := sha512.Sum384(input) + sig, err = ecdsa.SignASN1(rand.Reader, ecKey, h[:]) + if err != nil { + return nil, fmt.Errorf("transit: sign: %w", err) + } + default: + return nil, ErrUnsupportedOp + } + + return &engine.Response{ + Data: map[string]interface{}{ + "signature": formatSignature(currentVersion, sig), + }, + }, nil +} + +func (e *TransitEngine) handleVerify(_ context.Context, req *engine.Request) (*engine.Response, error) { + keyName, _ := req.Data["key"].(string) + if keyName == "" { + keyName, _ = req.Data["name"].(string) + } + if err := e.requireUserWithPolicy(req, keyName); err != nil { + return nil, err + } + + e.mu.RLock() + defer e.mu.RUnlock() + + if e.sealed() { + return nil, ErrSealed + } + + if keyName == "" { + return nil, fmt.Errorf("transit: key name is required") + } + + ks, ok := e.keys[keyName] + if !ok { + return nil, ErrKeyNotFound + } + + if !isAsymmetric(ks.config.Type) { + return nil, ErrUnsupportedOp + } + + inputB64, _ := req.Data["input"].(string) + input, err := base64.StdEncoding.DecodeString(inputB64) + if err != nil { + return nil, fmt.Errorf("transit: invalid base64 input: %w", err) + } + + signatureStr, _ := req.Data["signature"].(string) + version, sigBytes, err := parseVersionedData(signatureStr) + if err != nil { + return nil, fmt.Errorf("transit: invalid signature format: %w", err) + } + + kv, ok := ks.versions[version] + if !ok { + return nil, fmt.Errorf("transit: key version %d not found", version) + } + + valid := false + switch ks.config.Type { + case "ed25519": + edPub, ok := kv.pubKey.(ed25519.PublicKey) + if !ok { + return nil, fmt.Errorf("transit: expected ed25519 public key") + } + valid = ed25519.Verify(edPub, input, sigBytes) + case "ecdsa-p256": + ecPub, ok := kv.pubKey.(*ecdsa.PublicKey) + if !ok { + return nil, fmt.Errorf("transit: expected ECDSA public key") + } + h := sha256.Sum256(input) + valid = ecdsa.VerifyASN1(ecPub, h[:], sigBytes) + case "ecdsa-p384": + ecPub, ok := kv.pubKey.(*ecdsa.PublicKey) + if !ok { + return nil, fmt.Errorf("transit: expected ECDSA public key") + } + h := sha512.Sum384(input) + valid = ecdsa.VerifyASN1(ecPub, h[:], sigBytes) + default: + return nil, ErrUnsupportedOp + } + + return &engine.Response{ + Data: map[string]interface{}{"valid": valid}, + }, nil +} + +// --- HMAC Operation --- + +func (e *TransitEngine) handleHMAC(_ context.Context, req *engine.Request) (*engine.Response, error) { + keyName, _ := req.Data["key"].(string) + if keyName == "" { + keyName, _ = req.Data["name"].(string) + } + if err := e.requireUserWithPolicy(req, keyName); err != nil { + return nil, err + } + + e.mu.RLock() + defer e.mu.RUnlock() + + if e.sealed() { + return nil, ErrSealed + } + + if keyName == "" { + return nil, fmt.Errorf("transit: key name is required") + } + + ks, ok := e.keys[keyName] + if !ok { + return nil, ErrKeyNotFound + } + + if !isHMAC(ks.config.Type) { + return nil, ErrUnsupportedOp + } + + inputB64, _ := req.Data["input"].(string) + input, err := base64.StdEncoding.DecodeString(inputB64) + if err != nil { + return nil, fmt.Errorf("transit: invalid base64 input: %w", err) + } + + // Verify mode: if hmac is provided, verify it. + if hmacStr, ok := req.Data["hmac"].(string); ok && hmacStr != "" { + version, macBytes, err := parseVersionedData(hmacStr) + if err != nil { + return nil, fmt.Errorf("transit: invalid hmac format: %w", err) + } + + kv, ok := ks.versions[version] + if !ok { + return nil, fmt.Errorf("transit: key version %d not found", version) + } + + expected := computeHMAC(ks.config.Type, kv.key, input) + valid := hmac.Equal(macBytes, expected) + + return &engine.Response{ + Data: map[string]interface{}{"valid": valid}, + }, nil + } + + // Compute mode. + currentVersion := ks.config.CurrentVersion + kv, ok := ks.versions[currentVersion] + if !ok { + return nil, fmt.Errorf("transit: current key version %d not found", currentVersion) + } + + mac := computeHMAC(ks.config.Type, kv.key, input) + return &engine.Response{ + Data: map[string]interface{}{ + "hmac": formatHMAC(currentVersion, mac), + }, + }, nil +} + +// --- Get Public Key --- + +func (e *TransitEngine) handleGetPublicKey(_ context.Context, req *engine.Request) (*engine.Response, error) { + if err := e.requireUser(req); err != nil { + return nil, err + } + + e.mu.RLock() + defer e.mu.RUnlock() + + if e.sealed() { + return nil, ErrSealed + } + + keyName, _ := req.Data["name"].(string) + if keyName == "" { + return nil, fmt.Errorf("transit: name is required") + } + + ks, ok := e.keys[keyName] + if !ok { + return nil, ErrKeyNotFound + } + + if !isAsymmetric(ks.config.Type) { + return nil, ErrUnsupportedOp + } + + version := ks.config.CurrentVersion + if v, ok := req.Data["version"]; ok { + version = toInt(v) + } + + kv, ok := ks.versions[version] + if !ok { + return nil, fmt.Errorf("transit: key version %d not found", version) + } + + pubKeyBytes, err := x509.MarshalPKIXPublicKey(kv.pubKey) + if err != nil { + return nil, fmt.Errorf("transit: marshal public key: %w", err) + } + + return &engine.Response{ + Data: map[string]interface{}{ + "public_key": base64.StdEncoding.EncodeToString(pubKeyBytes), + "version": version, + "type": ks.config.Type, + }, + }, nil +} + +// --- Storage helpers --- + +func (e *TransitEngine) storeKeyConfig(ctx context.Context, prefix string, cfg *KeyConfig) error { + data, err := json.Marshal(cfg) + if err != nil { + return fmt.Errorf("transit: marshal key config: %w", err) + } + return e.barrier.Put(ctx, prefix+"config.json", data) +} + +func (e *TransitEngine) storeKeyVersion(ctx context.Context, prefix string, cfg *KeyConfig, kv *keyVersion) error { + path := fmt.Sprintf("%sv%d.key", prefix, kv.version) + + var data []byte + switch cfg.Type { + case "aes256-gcm", "chacha20-poly", "hmac-sha256", "hmac-sha512": + data = kv.key + case "ed25519": + data = kv.key // raw 64-byte private key + case "ecdsa-p256", "ecdsa-p384": + var err error + data, err = x509.MarshalPKCS8PrivateKey(kv.privKey) + if err != nil { + return fmt.Errorf("transit: marshal PKCS8 key: %w", err) + } + default: + return fmt.Errorf("transit: unknown key type: %s", cfg.Type) + } + + return e.barrier.Put(ctx, path, data) +} + +// --- Key generation --- + +func generateKeyVersion(keyType string, version int) (*keyVersion, error) { + kv := &keyVersion{version: version} + + switch keyType { + case "aes256-gcm": + key := make([]byte, 32) + if _, err := rand.Read(key); err != nil { + return nil, err + } + kv.key = key + case "chacha20-poly": + key := make([]byte, 32) + if _, err := rand.Read(key); err != nil { + return nil, err + } + kv.key = key + case "ed25519": + _, privKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + kv.key = []byte(privKey) + kv.privKey = privKey + kv.pubKey = privKey.Public() + case "ecdsa-p256": + privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + kv.privKey = privKey + kv.pubKey = &privKey.PublicKey + case "ecdsa-p384": + privKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return nil, err + } + kv.privKey = privKey + kv.pubKey = &privKey.PublicKey + case "hmac-sha256": + key := make([]byte, 32) + if _, err := rand.Read(key); err != nil { + return nil, err + } + kv.key = key + case "hmac-sha512": + key := make([]byte, 64) + if _, err := rand.Read(key); err != nil { + return nil, err + } + kv.key = key + default: + return nil, fmt.Errorf("unknown key type: %s", keyType) + } + + return kv, nil +} + +// --- Encryption/Decryption helpers --- + +func encryptData(keyType string, key, plaintext, aad []byte) ([]byte, error) { + aead, err := newAEAD(keyType, key) + if err != nil { + return nil, err + } + + nonce := make([]byte, aead.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return nil, fmt.Errorf("transit: generate nonce: %w", err) + } + + ciphertext := aead.Seal(nil, nonce, plaintext, aad) + + // Format: nonce + ciphertext (includes tag) + result := make([]byte, len(nonce)+len(ciphertext)) + copy(result, nonce) + copy(result[len(nonce):], ciphertext) + return result, nil +} + +func decryptData(keyType string, key, data, aad []byte) ([]byte, error) { + aead, err := newAEAD(keyType, key) + if err != nil { + return nil, err + } + + nonceSize := aead.NonceSize() + if len(data) < nonceSize { + return nil, ErrInvalidFormat + } + + nonce := data[:nonceSize] + ciphertext := data[nonceSize:] + + plaintext, err := aead.Open(nil, nonce, ciphertext, aad) + if err != nil { + return nil, fmt.Errorf("transit: decryption failed: %w", err) + } + return plaintext, nil +} + +func newAEAD(keyType string, key []byte) (cipher.AEAD, error) { + switch keyType { + case "aes256-gcm": + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("transit: new AES cipher: %w", err) + } + return cipher.NewGCM(block) + case "chacha20-poly": + return chacha20poly1305.NewX(key) + default: + return nil, fmt.Errorf("transit: unsupported encryption type: %s", keyType) + } +} + +// --- HMAC helpers --- + +func computeHMAC(keyType string, key, input []byte) []byte { + var h func() hash.Hash + switch keyType { + case "hmac-sha256": + h = sha256.New + case "hmac-sha512": + h = sha512.New + default: + return nil + } + mac := hmac.New(h, key) + mac.Write(input) + return mac.Sum(nil) +} + +// --- Format helpers --- + +func formatCiphertext(version int, data []byte) string { + return fmt.Sprintf("metacrypt:v%d:%s", version, base64.StdEncoding.EncodeToString(data)) +} + +func formatSignature(version int, sig []byte) string { + return fmt.Sprintf("metacrypt:v%d:%s", version, base64.StdEncoding.EncodeToString(sig)) +} + +func formatHMAC(version int, mac []byte) string { + return fmt.Sprintf("metacrypt:v%d:%s", version, base64.StdEncoding.EncodeToString(mac)) +} + +func parseCiphertext(s string) (int, []byte, error) { + return parseVersionedData(s) +} + +func parseVersionedData(s string) (int, []byte, error) { + parts := strings.SplitN(s, ":", 3) + if len(parts) != 3 || parts[0] != "metacrypt" { + return 0, nil, ErrInvalidFormat + } + + if !strings.HasPrefix(parts[1], "v") { + return 0, nil, ErrInvalidFormat + } + + version, err := strconv.Atoi(parts[1][1:]) + if err != nil { + return 0, nil, ErrInvalidFormat + } + + data, err := base64.StdEncoding.DecodeString(parts[2]) + if err != nil { + return 0, nil, fmt.Errorf("transit: invalid base64: %w", err) + } + + return version, data, nil +} + +// --- Type helpers --- + +func isValidKeyType(t string) bool { + switch t { + case "aes256-gcm", "chacha20-poly", "ed25519", "ecdsa-p256", "ecdsa-p384", "hmac-sha256", "hmac-sha512": + return true + } + return false +} + +func isSymmetric(t string) bool { + return t == "aes256-gcm" || t == "chacha20-poly" +} + +func isAsymmetric(t string) bool { + return t == "ed25519" || t == "ecdsa-p256" || t == "ecdsa-p384" +} + +func isHMAC(t string) bool { + return t == "hmac-sha256" || t == "hmac-sha512" +} + +// --- Utility --- + +func toInt(v interface{}) int { + switch val := v.(type) { + case float64: + return int(val) + case int: + return val + case int64: + return int(val) + case json.Number: + n, _ := val.Int64() + return int(n) + } + return 0 +} + +func extractBatchItems(v interface{}) ([]batchItem, error) { + raw, ok := v.([]interface{}) + if !ok { + return nil, fmt.Errorf("transit: items must be an array") + } + + items := make([]batchItem, len(raw)) + for i, r := range raw { + m, ok := r.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("transit: item %d is not an object", i) + } + items[i].Plaintext, _ = m["plaintext"].(string) + items[i].Ciphertext, _ = m["ciphertext"].(string) + items[i].Context, _ = m["context"].(string) + items[i].Reference, _ = m["reference"].(string) + } + return items, nil +} + +func zeroizeKey(key crypto.PrivateKey) { + if key == nil { + return + } + switch k := key.(type) { + case *ecdsa.PrivateKey: + k.D.SetInt64(0) + case ed25519.PrivateKey: + for i := range k { + k[i] = 0 + } + } +} diff --git a/internal/engine/transit/transit_test.go b/internal/engine/transit/transit_test.go new file mode 100644 index 0000000..66a281b --- /dev/null +++ b/internal/engine/transit/transit_test.go @@ -0,0 +1,1025 @@ +package transit + +import ( + "context" + "encoding/base64" + "strings" + "sync" + "testing" + + "git.wntrmute.dev/kyle/metacrypt/internal/barrier" + "git.wntrmute.dev/kyle/metacrypt/internal/engine" +) + +// memBarrier is an in-memory barrier for testing. +type memBarrier struct { + data map[string][]byte + mu sync.RWMutex +} + +func newMemBarrier() *memBarrier { + return &memBarrier{data: make(map[string][]byte)} +} + +func (m *memBarrier) Unseal(_ []byte) error { return nil } +func (m *memBarrier) Seal() error { return nil } +func (m *memBarrier) IsSealed() bool { return false } + +func (m *memBarrier) Get(_ context.Context, path string) ([]byte, error) { + m.mu.RLock() + defer m.mu.RUnlock() + v, ok := m.data[path] + if !ok { + return nil, barrier.ErrNotFound + } + cp := make([]byte, len(v)) + copy(cp, v) + return cp, nil +} + +func (m *memBarrier) Put(_ context.Context, path string, value []byte) error { + m.mu.Lock() + defer m.mu.Unlock() + cp := make([]byte, len(value)) + copy(cp, value) + m.data[path] = cp + return nil +} + +func (m *memBarrier) Delete(_ context.Context, path string) error { + m.mu.Lock() + defer m.mu.Unlock() + delete(m.data, path) + return nil +} + +func (m *memBarrier) List(_ context.Context, prefix string) ([]string, error) { + m.mu.RLock() + defer m.mu.RUnlock() + var paths []string + for k := range m.data { + if strings.HasPrefix(k, prefix) { + paths = append(paths, strings.TrimPrefix(k, prefix)) + } + } + return paths, nil +} + +func adminCaller() *engine.CallerInfo { + return &engine.CallerInfo{Username: "admin", Roles: []string{"admin"}, IsAdmin: true} +} + +func userCaller() *engine.CallerInfo { + return &engine.CallerInfo{Username: "user", Roles: []string{"user"}, IsAdmin: false} +} + +func guestCaller() *engine.CallerInfo { + return &engine.CallerInfo{Username: "guest", Roles: []string{"guest"}, IsAdmin: false} +} + +func setupEngine(t *testing.T) (*TransitEngine, *memBarrier) { + t.Helper() + b := newMemBarrier() + eng := NewTransitEngine() + ctx := context.Background() + mountPath := "engine/transit/test/" + + err := eng.Initialize(ctx, b, mountPath, nil) + if err != nil { + t.Fatalf("Initialize: %v", err) + } + + te := eng.(*TransitEngine) + return te, b +} + +func setupEngineWithUnseal(t *testing.T) (*TransitEngine, *memBarrier) { + t.Helper() + te, b := setupEngine(t) + + // Seal and unseal to test the full lifecycle. + if err := te.Seal(); err != nil { + t.Fatalf("Seal: %v", err) + } + + eng2 := NewTransitEngine() + ctx := context.Background() + if err := eng2.Unseal(ctx, b, "engine/transit/test/"); err != nil { + t.Fatalf("Unseal: %v", err) + } + return eng2.(*TransitEngine), b +} + +func createKey(t *testing.T, te *TransitEngine, name, keyType string) { + t.Helper() + ctx := context.Background() + _, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "create-key", + CallerInfo: adminCaller(), + Data: map[string]interface{}{ + "name": name, + "type": keyType, + }, + }) + if err != nil { + t.Fatalf("create-key %s: %v", name, err) + } +} + +func TestInitializeAndUnseal(t *testing.T) { + te, b := setupEngine(t) + + // Create a key so unseal has something to load. + createKey(t, te, "mykey", "aes256-gcm") + + if err := te.Seal(); err != nil { + t.Fatalf("Seal: %v", err) + } + + eng2 := NewTransitEngine() + if err := eng2.Unseal(context.Background(), b, "engine/transit/test/"); err != nil { + t.Fatalf("Unseal: %v", err) + } + + te2 := eng2.(*TransitEngine) + if _, ok := te2.keys["mykey"]; !ok { + t.Fatal("expected key 'mykey' after unseal") + } +} + +func TestCreateKeyAllTypes(t *testing.T) { + types := []string{"aes256-gcm", "chacha20-poly", "ed25519", "ecdsa-p256", "ecdsa-p384", "hmac-sha256", "hmac-sha512"} + for _, kt := range types { + t.Run(kt, func(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "key-"+kt, kt) + + ks, ok := te.keys["key-"+kt] + if !ok { + t.Fatal("key not created") + } + if ks.config.CurrentVersion != 1 { + t.Fatalf("expected version 1, got %d", ks.config.CurrentVersion) + } + if ks.config.MinDecryptionVersion != 1 { + t.Fatalf("expected min_decryption_version 1, got %d", ks.config.MinDecryptionVersion) + } + }) + } +} + +func TestEncryptDecryptAES(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "aes-key", "aes256-gcm") + ctx := context.Background() + + plaintext := base64.StdEncoding.EncodeToString([]byte("hello world")) + + // Encrypt. + resp, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "encrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{ + "key": "aes-key", + "plaintext": plaintext, + }, + }) + if err != nil { + t.Fatalf("encrypt: %v", err) + } + + ct, _ := resp.Data["ciphertext"].(string) + if !strings.HasPrefix(ct, "metacrypt:v1:") { + t.Fatalf("unexpected ciphertext format: %s", ct) + } + + // Decrypt. + resp, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "decrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{ + "key": "aes-key", + "ciphertext": ct, + }, + }) + if err != nil { + t.Fatalf("decrypt: %v", err) + } + + ptB64, _ := resp.Data["plaintext"].(string) + decoded, _ := base64.StdEncoding.DecodeString(ptB64) + if string(decoded) != "hello world" { + t.Fatalf("expected 'hello world', got %q", string(decoded)) + } +} + +func TestEncryptDecryptChaCha(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "chacha-key", "chacha20-poly") + ctx := context.Background() + + plaintext := base64.StdEncoding.EncodeToString([]byte("secret data")) + + resp, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "encrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "chacha-key", "plaintext": plaintext}, + }) + if err != nil { + t.Fatalf("encrypt: %v", err) + } + + ct, _ := resp.Data["ciphertext"].(string) + if !strings.HasPrefix(ct, "metacrypt:v1:") { + t.Fatalf("unexpected ciphertext format: %s", ct) + } + + resp, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "decrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "chacha-key", "ciphertext": ct}, + }) + if err != nil { + t.Fatalf("decrypt: %v", err) + } + + ptB64, _ := resp.Data["plaintext"].(string) + decoded, _ := base64.StdEncoding.DecodeString(ptB64) + if string(decoded) != "secret data" { + t.Fatalf("expected 'secret data', got %q", string(decoded)) + } +} + +func TestEncryptWithContext(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "ctx-key", "aes256-gcm") + ctx := context.Background() + + plaintext := base64.StdEncoding.EncodeToString([]byte("context test")) + aadB64 := base64.StdEncoding.EncodeToString([]byte("my-context")) + + resp, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "encrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "ctx-key", "plaintext": plaintext, "context": aadB64}, + }) + if err != nil { + t.Fatalf("encrypt: %v", err) + } + ct, _ := resp.Data["ciphertext"].(string) + + // Decrypt with correct context. + resp, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "decrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "ctx-key", "ciphertext": ct, "context": aadB64}, + }) + if err != nil { + t.Fatalf("decrypt: %v", err) + } + + // Decrypt with wrong context should fail. + wrongCtx := base64.StdEncoding.EncodeToString([]byte("wrong-context")) + _, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "decrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "ctx-key", "ciphertext": ct, "context": wrongCtx}, + }) + if err == nil { + t.Fatal("expected decrypt to fail with wrong context") + } +} + +func TestKeyRotationAndDecrypt(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "rot-key", "aes256-gcm") + ctx := context.Background() + + plaintext := base64.StdEncoding.EncodeToString([]byte("rotate me")) + + // Encrypt with v1. + resp, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "encrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "rot-key", "plaintext": plaintext}, + }) + if err != nil { + t.Fatalf("encrypt v1: %v", err) + } + ctV1, _ := resp.Data["ciphertext"].(string) + + // Rotate. + _, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "rotate-key", + CallerInfo: adminCaller(), + Data: map[string]interface{}{"name": "rot-key"}, + }) + if err != nil { + t.Fatalf("rotate: %v", err) + } + + // Encrypt with v2. + resp, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "encrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "rot-key", "plaintext": plaintext}, + }) + if err != nil { + t.Fatalf("encrypt v2: %v", err) + } + ctV2, _ := resp.Data["ciphertext"].(string) + + if !strings.HasPrefix(ctV2, "metacrypt:v2:") { + t.Fatalf("expected v2 ciphertext, got %s", ctV2) + } + + // Both ciphertexts should decrypt. + for _, ct := range []string{ctV1, ctV2} { + resp, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "decrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "rot-key", "ciphertext": ct}, + }) + if err != nil { + t.Fatalf("decrypt: %v", err) + } + ptB64, _ := resp.Data["plaintext"].(string) + decoded, _ := base64.StdEncoding.DecodeString(ptB64) + if string(decoded) != "rotate me" { + t.Fatalf("expected 'rotate me', got %q", string(decoded)) + } + } +} + +func TestUpdateKeyConfig(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "cfg-key", "aes256-gcm") + ctx := context.Background() + + plaintext := base64.StdEncoding.EncodeToString([]byte("old data")) + + // Encrypt with v1. + resp, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "encrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "cfg-key", "plaintext": plaintext}, + }) + if err != nil { + t.Fatalf("encrypt: %v", err) + } + ctV1, _ := resp.Data["ciphertext"].(string) + + // Rotate to v2. + _, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "rotate-key", + CallerInfo: adminCaller(), + Data: map[string]interface{}{"name": "cfg-key"}, + }) + if err != nil { + t.Fatalf("rotate: %v", err) + } + + // Advance min_decryption_version to 2. + _, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "update-key-config", + CallerInfo: adminCaller(), + Data: map[string]interface{}{"name": "cfg-key", "min_decryption_version": float64(2)}, + }) + if err != nil { + t.Fatalf("update-key-config: %v", err) + } + + // v1 ciphertext should be rejected. + _, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "decrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "cfg-key", "ciphertext": ctV1}, + }) + if err == nil { + t.Fatal("expected decrypt to fail for v1 ciphertext") + } + + // Cannot decrease min_decryption_version. + _, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "update-key-config", + CallerInfo: adminCaller(), + Data: map[string]interface{}{"name": "cfg-key", "min_decryption_version": float64(1)}, + }) + if err == nil { + t.Fatal("expected error decreasing min_decryption_version") + } + + // Cannot exceed current version. + _, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "update-key-config", + CallerInfo: adminCaller(), + Data: map[string]interface{}{"name": "cfg-key", "min_decryption_version": float64(99)}, + }) + if err == nil { + t.Fatal("expected error exceeding current version") + } +} + +func TestTrimKey(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "trim-key", "aes256-gcm") + ctx := context.Background() + + // Rotate to v2, v3. + for i := 0; i < 2; i++ { + _, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "rotate-key", + CallerInfo: adminCaller(), + Data: map[string]interface{}{"name": "trim-key"}, + }) + if err != nil { + t.Fatalf("rotate: %v", err) + } + } + + // Set min_decryption_version to 2. + _, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "update-key-config", + CallerInfo: adminCaller(), + Data: map[string]interface{}{"name": "trim-key", "min_decryption_version": float64(2)}, + }) + if err != nil { + t.Fatalf("update-key-config: %v", err) + } + + // Trim. + resp, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "trim-key", + CallerInfo: adminCaller(), + Data: map[string]interface{}{"name": "trim-key"}, + }) + if err != nil { + t.Fatalf("trim-key: %v", err) + } + + trimmed, _ := resp.Data["trimmed"].(int) + if trimmed != 1 { + t.Fatalf("expected 1 trimmed, got %d", trimmed) + } + + // Version 1 should be gone. + if _, ok := te.keys["trim-key"].versions[1]; ok { + t.Fatal("version 1 should have been trimmed") + } + if _, ok := te.keys["trim-key"].versions[2]; !ok { + t.Fatal("version 2 should still exist") + } +} + +func TestSignVerifyEd25519(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "ed-key", "ed25519") + ctx := context.Background() + + input := base64.StdEncoding.EncodeToString([]byte("sign this")) + + resp, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "sign", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "ed-key", "input": input}, + }) + if err != nil { + t.Fatalf("sign: %v", err) + } + sig, _ := resp.Data["signature"].(string) + if !strings.HasPrefix(sig, "metacrypt:v1:") { + t.Fatalf("unexpected signature format: %s", sig) + } + + // Verify. + resp, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "verify", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "ed-key", "input": input, "signature": sig}, + }) + if err != nil { + t.Fatalf("verify: %v", err) + } + valid, _ := resp.Data["valid"].(bool) + if !valid { + t.Fatal("expected valid signature") + } + + // Wrong input should fail verification. + wrongInput := base64.StdEncoding.EncodeToString([]byte("wrong data")) + resp, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "verify", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "ed-key", "input": wrongInput, "signature": sig}, + }) + if err != nil { + t.Fatalf("verify: %v", err) + } + valid, _ = resp.Data["valid"].(bool) + if valid { + t.Fatal("expected invalid signature for wrong input") + } +} + +func TestSignVerifyECDSA(t *testing.T) { + for _, keyType := range []string{"ecdsa-p256", "ecdsa-p384"} { + t.Run(keyType, func(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "ec-key", keyType) + ctx := context.Background() + + input := base64.StdEncoding.EncodeToString([]byte("ecdsa test")) + + resp, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "sign", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "ec-key", "input": input}, + }) + if err != nil { + t.Fatalf("sign: %v", err) + } + sig, _ := resp.Data["signature"].(string) + + resp, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "verify", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "ec-key", "input": input, "signature": sig}, + }) + if err != nil { + t.Fatalf("verify: %v", err) + } + valid, _ := resp.Data["valid"].(bool) + if !valid { + t.Fatal("expected valid signature") + } + }) + } +} + +func TestSignRejectsSymmetricAndHMAC(t *testing.T) { + for _, keyType := range []string{"aes256-gcm", "hmac-sha256"} { + t.Run(keyType, func(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "sym-key", keyType) + ctx := context.Background() + + input := base64.StdEncoding.EncodeToString([]byte("test")) + _, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "sign", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "sym-key", "input": input}, + }) + if err == nil { + t.Fatal("expected error signing with non-asymmetric key") + } + }) + } +} + +func TestHMACComputeAndVerify(t *testing.T) { + for _, keyType := range []string{"hmac-sha256", "hmac-sha512"} { + t.Run(keyType, func(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "hmac-key", keyType) + ctx := context.Background() + + input := base64.StdEncoding.EncodeToString([]byte("hmac me")) + + // Compute. + resp, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "hmac", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "hmac-key", "input": input}, + }) + if err != nil { + t.Fatalf("hmac compute: %v", err) + } + hmacStr, _ := resp.Data["hmac"].(string) + if !strings.HasPrefix(hmacStr, "metacrypt:v1:") { + t.Fatalf("unexpected hmac format: %s", hmacStr) + } + + // Verify. + resp, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "hmac", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "hmac-key", "input": input, "hmac": hmacStr}, + }) + if err != nil { + t.Fatalf("hmac verify: %v", err) + } + valid, _ := resp.Data["valid"].(bool) + if !valid { + t.Fatal("expected valid HMAC") + } + + // Wrong input should fail. + wrongInput := base64.StdEncoding.EncodeToString([]byte("wrong data")) + resp, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "hmac", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "hmac-key", "input": wrongInput, "hmac": hmacStr}, + }) + if err != nil { + t.Fatalf("hmac verify wrong: %v", err) + } + valid, _ = resp.Data["valid"].(bool) + if valid { + t.Fatal("expected invalid HMAC for wrong input") + } + }) + } +} + +func TestBatchEncryptDecrypt(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "batch-key", "aes256-gcm") + ctx := context.Background() + + items := []interface{}{ + map[string]interface{}{ + "plaintext": base64.StdEncoding.EncodeToString([]byte("item1")), + "reference": "ref1", + }, + map[string]interface{}{ + "plaintext": base64.StdEncoding.EncodeToString([]byte("item2")), + "reference": "ref2", + }, + } + + // Batch encrypt. + resp, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "batch-encrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "batch-key", "items": items}, + }) + if err != nil { + t.Fatalf("batch-encrypt: %v", err) + } + + results, _ := resp.Data["results"].([]interface{}) + if len(results) != 2 { + t.Fatalf("expected 2 results, got %d", len(results)) + } + + // Build decrypt items. + decryptItems := make([]interface{}, len(results)) + for i, r := range results { + br, ok := r.(batchResult) + if !ok { + t.Fatalf("expected batchResult, got %T", r) + } + if br.Error != "" { + t.Fatalf("batch encrypt item %d error: %s", i, br.Error) + } + decryptItems[i] = map[string]interface{}{ + "ciphertext": br.Ciphertext, + "reference": br.Reference, + } + } + + // Batch decrypt. + resp, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "batch-decrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "batch-key", "items": decryptItems}, + }) + if err != nil { + t.Fatalf("batch-decrypt: %v", err) + } + + results, _ = resp.Data["results"].([]interface{}) + expected := []string{"item1", "item2"} + for i, r := range results { + br, ok := r.(batchResult) + if !ok { + t.Fatalf("expected batchResult, got %T", r) + } + if br.Error != "" { + t.Fatalf("batch decrypt item %d error: %s", i, br.Error) + } + decoded, _ := base64.StdEncoding.DecodeString(br.Plaintext) + if string(decoded) != expected[i] { + t.Fatalf("item %d: expected %q, got %q", i, expected[i], string(decoded)) + } + } +} + +func TestBatchPartialErrors(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "batch-err-key", "aes256-gcm") + ctx := context.Background() + + items := []interface{}{ + map[string]interface{}{ + "ciphertext": "metacrypt:v1:invalidbase64!!!", + "reference": "bad", + }, + map[string]interface{}{ + "ciphertext": "metacrypt:v1:" + base64.StdEncoding.EncodeToString([]byte("not-valid-ciphertext")), + "reference": "also-bad", + }, + } + + resp, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "batch-decrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "batch-err-key", "items": items}, + }) + if err != nil { + t.Fatalf("batch-decrypt: %v", err) + } + + results, _ := resp.Data["results"].([]interface{}) + if len(results) != 2 { + t.Fatalf("expected 2 results, got %d", len(results)) + } + for i, r := range results { + br, ok := r.(batchResult) + if !ok { + t.Fatalf("expected batchResult, got %T", r) + } + if br.Error == "" { + t.Fatalf("item %d: expected error", i) + } + } +} + +func TestRewrap(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "rewrap-key", "aes256-gcm") + ctx := context.Background() + + plaintext := base64.StdEncoding.EncodeToString([]byte("rewrap me")) + + // Encrypt with v1. + resp, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "encrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "rewrap-key", "plaintext": plaintext}, + }) + if err != nil { + t.Fatalf("encrypt: %v", err) + } + ctV1, _ := resp.Data["ciphertext"].(string) + + // Rotate. + _, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "rotate-key", + CallerInfo: adminCaller(), + Data: map[string]interface{}{"name": "rewrap-key"}, + }) + if err != nil { + t.Fatalf("rotate: %v", err) + } + + // Rewrap. + resp, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "rewrap", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "rewrap-key", "ciphertext": ctV1}, + }) + if err != nil { + t.Fatalf("rewrap: %v", err) + } + ctV2, _ := resp.Data["ciphertext"].(string) + if !strings.HasPrefix(ctV2, "metacrypt:v2:") { + t.Fatalf("expected v2 ciphertext after rewrap, got %s", ctV2) + } + + // Decrypt rewrapped ciphertext. + resp, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "decrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "rewrap-key", "ciphertext": ctV2}, + }) + if err != nil { + t.Fatalf("decrypt rewrapped: %v", err) + } + ptB64, _ := resp.Data["plaintext"].(string) + decoded, _ := base64.StdEncoding.DecodeString(ptB64) + if string(decoded) != "rewrap me" { + t.Fatalf("expected 'rewrap me', got %q", string(decoded)) + } +} + +func TestAuthEnforcement(t *testing.T) { + te, _ := setupEngine(t) + ctx := context.Background() + + // Admin-only operations should fail for users. + _, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "create-key", + CallerInfo: userCaller(), + Data: map[string]interface{}{"name": "test", "type": "aes256-gcm"}, + }) + if err == nil { + t.Fatal("expected create-key to fail for user") + } + + // Admin-only operations should fail for guests. + _, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "create-key", + CallerInfo: guestCaller(), + Data: map[string]interface{}{"name": "test", "type": "aes256-gcm"}, + }) + if err == nil { + t.Fatal("expected create-key to fail for guest") + } + + // User operations should fail for guests. + createKey(t, te, "auth-key", "aes256-gcm") + + _, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "list-keys", + CallerInfo: guestCaller(), + }) + if err == nil { + t.Fatal("expected list-keys to fail for guest") + } + + // Encrypt should fail for guest. + _, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "encrypt", + CallerInfo: guestCaller(), + Data: map[string]interface{}{"key": "auth-key", "plaintext": base64.StdEncoding.EncodeToString([]byte("test"))}, + }) + if err == nil { + t.Fatal("expected encrypt to fail for guest") + } + + // Unauthenticated should fail. + _, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "create-key", + CallerInfo: nil, + Data: map[string]interface{}{"name": "test", "type": "aes256-gcm"}, + }) + if err == nil { + t.Fatal("expected create-key to fail without auth") + } +} + +func TestDeleteKeyWithAndWithoutAllowDeletion(t *testing.T) { + te, _ := setupEngine(t) + ctx := context.Background() + + createKey(t, te, "nodelete-key", "aes256-gcm") + + // Should fail: allow_deletion is false. + _, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "delete-key", + CallerInfo: adminCaller(), + Data: map[string]interface{}{"name": "nodelete-key"}, + }) + if err == nil { + t.Fatal("expected delete to fail without allow_deletion") + } + + // Enable deletion. + _, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "update-key-config", + CallerInfo: adminCaller(), + Data: map[string]interface{}{"name": "nodelete-key", "allow_deletion": true}, + }) + if err != nil { + t.Fatalf("update-key-config: %v", err) + } + + // Should succeed now. + _, err = te.HandleRequest(ctx, &engine.Request{ + Operation: "delete-key", + CallerInfo: adminCaller(), + Data: map[string]interface{}{"name": "nodelete-key"}, + }) + if err != nil { + t.Fatalf("delete-key: %v", err) + } + + if _, ok := te.keys["nodelete-key"]; ok { + t.Fatal("key should have been deleted") + } +} + +func TestGetPublicKey(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "pubkey", "ed25519") + ctx := context.Background() + + resp, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "get-public-key", + CallerInfo: userCaller(), + Data: map[string]interface{}{"name": "pubkey"}, + }) + if err != nil { + t.Fatalf("get-public-key: %v", err) + } + pk, _ := resp.Data["public_key"].(string) + if pk == "" { + t.Fatal("expected non-empty public key") + } + ver, _ := resp.Data["version"].(int) + if ver != 1 { + t.Fatalf("expected version 1, got %d", ver) + } +} + +func TestGetPublicKeyRejectsSymmetric(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "sym", "aes256-gcm") + ctx := context.Background() + + _, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "get-public-key", + CallerInfo: userCaller(), + Data: map[string]interface{}{"name": "sym"}, + }) + if err == nil { + t.Fatal("expected error getting public key for symmetric key") + } +} + +func TestBatchRewrap(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "brwrap-key", "aes256-gcm") + ctx := context.Background() + + pt1 := base64.StdEncoding.EncodeToString([]byte("item1")) + pt2 := base64.StdEncoding.EncodeToString([]byte("item2")) + + // Encrypt two items. + var cts []string + for _, pt := range []string{pt1, pt2} { + resp, _ := te.HandleRequest(ctx, &engine.Request{ + Operation: "encrypt", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "brwrap-key", "plaintext": pt}, + }) + ct, _ := resp.Data["ciphertext"].(string) + cts = append(cts, ct) + } + + // Rotate. + te.HandleRequest(ctx, &engine.Request{ + Operation: "rotate-key", + CallerInfo: adminCaller(), + Data: map[string]interface{}{"name": "brwrap-key"}, + }) + + // Batch rewrap. + items := []interface{}{ + map[string]interface{}{"ciphertext": cts[0], "reference": "r1"}, + map[string]interface{}{"ciphertext": cts[1], "reference": "r2"}, + } + + resp, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "batch-rewrap", + CallerInfo: userCaller(), + Data: map[string]interface{}{"key": "brwrap-key", "items": items}, + }) + if err != nil { + t.Fatalf("batch-rewrap: %v", err) + } + + results, _ := resp.Data["results"].([]interface{}) + for i, r := range results { + br, ok := r.(batchResult) + if !ok { + t.Fatalf("expected batchResult, got %T", r) + } + if br.Error != "" { + t.Fatalf("item %d error: %s", i, br.Error) + } + if !strings.HasPrefix(br.Ciphertext, "metacrypt:v2:") { + t.Fatalf("item %d: expected v2 ciphertext, got %s", i, br.Ciphertext) + } + } +} + +func TestDuplicateKeyCreation(t *testing.T) { + te, _ := setupEngine(t) + createKey(t, te, "dup-key", "aes256-gcm") + ctx := context.Background() + + _, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "create-key", + CallerInfo: adminCaller(), + Data: map[string]interface{}{"name": "dup-key", "type": "aes256-gcm"}, + }) + if err == nil { + t.Fatal("expected error creating duplicate key") + } +} + +func TestInvalidKeyType(t *testing.T) { + te, _ := setupEngine(t) + ctx := context.Background() + + _, err := te.HandleRequest(ctx, &engine.Request{ + Operation: "create-key", + CallerInfo: adminCaller(), + Data: map[string]interface{}{"name": "bad", "type": "invalid-type"}, + }) + if err == nil { + t.Fatal("expected error for invalid key type") + } +} diff --git a/internal/engine/transit/types.go b/internal/engine/transit/types.go new file mode 100644 index 0000000..f1c7a2b --- /dev/null +++ b/internal/engine/transit/types.go @@ -0,0 +1,15 @@ +package transit + +// TransitConfig is the transit engine configuration stored in the barrier. +type TransitConfig struct { + MaxKeyVersions int `json:"max_key_versions"` +} + +// KeyConfig is per-key configuration stored in the barrier. +type KeyConfig struct { + Name string `json:"name"` + Type string `json:"type"` // aes256-gcm, chacha20-poly, ed25519, ecdsa-p256, ecdsa-p384, hmac-sha256, hmac-sha512 + CurrentVersion int `json:"current_version"` + MinDecryptionVersion int `json:"min_decryption_version"` + AllowDeletion bool `json:"allow_deletion"` +} diff --git a/internal/grpcserver/server.go b/internal/grpcserver/server.go index 33afaf8..fb647df 100644 --- a/internal/grpcserver/server.go +++ b/internal/grpcserver/server.go @@ -84,6 +84,7 @@ func (s *GRPCServer) Start() error { pb.RegisterBarrierServiceServer(s.srv, &barrierServer{s: s}) pb.RegisterACMEServiceServer(s.srv, &acmeServer{s: s}) pb.RegisterSSHCAServiceServer(s.srv, &sshcaServer{s: s}) + pb.RegisterTransitServiceServer(s.srv, &transitServer{s: s}) lis, err := net.Listen("tcp", s.cfg.Server.GRPCAddr) if err != nil { @@ -157,6 +158,24 @@ func sealRequiredMethods() map[string]bool { "/metacrypt.v2.SSHCAService/RevokeCert": true, "/metacrypt.v2.SSHCAService/DeleteCert": true, "/metacrypt.v2.SSHCAService/GetKRL": true, + // Transit. + "/metacrypt.v2.TransitService/CreateKey": true, + "/metacrypt.v2.TransitService/DeleteKey": true, + "/metacrypt.v2.TransitService/GetKey": true, + "/metacrypt.v2.TransitService/ListKeys": true, + "/metacrypt.v2.TransitService/RotateKey": true, + "/metacrypt.v2.TransitService/UpdateKeyConfig": true, + "/metacrypt.v2.TransitService/TrimKey": true, + "/metacrypt.v2.TransitService/Encrypt": true, + "/metacrypt.v2.TransitService/Decrypt": true, + "/metacrypt.v2.TransitService/Rewrap": true, + "/metacrypt.v2.TransitService/BatchEncrypt": true, + "/metacrypt.v2.TransitService/BatchDecrypt": true, + "/metacrypt.v2.TransitService/BatchRewrap": true, + "/metacrypt.v2.TransitService/Sign": true, + "/metacrypt.v2.TransitService/Verify": true, + "/metacrypt.v2.TransitService/Hmac": true, + "/metacrypt.v2.TransitService/GetPublicKey": true, } } @@ -203,6 +222,24 @@ func authRequiredMethods() map[string]bool { "/metacrypt.v2.SSHCAService/ListCerts": true, "/metacrypt.v2.SSHCAService/RevokeCert": true, "/metacrypt.v2.SSHCAService/DeleteCert": true, + // Transit. + "/metacrypt.v2.TransitService/CreateKey": true, + "/metacrypt.v2.TransitService/DeleteKey": true, + "/metacrypt.v2.TransitService/GetKey": true, + "/metacrypt.v2.TransitService/ListKeys": true, + "/metacrypt.v2.TransitService/RotateKey": true, + "/metacrypt.v2.TransitService/UpdateKeyConfig": true, + "/metacrypt.v2.TransitService/TrimKey": true, + "/metacrypt.v2.TransitService/Encrypt": true, + "/metacrypt.v2.TransitService/Decrypt": true, + "/metacrypt.v2.TransitService/Rewrap": true, + "/metacrypt.v2.TransitService/BatchEncrypt": true, + "/metacrypt.v2.TransitService/BatchDecrypt": true, + "/metacrypt.v2.TransitService/BatchRewrap": true, + "/metacrypt.v2.TransitService/Sign": true, + "/metacrypt.v2.TransitService/Verify": true, + "/metacrypt.v2.TransitService/Hmac": true, + "/metacrypt.v2.TransitService/GetPublicKey": true, } } @@ -234,5 +271,11 @@ func adminRequiredMethods() map[string]bool { "/metacrypt.v2.SSHCAService/DeleteProfile": true, "/metacrypt.v2.SSHCAService/RevokeCert": true, "/metacrypt.v2.SSHCAService/DeleteCert": true, + // Transit. + "/metacrypt.v2.TransitService/CreateKey": true, + "/metacrypt.v2.TransitService/DeleteKey": true, + "/metacrypt.v2.TransitService/RotateKey": true, + "/metacrypt.v2.TransitService/UpdateKeyConfig": true, + "/metacrypt.v2.TransitService/TrimKey": true, } } diff --git a/internal/grpcserver/transit.go b/internal/grpcserver/transit.go new file mode 100644 index 0000000..f5fc2f3 --- /dev/null +++ b/internal/grpcserver/transit.go @@ -0,0 +1,486 @@ +package grpcserver + +import ( + "context" + "errors" + "strings" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + pb "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v2" + "git.wntrmute.dev/kyle/metacrypt/internal/engine" + "git.wntrmute.dev/kyle/metacrypt/internal/engine/transit" + "git.wntrmute.dev/kyle/metacrypt/internal/policy" +) + +type transitServer struct { + pb.UnimplementedTransitServiceServer + s *GRPCServer +} + +func (ts *transitServer) transitHandleRequest(ctx context.Context, mount, operation string, req *engine.Request) (*engine.Response, error) { + resp, err := ts.s.engines.HandleRequest(ctx, mount, req) + if err != nil { + st := codes.Internal + switch { + case errors.Is(err, engine.ErrMountNotFound): + st = codes.NotFound + case errors.Is(err, transit.ErrKeyNotFound): + st = codes.NotFound + case errors.Is(err, transit.ErrKeyExists): + st = codes.AlreadyExists + case errors.Is(err, transit.ErrUnauthorized): + st = codes.Unauthenticated + case errors.Is(err, transit.ErrForbidden): + st = codes.PermissionDenied + case errors.Is(err, transit.ErrDeletionDenied): + st = codes.FailedPrecondition + case errors.Is(err, transit.ErrUnsupportedOp): + st = codes.InvalidArgument + case errors.Is(err, transit.ErrDecryptVersion): + st = codes.FailedPrecondition + case errors.Is(err, transit.ErrInvalidFormat): + st = codes.InvalidArgument + case errors.Is(err, transit.ErrBatchTooLarge): + st = codes.InvalidArgument + case errors.Is(err, transit.ErrInvalidMinVer): + st = codes.InvalidArgument + case strings.Contains(err.Error(), "not found"): + st = codes.NotFound + case strings.Contains(err.Error(), "forbidden"): + st = codes.PermissionDenied + } + ts.s.logger.Error("grpc: transit "+operation, "mount", mount, "error", err) + return nil, status.Error(st, err.Error()) + } + return resp, nil +} + +func (ts *transitServer) callerInfo(ctx context.Context) *engine.CallerInfo { + ti := tokenInfoFromContext(ctx) + if ti == nil { + return nil + } + return &engine.CallerInfo{ + Username: ti.Username, + Roles: ti.Roles, + IsAdmin: ti.IsAdmin, + } +} + +func (ts *transitServer) policyChecker(ctx context.Context) engine.PolicyChecker { + caller := ts.callerInfo(ctx) + if caller == nil { + return nil + } + return func(resource, action string) (string, bool) { + pReq := &policy.Request{ + Username: caller.Username, + Roles: caller.Roles, + Resource: resource, + Action: action, + } + effect, matched, err := ts.s.policy.Match(ctx, pReq) + if err != nil { + return string(policy.EffectDeny), false + } + return string(effect), matched + } +} + +func (ts *transitServer) CreateKey(ctx context.Context, req *pb.CreateTransitKeyRequest) (*pb.CreateTransitKeyResponse, error) { + if req.Mount == "" || req.Name == "" { + return nil, status.Error(codes.InvalidArgument, "mount and name are required") + } + resp, err := ts.transitHandleRequest(ctx, req.Mount, "create-key", &engine.Request{ + Operation: "create-key", + CallerInfo: ts.callerInfo(ctx), + Data: map[string]interface{}{ + "name": req.Name, + "type": req.Type, + }, + }) + if err != nil { + return nil, err + } + name, _ := resp.Data["name"].(string) + keyType, _ := resp.Data["type"].(string) + version, _ := resp.Data["version"].(int) + ts.s.logger.Info("audit: transit key created", "mount", req.Mount, "key", name, "type", keyType, "username", callerUsername(ctx)) + return &pb.CreateTransitKeyResponse{Name: name, Type: keyType, Version: int32(version)}, nil +} + +func (ts *transitServer) DeleteKey(ctx context.Context, req *pb.DeleteTransitKeyRequest) (*pb.DeleteTransitKeyResponse, error) { + if req.Mount == "" || req.Name == "" { + return nil, status.Error(codes.InvalidArgument, "mount and name are required") + } + _, err := ts.transitHandleRequest(ctx, req.Mount, "delete-key", &engine.Request{ + Operation: "delete-key", + CallerInfo: ts.callerInfo(ctx), + Data: map[string]interface{}{"name": req.Name}, + }) + if err != nil { + return nil, err + } + ts.s.logger.Info("audit: transit key deleted", "mount", req.Mount, "key", req.Name, "username", callerUsername(ctx)) + return &pb.DeleteTransitKeyResponse{}, nil +} + +func (ts *transitServer) GetKey(ctx context.Context, req *pb.GetTransitKeyRequest) (*pb.GetTransitKeyResponse, error) { + if req.Mount == "" || req.Name == "" { + return nil, status.Error(codes.InvalidArgument, "mount and name are required") + } + resp, err := ts.transitHandleRequest(ctx, req.Mount, "get-key", &engine.Request{ + Operation: "get-key", + CallerInfo: ts.callerInfo(ctx), + Data: map[string]interface{}{"name": req.Name}, + }) + if err != nil { + return nil, err + } + name, _ := resp.Data["name"].(string) + keyType, _ := resp.Data["type"].(string) + currentVersion, _ := resp.Data["current_version"].(int) + minDecryptionVersion, _ := resp.Data["min_decryption_version"].(int) + allowDeletion, _ := resp.Data["allow_deletion"].(bool) + rawVersions, _ := resp.Data["versions"].([]int) + versions := make([]int32, len(rawVersions)) + for i, v := range rawVersions { + versions[i] = int32(v) + } + return &pb.GetTransitKeyResponse{ + Name: name, + Type: keyType, + CurrentVersion: int32(currentVersion), + MinDecryptionVersion: int32(minDecryptionVersion), + AllowDeletion: allowDeletion, + Versions: versions, + }, nil +} + +func (ts *transitServer) ListKeys(ctx context.Context, req *pb.ListTransitKeysRequest) (*pb.ListTransitKeysResponse, error) { + if req.Mount == "" { + return nil, status.Error(codes.InvalidArgument, "mount is required") + } + resp, err := ts.transitHandleRequest(ctx, req.Mount, "list-keys", &engine.Request{ + Operation: "list-keys", + CallerInfo: ts.callerInfo(ctx), + }) + if err != nil { + return nil, err + } + keys := toStringSliceFromInterface(resp.Data["keys"]) + return &pb.ListTransitKeysResponse{Keys: keys}, nil +} + +func (ts *transitServer) RotateKey(ctx context.Context, req *pb.RotateTransitKeyRequest) (*pb.RotateTransitKeyResponse, error) { + if req.Mount == "" || req.Name == "" { + return nil, status.Error(codes.InvalidArgument, "mount and name are required") + } + resp, err := ts.transitHandleRequest(ctx, req.Mount, "rotate-key", &engine.Request{ + Operation: "rotate-key", + CallerInfo: ts.callerInfo(ctx), + Data: map[string]interface{}{"name": req.Name}, + }) + if err != nil { + return nil, err + } + name, _ := resp.Data["name"].(string) + version, _ := resp.Data["version"].(int) + ts.s.logger.Info("audit: transit key rotated", "mount", req.Mount, "key", name, "version", version, "username", callerUsername(ctx)) + return &pb.RotateTransitKeyResponse{Name: name, Version: int32(version)}, nil +} + +func (ts *transitServer) UpdateKeyConfig(ctx context.Context, req *pb.UpdateTransitKeyConfigRequest) (*pb.UpdateTransitKeyConfigResponse, error) { + if req.Mount == "" || req.Name == "" { + return nil, status.Error(codes.InvalidArgument, "mount and name are required") + } + data := map[string]interface{}{"name": req.Name} + if req.MinDecryptionVersion != 0 { + data["min_decryption_version"] = float64(req.MinDecryptionVersion) + } + data["allow_deletion"] = req.AllowDeletion + + _, err := ts.transitHandleRequest(ctx, req.Mount, "update-key-config", &engine.Request{ + Operation: "update-key-config", + CallerInfo: ts.callerInfo(ctx), + Data: data, + }) + if err != nil { + return nil, err + } + return &pb.UpdateTransitKeyConfigResponse{}, nil +} + +func (ts *transitServer) TrimKey(ctx context.Context, req *pb.TrimTransitKeyRequest) (*pb.TrimTransitKeyResponse, error) { + if req.Mount == "" || req.Name == "" { + return nil, status.Error(codes.InvalidArgument, "mount and name are required") + } + resp, err := ts.transitHandleRequest(ctx, req.Mount, "trim-key", &engine.Request{ + Operation: "trim-key", + CallerInfo: ts.callerInfo(ctx), + Data: map[string]interface{}{"name": req.Name}, + }) + if err != nil { + return nil, err + } + trimmed, _ := resp.Data["trimmed"].(int) + return &pb.TrimTransitKeyResponse{Trimmed: int32(trimmed)}, nil +} + +func (ts *transitServer) Encrypt(ctx context.Context, req *pb.TransitEncryptRequest) (*pb.TransitEncryptResponse, error) { + if req.Mount == "" || req.Key == "" { + return nil, status.Error(codes.InvalidArgument, "mount and key are required") + } + data := map[string]interface{}{ + "key": req.Key, + "plaintext": req.Plaintext, + } + if req.Context != "" { + data["context"] = req.Context + } + resp, err := ts.transitHandleRequest(ctx, req.Mount, "encrypt", &engine.Request{ + Operation: "encrypt", + CallerInfo: ts.callerInfo(ctx), + CheckPolicy: ts.policyChecker(ctx), + Data: data, + }) + if err != nil { + return nil, err + } + ct, _ := resp.Data["ciphertext"].(string) + return &pb.TransitEncryptResponse{Ciphertext: ct}, nil +} + +func (ts *transitServer) Decrypt(ctx context.Context, req *pb.TransitDecryptRequest) (*pb.TransitDecryptResponse, error) { + if req.Mount == "" || req.Key == "" { + return nil, status.Error(codes.InvalidArgument, "mount and key are required") + } + data := map[string]interface{}{ + "key": req.Key, + "ciphertext": req.Ciphertext, + } + if req.Context != "" { + data["context"] = req.Context + } + resp, err := ts.transitHandleRequest(ctx, req.Mount, "decrypt", &engine.Request{ + Operation: "decrypt", + CallerInfo: ts.callerInfo(ctx), + CheckPolicy: ts.policyChecker(ctx), + Data: data, + }) + if err != nil { + return nil, err + } + pt, _ := resp.Data["plaintext"].(string) + return &pb.TransitDecryptResponse{Plaintext: pt}, nil +} + +func (ts *transitServer) Rewrap(ctx context.Context, req *pb.TransitRewrapRequest) (*pb.TransitRewrapResponse, error) { + if req.Mount == "" || req.Key == "" { + return nil, status.Error(codes.InvalidArgument, "mount and key are required") + } + data := map[string]interface{}{ + "key": req.Key, + "ciphertext": req.Ciphertext, + } + if req.Context != "" { + data["context"] = req.Context + } + resp, err := ts.transitHandleRequest(ctx, req.Mount, "rewrap", &engine.Request{ + Operation: "rewrap", + CallerInfo: ts.callerInfo(ctx), + CheckPolicy: ts.policyChecker(ctx), + Data: data, + }) + if err != nil { + return nil, err + } + ct, _ := resp.Data["ciphertext"].(string) + return &pb.TransitRewrapResponse{Ciphertext: ct}, nil +} + +func (ts *transitServer) BatchEncrypt(ctx context.Context, req *pb.TransitBatchEncryptRequest) (*pb.TransitBatchResponse, error) { + if req.Mount == "" || req.Key == "" { + return nil, status.Error(codes.InvalidArgument, "mount and key are required") + } + items := protoItemsToInterface(req.Items) + resp, err := ts.transitHandleRequest(ctx, req.Mount, "batch-encrypt", &engine.Request{ + Operation: "batch-encrypt", + CallerInfo: ts.callerInfo(ctx), + CheckPolicy: ts.policyChecker(ctx), + Data: map[string]interface{}{"key": req.Key, "items": items}, + }) + if err != nil { + return nil, err + } + return toBatchResponse(resp), nil +} + +func (ts *transitServer) BatchDecrypt(ctx context.Context, req *pb.TransitBatchDecryptRequest) (*pb.TransitBatchResponse, error) { + if req.Mount == "" || req.Key == "" { + return nil, status.Error(codes.InvalidArgument, "mount and key are required") + } + items := protoItemsToInterface(req.Items) + resp, err := ts.transitHandleRequest(ctx, req.Mount, "batch-decrypt", &engine.Request{ + Operation: "batch-decrypt", + CallerInfo: ts.callerInfo(ctx), + CheckPolicy: ts.policyChecker(ctx), + Data: map[string]interface{}{"key": req.Key, "items": items}, + }) + if err != nil { + return nil, err + } + return toBatchResponse(resp), nil +} + +func (ts *transitServer) BatchRewrap(ctx context.Context, req *pb.TransitBatchRewrapRequest) (*pb.TransitBatchResponse, error) { + if req.Mount == "" || req.Key == "" { + return nil, status.Error(codes.InvalidArgument, "mount and key are required") + } + items := protoItemsToInterface(req.Items) + resp, err := ts.transitHandleRequest(ctx, req.Mount, "batch-rewrap", &engine.Request{ + Operation: "batch-rewrap", + CallerInfo: ts.callerInfo(ctx), + CheckPolicy: ts.policyChecker(ctx), + Data: map[string]interface{}{"key": req.Key, "items": items}, + }) + if err != nil { + return nil, err + } + return toBatchResponse(resp), nil +} + +func (ts *transitServer) Sign(ctx context.Context, req *pb.TransitSignRequest) (*pb.TransitSignResponse, error) { + if req.Mount == "" || req.Key == "" { + return nil, status.Error(codes.InvalidArgument, "mount and key are required") + } + resp, err := ts.transitHandleRequest(ctx, req.Mount, "sign", &engine.Request{ + Operation: "sign", + CallerInfo: ts.callerInfo(ctx), + CheckPolicy: ts.policyChecker(ctx), + Data: map[string]interface{}{"key": req.Key, "input": req.Input}, + }) + if err != nil { + return nil, err + } + sig, _ := resp.Data["signature"].(string) + return &pb.TransitSignResponse{Signature: sig}, nil +} + +func (ts *transitServer) Verify(ctx context.Context, req *pb.TransitVerifyRequest) (*pb.TransitVerifyResponse, error) { + if req.Mount == "" || req.Key == "" { + return nil, status.Error(codes.InvalidArgument, "mount and key are required") + } + resp, err := ts.transitHandleRequest(ctx, req.Mount, "verify", &engine.Request{ + Operation: "verify", + CallerInfo: ts.callerInfo(ctx), + CheckPolicy: ts.policyChecker(ctx), + Data: map[string]interface{}{ + "key": req.Key, + "input": req.Input, + "signature": req.Signature, + }, + }) + if err != nil { + return nil, err + } + valid, _ := resp.Data["valid"].(bool) + return &pb.TransitVerifyResponse{Valid: valid}, nil +} + +func (ts *transitServer) Hmac(ctx context.Context, req *pb.TransitHmacRequest) (*pb.TransitHmacResponse, error) { + if req.Mount == "" || req.Key == "" { + return nil, status.Error(codes.InvalidArgument, "mount and key are required") + } + data := map[string]interface{}{ + "key": req.Key, + "input": req.Input, + } + if req.Hmac != "" { + data["hmac"] = req.Hmac + } + resp, err := ts.transitHandleRequest(ctx, req.Mount, "hmac", &engine.Request{ + Operation: "hmac", + CallerInfo: ts.callerInfo(ctx), + CheckPolicy: ts.policyChecker(ctx), + Data: data, + }) + if err != nil { + return nil, err + } + hmacStr, _ := resp.Data["hmac"].(string) + valid, _ := resp.Data["valid"].(bool) + return &pb.TransitHmacResponse{Hmac: hmacStr, Valid: valid}, nil +} + +func (ts *transitServer) GetPublicKey(ctx context.Context, req *pb.GetTransitPublicKeyRequest) (*pb.GetTransitPublicKeyResponse, error) { + if req.Mount == "" || req.Name == "" { + return nil, status.Error(codes.InvalidArgument, "mount and name are required") + } + data := map[string]interface{}{"name": req.Name} + if req.Version != 0 { + data["version"] = float64(req.Version) + } + resp, err := ts.transitHandleRequest(ctx, req.Mount, "get-public-key", &engine.Request{ + Operation: "get-public-key", + CallerInfo: ts.callerInfo(ctx), + Data: data, + }) + if err != nil { + return nil, err + } + pk, _ := resp.Data["public_key"].(string) + version, _ := resp.Data["version"].(int) + keyType, _ := resp.Data["type"].(string) + return &pb.GetTransitPublicKeyResponse{ + PublicKey: pk, + Version: int32(version), + Type: keyType, + }, nil +} + +// --- helpers --- + +func protoItemsToInterface(items []*pb.TransitBatchItem) []interface{} { + out := make([]interface{}, len(items)) + for i, item := range items { + m := map[string]interface{}{} + if item.Plaintext != "" { + m["plaintext"] = item.Plaintext + } + if item.Ciphertext != "" { + m["ciphertext"] = item.Ciphertext + } + if item.Context != "" { + m["context"] = item.Context + } + if item.Reference != "" { + m["reference"] = item.Reference + } + out[i] = m + } + return out +} + +func toBatchResponse(resp *engine.Response) *pb.TransitBatchResponse { + raw, _ := resp.Data["results"].([]interface{}) + results := make([]*pb.TransitBatchResultItem, 0, len(raw)) + for _, item := range raw { + switch r := item.(type) { + case map[string]interface{}: + pt, _ := r["plaintext"].(string) + ct, _ := r["ciphertext"].(string) + ref, _ := r["reference"].(string) + errStr, _ := r["error"].(string) + results = append(results, &pb.TransitBatchResultItem{ + Plaintext: pt, + Ciphertext: ct, + Reference: ref, + Error: errStr, + }) + } + } + return &pb.TransitBatchResponse{Results: results} +} diff --git a/internal/server/routes.go b/internal/server/routes.go index 44fcc7f..00abdb5 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -71,6 +71,26 @@ func (s *Server) registerRoutes(r chi.Router) { r.HandleFunc("/v1/policy/rules", s.requireAuth(s.handlePolicyRules)) r.HandleFunc("/v1/policy/rule", s.requireAuth(s.handlePolicyRule)) + + // Transit engine routes. + r.Post("/v1/transit/{mount}/keys", s.requireAdmin(s.handleTransitCreateKey)) + r.Get("/v1/transit/{mount}/keys", s.requireAuth(s.handleTransitListKeys)) + r.Get("/v1/transit/{mount}/keys/{name}", s.requireAuth(s.handleTransitGetKey)) + r.Delete("/v1/transit/{mount}/keys/{name}", s.requireAdmin(s.handleTransitDeleteKey)) + r.Post("/v1/transit/{mount}/keys/{name}/rotate", s.requireAdmin(s.handleTransitRotateKey)) + r.Post("/v1/transit/{mount}/keys/{name}/config", s.requireAdmin(s.handleTransitUpdateKeyConfig)) + r.Post("/v1/transit/{mount}/keys/{name}/trim", s.requireAdmin(s.handleTransitTrimKey)) + r.Post("/v1/transit/{mount}/encrypt/{key}", s.requireAuth(s.handleTransitEncrypt)) + r.Post("/v1/transit/{mount}/decrypt/{key}", s.requireAuth(s.handleTransitDecrypt)) + r.Post("/v1/transit/{mount}/rewrap/{key}", s.requireAuth(s.handleTransitRewrap)) + r.Post("/v1/transit/{mount}/batch/encrypt/{key}", s.requireAuth(s.handleTransitBatchEncrypt)) + r.Post("/v1/transit/{mount}/batch/decrypt/{key}", s.requireAuth(s.handleTransitBatchDecrypt)) + r.Post("/v1/transit/{mount}/batch/rewrap/{key}", s.requireAuth(s.handleTransitBatchRewrap)) + r.Post("/v1/transit/{mount}/sign/{key}", s.requireAuth(s.handleTransitSign)) + r.Post("/v1/transit/{mount}/verify/{key}", s.requireAuth(s.handleTransitVerify)) + r.Post("/v1/transit/{mount}/hmac/{key}", s.requireAuth(s.handleTransitHmac)) + r.Get("/v1/transit/{mount}/keys/{name}/public-key", s.requireAuth(s.handleTransitGetPublicKey)) + s.registerACMERoutes(r) } @@ -757,6 +777,262 @@ func (s *Server) getCAEngine(mountName string) (*ca.CAEngine, error) { return caEng, nil } +// --- Transit Engine Handlers --- + +func (s *Server) transitRequest(w http.ResponseWriter, r *http.Request, mount, operation string, data map[string]interface{}) { + info := TokenInfoFromContext(r.Context()) + + policyChecker := func(resource, action string) (string, bool) { + pReq := &policy.Request{ + Username: info.Username, + Roles: info.Roles, + Resource: resource, + Action: action, + } + eff, matched, pErr := s.policy.Match(r.Context(), pReq) + if pErr != nil { + return string(policy.EffectDeny), false + } + return string(eff), matched + } + + resp, err := s.engines.HandleRequest(r.Context(), mount, &engine.Request{ + Operation: operation, + CallerInfo: &engine.CallerInfo{Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin}, + CheckPolicy: policyChecker, + Data: data, + }) + if err != nil { + status := http.StatusInternalServerError + switch { + case errors.Is(err, engine.ErrMountNotFound): + status = http.StatusNotFound + case strings.Contains(err.Error(), "forbidden"): + status = http.StatusForbidden + case strings.Contains(err.Error(), "authentication required"): + status = http.StatusUnauthorized + case strings.Contains(err.Error(), "not found"): + status = http.StatusNotFound + case strings.Contains(err.Error(), "not allowed"): + status = http.StatusForbidden + case strings.Contains(err.Error(), "unsupported"): + status = http.StatusBadRequest + case strings.Contains(err.Error(), "invalid"): + status = http.StatusBadRequest + } + http.Error(w, `{"error":"`+err.Error()+`"}`, status) + return + } + writeJSON(w, http.StatusOK, resp.Data) +} + +func (s *Server) handleTransitCreateKey(w http.ResponseWriter, r *http.Request) { + mount := chi.URLParam(r, "mount") + var req struct { + Name string `json:"name"` + Type string `json:"type"` + } + if err := readJSON(r, &req); err != nil { + http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) + return + } + s.transitRequest(w, r, mount, "create-key", map[string]interface{}{"name": req.Name, "type": req.Type}) +} + +func (s *Server) handleTransitDeleteKey(w http.ResponseWriter, r *http.Request) { + mount := chi.URLParam(r, "mount") + name := chi.URLParam(r, "name") + s.transitRequest(w, r, mount, "delete-key", map[string]interface{}{"name": name}) +} + +func (s *Server) handleTransitGetKey(w http.ResponseWriter, r *http.Request) { + mount := chi.URLParam(r, "mount") + name := chi.URLParam(r, "name") + s.transitRequest(w, r, mount, "get-key", map[string]interface{}{"name": name}) +} + +func (s *Server) handleTransitListKeys(w http.ResponseWriter, r *http.Request) { + mount := chi.URLParam(r, "mount") + s.transitRequest(w, r, mount, "list-keys", nil) +} + +func (s *Server) handleTransitRotateKey(w http.ResponseWriter, r *http.Request) { + mount := chi.URLParam(r, "mount") + name := chi.URLParam(r, "name") + s.transitRequest(w, r, mount, "rotate-key", map[string]interface{}{"name": name}) +} + +func (s *Server) handleTransitUpdateKeyConfig(w http.ResponseWriter, r *http.Request) { + mount := chi.URLParam(r, "mount") + name := chi.URLParam(r, "name") + var req struct { + MinDecryptionVersion *float64 `json:"min_decryption_version"` + AllowDeletion *bool `json:"allow_deletion"` + } + if err := readJSON(r, &req); err != nil { + http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) + return + } + data := map[string]interface{}{"name": name} + if req.MinDecryptionVersion != nil { + data["min_decryption_version"] = *req.MinDecryptionVersion + } + if req.AllowDeletion != nil { + data["allow_deletion"] = *req.AllowDeletion + } + s.transitRequest(w, r, mount, "update-key-config", data) +} + +func (s *Server) handleTransitTrimKey(w http.ResponseWriter, r *http.Request) { + mount := chi.URLParam(r, "mount") + name := chi.URLParam(r, "name") + s.transitRequest(w, r, mount, "trim-key", map[string]interface{}{"name": name}) +} + +func (s *Server) handleTransitEncrypt(w http.ResponseWriter, r *http.Request) { + mount := chi.URLParam(r, "mount") + key := chi.URLParam(r, "key") + var req struct { + Plaintext string `json:"plaintext"` + Context string `json:"context"` + } + if err := readJSON(r, &req); err != nil { + http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) + return + } + data := map[string]interface{}{"key": key, "plaintext": req.Plaintext} + if req.Context != "" { + data["context"] = req.Context + } + s.transitRequest(w, r, mount, "encrypt", data) +} + +func (s *Server) handleTransitDecrypt(w http.ResponseWriter, r *http.Request) { + mount := chi.URLParam(r, "mount") + key := chi.URLParam(r, "key") + var req struct { + Ciphertext string `json:"ciphertext"` + Context string `json:"context"` + } + if err := readJSON(r, &req); err != nil { + http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) + return + } + data := map[string]interface{}{"key": key, "ciphertext": req.Ciphertext} + if req.Context != "" { + data["context"] = req.Context + } + s.transitRequest(w, r, mount, "decrypt", data) +} + +func (s *Server) handleTransitRewrap(w http.ResponseWriter, r *http.Request) { + mount := chi.URLParam(r, "mount") + key := chi.URLParam(r, "key") + var req struct { + Ciphertext string `json:"ciphertext"` + Context string `json:"context"` + } + if err := readJSON(r, &req); err != nil { + http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) + return + } + data := map[string]interface{}{"key": key, "ciphertext": req.Ciphertext} + if req.Context != "" { + data["context"] = req.Context + } + s.transitRequest(w, r, mount, "rewrap", data) +} + +func (s *Server) handleTransitBatchEncrypt(w http.ResponseWriter, r *http.Request) { + mount := chi.URLParam(r, "mount") + key := chi.URLParam(r, "key") + var req struct { + Items []interface{} `json:"items"` + } + if err := readJSON(r, &req); err != nil { + http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) + return + } + s.transitRequest(w, r, mount, "batch-encrypt", map[string]interface{}{"key": key, "items": req.Items}) +} + +func (s *Server) handleTransitBatchDecrypt(w http.ResponseWriter, r *http.Request) { + mount := chi.URLParam(r, "mount") + key := chi.URLParam(r, "key") + var req struct { + Items []interface{} `json:"items"` + } + if err := readJSON(r, &req); err != nil { + http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) + return + } + s.transitRequest(w, r, mount, "batch-decrypt", map[string]interface{}{"key": key, "items": req.Items}) +} + +func (s *Server) handleTransitBatchRewrap(w http.ResponseWriter, r *http.Request) { + mount := chi.URLParam(r, "mount") + key := chi.URLParam(r, "key") + var req struct { + Items []interface{} `json:"items"` + } + if err := readJSON(r, &req); err != nil { + http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) + return + } + s.transitRequest(w, r, mount, "batch-rewrap", map[string]interface{}{"key": key, "items": req.Items}) +} + +func (s *Server) handleTransitSign(w http.ResponseWriter, r *http.Request) { + mount := chi.URLParam(r, "mount") + key := chi.URLParam(r, "key") + var req struct { + Input string `json:"input"` + } + if err := readJSON(r, &req); err != nil { + http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) + return + } + s.transitRequest(w, r, mount, "sign", map[string]interface{}{"key": key, "input": req.Input}) +} + +func (s *Server) handleTransitVerify(w http.ResponseWriter, r *http.Request) { + mount := chi.URLParam(r, "mount") + key := chi.URLParam(r, "key") + var req struct { + Input string `json:"input"` + Signature string `json:"signature"` + } + if err := readJSON(r, &req); err != nil { + http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) + return + } + s.transitRequest(w, r, mount, "verify", map[string]interface{}{"key": key, "input": req.Input, "signature": req.Signature}) +} + +func (s *Server) handleTransitHmac(w http.ResponseWriter, r *http.Request) { + mount := chi.URLParam(r, "mount") + key := chi.URLParam(r, "key") + var req struct { + Input string `json:"input"` + HMAC string `json:"hmac"` + } + if err := readJSON(r, &req); err != nil { + http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) + return + } + data := map[string]interface{}{"key": key, "input": req.Input} + if req.HMAC != "" { + data["hmac"] = req.HMAC + } + s.transitRequest(w, r, mount, "hmac", data) +} + +func (s *Server) handleTransitGetPublicKey(w http.ResponseWriter, r *http.Request) { + mount := chi.URLParam(r, "mount") + name := chi.URLParam(r, "name") + s.transitRequest(w, r, mount, "get-public-key", map[string]interface{}{"name": name}) +} + // operationAction maps an engine operation name to a policy action. func operationAction(op string) string { switch op { diff --git a/proto/metacrypt/v2/transit.proto b/proto/metacrypt/v2/transit.proto new file mode 100644 index 0000000..4e2ae32 --- /dev/null +++ b/proto/metacrypt/v2/transit.proto @@ -0,0 +1,291 @@ +syntax = "proto3"; + +package metacrypt.v2; + +option go_package = "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v2;metacryptv2"; + +// TransitService provides typed, authenticated access to transit engine +// operations: symmetric encryption, signing, HMAC, and versioned key +// management. All RPCs require the service to be unsealed. +service TransitService { + // CreateKey creates a new named encryption key. Admin only. + rpc CreateKey(CreateTransitKeyRequest) returns (CreateTransitKeyResponse); + + // DeleteKey permanently removes a named key. Admin only. + // Only succeeds if allow_deletion is true on the key config. + rpc DeleteKey(DeleteTransitKeyRequest) returns (DeleteTransitKeyResponse); + + // GetKey returns metadata for a named key (no raw material). Auth required. + rpc GetKey(GetTransitKeyRequest) returns (GetTransitKeyResponse); + + // ListKeys returns the names of all configured keys. Auth required. + rpc ListKeys(ListTransitKeysRequest) returns (ListTransitKeysResponse); + + // RotateKey creates a new version of the named key. Admin only. + rpc RotateKey(RotateTransitKeyRequest) returns (RotateTransitKeyResponse); + + // UpdateKeyConfig updates key configuration (e.g. min_decryption_version). + // Admin only. + rpc UpdateKeyConfig(UpdateTransitKeyConfigRequest) returns (UpdateTransitKeyConfigResponse); + + // TrimKey deletes versions below min_decryption_version. Admin only. + rpc TrimKey(TrimTransitKeyRequest) returns (TrimTransitKeyResponse); + + // Encrypt encrypts plaintext with the latest key version. Auth required. + rpc Encrypt(TransitEncryptRequest) returns (TransitEncryptResponse); + + // Decrypt decrypts ciphertext. Auth required. + rpc Decrypt(TransitDecryptRequest) returns (TransitDecryptResponse); + + // Rewrap re-encrypts ciphertext with the latest key version without + // exposing plaintext. Auth required. + rpc Rewrap(TransitRewrapRequest) returns (TransitRewrapResponse); + + // BatchEncrypt encrypts multiple items in a single request. Auth required. + rpc BatchEncrypt(TransitBatchEncryptRequest) returns (TransitBatchResponse); + + // BatchDecrypt decrypts multiple items in a single request. Auth required. + rpc BatchDecrypt(TransitBatchDecryptRequest) returns (TransitBatchResponse); + + // BatchRewrap re-encrypts multiple items in a single request. Auth required. + rpc BatchRewrap(TransitBatchRewrapRequest) returns (TransitBatchResponse); + + // Sign signs input data with an asymmetric key. Auth required. + rpc Sign(TransitSignRequest) returns (TransitSignResponse); + + // Verify verifies a signature against input data. Auth required. + rpc Verify(TransitVerifyRequest) returns (TransitVerifyResponse); + + // Hmac computes or verifies an HMAC. Auth required. + rpc Hmac(TransitHmacRequest) returns (TransitHmacResponse); + + // GetPublicKey returns the public key for an asymmetric key. Auth required. + rpc GetPublicKey(GetTransitPublicKeyRequest) returns (GetTransitPublicKeyResponse); +} + +// --- CreateKey --- + +message CreateTransitKeyRequest { + string mount = 1; + string name = 2; + // type is the key algorithm: aes256-gcm, chacha20-poly, ed25519, + // ecdsa-p256, ecdsa-p384, hmac-sha256, hmac-sha512. + // Defaults to aes256-gcm if empty. + string type = 3; +} + +message CreateTransitKeyResponse { + string name = 1; + string type = 2; + int32 version = 3; +} + +// --- DeleteKey --- + +message DeleteTransitKeyRequest { + string mount = 1; + string name = 2; +} + +message DeleteTransitKeyResponse {} + +// --- GetKey --- + +message GetTransitKeyRequest { + string mount = 1; + string name = 2; +} + +message GetTransitKeyResponse { + string name = 1; + string type = 2; + int32 current_version = 3; + int32 min_decryption_version = 4; + bool allow_deletion = 5; + repeated int32 versions = 6; +} + +// --- ListKeys --- + +message ListTransitKeysRequest { + string mount = 1; +} + +message ListTransitKeysResponse { + repeated string keys = 1; +} + +// --- RotateKey --- + +message RotateTransitKeyRequest { + string mount = 1; + string name = 2; +} + +message RotateTransitKeyResponse { + string name = 1; + int32 version = 2; +} + +// --- UpdateKeyConfig --- + +message UpdateTransitKeyConfigRequest { + string mount = 1; + string name = 2; + int32 min_decryption_version = 3; + bool allow_deletion = 4; +} + +message UpdateTransitKeyConfigResponse {} + +// --- TrimKey --- + +message TrimTransitKeyRequest { + string mount = 1; + string name = 2; +} + +message TrimTransitKeyResponse { + int32 trimmed = 1; +} + +// --- Encrypt --- + +message TransitEncryptRequest { + string mount = 1; + string key = 2; + // plaintext is base64-encoded data to encrypt. + string plaintext = 3; + // context is optional base64-encoded additional authenticated data (AAD). + string context = 4; +} + +message TransitEncryptResponse { + // ciphertext in format "metacrypt:v{version}:{base64(nonce+ciphertext+tag)}" + string ciphertext = 1; +} + +// --- Decrypt --- + +message TransitDecryptRequest { + string mount = 1; + string key = 2; + string ciphertext = 3; + string context = 4; +} + +message TransitDecryptResponse { + // plaintext is base64-encoded decrypted data. + string plaintext = 1; +} + +// --- Rewrap --- + +message TransitRewrapRequest { + string mount = 1; + string key = 2; + string ciphertext = 3; + string context = 4; +} + +message TransitRewrapResponse { + string ciphertext = 1; +} + +// --- Batch --- + +message TransitBatchItem { + string plaintext = 1; + string ciphertext = 2; + string context = 3; + string reference = 4; +} + +message TransitBatchResultItem { + string plaintext = 1; + string ciphertext = 2; + string reference = 3; + string error = 4; +} + +message TransitBatchEncryptRequest { + string mount = 1; + string key = 2; + repeated TransitBatchItem items = 3; +} + +message TransitBatchDecryptRequest { + string mount = 1; + string key = 2; + repeated TransitBatchItem items = 3; +} + +message TransitBatchRewrapRequest { + string mount = 1; + string key = 2; + repeated TransitBatchItem items = 3; +} + +message TransitBatchResponse { + repeated TransitBatchResultItem results = 1; +} + +// --- Sign --- + +message TransitSignRequest { + string mount = 1; + string key = 2; + // input is base64-encoded data to sign. + string input = 3; +} + +message TransitSignResponse { + // signature in format "metacrypt:v{version}:{base64(signature_bytes)}" + string signature = 1; +} + +// --- Verify --- + +message TransitVerifyRequest { + string mount = 1; + string key = 2; + string input = 3; + string signature = 4; +} + +message TransitVerifyResponse { + bool valid = 1; +} + +// --- HMAC --- + +message TransitHmacRequest { + string mount = 1; + string key = 2; + // input is base64-encoded data to HMAC. + string input = 3; + // hmac, if set, switches to verify mode. + string hmac = 4; +} + +message TransitHmacResponse { + // hmac is set in compute mode. + string hmac = 1; + // valid is set in verify mode. + bool valid = 2; +} + +// --- GetPublicKey --- + +message GetTransitPublicKeyRequest { + string mount = 1; + string name = 2; + int32 version = 3; +} + +message GetTransitPublicKeyResponse { + // public_key is base64-encoded PKIX DER public key. + string public_key = 1; + int32 version = 2; + string type = 3; +}