summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/commands.c122
-rw-r--r--src/secret.c194
-rw-r--r--src/secret.h9
3 files changed, 311 insertions, 14 deletions
diff --git a/src/commands.c b/src/commands.c
index a587ce3..6100d6a 100644
--- a/src/commands.c
+++ b/src/commands.c
@@ -19,12 +19,16 @@
#include "ledger.h"
#include "log.h"
#include "reports.h"
+#include "secret.h"
#include "seed.h"
#include "sie.h"
#include "sru.h"
#include "util.h"
#include "version.h"
+/* src/secret.c is not part of CORE_SRC; compile it as part of this unit. */
+#include "secret.c"
+
/* ------------------------------------------------------------------ */
/* small helpers */
/* ------------------------------------------------------------------ */
@@ -2612,6 +2616,7 @@ static yyjson_mut_val *h_settings_get(struct req *r)
{
yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
int have_default = 0, have_bank = 0, have_receivable = 0, have_revenue = 0;
+ int have_password = 0, have_security = 0;
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(
r->db, "SELECT key,value FROM settings WHERE org_id=?1", -1, &st,
@@ -2621,6 +2626,10 @@ static yyjson_mut_val *h_settings_get(struct req *r)
const char *k = (const char *)sqlite3_column_text(st, 0);
const char *v = (const char *)sqlite3_column_text(st, 1);
if (k) {
+ if (secret_key_is_secret(k)) {
+ have_password = 1;
+ continue;
+ }
yyjson_mut_obj_add(o, yyjson_mut_strcpy(r->rdoc, k),
yyjson_mut_strcpy(r->rdoc, v ? v : ""));
if (strcmp(k, "default_series") == 0)
@@ -2631,6 +2640,8 @@ static yyjson_mut_val *h_settings_get(struct req *r)
have_receivable = 1;
if (strcmp(k, "invoice_revenue_account") == 0)
have_revenue = 1;
+ if (strcmp(k, "smtp_security") == 0)
+ have_security = 1;
}
}
sqlite3_finalize(st);
@@ -2645,24 +2656,100 @@ static yyjson_mut_val *h_settings_get(struct req *r)
if (!have_revenue)
yyjson_mut_obj_add_strcpy(r->rdoc, o, "invoice_revenue_account",
"3001");
+ if (!have_security)
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "smtp_security", "starttls");
+ yyjson_mut_obj_add_bool(r->rdoc, o, "smtp_password_set", have_password);
+ return o;
+}
+
+static yyjson_mut_val *settings_result(struct req *r, const char *key,
+ const char *value, int dry)
+{
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "key", key);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "value", value);
+ if (dry)
+ yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
return o;
}
+static char *settings_secret_audit_json(const char *key)
+{
+ yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
+ yyjson_mut_val *o = yyjson_mut_obj(doc);
+ yyjson_mut_doc_set_root(doc, o);
+ yyjson_mut_obj_add_strcpy(doc, o, "key", key);
+ yyjson_mut_obj_add_strcpy(doc, o, "value", "[redacted]");
+ char *json = yyjson_mut_write(doc, 0, NULL);
+ yyjson_mut_doc_free(doc);
+ return json ? json : xstrdup("{}");
+}
+
static yyjson_mut_val *h_settings_set(struct req *r)
{
const char *key = arg_str(r->args, "key");
const char *value = arg_str(r->args, "value");
if (!key || !value)
return fail(r, "INVALID_ARGS", "key and value are required");
+ if (secret_key_is_secret(key)) {
+ if (!secret_available())
+ return fail(r, "INTERNAL",
+ "BOKFD_SECRET_KEY is missing or invalid");
+ char *stored = NULL;
+ if (*value && secret_encrypt(value, &stored) != 0)
+ return fail(r, "INTERNAL",
+ "BOKFD_SECRET_KEY is missing or invalid");
+ if (r->dry_run) {
+ free(stored);
+ return settings_result(r, key, "[redacted]", 1);
+ }
+ sqlite3_stmt *st = NULL;
+ const char *sql = *value
+ ? "INSERT INTO settings(org_id,key,value)"
+ " VALUES(?1,?2,?3)"
+ " ON CONFLICT(org_id,key) DO UPDATE SET"
+ " value=excluded.value"
+ : "DELETE FROM settings WHERE org_id=?1"
+ " AND key=?2";
+ if (sqlite3_prepare_v2(r->db, sql, -1, &st, NULL) != SQLITE_OK) {
+ free(stored);
+ return fail(r, "INTERNAL", "database error");
+ }
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_text(st, 2, key, -1, SQLITE_TRANSIENT);
+ if (*value)
+ sqlite3_bind_text(st, 3, stored, -1, SQLITE_TRANSIENT);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ free(stored);
+ if (rc != SQLITE_DONE)
+ return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
+ char *reqjson = settings_secret_audit_json(key);
+ audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
+ "settings.set", reqjson, "OK", NULL);
+ free(reqjson);
+ return settings_result(r, key, "[redacted]", 0);
+ }
size_t maxlen;
- int digits_only = 0, bankgiro = 0;
+ int digits_only = 0, bankgiro = 0, port = 0, security = 0;
if (strcmp(key, "default_series") == 0)
maxlen = 8;
- else if (strcmp(key, "attachment_dir") == 0)
+ else if (strcmp(key, "attachment_dir") == 0 ||
+ strcmp(key, "smtp_host") == 0 ||
+ strcmp(key, "smtp_user") == 0)
maxlen = 255;
- else if (strcmp(key, "bank_account") == 0 ||
- strcmp(key, "invoice_receivable_account") == 0 ||
- strcmp(key, "invoice_revenue_account") == 0) {
+ else if (strcmp(key, "smtp_from") == 0 ||
+ strcmp(key, "smtp_reply_to") == 0)
+ maxlen = 254;
+ else if (strcmp(key, "smtp_port") == 0) {
+ maxlen = 5;
+ port = 1;
+ } else if (strcmp(key, "smtp_security") == 0) {
+ maxlen = 8;
+ security = 1;
+ } else if (strcmp(key, "bank_account") == 0 ||
+ strcmp(key, "invoice_receivable_account") == 0 ||
+ strcmp(key, "invoice_revenue_account") == 0) {
maxlen = 10;
digits_only = 1;
} else if (strcmp(key, "invoice_bankgiro") == 0) {
@@ -2675,7 +2762,7 @@ static yyjson_mut_val *h_settings_set(struct req *r)
return failf(r, "INVALID_ARGS", "%s must be 1-%zu characters", key,
maxlen);
for (const char *p = value; *p; p++) {
- if (digits_only && (*p < '0' || *p > '9'))
+ if ((digits_only || port) && (*p < '0' || *p > '9'))
return failf(r, "INVALID_ARGS", "%s must be digits only", key);
if (bankgiro && !((*p >= '0' && *p <= '9') || *p == '-'))
return failf(r, "INVALID_ARGS",
@@ -2684,13 +2771,17 @@ static yyjson_mut_val *h_settings_set(struct req *r)
return failf(r, "INVALID_ARGS",
"%s must not contain control characters", key);
}
- if (r->dry_run) {
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "key", key);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "value", value);
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- return o;
+ if (port) {
+ long v = strtol(value, NULL, 10);
+ if (v < 1 || v > 65535)
+ return failf(r, "INVALID_ARGS", "%s must be 1-65535", key);
}
+ if (security && strcmp(value, "starttls") != 0 &&
+ strcmp(value, "tls") != 0 && strcmp(value, "plain") != 0)
+ return failf(r, "INVALID_ARGS", "%s must be starttls, tls or plain",
+ key);
+ if (r->dry_run)
+ return settings_result(r, key, value, 1);
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(
r->db,
@@ -7644,8 +7735,11 @@ static const struct cmd_arg args_bokslut_post[] = {
static const struct cmd_arg args_settings_set[] = {
{ "key", ARG_STR, 1, NULL, NULL,
"default_series, attachment_dir, bank_account,"
- " invoice_receivable_account or invoice_revenue_account" },
- { "value", ARG_STR, 1, NULL, NULL, "Setting value" },
+ " invoice_receivable_account, invoice_revenue_account, smtp_host,"
+ " smtp_port, smtp_user, smtp_from, smtp_reply_to, smtp_security or"
+ " smtp_password" },
+ { "value", ARG_STR, 0, NULL, NULL,
+ "Setting value; an empty value clears smtp_password" },
};
static const struct cmd_arg args_bank_import[] = {
diff --git a/src/secret.c b/src/secret.c
new file mode 100644
index 0000000..7222ddd
--- /dev/null
+++ b/src/secret.c
@@ -0,0 +1,194 @@
+#include "secret.h"
+
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <openssl/crypto.h>
+#include <openssl/evp.h>
+#include <openssl/rand.h>
+
+#include "util.h"
+
+#define SECRET_PREFIX "enc:v1:"
+#define SECRET_KEY_LEN 32
+#define SECRET_NONCE_LEN 12
+#define SECRET_TAG_LEN 16
+
+static int hex_val(int c)
+{
+ if (c >= '0' && c <= '9')
+ return c - '0';
+ if (c >= 'a' && c <= 'f')
+ return c - 'a' + 10;
+ if (c >= 'A' && c <= 'F')
+ return c - 'A' + 10;
+ return -1;
+}
+
+static int secret_key(unsigned char out[SECRET_KEY_LEN])
+{
+ const char *s = getenv("BOKFD_SECRET_KEY");
+ if (!s || !*s)
+ return -1;
+ size_t n = strlen(s);
+ if (n == 2 * SECRET_KEY_LEN) {
+ int hex = 1;
+ for (size_t i = 0; i < n && hex; i++)
+ if (hex_val((unsigned char)s[i]) < 0)
+ hex = 0;
+ if (hex) {
+ for (size_t i = 0; i < SECRET_KEY_LEN; i++)
+ out[i] = (unsigned char)((hex_val(s[2 * i]) << 4) |
+ hex_val(s[2 * i + 1]));
+ return 0;
+ }
+ }
+ if (n % 4 == 1 || n > 172)
+ return -1;
+ char padded[176];
+ memcpy(padded, s, n);
+ while (n % 4)
+ padded[n++] = '=';
+ padded[n] = '\0';
+ unsigned char *raw = NULL;
+ size_t raw_n = 0;
+ if (util_b64_decode(padded, n, &raw, &raw_n) != 0)
+ return -1;
+ int ok = raw_n == SECRET_KEY_LEN;
+ if (ok)
+ memcpy(out, raw, SECRET_KEY_LEN);
+ free(raw);
+ return ok ? 0 : -1;
+}
+
+int secret_available(void)
+{
+ unsigned char key[SECRET_KEY_LEN] = { 0 };
+ int ok = secret_key(key) == 0;
+ OPENSSL_cleanse(key, sizeof key);
+ return ok;
+}
+
+int secret_encrypt(const char *plain, char **out)
+{
+ if (out)
+ *out = NULL;
+ if (!plain || !out)
+ return -1;
+ size_t plen = strlen(plain);
+ if (plen > (size_t)INT_MAX)
+ return -1;
+ unsigned char key[SECRET_KEY_LEN], nonce[SECRET_NONCE_LEN];
+ if (secret_key(key) != 0)
+ return -1;
+ if (RAND_bytes(nonce, SECRET_NONCE_LEN) != 1) {
+ OPENSSL_cleanse(key, sizeof key);
+ return -1;
+ }
+ size_t clen = plen + SECRET_TAG_LEN;
+ unsigned char *ct = xmalloc(clen);
+ EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
+ int len = 0, total = 0, ok = 0;
+ if (ctx &&
+ EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) == 1 &&
+ EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, SECRET_NONCE_LEN,
+ NULL) == 1 &&
+ EVP_EncryptInit_ex(ctx, NULL, NULL, key, nonce) == 1 &&
+ EVP_EncryptUpdate(ctx, ct, &len, (const unsigned char *)plain,
+ (int)plen) == 1) {
+ total = len;
+ ok = EVP_EncryptFinal_ex(ctx, ct + total, &len) == 1;
+ total += len;
+ }
+ if (ok)
+ ok = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, SECRET_TAG_LEN,
+ ct + total) == 1;
+ EVP_CIPHER_CTX_free(ctx);
+ OPENSSL_cleanse(key, sizeof key);
+ if (!ok || total != (int)plen) {
+ free(ct);
+ return -1;
+ }
+ total += SECRET_TAG_LEN;
+ char *nonce_b64 = util_b64(nonce, sizeof nonce);
+ char *ct_b64 = util_b64(ct, (size_t)total);
+ free(ct);
+ size_t olen =
+ strlen(SECRET_PREFIX) + strlen(nonce_b64) + 1 + strlen(ct_b64) + 1;
+ char *s = xmalloc(olen);
+ snprintf(s, olen, "%s%s:%s", SECRET_PREFIX, nonce_b64, ct_b64);
+ free(nonce_b64);
+ free(ct_b64);
+ *out = s;
+ return 0;
+}
+
+int secret_decrypt(const char *stored, char **out)
+{
+ if (out)
+ *out = NULL;
+ if (!stored || !out)
+ return -1;
+ if (strncmp(stored, SECRET_PREFIX, strlen(SECRET_PREFIX)) != 0)
+ return -1;
+ const char *p = stored + strlen(SECRET_PREFIX);
+ const char *sep = strchr(p, ':');
+ if (!sep || sep == p || !sep[1])
+ return -1;
+ unsigned char key[SECRET_KEY_LEN];
+ if (secret_key(key) != 0)
+ return -1;
+ unsigned char *nonce = NULL, *ct = NULL;
+ size_t nonce_n = 0, ct_n = 0;
+ if (util_b64_decode(p, (size_t)(sep - p), &nonce, &nonce_n) != 0) {
+ OPENSSL_cleanse(key, sizeof key);
+ return -1;
+ }
+ if (util_b64_decode(sep + 1, strlen(sep + 1), &ct, &ct_n) != 0) {
+ free(nonce);
+ OPENSSL_cleanse(key, sizeof key);
+ return -1;
+ }
+ if (nonce_n != SECRET_NONCE_LEN || ct_n < SECRET_TAG_LEN ||
+ ct_n - SECRET_TAG_LEN > (size_t)INT_MAX) {
+ free(nonce);
+ free(ct);
+ OPENSSL_cleanse(key, sizeof key);
+ return -1;
+ }
+ size_t plen = ct_n - SECRET_TAG_LEN;
+ unsigned char *pt = xmalloc(plen + 1);
+ EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
+ int len = 0, total = 0, ok = 0;
+ if (ctx &&
+ EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) == 1 &&
+ EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, SECRET_NONCE_LEN,
+ NULL) == 1 &&
+ EVP_DecryptInit_ex(ctx, NULL, NULL, key, nonce) == 1 &&
+ EVP_DecryptUpdate(ctx, pt, &len, ct, (int)plen) == 1) {
+ total = len;
+ if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, SECRET_TAG_LEN,
+ ct + plen) == 1)
+ ok = EVP_DecryptFinal_ex(ctx, pt + total, &len) == 1;
+ total += len;
+ }
+ EVP_CIPHER_CTX_free(ctx);
+ OPENSSL_cleanse(key, sizeof key);
+ free(nonce);
+ free(ct);
+ if (!ok || total != (int)plen) {
+ OPENSSL_cleanse(pt, plen + 1);
+ free(pt);
+ return -1;
+ }
+ pt[total] = '\0';
+ *out = (char *)pt;
+ return 0;
+}
+
+int secret_key_is_secret(const char *key)
+{
+ return key && strcmp(key, "smtp_password") == 0;
+}
diff --git a/src/secret.h b/src/secret.h
new file mode 100644
index 0000000..3dd03a4
--- /dev/null
+++ b/src/secret.h
@@ -0,0 +1,9 @@
+#ifndef BOKF_SECRET_H
+#define BOKF_SECRET_H
+
+int secret_available(void);
+int secret_encrypt(const char *plain, char **out);
+int secret_decrypt(const char *stored, char **out);
+int secret_key_is_secret(const char *key);
+
+#endif