Metadata-Version: 2.4
Name: envarmor
Version: 0.1.0
Summary: Securely load and manage encrypted environment variables
Author-email: Isaac Harrison Gutekunst <isaac@supercortex.io>
Maintainer: felix5572
License-Expression: MIT
Project-URL: Homepage, https://github.com/felix5572/envarmor
Project-URL: Repository, https://github.com/felix5572/envarmor
Project-URL: Issues, https://github.com/felix5572/envarmor/issues
Project-URL: Changelog, https://github.com/felix5572/envarmor#changelog
Keywords: environment,encryption,security,dotenv,configuration
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=41.0.0
Requires-Dist: click>=8.0.0
Requires-Dist: python-dotenv<2.0.0,>=1.0.0
Provides-Extra: keyring
Requires-Dist: keyring>=24.0.0; extra == "keyring"
Dynamic: license-file

# 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

```bash
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:

```bash
pip install "envarmor[keyring]"
```

## Quick Start

### 1. Initialize a new encrypted environment file

```bash
# 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

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

### 3. Run commands with encrypted environment

```bash
# 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.

```bash
# 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:

```bash
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.

```bash
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:

```bash
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

```bash
# 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

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

### 4. Production/CI usage

```bash
# 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

```bash
# 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.

```bash
# 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:

```bash
# 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:

```bash
# 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
```bash
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
```bash
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
```bash
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
```bash
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
```bash
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
```bash
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
```bash
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
```bash
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
```bash
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
```bash
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
```bash
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
```bash
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
```bash
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
```bash
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)
```bash
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

```python
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

```python
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

```python
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

```python
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

```python
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.

```bash
# 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
```fish
# 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
```bash
# 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
```yaml
- 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
```bash
# 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:

```python
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
```bash
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
```bash
# 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
```bash
uv run --frozen black --check src tests
uv run --frozen ruff check src tests
uv run --frozen mypy src
```

See the
[release checklist](https://github.com/felix5572/envarmor/blob/master/RELEASING.md)
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](LICENSE).

## Project Lineage

EnvArmor is derived from
[`igutekunst/encrypted-env-loader`](https://github.com/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](https://github.com/felix5572/envarmor/blob/master/AUDIT.md),
[roadmap](https://github.com/felix5572/envarmor/blob/master/ROADMAP.md), and
[versioning policy](https://github.com/felix5572/envarmor/blob/master/VERSIONING.md)
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` → `$EDITOR` → `vi` 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
