Skip to content

API Reference

envseal - Encrypt sensitive values in environment files using AES-GCM

EnvSealError

Bases: Exception

Base exception for EnvSeal operations

Source code in src/envseal/core.py
37
38
39
40
class EnvSealError(Exception):
    """Base exception for EnvSeal operations"""

    pass

PassphraseSource

Bases: Enum

Available sources for passphrases

Source code in src/envseal/core.py
43
44
45
46
47
48
49
50
class PassphraseSource(Enum):
    """Available sources for passphrases"""

    KEYRING = "keyring"
    HARDCODED = "hardcoded"
    ENV_VAR = "env_var"
    DOTENV = "dotenv"
    PROMPT = "prompt"

apply_sealed_env

apply_sealed_env(dotenv_path=None, passphrase_source=PassphraseSource.KEYRING, override=False, **passphrase_kwargs)

Load sealed environment variables and apply them to os.environ.

Parameters:

Name Type Description Default
dotenv_path Optional[Union[str, Path]]

Path to .env file

None
passphrase_source PassphraseSource

Source for the decryption passphrase

KEYRING
override bool

Whether to override existing environment variables

False
**passphrase_kwargs Any

Additional arguments passed to get_passphrase()

{}
Source code in src/envseal/core.py
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
def apply_sealed_env(
    dotenv_path: Optional[Union[str, Path]] = None,
    passphrase_source: PassphraseSource = PassphraseSource.KEYRING,
    override: bool = False,
    **passphrase_kwargs: Any,
) -> None:
    """
    Load sealed environment variables and apply them to os.environ.

    Args:
        dotenv_path: Path to .env file
        passphrase_source: Source for the decryption passphrase
        override: Whether to override existing environment variables
        **passphrase_kwargs: Additional arguments passed to get_passphrase()
    """
    env_vars = load_sealed_env(
        dotenv_path=dotenv_path,
        passphrase_source=passphrase_source,
        **passphrase_kwargs,
    )

    for key, value in env_vars.items():
        if key is not None and value is not None:
            if override or key not in os.environ:
                os.environ[key] = value

get_passphrase

get_passphrase(source=PassphraseSource.KEYRING, hardcoded_passphrase=None, env_var_name='ENVSEAL_PASSPHRASE', dotenv_path=None, dotenv_var_name='ENVSEAL_PASSPHRASE', app_name=APP_NAME, key_alias=KEY_ALIAS, prompt_text='EnvSeal master passphrase: ')

Get passphrase from various sources.

Parameters:

Name Type Description Default
source PassphraseSource

Where to get the passphrase from

KEYRING
hardcoded_passphrase Optional[str]

Passphrase to use when source is HARDCODED

None
env_var_name str

Environment variable name for ENV_VAR source

'ENVSEAL_PASSPHRASE'
dotenv_path Optional[Union[str, Path]]

Path to .env file for DOTENV source

None
dotenv_var_name str

Variable name in .env file for DOTENV source

'ENVSEAL_PASSPHRASE'
app_name str

Application name for keyring

APP_NAME
key_alias str

Key alias for keyring

KEY_ALIAS
prompt_text str

Text to show when prompting user

'EnvSeal master passphrase: '

Returns:

Name Type Description
bytes bytes

The passphrase as bytes

Raises:

Type Description
EnvSealError

If passphrase cannot be obtained from specified source

