Skip to content

How It Works

envsh uses a hybrid encryption scheme: AES-256-GCM for bulk data, with per-recipient key wrapping using SSH keys.

.env file (plaintext)
├─ 1. Generate random AES-256 key
├─ 2. Derive wrapping key via HKDF-SHA256
│ Salt: "envsh-v1" Info: "aes-key-wrap"
├─ 3. Encrypt plaintext with AES-256-GCM (random 12-byte nonce)
├─ 4. For each recipient (team member + machines):
│ ├─ Convert their Ed25519 public key → X25519
│ ├─ Generate ephemeral X25519 keypair
│ ├─ ECDH → shared secret
│ ├─ HKDF → wrapping key
│ └─ AES-GCM encrypt the AES key with wrapping key
├─ 5. Zero the AES key from memory
└─ 6. Upload: ciphertext + nonce + auth_tag + wrapped keys
Download bundle from server
├─ 1. Find our entry by SSH key fingerprint
├─ 2. Convert our Ed25519 private key → X25519
├─ 3. ECDH with ephemeral public key → shared secret
├─ 4. HKDF → wrapping key
├─ 5. Unwrap AES key
├─ 6. Decrypt ciphertext with AES-256-GCM
├─ 7. Verify SHA-256 checksum
└─ 8. Zero the AES key from memory

The server only ever sees:

  • Ciphertext — the encrypted blob
  • Nonce — random 12 bytes (public, used for AES-GCM)
  • Auth tag — GCM authentication tag
  • Wrapped keys — one per recipient, each encrypted with a different public key
  • Checksum — SHA-256 of the plaintext (for integrity verification after decryption)
  • Metadata — project, environment, version number, timestamp

It never sees the AES key, the plaintext, or anyone’s private key.

envsh uses your existing SSH keys. When you register a key, only the public key is sent to the server. Your private key never leaves your machine.

Supported key types:

  • Ed25519 (recommended)
  • RSA-4096 (fallback)

Machine identities use Ed25519 keypairs generated by envsh. The private key is printed once at creation and never stored on the server.

Machine format: envsh-machine-v1: + base64(Ed25519 private key)

Every push includes a base_version — the version you last pulled. If someone else pushed between your pull and push, the server rejects with a conflict error. This prevents silent overwrites.

Alice pulls v3
Bob pulls v3
Bob pushes v4 (base_version: 3) → success
Alice pushes (base_version: 3) → CONFLICT: v4 exists
Alice pulls v4, merges, pushes v5 (base_version: 4) → success