210 lines
8.1 KiB
C++
210 lines
8.1 KiB
C++
/*
|
|
* consent.h
|
|
* First-use consent: prompt wording and the stored-decision table
|
|
* (0:/config/consent.toml).
|
|
* Copyright (c) 2026 Daniel Hammer
|
|
*/
|
|
|
|
#pragma once
|
|
#include <montauk/syscall.h>
|
|
#include <montauk/string.h>
|
|
#include <montauk/toml.h>
|
|
#include <montauk/config.h>
|
|
#include <montauk/capabilities.h>
|
|
|
|
namespace montauk {
|
|
namespace consent {
|
|
|
|
inline constexpr const char* STORE_CONFIG = "consent";
|
|
inline constexpr const char* STORE_PREFIX = "decision.";
|
|
|
|
struct CapText {
|
|
uint64_t bit;
|
|
const char* action; // completes "<App> wants to ..."
|
|
const char* icon; // shown in the prompt
|
|
};
|
|
|
|
inline constexpr CapText TEXTS[] = {
|
|
{montauk::abi::CAP_STORAGE_ADMIN, "format, partition and mount disks",
|
|
"0:/os/icons/drive-harddisk.svg"},
|
|
{montauk::abi::CAP_NETWORK_ADMIN, "change network settings and join Wi-Fi networks",
|
|
"0:/os/icons/network-wired.svg"},
|
|
{montauk::abi::CAP_SET_TIME, "change the system clock and time zone",
|
|
"0:/os/icons/preferences-system-time.svg"},
|
|
{montauk::abi::CAP_DISPLAY_ADMIN, "change the screen resolution and brightness",
|
|
"0:/os/icons/preferences-desktop-display.svg"},
|
|
{montauk::abi::CAP_LOG_READ, "read the system log, which can reveal what other apps and devices are doing",
|
|
"0:/os/icons/utilities-log-viewer.svg"},
|
|
};
|
|
|
|
// Fallback for a capability without its own icon.
|
|
inline constexpr const char* DEFAULT_ICON = "0:/os/icons/lock.svg";
|
|
|
|
inline const char* action_for(uint64_t bit) {
|
|
for (const auto& t : TEXTS) {
|
|
if (t.bit == bit) return t.action;
|
|
}
|
|
return "use a protected system capability";
|
|
}
|
|
|
|
inline const char* icon_for(uint64_t bit) {
|
|
for (const auto& t : TEXTS) {
|
|
if (t.bit == bit) return t.icon;
|
|
}
|
|
return DEFAULT_ICON;
|
|
}
|
|
|
|
inline const char* name_for(uint64_t bit) {
|
|
for (const auto& n : montauk::caps::NAMES) {
|
|
if (n.bit == bit) return n.name;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// One stored decision, as read from the table. Only programs installed
|
|
// under 0:/os or 0:/apps are ever stored: those paths cannot be rewritten
|
|
// without CAP_SYSTEM_IMAGE, so the path alone identifies the program.
|
|
// The kernel reports this as ConsentRequest::imageProtected.
|
|
struct Decision {
|
|
char id[16];
|
|
char path[256];
|
|
uint64_t capability;
|
|
bool allow;
|
|
};
|
|
|
|
namespace detail {
|
|
inline void key(char* out, int cap, const char* id, const char* field) {
|
|
int n = 0;
|
|
for (const char* p = STORE_PREFIX; *p && n < cap - 1; p++) out[n++] = *p;
|
|
for (const char* p = id; *p && n < cap - 1; p++) out[n++] = *p;
|
|
if (n < cap - 1) out[n++] = '.';
|
|
for (const char* p = field; *p && n < cap - 1; p++) out[n++] = *p;
|
|
out[n] = '\0';
|
|
}
|
|
|
|
// Ids of "decision.<id>.path" entries, in file order.
|
|
inline int collect_ids(const montauk::toml::Doc& doc, char (*ids)[16], int max) {
|
|
int count = 0;
|
|
int prefixLen = montauk::slen(STORE_PREFIX);
|
|
for (int i = 0; i < doc.entries.count && count < max; i++) {
|
|
montauk::toml::Value* v = doc.entries.items[i];
|
|
if (v == nullptr || v->key == nullptr) continue;
|
|
if (!montauk::starts_with(v->key, STORE_PREFIX)) continue;
|
|
const char* rest = v->key + prefixLen;
|
|
int dot = 0;
|
|
while (rest[dot] && rest[dot] != '.') dot++;
|
|
if (rest[dot] != '.' || !montauk::streq(rest + dot + 1, "path")) continue;
|
|
if (dot <= 0 || dot >= 16) continue;
|
|
montauk::memcpy(ids[count], rest, dot);
|
|
ids[count][dot] = '\0';
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
inline bool read(const montauk::toml::Doc& doc, const char* id, Decision& out) {
|
|
char k[64];
|
|
montauk::memset(&out, 0, sizeof(out));
|
|
montauk::strncpy(out.id, id, sizeof(out.id));
|
|
key(k, sizeof(k), id, "path");
|
|
montauk::strncpy(out.path, doc.get_string(k, ""), sizeof(out.path));
|
|
key(k, sizeof(k), id, "capability");
|
|
out.capability = montauk::caps::bit_for_name(doc.get_string(k, ""));
|
|
key(k, sizeof(k), id, "decision");
|
|
out.allow = montauk::streq(doc.get_string(k, ""), "allow");
|
|
// "all" or an unknown name is never a valid stored decision.
|
|
return out.path[0] &&
|
|
(out.capability & montauk::abi::CAP_CONSENT_ELIGIBLE) != 0 &&
|
|
(out.capability & (out.capability - 1)) == 0;
|
|
}
|
|
|
|
inline void remove_id(montauk::toml::Doc* doc, const char* id) {
|
|
static const char* fields[] = {"path", "capability", "decision"};
|
|
char k[64];
|
|
for (const char* f : fields) {
|
|
key(k, sizeof(k), id, f);
|
|
montauk::config::unset(doc, k);
|
|
}
|
|
}
|
|
} // namespace detail
|
|
|
|
// Read up to `max` stored decisions. Returns the count.
|
|
inline int load_all(Decision* out, int max) {
|
|
montauk::toml::Doc doc = montauk::config::load(STORE_CONFIG);
|
|
char ids[128][16];
|
|
int idCount = detail::collect_ids(doc, ids, 128);
|
|
int count = 0;
|
|
for (int i = 0; i < idCount && count < max; i++) {
|
|
if (detail::read(doc, ids[i], out[count])) count++;
|
|
}
|
|
doc.destroy();
|
|
return count;
|
|
}
|
|
|
|
// Stored answer for (path, capability): 1 allow, 0 deny, -1 none.
|
|
inline int lookup(const char* path, uint64_t capability) {
|
|
montauk::toml::Doc doc = montauk::config::load(STORE_CONFIG);
|
|
char ids[128][16];
|
|
int idCount = detail::collect_ids(doc, ids, 128);
|
|
int result = -1;
|
|
for (int i = 0; i < idCount && result < 0; i++) {
|
|
Decision d;
|
|
if (!detail::read(doc, ids[i], d)) continue;
|
|
if (d.capability == capability && montauk::streq(d.path, path)) {
|
|
result = d.allow ? 1 : 0;
|
|
}
|
|
}
|
|
doc.destroy();
|
|
return result;
|
|
}
|
|
|
|
// Store an answer, replacing any earlier one for the same path and
|
|
// capability. Returns 0 on success, negative when the table cannot be
|
|
// written.
|
|
inline int record(const char* path, uint64_t capability, bool allow) {
|
|
montauk::toml::Doc doc = montauk::config::load(STORE_CONFIG);
|
|
char ids[128][16];
|
|
int idCount = detail::collect_ids(doc, ids, 128);
|
|
int nextId = 1;
|
|
for (int i = 0; i < idCount; i++) {
|
|
Decision d;
|
|
bool valid = detail::read(doc, ids[i], d);
|
|
if (valid && d.capability == capability && montauk::streq(d.path, path))
|
|
detail::remove_id(&doc, ids[i]);
|
|
int n = 0;
|
|
for (const char* p = ids[i]; *p >= '0' && *p <= '9'; p++) n = n * 10 + (*p - '0');
|
|
if (n >= nextId) nextId = n + 1;
|
|
}
|
|
|
|
char id[16];
|
|
int len = 0;
|
|
char rev[16];
|
|
for (int n = nextId; n > 0 && len < 15; n /= 10) rev[len++] = (char)('0' + n % 10);
|
|
for (int i = 0; i < len; i++) id[i] = rev[len - 1 - i];
|
|
id[len] = '\0';
|
|
|
|
char k[64];
|
|
detail::key(k, sizeof(k), id, "path");
|
|
montauk::config::set_string(&doc, k, path);
|
|
detail::key(k, sizeof(k), id, "capability");
|
|
montauk::config::set_string(&doc, k, name_for(capability));
|
|
detail::key(k, sizeof(k), id, "decision");
|
|
montauk::config::set_string(&doc, k, allow ? "allow" : "deny");
|
|
|
|
int rc = montauk::config::save(STORE_CONFIG, &doc);
|
|
doc.destroy();
|
|
return rc;
|
|
}
|
|
|
|
// Drop one stored decision by id; the program is asked again next time.
|
|
inline int forget(const char* id) {
|
|
montauk::toml::Doc doc = montauk::config::load(STORE_CONFIG);
|
|
detail::remove_id(&doc, id);
|
|
int rc = montauk::config::save(STORE_CONFIG, &doc);
|
|
doc.destroy();
|
|
return rc;
|
|
}
|
|
|
|
} // namespace consent
|
|
} // namespace montauk
|