Skip to main content

Securely load and manage encrypted environment variables

Project description

EnvArmor

EnvArmor is an independently maintained Python package for managing encrypted environment variables. It loads sensitive configuration from encrypted .env files using Fernet encryption, with optional keyring-backed workflows for local development.

Features

  • Secure encryption using Fernet (AES 128 in CBC mode with HMAC-SHA256)
  • Keyring integration for secure local key storage (macOS Keychain, Windows Credential Manager, Linux Secret Service)
  • Profile support for multiple environments (dev, prod, test, etc.)
  • CLI tools for file management and subprocess execution
  • Python API with context managers and decorators
  • Deterministic dotenv loading with literal values by default
  • Shell integration for fish, bash, and zsh
  • Safe editing with backup and validation
  • CI/CD friendly with quiet modes and secret masking
  • Modern Python with type hints and proper packaging

Installation

pip install envarmor

The base installation supports explicit keys and environment-variable key sources without installing an OS keyring stack. Install the optional extra only on machines that use keyring-backed workflows:

pip install "envarmor[keyring]"

Quick Start

1. Initialize a new encrypted environment file

# Generate new encrypted file with random key
envarmor init
# Output: Encryption key: <generated 60-character key>
# Output: Set your encryption key: export ENVARMOR_KEY='<generated key>'

# Set the key in your shell
export ENVARMOR_KEY='<paste the generated key here>'

Angle-bracketed values in this README are placeholders, not usable keys. EnvArmor preserves the audited legacy key representation: generated keys are 60-character URL-safe strings and must be copied exactly.

2. Edit your environment variables

# Opens decrypted content using $VISUAL, then $EDITOR, then vi
envarmor edit

3. Run commands with encrypted environment

# Run any command with encrypted env loaded
envarmor run -- python app.py
envarmor run -- ./deploy.sh

Or use shell integration (advanced)

Prefer envarmor run whenever possible because it does not require eval. load and clear remain available for changing the current interactive shell: they accept only portable ASCII variable names ([A-Za-z_][A-Za-z0-9_]*) and fail without writing any commands to stdout if one name is invalid. Only evaluate output decrypted from a file and key you trust.

# Load into current shell (fish)
eval (envarmor load)

# Load into current shell (bash/zsh)  
eval $(envarmor load)

Keyring Integration

For local development, store encryption keys securely in your system keyring instead of environment variables:

pip install "envarmor[keyring]"

The envarmor keyring ... commands remain discoverable in a base installation, but fail with this installation command if the optional support is absent. Python key resolution treats an absent extra as an unavailable fallback source, so explicit and environment keys continue to work; direct keyring mutations raise KeyringError with the same installation hint. A broken installed keyring package is not treated as absent—its import failure remains visible.

Key resolution is deterministic: an explicit Python API argument wins, then the selected key environment source, then keyring. The default environment source checks ENVARMOR_KEY first and the old ENCRYPTED_ENV_KEY name second as a read-only legacy fallback. If both are set, ENVARMOR_KEY wins. EnvArmor does not copy, delete, or rewrite either variable. A custom --key-env NAME selects only NAME and does not silently consult either standard name. --no-keyring disables the final keyring fallback completely.

0.1.0 migration — environment key name: Existing deployments using only ENCRYPTED_ENV_KEY continue to work and status labels that source as legacy. New configuration should use ENVARMOR_KEY. Before removing the old variable, run envarmor status in the intended environment and confirm that it reports Key source: $ENVARMOR_KEY.

0.1.0 migration — key-source priority: Machines configured with both sources use the selected environment variable instead of keyring. Before deployment, run envarmor status (with the intended --profile and --key-env, if any) and verify the reported Key source.

1. Choose a stable keyring service

Set a non-secret, project-specific identifier in the shell before using keyring. Keep it stable across repository renames, moves, and fresh clones.

export ENVARMOR_KEYRING_SERVICE="my-project"
# Backend service name: envarmor:my-project

The identifier accepts 1–128 ASCII letters, digits, dots, underscores, and hyphens, and must start and end with a letter or digit. A reverse-DNS name or a unique project slug works well. This setting must be available before EnvArmor can decrypt a file, so place it in shell or process configuration rather than inside the encrypted dotenv file.

Existing installations may explicitly retain their old repository-named entry as a read-only fallback:

export ENVARMOR_KEYRING_SERVICE="my-project"
export ENVARMOR_LEGACY_KEYRING_SERVICE="old-repository-name"
envarmor status
# Key source: keyring (legacy service: old-repository-name, profile: default)

Only a confirmed miss in the stable service reaches this fallback. Backend failure aborts instead. set-key and delete-key target only the stable service; EnvArmor never copies, overwrites, or deletes the legacy entry. After deliberately copying or rotating the correct key into the stable service, remove ENVARMOR_LEGACY_KEYRING_SERVICE to finish the transition.

If ENVARMOR_KEYRING_SERVICE is absent, EnvArmor temporarily retains the old Git repository-name behavior for compatibility and reports it as a legacy service. New setups should not rely on that rename-sensitive mode.

2. Store key in keyring

# Generate and store key for default profile
envarmor keyring set-key

# Store key for specific environment
envarmor keyring set-key --profile dev
envarmor keyring set-key --profile prod

3. Use encrypted environment with keyring