Source code in src/envseal/core.py
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def get_passphrase(
    source: PassphraseSource = PassphraseSource.KEYRING,
    hardcoded_passphrase: Optional[str] = None,
    env_var_name: str = "ENVSEAL_PASSPHRASE",
    dotenv_path: Optional[Union[str, Path]] = None,
    dotenv_var_name: str = "ENVSEAL_PASSPHRASE",
    app_name: str = APP_NAME,
    key_alias: str = KEY_ALIAS,
    prompt_text: str = "EnvSeal master passphrase: ",
) -> bytes:
    """
    Get passphrase from various sources.

    Args:
        source: Where to get the passphrase from
        hardcoded_passphrase: Passphrase to use when source is HARDCODED
        env_var_name: Environment variable name for ENV_VAR source
        dotenv_path: Path to .env file for DOTENV source
        dotenv_var_name: Variable name in .env file for DOTENV source
        app_name: Application name for keyring
        key_alias: Key alias for keyring
        prompt_text: Text to show when prompting user

    Returns:
        bytes: The passphrase as bytes

    Raises:
        EnvSealError: If passphrase cannot be obtained from specified source
    """
    passphrase = None

    if source == PassphraseSource.KEYRING:
        if not HAS_KEYRING:
            raise EnvSealError(
                "keyring package not available. Install with: pip install keyring"
            )
        try:
            passphrase = keyring.get_password(app_name, key_alias)
            if not passphrase:
                raise EnvSealError(
                    f"No passphrase found in keyring for {app_name}:{key_alias}"
                )
        except Exception as e:
            raise EnvSealError(f"Failed to get passphrase from keyring: {e}")

    elif source == PassphraseSource.HARDCODED:
        if not hardcoded_passphrase:
            raise EnvSealError(
                "hardcoded_passphrase must be provided when using HARDCODED source"
            )
        passphrase = hardcoded_passphrase

    elif source == PassphraseSource.ENV_VAR:
        passphrase = os.environ.get(env_var_name)
        if not passphrase:
            raise EnvSealError(f"Environment variable {env_var_name} not found")

    elif source == PassphraseSource.DOTENV:
        if not HAS_DOTENV:
            raise EnvSealError(
                "python-dotenv package not available. Install with: pip install python-dotenv"
            )

        if dotenv_path:
            # Load from specific file
            env_vars = dotenv_values(dotenv_path)
            passphrase = env_vars.get(dotenv_var_name)
        else:
            # Load from default locations
            load_dotenv()
            passphrase = os.environ.get(dotenv_var_name)

        if not passphrase:
            raise EnvSealError(f"Variable {dotenv_var_name} not found in .env file")

    elif source == PassphraseSource.PROMPT:
        try:
            passphrase = getpass.getpass(prompt_text)
            if not passphrase:
                raise EnvSealError("Empty passphrase provided")
        except (KeyboardInterrupt, EOFError):
            raise EnvSealError("Passphrase input cancelled")

    else:
        raise EnvSealError(f"Unknown passphrase source: {source}")

    return passphrase.encode("utf-8")

load_sealed_env

load_sealed_env(dotenv_path=None, passphrase_source=PassphraseSource.KEYRING, **passphrase_kwargs)

Load environment variables from a .env file, automatically unsealing encrypted values.

Parameters:

Name Type Description Default
dotenv_path Optional[Union[str, Path]]

Path to .env file. If None, uses default dotenv behavior

None
passphrase_source PassphraseSource

Source for the decryption passphrase

KEYRING
**passphrase_kwargs Any

Additional arguments passed to get_passphrase()

{}

Returns:

Name Type Description
dict Dict[str, str]

Environment variables with encrypted values decrypted

Raises:

Type Description
EnvSealError

If dotenv is not available or decryption fails

Source code in src/envseal/core.py
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
def load_sealed_env(
    dotenv_path: Optional[Union[str, Path]] = None,
    passphrase_source: PassphraseSource = PassphraseSource.KEYRING,
    **passphrase_kwargs: Any,
) -> Dict[str, str]:
    """
    Load environment variables from a .env file, automatically unsealing encrypted values.

    Args:
        dotenv_path: Path to .env file. If None, uses default dotenv behavior
        passphrase_source: Source for the decryption passphrase
        **passphrase_kwargs: Additional arguments passed to get_passphrase()

    Returns:
        dict: Environment variables with encrypted values decrypted

    Raises:
        EnvSealError: If dotenv is not available or decryption fails
    """
    if not HAS_DOTENV:
        raise EnvSealError(
            "python-dotenv package not available. Install with: pip install python-dotenv"
        )

    # Get passphrase
    passphrase = get_passphrase(source=passphrase_source, **passphrase_kwargs)

    # Load environment variables
    if dotenv_path:
        env_vars = dotenv_values(dotenv_path)
    else:
        # Load from default locations but don't modify os.environ
        env_vars = dotenv_values()

    # Decrypt sealed values
    result = {}
    for key, value in env_vars.items():
        if value and value.startswith(TOKEN_PREFIX):
            try:
                decrypted = unseal(value, passphrase)
                result[key] = decrypted.decode("utf-8")
            except EnvSealError as e:
                raise EnvSealError(f"Failed to unseal {key}: {e}")
        else:
            result[key] = value

    return result

