oxmc a48077078c Initial commit: PawletProfiled, Android device-owner priv-app
Android counterpart to git.oxmc.me/PawletOS/profiled's Linux daemon --
independent implementation, not a port. Same .vconfig profile format and
17 payload types, but implemented against DevicePolicyManager/VpnManager/
WifiManager/WallpaperManager/KeyChain instead of a native NDK Binder
daemon, since that's where AOSP actually exposes this functionality.

Self-provisions as device owner at first boot (DeviceOwnerProvisioner.kt)
to unlock the DevicePolicyManager-gated payload types (cert, pkcs12,
passcode, proxy, screensaver lock enforcement). 16 of 17 payload types are
real implementations; firewall is a documented platform dead end (no app
UID gets CAP_NET_ADMIN). See README's capability matrix for the full
per-payload breakdown.

Reviewed against the documented @SystemApi/hidden-API surface, not
compiled -- no AOSP toolchain available in this environment.
2026-07-25 01:35:58 -07:00

PawletProfiled

PawletOS's Android device management app — a platform-signed priv-app and device owner. Android counterpart to pawletprofiled, 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

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'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 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.

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
S
Description
No description provided
Readme
82 KiB
Languages
Kotlin 97.9%
AIDL 2.1%