# Automatically uses keyring-stored key
envarmor run --profile dev -- python app.py
envarmor load --profile dev
envarmor edit --profile dev

4. Production/CI usage

# Force environment variable usage (skip keyring)
envarmor run --profile prod --no-keyring -- ./deploy.sh
export ENVARMOR_KEY="your-key-here"
envarmor load --profile prod --no-keyring

5. Manage keyring keys

# Retrieve a key  
envarmor keyring get-key --profile dev

# Delete a key
envarmor keyring delete-key --profile dev

EnvArmor does not offer a profile-listing command. Python's portable keyring API can read a named entry but cannot reliably enumerate every username across backends. Keep the profile names in project configuration and query a known profile explicitly instead of treating a hard-coded probe as a complete list.

Key Benefits:

  • 🔐 Backend-aware: Key storage is delegated to the configured OS keyring backend and its security policy
  • 🚀 Convenient: No need to manage environment variables locally
  • 🔄 Flexible: Fallback to environment variables for production/CI
  • 📋 Profile-aware: Separate keys per environment automatically

Security Features

CI/CD Output Discipline

Commands intended for scripting provide quiet or machine-readable forms, but --quiet means reduced output—not “secret-free.” Commands whose primary result is a new or retrieved key still write that key to stdout. Capture those results only in a protected secret channel and never enable shell tracing around them.

# Capture a newly generated key without logging stdout
KEY=$(envarmor generate-key --quiet)

# Show names without values (names themselves may still be sensitive metadata)
envarmor show --names-only

# Validate files without output (exit codes only)
envarmor validate --quiet

Secret Masking

By default, commands mask sensitive values:

# Safe - shows masked values
envarmor show
# Output: DATABASE_URL=***
#         API_KEY=***

# Requires explicit flag to show values
envarmor show --show-values  # WARNING: exposes secrets

Examples and Testing

Run the comprehensive demo to see all features:

# Interactive demo with full output
./examples/demo.sh

# CI-friendly mode (no secrets exposed)
./examples/demo.sh --ci

# View usage examples
./examples/basic_usage.sh

CLI Reference

Core Operations

run - Execute commands with encrypted environment

envarmor run [--file FILE] [--profile PROFILE] [--no-keyring] [--interpolate] -- <command>

# Examples
envarmor run -- python app.py
envarmor run --profile prod -- ./deploy.sh
envarmor run --file .env.custom.encrypted -- npm start
envarmor run --profile dev --no-keyring -- python app.py  # force env var usage

load - Generate shell commands to load environment

envarmor load [--file FILE] [--profile PROFILE] [--shell SHELL] [--quiet] [--no-keyring] [--interpolate]

# Usage in fish shell
eval (envarmor load)
eval (envarmor load --profile dev)
eval (envarmor load --profile prod --no-keyring)  # force env var usage

# Usage in bash/zsh
eval $(envarmor load)
eval $(envarmor load --profile prod)

load validates the complete variable-name set before emitting anything, so an invalid name exits 1 without leaving a partial script on stdout. Prefer envarmor run for scripts and CI jobs that do not need to modify the caller's shell. Both commands keep ${NAME} text literal by default; add --interpolate only when the encrypted file deliberately depends on dotenv expansion and the caller's process environment.

clear - Generate shell commands to clear environment

envarmor clear [--file FILE] [--profile PROFILE] [--shell SHELL] [--quiet] [--no-keyring]

# Usage
eval (envarmor clear)  # fish
eval $(envarmor clear)  # bash/zsh

File Management

init - Create new encrypted environment file

envarmor init [--file FILE] [--profile PROFILE] [--key-file KEYFILE] [--quiet] [--force]

# Examples
envarmor init                           # creates .env.encrypted
envarmor init --profile dev             # creates .env.dev.encrypted
envarmor init --key-file .env.key      # saves key to file
envarmor init --quiet                   # CI-friendly (key only)

encrypt - Encrypt existing .env file

envarmor encrypt <source> [--output OUTPUT] [--profile PROFILE] [--quiet] [--force]

# Examples
envarmor encrypt .env                   # creates .env.encrypted
envarmor encrypt .env.dev --profile dev  # creates .env.dev.encrypted
envarmor encrypt .env --output custom.encrypted

decrypt - Decrypt to filesystem

envarmor decrypt [--file FILE] [--output OUTPUT] [--profile PROFILE] [--quiet] [--force]

# Examples
envarmor decrypt                        # decrypts to .env
envarmor decrypt --profile dev         # decrypts .env.dev.encrypted to .env
envarmor decrypt --output .env.backup  # custom output file

For init, encrypt, and decrypt, an existing destination requires an affirmative interactive confirmation. --quiet never grants overwrite permission: non-interactive jobs must add --force deliberately. For init, the same rule protects both the encrypted file and an explicit --key-file. When --key-file is used, its private atomic write is committed first, so a key-write failure cannot replace the ciphertext. The two files are not a joint filesystem transaction: a later ciphertext failure can leave the new key file beside the preserved old ciphertext. The Python API retains its documented programmatic replacement behavior.

The decrypt command writes authenticated plaintext bytes without interpreting dotenv syntax or expanding values. Parsing policy applies only when EnvArmor returns or loads a variable mapping.

edit - Safely edit encrypted files

envarmor edit [--file FILE] [--profile PROFILE] [--quiet] [--force]