seal

seal(plaintext, passphrase)

Encrypt plaintext using AES-GCM.

Parameters:

Name Type Description Default
plaintext Union[str, bytes]

Text to encrypt

required
passphrase bytes

Encryption passphrase

required

Returns:

Name Type Description
str str

Encrypted token with format "ENC[v1]:..."

Source code in src/envseal/core.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
def seal(plaintext: Union[str, bytes], passphrase: bytes) -> str:
    """
    Encrypt plaintext using AES-GCM.

    Args:
        plaintext: Text to encrypt
        passphrase: Encryption passphrase

    Returns:
        str: Encrypted token with format "ENC[v1]:..."
    """
    if isinstance(plaintext, str):
        plaintext = plaintext.encode("utf-8")

    # Generate random salt and nonce
    salt = os.urandom(16)
    nonce = os.urandom(12)

    # Derive key and encrypt
    key = _kdf(passphrase, salt)
    ciphertext = AESGCM(key).encrypt(nonce, plaintext, None)

    # Create token structure
    blob = {
        "s": base64.b64encode(salt).decode(),
        "n": base64.b64encode(nonce).decode(),
        "c": base64.b64encode(ciphertext).decode(),
    }

    # Encode as base64 JSON
    token_data = base64.b64encode(json.dumps(blob).encode()).decode()
    return f"{TOKEN_PREFIX}{token_data}"

seal_file

seal_file(file_path, passphrase, output_path=None, prefix_only=False)

Encrypt values in an environment file (key=value format).

Parameters:

Name Type Description Default
file_path Union[str, Path]

Path to the environment file to process

required
passphrase bytes

Encryption passphrase

required
output_path Optional[Union[str, Path]]

Output file path (if None, overwrites input file)

None
prefix_only bool

If True, only encrypt values that start with TOKEN_PREFIX (treating it as a marker)

False

Returns:

Name Type Description
int int

Number of values that were encrypted/re-encrypted

Raises:

Type Description
EnvSealError

If file operations or encryption fails

