Skip to main content

libseal

libseal is a Python library that encrypts a directory into one password-protected .libseal file. You can open that file back into a folder, or mount it and import the Python modules inside.

Use it when a library should stay encrypted on disk or in an artifact store, and should be decrypted only when your program asks for it. Encryption uses the cryptography package, built on OpenSSL. Each sealed file is authenticated, so a wrong password or an edited file is rejected. The key comes from hashlib.scrypt, and the packed directory is compressed with gzip.

Anyone who holds both the file and the password can read the contents. After a successful open or load, the plaintext is available to your process.

Requirements

  • Python 3.9 or newer
  • cryptography 42 or newer, installed automatically with this package

Install

pip install libseal

The distribution provides a libseal command and the same operations as a Python API.

Quick start

examples/library/greeter.py defines hello(name). This seals that directory, mounts it beside the caller, and imports greeter:

from pathlib import Path
from libseal import load_sealed_sync, seal_sync

root = Path(__file__).resolve().parent
password = "sample-password"

seal_sync(
    source=root / "library",
    to=root / "greeter.libseal",
    password=password,
)

greeter = load_sealed_sync(
    file=root / "greeter.libseal",
    module_path="greeter",
    password=password,
    root=root,
    force=True,
)

print(greeter.hello("Ada"))

examples/sample.py runs that program and prints Hello, Ada.

source is the directory to pack. module_path is a file inside the mounted library. The .py suffix is optional. The install folder is taken from the sealed file name: greeter.libseal becomes modules/greeter/. Pass root to choose the directory that contains modules. Leave root unset and the files are written inside the installed libseal package.

The sample password is an example. Use your own.

What is stored

Sealing walks the source directory and packs regular files. Paths inside the archive use / separators, and entries are stored in sorted path order.

  • File bytes are stored unchanged.
  • Permission bits are stored as mode & 0o777. Restoring them is best-effort on filesystems that do not keep Unix modes.
  • Empty directories are omitted.
  • Symbolic links are omitted and are not followed.
  • A path longer than 65,535 UTF-8 bytes, or a file larger than 4,294,967,295 bytes, is rejected.

Sealing the same directory twice produces two different files. Each seal chooses a new salt and a new IV. The password is not written into the file.

Opening extracts into a temporary directory in the destination's parent. The temporary directory replaces the destination only after every file has been written. If decryption or extraction fails, the destination is left as it was and the temporary directory is removed.

Password

Supply the password in one of these ways. Lookup stops at the first match:

  1. The password argument, when it is a non-empty string.
  2. The LIBSEAL_PASSWORD environment variable, when it is a non-empty string.
  3. A hidden terminal prompt. This step runs only for the async functions and the libseal command.

An empty string does not count, so lookup continues. seal_sync, open_sync, mount_sync, and load_sealed_sync do not prompt. Call them with password, or set LIBSEAL_PASSWORD first. If neither is set, they raise LibsealError.

The command never accepts the password as a flag. A value typed at the prompt is not echoed.

# Windows
set LIBSEAL_PASSWORD=open-sesame

# Unix
export LIBSEAL_PASSWORD=open-sesame

If stdin is not a terminal and no password was provided, the prompt raises LibsealError instead of reading a visible line.

Seal and open

from libseal import open_sync, seal_sync

sealed = seal_sync(
    source="./library",
    to="./greeter.libseal",
    password="sample-password",
)
restored = open_sync(
    file="./greeter.libseal",
    into="./restored",
    password="sample-password",
)

seal_sync returns a dict:

Key Meaning
to Absolute path of the sealed file
bytes Size of that file, in bytes

open_sync returns a dict:

Key Meaning
into Absolute path of the restored directory

seal_sync creates missing parent directories of to and writes the sealed file by creating a temporary file, then replacing the destination. The sealed file is created with mode 0o600, subject to the process umask.

open_sync creates missing parent directories of into. The restored directory is then given mode 0o700 when the filesystem allows it.

Replacing an existing directory

force defaults to False.

  • If into is missing, or exists and is empty, extraction proceeds.
  • If into already contains any entry and force is false, LibsealError is raised and the directory is not modified.
  • If force is true, the existing path is removed after a successful extract and replaced by the restored tree.