# Opens in $VISUAL, then $EDITOR, then vi
# Creates an encrypted backup before committing changes
# Validates .env format before re-encrypting
envarmor edit --profile prod

Editor settings are parsed as shell-style argument lists, so common values such as VISUAL="code --wait" and quoted executable paths work. EnvArmor passes the temporary plaintext path as one final argument and does not invoke a shell. An explicitly present setting must be non-empty and syntactically valid; unset VISUAL to fall back to EDITOR. A missing executable or nonzero editor exit is a failed command: the original ciphertext remains unchanged and no new EnvArmor backup is committed. Malformed dotenv syntax is detected before the backup or ciphertext is replaced. Interactive use asks before preserving such content; --quiet rejects it unless --force explicitly authorizes the save.

Key Management

generate-key - Generate new encryption key

envarmor generate-key [--quiet]

# Interactive mode
envarmor generate-key
# Output: Generated key: <generated 60-character key>

# CI mode
envarmor generate-key --quiet
# Output: <generated 60-character key>

rekey - Change encryption key

envarmor rekey [--file FILE] [--profile PROFILE] [--old-key-env VAR] [--new-key-env VAR] [--quiet]

# Examples
envarmor rekey --old-key-env OLD_KEY --new-key-env NEW_KEY --no-keyring
envarmor rekey --old-key-env OLD_KEY --no-keyring --quiet
# The second form generates and prints a new key only after a successful commit.

Rekey operates on authenticated raw bytes, verifies the new ciphertext before atomically replacing the old file, and never reconstructs dotenv text. It does not automatically update keyring entries. If the old key is found in keyring, the command fails before modifying the file; provide the old key through the selected environment variable and use --no-keyring, then update keyring explicitly after the successful rotation.

Keyring Management

keyring set-key - Store encryption key in system keyring

envarmor keyring set-key [--profile PROFILE] [--key KEY | --key-env NAME] [--quiet]

# Examples
envarmor keyring set-key                    # generates key for default profile
envarmor keyring set-key --profile dev     # generates key for dev profile
envarmor keyring set-key --key-env ENVARMOR_KEY --profile dev
envarmor keyring set-key --quiet           # prints a generated key after storage

--key-env reads exactly the named variable and is the preferred way to move an existing key into keyring without placing the key text in shell history or process arguments. --key remains an explicit compatibility input; avoid it on shared machines. EnvArmor validates supplied keys before any backend write. Quiet mode does not echo a supplied key, but it does print a newly generated key after the backend confirms storage.

keyring get-key - Retrieve encryption key from keyring

envarmor keyring get-key [--profile PROFILE] [--quiet]

# Examples
envarmor keyring get-key                    # get default profile key
envarmor keyring get-key --profile dev     # get dev profile key
envarmor keyring get-key --quiet           # key only, no messages

keyring delete-key - Remove encryption key from keyring

envarmor keyring delete-key [--profile PROFILE] [--quiet] [--force]

# Examples
envarmor keyring delete-key                 # delete default profile key
envarmor keyring delete-key --profile dev  # delete dev profile key
envarmor keyring delete-key --profile dev --quiet --force  # automation

Keyring write and deletion failures exit nonzero and are distinct from a confirmed missing entry. A delete checks the writable primary service before mutating it; legacy fallback entries remain read-only. Interactive deletion requires confirmation. --quiet never grants deletion permission, so non-interactive deletion must also include --force.

Keyring Key Storage:

  • Stable service: envarmor:{ENVARMOR_KEYRING_SERVICE}
  • Username format: {profile}-key (e.g., dev-key, default-key)
  • Legacy compatibility: repository name only when no stable service is configured
  • Optional legacy fallback: exact service named by ENVARMOR_LEGACY_KEYRING_SERVICE, read-only

Utilities

status - Show file information and variables

envarmor status [--file FILE] [--profile PROFILE] [--quiet] [--no-keyring]

# Example output with keyring:
# File: .env.encrypted
# Exists: True
# Size: 1024 bytes
# Key source: keyring (service: envarmor:my-project, profile: default)
# Status: Valid (contains 5 variables)
# Variables:
#   - DATABASE_URL
#   - SECRET_KEY

# Example output with environment variable:
# File: .env.encrypted  
# Exists: True
# Size: 1024 bytes
# Key source: $ENVARMOR_KEY
# Status: Valid (contains 5 variables)
# Variables:
#   - DATABASE_URL
#   - SECRET_KEY

# With --no-keyring flag:
# Key source: $ENVARMOR_KEY or $ENCRYPTED_ENV_KEY (legacy) (not found)
# Status: Cannot validate (no key)

# Both verbose and quiet modes exit 0 only after successful decryption.
# A missing file/key, invalid data, or another expected EnvArmor error exits 1.
# Every quiet token is written to stdout; use the exit code to distinguish success.

# Quiet mode prints one machine-readable result:
# <count>     valid file; exit 0
# NOT_FOUND   encrypted file is missing; exit 1
# NO_KEY      neither the configured environment variable nor keyring has a key; exit 1
# INVALID     key is wrong or ciphertext is damaged; exit 1
# ERROR       keyring backend failure or another expected EnvArmor error; exit 1

validate - Check if file can be decrypted

envarmor validate [--file FILE] [--profile PROFILE] [--quiet] [--no-keyring]
# Exit code 0 if valid, 1 if invalid

