All notable changes to Egide will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Release note: this batch includes a source break (
TransitErroris now#[non_exhaustive]) and an observable API surface change (create_keyrejectschacha20-poly1305). It must ship as0.2.0, not a patch release.
- Storage:
egide_storage::pattern, a public helper module exposingescape_like_patternandprefix_pattern. Any backend or engine building aLIKEpattern from caller-supplied text must route it through these and stateESCAPE '\'in the query. SQLite defines no default escape character, so the clause is mandatory rather than decorative.
- Prefer dependency versions compatible with the declared Rust 1.94 MSRV during Cargo updates.
- Transit:
create_keyrefuseschacha20-poly1305at creation time. That type was accepted since 0.1.0 but never actually implemented: keys created with it were always encrypted under AES-256-GCM regardless. Accepting it produced a policy row and ciphertext labels no client could trust. TransitErroris now#[non_exhaustive]. This is a source break for any consumer matching on it exhaustively (a wildcard arm is now required); it buys the room to add a variant in a future patch release instead of forcing a major version bump.- Secrets: the
secret_versionstable gains a nullablegeneration_saltcolumn. It is added automatically at startup by an idempotentALTER TABLE; no operator action and no downtime are required. - Secrets: versions written before this release carry no salt and are still
derived under the previous
egide-secrets-v2:{path}:{version}context. That context is retained on purpose and will remain as long as pre-upgrade rows can exist. Each row is self-describing: the presence or absence of a salt on the row alone selects the derivation.
- Transit: the ciphertext envelope now carries its own algorithm
(
egide:v{n}:{base64}for AES-256-GCM,egide:v{n}:{algorithm}:{base64}for anything else), anddecrypt/rewrapcheck it against the engine's actual implemented algorithm, not against the key's declaredkey_type. Under 0.1.0, a key declaredchacha20-poly1305was in fact always encrypted under AES-256-GCM, anddecryptperformed no algorithm check at all; existing 0.1.0 ciphertexts under such a key remain readable exactly as before. What changes isencrypt: it now refuses to run on a key whose declared type is not the engine's implemented algorithm (TransitError::KeyAlgorithmNotImplemented), instead of silently encrypting under AES-256-GCM while the key still claimschacha20-poly1305. - Storage:
putanddeleteare now atomic. Both backends previously read the current version, then wrote the new row and its history entry in separate statements. Two concurrent writers could read the same version and produce duplicated history versions, losing one of the two writes from the audit trail. The version counter is now owned by the database itself (INSERT ... ON CONFLICT DO UPDATE SET version = <existing row>.version + 1 ... RETURNING version) under the row lock, and the history entry is written in the same transaction. SQLite opens the transaction withBEGIN IMMEDIATEso the write lock is taken up front instead of being promoted mid-transaction. - Storage:
listescapes%,_and\in the caller-supplied prefix before building theLIKEpattern, on both the SQLite and PostgreSQL backends. A bound parameter stops SQL injection but never stoppedLIKEfrom interpreting metacharacters inside the bound value: a prefix such as%ora_matched keys outside the requested prefix. - Secrets:
listhad the same unescaped prefix, reachable through the HTTP and gRPC API. A prefix containing%or_returned secret paths outside the requested prefix. It now uses the shared escaping helper. This is a correctness fix on the prefix, not an access-control boundary:listapplies no path scoping, and an empty prefix still lists every secret.
- Secrets: each secret generation now binds a fresh 32-byte random salt into
the HKDF context (
egide-secrets-v3:{path}:{version}:{salt}). HKDF is deterministic, so the previous context was fully determined by the path and the version number: purging a deleted secret reset its versions to 1, and the next write at that path re-derived the exact same encryption key as the purged data. Two unrelated secret versions therefore shared a key. The salt is a derivation nonce, not a secret, and is stored hex-encoded in clear on each row ofsecret_versions. All versions of one generation share that generation's salt; a purge followed by a new write at the same path draws a new one.
- A transit key declared
chacha20-poly1305under 0.1.0 remains readable: its existing ciphertexts still decrypt. Any operation that produces a new ciphertext or a new key version on such a key now fails withTransitError::KeyAlgorithmNotImplemented(HTTP409 Conflict), includingencrypt,encrypt_with_version,generate_datakey,rotate_key, andrewrapof a ciphertext that is not already at the key's latest version.rewrapof a ciphertext already at the latest version is a no-op and still succeeds, but this is not a migration path: the key stays declared under an algorithm this build does not implement, and running a rewrap sweep over it will report success without changing anything. Do not keep such a key in place: migrate by re-encrypting its data under a new key created with the defaultaes256-gcmtype. - Storage: no schema change and no migration. Both storage fixes are code-only.
- This release removes the version duplications caused by concurrent writers.
It does not change one nominal behaviour: a
deleteremoves thekv_storerow, so a later write at the same path restarts at version 1 and adds another(key, 1)row tokv_history. A recreated key can therefore hold repeated versions in its history by design, with or without concurrency. - Rows written by an earlier version under concurrent load may already carry
concurrency-induced duplicates in
kv_history. This release does not repair them. Auditing an installation that ran under concurrent writers is worthwhile. - Case sensitivity of
LIKEstill differs between backends: SQLite is case-insensitive for ASCII, PostgreSQL is case-sensitive. Escaping is now identical across backends; matching semantics are not. Do not rely on a prefix listing being case-exact. - Secrets: the upgrade is one-way for newly written secrets. Data written after the upgrade uses the v3 context and cannot be read by 0.1.0: rolling the binary back makes those secrets unreadable. Nothing is lost, since the salt stays stored on the row and upgrading again restores readability, but plan the rollback window accordingly.
- Secrets written before the upgrade remain readable in both directions.
First public release. Egide provides a self-hosted Secrets Manager and Transit encryption as a service, with a REST and gRPC server, native service tokens, Shamir-based seal/unseal, and a SQLite storage backend. The KMS and PKI engines are planned (see docs/explanation/roadmap.md); their crates ship as placeholders with error types only.
- Initial workspace structure and crate layout
- Core cryptographic primitives (
egide-crypto) - Secrets Engine (
egide-secrets): versioned key/value store with hierarchical paths and soft delete - Transit Engine (
egide-transit): encryption as a service, rewrap, datakey generation, key rotation - Seal and unseal with Shamir secret sharing (
egide-seal) - Storage abstraction (
egide-storage) with a SQLite backend; a PostgreSQL backend crate ships but is not yet selectable at server startup - REST and gRPC API layer (
egide-api) - Native service tokens and root token authentication (
egide-auth) - CLI client (
egide-cli) and server binary (egide-server) - Placeholder crates for the planned KMS and PKI engines (
egide-kms,egide-pki)
- Upgraded the RustCrypto digest family to the 0.11 generation (hmac 0.13, sha2 0.11, hkdf 0.13)
- Upgraded rand to 0.10; key, nonce and token generation now surface CSPRNG failures as
CryptoError::RandomGenerationFailedinstead of aborting - Upgraded sqlx to 0.9; dynamic SQL statements are explicitly marked with
AssertSqlSafeafter audit - Raised the MSRV to Rust 1.94, required by sqlx 0.9
- CLI: the CLI now authenticates with
Authorization: Beareras documented; previously it sent a header the server ignored, so authenticated CLI commands failed with 401.
- AES-256-GCM for symmetric encryption, HKDF-SHA256 for key derivation, OS CSPRNG for randomness, memory zeroization for key material
- Shamir secret sharing for master key protection
- Secrets engine: the AES-256-GCM key is derived per secret version
(HKDF domain
egide-secrets-v2:{path}:{version}) and the AEAD associated data bindspath:version, eliminating the GCM random-nonce birthday bound on rotated secrets and blocking cross-version ciphertext splicing - Seal: malformed unseal shares (non-ASCII, odd-length or non-hex input) are
rejected with
SealError::InvalidShareinstead of panicking on a misaligned UTF-8 boundary, which would abort the process in release builds and made unseal a denial-of-service vector - Seal: dev mode requires the explicit
EGIDE_UNSAFE_DEV_MODE=1environment marker and is categorically refused in release builds or whenEGIDE_ENV=productionis set, on both the initial activation and the restart-time auto-unseal of a dev-mode data directory - Seal: the master key reconstruction HMAC is compared in constant time via
subtle::ConstantTimeEqinstead of a short-circuiting byte comparison