________________________________ ____________________________
/ _ / _____/ _ / _____/ ___ _______ /___/ _ / __ / _____/
/ / / / / / / \ \ | | / / ___/ / / / / ____/ \ \
/ / / / / / /___\ \ | |/ /_\ \ / / / / /_______\ \
/ ____/___/ /_______/________/ |___/_____/ ____/ /_______/_______/________/
/___/ /_______/
________ __ ___ ___________
/" ) /""\ |" (" _ ")
(: \___/ / \ || |)__/ \\__/
\___ \ /' /\ \|: | \\_ /
__/ \\ // __' \\ |___|. |
/" \ :) / \\ \\_|: \: |
(_______/___/ \___)______)__|
salt is a lightweight CLI tool that leverages libsodium's sealed-box primitives. It encrypts data using only a recipient's public key, so only the holder of the corresponding private key can decrypt the value.
"If I have seen further, it is by standing on the shoulders of giants." -- Sir Isaac Newton
salt is designed for workflows like GitHub Actions Secrets where you encrypt locally with a public key and send encrypted data to a remote API. Sealed boxes require only the recipient's public key at encryption time, which avoids sender-side key-management complexity in the CLI.
- Uses audited libsodium primitives (
crypto_box_seal) instead of custom crypto. - Supports both raw base64 keys and JSON key payloads used by API workflows.
- Produces deterministic, parseable stderr messages and stable exit codes for automation.
For installation and build instructions, use INSTALL.md.
Encrypt plaintext with a base64 public key:
salt --key "2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234" "hello-world"Encrypt for JSON API submission:
salt --output json --key '{"key_id":"012345678912345678","key":"2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234"}' 'hello-world'Render terminal demo assets (GIF and MP4) from vhs/salt.tape:
vhs vhs/salt.tapeThis requires vhs to be installed and available on PATH.
Output files are written to vhs/salt-demo.gif and vhs/salt-demo.mp4.
Validate tape syntax without rendering:
vhs validate vhs/salt.tape- Unix-style option parsing with short and long flags
- key input can be base64, JSON key object, or stdin
- JSON key objects are strict: only
keyand optionalkey_idare accepted - plaintext input can be positional arg or stdin
- plaintext length must be between 1 byte and 48 KiB (49152 bytes) to match GitHub Actions' per-secret value limit
(reference);
the
saltimplementation accepts up to 1 MiB outside that workflow-oriented CLI guard - stdin key input is capped at 16 KiB
- output: base64 ciphertext (
text) or REST-ready JSON (json) to stdout - errors: deterministic messages to stderr
Build requirements are documented in INSTALL.md.
For testing and fuzzing workflows, use TESTING.md.
salt [OPTIONS] [PLAINTEXT]| Long | Short | Required | Description |
|---|---|---|---|
--key VALUE |
-k VALUE |
Yes | Public key input. VALUE may be a base64 key, a JSON object string with key and optional key_id, or - to read key JSON/base64 from stdin. |
--key-id ID |
-i ID |
No | Key ID override/injector (required for --output json when key source does not provide key_id). |
--output text|json |
-o text|json |
No | Output mode. Default: text. |
--key-format auto|base64|json |
-f auto|base64|json |
No | Key parser mode. Default: auto. |
--help |
-h |
No | Print usage and exit 0. |
--version |
-V |
No | Print version and exit 0. |
- If
PLAINTEXTis omitted or-, plaintext is read from stdin. - If
--key -is used, key input is read from stdin. - Key and plaintext cannot both be read from stdin in one invocation.
--key-format autoinspects the trimmed key input and treats a leading{as JSON; use--key-format base64or--key-format jsonwhen you want to force one parser path during automation or debugging.
Security note: Sensitive plaintext should be passed via stdin. Positional arguments are visible in process listings (
ps,/proc/<pid>/cmdline).
Base64 key + positional plaintext (text mode):
salt -k "2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234" "Hello World!"JSON key object + JSON output:
salt --output json --key '{"key_id":"012345678912345678","key":"2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234"}' 'Hello World!' | jq -c '.'JSON key from stdin:
echo '{"key_id":"012345678912345678","key":"2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234"}' | salt --output json --key - "Hello World!" | jq -c '.'Value from stdin with JSON key:
echo 'Hello World!' | salt --output json --key '{"key_id":"012345678912345678","key":"2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234"}' - | jq -c '.'Base64 key + explicit key-id for JSON output:
salt --output json --key "2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234" --key-id "012345678912345678" "Hello World!" | jq -c '.'salt itself does not use configuration files or environment variables. The CLI is configured entirely through flags plus stdin for key/plaintext input.
The helper scripts under examples/ use environment variables such as GITHUB_TOKEN, OWNER, REPO, ORG, ENVIRONMENT_NAME, SECRET_NAME, SECRET_VALUE, and SECRET_VALUE_FILE because they orchestrate GitHub REST API calls around the salt binary.
| Format | Description | When to use |
|---|---|---|
text |
Prints the base64-encoded ciphertext on a single line. | Shell pipelines or when another tool already knows the key_id. |
json |
Prints {"encrypted_value":"...","key_id":"..."}. |
GitHub Actions Secrets API payload assembly and other REST workflows. |
json output requires a key_id from JSON key input or --key-id.
| Input format | Output needed | Use this command pattern |
|---|---|---|
| Base64 key only | Base64 ciphertext for a pipeline | salt -k "<base64-key>" "plaintext" |
| Base64 key + known key_id | JSON for REST API | salt --output json -k "<base64-key>" -i "<key-id>" "plaintext" |
| JSON key object | JSON for REST API | salt --output json -k '{"key_id":"...","key":"..."}' "plaintext" |
| JSON key object | Base64 ciphertext only | salt -k '{"key_id":"...","key":"..."}' "plaintext" |
Base64 key (32 bytes base64-encoded):
2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234
JSON key object (strict RFC 8259, only key and key_id fields):
{"key_id":"012345678912345678","key":"2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234"}Key from stdin (use - to read key input from stdin instead of command line).
The table below documents tested and validated build environments. Other Linux distributions and toolchains with C17 support, libsodium 1.0.18+, and POSIX APIs are expected to work but are not covered by CI gates.
| Environment | amd64 | Notes |
|---|---|---|
| Generic Linux source build | ✅ | Build with the canonical GNU flow (./configure && make && make check) when libsodium and cmocka development packages are available. |
| Ubuntu 22.04 container | ✅ | Tested via docker/ubuntu/22/Dockerfile. |
| Ubuntu 24.04 container | ✅ | Tested via docker/ubuntu/24/Dockerfile; main CI host environment. |
| Ubuntu 26.04 container | ✅ | Tested via docker/ubuntu/26/Dockerfile. |
| Release archives | ✅ | salt-linux-amd64.tar.gz (template layout rooted with docs, scripts, coverage HTML, man page, static salt, and dynamic salt-dynamic in bin/), Ubuntu 22/24/26 .deb packages, and matching SPDX JSON/CycloneDX JSON SBOMs. |
0: success1: input or usage error2: cryptographic error3: internal error4: runtime I/O or output-stream failure5: signal-driven interruption
1groups caller and input problems such as missing options, malformed JSON key input, invalid public-key encoding, and empty plaintext.2is reserved for libsodium-backed failures such as initialization or sealed-box encryption failure.3covers allocation and other internal failures where the program cannot safely continue.4reports runtime stdin/stdout/stderr failures such as broken pipes,/dev/full, or truncated output streams.5reports signal-driven interruption observed through the CLI signal path.- Deterministic stderr text still carries the detailed diagnosis, while the numeric exit codes stay stable enough for shell and CI automation to branch without scraping wording.
The manual page source is located at man/salt.1 and validated with:
make manGitHub Actions is used for CI/CD:
.github/workflows/ci.ymlrunsmake ci-fastfor the smoke gate andmake cifor the full GNU Autotools gate..github/workflows/release.ymlruns only for SemVer tags matchingv*, reuses the same out-of-tree GNU flow (./bootstrap,../../configure --enable-tests,make ci), and then publishes template-layoutsalt-linux-amd64.tar.gz(containing staticsaltand dynamicsalt-dynamicundersalt/bin/), Ubuntu 22/24/26.debpackages built withdpkg-deb, matching SPDX JSON and CycloneDX JSON SBOMs,SHA256SUMS, andSHA256SUMS.asc;.debartifacts are signed natively with embedded Debian signatures and the tarball/checksum manifest are detached-signed with GPG. CycloneDX SBOM enrichment includes Ubuntu package/version provenance for build inputs (for examplegcc,clang,make,libc-bin/ldd,binutils,autoconf,automake,libtool,pkg-config, and libsodium packages), while SPDX remains focused on runtime package and shipped-file metadata.
Release integrity can be verified by validating SBOMs and release checksums. This section covers how auditors and security teams can reproduce and cross-verify release artifacts.
Each release publishes four SBOMs (Software Bill of Materials):
salt-linux-amd64.spdx.jsonandsalt-linux-amd64.cyclonedx.json(dynamic binary)salt-static-linux-amd64.spdx.jsonandsalt-static-linux-amd64.cyclonedx.json(static binary)
To regenerate and validate an SBOM locally using Syft 1.44.0:
# Install Syft 1.44.0 pinned by SHA256
SYFT_VERSION="1.44.0"
SYFT_SHA256="0e91737aee2b5baf1d255b959630194a302335d848ff97bb07921eb6205b5f5a"
# See .github/workflows/release.yml for exact SHA256 and download URL
# Extract release staging binaries and regenerate SPDX/CycloneDX formats
syft dir:./salt-linux-amd64 \
--source-name salt \
--source-supplier CTFfactory \
--source-version v0.0.0 \
-o spdx-json=salt-linux-amd64-local.spdx.json \
-o cyclonedx-json=salt-linux-amd64-local.cyclonedx.jsonAfter generating SBOMs locally, verify they match the released checksums:
# Download and verify release checksum manifest
curl -sS -L -o SHA256SUMS https://github.com/CTFfactory/salt/releases/download/v0.0.0/SHA256SUMS
curl -sS -L -o SHA256SUMS.asc https://github.com/CTFfactory/salt/releases/download/v0.0.0/SHA256SUMS.asc
grep -E '(spdx|cyclonedx)' SHA256SUMS | sha256sum -c -
# Verify checksum manifest signature (release signing key required)
gpg --verify SHA256SUMS.asc SHA256SUMS
# Inspect embedded Debian signatures
debsigs --list salt_0.0.0-1ubuntu24_amd64.deb
# The output should confirm all SBOM checksums matchRelease SBOMs can be scanned for known vulnerabilities using Snyk or Trivy:
# Snyk scan (requires Snyk account)
snyk sbom test salt-linux-amd64.cyclonedx.json
# Trivy scan (no account required)
trivy sbom salt-linux-amd64.spdx.jsonFor guidance on SBOM standards and generation, see .github/instructions/sbom.instructions.md.
error: --key/-k is required: Provide--key, either base64 text, JSON object, or-for stdin key input.error: --key/-k value must not be empty:--keywas supplied with an empty string. Pass a non-empty key value, JSON object, or-for stdin.error: invalid option: <name>: An unrecognized short or long option was passed. Re-run with--helpto see the supported flags.error: key and plaintext cannot both be read from stdin: Use stdin for only one input source per invocation.error: invalid public key encoding or length: Ensure key decodes to exactlycrypto_box_PUBLICKEYBYTESbytes.error: JSON output requires key_id: Provide--key-idor pass JSON key input containingkey_id.error: plaintext must not be empty: Provide a non-empty positional plaintext argument or non-empty stdin input.error: invalid output format 'X' (expected text|json): Use--output textor--output json.error: invalid key format 'X' (expected auto|base64|json): Use--key-format auto,--key-format base64, or--key-format json.error: stdin input exceeds maximum length (N bytes): Keep stdin payload within limits (plaintext up to 48 KiB / 49152 bytes to satisfy GitHub Actions' secret-value cap, key input up to 16 KiB).- mlock() failures or
RLIMIT_MEMLOCKwarnings:saltattempts to lock sensitive buffers in resident memory usingsodium_mlock(). If the systemRLIMIT_MEMLOCKis too low (common in some container runtimes), locking fails non-fatally andsaltfalls back to zeroing memory on release withsodium_memzero(). This does not affect correctness, but means transient plaintext and key material may be paged to swap. To eliminate the warning or increase security, raise the limit withulimit -l unlimited(requires privilege) or configure your container runtime to allow higher locked-memory limits.
- Base64 ciphertext output is default because target APIs and shell pipelines commonly expect text payloads.
- The public library API intentionally stays base64-oriented; callers that need raw binary sealed boxes should call libsodium
crypto_box_seal()directly instead of going throughsalt. - Strict JSON key parsing rejects unknown fields and malformed objects to avoid ambiguous input handling.
- Error taxonomy is intentionally coarse at the CLI boundary to keep scripting behavior predictable.
- CLI signal handling is process-wide by design, so the embedded CLI entry points are not reentrant across concurrent threads in the same process.
Use this flow whenever you need to store a secret through the GitHub REST API:
- Fetch a target public key and
key_id. - Encrypt locally with
salt. - Pipe
saltJSON intojqandcurl --data-binary @-so the encrypted value, key ID, and final JSON request body are not placed on command lines.
Every API call below should include:
Accept: application/vnd.github+jsonAuthorization: Bearer ${GITHUB_TOKEN}X-GitHub-Api-Version: 2022-11-28
API Version Policy: This documentation and the example scripts reference GitHub REST API version 2022-11-28, which is the current stable version for Actions Secrets endpoints as of 2026-04-29. Consult the GitHub REST API versioning documentation if newer versions introduce changes to request/response formats or required headers.
The examples below and the packaged examples/set-*-secret.sh scripts use a small helper plus process substitution (-H @<(...)) so GITHUB_TOKEN does not appear in /proc/<pid>/cmdline. The packaged scripts also accept a SECRET_VALUE_FILE environment variable so plaintext does not enter /proc/<pid>/environ; prefer it over SECRET_VALUE for real secrets.
Token permissions by scope:
| Secret scope | Fine-grained token permission | Classic token scope |
|---|---|---|
| Repository | Secrets repository permission: write |
repo for private repos; public_repo for public repos |
| Environment | Environments repository permission: write |
repo |
| Organization | Secrets organization permission: write |
admin:org |
GitHub Actions secret values are capped at 48 KiB (49152 bytes). salt and the helper scripts enforce this workflow limit before encryption so an oversized plaintext fails locally instead of producing an encrypted payload that GitHub will reject.
The packaged helper performs the full fetch/encrypt/PUT flow:
export GITHUB_TOKEN OWNER REPO SECRET_NAME
export SECRET_VALUE_FILE=./secret-value.txt
./examples/set-repo-secret.shEquivalent request-shaping pattern for custom automation:
set -euo pipefail
readonly GITHUB_API_VERSION="2022-11-28"
auth_header_fd() { printf 'Authorization: Bearer %s\n' "${GITHUB_TOKEN}"; }
KEY_RESPONSE="$(curl -sS -L -w '\n%{http_code}' \
-H "Accept: application/vnd.github+json" \
-H @<(auth_header_fd) \
-H "X-GitHub-Api-Version: ${GITHUB_API_VERSION}" \
"https://api.github.com/repos/${OWNER}/${REPO}/actions/secrets/public-key")"
KEY_STATUS="${KEY_RESPONSE##*$'\n'}"
KEY_JSON="${KEY_RESPONSE%$'\n'*}"
if [ "${KEY_STATUS}" != "200" ]; then
printf 'failed to fetch repository public key, status=%s\n' "${KEY_STATUS}" >&2
exit 1
fi
KEY_ID="$(printf '%s' "${KEY_JSON}" | jq -r '.key_id')"
PUB_KEY="$(printf '%s' "${KEY_JSON}" | jq -r '.key')"
SALT_JSON="$(salt --output json --key "${PUB_KEY}" --key-id "${KEY_ID}" - <"${SECRET_VALUE_FILE}")"
STATUS="$(printf '%s' "${SALT_JSON}" | jq -c '{encrypted_value:.encrypted_value,key_id:.key_id}' | \
curl -sS -L -o /dev/null -w '%{http_code}' \
-X PUT \
-H "Accept: application/vnd.github+json" \
-H "Content-Type: application/json" \
-H @<(auth_header_fd) \
-H "X-GitHub-Api-Version: ${GITHUB_API_VERSION}" \
"https://api.github.com/repos/${OWNER}/${REPO}/actions/secrets/${SECRET_NAME}" \
--data-binary @-)"
if [ "${STATUS}" = "201" ] || [ "${STATUS}" = "204" ]; then
printf 'repository secret %s applied\n' "${SECRET_NAME}"
else
printf 'repository secret apply failed, status=%s\n' "${STATUS}" >&2
exit 1
fiEnvironment names are URL-encoded in API paths. The packaged helper performs that encoding and applies the same safe request-shaping pattern:
export GITHUB_TOKEN OWNER REPO ENVIRONMENT_NAME SECRET_NAME
export SECRET_VALUE_FILE=./secret-value.txt
./examples/set-environment-secret.shOrganization secrets require VISIBILITY=all, private, or selected (default: private). When VISIBILITY=selected, provide SELECTED_REPOSITORY_IDS as a JSON array of unique positive integer repository IDs; the helper validates and sorts the IDs before sending selected_repository_ids.
export GITHUB_TOKEN ORG SECRET_NAME
export SECRET_VALUE_FILE=./secret-value.txt
export VISIBILITY=selected
export SELECTED_REPOSITORY_IDS='[123456789,987654321]'
./examples/set-org-secret.sh- Use
set -euo pipefailin automation scripts. - Always fetch a fresh
key_idand public key before encrypting. - Check the HTTP status for public-key fetches before parsing JSON; do not print response bodies on failures because API errors may include sensitive context.
- Validate
SECRET_NAMEagainst GitHub's[A-Za-z_][A-Za-z0-9_]*naming rule and reject the reservedGITHUB_prefix before attempting an upload. - Reject plaintext values larger than 48 KiB (49152 bytes) up front.
- Pipe the
salt --output jsonresult tojqand then tocurl --data-binary @-; avoid command substitution withcurl -d, and do not passencrypted_valueorkey_idthroughjqcommand-line arguments. - Use environment secrets for production credentials to enforce deployment protection rules such as required reviewers.
- Prefer OIDC-based short-lived credentials for cloud providers instead of storing long-lived API keys.
- Treat only
201and204as success when creating/updating secrets via REST.
See executable scope-specific scripts in examples/.
- libsodium sealed boxes: https://doc.libsodium.org/public-key_cryptography/sealed_boxes
- libsodium base64 helpers: https://doc.libsodium.org/helpers
- GitHub Actions Secrets API workflow: https://docs.github.com/en/rest/actions/secrets
This project is under active development with stable CLI behavior and API contracts. The repository has not yet published a tagged release (version 0.1.0 or 1.0.0). Once a semver tag is applied, the release will indicate production-readiness and semantic versioning guarantees will apply to the public API surface defined in include/salt.h and the CLI interface documented in the man page and README.
Until then, treat main as the development line with evolving documentation, test coverage, and CI infrastructure, while keeping the core encryption and CLI behavior functionally stable.
- README.md (this file) - Project overview, usage guide, and GitHub Actions Secrets API workflow
- INSTALL.md - Build and installation instructions for contributors and package maintainers
- TESTING.md - Testing strategy, quality gates, and validation requirements
- CONTRIBUTING.md - Contribution workflow, release process, and documentation maintenance policy
- SECURITY.md - Vulnerability disclosure policy and documented attack surfaces
- AGENTS.md - Repository guidance for coding agents and automated tooling
- CHANGELOG.md - Version history and release notes following Keep a Changelog format
- man/salt.1 - Manual page for the
saltCLI (validate withmake man) - docs/ - Supporting documentation including lint suppressions, libsodium usage inventory, and Ubuntu package lists
- examples/ - Executable helper scripts for GitHub Actions secret workflows (repository, environment, and organization scopes)
- m4/ - Custom GNU Autotools macros for libsodium/cmocka discovery and SBOM tool detection (maintainer reference only)
See CONTRIBUTING.md for the complete contribution workflow, quality gates, validation requirements, and development setup.
See SECURITY.md for the disclosure policy and documented attack surfaces.
See TESTING.md for the testing strategy that covers trust boundaries, test taxonomy, and quality-control gates.
Runtime hardening built into the production binary:
- Sealed-box plaintext, decoded public key, ciphertext, and stdin/argv copies are
sodium_mlock'd so they cannot be paged to swap, and zeroed viasodium_munlockon every exit path. Locking failures (typically a lowRLIMIT_MEMLOCKinside containers) are non-fatal and fall back to plainsodium_memzero-on-release. main()callssetrlimit(RLIMIT_CORE, 0)andprctl(PR_SET_DUMPABLE, 0)on Linux so transient secrets cannot be extracted from a core file or by an attaching debugger sharing the same UID. Library callers (salt_cli_run_with_streams) opt in to their own hardening.- Plaintext passed via the positional argument is copied into mlock'd memory and the original
argvslot is scrubbed in place. Prefer stdin for sensitive plaintext anyway —--helpdocuments the trade-off. - The hardened build is compiled with
-D_FORTIFY_SOURCE=2,-fstack-protector-strong, and linked with-Wl,-z,relro,-z,now.
This project is released under The Unlicense. See LICENSE.
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to https://unlicense.org