Using uacrypt
uacrypt encrypt/decrypt/hash (docs/TASKS.md T-16, docs/DECISIONS.md D-52) are the real,
misuse-resistant top-level commands — mode, nonce, and algorithm are all hardcoded, nothing to
misconfigure:
cargo build -p uacrypt --release
uacrypt keygen --out key.bin
uacrypt encrypt --key key.bin --in message.bin --out sealed.bin
uacrypt decrypt --key key.bin --in sealed.bin --out message.bin
uacrypt hash --in file.bin --out digest.bin
encrypt/decrypt have no message-length cap and stream --in/--out in fixed-size chunks —
as of 2026-07-25 they’re built over dstu_core::crypto_secretstream (docs/TASKS.md T-40/T-70,
docs/DECISIONS.md D-68), a genuinely chunked construction over hazmat::kalyna_gcm, not the earlier
whole-buffer crypto_secretbox (docs/TASKS.md T-37, docs/DECISIONS.md D-51/D-63) - a large input file no
longer means a correspondingly large in-memory buffer. Breaking wire-format change: a file the
prior crypto_secretbox-backed encrypt produced cannot be read by this decrypt, and vice versa
- acceptable pre-1.0.
crypto_secretboxitself is unchanged and still available as a library primitive for whole-message use, just no longer what this CLI command uses.--keyis a raw 32-byte file (crypto_secretstream::Key’s size) —uacrypt keygen --out key.bingenerates one from the OS CSPRNG (docs/TASKS.mdT-115).encryptdraws a fresh random header internally on every call and embeds it in--out; there is no--nonce/--headerflag to supply or reuse by mistake.hashhas no such limit either — it streams--infrom disk in fixed-size chunks regardless of size, fixed to Kupyna-256 (32-byte digest, no--variantchoice).
uacrypt sign-keygen/sign-pubkey/sign/verify (docs/TASKS.md T-124, docs/DECISIONS.md D-73) are the
digital-signature equivalent, built over dstu_core::crypto_sign (DSTU 4145): a signature proves a
file came from whoever holds the signing key and hasn’t been changed since — unlike encrypt, it
does not hide the file’s contents, only attests to who signed it and that it’s unmodified. Every
command below was run for real against the release binary before being written here:
uacrypt sign-keygen --out signing.key
uacrypt sign-pubkey --key signing.key --out verifying.key
uacrypt sign --key signing.key --in message.bin --out message.bin.sig
uacrypt verify --key verifying.key --in message.bin --sig message.bin.sig
sign-keygen’s output (signing.key, 21 raw bytes) is secret — keep it like any other private key.
sign-pubkey derives the matching verifying.key (42 raw bytes) from it, safe to share or publish.
verify prints nothing and exits 0 on a valid signature; on a tampered file, a tampered signature,
or the wrong verifying key, it exits 1 with an error and writes nothing — it does not, and cannot,
silently accept a mismatch:
$ uacrypt verify --key verifying.key --in message.bin --sig message.bin.sig
$ echo $?
0
$ echo "tampered" > message.bin
$ uacrypt verify --key verifying.key --in message.bin --sig message.bin.sig
uacrypt: verify: signature does not verify - message, signature, or key do not match
$ echo $?
1
uacrypt sign-keygen257/sign-pubkey257/sign257 (docs/TASKS.md T-199, docs/DECISIONS.md
D-185/D-186) mirror the four commands above exactly, over DSTU 4145’s m=257 curve instead of
m=163 - a separate set of commands, not a --curve flag, since the two produce distinct,
incompatible key shapes. m=257 is what real Diia-issued qualified signatures use in production
(confirmed from an actual issued certificate) - m=163 stays the default (sign-keygen) only
because it shipped first, not because it’s recommended over m=257. verify is shared - it reads
a curve-tag byte from --key and handles both curves, so there is no separate verify257:
uacrypt sign-keygen257 --out signing257.key
uacrypt sign-pubkey257 --key signing257.key --out verifying257.key
uacrypt sign257 --key signing257.key --in message.bin --out message.bin.sig257
uacrypt verify --key verifying257.key --in message.bin --sig message.bin.sig257
uacrypt box-keygen/box-pubkey/box-seal/box-open (docs/TASKS.md T-178, docs/DECISIONS.md
D-169) are public-key encryption, built over dstu_core::crypto_box (DSTU 9041, hybrid via KDF):
unlike encrypt (which needs a shared symmetric key both sides already have), box-seal only needs
the recipient’s public key — anyone can seal a message only the matching secret key can open:
uacrypt box-keygen --out box.key
uacrypt box-pubkey --key box.key --out box.pub
uacrypt box-seal --key box.pub --in message.bin --out message.bin.box
uacrypt box-open --key box.key --in message.bin.box --out message.bin
box-keygen’s output (box.key, 32 raw bytes) is secret. box-pubkey derives the matching
box.pub (32 raw bytes, the curve point’s x-coordinate only) from it, safe to share or publish.
box-seal/box-open are not memory-bounded yet — --in is read whole into memory, unlike
encrypt/decrypt’s bounded-chunk streaming (see crypto_box’s own module doc for why).
uacrypt box-keygen512/box-pubkey512/box-seal512/box-open512 (docs/TASKS.md T-193,
docs/DECISIONS.md D-182) mirror the four commands above exactly, over DSTU 9041’s l(p)=512
curve (E512/1) instead of l(p)=256 - a separate set of commands, not a --curve flag, since the
two produce distinct, incompatible key shapes (64-byte keys instead of 32-byte):
uacrypt box-keygen512 --out box512.key
uacrypt box-pubkey512 --key box512.key --out box512.pub
uacrypt box-seal512 --key box512.pub --in message.bin --out message.bin.box512
uacrypt box-open512 --key box512.key --in message.bin.box512 --out message.bin
What exists below this level: kalyna-block, kupyna-digest, and strumok-crypt, all
hazmat-scoped commands added for binary-level performance comparisons
(docs/PERFORMANCE.md, docs/DECISIONS.md D-31/D-34) rather than everyday use - hash and
encrypt/decrypt above are what most users want.
kalyna-block is a single block (no mode, no padding):
uacrypt kalyna-block encrypt --variant 128-128 --key key.bin --in block.bin --out ct.bin
uacrypt kalyna-block decrypt --variant 128-128 --key key.bin --in ct.bin --out pt.bin
--key/--in/--out are raw binary files of the variant’s exact byte length (16/32/64 bytes
depending on variant — see --variant’s five values).
kupyna-digest hashes with a selectable variant (hash above is simpler for everyday use - fixed
to Kupyna-256, no --variant flag):
uacrypt kupyna-digest --variant 512 --in report.pdf --out report.pdf.kupyna512
strumok-crypt is the bare Strumok keystream cipher (XOR-based) - not authenticated: a
tampered output decrypts silently into wrong plaintext, there is no tag to detect it. Never reuse
the same --key/--iv pair for two different messages - doing so lets an attacker recover both
messages by XORing the two ciphertexts together. Use encrypt/decrypt instead for a file cipher
that detects tampering:
uacrypt strumok-crypt --variant 256 --key key.bin --iv iv.bin --in message.bin --out sealed.bin
--variant is the key size in bits (256 or 512); --iv is always exactly 32 bytes regardless of
variant.
kalyna-ccm (docs/DECISIONS.md D-41) additionally encrypts/authenticates arbitrary-length short
messages (plaintext and --aad each capped at 255 bytes — a sourced property of the construction,
not a CLI restriction, see hazmat::kalyna_ccm’s doc comment) using a provisional, dual-oracle-
verified Kalyna-alone CCM mode, not yet confirmed against the primary DSTU 7624:2014 text:
uacrypt kalyna-ccm encrypt --variant 128-128 --key key.bin --nonce nonce.bin --aad aad.bin --in msg.bin --out ct.bin --tag tag.bin
uacrypt kalyna-ccm decrypt --variant 128-128 --key key.bin --nonce nonce.bin --aad aad.bin --in ct.bin --out pt.bin --tag tag.bin
--nonce is a raw file of exactly the variant’s block length (16/32/64 bytes) — but it’s an
output on encrypt, not an input: encrypt generates a fresh random nonce itself (via the OS
CSPRNG) and writes it there, so there is nothing for you to supply or accidentally reuse. decrypt
reads --nonce back (the value encrypt produced) as an input, same as --tag. --aad is
optional (an empty AAD is used if omitted); decrypt verifies the tag before writing --out and
fails without writing anything on a mismatch. See docs/DECISIONS.md D-40 for why a random nonce is
safe here (128 bits minimum across all five variants) and its per-key message-count guideline.
Five more hazmat-scoped, per-mode benchmarking/interop commands exist alongside kalyna-ccm
(docs/DECISIONS.md D-31/D-71), all with the same --variant (one of the five Kalyna block/
key-size combinations) and --iterations (benchmark timing) flags:
kalyna-gcm encrypt/decrypt— Kalyna-GCM (--nonce/--tagfiles,--aadoptional), the same provisional constructioncrypto_secretbox/crypto_secretstreambuild on internally, exposed here with no message-length cap and no hidden nonce.kalyna-cmac compute/verify— Kalyna-CMAC, a 16-byte tag, no encryption.kalyna-gmac compute/verify— Kalyna-GMAC, a full-block tag, no encryption, no nonce.kalyna-kw wrap/unwrap— Kalyna key wrap (1..=20 block-aligned blocks in, one block longer out, checksummed).kalyna-xts encrypt/decrypt— Kalyna-XTS disk-sector mode (confidentiality only, no tag — the correct design for this use case, not a gap; caller supplies the--tweakblock).
Run any of them with no arguments (or --help) for the full flag reference and an example -
covered here at a summary level since their shape mirrors kalyna-ccm/kalyna-block above rather
than needing separate full walkthroughs.
None of kalyna-block/kupyna-digest/strumok-crypt/kalyna-ccm/kalyna-gcm/kalyna-cmac/
kalyna-gmac/kalyna-kw/kalyna-xts is the encrypt/decrypt/hash surface above - all nine
stay as lower-level, hazmat-scoped tools for anyone who explicitly wants that level of control, or
is benchmarking a specific primitive directly.