Validation authenticates the ciphertext and key. It intentionally does not apply dotenv parsing policy, so malformed but recoverable plaintext can still be decrypted to a file or repaired with edit.

show - Display variables (with security options)

envarmor show [--file FILE] [--profile PROFILE] [--names-only] [--show-values] [--no-keyring] [--interpolate]

# Safe default (masks values)
envarmor show
# Output: DATABASE_URL=***

# CI-safe (names only)
envarmor show --names-only
# Output: DATABASE_URL
#         API_KEY

# Explicit flag required to show values
envarmor show --show-values  # WARNING: exposes secrets

Python API

Basic Usage

from envarmor import load_encrypted_env

# Load with default settings
# (.env.encrypted, ENVARMOR_KEY, legacy ENCRYPTED_ENV_KEY, then keyring)
env_vars = load_encrypted_env()

# Load with specific parameters
env_vars = load_encrypted_env(
    key="base64-encoded-key",
    file_path=".env.prod.encrypted",
    profile="prod",
    change_os_env=True,  # Updates os.environ
    use_keyring=True     # Allow keyring fallback (default: True)
)

# Force environment variable usage (skip keyring)
env_vars = load_encrypted_env(
    profile="prod",
    use_keyring=False
)

# Explicit compatibility mode for files that intentionally use ${NAME}
env_vars = load_encrypted_env(interpolate=True)

Dotenv Parsing and Interpolation

EnvArmor decodes loaded dotenv content as UTF-8 and rejects parser-reported invalid lines instead of silently returning a partial mapping. A variable with no assignment, such as NAME, remains accepted but is omitted from the loaded mapping; NAME= remains an empty string.

Values are literal by default. For example, URL=https://${HOST} loads with ${HOST} unchanged, so an unrelated value in the caller's environment cannot silently rewrite encrypted configuration. Legacy expansion remains available through --interpolate on run, load, and show, or interpolate=True on decrypt_env_file, load_encrypted_env, encrypted_env_context, and with_encrypted_env. Opt-in expansion follows python-dotenv semantics and can read earlier dotenv values and the process environment, so use it only for trusted files with that explicit dependency.

This policy affects parsed mappings only. Encryption, rekey, filesystem decryption, and edit all preserve authenticated plaintext bytes. Existing v0.2.2 files remain decryptable byte-for-byte; callers that relied on expanded return values must opt in after upgrading to 0.1.0.

Context Manager

from envarmor import encrypted_env_context
import os

with encrypted_env_context(profile="dev"):
    # Environment variables loaded here (uses keyring automatically)
    database_url = os.getenv("DATABASE_URL")
    secret_key = os.getenv("SECRET_KEY")
# Variables loaded by EnvArmor are restored when exiting context

# Force environment variable usage  
with encrypted_env_context(profile="prod", use_keyring=False):
    # Uses ENVARMOR_KEY, with legacy ENCRYPTED_ENV_KEY fallback
    pass

# Expansion is explicit here too.
with encrypted_env_context(profile="dev", interpolate=True):
    pass

Context cleanup records only the variable names loaded from the encrypted file. It restores their previous values, or removes them if they were previously absent, while preserving additions, modifications, and deletions to unrelated environment variables. Nested contexts restore in stack order, including after exceptions. Because os.environ is process-global, this is not thread isolation: concurrent writes to the same loaded variable can still race.

Decorator

from envarmor import with_encrypted_env
import os

@with_encrypted_env(profile="prod")
def deploy_application():
    # Function runs with encrypted env loaded (selected env var, then keyring)
    api_key = os.getenv("API_KEY")
    database_url = os.getenv("DATABASE_URL")
    # Environment restored after function returns

@with_encrypted_env(profile="prod", use_keyring=False)
def ci_deploy():
    # Force environment variable usage for CI/production
    pass

deploy_application()

Keyring Functions

import os

from envarmor import (
    KeyringError,
    generate_key,
    get_key_from_keyring,
    set_key_in_keyring,
    delete_key_from_keyring,
)

# Configure a stable, non-secret namespace before keyring operations.
os.environ["ENVARMOR_KEYRING_SERVICE"] = "my-project"

# Store a generated key in keyring
key = generate_key()
set_key_in_keyring(key, profile="dev")

# Retrieve key from keyring  
stored_key = get_key_from_keyring(profile="dev")

# Delete returns False only when the backend confirms the entry is absent.
deleted = delete_key_from_keyring(profile="dev")

# Backend failures from all three operations raise KeyringError.

Utility Functions

from envarmor import (
    generate_key,
    encrypt_env_file,
    decrypt_env_file,
    validate_encrypted_file
)

# Generate encryption key
key = generate_key()

# Encrypt a file (supports keyring)
encrypt_env_file(".env", ".env.encrypted", key)
encrypt_env_file(".env", ".env.encrypted", profile="dev")  # uses keyring
encrypt_env_file(".env", ".env.encrypted", profile="dev", use_keyring=False)  # force env var

# Decrypt and get variables (supports keyring)
env_vars = decrypt_env_file(".env.encrypted", key=key)
env_vars = decrypt_env_file(".env.encrypted", profile="dev")  # uses keyring
env_vars = decrypt_env_file(".env.encrypted", profile="dev", use_keyring=False)  # force env var
env_vars = decrypt_env_file(".env.encrypted", key=key, interpolate=True)  # legacy expansion

