py-ninja-vault: Recursive Directory Encryption with GnuPG

py-ninja-vault is a Python CLI tool that walks a directory tree and applies GnuPG operations to every file — encrypting, decrypting, signing, or verifying — while silently skipping hidden paths. It is designed for developers and system administrators who need to protect sensitive directories from a single command.

Architecture Overview

The project is structured around a single Python package (ninja_vault) and a CLI entry point (py-njv):

  • ninja_vault/__init__.py — Core library: file iteration, GPG wrappers, configuration helpers
  • bin/py-njv — CLI entry point, argument parsing, dispatches to the library
  • ~/.py_njvrc — Per-user configuration file holding the default GPG key ID
  • ~/.gnupg/gpg.conf — Standard GnuPG config used to resolve the default-key

The library delegates all cryptographic operations to the python-gnupg module, which is a thin wrapper around the system gpg binary. Progress bars are rendered via tqdm when available.

Installation

Clone the repository and install with pip:

git clone https://github.com/flasomm/ninja-vault.git
cd ninja-vault
pip install .

This installs the py-njv command globally and pulls in python-gnupg and tqdm as dependencies.

⚠️ GnuPG 2.x must be available in your PATH. GnuPG 1.x may work for some operations but is not officially supported.

Configuration

Create ~/.py_njvrc and set your key identifier:

[DEFAULT]
UseKey=YOUR_KEY_ID

Alternatively, pass -K <id> on the command line to override the config value, or use -D to read default-key from ~/.gnupg/gpg.conf.

The config file is only required when neither -K nor -D is provided.

Core Library API

All public functions follow the same pattern: validate the target directory, reload the GPG agent, prompt for the passphrase, then iterate over files with an optional progress bar.

encrypt_dir

Recursively encrypts every non-hidden file under dir_to_encrypt. Each file is replaced by a .gpg counterpart; the original is removed unless --no-delete is set.

from ninja_vault import encrypt_dir

encrypt_dir(
    dir_to_encrypt='/path/to/secrets',
    key_id='A1B2C3D4',          # or None to read from ~/.py_njvrc
    home_dir=None,               # defaults to ~
    delete_original=True,        # remove plaintext after encryption
    dry_run=False,               # set True to preview without writing
    verbose=False,
)

Hidden files and directories (names starting with .) are always skipped.

decrypt_dir

Mirrors encrypt_dir. It targets *.gpg files and writes the decrypted output alongside them, then removes the .gpg file unless --no-delete is active.

from ninja_vault import decrypt_dir

decrypt_dir(
    dir_to_decrypt='/path/to/secrets',
    key_id=None,
    home_dir=None,
    delete_original=True,
    dry_run=False,
    verbose=False,
)

sign_dir and verify_dir

sign_dir creates a detached .sig signature for every file. verify_dir then walks the same tree and verifies each .sig file, raising ValueError on failure.

from ninja_vault import sign_dir, verify_dir

sign_dir('/path/to/releases', key_id='A1B2C3D4')
verify_dir('/path/to/releases')

Signatures are detached — the original files are not modified.

Internal Helpers

A few noteworthy private utilities power the library:

def _is_hidden(path: str) -> bool:
    """Return True if any path component starts with '.'."""
    return any(part.startswith('.') for part in path.split(os.sep) if part)

def _sanitize_gpg_stderr(stderr: str) -> str:
    """Strip key identity lines from GPG output before printing."""
    ...

def _reload_agent() -> None:
    """Run gpgconf --reload gpg-agent; silently skipped if gpgconf is absent."""
    ...

_sanitize_gpg_stderr is a subtle security measure: it removes lines containing key identities (e.g. encrypted with … <email>) so that key metadata is not leaked to terminal logs.

CLI Usage

# Encrypt a directory
py-njv -e /path/to/secrets

# Decrypt a directory
py-njv -d /path/to/secrets

# Sign all files
py-njv --sign /path/to/releases

# Verify all signatures
py-njv --verify /path/to/releases

# Preview without writing (dry run)
py-njv -e /path/to/secrets --dry-run

# Keep source files after encryption
py-njv -e /path/to/secrets --no-delete

# Use a specific key inline
py-njv -e /path/to/secrets -K A1B2C3D4

# Use an alternate GPG home
py-njv -e /path/to/secrets -g /home/ci-user

Always run with --dry-run first on directories you have never encrypted before.

Security Notes

BehaviourDetail
Source file removalPerformed with a standard os.remove call — not a secure wipe
Hidden pathsSkipped unconditionally; no option to include them
Key identity leakFiltered from GPG stderr before printing
GPG agent reloadExecuted before every operation to flush cached credentials

Because file removal is not a secure wipe, sensitive data may be recoverable from disk after encryption. For high-security environments, combine py-ninja-vault with a tool like shred or operate on an encrypted filesystem.

Running the Tests

python -m pytest ninja_vault/tests/test_njv.py -v

Test fixtures live in ninja_vault/tests/ alongside the test module, so the suite is fully self-contained.


py-ninja-vault offers a pragmatic, dependency-light solution for bulk GPG operations. Whether you are archiving project secrets before committing to a shared repository or signing release artifacts in a CI pipeline, a single command is all it takes.

Have fun !