lcurs-administrator is a comprehensive command-line administration toolkit for the LCURS ecosystem. It provides secure key management, file encryption/decryption using Age, an encrypted account registry, TOTP (time-based one-time password) support, API token generation and verification, and a robust authentication system.
This document covers everything: installation, usage, command reference, security model, architecture, API, and development guide.
- Overview
- Installation
- Quick Start
- Architecture & Data Flow
- Command Reference
- Environment Variables
- Security Model
- API Reference for Library Users
- Development Guide
- License
lcurs-admin is the administrative CLI tool for the LCURS project. Its primary features are:
- Age encryption/decryption – uses the modern age encryption format (X25519 + ChaCha20‑Poly1305).
- Encrypted account registry – stores accounts (name, public key, linked private key path, metadata, OTP secret, API token) in an encrypted file (
meta.bin), protected with a master secret. - TOTP (RFC 6238) – generate secrets, produce OTP codes, and verify them with drift tolerance.
- API token management – generate, show, revoke hex tokens (32 bytes) per account.
- Authentication flow – verify API token + OTP for application‑level authentication.
- Secure configuration storage – an optional encrypted
config.binto persist environment settings (LCURS_KEYS_DIR,LCURS_MASTER_SECRET).
All commands produce structured JSON output (pretty‑printed by default), making it easy to integrate with scripts and other tools. Logging goes to stderr and can be controlled via verbosity flags or RUST_LOG.
cargo install lcurs-administratorgit clone https://github.com/neuxdotdev/lcurs-administrator.git
cd lcurs-administrator
cargo install --path .After installation, the binary is named lcurs-admin.
- Rust 1.70 or later (for building from source)
- Unix-like operating system (Linux, macOS) – Windows is not fully tested, but basic functionality may work (file permissions are not enforced on Windows).
This section walks you through a typical workflow: initializing keys, creating an account, enabling OTP, generating an API token, and verifying authentication.
# 1. Set the mandatory master secret (used to encrypt the account registry)
export LCURS_MASTER_SECRET=$(openssl rand -hex 32)
# 2. Generate a default Age key pair (private and public)
lcurs-admin init
# 3. Encrypt a test file using the default recipient
echo "sensitive data" > secret.txt
lcurs-admin encrypt -i secret.txt -o secret.age
# 4. Decrypt it back
lcurs-admin decrypt -i secret.age -o secret.dec
cat secret.dec # prints "sensitive data"
# 5. Create an account named "alice" with the default public key
# (the default recipient from the init step)
DEFAULT_RECIPIENT=$(lcurs-admin show | jq -r .recipient)
lcurs-admin account add alice --key "$DEFAULT_RECIPIENT"
# 6. Enable TOTP for the account – you will get a secret and an otpauth:// URI
lcurs-admin account otp enable alice
# Save the secret and URI, then add it to an authenticator app (e.g., Google Authenticator)
# 7. Generate an API token for alice
lcurs-admin auth generate-token alice
# The output contains a 64‑character hex token – keep it secure.
# 8. Verify authentication using the token and a current OTP code
OTP_CODE=$(lcurs-admin auth code alice | jq -r .code)
lcurs-admin auth verify alice --token <TOKEN> --otp "$OTP_CODE"
# If successful, you receive: { "success": true, "message": "Authentication successful" }The following diagram illustrates the main components and data flow of lcurs-admin.
flowchart LR
subgraph Commands
direction TB
INFO[info]
SETUP[setup]
INIT[init]
SHOW[show]
ENCRYPT[encrypt]
DECRYPT[decrypt]
ACCOUNT[account]
AUTH[auth]
end
subgraph Internals
ENC_SYS[encryption_system]
DEC_SYS[decryption_system]
ACC_MGR[account_manager]
AUTH_SYS[authenticator_systems]
OTP[otp_systems]
CONFIG_SEC[config_secure]
end
subgraph Storage
AGE_KEYS[age keypair]
REGISTRY[encrypted registry]
ENV_VARS[LCURS_MASTER_SECRET]
end
INIT --> AGE_KEYS
ENCRYPT --> ENC_SYS --> AGE_KEYS
DECRYPT --> DEC_SYS --> AGE_KEYS
ACCOUNT --> ACC_MGR --> REGISTRY
AUTH --> AUTH_SYS --> REGISTRY
SETUP --> CONFIG_SEC
REGISTRY -.-> ENV_VARS
- api::init_keys – Generates a new Age key pair and stores the private key with
0o600permissions, public key with0o644. - api::encryption_system – Encrypts a file for a list of recipients (either provided directly or resolved from registry accounts). Supports both ASCII‑armored and binary output.
- api::decryption_system – Decrypts an Age‑encrypted file using provided identities or the default private key. Can also iterate over all registered private keys (
--all-registry). - api::account_manager – Manages the encrypted registry. It uses ChaCha20‑Poly1305 + HMAC‑SHA256 for authenticated encryption. The encryption key is derived via HKDF from
LCURS_MASTER_SECRETand a random salt stored in the registry. - api::authenticator_systems – Provides high‑level authentication functions: register user, generate/revoke API token, verify token+OTP, produce OTP codes.
- api::otp_systems – Pure TOTP implementation (RFC 6238) with HMAC‑SHA1, 30‑second time step, 6 or 8 digits (configurable; default is 6 digits in the current code).
- core::config_secure – An optional encrypted configuration file (
config.bin) that can storeLCURS_KEYS_DIRandLCURS_MASTER_SECRET, protected by a user‑provided password (Argon2 + ChaCha20‑Poly1305 + HMAC). - command/executor – Glues everything together, parses CLI arguments, and produces JSON output.
- On first use (e.g.,
account add), a random 32‑byte salt is generated. LCURS_MASTER_SECRETis read from the environment (or fromconfig.binifsetupwas run).- HKDF‑SHA256 expands the master secret together with the salt to produce two 32‑byte keys:
enc_keyandmac_key. - The registry payload (JSON) is encrypted with
enc_keyusing ChaCha20‑Poly1305, prefixed with the salt and a random nonce. - An HMAC‑SHA256 tag is computed over the entire encrypted blob using
mac_keyand appended. - The resulting binary blob is written atomically to
meta.bin(and a backup copy).
On subsequent loads, the process is reversed: read blob, verify HMAC, decrypt, and deserialize.
These options can be used with any command and must appear before the subcommand.
| Option | Description |
|---|---|
-v, --verbose |
Increase logging verbosity (can be repeated: -v = debug, -vv = trace). |
-q, --quiet |
Suppress all logging except errors. |
--json |
Force JSON output (already default, kept for compatibility). |
--output <human|json> |
Output format; default human (pretty JSON). |
--log-format <pretty|json|compact> |
Format of log messages (stderr). |
--no-color |
Disable coloured log output. |
-h, --help |
Show help. |
-V, --version |
Show version. |
Example:
lcurs-admin -v --output json account listDisplays build and package information.
lcurs-admin infoExample output:
{
"package": {
"name": "lcurs-administrator",
"version": "0.1.0",
"description": "Administrator tools for LCURS package",
"repository": "https://github.com/neuxdotdev/lcurs-administrator",
"license": "MIT"
},
"build": {
"time": "2025-04-21 20:00:00 +07:00",
"profile": "release",
"target": "x86_64-unknown-linux-gnu",
"rustc": "1.85.0",
"build_id": "a1b2c3d4e5f67890",
"features": []
}
}Generates a new Age X25519 key pair. The private key is saved as privateKeys.asc (permission 0o600), and the public key as publicKeys.pub.asc (permission 0o644) inside the keys directory. If keys already exist, you are prompted before overwriting.
lcurs-admin initOutput:
{
"status": "success",
"recipient": "age1...",
"paths": {
"secret": "/home/user/.config/lcurs/lcurs-administrator/privateKeys.asc",
"public": "/home/user/.config/lcurs/lcurs-administrator/publicKeys.pub.asc"
}
}Prints the default recipient (public key) to stdout.
lcurs-admin showOutput:
{
"recipient": "age1qxxc3we3t75mgfjj4qf3qha7qnscepm2acfk2gmrlqwu8epm0ahs3d4qey"
}Encrypts a file for one or more recipients.
Syntax:
lcurs-admin encrypt -i <INPUT> -o <OUTPUT> [OPTIONS]Options:
| Option | Description |
|---|---|
-i, --input <FILE> |
Input file (required). |
-o, --output <FILE> |
Output encrypted file (required). |
-r, --recipient <KEY or FILE> |
Age public key (starts with age1) or a path to a file containing the key. Can be repeated. |
--to <NAME> |
Use the public key of an existing account from the registry. Can be repeated. |
--armor |
Produce ASCII‑armored output (default: true). Use --armor false for binary. |
-f, --force |
Overwrite output file without confirmation. |
If neither --recipient nor --to is provided, the default recipient (from show) is used.
Examples:
# Encrypt using default recipient
lcurs-admin encrypt -i doc.pdf -o doc.pdf.age
# Encrypt for two explicit recipients
lcurs-admin encrypt -i secret.txt -o secret.age -r age1... -r age2...
# Encrypt for accounts alice and bob (must already exist in registry)
lcurs-admin encrypt -i data.bin -o data.age --to alice --to bob
# Binary mode (no ASCII armor)
lcurs-admin encrypt -i file -o file.age --armor falseOutput:
{
"status": "success",
"input": "secret.txt",
"output": "secret.age",
"mode": "Armored",
"bytes_processed": 12345,
"recipient_count": 2
}Decrypts an Age‑encrypted file.
Syntax:
lcurs-admin decrypt -i <INPUT> -o <OUTPUT> [OPTIONS]Options:
| Option | Description |
|---|---|
-i, --input <FILE> |
Encrypted input file (required). |
-o, --output <FILE> |
Decrypted output file (required). |
-i, --identity <FILE> |
Path to an Age private key file (can be repeated). |
--all-registry |
Attempt decryption using every private key linked to accounts in the registry. |
-f, --force |
Overwrite output file without confirmation. |
If no identity is provided and --all-registry is not used, the default private key (privateKeys.asc) is used.
Examples:
# Decrypt using default private key
lcurs-admin decrypt -i secret.age -o secret.txt
# Decrypt using a specific private key file
lcurs-admin decrypt -i secret.age -o out.txt --identity bob.key
# Decrypt by trying all registry-linked keys
lcurs-admin decrypt -i shared.age -o shared.txt --all-registryOutput:
{
"status": "success",
"input": "secret.age",
"output": "secret.txt",
"decrypted_bytes": 12345
}All account commands operate on an encrypted registry stored in $LCURS_KEYS_DIR/.registry/meta.bin. The registry requires LCURS_MASTER_SECRET to be set.
Lists all registered accounts with their metadata and active status.
lcurs-admin account listOutput:
{
"accounts": [
{
"name": "alice",
"public_key": "age1...",
"private_key_path": "/home/user/.config/lcurs/keys/alice.key",
"metadata": { "role": "admin", "email": "alice@example.com" },
"created_at": 1713715200,
"last_used": 1713715200,
"active": true
},
{
"name": "bob",
"public_key": "age1...",
"private_key_path": null,
"metadata": { "role": "user" },
"created_at": 1713715300,
"last_used": 1713715300,
"active": false
}
],
"count": 2
}Creates a new account.
Syntax:
lcurs-admin account add <NAME> --key <KEY> [--private-key <FILE>] [--meta KEY=VALUE]...<NAME>: unique account name.-k, --key: Age public key (must start withage1).--private-key: optional path to the associated private key file.--meta: arbitrary metadata (can be repeated).
Example:
lcurs-admin account add alice --key age1... --private-key alice.asc --meta role=admin --meta "department=IT"Output:
{ "status": "success", "name": "alice" }Removes an account from the registry. Prompts for confirmation unless -f is given.
lcurs-admin account remove alice -fSets the active account. The active account is used as a default when --to is omitted in encrypt (if the encrypt command uses registry‑based recipients) or when --all-registry is used in decrypt (it will always try all keys, but active does not affect that). Currently, the active account is mostly informative.
lcurs-admin account use aliceDisplays the currently active account name, or null if none.
lcurs-admin account activeOutput:
{ "active_account": "alice" }Links a private key file to an existing account.
lcurs-admin account import-private alice --key-file /path/to/alice.keySubcommands for managing TOTP on an account.
enable– Generates a new TOTP secret, stores it in the registry, and outputs the secret andotpauth://URI. Use the URI to add the account to an authenticator app.disable– Removes the OTP secret from the account.show– Displays the current secret and URI (without modifying anything).verify– Checks a user‑provided OTP code against the current time window (with one step drift tolerance).
Examples:
lcurs-admin account otp enable alice
lcurs-admin account otp show alice
lcurs-admin account otp verify alice --code 123456
lcurs-admin account otp disable aliceEnable output:
{
"status": "success",
"name": "alice",
"secret": "JBSWY3DPEHPK3PXP",
"uri": "otpauth://totp/LCURS:alice?secret=JBSWY3DPEHPK3PXP&issuer=LCURS&digits=6"
}The auth family of commands provides a higher‑level authentication interface intended for application use (e.g., an API server). It reuses the same registry as account.
Registers a new user (creates an account). Requires a public key (must start with age1). Metadata can be added.
lcurs-admin auth register alice --public-key age1... --meta "source=cli"Output:
{
"status": "success",
"user": {
"name": "alice",
"public_key": "age1...",
"metadata": { "source": "cli" },
"created_at": 1713715400,
"otp_enabled": false,
"api_token_exists": false
}
}Generates a new API token for a user (random 32 bytes → hex). The token is stored in the registry.
lcurs-admin auth generate-token aliceOutput:
{ "token": "a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef12345678" }Displays the current token for a user (if any).
lcurs-admin auth show-token aliceRemoves the token from the user’s account.
lcurs-admin auth revoke-token aliceVerifies a user’s API token and a current OTP code. Returns success/failure.
lcurs-admin auth verify alice --token <TOKEN> --otp <CODE>Output (success):
{ "success": true, "message": "Authentication successful" }Output (failure):
{ "success": false, "message": "Invalid OTP code" }Generates a TOTP code for the current time (without verification). Useful for testing or for the client to obtain the code before sending to auth verify.
lcurs-admin auth code aliceOutput:
{ "code": "123456" }With --watch, it continuously updates the code every second (this mode cannot be used with --json because it prints live updates to the console).
lcurs-admin auth code alice --watchLists all registered users (same as account list but under the auth command).
lcurs-admin auth list-users| Variable | Required | Description |
|---|---|---|
LCURS_MASTER_SECRET |
Yes (for registry) | 32‑byte key used to derive the registry encryption keys. Can be hex (64 characters) or base64. Generate with openssl rand -hex 32. |
LCURS_KEYS_DIR |
No | Absolute path to the directory where keys and the registry are stored. Default: $HOME/.config/lcurs/lcurs-administrator. |
LCURS_NO_PROGRESS |
No | If set to any value, disables the progress bar (useful for CI environments). |
RUST_LOG |
No | Overrides the log level (e.g., RUST_LOG=debug). Takes precedence over -v/-q. |
Note: The LCURS_MASTER_SECRET is critical for the security of the registry. Keep it secret and never commit it to version control. You can store it in a .env file or use the setup command to save it in an encrypted config.bin.
lcurs-administrator employs multiple layers of security to protect sensitive data.
- Default key directory permissions:
0o700(drwx------). - Private key files:
0o600(-rw-------). - Public key files:
0o644(-rw-r--r--). - On non‑Unix platforms, permissions are not enforced; a warning is logged.
- Algorithm: ChaCha20‑Poly1305 (authenticated encryption) + HMAC‑SHA256.
- Key derivation: HKDF‑SHA256 using
LCURS_MASTER_SECRETas input keying material (IKM) and a random 32‑byte salt stored in the registry header. Two output keys are produced: one for encryption, one for MAC. - Nonce: Random 12‑byte nonce generated for each encryption operation.
- Integrity: An HMAC‑SHA256 tag over the entire encrypted blob (including salt and nonce) ensures tamper detection.
- Atomic writes: All writes go to a temporary file first, then
rename()to the target, preventing partial writes or corruption. - Automatic backup: A copy
meta.bin.bakis kept.
- The
LCURS_MASTER_SECRETis never stored on disk in plaintext (unless you choose to write it in an environment file). It is loaded into memory as aZeroizingbuffer, which zeroes the memory on drop. - It can be supplied via environment variable or via the encrypted
config.bin(which itself is protected by a user password).
- Encrypted using Argon2 (key derivation) + ChaCha20‑Poly1305 + HMAC.
- The user provides a password each time
setupis run; the password is not stored. - This file can safely store
LCURS_KEYS_DIRandLCURS_MASTER_SECRETso that you don’t need to export them in your shell.
When linking a private key file to an account, the path is canonicalized and checked to be inside the keys directory. Directory traversal (e.g., ../) is rejected.
Sensitive data structures (SecretString, Zeroizing arrays) are used for cryptographic keys, passwords, and secrets. They automatically overwrite their memory when dropped, reducing the risk of exposure in core dumps or memory analysis.
Logs (stderr) may contain debug information when -v is used, but they never include private keys or full secrets. OTP secrets are not logged.
While the primary use of this crate is the binary, many internal modules are public and can be used as a library. Add lcurs-administrator as a dependency in your Cargo.toml to leverage the following APIs.
generate_keypair() -> (SecretString, String)– returns a new Age private key (as aSecretString) and its public key string.init_keys() -> Result<(SecretString, String), io::Error>– generates a key pair and saves it to the default location.
encrypt_file<F>(input_path, output_path, recipients, mode, progress_cb) -> Result<(), HandlerError>– encrypts a file.recipientsis a slice of public key strings.modecan beArmorMode::ArmoredorBinary.progress_cbis an optional closure receiving(current, total).ArmorModeenum.
decrypt_file<F>(input_path, output_path, identities, progress_cb) -> Result<(), HandlerError>– decrypts a file.identitiesis a slice ofage::x25519::Identity. If empty, the default identity is used.load_identity_from_file(path) -> Result<X25519Identity, HandlerError>– loads an Age private key from a file.
SecureRegistrystruct – provides low‑level access to the encrypted registry.load() -> Result<Self, HandlerError>load_or_init() -> Result<Self, HandlerError>save(&mut self) -> Result<(), HandlerError>add_account(...),remove_account(...),set_active(...),list_accounts(), etc.
- High‑level functions:
add_account,remove_account,use_account,list_accounts_json,enable_account_otp,verify_account_otp,disable_account_otp,get_account_otp_secret,generate_api_token, etc.
generate_secret() -> String– returns a Base32‑encoded secret (20 bytes).totp(secret: &str, timestamp: u64) -> Result<String, OtpError>– computes the TOTP code for the given UNIX timestamp (seconds).verify(secret: &str, code: &str) -> Result<bool, OtpError>– verifies a user code against current time (±1 time step).generate_otp_uri(account_name, secret, issuer) -> String– returns anotpauth://URI.
register_user(name, public_key, metadata) -> Result<RegisteredUser, HandlerError>generate_api_token_for_user(name) -> Result<String, HandlerError>verify_user_auth(name, token, otp_code) -> Result<AuthResult, HandlerError>generate_current_otp_code(name) -> Result<String, HandlerError>watch_otp_code(name, callback) -> JoinHandle<()>
All public functions return a Result<T, HandlerError>, where HandlerError is a comprehensive error enum (IO, key generation, validation, config, etc.). The WithContext trait allows adding context to errors.
- Rust 1.70+
- Cargo
- (Optional)
gitfor embedding commit info viabuiltcrate.
cargo build --releaseThe binary will be in target/release/lcurs-admin.
cargo testIntegration tests are in the tests/ directory (if any). The existing unit tests are inside each module.
build.rs uses the built crate to collect metadata (version, dependencies, Git commit, etc.) and exposes them via built.rs which is included in src/core/info.rs. If you are building in an environment without Git (e.g., a released source tarball), the Git commit will be omitted.
src/api/– core cryptographic and registry operations.src/command/– CLI parsing and command execution.src/core/– configuration, prompting, build info.src/handler/– error types, logging setup.
- Define a new variant in
Commandenum insrc/command/types.rs. - Add the corresponding parsing logic (if needed) using
clapattributes. - Implement a handler function (e.g.,
handle_mycmd) insrc/command/executor.rsthat returnsCommandResult(aResult<serde_json::Value, HandlerError>). - Match the new variant in the
executemethod. - Ensure the handler returns a JSON value (the final output) and does not print anything directly (unless it is a watch mode or interactive). The outer
executewill print the JSON.
Use the tracing macros (info!, debug!, warn!, error!) for logging. Logs go to stderr and are formatted according to --log-format. Do not use println! for debugging; it would interfere with JSON output. Use eprintln! only for interactive prompts or watch mode.
Progress bars are provided by the indicatif crate. They are automatically disabled when LCURS_NO_PROGRESS is set. To add a progress bar in a new command, follow the pattern in encrypt_with_progress.
- Always use
Zeroizingfor secrets. - Never log secrets or full OTP secrets.
- Validate file paths to prevent directory traversal.
- Use atomic write patterns for any file that could be partially written.
- Keep the registry encryption scheme auditable.
This project is licensed under the MIT License. See the LICENSE file for details.
Maintained by neuxdotdev.
For issues, feature requests, or contributions, please use the GitHub issue tracker.