Files
FastSync/src/shared/credentials.h
T
TapTap accd34ad60 fix(d5-daemon-auth): address auth review findings (Wave B)
- test_credentials.c: NUL-terminate the overlong-line stack buffer before
  make_tmp_file's strlen() (was a stack-buffer-overflow READ under ASan);
  still exercises the overlong-rejection path.
- Add redacted protocol string variants (protocol_send_str_redacted /
  receive + fd send_str_redacted/receive_str_redacted) and use them for the
  daemon auth username/digest so --verbose / LOG_DEBUG_ALL never logs a
  replayable credential while other protocol strings keep their debug trace.
- credentials_verify/gate: replace byte-wise-short-circuiting strcmp with a
  fixed-length constant-time username compare (closes user-enumeration oracle);
  update doc comment to match.
- read_secret_file: preserve password exact bytes (only strip trailing CR/LF)
  and burn the stack line buffer; document the whitespace behavior.
- test_server_cli.c: note the parser zero-inits opts on failure.
- Add debug-level daemon test asserting the digest never appears under --verbose.

PROTOCOL_VERSION stays 2.15.0.
2026-09-10 13:20:42 +02:00

126 lines
6.7 KiB
C

#ifndef CREDENTIALS_H
#define CREDENTIALS_H
#include <stdbool.h>
#include <stddef.h>
/* Daemon password authentication (Wave B).
*
* FastSync authenticates a daemon connection with a username plus a SHA-256
* hex digest of that username's password. The digest is what crosses the
* wire: a challenge-less credential exchange, so the literal password is never
* transmitted (and never stored on the daemon host). A module that declares
* `auth users` demands that the presented username is on its list AND that the
* presented digest matches the credential store's entry for that username.
* The digest comparison is constant-time; a module with `auth users` whose
* store is missing/misconfigured fails CLOSED (never falls open).
*
* Credential store format (server --password-file and --early-input): one
* `user:SHA256HEX` entry per line. SHA256HEX is the lowercase hex SHA-256 of
* the user's password -- the exact value a FastSync client transmits. Blank
* lines and lines whose first non-space character is '#' or ';' are comments.
* The parser is STRICT: a malformed line (no ':', an empty/whitespace user, a
* secret that is not 64 lowercase hex chars, a line longer than
* CREDENTIAL_MAX_LINE) fails the whole load so a typo can never silently
* change who may log in.
*
* Client --password-file format: the FIRST meaningful (non-comment, non-blank)
* line is `user:password`, holding the literal password. The client hashes it
* and sends only the digest; the file should be mode 0600 and readable only by
* its owner.
*/
/* Lowercase hex length of a SHA-256 digest (what travels on the wire and what
* the server store holds). */
#define CREDENTIAL_HASH_HEX_LEN 64
/* Longest accepted credential-file line (excluding the trailing newline). */
#define CREDENTIAL_MAX_LINE 4096
/* Upper bound on a username in a credential file and on the wire. Kept well
* below MAX_STRING_SIZE so a wire username can never exhaust anything. */
#define CREDENTIAL_MAX_USER_LEN 256
/* Upper bound on a client-file password (before hashing). */
#define CREDENTIAL_MAX_PASSWORD_LEN 1024
typedef struct CredentialStore CredentialStore;
/* Load the daemon credential store.
*
* password_file and early_input_file are both NULL-or-path, matching the
* server's --password-file and --early-input options. A file that cannot be
* opened or that fails the strict grammar is a hard error (err filled, NULL
* returned) -- the daemon fails CLOSED rather than serving an auth-required
* module with a partial store. Both files may be NULL, which yields an empty
* store (every auth-required module then refuses connections). When both are
* given, the --early-input file is layered over --password-file: a duplicate
* username whose secret matches is deduplicated; one whose secret differs is
* an error (the two sources disagree), never a silent pick.
*
* The returned store is heap-owned; free it with credentials_free. */
CredentialStore* credentials_load(const char* password_file, const char* early_input_file,
char* err, size_t err_size);
void credentials_free(CredentialStore* store);
/* True when `hash_hex` is exactly CREDENTIAL_HASH_HEX_LEN lowercase hex digits
* (the wire/store digest form). Used to reject a malformed presented digest
* before it reaches the comparison. */
bool credentials_hash_valid(const char* hash_hex);
/* Compute the lowercase hex SHA-256 of `password` into out_hex, which must
* hold at least CREDENTIAL_HASH_HEX_LEN + 1 bytes. Returns false on a NULL
* password or a hashing failure. The output is NUL-terminated. */
bool credentials_hash_password(const char* password, char* out_hex);
/* Read the CLIENT-side secret file: the first meaningful line is
* `user:password` (the literal password). *user_out and *password_out are
* freshly allocated on success (password is plaintext -- the caller hashes it
* and then burns/frees it); both are NULL on error. Returns 0 on success, -1
* on failure (err filled: the path is named, never the credential itself).
* Only the line's trailing CR/LF are stripped: the password's bytes are
* otherwise preserved exactly, so a password with leading/trailing whitespace
* (after the ':') is kept usable. The username is trimmed of surrounding
* space/tabs. */
int credentials_read_secret_file(const char* path, char** user_out, char** password_out, char* err,
size_t err_size);
/* Constant-time equality over exactly len bytes. Returns true when the two
* buffers match. No early exit: the whole length is always scanned, so a
* timing side-channel cannot reveal how many leading bytes matched. */
bool credentials_secure_equal(const char* a, const char* b, size_t len);
/* Overwrite secret[0..len) with zeros (best-effort wipe of a plaintext
* password that is about to be freed). */
void credentials_burn(char* secret, size_t len);
/* Verify a presented (user, digest) against the store. Returns true only when
* the store holds an entry for `user` whose stored digest equals the presented
* one. A NULL store, NULL user/digest, unknown user and wrong digest all
* return false. The digest comparison runs over a fixed dummy whenever the
* user is absent, and the username lookup is a single constant-time
* full-length compare (no byte-wise early exit), so neither "unknown user" vs
* "wrong password" nor a username prefix match can be distinguished by timing
* (no user-enumeration oracle in the comparison path). */
bool credentials_verify(const CredentialStore* store, const char* user,
const char* presented_hash_hex);
/* The daemon's per-module auth decision, in one pure, unit-testable function.
* `module_users`/`module_user_count` are the module's `auth users` list; a
* module that declares auth users requires the presented user to be ON that
* list AND to verify against the store. Returns false (fail closed) when the
* store is NULL, when no credential was presented, when the user is not on the
* module's list, or when verification fails. This is the single decision the
* server_module_gate seam applies to an auth-required module. Like
* credentials_verify, username matches here use a constant-time full-length
* compare rather than a byte-wise-short-circuiting strcmp. */
bool credentials_gate_allows(const CredentialStore* store, const char* const* module_users,
int module_user_count, const char* presented_user,
const char* presented_hash_hex);
/* Number of entries currently in the store (tests/introspection). */
int credentials_store_size(const CredentialStore* store);
/* Whether the store contains an entry for `user` (tests/introspection). */
bool credentials_store_has(const CredentialStore* store, const char* user);
#endif