Source code in src/envseal/core.py
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
def seal_file(
    file_path: Union[str, Path],
    passphrase: bytes,
    output_path: Optional[Union[str, Path]] = None,
    prefix_only: bool = False,
) -> int:
    """
    Encrypt values in an environment file (key=value format).

    Args:
        file_path: Path to the environment file to process
        passphrase: Encryption passphrase
        output_path: Output file path (if None, overwrites input file)
        prefix_only: If True, only encrypt values that start with TOKEN_PREFIX (treating it as a marker)

    Returns:
        int: Number of values that were encrypted/re-encrypted

    Raises:
        EnvSealError: If file operations or encryption fails
    """
    file_path = Path(file_path)
    if not file_path.exists():
        raise EnvSealError(f"File not found: {file_path}")

    if output_path is None:
        output_path = file_path
    else:
        output_path = Path(output_path)

    try:
        # Read the file
        content = file_path.read_text(encoding="utf-8")
        lines = content.splitlines()

        modified_count = 0
        processed_lines = []

        # Pattern to match key=value lines (allowing for whitespace)
        env_pattern = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$")

        for line in lines:
            match = env_pattern.match(line)

            if match:
                indent, key, value = match.groups()

                # Remove quotes if present
                if (value.startswith('"') and value.endswith('"')) or (
                    value.startswith("'") and value.endswith("'")
                ):
                    value = value[1:-1]
                    quoted = True
                else:
                    quoted = False

                should_encrypt = False

                if prefix_only:
                    # Only encrypt if it starts with our prefix (treating prefix as a marker)
                    if value.startswith(TOKEN_PREFIX):
                        should_encrypt = True

                        # Check if it's already properly encrypted
                        try:
                            # Try to decrypt - if successful, it's already encrypted, so re-encrypt
                            decrypted = unseal(value, passphrase)
                            value = decrypted.decode("utf-8")
                        except EnvSealError:
                            # If decryption fails, treat everything after TOKEN_PREFIX as plaintext
                            # This handles cases like: port=ENC[v1]:5433 where 5433 is not encrypted
                            value = value[len(TOKEN_PREFIX) :]
                else:
                    # Encrypt all values except those already properly encrypted
                    if value.strip():
                        # Check if it's already properly encrypted
                        if value.startswith(TOKEN_PREFIX):
                            try:
                                # Try to decrypt - if successful, it's already encrypted
                                unseal(value, passphrase)
                                should_encrypt = False  # Already encrypted, skip
                            except EnvSealError:
                                # Not properly encrypted, so encrypt it
                                should_encrypt = True
                                value = value[len(TOKEN_PREFIX) :]
                        else:
                            # Not encrypted at all
                            should_encrypt = True

                if should_encrypt:
                    # Encrypt the value
                    encrypted_value = seal(value, passphrase)

                    # Preserve quoting if original was quoted
                    if quoted:
                        encrypted_value = f'"{encrypted_value}"'

                    processed_lines.append(f"{indent}{key}={encrypted_value}")
                    modified_count += 1
                else:
                    # Keep the line as-is
                    processed_lines.append(line)
            else:
                # Not a key=value line, keep as-is
                processed_lines.append(line)

        # Write the output
        output_content = "\n".join(processed_lines)
        if content.endswith("\n"):
            output_content += "\n"

        output_path.write_text(output_content, encoding="utf-8")

        return modified_count

    except Exception as e:
        if isinstance(e, EnvSealError):
            raise
        raise EnvSealError(f"Failed to process file {file_path}: {e}")

store_passphrase_in_keyring

store_passphrase_in_keyring(passphrase, app_name=APP_NAME, key_alias=KEY_ALIAS)

Store passphrase in OS keyring for future use.

Parameters:

Name Type Description Default
passphrase str

The passphrase to store

required
app_name str

Application name for keyring

APP_NAME
key_alias str

Key alias for keyring

KEY_ALIAS

Raises:

Type Description
EnvSealError

If keyring is not available or storage fails

Source code in src/envseal/core.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
def store_passphrase_in_keyring(
    passphrase: str, app_name: str = APP_NAME, key_alias: str = KEY_ALIAS
) -> None:
    """
    Store passphrase in OS keyring for future use.

    Args:
        passphrase: The passphrase to store
        app_name: Application name for keyring
        key_alias: Key alias for keyring

    Raises:
        EnvSealError: If keyring is not available or storage fails
    """
    if not HAS_KEYRING:
        raise EnvSealError(
            "keyring package not available. Install with: pip install keyring"
        )

    try:
        keyring.set_password(app_name, key_alias, passphrase)
    except Exception as e:
        raise EnvSealError(f"Failed to store passphrase in keyring: {e}")

unseal

unseal(token, passphrase)

Decrypt an encrypted token.

Parameters:

Name Type Description Default
token str

Encrypted token starting with "ENC[v1]:"

required
passphrase bytes

Decryption passphrase

required

Returns:

Name Type Description
bytes bytes

Decrypted plaintext

Raises:

Type Description
EnvSealError

If token is malformed or decryption fails