open_sync(
    file="./greeter.libseal",
    into="./restored",
    password="sample-password",
    force=True,
)

Key cost

scrypt overrides the cost parameters stored in the file:

seal_sync(
    source="./library",
    to="./greeter.libseal",
    password="sample-password",
    scrypt={"N": 16384, "r": 8, "p": 1},
)
Parameter Default Allowed values
N 16384 Power of two from 1024 through 1048576
r 8 Integer from 1 through 32
p 1 Integer from 1 through 16

Omitted keys keep the default. Opening reads the cost from the file, so the same scrypt dict is not required again. A value outside those bounds raises LibsealError before any file is written. Higher N uses more memory and CPU. The memory ceiling passed to hashlib.scrypt is 256 * N * r.

LibsealError is also raised when source is missing or is not a directory, or when source or to is empty.

Mount and import

Mounting opens a sealed file into its own folder, so a second library does not overwrite the first.

modules/
  greeter/
    greeter.py
    helper.py
  billing/
    quote.py
from libseal import mount_sync, read_module

mounted = mount_sync(
    file="./greeter.libseal",
    password="sample-password",
    root=".",
    force=True,
)

greeter = read_module("greeter", store=mounted["install_dir"])
print(greeter.hello("Ada"))
print(mounted["modules"])

mount_sync returns a dict:

Key Meaning
store Absolute path of the store directory
name Library folder name
install_dir Absolute path of store/name
modules Names found directly inside install_dir

Mount options

Argument Default Meaning
file required Path to the .libseal file
password LIBSEAL_PASSWORD, then a prompt on mount Password for that file
root The installed libseal package Directory that contains the store when store is relative
store "modules" Store folder name, or an absolute path. An absolute store ignores root
name File name without its extension Subfolder created inside the store. greeter.libseal yields greeter
force False Replace that subfolder when it already has files

name must be a relative name with no empty, ., or .. segment. An absolute name is rejected.

Loading a module

read_module(module_path, *, root=None, store=None, reload=False) loads one file and returns it.

  • A .py file is executed and returned as a module object.
  • A .json file is parsed and returned as the resulting object.
  • The suffix may be omitted. Resolution tries the exact file, then .py, then .json, then __init__.py or index.py inside a directory.
  • Absolute paths and any . or .. segment are rejected.
  • The resolved file must stay inside the store. A missing store raises LibsealError and tells you to mount a library first.
  • reload=True executes the file again. Otherwise a later call returns the cached module.

load_sealed_sync mounts the file and then calls read_module with reload=True. Its module_path is relative to the installed library, so "greeter" means greeter.py inside modules/greeter/, not the modules/greeter directory itself.

from libseal import load_sealed_sync

greeter = load_sealed_sync(
    file="./greeter.libseal",
    module_path="greeter",
    password="sample-password",
    root=".",
    force=True,
)

The directory containing a loaded .py file is appended to sys.path, so that module can import another module sealed next to it. Bytecode is not written beside the mounted sources.

Loaded code runs in the current process. Mount and import libraries you sealed yourself. Nothing is decrypted until open, mount, or load_sealed is called.

Listing

list_sync() returns the library names in the store, sorted. list_sync(name="greeter") returns the entries directly inside that library.

A listed name is a directory, or a .py or .json file with the suffix removed. Other files are still restored; they are omitted from this list. A missing directory returns [].

from libseal import list_sync

list_sync(root=".")
list_sync(root=".", name="greeter")

Releasing a cached module

release_module(module_path, *, root=None, store=None) drops one module from the libseal cache. It returns True when an entry was removed and False when that module was not cached or could not be resolved. The files on disk stay. Mount again with force=True to replace them.

from libseal import release_module

release_module("greeter", store=mounted["install_dir"])

Asynchronous API

seal, open, mount, load_sealed, and list are coroutines. They take the same arguments as the sync functions. seal, open, mount, and load_sealed can prompt for a password. list only reads the store.

import asyncio
from libseal import open, seal

asyncio.run(seal(source="./library", to="./greeter.libseal"))
asyncio.run(open(file="./greeter.libseal", into="./restored"))

