Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 58 additions & 11 deletions src/gateway/auth.c
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,62 @@ static int ensure_tokens_dir(const char *path)
return 0;
}

static void discard_tokens_tmp(int fd, char *tmp_path)
{
if (fd >= 0)
(void)close(fd);
if (tmp_path) {
unlink(tmp_path);
free(tmp_path);
}
}

/*
* Replace tokens via temp+rename so O_TRUNC cannot wipe auth_tokens.json
* before the new JSON is fully on disk (ENOSPC / crash / fdopen failure).
*/
static int write_tokens_atomic(const char *path, const char *json)
{
size_t json_len;
size_t off;
char *tmp_path;
int fd;

if (!path || !json) return -1;
json_len = strlen(json);
tmp_path = malloc(strlen(path) + 8);
if (!tmp_path) return -1;
snprintf(tmp_path, strlen(path) + 8, "%s.tmp", path);
fd = open(tmp_path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (fd < 0) {
free(tmp_path);
return -1;
}
off = 0;
while (off < json_len) {
ssize_t n = write(fd, json + off, json_len - off);
if (n <= 0) {
discard_tokens_tmp(fd, tmp_path);
return -1;
}
off += (size_t)n;
}
if (fsync(fd) != 0) {
discard_tokens_tmp(fd, tmp_path);
return -1;
}
if (close(fd) != 0) {
discard_tokens_tmp(-1, tmp_path);
return -1;
}
if (rename(tmp_path, path) != 0) {
discard_tokens_tmp(-1, tmp_path);
return -1;
}
free(tmp_path);
return 0;
}

int auth_pair(auth_ctx_t *ctx, const char *code, char *token_out, size_t token_size)
{
if (!ctx || !ctx->tokens_path || !code || !token_out || token_size == 0) return -1;
Expand All @@ -174,7 +230,7 @@ int auth_pair(auth_ctx_t *ctx, const char *code, char *token_out, size_t token_s
!constant_time_cmp(code, ctx->pending_pairing_code, PAIRING_CODE_LEN))
return -1;
char new_token[TOKEN_LEN + 1];
generate_random_hex(new_token, TOKEN_LEN);
if (generate_random_hex(new_token, TOKEN_LEN) != 0) return -1;
/* Read existing tokens and append (multi-device support). */
cJSON *arr = NULL;
{
Expand Down Expand Up @@ -204,19 +260,10 @@ int auth_pair(auth_ctx_t *ctx, const char *code, char *token_out, size_t token_s
free(json);
return -1;
}
int fd = open(ctx->tokens_path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (fd < 0) {
free(json);
return -1;
}
FILE *out = fdopen(fd, "w");
if (!out) {
close(fd);
if (write_tokens_atomic(ctx->tokens_path, json) != 0) {
free(json);
return -1;
}
fprintf(out, "%s", json);
fclose(out);
free(json);
size_t copy_len = (size_t)TOKEN_LEN < token_size - 1 ? (size_t)TOKEN_LEN : token_size - 1;
memcpy(token_out, new_token, copy_len);
Expand Down
65 changes: 65 additions & 0 deletions tests/test_auth.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
#define _POSIX_C_SOURCE 200809L

#include "gateway/auth.h"
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
#include <unistd.h>

#define ASSERT(c) do { if (!(c)) { fprintf(stderr, "FAIL: %s:%d %s\n", __FILE__, __LINE__, #c); return 1; } } while (0)
Expand Down Expand Up @@ -100,6 +102,65 @@ static int test_auth_validate_token(void)
return 0;
}

static int test_auth_pair_write_failure_preserves_existing_tokens(void)
{
char dir_template[] = "/tmp/shellclaw_auth_atomic_XXXXXX";
char path[512];
char tmp_path[512];
char token[64];
char *dir;
char *code;
auth_ctx_t *ctx;
FILE *tokens_file;
struct rlimit old_lim;
struct rlimit new_lim;
int pair_ret;
const char *existing = "existingtokenexistingtokenexist01";

dir = mkdtemp(dir_template);
ASSERT(dir != NULL);
snprintf(path, sizeof(path), "%s/auth_tokens.json", dir);
snprintf(tmp_path, sizeof(tmp_path), "%s/auth_tokens.json.tmp", dir);

ctx = auth_init(path);
ASSERT(ctx != NULL);
code = auth_get_or_create_pairing_code(ctx);
ASSERT(code != NULL);

/* Pairing code is in memory; tokens file already has a device (append window). */
tokens_file = fopen(path, "w");
ASSERT(tokens_file != NULL);
ASSERT(fprintf(tokens_file, "[\"%s\"]", existing) > 0);
ASSERT(fclose(tokens_file) == 0);
ASSERT(auth_validate_token(ctx, existing) == 1);

ASSERT(getrlimit(RLIMIT_FSIZE, &old_lim) == 0);
new_lim = old_lim;
new_lim.rlim_cur = 8;
(void)signal(SIGXFSZ, SIG_IGN);
ASSERT(setrlimit(RLIMIT_FSIZE, &new_lim) == 0);

memset(token, 0, sizeof(token));
pair_ret = auth_pair(ctx, code, token, sizeof(token));
ASSERT(setrlimit(RLIMIT_FSIZE, &old_lim) == 0);

ASSERT(pair_ret != 0);
ASSERT(token[0] == '\0');
ASSERT(auth_validate_token(ctx, existing) == 1);

memset(token, 0, sizeof(token));
ASSERT(auth_pair(ctx, code, token, sizeof(token)) == 0);
ASSERT(auth_validate_token(ctx, existing) == 1);
ASSERT(auth_validate_token(ctx, token) == 1);

free(code);
auth_cleanup(ctx);
unlink(path);
unlink(tmp_path);
ASSERT(rmdir(dir) == 0);
return 0;
}

static int test_auth_multi_token(void)
{
const char *path = "/tmp/shellclaw_test_tokens_multi.json";
Expand Down Expand Up @@ -215,6 +276,10 @@ int main(void)
if (test_auth_pair_valid_code() != 0) { fprintf(stderr, "test_auth_pair_valid_code failed\n"); failed++; }
if (test_auth_pair_invalid_code() != 0) { fprintf(stderr, "test_auth_pair_invalid_code failed\n"); failed++; }
if (test_auth_validate_token() != 0) { fprintf(stderr, "test_auth_validate_token failed\n"); failed++; }
if (test_auth_pair_write_failure_preserves_existing_tokens() != 0) {
fprintf(stderr, "test_auth_pair_write_failure_preserves_existing_tokens failed\n");
failed++;
}
if (test_auth_multi_token() != 0) { fprintf(stderr, "test_auth_multi_token failed\n"); failed++; }
if (test_pair_lockout_triggers_after_max_fails() != 0) { fprintf(stderr, "test_pair_lockout_triggers_after_max_fails failed\n"); failed++; }
if (test_pair_lockout_expires() != 0) { fprintf(stderr, "test_pair_lockout_expires failed\n"); failed++; }
Expand Down
Loading