Source code in src/envseal/core.py
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def unseal(token: str, passphrase: bytes) -> bytes:
    """
    Decrypt an encrypted token.

    Args:
        token: Encrypted token starting with "ENC[v1]:"
        passphrase: Decryption passphrase

    Returns:
        bytes: Decrypted plaintext

    Raises:
        EnvSealError: If token is malformed or decryption fails
    """
    if not token.startswith(TOKEN_PREFIX):
        raise EnvSealError(
            f"Invalid token format. Expected token to start with {TOKEN_PREFIX}"
        )

    try:
        # Extract and decode token data
        token_data = token[len(TOKEN_PREFIX) :]
        blob_json = base64.b64decode(token_data)
        blob = json.loads(blob_json)

        # Extract components
        salt = base64.b64decode(blob["s"])
        nonce = base64.b64decode(blob["n"])
        ciphertext = base64.b64decode(blob["c"])

    except (KeyError, json.JSONDecodeError, binascii.Error) as e:
        raise EnvSealError(f"Malformed token: {e}")

    try:
        # Derive key and decrypt
        key = _kdf(passphrase, salt)
        plaintext = AESGCM(key).decrypt(nonce, ciphertext, None)
        return plaintext

    except InvalidTag:
        raise EnvSealError("Decryption failed. Wrong passphrase or corrupted token.")

unseal_file

unseal_file(file_path, passphrase, output_path=None, prefix_only=False)

Decrypt encrypted values in an environment file (key=value format).

Parameters:

Name Type Description Default
file_path Union[str, Path]

Path to the environment file to process

required
passphrase bytes

Decryption passphrase

required
output_path Optional[Union[str, Path]]

Output file path (if None, overwrites input file)

None
prefix_only bool

If True, only decrypt values that start with TOKEN_PREFIX

False

Returns:

Name Type Description
int int

Number of values that were decrypted

Raises:

Type Description
EnvSealError

If file operations or decryption fails

Source code in src/envseal/core.py
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
def unseal_file(
    file_path: Union[str, Path],
    passphrase: bytes,
    output_path: Optional[Union[str, Path]] = None,
    prefix_only: bool = False,
) -> int:
    """
    Decrypt encrypted values in an environment file (key=value format).

    Args:
        file_path: Path to the environment file to process
        passphrase: Decryption passphrase
        output_path: Output file path (if None, overwrites input file)
        prefix_only: If True, only decrypt values that start with TOKEN_PREFIX

    Returns:
        int: Number of values that were decrypted

    Raises:
        EnvSealError: If file operations or decryption fails
    """
    file_path = Path(file_path)
    if not file_path.exists():
        raise EnvSealError(f"File not found: {file_path}")

    if output_path is None:
        output_path = file_path
    else:
        output_path = Path(output_path)

    try:
        # Read the file
        content = file_path.read_text(encoding="utf-8")
        lines = content.splitlines()

        modified_count = 0
        processed_lines = []

        # Pattern to match key=value lines (allowing for whitespace)
        env_pattern = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$")

        for line in lines:
            match = env_pattern.match(line)

            if match:
                indent, key, value = match.groups()

                # Remove quotes if present
                if (value.startswith('"') and value.endswith('"')) or (
                    value.startswith("'") and value.endswith("'")
                ):
                    value = value[1:-1]
                    quoted = True
                else:
                    quoted = False

                should_decrypt = False

                if prefix_only:
                    # Only decrypt if it starts with our prefix
                    if value.startswith(TOKEN_PREFIX):
                        should_decrypt = True
                else:
                    # Decrypt all values that start with TOKEN_PREFIX
                    if value.startswith(TOKEN_PREFIX):
                        should_decrypt = True

                if should_decrypt:
                    try:
                        # Decrypt the value
                        decrypted_bytes = unseal(value, passphrase)
                        decrypted_value = decrypted_bytes.decode("utf-8")

                        # Preserve quoting if original was quoted
                        if quoted:
                            decrypted_value = f'"{decrypted_value}"'

                        processed_lines.append(f"{indent}{key}={decrypted_value}")
                        modified_count += 1
                    except EnvSealError as e:
                        raise EnvSealError(f"Failed to decrypt {key}: {e}")
                else:
                    # Keep the line as-is
                    processed_lines.append(line)
            else:
                # Not a key=value line, keep as-is
                processed_lines.append(line)

        # Write the output
        output_content = "\n".join(processed_lines)
        if content.endswith("\n"):
            output_content += "\n"

        output_path.write_text(output_content, encoding="utf-8")

        return modified_count

    except Exception as e:
        if isinstance(e, EnvSealError):
            raise
        raise EnvSealError(f"Failed to process file {file_path}: {e}")