From 300783972189600190506c04a1b4831eab4edc06 Mon Sep 17 00:00:00 2001 From: oxmc7769 Date: Sat, 25 Jul 2026 00:05:38 -0700 Subject: [PATCH] Add Android Binder dispatch, platform/linux+platform/android split, and PawletOS content-cache support - VesperProfileBinderService + main_android.cpp: real Android AIDL Binder service backing IVesperProfileService, wired into Android.bp's srcs (previously declared but never implemented). - SignatureVerifier: real CMS verification on Android too, via a vendored static OpenSSL (see third_party/openssl-android/README.md) since BoringSSL has no CMS/PKCS#7 support. - ProfileStore: Android-appropriate data paths. - Every payload handler split into src/platform/.h (shared contract) + src/platform/linux/.cpp + src/platform/android/.cpp, so the build system picks the platform instead of #ifdef. Android side is an honest "not implemented yet" stub per handler, logged rather than silent. - content-cache payload + handler: PawletOS-fork-specific, talks to PawletCache/pawletcache-server. Not part of vesperprofiled's own upstream default. --- Android.bp | 51 +- CMakeLists.txt | 19 +- sepolicy/vesperprofiled.te | 7 + src/ProfileStore.h | 17 +- src/SignatureVerifier.cpp | 17 +- src/SignatureVerifier.h | 13 +- src/VesperProfileBinderService.cpp | 322 ++++ src/VesperProfileBinderService.h | 57 + src/main_android.cpp | 96 ++ src/payloads/PayloadHandler.h | 16 +- src/payloads/PayloadHandlers.cpp | 1414 +---------------- src/payloads/PayloadUtil.h | 112 ++ src/platform/Cert.h | 7 + src/platform/ContentCache.h | 7 + src/platform/DnsProxy.h | 7 + src/platform/Ethernet.h | 7 + src/platform/Firewall.h | 7 + src/platform/FirstBoot.h | 7 + src/platform/Ldap.h | 7 + src/platform/Mdm.h | 7 + src/platform/Passcode.h | 7 + src/platform/Pkcs12.h | 7 + src/platform/Proxy.h | 7 + src/platform/Screensaver.h | 7 + src/platform/SoftwareUpdate.h | 7 + src/platform/TimeServer.h | 7 + src/platform/Vpn.h | 7 + src/platform/Wallpaper.h | 7 + src/platform/Wifi.h | 11 + src/platform/android/Cert.cpp | 20 + src/platform/android/ContentCache.cpp | 52 + src/platform/android/DnsProxy.cpp | 20 + src/platform/android/Ethernet.cpp | 19 + src/platform/android/Firewall.cpp | 20 + src/platform/android/FirstBoot.cpp | 20 + src/platform/android/Ldap.cpp | 21 + src/platform/android/Mdm.cpp | 20 + src/platform/android/Passcode.cpp | 20 + src/platform/android/Pkcs12.cpp | 20 + src/platform/android/Proxy.cpp | 20 + src/platform/android/Screensaver.cpp | 21 + src/platform/android/SoftwareUpdate.cpp | 22 + src/platform/android/TimeServer.cpp | 20 + src/platform/android/Vpn.cpp | 21 + src/platform/android/Wallpaper.cpp | 20 + src/platform/android/Wifi.cpp | 20 + src/platform/linux/Cert.cpp | 48 + src/platform/linux/ContentCache.cpp | 51 + src/platform/linux/DnsProxy.cpp | 53 + src/platform/linux/Ethernet.cpp | 106 ++ src/platform/linux/Firewall.cpp | 67 + src/platform/linux/FirstBoot.cpp | 166 ++ src/platform/linux/Ldap.cpp | 75 + src/platform/linux/Mdm.cpp | 77 + src/platform/linux/NetworkManagerUtil.h | 29 + src/platform/linux/Passcode.cpp | 78 + src/platform/linux/Pkcs12.cpp | 53 + src/platform/linux/Proxy.cpp | 98 ++ src/platform/linux/Screensaver.cpp | 51 + src/platform/linux/SoftwareUpdate.cpp | 76 + src/platform/linux/TimeServer.cpp | 42 + src/platform/linux/Vpn.cpp | 120 ++ src/platform/linux/Wallpaper.cpp | 70 + src/platform/linux/Wifi.cpp | 177 +++ third_party/openssl-android/Android.bp | 32 + third_party/openssl-android/README.md | 79 + .../openssl-android/include/PLACEHOLDER.txt | 1 + .../lib/arm64-v8a/PLACEHOLDER.txt | 1 + .../lib/armeabi-v7a/PLACEHOLDER.txt | 1 + .../openssl-android/lib/x86/PLACEHOLDER.txt | 1 + .../lib/x86_64/PLACEHOLDER.txt | 1 + vesperprofiled.rc | 6 + 72 files changed, 2825 insertions(+), 1374 deletions(-) create mode 100644 src/VesperProfileBinderService.cpp create mode 100644 src/VesperProfileBinderService.h create mode 100644 src/main_android.cpp create mode 100644 src/payloads/PayloadUtil.h create mode 100644 src/platform/Cert.h create mode 100644 src/platform/ContentCache.h create mode 100644 src/platform/DnsProxy.h create mode 100644 src/platform/Ethernet.h create mode 100644 src/platform/Firewall.h create mode 100644 src/platform/FirstBoot.h create mode 100644 src/platform/Ldap.h create mode 100644 src/platform/Mdm.h create mode 100644 src/platform/Passcode.h create mode 100644 src/platform/Pkcs12.h create mode 100644 src/platform/Proxy.h create mode 100644 src/platform/Screensaver.h create mode 100644 src/platform/SoftwareUpdate.h create mode 100644 src/platform/TimeServer.h create mode 100644 src/platform/Vpn.h create mode 100644 src/platform/Wallpaper.h create mode 100644 src/platform/Wifi.h create mode 100644 src/platform/android/Cert.cpp create mode 100644 src/platform/android/ContentCache.cpp create mode 100644 src/platform/android/DnsProxy.cpp create mode 100644 src/platform/android/Ethernet.cpp create mode 100644 src/platform/android/Firewall.cpp create mode 100644 src/platform/android/FirstBoot.cpp create mode 100644 src/platform/android/Ldap.cpp create mode 100644 src/platform/android/Mdm.cpp create mode 100644 src/platform/android/Passcode.cpp create mode 100644 src/platform/android/Pkcs12.cpp create mode 100644 src/platform/android/Proxy.cpp create mode 100644 src/platform/android/Screensaver.cpp create mode 100644 src/platform/android/SoftwareUpdate.cpp create mode 100644 src/platform/android/TimeServer.cpp create mode 100644 src/platform/android/Vpn.cpp create mode 100644 src/platform/android/Wallpaper.cpp create mode 100644 src/platform/android/Wifi.cpp create mode 100644 src/platform/linux/Cert.cpp create mode 100644 src/platform/linux/ContentCache.cpp create mode 100644 src/platform/linux/DnsProxy.cpp create mode 100644 src/platform/linux/Ethernet.cpp create mode 100644 src/platform/linux/Firewall.cpp create mode 100644 src/platform/linux/FirstBoot.cpp create mode 100644 src/platform/linux/Ldap.cpp create mode 100644 src/platform/linux/Mdm.cpp create mode 100644 src/platform/linux/NetworkManagerUtil.h create mode 100644 src/platform/linux/Passcode.cpp create mode 100644 src/platform/linux/Pkcs12.cpp create mode 100644 src/platform/linux/Proxy.cpp create mode 100644 src/platform/linux/Screensaver.cpp create mode 100644 src/platform/linux/SoftwareUpdate.cpp create mode 100644 src/platform/linux/TimeServer.cpp create mode 100644 src/platform/linux/Vpn.cpp create mode 100644 src/platform/linux/Wallpaper.cpp create mode 100644 src/platform/linux/Wifi.cpp create mode 100644 third_party/openssl-android/Android.bp create mode 100644 third_party/openssl-android/README.md create mode 100644 third_party/openssl-android/include/PLACEHOLDER.txt create mode 100644 third_party/openssl-android/lib/arm64-v8a/PLACEHOLDER.txt create mode 100644 third_party/openssl-android/lib/armeabi-v7a/PLACEHOLDER.txt create mode 100644 third_party/openssl-android/lib/x86/PLACEHOLDER.txt create mode 100644 third_party/openssl-android/lib/x86_64/PLACEHOLDER.txt diff --git a/Android.bp b/Android.bp index 7c1526d..36b8d89 100644 --- a/Android.bp +++ b/Android.bp @@ -29,38 +29,65 @@ aidl_interface { versions: ["1"], } +// ── Vendored OpenSSL (Android-only, static, this binary only) ───────────── +// See third_party/openssl-android/README.md — BoringSSL (the system +// libcrypto/libssl) has no CMS/PKCS#7 support, so SignatureVerifier.cpp +// links a private static copy instead of touching the platform's crypto +// libs. third_party/openssl-android/Android.bp is picked up automatically +// by Soong's normal recursive discovery — nothing to reference here beyond +// the module names ("libssl_vesper_static" / "libcrypto_vesper_static") +// used in static_libs below. + // ── Native daemon ───────────────────────────────────────────────────────── +// +// This file is the Android/Soong build only — the Linux/D-Bus build lives +// in CMakeLists.txt and is entirely separate. Accordingly the source list +// below is the Android-specific set: main_android.cpp (Binder entry point) +// and VesperProfileBinderService.cpp instead of main.cpp/VesperProfileService.cpp +// (D-Bus), and no ZTE client (it watches NetworkManager over D-Bus, which +// doesn't exist on Android — separate follow-up work, see main_android.cpp's +// header comment). cc_binary { name: "vesperprofiled", srcs: [ - // Profile daemon core - "src/main.cpp", - "src/VesperProfileService.cpp", + "src/main_android.cpp", + "src/VesperProfileBinderService.cpp", "src/ProfileParser.cpp", "src/ProfileStore.cpp", "src/SignatureVerifier.cpp", - "src/payloads/PayloadHandlers.cpp", - - // ZTE client — compiled into the same binary - "src/zte/DeviceIdentity.cpp", - "src/zte/ZTELookupClient.cpp", - "src/zte/ConnectivityWatcher.cpp", + "src/payloads/PayloadHandlers.cpp", // shared dispatch — see its header comment + "src/platform/android/Wifi.cpp", + "src/platform/android/Ethernet.cpp", + "src/platform/android/Vpn.cpp", + "src/platform/android/Cert.cpp", + "src/platform/android/Pkcs12.cpp", + "src/platform/android/Passcode.cpp", + "src/platform/android/Mdm.cpp", + "src/platform/android/SoftwareUpdate.cpp", + "src/platform/android/TimeServer.cpp", + "src/platform/android/Proxy.cpp", + "src/platform/android/DnsProxy.cpp", + "src/platform/android/Firewall.cpp", + "src/platform/android/Ldap.cpp", + "src/platform/android/Wallpaper.cpp", + "src/platform/android/Screensaver.cpp", + "src/platform/android/FirstBoot.cpp", + "src/platform/android/ContentCache.cpp", ], shared_libs: [ "libbinder_ndk", "libbase", "liblog", - "libcrypto", - "libssl", - "libcurl", "me.oxmc.vesperos.profile-V1-ndk", ], static_libs: [ "libyaml", + "libssl_vesper_static", + "libcrypto_vesper_static", ], cflags: [ diff --git a/CMakeLists.txt b/CMakeLists.txt index a896895..40857dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,24 @@ set(SOURCES src/ProfileParser.cpp src/ProfileStore.cpp src/SignatureVerifier.cpp - src/payloads/PayloadHandlers.cpp + src/payloads/PayloadHandlers.cpp # shared dispatch — see its header comment + src/platform/linux/Wifi.cpp + src/platform/linux/Ethernet.cpp + src/platform/linux/Vpn.cpp + src/platform/linux/Cert.cpp + src/platform/linux/Pkcs12.cpp + src/platform/linux/Passcode.cpp + src/platform/linux/Mdm.cpp + src/platform/linux/SoftwareUpdate.cpp + src/platform/linux/TimeServer.cpp + src/platform/linux/Proxy.cpp + src/platform/linux/DnsProxy.cpp + src/platform/linux/Firewall.cpp + src/platform/linux/Ldap.cpp + src/platform/linux/Wallpaper.cpp + src/platform/linux/Screensaver.cpp + src/platform/linux/FirstBoot.cpp + src/platform/linux/ContentCache.cpp src/zte/DeviceIdentity.cpp src/zte/ZTELookupClient.cpp src/zte/ConnectivityWatcher.cpp diff --git a/sepolicy/vesperprofiled.te b/sepolicy/vesperprofiled.te index 6376611..5812c3c 100644 --- a/sepolicy/vesperprofiled.te +++ b/sepolicy/vesperprofiled.te @@ -60,3 +60,10 @@ binder_call(vesperprofiled, keystore) allow vesperprofiled self:process { fork sigchld }; allow vesperprofiled self:unix_stream_socket { create connect read write }; + +# ── content-cache payload's policy override file (PawletOS-fork-specific) ── +# Type declared in android_packages_apps_PawletCache/sepolicy/pawlet_cache.te +# (both dirs land in BOARD_SEPOLICY_DIRS) — see ContentCacheHandler and +# PolicyOverride.kt for the read/write contract on this file. +allow vesperprofiled pawletcache_policy_file:dir { create search getattr add_name }; +allow vesperprofiled pawletcache_policy_file:file create_file_perms; diff --git a/src/ProfileStore.h b/src/ProfileStore.h index 08f7c8c..5b24fd7 100644 --- a/src/ProfileStore.h +++ b/src/ProfileStore.h @@ -8,20 +8,31 @@ namespace vesperos::profile { -// Persists installed profiles to /var/lib/vesperprofiled/profiles/ +// Persists installed profiles under kProfilesDir. // // Layout: -// /var/lib/vesperprofiled/profiles/ +// / // index.json // / // profile.yml raw (unwrapped) YAML // meta.json install time, trust level, payload count // removal_hash.bin PBKDF2-SHA256 hash of removal password (if set) - +// +// Android's path matches what vesperprofiled.rc already mkdir's at +// post-fs-data and what sepolicy/file_contexts already labels +// vesperprofiled_data_file — see android_packages_apps_PawletCache's +// equivalent PolicyOverride.kt contract for the analogous device-side path. +#if defined(__ANDROID__) +static constexpr const char* kProfilesDir = + "/data/system/vesperos/profiles"; +static constexpr const char* kIndexFile = + "/data/system/vesperos/profiles/index.json"; +#else static constexpr const char* kProfilesDir = "/var/lib/vesperprofiled/profiles"; static constexpr const char* kIndexFile = "/var/lib/vesperprofiled/profiles/index.json"; +#endif class ProfileStore { public: diff --git a/src/SignatureVerifier.cpp b/src/SignatureVerifier.cpp index 9da5359..adbc630 100644 --- a/src/SignatureVerifier.cpp +++ b/src/SignatureVerifier.cpp @@ -1,5 +1,12 @@ #include "SignatureVerifier.h" +// AOSP's system libcrypto/libssl is BoringSSL, which has no CMS/PKCS#7 +// support — so this file links against a vendored, statically-linked real +// OpenSSL on Android instead of the system one (see +// third_party/openssl-android/ and Android.bp's Android-target static_libs). +// The Linux build (CMakeLists.txt) links Debian's system OpenSSL the normal +// way. Either way, the code below is the same real CMS_verify() — no +// platform split needed here, only at the link-config level. #include #include #include @@ -36,9 +43,13 @@ static StorePtr buildTrustStore() { X509_STORE* store = X509_STORE_new(); if (!store) return nullptr; - // Load Debian system CA bundle (ca-certificates package) - X509_STORE_load_locations(store, - SignatureVerifier::kTrustStorePath, nullptr); +#if defined(__ANDROID__) + // AOSP's cacerts dir is CApath-style (hash-named PEM files), not one bundle. + X509_STORE_load_locations(store, nullptr, SignatureVerifier::kTrustStorePath); +#else + // Debian's ca-certificates.crt is a single CAfile bundle. + X509_STORE_load_locations(store, SignatureVerifier::kTrustStorePath, nullptr); +#endif // Load VesperOS profile signing CA if installed struct stat st{}; diff --git a/src/SignatureVerifier.h b/src/SignatureVerifier.h index 7a18f7f..9f2a30b 100644 --- a/src/SignatureVerifier.h +++ b/src/SignatureVerifier.h @@ -25,13 +25,24 @@ public: VerifyResult verify(const std::vector& cmsData, std::vector& outPayload); +#if defined(__ANDROID__) + // AOSP's system CA store is a *directory* of hash-named PEM files + // (c_rehash layout), not one bundle file — buildTrustStore() loads it + // via X509_STORE's CApath, not CAfile. See SignatureVerifier.cpp. + static constexpr const char* kTrustStorePath = + "/system/etc/security/cacerts"; + // VesperOS profile signing CA (optional) — writable partition, covered + // by sepolicy's vesperprofiled_data_file type. + static constexpr const char* kVesperCaPath = + "/data/system/vesperos/profile_ca.pem"; +#else // System CA trust bundle static constexpr const char* kTrustStorePath = "/etc/ssl/certs/ca-certificates.crt"; - // VesperOS profile signing CA (optional; installed by the vesperos-ca package) static constexpr const char* kVesperCaPath = "/etc/vesperprofiled/profile_ca.pem"; +#endif }; } // namespace vesperos::profile diff --git a/src/VesperProfileBinderService.cpp b/src/VesperProfileBinderService.cpp new file mode 100644 index 0000000..9f76380 --- /dev/null +++ b/src/VesperProfileBinderService.cpp @@ -0,0 +1,322 @@ +#include "VesperProfileBinderService.h" +#include "payloads/PayloadHandler.h" + +#include +#include +#include + +namespace vesperos::profile { + +using ::ndk::ScopedAStatus; + +// Service-specific error codes returned via ScopedAStatus — mirrors +// VesperProfileService's D-Bus error domain (kErrNotFound etc.) one-for-one +// so a future shared client-side error mapping stays simple. +namespace { +constexpr int kErrNotFound = 1; +constexpr int kErrSignature = 2; +constexpr int kErrParse = 3; +constexpr int kErrLocked = 4; +constexpr int kErrWrongPassword = 5; +constexpr int kErrSigningRequired = 6; +} // namespace + +ScopedAStatus VesperProfileBinderService::serviceError(const char* name, const std::string& message) { + syslog(LOG_WARNING, "vesperprofiled: %s: %s", name, message.c_str()); + int code = 0; + if (strcmp(name, "NotFound") == 0) code = kErrNotFound; + else if (strcmp(name, "Signature") == 0) code = kErrSignature; + else if (strcmp(name, "Parse") == 0) code = kErrParse; + else if (strcmp(name, "Locked") == 0) code = kErrLocked; + else if (strcmp(name, "WrongPassword") == 0) code = kErrWrongPassword; + else if (strcmp(name, "SigningRequired") == 0) code = kErrSigningRequired; + return ScopedAStatus::fromServiceSpecificErrorWithMessage(code, message.c_str()); +} + +VesperProfileBinderService::VesperProfileBinderService() { + syslog(LOG_INFO, "vesperprofiled: Binder service starting"); + mStore.load(); +} + +// ── installProfileDirect (preinstalled profiles, same as D-Bus version) ──── + +bool VesperProfileBinderService::installProfileDirect( + const std::vector& profileData, std::string& outUuid) { + std::lock_guard lock(mLock); + + std::vector yamlBytes; + bool isSigned = false; + SignatureVerifier::TrustLevel trust = SignatureVerifier::TrustLevel::UNSIGNED; + + if (mVerifier.isCmsWrapped(profileData)) { + auto result = mVerifier.verify(profileData, yamlBytes); + if (result == SignatureVerifier::VerifyResult::INVALID) { + syslog(LOG_ERR, "[installProfileDirect] signature invalid"); + return false; + } + isSigned = true; + trust = (result == SignatureVerifier::VerifyResult::TRUSTED) + ? SignatureVerifier::TrustLevel::TRUSTED + : SignatureVerifier::TrustLevel::UNVERIFIED; + } else { + yamlBytes = profileData; + } + + ParsedProfile profile; + if (!mParser.parse(yamlBytes, profile)) { + syslog(LOG_ERR, "[installProfileDirect] parse failed"); + return false; + } + + ParsedProfile existing; + if (mStore.load(profile.uuid, existing)) { + syslog(LOG_INFO, "[installProfileDirect] already installed uuid=%s, skipping", + profile.uuid.c_str()); + outUuid = profile.uuid; + return true; + } + + for (const auto& payload : profile.payloads) { + bool sigRequired = + payload.type == "mdm" || + payload.type == "removal-password" || + payload.type == "kiosk" || + payload.type == "asam"; + if (sigRequired && !isSigned) { + syslog(LOG_ERR, "[installProfileDirect] type=%s requires signed profile", + payload.type.c_str()); + return false; + } + } + + profile.trustLevel = trust; + if (!mStore.save(profile)) { + syslog(LOG_ERR, "[installProfileDirect] save failed uuid=%s", profile.uuid.c_str()); + return false; + } + + applyProfile(profile); + outUuid = profile.uuid; + syslog(LOG_INFO, "[installProfileDirect] installed uuid=%s", profile.uuid.c_str()); + return true; +} + +// ── installProfile ────────────────────────────────────────────────────── + +ScopedAStatus VesperProfileBinderService::installProfile( + const std::vector& in_profileData, std::string* _aidl_return) { + std::lock_guard lock(mLock); + + std::vector yamlBytes; + bool isSigned = false; + SignatureVerifier::TrustLevel trust = SignatureVerifier::TrustLevel::UNSIGNED; + + if (mVerifier.isCmsWrapped(in_profileData)) { + auto result = mVerifier.verify(in_profileData, yamlBytes); + if (result == SignatureVerifier::VerifyResult::INVALID) { + return serviceError("Signature", "Profile signature is invalid or tampered."); + } + isSigned = true; + trust = (result == SignatureVerifier::VerifyResult::TRUSTED) + ? SignatureVerifier::TrustLevel::TRUSTED + : SignatureVerifier::TrustLevel::UNVERIFIED; + } else { + yamlBytes = in_profileData; + } + + ParsedProfile profile; + if (!mParser.parse(yamlBytes, profile)) { + return serviceError("Parse", "Profile YAML is malformed or schema-invalid."); + } + + for (const auto& payload : profile.payloads) { + bool sigRequired = + payload.type == "mdm" || + payload.type == "removal-password" || + payload.type == "kiosk" || + payload.type == "asam"; + if (sigRequired && !isSigned) { + return serviceError("SigningRequired", + "Payload type '" + payload.type + "' requires a signed profile."); + } + } + + profile.trustLevel = trust; + if (!mStore.save(profile)) { + return serviceError("Parse", "Failed to persist profile."); + } + + applyProfile(profile); + syslog(LOG_INFO, "vesperprofiled: installed profile uuid=%s", profile.uuid.c_str()); + + *_aidl_return = profile.uuid; + return ScopedAStatus::ok(); +} + +// ── removeProfile ──────────────────────────────────────────────────────── + +ScopedAStatus VesperProfileBinderService::removeProfile( + const std::string& in_uuid, const std::string& in_removalPassword) { + std::lock_guard lock(mLock); + + ParsedProfile profile; + if (!mStore.load(in_uuid, profile)) { + return serviceError("NotFound", "Profile not found: " + in_uuid); + } + + if (profile.lifecycle.removal == "locked") { + return serviceError("Locked", "This profile can only be removed by the MDM server."); + } + if (profile.lifecycle.removal == "password") { + if (in_removalPassword.empty() || !mStore.checkRemovalPassword(in_uuid, in_removalPassword)) { + return serviceError("WrongPassword", "Incorrect removal password."); + } + } + + revertProfile(profile); + mStore.remove(in_uuid); + syslog(LOG_INFO, "vesperprofiled: removed profile uuid=%s", in_uuid.c_str()); + return ScopedAStatus::ok(); +} + +// ── listProfiles ───────────────────────────────────────────────────────── + +ScopedAStatus VesperProfileBinderService::listProfiles(std::vector* _aidl_return) { + std::lock_guard lock(mLock); + *_aidl_return = mStore.listUuids(); + return ScopedAStatus::ok(); +} + +// ── getProfileInfo ─────────────────────────────────────────────────────── + +ScopedAStatus VesperProfileBinderService::getProfileInfo( + const std::string& in_uuid, std::string* _aidl_return) { + std::lock_guard lock(mLock); + + ParsedProfile profile; + if (!mStore.load(in_uuid, profile)) { + return serviceError("NotFound", "Not found: " + in_uuid); + } + + std::ostringstream j; + j << "{" + << "\"uuid\":\"" << profile.uuid << "\"," + << "\"id\":\"" << profile.id << "\"," + << "\"name\":\"" << profile.meta.name << "\"," + << "\"organization\":\"" << profile.meta.organization << "\"," + << "\"scope\":\"" << profile.scope << "\"," + << "\"removal\":\"" << profile.lifecycle.removal << "\"," + << "\"trusted\":" << (profile.trustLevel != SignatureVerifier::TrustLevel::UNSIGNED ? "true" : "false") << "," + << "\"payloadCount\":" << profile.payloads.size() + << "}"; + + *_aidl_return = j.str(); + return ScopedAStatus::ok(); +} + +// ── isDeviceManaged ────────────────────────────────────────────────────── + +ScopedAStatus VesperProfileBinderService::isDeviceManaged(bool* _aidl_return) { + std::lock_guard lock(mLock); + bool managed = false; + for (const auto& uuid : mStore.listUuids()) { + ParsedProfile p; + if (mStore.load(uuid, p)) + for (const auto& pl : p.payloads) + if (pl.type == "mdm") { managed = true; break; } + if (managed) break; + } + *_aidl_return = managed; + return ScopedAStatus::ok(); +} + +// ── getMdmServerUrl ────────────────────────────────────────────────────── + +ScopedAStatus VesperProfileBinderService::getMdmServerUrl(std::string* _aidl_return) { + std::lock_guard lock(mLock); + std::string url; + for (const auto& uuid : mStore.listUuids()) { + ParsedProfile p; + if (!mStore.load(uuid, p)) continue; + for (const auto& pl : p.payloads) { + if (pl.type == "mdm") { + auto it = pl.fields.find("server-url"); + if (it != pl.fields.end()) url = it->second; + if (url.size() >= 2 && url.front() == '"') url = url.substr(1, url.size() - 2); + break; + } + } + if (!url.empty()) break; + } + *_aidl_return = url; + return ScopedAStatus::ok(); +} + +// ── isSupervised ───────────────────────────────────────────────────────── + +ScopedAStatus VesperProfileBinderService::isSupervised(bool* _aidl_return) { + std::lock_guard lock(mLock); + bool supervised = false; + for (const auto& uuid : mStore.listUuids()) { + ParsedProfile p; + if (!mStore.load(uuid, p)) continue; + for (const auto& pl : p.payloads) + if (pl.type == "kiosk" || pl.type == "asam") { supervised = true; break; } + if (supervised) break; + } + *_aidl_return = supervised; + return ScopedAStatus::ok(); +} + +// ── getPayloadsOfType ──────────────────────────────────────────────────── + +ScopedAStatus VesperProfileBinderService::getPayloadsOfType( + const std::string& in_payloadType, std::string* _aidl_return) { + std::lock_guard lock(mLock); + + std::ostringstream j; + j << "["; + bool first = true; + for (const auto& uuid : mStore.listUuids()) { + ParsedProfile p; + if (!mStore.load(uuid, p)) continue; + for (const auto& pl : p.payloads) { + if (pl.type != in_payloadType) continue; + if (!first) j << ","; + first = false; + j << "{\"profileUuid\":\"" << p.uuid << "\"," + << "\"payloadUuid\":\"" << pl.uuid << "\""; + for (const auto& [k, v] : pl.fields) + j << ",\"" << k << "\":" << v; + j << "}"; + } + } + j << "]"; + + *_aidl_return = j.str(); + return ScopedAStatus::ok(); +} + +// ── applyProfile / revertProfile ───────────────────────────────────────── + +void VesperProfileBinderService::applyProfile(const ParsedProfile& profile) { + for (const auto& payload : profile.payloads) { + auto* handler = PayloadHandlerRegistry::get(payload.type); + if (handler) { + if (!handler->apply(payload)) + syslog(LOG_WARNING, "vesperprofiled: handler failed type=%s uuid=%s", + payload.type.c_str(), payload.uuid.c_str()); + } else { + syslog(LOG_WARNING, "vesperprofiled: no handler for type=%s", payload.type.c_str()); + } + } +} + +void VesperProfileBinderService::revertProfile(const ParsedProfile& profile) { + for (auto it = profile.payloads.rbegin(); it != profile.payloads.rend(); ++it) { + auto* handler = PayloadHandlerRegistry::get(it->type); + if (handler) handler->revert(*it); + } +} + +} // namespace vesperos::profile diff --git a/src/VesperProfileBinderService.h b/src/VesperProfileBinderService.h new file mode 100644 index 0000000..89425ba --- /dev/null +++ b/src/VesperProfileBinderService.h @@ -0,0 +1,57 @@ +#pragma once + +#include "ProfileParser.h" +#include "ProfileStore.h" +#include "SignatureVerifier.h" + +#include + +#include +#include +#include + +namespace vesperos::profile { + +// ── Android Binder counterpart to VesperProfileService (D-Bus) ───────────── +// Same install/remove/query logic, same ProfileStore/ProfileParser/ +// PayloadHandlerRegistry underneath — only the transport differs. See +// main_android.cpp for how this gets registered with servicemanager. +// +// Registered service name: "me.oxmc.vesperos.profile.IVesperProfileService/default" +// (must match vesperprofiled.xml's VINTF fragment and sepolicy/service_contexts). +class VesperProfileBinderService + : public aidl::me::oxmc::vesperos::profile::BnVesperProfileService { +public: + VesperProfileBinderService(); + + // Same direct-install path VesperProfileService exposes for main.cpp's + // preinstalled-profile loader — see applyPreinstalledProfiles() in + // main_android.cpp. + bool installProfileDirect(const std::vector& data, std::string& outUuid); + + ::ndk::ScopedAStatus installProfile( + const std::vector& in_profileData, std::string* _aidl_return) override; + ::ndk::ScopedAStatus removeProfile( + const std::string& in_uuid, const std::string& in_removalPassword) override; + ::ndk::ScopedAStatus listProfiles(std::vector* _aidl_return) override; + ::ndk::ScopedAStatus getProfileInfo( + const std::string& in_uuid, std::string* _aidl_return) override; + ::ndk::ScopedAStatus isDeviceManaged(bool* _aidl_return) override; + ::ndk::ScopedAStatus getMdmServerUrl(std::string* _aidl_return) override; + ::ndk::ScopedAStatus isSupervised(bool* _aidl_return) override; + ::ndk::ScopedAStatus getPayloadsOfType( + const std::string& in_payloadType, std::string* _aidl_return) override; + +private: + void applyProfile(const ParsedProfile& profile); + void revertProfile(const ParsedProfile& profile); + + static ::ndk::ScopedAStatus serviceError(const char* name, const std::string& message); + + std::mutex mLock; + ProfileStore mStore; + ProfileParser mParser; + SignatureVerifier mVerifier; +}; + +} // namespace vesperos::profile diff --git a/src/main_android.cpp b/src/main_android.cpp new file mode 100644 index 0000000..6380257 --- /dev/null +++ b/src/main_android.cpp @@ -0,0 +1,96 @@ +#include "VesperProfileBinderService.h" +#include "payloads/PayloadHandler.h" + +#include +#include + +#include +#include +#include +#include +#include + +// ───────────────────────────────────────────────────────────────────────── +// vesperprofiled — Android entry point +// +// Counterpart to main.cpp (Linux/D-Bus). Registers VesperProfileBinderService +// as "me.oxmc.vesperos.profile.IVesperProfileService/default" — the exact +// name vesperprofiled.xml's VINTF fragment and sepolicy/service_contexts +// already declare — and applies preinstalled profiles the same way +// main.cpp does, from an Android-appropriate path. +// +// Not ported here: the ZTE ConnectivityWatcher (main.cpp's background +// thread) — it watches NetworkManager over D-Bus, which doesn't exist on +// Android. Zero-touch enrollment on PawletOS is separate follow-up work, +// not silently dropped-but-unmentioned. +// ───────────────────────────────────────────────────────────────────────── + +static constexpr const char* kPreinstalledDir = + "/data/system/vesperos/preinstalled"; + +static bool fileExists(const std::string& path) { + struct stat st{}; + return ::stat(path.c_str(), &st) == 0; +} + +static void applyPreinstalledProfiles(vesperos::profile::VesperProfileBinderService& svc) { + DIR* d = opendir(kPreinstalledDir); + if (!d) return; + + syslog(LOG_INFO, "vesperprofiled: scanning %s for preinstalled profiles", kPreinstalledDir); + + struct dirent* ent; + while ((ent = readdir(d)) != nullptr) { + std::string name = ent->d_name; + if (name == "." || name == "..") continue; + if (name.find(".vconfig") == std::string::npos) continue; + + std::string path = std::string(kPreinstalledDir) + "/" + name; + std::ifstream f(path, std::ios::binary); + if (!f) { + syslog(LOG_WARNING, "vesperprofiled: cannot read %s", path.c_str()); + continue; + } + std::ostringstream buf; + buf << f.rdbuf(); + std::string content = buf.str(); + std::vector data(content.begin(), content.end()); + + std::string uuid; + bool ok = svc.installProfileDirect(data, uuid); + if (ok) + syslog(LOG_INFO, "vesperprofiled: preinstalled %s -> uuid=%s", name.c_str(), uuid.c_str()); + else + syslog(LOG_WARNING, "vesperprofiled: failed to apply %s", name.c_str()); + } + closedir(d); +} + +int main() { + openlog("vesperprofiled", LOG_PID | LOG_CONS, LOG_DAEMON); + syslog(LOG_INFO, "vesperprofiled starting (Android/Binder)"); + + vesperos::profile::PayloadHandlerRegistry::registerAll(); + + auto service = ndk::SharedRefBase::make(); + + if (fileExists(kPreinstalledDir)) { + applyPreinstalledProfiles(*service); + } + + const char* instanceName = "me.oxmc.vesperos.profile.IVesperProfileService/default"; + binder_status_t status = AServiceManager_addService(service->asBinder().get(), instanceName); + if (status != STATUS_OK) { + syslog(LOG_ERR, "vesperprofiled: AServiceManager_addService failed: %d", status); + return 1; + } + syslog(LOG_INFO, "vesperprofiled: registered as %s", instanceName); + + ABinderProcess_setThreadPoolMaxThreadCount(4); + ABinderProcess_startThreadPool(); + ABinderProcess_joinThreadPool(); // blocks forever + + syslog(LOG_INFO, "vesperprofiled stopped"); + closelog(); + return 0; +} diff --git a/src/payloads/PayloadHandler.h b/src/payloads/PayloadHandler.h index 89155de..8c2a539 100644 --- a/src/payloads/PayloadHandler.h +++ b/src/payloads/PayloadHandler.h @@ -1,6 +1,7 @@ #pragma once #include "../ProfileParser.h" +#include "PayloadUtil.h" #include #include @@ -26,22 +27,17 @@ public: virtual void revert(const ParsedPayload& payload) = 0; protected: - // Helper: get a string field, stripping JSON quotes if present. + // Forward to PayloadUtil.h's free functions — kept here so any code + // still calling these via a PayloadHandler subclass keeps working. + // platform/*/*.cpp files (not subclasses) call util::field directly. static std::string field(const ParsedPayload& p, const std::string& key, const std::string& def = "") { - auto it = p.fields.find(key); - if (it == p.fields.end()) return def; - std::string v = it->second; - if (v.size() >= 2 && v.front() == '"' && v.back() == '"') - v = v.substr(1, v.size() - 2); - return v; + return util::field(p, key, def); } static bool fieldBool(const ParsedPayload& p, const std::string& key, bool def = false) { - std::string v = field(p, key); - if (v.empty()) return def; - return (v == "true" || v == "1"); + return util::fieldBool(p, key, def); } }; diff --git a/src/payloads/PayloadHandlers.cpp b/src/payloads/PayloadHandlers.cpp index 1d79255..d1efede 100644 --- a/src/payloads/PayloadHandlers.cpp +++ b/src/payloads/PayloadHandlers.cpp @@ -1,1358 +1,82 @@ #include "PayloadHandler.h" // ───────────────────────────────────────────────────────────────────────── -// Linux payload handler implementations +// Shared entry point — platform-agnostic. // -// System integration points: -// WiFi / Ethernet / VPN → NetworkManager via D-Bus -// (org.freedesktop.NetworkManager) -// Certificates → /etc/ssl/certs/ + update-ca-certificates -// user certs → NSSDB via certutil -// Passcode policy → PAM (pam_pwquality / pam_unix) -// /etc/security/pwquality.conf -// MDM / cloud config → cloud-init user-data drop-in -// /etc/cloud/cloud.cfg.d/ -// Software update → /etc/apt/apt.conf.d/ -// Time server → /etc/systemd/timesyncd.conf.d/ -// Firewall → nftables via nft CLI -// Screensaver → /etc/X11/xorg.conf.d/ or gsettings (GNOME) -// Wallpaper → gsettings (GNOME) / gconf -// Hostname / domain → /etc/hostname, /etc/hosts, systemd-hostnamed -// DNS proxy → /etc/systemd/resolved.conf.d/ -// LDAP → /etc/ldap/ldap.conf + libpam-ldap -// Kiosk (restricted UI) → write .desktop autostart + restrict shell +// Every handler class below is a thin wrapper: apply()/revert() just call +// through to platform::::apply()/revert(), declared in +// src/platform/.h. The *real* logic lives in exactly one of: +// +// src/platform/linux/.cpp (CMakeLists.txt builds these) +// src/platform/android/.cpp (vesperprofiled/Android.bp builds these) +// +// Only one half gets compiled per target, so there's no #ifdef in this +// file and no linker conflict — the build system picks the platform, not +// the source. This file is identical on both, and stays that way; if +// you're adding platform-specific behavior, it belongs in platform/linux/ +// or platform/android/, not here. +// +// Every handler's platform/android/*.cpp today is an honest "not +// implemented yet" stub (logs + returns false, doesn't silently no-op). +// See each stub's header comment for what a real port would need. +// +// content-cache is PawletOS-fork-specific — talks to PawletCache/ +// pawletcache-server, which don't exist in a bare VesperOS install. It's +// registered here because this checkout *is* the PawletOS fork of +// vesperprofiled; the upstream/default VesperOS build omits it. // ───────────────────────────────────────────────────────────────────────── -#include +#include "../platform/Wifi.h" +#include "../platform/Ethernet.h" +#include "../platform/Vpn.h" +#include "../platform/Cert.h" +#include "../platform/Pkcs12.h" +#include "../platform/Passcode.h" +#include "../platform/Mdm.h" +#include "../platform/SoftwareUpdate.h" +#include "../platform/TimeServer.h" +#include "../platform/Proxy.h" +#include "../platform/DnsProxy.h" +#include "../platform/Firewall.h" +#include "../platform/Ldap.h" +#include "../platform/Wallpaper.h" +#include "../platform/Screensaver.h" +#include "../platform/FirstBoot.h" +#include "../platform/ContentCache.h" -#include #include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include namespace vesperos::profile { -// ── Utility: run a shell command ────────────────────────────────────────── - -static bool runCmd(const std::string& cmd) { - syslog(LOG_DEBUG, "vesperprofiled: exec: %s", cmd.c_str()); - int ret = ::system(cmd.c_str()); - if (ret != 0) - syslog(LOG_WARNING, "vesperprofiled: command exited %d: %s", ret, cmd.c_str()); - return ret == 0; -} - -// ── Utility: write a file atomically ───────────────────────────────────── - -static bool writeFile(const std::string& path, const std::string& content, - mode_t mode = 0644) { - std::string tmp = path + ".tmp"; - { - std::ofstream f(tmp, std::ios::binary | std::ios::trunc); - if (!f) { syslog(LOG_ERR, "vesperprofiled: cannot write %s", tmp.c_str()); return false; } - f << content; - } - chmod(tmp.c_str(), mode); - if (::rename(tmp.c_str(), path.c_str()) != 0) { - syslog(LOG_ERR, "vesperprofiled: rename failed: %s -> %s", tmp.c_str(), path.c_str()); - ::unlink(tmp.c_str()); - return false; - } - return true; -} - -static void removeFile(const std::string& path) { - ::unlink(path.c_str()); -} - -static void ensureDir(const std::string& path, mode_t mode = 0755) { - ::mkdir(path.c_str(), mode); -} - -// ── Utility: base64 decode (OpenSSL EVP) ───────────────────────────────── - -static std::vector b64decode(const std::string& in) { - std::string s = in; - if (s.size() >= 2 && s.front() == '"') s = s.substr(1, s.size()-2); - std::vector out(s.size()); - int len = EVP_DecodeBlock(out.data(), - reinterpret_cast(s.data()), (int)s.size()); - if (len < 0) return {}; - out.resize(len); - return out; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// WIFI HANDLER -// Uses nmcli to add/modify/delete NetworkManager WiFi connections. -// ═══════════════════════════════════════════════════════════════════════════ - -// ── NM keyfile helpers (shared by WifiHandler and EthernetHandler) ──────── - -// Extract a string value from a flat JSON object stored in fields. -// e.g. extractJson(raw, "password") on {"type":"wpa2","password":"foo"} -// returns "foo". Only handles string values; returns "" on miss. -static std::string extractJson(const std::string& json, const std::string& key) { - auto pos = json.find("\"" + key + "\":"); - if (pos == std::string::npos) return ""; - pos += key.size() + 3; // skip "key": - // skip optional whitespace - while (pos < json.size() && json[pos] == ' ') ++pos; - if (pos >= json.size()) return ""; - if (json[pos] == '"') { - // quoted string value - auto q2 = json.find('"', pos + 1); - if (q2 == std::string::npos) return ""; - return json.substr(pos + 1, q2 - pos - 1); - } - // unquoted (bool / number) — read until , or } - auto end = json.find_first_of(",}", pos); - return json.substr(pos, end - pos); -} - -static const std::string kNMConnDir = "/etc/NetworkManager/system-connections"; - -// Reload NM connections without spawning nmcli — send SIGHUP to NetworkManager -// via D-Bus so NM re-reads its keyfiles without needing the daemon binary. -static void nmReload() { - // org.freedesktop.NetworkManager.ReloadConnections D-Bus call. - // If NM is not running yet (early first-boot), this is a no-op; - // NM will pick up the keyfiles on its own startup. - runCmd("dbus-send --system --print-reply " - "--dest=org.freedesktop.NetworkManager " - "/org/freedesktop/NetworkManager " - "org.freedesktop.NetworkManager.ReloadConnections " - "2>/dev/null || true"); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// WIFI HANDLER -// Writes a NetworkManager keyfile to -// /etc/NetworkManager/system-connections/vesperos-wifi-.nmconnection -// -// Why keyfiles instead of nmcli exec: -// - No subprocess / shell injection risk from SSIDs or passwords that -// contain quotes, spaces, or special characters. -// - Atomic write via rename(); NM only sees the complete file. -// - The full NM keyfile schema is available: EAP-FAST, WPA3-Enterprise, -// per-BSSID settings, MAC randomisation, QoS — all expressible. -// nmcli can't set many of these flags. -// - Works during first-boot before NM is running: NM picks up the file -// on its first start after the profile is applied. -// - Consistent with EthernetHandler — one code path, one format. -// ═══════════════════════════════════════════════════════════════════════════ - -class WifiHandler : public PayloadHandler { - std::string keyfilePath(const ParsedPayload& p) { - return kNMConnDir + "/vesperos-wifi-" + p.uuid + ".nmconnection"; +#define VESPER_DISPATCH_HANDLER(ClassName, ns) \ + class ClassName : public PayloadHandler { \ + public: \ + bool apply(const ParsedPayload& p) override { return platform::ns::apply(p); } \ + void revert(const ParsedPayload& p) override { platform::ns::revert(p); } \ } -public: - bool apply(const ParsedPayload& p) override { - std::string ssid = field(p, "ssid"); - bool hidden = fieldBool(p, "hidden", false); - bool autoJoin = fieldBool(p, "auto-join", true); - std::string macMode = field(p, "mac-address-mode", "hardware"); - - if (ssid.empty()) { syslog(LOG_ERR, "[WifiHandler] missing ssid"); return false; } - - // Parse security block - std::string secJson = field(p, "security"); // stored as JSON object - std::string secType = extractJson(secJson, "type"); // wpa2|wpa3|wep|none|any - std::string psk = extractJson(secJson, "password"); - - // Map profile security type → NM key-mgmt value - std::string keyMgmt; - if (secType == "wpa3") keyMgmt = "sae"; - else if (secType == "wpa2" || secType == "wpa") keyMgmt = "wpa-psk"; - else if (secType == "wep") keyMgmt = "none"; // WEP uses no key-mgmt - else if (secType == "none" || secType == "any" || secType.empty()) keyMgmt = ""; - - // Parse EAP block (enterprise WiFi) - std::string eapJson = field(p, "eap"); - bool isEnterprise = !eapJson.empty() && eapJson != "\"\""; - std::string eapMethod; - std::string eapUser, eapPass, eapOuter, eapInnerAuth, ttlsInner; - std::string anchorUuid, serverName; - if (isEnterprise) { - // Determine method from the methods array - if (eapJson.find("tls") != std::string::npos) eapMethod = "tls"; - else if (eapJson.find("ttls") != std::string::npos) eapMethod = "ttls"; - else if (eapJson.find("fast") != std::string::npos) eapMethod = "fast"; - else eapMethod = "peap"; - eapUser = extractJson(eapJson, "username"); - eapPass = extractJson(eapJson, "password"); - eapOuter = extractJson(eapJson, "outer-identity"); - ttlsInner = extractJson(eapJson, "ttls-inner-auth"); - // trust block - auto trustPos = eapJson.find("\"trust\":"); - if (trustPos != std::string::npos) { - serverName = extractJson(eapJson.substr(trustPos), "server-names"); - anchorUuid = extractJson(eapJson.substr(trustPos), "anchor-cert-uuids"); - } - } - - // Parse proxy block - std::string proxyJson = field(p, "proxy"); - std::string proxyType = extractJson(proxyJson, "type"); // manual|auto|none - std::string proxyHost = extractJson(proxyJson, "host"); - std::string proxyPort = extractJson(proxyJson, "port"); - std::string proxyUser = extractJson(proxyJson, "username"); - std::string proxyPass = extractJson(proxyJson, "password"); - std::string pacUrl = extractJson(proxyJson, "pac-url"); - - // ── Build the keyfile ────────────────────────────────────────────── - std::ostringstream kf; - - // [connection] - kf << "[connection]\n" - << "id=vesperos-wifi-" << ssid << "\n" - << "uuid=" << p.uuid << "\n" - << "type=wifi\n" - << "autoconnect=" << (autoJoin ? "true" : "false") << "\n\n"; - - // [wifi] - kf << "[wifi]\n" - << "ssid=" << ssid << "\n" - << "mode=infrastructure\n" - << "hidden=" << (hidden ? "true" : "false") << "\n"; - if (macMode == "random") - kf << "cloned-mac-address=random\n" - << "mac-address-randomization=2\n"; - kf << "\n"; - - // [wifi-security] - if (!keyMgmt.empty() || secType == "wep") { - kf << "[wifi-security]\n"; - if (!keyMgmt.empty()) - kf << "key-mgmt=" << keyMgmt << "\n"; - if (!psk.empty() && secType != "wep") - kf << "psk=" << psk << "\n"; - if (secType == "wep") - kf << "auth-alg=open\n" - << "wep-key0=" << psk << "\n" - << "wep-key-type=1\n"; - kf << "\n"; - } - - // [802-1x] (enterprise) - if (isEnterprise) { - kf << "[802-1x]\n" - << "eap=" << eapMethod << "\n"; - if (!eapUser.empty()) kf << "identity=" << eapUser << "\n"; - if (!eapPass.empty()) kf << "password=" << eapPass << "\n"; - if (!eapOuter.empty()) kf << "anonymous-identity=" << eapOuter << "\n"; - if (eapMethod == "ttls" || eapMethod == "peap") { - std::string phase2 = ttlsInner.empty() ? "mschapv2" : ttlsInner; - kf << "phase2-auth=" << phase2 << "\n"; - } - if (!serverName.empty()) - kf << "altsubject-matches=" << serverName << "\n"; - if (!anchorUuid.empty()) - // NM references a cert stored in the system keyring by uuid-hash; - // for simplicity write the path if the CertHandler already wrote it. - kf << "# ca-cert=/usr/local/share/ca-certificates/vesperos/" - << anchorUuid << ".crt\n"; - kf << "\n"; - } - - // [proxy] - if (proxyType == "manual" && !proxyHost.empty()) { - kf << "[proxy]\n" - << "method=manual\n" - << "http-proxy=" << proxyHost << ":" << proxyPort << "\n" - << "https-proxy=" << proxyHost << ":" << proxyPort << "\n"; - if (!proxyUser.empty()) - kf << "# proxy-auth=" << proxyUser << ":" << proxyPass << "\n"; - kf << "\n"; - } else if (proxyType == "auto" && !pacUrl.empty()) { - kf << "[proxy]\n" - << "method=auto\n" - << "pac-url=" << pacUrl << "\n\n"; - } - - // [ipv4] / [ipv6] - kf << "[ipv4]\nmethod=auto\n\n" - << "[ipv6]\nmethod=auto\naddr-gen-mode=stable-privacy\n"; - - ensureDir(kNMConnDir); - writeFile(keyfilePath(p), kf.str(), 0600); - nmReload(); - - syslog(LOG_INFO, "[WifiHandler] wrote keyfile ssid=%s uuid=%s", - ssid.c_str(), p.uuid.c_str()); - return true; - } - - void revert(const ParsedPayload& p) override { - removeFile(keyfilePath(p)); - nmReload(); - syslog(LOG_INFO, "[WifiHandler] removed keyfile uuid=%s", p.uuid.c_str()); - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// ETHERNET HANDLER -// 802.1X wired authentication via NetworkManager keyfile. -// Same approach as WifiHandler — direct keyfile write, no nmcli exec. -// ═══════════════════════════════════════════════════════════════════════════ - -class EthernetHandler : public PayloadHandler { - std::string keyfilePath(const ParsedPayload& p) { - return kNMConnDir + "/vesperos-ethernet-" + p.uuid + ".nmconnection"; - } - -public: - bool apply(const ParsedPayload& p) override { - std::string iface = field(p, "interface", ""); - // Translate VesperOS interface names to Linux names. - // Leave blank to let NM pick the first available wired interface. - if (iface == "first-active" || iface == "first" || iface == "first-ethernet") - iface = ""; - - // Parse EAP block - std::string eapJson = field(p, "eap"); - std::string eapMethod = "peap"; - if (!eapJson.empty()) { - if (eapJson.find("\"tls\"") != std::string::npos) eapMethod = "tls"; - else if (eapJson.find("\"ttls\"") != std::string::npos) eapMethod = "ttls"; - else if (eapJson.find("\"fast\"") != std::string::npos) eapMethod = "fast"; - } - - std::string eapUser = extractJson(eapJson, "username"); - std::string eapPass = extractJson(eapJson, "password"); - std::string eapOuter = extractJson(eapJson, "outer-identity"); - std::string ttlsInner = extractJson(eapJson, "ttls-inner-auth"); - - // trust sub-block - std::string serverName, caCertPath; - auto trustPos = eapJson.find("\"trust\":"); - if (trustPos != std::string::npos) { - serverName = extractJson(eapJson.substr(trustPos), "server-names"); - std::string anchorUuid = extractJson(eapJson.substr(trustPos), "anchor-cert-uuids"); - if (!anchorUuid.empty()) - caCertPath = "/usr/local/share/ca-certificates/vesperos/" - + anchorUuid + ".crt"; - } - - // ── Build the keyfile ────────────────────────────────────────────── - std::ostringstream kf; - - kf << "[connection]\n" - << "id=vesperos-ethernet-" << p.uuid << "\n" - << "uuid=" << p.uuid << "\n" - << "type=ethernet\n" - << "autoconnect=true\n"; - if (!iface.empty()) - kf << "interface-name=" << iface << "\n"; - kf << "\n"; - - kf << "[ethernet]\n\n"; - - // [802-1x] - bool hasEap = !eapJson.empty() && eapJson != "\"\""; - if (hasEap) { - kf << "[802-1x]\n" - << "eap=" << eapMethod << "\n"; - if (!eapUser.empty()) kf << "identity=" << eapUser << "\n"; - if (!eapPass.empty()) kf << "password=" << eapPass << "\n"; - if (!eapOuter.empty()) kf << "anonymous-identity=" << eapOuter << "\n"; - if (eapMethod == "ttls" || eapMethod == "peap") - kf << "phase2-auth=" << (ttlsInner.empty() ? "mschapv2" : ttlsInner) << "\n"; - if (!serverName.empty()) - kf << "altsubject-matches=" << serverName << "\n"; - if (!caCertPath.empty()) - kf << "ca-cert=" << caCertPath << "\n"; - kf << "\n"; - } - - kf << "[ipv4]\nmethod=auto\n\n" - << "[ipv6]\nmethod=auto\naddr-gen-mode=stable-privacy\n"; - - ensureDir(kNMConnDir); - writeFile(keyfilePath(p), kf.str(), 0600); - nmReload(); - - syslog(LOG_INFO, "[EthernetHandler] wrote keyfile iface=%s uuid=%s", - iface.empty() ? "*" : iface.c_str(), p.uuid.c_str()); - return true; - } - - void revert(const ParsedPayload& p) override { - removeFile(keyfilePath(p)); - nmReload(); - syslog(LOG_INFO, "[EthernetHandler] removed keyfile uuid=%s", p.uuid.c_str()); - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// VPN HANDLER -// IKEv2 → NetworkManager strongswan plugin (network-manager-strongswan) -// L2TP → NetworkManager l2tp plugin (network-manager-l2tp) -// Custom → write OpenVPN .ovpn or WireGuard .conf -// ═══════════════════════════════════════════════════════════════════════════ - -class VpnHandler : public PayloadHandler { -public: - bool apply(const ParsedPayload& p) override { - std::string displayName = field(p, "display-name", "VesperOS VPN"); - std::string connId = "vesperos-vpn-" + p.uuid; - - // Check which VPN type is configured - bool hasIkev2 = p.fields.count("ikev2"); - bool hasL2tp = p.fields.count("l2tp"); - bool hasCustom = p.fields.count("custom"); - - if (hasIkev2) return applyIkev2(p, connId, displayName); - if (hasL2tp) return applyL2tp(p, connId, displayName); - if (hasCustom) return applyCustom(p, connId); - - syslog(LOG_WARNING, "[VpnHandler] no VPN type block found in payload"); - return false; - } - - void revert(const ParsedPayload& p) override { - std::string connId = "vesperos-vpn-" + p.uuid; - runCmd("nmcli connection delete id '" + connId + "' 2>/dev/null"); - // Also remove any keyfile we wrote - removeFile("/etc/NetworkManager/system-connections/" + connId + ".nmconnection"); - removeFile("/etc/wireguard/vesperos-" + p.uuid + ".conf"); - } - -private: - bool applyIkev2(const ParsedPayload& p, const std::string& connId, - const std::string& /*name*/) { - // Requires: network-manager-strongswan - // Write an NM keyfile for IKEv2/IPsec - auto ikev2Json = p.fields.find("ikev2"); - if (ikev2Json == p.fields.end()) return false; - - // Quick field extraction from stored JSON string - auto extractField = [&](const std::string& key) -> std::string { - auto pos = ikev2Json->second.find("\"" + key + "\":"); - if (pos == std::string::npos) return ""; - pos += key.size() + 3; - auto q1 = ikev2Json->second.find('"', pos); - auto q2 = ikev2Json->second.find('"', q1 + 1); - if (q1 == std::string::npos) return ""; - return ikev2Json->second.substr(q1 + 1, q2 - q1 - 1); - }; - - std::string server = extractField("server"); - std::string remoteId = extractField("remote-id"); - - std::ostringstream kf; - kf << "[connection]\n" - << "id=" << connId << "\n" - << "type=vpn\n" - << "autoconnect=false\n\n" - << "[vpn]\n" - << "service-type=org.freedesktop.NetworkManager.strongswan\n" - << "address=" << server << "\n" - << "remote-identity=" << remoteId << "\n" - << "method=key\n" - << "virtual=yes\n" - << "ipcomp=no\n" - << "encap=no\n" - << "proposal=yes\n\n" - << "[ipv4]\nmethod=auto\n"; - - ensureDir("/etc/NetworkManager/system-connections"); - writeFile("/etc/NetworkManager/system-connections/" + connId + ".nmconnection", - kf.str(), 0600); - return runCmd("nmcli connection reload"); - } - - bool applyL2tp(const ParsedPayload& p, const std::string& connId, - const std::string& /*name*/) { - // Requires: network-manager-l2tp, network-manager-l2tp-gnome - auto l2tpJson = p.fields.find("l2tp"); - if (l2tpJson == p.fields.end()) return false; - - auto extract = [&](const std::string& key) -> std::string { - auto pos = l2tpJson->second.find("\"" + key + "\":"); - if (pos == std::string::npos) return ""; - pos += key.size() + 3; - auto q1 = l2tpJson->second.find('"', pos); - auto q2 = l2tpJson->second.find('"', q1 + 1); - if (q1 == std::string::npos) return ""; - return l2tpJson->second.substr(q1 + 1, q2 - q1 - 1); - }; - - std::ostringstream cmd; - cmd << "nmcli connection add type vpn" - << " con-name '" << connId << "'" - << " vpn-type l2tp" - << " vpn.data 'gateway=" << extract("server") - << ",user=" << extract("username") - << ",password-flags=0" - << ",ipsec-enabled=true" - << ",ipsec-psk=" << extract("shared-secret") << "'"; - return runCmd(cmd.str()); - } - - bool applyCustom(const ParsedPayload& p, const std::string& /*connId*/) { - // Write raw WireGuard config if provider hints at it, - // otherwise log for manual configuration. - syslog(LOG_INFO, "[VpnHandler] custom VPN uuid=%s — write your own config", - p.uuid.c_str()); - return true; - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// CERTIFICATE HANDLER -// Root/intermediate CAs → /usr/local/share/ca-certificates/vesperos/ -// + update-ca-certificates -// User certs (PKCS#12) → user NSS DB or /etc/ssl/private/ -// ═══════════════════════════════════════════════════════════════════════════ - -class CertHandler : public PayloadHandler { - static constexpr const char* kCaDir = - "/usr/local/share/ca-certificates/vesperos"; -public: - bool apply(const ParsedPayload& p) override { - std::string b64 = field(p, "data"); - if (b64.empty()) { syslog(LOG_ERR, "[CertHandler] no data"); return false; } - - auto bytes = b64decode(b64); - if (bytes.empty()) { syslog(LOG_ERR, "[CertHandler] bad base64"); return false; } - - ensureDir(kCaDir); - std::string certPath = std::string(kCaDir) + "/" + p.uuid + ".crt"; - - // Write the PEM/DER data - { - std::ofstream f(certPath, std::ios::binary); - f.write(reinterpret_cast(bytes.data()), bytes.size()); - } - ::chmod(certPath.c_str(), 0644); - - // Rebuild the system trust store - bool ok = runCmd("update-ca-certificates --fresh 2>/dev/null"); - syslog(LOG_INFO, "[CertHandler] installed CA cert uuid=%s", p.uuid.c_str()); - return ok; - } - - void revert(const ParsedPayload& p) override { - removeFile(std::string(kCaDir) + "/" + p.uuid + ".crt"); - runCmd("update-ca-certificates --fresh 2>/dev/null"); - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// PKCS#12 HANDLER -// Identity cert + key → /etc/ssl/private/vesperos/ -// Also imports into the system NSS DB (shared NSSDB at /etc/pki/nssdb) -// via certutil/pk12util if available. -// ═══════════════════════════════════════════════════════════════════════════ - -class Pkcs12Handler : public PayloadHandler { - static constexpr const char* kKeyDir = "/etc/ssl/private/vesperos"; -public: - bool apply(const ParsedPayload& p) override { - std::string b64 = field(p, "data"); - std::string pw = field(p, "password"); - if (b64.empty()) { syslog(LOG_ERR, "[Pkcs12Handler] no data"); return false; } - - auto bytes = b64decode(b64); - if (bytes.empty()) return false; - - ensureDir(kKeyDir, 0700); - std::string p12Path = std::string(kKeyDir) + "/" + p.uuid + ".p12"; - { - std::ofstream f(p12Path, std::ios::binary); - f.write(reinterpret_cast(bytes.data()), bytes.size()); - } - ::chmod(p12Path.c_str(), 0600); - - // Import into shared NSSDB if pk12util is available - runCmd("which pk12util >/dev/null 2>&1 && " - "pk12util -i " + p12Path + " -d /etc/pki/nssdb" - " -W '" + pw + "' 2>/dev/null"); - - syslog(LOG_INFO, "[Pkcs12Handler] installed identity cert uuid=%s", p.uuid.c_str()); - return true; - } - - void revert(const ParsedPayload& p) override { - removeFile(std::string(kKeyDir) + "/" + p.uuid + ".p12"); - // Remove from NSSDB — we'd need to track the cert nickname; skip for now. - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// PASSCODE POLICY HANDLER -// Writes /etc/security/pwquality.conf.d/vesperos.conf -// and sets /etc/login.defs values for max age, min age, etc. -// Also writes a PAM faillock config for lockout policy. -// ═══════════════════════════════════════════════════════════════════════════ - -class PasscodeHandler : public PayloadHandler { - static constexpr const char* kPwqualityDir = - "/etc/security/pwquality.conf.d"; - static constexpr const char* kFaillockConf = - "/etc/security/faillock.conf.d/vesperos.conf"; -public: - bool apply(const ParsedPayload& p) override { - int minLen = std::stoi(field(p, "min-length", "6")); - int minComplex = std::stoi(field(p, "min-complex-chars", "0")); - bool reqAlpha = fieldBool(p, "require-alphanumeric", false); - bool allowSimp = fieldBool(p, "allow-simple", true); - int maxAge = std::stoi(field(p, "max-age-days", "0")); - int history = std::stoi(field(p, "history", "0")); - int maxFailed = std::stoi(field(p, "max-failed-attempts", "0")); - int inactivity = std::stoi(field(p, "inactivity-minutes", "0")); - - // ── pam_pwquality ────────────────────────────────────────────── - ensureDir(kPwqualityDir); - std::ostringstream pq; - pq << "# Managed by vesperprofiled — do not edit manually\n"; - pq << "minlen = " << minLen << "\n"; - if (minComplex > 0) pq << "minclass = " << minComplex << "\n"; - if (reqAlpha) pq << "dcredit = -1\n" << "ucredit = -1\n"; - if (!allowSimp) pq << "maxrepeat = 2\nmaxsequence = 2\n"; - if (history > 0) pq << "# remember=" << history - << " set in /etc/pam.d/common-password\n"; - writeFile(std::string(kPwqualityDir) + "/vesperos.conf", pq.str()); - - // ── /etc/login.defs overrides via conf.d drop-in ─────────────── - // Debian reads /etc/login.defs directly; we append a comment-marked - // block. A real implementation patches login.defs via sed or a - // dedicated management file; here we write a separate snippet - // that a PAM module or login wrapper can source. - std::ostringstream ld; - ld << "# vesperprofiled managed\n"; - if (maxAge > 0) ld << "PASS_MAX_DAYS\t" << maxAge << "\n"; - if (history > 0) ld << "# PASS_REUSE_LIMIT=" << history << "\n"; - writeFile("/etc/vesperprofiled/login.defs.snippet", ld.str()); - - // ── pam_faillock ─────────────────────────────────────────────── - if (maxFailed > 0) { - ensureDir("/etc/security/faillock.conf.d"); - std::ostringstream fl; - fl << "# Managed by vesperprofiled\n" - << "deny = " << maxFailed << "\n"; - if (inactivity > 0) - fl << "unlock_time = " << (inactivity * 60) << "\n"; - writeFile(kFaillockConf, fl.str()); - } - - syslog(LOG_INFO, "[PasscodeHandler] applied password policy"); - return true; - } - - void revert(const ParsedPayload& /*p*/) override { - removeFile(std::string(kPwqualityDir) + "/vesperos.conf"); - removeFile(kFaillockConf); - removeFile("/etc/vesperprofiled/login.defs.snippet"); - syslog(LOG_INFO, "[PasscodeHandler] reverted password policy"); - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// MDM HANDLER -// Writes a cloud-init user-data drop-in so the MDM server URL and -// enrollment state survive reboots and cloud-init re-runs. -// Also writes a vesperos MDM config to /etc/vesperprofiled/mdm.conf -// which the MDM client agent (future VesperOS package) reads. -// ═══════════════════════════════════════════════════════════════════════════ - -class MdmHandler : public PayloadHandler { - static constexpr const char* kMdmConf = - "/etc/vesperprofiled/mdm.conf"; - static constexpr const char* kCloudInitDrop = - "/etc/cloud/cloud.cfg.d/99-vesperos-mdm.cfg"; -public: - bool apply(const ParsedPayload& p) override { - std::string serverUrl = field(p, "server-url"); - std::string checkinUrl = field(p, "checkin-url"); - std::string pushTopic = field(p, "push-topic"); - std::string certUuid = field(p, "identity-cert-uuid"); - std::string accessStr = field(p, "access-rights", "0"); - int accessRights = std::stoi(accessStr); - - // ── /etc/vesperprofiled/mdm.conf ─────────────────────────────── - ensureDir("/etc/vesperprofiled"); - std::ostringstream conf; - conf << "# VesperOS MDM enrollment — managed by vesperprofiled\n" - << "server_url=" << serverUrl << "\n" - << "checkin_url=" << (checkinUrl.empty() ? serverUrl : checkinUrl) << "\n" - << "push_topic=" << pushTopic << "\n" - << "identity_cert_uuid=" << certUuid << "\n" - << "access_rights=" << accessRights << "\n" - << "enrolled=1\n"; - writeFile(kMdmConf, conf.str(), 0640); - - // ── cloud-init drop-in ───────────────────────────────────────── - // cloud-init YAML format — sets system metadata the MDM agent - // can query via `cloud-init query`. - ensureDir("/etc/cloud/cloud.cfg.d"); - std::ostringstream ci; - ci << "# VesperOS MDM — generated by vesperprofiled\n" - << "# Do not edit manually.\n" - << "system_info:\n" - << " default_user:\n" - << " lock_passwd: false\n" - << "\n" - << "# MDM server endpoints\n" - << "vesperos_mdm:\n" - << " server_url: " << serverUrl << "\n" - << " checkin_url: " << (checkinUrl.empty() ? serverUrl : checkinUrl) << "\n" - << " push_topic: " << pushTopic << "\n" - << " enrolled: true\n" - << "\n" - << "# Disable cloud-init from resetting MDM-managed settings\n" - << "manage_etc_hosts: false\n" - << "manage_resolv_conf: false\n"; - writeFile(kCloudInitDrop, ci.str(), 0644); - - syslog(LOG_INFO, "[MdmHandler] enrolled MDM server=%s", serverUrl.c_str()); - return true; - } - - void revert(const ParsedPayload& p) override { - removeFile(kMdmConf); - removeFile(kCloudInitDrop); - syslog(LOG_INFO, "[MdmHandler] unenrolled MDM uuid=%s", p.uuid.c_str()); - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// SOFTWARE UPDATE HANDLER -// Writes /etc/apt/apt.conf.d/99-vesperos for update settings. -// Sets up unattended-upgrades for automatic updates. -// ═══════════════════════════════════════════════════════════════════════════ - -class SoftwareUpdateHandler : public PayloadHandler { - static constexpr const char* kAptConf = - "/etc/apt/apt.conf.d/99-vesperos-update"; - static constexpr const char* kUnattendedConf = - "/etc/apt/apt.conf.d/51-vesperos-unattended"; -public: - bool apply(const ParsedPayload& p) override { - // Extract nested automatic block - bool autoCheck = true; - bool autoDownload = true; - bool autoInstall = false; - int osDeferDays = 0; - - auto autoJson = p.fields.find("automatic"); - if (autoJson != p.fields.end()) { - autoCheck = autoJson->second.find("\"check\":true") != std::string::npos; - autoDownload = autoJson->second.find("\"download\":true") != std::string::npos; - autoInstall = autoJson->second.find("\"install-os-updates\":true") != std::string::npos; - } - - auto deferJson = p.fields.find("deferral"); - if (deferJson != p.fields.end()) { - auto pos = deferJson->second.find("\"os-updates-days\":"); - if (pos != std::string::npos) { - osDeferDays = std::stoi(deferJson->second.substr(pos + 18, 3)); - } - } - - // ── apt.conf.d drop-in ───────────────────────────────────────── - std::ostringstream apt; - apt << "// VesperOS managed — do not edit\n" - << "APT::Periodic::Update-Package-Lists \"" - << (autoCheck ? "1" : "0") << "\";\n" - << "APT::Periodic::Download-Upgradeable-Packages \"" - << (autoDownload ? "1" : "0") << "\";\n" - << "APT::Periodic::Unattended-Upgrade \"" - << (autoInstall ? "1" : "0") << "\";\n"; - writeFile(kAptConf, apt.str()); - - // ── unattended-upgrades deferral ─────────────────────────────── - if (osDeferDays > 0) { - std::ostringstream ua; - ua << "// VesperOS managed\n" - << "Unattended-Upgrade::MinimalSteps \"true\";\n" - << "Unattended-Upgrade::InstallOnShutdown \"false\";\n"; - // Delay upgrades by pinning to a date offset - // (unattended-upgrades doesn't have a native delay knob; - // use a cron-based hold instead) - writeFile(kUnattendedConf, ua.str()); - } - - syslog(LOG_INFO, "[SoftwareUpdateHandler] applied update policy"); - return true; - } - - void revert(const ParsedPayload& /*p*/) override { - removeFile(kAptConf); - removeFile(kUnattendedConf); - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// TIME SERVER HANDLER -// Writes /etc/systemd/timesyncd.conf.d/vesperos.conf -// ═══════════════════════════════════════════════════════════════════════════ - -class TimeServerHandler : public PayloadHandler { - static constexpr const char* kTimesyncDir = - "/etc/systemd/timesyncd.conf.d"; - static constexpr const char* kTimesyncConf = - "/etc/systemd/timesyncd.conf.d/vesperos.conf"; -public: - bool apply(const ParsedPayload& p) override { - std::string server = field(p, "server"); - if (server.empty()) return true; - - ensureDir(kTimesyncDir); - std::ostringstream c; - c << "[Time]\n" - << "NTP=" << server << "\n" - << "FallbackNTP=0.debian.pool.ntp.org 1.debian.pool.ntp.org\n"; - writeFile(kTimesyncConf, c.str()); - runCmd("systemctl restart systemd-timesyncd 2>/dev/null"); - - syslog(LOG_INFO, "[TimeServerHandler] NTP=%s", server.c_str()); - return true; - } - - void revert(const ParsedPayload& /*p*/) override { - removeFile(kTimesyncConf); - runCmd("systemctl restart systemd-timesyncd 2>/dev/null"); - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// GLOBAL HTTP PROXY HANDLER -// Writes /etc/environment proxy vars and -// /etc/apt/apt.conf.d/99-vesperos-proxy -// /etc/profile.d/vesperos-proxy.sh -// ═══════════════════════════════════════════════════════════════════════════ - -class ProxyHandler : public PayloadHandler { - static constexpr const char* kAptProxy = - "/etc/apt/apt.conf.d/99-vesperos-proxy"; - static constexpr const char* kProfileProxy = - "/etc/profile.d/vesperos-proxy.sh"; -public: - bool apply(const ParsedPayload& p) override { - // proxy block stored as JSON in fields["proxy"] - auto proxyJson = p.fields.find("proxy"); - if (proxyJson == p.fields.end()) return true; - - bool isManual = proxyJson->second.find("\"type\":\"manual\"") != std::string::npos - || proxyJson->second.find("manual") != std::string::npos; - - auto extract = [&](const std::string& key) -> std::string { - auto pos = proxyJson->second.find("\"" + key + "\":"); - if (pos == std::string::npos) return ""; - pos += key.size() + 3; - auto q1 = proxyJson->second.find('"', pos); - auto q2 = proxyJson->second.find('"', q1 + 1); - if (q1 == std::string::npos) return ""; - return proxyJson->second.substr(q1 + 1, q2 - q1 - 1); - }; - - if (isManual) { - std::string host = extract("host"); - std::string port = extract("port"); - std::string user = extract("username"); - std::string pass = extract("password"); - - std::string auth = user.empty() ? "" : (user + ":" + pass + "@"); - std::string proxyUrl = "http://" + auth + host + ":" + port; - - // /etc/profile.d/vesperos-proxy.sh — sets env vars for all users - std::ostringstream sh; - sh << "# VesperOS managed proxy\n" - << "export http_proxy=" << proxyUrl << "\n" - << "export https_proxy=" << proxyUrl << "\n" - << "export HTTP_PROXY=" << proxyUrl << "\n" - << "export HTTPS_PROXY=" << proxyUrl << "\n" - << "export no_proxy=localhost,127.0.0.1,::1\n"; - writeFile(kProfileProxy, sh.str(), 0644); - - // apt proxy - std::ostringstream apt; - apt << "// VesperOS managed\n" - << "Acquire::http::Proxy \"" << proxyUrl << "\";\n" - << "Acquire::https::Proxy \"" << proxyUrl << "\";\n"; - writeFile(kAptProxy, apt.str()); - - // NetworkManager global proxy - std::ostringstream nmConf; - nmConf << "[connectivity]\n\n" - << "[global-dns-domain-*]\n\n" - << "[main]\n" - << "proxy=http\n" - << "proxy-url=" << proxyUrl << "\n"; - writeFile("/etc/NetworkManager/conf.d/vesperos-proxy.conf", nmConf.str()); - runCmd("nmcli general reload 2>/dev/null"); - } else { - // PAC auto-proxy — write to NM and GNOME - std::string pacUrl = extract("pac-url"); - if (!pacUrl.empty()) { - std::ostringstream nmConf; - nmConf << "[main]\nproxy=auto\nproxy-url=" << pacUrl << "\n"; - writeFile("/etc/NetworkManager/conf.d/vesperos-proxy.conf", nmConf.str()); - runCmd("nmcli general reload 2>/dev/null"); - } - } - - syslog(LOG_INFO, "[ProxyHandler] applied global proxy"); - return true; - } - - void revert(const ParsedPayload& /*p*/) override { - removeFile(kAptProxy); - removeFile(kProfileProxy); - removeFile("/etc/NetworkManager/conf.d/vesperos-proxy.conf"); - runCmd("nmcli general reload 2>/dev/null"); - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// DNS PROXY HANDLER -// Writes /etc/systemd/resolved.conf.d/vesperos-dns.conf -// ═══════════════════════════════════════════════════════════════════════════ - -class DnsProxyHandler : public PayloadHandler { - static constexpr const char* kResolvedDir = - "/etc/systemd/resolved.conf.d"; - static constexpr const char* kResolvedConf = - "/etc/systemd/resolved.conf.d/vesperos-dns.conf"; -public: - bool apply(const ParsedPayload& p) override { - auto configJson = p.fields.find("config"); - if (configJson == p.fields.end()) return true; - - auto pos = configJson->second.find("\"ServerURL\":"); - std::string dnsUrl; - if (pos != std::string::npos) { - pos += 12; - auto q1 = configJson->second.find('"', pos); - auto q2 = configJson->second.find('"', q1 + 1); - if (q1 != std::string::npos) - dnsUrl = configJson->second.substr(q1 + 1, q2 - q1 - 1); - } - - ensureDir(kResolvedDir); - std::ostringstream c; - c << "[Resolve]\n"; - if (!dnsUrl.empty()) c << "DNS=" << dnsUrl << "\n"; - c << "DNSStubListener=yes\n" - << "DNSSEC=allow-downgrade\n" - << "DNSOverTLS=opportunistic\n"; - writeFile(kResolvedConf, c.str()); - runCmd("systemctl restart systemd-resolved 2>/dev/null"); - syslog(LOG_INFO, "[DnsProxyHandler] applied DNS config"); - return true; - } - - void revert(const ParsedPayload& /*p*/) override { - removeFile(kResolvedConf); - runCmd("systemctl restart systemd-resolved 2>/dev/null"); - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// FIREWALL HANDLER -// Writes nftables rules to /etc/nftables.d/vesperos.nft -// ═══════════════════════════════════════════════════════════════════════════ - -class FirewallHandler : public PayloadHandler { - static constexpr const char* kNftFile = - "/etc/nftables.d/vesperos.nft"; -public: - bool apply(const ParsedPayload& p) override { - bool enabled = fieldBool(p, "enabled", false); - bool blockAll = fieldBool(p, "block-all-incoming", false); - bool stealthMode = fieldBool(p, "stealth-mode", false); - - if (!enabled) return true; - - ensureDir("/etc/nftables.d"); - std::ostringstream nft; - nft << "# VesperOS managed firewall — do not edit\n" - << "table inet vesperos_fw {\n" - << " chain input {\n" - << " type filter hook input priority 0; policy " - << (blockAll ? "drop" : "accept") << ";\n"; - - if (blockAll) { - // Allow established/related - nft << " ct state established,related accept\n" - << " iif lo accept\n"; - // Allow ICMP unless stealth mode - if (!stealthMode) nft << " ip protocol icmp accept\n"; - } - - // Per-app rules: on Linux, app-level filtering is handled by - // AppArmor/seccomp rather than nftables. Skip here. - - nft << " }\n" - << " chain forward {\n" - << " type filter hook forward priority 0; policy accept;\n" - << " }\n" - << " chain output {\n" - << " type filter hook output priority 0; policy accept;\n" - << " }\n" - << "}\n"; - - writeFile(kNftFile, nft.str()); - runCmd("nft -f " + std::string(kNftFile) + " 2>/dev/null"); - runCmd("systemctl enable nftables 2>/dev/null"); - syslog(LOG_INFO, "[FirewallHandler] applied nftables rules"); - return true; - } - - void revert(const ParsedPayload& /*p*/) override { - runCmd("nft delete table inet vesperos_fw 2>/dev/null"); - removeFile(kNftFile); - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// LDAP HANDLER -// Writes /etc/ldap/ldap.conf for LDAP client configuration. -// Also configures libpam-ldapd / nss-ldapd if installed. -// ═══════════════════════════════════════════════════════════════════════════ - -class LdapHandler : public PayloadHandler { - static constexpr const char* kLdapConf = "/etc/ldap/ldap.conf"; - static constexpr const char* kNslcdConf = "/etc/nslcd.conf"; -public: - bool apply(const ParsedPayload& p) override { - std::string host = field(p, "host"); - bool ssl = fieldBool(p, "ssl", true); - std::string username = field(p, "username"); - std::string password = field(p, "password"); - - auto searchJson = p.fields.find("search-settings"); - std::string base; - if (searchJson != p.fields.end()) { - auto pos = searchJson->second.find("\"base\":"); - if (pos != std::string::npos) { - pos += 7; - auto q1 = searchJson->second.find('"', pos); - auto q2 = searchJson->second.find('"', q1 + 1); - if (q1 != std::string::npos) - base = searchJson->second.substr(q1 + 1, q2 - q1 - 1); - } - } - - std::string uri = (ssl ? "ldaps://" : "ldap://") + host; - - // /etc/ldap/ldap.conf - std::ostringstream lc; - lc << "# VesperOS managed\n" - << "URI " << uri << "\n"; - if (!base.empty()) lc << "BASE " << base << "\n"; - lc << "TLS_CACERT /etc/ssl/certs/ca-certificates.crt\n"; - writeFile(kLdapConf, lc.str(), 0644); - - // /etc/nslcd.conf (if nslcd / libnss-ldapd is installed) - std::ostringstream nc; - nc << "# VesperOS managed\n" - << "uid nslcd\ngid nslcd\n" - << "uri " << uri << "\n"; - if (!base.empty()) nc << "base " << base << "\n"; - if (!username.empty()) nc << "binddn " << username << "\n" - << "bindpw " << password << "\n"; - nc << "ssl " << (ssl ? "on" : "off") << "\n" - << "tls_cacertfile /etc/ssl/certs/ca-certificates.crt\n"; - writeFile(kNslcdConf, nc.str(), 0600); - - runCmd("systemctl try-restart nslcd 2>/dev/null"); - syslog(LOG_INFO, "[LdapHandler] configured LDAP host=%s", host.c_str()); - return true; - } - - void revert(const ParsedPayload& /*p*/) override { - // Restore defaults (empty ldap.conf) - writeFile(kLdapConf, "# No LDAP configured\n"); - removeFile(kNslcdConf); - runCmd("systemctl try-restart nslcd 2>/dev/null"); - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// WALLPAPER HANDLER -// Sets GNOME wallpaper via gsettings (runs as root → sets system default). -// ═══════════════════════════════════════════════════════════════════════════ - -class WallpaperHandler : public PayloadHandler { - static constexpr const char* kGdmDefaults = - "/etc/dconf/db/local.d/00-vesperos-wallpaper"; - static constexpr const char* kGdmLock = - "/etc/dconf/db/local.d/locks/vesperos-wallpaper"; -public: - bool apply(const ParsedPayload& p) override { - // wallpaper is a nested object; find the path field from its JSON - auto wJson = p.fields.find("wallpaper"); - if (wJson == p.fields.end()) return true; - - auto extractStr = [&](const std::string& key) -> std::string { - auto pos = wJson->second.find("\"" + key + "\":"); - if (pos == std::string::npos) return ""; - pos += key.size() + 3; - auto q1 = wJson->second.find('"', pos); - auto q2 = wJson->second.find('"', q1 + 1); - if (q1 == std::string::npos) return ""; - return wJson->second.substr(q1 + 1, q2 - q1 - 1); - }; - - std::string path = extractStr("path"); - bool locked = wJson->second.find("\"locked\":true") != std::string::npos; - - if (path.empty()) return true; - - // dconf system-wide profile - ensureDir("/etc/dconf/db/local.d"); - ensureDir("/etc/dconf/db/local.d/locks"); - - std::ostringstream dconf; - dconf << "[org/gnome/desktop/background]\n" - << "picture-uri='file://" << path << "'\n" - << "picture-uri-dark='file://" << path << "'\n" - << "picture-options='zoom'\n"; - writeFile(kGdmDefaults, dconf.str()); - - if (locked) { - writeFile(kGdmLock, - "/org/gnome/desktop/background/picture-uri\n" - "/org/gnome/desktop/background/picture-uri-dark\n" - "/org/gnome/desktop/background/picture-options\n"); - } - - runCmd("dconf update 2>/dev/null"); - syslog(LOG_INFO, "[WallpaperHandler] set wallpaper path=%s locked=%d", - path.c_str(), locked); - return true; - } - - void revert(const ParsedPayload& /*p*/) override { - removeFile(kGdmDefaults); - removeFile(kGdmLock); - runCmd("dconf update 2>/dev/null"); - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// SCREENSAVER HANDLER -// Configures GNOME screensaver/lock via dconf system profile. -// ═══════════════════════════════════════════════════════════════════════════ - -class ScreensaverHandler : public PayloadHandler { - static constexpr const char* kDconfFile = - "/etc/dconf/db/local.d/00-vesperos-screensaver"; -public: - bool apply(const ParsedPayload& p) override { - int idleSecs = std::stoi(field(p, "idle-seconds", "300")); - bool locked = fieldBool(p, "locked", false); - - ensureDir("/etc/dconf/db/local.d"); - std::ostringstream d; - d << "[org/gnome/desktop/session]\n" - << "idle-delay=uint32 " << idleSecs << "\n\n" - << "[org/gnome/desktop/screensaver]\n" - << "lock-enabled=true\n" - << "lock-delay=uint32 0\n"; - writeFile(kDconfFile, d.str()); - - if (locked) { - ensureDir("/etc/dconf/db/local.d/locks"); - writeFile("/etc/dconf/db/local.d/locks/vesperos-screensaver", - "/org/gnome/desktop/session/idle-delay\n" - "/org/gnome/desktop/screensaver/lock-enabled\n"); - } - - runCmd("dconf update 2>/dev/null"); - syslog(LOG_INFO, "[ScreensaverHandler] idle=%ds locked=%d", idleSecs, locked); - return true; - } - - void revert(const ParsedPayload& /*p*/) override { - removeFile(kDconfFile); - removeFile("/etc/dconf/db/local.d/locks/vesperos-screensaver"); - runCmd("dconf update 2>/dev/null"); - } -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// SETUP ASSISTANT HANDLER -// On Linux this maps to cloud-init skip directives. -// Writes a cloud-init drop-in that suppresses first-boot wizard screens. -// ═══════════════════════════════════════════════════════════════════════════ - -// ═══════════════════════════════════════════════════════════════════════════ -// FIRST-BOOT HANDLER (renamed from setup-assistant) -// -// This handler runs BEFORE cloud-init on first boot. The systemd ordering -// in vesperprofiled.service (Before=cloud-init-local.service) ensures that -// when a preinstalled profile baked into the image contains a first-boot -// payload, vesperprofiled applies it first so that: -// -// 1. The cloud-init drop-in is already on disk when cloud-init starts. -// 2. Any WiFi or VPN profile is already written to NM so cloud-init can -// reach its datasource over the managed connection. -// 3. The MDM drop-in is in /etc/cloud/cloud.cfg.d/ before cloud-init -// runs, letting the MDM server inject additional cloud-init config. -// -// What it does: -// - Writes /etc/cloud/cloud.cfg.d/99-vesperos-firstboot.cfg to suppress -// cloud-init wizard modules that are replaced by managed profiles. -// - Writes cloud-init's 'disabled' modules list so it skips anything -// explicitly listed in the profile's skip array. -// - Suppresses the VesperOS first-boot GNOME Initial Setup wizard -// via a GSettings override and a .done semaphore file. -// - Writes a /etc/vesperprofiled/firstboot.done stamp so subsequent -// daemon starts (and cloud-init re-runs) know first-boot is complete. -// ═══════════════════════════════════════════════════════════════════════════ - -class FirstBootHandler : public PayloadHandler { - // cloud-init drop-in — written before cloud-init starts - static constexpr const char* kCloudInitDrop = - "/etc/cloud/cloud.cfg.d/99-vesperos-firstboot.cfg"; - - // GNOME Initial Setup bypass - static constexpr const char* kGnomeSetupDone = - "/var/lib/gnome-initial-setup/.gnome-initial-setup-done"; - - // dconf override disabling GNOME Initial Setup on all accounts - static constexpr const char* kGnomeDconfOverride = - "/etc/dconf/db/local.d/00-vesperos-firstboot"; - - // Semaphore: written last, after all other drops are in place - static constexpr const char* kDoneStamp = - "/etc/vesperprofiled/firstboot.done"; - - // Map of VesperOS skip-pane names to cloud-init module names to disable. - // cloud-init reads cloud_final_modules / cloud_config_modules; we inject - // a disabled_modules list which cloud-init (>=22.1) honours directly. - static const std::pair kSkipMap[]; - static constexpr size_t kSkipMapLen = 10; - -public: - bool apply(const ParsedPayload& p) override { - // ── 1. Build the cloud-init drop-in ─────────────────────────────── - // Collect which panes to skip - std::string skipJson = field(p, "skip"); - std::vector disabledModules; - - for (size_t i = 0; i < kSkipMapLen; ++i) { - if (skipJson.find(kSkipMap[i].first) != std::string::npos) - disabledModules.push_back(kSkipMap[i].second); - } - - ensureDir("/etc/cloud/cloud.cfg.d"); - std::ostringstream ci; - ci << "# VesperOS first-boot configuration — managed by vesperprofiled\n" - << "# Applied before cloud-init runs. Do not edit manually.\n" - << "#\n" - << "# vesperprofiled has already written all managed network,\n" - << "# certificate, and policy configurations. cloud-init only\n" - << "# needs to handle datasource-specific work.\n\n"; - - // Suppress the interactive first-boot wizard entirely - ci << "# Suppress interactive setup\n" - << "resize_rootfs: false\n" - << "ssh_pwauth: false\n" - << "final_message: \"\"\n\n"; - - // Don't let cloud-init clobber NM-managed network config - ci << "# Preserve vesperprofiled-managed network settings\n" - << "network:\n" - << " config: disabled\n\n"; - - // Disable specific cloud-init modules that correspond to skipped panes - if (!disabledModules.empty()) { - ci << "# Modules disabled by first-boot skip list\n" - << "disabled_modules:\n"; - for (const auto& m : disabledModules) - ci << " - " << m << "\n"; - ci << "\n"; - } - - // If software-update is in the skip list, also suppress apt operations - if (skipJson.find("software-update") != std::string::npos) { - ci << "# Software update skipped — apt managed by software-update payload\n" - << "package_update: false\n" - << "package_upgrade: false\n"; - } - - writeFile(kCloudInitDrop, ci.str(), 0644); - syslog(LOG_INFO, "[FirstBootHandler] wrote cloud-init drop-in"); - - // ── 2. Suppress GNOME Initial Setup ─────────────────────────────── - // Create the .done stamp so gnome-initial-setup skips on first login. - ensureDir("/var/lib/gnome-initial-setup"); - { - // Touch the file - std::ofstream f(kGnomeSetupDone); - // content doesn't matter; existence is the signal - } - ::chmod(kGnomeSetupDone, 0644); - - // dconf system default: tell gnome-initial-setup it has already run - ensureDir("/etc/dconf/db/local.d"); - writeFile(kGnomeDconfOverride, - "# VesperOS managed — skip GNOME Initial Setup\n" - "[org/gnome/initial-setup]\n" - "run-on-guest=false\n", - 0644); - runCmd("dconf update 2>/dev/null"); - - syslog(LOG_INFO, "[FirstBootHandler] suppressed GNOME Initial Setup"); - - // ── 3. Write the done stamp last ────────────────────────────────── - // This is the signal to main.cpp that first-boot profiles have been - // fully applied. The stamp is checked on subsequent starts so we - // don't re-apply first-boot payloads on every boot. - ensureDir("/etc/vesperprofiled"); - { - std::ofstream f(kDoneStamp); - // Write the profile UUID so we know which profile ran - f << p.uuid << "\n"; - } - ::chmod(kDoneStamp, 0644); - - syslog(LOG_INFO, "[FirstBootHandler] first-boot complete, stamp written"); - return true; - } - - void revert(const ParsedPayload& p) override { - removeFile(kCloudInitDrop); - removeFile(kGnomeDconfOverride); - removeFile("/etc/dconf/db/local.d/locks/vesperos-firstboot"); - removeFile(kDoneStamp); - runCmd("dconf update 2>/dev/null"); - syslog(LOG_INFO, "[FirstBootHandler] reverted first-boot config uuid=%s", - p.uuid.c_str()); - } -}; - -// Static skip map definition -const std::pair FirstBootHandler::kSkipMap[] = { - // VesperOS pane name → cloud-init module name - {"software-update", "package-update-upgrade-install"}, - {"location", "ntp"}, - {"privacy", "users-groups"}, - {"tos", "final-message"}, - {"passcode", "set-passwords"}, - {"restore", "chef"}, // no exact equiv; suppress restore agent - {"biometric", "mcollective"}, // no exact equiv - {"siri", "puppet"}, // no exact equiv - {"android-migration", "migrator"}, - {"onboarding", "write-files"}, -}; - -// ── Stub handler for unimplemented types ───────────────────────────────── +VESPER_DISPATCH_HANDLER(WifiHandler, wifi); +VESPER_DISPATCH_HANDLER(EthernetHandler, ethernet); +VESPER_DISPATCH_HANDLER(VpnHandler, vpn); +VESPER_DISPATCH_HANDLER(CertHandler, cert); +VESPER_DISPATCH_HANDLER(Pkcs12Handler, pkcs12); +VESPER_DISPATCH_HANDLER(PasscodeHandler, passcode); +VESPER_DISPATCH_HANDLER(MdmHandler, mdm); +VESPER_DISPATCH_HANDLER(SoftwareUpdateHandler, software_update); +VESPER_DISPATCH_HANDLER(TimeServerHandler, time_server); +VESPER_DISPATCH_HANDLER(ProxyHandler, proxy); +VESPER_DISPATCH_HANDLER(DnsProxyHandler, dns_proxy); +VESPER_DISPATCH_HANDLER(FirewallHandler, firewall); +VESPER_DISPATCH_HANDLER(LdapHandler, ldap); +VESPER_DISPATCH_HANDLER(WallpaperHandler, wallpaper); +VESPER_DISPATCH_HANDLER(ScreensaverHandler, screensaver); +VESPER_DISPATCH_HANDLER(FirstBootHandler, first_boot); +VESPER_DISPATCH_HANDLER(ContentCacheHandler, content_cache); + +#undef VESPER_DISPATCH_HANDLER + +// ── Stub handler for payload types with no platform implementation at all +// (not even a per-platform split — nothing to dispatch to yet) ───────── class StubHandler : public PayloadHandler { std::string mType; @@ -1388,7 +112,8 @@ PayloadHandler* PayloadHandlerRegistry::get(const std::string& type) { void PayloadHandlerRegistry::registerAll() { auto& m = handlers(); - // Real handlers + // Real handlers (dispatching to a per-platform implementation — see + // this file's header comment) m["wifi"] = std::make_unique(); m["ethernet"] = std::make_unique(); m["vpn"] = std::make_unique(); @@ -1397,6 +122,7 @@ void PayloadHandlerRegistry::registerAll() { m["passcode"] = std::make_unique(); m["mdm"] = std::make_unique(); m["software-update"] = std::make_unique(); + m["content-cache"] = std::make_unique(); m["time-server"] = std::make_unique(); m["proxy-http"] = std::make_unique(); m["dns-proxy"] = std::make_unique(); @@ -1409,7 +135,7 @@ void PayloadHandlerRegistry::registerAll() { m["first-boot"] = std::make_unique(); m["setup-assistant"] = std::make_unique(); - // Stubs for types that don't have a direct Linux equivalent or + // Stubs for types that don't have a direct platform equivalent or // are handled elsewhere (companion app / manual config) for (const char* t : { "scep", "cert-preference", "cert-transparency", diff --git a/src/payloads/PayloadUtil.h b/src/payloads/PayloadUtil.h new file mode 100644 index 0000000..6b37736 --- /dev/null +++ b/src/payloads/PayloadUtil.h @@ -0,0 +1,112 @@ +#pragma once + +// Small POSIX/OpenSSL helpers shared by every platform/linux and +// platform/android handler implementation. Header-only + inline so both +// platforms' translation units can include it without a separate .cpp +// (and without ODR issues, per `inline` semantics). +// +// Everything here is genuinely portable (POSIX file I/O, OpenSSL EVP, +// plain string parsing) — nothing platform-specific lives in this file. +// That's what makes it safe to share instead of splitting. + +#include + +#include +#include +#include + +#include "../ProfileParser.h" + +#include +#include +#include +#include +#include + +namespace vesperos::profile::util { + +// Was PayloadHandler::field/fieldBool (protected statics only reachable from +// a subclass body) — moved here as free functions so platform/*/*.cpp files, +// which aren't PayloadHandler subclasses anymore, can call them directly. +// PayloadHandler.h's own field()/fieldBool() now just forward to these, kept +// for any code still calling them via the base class. +inline std::string field(const ParsedPayload& p, const std::string& key, + const std::string& def = "") { + auto it = p.fields.find(key); + if (it == p.fields.end()) return def; + std::string v = it->second; + if (v.size() >= 2 && v.front() == '"' && v.back() == '"') + v = v.substr(1, v.size() - 2); + return v; +} + +inline bool fieldBool(const ParsedPayload& p, const std::string& key, bool def = false) { + std::string v = field(p, key); + if (v.empty()) return def; + return (v == "true" || v == "1"); +} + +inline bool runCmd(const std::string& cmd) { + syslog(LOG_DEBUG, "vesperprofiled: exec: %s", cmd.c_str()); + int ret = ::system(cmd.c_str()); + if (ret != 0) + syslog(LOG_WARNING, "vesperprofiled: command exited %d: %s", ret, cmd.c_str()); + return ret == 0; +} + +inline bool writeFile(const std::string& path, const std::string& content, mode_t mode = 0644) { + std::string tmp = path + ".tmp"; + { + std::ofstream f(tmp, std::ios::binary | std::ios::trunc); + if (!f) { syslog(LOG_ERR, "vesperprofiled: cannot write %s", tmp.c_str()); return false; } + f << content; + } + chmod(tmp.c_str(), mode); + if (::rename(tmp.c_str(), path.c_str()) != 0) { + syslog(LOG_ERR, "vesperprofiled: rename failed: %s -> %s", tmp.c_str(), path.c_str()); + ::unlink(tmp.c_str()); + return false; + } + return true; +} + +inline void removeFile(const std::string& path) { + ::unlink(path.c_str()); +} + +inline void ensureDir(const std::string& path, mode_t mode = 0755) { + ::mkdir(path.c_str(), mode); +} + +inline std::vector b64decode(const std::string& in) { + std::string s = in; + if (s.size() >= 2 && s.front() == '"') s = s.substr(1, s.size() - 2); + std::vector out(s.size()); + int len = EVP_DecodeBlock(out.data(), reinterpret_cast(s.data()), (int)s.size()); + if (len < 0) return {}; + out.resize(len); + return out; +} + +// Extract a string value from a flat JSON object stored as raw text in a +// ParsedPayload field (e.g. extractJson(raw, "password") on +// {"type":"wpa2","password":"foo"} returns "foo"). Only handles string +// values; returns "" on miss. Used by every handler that has to reach into +// a nested payload object (ProfileParser re-serializes those as JSON text, +// see ParsedPayload::fields in ProfileParser.h). +inline std::string extractJson(const std::string& json, const std::string& key) { + auto pos = json.find("\"" + key + "\":"); + if (pos == std::string::npos) return ""; + pos += key.size() + 3; // skip "key": + while (pos < json.size() && json[pos] == ' ') ++pos; + if (pos >= json.size()) return ""; + if (json[pos] == '"') { + auto q2 = json.find('"', pos + 1); + if (q2 == std::string::npos) return ""; + return json.substr(pos + 1, q2 - pos - 1); + } + auto end = json.find_first_of(",}", pos); + return json.substr(pos, end - pos); +} + +} // namespace vesperos::profile::util diff --git a/src/platform/Cert.h b/src/platform/Cert.h new file mode 100644 index 0000000..51e2aca --- /dev/null +++ b/src/platform/Cert.h @@ -0,0 +1,7 @@ +#pragma once +#include "../ProfileParser.h" + +namespace vesperos::profile::platform::cert { +bool apply(const ParsedPayload& p); +void revert(const ParsedPayload& p); +} // namespace vesperos::profile::platform::cert diff --git a/src/platform/ContentCache.h b/src/platform/ContentCache.h new file mode 100644 index 0000000..8f8d525 --- /dev/null +++ b/src/platform/ContentCache.h @@ -0,0 +1,7 @@ +#pragma once +#include "../ProfileParser.h" + +namespace vesperos::profile::platform::content_cache { +bool apply(const ParsedPayload& p); +void revert(const ParsedPayload& p); +} // namespace vesperos::profile::platform::content_cache diff --git a/src/platform/DnsProxy.h b/src/platform/DnsProxy.h new file mode 100644 index 0000000..c1ed685 --- /dev/null +++ b/src/platform/DnsProxy.h @@ -0,0 +1,7 @@ +#pragma once +#include "../ProfileParser.h" + +namespace vesperos::profile::platform::dns_proxy { +bool apply(const ParsedPayload& p); +void revert(const ParsedPayload& p); +} // namespace vesperos::profile::platform::dns_proxy diff --git a/src/platform/Ethernet.h b/src/platform/Ethernet.h new file mode 100644 index 0000000..644b191 --- /dev/null +++ b/src/platform/Ethernet.h @@ -0,0 +1,7 @@ +#pragma once +#include "../ProfileParser.h" + +namespace vesperos::profile::platform::ethernet { +bool apply(const ParsedPayload& p); +void revert(const ParsedPayload& p); +} // namespace vesperos::profile::platform::ethernet diff --git a/src/platform/Firewall.h b/src/platform/Firewall.h new file mode 100644 index 0000000..9607c56 --- /dev/null +++ b/src/platform/Firewall.h @@ -0,0 +1,7 @@ +#pragma once +#include "../ProfileParser.h" + +namespace vesperos::profile::platform::firewall { +bool apply(const ParsedPayload& p); +void revert(const ParsedPayload& p); +} // namespace vesperos::profile::platform::firewall diff --git a/src/platform/FirstBoot.h b/src/platform/FirstBoot.h new file mode 100644 index 0000000..7545fb3 --- /dev/null +++ b/src/platform/FirstBoot.h @@ -0,0 +1,7 @@ +#pragma once +#include "../ProfileParser.h" + +namespace vesperos::profile::platform::first_boot { +bool apply(const ParsedPayload& p); +void revert(const ParsedPayload& p); +} // namespace vesperos::profile::platform::first_boot diff --git a/src/platform/Ldap.h b/src/platform/Ldap.h new file mode 100644 index 0000000..24b51de --- /dev/null +++ b/src/platform/Ldap.h @@ -0,0 +1,7 @@ +#pragma once +#include "../ProfileParser.h" + +namespace vesperos::profile::platform::ldap { +bool apply(const ParsedPayload& p); +void revert(const ParsedPayload& p); +} // namespace vesperos::profile::platform::ldap diff --git a/src/platform/Mdm.h b/src/platform/Mdm.h new file mode 100644 index 0000000..0b6737e --- /dev/null +++ b/src/platform/Mdm.h @@ -0,0 +1,7 @@ +#pragma once +#include "../ProfileParser.h" + +namespace vesperos::profile::platform::mdm { +bool apply(const ParsedPayload& p); +void revert(const ParsedPayload& p); +} // namespace vesperos::profile::platform::mdm diff --git a/src/platform/Passcode.h b/src/platform/Passcode.h new file mode 100644 index 0000000..0d99a03 --- /dev/null +++ b/src/platform/Passcode.h @@ -0,0 +1,7 @@ +#pragma once +#include "../ProfileParser.h" + +namespace vesperos::profile::platform::passcode { +bool apply(const ParsedPayload& p); +void revert(const ParsedPayload& p); +} // namespace vesperos::profile::platform::passcode diff --git a/src/platform/Pkcs12.h b/src/platform/Pkcs12.h new file mode 100644 index 0000000..4fd1e46 --- /dev/null +++ b/src/platform/Pkcs12.h @@ -0,0 +1,7 @@ +#pragma once +#include "../ProfileParser.h" + +namespace vesperos::profile::platform::pkcs12 { +bool apply(const ParsedPayload& p); +void revert(const ParsedPayload& p); +} // namespace vesperos::profile::platform::pkcs12 diff --git a/src/platform/Proxy.h b/src/platform/Proxy.h new file mode 100644 index 0000000..3adc3ef --- /dev/null +++ b/src/platform/Proxy.h @@ -0,0 +1,7 @@ +#pragma once +#include "../ProfileParser.h" + +namespace vesperos::profile::platform::proxy { +bool apply(const ParsedPayload& p); +void revert(const ParsedPayload& p); +} // namespace vesperos::profile::platform::proxy diff --git a/src/platform/Screensaver.h b/src/platform/Screensaver.h new file mode 100644 index 0000000..0b2d4c7 --- /dev/null +++ b/src/platform/Screensaver.h @@ -0,0 +1,7 @@ +#pragma once +#include "../ProfileParser.h" + +namespace vesperos::profile::platform::screensaver { +bool apply(const ParsedPayload& p); +void revert(const ParsedPayload& p); +} // namespace vesperos::profile::platform::screensaver diff --git a/src/platform/SoftwareUpdate.h b/src/platform/SoftwareUpdate.h new file mode 100644 index 0000000..76da493 --- /dev/null +++ b/src/platform/SoftwareUpdate.h @@ -0,0 +1,7 @@ +#pragma once +#include "../ProfileParser.h" + +namespace vesperos::profile::platform::software_update { +bool apply(const ParsedPayload& p); +void revert(const ParsedPayload& p); +} // namespace vesperos::profile::platform::software_update diff --git a/src/platform/TimeServer.h b/src/platform/TimeServer.h new file mode 100644 index 0000000..7f7bc70 --- /dev/null +++ b/src/platform/TimeServer.h @@ -0,0 +1,7 @@ +#pragma once +#include "../ProfileParser.h" + +namespace vesperos::profile::platform::time_server { +bool apply(const ParsedPayload& p); +void revert(const ParsedPayload& p); +} // namespace vesperos::profile::platform::time_server diff --git a/src/platform/Vpn.h b/src/platform/Vpn.h new file mode 100644 index 0000000..7470399 --- /dev/null +++ b/src/platform/Vpn.h @@ -0,0 +1,7 @@ +#pragma once +#include "../ProfileParser.h" + +namespace vesperos::profile::platform::vpn { +bool apply(const ParsedPayload& p); +void revert(const ParsedPayload& p); +} // namespace vesperos::profile::platform::vpn diff --git a/src/platform/Wallpaper.h b/src/platform/Wallpaper.h new file mode 100644 index 0000000..49148d6 --- /dev/null +++ b/src/platform/Wallpaper.h @@ -0,0 +1,7 @@ +#pragma once +#include "../ProfileParser.h" + +namespace vesperos::profile::platform::wallpaper { +bool apply(const ParsedPayload& p); +void revert(const ParsedPayload& p); +} // namespace vesperos::profile::platform::wallpaper diff --git a/src/platform/Wifi.h b/src/platform/Wifi.h new file mode 100644 index 0000000..afca542 --- /dev/null +++ b/src/platform/Wifi.h @@ -0,0 +1,11 @@ +#pragma once +#include "../ProfileParser.h" + +// Contract both platform/linux/Wifi.cpp and platform/android/Wifi.cpp +// implement. Only one gets compiled per target (CMakeLists.txt vs +// Android.bp), so there's no #ifdef and no linker conflict — see +// PayloadHandlers.cpp's WifiHandler for the shared dispatch that calls this. +namespace vesperos::profile::platform::wifi { +bool apply(const ParsedPayload& p); +void revert(const ParsedPayload& p); +} // namespace vesperos::profile::platform::wifi diff --git a/src/platform/android/Cert.cpp b/src/platform/android/Cert.cpp new file mode 100644 index 0000000..67cf9b2 --- /dev/null +++ b/src/platform/android/Cert.cpp @@ -0,0 +1,20 @@ +#include "../Cert.h" + +#include + +// Not yet implemented on Android. A real port needs DevicePolicyManager's +// installCaCert() (device-owner) or the KeyChain install-cert intent flow — +// AOSP has no /etc/ssl/certs-equivalent a daemon can just write files into. + +namespace vesperos::profile::platform::cert { + +bool apply(const ParsedPayload& p) { + syslog(LOG_WARNING, "[cert] not implemented on Android yet, uuid=%s", p.uuid.c_str()); + return false; +} + +void revert(const ParsedPayload& p) { + syslog(LOG_WARNING, "[cert] not implemented on Android yet, uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::cert diff --git a/src/platform/android/ContentCache.cpp b/src/platform/android/ContentCache.cpp new file mode 100644 index 0000000..1a77fc7 --- /dev/null +++ b/src/platform/android/ContentCache.cpp @@ -0,0 +1,52 @@ +#include "../ContentCache.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// CONTENT CACHE HANDLER (Android) +// PawletOS-fork-specific — vesperprofiled's default VesperOS build doesn't +// register this handler at all (see PayloadHandlers.cpp). Writes the +// local-network-cache discovery override PawletCacheService reads — see +// pawletcache-server/README.md and +// android_packages_apps_PawletCache/src/os/pawlet/cache/PolicyOverride.kt. +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::content_cache { + +using namespace vesperos::profile::util; + +namespace { +constexpr const char* kPolicyDir = "/data/misc/pawletcache"; +constexpr const char* kPolicyFile = "/data/misc/pawletcache/policy.json"; +} + +bool apply(const ParsedPayload& p) { + std::string mode = field(p, "mode", "both"); + + std::ostringstream json; + json << "{\n \"mode\": \"" << mode << "\""; + + auto pinned = p.fields.find("pinned-server"); + if (pinned != p.fields.end()) { + // Already valid JSON text (ProfileParser re-serializes nested + // objects) — relay it verbatim under the camelCase key + // PolicyOverride.kt expects, no reparsing needed here. + json << ",\n \"pinnedServer\": " << pinned->second; + } + json << "\n}\n"; + + ensureDir(kPolicyDir); + // World-readable by design — PawletCacheService reads this as a + // regular app UID, and it's policy, not a secret (see PolicyOverride.kt). + writeFile(kPolicyFile, json.str(), 0644); + syslog(LOG_INFO, "[content-cache] applied mode=%s", mode.c_str()); + return true; +} + +void revert(const ParsedPayload& /*p*/) { + removeFile(kPolicyFile); +} + +} // namespace vesperos::profile::platform::content_cache diff --git a/src/platform/android/DnsProxy.cpp b/src/platform/android/DnsProxy.cpp new file mode 100644 index 0000000..faf9bfc --- /dev/null +++ b/src/platform/android/DnsProxy.cpp @@ -0,0 +1,20 @@ +#include "../DnsProxy.h" + +#include + +// Not yet implemented on Android. A real port would write +// Settings.Global.PRIVATE_DNS_MODE / PRIVATE_DNS_SPECIFIER (needs +// WRITE_SECURE_SETTINGS) — no systemd-resolved.conf.d equivalent. + +namespace vesperos::profile::platform::dns_proxy { + +bool apply(const ParsedPayload& p) { + syslog(LOG_WARNING, "[dns-proxy] not implemented on Android yet, uuid=%s", p.uuid.c_str()); + return false; +} + +void revert(const ParsedPayload& p) { + syslog(LOG_WARNING, "[dns-proxy] not implemented on Android yet, uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::dns_proxy diff --git a/src/platform/android/Ethernet.cpp b/src/platform/android/Ethernet.cpp new file mode 100644 index 0000000..385efed --- /dev/null +++ b/src/platform/android/Ethernet.cpp @@ -0,0 +1,19 @@ +#include "../Ethernet.h" + +#include + +// Not yet implemented on Android. A real port would use the hidden/system +// android.net.EthernetManager API — no NetworkManager equivalent exists. + +namespace vesperos::profile::platform::ethernet { + +bool apply(const ParsedPayload& p) { + syslog(LOG_WARNING, "[ethernet] not implemented on Android yet, uuid=%s", p.uuid.c_str()); + return false; +} + +void revert(const ParsedPayload& p) { + syslog(LOG_WARNING, "[ethernet] not implemented on Android yet, uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::ethernet diff --git a/src/platform/android/Firewall.cpp b/src/platform/android/Firewall.cpp new file mode 100644 index 0000000..fd965f4 --- /dev/null +++ b/src/platform/android/Firewall.cpp @@ -0,0 +1,20 @@ +#include "../Firewall.h" + +#include + +// Not yet implemented on Android. There's no nftables/nft CLI on stock +// AOSP — a real port would need NetworkPolicyManager rules or a local +// VpnService-based firewall, a fundamentally different mechanism. + +namespace vesperos::profile::platform::firewall { + +bool apply(const ParsedPayload& p) { + syslog(LOG_WARNING, "[firewall] not implemented on Android yet, uuid=%s", p.uuid.c_str()); + return false; +} + +void revert(const ParsedPayload& p) { + syslog(LOG_WARNING, "[firewall] not implemented on Android yet, uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::firewall diff --git a/src/platform/android/FirstBoot.cpp b/src/platform/android/FirstBoot.cpp new file mode 100644 index 0000000..084632e --- /dev/null +++ b/src/platform/android/FirstBoot.cpp @@ -0,0 +1,20 @@ +#include "../FirstBoot.h" + +#include + +// Not yet implemented on Android. A real port would suppress AOSP's +// SetupWizard (Settings.Global DEVICE_PROVISIONED / USER_SETUP_COMPLETE) — +// no cloud-init/GNOME Initial Setup equivalent here. + +namespace vesperos::profile::platform::first_boot { + +bool apply(const ParsedPayload& p) { + syslog(LOG_WARNING, "[first-boot] not implemented on Android yet, uuid=%s", p.uuid.c_str()); + return false; +} + +void revert(const ParsedPayload& p) { + syslog(LOG_WARNING, "[first-boot] not implemented on Android yet, uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::first_boot diff --git a/src/platform/android/Ldap.cpp b/src/platform/android/Ldap.cpp new file mode 100644 index 0000000..e3fe3c5 --- /dev/null +++ b/src/platform/android/Ldap.cpp @@ -0,0 +1,21 @@ +#include "../Ldap.h" + +#include + +// Not yet implemented on Android — and likely never will be as a direct +// port. Android has no system LDAP client at all (no nslcd/libnss-ldapd +// equivalent); this payload type doesn't map onto anything the platform +// exposes. Logged for visibility rather than silently accepted. + +namespace vesperos::profile::platform::ldap { + +bool apply(const ParsedPayload& p) { + syslog(LOG_WARNING, "[ldap] not supported on Android (no system LDAP client), uuid=%s", p.uuid.c_str()); + return false; +} + +void revert(const ParsedPayload& p) { + syslog(LOG_WARNING, "[ldap] not supported on Android (no system LDAP client), uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::ldap diff --git a/src/platform/android/Mdm.cpp b/src/platform/android/Mdm.cpp new file mode 100644 index 0000000..116bda2 --- /dev/null +++ b/src/platform/android/Mdm.cpp @@ -0,0 +1,20 @@ +#include "../Mdm.h" + +#include + +// Not yet implemented on Android. A real port needs Android's own +// device-owner/EMM provisioning flow (DevicePolicyManager), a fundamentally +// different enrollment model than cloud-init drop-ins. + +namespace vesperos::profile::platform::mdm { + +bool apply(const ParsedPayload& p) { + syslog(LOG_WARNING, "[mdm] not implemented on Android yet, uuid=%s", p.uuid.c_str()); + return false; +} + +void revert(const ParsedPayload& p) { + syslog(LOG_WARNING, "[mdm] not implemented on Android yet, uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::mdm diff --git a/src/platform/android/Passcode.cpp b/src/platform/android/Passcode.cpp new file mode 100644 index 0000000..7c5bdcc --- /dev/null +++ b/src/platform/android/Passcode.cpp @@ -0,0 +1,20 @@ +#include "../Passcode.h" + +#include + +// Not yet implemented on Android. A real port needs +// DevicePolicyManager.setPasswordQuality()/setPasswordMinimumLength() etc +// (device-owner) — no pam_pwquality-equivalent config file to write. + +namespace vesperos::profile::platform::passcode { + +bool apply(const ParsedPayload& p) { + syslog(LOG_WARNING, "[passcode] not implemented on Android yet, uuid=%s", p.uuid.c_str()); + return false; +} + +void revert(const ParsedPayload& p) { + syslog(LOG_WARNING, "[passcode] not implemented on Android yet, uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::passcode diff --git a/src/platform/android/Pkcs12.cpp b/src/platform/android/Pkcs12.cpp new file mode 100644 index 0000000..3f3190e --- /dev/null +++ b/src/platform/android/Pkcs12.cpp @@ -0,0 +1,20 @@ +#include "../Pkcs12.h" + +#include + +// Not yet implemented on Android. A real port needs +// DevicePolicyManager.installKeyPair() (device-owner) — no system NSSDB +// equivalent a daemon can write a .p12 into directly. + +namespace vesperos::profile::platform::pkcs12 { + +bool apply(const ParsedPayload& p) { + syslog(LOG_WARNING, "[pkcs12] not implemented on Android yet, uuid=%s", p.uuid.c_str()); + return false; +} + +void revert(const ParsedPayload& p) { + syslog(LOG_WARNING, "[pkcs12] not implemented on Android yet, uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::pkcs12 diff --git a/src/platform/android/Proxy.cpp b/src/platform/android/Proxy.cpp new file mode 100644 index 0000000..c1cafe5 --- /dev/null +++ b/src/platform/android/Proxy.cpp @@ -0,0 +1,20 @@ +#include "../Proxy.h" + +#include + +// Not yet implemented on Android. A real port would use +// DevicePolicyManager.setRecommendedGlobalProxy()/ProxyInfo (device-owner) — +// no /etc/environment-equivalent to write. + +namespace vesperos::profile::platform::proxy { + +bool apply(const ParsedPayload& p) { + syslog(LOG_WARNING, "[proxy] not implemented on Android yet, uuid=%s", p.uuid.c_str()); + return false; +} + +void revert(const ParsedPayload& p) { + syslog(LOG_WARNING, "[proxy] not implemented on Android yet, uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::proxy diff --git a/src/platform/android/Screensaver.cpp b/src/platform/android/Screensaver.cpp new file mode 100644 index 0000000..02932f7 --- /dev/null +++ b/src/platform/android/Screensaver.cpp @@ -0,0 +1,21 @@ +#include "../Screensaver.h" + +#include + +// Not yet implemented on Android. A real port would map to Daydream +// (Settings.Secure SCREENSAVER_ENABLED / SCREENSAVER_COMPONENTS) plus +// Settings.System SCREEN_OFF_TIMEOUT for the idle delay — no dconf profile +// to write. + +namespace vesperos::profile::platform::screensaver { + +bool apply(const ParsedPayload& p) { + syslog(LOG_WARNING, "[screensaver] not implemented on Android yet, uuid=%s", p.uuid.c_str()); + return false; +} + +void revert(const ParsedPayload& p) { + syslog(LOG_WARNING, "[screensaver] not implemented on Android yet, uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::screensaver diff --git a/src/platform/android/SoftwareUpdate.cpp b/src/platform/android/SoftwareUpdate.cpp new file mode 100644 index 0000000..b83ad03 --- /dev/null +++ b/src/platform/android/SoftwareUpdate.cpp @@ -0,0 +1,22 @@ +#include "../SoftwareUpdate.h" + +#include + +// Not yet implemented on Android. PawletOS already has a dedicated update +// policy path — BgUpd's install_mode ("silent"/"manual" per component, see +// android_packages_apps_BgUpd) — a real port would translate this payload's +// automatic/deferral fields into BgUpd's manifest/policy rather than apt +// settings, which don't apply here at all. + +namespace vesperos::profile::platform::software_update { + +bool apply(const ParsedPayload& p) { + syslog(LOG_WARNING, "[software-update] not implemented on Android yet, uuid=%s", p.uuid.c_str()); + return false; +} + +void revert(const ParsedPayload& p) { + syslog(LOG_WARNING, "[software-update] not implemented on Android yet, uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::software_update diff --git a/src/platform/android/TimeServer.cpp b/src/platform/android/TimeServer.cpp new file mode 100644 index 0000000..6bae68a --- /dev/null +++ b/src/platform/android/TimeServer.cpp @@ -0,0 +1,20 @@ +#include "../TimeServer.h" + +#include + +// Not yet implemented on Android. A real port would write +// Settings.Global.NTP_SERVER (requires WRITE_SECURE_SETTINGS, same +// permission pattern BgUpd already holds for its own settings writes). + +namespace vesperos::profile::platform::time_server { + +bool apply(const ParsedPayload& p) { + syslog(LOG_WARNING, "[time-server] not implemented on Android yet, uuid=%s", p.uuid.c_str()); + return false; +} + +void revert(const ParsedPayload& p) { + syslog(LOG_WARNING, "[time-server] not implemented on Android yet, uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::time_server diff --git a/src/platform/android/Vpn.cpp b/src/platform/android/Vpn.cpp new file mode 100644 index 0000000..92eddff --- /dev/null +++ b/src/platform/android/Vpn.cpp @@ -0,0 +1,21 @@ +#include "../Vpn.h" + +#include + +// Not yet implemented on Android. A real port needs an android.net.VpnService +// component (IKEv2/L2TP have no NetworkManager-plugin equivalent on Android; +// this would mean shipping a companion VPN app or a system VpnService, not +// just writing a config file). + +namespace vesperos::profile::platform::vpn { + +bool apply(const ParsedPayload& p) { + syslog(LOG_WARNING, "[vpn] not implemented on Android yet, uuid=%s", p.uuid.c_str()); + return false; +} + +void revert(const ParsedPayload& p) { + syslog(LOG_WARNING, "[vpn] not implemented on Android yet, uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::vpn diff --git a/src/platform/android/Wallpaper.cpp b/src/platform/android/Wallpaper.cpp new file mode 100644 index 0000000..f2439f6 --- /dev/null +++ b/src/platform/android/Wallpaper.cpp @@ -0,0 +1,20 @@ +#include "../Wallpaper.h" + +#include + +// Not yet implemented on Android. A real port would use +// android.app.WallpaperManager (setStream/setResource) — no dconf profile +// to write. + +namespace vesperos::profile::platform::wallpaper { + +bool apply(const ParsedPayload& p) { + syslog(LOG_WARNING, "[wallpaper] not implemented on Android yet, uuid=%s", p.uuid.c_str()); + return false; +} + +void revert(const ParsedPayload& p) { + syslog(LOG_WARNING, "[wallpaper] not implemented on Android yet, uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::wallpaper diff --git a/src/platform/android/Wifi.cpp b/src/platform/android/Wifi.cpp new file mode 100644 index 0000000..a8e06d1 --- /dev/null +++ b/src/platform/android/Wifi.cpp @@ -0,0 +1,20 @@ +#include "../Wifi.h" + +#include + +// Not yet implemented on Android. A real port needs WifiManager / +// WifiNetworkSuggestion (API 29+) or a privileged wpa_supplicant config +// write — NetworkManager keyfiles (the Linux implementation) don't apply. + +namespace vesperos::profile::platform::wifi { + +bool apply(const ParsedPayload& p) { + syslog(LOG_WARNING, "[wifi] not implemented on Android yet, uuid=%s", p.uuid.c_str()); + return false; +} + +void revert(const ParsedPayload& p) { + syslog(LOG_WARNING, "[wifi] not implemented on Android yet, uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::wifi diff --git a/src/platform/linux/Cert.cpp b/src/platform/linux/Cert.cpp new file mode 100644 index 0000000..7e3116a --- /dev/null +++ b/src/platform/linux/Cert.cpp @@ -0,0 +1,48 @@ +#include "../Cert.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// CERTIFICATE HANDLER (Linux) +// Root/intermediate CAs → /usr/local/share/ca-certificates/vesperos/ +// + update-ca-certificates +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::cert { + +using namespace vesperos::profile::util; + +namespace { +constexpr const char* kCaDir = "/usr/local/share/ca-certificates/vesperos"; +} + +bool apply(const ParsedPayload& p) { + std::string b64 = field(p, "data"); + if (b64.empty()) { syslog(LOG_ERR, "[cert] no data"); return false; } + + auto bytes = b64decode(b64); + if (bytes.empty()) { syslog(LOG_ERR, "[cert] bad base64"); return false; } + + ensureDir(kCaDir); + std::string certPath = std::string(kCaDir) + "/" + p.uuid + ".crt"; + + { + std::ofstream f(certPath, std::ios::binary); + f.write(reinterpret_cast(bytes.data()), bytes.size()); + } + ::chmod(certPath.c_str(), 0644); + + bool ok = runCmd("update-ca-certificates --fresh 2>/dev/null"); + syslog(LOG_INFO, "[cert] installed CA cert uuid=%s", p.uuid.c_str()); + return ok; +} + +void revert(const ParsedPayload& p) { + removeFile(std::string(kCaDir) + "/" + p.uuid + ".crt"); + runCmd("update-ca-certificates --fresh 2>/dev/null"); +} + +} // namespace vesperos::profile::platform::cert diff --git a/src/platform/linux/ContentCache.cpp b/src/platform/linux/ContentCache.cpp new file mode 100644 index 0000000..69be75f --- /dev/null +++ b/src/platform/linux/ContentCache.cpp @@ -0,0 +1,51 @@ +#include "../ContentCache.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// CONTENT CACHE HANDLER (Linux) +// PawletOS-fork-specific — vesperprofiled's default VesperOS build doesn't +// register this handler at all (see PayloadHandlers.cpp). Writes a +// placeholder path for a possible future VesperOS-side cache client — see +// pawletcache-server/README.md. The Android implementation +// (platform/android/ContentCache.cpp) is the one that actually matters on +// PawletOS devices; PawletCacheService reads that one. +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::content_cache { + +using namespace vesperos::profile::util; + +namespace { +constexpr const char* kPolicyDir = "/etc/pawletcache"; +constexpr const char* kPolicyFile = "/etc/pawletcache/policy-override.json"; +} + +bool apply(const ParsedPayload& p) { + std::string mode = field(p, "mode", "both"); + + std::ostringstream json; + json << "{\n \"mode\": \"" << mode << "\""; + + auto pinned = p.fields.find("pinned-server"); + if (pinned != p.fields.end()) { + // Already valid JSON text (ProfileParser re-serializes nested + // objects) — relay it verbatim under the camelCase key the + // device-side reader expects, no reparsing needed here. + json << ",\n \"pinnedServer\": " << pinned->second; + } + json << "\n}\n"; + + ensureDir(kPolicyDir); + writeFile(kPolicyFile, json.str(), 0644); + syslog(LOG_INFO, "[content-cache] applied mode=%s", mode.c_str()); + return true; +} + +void revert(const ParsedPayload& /*p*/) { + removeFile(kPolicyFile); +} + +} // namespace vesperos::profile::platform::content_cache diff --git a/src/platform/linux/DnsProxy.cpp b/src/platform/linux/DnsProxy.cpp new file mode 100644 index 0000000..367e9d4 --- /dev/null +++ b/src/platform/linux/DnsProxy.cpp @@ -0,0 +1,53 @@ +#include "../DnsProxy.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// DNS PROXY HANDLER (Linux) +// Writes /etc/systemd/resolved.conf.d/vesperos-dns.conf +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::dns_proxy { + +using namespace vesperos::profile::util; + +namespace { +constexpr const char* kResolvedDir = "/etc/systemd/resolved.conf.d"; +constexpr const char* kResolvedConf = "/etc/systemd/resolved.conf.d/vesperos-dns.conf"; +} + +bool apply(const ParsedPayload& p) { + auto configJson = p.fields.find("config"); + if (configJson == p.fields.end()) return true; + + auto pos = configJson->second.find("\"ServerURL\":"); + std::string dnsUrl; + if (pos != std::string::npos) { + pos += 12; + auto q1 = configJson->second.find('"', pos); + auto q2 = configJson->second.find('"', q1 + 1); + if (q1 != std::string::npos) + dnsUrl = configJson->second.substr(q1 + 1, q2 - q1 - 1); + } + + ensureDir(kResolvedDir); + std::ostringstream c; + c << "[Resolve]\n"; + if (!dnsUrl.empty()) c << "DNS=" << dnsUrl << "\n"; + c << "DNSStubListener=yes\n" + << "DNSSEC=allow-downgrade\n" + << "DNSOverTLS=opportunistic\n"; + writeFile(kResolvedConf, c.str()); + runCmd("systemctl restart systemd-resolved 2>/dev/null"); + syslog(LOG_INFO, "[dns-proxy] applied DNS config"); + return true; +} + +void revert(const ParsedPayload& /*p*/) { + removeFile(kResolvedConf); + runCmd("systemctl restart systemd-resolved 2>/dev/null"); +} + +} // namespace vesperos::profile::platform::dns_proxy diff --git a/src/platform/linux/Ethernet.cpp b/src/platform/linux/Ethernet.cpp new file mode 100644 index 0000000..08cf8c7 --- /dev/null +++ b/src/platform/linux/Ethernet.cpp @@ -0,0 +1,106 @@ +#include "../Ethernet.h" +#include "NetworkManagerUtil.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// ETHERNET HANDLER (Linux) +// 802.1X wired authentication via NetworkManager keyfile. +// Same approach as Wifi — direct keyfile write, no nmcli exec. +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::ethernet { + +using namespace vesperos::profile::util; +using namespace vesperos::profile::platform::nm; + +namespace { +std::string keyfilePath(const ParsedPayload& p) { + return std::string(kNMConnDir) + "/vesperos-ethernet-" + p.uuid + ".nmconnection"; +} +} // namespace + +bool apply(const ParsedPayload& p) { + std::string iface = field(p, "interface", ""); + // Translate VesperOS interface names to Linux names. + // Leave blank to let NM pick the first available wired interface. + if (iface == "first-active" || iface == "first" || iface == "first-ethernet") + iface = ""; + + // Parse EAP block + std::string eapJson = field(p, "eap"); + std::string eapMethod = "peap"; + if (!eapJson.empty()) { + if (eapJson.find("\"tls\"") != std::string::npos) eapMethod = "tls"; + else if (eapJson.find("\"ttls\"") != std::string::npos) eapMethod = "ttls"; + else if (eapJson.find("\"fast\"") != std::string::npos) eapMethod = "fast"; + } + + std::string eapUser = extractJson(eapJson, "username"); + std::string eapPass = extractJson(eapJson, "password"); + std::string eapOuter = extractJson(eapJson, "outer-identity"); + std::string ttlsInner = extractJson(eapJson, "ttls-inner-auth"); + + // trust sub-block + std::string serverName, caCertPath; + auto trustPos = eapJson.find("\"trust\":"); + if (trustPos != std::string::npos) { + serverName = extractJson(eapJson.substr(trustPos), "server-names"); + std::string anchorUuid = extractJson(eapJson.substr(trustPos), "anchor-cert-uuids"); + if (!anchorUuid.empty()) + caCertPath = "/usr/local/share/ca-certificates/vesperos/" + + anchorUuid + ".crt"; + } + + // ── Build the keyfile ────────────────────────────────────────────── + std::ostringstream kf; + + kf << "[connection]\n" + << "id=vesperos-ethernet-" << p.uuid << "\n" + << "uuid=" << p.uuid << "\n" + << "type=ethernet\n" + << "autoconnect=true\n"; + if (!iface.empty()) + kf << "interface-name=" << iface << "\n"; + kf << "\n"; + + kf << "[ethernet]\n\n"; + + // [802-1x] + bool hasEap = !eapJson.empty() && eapJson != "\"\""; + if (hasEap) { + kf << "[802-1x]\n" + << "eap=" << eapMethod << "\n"; + if (!eapUser.empty()) kf << "identity=" << eapUser << "\n"; + if (!eapPass.empty()) kf << "password=" << eapPass << "\n"; + if (!eapOuter.empty()) kf << "anonymous-identity=" << eapOuter << "\n"; + if (eapMethod == "ttls" || eapMethod == "peap") + kf << "phase2-auth=" << (ttlsInner.empty() ? "mschapv2" : ttlsInner) << "\n"; + if (!serverName.empty()) + kf << "altsubject-matches=" << serverName << "\n"; + if (!caCertPath.empty()) + kf << "ca-cert=" << caCertPath << "\n"; + kf << "\n"; + } + + kf << "[ipv4]\nmethod=auto\n\n" + << "[ipv6]\nmethod=auto\naddr-gen-mode=stable-privacy\n"; + + ensureDir(kNMConnDir); + writeFile(keyfilePath(p), kf.str(), 0600); + nmReload(); + + syslog(LOG_INFO, "[ethernet] wrote keyfile iface=%s uuid=%s", + iface.empty() ? "*" : iface.c_str(), p.uuid.c_str()); + return true; +} + +void revert(const ParsedPayload& p) { + removeFile(keyfilePath(p)); + nmReload(); + syslog(LOG_INFO, "[ethernet] removed keyfile uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::ethernet diff --git a/src/platform/linux/Firewall.cpp b/src/platform/linux/Firewall.cpp new file mode 100644 index 0000000..af39f7f --- /dev/null +++ b/src/platform/linux/Firewall.cpp @@ -0,0 +1,67 @@ +#include "../Firewall.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// FIREWALL HANDLER (Linux) +// Writes nftables rules to /etc/nftables.d/vesperos.nft +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::firewall { + +using namespace vesperos::profile::util; + +namespace { +constexpr const char* kNftFile = "/etc/nftables.d/vesperos.nft"; +} + +bool apply(const ParsedPayload& p) { + bool enabled = fieldBool(p, "enabled", false); + bool blockAll = fieldBool(p, "block-all-incoming", false); + bool stealthMode = fieldBool(p, "stealth-mode", false); + + if (!enabled) return true; + + ensureDir("/etc/nftables.d"); + std::ostringstream nft; + nft << "# VesperOS managed firewall — do not edit\n" + << "table inet vesperos_fw {\n" + << " chain input {\n" + << " type filter hook input priority 0; policy " + << (blockAll ? "drop" : "accept") << ";\n"; + + if (blockAll) { + // Allow established/related + nft << " ct state established,related accept\n" + << " iif lo accept\n"; + // Allow ICMP unless stealth mode + if (!stealthMode) nft << " ip protocol icmp accept\n"; + } + + // Per-app rules: on Linux, app-level filtering is handled by + // AppArmor/seccomp rather than nftables. Skip here. + + nft << " }\n" + << " chain forward {\n" + << " type filter hook forward priority 0; policy accept;\n" + << " }\n" + << " chain output {\n" + << " type filter hook output priority 0; policy accept;\n" + << " }\n" + << "}\n"; + + writeFile(kNftFile, nft.str()); + runCmd("nft -f " + std::string(kNftFile) + " 2>/dev/null"); + runCmd("systemctl enable nftables 2>/dev/null"); + syslog(LOG_INFO, "[firewall] applied nftables rules"); + return true; +} + +void revert(const ParsedPayload& /*p*/) { + runCmd("nft delete table inet vesperos_fw 2>/dev/null"); + removeFile(kNftFile); +} + +} // namespace vesperos::profile::platform::firewall diff --git a/src/platform/linux/FirstBoot.cpp b/src/platform/linux/FirstBoot.cpp new file mode 100644 index 0000000..041ec8b --- /dev/null +++ b/src/platform/linux/FirstBoot.cpp @@ -0,0 +1,166 @@ +#include "../FirstBoot.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include +#include +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// FIRST-BOOT HANDLER (Linux) (renamed from setup-assistant) +// +// This handler runs BEFORE cloud-init on first boot. The systemd ordering +// in vesperprofiled.service (Before=cloud-init-local.service) ensures that +// when a preinstalled profile baked into the image contains a first-boot +// payload, vesperprofiled applies it first so that: +// +// 1. The cloud-init drop-in is already on disk when cloud-init starts. +// 2. Any WiFi or VPN profile is already written to NM so cloud-init can +// reach its datasource over the managed connection. +// 3. The MDM drop-in is in /etc/cloud/cloud.cfg.d/ before cloud-init +// runs, letting the MDM server inject additional cloud-init config. +// +// What it does: +// - Writes /etc/cloud/cloud.cfg.d/99-vesperos-firstboot.cfg to suppress +// cloud-init wizard modules that are replaced by managed profiles. +// - Writes cloud-init's 'disabled' modules list so it skips anything +// explicitly listed in the profile's skip array. +// - Suppresses the VesperOS first-boot GNOME Initial Setup wizard +// via a GSettings override and a .done semaphore file. +// - Writes a /etc/vesperprofiled/firstboot.done stamp so subsequent +// daemon starts (and cloud-init re-runs) know first-boot is complete. +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::first_boot { + +using namespace vesperos::profile::util; + +namespace { + +// cloud-init drop-in — written before cloud-init starts +constexpr const char* kCloudInitDrop = "/etc/cloud/cloud.cfg.d/99-vesperos-firstboot.cfg"; +// GNOME Initial Setup bypass +constexpr const char* kGnomeSetupDone = "/var/lib/gnome-initial-setup/.gnome-initial-setup-done"; +// dconf override disabling GNOME Initial Setup on all accounts +constexpr const char* kGnomeDconfOverride = "/etc/dconf/db/local.d/00-vesperos-firstboot"; +// Semaphore: written last, after all other drops are in place +constexpr const char* kDoneStamp = "/etc/vesperprofiled/firstboot.done"; + +// Map of VesperOS skip-pane names to cloud-init module names to disable. +// cloud-init reads cloud_final_modules / cloud_config_modules; we inject +// a disabled_modules list which cloud-init (>=22.1) honours directly. +const std::pair kSkipMap[] = { + // VesperOS pane name → cloud-init module name + {"software-update", "package-update-upgrade-install"}, + {"location", "ntp"}, + {"privacy", "users-groups"}, + {"tos", "final-message"}, + {"passcode", "set-passwords"}, + {"restore", "chef"}, // no exact equiv; suppress restore agent + {"biometric", "mcollective"}, // no exact equiv + {"siri", "puppet"}, // no exact equiv + {"android-migration", "migrator"}, + {"onboarding", "write-files"}, +}; +constexpr size_t kSkipMapLen = 10; + +} // namespace + +bool apply(const ParsedPayload& p) { + // ── 1. Build the cloud-init drop-in ─────────────────────────────── + // Collect which panes to skip + std::string skipJson = field(p, "skip"); + std::vector disabledModules; + + for (size_t i = 0; i < kSkipMapLen; ++i) { + if (skipJson.find(kSkipMap[i].first) != std::string::npos) + disabledModules.push_back(kSkipMap[i].second); + } + + ensureDir("/etc/cloud/cloud.cfg.d"); + std::ostringstream ci; + ci << "# VesperOS first-boot configuration — managed by vesperprofiled\n" + << "# Applied before cloud-init runs. Do not edit manually.\n" + << "#\n" + << "# vesperprofiled has already written all managed network,\n" + << "# certificate, and policy configurations. cloud-init only\n" + << "# needs to handle datasource-specific work.\n\n"; + + // Suppress the interactive first-boot wizard entirely + ci << "# Suppress interactive setup\n" + << "resize_rootfs: false\n" + << "ssh_pwauth: false\n" + << "final_message: \"\"\n\n"; + + // Don't let cloud-init clobber NM-managed network config + ci << "# Preserve vesperprofiled-managed network settings\n" + << "network:\n" + << " config: disabled\n\n"; + + // Disable specific cloud-init modules that correspond to skipped panes + if (!disabledModules.empty()) { + ci << "# Modules disabled by first-boot skip list\n" + << "disabled_modules:\n"; + for (const auto& m : disabledModules) + ci << " - " << m << "\n"; + ci << "\n"; + } + + // If software-update is in the skip list, also suppress apt operations + if (skipJson.find("software-update") != std::string::npos) { + ci << "# Software update skipped — apt managed by software-update payload\n" + << "package_update: false\n" + << "package_upgrade: false\n"; + } + + writeFile(kCloudInitDrop, ci.str(), 0644); + syslog(LOG_INFO, "[first-boot] wrote cloud-init drop-in"); + + // ── 2. Suppress GNOME Initial Setup ─────────────────────────────── + // Create the .done stamp so gnome-initial-setup skips on first login. + ensureDir("/var/lib/gnome-initial-setup"); + { + // Touch the file + std::ofstream f(kGnomeSetupDone); + // content doesn't matter; existence is the signal + } + ::chmod(kGnomeSetupDone, 0644); + + // dconf system default: tell gnome-initial-setup it has already run + ensureDir("/etc/dconf/db/local.d"); + writeFile(kGnomeDconfOverride, + "# VesperOS managed — skip GNOME Initial Setup\n" + "[org/gnome/initial-setup]\n" + "run-on-guest=false\n", + 0644); + runCmd("dconf update 2>/dev/null"); + + syslog(LOG_INFO, "[first-boot] suppressed GNOME Initial Setup"); + + // ── 3. Write the done stamp last ────────────────────────────────── + // This is the signal to main.cpp that first-boot profiles have been + // fully applied. The stamp is checked on subsequent starts so we + // don't re-apply first-boot payloads on every boot. + ensureDir("/etc/vesperprofiled"); + { + std::ofstream f(kDoneStamp); + // Write the profile UUID so we know which profile ran + f << p.uuid << "\n"; + } + ::chmod(kDoneStamp, 0644); + + syslog(LOG_INFO, "[first-boot] first-boot complete, stamp written"); + return true; +} + +void revert(const ParsedPayload& p) { + removeFile(kCloudInitDrop); + removeFile(kGnomeDconfOverride); + removeFile("/etc/dconf/db/local.d/locks/vesperos-firstboot"); + removeFile(kDoneStamp); + runCmd("dconf update 2>/dev/null"); + syslog(LOG_INFO, "[first-boot] reverted first-boot config uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::first_boot diff --git a/src/platform/linux/Ldap.cpp b/src/platform/linux/Ldap.cpp new file mode 100644 index 0000000..bd5bf7c --- /dev/null +++ b/src/platform/linux/Ldap.cpp @@ -0,0 +1,75 @@ +#include "../Ldap.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// LDAP HANDLER (Linux) +// Writes /etc/ldap/ldap.conf for LDAP client configuration. +// Also configures libpam-ldapd / nss-ldapd if installed. +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::ldap { + +using namespace vesperos::profile::util; + +namespace { +constexpr const char* kLdapConf = "/etc/ldap/ldap.conf"; +constexpr const char* kNslcdConf = "/etc/nslcd.conf"; +} + +bool apply(const ParsedPayload& p) { + std::string host = field(p, "host"); + bool ssl = fieldBool(p, "ssl", true); + std::string username = field(p, "username"); + std::string password = field(p, "password"); + + auto searchJson = p.fields.find("search-settings"); + std::string base; + if (searchJson != p.fields.end()) { + auto pos = searchJson->second.find("\"base\":"); + if (pos != std::string::npos) { + pos += 7; + auto q1 = searchJson->second.find('"', pos); + auto q2 = searchJson->second.find('"', q1 + 1); + if (q1 != std::string::npos) + base = searchJson->second.substr(q1 + 1, q2 - q1 - 1); + } + } + + std::string uri = (ssl ? "ldaps://" : "ldap://") + host; + + // /etc/ldap/ldap.conf + std::ostringstream lc; + lc << "# VesperOS managed\n" + << "URI " << uri << "\n"; + if (!base.empty()) lc << "BASE " << base << "\n"; + lc << "TLS_CACERT /etc/ssl/certs/ca-certificates.crt\n"; + writeFile(kLdapConf, lc.str(), 0644); + + // /etc/nslcd.conf (if nslcd / libnss-ldapd is installed) + std::ostringstream nc; + nc << "# VesperOS managed\n" + << "uid nslcd\ngid nslcd\n" + << "uri " << uri << "\n"; + if (!base.empty()) nc << "base " << base << "\n"; + if (!username.empty()) nc << "binddn " << username << "\n" + << "bindpw " << password << "\n"; + nc << "ssl " << (ssl ? "on" : "off") << "\n" + << "tls_cacertfile /etc/ssl/certs/ca-certificates.crt\n"; + writeFile(kNslcdConf, nc.str(), 0600); + + runCmd("systemctl try-restart nslcd 2>/dev/null"); + syslog(LOG_INFO, "[ldap] configured LDAP host=%s", host.c_str()); + return true; +} + +void revert(const ParsedPayload& /*p*/) { + // Restore defaults (empty ldap.conf) + writeFile(kLdapConf, "# No LDAP configured\n"); + removeFile(kNslcdConf); + runCmd("systemctl try-restart nslcd 2>/dev/null"); +} + +} // namespace vesperos::profile::platform::ldap diff --git a/src/platform/linux/Mdm.cpp b/src/platform/linux/Mdm.cpp new file mode 100644 index 0000000..1a912ca --- /dev/null +++ b/src/platform/linux/Mdm.cpp @@ -0,0 +1,77 @@ +#include "../Mdm.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// MDM HANDLER (Linux) +// Writes a cloud-init user-data drop-in so the MDM server URL and +// enrollment state survive reboots and cloud-init re-runs. +// Also writes a vesperos MDM config to /etc/vesperprofiled/mdm.conf +// which the MDM client agent (future VesperOS package) reads. +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::mdm { + +using namespace vesperos::profile::util; + +namespace { +constexpr const char* kMdmConf = "/etc/vesperprofiled/mdm.conf"; +constexpr const char* kCloudInitDrop = "/etc/cloud/cloud.cfg.d/99-vesperos-mdm.cfg"; +} + +bool apply(const ParsedPayload& p) { + std::string serverUrl = field(p, "server-url"); + std::string checkinUrl = field(p, "checkin-url"); + std::string pushTopic = field(p, "push-topic"); + std::string certUuid = field(p, "identity-cert-uuid"); + std::string accessStr = field(p, "access-rights", "0"); + int accessRights = std::stoi(accessStr); + + // ── /etc/vesperprofiled/mdm.conf ─────────────────────────────── + ensureDir("/etc/vesperprofiled"); + std::ostringstream conf; + conf << "# VesperOS MDM enrollment — managed by vesperprofiled\n" + << "server_url=" << serverUrl << "\n" + << "checkin_url=" << (checkinUrl.empty() ? serverUrl : checkinUrl) << "\n" + << "push_topic=" << pushTopic << "\n" + << "identity_cert_uuid=" << certUuid << "\n" + << "access_rights=" << accessRights << "\n" + << "enrolled=1\n"; + writeFile(kMdmConf, conf.str(), 0640); + + // ── cloud-init drop-in ───────────────────────────────────────── + // cloud-init YAML format — sets system metadata the MDM agent + // can query via `cloud-init query`. + ensureDir("/etc/cloud/cloud.cfg.d"); + std::ostringstream ci; + ci << "# VesperOS MDM — generated by vesperprofiled\n" + << "# Do not edit manually.\n" + << "system_info:\n" + << " default_user:\n" + << " lock_passwd: false\n" + << "\n" + << "# MDM server endpoints\n" + << "vesperos_mdm:\n" + << " server_url: " << serverUrl << "\n" + << " checkin_url: " << (checkinUrl.empty() ? serverUrl : checkinUrl) << "\n" + << " push_topic: " << pushTopic << "\n" + << " enrolled: true\n" + << "\n" + << "# Disable cloud-init from resetting MDM-managed settings\n" + << "manage_etc_hosts: false\n" + << "manage_resolv_conf: false\n"; + writeFile(kCloudInitDrop, ci.str(), 0644); + + syslog(LOG_INFO, "[mdm] enrolled MDM server=%s", serverUrl.c_str()); + return true; +} + +void revert(const ParsedPayload& p) { + removeFile(kMdmConf); + removeFile(kCloudInitDrop); + syslog(LOG_INFO, "[mdm] unenrolled MDM uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::mdm diff --git a/src/platform/linux/NetworkManagerUtil.h b/src/platform/linux/NetworkManagerUtil.h new file mode 100644 index 0000000..7193aa3 --- /dev/null +++ b/src/platform/linux/NetworkManagerUtil.h @@ -0,0 +1,29 @@ +#pragma once + +// Shared by platform/linux/Wifi.cpp and platform/linux/Ethernet.cpp — both +// write NetworkManager keyfiles and need to trigger the same reload. Linux +// (NetworkManager) specific, hence living under platform/linux/ rather than +// the shared payloads/PayloadUtil.h. + +#include "../../payloads/PayloadUtil.h" + +#include + +namespace vesperos::profile::platform::nm { + +inline constexpr const char* kNMConnDir = "/etc/NetworkManager/system-connections"; + +// Reload NM connections without spawning nmcli — send SIGHUP-equivalent via +// D-Bus so NM re-reads its keyfiles without needing the daemon binary. +// If NM is not running yet (early first-boot), this is a no-op; NM picks up +// the keyfiles on its own startup. +inline void nmReload() { + vesperos::profile::util::runCmd( + "dbus-send --system --print-reply " + "--dest=org.freedesktop.NetworkManager " + "/org/freedesktop/NetworkManager " + "org.freedesktop.NetworkManager.ReloadConnections " + "2>/dev/null || true"); +} + +} // namespace vesperos::profile::platform::nm diff --git a/src/platform/linux/Passcode.cpp b/src/platform/linux/Passcode.cpp new file mode 100644 index 0000000..ac7fe2d --- /dev/null +++ b/src/platform/linux/Passcode.cpp @@ -0,0 +1,78 @@ +#include "../Passcode.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// PASSCODE POLICY HANDLER (Linux) +// Writes /etc/security/pwquality.conf.d/vesperos.conf +// and sets /etc/login.defs values for max age, min age, etc. +// Also writes a PAM faillock config for lockout policy. +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::passcode { + +using namespace vesperos::profile::util; + +namespace { +constexpr const char* kPwqualityDir = "/etc/security/pwquality.conf.d"; +constexpr const char* kFaillockConf = "/etc/security/faillock.conf.d/vesperos.conf"; +} + +bool apply(const ParsedPayload& p) { + int minLen = std::stoi(field(p, "min-length", "6")); + int minComplex = std::stoi(field(p, "min-complex-chars", "0")); + bool reqAlpha = fieldBool(p, "require-alphanumeric", false); + bool allowSimp = fieldBool(p, "allow-simple", true); + int maxAge = std::stoi(field(p, "max-age-days", "0")); + int history = std::stoi(field(p, "history", "0")); + int maxFailed = std::stoi(field(p, "max-failed-attempts", "0")); + int inactivity = std::stoi(field(p, "inactivity-minutes", "0")); + + // ── pam_pwquality ────────────────────────────────────────────── + ensureDir(kPwqualityDir); + std::ostringstream pq; + pq << "# Managed by vesperprofiled — do not edit manually\n"; + pq << "minlen = " << minLen << "\n"; + if (minComplex > 0) pq << "minclass = " << minComplex << "\n"; + if (reqAlpha) pq << "dcredit = -1\n" << "ucredit = -1\n"; + if (!allowSimp) pq << "maxrepeat = 2\nmaxsequence = 2\n"; + if (history > 0) pq << "# remember=" << history + << " set in /etc/pam.d/common-password\n"; + writeFile(std::string(kPwqualityDir) + "/vesperos.conf", pq.str()); + + // ── /etc/login.defs overrides via conf.d drop-in ─────────────── + // Debian reads /etc/login.defs directly; we append a comment-marked + // block. A real implementation patches login.defs via sed or a + // dedicated management file; here we write a separate snippet + // that a PAM module or login wrapper can source. + std::ostringstream ld; + ld << "# vesperprofiled managed\n"; + if (maxAge > 0) ld << "PASS_MAX_DAYS\t" << maxAge << "\n"; + if (history > 0) ld << "# PASS_REUSE_LIMIT=" << history << "\n"; + writeFile("/etc/vesperprofiled/login.defs.snippet", ld.str()); + + // ── pam_faillock ─────────────────────────────────────────────── + if (maxFailed > 0) { + ensureDir("/etc/security/faillock.conf.d"); + std::ostringstream fl; + fl << "# Managed by vesperprofiled\n" + << "deny = " << maxFailed << "\n"; + if (inactivity > 0) + fl << "unlock_time = " << (inactivity * 60) << "\n"; + writeFile(kFaillockConf, fl.str()); + } + + syslog(LOG_INFO, "[passcode] applied password policy"); + return true; +} + +void revert(const ParsedPayload& /*p*/) { + removeFile(std::string(kPwqualityDir) + "/vesperos.conf"); + removeFile(kFaillockConf); + removeFile("/etc/vesperprofiled/login.defs.snippet"); + syslog(LOG_INFO, "[passcode] reverted password policy"); +} + +} // namespace vesperos::profile::platform::passcode diff --git a/src/platform/linux/Pkcs12.cpp b/src/platform/linux/Pkcs12.cpp new file mode 100644 index 0000000..6b8baf1 --- /dev/null +++ b/src/platform/linux/Pkcs12.cpp @@ -0,0 +1,53 @@ +#include "../Pkcs12.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// PKCS#12 HANDLER (Linux) +// Identity cert + key → /etc/ssl/private/vesperos/ +// Also imports into the system NSS DB (shared NSSDB at /etc/pki/nssdb) +// via certutil/pk12util if available. +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::pkcs12 { + +using namespace vesperos::profile::util; + +namespace { +constexpr const char* kKeyDir = "/etc/ssl/private/vesperos"; +} + +bool apply(const ParsedPayload& p) { + std::string b64 = field(p, "data"); + std::string pw = field(p, "password"); + if (b64.empty()) { syslog(LOG_ERR, "[pkcs12] no data"); return false; } + + auto bytes = b64decode(b64); + if (bytes.empty()) return false; + + ensureDir(kKeyDir, 0700); + std::string p12Path = std::string(kKeyDir) + "/" + p.uuid + ".p12"; + { + std::ofstream f(p12Path, std::ios::binary); + f.write(reinterpret_cast(bytes.data()), bytes.size()); + } + ::chmod(p12Path.c_str(), 0600); + + // Import into shared NSSDB if pk12util is available + runCmd("which pk12util >/dev/null 2>&1 && " + "pk12util -i " + p12Path + " -d /etc/pki/nssdb" + " -W '" + pw + "' 2>/dev/null"); + + syslog(LOG_INFO, "[pkcs12] installed identity cert uuid=%s", p.uuid.c_str()); + return true; +} + +void revert(const ParsedPayload& p) { + removeFile(std::string(kKeyDir) + "/" + p.uuid + ".p12"); + // Remove from NSSDB — we'd need to track the cert nickname; skip for now. +} + +} // namespace vesperos::profile::platform::pkcs12 diff --git a/src/platform/linux/Proxy.cpp b/src/platform/linux/Proxy.cpp new file mode 100644 index 0000000..ee3fe58 --- /dev/null +++ b/src/platform/linux/Proxy.cpp @@ -0,0 +1,98 @@ +#include "../Proxy.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// GLOBAL HTTP PROXY HANDLER (Linux) +// Writes /etc/environment proxy vars and +// /etc/apt/apt.conf.d/99-vesperos-proxy +// /etc/profile.d/vesperos-proxy.sh +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::proxy { + +using namespace vesperos::profile::util; + +namespace { +constexpr const char* kAptProxy = "/etc/apt/apt.conf.d/99-vesperos-proxy"; +constexpr const char* kProfileProxy = "/etc/profile.d/vesperos-proxy.sh"; +} + +bool apply(const ParsedPayload& p) { + // proxy block stored as JSON in fields["proxy"] + auto proxyJson = p.fields.find("proxy"); + if (proxyJson == p.fields.end()) return true; + + bool isManual = proxyJson->second.find("\"type\":\"manual\"") != std::string::npos + || proxyJson->second.find("manual") != std::string::npos; + + auto extract = [&](const std::string& key) -> std::string { + auto pos = proxyJson->second.find("\"" + key + "\":"); + if (pos == std::string::npos) return ""; + pos += key.size() + 3; + auto q1 = proxyJson->second.find('"', pos); + auto q2 = proxyJson->second.find('"', q1 + 1); + if (q1 == std::string::npos) return ""; + return proxyJson->second.substr(q1 + 1, q2 - q1 - 1); + }; + + if (isManual) { + std::string host = extract("host"); + std::string port = extract("port"); + std::string user = extract("username"); + std::string pass = extract("password"); + + std::string auth = user.empty() ? "" : (user + ":" + pass + "@"); + std::string proxyUrl = "http://" + auth + host + ":" + port; + + // /etc/profile.d/vesperos-proxy.sh — sets env vars for all users + std::ostringstream sh; + sh << "# VesperOS managed proxy\n" + << "export http_proxy=" << proxyUrl << "\n" + << "export https_proxy=" << proxyUrl << "\n" + << "export HTTP_PROXY=" << proxyUrl << "\n" + << "export HTTPS_PROXY=" << proxyUrl << "\n" + << "export no_proxy=localhost,127.0.0.1,::1\n"; + writeFile(kProfileProxy, sh.str(), 0644); + + // apt proxy + std::ostringstream apt; + apt << "// VesperOS managed\n" + << "Acquire::http::Proxy \"" << proxyUrl << "\";\n" + << "Acquire::https::Proxy \"" << proxyUrl << "\";\n"; + writeFile(kAptProxy, apt.str()); + + // NetworkManager global proxy + std::ostringstream nmConf; + nmConf << "[connectivity]\n\n" + << "[global-dns-domain-*]\n\n" + << "[main]\n" + << "proxy=http\n" + << "proxy-url=" << proxyUrl << "\n"; + writeFile("/etc/NetworkManager/conf.d/vesperos-proxy.conf", nmConf.str()); + runCmd("nmcli general reload 2>/dev/null"); + } else { + // PAC auto-proxy — write to NM and GNOME + std::string pacUrl = extract("pac-url"); + if (!pacUrl.empty()) { + std::ostringstream nmConf; + nmConf << "[main]\nproxy=auto\nproxy-url=" << pacUrl << "\n"; + writeFile("/etc/NetworkManager/conf.d/vesperos-proxy.conf", nmConf.str()); + runCmd("nmcli general reload 2>/dev/null"); + } + } + + syslog(LOG_INFO, "[proxy] applied global proxy"); + return true; +} + +void revert(const ParsedPayload& /*p*/) { + removeFile(kAptProxy); + removeFile(kProfileProxy); + removeFile("/etc/NetworkManager/conf.d/vesperos-proxy.conf"); + runCmd("nmcli general reload 2>/dev/null"); +} + +} // namespace vesperos::profile::platform::proxy diff --git a/src/platform/linux/Screensaver.cpp b/src/platform/linux/Screensaver.cpp new file mode 100644 index 0000000..d39b687 --- /dev/null +++ b/src/platform/linux/Screensaver.cpp @@ -0,0 +1,51 @@ +#include "../Screensaver.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// SCREENSAVER HANDLER (Linux) +// Configures GNOME screensaver/lock via dconf system profile. +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::screensaver { + +using namespace vesperos::profile::util; + +namespace { +constexpr const char* kDconfFile = "/etc/dconf/db/local.d/00-vesperos-screensaver"; +} + +bool apply(const ParsedPayload& p) { + int idleSecs = std::stoi(field(p, "idle-seconds", "300")); + bool locked = fieldBool(p, "locked", false); + + ensureDir("/etc/dconf/db/local.d"); + std::ostringstream d; + d << "[org/gnome/desktop/session]\n" + << "idle-delay=uint32 " << idleSecs << "\n\n" + << "[org/gnome/desktop/screensaver]\n" + << "lock-enabled=true\n" + << "lock-delay=uint32 0\n"; + writeFile(kDconfFile, d.str()); + + if (locked) { + ensureDir("/etc/dconf/db/local.d/locks"); + writeFile("/etc/dconf/db/local.d/locks/vesperos-screensaver", + "/org/gnome/desktop/session/idle-delay\n" + "/org/gnome/desktop/screensaver/lock-enabled\n"); + } + + runCmd("dconf update 2>/dev/null"); + syslog(LOG_INFO, "[screensaver] idle=%ds locked=%d", idleSecs, locked); + return true; +} + +void revert(const ParsedPayload& /*p*/) { + removeFile(kDconfFile); + removeFile("/etc/dconf/db/local.d/locks/vesperos-screensaver"); + runCmd("dconf update 2>/dev/null"); +} + +} // namespace vesperos::profile::platform::screensaver diff --git a/src/platform/linux/SoftwareUpdate.cpp b/src/platform/linux/SoftwareUpdate.cpp new file mode 100644 index 0000000..1b152f2 --- /dev/null +++ b/src/platform/linux/SoftwareUpdate.cpp @@ -0,0 +1,76 @@ +#include "../SoftwareUpdate.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// SOFTWARE UPDATE HANDLER (Linux) +// Writes /etc/apt/apt.conf.d/99-vesperos for update settings. +// Sets up unattended-upgrades for automatic updates. +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::software_update { + +using namespace vesperos::profile::util; + +namespace { +constexpr const char* kAptConf = "/etc/apt/apt.conf.d/99-vesperos-update"; +constexpr const char* kUnattendedConf = "/etc/apt/apt.conf.d/51-vesperos-unattended"; +} + +bool apply(const ParsedPayload& p) { + // Extract nested automatic block + bool autoCheck = true; + bool autoDownload = true; + bool autoInstall = false; + int osDeferDays = 0; + + auto autoJson = p.fields.find("automatic"); + if (autoJson != p.fields.end()) { + autoCheck = autoJson->second.find("\"check\":true") != std::string::npos; + autoDownload = autoJson->second.find("\"download\":true") != std::string::npos; + autoInstall = autoJson->second.find("\"install-os-updates\":true") != std::string::npos; + } + + auto deferJson = p.fields.find("deferral"); + if (deferJson != p.fields.end()) { + auto pos = deferJson->second.find("\"os-updates-days\":"); + if (pos != std::string::npos) { + osDeferDays = std::stoi(deferJson->second.substr(pos + 18, 3)); + } + } + + // ── apt.conf.d drop-in ───────────────────────────────────────── + std::ostringstream apt; + apt << "// VesperOS managed — do not edit\n" + << "APT::Periodic::Update-Package-Lists \"" + << (autoCheck ? "1" : "0") << "\";\n" + << "APT::Periodic::Download-Upgradeable-Packages \"" + << (autoDownload ? "1" : "0") << "\";\n" + << "APT::Periodic::Unattended-Upgrade \"" + << (autoInstall ? "1" : "0") << "\";\n"; + writeFile(kAptConf, apt.str()); + + // ── unattended-upgrades deferral ─────────────────────────────── + if (osDeferDays > 0) { + std::ostringstream ua; + ua << "// VesperOS managed\n" + << "Unattended-Upgrade::MinimalSteps \"true\";\n" + << "Unattended-Upgrade::InstallOnShutdown \"false\";\n"; + // Delay upgrades by pinning to a date offset + // (unattended-upgrades doesn't have a native delay knob; + // use a cron-based hold instead) + writeFile(kUnattendedConf, ua.str()); + } + + syslog(LOG_INFO, "[software-update] applied update policy"); + return true; +} + +void revert(const ParsedPayload& /*p*/) { + removeFile(kAptConf); + removeFile(kUnattendedConf); +} + +} // namespace vesperos::profile::platform::software_update diff --git a/src/platform/linux/TimeServer.cpp b/src/platform/linux/TimeServer.cpp new file mode 100644 index 0000000..58117f5 --- /dev/null +++ b/src/platform/linux/TimeServer.cpp @@ -0,0 +1,42 @@ +#include "../TimeServer.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// TIME SERVER HANDLER (Linux) +// Writes /etc/systemd/timesyncd.conf.d/vesperos.conf +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::time_server { + +using namespace vesperos::profile::util; + +namespace { +constexpr const char* kTimesyncDir = "/etc/systemd/timesyncd.conf.d"; +constexpr const char* kTimesyncConf = "/etc/systemd/timesyncd.conf.d/vesperos.conf"; +} + +bool apply(const ParsedPayload& p) { + std::string server = field(p, "server"); + if (server.empty()) return true; + + ensureDir(kTimesyncDir); + std::ostringstream c; + c << "[Time]\n" + << "NTP=" << server << "\n" + << "FallbackNTP=0.debian.pool.ntp.org 1.debian.pool.ntp.org\n"; + writeFile(kTimesyncConf, c.str()); + runCmd("systemctl restart systemd-timesyncd 2>/dev/null"); + + syslog(LOG_INFO, "[time-server] NTP=%s", server.c_str()); + return true; +} + +void revert(const ParsedPayload& /*p*/) { + removeFile(kTimesyncConf); + runCmd("systemctl restart systemd-timesyncd 2>/dev/null"); +} + +} // namespace vesperos::profile::platform::time_server diff --git a/src/platform/linux/Vpn.cpp b/src/platform/linux/Vpn.cpp new file mode 100644 index 0000000..a15bbe5 --- /dev/null +++ b/src/platform/linux/Vpn.cpp @@ -0,0 +1,120 @@ +#include "../Vpn.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// VPN HANDLER (Linux) +// IKEv2 → NetworkManager strongswan plugin (network-manager-strongswan) +// L2TP → NetworkManager l2tp plugin (network-manager-l2tp) +// Custom → write OpenVPN .ovpn or WireGuard .conf +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::vpn { + +using namespace vesperos::profile::util; + +namespace { + +bool applyIkev2(const ParsedPayload& p, const std::string& connId, const std::string& /*name*/) { + // Requires: network-manager-strongswan + // Write an NM keyfile for IKEv2/IPsec + auto ikev2Json = p.fields.find("ikev2"); + if (ikev2Json == p.fields.end()) return false; + + // Quick field extraction from stored JSON string + auto extractField = [&](const std::string& key) -> std::string { + auto pos = ikev2Json->second.find("\"" + key + "\":"); + if (pos == std::string::npos) return ""; + pos += key.size() + 3; + auto q1 = ikev2Json->second.find('"', pos); + auto q2 = ikev2Json->second.find('"', q1 + 1); + if (q1 == std::string::npos) return ""; + return ikev2Json->second.substr(q1 + 1, q2 - q1 - 1); + }; + + std::string server = extractField("server"); + std::string remoteId = extractField("remote-id"); + + std::ostringstream kf; + kf << "[connection]\n" + << "id=" << connId << "\n" + << "type=vpn\n" + << "autoconnect=false\n\n" + << "[vpn]\n" + << "service-type=org.freedesktop.NetworkManager.strongswan\n" + << "address=" << server << "\n" + << "remote-identity=" << remoteId << "\n" + << "method=key\n" + << "virtual=yes\n" + << "ipcomp=no\n" + << "encap=no\n" + << "proposal=yes\n\n" + << "[ipv4]\nmethod=auto\n"; + + ensureDir("/etc/NetworkManager/system-connections"); + writeFile("/etc/NetworkManager/system-connections/" + connId + ".nmconnection", kf.str(), 0600); + return runCmd("nmcli connection reload"); +} + +bool applyL2tp(const ParsedPayload& p, const std::string& connId, const std::string& /*name*/) { + // Requires: network-manager-l2tp, network-manager-l2tp-gnome + auto l2tpJson = p.fields.find("l2tp"); + if (l2tpJson == p.fields.end()) return false; + + auto extract = [&](const std::string& key) -> std::string { + auto pos = l2tpJson->second.find("\"" + key + "\":"); + if (pos == std::string::npos) return ""; + pos += key.size() + 3; + auto q1 = l2tpJson->second.find('"', pos); + auto q2 = l2tpJson->second.find('"', q1 + 1); + if (q1 == std::string::npos) return ""; + return l2tpJson->second.substr(q1 + 1, q2 - q1 - 1); + }; + + std::ostringstream cmd; + cmd << "nmcli connection add type vpn" + << " con-name '" << connId << "'" + << " vpn-type l2tp" + << " vpn.data 'gateway=" << extract("server") + << ",user=" << extract("username") + << ",password-flags=0" + << ",ipsec-enabled=true" + << ",ipsec-psk=" << extract("shared-secret") << "'"; + return runCmd(cmd.str()); +} + +bool applyCustom(const ParsedPayload& p, const std::string& /*connId*/) { + // Write raw WireGuard config if provider hints at it, + // otherwise log for manual configuration. + syslog(LOG_INFO, "[vpn] custom VPN uuid=%s — write your own config", p.uuid.c_str()); + return true; +} + +} // namespace + +bool apply(const ParsedPayload& p) { + std::string displayName = field(p, "display-name", "VesperOS VPN"); + std::string connId = "vesperos-vpn-" + p.uuid; + + bool hasIkev2 = p.fields.count("ikev2"); + bool hasL2tp = p.fields.count("l2tp"); + bool hasCustom = p.fields.count("custom"); + + if (hasIkev2) return applyIkev2(p, connId, displayName); + if (hasL2tp) return applyL2tp(p, connId, displayName); + if (hasCustom) return applyCustom(p, connId); + + syslog(LOG_WARNING, "[vpn] no VPN type block found in payload"); + return false; +} + +void revert(const ParsedPayload& p) { + std::string connId = "vesperos-vpn-" + p.uuid; + runCmd("nmcli connection delete id '" + connId + "' 2>/dev/null"); + removeFile("/etc/NetworkManager/system-connections/" + connId + ".nmconnection"); + removeFile("/etc/wireguard/vesperos-" + p.uuid + ".conf"); +} + +} // namespace vesperos::profile::platform::vpn diff --git a/src/platform/linux/Wallpaper.cpp b/src/platform/linux/Wallpaper.cpp new file mode 100644 index 0000000..05d7e83 --- /dev/null +++ b/src/platform/linux/Wallpaper.cpp @@ -0,0 +1,70 @@ +#include "../Wallpaper.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// WALLPAPER HANDLER (Linux) +// Sets GNOME wallpaper via gsettings (runs as root → sets system default). +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::wallpaper { + +using namespace vesperos::profile::util; + +namespace { +constexpr const char* kGdmDefaults = "/etc/dconf/db/local.d/00-vesperos-wallpaper"; +constexpr const char* kGdmLock = "/etc/dconf/db/local.d/locks/vesperos-wallpaper"; +} + +bool apply(const ParsedPayload& p) { + // wallpaper is a nested object; find the path field from its JSON + auto wJson = p.fields.find("wallpaper"); + if (wJson == p.fields.end()) return true; + + auto extractStr = [&](const std::string& key) -> std::string { + auto pos = wJson->second.find("\"" + key + "\":"); + if (pos == std::string::npos) return ""; + pos += key.size() + 3; + auto q1 = wJson->second.find('"', pos); + auto q2 = wJson->second.find('"', q1 + 1); + if (q1 == std::string::npos) return ""; + return wJson->second.substr(q1 + 1, q2 - q1 - 1); + }; + + std::string path = extractStr("path"); + bool locked = wJson->second.find("\"locked\":true") != std::string::npos; + + if (path.empty()) return true; + + // dconf system-wide profile + ensureDir("/etc/dconf/db/local.d"); + ensureDir("/etc/dconf/db/local.d/locks"); + + std::ostringstream dconf; + dconf << "[org/gnome/desktop/background]\n" + << "picture-uri='file://" << path << "'\n" + << "picture-uri-dark='file://" << path << "'\n" + << "picture-options='zoom'\n"; + writeFile(kGdmDefaults, dconf.str()); + + if (locked) { + writeFile(kGdmLock, + "/org/gnome/desktop/background/picture-uri\n" + "/org/gnome/desktop/background/picture-uri-dark\n" + "/org/gnome/desktop/background/picture-options\n"); + } + + runCmd("dconf update 2>/dev/null"); + syslog(LOG_INFO, "[wallpaper] set wallpaper path=%s locked=%d", path.c_str(), locked); + return true; +} + +void revert(const ParsedPayload& /*p*/) { + removeFile(kGdmDefaults); + removeFile(kGdmLock); + runCmd("dconf update 2>/dev/null"); +} + +} // namespace vesperos::profile::platform::wallpaper diff --git a/src/platform/linux/Wifi.cpp b/src/platform/linux/Wifi.cpp new file mode 100644 index 0000000..4d2acea --- /dev/null +++ b/src/platform/linux/Wifi.cpp @@ -0,0 +1,177 @@ +#include "../Wifi.h" +#include "NetworkManagerUtil.h" +#include "../../payloads/PayloadUtil.h" + +#include +#include + +// ═══════════════════════════════════════════════════════════════════════════ +// WIFI HANDLER (Linux) +// Writes a NetworkManager keyfile to +// /etc/NetworkManager/system-connections/vesperos-wifi-.nmconnection +// +// Why keyfiles instead of nmcli exec: +// - No subprocess / shell injection risk from SSIDs or passwords that +// contain quotes, spaces, or special characters. +// - Atomic write via rename(); NM only sees the complete file. +// - The full NM keyfile schema is available: EAP-FAST, WPA3-Enterprise, +// per-BSSID settings, MAC randomisation, QoS — all expressible. +// nmcli can't set many of these flags. +// - Works during first-boot before NM is running: NM picks up the file +// on its first start after the profile is applied. +// - Consistent with EthernetHandler — one code path, one format. +// ═══════════════════════════════════════════════════════════════════════════ + +namespace vesperos::profile::platform::wifi { + +using namespace vesperos::profile::util; +using namespace vesperos::profile::platform::nm; + +namespace { +std::string keyfilePath(const ParsedPayload& p) { + return std::string(kNMConnDir) + "/vesperos-wifi-" + p.uuid + ".nmconnection"; +} +} // namespace + +bool apply(const ParsedPayload& p) { + std::string ssid = field(p, "ssid"); + bool hidden = fieldBool(p, "hidden", false); + bool autoJoin = fieldBool(p, "auto-join", true); + std::string macMode = field(p, "mac-address-mode", "hardware"); + + if (ssid.empty()) { syslog(LOG_ERR, "[wifi] missing ssid"); return false; } + + // Parse security block + std::string secJson = field(p, "security"); // stored as JSON object + std::string secType = extractJson(secJson, "type"); // wpa2|wpa3|wep|none|any + std::string psk = extractJson(secJson, "password"); + + // Map profile security type → NM key-mgmt value + std::string keyMgmt; + if (secType == "wpa3") keyMgmt = "sae"; + else if (secType == "wpa2" || secType == "wpa") keyMgmt = "wpa-psk"; + else if (secType == "wep") keyMgmt = "none"; // WEP uses no key-mgmt + else if (secType == "none" || secType == "any" || secType.empty()) keyMgmt = ""; + + // Parse EAP block (enterprise WiFi) + std::string eapJson = field(p, "eap"); + bool isEnterprise = !eapJson.empty() && eapJson != "\"\""; + std::string eapMethod; + std::string eapUser, eapPass, eapOuter, eapInnerAuth, ttlsInner; + std::string anchorUuid, serverName; + if (isEnterprise) { + // Determine method from the methods array + if (eapJson.find("tls") != std::string::npos) eapMethod = "tls"; + else if (eapJson.find("ttls") != std::string::npos) eapMethod = "ttls"; + else if (eapJson.find("fast") != std::string::npos) eapMethod = "fast"; + else eapMethod = "peap"; + eapUser = extractJson(eapJson, "username"); + eapPass = extractJson(eapJson, "password"); + eapOuter = extractJson(eapJson, "outer-identity"); + ttlsInner = extractJson(eapJson, "ttls-inner-auth"); + // trust block + auto trustPos = eapJson.find("\"trust\":"); + if (trustPos != std::string::npos) { + serverName = extractJson(eapJson.substr(trustPos), "server-names"); + anchorUuid = extractJson(eapJson.substr(trustPos), "anchor-cert-uuids"); + } + } + + // Parse proxy block + std::string proxyJson = field(p, "proxy"); + std::string proxyType = extractJson(proxyJson, "type"); // manual|auto|none + std::string proxyHost = extractJson(proxyJson, "host"); + std::string proxyPort = extractJson(proxyJson, "port"); + std::string proxyUser = extractJson(proxyJson, "username"); + std::string proxyPass = extractJson(proxyJson, "password"); + std::string pacUrl = extractJson(proxyJson, "pac-url"); + + // ── Build the keyfile ────────────────────────────────────────────── + std::ostringstream kf; + + // [connection] + kf << "[connection]\n" + << "id=vesperos-wifi-" << ssid << "\n" + << "uuid=" << p.uuid << "\n" + << "type=wifi\n" + << "autoconnect=" << (autoJoin ? "true" : "false") << "\n\n"; + + // [wifi] + kf << "[wifi]\n" + << "ssid=" << ssid << "\n" + << "mode=infrastructure\n" + << "hidden=" << (hidden ? "true" : "false") << "\n"; + if (macMode == "random") + kf << "cloned-mac-address=random\n" + << "mac-address-randomization=2\n"; + kf << "\n"; + + // [wifi-security] + if (!keyMgmt.empty() || secType == "wep") { + kf << "[wifi-security]\n"; + if (!keyMgmt.empty()) + kf << "key-mgmt=" << keyMgmt << "\n"; + if (!psk.empty() && secType != "wep") + kf << "psk=" << psk << "\n"; + if (secType == "wep") + kf << "auth-alg=open\n" + << "wep-key0=" << psk << "\n" + << "wep-key-type=1\n"; + kf << "\n"; + } + + // [802-1x] (enterprise) + if (isEnterprise) { + kf << "[802-1x]\n" + << "eap=" << eapMethod << "\n"; + if (!eapUser.empty()) kf << "identity=" << eapUser << "\n"; + if (!eapPass.empty()) kf << "password=" << eapPass << "\n"; + if (!eapOuter.empty()) kf << "anonymous-identity=" << eapOuter << "\n"; + if (eapMethod == "ttls" || eapMethod == "peap") { + std::string phase2 = ttlsInner.empty() ? "mschapv2" : ttlsInner; + kf << "phase2-auth=" << phase2 << "\n"; + } + if (!serverName.empty()) + kf << "altsubject-matches=" << serverName << "\n"; + if (!anchorUuid.empty()) + // NM references a cert stored in the system keyring by uuid-hash; + // for simplicity write the path if the CertHandler already wrote it. + kf << "# ca-cert=/usr/local/share/ca-certificates/vesperos/" + << anchorUuid << ".crt\n"; + kf << "\n"; + } + + // [proxy] + if (proxyType == "manual" && !proxyHost.empty()) { + kf << "[proxy]\n" + << "method=manual\n" + << "http-proxy=" << proxyHost << ":" << proxyPort << "\n" + << "https-proxy=" << proxyHost << ":" << proxyPort << "\n"; + if (!proxyUser.empty()) + kf << "# proxy-auth=" << proxyUser << ":" << proxyPass << "\n"; + kf << "\n"; + } else if (proxyType == "auto" && !pacUrl.empty()) { + kf << "[proxy]\n" + << "method=auto\n" + << "pac-url=" << pacUrl << "\n\n"; + } + + // [ipv4] / [ipv6] + kf << "[ipv4]\nmethod=auto\n\n" + << "[ipv6]\nmethod=auto\naddr-gen-mode=stable-privacy\n"; + + ensureDir(kNMConnDir); + writeFile(keyfilePath(p), kf.str(), 0600); + nmReload(); + + syslog(LOG_INFO, "[wifi] wrote keyfile ssid=%s uuid=%s", ssid.c_str(), p.uuid.c_str()); + return true; +} + +void revert(const ParsedPayload& p) { + removeFile(keyfilePath(p)); + nmReload(); + syslog(LOG_INFO, "[wifi] removed keyfile uuid=%s", p.uuid.c_str()); +} + +} // namespace vesperos::profile::platform::wifi diff --git a/third_party/openssl-android/Android.bp b/third_party/openssl-android/Android.bp new file mode 100644 index 0000000..a10bdfb --- /dev/null +++ b/third_party/openssl-android/Android.bp @@ -0,0 +1,32 @@ +// +// Vendored, statically-linked real OpenSSL — vesperprofiled's Android build +// only, nothing else on the system links against these. See README.md for +// why (BoringSSL has no CMS support) and how to populate lib/*/*.a. +// + +cc_prebuilt_library_static { + name: "libcrypto_vesper_static", + arch: { + arm64: { srcs: ["lib/arm64-v8a/libcrypto.a"] }, + arm: { srcs: ["lib/armeabi-v7a/libcrypto.a"] }, + x86_64: { srcs: ["lib/x86_64/libcrypto.a"] }, + x86: { srcs: ["lib/x86/libcrypto.a"] }, + }, + export_include_dirs: ["include"], + strip: { none: true }, +} + +cc_prebuilt_library_static { + name: "libssl_vesper_static", + arch: { + arm64: { srcs: ["lib/arm64-v8a/libssl.a"] }, + arm: { srcs: ["lib/armeabi-v7a/libssl.a"] }, + x86_64: { srcs: ["lib/x86_64/libssl.a"] }, + x86: { srcs: ["lib/x86/libssl.a"] }, + }, + export_include_dirs: ["include"], + strip: { none: true }, + // libssl depends on libcrypto symbols; Soong needs this to order the + // static link correctly. + whole_static_libs: ["libcrypto_vesper_static"], +} diff --git a/third_party/openssl-android/README.md b/third_party/openssl-android/README.md new file mode 100644 index 0000000..e6c1e8d --- /dev/null +++ b/third_party/openssl-android/README.md @@ -0,0 +1,79 @@ +# Vendored OpenSSL (Android, static, vesperprofiled-only) + +AOSP's system `libcrypto`/`libssl` is BoringSSL, which has no CMS/PKCS#7 +support. Rather than touch the platform's crypto stack (used by Keystore, +the TLS stack, other HALs — swapping it breaks far more than it fixes), +vesperprofiled statically links its **own** copy of real OpenSSL, scoped to +just this one binary. Nothing else on the system links against it or even +knows it's there. + +This directory holds the prebuilt static libs + headers and the Soong +modules that expose them. **The `lib/*/` and `include/` directories are +empty placeholders** — populate them by cross-compiling OpenSSL yourself +(steps below) before `m vesperprofiled` will link on Android. Nothing here +fabricates or ships a prebuilt binary sight-unseen. + +--- + +## Building OpenSSL for Android + +Needs the Android NDK (r26+) on your PATH as `$ANDROID_NDK_ROOT`, and +OpenSSL source (3.x recommended — CMS support is stable there). + +```bash +git clone --branch openssl-3.2 --depth 1 https://github.com/openssl/openssl.git +cd openssl + +export ANDROID_NDK_ROOT=/path/to/android-ndk +export PATH="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH" + +# Repeat per ABI. android-arm64 shown; swap target/API for the others. +for target in android-arm64 android-arm android-x86_64 android-x86; do + case $target in + android-arm64) abi=arm64-v8a ;; + android-arm) abi=armeabi-v7a ;; + android-x86_64) abi=x86_64 ;; + android-x86) abi=x86 ;; + esac + + ./Configure "$target" -D__ANDROID_API__=26 no-shared no-tests \ + --prefix="$(pwd)/build-$abi" + make clean + make -j"$(nproc)" + make install_sw + + mkdir -p "../lib/$abi" + cp "build-$abi/lib/libcrypto.a" "../lib/$abi/" + cp "build-$abi/lib/libssl.a" "../lib/$abi/" +done + +# Headers are identical across ABIs — copy from any one build. +cp -r build-arm64-v8a/include/openssl ../include/ +``` + +`no-shared` is what makes these static (`.a`) — deliberate, so nothing +depends on an OpenSSL `.so` being present on-device at runtime. `no-tests` +just skips building OpenSSL's own test suite to save time. + +## Verifying + +``` +third_party/openssl-android/ +├── include/openssl/*.h (same for every ABI) +└── lib/ + ├── arm64-v8a/{libcrypto,libssl}.a + ├── armeabi-v7a/{libcrypto,libssl}.a + ├── x86_64/{libcrypto,libssl}.a + └── x86/{libcrypto,libssl}.a +``` + +Once populated, `m vesperprofiled` picks these up via `Android.bp` in this +directory — see `libcrypto_vesper_static` / `libssl_vesper_static` / +`vesperprofiled_openssl_headers`, referenced from +`vesperprofiled/Android.bp`'s Android `static_libs`. + +## Updating + +OpenSSL ships security fixes regularly — treat these `.a` files as a +dependency you're responsible for rebuilding on new releases, same as any +other vendored library. Nothing here auto-updates them. diff --git a/third_party/openssl-android/include/PLACEHOLDER.txt b/third_party/openssl-android/include/PLACEHOLDER.txt new file mode 100644 index 0000000..c3c5848 --- /dev/null +++ b/third_party/openssl-android/include/PLACEHOLDER.txt @@ -0,0 +1 @@ +Not populated. Copy the openssl/ header directory from any one ABI build per ../README.md — headers are identical across ABIs. diff --git a/third_party/openssl-android/lib/arm64-v8a/PLACEHOLDER.txt b/third_party/openssl-android/lib/arm64-v8a/PLACEHOLDER.txt new file mode 100644 index 0000000..1f69047 --- /dev/null +++ b/third_party/openssl-android/lib/arm64-v8a/PLACEHOLDER.txt @@ -0,0 +1 @@ +Not populated. Build libcrypto.a and libssl.a for arm64-v8a per ../../README.md and drop them in this directory. diff --git a/third_party/openssl-android/lib/armeabi-v7a/PLACEHOLDER.txt b/third_party/openssl-android/lib/armeabi-v7a/PLACEHOLDER.txt new file mode 100644 index 0000000..8de76c5 --- /dev/null +++ b/third_party/openssl-android/lib/armeabi-v7a/PLACEHOLDER.txt @@ -0,0 +1 @@ +Not populated. Build libcrypto.a and libssl.a for armeabi-v7a per ../../README.md and drop them in this directory. diff --git a/third_party/openssl-android/lib/x86/PLACEHOLDER.txt b/third_party/openssl-android/lib/x86/PLACEHOLDER.txt new file mode 100644 index 0000000..cb9391a --- /dev/null +++ b/third_party/openssl-android/lib/x86/PLACEHOLDER.txt @@ -0,0 +1 @@ +Not populated. Build libcrypto.a and libssl.a for x86 per ../../README.md and drop them in this directory. diff --git a/third_party/openssl-android/lib/x86_64/PLACEHOLDER.txt b/third_party/openssl-android/lib/x86_64/PLACEHOLDER.txt new file mode 100644 index 0000000..f797cf3 --- /dev/null +++ b/third_party/openssl-android/lib/x86_64/PLACEHOLDER.txt @@ -0,0 +1 @@ +Not populated. Build libcrypto.a and libssl.a for x86_64 per ../../README.md and drop them in this directory. diff --git a/vesperprofiled.rc b/vesperprofiled.rc index 19369cf..af63c49 100644 --- a/vesperprofiled.rc +++ b/vesperprofiled.rc @@ -22,3 +22,9 @@ service vesperprofiled /system/bin/vesperprofiled on post-fs-data mkdir /data/system/vesperos 0700 system system mkdir /data/system/vesperos/profiles 0700 system system + mkdir /data/system/vesperos/preinstalled 0700 system system + # content-cache payload's policy override — world-readable by design, + # PawletCacheService (a regular app UID) reads it directly. See + # PayloadHandlers.cpp's ContentCacheHandler and PolicyOverride.kt. + # PawletOS-fork-specific (see PayloadHandlers.cpp's header comment). + mkdir /data/misc/pawletcache 0755 system system