xenvsync

Security Model

How xenvsync protects your secrets at rest and in transit.

Overview

xenvsync uses AES-256-GCM for vault encryption and X25519 ECDHfor team key exchange. All cryptography uses Go's standard crypto/aes, crypto/cipher, and golang.org/x/crypto/curve25519 packages. No third-party cryptography libraries are used.

Encryption Properties

AlgorithmAES-256-GCM (authenticated encryption with associated data)
Key size256 bits (32 bytes), generated via crypto/rand
Key encodingHex-encoded for safe storage in .xenvsync.key
Nonce size96 bits (12 bytes), fresh random nonce per encryption
Auth tag128 bits (16 bytes), appended by GCM — detects any tampering
Ciphertext layout[nonce (12 B) ‖ ciphertext ‖ GCM tag (16 B)]
V1 vault formatBase64 with header/footer markers, 76-char line wrapping
V2 vault formatJSON key slots + base64 ciphertext; per-member ephemeral X25519 key exchange
Key exchangeX25519 ECDH shared secret → SHA-256 → AES-GCM key wrapping (V2 team mode)

Key Management

Generation

Keys are generated using Go's crypto/rand.Reader, which sources entropy from the OS CSPRNG (/dev/urandom on Linux, CryptGenRandom on Windows).

Storage

The key is written to .xenvsync.key with file permissions 0600 (owner read/write only). Permissions are validated on every operation.

Isolation

The key is never embedded in the vault output, logged, or printed to stdout. It is automatically added to .gitignore during init.

Sharing (V2)

Each team member generates an X25519 keypair (keygen). The vault is encrypted per-member using ECDH key exchange with ephemeral keys — no shared symmetric key leaves any machine. Members are managed via team add/remove.

Nonce Handling

A fresh 12-byte random nonce is generated for every call to xenvsync push. This means encrypting the same .env file twice produces completely different ciphertext, which is important for:

  • Security — prevents ciphertext analysis across multiple versions
  • Git diffs— the entire vault changes on each push, so it's clear when secrets were updated
Note: The nonce is prepended to the ciphertext — you don't need to manage it separately. xenvsync handles this automatically.

Tamper Detection

GCM provides built-in authentication. If any byte of the vault file is modified (accidentally or maliciously), decryption fails with an authentication error. This protects against:

Corrupted vault files
Bit-flipping attacks
Truncated payloads

In-Memory Injection

The xenvsync run command decrypts the vault entirely in memory and passes secrets to the child process via its environment block. The plaintext is never written to a file. This reduces the attack surface compared to writing a .env file, which could be:

  • Read by other processes on the same machine
  • Persisted in filesystem snapshots or backups
  • Accidentally committed to Git

Passphrase Protection

Key files can optionally be encrypted with a passphrase using a key-encryption-key pattern:

KDFscrypt (N=32768, r=8, p=1)
Salt16 random bytes per encryption
CipherAES-256-GCM wrapping the raw key
Formatenc: prefix + hex(salt ‖ nonce ‖ ciphertext+tag)
Note: Enable with xenvsync init --passphrase. Set XENVSYNC_PASSPHRASE for automated workflows.

Memory Zeroing

Key material is zeroed in memory after use. Raw hex key data is cleared immediately after decoding, and symmetric keys are zeroed via defer after decryption operations complete. This reduces the window where secrets exist in process memory.

Key Rotation

The xenvsync rotate command re-encrypts the vault atomically:

  • V1 mode — generates a new symmetric key and re-encrypts (old key file is replaced only after encryption succeeds)
  • V2 mode — re-encrypts with fresh ephemeral keys for all roster members
  • --revoke — removes a member and rotates in one atomic step

Threat Model

ThreatMitigation
Secrets committed to Git.env added to .gitignore on init; vault is encrypted
Key file leaked0600 permissions; auto-gitignored; permission warnings on load
Vault file tamperedGCM authentication tag rejects modified ciphertext
Plaintext .env on diskUse `run` command for in-memory injection instead
Weak encryption key256-bit key from OS CSPRNG; validated on decode
Team member leavesrotate --revoke removes member and re-encrypts atomically
Key file stolenOptional passphrase protection (scrypt + AES-GCM)
Key in process memoryMemory zeroed after use via crypto.ZeroBytes

Reporting Vulnerabilities

If you discover a security vulnerability, please report it privately via GitHub Security Advisories rather than opening a public issue. We take all reports seriously and will respond promptly.