# Validate file (supports keyring)
is_valid = validate_encrypted_file(".env.encrypted", key=key)
is_valid = validate_encrypted_file(".env.encrypted", profile="dev")  # uses keyring
is_valid = validate_encrypted_file(".env.encrypted", profile="dev", use_keyring=False)  # force env var

Profiles

Profiles allow managing multiple environment configurations:

Profile names are identifiers, not paths. EnvArmor accepts 1–64 ASCII letters, digits, dots, underscores, or hyphens. A profile must start and end with a letter or digit, and dots cannot be consecutive. Empty values, path separators, whitespace, control characters, and Unicode are rejected before file or keyring access. The same validation applies when an explicit --file, --output, or Python API key is supplied, so those inputs cannot bypass the namespace boundary. default remains the default profile name.

# Profile-based file naming
.env.encrypted          # default profile
.env.dev.encrypted      # dev profile  
.env.prod.encrypted     # prod profile
.env.test.encrypted     # test profile

# Usage
envarmor init --profile dev
envarmor run --profile prod -- python app.py
envarmor edit --profile test

Shell Integration

Use shell integration only when the current interactive shell must change. For automation, prefer envarmor run; eval remains an execution boundary even though EnvArmor validates names and quotes values before emitting commands.

Fish Shell

# Load environment
eval (envarmor load --profile dev)

# Clear environment  
eval (envarmor clear --profile dev)

# One-liner with auto-clear
envarmor run --profile dev -- python app.py

Bash/Zsh

# Load environment
eval $(envarmor load --profile dev)

# Clear environment
eval $(envarmor clear --profile dev)

# Use in loops (eval once, use many times)
eval $(envarmor load)
for i in {1..100}; do
    curl -H "Authorization: $SECRET_TOKEN" api.example.com/data/$i
done
eval $(envarmor clear)

CI/CD Integration

GitHub Actions Example

- name: Setup encrypted environment
  env:
    ENVARMOR_KEY: ${{ secrets.ENVARMOR_KEY }}
  run: |
    # Validate encrypted file
    envarmor validate --quiet
    
    # Run tests with encrypted environment
    envarmor run -- pytest

Security Best Practices

# Avoid plaintext values in CI logs
envarmor show --names-only          # Values hidden; names remain visible
envarmor validate --quiet           # No key or plaintext output

# Treat these outputs as sensitive
envarmor generate-key --quiet       # Prints a newly generated key
envarmor keyring get-key --quiet    # Prints the stored key
envarmor show --show-values         # Prints decrypted values
envarmor status                     # Prints paths and variable names

Security Considerations

  • Key Storage: Never commit encryption keys to version control; keyring security depends on the selected backend and host policy
  • Key Rotation: Rekey preserves authenticated raw bytes and verifies the new ciphertext before atomic replacement; still keep an independent backup of both ciphertext and keys, and remember that keyring entries are not updated automatically
  • Plaintext Lifetime: Normal load, clear, run, show, and status operations parse decrypted data in memory; validate authenticates it without dotenv parsing; decrypt (including its default .env output) and edit intentionally place plaintext on disk
  • External Editor Boundary: The edit plaintext file is mode 0600, but an external editor may create swap, recovery, or backup files under its own policy; configure the editor to protect or disable those artifacts. $VISUAL and $EDITOR are trusted executable configuration—even though EnvArmor never adds an implicit shell, explicitly selecting a shell still executes it
  • Atomic Writes: File output is flushed and synced in a private same-directory temporary file before os.replace; failures before replacement preserve the previous regular file
  • Crash Residue: An uncatchable termination can leave a hidden mode-0600 temporary file, and parent-directory fsync is not yet guaranteed; verify the target before removing any residue after a crash
  • File Permissions: Key files, explicit plaintext output, and edit plaintext temporary files are forced to mode 0600; new ciphertext also starts at 0600, while replacing ciphertext preserves its existing permission bits
  • Output Types: Write targets must be absent or regular files; symbolic links and other non-regular targets are rejected
  • Destructive Authorization: --quiet only changes output; existing destinations and keyring deletion require confirmation or an explicit --force
  • Multi-output Init: init --key-file commits the key first so key-write failure preserves old ciphertext, but the key file and ciphertext do not have cross-file atomicity
  • Backup Strategy: Keep secure backups of both encrypted files and keys
  • Environment Isolation: Use profiles to separate dev/staging/prod secrets
  • Deterministic Values: ${NAME} remains literal unless interpolation is explicitly enabled; opt-in interpolation may read the caller's process environment
  • CI/CD Output: --names-only hides values but not names; key-producing quiet commands still emit key material and must not be logged

Error Handling

The package provides specific exception types:

from envarmor import (
    DecryptionError,
    DotenvParseError,
    EncryptedEnvError,
    EncryptionKeyError,
    KeyringError,
    load_encrypted_env,
)

try:
    load_encrypted_env()
except EncryptionKeyError:
    print("Encryption key missing or invalid")
except DecryptionError:
    print("File cannot be decrypted - wrong key or corrupted data")
except DotenvParseError:
    print("Authenticated plaintext contains invalid dotenv syntax")
except KeyringError:
    print("Keyring support is unavailable or its backend failed")