open is the async function. from libseal import open shadows the builtin. Use import libseal or import open_sync when you need the builtin in the same scope.

Command line

libseal seal <directory> -o <file.libseal>
libseal open <file.libseal> -o <directory> [--force]
libseal mount <file.libseal> [--root <dir>] [--store <dir>] [--name <name>] [--force]
set LIBSEAL_PASSWORD=open-sesame
libseal seal ./library -o greeter.libseal
libseal open greeter.libseal -o ./restored
libseal mount greeter.libseal --root . --force

--output is the long form of -o. --no-force selects the default replace behavior. --help and -h print usage and exit 0. A missing command prints usage and exits 1. An unknown command or option prints the error and exits 1.

seal prints the source, the absolute output path, and the file size. open prints the source file and the absolute destination. mount prints the install path, the library name, and the module names it found. An empty library prints Modules: (none).

python -m libseal runs the same command.

Errors

Expected failures raise libseal.LibsealError. The message names the problem. Typical cases:

Situation Result
Wrong password Authentication fails. The destination is not created or replaced
Truncated file, bad magic, or unsupported version LibsealError before extraction
File edited after sealing Authentication fails
Destination already has files, without force LibsealError. Existing files stay in place
Archive path escapes the destination LibsealError. Nothing outside the destination is written
module_path missing on load LibsealError
Module path is absolute or contains .. LibsealError

A failed open removes its temporary directory. A sealed file is replaced only after the temporary file has been written.

File format

A .libseal file is a gzip archive encrypted with AES-256-GCM. The 32-byte key is derived with scrypt. Header integers are little-endian.

Offset Size Field
0 4 Magic LSL1
4 1 Version 1
5 1 KDF id 1 (scrypt)
6 4 N
10 4 r
14 4 p
18 1 Salt length
19 salt length Salt
12 IV
16 Authentication tag
rest Ciphertext

The decrypted plaintext is a gzip stream. After decompression, the archive begins with magic LSA1, one version byte, and a 4-byte file count. Each entry is then:

Size Field
2 Path length, in bytes
path length UTF-8 path, using /
2 Permission mode
4 File size
file size File bytes

Every extracted path has to stay inside the destination. Empty segments, ., .., absolute paths, and Windows drive paths are rejected.

API summary

Name Kind Role
seal_sync / seal function / coroutine Seal a directory
open_sync / open function / coroutine Restore a sealed file
mount_sync / mount function / coroutine Restore into the module store
load_sealed_sync / load_sealed function / coroutine Mount, then import one module
read_module function Import one module from the store
release_module function Drop one module from the cache
list_sync / list function / coroutine List libraries or the entries in one library
resolve_store function Resolve root and store to an absolute path
LibsealError exception Raised for expected failures
DEFAULT_SCRYPT mapping {"N": 16384, "r": 8, "p": 1}
DEFAULT_STORE str "modules"
ENV_NAME str "LIBSEAL_PASSWORD"
PACKAGE_ROOT str Directory of the installed package

Development

From a checkout:

python -m unittest discover -s tests -v

License

MIT. See LICENSE.

Metadata

Release files for libseal 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for libseal 1.1.0
File Size Uploaded Provenance
libseal-1.1.0.tar.gz 25.6 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for libseal 1.1.0
File Interpreter ABI Platform
libseal-1.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 43.8 kB

Release files / libseal-1.1.0.tar.gz

Download URL libseal-1.1.0.tar.gz
Size 25.6 kB
Tags Source
SHA-256 checksum
How to use checksums
96dbfb3dc45ffcc332eccb154d5c5cbfbe9266c9d0197312f374d82b17c76042
BLAKE2b-256 checksum
How to use checksums
6c88838872720755f48e3bee2ed406ce94fda84bdad3388cb6a1cd615919eee0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.5

Release files / libseal-1.1.0-py3-none-any.whl

Download URL libseal-1.1.0-py3-none-any.whl
Size 18.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1d703b7ab636798cf2a0054e3a2a8b27e523b3d1d3924d5210083dee9022ae78
BLAKE2b-256 checksum
How to use checksums
2b915e39aedda6ac070f0c28c42c715b2b49964bd88859a37c850b524b965e37
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.5

Security for libseal 1.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page