Onym Moderation ↔ Device Mark: Google Play Integrity Device Recall Implementation
Implementation profile draft 0.2 — August 2026
This profile maps the abstract device-mark rail onto Play Integrity's device recall feature: three per-device values scoped to a Google Play developer account, written server-side and read inside integrity verdicts, surviving app reinstallation and device reset for a bounded retention term.
This document is a concrete implementation of Moderation.md §5.7 (device marks and enforcement binding) for Android. The abstract contract remains authoritative for mandates, reports, cases, verdicts, obligations, and invariants. This document defines only the Google platform mapping and records where it falls short of the abstract rail.
The sibling DeviceCheck profile is the iOS counterpart. The two profiles deliberately share consent, verdict validation, gate-state, reconciliation, and enforcement-backend requirements where the platforms permit it.
The document distinguishes:
- platform behavior, which Google Play Integrity provides;
- profile requirements, which a conforming interface vendor's app and enforcement backend must implement; and
- gaps, where the platform or current code cannot yet meet the abstract contract and a declared mitigation applies.
The concrete Authority-side v1 wire and implemented lifecycle follow merged
onym-moderation main
(through PR #10). No current Onym Android code implements this profile. The
existing Android application has no moderation domain, authenticated manifest
review, Play Integrity token client, enforcement backend transport, gate, or
device recall integration. Every implementation statement below is therefore
a requirement, not a claim about deployed behavior; §9 records that boundary.
1. Conformance declaration
| Abstract concept | Device recall mapping |
|---|---|
| Enforcement profile | onym:moderation-enforcement-profile:google-device-recall-v1, version 1 |
| Gate notice schema | onym-moderation-google-device-recall-case-notice-v1 |
| Device-mark platform | Google Play Integrity API, device recall (beta) |
| Mark scope (per interface vendor) | Three values are per device per Google Play developer account. Every app in that account can access the same values; they are not isolated by package name or linked Cloud project |
case-open mark |
bitFirst |
banned mark |
bitSecond |
| Reserved | bitThird, left unchanged by this profile |
| Mark write path | Vendor backend → v1/{packageName}/deviceRecall:write, authenticated through the app's linked Google Cloud project and targeting the device represented by a verified integrity token |
| Mark read path | deviceRecall.values and writeDates inside a decoded integrity verdict after device recall is enabled for the app |
deviceBinding (mandate/verdict field) |
An opaque vendor-local enrollment identifier; not an integrity token or a platform device identifier |
| Device attestation of the request | A standard or classic integrity token generated by the app and decoded and validated by the backend; standard requests bind the gate request through requestHash |
| Mark persistence | Google documents recall across app reinstall and device reset; values are retained for three years after the last read or write, so this is durable leased state, not permanent storage |
The table instantiates this signed enforcement-profile object:
{
"profileVersion": 1,
"profileId": "onym:moderation-enforcement-profile:google-device-recall-v1",
"platform": "google-device-recall",
"bindings": {
"enroll-device": {"requestSchema": "onym-moderation-google-device-recall-enrollment-request-v1", "resultSchema": "onym-moderation-google-device-recall-device-enrollment-v1"},
"countersign-mandate": {"requestSchema": "onym-moderation-mandate-v1", "resultSchema": "onym-moderation-google-device-recall-interface-signature-v1"},
"register-mandate": {"requestSchema": "onym-moderation-mandate-v1", "resultSchema": "onym-moderation-mandate-receipt-v1"},
"gate-check": {"requestSchema": "onym-moderation-google-device-recall-gate-request-v1", "resultSchema": "onym-moderation-google-device-recall-gate-result-v1"},
"deliver-verdict": {"requestSchema": "onym-moderation-verdict-submission-v1", "resultSchema": "onym-moderation-verdict-receipt-v1"}
},
"caseNoticeSchema": "onym-moderation-google-device-recall-case-notice-v1",
"markBindings": {"case-open": "bitFirst", "banned": "bitSecond"},
"specification": "<content-address-of-this-profile-specification>"
}
Sections 4–6 define the registered schema and mark meanings. The Interface
directory authenticates this object as Moderation.md §6.4 requires, and the
signed mandate pins its ID/version pair. Reissuing that pair with different
bindings is nonconforming. bitThird remains outside markBindings and this
profile never writes it.
The account-wide scope is broader than one app. To preserve the abstract contract's exclusive interface-vendor write boundary, a conforming deployment must either dedicate its Play developer account to interfaces that share this exact bit contract or prove that no other app or backend in the account can write these values. A package-scoped Cloud project alone does not provide that separation. An app transfer moves the app to the new developer account's value namespace; it does not carry the old account's device marks with it.
2. Ownership mapping
| Component | Owner | Holds |
|---|---|---|
| Android interface app | Interface vendor | Integrity token requests, request-hash construction, gate and ban-state UX |
| Enforcement backend | Interface vendor | Linked Google Cloud service-account credentials, token decoding and validation, verdict validation, reconciliation, and the mark write path |
| Google Play Integrity | Recall-value storage, integrity verdict production, and token validation; judges nothing | |
| Moderation authority | Independent operator | Verdict signing keys; no Google credentials and no write path |
The vendor's Play developer account and linked Cloud credentials physically route writes through the interface vendor. The profile turns that constraint into the abstract separation: the authority signs, the vendor validates and executes, and Google stores. Because sibling apps in one developer account can reach the same values, the vendor must enforce the account-level isolation rule in §1 rather than claiming package-level isolation it does not have.
3. Physical topology
┌──────────────────────┐ token + requestHash ┌──────────────────────┐
│ Android interface app│──────────────────────────────>│ Enforcement backend │
│ case / ban / appeal │<──────────────────────────────│ interface vendor │
└──────────────────────┘ gate result + state └──────┬───────▲───────┘
decode / write │ │ signed
v │ Verdict
┌──────────────────────────┐ │
│ Google Play Integrity │ │
│ verdicts · device recall │ │
└──────────────────────────┘ │
┌───────────────┴──────┐
│ Moderation authority │
│ (no Google access) │
└──────────────────────┘
4. Value mapping
| Value | Abstract mark | Set when | Cleared when |
|---|---|---|---|
bitFirst |
case-open |
Backend validates an interim open-case verdict against this device's enrollment |
Dismissal, superseding ban, decision-deadline default, or designation revocation |
bitSecond |
banned |
Backend validates a ban verdict whose executeAfter has arrived; a non-suspensive ban may execute before final becomes true |
banExpires, reversal/new-holder remedy, or designation revocation where no forum survives |
bitThird |
— | — | — |
This profile neither sets nor clears bitThird; every write omits it, which
preserves its account-wide value.
Profile requirements:
- values change only inside verdict execution and declared
deadline/expiry/revocation reconciliation; no administrative, support,
sibling-app, or store-pressure path may reach
deviceRecall:write; - every write is logged against the authorizing verdict hash or declared clearing rule, and the log is auditable by the audit seat;
- writes specify only values this transition changes. In particular,
bitThirdis omitted rather than reset, because unspecified values remain unchanged and all apps in the developer account share it; writeDatesare month-and-year consistency signals, never authorization, execution, or expiry sources. Setting a value true updates its correspondingyyyymm*field; setting it false removes that field. Real timing lives in the verdict and backend log; and- the backend accounts for Google's documented write-to-read propagation delay of up to 30 seconds and never treats an immediately stale read as permission to undo or bypass a just-executed verdict.
5. Consent, enrollment, and gate checks
5.1 Reviewed terms and enrollment
Consent follows the abstract one-artifact transaction. A conforming Android repository:
- fetches the authority listing, exact manifest bytes, and detached signature once for human review;
- verifies the directory-pinned operator key, signature over the exact bytes, component/operator bindings, consent-time conditions, and lowercase SHA-256 hash inside that repository boundary;
- returns an opaque reviewed artifact whose constructor is unavailable to application callers, so decoded fields cannot be paired with different raw bytes; and
- after agreement, accepts only that same artifact, re-runs time-sensitive validity checks, and does not refetch. The signed mandate's classes and hash and the retained display bytes all derive from exactly what the user saw.
Enrollment then proceeds:
- the app prepares a standard integrity token provider and constructs an enrollment request containing the user key, UTC timestamp, and a fresh backend-issued challenge;
- it hashes a stable serialization of every security-relevant enrollment
field into Play Integrity's
requestHash, obtains a token, and sends the fields, token, and identity signature to the backend; - the backend decodes the token through Google, verifies the package,
certificate, licensing and device verdicts required by the deployment,
timestamp/freshness, identity signature, and exact
requestHashbinding, and reads the current recall values; - after resolving the gate outcome, the backend returns an opaque vendor-local
deviceBindingonly when enrollment may proceed; - the client builds and signs the mandate from its retained reviewed artifact and that binding, including this enforcement-profile ID/version;
- the backend returns only its detached countersignature. It cannot replace mandate fields or ask the client to persist a rebuilt mandate; and
- the interface registers the exact finalized two-signature mandate with the named
authority as Moderation.md §6.2 requires. Registration is idempotent by
mandateRef; failure leaves authority enrollment incomplete. The signed consent artifact remains immutable, but the gate fails closed and retries only transient failure. If the Authority's published manifest changed, the app presents the new authenticated artifact for review and obtains fresh consent/signatures rather than retrying forever or substituting terms.
Steps 1–4 bind enroll-device, steps 5–6 bind countersign-mandate, and step
7 binds register-mandate; the next section binds gate-check. Signed
interim and terminal verdicts enter the backend through deliver-verdict, and
the gate derives the notice or ban state returned to the app. Enrollment,
countersigning, and gating belong to the independently versioned
interface-enforcement profile; registration and delivery are cross-owner
authority-profile facets.
The cross-language canonical serialization, signature domains, precise integrity-verdict policy, and freshness window must be frozen in published fixtures before implementation. They are not inherited accidentally from the current Swift implementation. At minimum, enrollment and gate requests use different signature domains and bind every transmitted security-relevant field.
5.2 Gate checks
At launch, on foreground, on explicit retry, and at least once per the declared interval while the app runs:
- the app obtains an execution slot from its throttled gate scheduler, reuses
a prepared standard token provider when valid, constructs a
GateCheckRequestcontaining the user key, activemandateRef, timestamp, and fresh backend challenge, and binds the stable request serialization throughrequestHash; - the backend decodes and validates the token and request as in enrollment,
reads
deviceRecall.values, resolves the enrollment where possible, and reconciles platform state with its verdict records before responding; bitSecondset → it returnsbannedeven if the enrollment cannot yet be resolved. A resolved enrollment supplies the verdict reference, authority contact, expiry, ordinary appeal, and new-holder path. An unresolved one still refuses operation and displays the designated authority plus its new-holder/re-identification procedure; successful re-identification then supplies the governing verdict or clears the value. A silentcheckRequiredbrick is nonconforming;bitSecondclear andbitFirstset → it returnscaseOpenwith notices. The app operates normally and displays the procedural case state;- a present
deviceRecallobject whose values are false or absent → it returnsclearonly after the prerequisite verdict tuple below and reconciliation succeed; and - a missing
deviceRecallobject, failed prerequisite, invalid request, exhausted offline grace, or other untrustworthy unmarked answer →checkRequired, which blocks operation.
The concrete evaluation rule is deliberately stricter than testing whether
values is empty. Before interpreting any recall value, the backend requires:
- matching and fresh
requestDetails, including package name andrequestHash; appRecognitionVerdict == PLAY_RECOGNIZED, with the expected package and signing-certificate digest;appLicensingVerdict == LICENSED;deviceRecognitionVerdictcontainingMEETS_DEVICE_INTEGRITY; and- the
deviceIntegrity.deviceRecallobject to be present.
Failure of any condition is checkRequired, regardless of the contents of
values. If all five pass, a present deviceRecall object with empty maps is
the profile's clean never-written state; absent bit fields are false. Google
does not expose a separate "device recall evaluated" boolean and also
documents an unavailable recall value as an object with empty values and
writeDates. A technically unavailable result that nevertheless satisfies
all five classifier conditions — including the present but empty object — is
therefore indistinguishable from a clean device. The profile cannot honestly
claim to fail closed across that final ambiguity; §8 records the limitation
and required monitoring.
On an otherwise clean device, absence of an active mandate is a consent-gate
condition, not clear. A set mark takes precedence and follows the caseOpen
or banned path above even when mandate storage is missing. Losing local
mandate storage therefore leads to a fresh mark check before re-consent, never
into an unmoderated app.
The backend associates the enrollment with the mandate's deviceBinding at
first validated token. Integrity tokens are request artifacts, not stable
identifiers. The linkage model is:
- Reading is platform-state-first. The recall values are the sole durable
device state the refusal consults after reinstall: a set
bitSecondrefuses service even when the backend cannot initially resolve an enrollment. - Writing and reconciliation are session-mediated. The identity signature and token presented together in one session are the only link from the current device request to a vendor-local enrollment. The backend never tries to derive an identifier from the token.
- Unresolvable marked state routes to re-identification. A marked device whose session identity resolves no active verdict is not treated as clean. It receives the manifest's new-holder/re-identification procedure; the outcome is a reversal that clears values or a confirmation that restores linkage.
Gate checks run at a declared interval (default P1D). An unreachable backend
may serve the last successfully reconciled state for a declared offline grace
window (default P3D), then degrades to checkRequired. The client persists
the result and success time, rejects clock rollback, prevents an older
concurrent result from replacing a newer one, and explicitly checks on
foreground rather than relying only on a background timer. Backend identity
refusal remains in force independently of the device gate.
Launch, foreground, retry, and interval triggers feed one scheduler; they do
not each force a provider preparation or token request. The scheduler
coalesces concurrent triggers and holds one provider. It prepares on cold
start, after INTEGRITY_TOKEN_PROVIDER_INVALID, and after a recall write when
the next eligible read must observe refreshed values, while enforcing Google's
limit of at most five preparations per app instance per minute. Token requests
are likewise coalesced, exponentially backed off on TOO_MANY_REQUESTS, and
metered against a declared budget below the deployment's daily quota, with
usage alerts enabled. A throttled trigger serves the last reconciled result
only within offline grace and schedules the next eligible attempt; without
history or after grace it returns checkRequired. Foreground or retry thrash
must never create unbounded Play calls or discard a still-valid known state.
For standard requests, device recall is refreshed during provider preparation. After a write, the app must prepare again before relying on a subsequent token to observe the new value. The backend carries the just-executed state across Google's propagation window rather than accepting a cached pre-write value as authoritative.
6. Verdict execution and reconciliation
On receiving a verdict from the designated authority, the backend:
- validates the authority signature against the key in the mandate's exact
consented manifest; manifest hash, authority, mandate reference, accused
keys,
deviceBinding, and class bindings; mandatory reasoning; disposition and marks;appealDeadline;executeAfteragainst the consented appeal effect; andbanExpiresagainst the consented ban term. A valid ban beforeexecuteAfteris stored, not executed; - resolves
deviceBindingto an enrollment and selects an already decoded, verified token from the target device's authenticated session. Google permits a verified token to authorize a write for up to 14 days, but this profile uses the current execution/reconciliation session where possible and never treats token age alone as identity-to-device linkage; - if there is no usable target-device session, queues the mark write until the next session presenting a valid token with an identity named by the mandate. Backend identity refusal applies immediately, so the queue does not restore access under the banned key;
- calls
deviceRecall:writewith only the values this transition changes and records the response against the authorizing object; and - schedules and lazily reconciles clearing actions authorized by expiry, dismissal/reversal, decision-deadline default, or the abstract revocation forum rule.
The case-open write normally executes during notice service, which already
requires a connected accused session. Clearing and delayed writes require a
usable token from the target device. When the device returns, the backend
reconciles before answering its gate. A resolvable enrollment applies expiry,
dismissal/reversal, decision defaults, and revocation directly; an unresolvable
marked holder enters the re-identification path. A stale value on a device that
never returns is inert because no conforming gate acts on it without
reconciliation.
Google retains values for three years after the last read or write. Ordinary gate reads refresh that lease for active devices. If a marked device stays away longer than the retention term, its values may disappear; a returning mandated identity can be re-marked from backend verdict state, but a new identity on expired platform state cannot be recognized as the old device. This limitation is disclosed in §8 and means the profile cannot promise unconditional device enforcement after more than three years of platform inactivity.
7. Error mapping
| Abstract error | Device recall condition | Backend behavior |
|---|---|---|
mark_write_failed |
Write rejected, token unusable, API failure, or rate limit | Retry in the next valid target-device session; verdict and identity refusal remain in force |
verdict_invalid |
Pre-platform validation failure | Refuse execution; never call Google |
| Clean platform state | deviceRecall present with values absent or false, and the full request/app/licensing/device prerequisite tuple passes |
Reconcile against the resolved enrollment before returning clear |
| Token/request invalid | Decode failure, replay-cleared verdict, bad package/certificate/verdict, stale request, identity signature failure, or requestHash mismatch |
Reject and re-request; repeated failure becomes checkRequired |
| Recall prerequisites fail | deviceRecall missing, app not PLAY_RECOGNIZED, account not LICENSED, or device missing MEETS_DEVICE_INTEGRITY |
checkRequired; never treat as clean |
| Recall object present but empty | All prerequisite verdicts pass and both maps are empty | Treat as never-written clean state, subject to Google's documented evaluation ambiguity in §8 |
| No supported Play environment | Play Store/services absent or outdated, unsupported device, emulator | checkRequired; never unmoderated operation |
| Token/provider throttled | TOO_MANY_REQUESTS, daily token quota, or provider-prepare limit |
Coalesce and back off; serve the last reconciled result within grace, otherwise checkRequired; schedule the next eligible attempt |
| Propagation/cache lag | A post-write token still carries old values | Preserve just-executed backend state, re-prepare provider, and retry after the propagation window |
new_holder_claim |
Holder asserts transfer at ban UX or marked state cannot resolve | Route to the manifest's procedure and execute the resulting reversal or confirmation normally |
8. Known gaps
- Beta availability. Device recall remains a Play Integrity beta that requires approval and Play Console opt-in. Quotas, availability, and terms can change; every deployment must re-verify them before claiming conformance.
- The values are developer-account-wide. All apps in one Play developer account see the same three values. Package endpoints and Cloud credentials do not create separate bit namespaces. A dedicated account or demonstrably common bit contract is required; otherwise a sibling app can corrupt or act on moderation state outside the consenting surface.
- Retention is bounded. Values expire after three years without a read or write. Active gate checks refresh the lease, but a device absent beyond the horizon can return with clean platform state. Permanent sanctions therefore retain backend identity refusal but cannot promise a permanent device mark.
- The values survive the person. Google documents persistence through reinstall and reset, so resale and refurbishment can carry a sanction to a new holder. The reference Authority's unauthenticated, bounded new-holder claim path offers human review but is not proof of transfer and can be exhausted. Reset and transfer behavior must still be exercised on deployment devices.
- The rail excludes valid Android environments. Device recall requires
recent enabled Play Store and Play services, a Play-licensed account, and a
supported physical device; emulators and de-Googled or otherwise unsupported
distributions fail to
checkRequired. The interface manifest must disclose this reach limitation. - Google exposes no definitive evaluated/clean discriminator. Both a
never-written device and an unavailable recall value can produce a present
deviceRecallobject with emptyvaluesandwriteDates. The profile requires the request/app/licensing/device tuple in §5.2 and fails closed when the object or any prerequisite is missing, but a technical recall failure with every outer prerequisite still passing is indistinguishable from clean state. Deployments must monitor empty-result anomalies and disclose this residual fail-open platform ambiguity; no implementation may claim a stronger signal than Google returns. - Three values and coarse dates are not a verdict store. Verdict
references, classes, expiries, and appeal state live in the vendor backend.
bitThirdremains reserved. Losing backend state makes the values uninterpretable and cannot be repaired from month/year write dates. - A queued write can be outrun. A verdict received without a usable target-device token must wait for another session. Wiping the app before that session and never returning under a mandated identity can evade the device write, though backend refusal of the named identity remains. Standard-request caching and up-to-30-second propagation also require explicit handling.
- No third-party proof of faithful writes exists. Google attests the device and stores values, not the vendor's obedience. Signed write logs and audit-seat attestation are the declared substitute and remain a paper control until a deployed auditor verifies them.
- Google is a silent party. Developer-account or Cloud-project failure, app transfer, quota policy, beta change, or feature deprecation can alter or orphan state outside this contract. The dependency and its failure behavior must be disclosed.
- No Onym Android implementation or frozen wire contract exists. The current app contains none of the client or backend components in this profile. Cross-language consent, request, mandate, verdict, and signature fixtures must be published before implementation; passing the iOS package's tests would not by itself establish Android or backend conformance.
9. Current Onym Android implementation
The current onym-android repository contains no moderation or Play Integrity device recall slice:
| Contract/profile responsibility | Current Android status |
|---|---|
| Authority directory and manifest authenticity | Not implemented |
| One-snapshot reviewed consent artifact | Not implemented |
| Mandate enrollment and signature-only countersigning | Not implemented |
| Standard integrity token provider and request-hash binding | Not implemented |
| Enforcement backend transport | Not implemented |
| Launch/foreground/interval gate and offline grace | Not implemented |
| Verdict validation and execution timing | Not implemented |
| Recall reads, writes, reconciliation, and write logs | Not implemented |
| Case, ban, ordinary appeal, and new-holder UX | Not implemented |
This table is descriptive and deliberately narrower than the profile's acceptance criteria. The abstract contract and the implemented iOS domain slice are design inputs, not evidence that Android or Google-backed enforcement exists.
10. Conformance tests
Fixtures must cover:
- exact-byte manifest authenticity, raw-bytes/decoded-fields mismatch, and a hosted manifest changing between review and agreement without changing the signed mandate artifact;
- standard token preparation and decoding; package, certificate, licensing,
device, timestamp, replay, identity-signature, and
requestHashvalidation; - the exact recall classifier: every failed request/app/licensing/device
prerequisite, missing
deviceRecall, present-but-empty maps, and populated false values, including the documented clean/unavailable ambiguity; - verdict-to-value execution for every disposition, including suspensive/non-suspensive timing and refusal of early writes;
- writes that preserve unspecified
bitThird, removal of the correspondingyyyymm*field on clear, separate rate limiting, propagation delay, and mandatory provider refresh; - queued writes and their execution only in a valid target-device session;
- lazy reconciliation for expiry, dismissal/reversal, deadline default, and revocation;
- re-linking after reinstall and routing unresolved marked devices to re-identification/new-holder review;
- developer-account sibling-app visibility and app-transfer namespace change;
- the three-year retention boundary, reset/reinstall behavior, and device transfer; and
- interval/grace enforcement, clock rollback, stale concurrent completions, coalesced foreground/retry thrash, the five-per-minute provider-prepare cap, daily token quota exhaustion, missing Play services, unlicensed accounts, unsupported devices, and emulators.
11. Acceptance criteria
This profile is successfully implemented when:
- an authenticated manifest is reviewed and the exact retained artifact—not a second fetch or caller-built byte/field pair—supplies the signed mandate;
- a ban verdict against an enrolled device sets
bitSecondonly at or after its validexecuteAfter, and the app refuses service with complete ban and appeal UX using only the vendor's authorized backend path; - reinstalling or resetting the device restores refusal from recall state within Google's declared retention and platform-support bounds, without a backend account lookup deciding whether a set banned value should gate;
- expiry, dismissal/reversal, deadline default, and applicable revocation clear values mechanically in a subsequent valid device session;
- no path outside validated verdict execution and declared reconciliation can
write
bitFirstorbitSecond, no profile path touchesbitThird, and the log accounts for every call; - cached reads, propagation lag, rate limits, a missing
deviceRecallobject, failed prerequisite verdicts, and unsupported Play environments cannot produceclear, while the irreducible present-empty ambiguity is tested, monitored, and disclosed; - the deployment satisfies and tests the developer-account-wide ownership rule instead of claiming false package isolation; and
- the shared object and validation fixtures let independent Android, iOS, and backend authors implement the abstract contract without private coordination.
References
- Onym moderation contract boundary: Moderation.md
- Sibling iOS profile: Moderation-DeviceCheck.md
- Google Play Integrity device recall: https://developer.android.com/google/play/integrity/device-recall
- Standard Play Integrity requests: https://developer.android.com/google/play/integrity/standard
- Play Integrity verdicts: https://developer.android.com/google/play/integrity/verdicts
- Play Integrity terms and data safety: https://developer.android.com/google/play/integrity/terms
- Onym audit seat (write-log attestation): ../audit/Audit.md
- Current Onym Android application: https://github.com/onymchat/onym-android
This document is maintained in the public onym-system repository and rendered here from a pinned commit. The repository is the authoritative source — latest version on main →