public final class AppShield
- Object
- 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 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.
Fields
public static final String REJECT_HEADER = "X-CN1-Attest-Reject" | Response header a backend sets to say it rejected the attestation token, as opposed to the user’s own credentials. |
Methods
Inherited methods
Field details
REJECT_HEADER
public static final String REJECT_HEADER = "X-CN1-Attest-Reject"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.
Method details
init
public static void init(ShieldConfig cfg)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
public static NetworkGuard getNetworkGuard()The shield’s own NetworkGuard, for an app that has to install a guard of its own.
NetworkManager holds 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 only
NetworkGuard.beforeRequest(ConnectionRequest): attaching the token is the visible
half of the shield, and the certificate callbacks are the half that enforces
HostPolicy.isEnforcePins(). An app that forwards only beforeRequest gets 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 the headers array handed to afterResponse is 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 calling
init(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()getEngineName
public static String getEngineName()getConfig
public static ShieldConfig getConfig()init(ShieldConfig), or defaults if it has not been called.fetchToken
public static AsyncResource<ShieldToken> fetchToken()fetchToken
public static AsyncResource<ShieldToken> fetchToken(String bindingData)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
bindingDataString- the data to bind to, typically a digest of the request body
invalidateToken
public static void invalidateToken()attach
public static void attach(ConnectionRequest request)
throws ShieldExceptionAttaches 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: under FailureMode.OPEN a token failure leaves the
request untouched, under FailureMode.CLOSED it propagates.
Throws
headersFor
public static Hashtable headersFor(String url)The headers a protected URL should carry, for network paths that do not go through
ConnectionRequest – notably BrowserComponent.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
public static ShieldToken getCachedToken()addProtectedHost
public static void addProtectedHost(String host, HostPolicy policy)Registers a protected host after init(ShieldConfig), for a backend discovered at
runtime.
Takes the same patterns as ShieldConfig.protect(String, HostPolicy) – an exact
host or a leading *. wildcard covering its subdomains – and resolves them the same
way, most specific first. A registration made here wins over a configured one for the
same pattern, being the later statement of intent, but does not override a more
specific configured host.
addProtectedHost
public static void addProtectedHost(String host)ShieldConfig.defaultFailureMode(FailureMode).protectedHosts
public static Enumeration protectedHosts()Every host currently protected: the ones ShieldConfig carried into
init(ShieldConfig), plus anything registered since through
addProtectedHost(String, HostPolicy).
The config alone is not the answer to “what is protected”, and treating it as such
is a quiet way to lose the runtime registrations. An engine building a pin set from
the config would leave a backend discovered at runtime with no pins, so
PinSet.isEnforcedFor returns false for it and the certificate check is skipped
entirely – the host the app went out of its way to register is the one host not
pinned.
policyFor
public static HostPolicy policyFor(String host)HostPolicy.UNPROTECTED for anything not
registered, which is the great majority of hosts an app talks to.getPinSet
public static PinSet getPinSet()PinSet.EMPTY.getSignals
public static ShieldSignal[] 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
public static ShieldStatus getStatus()ShieldStatus.NOT_INITIALIZED before init(ShieldConfig).addListener
public static void addListener(ShieldListener l)removeListener
public static void removeListener(ShieldListener l)