/* Warning: this file is autogenerated by cbindgen. Do not modify manually. */ #ifndef SOLITON_H #define SOLITON_H #include #include #include #include /* ─── Usage contract ──────────────────────────────────────────────────── */ /* * THREAD SAFETY: Opaque handles (SolitonRatchet*, SolitonKeyRing*, * SolitonCallKeys*) are NOT thread-safe. Concurrent access to the same * handle from multiple threads is undefined behavior. Callers must * serialize access externally (e.g., mutex) or use one handle per thread. * Stateless functions (sha3_256, hmac, hkdf, aead, xwing, identity, * verification_phrase) are safe to call concurrently with distinct buffers. * * OWNERSHIP: Opaque handle _free functions take a double pointer * (e.g., `soliton_ratchet_free(&ptr)`) and set the handle to NULL after * freeing, making double-free a safe no-op. Cross-type free (e.g., passing * a SolitonRatchet** to soliton_keyring_free) is undefined behavior. * * Compound struct _free functions (soliton_encrypted_message_free, * soliton_kex_initiated_session_free, soliton_kex_received_session_free, * soliton_decoded_session_init_free) take a single pointer and free * internal buffers. They do NOT null the caller's pointer (the struct is * caller-allocated, not library-allocated). Repeated calls are safe (no-op) * because internal buffers are nulled after freeing. * * FIXED-SIZE PARAMETERS: Functions accepting raw pointers for fixed-size * inputs (keys, nonces, fingerprints) require exactly the documented number * of bytes. Passing a shorter buffer is undefined behavior (out-of-bounds * read). Required sizes are documented per function and as SOLITON_*_SIZE * constants below. */ /* ─── Size constants (bytes) ──────────────────────────────────────────── */ #define SOLITON_PUBLIC_KEY_SIZE 3200 #define SOLITON_SECRET_KEY_SIZE 2496 #define SOLITON_XWING_PK_SIZE 1216 #define SOLITON_XWING_SK_SIZE 2432 #define SOLITON_XWING_CT_SIZE 1120 #define SOLITON_ED25519_SIG_SIZE 64 #define SOLITON_HYBRID_SIG_SIZE 3373 #define SOLITON_MLDSA_SIG_SIZE 3309 #define SOLITON_SHARED_SECRET_SIZE 32 #define SOLITON_FINGERPRINT_SIZE 32 #define SOLITON_AEAD_TAG_SIZE 16 #define SOLITON_AEAD_NONCE_SIZE 24 #define SOLITON_CALL_ID_SIZE 16 #define SOLITON_STREAM_HEADER_SIZE 26 #define SOLITON_STREAM_CHUNK_SIZE 1048576 #define SOLITON_STREAM_ENCRYPT_MAX 1048849 /* ─── Error codes ─────────────────────────────────────────────────────── */ #define SOLITON_OK 0 #define SOLITON_ERR_INVALID_LENGTH -1 #define SOLITON_ERR_DECAPSULATION -2 #define SOLITON_ERR_VERIFICATION -3 #define SOLITON_ERR_AEAD -4 #define SOLITON_ERR_BUNDLE -5 #define SOLITON_ERR_TOO_MANY_SKIPPED -6 #define SOLITON_ERR_DUPLICATE -7 /* -8 reserved (was SOLITON_ERR_SKIPPED_KEY, removed) */ #define SOLITON_ERR_ALGORITHM -9 #define SOLITON_ERR_VERSION -10 #define SOLITON_ERR_DECOMPRESSION -11 #define SOLITON_ERR_INTERNAL -12 #define SOLITON_ERR_NULL_POINTER -13 #define SOLITON_ERR_FLAGS -14 #define SOLITON_ERR_CHAIN_EXHAUSTED -15 #define SOLITON_ERR_CRYPTO_VERSION -16 #define SOLITON_ERR_INVALID_DATA -17 #define SOLITON_ERR_CONCURRENT_ACCESS -18 /** * Opaque call keys state. * * # Undefined Behavior * * **Do not duplicate handles.** Copying the pointer and using both copies * produces inconsistent state — both aliases share a `step_count` counter, * so sequential `advance` calls via different aliases desynchronize key * material. Freeing one alias invalidates the other (use-after-free). The * reentrancy guard only prevents *concurrent* access, not *sequential* misuse. */ typedef struct SolitonCallKeys SolitonCallKeys; /** * Opaque storage keyring. * * # Undefined Behavior * * **Do not duplicate handles.** Copying the pointer and using both copies * produces undefined behavior: freeing one alias invalidates the other * (use-after-free). The reentrancy guard only prevents *concurrent* access, * not *sequential* use from aliased pointers to freed memory. */ typedef struct SolitonKeyRing SolitonKeyRing; /** * Opaque ratchet state. * * The `in_use` flag is a reentrancy guard — CAPI functions acquire it on * entry and release on exit. Concurrent access from multiple threads returns * `SOLITON_ERR_CONCURRENT_ACCESS` instead of silently proceeding (which * would cause AEAD nonce reuse on encrypt). * * # Undefined Behavior * * **Do not duplicate handles.** Copying the pointer (e.g., via `memcpy` on * the `SolitonRatchet*` variable, or assigning a second variable to the * same address) and using both copies for encrypt/decrypt produces * catastrophic nonce reuse — both copies share a `send_count` counter, * and the reentrancy guard only prevents *concurrent* access, not * *sequential* use from two copies. Nonce uniqueness depends on each * ratchet handle having exactly one owner. */ typedef struct SolitonRatchet SolitonRatchet; /** * Opaque streaming decryptor handle. */ typedef struct SolitonStreamDecryptor SolitonStreamDecryptor; /** * Opaque streaming encryptor handle. */ typedef struct SolitonStreamEncryptor SolitonStreamEncryptor; /** * A library-allocated byte buffer. * * Must be freed with `soliton_buf_free`. Do not free `ptr` directly. * The `ptr` field points to the data region; the original allocation size * is stored in an internal header at `ptr - 8`, inaccessible to callers. */ typedef struct SolitonBuf { uint8_t *ptr; uintptr_t len; } SolitonBuf; /** * Ratchet header returned from encrypt, passed to decrypt. */ typedef struct SolitonRatchetHeader { /** * Sender's ratchet public key (library-allocated). */ struct SolitonBuf ratchet_pk; /** * KEM ciphertext, if present (library-allocated; ptr is null if absent). */ struct SolitonBuf kem_ct; /** * Message number within current send chain. */ uint32_t n; /** * Length of the previous send chain. */ uint32_t pn; } SolitonRatchetHeader; /** * Encrypted message returned from ratchet encrypt. */ typedef struct SolitonEncryptedMessage { struct SolitonRatchetHeader header; /** * Ciphertext (library-allocated). */ struct SolitonBuf ciphertext; } SolitonEncryptedMessage; /** * Result of session initiation, returned as a flat C struct. * * Contains both the encoded session init blob (`session_init_encoded`) and * the individual fields. Callers should send the individual fields over the * wire so that Bob can pass them to `soliton_kex_receive`. * * **Key usage order (critical):** * 1. Pass `initial_chain_key` to `soliton_ratchet_encrypt_first` to * encrypt the first application message. Do NOT pass it as a raw key * to `soliton_aead_encrypt`. * 2. Pass `root_key` AND the `ratchet_init_key_out` from * `soliton_ratchet_encrypt_first` (as `chain_key`) to * `soliton_ratchet_init_alice`. Do NOT pass `initial_chain_key` * as `chain_key` — that is the pre-encrypt value. * * Using the wrong key at either step produces no immediate error — AEAD * encryption succeeds with any 32-byte key. The mismatch only surfaces * when the receiver fails to decrypt. * * Inline key fields (`root_key`, `initial_chain_key`, `sender_ik_fingerprint`, * `recipient_ik_fingerprint`) are zeroized by `soliton_kex_initiated_session_free`. * Do NOT use `memset`/`free` directly — call the free function for safe cleanup. * * # Struct Copy Warning * * C struct assignment (`SolitonInitiatedSession copy = *out;`) or `memcpy` * creates a bitwise copy containing `root_key` and `initial_chain_key`. * `soliton_kex_initiated_session_free` only zeroizes the original — the copy * retains secret keys indefinitely. Avoid copying this struct; pass by * pointer and free as soon as keys have been extracted. * # GC Safety * * `root_key` and `initial_chain_key` are inline secret material. In GC'd * runtimes (C#, Go, Python), the GC may copy the containing struct during * compaction, leaving unzeroized copies of key material. To mitigate: * - Pin the struct in unmanaged memory (C# `Marshal.AllocHGlobal`, * Go `C.malloc`, Python `ctypes.create_string_buffer`) * - Call `soliton_kex_initiated_session_free` as soon as the keys have been * passed to `soliton_ratchet_init_alice` — minimize the pinned lifetime * - Do NOT copy this struct to managed heap objects */ typedef struct SolitonInitiatedSession { /** * Encoded session init message (caller frees). */ struct SolitonBuf session_init_encoded; /** * Root key (32 bytes, inline). */ uint8_t root_key[32]; /** * Initial chain key (32 bytes, inline). */ uint8_t initial_chain_key[32]; /** * Alice's ephemeral X-Wing public key (caller frees). */ struct SolitonBuf ek_pk; /** * Alice's ephemeral X-Wing secret key. SECURITY: free via * `soliton_kex_initiated_session_free` only — do NOT call * `soliton_buf_free` on this field directly. */ struct SolitonBuf ek_sk; /** * SHA3-256(Alice.IK_pub) — 32 bytes, inline. */ uint8_t sender_ik_fingerprint[32]; /** * SHA3-256(Bob.IK_pub) — 32 bytes, inline. Pass to `soliton_kex_receive` * as `recipient_ik_fingerprint` when transmitting individual fields over the wire. */ uint8_t recipient_ik_fingerprint[32]; /** * X-Wing ciphertext encapsulated to Bob's IK (caller frees). */ struct SolitonBuf ct_ik; /** * X-Wing ciphertext encapsulated to Bob's SPK (caller frees). */ struct SolitonBuf ct_spk; /** * Bob's signed pre-key ID. */ uint32_t spk_id; /** * X-Wing ciphertext encapsulated to Bob's OPK (caller frees; ptr=null if absent). */ struct SolitonBuf ct_opk; /** * Bob's one-time pre-key ID (0 if OPK absent). */ uint32_t opk_id; /** * Whether an OPK was used (0 = no, 1 = yes). * * `#[repr(C)]` inserts 3 bytes of padding between this `u8` and the next * pointer-aligned `SolitonBuf` on 64-bit targets (`has_opk` at offset 236, * next 8-aligned boundary at 240). `soliton_kex_initiate` zero-initializes * the output struct via `write_bytes`, then uses field-by-field assignment * to preserve zeroed padding bytes. */ uint8_t has_opk; /** * Alice's hybrid signature over the encoded SessionInit (caller frees via soliton_kex_initiated_session_free). */ struct SolitonBuf sender_sig; } SolitonInitiatedSession; /** * Result of successful session receipt (Bob's side, §5.5). * * Symmetric with [`SolitonInitiatedSession`] on Alice's side. * * # GC Safety * * `root_key` and `chain_key` are inline secret material. See * [`SolitonInitiatedSession`] for GC-safety guidance — the same * pinning and lifetime-minimization advice applies. * * **Key usage order (critical):** * 1. Pass `chain_key` to `soliton_ratchet_decrypt_first` to decrypt Alice's * first message. Do NOT use it as a raw AEAD key. * 2. Pass `root_key` AND the `ratchet_init_key_out` from * `soliton_ratchet_decrypt_first` (as `chain_key`) to * `soliton_ratchet_init_bob`. Do NOT pass `chain_key` from this struct * as `chain_key` — that is the pre-decrypt value. * * Using the wrong key at either step produces no immediate error. The mismatch * only surfaces when decryption fails. * * `root_key` and `chain_key` are zeroized by * `soliton_kex_received_session_free`. `peer_ek` is library-allocated and * freed by the same function. Do NOT call `soliton_buf_free` on `peer_ek` * directly; use `soliton_kex_received_session_free` for safe cleanup. */ typedef struct SolitonReceivedSession { /** * Root key (32 bytes, inline). Zeroized by `soliton_kex_received_session_free`. */ uint8_t root_key[32]; /** * Initial chain key (32 bytes, inline). Zeroized by `soliton_kex_received_session_free`. */ uint8_t chain_key[32]; /** * Alice's ephemeral X-Wing public key (library-allocated; freed by * `soliton_kex_received_session_free`). */ struct SolitonBuf peer_ek; } SolitonReceivedSession; /** * Wire fields of a received session init message (Bob's input, §5.5). * * All fields are transmitted by Alice and received by Bob over the network. * Populate this struct from the deserialized message before calling * [`soliton_kex_receive`]. The struct holds raw pointers into caller-owned * buffers and neither allocates nor frees memory. * * Field names correspond to [`SolitonInitiatedSession`] fields (excluding * Alice-only fields such as `root_key`, `ek_sk`, and `session_init_encoded`). * * `sender_ik_fingerprint` and `recipient_ik_fingerprint` are fixed-size * (32 bytes each); the `_len` fields must be exactly 32. */ typedef struct SolitonSessionInitWire { /** * Alice's hybrid signature over the encoded SessionInit (caller-owned). */ const uint8_t *sender_sig; uintptr_t sender_sig_len; /** * SHA3-256(Alice.IK_pub) — must point to exactly 32 bytes. */ const uint8_t *sender_ik_fingerprint; uintptr_t sender_ik_fingerprint_len; /** * SHA3-256(Bob.IK_pub) — must point to exactly 32 bytes. */ const uint8_t *recipient_ik_fingerprint; uintptr_t recipient_ik_fingerprint_len; /** * Alice's ephemeral X-Wing public key (caller-owned). */ const uint8_t *sender_ek; uintptr_t sender_ek_len; /** * X-Wing ciphertext encapsulated to Bob's IK (caller-owned). */ const uint8_t *ct_ik; uintptr_t ct_ik_len; /** * X-Wing ciphertext encapsulated to Bob's SPK (caller-owned). */ const uint8_t *ct_spk; uintptr_t ct_spk_len; /** * Bob's signed pre-key ID. Must match the key in `SolitonSessionDecapKeys`. */ uint32_t spk_id; /** * X-Wing ciphertext encapsulated to Bob's OPK (null if no OPK was used). */ const uint8_t *ct_opk; uintptr_t ct_opk_len; /** * Bob's one-time pre-key ID (must be 0 when `ct_opk` is null; non-zero returns `SOLITON_ERR_INVALID_DATA`). */ uint32_t opk_id; /** * Null-terminated crypto version string (e.g. `"lo-crypto-v1"`). */ const char *crypto_version; } SolitonSessionInitWire; /** * Bob's pre-key secret keys needed to decapsulate a received session init (§5.5). * * Bob fetches these from his key store using `spk_id` and `opk_id` from * [`SolitonSessionInitWire`]. The struct holds raw pointers into caller-owned * buffers and neither allocates nor frees memory. * * If `ct_opk` in the wire struct is null, `opk_sk` must also be null. * Providing `opk_sk` when `ct_opk` is null is rejected as `InvalidData`. */ typedef struct SolitonSessionDecapKeys { /** * Bob's signed pre-key secret key (X-Wing, 2432 bytes, caller-owned). */ const uint8_t *spk_sk; uintptr_t spk_sk_len; /** * Bob's one-time pre-key secret key (null if OPK was not used). */ const uint8_t *opk_sk; uintptr_t opk_sk_len; } SolitonSessionDecapKeys; /** * Decoded session init. Produced by [`soliton_kex_decode_session_init`]. * * Free with [`soliton_decoded_session_init_free`] when done. This releases * the library-allocated `crypto_version` buffer. All other fields are inline. * * `ct_opk` and `opk_id` are only valid when `has_opk == 0x01`. * # Stack Size * * This struct is ~4.7 KiB due to three inline `[u8; 1120]` ciphertext * arrays plus a `[u8; 1216]` ephemeral key. Inline arrays avoid heap * allocation and lifetime management for FFI callers. Binding authors * targeting small-stack runtimes (e.g., Go goroutines with 8 KiB initial * stacks) should heap-allocate this struct or use `Box>`. */ typedef struct SolitonDecodedSessionInit { /** * UTF-8 crypto version string (library-allocated). */ struct SolitonBuf crypto_version; /** * SHA3-256(Alice.IK_pub) — 32 bytes, inline. */ uint8_t sender_fp[32]; /** * SHA3-256(Bob.IK_pub) — 32 bytes, inline. */ uint8_t recipient_fp[32]; /** * Alice's ephemeral X-Wing public key — 1216 bytes, inline. */ uint8_t sender_ek[SOLITON_XWING_PK_SIZE]; /** * X-Wing ciphertext to Bob's IK — 1120 bytes, inline. */ uint8_t ct_ik[SOLITON_XWING_CT_SIZE]; /** * X-Wing ciphertext to Bob's SPK — 1120 bytes, inline. */ uint8_t ct_spk[SOLITON_XWING_CT_SIZE]; /** * Bob's signed pre-key ID. */ uint32_t spk_id; /** * 0x01 if a one-time pre-key was used; 0x00 otherwise. */ uint8_t has_opk; /** * X-Wing ciphertext to Bob's OPK — 1120 bytes, inline. Valid only when `has_opk == 0x01`. * * `[u8; N]` has alignment 1, so this field directly follows `has_opk` * with no padding between them. */ uint8_t ct_opk[SOLITON_XWING_CT_SIZE]; /** * Bob's one-time pre-key ID. Valid only when `has_opk == 0x01`. * * `#[repr(C)]` inserts 3 bytes of padding between `ct_opk` and this * `u32` field to satisfy its 4-byte alignment requirement. The struct is * zero-filled by `soliton_kex_decode_session_init` before population, so * padding bytes are always zeroed. */ uint32_t opk_id; } SolitonDecodedSessionInit; /** * Free a library-allocated buffer, zeroizing its contents first. * * All buffers are zeroized before freeing to prevent sensitive data from * lingering in freed heap memory. After calling this, the buffer's `ptr` * is set to null and `len` to 0, making double-free a safe no-op. * Safe to call with a null `buf` pointer or a buf whose `ptr` is null (no-op). * * Deallocation uses the internally stored allocation size (in the header * at `ptr - 8`), not the caller-visible `len` — a buggy caller that * modifies `len` cannot corrupt the heap or skip zeroization. */ void soliton_buf_free(struct SolitonBuf *buf); /** * Zeroize a caller-owned memory region, guaranteed not to be optimized out. * * Standard C `memset` may be elided by the compiler if the buffer is not * read afterward. This function delegates to the `zeroize` crate, which * uses volatile writes to ensure the zeroing is never removed by * optimization passes. FFI consumers should call this on any buffer that * held secret material (e.g., chain keys copied out of `soliton_ratchet_encrypt_first`). * * `ptr`: pointer to the memory region to zeroize. * `len`: number of bytes to zeroize. Zero is a valid no-op. * * Safe to call with a null `ptr` (no-op). */ void soliton_zeroize(uint8_t *ptr, uintptr_t len); /** * Return the soliton version string (null-terminated, static lifetime). */ const char *soliton_version(void); /** * Fill `buf` with `len` cryptographically random bytes from the OS CSPRNG. * * `buf`: caller-allocated output buffer (at least `len` bytes). * `len`: number of random bytes to generate. Zero is a valid no-op. * Maximum 256 MiB (consistent with other CAPI length caps). * * Returns 0 on success, `NullPointer` if `buf` is null, * `InvalidLength` if `len` exceeds 256 MiB. */ int32_t soliton_random_bytes(uint8_t *buf, uintptr_t len); /** * Compute SHA3-256 hash. * * `data` / `data_len`: input data. Null with `data_len = 0` hashes the empty string. * `out` / `out_len`: caller-allocated buffer, must be exactly 32 bytes. * * Returns 0 on success, `SOLITON_ERR_INVALID_LENGTH` if `out_len != 32` or * `data_len > 256 MiB`, `SOLITON_ERR_NULL_POINTER` if `out` is null or * `data` is null with `data_len != 0`. */ int32_t soliton_sha3_256(const uint8_t *data, uintptr_t data_len, uint8_t *out, uintptr_t out_len); /** * Generate a LO composite identity keypair. * * On success: * - `pk_out` receives the public key bytes. * - `sk_out` receives the secret key bytes. * - `fingerprint_hex_out` receives the hex fingerprint (null-terminated, caller frees with `soliton_buf_free`). * * # Security * * `sk_out` contains the raw 2496-byte identity secret key. Free it with * `soliton_buf_free`, NOT `free()`. `soliton_buf_free` zeroizes * the buffer contents before releasing memory; `free()` does not. * During construction, a brief two-copy window exists: the `IdentitySecretKey` * (zeroized on drop) and the `SolitonBuf`-held copy coexist until the * former drops at function return. * * Returns 0 on success, negative on error. */ int32_t soliton_identity_generate(struct SolitonBuf *pk_out, struct SolitonBuf *sk_out, struct SolitonBuf *fingerprint_hex_out); /** * Compute the raw fingerprint (SHA3-256) of a public key. * * `pk` / `pk_len`: identity public key (must be `SOLITON_PUBLIC_KEY_SIZE` bytes). * `out` / `out_len`: caller-allocated buffer, must be exactly 32 bytes. * * Returns 0 on success, `InvalidLength` if `pk_len` or `out_len` is wrong. */ int32_t soliton_identity_fingerprint(const uint8_t *pk, uintptr_t pk_len, uint8_t *out, uintptr_t out_len); /** * Sign a message with hybrid Ed25519 + ML-DSA-65. * * `sk` / `sk_len`: secret key bytes. * `message` / `message_len`: message to sign. * `sig_out`: receives the signature buffer (caller frees with `soliton_buf_free`). * * Returns 0 on success, negative on error. */ int32_t soliton_identity_sign(const uint8_t *sk, uintptr_t sk_len, const uint8_t *message, uintptr_t message_len, struct SolitonBuf *sig_out); /** * Verify a hybrid signature (Ed25519 + ML-DSA-65). * * `pk` / `pk_len`: identity public key. * `message` / `message_len`: signed message. Null with `message_len = 0` verifies an empty message. * `sig` / `sig_len`: hybrid signature to verify. * * Returns 0 if valid, `VerificationFailed` (-3) if the signature is invalid, * or another negative error code on parse failure. */ int32_t soliton_identity_verify(const uint8_t *pk, uintptr_t pk_len, const uint8_t *message, uintptr_t message_len, const uint8_t *sig, uintptr_t sig_len); /** * Encapsulate to an identity key's X-Wing component. * * `pk` / `pk_len`: identity public key. * `ct_out`: receives X-Wing ciphertext (caller frees). * `ss_out` / `ss_out_len`: must point to exactly 32 bytes for the shared secret. * On error, `ss_out` is zeroed. Caller must check return code before using. * * # Security * * The 32-byte shared secret written to `ss_out` is raw key material. The * caller must zeroize `ss_out` when it is no longer needed (e.g., * `explicit_bzero` on POSIX, `SecureZeroMemory` on Windows). * * Returns 0 on success, negative on error. */ int32_t soliton_identity_encapsulate(const uint8_t *pk, uintptr_t pk_len, struct SolitonBuf *ct_out, uint8_t *ss_out, uintptr_t ss_out_len); /** * Decapsulate using an identity key's X-Wing component. * * `sk` / `sk_len`: identity secret key. * `ct` / `ct_len`: X-Wing ciphertext. * `ss_out` / `ss_out_len`: must point to exactly 32 bytes for the shared secret. * On error, `ss_out` is zeroed. Caller must check return code before using. * * # Security * * The 32-byte shared secret written to `ss_out` is raw key material. The * caller must zeroize `ss_out` when it is no longer needed (e.g., * `explicit_bzero` on POSIX, `SecureZeroMemory` on Windows). * * Returns 0 on success, negative on error. */ int32_t soliton_identity_decapsulate(const uint8_t *sk, uintptr_t sk_len, const uint8_t *ct, uintptr_t ct_len, uint8_t *ss_out, uintptr_t ss_out_len); /** * Server-side: generate an authentication challenge. * * `client_pk` / `client_pk_len`: client's identity public key. * `ct_out`: receives the X-Wing ciphertext (caller frees). * `token_out` / `token_out_len`: must point to exactly 32 bytes for the expected token. * * # Security * * `token_out` receives the expected authentication token (secret). The caller * must zeroize it after verifying the client's proof. * * Returns 0 on success, negative on error. */ int32_t soliton_auth_challenge(const uint8_t *client_pk, uintptr_t client_pk_len, struct SolitonBuf *ct_out, uint8_t *token_out, uintptr_t token_out_len); /** * Client-side: respond to an authentication challenge. * * `client_sk` / `client_sk_len`: client's identity secret key. * `ct` / `ct_len`: ciphertext from the challenge. * `proof_out` / `proof_out_len`: must point to exactly 32 bytes for the proof. * * # Security * * `proof_out` receives a sensitive authentication proof. The caller must * zeroize it after transmitting. * * Returns 0 on success, negative on error. */ int32_t soliton_auth_respond(const uint8_t *client_sk, uintptr_t client_sk_len, const uint8_t *ct, uintptr_t ct_len, uint8_t *proof_out, uintptr_t proof_out_len); /** * Server-side: verify an authentication proof. * * `expected_token`: 32 bytes. * `proof`: 32 bytes. * * # Security * * Comparison is constant-time (`subtle::ConstantTimeEq`), preventing timing * side-channels that leak information about the expected token. * * Returns 0 if valid, -3 (VerificationFailed) if invalid. */ int32_t soliton_auth_verify(const uint8_t *expected_token, uintptr_t expected_token_len, const uint8_t *proof, uintptr_t proof_len); /** * Initialize ratchet state for Alice (initiator). * * `root_key`: 32 bytes. * `chain_key`: 32 bytes (initial epoch key for counter-mode derivation). * `local_fp`: local identity fingerprint (32 bytes). * `remote_fp`: remote identity fingerprint (32 bytes). * `ek_pk` / `ek_pk_len`: Alice's ephemeral X-Wing public key. * `ek_sk` / `ek_sk_len`: Alice's ephemeral X-Wing secret key. * `out`: receives an opaque `SolitonRatchet*` (caller frees with `soliton_ratchet_free`). * * # Security * * `root_key` and `chain_key` are **copied** into the ratchet state. The caller * should zeroize its copies immediately after this call (e.g., `explicit_bzero` on POSIX, `SecureZeroMemory` on Windows). * * Returns 0 on success, negative on error. */ int32_t soliton_ratchet_init_alice(const uint8_t *root_key, uintptr_t root_key_len, const uint8_t *chain_key, uintptr_t chain_key_len, const uint8_t *local_fp, uintptr_t local_fp_len, const uint8_t *remote_fp, uintptr_t remote_fp_len, const uint8_t *ek_pk, uintptr_t ek_pk_len, const uint8_t *ek_sk, uintptr_t ek_sk_len, struct SolitonRatchet **out); /** * Initialize ratchet state for Bob (responder). * * `root_key`: 32 bytes. * `chain_key`: 32 bytes (initial epoch key for counter-mode derivation). * `local_fp`: local identity fingerprint (32 bytes). * `remote_fp`: remote identity fingerprint (32 bytes). * `peer_ek` / `peer_ek_len`: Alice's ephemeral X-Wing public key. * `out`: receives an opaque `SolitonRatchet*` (caller frees with `soliton_ratchet_free`). * * # Security * * `root_key` and `chain_key` are **copied** into the ratchet state. The caller * should zeroize its copies immediately after this call (e.g., `explicit_bzero` on POSIX, `SecureZeroMemory` on Windows). * * Returns 0 on success, negative on error. */ int32_t soliton_ratchet_init_bob(const uint8_t *root_key, uintptr_t root_key_len, const uint8_t *chain_key, uintptr_t chain_key_len, const uint8_t *local_fp, uintptr_t local_fp_len, const uint8_t *remote_fp, uintptr_t remote_fp_len, const uint8_t *peer_ek, uintptr_t peer_ek_len, struct SolitonRatchet **out); /** * Encrypt a message using the ratchet. * * `ratchet`: opaque ratchet state (fingerprints bound at init time). * `plaintext` / `plaintext_len`: message to encrypt. * `out`: receives the encrypted message (caller frees components with `soliton_encrypted_message_free`). * * Nonce construction: the ratchet derives a 24-byte XChaCha20-Poly1305 nonce * from the message counter (`n`). See Specification.md §6.5 for the construction. * AAD is built internally from the sender/recipient fingerprints (bound at * init time) and the ratchet header fields. * * # Possible Errors * * - `SOLITON_ERR_NULL_POINTER` (-13): any pointer argument is null. * - `SOLITON_ERR_INVALID_DATA` (-17): magic check failed, or session is dead * (post-reset or all-zero root key). * - `SOLITON_ERR_CONCURRENT_ACCESS` (-18): ratchet handle is in use by another call. * - `SOLITON_ERR_CHAIN_EXHAUSTED` (-15): send counter reached `u32::MAX`. * - `SOLITON_ERR_AEAD` (-4): AEAD encryption failed (structurally * infallible for valid inputs; defense-in-depth). On this error, the ratchet * state is permanently reset (all keys zeroized). The handle remains valid * for `soliton_ratchet_free` but all subsequent encrypt/decrypt calls will * return `SOLITON_ERR_INVALID_DATA`. The caller must re-establish the session * via KEX. * * On error, `*out` is zeroed — `soliton_encrypted_message_free` is safe to call * (it will be a no-op on the zeroed struct). * * Returns 0 on success, negative on error. */ int32_t soliton_ratchet_encrypt(struct SolitonRatchet *ratchet, const uint8_t *plaintext, uintptr_t plaintext_len, struct SolitonEncryptedMessage *out); /** * Decrypt a message using the ratchet. * * Parameters correspond to fields of `SolitonEncryptedMessage` returned by * `soliton_ratchet_encrypt`: * * - `ratchet_pk` / `ratchet_pk_len` ← `msg.header.ratchet_pk` * - `kem_ct` / `kem_ct_len` ← `msg.header.kem_ct` (pass null + 0 if `.ptr` is null) * - `n` ← `msg.header.n` * - `pn` ← `msg.header.pn` * - `ciphertext` / `ciphertext_len` ← `msg.ciphertext` * * `ratchet`: opaque ratchet state (fingerprints bound at init time). * `plaintext_out`: receives the decrypted message (caller frees). * * # Possible Errors * * - `SOLITON_ERR_NULL_POINTER` (-13): any required pointer argument is null. * - `SOLITON_ERR_INVALID_LENGTH` (-1): `ratchet_pk_len` is not exactly * `SOLITON_XWING_PK_SIZE` (1216), or `kem_ct_len` is not exactly * `SOLITON_XWING_CT_SIZE` (1120) when `kem_ct` is non-null. * - `SOLITON_ERR_CONCURRENT_ACCESS` (-18): ratchet handle is in use by another call. * - `SOLITON_ERR_INVALID_DATA` (-17): magic check failed, session is dead * (post-reset or all-zero root key), or message is structurally invalid. * - `SOLITON_ERR_AEAD` (-4): AEAD decryption failed (invalid * ciphertext, wrong key, or tampered message). * - `SOLITON_ERR_CHAIN_EXHAUSTED` (-15): message counter `n` is `u32::MAX`, * or `recv_seen` set is full (MAX_RECV_SEEN). * - `SOLITON_ERR_DUPLICATE` (-7): message already decrypted (duplicate counter). * - `SOLITON_ERR_DECAPSULATION` (-2): KEM decapsulation failed during * a new-epoch ratchet step. * * On **all** errors, the ratchet state performs a **full rollback** to its * pre-call state — the session remains functional and subsequent * encrypt/decrypt calls will succeed normally. This differs from `encrypt`, * where AEAD failure is catastrophic and triggers permanent reset. * * # Security * * The returned buffer contains sensitive plaintext. Free it with * `soliton_buf_free`, NOT `free()`. `soliton_buf_free` zeroizes * the buffer contents before releasing memory; `free()` does not. * * Returns 0 on success, negative on error. */ int32_t soliton_ratchet_decrypt(struct SolitonRatchet *ratchet, const uint8_t *ratchet_pk, uintptr_t ratchet_pk_len, const uint8_t *kem_ct, uintptr_t kem_ct_len, uint32_t n, uint32_t pn, const uint8_t *ciphertext, uintptr_t ciphertext_len, struct SolitonBuf *plaintext_out); /** * Encrypt the first message of a session (session init payload). * * `chain_key` / `chain_key_len`: the `initial_chain_key` from * [`SolitonInitiatedSession`] (exactly 32 bytes). * `plaintext` / `plaintext_len`: message to encrypt. * `aad` / `aad_len`: additional authenticated data. Construct using * `soliton_kex_build_first_message_aad` with the sender/recipient * fingerprints and the encoded `SessionInit` from step 1. * `payload_out`: receives nonce || ciphertext (caller frees). * `ratchet_init_key_out` / `ratchet_init_key_out_len`: receives the derived * chain key for `soliton_ratchet_init_alice` (exactly 32 bytes). * * # Caller Obligations — Key Protocol Sequence * * The KEX→ratchet handoff requires three steps in order: * * 1. `soliton_kex_initiate` → produces `SolitonInitiatedSession` with * `initial_chain_key` and `root_key`. * 2. **This function** → takes `initial_chain_key` as input, outputs * `ratchet_init_key_out` (the same epoch key, passed through unchanged * under counter-mode). * 3. `soliton_ratchet_init_alice` → takes `root_key` from step 1 and * `ratchet_init_key_out` from step 2 as `chain_key`. * * **Note:** Under counter-mode, `ratchet_init_key_out` equals * `initial_chain_key` (the epoch key passes through unchanged). Step 2 * must still be called — it encrypts the first message and the API * enforces the correct ordering via ownership semantics on the Rust side. * * # Security * * `ratchet_init_key_out` contains sensitive chain-key material. Zeroize it * (e.g., `explicit_bzero` on POSIX, `SecureZeroMemory` on Windows) immediately after passing it to * `soliton_ratchet_init_alice`. Do not store it beyond that call. * * Returns 0 on success, negative on error. */ int32_t soliton_ratchet_encrypt_first(const uint8_t *chain_key, uintptr_t chain_key_len, const uint8_t *plaintext, uintptr_t plaintext_len, const uint8_t *aad, uintptr_t aad_len, struct SolitonBuf *payload_out, uint8_t *ratchet_init_key_out, uintptr_t ratchet_init_key_out_len); /** * Decrypt the first message of a session. * * `chain_key` / `chain_key_len`: 32 bytes exactly. * `encrypted_payload` / `encrypted_payload_len`: nonce || ciphertext. * `aad` / `aad_len`: additional authenticated data. * `plaintext_out`: receives the decrypted message (caller frees). * `ratchet_init_key_out` / `ratchet_init_key_out_len`: receives the chain key * for `soliton_ratchet_init_bob` (exactly 32 bytes). Do NOT use the * `chain_key` input for ratchet init. * * # Security * * `ratchet_init_key_out` contains sensitive chain-key material. Zeroize it * (e.g., `explicit_bzero` on POSIX, `SecureZeroMemory` on Windows) immediately after passing it to * `soliton_ratchet_init_bob`. Do not store it beyond that call. * * The returned buffer contains sensitive plaintext. Free it with * `soliton_buf_free`, NOT `free()`. `soliton_buf_free` zeroizes * the buffer contents before releasing memory; `free()` does not. * * Returns 0 on success, negative on error. */ int32_t soliton_ratchet_decrypt_first(const uint8_t *chain_key, uintptr_t chain_key_len, const uint8_t *encrypted_payload, uintptr_t encrypted_payload_len, const uint8_t *aad, uintptr_t aad_len, struct SolitonBuf *plaintext_out, uint8_t *ratchet_init_key_out, uintptr_t ratchet_init_key_out_len); /** * Reset a ratchet session, zeroizing all state. * * `ratchet`: opaque ratchet state to reset in-place. * * Returns 0 on success, negative on error. */ int32_t soliton_ratchet_reset(struct SolitonRatchet *ratchet); /** * Free a ratchet state object, zeroizing all contained key material. * * `ratchet`: pointer to the caller's `SolitonRatchet*` variable. After * freeing, the caller's pointer is set to NULL, making double-free a no-op. * Null outer pointer and null inner pointer are both no-ops (returns 0). * * Returns 0 on success, `SOLITON_ERR_CONCURRENT_ACCESS` (-18) if the handle * is currently in use by another operation. On concurrent-access failure, the * handle is NOT freed — the caller must retry after the in-flight operation * completes. GC finalizers that run exactly once must ensure no operations * are in flight before calling free. */ int32_t soliton_ratchet_free(struct SolitonRatchet **ratchet); /** * Free an encrypted message's internal buffers — does NOT free the * `SolitonEncryptedMessage` struct itself (it is a value type written * into caller-provided storage, not heap-allocated by soliton). * * `msg`: encrypted message pointer (null-safe; no-op if null). * * Frees each buffer (ratchet_pk, kem_ct, ciphertext) via `soliton_buf_free` * (which zeroizes before freeing). */ void soliton_encrypted_message_free(struct SolitonEncryptedMessage *msg); /** * Create a storage keyring with one initial key. * * `key`: 32-byte encryption key (must not be all-zero). * `version`: key version (1-255). Version 0 returns * `SOLITON_ERR_VERSION`. * `out`: receives the opaque `SolitonKeyRing*` (caller frees with `soliton_keyring_free`). * * Returns 0 on success, negative error code on failure. */ int32_t soliton_keyring_new(const uint8_t *key, uintptr_t key_len, uint8_t version, struct SolitonKeyRing **out); /** * Add a key to the storage keyring. * * `key`: 32-byte encryption key. * `version`: key version (1-255). Version 0 returns `SOLITON_ERR_VERSION`. * `make_active`: if non-zero, this key becomes the active key for new writes. * * If `version` matches the current active version and `make_active` is 0, * returns `SOLITON_ERR_INVALID_DATA` — replacing the active key's material * without re-activating would silently invalidate existing blobs. * * Returns 0 on success, negative error code on failure. */ int32_t soliton_keyring_add_key(struct SolitonKeyRing *keyring, const uint8_t *key, uintptr_t key_len, uint8_t version, int32_t make_active); /** * Remove a key version from the storage keyring. * * `keyring`: opaque storage keyring. * `version`: key version to remove. Version 0 returns `SOLITON_ERR_VERSION`. * Removing the active version returns `SOLITON_ERR_INVALID_DATA` — callers must * set a new active key via `soliton_keyring_add_key(make_active=1)` before removing * the old one. * * Returns 0 on success, negative on error. */ int32_t soliton_keyring_remove_key(struct SolitonKeyRing *keyring, uint8_t version); /** * Free a storage keyring, zeroizing all contained key material. * * `keyring`: pointer to the caller's `SolitonKeyRing*` variable. After * freeing, the caller's pointer is set to NULL, making double-free a no-op. * Null outer pointer and null inner pointer are both no-ops (returns 0). * * Returns 0 on success, `SOLITON_ERR_CONCURRENT_ACCESS` (-18) if the handle * is currently in use. See `soliton_ratchet_free` for retry semantics. */ int32_t soliton_keyring_free(struct SolitonKeyRing **keyring); /** * Encrypt data for storage. * * `keyring`: storage keyring (uses the active key). * `plaintext` / `plaintext_len`: data to encrypt. * `channel_id`: null-terminated channel ID string. * `segment_id`: null-terminated segment ID string. * `compress`: non-zero to enable zstd compression. * `blob_out`: receives the encrypted blob (caller frees with `soliton_buf_free`). * * # Possible Errors * * - `SOLITON_ERR_NULL_POINTER` (-13): any required pointer argument is null. * - `SOLITON_ERR_INVALID_LENGTH` (-1): `plaintext_len` exceeds 256 MiB. * - `SOLITON_ERR_INVALID_DATA` (-17): magic check failed, `channel_id` or * `segment_id` is not valid UTF-8, or keyring has no active key. * - `SOLITON_ERR_CONCURRENT_ACCESS` (-18): keyring handle is in use by another call. * - `SOLITON_ERR_AEAD` (-4): AEAD encryption failed (defense-in-depth). * * # Security * * When compression is enabled, zstd internal buffers may retain plaintext * in freed heap memory. * * Returns 0 on success, negative on error. */ int32_t soliton_storage_encrypt(const struct SolitonKeyRing *keyring, const uint8_t *plaintext, uintptr_t plaintext_len, const char *channel_id, const char *segment_id, int32_t compress, struct SolitonBuf *blob_out); /** * Decrypt a storage blob. * * `keyring`: storage keyring (looks up key by version byte in blob). * `blob` / `blob_len`: encrypted blob. * `channel_id`: null-terminated channel ID string. * `segment_id`: null-terminated segment ID string. * `plaintext_out`: receives the decrypted data (caller frees with `soliton_buf_free`). * * # Possible Errors * * - `SOLITON_ERR_NULL_POINTER` (-13): any required pointer argument is null. * - `SOLITON_ERR_INVALID_LENGTH` (-1): `blob_len` is 0 or exceeds 256 MiB. * - `SOLITON_ERR_CONCURRENT_ACCESS` (-18): keyring handle is in use by another call. * - `SOLITON_ERR_AEAD` (-4): any decryption or post-decryption failure. All * distinct internal errors (AEAD tag failure, key version not found, * decompression failure, unsupported flags, structurally invalid blob) are * collapsed to this single code to prevent error-oracle attacks. * - `SOLITON_ERR_INVALID_DATA` (-17): magic check failed, or `channel_id` / * `segment_id` is not valid UTF-8. * * # Security * * The returned buffer contains sensitive plaintext. Free it with * `soliton_buf_free`, NOT `free()`. `soliton_buf_free` zeroizes * the buffer contents before releasing memory; `free()` does not. * * Returns 0 on success, negative on error. */ int32_t soliton_storage_decrypt(const struct SolitonKeyRing *keyring, const uint8_t *blob, uintptr_t blob_len, const char *channel_id, const char *segment_id, struct SolitonBuf *plaintext_out); /** * Encrypt data for DM queue storage (§11.4.2). * * `keyring`: storage keyring (uses the active key). * `plaintext` / `plaintext_len`: data to encrypt. * `recipient_fp` / `recipient_fp_len`: recipient identity fingerprint * (exactly 32 bytes). * `batch_id`: null-terminated batch ID string. * `compress`: non-zero to enable zstd compression. * `blob_out`: receives the encrypted blob (caller frees with `soliton_buf_free`). * * # Possible Errors * * - `SOLITON_ERR_NULL_POINTER` (-13): any required pointer argument is null. * - `SOLITON_ERR_INVALID_DATA` (-17): magic check failed, `batch_id` is not * valid UTF-8, or keyring has no active key. * - `SOLITON_ERR_INVALID_LENGTH` (-1): `plaintext_len` exceeds 256 MiB, or * `recipient_fp_len` is not 32. * - `SOLITON_ERR_CONCURRENT_ACCESS` (-18): keyring handle is in use by another call. * - `SOLITON_ERR_AEAD` (-4): AEAD encryption failed (defense-in-depth). * * # Security * * The recipient fingerprint is bound into the AAD — a blob encrypted for * one recipient cannot be decrypted with a different fingerprint. * * Returns 0 on success, negative on error. */ int32_t soliton_dm_queue_encrypt(const struct SolitonKeyRing *keyring, const uint8_t *plaintext, uintptr_t plaintext_len, const uint8_t *recipient_fp, uintptr_t recipient_fp_len, const char *batch_id, int32_t compress, struct SolitonBuf *blob_out); /** * Decrypt a DM queue storage blob (§11.4.2). * * `keyring`: storage keyring (looks up key by version byte in blob). * `blob` / `blob_len`: encrypted blob. * `recipient_fp` / `recipient_fp_len`: recipient identity fingerprint * (exactly 32 bytes). * `batch_id`: null-terminated batch ID string. * `plaintext_out`: receives the decrypted data (caller frees with `soliton_buf_free`). * * # Possible Errors * * - `SOLITON_ERR_NULL_POINTER` (-13): any required pointer argument is null. * - `SOLITON_ERR_INVALID_LENGTH` (-1): `blob_len` is 0 or exceeds 256 MiB, * or `recipient_fp_len` is not 32. * - `SOLITON_ERR_CONCURRENT_ACCESS` (-18): keyring handle is in use by another call. * - `SOLITON_ERR_AEAD` (-4): any decryption or post-decryption failure. * - `SOLITON_ERR_INVALID_DATA` (-17): magic check failed, or `batch_id` * is not valid UTF-8. * * # Security * * The returned buffer contains sensitive plaintext. Free it with * `soliton_buf_free`, NOT `free()`. * * Returns 0 on success, negative on error. */ int32_t soliton_dm_queue_decrypt(const struct SolitonKeyRing *keyring, const uint8_t *blob, uintptr_t blob_len, const uint8_t *recipient_fp, uintptr_t recipient_fp_len, const char *batch_id, struct SolitonBuf *plaintext_out); /** * Compute HMAC-SHA3-256. * * `key` / `key_len`: HMAC key. * `data` / `data_len`: message to authenticate. * `out` / `out_len`: must be exactly 32 bytes. * * Returns 0 on success, negative on error. */ int32_t soliton_hmac_sha3_256(const uint8_t *key, uintptr_t key_len, const uint8_t *data, uintptr_t data_len, uint8_t *out, uintptr_t out_len); /** * Constant-time comparison of two 32-byte HMAC tags. * * `tag_a`: pointer to 32-byte computed HMAC tag. * `tag_b`: pointer to 32-byte expected HMAC tag. * * # Security * * Uses `subtle::ConstantTimeEq` — execution time does not depend on which * bytes differ, preventing timing side-channels. Callers should use this * instead of `memcmp` to avoid leaking information about the tag value. * * Returns 0 if the tags match, `SOLITON_ERR_VERIFICATION` if they differ, * `SOLITON_ERR_NULL_POINTER` if either pointer is null. */ int32_t soliton_hmac_sha3_256_verify(const uint8_t *tag_a, uintptr_t tag_a_len, const uint8_t *tag_b, uintptr_t tag_b_len); /** * Compute HKDF-SHA3-256 (extract-and-expand). * * `salt` / `salt_len`: HKDF salt. * `ikm` / `ikm_len`: input keying material. * `info` / `info_len`: context and application-specific info. * `out` / `out_len`: output buffer (1-8160 bytes; `out_len == 0` returns * `SOLITON_ERR_INVALID_LENGTH`). * * Returns 0 on success, negative on error. */ int32_t soliton_hkdf_sha3_256(const uint8_t *salt, uintptr_t salt_len, const uint8_t *ikm, uintptr_t ikm_len, const uint8_t *info, uintptr_t info_len, uint8_t *out, uintptr_t out_len); /** * Encrypt data with XChaCha20-Poly1305. * * `key`: 32-byte encryption key. * `nonce`: 24-byte nonce. * `plaintext` / `plaintext_len`: data to encrypt. * `aad` / `aad_len`: additional authenticated data. * `ciphertext_out`: receives ciphertext || 16-byte tag (caller frees). * * Returns 0 on success, negative on error. */ int32_t soliton_aead_encrypt(const uint8_t *key, uintptr_t key_len, const uint8_t *nonce, uintptr_t nonce_len, const uint8_t *plaintext, uintptr_t plaintext_len, const uint8_t *aad, uintptr_t aad_len, struct SolitonBuf *ciphertext_out); /** * Decrypt XChaCha20-Poly1305 ciphertext. * * `key`: 32-byte encryption key. * `nonce`: 24-byte nonce. * `ciphertext` / `ciphertext_len`: ciphertext || 16-byte tag. * `aad` / `aad_len`: additional authenticated data. * `plaintext_out`: receives decrypted plaintext (caller frees). * * # Security * * The returned buffer contains sensitive plaintext. Free it with * `soliton_buf_free`, NOT `free()`. `soliton_buf_free` zeroizes * the buffer contents before releasing memory; `free()` does not. * * Returns 0 on success, negative on error (authentication failure). */ int32_t soliton_aead_decrypt(const uint8_t *key, uintptr_t key_len, const uint8_t *nonce, uintptr_t nonce_len, const uint8_t *ciphertext, uintptr_t ciphertext_len, const uint8_t *aad, uintptr_t aad_len, struct SolitonBuf *plaintext_out); /** * Derive key material from a passphrase using Argon2id (RFC 9106). * * `password` / `password_len`: passphrase bytes. May be null with `password_len = 0`. * `salt` / `salt_len`: random salt; must be at least 8 bytes. * `m_cost`: memory cost in KiB (recommended 65536 = 64 MiB; maximum 4194304 = 4 GiB). * Minimum is enforced by the upstream Argon2 library (currently 8 KiB). * `t_cost`: time cost — number of passes (recommended 3; maximum 256). * Minimum is enforced by the upstream Argon2 library (currently 1). * `p_cost`: parallelism — number of lanes (recommended 4; maximum 256). * Minimum is enforced by the upstream Argon2 library (currently 1). * `out` / `out_len`: caller-allocated output buffer for derived key material * (1-4096 bytes). * * Presets: use m=65536, t=3, p=4 for locally stored keypairs; m=19456, t=2, p=1 * for interactive logins. * * Returns 0 on success, `SOLITON_ERR_INVALID_DATA` if cost params exceed * maximum bounds, `SOLITON_ERR_INVALID_LENGTH` if salt or output size is * out of range, negative on other errors. */ int32_t soliton_argon2id(const uint8_t *password, uintptr_t password_len, const uint8_t *salt, uintptr_t salt_len, uint32_t m_cost, uint32_t t_cost, uint32_t p_cost, uint8_t *out, uintptr_t out_len); /** * Sign a pre-key's public key with the identity key. * * `ik_sk` / `ik_sk_len`: identity secret key. * `spk_pub` / `spk_pub_len`: pre-key public key (X-Wing, 1216 bytes). * `sig_out`: receives the hybrid signature (caller frees). * * Returns 0 on success, negative on error. */ int32_t soliton_kex_sign_prekey(const uint8_t *ik_sk, uintptr_t ik_sk_len, const uint8_t *spk_pub, uintptr_t spk_pub_len, struct SolitonBuf *sig_out); /** * Initiate a session (Alice's side, §5.4). * * `alice_ik_pk` / `alice_ik_pk_len`: Alice's identity public key. * `alice_ik_sk` / `alice_ik_sk_len`: Alice's identity secret key (used to sign the SessionInit). * `bob_ik_pk` / `bob_ik_pk_len`: Bob's identity public key (from bundle). * `bob_spk_pub` / `bob_spk_pub_len`: Bob's signed pre-key public key. * `bob_spk_id`: Bob's signed pre-key ID. * `bob_spk_sig` / `bob_spk_sig_len`: Hybrid signature over Bob's SPK. * `bob_opk_pub` / `bob_opk_pub_len`: Bob's one-time pre-key (null if absent). * `bob_opk_id`: Bob's one-time pre-key ID (unused when opk is null; value is discarded). * `crypto_version`: null-terminated crypto version string. * `out`: receives the initiated session result, including `sender_sig` (Alice's * hybrid signature over the encoded SessionInit). * * # Security * * `out.ek_sk` contains the ephemeral secret key. A second heap copy exists * briefly alongside the original inside the `InitiatedSession` struct. * Call `soliton_kex_initiated_session_free` as soon as the session result * is no longer needed to minimize the lifetime of both copies. * Do NOT use manual `free()` calls — the free function zeroizes secret key * buffers before releasing memory. * * # Possible Errors * * - `SOLITON_ERR_NULL_POINTER` (-13): any required pointer argument is null. * - `SOLITON_ERR_INVALID_LENGTH` (-1): a key or signature buffer has the wrong * size. Required sizes: `alice_ik_pk_len` = `SOLITON_PUBLIC_KEY_SIZE` (3200), * `alice_ik_sk_len` = `SOLITON_SECRET_KEY_SIZE` (2496), * `bob_ik_pk_len` = `SOLITON_PUBLIC_KEY_SIZE` (3200), * `bob_spk_pub_len` = `SOLITON_XWING_PK_SIZE` (1216), * `bob_spk_sig_len` = `SOLITON_HYBRID_SIG_SIZE` (3373), * `bob_opk_pub_len` (if non-null) = `SOLITON_XWING_PK_SIZE` (1216). * - `SOLITON_ERR_INVALID_DATA` (-17): all-zero key. * - `SOLITON_ERR_BUNDLE` (-5): IK mismatch, SPK signature verification failed, * or crypto version mismatch (collapsed to avoid oracle). * - `SOLITON_ERR_CRYPTO_VERSION` (-16): unrecognized `crypto_version` in * the session init wire format (decode path, not bundle verification). * * Returns 0 on success, negative on error. On error, `out` is zeroed; * it is safe (but unnecessary) to call `soliton_kex_initiated_session_free` on it. */ int32_t soliton_kex_initiate(const uint8_t *alice_ik_pk, uintptr_t alice_ik_pk_len, const uint8_t *alice_ik_sk, uintptr_t alice_ik_sk_len, const uint8_t *bob_ik_pk, uintptr_t bob_ik_pk_len, const uint8_t *bob_spk_pub, uintptr_t bob_spk_pub_len, uint32_t bob_spk_id, const uint8_t *bob_spk_sig, uintptr_t bob_spk_sig_len, const uint8_t *bob_opk_pub, uintptr_t bob_opk_pub_len, uint32_t bob_opk_id, const char *crypto_version, struct SolitonInitiatedSession *out); /** * Free an initiated session result. * * Zeroizes inline key material (root_key, initial_chain_key, * sender_ik_fingerprint, recipient_ik_fingerprint) and frees library-allocated * buffers (session_init_encoded, ek_pk, ek_sk, ct_ik, ct_spk, ct_opk, sender_sig). */ void soliton_kex_initiated_session_free(struct SolitonInitiatedSession *session); /** * Free a received session result, zeroizing inline key material. * * Zeroizes `root_key` and `chain_key` and frees `peer_ek`. * Safe to call on a null pointer (no-op). */ void soliton_kex_received_session_free(struct SolitonReceivedSession *session); /** * Receive a session init (Bob's side, §5.5). * * `bob_ik_pk` / `bob_ik_pk_len`: Bob's identity public key. * `bob_ik_sk` / `bob_ik_sk_len`: Bob's identity secret key. * `alice_ik_pk` / `alice_ik_pk_len`: Alice's identity public key (from the trust store). * `wire`: wire fields of the received SessionInit. Must be non-null; all pointer * fields except `ct_opk` / `opk_sk` must be non-null. * `decap_keys`: Bob's pre-key secret keys, fetched from the key store using * `wire->spk_id` and `wire->opk_id`. Must be non-null; `spk_sk` must be non-null. * `out`: receives the derived session keys on success; zeroed on error. Must be non-null. * * # Security * * `out->root_key` and `out->chain_key` contain sensitive key material. Zeroize * them (via `soliton_kex_received_session_free` or `explicit_bzero`) immediately * after passing them to `soliton_ratchet_decrypt_first` and `soliton_ratchet_init_bob` * respectively. Do not store or copy them beyond those calls. * * # Caller Obligations — Key-ID Mapping * * `wire->spk_id` and `wire->opk_id` are opaque 4-byte identifiers chosen by * the sender. The caller must map these IDs to the correct secret keys in * `decap_keys` using its own key store. This library performs no key-ID lookup * or validation — if the wrong secret key is supplied for a given ID, the KEX * will silently produce incorrect shared secrets and the first ratchet message * will fail AEAD authentication. The caller must also handle unknown or expired * key IDs (e.g., return an application-level error) before invoking this function. * * # Possible Errors * * - `SOLITON_ERR_NULL_POINTER` (-13): any required pointer argument is null. * - `SOLITON_ERR_INVALID_LENGTH` (-1): a key, ciphertext, or signature buffer * has the wrong size. * - `SOLITON_ERR_INVALID_DATA` (-17): self-pair (Alice IK == Bob IK), all-zero * key, sender signature verification failed, co-presence violation * (`ct_opk`/`opk_sk` must both be present or both absent), or crypto version * mismatch. * - `SOLITON_ERR_DECAPSULATION` (-2): X-Wing decapsulation failed for IK, SPK, * or OPK ciphertext. * - `SOLITON_ERR_CRYPTO_VERSION` (-16): unrecognized `crypto_version`. * * Returns 0 on success, negative on error. On error `out` is zeroed; it is * safe (but unnecessary) to call `soliton_kex_received_session_free` on it. */ int32_t soliton_kex_receive(const uint8_t *bob_ik_pk, uintptr_t bob_ik_pk_len, const uint8_t *bob_ik_sk, uintptr_t bob_ik_sk_len, const uint8_t *alice_ik_pk, uintptr_t alice_ik_pk_len, const struct SolitonSessionInitWire *wire, const struct SolitonSessionDecapKeys *decap_keys, struct SolitonReceivedSession *out); /** * Generate a verification phrase from two identity public keys. * * `pk_a` / `pk_a_len`: first identity public key. * `pk_b` / `pk_b_len`: second identity public key. * `phrase_out`: receives the phrase as a null-terminated string (caller frees). * * Returns 0 on success, negative on error. */ int32_t soliton_verification_phrase(const uint8_t *pk_a, uintptr_t pk_a_len, const uint8_t *pk_b, uintptr_t pk_b_len, struct SolitonBuf *phrase_out); /** * Serialize a ratchet state to bytes, consuming the ratchet. * * `ratchet`: pointer to the opaque ratchet pointer. On success, `*ratchet` is * set to null — the ratchet is consumed and must not be used or freed * after this call. To continue using the ratchet, deserialize the * returned bytes with `soliton_ratchet_from_bytes`. * `data_out`: receives the serialized state (caller frees with `soliton_buf_free`). * `epoch_out`: if non-null, receives the monotonic epoch embedded in the * serialized blob. The caller should persist this value and pass it as * `min_epoch` to [`soliton_ratchet_from_bytes_with_min_epoch`] on the next * load to enforce anti-rollback protection. If null, the epoch is not * returned (backward-compatible). * * # Ownership * * This function consumes the ratchet to prevent session forking: if the caller * could serialize and keep using the original, a later restore would produce a * duplicate ratchet with the same chain keys and counters — catastrophic * AEAD nonce reuse. * * # Possible Errors * * - `SOLITON_ERR_NULL_POINTER` (-13): `ratchet`, `*ratchet`, or `data_out` is null. * - `SOLITON_ERR_INVALID_DATA` (-17): magic check failed. * - `SOLITON_ERR_CONCURRENT_ACCESS` (-18): ratchet handle is in use by another call. * - `SOLITON_ERR_CHAIN_EXHAUSTED` (-15): a counter is at `u32::MAX`, preventing * safe epoch increment during serialization. On this error, `*ratchet` is NOT * consumed — the handle remains valid and the session is still usable. The * caller should send/receive more messages to advance the ratchet state, then * retry serialization. * * # Security * The returned buffer contains ALL ratchet secret key material (root key, * chain keys, ratchet secret key). MUST be freed with `soliton_buf_free()`. * Calling `free()` directly on `ptr` skips zeroization and leaves ephemeral * key material in freed heap memory — this is a security defect, not merely * an API violation. Do not copy into unmanaged memory. * * Returns 0 on success, negative on error. */ int32_t soliton_ratchet_to_bytes(struct SolitonRatchet **ratchet, struct SolitonBuf *data_out, uint64_t *epoch_out); /** * Deserialize a ratchet state from bytes. * * `data` / `data_len`: serialized ratchet state (produced by `soliton_ratchet_to_bytes`; * maximum 1 MiB / 1048576 bytes). * `out`: receives an opaque `SolitonRatchet*`. On error, `*out` is set to null. * * # Possible Errors * * - `SOLITON_ERR_NULL_POINTER` (-13): `data` or `out` is null. * - `SOLITON_ERR_INVALID_LENGTH` (-1): `data_len` is 0 or exceeds 1 MiB. * - `SOLITON_ERR_INVALID_DATA` (-17): blob is structurally invalid (bad magic, * truncated, corrupted fields). Also returned for anti-rollback rejection * when using `soliton_ratchet_from_bytes_with_min_epoch` — callers cannot * distinguish corruption from rollback programmatically (by design: an * attacker replaying an old blob should not learn which check failed). * - `SOLITON_ERR_VERSION` (-10): blob version byte is unrecognized. * * # Security * * On success the caller owns the returned `SolitonRatchet*` and MUST free it * with `soliton_ratchet_free`. The ratchet holds all session key material * (root key, chain keys, ratchet secret key); `soliton_ratchet_free` * zeroizes this material before releasing memory. * * Returns 0 on success, negative on error. */ int32_t soliton_ratchet_from_bytes(const uint8_t *data, uintptr_t data_len, struct SolitonRatchet **out); /** * Read the anti-rollback epoch from a ratchet state. * * `ratchet`: opaque ratchet state. Must be non-null. * `epoch_out`: receives the epoch value. Must be non-null. * * The epoch is monotonically increasing per `to_bytes` call. The caller * should persist this value and pass it as `min_epoch` to * `soliton_ratchet_from_bytes_with_min_epoch` on the next load. * * Returns 0 on success, negative on error. */ int32_t soliton_ratchet_epoch(const struct SolitonRatchet *ratchet, uint64_t *epoch_out); /** * Deserialize a ratchet state from bytes with anti-rollback epoch validation. * * Equivalent to `soliton_ratchet_from_bytes` but additionally rejects blobs * whose epoch is ≤ `min_epoch`. This prevents storage-level replay attacks * where an attacker substitutes an older encrypted ratchet blob. * * `data` / `data_len`: serialized ratchet state (maximum 1 MiB / 1048576 bytes). * `min_epoch`: the epoch from the last successfully loaded state. * `out`: receives an opaque `SolitonRatchet*`. On error, `*out` is set to null. * * # Anti-Rollback Protocol * * 1. Load: `soliton_ratchet_from_bytes_with_min_epoch(blob, last_epoch, &ratchet)` * 2. On success, read the new epoch via `soliton_ratchet_epoch(ratchet)` and * persist it as the new `last_epoch`. * 3. On next load, pass the persisted epoch as `min_epoch`. * * # Possible Errors * * Same as `soliton_ratchet_from_bytes`, plus anti-rollback rejection * (`SOLITON_ERR_INVALID_DATA` when `epoch ≤ min_epoch`). * * Returns 0 on success, negative on error. */ int32_t soliton_ratchet_from_bytes_with_min_epoch(const uint8_t *data, uintptr_t data_len, uint64_t min_epoch, struct SolitonRatchet **out); /** * Verify a pre-key bundle (§5.4 Step 1). * * Checks that: * 1. `bundle_ik_pk` matches `known_ik_pk`. * 2. The SPK signature `spk_sig` is valid over `spk_pub`. * 3. The crypto version matches the library's supported version. * * `bundle_ik_pk` / `bundle_ik_pk_len`: identity public key from the bundle. * `known_ik_pk` / `known_ik_pk_len`: the known/trusted identity public key for this peer. * `spk_pub` / `spk_pub_len`: signed pre-key public key (X-Wing, 1216 bytes). * `spk_sig` / `spk_sig_len`: hybrid signature over the SPK. * `crypto_version`: null-terminated crypto version string. * * Returns 0 if valid, negative on error. */ int32_t soliton_kex_verify_bundle(const uint8_t *bundle_ik_pk, uintptr_t bundle_ik_pk_len, const uint8_t *known_ik_pk, uintptr_t known_ik_pk_len, const uint8_t *spk_pub, uintptr_t spk_pub_len, const uint8_t *spk_sig, uintptr_t spk_sig_len, const char *crypto_version); /** * Build AAD for the first message of a session (§7.3). * * This is needed by Bob when calling `soliton_ratchet_decrypt_first` — * both sides must use identical AAD. * * `sender_fp`: sender's fingerprint (32 bytes). * `recipient_fp`: recipient's fingerprint (32 bytes). * `session_init_encoded` / `session_init_encoded_len`: encoded session init * (as returned in `SolitonInitiatedSession::session_init_encoded`; * maximum 8 KiB / 8192 bytes). * `aad_out`: receives the AAD bytes (caller frees with `soliton_buf_free`). * * Returns 0 on success, negative on error. */ int32_t soliton_kex_build_first_message_aad(const uint8_t *sender_fp, uintptr_t sender_fp_len, const uint8_t *recipient_fp, uintptr_t recipient_fp_len, const uint8_t *session_init_encoded, uintptr_t session_init_encoded_len, struct SolitonBuf *aad_out); /** * Encode a session init message for AAD construction (§7.4). * * Bob receives a session init's component fields over the wire and needs * to encode them identically to how Alice encoded them, for AAD matching. * * This is a convenience wrapper — Bob can also use the pre-encoded bytes * from Alice's message directly with `soliton_kex_build_first_message_aad`. * * `crypto_version`: null-terminated crypto version string. * `sender_fp`: sender's IK fingerprint (32 bytes, non-null). * `recipient_fp`: recipient's IK fingerprint (32 bytes, non-null). * `sender_ek` / `sender_ek_len`: sender's ephemeral X-Wing public key. * `ct_ik` / `ct_ik_len`: IK ciphertext. * `ct_spk` / `ct_spk_len`: SPK ciphertext. * `spk_id`: signed pre-key ID. * `ct_opk` / `ct_opk_len`: OPK ciphertext (null if absent). * `opk_id`: one-time pre-key ID (must be 0 if ct_opk is null; non-zero returns `SOLITON_ERR_INVALID_DATA`). * `encoded_out`: receives the encoded bytes (caller frees). * * Returns 0 on success, negative on error. */ int32_t soliton_kex_encode_session_init(const char *crypto_version, const uint8_t *sender_fp, uintptr_t sender_fp_len, const uint8_t *recipient_fp, uintptr_t recipient_fp_len, const uint8_t *sender_ek, uintptr_t sender_ek_len, const uint8_t *ct_ik, uintptr_t ct_ik_len, const uint8_t *ct_spk, uintptr_t ct_spk_len, uint32_t spk_id, const uint8_t *ct_opk, uintptr_t ct_opk_len, uint32_t opk_id, struct SolitonBuf *encoded_out); /** * Decode a session init from the binary wire format produced by * [`soliton_kex_encode_session_init`]. * * `encoded` / `encoded_len`: the wire bytes received from Alice (non-null; * maximum 64 KiB / 65536 bytes). * `out`: receives the decoded fields (non-null). Zeroed on error return. * * On success, the caller must call [`soliton_decoded_session_init_free`] * on `out` when done to release the library-allocated `crypto_version` buffer. * * Returns 0 on success, negative on error. */ int32_t soliton_kex_decode_session_init(const uint8_t *encoded, uintptr_t encoded_len, struct SolitonDecodedSessionInit *out); /** * Free a decoded session init's library-allocated fields. * * `out`: pointer to a `SolitonDecodedSessionInit` previously populated by * [`soliton_kex_decode_session_init`]. Frees `crypto_version` via * `soliton_buf_free`. Safe to call with a null pointer (no-op). * After this call, `crypto_version.ptr` is null and `crypto_version.len` is 0. */ void soliton_decoded_session_init_free(struct SolitonDecodedSessionInit *out); /** * Generate an X-Wing keypair. * * `pk_out`: receives the public key (1216 bytes, caller frees). * `sk_out`: receives the secret key (2432 bytes, caller frees). * * # Security * * `sk_out` contains the raw X-Wing secret key. Free it with * `soliton_buf_free`, NOT `free()`. `soliton_buf_free` zeroizes * the buffer contents before releasing memory; `free()` does not. * * Returns 0 on success, negative on error. */ int32_t soliton_xwing_keygen(struct SolitonBuf *pk_out, struct SolitonBuf *sk_out); /** * Encapsulate to an X-Wing public key. * * `pk` / `pk_len`: X-Wing public key (1216 bytes). * `ct_out`: receives the ciphertext (1120 bytes, caller frees). * `ss_out` / `ss_out_len`: must point to exactly 32 bytes for the shared secret. * On error, `ss_out` is zeroed. Caller must check return code before using. * * # Security * * The 32-byte shared secret written to `ss_out` is raw key material. The * caller must zeroize `ss_out` when it is no longer needed (e.g., * `explicit_bzero` on POSIX, `SecureZeroMemory` on Windows). * * Returns 0 on success, negative on error. */ int32_t soliton_xwing_encapsulate(const uint8_t *pk, uintptr_t pk_len, struct SolitonBuf *ct_out, uint8_t *ss_out, uintptr_t ss_out_len); /** * Decapsulate an X-Wing ciphertext. * * `sk` / `sk_len`: X-Wing secret key. * `ct` / `ct_len`: X-Wing ciphertext (1120 bytes). * `ss_out` / `ss_out_len`: must point to exactly 32 bytes for the shared secret. * On error, `ss_out` is zeroed. Caller must check return code before using. * * # Security * * The 32-byte shared secret written to `ss_out` is raw key material. The * caller must zeroize `ss_out` when it is no longer needed (e.g., * `explicit_bzero` on POSIX, `SecureZeroMemory` on Windows). * * Returns 0 on success, negative on error. */ int32_t soliton_xwing_decapsulate(const uint8_t *sk, uintptr_t sk_len, const uint8_t *ct, uintptr_t ct_len, uint8_t *ss_out, uintptr_t ss_out_len); /** * Derive call encryption keys from a ratchet session's root key and an * ephemeral KEM shared secret. * * `ratchet`: opaque ratchet state (read-only — not modified). * `kem_ss`: 32-byte ephemeral X-Wing shared secret from call signaling. * `call_id`: 16-byte random call identifier (generated by the initiator). * `out`: receives an opaque `SolitonCallKeys*` (caller frees with * `soliton_call_keys_free`). * * Fingerprints are taken from the ratchet state (set at init time), not * passed as parameters. * * # Security * * The derived keys depend on the ratchet's current root key, which changes * after every KEM ratchet step. If ratchet messages are exchanged between * call-offer and call-answer, the two parties' root keys will differ and * the call keys will not match (manifesting as AEAD failure on the first * audio packet). Callers should ensure both parties derive call keys from * the same ratchet epoch. * * The returned `SolitonCallKeys` contains call encryption key material. * Free it with `soliton_call_keys_free` when the call ends — this * zeroizes all keys. Do NOT use `free()` directly. * * # Possible Errors * * - `SOLITON_ERR_NULL_POINTER` (-13): any pointer argument is null. * - `SOLITON_ERR_INVALID_DATA` (-17): magic check failed, ratchet is dead * (all-zero root key — post-reset session), `kem_ss` is all-zero, or * `call_id` is all-zero. * - `SOLITON_ERR_INVALID_LENGTH` (-1): `kem_ss_len` is not 32, or `call_id_len` * is not 16. * - `SOLITON_ERR_CONCURRENT_ACCESS` (-18): ratchet handle is in use by another call. * * Returns 0 on success, negative on error. */ int32_t soliton_ratchet_derive_call_keys(const struct SolitonRatchet *ratchet, const uint8_t *kem_ss, uintptr_t kem_ss_len, const uint8_t *call_id, uintptr_t call_id_len, struct SolitonCallKeys **out); /** * Copy the current send key (32 bytes) to a caller-allocated buffer. * * `keys`: opaque call keys state. * `out` / `out_len`: caller-allocated buffer. `out_len` must be exactly 32. * * # Security * * The caller must zeroize `out` when the key is no longer needed. * * Returns 0 on success, negative on error. */ int32_t soliton_call_keys_send_key(const struct SolitonCallKeys *keys, uint8_t *out, uintptr_t out_len); /** * Copy the current recv key (32 bytes) to a caller-allocated buffer. * * `keys`: opaque call keys state. * `out` / `out_len`: caller-allocated buffer. `out_len` must be exactly 32. * * # Security * * The caller must zeroize `out` when the key is no longer needed. * * Returns 0 on success, negative on error. */ int32_t soliton_call_keys_recv_key(const struct SolitonCallKeys *keys, uint8_t *out, uintptr_t out_len); /** * Advance the call chain, replacing the current call keys with fresh * key material derived from the internal chain key. * * After this call, `soliton_call_keys_send_key` and * `soliton_call_keys_recv_key` return the new keys. The old keys * and chain key are zeroized. * * # Possible Errors * * - `SOLITON_ERR_NULL_POINTER` (-13): `keys` is null. * - `SOLITON_ERR_CHAIN_EXHAUSTED` (-15): advance count has reached * `MAX_CALL_ADVANCE`. All keys are zeroized — the `CallKeys` handle * is dead and must be freed. * - `SOLITON_ERR_CONCURRENT_ACCESS` (-18): handle is in use by another call. * * Returns 0 on success, negative on error. */ int32_t soliton_call_keys_advance(struct SolitonCallKeys *keys); /** * Free a `SolitonCallKeys` object, zeroizing all key material. * * `keys`: pointer to the caller's `SolitonCallKeys*` variable. After * freeing, the caller's pointer is set to NULL, making double-free a no-op. * Null outer pointer and null inner pointer are both no-ops (returns 0). * * Returns 0 on success, `SOLITON_ERR_CONCURRENT_ACCESS` (-18) if the handle * is currently in use. See `soliton_ratchet_free` for retry semantics. */ int32_t soliton_call_keys_free(struct SolitonCallKeys **keys); /** * Initialize a streaming encryptor. * * `key`: 32-byte encryption key. Copied internally; caller may zeroize after. * `aad`: optional caller-supplied AAD context. NULL with aad_len=0 for none. * `compress`: if true, each chunk is zstd-compressed before encryption. * `out`: receives the allocated handle. Caller frees via * `soliton_stream_encrypt_free`. */ int32_t soliton_stream_encrypt_init(const uint8_t *key, uintptr_t key_len, const uint8_t *aad, uintptr_t aad_len, bool compress, struct SolitonStreamEncryptor **out); /** * Copy the 26-byte header into a caller-allocated buffer. */ int32_t soliton_stream_encrypt_header(const struct SolitonStreamEncryptor *enc, uint8_t *out, uintptr_t out_len); /** * Encrypt one chunk into a caller-allocated buffer. * * `out_len` must be >= `SOLITON_STREAM_ENCRYPT_MAX`. * `out_written` receives actual bytes written. Set to 0 on error. */ int32_t soliton_stream_encrypt_chunk(struct SolitonStreamEncryptor *enc, const uint8_t *plaintext, uintptr_t plaintext_len, bool is_last, uint8_t *out, uintptr_t out_len, uintptr_t *out_written); /** * Encrypt a specific chunk by index (random access). * * Stateless: does not advance the sequential counter, does not set the * `finalized` flag, and does not enforce the post-finalization guard. * The caller is responsible for assigning unique indices and identifying * the final chunk. * * Enables parallel chunk encryption. Because nonces and AAD are * index-derived, independent chunks may be dispatched concurrently — * the CAPI reentrancy guard (§13.6) prevents concurrent calls on the same * handle, so parallel encryption requires one encryptor handle per thread. * * # Parameters * * - `enc`: encryptor handle (const — no state mutation); must not be NULL. * - `index`: chunk index to encrypt at. * - `plaintext`: plaintext bytes; may be NULL if `plaintext_len == 0`. * - `plaintext_len`: length of plaintext. Must be exactly * `SOLITON_STREAM_CHUNK_SIZE` for non-final chunks; * `0..=SOLITON_STREAM_CHUNK_SIZE` for the final chunk. * - `is_last`: `true` if this is the final chunk of the stream. * - `out`: caller-allocated output buffer; must not be NULL. * - `out_len`: size of `out` in bytes; must be >= `SOLITON_STREAM_ENCRYPT_MAX`. * - `out_written`: receives the actual bytes written; set to 0 on error. * * # Returns * * 0 on success; negative `SolitonError` code on failure. */ int32_t soliton_stream_encrypt_chunk_at(const struct SolitonStreamEncryptor *enc, uint64_t index, const uint8_t *plaintext, uintptr_t plaintext_len, bool is_last, uint8_t *out, uintptr_t out_len, uintptr_t *out_written); /** * Return whether the encryptor has been finalized. */ int32_t soliton_stream_encrypt_is_finalized(const struct SolitonStreamEncryptor *enc, bool *out); /** * Free the streaming encryptor. Sets `*enc` to NULL after freeing. */ int32_t soliton_stream_encrypt_free(struct SolitonStreamEncryptor **enc); /** * Initialize a streaming decryptor from the 26-byte header. */ int32_t soliton_stream_decrypt_init(const uint8_t *key, uintptr_t key_len, const uint8_t *header, uintptr_t header_len, const uint8_t *aad, uintptr_t aad_len, struct SolitonStreamDecryptor **out); /** * Decrypt next sequential chunk into a caller-allocated buffer. * * `out_len` must be >= `SOLITON_STREAM_CHUNK_SIZE`. * `out_written` receives actual plaintext bytes. Set to 0 on error. * `is_last` set to true if this was the final chunk. Set to false on error. */ int32_t soliton_stream_decrypt_chunk(struct SolitonStreamDecryptor *dec, const uint8_t *chunk, uintptr_t chunk_len, uint8_t *out, uintptr_t out_len, uintptr_t *out_written, bool *is_last); /** * Decrypt a specific chunk by index (random access). * * Does not advance the sequential counter. Can be called after finalization. */ int32_t soliton_stream_decrypt_chunk_at(const struct SolitonStreamDecryptor *dec, uint64_t index, const uint8_t *chunk, uintptr_t chunk_len, uint8_t *out, uintptr_t out_len, uintptr_t *out_written, bool *is_last); /** * Return whether the decryptor has been finalized. */ int32_t soliton_stream_decrypt_is_finalized(const struct SolitonStreamDecryptor *dec, bool *out); /** * Return the next expected sequential chunk index. */ int32_t soliton_stream_decrypt_expected_index(const struct SolitonStreamDecryptor *dec, uint64_t *out); /** * Free the streaming decryptor. Sets `*dec` to NULL after freeing. */ int32_t soliton_stream_decrypt_free(struct SolitonStreamDecryptor **dec); #endif /* SOLITON_H */