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
cryptography42 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:
- The
passwordargument, when it is a non-empty string. - The
LIBSEAL_PASSWORDenvironment variable, when it is a non-empty string. - A hidden terminal prompt. This step runs only for the async functions and the
libsealcommand.
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
intois missing, or exists and is empty, extraction proceeds. - If
intoalready contains any entry andforceis false,LibsealErroris raised and the directory is not modified. - If
forceis 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
.pyfile is executed and returned as a module object. - A
.jsonfile is parsed and returned as the resulting object. - The suffix may be omitted. Resolution tries the exact file, then
.py, then.json, then__init__.pyorindex.pyinside a directory. - Absolute paths and any
.or..segment are rejected. - The resolved file must stay inside the store. A missing store raises
LibsealErrorand tells you to mount a library first. reload=Trueexecutes 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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| libseal-1.1.0.tar.gz | 25.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.