457 lines
15 KiB
C
457 lines
15 KiB
C
#include "include/conf.h"
|
|
|
|
#include "include/cmd.h"
|
|
|
|
#include "include/auth.h"
|
|
#include "include/admin.h"
|
|
#include "include/blake2s.h"
|
|
|
|
#include <time.h>
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <sys/random.h>
|
|
|
|
typedef void (*dp_AdminCommandFunction)(int argc,
|
|
char *argv[],
|
|
sqlite3 *database);
|
|
|
|
typedef struct {
|
|
const char *name;
|
|
const char *help;
|
|
dp_AdminCommandFunction func;
|
|
} dp_AdminCommand;
|
|
|
|
static void dp_admin_cmd_help(int argc, char *argv[], sqlite3 *database);
|
|
static void dp_admin_cmd_newsecret(int argc, char *argv[], sqlite3 *database);
|
|
static void dp_admin_cmd_exit(int argc, char *argv[], sqlite3 *database);
|
|
static void dp_admin_cmd_listsecrets(int argc, char *argv[], sqlite3 *database);
|
|
static void
|
|
dp_admin_cmd_revokesecret(int argc, char *argv[], sqlite3 *database);
|
|
static void
|
|
dp_admin_cmd_updatesecret(int argc, char *argv[], sqlite3 *database);
|
|
|
|
/* clang-format off */
|
|
static dp_AdminCommand dp_admin_commands[] = {
|
|
{"help", "Show this help message", dp_admin_cmd_help},
|
|
{"newsecret", "newsecret <domain> \"<description>\" [expires]. Create a new secret for the domain", dp_admin_cmd_newsecret},
|
|
{"listsecrets", "List all secrets", dp_admin_cmd_listsecrets},
|
|
{"revokesecret", "revokesecret <id|domain|secret>. Remove secret by id, domain, or exact secret", dp_admin_cmd_revokesecret},
|
|
{"updatesecret", "updatesecret <domain> <domain|expires?description> <value>. Update description or expires", dp_admin_cmd_updatesecret},
|
|
{"exit", "Exit the REPL", dp_admin_cmd_exit},
|
|
{NULL, NULL, NULL},
|
|
};
|
|
/* clang-format on */
|
|
|
|
static bool dp_admin_repl_running = true;
|
|
|
|
static void dp_admin_cmd_help(int argc, char *argv[], sqlite3 *database) {
|
|
(void)argc;
|
|
(void)argv;
|
|
(void)database;
|
|
puts("Available commands:");
|
|
for (dp_AdminCommand *cmd = dp_admin_commands; cmd->name; ++cmd) {
|
|
printf(" %s - %s\n", cmd->name, cmd->help);
|
|
}
|
|
}
|
|
|
|
static void dp_admin_cmd_newsecret(int argc, char *argv[], sqlite3 *database) {
|
|
if (argc < 3 || argc > 4) {
|
|
printf("Usage: newsecret <domain> \"<description>\" [expires]\n");
|
|
printf(" expires format: 0 or '1h', '1d', '1m', '1y' (h=hour, "
|
|
"d=day, m=month, y=year)\n");
|
|
return;
|
|
}
|
|
const char *domain = argv[1];
|
|
const char *description = argv[2];
|
|
const char *expires_str = NULL;
|
|
if (argc == 4) {
|
|
expires_str = argv[3];
|
|
}
|
|
|
|
uint8_t secret[DP_AUTH_SECRET_SIZE] = {0};
|
|
uint8_t key[DP_AUTH_KEY_SIZE] = {0};
|
|
uint64_t timestamp = 0;
|
|
if (!dp_auth_gen_secret(secret, key, ×tamp)) {
|
|
(void)fputs("Failed to generate a new key\n", stderr);
|
|
return;
|
|
}
|
|
|
|
uint8_t salt[DP_BLAKE2S_SALT_SIZE] = {0};
|
|
uint8_t key_hash[DP_BLAKE2S_OUTPUT_SIZE_MAX] = {0};
|
|
|
|
if (!dp_BLAKE2s_hash_password_gen(key, DP_AUTH_KEY_SIZE, key_hash, salt)) {
|
|
(void)fputs("Failed to hash the key\n", stderr);
|
|
return;
|
|
}
|
|
|
|
uint64_t expires = 0;
|
|
if (expires_str && expires_str[0] != '\0') {
|
|
const uint64_t duration_seconds =
|
|
dp_parse_duration_to_seconds(expires_str);
|
|
|
|
if (duration_seconds == 0 && strcmp(expires_str, "0") != 0) {
|
|
printf("Warning: Invalid expires format '%s', setting to never "
|
|
"expire (0).\n",
|
|
expires_str);
|
|
expires = 0;
|
|
} else {
|
|
expires =
|
|
(duration_seconds == 0) ? 0 : timestamp + duration_seconds;
|
|
}
|
|
}
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
const char *sql =
|
|
"INSERT INTO secrets(domain, secret, key_hash, key_salt, "
|
|
"timestamp, expires, description) VALUES (?, ?, ?, ?, ?, ?, ?)";
|
|
|
|
if (sqlite3_prepare_v2(database, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
(void)fprintf(stderr, "Failed to prepare statement: %s\n",
|
|
sqlite3_errmsg(database));
|
|
return;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, domain, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, (const char *)secret, DP_AUTH_SECRET_SIZE,
|
|
SQLITE_TRANSIENT);
|
|
sqlite3_bind_blob(stmt, 3, key_hash, DP_BLAKE2S_OUTPUT_SIZE_MAX,
|
|
SQLITE_TRANSIENT);
|
|
sqlite3_bind_blob(stmt, 4, salt, DP_BLAKE2S_SALT_SIZE, SQLITE_TRANSIENT);
|
|
sqlite3_bind_int64(stmt, 5, (sqlite3_int64)timestamp);
|
|
sqlite3_bind_int64(stmt, 6, (sqlite3_int64)expires);
|
|
sqlite3_bind_text(stmt, 7, description, -1, SQLITE_TRANSIENT);
|
|
|
|
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
|
(void)fprintf(stderr, "Failed to execute statement: %s\n",
|
|
sqlite3_errmsg(database));
|
|
sqlite3_finalize(stmt);
|
|
return;
|
|
}
|
|
|
|
const sqlite3_int64 last_id = sqlite3_last_insert_rowid(database);
|
|
sqlite3_finalize(stmt);
|
|
|
|
puts("New secret generated and inserted:");
|
|
printf("id: %lld\n", last_id);
|
|
printf("domain: %s\n", domain);
|
|
printf("secret: %.*s\n", DP_AUTH_SECRET_SIZE, secret);
|
|
printf("key: %.*s\n", DP_AUTH_KEY_SIZE, key);
|
|
printf("timestamp: %lu\n", timestamp);
|
|
printf("*expires: %lu\n", expires);
|
|
printf("*description: %s\n", description);
|
|
puts("(fields starting with `*` are NOT directly used for authentication, "
|
|
"hence should be ignored)");
|
|
}
|
|
|
|
static void dp_admin_cmd_exit(int argc, char *argv[], sqlite3 *database) {
|
|
(void)argc;
|
|
(void)argv;
|
|
(void)database;
|
|
dp_admin_repl_running = false;
|
|
}
|
|
|
|
static void
|
|
dp_admin_cmd_listsecrets(int argc, char *argv[], sqlite3 *database) {
|
|
(void)argc;
|
|
(void)argv;
|
|
|
|
const char *sql = "SELECT id, domain, secret, description, timestamp, "
|
|
"expires FROM secrets";
|
|
sqlite3_stmt *stmt = NULL;
|
|
|
|
if (sqlite3_prepare_v2(database, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
(void)fprintf(stderr, "Failed to prepare statement: %s\n",
|
|
sqlite3_errmsg(database));
|
|
return;
|
|
}
|
|
|
|
/* clang-format off */
|
|
puts("ID | Domain | Secret | Description | Timestamp | Expires ");
|
|
puts("--------------------------------------------------------------------------------------------------------------------------------------");
|
|
/* clang-format on */
|
|
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
sqlite3_int64 id = sqlite3_column_int64(stmt, 0);
|
|
const uint8_t *domain = sqlite3_column_text(stmt, 1);
|
|
const uint8_t *secret = sqlite3_column_text(stmt, 2);
|
|
const uint8_t *description = sqlite3_column_text(stmt, 3);
|
|
sqlite3_int64 timestamp = sqlite3_column_int64(stmt, 4);
|
|
sqlite3_int64 expires = sqlite3_column_int64(stmt, 5);
|
|
|
|
printf("%-3lld | %-22s | %-24s | %-30s | %-20lld | ", id,
|
|
domain ? (const char *)domain : "",
|
|
secret ? (const char *)secret : "",
|
|
description ? (const char *)description : "", timestamp);
|
|
|
|
if (expires == 0) {
|
|
printf("never\n");
|
|
} else {
|
|
printf("%-20lld\n", expires);
|
|
}
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
|
|
static void
|
|
dp_admin_cmd_revokesecret(int argc, char *argv[], sqlite3 *database) {
|
|
if (argc != 2) {
|
|
printf("Usage: revokesecret <id|domain|secret>\n");
|
|
return;
|
|
}
|
|
|
|
const char *arg = argv[1];
|
|
sqlite3_stmt *stmt = NULL;
|
|
const char *sql = NULL;
|
|
int ret = 0;
|
|
int changes = 0;
|
|
|
|
/* 1. Try by id (integer) */
|
|
if (dp_isnumber(arg)) {
|
|
sql = "DELETE FROM secrets WHERE id = ?";
|
|
if (sqlite3_prepare_v2(database, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
(void)fprintf(stderr, "Failed to prepare statement: %s\n",
|
|
sqlite3_errmsg(database));
|
|
return;
|
|
}
|
|
|
|
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)dp_str2u64(arg));
|
|
ret = sqlite3_step(stmt);
|
|
if (ret != SQLITE_DONE) {
|
|
(void)fprintf(stderr, "Failed to execute delete by id: %s\n",
|
|
sqlite3_errmsg(database));
|
|
sqlite3_finalize(stmt);
|
|
return;
|
|
}
|
|
|
|
changes = sqlite3_changes(database);
|
|
sqlite3_finalize(stmt);
|
|
if (changes > 0) {
|
|
printf("Deleted %d secret(s) by id %s\n", changes, arg);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* 2. Try by domain */
|
|
sql = "DELETE FROM secrets WHERE domain = ?";
|
|
if (sqlite3_prepare_v2(database, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
(void)fprintf(stderr, "Failed to prepare statement: %s\n",
|
|
sqlite3_errmsg(database));
|
|
return;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, arg, -1, SQLITE_TRANSIENT);
|
|
ret = sqlite3_step(stmt);
|
|
if (ret != SQLITE_DONE) {
|
|
(void)fprintf(stderr, "Failed to execute delete by domain: %s\n",
|
|
sqlite3_errmsg(database));
|
|
sqlite3_finalize(stmt);
|
|
return;
|
|
}
|
|
|
|
changes = sqlite3_changes(database);
|
|
sqlite3_finalize(stmt);
|
|
if (changes > 0) {
|
|
printf("Deleted %d secret(s) by domain '%s'\n", changes, arg);
|
|
return;
|
|
}
|
|
|
|
/* 3. Try by secret */
|
|
sql = "DELETE FROM secrets WHERE secret = ?";
|
|
if (sqlite3_prepare_v2(database, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
(void)fprintf(stderr, "Failed to prepare statement: %s\n",
|
|
sqlite3_errmsg(database));
|
|
return;
|
|
}
|
|
sqlite3_bind_text(stmt, 1, arg, -1, SQLITE_TRANSIENT);
|
|
ret = sqlite3_step(stmt);
|
|
|
|
if (ret != SQLITE_DONE) {
|
|
(void)fprintf(stderr, "Failed to execute delete by secret: %s\n",
|
|
sqlite3_errmsg(database));
|
|
sqlite3_finalize(stmt);
|
|
return;
|
|
}
|
|
changes = sqlite3_changes(database);
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (changes > 0) {
|
|
printf("Deleted %d secret(s) by secret '%s'\n", changes, arg);
|
|
return;
|
|
}
|
|
|
|
printf("No secret found matching '%s'\n", arg);
|
|
}
|
|
|
|
static void
|
|
dp_admin_cmd_updatesecret(int argc, char *argv[], sqlite3 *database) {
|
|
if (argc != 4) {
|
|
printf("Usage: updatesecret <domain> <domain|expires|description> "
|
|
"<value>\n");
|
|
printf("Fields supported: domain, expires, description\n");
|
|
return;
|
|
}
|
|
|
|
const char *domain = argv[1];
|
|
const char *field = argv[2];
|
|
const char *value = argv[3];
|
|
|
|
if (strcmp(field, "domain") == 0) {
|
|
const char *sql = "UPDATE secrets SET domain = ? WHERE domain = ?";
|
|
sqlite3_stmt *stmt = NULL;
|
|
|
|
if (sqlite3_prepare_v2(database, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
(void)fprintf(stderr, "Failed to prepare statement: %s\n",
|
|
sqlite3_errmsg(database));
|
|
return;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, value, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, domain, -1, SQLITE_TRANSIENT);
|
|
|
|
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
|
(void)fprintf(stderr, "Failed to update domain: %s\n",
|
|
sqlite3_errmsg(database));
|
|
sqlite3_finalize(stmt);
|
|
return;
|
|
}
|
|
|
|
int changed = sqlite3_changes(database);
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (changed == 0) {
|
|
printf("No secret found with domain '%s'.\n", domain);
|
|
} else {
|
|
printf("Updated domain from '%s' to '%s'.\n", domain, value);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (strcmp(field, "expires") == 0) {
|
|
uint64_t expires = dp_parse_duration_to_seconds(value);
|
|
|
|
if (expires == 0 && strcmp(value, "0") != 0) {
|
|
printf("Warning: invalid expires format '%s', setting to never "
|
|
"expire (0)\n",
|
|
value);
|
|
expires = 0;
|
|
} else if (expires != 0) {
|
|
bool numeric = dp_isnumber(value);
|
|
|
|
if (numeric) {
|
|
expires = (uint64_t)strtoull(value, NULL, 10);
|
|
} else {
|
|
/* If original value was like "1h", "1d", add current time: */
|
|
expires = (uint64_t)time(NULL) + expires;
|
|
}
|
|
}
|
|
|
|
const char *sql = "UPDATE secrets SET expires = ? WHERE domain = ?";
|
|
sqlite3_stmt *stmt = NULL;
|
|
|
|
if (sqlite3_prepare_v2(database, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
(void)fprintf(stderr, "Failed to prepare statement: %s\n",
|
|
sqlite3_errmsg(database));
|
|
return;
|
|
}
|
|
|
|
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)expires);
|
|
sqlite3_bind_text(stmt, 2, domain, -1, SQLITE_TRANSIENT);
|
|
|
|
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
|
(void)fprintf(stderr, "Failed to update expires: %s\n",
|
|
sqlite3_errmsg(database));
|
|
sqlite3_finalize(stmt);
|
|
return;
|
|
}
|
|
|
|
int changed = sqlite3_changes(database);
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (changed == 0) {
|
|
printf("No secret found with domain '%s'.\n", domain);
|
|
} else {
|
|
printf("Updated expires for domain '%s'.\n", domain);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (strcmp(field, "description") == 0) {
|
|
const char *sql = "UPDATE secrets SET description = ? WHERE domain = ?";
|
|
sqlite3_stmt *stmt = NULL;
|
|
|
|
if (sqlite3_prepare_v2(database, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
(void)fprintf(stderr, "Failed to prepare statement: %s\n",
|
|
sqlite3_errmsg(database));
|
|
return;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, value, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, domain, -1, SQLITE_TRANSIENT);
|
|
|
|
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
|
(void)fprintf(stderr, "Failed to update description: %s\n",
|
|
sqlite3_errmsg(database));
|
|
sqlite3_finalize(stmt);
|
|
return;
|
|
}
|
|
|
|
int changed = sqlite3_changes(database);
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (changed == 0) {
|
|
printf("No secret found with domain '%s'.\n", domain);
|
|
} else {
|
|
printf("Updated description for domain '%s'.\n", domain);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
printf("Invalid field '%s'. Valid fields: domain, expires, description\n",
|
|
field);
|
|
}
|
|
|
|
bool dp_admin_repl(sqlite3 *database) {
|
|
char line[1024] = {0};
|
|
|
|
puts("Admin REPL started. Type 'help' for commands.");
|
|
|
|
while (dp_admin_repl_running) {
|
|
size_t len = dp_read_line("\nadmin> ", line, sizeof(line));
|
|
if (len == 0) {
|
|
(void)putchar('\n');
|
|
break;
|
|
}
|
|
if (len == 1) {
|
|
continue;
|
|
}
|
|
|
|
char *argv[DP_ADMIN_MAX_ARGS] = {0};
|
|
const int argc = dp_parse_line(line, argv, DP_ADMIN_MAX_ARGS);
|
|
if (argc == 0) {
|
|
continue;
|
|
}
|
|
|
|
dp_AdminCommand *cmd = NULL;
|
|
for (dp_AdminCommand *cmd_p = dp_admin_commands; cmd_p->name; ++cmd_p) {
|
|
if (strcmp(cmd_p->name, argv[0]) == 0) {
|
|
cmd = cmd_p;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!cmd) {
|
|
printf("Unknown command: %s\n", argv[0]);
|
|
puts("Type 'help' for a list of commands.");
|
|
continue;
|
|
}
|
|
|
|
cmd->func(argc, argv, database);
|
|
}
|
|
|
|
return true;
|
|
}
|