except EncryptedEnvError:
    print("General error with encrypted environment operations")

EncryptionKeyError, DecryptionError, and KeyringError derive from EncryptedEnvError, so catch the specific types first. Expected decoding, authentication, keyring, text, and filesystem failures retain their original exception as __cause__. The old envarmor.core.KeyError name remains a deprecated compatibility alias for the transition; new code should not import it or confuse it with Python's built-in KeyError.

Development

Setup

git clone https://github.com/felix5572/envarmor
cd envarmor
uv sync --frozen --group dev

Development and release tools use standard dependency groups and the committed uv.lock; they are not published as package extras. The development group includes keyring support so the complete mocked test suite can exercise both base and keyring-backed behavior. Runtime users continue to install with pip. Consumer deployments should pin the selected EnvArmor version and generate their own platform-specific lock or constraints file.

Testing

# Run unit tests
uv run --frozen pytest

# Run full demo/integration tests
./examples/demo.sh

# Run CI-safe tests
./examples/demo.sh --ci

# Test coverage
uv run --frozen pytest --cov=envarmor

Code Quality

uv run --frozen black --check src tests
uv run --frozen ruff check src tests
uv run --frozen mypy src

See the release checklist for the complete source, artifact, clean-wheel, credential, tagging, and maintainer-approved trusted-publishing procedure.

License

EnvArmor is distributed under the SPDX MIT license expression. The complete license text is included in source and wheel distributions; see LICENSE.

Project Lineage

EnvArmor is derived from igutekunst/encrypted-env-loader at Git tag v0.2.2 under the MIT License. The original copyright and Git history are retained. The uncommitted upstream PyPI 0.3.0 source was not merged; EnvArmor changes are implemented independently from the audited Git baseline. See the repository's audit, roadmap, and versioning policy for audit, implementation, and release details.

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

Changelog

0.1.0

  • Established EnvArmor as the independent envarmor distribution, import package, and CLI while retaining the upstream MIT attribution and auditable history
  • Preserved encrypted-env-loader v0.2.2 ciphertext, double-Base64 keys, filenames, raw plaintext bytes, and the legacy ENCRYPTED_ENV_KEY read path
  • Made shell output, overwrite authorization, file permissions, atomic writes, transactional rekey, profile handling, and editor execution fail closed under the audited threat model
  • Separated authenticated byte operations from strict dotenv parsing; values are literal by default and legacy interpolation requires explicit opt-in
  • Defined deterministic key-source priority, stable keyring services with explicit legacy fallback, and an optional envarmor[keyring] installation
  • Added precise public exceptions, scoped environment restoration, source and wheel metadata guards, and compatibility tests against the audited legacy package
  • Verified source, sdist, base wheel, keyring wheel, Python 3.9, and minimum-dependency installations through the credential-conscious, maintainer-controlled release checklist
  • Required an explicit local-package reinstall during release setup so cached editable metadata cannot retain a preceding development version
  • Added a least-privilege PyPI Trusted Publisher that builds only annotated version-matching tags, skips publication for internal .devN tags, pins every Action by commit SHA, separates verification from OIDC publication, and waits for the protected pypi environment

Migration highlights: install and invoke envarmor, import from envarmor, prefer ENVARMOR_KEY, add --interpolate only where legacy expansion is required, add --force to intentional non-interactive overwrites, and install envarmor[keyring] only on machines that use the credential-store workflow.

0.1.0.dev21

  • Moved contributor and release tools from a published dev extra into standard dependency groups and added a cross-Python uv.lock
  • Replaced the stale isort/Flake8 commands with the locked Black 25.1.0, Ruff, and mypy gates actually enforced by the repository
  • Completed the remaining type-checking contract without changing runtime behavior
  • Added installed-wheel checks for the sole keyring extra and public project URLs
  • Made the CI demo explicitly independent of the host keyring backend
  • Completed the source archive with public compatibility fixtures, examples, maintenance documents, and the maintenance lock
  • Added the manual, credential-conscious release checklist and recorded the direct-to-0.1.0 release decision

0.1.0.dev20

  • Validated key material before any keyring write so unusable credentials cannot be stored as successful EnvArmor keys
  • Added exact --key-env NAME ingestion for moving existing keys without placing secret text in process arguments
  • Stopped quiet keyring writes from echoing caller-supplied keys while retaining post-success output for newly generated keys
  • Required explicit --force for non-interactive keyring deletion so --quiet never acts as destructive authorization
  • Replaced token-shaped documentation placeholders and clarified that quiet output can still contain key material

0.1.0.dev19

  • Applied Black consistently across every Python source and test file, clearing the inherited five-file formatting baseline
  • Removed the final two redundant literal f-string prefixes so the complete src and tests trees pass Ruff
  • Kept the batch behavior-neutral; normalized AST comparison confirms that code changes are limited to formatting, docstring whitespace, and the equivalent literal-string cleanup

0.1.0.dev18

  • Replaced the deprecated license table with the PEP 639 SPDX expression MIT (AUD-014)
  • Declared LICENSE explicitly so build backends include and identify the complete license text in source and wheel distributions
  • Removed the deprecated license classifier and raised the setuptools build requirement to the first fully supported PEP 639 release
  • Added installed-wheel regression coverage for License-Expression, License-File, classifier removal, and the packaged license artifact
  • Eliminated the license metadata deprecation warnings from isolated builds

