Class AppShield
API shielding: proves to your own backend that a request came from a genuine, unmodified build of your app running on a device that has not been tampered with.
How it differs from DeviceIntegrity
DeviceIntegrity is the raw platform primitive -- it hands you a Play Integrity or App Attest
blob and leaves verification, policy and enforcement to you. AppShield is the managed layer
on top: the attestation is verified server side against Apple and Google, evaluated against a
policy you control, and turned into a short-lived signed token your backend can check with a
few lines of middleware. Both remain available and DeviceIntegrity keeps working unchanged;
an app that only needs the raw blob should keep using it.
The shape of the thing
AppShield.init(new ShieldConfig()
.protect("api.mybank.example", HostPolicy.PROTECTED));
// ...then just make requests. Protected hosts get the header and the pin check.
ConnectionRequest r = new ConnectionRequest("https://api.mybank.example/transfer", true);
NetworkManager.getInstance().addToQueueAndWait(r);
Your backend rejects any request whose token is missing, expired or unsigned. That check is where the security actually lives -- not in this class. A device the attacker fully controls can always strip a header; what it cannot do is mint a token, because the token is signed by a service the attacker does not control, on the strength of a statement from Apple or Google.
When the engine is absent
Builds without the enterprise attestation engine -- open-source builds, and any project not
entitled to it -- get a working, inert implementation. isProtected() returns false,
fetchToken() completes with ShieldStatus.UNPROTECTED rather than hanging, attach(ConnectionRequest) does
nothing, and no request is ever blocked. The API is safe to call unconditionally; there is no
need to guard call sites.
Threading
fetchToken() is asynchronous. attach(ConnectionRequest) blocks and must not be called on
the EDT -- in normal use you never call it yourself, because a protected host is handled
automatically on the network thread.
-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final StringResponse header a backend sets to say it rejected the attestation token, as opposed to the user's own credentials. -
Method Summary
Modifier and TypeMethodDescriptionstatic voidRegisters a listener for status and signal changes.static voidaddProtectedHost(String host) Registers a host with the default policy, honouringShieldConfig.defaultFailureMode(FailureMode).static voidaddProtectedHost(String host, HostPolicy policy) Registers a protected host afterinit(ShieldConfig), for a backend discovered at runtime.static voidattach(ConnectionRequest request) Attaches the attestation header to a request.static AsyncResource<ShieldToken> Fetches a time-limited token, reusing the cached one when it is still good.static AsyncResource<ShieldToken> fetchToken(String bindingData) Fetches a token bound to specific request data.static ShieldTokenThe cached token without triggering a fetch.static ShieldConfigThe configuration passed toinit(ShieldConfig), or defaults if it has not been called.static StringThe active engine's name, for diagnostics and support logs.static NetworkGuardThe shield's ownNetworkGuard, for an app that has to install a guard of its own.static PinSetThe pin set currently in force.static ShieldSignal[]The runtime self-protection observations recorded so far, combining what the engine detected with anything the app or a library reported toShieldSignals.static ShieldStatusThe most recent token status.static HashtableheadersFor(String url) The headers a protected URL should carry, for network paths that do not go throughConnectionRequest-- notablyBrowserComponent.setURL(url, headers).static voidinit(ShieldConfig cfg) Initializes the shield.static voidDiscards any cached token.static booleanTrue when a real attestation engine is present and available.static HostPolicyThe policy in force for a host.static void
-
Field Details
-
REJECT_HEADER
Response header a backend sets to say it rejected the attestation token, as opposed to the user's own credentials.
Without it a 401 or 403 is ambiguous: protected APIs normally carry ordinary user authorization too, and treating every such response as an attestation rejection would make a client re-attest through an entire login failure. Emit it only when the token itself was the problem; the value is ignored.
- See Also:
-
-
Method Details
-
init
Initializes the shield. Call once during app startup, after
Display.init.Safe to call in a build with no attestation engine: it logs one line and leaves the shield inert. Calling it twice is a no-op.
-
getNetworkGuard
The shield's own
NetworkGuard, for an app that has to install a guard of its own.NetworkManagerholds a single guard and seals the slot on first install, so an app with its own guard leaves no room for the shield's. Delegating to this one is the supported way to have both. Delegate every method, not onlyNetworkGuard.beforeRequest(ConnectionRequest): attaching the token is the visible half of the shield, and the certificate callbacks are the half that enforcesHostPolicy.isEnforcePins(). An app that forwards onlybeforeRequestgets tokens and no pinning, and nothing about its behaviour says so.final NetworkGuard shield = AppShield.getNetworkGuard(); NetworkManager.setNetworkGuard(new NetworkGuard() { public void beforeRequest(ConnectionRequest r) throws IOException { myOwnHeaders(r); shield.beforeRequest(r); } public boolean isCertificateCheckRequired(String url) { return myOwnCheckNeeded(url) || shield.isCertificateCheckRequired(url); } public void checkCertificates(ConnectionRequest r, ConnectionRequest.SSLCertificate[] c) throws IOException { myOwnCheck(r, c); shield.checkCertificates(r, c); } public String[] interestingResponseHeaders() { return concat(myOwnHeaderNames(), shield.interestingResponseHeaders()); } public void afterResponse(ConnectionRequest r, int code, String[] headers) { shield.afterResponse(r, code, headers); } }); AppShield.init(cfg);Note that
interestingResponseHeaders()has to be the union of both guards' names, and that theheadersarray handed toafterResponseis positional against it -- so a composing guard must pass the shield the slice that corresponds to the shield's own names, in that order. Installing the shield's guard directly, by callinginit(ShieldConfig)before installing anything of your own, avoids the bookkeeping entirely and is what most apps should do.Safe to call before
init(ShieldConfig); the returned guard reads the configuration live rather than capturing it. -
isProtected
public static boolean isProtected()True when a real attestation engine is present and available. False in an open-source or unentitled build, and in the simulator unless simulation is switched on. -
getEngineName
The active engine's name, for diagnostics and support logs. -
getConfig
The configuration passed toinit(ShieldConfig), or defaults if it has not been called. -
fetchToken
Fetches a time-limited token, reusing the cached one when it is still good. -
fetchToken
Fetches a token bound to specific request data.
Binding ties the token to one request, so a token lifted off a captured request cannot be replayed against a different one. Worth the extra round trip on the calls that matter -- a transfer, a password change -- and not worth it on the rest, which should use the plain
fetchToken().- Parameters:
bindingData- the data to bind to, typically a digest of the request body
-
invalidateToken
public static void invalidateToken()Discards any cached token. Call this when your backend rejects a token, so the next request re-attests rather than replaying the token that was just refused. -
attach
Attaches the attestation header to a request.
Blocks while a token is fetched, so it must be called on a network thread. Requests to hosts registered via
ShieldConfig.protect(String, HostPolicy)are handled automatically and do not need this; use it for a request built outside the normal path.Honours the host's
FailureMode: underFailureMode.OPENa token failure leaves the request untouched, underFailureMode.CLOSEDit propagates.- Throws:
ShieldException
-
headersFor
The headers a protected URL should carry, for network paths that do not go through
ConnectionRequest-- notablyBrowserComponent.setURL(url, headers).Returns an empty table when the host is unprotected or no token is available. Never blocks: it uses the cached token only, because the callers are typically on the EDT.
Note this covers only the initial navigation. Requests the loaded page makes itself are not visible to the framework and cannot be given a token or pinned.
-
getCachedToken
The cached token without triggering a fetch. May be null or lapsed. Never blocks. -
addProtectedHost
Registers a protected host afterinit(ShieldConfig), for a backend discovered at runtime. -
addProtectedHost
Registers a host with the default policy, honouringShieldConfig.defaultFailureMode(FailureMode). -
policyFor
The policy in force for a host. ReturnsHostPolicy.UNPROTECTEDfor anything not registered, which is the great majority of hosts an app talks to. -
getPinSet
The pin set currently in force. Never null; may bePinSet.EMPTY. -
getSignals
The runtime self-protection observations recorded so far, combining what the engine detected with anything the app or a library reported to
ShieldSignals.Informational. The attestation service applies the policy, and it may reach a different conclusion than a naive reading of this array -- an emulator signal, for instance, is normal on a developer's machine.
-
getStatus
The most recent token status.ShieldStatus.NOT_INITIALIZEDbeforeinit(ShieldConfig). -
addListener
Registers a listener for status and signal changes. Callbacks arrive on the EDT. -
removeListener
-