Transparent file encryption in git -- a Rust implementation compatible with git-crypt.
Gitveil lets you store sensitive files (API keys, credentials, private configs) alongside public code in a git repository. Files you mark for encryption are automatically encrypted when committed and decrypted when checked out. Everyone else uses git normally -- they just can't read the encrypted files without the key.
Gitveil hooks into git's clean/smudge filter mechanism:
- On
git add(clean filter): plaintext is encrypted before being stored in the repository - On
git checkout(smudge filter): encrypted blobs are decrypted into your working copy - On
git diff(textconv): encrypted files are decrypted on the fly for readable diffs
This is completely transparent -- after setup, you just use git as usual.
Gitveil is byte-compatible with git-crypt. Repositories encrypted with git-crypt can be unlocked with gitveil and vice versa. They use the same:
- Key file format (FORMAT_VERSION 2, TLV-encoded)
- Encrypted file format (
\0GITCRYPT\0header + HMAC-SHA1 nonce + AES-256-CTR ciphertext) - Git filter configuration (
filter=git-crypt,diff=git-crypt) .git-crypt/directory structure for GPG-encrypted keys
- AES-256-CTR for file encryption
- HMAC-SHA1 to derive a deterministic nonce from the file contents (so identical plaintext produces identical ciphertext -- required for git to detect unchanged files)
- 32-byte AES key + 64-byte HMAC key per key entry, generated from OS-level CSPRNG
- Key material is zeroized on drop via the
zeroizecrate
brew install lucatescari/gitveil/gitveilgit clone https://github.com/lucatescari/gitveil.git
cd gitveil
cargo build --release
cp target/release/gitveil /usr/local/bin/cd my-repo
gitveil initThis generates a symmetric key (stored in .git/git-crypt/keys/default) and configures git's clean/smudge/diff filters.
Create or edit .gitattributes in your repo root:
# Encrypt all .key files
*.key filter=git-crypt diff=git-crypt
# Encrypt a specific file
secrets.env filter=git-crypt diff=git-crypt
# Encrypt everything in a directory
config/private/** filter=git-crypt diff=git-crypt
Important: Add .gitattributes before adding the files you want encrypted. If a file was already committed in plaintext, gitveil cannot retroactively encrypt its history.
echo "API_KEY=sk-secret-123" > secrets.env
git add secrets.env
git commit -m "add secrets"The file is encrypted in the repository but appears as plaintext in your working copy.
Option A: Symmetric key (simpler, you handle secure transport)
gitveil export-key ~/gitveil-key
# Send the key file securely to your collaboratorYour collaborator then runs:
gitveil unlock ~/gitveil-keyOption B: GPG (key is encrypted to each user's GPG public key)
gitveil add-gpg-user collaborator@example.comYour collaborator then runs:
gitveil unlockGPG decrypts the symmetric key automatically using their private key.
gitveil lockThis removes the key from your machine and re-encrypts files in your working copy.
Generate a key and prepare the repository for encryption.
gitveil init [-k <key-name>]
| Option | Description |
|---|---|
-k, --key-name |
Use a named key instead of default |
Remove the key and re-encrypt files in the working copy.
gitveil lock [-k <key-name>] [-a] [-f]
| Option | Description |
|---|---|
-k, --key-name |
Lock a specific named key |
-a, --all |
Lock all keys |
-f, --force |
Force lock even with uncommitted changes |
Decrypt the repository.
gitveil unlock [<key-file>...]
Without arguments, attempts GPG-based unlock using keys in .git-crypt/. With key file arguments, uses symmetric key files.
If your GPG private key is passphrase-protected, gitveil will let gpg-agent invoke pinentry to prompt you (terminal or GUI), just like git-crypt. Only collaborator files matching a secret key in your local keyring are tried, so pinentry fires at most once.
Add a GPG user as a collaborator.
gitveil add-gpg-user [-k <key-name>] [-n] [--trusted] [--from <source>] [<GPG_USER_ID>]
| Option | Description |
|---|---|
-k, --key-name |
Use a specific named key |
-n, --no-commit |
Don't auto-commit the GPG-encrypted key |
--trusted |
Skip GPG Web of Trust verification |
--from <source> |
Import GPG key(s) from a file, directory, or git URL |
When called with no arguments and no --from, gitveil checks for a globally configured keyring directory (see gitveil config set-keyring). If configured, it scans the directory and shows an interactive picker.
If your team stores GPG public keys in a shared repository, you can import them directly:
# Import a single key file
gitveil add-gpg-user --from /path/to/keys/alice.asc
# Browse a directory and pick interactively
gitveil add-gpg-user --from /path/to/keys/
# Clone a git repo and pick from it
gitveil add-gpg-user --from git@github.com:company/gpg-keys.gitWhen pointing at a directory (or git URL), gitveil scans for .asc, .gpg, .pub, and .key files, shows a list of found keys (name, email, fingerprint), and lets you select one or more to add as collaborators.
Manage global gitveil configuration.
# Set a global GPG keyring directory
gitveil config set-keyring /path/to/team-keys
# Show current configuration
gitveil config show
# Remove the keyring setting
gitveil config unset-keyringWhen a keyring directory is configured, gitveil add-gpg-user (with no arguments and no --from) will automatically scan the keyring directory and present an interactive picker to select GPG keys. This is useful when your team stores GPG public keys in a shared folder or git repository.
The keyring directory has no special format -- it's just a folder containing GPG public key files (exported with gpg --export or gpg --armor --export). Files are matched by extension (.asc, .gpg, .pub, .key) and can be organized in subdirectories. Non-key files and symlinks are ignored.
team-keys/
├── engineering/
│ ├── alice.asc
│ └── bob.pub
├── design/
│ └── carol.gpg
└── README.md # ignored (not a key extension)
The keyring path is stored in ~/.config/gitveil/config (respects $XDG_CONFIG_HOME). The config file is created with 0600 permissions and the config directory with 0700 permissions.
Remove a GPG user's access.
gitveil rm-gpg-user [-k <key-name>] [-n] <GPG_USER_ID>
| Option | Description |
|---|---|
-k, --key-name |
Remove from a specific named key |
-n, --no-commit |
Don't auto-commit the removal |
Note: this only prevents future unlocks. For full revocation, rotate the key.
List GPG users who have access.
gitveil ls-gpg-users [-k <key-name>]
Shows each user's name, email, and fingerprint. Without -k, lists users for all keys.
Generate shell completions.
gitveil completions bash >> ~/.bashrc
gitveil completions zsh > ~/.zfunc/_gitveil
gitveil completions fish > ~/.config/fish/completions/gitveil.fishExport the symmetric key to a file.
gitveil export-key [-k <key-name>] [<output-file>]
Omit the output file to write to stdout.
Show the encryption status of files in the repository.
gitveil status [-e] [-u] [-a | --all] [-f | --fix]
| Option | Description |
|---|---|
| (none) | List files marked for encryption (the actionable set), tracked + untracked |
-e |
Show only files whose committed blob is encrypted |
-u |
Show only files marked for encryption whose blob is plaintext (the set needing re-encryption — pair with -f to fix) |
-a, --all |
Include files without the git-crypt filter too (verbose git-crypt-style listing) |
-f, --fix |
Re-stage tracked files whose committed blob is plaintext but should be encrypted (skips files deleted from the working tree; never auto-adds untracked files) |
By default, status is focused on files governed by a git-crypt filter — for a large repo this is the actionable subset. Tracked and untracked filter-matched files are shown. Use -a/--all for the full git-crypt-style listing that also includes non-filter files.
When a filter-marked file's committed blob is plaintext (typically because it was staged before .gitattributes took effect), *** WARNING *** is appended to its line and a summary at the end suggests gitveil status -f. Untracked filter-marked files appear with an (untracked) suffix to make it clear that the file on disk is still plaintext — it'll be encrypted on staging.
Works without gitveil init -- the command is informational and can be used to audit filter coverage before initializing. If filter-marked files were committed without init, status surfaces them as WARNINGs.
Performance: at most one git ls-files per category, one batched git check-attr, and one batched git cat-file regardless of repo size. On a Unity project with ~4,000 files it completes in ~130 ms vs ~65 seconds for git-crypt -- roughly 500x faster.
Gitveil supports multiple named keys, allowing you to share different files with different groups of people:
# Initialize a named key
gitveil init -k team-backend
# Use it in .gitattributes
# db-credentials.env filter=git-crypt-team-backend diff=git-crypt-team-backend
# Share with specific people
gitveil add-gpg-user -k team-backend backend-dev@company.com
# Export the named key
gitveil export-key -k team-backend ~/backend-key- File contents only. Gitveil cannot encrypt filenames, commit messages, branch names, or other git metadata.
- No history rewriting. If a file was committed in plaintext before adding the
filter=git-cryptattribute, the plaintext remains in git history. - Encrypted files are opaque blobs. Git cannot compute deltas on encrypted content, so storage efficiency is reduced for encrypted files.
- No key rotation or revocation. Removing a collaborator's GPG key does not re-encrypt with a new symmetric key. They still have the old key.
AES-256-CTR provides confidentiality but not integrity. An attacker with push access to the repository can flip bits in the ciphertext, which flips the corresponding bits in the plaintext. The HMAC-SHA1 is used only for deterministic nonce derivation, not for authentication. There is no tamper detection on decryption. This is inherited from git-crypt's design -- adding MAC verification would break compatibility.
Gitveil respects the gpg.program git config setting, which means the GPG binary is determined by local repository config. An attacker who can modify .git/config (e.g., via a malicious clone) could point this to an arbitrary program. This is the same trust model as git itself -- local config is trusted. Be cautious when running gitveil in repositories you did not create.
The clean filter must read the entire file into memory to compute the HMAC-SHA1 nonce before encryption can begin. Very large files (multi-GiB) may cause high memory usage.
src/
main.rs # Entry point + CLI dispatch
cli.rs # clap CLI definitions
config.rs # Global configuration (XDG keyring path)
constants.rs # Magic bytes, sizes, field IDs
error.rs # Error types
crypto/
aes_ctr.rs # AES-256-CTR encryption
hmac.rs # HMAC-SHA1 nonce derivation
random.rs # Secure random generation
key/
format.rs # TLV field serialization
entry.rs # Key entry (version + AES key + HMAC key)
key_file.rs # Multi-version key file container
filter/
clean.rs # Encrypt on git add
smudge.rs # Decrypt on git checkout
diff.rs # Decrypt for git diff
commands/
init.rs # Initialize repository encryption
lock.rs # Secure repository, remove keys
unlock.rs # Decrypt repository
status.rs # Show encryption status
export_key.rs # Export symmetric key
add_gpg_user.rs # Add GPG collaborator
config.rs # Global config management
rm_gpg_user.rs # Remove GPG collaborator
ls_gpg_users.rs # List GPG collaborators
git/
repo.rs # Repository inspection
config.rs # Git filter configuration
checkout.rs # Force checkout for lock/unlock
gpg/
operations.rs # GPG encrypt/decrypt via subprocess
import.rs # Import GPG keys from files/URLs
benchmark/ # Performance benchmarks (gitveil vs git-crypt)