0.1.0.dev17

  • Moved keyring out of the base dependency closure and made envarmor[keyring] the explicit installation path for keyring-backed workflows (AUD-007, AUD-014)
  • Kept explicit-key and environment-variable workflows available in the base installation without importing an OS keyring stack
  • Added actionable Python API and CLI diagnostics when a keyring mutation is requested without the optional extra
  • Distinguished a genuinely absent extra from a broken installed keyring package, whose transitive import failure remains visible
  • Added wheel metadata regression coverage so keyring cannot silently become an unconditional dependency again
  • Kept the development extra complete by including keyring support for the full backend-mocking test suite

0.1.0.dev16

  • Made dotenv values literal by default so ambient process variables cannot silently rewrite encrypted configuration (AUD-009)
  • Added explicit interpolation compatibility through CLI --interpolate and Python interpolate=True loading paths
  • Rejected parser-reported invalid dotenv lines with public DotenvParseError instead of accepting partial mappings
  • Bounded the supported python-dotenv parser major while verifying both the declared 1.0.0 minimum and current 1.x behavior
  • Separated raw CLI decryption and cryptographic validation from dotenv parsing so malformed plaintext can still be recovered and repaired
  • Removed the last --quiet implicit-consent path in edit; malformed saves now require an interactive confirmation or explicit --force

0.1.0.dev15

  • Added conventional $VISUAL$EDITORvi resolution with quoted, multi-argument editor commands (AUD-012)
  • Parsed editor configuration into an argv list without an implicit shell and rejected empty or malformed settings without echoing their contents
  • Made editor startup failures and nonzero exits return failure while preserving the original ciphertext and cleaning the mode-0600 plaintext temporary file
  • Delayed the encrypted .backup commit until edited plaintext is ready, and moved it onto the atomic writer so symlinks and non-regular backup targets are rejected
  • Documented that external editor swap, recovery, and backup artifacts remain outside EnvArmor's permission guarantees

0.1.0.dev14

  • Added one profile identifier grammar for file paths, key resolution, and keyring usernames (AUD-012)
  • Rejected empty, path-like, control-character, Unicode, consecutive-dot, and overlong profiles before file or credential backend access
  • Kept None and the explicit default profile compatible while defining deterministic 1–64 character ASCII identifiers
  • Made explicit files, outputs, and keys obey the same profile boundary and prevented failed CLI validation from prompting, generating keys, or echoing hostile input

0.1.0.dev13

  • Reworked encrypted_env_context to restore only environment variables it actually overlays (AUD-011)
  • Preserved unrelated additions, modifications, and deletions across both normal and exceptional context exits
  • Froze each context's cleanup scope before yielding its mutable result dictionary, preventing caller mutation from changing restoration targets
  • Verified nested contexts restore overlapping variables in stack order and documented that process-global same-name writes can still race
  • Promoted the final strict xfail into an ordinary passing regression test

0.1.0.dev12

  • Made ENVARMOR_KEY the preferred default key environment variable while retaining ENCRYPTED_ENV_KEY as a read-only legacy fallback
  • Defined default priority as explicit key → ENVARMOR_KEY → legacy ENCRYPTED_ENV_KEY → keyring, with exact lookup for a custom --key-env
  • Made status report the actual preferred or legacy variable and list both ordered defaults when neither is present
  • Applied the same migration lookup to default rekey operations without rewriting or deleting either environment variable
  • Escaped custom environment source names before diagnostic display so terminal control characters remain inert

0.1.0.dev11

  • Made keyring writes and deletions fail closed with public KeyringError diagnostics and retained backend exception chains (AUD-007)
  • Kept successful mutation return values compatible while reserving False deletion results for a backend-confirmed missing entry
  • Delayed generated-key output until storage succeeds, so failed writes do not expose an unstored key
  • Removed the misleading list-keys command and list_keyring_profiles API because portable keyring backends do not support complete profile enumeration
  • Narrowed keyring management CLI catches to expected EnvArmor errors so unexpected programming faults remain visible

0.1.0.dev10

  • Added explicit stable keyring namespaces through ENVARMOR_KEYRING_SERVICE, stored as envarmor:{service-id} (AUD-007)
  • Preserved repository-named keyring access as compatibility mode and added an explicit ENVARMOR_LEGACY_KEYRING_SERVICE read-only fallback
  • Made stable lookup win on hit, legacy lookup occur only after a confirmed miss, and every backend failure remain fail-closed
  • Kept set, delete, and profile probing confined to the writable primary service; no legacy key is copied, overwritten, or deleted automatically
  • Extended non-secret source metadata so status identifies the actual stable or legacy service used

0.1.0.dev9

  • Defined key resolution priority as explicit argument → selected environment variable → keyring, so stale local entries cannot mask an intentional override (AUD-007)
  • Added non-secret source metadata for resolved keys and made status report the source it actually used
  • Added and exported KeyringError; confirmed misses remain ordinary no-key results while backend failures retain their cause and produce status ERROR
  • Unified ordinary and transactional rekey reads on one fail-closed keyring lookup path
  • Added an upgrade check for machines with both key sources and retained actionable --no-keyring guidance when rekey cannot query its backend

