aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-20 15:41:55 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-20 15:42:40 +0200
commitf3b3f2c4f944c98a341f406abd2e4a6b7b69da36 (patch)
treef7b2e25937c4c10466e2cce7c2c7a9a6eeb6d7c8 /tests
parent21057cd2f2ab298300a246b9fba0d38d740c9697 (diff)
downloadbokf-f3b3f2c4f944c98a341f406abd2e4a6b7b69da36.tar.gz
bokf-f3b3f2c4f944c98a341f406abd2e4a6b7b69da36.zip
settings: encrypted secrets and SMTP settings
Diffstat (limited to 'tests')
-rw-r--r--tests/test_core.c328
1 files changed, 317 insertions, 11 deletions
diff --git a/tests/test_core.c b/tests/test_core.c
index fa7b5e4..47b6acf 100644
--- a/tests/test_core.c
+++ b/tests/test_core.c
@@ -18,6 +18,7 @@
#include "db.h"
#include "invoice.h"
#include "protocol.h"
+#include "secret.h"
#include "sessions.h"
#include "util.h"
#include "yyjson.h"
@@ -282,7 +283,8 @@ static void echo_child(int wfd, int tls)
if (memchr(buf, '\n', got))
break;
}
- (void)write(cfd, "tcp-ok", 6);
+ ssize_t wr = write(cfd, "tcp-ok", 6);
+ (void)wr;
}
close(cfd);
close(sfd);
@@ -567,6 +569,106 @@ static void test_pre_migration_snapshot(const char *tmpdir,
unlink(blocker);
}
+static void test_secret(void)
+{
+ static const char hexkey[] =
+ "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
+ static const char otherkey[] =
+ "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
+ unsigned char raw[32];
+ for (int i = 0; i < 32; i++)
+ raw[i] = (unsigned char)(i * 7 + 1);
+ char *b64key = util_b64(raw, sizeof raw);
+
+ CHECK(secret_key_is_secret("smtp_password"));
+ CHECK(!secret_key_is_secret("smtp_host"));
+ CHECK(!secret_key_is_secret(NULL));
+
+ unsetenv("BOKFD_SECRET_KEY");
+ CHECK(!secret_available());
+ char *out = NULL;
+ CHECK(secret_encrypt("hemlig", &out) == -1);
+ CHECK(out == NULL);
+ CHECK(secret_decrypt("enc:v1:AAAA:BBBB", &out) == -1);
+ CHECK(out == NULL);
+ CHECK(secret_decrypt("plaintext", &out) == -1);
+
+ setenv("BOKFD_SECRET_KEY", hexkey, 1);
+ CHECK(secret_available());
+ out = NULL;
+ CHECK(secret_encrypt("hemligt lösenord", &out) == 0);
+ CHECK(out && strncmp(out, "enc:v1:", 7) == 0);
+ CHECK(out && strstr(out, "hemligt") == NULL);
+ char *dec = NULL;
+ CHECK(secret_decrypt(out, &dec) == 0);
+ CHECK(dec && strcmp(dec, "hemligt lösenord") == 0);
+ free(dec);
+
+ char *tampered = xstrdup(out);
+ char *sep = strchr(tampered + 7, ':');
+ CHECK(sep != NULL);
+ if (sep) {
+ char *ctpart = sep + 1;
+ ctpart[0] = ctpart[0] == 'A' ? 'B' : 'A';
+ }
+ dec = NULL;
+ CHECK(secret_decrypt(tampered, &dec) == -1);
+ CHECK(dec == NULL);
+ free(tampered);
+
+ tampered = xstrdup(out);
+ tampered[7] = tampered[7] == 'A' ? 'B' : 'A';
+ dec = NULL;
+ CHECK(secret_decrypt(tampered, &dec) == -1);
+ free(tampered);
+
+ setenv("BOKFD_SECRET_KEY", otherkey, 1);
+ dec = NULL;
+ CHECK(secret_decrypt(out, &dec) == -1);
+ CHECK(dec == NULL);
+ free(out);
+
+ setenv("BOKFD_SECRET_KEY", b64key, 1);
+ CHECK(secret_available());
+ out = NULL;
+ CHECK(secret_encrypt("kort", &out) == 0);
+ dec = NULL;
+ CHECK(secret_decrypt(out, &dec) == 0);
+ CHECK(dec && strcmp(dec, "kort") == 0);
+ free(dec);
+ free(out);
+
+ char *nopad = xstrdup(b64key);
+ size_t n = strlen(nopad);
+ while (n > 0 && nopad[n - 1] == '=')
+ nopad[--n] = '\0';
+ setenv("BOKFD_SECRET_KEY", nopad, 1);
+ CHECK(secret_available());
+ out = NULL;
+ CHECK(secret_encrypt("utan padding", &out) == 0);
+ dec = NULL;
+ CHECK(secret_decrypt(out, &dec) == 0);
+ CHECK(dec && strcmp(dec, "utan padding") == 0);
+ free(dec);
+ free(out);
+ free(nopad);
+
+ setenv("BOKFD_SECRET_KEY", "abcd", 1);
+ CHECK(!secret_available());
+ out = NULL;
+ CHECK(secret_encrypt("x", &out) == -1);
+ CHECK(secret_decrypt("enc:v1:AAAA:BBBB", &out) == -1);
+
+ setenv("BOKFD_SECRET_KEY", "000102030405060708090a0b0c0d0e0f", 1);
+ CHECK(!secret_available());
+ setenv("BOKFD_SECRET_KEY", "zzzz", 1);
+ CHECK(!secret_available());
+
+ unsetenv("BOKFD_SECRET_KEY");
+ CHECK(!secret_available());
+ free(b64key);
+}
+
int main(void)
{
char tmpdir[] = "/tmp/bokf-test-XXXXXX";
@@ -603,6 +705,8 @@ int main(void)
}
sessions_init(3600);
+ test_secret();
+
yyjson_doc *d;
d = call("{\"v\":1,\"id\":\"1\",\"cmd\":\"health\"}");
@@ -1322,7 +1426,7 @@ int main(void)
CHECK_OK(d);
CHECK_STR(d, "result.org_nr", "559331-2126");
CHECK_STR(d, "result.period", "202601");
- char *b64 = jstr(d, "result.content_base64");
+ const char *b64 = jstr(d, "result.content_base64");
unsigned char *xmlb = NULL;
size_t xmll = 0;
CHECK(b64 && util_b64_decode(b64, strlen(b64), &xmlb, &xmll) == 0);
@@ -1844,14 +1948,14 @@ int main(void)
"#TRANS 2010 {} -1000.00\r\n"
"#TRANS 2010 {} 0.00\r\n"
"}\r\n";
- char *b64 = util_b64((const unsigned char *)sie_crlf,
- strlen(sie_crlf));
- d = call_sie_import(g_session, org2, b64, 0);
+ char *sie_b64a = util_b64((const unsigned char *)sie_crlf,
+ strlen(sie_crlf));
+ d = call_sie_import(g_session, org2, sie_b64a, 0);
CHECK_OK(d);
CHECK(jint(d, "result.vouchers") == 1);
int64_t fy_a = jint(d, "result.fiscal_year_id");
yyjson_doc_free(d);
- free(b64);
+ free(sie_b64a);
d = call(reqf("{\"v\":1,\"id\":\"77a\",\"cmd\":\"fiscal_year.get\","
"\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}",
@@ -1877,13 +1981,13 @@ int main(void)
"#TRANS 1930 {} 50.00\n"
"#TRANS 2010 {} -50.00\n"
"}\n";
- char *b64 = util_b64((const unsigned char *)sie_year2,
- strlen(sie_year2));
- d = call_sie_import(g_session, org2, b64, 0);
+ char *sie_b64b = util_b64((const unsigned char *)sie_year2,
+ strlen(sie_year2));
+ d = call_sie_import(g_session, org2, sie_b64b, 0);
CHECK_OK(d);
int64_t fy_b = jint(d, "result.fiscal_year_id");
yyjson_doc_free(d);
- free(b64);
+ free(sie_b64b);
d = call(reqf("{\"v\":1,\"id\":\"77b\",\"cmd\":\"report.trial_balance\","
"\"session\":\"%s\",\"org\":%d,\"args\":"
@@ -1972,6 +2076,208 @@ int main(void)
CHECK_STR(d, "result.attachment_dir", "/tmp/bilagor");
yyjson_doc_free(d);
+ /* ---------------- settings: SMTP and encrypted password ----------- */
+
+ d = call(reqf("{\"v\":1,\"id\":\"102\",\"cmd\":\"settings.get\","
+ "\"session\":\"%s\",\"org\":%d}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.smtp_security", "starttls");
+ CHECK(jget(d, "result.smtp_password") == NULL);
+ CHECK(!jbool(d, "result.smtp_password_set"));
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"103\",\"cmd\":\"settings.set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"key\":\"smtp_host\",\"value\":\"smtp.example.se\"}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.value", "smtp.example.se");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"104\",\"cmd\":\"settings.set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"key\":\"smtp_host\",\"value\":\"bad\\u0001host\"}}",
+ g_session, (int)org_id));
+ CHECK_STR(d, "error.code", "INVALID_ARGS");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"105\",\"cmd\":\"settings.set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"key\":\"smtp_port\",\"value\":\"587\"}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.value", "587");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"106\",\"cmd\":\"settings.set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"key\":\"smtp_port\",\"value\":\"0\"}}",
+ g_session, (int)org_id));
+ CHECK_STR(d, "error.code", "INVALID_ARGS");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"107\",\"cmd\":\"settings.set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"key\":\"smtp_port\",\"value\":\"70000\"}}",
+ g_session, (int)org_id));
+ CHECK_STR(d, "error.code", "INVALID_ARGS");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"108\",\"cmd\":\"settings.set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"key\":\"smtp_security\",\"value\":\"ssl\"}}",
+ g_session, (int)org_id));
+ CHECK_STR(d, "error.code", "INVALID_ARGS");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"109\",\"cmd\":\"settings.set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"key\":\"smtp_user\",\"value\":\"faktura@example.se\"}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"110\",\"cmd\":\"settings.set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"key\":\"smtp_from\",\"value\":\"Bokf AB <a@b.se>\"}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"111\",\"cmd\":\"settings.set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"key\":\"smtp_reply_to\",\"value\":\"svar@example.se\"}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"112\",\"cmd\":\"settings.set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"key\":\"smtp_security\",\"value\":\"tls\"}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.value", "tls");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"113\",\"cmd\":\"settings.get\","
+ "\"session\":\"%s\",\"org\":%d}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.smtp_host", "smtp.example.se");
+ CHECK_STR(d, "result.smtp_port", "587");
+ CHECK_STR(d, "result.smtp_user", "faktura@example.se");
+ CHECK_STR(d, "result.smtp_from", "Bokf AB <a@b.se>");
+ CHECK_STR(d, "result.smtp_reply_to", "svar@example.se");
+ CHECK_STR(d, "result.smtp_security", "tls");
+ yyjson_doc_free(d);
+
+ unsetenv("BOKFD_SECRET_KEY");
+ d = call(reqf("{\"v\":1,\"id\":\"114\",\"cmd\":\"settings.set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"key\":\"smtp_password\",\"value\":\"hunter2\"}}",
+ g_session, (int)org_id));
+ CHECK_STR(d, "error.code", "INTERNAL");
+ CHECK(jstr(d, "error.message") &&
+ strstr(jstr(d, "error.message"), "BOKFD_SECRET_KEY") != NULL);
+ yyjson_doc_free(d);
+
+ char *secret_stored = db_text(g_db, reqf("SELECT value FROM settings WHERE"
+ " org_id=%d AND key='smtp_password'",
+ (int)org_id));
+ CHECK(secret_stored == NULL);
+ free(secret_stored);
+
+ setenv("BOKFD_SECRET_KEY", "abcd", 1);
+ d = call(reqf("{\"v\":1,\"id\":\"115\",\"cmd\":\"settings.set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"key\":\"smtp_password\",\"value\":\"hunter2\"}}",
+ g_session, (int)org_id));
+ CHECK_STR(d, "error.code", "INTERNAL");
+ yyjson_doc_free(d);
+ unsetenv("BOKFD_SECRET_KEY");
+
+ setenv("BOKFD_SECRET_KEY",
+ "000102030405060708090a0b0c0d0e0f"
+ "101112131415161718191a1b1c1d1e1f",
+ 1);
+ d = call(reqf("{\"v\":1,\"id\":\"116\",\"cmd\":\"settings.set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"key\":\"smtp_password\",\"value\":\"hunter2\"}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.value", "[redacted]");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"117\",\"cmd\":\"settings.get\","
+ "\"session\":\"%s\",\"org\":%d}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(jbool(d, "result.smtp_password_set"));
+ CHECK(jget(d, "result.smtp_password") == NULL);
+ yyjson_doc_free(d);
+
+ secret_stored = db_text(g_db, reqf("SELECT value FROM settings WHERE"
+ " org_id=%d AND key='smtp_password'",
+ (int)org_id));
+ CHECK(secret_stored != NULL);
+ CHECK(secret_stored && strncmp(secret_stored, "enc:v1:", 7) == 0);
+ CHECK(secret_stored && strstr(secret_stored, "hunter2") == NULL);
+ char *plain = NULL;
+ CHECK(secret_stored && secret_decrypt(secret_stored, &plain) == 0);
+ CHECK(plain && strcmp(plain, "hunter2") == 0);
+ free(plain);
+ free(secret_stored);
+
+ char *audit = db_text(g_db,
+ "SELECT request_json FROM audit_log"
+ " WHERE action='settings.set'"
+ " ORDER BY seq DESC LIMIT 1");
+ CHECK(audit != NULL);
+ CHECK(audit && strstr(audit, "\"key\":\"smtp_password\"") != NULL);
+ CHECK(audit && strstr(audit, "[redacted]") != NULL);
+ CHECK(audit && strstr(audit, "hunter2") == NULL);
+ free(audit);
+
+ d = call(reqf("{\"v\":1,\"id\":\"118\",\"cmd\":\"settings.set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"key\":\"smtp_password\",\"value\":\"\"}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.value", "[redacted]");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"119\",\"cmd\":\"settings.get\","
+ "\"session\":\"%s\",\"org\":%d}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(!jbool(d, "result.smtp_password_set"));
+ CHECK(jget(d, "result.smtp_password") == NULL);
+ yyjson_doc_free(d);
+
+ secret_stored = db_text(g_db, reqf("SELECT value FROM settings WHERE"
+ " org_id=%d AND key='smtp_password'",
+ (int)org_id));
+ CHECK(secret_stored == NULL);
+ free(secret_stored);
+
+ audit = db_text(g_db,
+ "SELECT request_json FROM audit_log"
+ " WHERE action='settings.set'"
+ " ORDER BY seq DESC LIMIT 1");
+ CHECK(audit && strstr(audit, "\"key\":\"smtp_password\"") != NULL);
+ CHECK(audit && strstr(audit, "[redacted]") != NULL);
+ free(audit);
+
+ unsetenv("BOKFD_SECRET_KEY");
+ d = call(reqf("{\"v\":1,\"id\":\"120\",\"cmd\":\"settings.set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"key\":\"smtp_password\",\"value\":\"\"}}",
+ g_session, (int)org_id));
+ CHECK_STR(d, "error.code", "INTERNAL");
+ yyjson_doc_free(d);
+ unsetenv("BOKFD_SECRET_KEY");
+
/* ---------------- bank reconciliation (schema v8) ----------------- */
static const char seb_csv[] =
@@ -3042,7 +3348,7 @@ int main(void)
CHECK_OK(d);
CHECK_STR(d, "result.period", "2026P4");
CHECK_STR(d, "result.blankett", "INK2-2026P4");
- char *sb64 = jstr(d, "result.blanketter.content_base64");
+ const char *sb64 = jstr(d, "result.blanketter.content_base64");
unsigned char *srub = NULL;
size_t srul = 0;
CHECK(sb64 && util_b64_decode(sb64, strlen(sb64), &srub, &srul) == 0);