# PawletProfiled PawletOS's Android device management app — a platform-signed priv-app and device owner. Android counterpart to [`pawletprofiled`](https://git.oxmc.me/PawletOS/profiled), the Linux daemon: same `.vconfig` profile format, same payload types, same `os.pawlet.profiled` AIDL contract — but an independent implementation, not shared code. Most of what these payloads need on Android (`DevicePolicyManager`, `VpnManager`, `WifiManager`, `WallpaperManager`, `KeyChain`) is Java-SDK-first and not cleanly reachable from a native process, which is why this is an app instead of another NDK binder daemon. --- ## Contents - [Why an app, not a daemon](#why-an-app-not-a-daemon) - [Device owner](#device-owner) - [Payload capability matrix](#payload-capability-matrix) - [Architecture](#architecture) - [Third-party dependencies](#third-party-dependencies) - [Building](#building) - [Package layout](#package-layout) --- ## Why an app, not a daemon The Linux daemon is a native root process that owns the machine outright — writing NetworkManager keyfiles, editing PAM/nftables config, calling `update-ca-certificates`. Android doesn't expose an equivalent to *any* app UID, root included: Wi-Fi, VPN, certificate installation, and password policy all go through `DevicePolicyManager`/`WifiManager`/ `VpnManager`, which are Java APIs gated on **device owner** status, not Linux-style file permissions. Standing up a native process just to `AIDL`-call back into `system_server` for everything would be strictly more code than calling those APIs directly — so this is a Kotlin app. ## Device owner `DeviceOwnerProvisioner.kt` self-provisions via `DevicePolicyManager.setDeviceOwner()` — the same `@SystemApi` call ManagedProvisioning itself uses — from `BootCompletedReceiver`, guarded by `isDeviceOwnerApp()`. This only succeeds pre-SUW, before any account exists, which a factory-fresh PawletOS image satisfies on first boot. Every later boot's call is expected to fail and is treated as "already provisioned," not an error. ## Payload capability matrix | Payload | Status | Notes | |---|---|---| | `wifi` | Real | `WifiManager.addNetwork()` (privileged path — the only one covering WEP + enterprise + static proxy in one object) | | `ethernet` | Real (IP/proxy) | `EthernetManager`. 802.1X not applied — no stable cross-device `@SystemApi` for wired EAP; depends on the board's Ethernet HAL | | `vpn` | Real (IKEv2 only) | `VpnManager`/`Ikev2VpnProfile`, PSK or username+password. L2TP: no AOSP client exists at all. Custom: no bundled tunnel backend, same as the Linux side's own placeholder | | `cert` | Real | `DevicePolicyManager.installCaCert()` | | `pkcs12` | Real | Parsed locally via `KeyStore("PKCS12")`, installed via `DevicePolicyManager.installKeyPair()` | | `passcode` | Real | `DevicePolicyManager` password-quality/history/lockout setters | | `mdm` | Real | Device-owner status *is* the enrollment; this records server metadata for `isDeviceManaged()`/`getMdmServerUrl()` | | `software-update` | Bridged | Writes a policy file for BgUpd to read — BgUpd doesn't read it yet, same "wired one side, not both" state as the Linux daemon's own comment about this integration | | `time-server` | Real | `Settings.Global.NTP_SERVER` | | `proxy` | Real | `DevicePolicyManager.setRecommendedGlobalProxy()`. No proxy-auth — `ProxyInfo` doesn't carry credentials | | `dns-proxy` | Real (narrower) | Android Private DNS is DoT-hostname only, not arbitrary DoH URLs; the host is extracted from whatever URL the payload carries | | `firewall` | **Not applicable** | No app UID gets `CAP_NET_ADMIN`, device owner included, and stock Android has no unsolicited-inbound-connection surface to protect. The one genuine dead end — not a missing library, missing kernel privilege | | `ldap` | Real | Bundled UnboundID LDAP SDK (Android has no JNDI/system LDAP client at all) actually binds and searches at apply time. No OS-level directory-accounts sync (Android has nothing like macOS Open Directory) | | `wallpaper` | Real | `WallpaperManager`. `locked` recorded, not enforced (no such API) | | `screensaver` | Real | Daydream settings + `DevicePolicyManager.setMaximumTimeToLock()` for the actual security-relevant lock enforcement | | `first-boot` | Real (coarser) | `DEVICE_PROVISIONED`/`user_setup_complete` skip the *entire* Setup Wizard in one step, vs. the Linux side's pane-by-pane cloud-init module list | | `content-cache` | Real | Writes `/data/misc/pawletcache/policy.json` — PawletOS-specific, read by [`android_packages_apps_PawletCache`](https://git.oxmc.me/PawletOS/android_packages_apps_PawletCache)'s `PolicyOverride.kt` | ## Architecture ``` BootCompletedReceiver ──▶ DeviceOwnerProvisioner (first boot only) │ │ │ ▼ │ preinstalled/*.vconfig ──▶ PawletProfileApplication │ │ └──▶ ZteLookupClient (enrollment) ─────────────────────┤ ▼ IPawletProfileService.Stub (PawletProfileService) ──▶ PawletProfileApplication │ ┌─────────────────────┤ ▼ ▼ SignatureVerifier ProfileParser (Bouncy Castle CMS) (SnakeYAML) │ │ └──────────┬──────────┘ ▼ ProfileStore (/data/system/pawletos/profiles) │ ▼ PayloadHandlerRegistry.forType() ──▶ one of 17 PayloadHandlers ``` `PawletProfileApplication` is the shared core (mirrors the Linux daemon's `PawletProfileService.cpp`) — both the boot-time preinstalled-profile path and the AIDL-facing bound-service path install through the same `installProfileDirect()`, against the same `ProfileStore`, so there's one source of truth regardless of which path a profile came in through. ## Third-party dependencies Not vendored as jars in this repo — pulled in via [`maven-to-lib`](https://git.oxmc.me/PawletOS/maven-to-lib) as `prebuilt_libs` repos on the local manifest, same as every other Maven dependency in the PawletOS tree. See the comment block at the top of `Android.bp` for the exact `config.yml` entries. Three libraries, all pure Java (no native/JNI component, no per-ABI split needed): | Library | Fills the gap left by | |---|---| | SnakeYAML | No YAML parser anywhere in the Android platform/SDK | | Bouncy Castle (`bcprov`+`bcpkix`) | BoringSSL has no CMS/PKCS#7 support — same gap the Linux daemon solves by vendoring a static OpenSSL, solved here with a pure-Java library instead | | UnboundID LDAP SDK | Android has no `javax.naming`/JNDI and no system LDAP client | ## Building Not independently buildable outside an AOSP tree — this is a `platform_apis: true`, `certificate: "platform"` priv-app. ```bash cp -r android_packages_apps_PawletProfiled $AOSP_ROOT/vendor/oxmc/PawletProfiled/ echo 'PRODUCT_PACKAGES += PawletProfiled' >> device/oxmc/pawletos/pawletos.mk echo 'BOARD_SEPOLICY_DIRS += vendor/oxmc/PawletProfiled/sepolicy' >> device/oxmc/pawletos/BoardConfig.mk echo 'PRODUCT_COPY_FILES += vendor/oxmc/PawletProfiled/etc/privapp-permissions-os.pawlet.profiled.xml:$(TARGET_COPY_OUT_SYSTEM)/etc/permissions/privapp-permissions-os.pawlet.profiled.xml' \ >> device/oxmc/pawletos/pawletos.mk source build/envsetup.sh && lunch pawletos_arm64-userdebug m PawletProfiled ``` Same disclaimer as the rest of this session's Android work: statically reviewed against the documented `@SystemApi`/hidden-API surface, never compiled — no AOSP toolchain available in this environment. ## Package layout ``` android_packages_apps_PawletProfiled/ ├── Android.bp ├── AndroidManifest.xml ├── aidl/os/pawlet/profiled/ │ └── IPawletProfileService.aidl ├── etc/privapp-permissions-os.pawlet.profiled.xml ├── res/ │ ├── values/strings.xml │ └── xml/device_admin_receiver.xml ├── sepolicy/ │ ├── pawletprofiled.te │ └── file_contexts └── src/os/pawlet/profiled/ ├── PawletProfileApplication.kt Shared core: install/remove/query, mirrors the Linux D-Bus service ├── PawletProfileService.kt Bound Service hosting IPawletProfileService.Stub ├── PawletDeviceAdminReceiver.kt DeviceAdminReceiver + MDM-enrolled wipe-on-disable ├── BootCompletedReceiver.kt Device-owner provisioning, preinstalled profiles, ZTE ├── DeviceOwnerProvisioner.kt ├── ProfileParser.kt SnakeYAML ├── ProfileStore.kt Same on-disk layout as ProfileStore.cpp ├── SignatureVerifier.kt Bouncy Castle CMS ├── ProfileModels.kt / Fields.kt ├── payloads/ 17 handlers, one per payload type └── zte/ ├── DeviceIdentity.kt / AttestationKeyHasher.kt └── ZteLookupClient.kt ```