Files
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

170 lines
9.5 KiB
Markdown

# 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
```