0.1.0.dev8

  • Added and exported EncryptionKeyError without shadowing Python's built-in KeyError (AUD-010)
  • Retained envarmor.core.KeyError as a warning-emitting compatibility alias for existing imports
  • Preserved decoder, Fernet authentication, Unicode, and filesystem root causes through explicit exception chaining
  • Stopped masking unexpected encryption and dotenv parser bugs as generic file failures
  • Simplified CLI handling to catch the package exception base while retaining specific public subclasses

0.1.0.dev7

  • Separated quiet output from overwrite authorization for init, encrypt, and decrypt (AUD-012 overwrite subset)
  • Added interactive confirmation for existing destinations and an explicit --force path for automation
  • Protected both init outputs before key generation, so refusing an existing key file leaves the encrypted destination untouched
  • Ordered init --key-file commits so key-write failure cannot replace the old ciphertext, while documenting the remaining cross-file crash boundary
  • Retained atomic replacement, private plaintext/key modes, and core API compatibility after authorization

0.1.0.dev6

  • Reworked rekey as authenticated raw-byte rotation without dotenv reconstruction (AUD-002)
  • Verified new ciphertext against the original plaintext bytes before atomic replacement
  • Delayed generated-key output until the file transaction succeeds and kept the old ciphertext on every tested failure path
  • Made rekey keyring lookup fail closed: backend failures abort, confirmed misses may fall back to the environment, and keyring-sourced rotation is rejected before modification pending an explicit cross-store design (AUD-007)

0.1.0.dev5

  • Added flushed, file-synced same-directory temporary writes followed by atomic replacement (AUD-003)
  • Preserved existing targets under injected fsync and replace failures and removed abandoned temporary files
  • Forced key and plaintext output to 0600, including replacements and editor plaintext (AUD-004)
  • Defined ciphertext permission and output-target policies: new files start private, existing modes are retained, and symlinks or other non-regular files are rejected

0.1.0.dev4

  • Split raw byte encryption/decryption from dotenv parsing (AUD-005, AUD-009)
  • Kept ordinary load, clear, run, show, status, and validate plaintext parsing in memory
  • Preserved legacy ciphertext, key representation, interpolation, and None filtering behavior

0.1.0.dev3

  • Rejected non-portable variable names before generating load or clear shell code (AUD-001)
  • Guaranteed that invalid names fail without partial commands on stdout
  • Added distinct literal-value quoting contracts for Bash/Zsh and Fish

0.1.0.dev2

  • Fixed the edit and status Click parameter mismatch (AUD-006)
  • Added defined status --quiet output and failure exit codes
  • Standardized every quiet status token on stdout and documented verbose failure exits
  • Fixed status fallback from a missing keyring entry to the configured key environment variable
  • Kept profile selection explicit instead of adopting PyPI 0.3.0 ENVIRONMENT inference

0.1.0.dev1

  • Added fixed legacy key, ciphertext, and complex dotenv compatibility fixtures
  • Added optional interoperability tests against audited legacy distributions
  • Added CLI, keyring, environment isolation, and file behavior test guards
  • Recorded known audit failures as strict expected-failure specifications

0.1.0.dev0

  • Renamed the distribution, import package, and CLI to EnvArmor / envarmor
  • Established an independently maintained baseline from upstream Git v0.2.2

Upstream 0.2.0

  • Keyring integration for secure local key storage
  • Automatic git repo detection for keyring service naming
  • Key resolution priority: explicit key → keyring → environment variable
  • New CLI command group: keyring with set-key, get-key, delete-key, list-keys
  • Enhanced commands: All file operations support --no-keyring flag
  • Improved status reporting: Shows key source (keyring vs environment variable)
  • Python API extensions: All functions support use_keyring parameter
  • Production/CI compatibility: Graceful fallback to environment variables

Upstream 0.1.0

  • Initial release
  • Basic encryption/decryption functionality
  • CLI with all core commands
  • Python API with context managers and decorators
  • Profile support
  • Shell integration
  • CI/CD safety features with quiet modes and secret masking

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

envarmor-0.1.0.tar.gz (220.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

envarmor-0.1.0-py3-none-any.whl (32.1 kB view details)

Uploaded Python 3

File details

Details for the file envarmor-0.1.0.tar.gz.

File metadata

  • Download URL: envarmor-0.1.0.tar.gz
  • Upload date:
  • Size: 220.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for envarmor-0.1.0.tar.gz
Algorithm Hash digest
SHA256 75a1bae3d3cbd076d03b00e33c7f8097a250a378ac1e0ec92c515810dde33a65
MD5 bffd6cce93301f20c63d1fe64b65bfbd
BLAKE2b-256 22922bab1335c1455adac45b448f90f9ff875826bc832aa4f1a456d7e5da0fc0

See more details on using hashes here.

Provenance

The following attestation bundles were made for envarmor-0.1.0.tar.gz:

Publisher: release.yml on felix5572/envarmor

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file envarmor-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: envarmor-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 32.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for envarmor-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 caaa540588878c3f2974007e260634451c25fd90e1d08eedcf1318fa43bb0ee8
MD5 35bc904b8edd9f73c7bce2820f44bb77
BLAKE2b-256 de2e1ea171abe579bc36d1376aba4f5bd9abbbeebcfd2e31c497e69518e85ef5

See more details on using hashes here.

Provenance

The following attestation bundles were made for envarmor-0.1.0-py3-none-any.whl:

Publisher: release.yml on felix5572/envarmor

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page