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.
170 lines
9.5 KiB
Markdown
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
|
|
```
|