Skip to content

Latest commit

 

History

History
151 lines (105 loc) · 5.85 KB

File metadata and controls

151 lines (105 loc) · 5.85 KB

Security

README · Architecture · Security · Contributing

Cryptographic Design

zefer-cli uses the same cryptographic primitives as the zefer web app. The binary output is identical.

Symmetric Encryption: AES-256-GCM

  • Key size: 256 bits
  • Nonce (IV): 96 bits, randomly generated per block, per chunk
  • Auth tag: 128 bits (appended to ciphertext — same layout as Web Crypto API)
  • Implementation: Node.js crypto.createCipheriv('aes-256-gcm', key, iv)

AES-GCM provides both confidentiality and authenticity. Any modification to the ciphertext or associated data causes decryption to throw — the auth tag is verified before any plaintext is returned.

Key Derivation: PBKDF2-SHA256

  • Iterations: 300,000 / 600,000 / 1,000,000 (auto-benchmarked or user-selected)
  • Salt: 256 bits (random per encryption)
  • Output key: 256 bits
  • Implementation: Node.js crypto.pbkdf2 (async, runs in libuv thread pool)

The salt is stored in the file and is not secret. Its purpose is to prevent rainbow table attacks and ensure that two encryptions with the same passphrase produce different keys.

Answer Hashing: PBKDF2-SHA256

  • Iterations: 100,000
  • Salt: Deterministic — SHA-256("ZEFER_ANSWER_SALT:" + normalized_answer).slice(0, 16)
  • Normalization: answer.trim().toLowerCase()
  • Output: 256 bits, stored as base64 inside the encrypted payload

The deterministic salt means the hash is reproducible from the answer alone, without storing extra state. The hash is never visible without decrypting the file first.

Random Generation

  • crypto.randomBytes(n) — Node.js binding to OS-level CSPRNG (/dev/urandom on Linux/macOS, BCryptGenRandom on Windows)
  • Used for: salt (32 bytes), base IV (12 bytes), key generation

Chunk IV Derivation

Each 16 MB chunk has a unique IV derived from the base IV:

chunkIv = baseIv with last 4 bytes XOR'd with chunk index

This ensures nonce uniqueness across all chunks without generating a new random IV per chunk (which would require storing them all in the file header).

Threat Model

What zefer-cli protects against

Threat Protection
Passive file disclosure AES-256-GCM — key required to read any content
Passphrase brute force PBKDF2 with 300k–1M iterations slows enumeration
Ciphertext tampering GCM auth tag — any modification causes decryption failure
Two-party secret sharing Dual key mode — both passphrases required
Credential delegation Reveal key — share access without exposing main passphrase
Social engineering Secret question + PBKDF2-hashed answer
Exfiltration window TTL-based expiration inside encrypted payload
Automated brute force Max attempts counter at ~/.zefer/attempts.json
File enumeration All security metadata inside encrypted payload — invisible without key
Timing side-channels 100ms minimum response on wrong passphrase

What zefer-cli does NOT protect against

Limitation Reason
Passphrase in ps aux / shell history Using -p flag exposes it; use interactive prompt or env var
~/.zefer/attempts.json deletion Client-side deterrent only, not a hard guarantee
Clock manipulation to bypass TTL Expiration is checked against Date.now() on the decrypting machine
IP restriction bypass IP is compared from the decrypting machine; CLI has no external IP detection
Memory inspection during session Decrypted content exists in Node.js heap briefly before being written
Keyloggers or compromised OS Out of scope for any application-level tool
Quantum computing AES-256-GCM provides approximately 128-bit quantum security

Passphrase Security Recommendations

# Avoid: passphrase visible in process list
zefer encrypt file.txt -p "my-secret"

# Better: interactive prompt (never leaves memory as a string in shell)
zefer encrypt file.txt

# Better: environment variable (not visible in ps aux on most systems)
zefer encrypt file.txt -p "$ZEFER_PASS"

# Best: use a strong passphrase generated by zefer itself
zefer keygen -m secure -l 64

File Metadata Visibility

The public header (readable without passphrase) contains only:

{
  "iterations": 600000,
  "compression": "gzip",
  "hint": "optional public hint",
  "note": "optional public note",
  "mode": "file"
}

Everything sensitive is inside the AES-256-GCM encrypted payload:

  • File name and type
  • File size
  • Expiration timestamp
  • Secret question hash
  • IP allowlist
  • Max attempts

An attacker with only the .zefer file cannot determine whether any of these security features are enabled, or what values they contain.

Dual Key Security

When --dual-key and -2 are used, the two passphrases are combined as:

combinedKey = passphrase1 + "\x00ZEFER_DUAL\x00" + passphrase2

This combined string is then used as the single PBKDF2 input. The separator \x00ZEFER_DUAL\x00 prevents trivial collision attacks (e.g., "ab" + "c" vs "a" + "bc").

Reveal Key Security

The ZEFR3 format contains two independently encrypted blocks:

  • Main block: encrypted with the main passphrase (or dual key combination)
  • Reveal block: encrypted with the reveal key

Both blocks contain the same plaintext payload. The reveal key holder can decrypt the file but does not learn the main passphrase. The two blocks use independent random salts and IVs.

Backward Compatibility

zefer-cli supports decrypting legacy formats:

Format Support Notes
ZEFB3 Read + Write Current binary format
ZEFR3 Read + Write Current binary format with reveal key
ZEFER3 Read only Legacy text format (no new files written)
ZEFER2 Read only Legacy JSON format (no new files written)

New files are always written in ZEFB3 (single key) or ZEFR3 (with reveal key).