/* * consent.h * First-use consent: prompt wording and the stored-decision table * (0:/config/consent.toml). See docs/consent.md. * Copyright (c) 2026 Daniel Hammer */ #pragma once #include #include #include #include #include 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 " 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..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