Chapter 40: Security¶
Android's security architecture is one of the most comprehensive and layered defenses deployed at consumer-device scale. Every component, from the bootloader through the kernel, system services, and application framework, participates in a multi-level "defense-in-depth" strategy. This chapter walks through each major subsystem -- SELinux mandatory access control, Verified Boot, hardware-backed key storage, the Trusty TEE, biometric authentication, app sandboxing, encryption, and network security -- reading the actual AOSP source code at every step.
40.1 Android Security Model¶
40.1.1 Design Principles¶
Android's security model rests on four fundamental principles:
- Application sandboxing -- each application runs with its own Linux UID and in its own process, with its own private data directory.
- Least privilege -- applications start with almost no permissions and must request them explicitly. The user or system policy grants (or denies) each one.
- Defense in depth -- no single mechanism is trusted in isolation. The kernel sandbox, SELinux MAC, seccomp-BPF, and userspace permission checks each serve as independent layers.
- Secure by default -- new features ship in a locked-down state and must be explicitly relaxed.
40.1.2 Layered Defense Overview¶
graph TB
subgraph "Hardware Root of Trust"
HW[Boot ROM / Fuses]
TEE[TEE: Trusty / TrustZone]
StrongBox[StrongBox SE]
end
subgraph "Bootloader"
AVB[Android Verified Boot]
RollbackProt[Rollback Protection]
end
subgraph "Kernel Layer"
SELinux[SELinux MAC]
Seccomp[seccomp-BPF]
Namespaces[Linux Namespaces]
DmVerity[dm-verity]
FBE[File-Based Encryption]
end
subgraph "Framework Layer"
Permissions[Runtime Permissions]
Sandbox[App Sandbox / UID isolation]
Keystore[Keystore2 Service]
Gatekeeper[Gatekeeper]
Biometrics[Biometrics Framework]
NetSec[Network Security Config]
end
subgraph "Application Layer"
AppSig[APK Signing v2/v3/v4]
AppPerms[Declared Permissions]
AppSandbox[Per-app sandbox]
end
HW --> AVB
TEE --> Keystore
TEE --> Gatekeeper
StrongBox --> Keystore
AVB --> DmVerity
SELinux --> Sandbox
Seccomp --> Sandbox
Namespaces --> Sandbox
FBE --> Keystore
Permissions --> AppPerms
Sandbox --> AppSandbox
40.1.3 Application Signing¶
Every APK must be signed before it can be installed. Android supports multiple signature schemes:
| Scheme | Introduced | How it works |
|---|---|---|
| v1 (JAR) | Android 1.0 | Signs individual ZIP entries via META-INF/ |
| v2 | Android 7.0 | Signs the entire APK as a binary blob |
| v3 | Android 9 | Extends v2 with key rotation support |
| v3.1 | Android 13 | Adds rotation-min-sdk-version |
| v4 | Android 11 | Produces a separate .idsig file for incremental installs |
The signature establishes the identity of the developer. The package manager uses it for:
- Upgrade verification -- an update must be signed with the same key as the installed package.
- Shared UID -- two packages signed with the same key can request the same Linux UID and share data.
- Signature permissions -- certain permissions are only grantable to packages signed with the platform key.
40.1.4 Permission Model¶
Android defines three protection levels for permissions:
- normal -- granted automatically at install time (e.g.
INTERNET). - dangerous -- requires explicit user consent at runtime (e.g.
CAMERA,READ_CONTACTS). - signature -- only granted to apps signed with the same certificate as the app that declared the permission, or the platform.
The permission enforcement happens at multiple layers:
- Framework checks --
Context.checkPermission()in Java/Kotlin code. - Binder service checks -- services check the calling UID and PID.
- Kernel-level checks -- SELinux and Linux DAC prevent unauthorized access even if framework checks are bypassed.
40.1.5 Trust Chain from Hardware to Application¶
flowchart LR
A["Hardware Root of Trust<br/>Fuses + Boot ROM"] --> B["Bootloader<br/>Verified Boot"]
B --> C["Kernel + dm-verity<br/>Verified Partitions"]
C --> D["init + SELinux<br/>MAC Policy Loaded"]
D --> E["Zygote<br/>Forks App Processes"]
E --> F["App Sandbox<br/>UID + SELinux + seccomp"]
The chain of trust starts at non-modifiable hardware (fused public keys in the SoC's boot ROM) and extends through every layer until it reaches the application. If any link in this chain is broken, the device can detect it and respond (refuse to boot, show a warning, or wipe data depending on policy).
40.1.6 Security Boundary Definitions¶
Understanding Android security requires clear definitions of trust boundaries:
| Boundary | Inside (Trusted) | Outside (Untrusted) |
|---|---|---|
| Hardware root of trust | Fused keys, Boot ROM | All software |
| TEE boundary | Trusty kernel + TAs | Linux kernel, Android framework |
| Kernel boundary | Kernel code, loaded modules | All userspace processes |
| System service boundary | system_server, privileged daemons | Apps, untrusted code |
| App sandbox boundary | App's own process and data | Other apps, system internals |
Each boundary is enforced by a different mechanism:
- Hardware root of trust: physical fuses, one-time-programmable memory.
- TEE boundary: ARM TrustZone hardware (TZASC, secure monitor at EL3).
- Kernel boundary: CPU privilege rings (EL0 vs EL1), page table isolation.
- System service boundary: SELinux type enforcement + Linux DAC.
- App sandbox: UID isolation + SELinux + seccomp-BPF + mount namespaces.
40.1.7 Threat Model¶
Android's security model considers the following threat actors:
- Malicious apps -- apps that attempt to steal data from other apps, escalate privileges, or persist after uninstallation.
- Network attackers -- adversaries who can observe and modify network traffic (e.g., on public WiFi).
- Physical attackers -- adversaries with physical access to the device (stolen phones, forensic examination).
- Supply chain attackers -- attempts to inject malicious code into the OS image or bootloader.
- Insider threats -- compromised vendor code or HALs running with elevated privileges.
Each security subsystem addresses specific threat actors:
graph LR
subgraph "Threats"
MA[Malicious Apps]
NA[Network Attackers]
PA[Physical Attackers]
SC[Supply Chain]
end
subgraph "Defenses"
Sandbox[App Sandbox]
TLS[TLS + Cert Pinning]
FBE_d[FBE + Verified Boot]
AVB_d[AVB + dm-verity]
end
MA --> Sandbox
NA --> TLS
PA --> FBE_d
SC --> AVB_d
40.1.8 Multi-User Security¶
Android supports multiple users on a single device. Each user gets:
- A unique
userId(0, 10, 11, ...). - Separate encrypted storage (CE and DE keys per user).
- Separate installed apps with per-user UIDs (appId + userId * 100000).
- Independent lock screen credentials.
- MLS (Multi-Level Security) categories in SELinux to prevent cross-user data access even within the same app.
The SELinux MLS categories are assigned based on the user ID, creating cryptographic separation between users at the MAC level.
40.1.9 Work Profile Security¶
Android's work profile (managed profile) extends multi-user security for enterprise use cases:
- Work apps run with a separate user ID (e.g., user 10).
- A separate encryption key protects work data.
- IT administrators can remotely wipe the work profile without affecting personal data.
- Cross-profile data sharing is controlled by the device policy controller.
- Work profile apps appear alongside personal apps but are sandboxed.
40.2 SELinux¶
SELinux (Security-Enhanced Linux) provides mandatory access control (MAC) on Android. Unlike traditional discretionary access control (DAC) where file owners control permissions, SELinux enforces a centralized policy that even root processes cannot override.
40.2.1 SELinux Architecture on Android¶
Android has shipped with SELinux in enforcing mode since Android 5.0. The policy is compiled at build time from source files under:
The directory structure (15 subdirectories) includes:
| Directory | Purpose |
|---|---|
public/ |
Type and attribute definitions visible to vendor policy |
private/ |
Platform-private policy (allow, neverallow rules) |
vendor/ |
Vendor HAL policies |
contexts/ |
File, property, service contexts |
mac_permissions/ |
MAC permissions XML for app signing |
build/ |
Build system integration |
compat/ |
Compatibility mappings between platform versions |
tools/ |
Policy analysis tools |
tests/ |
CTS-compatible policy tests |
apex/ |
APEX-specific policy |
prebuilts/ |
Prebuilt policy for API-level compatibility |
reqd_mask/ |
Required policy mask |
flagging/ |
Feature-flag-gated policy |
microdroid/ |
Policy for pVM microdroid |
treble_sepolicy_tests_for_release/ |
Treble compatibility tests |
40.2.2 Type Enforcement (TE)¶
SELinux type enforcement is the core mechanism. Every process is assigned a
domain (type) and every object (file, socket, binder, etc.) is assigned a
type. Access is granted only if an explicit allow rule exists.
The fundamental rule syntax is:
For example, here is the base policy for all zygote-spawned apps, from
system/sepolicy/public/app.te:
###
### Domain for all zygote spawned apps
###
### This file is the base policy for all zygote spawned apps.
### Other policy files, such as isolated_app.te, untrusted_app.te, etc
### extend from this policy. Only policies which should apply to ALL
### zygote spawned apps should be added here.
###
type appdomain_tmpfs, file_type;
Note the design comment: public/ contains only type and attribute
definitions, never allow or neverallow statements. Those go in
private/.
40.2.3 Domains and Attributes¶
Attributes are groups of types. They allow writing rules that apply to many
domains at once. From system/sepolicy/public/attributes (490 lines):
# All types used for devices.
attribute dev_type;
# All types used for processes.
attribute domain;
# All types used for filesystems.
attribute fs_type;
# All types used for files that can exist on a labeled fs.
attribute file_type;
# All types used for domain entry points.
attribute exec_type;
# All types used for /data files.
attribute data_file_type;
# All types used for app private data files in seapp_contexts.
attribute app_data_file_type;
The domain attribute is critical -- it is applied to every process type.
Rules in private/domain.te apply to every process on the system:
# Rules for all domains.
# Allow reaping by init.
allow domain init:process sigchld;
# Intra-domain accesses.
allow domain self:process {
fork
sigchld
sigkill
sigstop
signull
signal
getsched
setsched
getsession
getpgid
getcap
setcap
getattr
setrlimit
};
allow domain self:fd use;
allow domain proc:dir r_dir_perms;
40.2.4 Type Transitions¶
When a process executes a new binary, SELinux can automatically transition it
to a new domain. This is how init spawns daemons in the correct domain:
# When init runs /system/bin/vold, transition to vold domain
domain_auto_trans(init, vold_exec, vold)
The domain_auto_trans macro expands to:
type_transition init vold_exec:process vold;
allow init vold:process transition;
allow vold vold_exec:file { read getattr map execute entrypoint };
40.2.5 App Domain Assignment via seapp_contexts¶
The file system/sepolicy/private/seapp_contexts (216 lines) maps
applications to SELinux domains based on their properties:
# Input selectors:
# isSystemServer (boolean)
# isEphemeralApp (boolean)
# user (string)
# seinfo (string)
# name (string)
# isPrivApp (boolean)
# minTargetSdkVersion (unsigned integer)
# fromRunAs (boolean)
Sample mappings:
| Selector | Domain |
|---|---|
isSystemServer=true |
system_server |
user=system seinfo=platform |
system_app |
user=_app minTargetSdkVersion=37 |
untrusted_app |
user=_app minTargetSdkVersion=34 |
untrusted_app_34 |
user=_app minTargetSdkVersion=32 |
untrusted_app_32 |
user=_app minTargetSdkVersion=30 |
untrusted_app_30 |
user=_app minTargetSdkVersion=29 |
untrusted_app_29 |
user=_isolated |
isolated_app |
The versioned domains (untrusted_app_25, untrusted_app_27, etc.) allow
progressively tighter restrictions on older apps while maintaining backward
compatibility. Newer targetSdkVersion apps get the strictest rules.
40.2.6 Neverallow Rules¶
Neverallow rules are compile-time assertions. They do not generate runtime policy but instead prevent anyone (including vendor policy authors) from writing rules that violate the stated invariant. If a policy change would violate a neverallow, the build fails.
From system/sepolicy/private/app_neverallows.te (338 lines), here are
representative neverallow rules:
define(`all_untrusted_apps',`{
ephemeral_app
isolated_app
isolated_app_all
isolated_compute_app
mediaprovider
mediaprovider_app
untrusted_app
untrusted_app_25
untrusted_app_27
untrusted_app_29
untrusted_app_30
untrusted_app_all
}')
# Receive or send uevent messages.
neverallow all_untrusted_apps domain:netlink_kobject_uevent_socket *;
# Do not allow untrusted apps to register services.
neverallow all_untrusted_apps service_manager_type:service_manager add;
# Do not allow untrusted apps to use VendorBinder
neverallow all_untrusted_apps vndbinder_device:chr_file *;
# Do not allow untrusted apps to connect to the property service
neverallow { all_untrusted_apps -mediaprovider } property_socket:sock_file write;
neverallow { all_untrusted_apps -mediaprovider } init:unix_stream_socket connectto;
neverallow { all_untrusted_apps -mediaprovider } property_type:property_service set;
# Block calling execve() on files in an apps home directory.
# This is a W^X violation. For compatibility, allow for targetApi <= 28.
neverallow {
all_untrusted_apps
-untrusted_app_25
-untrusted_app_27
-runas_app
} { app_data_file privapp_data_file }:file execute_no_trans;
Key categories of neverallow rules for untrusted apps:
- No service registration -- apps cannot add services to servicemanager.
- No VendorBinder access -- apps cannot talk directly to vendor HALs.
- No property modification -- apps cannot set system properties.
- No kernel interface access -- no uevent sockets, no sysfs writes, no debugfs reads, no /proc sensitive files.
- No W^X violation -- apps with targetSdk >= 29 cannot execute files in their home directory.
- No hard links -- prevents installd deletion bypasses.
- Restricted socket types -- only TCP/UDP/VSOCK permitted, no raw/netlink.
40.2.7 HAL Neverallows¶
HAL servers also face restrictions. From system/sepolicy/private/hal_neverallows.te:
# only HALs responsible for network hardware should have privileged
# network capabilities
neverallow {
halserverdomain
-hal_bluetooth_server
-hal_wifi_server
-hal_wifi_hostapd_server
-hal_wifi_supplicant_server
-hal_telephony_server
-hal_uwb_server
...
} self:global_capability_class_set { net_admin net_raw };
This ensures that a compromised audio or camera HAL cannot gain network capabilities.
40.2.8 Vendor Sepolicy Split (Treble)¶
Project Treble introduced the split between platform and vendor sepolicy. The split enables independent platform and vendor updates:
graph LR
subgraph "Platform (Google)"
Public["public/<br/>Exported types & attrs"]
Private["private/<br/>Platform-only rules"]
end
subgraph "Vendor (OEM)"
Vendor["vendor/<br/>HAL-specific rules"]
end
Public --> Vendor
Private -.->|NOT visible| Vendor
subgraph "Compatibility"
Compat["compat/<br/>Version mappings"]
end
Compat --> Vendor
Rules for the split:
- public/ types and attributes are the API surface -- vendor policy can reference these.
- private/ types and rules are invisible to vendor policy. Vendor policy
cannot use
allowrules targeting private types. - compat/ contains mapping files that translate old type names to new ones across platform versions.
- vendor/ contains policy specific to the vendor's HAL implementations.
The vendor sepolicy directory contains files like:
vendor/
file.te
file_contexts
hal_atrace_default.te
hal_audio_default.te
hal_bluetooth_default.te
hal_camera_default.te
hal_fingerprint_default.te
hal_gatekeeper_default.te
hal_keymint_default.te
...
40.2.9 Global Domain Rules¶
The private/domain.te file contains rules that apply to every process on
the system. It is one of the most important policy files (hundreds of lines).
Representative rules from system/sepolicy/private/domain.te:
# Root fs.
allow domain tmpfs:dir { getattr search };
allow domain rootfs:dir search;
allow domain rootfs:lnk_file { read getattr };
# Device accesses.
allow domain device:dir search;
allow domain dev_type:lnk_file r_file_perms;
allow domain null_device:chr_file rw_file_perms;
allow domain zero_device:chr_file rw_file_perms;
# /dev/binder can be accessed by ... everyone! :)
allow { domain -hwservicemanager -vndservicemanager }
binder_device:chr_file rw_file_perms;
# Restrict binder ioctls to an allowlist.
allowxperm domain binder_device:chr_file
ioctl { unpriv_binder_ioctls };
# /dev/binderfs needs to be accessed by everyone too!
allow domain binderfs:dir { getattr search };
allow domain binderfs_features:dir search;
allow domain binderfs_features:file r_file_perms;
# Global access to cacerts, seccomp_policy, system libs
allow domain system_seccomp_policy_file:file r_file_perms;
allow domain system_security_cacerts_file:file r_file_perms;
allow domain system_linker_exec:file { execute read open getattr map };
allow domain system_lib_file:file { execute read open getattr map };
Note how even the global rules are carefully scoped. Binder access is
universal (it is the IPC backbone), but the allowed ioctls are restricted
to an unprivileged set. The hwservicemanager and vndservicemanager are
explicitly excluded from accessing the standard binder device because they
use their own (hwbinder_device, vndbinder_device).
40.2.10 The App Domain Policy (private/app.te)¶
The file system/sepolicy/private/app.te (844 lines) defines rules for all
zygote-spawned app processes. Key categories of access:
Keystore access:
allow { appdomain -isolated_app_all -ephemeral_app -sdk_sandbox_all }
keystore:keystore2_key { delete use get_info grant rebind update };
use_keystore({ appdomain -isolated_app_all -ephemeral_app -sdk_sandbox_all })
App sandbox file access:
# App sandbox file accesses.
allow { appdomain -isolated_app_all -mlstrustedsubject -sdk_sandbox_all } {
app_data_file
privapp_data_file
}:dir create_dir_perms;
allow { appdomain -isolated_app_all -mlstrustedsubject -sdk_sandbox_all } {
app_data_file
privapp_data_file
}:file create_file_perms;
Binder IPC:
# Use the Binder.
binder_use(appdomain)
# Perform binder IPC to binder services.
binder_call(appdomain, binderservicedomain)
# Perform binder IPC to other apps.
binder_call(appdomain, appdomain)
# Perform binder IPC to ephemeral apps.
binder_call(appdomain, ephemeral_app)
Neverallow rules in app.te (excerpts from the ~300 neverallow rules):
# Superuser capabilities.
neverallow { appdomain -bluetooth -network_stack -nfc }
self:capability_class_set *;
# Block device access.
neverallow appdomain dev_type:blk_file { read write };
# ptrace access to non-app domains.
neverallow appdomain { domain -appdomain }:process ptrace;
# The Android security model guarantees the confidentiality and
# integrity of application data and execution state. Ptrace bypasses
# those confidentiality guarantees.
neverallow {
domain
-appdomain
-crash_dump
} appdomain:process ptrace;
# Write to rootfs.
neverallow appdomain rootfs:dir_file_class_set
{ create write setattr relabelfrom relabelto append unlink link rename };
# Write to /system.
neverallow appdomain system_file_type:dir_file_class_set
{ create write setattr relabelfrom relabelto append unlink link rename };
# Write to system-owned parts of /data.
neverallow appdomain system_data_file:dir_file_class_set
{ create write setattr relabelfrom relabelto append unlink link rename };
# Transition to a non-app domain (prevent domain escalation).
neverallow { appdomain -shell }
{ domain -appdomain -crash_dump -rs -virtualizationmanager }:process
{ transition };
# Sensitive app domains are not allowed to execute from /data
# to prevent persistence attacks.
neverallow {
bluetooth
isolated_app_all
nfc
radio
shared_relro
sdk_sandbox_all
system_app
} {
data_file_type
-apex_art_data_file
-dalvikcache_data_file
-system_data_file
-apk_data_file
}:file no_x_file_perms;
# Don't allow apps access to character devices.
neverallow appdomain {
audio_device
camera_device
dm_device
radio_device
rpmsg_device
}:chr_file { read write };
# Apps cannot access proc/net tcp/udp tables.
neverallow { appdomain -shell } proc_net_tcp_udp:file *;
These neverallow rules collectively ensure that even if a framework bug allows a code path to be reached, the kernel-level MAC policy blocks the operation.
40.2.11 Policy Compilation and Loading¶
The SELinux policy is compiled at build time and loaded by init during
early boot. The build process:
- m4 preprocessing -- macros like
domain_auto_trans,app_domain,net_domainare expanded. - Policy compilation --
checkpolicycompiles.tefiles into a binary policy. - Context compilation --
sefcontext_compilecompilesfile_contextsinto binary format. - CIL compilation -- since Android 8.0, policy is compiled to CIL (Common Intermediate Language) for the platform/vendor split.
- Policy validation --
sepolicy_testsand neverallow checks run as build-time assertions. - Loading --
initloads the compiled policy from/system/etc/selinux/via/sys/fs/selinux/load.
The split policy loading flow:
sequenceDiagram
participant Build as Build System
participant Init as init (PID 1)
participant Kernel as Linux Kernel
Build->>Build: Compile platform policy (CIL)
Build->>Build: Compile vendor policy (CIL)
Build->>Build: Merge + check neverallows
Build->>Build: Generate binary policy
Init->>Init: Mount /system, /vendor
Init->>Kernel: Load precompiled policy (/vendor/etc/selinux/precompiled_sepolicy)
Kernel->>Kernel: Parse and install policy
Kernel->>Init: Policy active, all processes labeled
Init->>Init: Set SELinux to enforcing mode
40.2.12 Using audit2allow¶
When SELinux blocks an operation, it generates an audit log (a "denial").
The audit2allow tool converts these denials into candidate allow rules:
# Capture denials from the device
adb shell dmesg | grep 'avc: denied' > denials.txt
# Generate allow rules
audit2allow -i denials.txt
# Generate a loadable policy module
audit2allow -i denials.txt -M my_module
Example denial and generated rule:
# Denial:
avc: denied { read } for pid=1234 comm="my_daemon"
name="config.xml" dev="sda1" ino=5678
scontext=u:r:my_daemon:s0 tcontext=u:object_r:system_file:s0
tclass=file permissive=0
# audit2allow output:
allow my_daemon system_file:file read;
However, blindly applying audit2allow output is dangerous. The correct
approach is usually to create a more specific type for the target file rather
than granting broad access to system_file.
40.2.13 SELinux Contexts Files¶
Several context files map filesystem paths, properties, and services to SELinux labels:
| File | Purpose |
|---|---|
file_contexts |
Maps filesystem paths to file types |
property_contexts |
Maps system properties to types |
service_contexts |
Maps binder services to types |
hwservice_contexts |
Maps HIDL HW services to types |
seapp_contexts |
Maps apps to domains and data types |
mac_permissions.xml |
Maps app signatures to seinfo tags |
40.2.14 Common Debugging Techniques¶
When developing new system services or HALs, SELinux denials are common. Here is the systematic approach:
Step 1: Identify the denial
avc: denied { write } for pid=2456 comm="my_service"
path="/data/misc/my_service/config"
scontext=u:r:my_service:s0
tcontext=u:object_r:system_data_file:s0
tclass=file permissive=0
Step 2: Analyze the denial components
source:my_service(the process trying to access)target:system_data_file(the object being accessed)class:file(the type of object)permission:write(the operation attempted)
Step 3: Determine the correct fix
Wrong approach (too broad):
Correct approach (create specific type):
# In file.te:
type my_service_data_file, file_type, data_file_type;
# In file_contexts:
/data/misc/my_service(/.*)? u:object_r:my_service_data_file:s0
# In my_service.te:
allow my_service my_service_data_file:file create_file_perms;
allow my_service my_service_data_file:dir create_dir_perms;
Step 4: Verify with neverallow checks
After writing the policy, rebuild and check that no neverallow rules are violated.
40.2.15 SELinux MLS/MCS for User Isolation¶
Android uses MLS (Multi-Level Security) categories to isolate users and apps. Each app process receives categories based on its user ID and app ID:
The c42,c256,c512,c768 are MLS categories derived from the UID. Two
processes can only access each other's files if their categories are
compatible. This provides kernel-level isolation between:
- Different Android users (user 0 vs user 10).
- Different apps within the same user.
- Work profile apps vs personal apps.
The category assignment is controlled by seapp_contexts:
# levelFrom=all determines the level from both UID and user ID.
# levelFrom=user determines the level from the user ID.
# levelFrom=app determines the level from the process UID.
40.3 Verified Boot (AVB)¶
Android Verified Boot (AVB) ensures that all executed code comes from a trusted source rather than from an attacker or corruption. The implementation lives in:
40.3.1 AVB Architecture¶
The Verified Boot process establishes a chain of trust from hardware fuses to every partition:
flowchart TD
A["Hardware Root of Trust<br/>Boot ROM + Fused Public Key"] --> B[Bootloader Stage 1]
B --> C["Bootloader Stage 2<br/>Reads vbmeta partition"]
C --> D{"Verify vbmeta signature<br/>against fused key"}
D -->|Valid| E[Parse vbmeta descriptors]
D -->|Invalid| F["Show RED warning<br/>Refuse to boot or<br/>Allow with ORANGE warning"]
E --> G["Hash Descriptors<br/>Verify boot, dtbo images"]
E --> H["Hashtree Descriptors<br/>Set up dm-verity for<br/>system, vendor, product"]
E --> I["Chain Partition Descriptors<br/>Follow vbmeta chain to<br/>other partitions"]
G --> J[Load & boot kernel]
H --> J
I --> E
40.3.2 vbmeta Image Format¶
The vbmeta image is the core data structure. From
external/avb/libavb/avb_vbmeta_image.h:
/* Size of the vbmeta image header. */
#define AVB_VBMETA_IMAGE_HEADER_SIZE 256
/* Magic for the vbmeta image header. */
#define AVB_MAGIC "AVB0"
#define AVB_MAGIC_LEN 4
The image consists of three blocks:
+-----------------------------------------+
| Header data - fixed size (256 bytes) |
+-----------------------------------------+
| Authentication data - variable size |
+-----------------------------------------+
| Auxiliary data - variable size |
+-----------------------------------------+
The header structure from the source:
typedef struct AvbVBMetaImageHeader {
/* 0: Four bytes equal to "AVB0" (AVB_MAGIC). */
uint8_t magic[AVB_MAGIC_LEN];
/* 4: The major version of libavb required for this header. */
uint32_t required_libavb_version_major;
/* 8: The minor version of libavb required for this header. */
uint32_t required_libavb_version_minor;
/* 12: The size of the signature block. */
uint64_t authentication_data_block_size;
/* 20: The size of the auxiliary data block. */
uint64_t auxiliary_data_block_size;
/* 28: The verification algorithm used. */
uint32_t algorithm_type;
/* 32: Offset into the "Authentication data" block of hash data. */
uint64_t hash_offset;
/* 40: Length of the hash data. */
uint64_t hash_size;
/* 48: Offset into the "Authentication data" block of signature data. */
uint64_t signature_offset;
/* 56: Length of the signature data. */
uint64_t signature_size;
/* 64: Offset into the "Auxiliary data" block of public key data. */
uint64_t public_key_offset;
/* 72: Length of the public key data. */
uint64_t public_key_size;
/* 112: The rollback index for rollback protection. */
uint64_t rollback_index;
/* 120: Flags from the AvbVBMetaImageFlags enumeration. */
uint32_t flags;
/* 124: The location of the rollback index. */
uint32_t rollback_index_location;
/* 128: The release string from avbtool. */
uint8_t release_string[AVB_RELEASE_STRING_SIZE];
/* 176: Padding to ensure struct is size 256 bytes. */
uint8_t reserved[80];
} AVB_ATTR_PACKED AvbVBMetaImageHeader;
40.3.3 Verification Result Codes¶
The verification produces one of several results, defined in the same header:
typedef enum {
AVB_VBMETA_VERIFY_RESULT_OK,
AVB_VBMETA_VERIFY_RESULT_OK_NOT_SIGNED,
AVB_VBMETA_VERIFY_RESULT_INVALID_VBMETA_HEADER,
AVB_VBMETA_VERIFY_RESULT_UNSUPPORTED_VERSION,
AVB_VBMETA_VERIFY_RESULT_HASH_MISMATCH,
AVB_VBMETA_VERIFY_RESULT_SIGNATURE_MISMATCH,
} AvbVBMetaVerifyResult;
The bootloader must also verify that the embedded public key matches a known trusted key. As the source comments emphasize:
VERY IMPORTANT: Even if
AVB_VBMETA_VERIFY_RESULT_OKis returned, you still need to check that the public key embedded in the image matches a known key!
40.3.4 Slot Verification¶
The high-level API for verifying a complete slot is avb_slot_verify(), from
external/avb/libavb/avb_slot_verify.h:
AvbSlotVerifyResult avb_slot_verify(
AvbOps* ops,
const char* const* requested_partitions,
const char* ab_suffix,
AvbSlotVerifyFlags flags,
AvbHashtreeErrorMode hashtree_error_mode,
AvbSlotVerifyData** out_data);
Result codes indicate the nature of any failure:
typedef enum {
AVB_SLOT_VERIFY_RESULT_OK,
AVB_SLOT_VERIFY_RESULT_ERROR_OOM,
AVB_SLOT_VERIFY_RESULT_ERROR_IO,
AVB_SLOT_VERIFY_RESULT_ERROR_VERIFICATION,
AVB_SLOT_VERIFY_RESULT_ERROR_ROLLBACK_INDEX,
AVB_SLOT_VERIFY_RESULT_ERROR_PUBLIC_KEY_REJECTED,
AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_METADATA,
AVB_SLOT_VERIFY_RESULT_ERROR_UNSUPPORTED_VERSION,
AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_ARGUMENT
} AvbSlotVerifyResult;
The implementation in external/avb/libavb/avb_slot_verify.c defines key
constants:
/* Maximum number of partitions that can be loaded with avb_slot_verify(). */
#define MAX_NUMBER_OF_LOADED_PARTITIONS 32
/* Maximum number of vbmeta images that can be loaded. */
#define MAX_NUMBER_OF_VBMETA_IMAGES 32
/* Maximum size of a vbmeta image - 64 KiB. */
#define VBMETA_MAX_SIZE (64 * 1024)
40.3.5 Chain Partition Descriptors¶
Large systems split vbmeta across multiple partitions. A chain partition
descriptor points to another partition's vbmeta, creating a verification
tree. From external/avb/libavb/avb_chain_partition_descriptor.h:
typedef struct AvbChainPartitionDescriptor {
AvbDescriptor parent_descriptor;
uint32_t rollback_index_location;
uint32_t partition_name_len;
uint32_t public_key_len;
uint32_t flags;
uint8_t reserved[60];
} AVB_ATTR_PACKED AvbChainPartitionDescriptor;
This allows, for example, the vendor_boot partition to have its own
signing key and rollback index, independent of the main vbmeta.
40.3.6 Rollback Protection¶
Each vbmeta image contains a rollback_index -- a monotonically increasing
counter. The bootloader stores the minimum accepted rollback index in tamper-
evident storage (typically RPMB or fuse-based). If an attacker tries to flash
an older (vulnerable) image:
- The old image's rollback index is lower than the stored value.
avb_slot_verify()returnsAVB_SLOT_VERIFY_RESULT_ERROR_ROLLBACK_INDEX.- The bootloader refuses to boot the image.
There are up to 32 rollback index locations:
40.3.7 The AVB Footer¶
For partitions that contain both image data and vbmeta, the vbmeta is appended
after the image, and a footer at the very end of the partition points to it.
From external/avb/libavb/avb_footer.h:
#define AVB_FOOTER_MAGIC "AVBf"
#define AVB_FOOTER_SIZE 64
typedef struct AvbFooter {
uint8_t magic[AVB_FOOTER_MAGIC_LEN];
uint32_t version_major;
uint32_t version_minor;
uint64_t original_image_size;
uint64_t vbmeta_offset;
uint64_t vbmeta_size;
uint8_t reserved[28];
} AVB_ATTR_PACKED AvbFooter;
40.3.8 dm-verity for Runtime Protection¶
Verified Boot would be incomplete without runtime integrity checks. For read-only partitions (system, vendor, product), AVB sets up dm-verity -- a Linux device-mapper target that verifies each disk block against a Merkle tree hash on read.
The hashtree error modes determine what happens when corruption is detected:
typedef enum {
AVB_HASHTREE_ERROR_MODE_RESTART_AND_INVALIDATE,
AVB_HASHTREE_ERROR_MODE_RESTART,
AVB_HASHTREE_ERROR_MODE_EIO,
AVB_HASHTREE_ERROR_MODE_LOGGING,
AVB_HASHTREE_ERROR_MODE_MANAGED_RESTART_AND_EIO,
AVB_HASHTREE_ERROR_MODE_PANIC
} AvbHashtreeErrorMode;
In production, RESTART_AND_INVALIDATE is typical: the device invalidates the
corrupted slot and reboots, falling back to the other A/B slot if available.
40.3.9 Locked vs. Unlocked Bootloader¶
| State | Behavior |
|---|---|
| Locked | Only images signed with the OEM key boot. Verification failure = no boot. |
| Unlocked | Verification still runs but failures are permitted. ALLOW_VERIFICATION_ERROR flag is set. An ORANGE warning screen is shown. |
typedef enum {
AVB_SLOT_VERIFY_FLAGS_NONE = 0,
AVB_SLOT_VERIFY_FLAGS_ALLOW_VERIFICATION_ERROR = (1 << 0),
AVB_SLOT_VERIFY_FLAGS_RESTART_CAUSED_BY_HASHTREE_CORRUPTION = (1 << 1),
AVB_SLOT_VERIFY_FLAGS_NO_VBMETA_PARTITION = (1 << 2),
} AvbSlotVerifyFlags;
When a device is unlocked:
- All user data is wiped (factory reset).
- The bootloader shows a warning on every boot.
- Attestation certificates include the unlocked state, so relying parties (banks, enterprise MDM) know the device may be compromised.
40.3.10 The Verification Implementation¶
The actual verification logic in avb_slot_verify.c follows a careful pattern
of loading and checking each partition:
// From external/avb/libavb/avb_slot_verify.c
static inline bool result_should_continue(AvbSlotVerifyResult result) {
switch (result) {
case AVB_SLOT_VERIFY_RESULT_ERROR_OOM:
case AVB_SLOT_VERIFY_RESULT_ERROR_IO:
case AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_METADATA:
case AVB_SLOT_VERIFY_RESULT_ERROR_UNSUPPORTED_VERSION:
case AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_ARGUMENT:
return false;
case AVB_SLOT_VERIFY_RESULT_OK:
case AVB_SLOT_VERIFY_RESULT_ERROR_VERIFICATION:
case AVB_SLOT_VERIFY_RESULT_ERROR_ROLLBACK_INDEX:
case AVB_SLOT_VERIFY_RESULT_ERROR_PUBLIC_KEY_REJECTED:
return true;
}
return false;
}
This function determines whether verification should continue when
ALLOW_VERIFICATION_ERROR is set (unlocked bootloader). Fatal errors (OOM,
IO, bad metadata) always abort, but verification-related errors are allowed
to continue so the unlocked device can still boot.
The verification flow for each partition:
flowchart TD
A[Load vbmeta from partition] --> B{Footer present?}
B -->|Yes| C[Read footer to find vbmeta offset]
B -->|No| D[Read from start of partition]
C --> E[Load vbmeta data]
D --> E
E --> F[avb_vbmeta_image_verify]
F --> G{Magic == AVB0?}
G -->|No| H[INVALID_VBMETA_HEADER]
G -->|Yes| I{Version supported?}
I -->|No| J[UNSUPPORTED_VERSION]
I -->|Yes| K[Compute hash of header + aux data]
K --> L{Hash matches?}
L -->|No| M[HASH_MISMATCH]
L -->|Yes| N[Verify RSA signature]
N --> O{Signature valid?}
O -->|No| P[SIGNATURE_MISMATCH]
O -->|Yes| Q[Check public key against trusted set]
Q --> R{Key trusted?}
R -->|No| S[PUBLIC_KEY_REJECTED]
R -->|Yes| T[Check rollback index]
T --> U{Index >= stored?}
U -->|No| V[ERROR_ROLLBACK_INDEX]
U -->|Yes| W[Parse descriptors]
W --> X[Process hash/hashtree descriptors]
X --> Y[RESULT_OK]
40.3.11 Descriptor Types¶
AVB uses several descriptor types to describe partitions:
| Descriptor | Purpose |
|---|---|
| Hash | Stores the hash of an entire partition (boot, dtbo) |
| Hashtree | Stores the root hash of a Merkle tree (system, vendor) |
| Kernel Cmdline | Adds parameters to the kernel command line |
| Chain Partition | Points to another partition's vbmeta |
| Property | Stores key-value pairs |
Hash descriptors are used for small partitions where the entire image can be verified before use. Hashtree descriptors are for large partitions where block-by-block verification (dm-verity) is necessary.
40.3.12 dm-verity Merkle Tree¶
The Merkle tree for dm-verity is structured as follows:
Root Hash (stored in vbmeta)
/ \
Hash(L1[0]) Hash(L1[1])
/ \ / \
Hash(L0[0]) Hash(L0[1]) Hash(L0[2]) Hash(L0[3])
| | | |
Block 0 Block 1 Block 2 Block 3
(4096 B) (4096 B) (4096 B) (4096 B)
When any block is read from disk:
- The block's hash is computed (SHA-256).
- The hash is verified against its entry in the hash tree.
- The hash tree entry is verified against its parent.
- This chain continues up to the root hash.
- The root hash was already verified during boot by AVB.
If any block has been tampered with, the hash mismatch is detected and the configured error mode determines the response (restart, EIO, logging, panic).
40.3.13 avbtool Command Reference¶
The avbtool.py script at external/avb/avbtool.py provides the build-time
tooling:
| Command | Purpose |
|---|---|
make_vbmeta_image |
Create a standalone vbmeta image |
add_hash_footer |
Add a hash footer to a partition image |
add_hashtree_footer |
Add a hashtree footer to a partition image |
erase_footer |
Remove an AVB footer |
info_image |
Display AVB information about an image |
extract_public_key |
Extract the public key from a private key |
calculate_vbmeta_digest |
Calculate the digest of vbmeta images |
resize_image |
Resize a partition image |
set_ab_metadata |
Set A/B metadata |
40.3.14 Kernel Command-Line Parameters¶
AVB sets several androidboot.vbmeta.* kernel command-line parameters:
androidboot.veritymode:enforcing,eio,disabled, orloggingandroidboot.vbmeta.device_state:lockedorunlockedandroidboot.vbmeta.hash_alg,size,digest: integrity of vbmeta chainandroidboot.vbmeta.invalidate_on_error:yesfor restart-and-invalidate modeandroidboot.vbmeta.avb_version: the AVB library version (e.g., "1.0")androidboot.vbmeta.device: PARTUUID of the vbmeta partitionandroidboot.vbmeta.public_key_digest: SHA-256 of the signing public keyandroidboot.veritymode.managed:yesif using managed restart-and-EIO
From the source documentation:
androidboot.veritymode: This is set to 'disabled' if the
AVB_VBMETA_IMAGE_FLAGS_HASHTREE_DISABLED flag is set in top-level
vbmeta struct. Otherwise it is set to 'enforcing' if the
passed-in hashtree error mode is AVB_HASHTREE_ERROR_MODE_RESTART
or AVB_HASHTREE_ERROR_MODE_RESTART_AND_INVALIDATE, 'eio' if it's
set to AVB_HASHTREE_ERROR_MODE_EIO, and 'logging' if it's set to
AVB_HASHTREE_ERROR_MODE_LOGGING.
These parameters are consumed by the init process and Android framework
to determine the device's integrity state and adjust behavior accordingly.
40.3.15 Managed Verity Mode State Machine¶
The MANAGED_RESTART_AND_EIO error mode implements a state machine:
stateDiagram-v2
[*] --> Restart: Default mode
Restart --> EIO: dm-verity corruption detected, device restarts
note right of EIO: RESTART_CAUSED_BY_HASHTREE_CORRUPTION flag set on next boot
EIO --> Restart: New OS version detected, vbmeta digest changed
note right of Restart
Blocks corrupted data,
restarts device
end note
note right of EIO
Returns I/O errors to apps
instead of restarting
end note
This design prevents a boot loop: if corruption is persistent, the device switches to EIO mode (returning errors to apps) instead of continuously restarting. A RED warning screen is shown to inform the user.
40.3.16 Integration with A/B Updates¶
AVB works closely with the A/B update system:
- The current slot (e.g., slot A) is verified and booted.
- An OTA update is applied to slot B while A is running.
- After update, the bootloader switches the active slot to B.
- AVB verifies slot B's vbmeta and all partitions.
- If verification succeeds, slot B becomes the new default.
- If verification fails, the bootloader falls back to slot A.
- After successful boot, the rollback indexes may be updated to prevent rollback to the old version.
40.4 Keystore and KeyMint¶
40.4.1 Overview¶
The Android Keystore system provides hardware-backed cryptographic key storage and operations. The implementation has evolved through several generations:
| Generation | Interface | Since |
|---|---|---|
| Keymaster 0.x | C HAL | Android 4.3 |
| Keymaster 1.0 | HIDL 1.0 | Android 6.0 |
| Keymaster 2.0 | HIDL 2.0 | Android 7.0 |
| Keymaster 3.0 | HIDL 3.0 | Android 8.0 |
| Keymaster 4.0 | HIDL 4.0 | Android 9 |
| KeyMint 1.0 | AIDL | Android 12 |
| KeyMint 2.0 | AIDL | Android 13 |
| KeyMint 3.0 | AIDL | Android 14 |
The Keymaster-to-KeyMint transition moved from HIDL to AIDL and introduced improvements in attestation, key upgrade, and remote provisioning.
40.4.2 Keystore2 Architecture¶
The Keystore 2.0 service is implemented in Rust at
system/security/keystore2/. The main library (src/lib.rs) exposes these
modules:
//! This crate implements the Android Keystore 2.0 service.
pub mod apc;
pub mod async_task;
pub mod authorization;
pub mod boot_level_keys;
pub mod database;
pub mod ec_crypto;
pub mod enforcements;
pub mod entropy;
pub mod error;
pub mod globals;
pub mod id_rotation;
pub mod key_parameter;
pub mod legacy_blob;
pub mod legacy_importer;
pub mod maintenance;
pub mod metrics;
pub mod metrics_store;
pub mod operation;
pub mod permission;
pub mod raw_device;
pub mod remote_provisioning;
pub mod security_level;
pub mod service;
pub mod shared_secret_negotiation;
pub mod utils;
mod attestation_key_utils;
mod audit_log;
mod gc;
mod km_compat;
mod super_key;
mod sw_keyblob;
mod watchdog_helper;
graph TB
subgraph "Application"
App[App using android.security.keystore]
end
subgraph "Framework"
KSM["KeyStore Service<br/>system/security/keystore2"]
DB["(SQLite Database<br/>Persistent key entries)"]
BootDB["(Per-boot Database<br/>Grants)"]
end
subgraph "HAL Layer"
KM["IKeyMintDevice<br/>AIDL HAL"]
SC[ISecureClock]
SS[ISharedSecret]
end
subgraph "Secure World"
TA["KeyMint TA<br/>in TEE/StrongBox"]
end
App -->|Binder IPC| KSM
KSM --> DB
KSM --> BootDB
KSM -->|Binder IPC| KM
KSM -->|Binder IPC| SC
KSM -->|Binder IPC| SS
KM -->|TIPC / SMC| TA
40.4.3 Security Levels¶
Keystore2 supports multiple security levels. The KeystoreSecurityLevel
struct in system/security/keystore2/src/security_level.rs:
/// Implementation of the IKeystoreSecurityLevel Interface.
pub struct KeystoreSecurityLevel {
security_level: SecurityLevel,
keymint: Strong<dyn IKeyMintDevice>,
hw_info: KeyMintHardwareInfo,
km_uuid: Uuid,
operation_db: OperationDb,
rem_prov_state: RemProvState,
id_rotation_state: IdRotationState,
}
The three security levels are:
| Level | Description |
|---|---|
SOFTWARE |
Keys stored in software (TEE unavailable) |
TRUSTED_ENVIRONMENT |
Keys in the TEE (ARM TrustZone, etc.) |
STRONGBOX |
Keys in a dedicated secure element |
40.4.4 Database Module¶
The database design is documented in system/security/keystore2/src/database.rs:
//! This is the Keystore 2.0 database module.
//! The database module provides a connection to the backing SQLite store.
//! We have two databases one for persistent key blob storage and one for
//! items that have a per boot life cycle.
//!
//! ## Persistent database
//! The persistent database has tables for key blobs. They are organized
//! as follows:
//! The `keyentry` table is the primary table for key entries. It is
//! accompanied by two tables for blobs and parameters.
//! Each key entry occupies exactly one row in the `keyentry` table and
//! zero or more rows in the tables `blobentry` and `keyparameter`.
//!
//! ## Per boot database
//! The per boot database stores items with a per boot lifecycle.
//! Currently, there is only the `grant` table in this database.
//! Grants are references to a key that can be used to access a key by
//! clients that don't own that key.
40.4.5 Access Control via SELinux¶
Keystore2 uses SELinux for fine-grained access control. The permission module
(system/security/keystore2/src/permission.rs) defines the keystore2_key
SELinux class:
implement_class!(
/// KeyPerm provides a convenient abstraction from the SELinux class
/// `keystore2_key`.
#[selinux(class_name = keystore2_key)]
pub enum KeyPerm {
/// Checked when convert_storage_key_to_ephemeral is called.
#[selinux(name = convert_storage_key_to_ephemeral)]
ConvertStorageKeyToEphemeral = ...,
/// Checked when the caller tries to delete a key.
#[selinux(name = delete)]
Delete = ...,
/// Checked when the caller tries to use a unique id.
#[selinux(name = gen_unique_id)]
GenUniqueId = ...,
/// Checked when the caller tries to load a key.
#[selinux(name = get_info)]
GetInfo = ...,
/// Checked when the caller attempts to grant a key to another uid.
#[selinux(name = grant)]
Grant = ...,
/// Checked when the caller attempts to use Domain::BLOB.
#[selinux(name = manage_blob)]
ManageBlob = ...,
}
);
This means every key operation is checked against the caller's SELinux context, not just their UID. A process must have both the right UID (or a valid grant) AND the right SELinux permissions.
40.4.6 Enforcements Module¶
The enforcements module (system/security/keystore2/src/enforcements.rs)
handles authentication requirements for key operations. Key use can require:
- User authentication -- a recent unlock (PIN, pattern, password, or biometric) is required before the key can be used.
- Per-operation authentication -- the user must authenticate for each individual cryptographic operation.
- Timeout-based authentication -- authentication is valid for a specified time window.
- Boot-level keys -- keys that become inaccessible after a certain boot phase completes.
#[derive(Debug)]
enum AuthRequestState {
/// An outstanding per operation authorization request.
OpAuth,
/// An outstanding request for a timestamp token.
TimeStamp(Mutex<Receiver<Result<TimeStampToken, Error>>>),
}
#[derive(Debug)]
struct AuthRequest {
state: AuthRequestState,
/// This need to be set to Some to fulfill an AuthRequestState::OpAuth.
hat: Mutex<Option<HardwareAuthToken>>,
}
40.4.7 Key Attestation¶
Key attestation proves to a remote party that a key was generated inside secure hardware. The attestation chain consists of:
- Attestation certificate -- signed by the TEE's attestation key, containing the key's properties (algorithm, purpose, auth requirements).
- Intermediate certificate(s) -- linking the TEE's key to a root.
- Root certificate -- the Google Hardware Attestation Root or the manufacturer's root.
The attestation includes the device's verified boot state (locked/unlocked)
and the OS version, enabling relying parties to make trust decisions.
40.4.8 Operation Lifecycle¶
Keystore2 manages cryptographic operations with a well-defined lifecycle.
From system/security/keystore2/src/operation.rs:
//! Operations implement the API calls update, finish, and abort.
//! Additionally, an operation can be dropped and pruned. The former
//! happens if the client deletes a binder to the operation object.
//! An existing operation may get pruned when running out of operation
//! slots and a new operation takes precedence.
//!
//! ## Operation Lifecycle
//! An operation gets created when the client calls
//! `IKeystoreSecurityLevel::create`.
//! It may receive zero or more update request. The lifecycle ends when:
//! * `update` yields an error.
//! * `finish` is called.
//! * `abort` is called.
//! * The operation gets dropped.
//! * The operation gets pruned.
The operation pruning strategy is important for devices with limited TEE resources. When a new operation is requested but all slots are full:
sequenceDiagram
participant App as New App
participant KS2 as Keystore2
participant DB as OperationDb
participant TEE as KeyMint TA
App->>KS2: createOperation()
KS2->>DB: Check available slots
DB->>KS2: All slots full
KS2->>DB: Find least-recently-used operation
DB->>KS2: Operation from older app
KS2->>TEE: Abort pruned operation
TEE->>KS2: Operation freed
KS2->>TEE: Begin new operation
TEE->>KS2: Operation handle
KS2->>App: CreateOperationResponse
40.4.9 Super Keys and User Authentication¶
The super key module (system/security/keystore2/src/super_key.rs) manages
the keys that protect per-user Keystore data. There are several types of
super encryption:
/// Encryption algorithm used by a particular type of superencryption key
pub enum SuperEncryptionAlgorithm {
/// Symmetric encryption with AES-256-GCM
Aes256Gcm,
/// Asymmetric encryption with ECDH P-521
EcdhP521,
}
/// Specify which keys should be wiped given a particular user's UserSuperKeys
pub enum WipeKeyOption {
/// Wipe unlocked_device_required_symmetric/private and biometric_unlock keys
PlaintextAndBiometric,
/// Wipe only unlocked_device_required_symmetric/private keys
PlaintextOnly,
}
The biometric unlock timeout is carefully tuned:
/// Allow up to 15 seconds between the user unlocking using a biometric, and
/// the auth token being used to unlock in
/// [`SuperKeyManager::try_unlock_user_with_biometric`].
/// This seems short enough for security purposes, while long enough that even
/// the very slowest device will present the auth token in time.
const BIOMETRIC_AUTH_TIMEOUT_S: i32 = 15; // seconds
40.4.10 Key Garbage Collection¶
The key garbage collector (system/security/keystore2/src/gc.rs) handles
secure deletion of key material:
//! This module implements the key garbage collector.
//! The key garbage collector has one public function `notify_gc()`.
//! This will create a thread on demand which will query the database
//! for unreferenced key entries, optionally dispose of sensitive key
//! material appropriately, and then delete the key entry from the
//! database.
When a key is deleted:
- The database entry is marked for deletion.
- The GC thread wakes up.
- If the key has a hardware-backed blob, the HAL is called to delete it from the TEE.
- The database entry and all associated blobs are removed.
- The underlying storage blocks are overwritten (secure discard).
40.4.11 Remote Key Provisioning (RKP)¶
Starting with Android 12, devices support Remote Key Provisioning. Instead of burning attestation keys in the factory, keys are provisioned from a Google backend after the device passes integrity checks. The relevant modules are:
Benefits:
- Eliminates factory key injection infrastructure.
- Supports key rotation and revocation at scale.
- Reduces the blast radius of key compromise.
Keystore2 decides whether to route attestation through RKP in
RemProvState::get_rkpd_attestation_key_and_certs
(system/security/keystore2/src/remote_provisioning.rs). RKP is attempted
only for asymmetric keys (is_asymmetric_key) bound to an app domain, and
only when RKP is actually enabled on the device. The hardening details that
landed for Android 17 are covered in section 40.10.5.
The Keystore2 logic above is only the consumer. The work of talking to the RKP backend, generating certificate signing requests, and caching the signed keys belongs to a separate mainline module described next.
40.4.11.1 The RKPD Mainline Module¶
The provisioning daemon is the com.android.rkpd APEX, defined in
packages/modules/RemoteKeyProvisioning. The APEX manifest
(apex/manifest.json) names it com.android.rkpd, and the payload is the
rkpdapp Android application under app/
(com.android.rkpdapp, registered for preinstall in
app/sysconfig/preinstalled-packages-com.android.rkpdapp.xml). Packaging the
daemon as an APEX-bundled app lets the provisioning logic and its server
protocol ship updates over the network without a full system OTA.
The daemon is split into three pieces:
| Piece | Location | Role |
|---|---|---|
rkpdapp |
app/src/com/android/rkpdapp/ |
The provisioning app: server protocol, CSR generation, key database |
| system-server shim | system-server/src/android/security/rkp/service/ |
RegistrationProxy / RemotelyProvisionedKey, the in-process API Keystore2 and other callers bind to |
| registration util | util/src/com/android/rkpdapp/ |
RkpRegistrationCheck, a command-line check of provisioning status |
The shim exists because rkpdapp cannot host a system service itself and needs
network access that system_server is not allowed to perform. As the
system-server/README.md explains, system_server hosts a visible service and
proxies every call through to the rkpdapp process, which does the network
work. The app exposes two components in app/AndroidManifest.xml: the
RemoteProvisioningService bound through the com.android.rkpdapp.IRemoteProvisioning
action, and PeriodicProvisioner, a WorkManager Worker
(app/src/com/android/rkpdapp/provisioner/PeriodicProvisioner.java) that
refills the key pool on a schedule.
When a caller asks for a registration,
RemoteProvisioningService.getRegistration
(app/src/com/android/rkpdapp/service/RemoteProvisioningService.java) first
rejects the request if no provisioning URL is configured (Settings.getDefaultUrl
empty), and restricts binders to the system UID and itself. It then resolves
the named IRemotelyProvisionedComponent through ServiceManagerInterface and
hands back an IRegistration backed by RegistrationBinder.
40.4.11.2 The Provisioning Round-Trip¶
Provisioner.provisionKeys
(app/src/com/android/rkpdapp/provisioner/Provisioner.java) drives the
end-to-end flow against two collaborators: SystemInterface, which wraps the
KeyMint IRemotelyProvisionedComponent HAL, and ServerInterface, which speaks
HTTP to the RKP backend (app/src/com/android/rkpdapp/interfaces/).
The HAL side generates the raw material. SystemInterface.generateKey calls
IRemotelyProvisionedComponent.generateEcdsaP256KeyPair, which returns a
MacedPublicKey (a public key MAC'd by a device-private key the host never
sees). A batch of these is then passed to SystemInterface.generateCsr, which
calls generateCertificateRequestV2 (the v3 HAL path) or the older
generateCertificateRequest plus DeviceInfo / ProtectedData (v1), producing
a CBOR certificate signing request.
The server side runs in two HTTP operations defined in ServerInterface:
fetchGeekGETs:fetchEekChain, retrieving the Google Endpoint Encryption Key chain (GEEK) and a challenge. The GEEK chain is what the CSR is encrypted to, binding the request to the genuine backend.requestSignedCertificatesPOSTs the CSR to:signCertificates, and the backend returns an X.509 certificate chain for each provisioned key.
Provisioner stores the signed keys in a Room database
(app/src/com/android/rkpdapp/database/, ProvisionedKeyDao /
ProvisionedKey) keyed by the owning IRemotelyProvisionedComponent and an
expiration time, so Keystore2 can later draw a pre-attested key from the pool
without a fresh network round-trip. WidevineProvisioner reuses the same
machinery for the Widevine DRM component.
sequenceDiagram
participant KS as "Keystore2 (rkpd_client)"
participant SS as "system_server shim (RegistrationProxy)"
participant APP as "rkpdapp (Provisioner)"
participant HAL as "IRemotelyProvisionedComponent (KeyMint HAL)"
participant SRV as "RKP backend"
KS->>SS: getRegistration(irpcName)
SS->>APP: bind IRemoteProvisioning
APP->>HAL: generateEcdsaP256KeyPair()
HAL-->>APP: MacedPublicKey[]
APP->>SRV: GET :fetchEekChain (GEEK + challenge)
SRV-->>APP: GEEK chain
APP->>HAL: generateCertificateRequestV2(keys, challenge)
HAL-->>APP: CSR (CBOR)
APP->>SRV: POST :signCertificates (CSR)
SRV-->>APP: X.509 cert chains
APP->>APP: store in ProvisionedKey DB
KS->>SS: getKey() draws pooled key + chain
Behavior is gated by aconfig flags in flags/rkpd_flags.aconfig
(com.android.rkpd.flags), including enable_feedback_loop (report cert use
back to the server via ConfirmCertificates), report_device_reset, and
enable_widevine_multiple_round_trips. The post-processing that may rewrite
the returned chain, and the Keystore2-side gating and concurrency cap, are
separate and covered in sections 40.10.5 and 40.10.6.
40.4.12 KeyMint AIDL Interface¶
The KeyMint HAL is defined in AIDL at
hardware/interfaces/security/keymint/aidl/. Key AIDL files include:
| File | Purpose |
|---|---|
IKeyMintDevice.aidl |
Main device interface (generateKey, importKey, begin) |
IKeyMintOperation.aidl |
Per-operation interface (update, finish, abort) |
SecurityLevel.aidl |
TEE, StrongBox, Software levels |
Algorithm.aidl |
RSA, EC, ML_DSA, AES, HMAC, 3DES |
MlDsaVariant.aidl |
Post-quantum ML-DSA-65 / ML-DSA-87 selector (Android 17) |
KeyCharacteristics.aidl |
Key properties returned from generateKey |
KeyCreationResult.aidl |
Key blob + characteristics + certificates |
HardwareAuthToken.aidl |
Authentication token structure |
Tag.aidl |
Key parameter tags (PURPOSE, ALGORITHM, KEY_SIZE, ML_DSA_VARIANT, etc.) |
ErrorCode.aidl |
Detailed error codes (including UNSUPPORTED_ML_DSA_VARIANT) |
Algorithm.aidl gained an ML_DSA = 4 value in Android 17, introducing the
first post-quantum key type into the HAL; the full story is in section 40.10.
The IKeyMintDevice interface defines these core operations:
interface IKeyMintDevice {
KeyMintHardwareInfo getHardwareInfo();
void addRngEntropy(in byte[] data);
KeyCreationResult generateKey(in KeyParameter[] keyParams,
in AttestationKey attestationKey);
KeyCreationResult importKey(in KeyParameter[] keyParams,
in KeyFormat keyFormat,
in byte[] keyData,
in AttestationKey attestationKey);
KeyCreationResult importWrappedKey(in byte[] wrappedKeyData,
in byte[] wrappingKeyBlob,
in byte[] maskingKey, ...);
byte[] upgradeKey(in byte[] keyBlobToUpgrade,
in KeyParameter[] upgradeParams);
void deleteKey(in byte[] keyBlob);
void deleteAllKeys();
void destroyAttestationIds();
BeginResult begin(in KeyPurpose purpose,
in byte[] keyBlob,
in KeyParameter[] params,
in HardwareAuthToken authToken);
void deviceLocked(in boolean passwordOnly,
in @nullable TimeStampToken timestampToken);
void earlyBootEnded();
...
}
deviceLocked is marked @deprecated in
hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/IKeyMintDevice.aidl:
the comment notes the method "has never been used due to design limitations,"
so implementations should return ErrorCode::UNIMPLEMENTED. The
TimeStampToken argument is therefore vestigial here. Where authenticated
timestamps actually come from in Android 17 is described in section 40.10.3.
40.4.13 Keystore2 Authorization Flow¶
The authorization flow for a key operation involves multiple checks:
flowchart TD
A[App calls generateKey/begin] --> B[Keystore2 receives Binder call]
B --> C{"Check SELinux permission<br/>keystore2_key class"}
C -->|Denied| D[Return PERMISSION_DENIED]
C -->|Allowed| E{"Check UID-based ownership<br/>or grant"}
E -->|Not owner, no grant| D
E -->|Owner or valid grant| F{Key auth-bound?}
F -->|No| G[Proceed to KeyMint TA]
F -->|Yes| H{Valid HardwareAuthToken?}
H -->|No| I[Return KEY_USER_NOT_AUTHENTICATED]
H -->|Yes| J{Token recent enough?}
J -->|No| I
J -->|Yes| G
G --> K[KeyMint TA processes request]
K --> L{TA-level checks pass?}
L -->|No| M[Return error code]
L -->|Yes| N[Return result to app]
This multi-layer authorization ensures:
- SELinux prevents unauthorized access even from root.
- UID checks prevent cross-app key access.
- Auth-binding requires recent user authentication.
- The TEE performs its own validation independent of the framework.
40.4.14 Key Import and Wrapping¶
Keystore2 supports importing existing keys into hardware:
- Plain import: the key material is sent to the TEE, which wraps it with a device-bound key. The original plaintext key exists briefly in transit.
- Wrapped import: the key is wrapped by a transport key before leaving the source. The TEE unwraps it internally, so the plaintext key never exists outside secure hardware. This is used for bulk key provisioning.
40.4.15 StrongBox¶
StrongBox is a dedicated secure element (SE) that provides the highest security level. Unlike the TEE, which shares the main CPU, StrongBox uses a separate processor with its own:
- CPU
- Secure storage
- True random number generator
- Tamper-resistance mechanisms
- Independent clock
StrongBox supports a subset of KeyMint algorithms and is mandatory for devices launching with Android 9+ (for certain key types).
40.5 TEE: Trusty¶
40.5.1 Overview¶
Trusty is Google's open-source Trusted Execution Environment (TEE) operating system. It runs alongside Android in the ARM TrustZone secure world (or analogous isolation on other architectures). The source tree is:
trusty/
kernel/ - Trusty kernel (Little Kernel based)
user/ - Userspace TAs (Trusted Applications)
device/ - Device-specific configurations
hardware/ - Hardware abstraction
host/ - Host-side tools
vendor/ - Vendor TAs
The Android-side integration for communicating with Trusty lives in:
system/core/trusty/
keymint/ - KeyMint HAL backed by Trusty
keymaster/ - Legacy Keymaster HAL backed by Trusty
gatekeeper/ - Gatekeeper HAL backed by Trusty
storage/ - Secure storage proxy
secretkeeper/ - SecretKeeper HAL for pVM secrets
confirmationui/ - Protected Confirmation UI
metrics/ - TEE metrics reporting
libtrusty/ - IPC library for talking to Trusty
libtrusty-rs/ - Rust bindings for libtrusty
40.5.2 TrustZone Architecture¶
ARM TrustZone divides the SoC into two worlds:
graph LR
subgraph "Normal World (Android)"
Linux[Linux Kernel]
HAL[HAL Services]
Apps[Applications]
end
subgraph "Secure World (Trusty)"
TrustyKernel["Trusty Kernel<br/>Little Kernel based"]
KM_TA[KeyMint TA]
GK_TA[Gatekeeper TA]
Storage_TA[Storage TA]
CUI_TA[ConfirmationUI TA]
end
subgraph "Hardware"
Monitor["Secure Monitor<br/>EL3"]
TZASC["TrustZone ASC<br/>Memory Protection"]
end
Linux -->|SMC instruction| Monitor
Monitor --> TrustyKernel
HAL --> Linux
TrustyKernel --> KM_TA
TrustyKernel --> GK_TA
TrustyKernel --> Storage_TA
TrustyKernel --> CUI_TA
Key properties of TrustZone:
- Hardware-enforced isolation -- the normal world physically cannot access secure world memory. The TrustZone Address Space Controller (TZASC) blocks all normal world bus transactions to secure memory regions.
- SMC (Secure Monitor Call) -- the only entry point from normal to secure world. This is a privileged ARM instruction that traps to EL3 (the secure monitor), which then dispatches to the Trusty kernel.
- Separate address spaces -- Trusty has its own page tables, separate from Linux.
40.5.3 The SMC Communication Path¶
sequenceDiagram
participant App as Android App
participant HAL as KeyMint HAL (Normal World)
participant LibTrusty as libtrusty
participant KernelDriver as Trusty IPC Driver (Linux kernel)
participant Monitor as Secure Monitor (EL3)
participant TrustyKernel as Trusty Kernel (Secure World)
participant TA as KeyMint TA
App->>HAL: generateKey() via Binder
HAL->>LibTrusty: tipc_connect() + send()
LibTrusty->>KernelDriver: ioctl(TIPC_IOC_CONNECT)
KernelDriver->>Monitor: SMC instruction
Monitor->>TrustyKernel: Dispatch to secure world
TrustyKernel->>TA: Route to KeyMint TA
TA->>TA: Generate key in secure memory
TA->>TrustyKernel: Response
TrustyKernel->>Monitor: Return to normal world
Monitor->>KernelDriver: SMC return
KernelDriver->>LibTrusty: ioctl returns
LibTrusty->>HAL: Response bytes
HAL->>App: KeyCreationResult via Binder
40.5.4 libtrusty -- The IPC Library¶
The libtrusty library (system/core/trusty/libtrusty/trusty.c) provides the
normal-world interface for connecting to Trusty TAs. It supports two
transport mechanisms:
- TIPC over kernel driver -- uses
/dev/trusty-ipc-dev0and ioctl calls. - TIPC over VSOCK -- for virtual machine environments where Trusty runs in a separate VM.
static bool use_vsock_connection = false;
static int tipc_vsock_connect(const char* type_cid_port_str,
const char* srv_name) {
// Parse "STREAM:cid:port" or "SEQPACKET:cid:port"
int fd = socket(AF_VSOCK, socket_type, 0);
// Connect to the Trusty VM...
}
40.5.5 KeyMint HAL in Trusty¶
The Trusty KeyMint HAL (system/core/trusty/keymint/src/keymint_hal_main.rs)
is implemented in Rust. It connects to the Trusty KeyMint TA over TIPC:
const TRUSTY_KEYMINT_RUST_SERVICE_NAME: &str = "com.android.trusty.keymint";
impl SerializedChannel for TipcChannel {
const MAX_SIZE: usize = 4000;
fn execute(&mut self, serialized_req: &[u8]) -> binder::Result<Vec<u8>> {
self.0.send(serialized_req).map_err(|e| { ... })?;
let mut expect_more_msgs = true;
let mut full_rsp = Vec::new();
while expect_more_msgs {
let mut recv_buf = Vec::new();
self.0.recv(&mut recv_buf).map_err(|e| { ... })?;
let current_rsp_content;
(expect_more_msgs, current_rsp_content) = extract_rsp(&recv_buf)?;
full_rsp.extend_from_slice(current_rsp_content);
}
Ok(full_rsp)
}
}
The main function connects to the TA and registers all HAL services:
fn inner_main() -> Result<(), HalServiceError> {
// Create connection to the TA
let connection =
trusty::TipcChannel::connect(args.dev.as_str(),
TRUSTY_KEYMINT_RUST_SERVICE_NAME)
.map_err(|e| { ... })?;
let tipc_channel = Arc::new(Mutex::new(TipcChannel(connection)));
register_binder_services(&tipc_channel, ALL_HALS, SERVICE_INSTANCE)?;
// Send the HAL service information to the TA
send_hal_info(tipc_channel.lock().unwrap().deref_mut())?;
binder::ProcessState::join_thread_pool();
Ok(())
}
The tamper-evident storage that backs counters such as the Keymaster rollback
state is modeled by the access-controlled NVRAM HAL. AOSP ships a reference
implementation under system/nvram/ (core/ is the portable reference logic; a
userspace "fake" HAL implements the full API surface for illustration but, as
its README states, does not meet the persistence and tamper-evidence
requirements of a real deployment) -- a production device backs these NVRAM
spaces with hardware such as RPMB or a TEE.
40.5.6 Confirmation UI¶
Protected Confirmation (Confirmation UI) displays a trusted prompt to the
user that cannot be spoofed by malware. The Trusty implementation lives in
system/core/trusty/confirmationui/.
class TrustyConfirmationUI : public BnConfirmationUI {
public:
::ndk::ScopedAStatus
promptUserConfirmation(
const shared_ptr<IConfirmationResultCallback>& resultCB,
const vector<uint8_t>& promptText,
const vector<uint8_t>& extraData,
const string& locale,
const vector<UIOption>& uiOptions) override;
::ndk::ScopedAStatus
deliverSecureInputEvent(
const HardwareAuthToken& secureInputToken) override;
::ndk::ScopedAStatus abort() override;
};
The TA in the secure world controls the display directly (or via a secure
display path), ensuring the normal world OS cannot modify what the user sees.
The user's confirmation is signed by the TA, producing a
HardwareAuthToken that cryptographically proves the user approved the
displayed content.
The actual on-screen prompt is drawn by a small, dependency-light C++ rendering
engine, libteeui (system/teeui/libteeui/). It rasterizes the confirmation
layout -- text labels, buttons, and fonts -- using a bundled FreeType build
(libft2.nodep) so the renderer can run inside the resource-constrained secure
world with no reliance on the normal-world UI toolkit. The Trusty
ConfirmationUI HAL links the libteeui_hal_support variant of this library
(see its static_libs in system/core/trusty/confirmationui/Android.bp), which
supplies the message-formatting and secure-input glue around the core renderer.
40.5.7 SecretKeeper¶
The SecretKeeper HAL (system/core/trusty/secretkeeper/) manages secrets
for protected Virtual Machines (pVMs). It ensures that secrets bound to a
specific VM identity are only released to authenticated VMs, supporting
the Android Virtualization Framework's security model.
40.5.8 Trusty Build and Configuration¶
Trusty device configurations live in trusty/device/ and
trusty/vendor/. The Android-side build integration is handled by
makefiles in system/core/trusty/:
trusty-base.mk-- base Trusty configurationtrusty-storage-cf.mk-- Cuttlefish (emulator) storage configurationtrusty-storage.mk-- production storage configurationtrusty-keymint-apex.mk-- APEX packaging for KeyMinttrusty-keymint.mk-- KeyMint HAL build rulestrusty-test.mk-- Test configuration
40.5.9 Trusty Kernel Architecture¶
The Trusty kernel is based on Little Kernel (LK), a small real-time OS designed for resource-constrained environments. Key properties:
- Microkernel design -- minimal kernel with IPC, scheduling, and memory management. TAs run as userspace processes.
- Capability-based security -- TAs request capabilities (storage access, crypto hardware, etc.) at build time.
- No dynamic loading -- all TAs are loaded at boot from a signed image. No runtime code loading is permitted.
- Small TCB -- the Trusted Computing Base is much smaller than Linux, reducing the attack surface.
40.5.10 Trusty vs Other TEEs¶
Android supports multiple TEE implementations:
| TEE | Provider | Key Properties |
|---|---|---|
| Trusty | Google (open source) | LK-based, reference implementation |
| OP-TEE | Linaro (open source) | Linux-style API, widely used in SBCs |
| QSEE | Qualcomm (proprietary) | Used on Snapdragon SoCs |
| Kinibi | Trustonic (proprietary) | Used on Samsung Exynos, MediaTek |
| iTrustee | Huawei (proprietary) | Used on Kirin SoCs |
The Android HAL interfaces (KeyMint, Gatekeeper, etc.) are TEE-agnostic.
The HAL implementation translates between the Android AIDL interface and the
specific TEE's native API. This is why system/core/trusty/keymint/ exists
specifically for Trusty, while other TEE vendors provide their own HAL
implementations.
40.5.11 TIPC Protocol¶
Trusty IPC (TIPC) is a message-based protocol:
graph LR
subgraph "Normal World"
Client[Client Process]
Driver[TIPC Kernel Driver]
end
subgraph "Shared Memory"
Ring["Ring Buffer<br/>Message Queue"]
end
subgraph "Secure World"
Dispatcher[TIPC Dispatcher]
TA[Trusted Application]
end
Client -->|ioctl| Driver
Driver -->|Write to shared memory| Ring
Driver -->|SMC| Dispatcher
Dispatcher -->|Read from shared memory| Ring
Dispatcher -->|Deliver message| TA
TA -->|Response to shared memory| Ring
TA -->|SMC return| Driver
TIPC channels have these properties:
- Maximum message size of 4000 bytes (as seen in the KeyMint HAL).
- Messages exceeding this limit are split and reassembled.
- Channels are authenticated by the Trusty kernel, which knows the normal-world caller's identity.
40.5.12 Secure Storage¶
The Trusty Storage proxy (system/core/trusty/storage/) provides persistent
storage for TAs. Since the secure world typically cannot directly access the
filesystem, the proxy runs in the normal world and services storage requests
from TAs through a secure protocol:
Normal World Secure World
+------------------+ +------------------+
| Storage Proxy |<-- TIPC -->| Storage TA |
| (runs as daemon) | | (in Trusty) |
| Writes to | | Encrypts data |
| /data/vendor/ss | | with TA-bound key|
+------------------+ +------------------+
The data is encrypted and integrity-protected by the TA before it crosses to the normal world for persistence.
40.5.13 Trusty for the Android Desktop Form Factor¶
Android 17 adds a Trusty build aimed at the "Android Desktop" device class: Android running as a desktop OS on PC-class arm64 and x86_64 hardware. This is a form-factor signal in the TEE, separate from desktop windowing (freeform multi-window, desks, window decorations), which lives in WindowManager and WMShell and is covered in chapters 22 and 23. What Android 17 adds here is the secure-world bring-up for that hardware, not any window-management code.
The device configs live in trusty/device/desktop/ (copyright 2024). The
common include trusty/device/desktop/common/desktop-inc.mk is headed
"Configurations common to Trusty builds for Android Desktop," and two project
trees build the targets: trusty/device/desktop/arm64/desktop-arm64/ and
trusty/device/desktop/x86_64/desktop-x86_64/, each with the base project
makefile plus _ext_boot, -test, and -test-debug variants (for example
project/desktop-x86_64.mk and project/desktop-arm64_ext_boot.mk). The LK
kernel platform glue for these targets is the Rust code under
trusty/kernel/platform/desktop/. The desktop Trusty image is signed and
packaged as a Trusty VM rather than burned into a TrustZone secure world; the
Android-17 release tooling renames the signing tool to ResignTrustyVM for
this (the desktop Trusty VM is installed under vm/trusty_vm, per
trusty/vendor/google/aosp/scripts/Android.bp).
The trusted apps for this target are gathered under trusty/user/desktop/app/.
The set is the security stack a desktop device needs: gsc_svc is the service
for the Google Security Chip (a Titan-class security chip); pinweaver does
knowledge-factor throttling; finger_guard guards fingerprint enrollment and
matching; keymint_provisioning and keymint_strongbox provide KeyMint and
its StrongBox security level; secretkeeper and weaver provide the secret
storage primitives covered in sections 40.5.7 and 40.14; and hwbcc, hwkey,
and vm_rkp supply boot-certificate, hardware-key, and remote key provisioning
support for the VM. The gsc_svc app serves the IGsc AIDL interface
(android.system.desktop.security.gsc.IGsc, in
trusty/user/desktop/interface/gscd/), whose one method transmit(byte[])
relays a serialized TPM 2.0 command to the GSC firmware and returns the
response. Other desktop AIDL interfaces sit alongside it under
trusty/user/desktop/interface/ (pinweaver, finger_guard, boot_params,
keymint_provisioning), backed by support libraries under
trusty/user/desktop/lib/ such as gsc_svc_client, tpm_commands,
keymint_access_policy, and secretkeeper_secure_store.
40.6 Gatekeeper and Biometrics¶
40.6.1 Gatekeeper -- PIN/Pattern/Password Verification¶
Gatekeeper verifies the user's knowledge factor (PIN, pattern, or password)
in the TEE. The Trusty implementation is in
system/core/trusty/gatekeeper/:
class TrustyGateKeeperDevice : public BnGatekeeper {
public:
::ndk::ScopedAStatus enroll(
int32_t uid,
const std::vector<uint8_t>& currentPasswordHandle,
const std::vector<uint8_t>& currentPassword,
const std::vector<uint8_t>& desiredPassword,
GatekeeperEnrollResponse* _aidl_return) override;
::ndk::ScopedAStatus verify(
int32_t uid,
int64_t challenge,
const std::vector<uint8_t>& enrolledPasswordHandle,
const std::vector<uint8_t>& providedPassword,
GatekeeperVerifyResponse* _aidl_return) override;
::ndk::ScopedAStatus deleteAllUsers() override;
::ndk::ScopedAStatus deleteUser(int32_t uid) override;
};
The flow works as follows:
sequenceDiagram
participant User
participant LockSettings as LockSettingsService
participant GK_HAL as Gatekeeper HAL
participant TEE as Gatekeeper TA (in Trusty)
participant Keystore as Keystore2
User->>LockSettings: Enter PIN
LockSettings->>LockSettings: Derive secret from PIN + salt
LockSettings->>GK_HAL: verify(uid, challenge, handle, password)
GK_HAL->>TEE: GK_VERIFY command via TIPC
TEE->>TEE: Compare HMAC of password with enrolled handle
alt Password matches
TEE->>TEE: Generate HardwareAuthToken (HAT)
TEE->>GK_HAL: HAT + auth token
GK_HAL->>LockSettings: GatekeeperVerifyResponse with HAT
LockSettings->>Keystore: addAuthToken(HAT)
Keystore->>Keystore: Unlock auth-bound keys
else Password wrong
TEE->>TEE: Increment failure counter
TEE->>GK_HAL: Error + retry timeout
GK_HAL->>LockSettings: Failure response
end
Key security properties:
- Throttling in hardware -- the TEE enforces exponentially increasing delays after failed attempts (30s after 5 failures, with the wait time doubling). The normal world cannot bypass this.
- Per-user isolation -- each user has their own enrolled handle, stored
in
/data/system_de/<userId>/gatekeeper/. - Challenge binding -- the verification challenge prevents replay attacks.
40.6.2 Enrollment¶
During enrollment, the TEE:
- Receives the new password (or a derivative thereof).
- Generates an HMAC using a hardware-bound key.
- Returns a "password handle" -- the HMAC plus metadata.
- The framework stores the handle; the original password is never persisted.
For password changes, the old password must be verified first (to prevent an attacker with physical access from simply enrolling a new password).
40.6.3 Biometrics Framework¶
Android supports multiple biometric modalities through the HAL interfaces in
hardware/interfaces/biometrics/:
hardware/interfaces/biometrics/
common/ - Shared types (ICancellationSignal, OperationContext)
fingerprint/ - Fingerprint HAL
aidl/ - AIDL interface definitions
2.1/ - Legacy HIDL 2.1
2.2/ - Legacy HIDL 2.2
2.3/ - Legacy HIDL 2.3
face/ - Face HAL
aidl/ - AIDL interface definitions
1.0/ - Legacy HIDL 1.0
40.6.4 Fingerprint HAL¶
The AIDL fingerprint interface (IFingerprint.aidl) is clean and session-based:
@VintfStability
interface IFingerprint {
SensorProps[] getSensorProps();
ISession createSession(in int sensorId, in int userId,
in ISessionCallback cb);
}
The ISession interface defines all operations:
@VintfStability
interface ISession {
void generateChallenge();
void revokeChallenge(in long challenge);
ICancellationSignal enroll(in HardwareAuthToken hat);
ICancellationSignal authenticate(in long operationId);
ICancellationSignal detectInteraction();
void enumerateEnrollments();
void removeEnrollments(in int[] enrollmentIds);
void getAuthenticatorId();
void invalidateAuthenticatorId();
void resetLockout(in HardwareAuthToken hat);
void close();
// For under-display sensors
void onPointerDown(in int pointerId, in int x, in int y,
in float minor, in float major);
void onPointerUp(in int pointerId);
void onUiReady();
}
Sensor types supported:
| Type | Description |
|---|---|
REAR |
Capacitive sensor on the back |
UNDER_DISPLAY_ULTRASONIC |
Ultrasonic sensor under the display |
UNDER_DISPLAY_OPTICAL |
Optical sensor under the display |
POWER_BUTTON |
Integrated into the power button |
40.6.5 Sensor Strength Levels¶
Biometric sensors are classified by strength:
| Strength | Description | Can unlock Keystore keys? |
|---|---|---|
CONVENIENCE |
Spoofable; for UX convenience only | No |
WEAK |
Harder to spoof but no crypto guarantee | No |
STRONG |
Meets CDD requirements; produces HATs | Yes |
Only STRONG sensors can produce HardwareAuthTokens that unlock
authentication-bound keys in Keystore.
40.6.6 The HardwareAuthToken (HAT) Flow¶
flowchart TD
A[User touches fingerprint sensor] --> B["Fingerprint TA<br/>matches template"]
B --> C{Match?}
C -->|Yes| D["TA generates HardwareAuthToken<br/>with HMAC using shared secret"]
C -->|No| E["Report failure<br/>Increment lockout counter"]
D --> F["HAL sends HAT to framework<br/>via onAuthenticationSucceeded"]
F --> G["Framework forwards HAT<br/>to Keystore2"]
G --> H["Keystore2 validates HAT HMAC<br/>using shared secret from<br/>ISharedSecret.computeSharedSecret"]
H --> I["Auth-bound keys unlocked<br/>for the timeout duration"]
The HMAC key used to authenticate HATs is established through the
ISharedSecret interface, where all TEE components (KeyMint, Gatekeeper,
Biometrics) agree on a shared secret at boot time.
40.6.7 Lockout Policy¶
The biometrics framework enforces strict lockout:
- After 5 failed authentication attempts: 30-second timed lockout.
- After continued failures: lockout duration increases.
- After 20 cumulative failures: permanent lockout (requires PIN/pattern/ password).
- Lockout persists across reboots (stored in the TA's secure storage).
From the ISession.aidl:
Note that lockout states MUST persist after device reboots, HAL crashes, etc.
See the Android CDD section 7.3.10 for the full set of lockout and
rate-limiting requirements.
40.6.8 Face Authentication¶
The face HAL (hardware/interfaces/biometrics/face/aidl/) follows a similar
pattern to the fingerprint HAL but supports face-specific features:
- Enrollment types:
DEFAULTvsACCESSIBILITY(for users who cannot move their head normally). - Multiple enrollment stages: the HAL guides the user through head movements.
- Active vs. passive sensing: some implementations use IR flood illuminators, others use structured light depth cameras.
40.6.9 Authentication Flow Comparison¶
flowchart TD
subgraph "PIN/Pattern/Password"
U1[User enters credential] --> L1[LockSettingsService]
L1 --> G1[Gatekeeper HAL]
G1 --> T1[Gatekeeper TA in TEE]
T1 --> HAT1[HardwareAuthToken]
HAT1 --> KS1[Keystore2 unlocks keys]
end
subgraph "Fingerprint"
U2[User touches sensor] --> F2[Fingerprint HAL]
F2 --> T2[Fingerprint TA in TEE]
T2 --> HAT2[HardwareAuthToken]
HAT2 --> KS2[Keystore2 unlocks keys]
end
subgraph "Face"
U3[User faces camera] --> FC3[Face HAL]
FC3 --> T3[Face TA in TEE]
T3 --> HAT3[HardwareAuthToken]
HAT3 --> KS3[Keystore2 unlocks keys]
end
All three authentication methods produce the same output:
a HardwareAuthToken that Keystore2 can validate. This unified design
means auth-bound keys do not need to know which authentication method was
used -- they just need a valid token.
40.6.10 Shared Secret Negotiation¶
At boot time, all authentication-related TAs (KeyMint, Gatekeeper,
Fingerprint, Face) negotiate a shared HMAC key through the
ISharedSecret interface:
- Each TA generates a random nonce and contributes it.
- The
ISharedSecret.computeSharedSecret()method combines all nonces. - All TAs derive the same HMAC key using a KDF.
- This key is used to sign and verify HardwareAuthTokens.
If any TA is compromised, it cannot forge tokens that the others would accept, because the shared secret depends on contributions from all TAs.
40.6.11 Biometric Prompt¶
Android's BiometricPrompt API provides a unified UI for biometric
authentication. It handles:
- Choosing the best available biometric modality.
- Displaying a consistent system-controlled UI.
- Falling back to PIN/pattern/password when biometrics fail.
- Managing lockout states across modalities.
- Returning a
CryptoObjectfor hardware-bound authentication.
The BiometricPrompt flow:
sequenceDiagram
participant App
participant BP as BiometricPrompt
participant FW as BiometricService
participant FP as Fingerprint HAL
participant Face as Face HAL
App->>BP: authenticate(CryptoObject, ...)
BP->>FW: Request authentication
FW->>FW: Choose available sensor(s)
FW->>FP: authenticate(operationId)
FW->>Face: authenticate(operationId)
Note over FP,Face: Race between modalities
alt Fingerprint first
FP->>FW: onAuthenticationSucceeded(HAT)
FW->>Face: cancel()
else Face first
Face->>FW: onAuthenticationSucceeded(HAT)
FW->>FP: cancel()
end
FW->>BP: Authentication result + HAT
BP->>App: onAuthenticationSucceeded(result)
40.7 App Sandbox¶
40.7.1 UID-per-App Isolation¶
Every installed application receives a unique Linux UID. This UID is assigned at install time by the Package Manager and never changes. The UID forms the basis of the sandbox:
graph TB
subgraph "Linux Kernel"
DAC[DAC: UID/GID checks]
SEL[SELinux MAC checks]
SEC[seccomp-BPF]
end
subgraph "App A (uid=10042)"
A_Proc[Process]
A_Data["/data/data/com.example.a/<br/>Owned by uid 10042"]
end
subgraph "App B (uid=10043)"
B_Proc[Process]
B_Data["/data/data/com.example.b/<br/>Owned by uid 10043"]
end
A_Proc -->|Allowed| A_Data
A_Proc -->|Blocked by DAC| B_Data
B_Proc -->|Allowed| B_Data
B_Proc -->|Blocked by DAC| A_Data
The app data directory is created with permissions 0700 and owned by the
app's UID, so no other (non-root) process can read or write it.
40.7.2 SELinux Domains for Apps¶
Apps run in type-specific SELinux domains based on their targetSdkVersion
and other properties. The domain assignment is driven by seapp_contexts:
# From system/sepolicy/private/seapp_contexts
# System server
isSystemServer=true domain=system_server_startup
# Apps with targetSdkVersion >= 37
user=_app minTargetSdkVersion=37 domain=untrusted_app type=app_data_file levelFrom=all
# Apps with targetSdkVersion 34-36 / 32-33
user=_app minTargetSdkVersion=34 domain=untrusted_app_34 ...
user=_app minTargetSdkVersion=32 domain=untrusted_app_32 ...
# Apps with targetSdkVersion 30-31
user=_app minTargetSdkVersion=30 domain=untrusted_app_30 ...
# Isolated processes
user=_isolated domain=isolated_app ...
The untrusted_app domain (system/sepolicy/private/untrusted_app.te):
###
### Untrusted apps.
###
### This file defines the rules for untrusted apps running with
### targetSdkVersion >= 37.
###
typeattribute untrusted_app coredomain;
app_domain(untrusted_app)
untrusted_app_domain(untrusted_app)
net_domain(untrusted_app)
bluetooth_domain(untrusted_app)
40.7.3 Isolated Processes¶
The isolated_app domain is the most restricted:
Isolated processes:
- Cannot access the network directly (no
net_domain). - Cannot access any content provider or service by default.
- Cannot read/write any files except those explicitly passed to them via file descriptors.
- Run as a unique UID each time (drawn from a reserved range).
From system/sepolicy/private/isolated_app.te:
# Allow access to network sockets received over IPC.
# New socket creation is not permitted.
allow isolated_app { ephemeral_app priv_app untrusted_app_all }:{
tcp_socket udp_socket
} { rw_socket_perms_no_ioctl };
# b/32896414: Allow accessing sdcard file descriptors passed to
# isolated_apps by other processes. Open should never be allowed.
allow isolated_app { sdcard_type fuse media_rw_data_file }:file {
read write append getattr lock map
};
40.7.4 seccomp-BPF Filtering¶
Android applies seccomp-BPF (Secure Computing mode with Berkeley Packet Filter) to app processes. This restricts which system calls an app can make, even before SELinux is consulted.
The seccomp filter is applied by the Zygote during process specialization. Blocked syscalls include:
| Category | Examples |
|---|---|
| Kernel module loading | init_module, finit_module, delete_module |
| Raw I/O | ioperm, iopl |
| Process tracing | ptrace (unless debuggable) |
| Namespace manipulation | unshare, setns |
| Clock manipulation | clock_settime, settimeofday |
| Mount operations | mount, umount2 |
| Swap management | swapon, swapoff |
| Reboot | reboot |
A blocked syscall results in process termination (SIGKILL) or an error return, depending on the filter rule.
40.7.5 Namespace Isolation¶
Starting with Android 10, app processes use Linux mount namespaces to further restrict their view of the filesystem:
- Each app has its own mount namespace.
- FUSE-mounted external storage is presented with per-app views.
/proc/netis filtered to hide network information from apps targeting Android 10+.
40.7.6 Zygote Specialization¶
The Zygote is the parent process for all app processes. When an app is launched, the Zygote forks and then "specializes" the child process:
sequenceDiagram
participant AM as ActivityManager
participant Zygote
participant Child as App Process
AM->>Zygote: Fork request (via socket)
Zygote->>Child: fork()
Note over Child: Child process created
Child->>Child: setuid(app_uid)
Child->>Child: setgid(app_gid)
Child->>Child: setgroups(supplementary_groups)
Child->>Child: Apply seccomp-BPF filter
Child->>Child: Set SELinux context (via setcon)
Child->>Child: Set mount namespace
Child->>Child: Drop capabilities
Child->>Child: Close Zygote socket
Child->>Child: Load application code
During specialization:
- UID/GID set -- to the app's assigned UID.
- Supplementary groups -- set based on permissions (e.g.,
inetgroup for INTERNET permission,media_rwfor storage access). - seccomp filter applied -- restricts available syscalls.
- SELinux domain transition -- from
zygoteto the appropriate app domain (e.g.,untrusted_app). - Mount namespace -- isolated mount view created.
- Capabilities dropped -- no Linux capabilities remain.
- Zygote socket closed -- the child cannot fork more processes.
40.7.7 Permission to Group Mapping¶
The INTERNET permission is enforced at the kernel level through group
membership. When granted, the app's process gets the inet supplementary
group (GID 3003), which allows it to create AF_INET/AF_INET6 sockets. The
kernel's paranoid_networking feature restricts socket creation to processes
in specific groups:
| Group | GID | Permission |
|---|---|---|
inet |
3003 | Network socket creation |
net_raw |
3004 | Raw socket creation (ping) |
sdcard_rw |
1015 | External storage write |
media_rw |
1023 | Media storage write |
40.7.8 Detailed seccomp-BPF Policy¶
The seccomp-BPF filter is defined per architecture. For ARM64, the policy blocks dangerous syscalls while allowing the hundreds of syscalls needed for normal app operation. The filter structure:
flowchart TD
A[Syscall from app process] --> B{syscall number in allowlist?}
B -->|Yes| C[Allow syscall to proceed]
B -->|No| D{syscall number in blocklist?}
D -->|Yes| E[Return EPERM or SIGSYS]
D -->|No| F{Architecture-specific handling}
F --> G[Default: allow or block based on policy]
The seccomp policy files are at:
Example blocked syscalls and their security rationale:
| Syscall | Rationale for blocking |
|---|---|
init_module |
Loading kernel modules would be game-over |
delete_module |
Unloading security modules |
mount |
Mounting new filesystems could bypass sandbox |
umount2 |
Unmounting could expose raw block devices |
ptrace |
Debugging other processes leaks data |
unshare |
Namespace manipulation could escape sandbox |
setns |
Entering other namespaces |
reboot |
Denial of service |
swapon/swapoff |
System resource manipulation |
settimeofday |
Clock manipulation affects auth tokens |
pivot_root |
Filesystem root manipulation |
acct |
Process accounting control |
kexec_load |
Loading a new kernel |
40.7.9 Process-Level Isolation Details¶
Each app process has the following isolation properties:
File descriptor table: Forked from Zygote but scrubbed of sensitive FDs. The Zygote socket FD is closed immediately after fork.
Signal handling: Apps can only send signals to processes in the same app (same UID), except for SIGCHLD (parent notification) and signal 0 (existence test).
Memory protection: App processes have ASLR (Address Space Layout Randomization), stack canaries, and execute-only memory for code segments.
Resource limits: setrlimit is called to restrict:
- Maximum number of file descriptors
- Maximum stack size
- Maximum virtual memory size
- Core dump size (typically zero)
40.7.10 Intent-Based Communication Security¶
Apps communicate primarily through Intents, which are mediated by
system_server. The security checks on Intent delivery:
- Permission check -- if the receiver declares a permission, the sender must hold it.
- Export check -- unexported components cannot be targeted by external apps.
- SELinux check -- binder_call permissions must allow the IPC.
- App visibility -- Android 11+ restricts which apps can see each other
based on
<queries>declarations in the manifest.
40.7.11 Content Provider Security¶
Content Providers have their own access control:
<provider
android:name=".MyProvider"
android:authorities="com.example.provider"
android:exported="true"
android:readPermission="com.example.READ"
android:writePermission="com.example.WRITE">
<path-permission
android:path="/sensitive"
android:readPermission="com.example.READ_SENSITIVE" />
</provider>
exported=false(default for targetSdk >= 31): only the same app can access it.readPermission/writePermission: separate read and write permissions.<path-permission>: per-path permissions for fine-grained control.- URI grants: temporary one-time permission to specific URIs.
40.7.12 SDK Sandbox¶
Android 13 introduced the SDK Sandbox, a separate process for running advertising and analytics SDKs in isolation from the host app:
The SDK sandbox runs with its own UID and SELinux domain, preventing SDKs from accessing the host app's data or other sensitive system resources.
The SDK Sandbox architecture:
graph TB
subgraph "App Process (uid=10042)"
App[App Code]
SDK_Client[SDK Client API]
end
subgraph "SDK Sandbox Process (uid=20042)"
SDK_Runtime[SDK Runtime]
AdSDK[Ad SDK Code]
AnalyticsSDK[Analytics SDK]
end
subgraph "System"
SS["SdkSandboxManager<br/>in system_server"]
end
App --> SDK_Client
SDK_Client -->|Binder IPC| SS
SS -->|Manages lifecycle| SDK_Runtime
SDK_Runtime --> AdSDK
SDK_Runtime --> AnalyticsSDK
AdSDK -.->|Cannot access| App
Key restrictions on the SDK Sandbox:
- No access to the host app's files or shared preferences.
- Limited network access.
- Cannot read device identifiers.
- Cannot access location.
- Runs with its own separate storage area.
40.7.13 App Cloning and Profile Security¶
Android supports app cloning (running two instances of the same app) through the multi-user framework. Each clone:
- Gets a unique UID in a different user space.
- Has its own encrypted CE/DE storage.
- Is isolated by SELinux MLS categories.
- Cannot access the other clone's data.
40.7.14 WebView Isolation¶
WebView content runs in isolated renderer processes:
# WebView renderers use isolated_app domain
typeattribute isolated_app coredomain;
app_domain(isolated_app)
isolated_app_domain(isolated_app)
The WebView process:
- Runs as
isolated_appwith the most restrictive SELinux policy. - Cannot open files (only use passed file descriptors).
- Cannot create network connections (only use passed sockets).
- Cannot access the parent app's data directory.
- Is killed when the WebView is destroyed.
This ensures that a compromised renderer (e.g., via a browser exploit) has minimal access to the device.
40.8 Encryption¶
40.8.1 File-Based Encryption (FBE)¶
FBE, introduced in Android 7.0 and mandatory since Android 10, encrypts
different files with different keys, allowing each user's data to be
encrypted independently. The implementation is in system/vold/FsCrypt.cpp.
FBE uses the kernel's native filesystem encryption (fscrypt) support:
Key functions from system/vold/FsCrypt.h:
bool fscrypt_initialize_systemwide_keys();
bool fscrypt_init_user0();
bool fscrypt_create_user_keys(userid_t user_id, bool ephemeral);
bool fscrypt_destroy_user_keys(userid_t user_id);
bool fscrypt_set_ce_key_protection(userid_t user_id,
const std::vector<uint8_t>& secret);
bool fscrypt_unlock_ce_storage(userid_t user_id,
const std::vector<uint8_t>& secret);
bool fscrypt_lock_ce_storage(userid_t user_id);
bool fscrypt_prepare_user_storage(const std::string& volume_uuid,
userid_t user_id, int flags);
40.8.2 FBE Key Classes¶
FBE uses two classes of encryption keys per user:
graph TB
subgraph "Device Encrypted (DE)"
DE_Key["DE Key<br/>Available at boot"]
DE_Data["/data/system_de/<userId>/<br/>/data/misc_de/<userId>/<br/>/data/vendor_de/<userId>/"]
end
subgraph "Credential Encrypted (CE)"
CE_Key["CE Key<br/>Available after first unlock"]
CE_Data["/data/system_ce/<userId>/<br/>/data/misc_ce/<userId>/<br/>/data/vendor_ce/<userId>/<br/>/data/data/ (user 0)"]
end
subgraph "Key Storage"
KS["/data/misc/vold/user_keys/<br/>ce/<userId>/<br/>de/<userId>/"]
end
DE_Key --> DE_Data
CE_Key --> CE_Data
KS --> DE_Key
KS --> CE_Key
| Key Class | Unlocked When | Protects |
|---|---|---|
| DE (Device Encrypted) | Device boots | Alarm, telephony, accessibility settings |
| CE (Credential Encrypted) | User enters credential | App data, contacts, messages |
This design enables Direct Boot: the device can boot and run essential services (alarms, phone calls) before the user has entered their credential.
40.8.3 FBE Key Path Structure¶
The encryption keys are organized in a directory hierarchy under
/data/misc/vold/user_keys/. From system/vold/FsCrypt.cpp:
const std::string device_key_dir =
std::string() + DATA_MNT_POINT + fscrypt_unencrypted_folder;
const std::string device_key_path = device_key_dir + "/key";
const std::string user_key_dir =
std::string() + DATA_MNT_POINT + "/misc/vold/user_keys";
const std::string systemwide_volume_key_dir =
std::string() + DATA_MNT_POINT + "/misc/vold/volume_keys";
Key directory layout:
/data/misc/vold/user_keys/
ce/
0/ # User 0 CE keys
current/ # Currently active CE key
10/ # User 10 CE keys
de/
0/ # User 0 DE keys
10/ # User 10 DE keys
Helper functions for key path resolution:
static std::string get_de_key_path(userid_t user_id) {
return StringPrintf("%s/de/%d", user_key_dir.c_str(), user_id);
}
static std::string get_ce_key_directory_path(userid_t user_id) {
return StringPrintf("%s/ce/%d", user_key_dir.c_str(), user_id);
}
The internal state tracking uses maps to manage installed encryption policies:
// The currently installed CE and DE keys for each user.
// Protected by VolumeManager::mCryptLock.
struct UserPolicies {
EncryptionPolicy internal;
std::map<std::string, EncryptionPolicy> adoptable;
};
std::map<userid_t, UserPolicies> s_ce_policies;
std::map<userid_t, UserPolicies> s_de_policies;
40.8.4 Key Derivation and Storage¶
From system/vold/KeyStorage.cpp:
const KeyAuthentication kEmptyAuthentication{""};
static constexpr size_t AES_KEY_BYTES = 32;
static constexpr size_t GCM_NONCE_BYTES = 12;
static constexpr size_t GCM_MAC_BYTES = 16;
static constexpr size_t SECDISCARDABLE_BYTES = 1 << 14;
static const char* kHashPrefix_secdiscardable =
"Android secdiscardable SHA512";
static const char* kHashPrefix_keygen =
"Android key wrapping key generation SHA512";
The key storage mechanism:
- A random 256-bit encryption key is generated.
- The key is wrapped using a key derived from:
- The user's credential (for CE keys)
- A hardware-bound key from Keystore (bound to the device)
- A "secdiscardable" random file (16 KiB) that is securely deleted when the key is no longer needed
- The wrapped key is stored in
/data/misc/vold/user_keys/.
40.8.5 Encryption Key Lifecycle¶
stateDiagram-v2
[*] --> Created: fscrypt_create_user_keys
Created --> DE_Active: Device boots, DE key installed to kernel
DE_Active --> CE_Active: User enters credential, fscrypt_unlock_ce_storage
CE_Active --> CE_Locked: Device locks, fscrypt_lock_ce_storage
CE_Locked --> CE_Active: User re-enters credential
CE_Active --> Destroyed: User removed, fscrypt_destroy_user_keys
DE_Active --> Destroyed: User removed
Destroyed --> [*]
The key installation process uses the kernel's fscrypt API:
- The raw key (or hardware-wrapped key) is retrieved from storage.
- The key is installed into the kernel's fscrypt keyring using
FS_IOC_ADD_ENCRYPTION_KEY. - The kernel returns a key identifier.
- The identifier is set as the encryption policy for directories using
FS_IOC_SET_ENCRYPTION_POLICY. - The kernel encrypts/decrypts file contents transparently.
40.8.6 Metadata Encryption¶
Metadata encryption protects filesystem metadata (filenames, permissions,
directory structure) that FBE does not cover. The implementation is in
system/vold/MetadataCrypt.cpp:
// Parsed from metadata options
struct CryptoOptions {
struct CryptoType cipher = invalid_crypto_type;
bool use_legacy_options_format = false;
bool set_dun = true;
bool use_hw_wrapped_key = false;
};
// The first entry in this table is the default crypto type.
constexpr CryptoType supported_crypto_types[] = {aes_256_xts, adiantum};
Metadata encryption uses dm-default-key, a device-mapper target that
transparently encrypts all data written to the underlying block device with
a single key. This key is available at boot (not credential-bound) and
protects data at rest when the device is powered off.
Supported ciphers:
| Cipher | Performance | Security |
|---|---|---|
| AES-256-XTS | Fast with hardware acceleration | Standard choice |
| Adiantum | Fast without hardware acceleration | For low-end devices without AES instructions |
40.8.7 Full-Disk Encryption (FDE) -- Legacy¶
FDE was the encryption model before FBE (Android 5.0-6.0). It encrypted the
entire /data partition with a single key derived from the user's credential.
FDE had significant usability issues:
- The device could not boot functional services until the user entered their credential.
- No Direct Boot: alarms, phone calls, and accessibility services were unavailable before unlock.
- A single key compromise exposed all data.
FDE is deprecated and no longer supported on new devices launching with Android 10+.
40.8.8 Hardware-Wrapped Keys¶
For devices with inline encryption engines (common on modern SoCs), keys can be "hardware-wrapped":
- The key is generated inside the inline crypto engine.
- Only an encrypted ("wrapped") version of the key is ever visible to software.
- The inline crypto engine unwraps the key internally during I/O.
This means that even a kernel compromise cannot extract the raw encryption key.
The use_hw_wrapped_key option in metadata encryption enables this feature.
40.8.9 The dm-default-key Implementation¶
Metadata encryption uses the dm-default-key device-mapper target. From
system/vold/MetadataCrypt.cpp, the setup involves:
static bool create_crypto_blk_dev(
const std::string& dm_name,
const std::string& blk_device,
const KeyBuffer& key,
const CryptoOptions& options,
std::string* crypto_blkdev,
uint64_t* nr_sec,
bool is_userdata)
{
if (!get_number_of_sectors(blk_device, nr_sec)) return false;
// dm-default-key uses 4096-byte sectors
*nr_sec &= ~7;
KeyBuffer module_key;
if (options.use_hw_wrapped_key) {
if (!exportWrappedStorageKey(key, &module_key)) {
LOG(ERROR) << "Failed to get ephemeral wrapped key";
return false;
}
} else {
module_key = key;
}
// ... set up dm-default-key target ...
}
The metadata encryption layer sits between the filesystem and the block device:
Filesystem (ext4/f2fs)
|
dm-default-key (metadata encryption, AES-256-XTS)
|
Raw block device (/dev/block/by-name/userdata)
40.8.10 The Secdiscardable Mechanism¶
A unique security feature is the "secdiscardable" file:
static constexpr size_t SECDISCARDABLE_BYTES = 1 << 14; // 16384 bytes
static const char* kHashPrefix_secdiscardable =
"Android secdiscardable SHA512";
This 16 KiB random file is stored alongside each key. Its hash is mixed into the key derivation. When a key needs to be permanently destroyed:
- The secdiscardable file is securely erased using
BLKDISCARDorFITRIMioctls. - Even if the encrypted key blob is somehow recovered, the loss of the secdiscardable makes it useless.
- On flash storage, the DISCARD command tells the flash controller to erase the physical blocks, making recovery extremely difficult.
This provides a defense against forensic recovery of deleted keys.
40.8.11 CE Key Protection with User Credential¶
When a user sets a credential (PIN, pattern, password), the CE key becomes protected by a key derived from the credential. The protection flow:
sequenceDiagram
participant User
participant LS as LockSettingsService
participant GK as Gatekeeper
participant Vold
participant KS as Keystore
User->>LS: Set new PIN "1234"
LS->>GK: enroll(password_derived_secret)
GK->>LS: Password handle
LS->>LS: Store handle in /data/system/gatekeeper.pattern.key
LS->>KS: Create synthetic password (SP)
Note over KS: SP = random 256-bit value
KS->>KS: Encrypt SP with credential-derived key
KS->>KS: Store encrypted SP
LS->>Vold: fscrypt_set_ce_key_protection(userId, SP)
Vold->>Vold: Re-wrap CE key with SP
Note over Vold: CE key now requires SP to unlock
On unlock:
sequenceDiagram
participant User
participant LS as LockSettingsService
participant GK as Gatekeeper
participant Vold
User->>LS: Enter PIN "1234"
LS->>GK: verify(password_derived_secret)
GK->>LS: HardwareAuthToken
LS->>LS: Use token + credential to decrypt SP
LS->>Vold: fscrypt_unlock_ce_storage(userId, SP)
Vold->>Vold: Unwrap CE key using SP
Vold->>Vold: Install CE key to kernel fscrypt keyring
Note over Vold: CE storage now accessible
40.8.12 Encryption Architecture Diagram¶
graph TB
subgraph "At Rest (Power Off)"
Disk["/data partition<br/>Everything encrypted"]
end
subgraph "After Boot (Before Unlock)"
MD["Metadata Encryption<br/>dm-default-key active"]
DE_Unlocked["DE keys available<br/>DE storage readable"]
CE_Locked["CE keys locked<br/>CE storage inaccessible"]
end
subgraph "After User Unlock"
CE_Unlocked["CE keys unlocked<br/>All user data accessible"]
end
Disk -->|Boot| MD
MD --> DE_Unlocked
MD --> CE_Locked
CE_Locked -->|User enters credential| CE_Unlocked
40.8.13 SecureBox and the Recoverable Key Store¶
The encryption subsystems covered above (FBE, metadata encryption, dm-default-key)
protect data at rest on the device. Android also needs to protect keys
that leave the device — most importantly, the user's lock-screen-protected
keys when they are backed up to a remote vault for later recovery on a new
device. The "SecureBox" library at frameworks/base/libs/securebox/
provides the cryptographic primitive that the recoverable key store uses
to wrap those keys end-to-end.
This subsection walks through the SecureBox v2 wire format, why it is
shaped the way it is, and how RecoverableKeyStoreManager consumes it.
What SecureBox Is¶
com.android.security.SecureBox is a 461-line, dependency-free Java
class that implements an authenticated public-key + shared-secret hybrid
encryption scheme over the NIST P-256 elliptic curve, AES-128-GCM, and
HKDF-SHA-256. It exposes exactly four public methods:
// Source: frameworks/base/libs/securebox/src/com/android/security/SecureBox.java:142
public static KeyPair genKeyPair() throws NoSuchAlgorithmException;
public static byte[] encrypt(@Nullable PublicKey theirPublicKey,
@Nullable byte[] sharedSecret,
@Nullable byte[] header,
@Nullable byte[] payload);
public static byte[] decrypt(@Nullable PrivateKey ourPrivateKey,
@Nullable byte[] sharedSecret,
@Nullable byte[] header,
byte[] encryptedPayload);
public static byte[] encodePublicKey(PublicKey publicKey);
public static PublicKey decodePublicKey(byte[] keyBytes);
The signatures encode the design contract:
- A caller can encrypt with the recipient's public key, with a shared secret, or with both (in which case both are required to decrypt). At least one of the two must be non-null.
headeris authenticated but not encrypted — it travels in cleartext but the GCM tag binds it to the payload, so any modification fails decryption withAEADBadTagException.payloadis the encrypted body. Either input may be null/empty.
The library is built as a plain java_library with no static_libs and
no Android dependencies beyond @hide annotations and an
ArrayUtils.concat helper — a deliberate choice so that the same code
can be vetted as a self-contained cryptographic unit.
The SecureBox v2 Wire Format¶
The output of encrypt(...) is a single byte array with the layout:
+--------+----------------+--------+----------------------------+
| | | | |
| VERSION| sender pubKey | nonce | AES-GCM(ciphertext || tag)|
| 2 B | 65 B (DH only) | 12 B | variable |
| | | | |
+--------+----------------+--------+----------------------------+
- VERSION is the constant
0x02 0x00— little-endian 2 (the "v2" in SecureBox v2). - sender pubKey is present only in the public-key paths
(
theirPublicKey != null). The sender generates a fresh ephemeral P-256 key pair perencrypt(...)call, performs ECDH against the recipient's public key, and emits its own public key in uncompressed-point form (0x04 || X (32 B) || Y (32 B),EC_PUBLIC_KEY_LEN_BYTES = 65). - nonce is 12 freshly-random bytes per call (
SecureRandom). - AES-GCM body is the AES-128-GCM ciphertext with the 16-byte authentication tag appended (standard JCE GCM layout).
The HKDF info string differs by mode:
// Source: frameworks/base/libs/securebox/src/com/android/security/SecureBox.java:77
private static final byte[] HKDF_INFO_WITH_PUBLIC_KEY =
"P256 HKDF-SHA-256 AES-128-GCM".getBytes(StandardCharsets.UTF_8);
private static final byte[] HKDF_INFO_WITHOUT_PUBLIC_KEY =
"SHARED HKDF-SHA-256 AES-128-GCM".getBytes(StandardCharsets.UTF_8);
Mixing the two info strings is what makes the same library unambiguously cover both the ECDH and pure-shared-secret cases — the derived AES key is bound to its derivation mode, so a payload encrypted in one mode cannot decrypt in the other even if the keying material collides.
The full key-derivation chain is:
dhSecret = ECDH(senderPrivate, recipientPublic)(empty in the pure-shared-secret mode)keyingMaterial = dhSecret || sharedSecretprk = HMAC-SHA-256(salt = "SECUREBOX" || 0x02 0x00, ikm = keyingMaterial)K = first 16 bytes of HMAC-SHA-256(prk, info || 0x01)(one HKDF block)- AES-128-GCM with
K,nonce, AAD =header
This is a textbook HKDF construction; the only Android-specific bits
are the "SECUREBOX" || 0x02 0x00 salt and the API-37-stable info
strings.
flowchart LR
subgraph Encrypt["encrypt(theirPub, sharedSecret, header, payload)"]
EphGen["genKeyPair → senderPriv, senderPub"]
ECDH["dhSecret = ECDH(senderPriv, theirPub)"]
Concat["keyingMaterial = dhSecret || sharedSecret"]
HKDF["HKDF-SHA-256(salt='SECUREBOX'||0x02 0x00,<br/>info='P256 HKDF-SHA-256 AES-128-GCM',<br/>ikm=keyingMaterial) → K (16 B)"]
Nonce["genRandomNonce → 12 B"]
GCM["AES-128-GCM-Encrypt(K, nonce, aad=header, pt=payload)"]
Out["VERSION (2) || senderPub (65) || nonce (12) || ciphertext"]
EphGen --> ECDH --> Concat --> HKDF --> GCM --> Out
Nonce --> GCM
end
Curve Validation on Decode¶
SecureBox ships its own P-256 parameters (the canonical NIST p, a, b,
G, n constants in the static initializer) instead of trusting the JCE
to resolve them. The reason is decodePublicKey(...) validates that
the received point actually lies on the curve:
// Source: frameworks/base/libs/securebox/src/com/android/security/SecureBox.java:430
private static void validateEcPoint(BigInteger x, BigInteger y) throws InvalidKeyException {
if (x.compareTo(EC_PARAM_P) >= 0
|| y.compareTo(EC_PARAM_P) >= 0
|| x.signum() == -1
|| y.signum() == -1) {
throw new InvalidKeyException("Point lies outside of the expected curve");
}
// Points on the curve satisfy y^2 = x^3 + ax + b (mod p)
BigInteger lhs = y.modPow(BIG_INT_02, EC_PARAM_P);
BigInteger rhs = x.modPow(BIG_INT_02, EC_PARAM_P)
.add(EC_PARAM_A).mod(EC_PARAM_P)
.multiply(x).add(EC_PARAM_B).mod(EC_PARAM_P);
if (!lhs.equals(rhs)) {
throw new InvalidKeyException("Point lies outside of the expected curve");
}
}
This blocks invalid-curve attacks: an attacker who controls the
encryptedPayload cannot smuggle a malicious "public key" that lives
on a weaker curve and use the resulting ECDH leakage to recover the
recipient's private key. The check matches IETF / NIST SP 800-56A
public-key-validation guidance and is performed before the key is
handed to KeyAgreement.
The encoded form is also chosen deliberately: uncompressed
0x04 || X || Y, fixed 65 bytes, with arraycopy careful about
two's-complement sign-bit leading zeros (lines 386-398). The fixed-width
encoding is what RecoverableKeyStoreManager later persists into the
"vault params" blob handed to the recovery service.
How the Recoverable Key Store Uses It¶
The principal consumer is the LockSettings recoverable key store
implementation in frameworks/base/services/core/java/com/android/server/locksettings/recoverablekeystore/:
| File | Use of SecureBox |
|---|---|
RecoverableKeyStoreManager.java |
Top-level service handling RecoveryController AIDLs; uses SecureBox to wrap session keys end-to-end against a recovery agent's public key. |
KeySyncUtils.java |
Computes the locally-stored encrypted snapshot of "application keys + per-user recovery key" using SecureBox with the user's LSKF-derived shared secret. |
storage/RemoteLockscreenValidationSessionStorage.java |
Maintains the ephemeral SecureBox key pair that protects a remote-unlock session (used during Find My Device flows). |
The high-level recovery flow:
- On a source device, the user opts into Cloud Key Vault backups
via Settings. The system derives a recovery key from the
lock-screen credential (Gatekeeper-anchored), then uses
SecureBox.encrypt(theirPublicKey = recoveryServicePublicKey, sharedSecret = recoverySalt, ...)to wrap the user's application keys for the chosen recovery agent. - The wrapped blob travels via the recovery agent (e.g. Google's
account-tied cloud service). The cloud service stores it but cannot
read it — only the recipient device with the corresponding
recoveryServicePrivateKeyplus the user-derivedsharedSecretcanSecureBox.decrypt(...)it. - On a target device, after successful lock-screen credential
re-entry against the cloud-issued challenge, the system retrieves
the wrapped blob and
decrypt(...)s it, repopulating the keystore with the user's recoverable keys.
The header argument is used to bind each blob to its context
(recovery agent ID, vault version, intended slot). Any tampering with
that context invalidates the GCM tag and fails recovery — exactly the
property the recovery protocol needs.
The Remote Lock-Screen Validation Variant¶
com.android.settings.password.RemoteLockscreenValidationFragment
(in packages/apps/Settings/) uses the same SecureBox primitive for a
different flow: a remote unlock confirmation during Find My Device
flows. Settings generates an ephemeral P-256 key pair via
SecureBox.genKeyPair(), sends the public key to the cloud service,
and decrypts the cloud's challenge response with the private key. The
public-key path (no shared secret) of SecureBox.encrypt is what the
cloud uses on its end.
This second consumer is why SecureBox exposes the ECDH-only mode as
a first-class API path rather than insisting on a shared secret.
Why a Separate Library Instead of a JCE Provider¶
SecureBox could have been shipped as a JCE Cipher provider, but it
is deliberately not:
- The wire format is a single fixed shape with an explicit version byte — easier to audit as a sealed primitive than as a configurable algorithm.
- The library is consumed by
system_server(LockSettings) and by an app (Settings). Packaging it as ajava_librarywith no Android framework dependencies means the same compiled class file is used in both contexts, with no provider-installation differences. - The implementation must run on every Android device that supports recoverable key stores. Avoiding a provider lets the code work even when the active JCE provider has been replaced or restricted (e.g. by FIPS-mode builds that disallow generic GCM modes).
SecureBox is therefore a small, self-contained, intentionally
boring building block — the boringness is the security argument. The
v2 format has not changed since the file was added (2017 copyright
header), and the consumer code can rely on a stable wire shape across
the entire fleet that supports recoverable key stores.
40.9 Network Security¶
40.9.1 Network Security Config¶
The Network Security Config system allows apps to declare their network security preferences in a declarative XML format. The implementation is in:
The main class, NetworkSecurityConfig.java, manages:
public final class NetworkSecurityConfig {
public static final boolean DEFAULT_CLEARTEXT_TRAFFIC_PERMITTED = true;
public static final boolean DEFAULT_HSTS_ENFORCED = false;
private final boolean mCleartextTrafficPermitted;
private final boolean mHstsEnforced;
private final boolean mCertificateTransparencyVerificationRequired;
private final PinSet mPins;
private final List<CertificatesEntryRef> mCertificatesEntryRefs;
private Set<TrustAnchor> mAnchors;
}
Key source files in the implementation:
| File | Purpose |
|---|---|
NetworkSecurityConfig.java |
Core configuration class |
XmlConfigSource.java |
Parses XML config from AndroidManifest |
ManifestConfigSource.java |
Reads config reference from manifest |
NetworkSecurityTrustManager.java |
TLS trust manager with config awareness |
RootTrustManager.java |
Root trust manager that routes to per-domain configs |
Pin.java / PinSet.java |
Certificate pinning support |
Domain.java |
Domain matching for per-domain config |
SystemCertificateSource.java |
System CA certificate provider |
UserCertificateSource.java |
User-installed CA certificate provider |
40.9.2 XML Configuration Format¶
Apps declare their network security policy in res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Base config applied to all connections -->
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<!-- Per-domain overrides -->
<domain-config>
<domain includeSubdomains="true">api.example.com</domain>
<pin-set expiration="2025-01-01">
<pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
<!-- Backup pin -->
<pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=</pin>
</pin-set>
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</domain-config>
<!-- Debug overrides (only in debuggable builds) -->
<debug-overrides>
<trust-anchors>
<certificates src="user" />
</trust-anchors>
</debug-overrides>
</network-security-config>
40.9.3 Cleartext Traffic Restrictions¶
Android progressively restricts cleartext (non-TLS) HTTP traffic:
| targetSdkVersion | Default cleartext | Behavior |
|---|---|---|
| <= 23 | Permitted | No restriction |
| 24-27 | Permitted | Can opt out via config |
| >= 28 | Blocked | Must explicitly opt in for cleartext |
When cleartext traffic is blocked, HttpURLConnection and OkHttp refuse to
make HTTP connections, and the system logs a warning.
40.9.4 Certificate Pinning¶
Certificate pinning binds a domain to specific public keys, preventing man-in-the-middle attacks even if a CA is compromised:
flowchart TD
A[App connects to api.example.com] --> B[TLS handshake]
B --> C[Server presents certificate chain]
C --> D{Chain valid per system CAs?}
D -->|No| E[Connection rejected]
D -->|Yes| F{Pin-set configured?}
F -->|No| G[Connection allowed]
F -->|Yes| H{"Any cert in chain<br/>matches a pin?"}
H -->|Yes| G
H -->|No| I["Connection rejected<br/>Pin mismatch"]
Pin-sets have mandatory features:
- Expiration date -- pins expire so that a wrong pin does not permanently brick the app's connectivity.
- Backup pins -- at least two pins must be specified (one current, one backup) to enable key rotation.
40.9.5 Certificate Transparency¶
Starting with Android 16 (Baklava), Certificate Transparency (CT) verification is enabled by default for apps targeting the new SDK level:
@ChangeId
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
static final long DEFAULT_ENABLE_CERTIFICATE_TRANSPARENCY = 407952621L;
CT ensures that certificates issued by CAs are publicly logged, making it harder for CAs to issue fraudulent certificates without detection.
40.9.6 Trust Anchor Configuration¶
The Network Security Config supports three certificate sources:
| Source | Description |
|---|---|
system |
System CA store (/system/etc/security/cacerts/) |
user |
User-installed certificates |
@raw/my_ca |
App-bundled CA certificate |
For apps targeting Android 7.0+, user-installed CAs are NOT trusted by default. This prevents enterprise or parental-control proxies from silently intercepting app traffic.
40.9.7 Network Security Architecture¶
graph TB
subgraph "Application"
App[App Code]
NSC[network_security_config.xml]
end
subgraph "Framework"
ASP[ApplicationConfig]
ManifestSrc[ManifestConfigSource]
XmlSrc[XmlConfigSource]
RTM[RootTrustManager]
NSTM[NetworkSecurityTrustManager]
end
subgraph "Platform"
SysCA["System CA Store<br/>/system/etc/security/cacerts/"]
UserCA["User CA Store<br/>/data/misc/user/0/cacerts-added/"]
Conscrypt[Conscrypt TLS Provider]
end
App --> NSC
NSC --> ManifestSrc
ManifestSrc --> XmlSrc
XmlSrc --> ASP
ASP --> RTM
RTM --> NSTM
NSTM --> Conscrypt
Conscrypt --> SysCA
Conscrypt --> UserCA
40.9.8 The XmlConfigSource Parser¶
The XmlConfigSource.java parses the app's network security configuration
XML. The parsing handles nested structures:
<network-security-config>
<!-- Global defaults -->
<base-config>
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<!-- Per-domain overrides (most specific wins) -->
<domain-config>
<domain includeSubdomains="true">example.com</domain>
<trust-anchors>
<certificates src="system" />
<certificates src="@raw/custom_ca" />
</trust-anchors>
</domain-config>
<!-- Nested domain configs for sub-paths -->
<domain-config>
<domain includeSubdomains="true">example.com</domain>
<domain-config>
<domain>api.example.com</domain>
<pin-set>
<pin digest="SHA-256">...</pin>
</pin-set>
</domain-config>
</domain-config>
</network-security-config>
Domain matching follows a most-specific-match rule: api.example.com would
match the nested config with pinning, while www.example.com would match the
parent config with the custom CA.
40.9.9 Certificate Transparency Details¶
Certificate Transparency works by requiring that TLS certificates be logged to publicly auditable CT logs before they are accepted. The verification flow:
flowchart TD
A[TLS Handshake] --> B[Server presents certificate]
B --> C["Extract SCTs<br/>Signed Certificate Timestamps"]
C --> D{SCTs present?}
D -->|No| E{CT required?}
E -->|Yes| F[Connection rejected]
E -->|No| G[Continue without CT]
D -->|Yes| H["Verify SCT signatures<br/>against known CT logs"]
H --> I{Signatures valid?}
I -->|No| F
I -->|Yes| J{"Enough SCTs?<br/>Minimum depends on cert lifetime"}
J -->|No| F
J -->|Yes| K[Connection accepted]
SCTs (Signed Certificate Timestamps) can be delivered through three mechanisms:
- TLS extension -- the server includes SCTs in the TLS handshake.
- OCSP stapling -- SCTs are embedded in OCSP responses.
- X.509 extension -- SCTs are embedded in the certificate itself.
40.9.10 HTTPS Enforcement Evolution¶
Android has progressively tightened HTTPS requirements:
| Android Version | Change |
|---|---|
| 6.0 | usesCleartextTraffic manifest attribute introduced |
| 7.0 | Network Security Config introduced; user CAs not trusted by default |
| 9.0 | cleartextTrafficPermitted defaults to false for targetSdk >= 28 |
| 10 | TLS 1.3 enabled by default |
| 14 | System-only CA trust for targetSdk >= 34 |
| 16 (Baklava) | Certificate Transparency enabled by default |
| 17 (Cinnamon Bun) | usesCleartextTraffic manifest flag deprecated; ignored for targetSdk >= 37 (see ch35 §35.8.10) |
40.9.11 DNS over TLS / DNS over HTTPS¶
Android 9+ supports Private DNS (DNS over TLS, port 853). Android 13+
adds DNS over HTTPS (DoH). These prevent network observers from seeing DNS
queries in plaintext. The implementation is in the DnsResolver module
(an updatable Mainline module), separate from the app-level Network Security
Config.
Private DNS modes:
| Mode | Behavior |
|---|---|
| Off | Standard DNS (plaintext UDP port 53) |
| Automatic | Try DoT, fall back to standard DNS |
| Private DNS provider | Always use DoT to specified hostname, fail if unavailable |
The automatic mode discovery works by:
- Attempting a TLS connection to the network-provided DNS server on port 853.
- If the server supports DoT and its certificate is valid, using DoT.
- If not, falling back to standard DNS.
40.9.12 VPN Security Integration¶
Android's VPN framework integrates with the security model:
- VPN apps receive the
BIND_VPN_SERVICEpermission. - VPN traffic is routed through a TUN interface.
- The VPN app can see all DNS queries and network traffic, but:
- It runs in the
untrusted_appSELinux domain. - It cannot access other apps' files or processes.
- It cannot escalate privileges beyond network observation.
- Always-on VPN can be configured to block all traffic when VPN disconnects.
40.9.13 Network Security for System Services¶
System services have different network security properties:
# From private/app.te - apps cannot access certain network interfaces
neverallow all_untrusted_apps domain:netlink_kobject_uevent_socket *;
neverallow all_untrusted_apps domain:netlink_socket *;
# Restricted socket ioctls
neverallowxperm all_untrusted_apps domain:{
icmp_socket rawip_socket tcp_socket udp_socket
} ioctl priv_sock_ioctls;
This ensures that even if an app compromises the network stack, it cannot use privileged network operations like raw sockets, ICMP manipulation, or netlink configuration.
40.10 Android 17 Security Updates¶
Android 17 pushes the hardware-backed security stack in three directions at
once: it adds the first post-quantum key algorithm to KeyMint, it consolidates
the secure-clock interface inside the KeyMint trusted application, and it lets
the whole KeyMint/Gatekeeper trust domain run inside a protected VM reached over
IAccessor. Keystore2 also gains an optional attestation-certificate
post-processing hook and a set of Remote Key Provisioning safety limits. This
section folds those changes into the architecture described above.
(The unrelated in-process native sandbox introduced in Android 17 is covered in its own chapter on the Lightweight Fault Isolation sandbox; this chapter only notes that KeyMint and Gatekeeper are unaffected by it.)
40.10.1 ML-DSA Post-Quantum Keys¶
The headline cryptographic change is ML-DSA (Module-Lattice Digital Signature
Algorithm, FIPS 204), the standardized form of CRYSTALS-Dilithium. It is the
first post-quantum signature scheme exposed through the KeyMint HAL. The HAL
adds it as a new top-level algorithm in
hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/Algorithm.aidl:
enum Algorithm {
RSA = 1,
// 2 removed, do not reuse.
EC = 3,
ML_DSA = 4,
AES = 32,
TRIPLE_DES = 33,
HMAC = 128,
}
Two parameter sets are supported, selected by MlDsaVariant.aidl:
Because ML-DSA parameter sets are not described by a single key-size integer
the way RSA and EC are, the variant is carried by a new tag rather than
Tag::KEY_SIZE. Tag.aidl defines ML_DSA_VARIANT = TagType.ENUM | 11, and
KeyParameterValue.aidl adds a matching MlDsaVariant mlDsaVariant arm. The
contract in
hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/IKeyMintDevice.aidl
makes the rules explicit: Tag::ML_DSA_VARIANT must be supplied to generate an
ML-DSA key (otherwise generateKey returns
ErrorCode::UNSUPPORTED_ML_DSA_VARIANT, defined as -87 in ErrorCode.aidl);
no KEY_SIZE is passed; TEE implementations must support both ML-DSA-65 and
ML-DSA-87; StrongBox does not support ML-DSA at all; the only purposes allowed
are SIGN or ATTEST_KEY (never both); and the only digest is Digest::NONE.
The variant is also recorded in the attestation record. The
AuthorizationList schema documented in KeyCreationResult.aidl carries it as
mlDsaVariant [11] EXPLICIT INTEGER OPTIONAL, the ASN.1 counterpart of the new
tag.
The reference KeyMint TA implements the algorithm in
system/keymint/common/src/crypto/mldsa.rs. Public keys are encoded as a
SubjectPublicKeyInfo whose AlgorithmIdentifier OID is
2.16.840.1.101.3.4.3.18 for ML-DSA-65 or 2.16.840.1.101.3.4.3.19 for
ML-DSA-87 (RFC 9881), with absent parameters.
The KeyMint algorithm taxonomy after Android 17 looks like this.
flowchart TD
A["Algorithm.aidl"] --> B["Asymmetric"]
A --> C["Symmetric block"]
A --> D["MAC"]
B --> B1["RSA = 1"]
B --> B2["EC = 3"]
B --> B3["ML_DSA = 4 (post-quantum, Android 17)"]
C --> C1["AES = 32"]
C --> C2["TRIPLE_DES = 33"]
D --> D1["HMAC = 128"]
B3 --> E["MlDsaVariant.aidl"]
E --> E1["ML_DSA_65 = 1"]
E --> E2["ML_DSA_87 = 2"]
40.10.2 ML-DSA Seed Import¶
An ML-DSA private key is fully determined by a 32-byte seed; the large expanded
key (4032 bytes for ML-DSA-65, 4896 bytes for ML-DSA-87) is derived from it.
Android 17 therefore lets a key be imported in seed form rather than as the
expanded blob. system/keymint/common/src/crypto/mldsa.rs defines
SEED_SIZE = 32 and stores the key as the seed itself:
import_raw_key accepts a bare 32-byte seed plus a variant, while
import_pkcs8_key accepts the PKCS#8 form defined in RFC 9881 section 6. That
PKCS#8 structure has a CHOICE of seed / expandedKey / both; the
implementation supports only the seed alternative. Because the seed has a
fixed 32-byte length, the code skips full ASN.1 parsing and instead matches a
22-byte DER prefix (the SEQUENCE, version, algorithm OID, and the
context-tagged seed wrapper) followed by the seed bytes. Any other length or
prefix is rejected with InvalidArgument. Keystore2 mirrors this on the
service side: insecure (software) import of an ML-DSA private key expects the
PKCS#8 seed format.
40.10.3 The Timestamp Interface Folds Into the KeyMint HAL¶
Authenticated timestamps, defined by ISecureClock in
hardware/interfaces/security/secureclock/aidl/, are used to bound the freshness
of HardwareAuthTokens. In Android 17 the secure-clock functionality is served
by the KeyMint trusted application itself rather than by a separate timestamp
service. The KeyMint TA dispatch loop in system/keymint/ta/src/lib.rs handles
the SecureClockGenerateTimeStamp request directly, calling
generate_timestamp in system/keymint/ta/src/clock.rs:
pub fn generate_timestamp(&self, challenge: i64) -> Result<TimeStampToken, Error> {
if let Some(clock) = &self.imp.clock {
let mut ret =
TimeStampToken { challenge, timestamp: clock.now().into(), mac: Vec::new() };
let mac_input = self.dev.keys.timestamp_token_mac_input(&ret)?;
ret.mac = self.device_hmac(&mac_input)?;
Ok(ret)
} else {
Err(km_err!(Unimplemented, "no clock available"))
}
}
Because KeyMint and the secure clock now share the same device_hmac shared
secret, a single trusted application can both issue and verify the timestamp
token, removing the need for a standalone hal_timestamp_service. The SELinux
policy was updated to match: system/sepolicy/private/hal_keymint.te no longer
attaches a separate timestamp service attribute, and the documented model is
that "the keymint HAL serves the timestamp interface also."
This change also explains the deprecation noted in section 40.4.12. The
deviceLocked method in IKeyMintDevice.aidl still takes a TimeStampToken
argument, but it is marked @deprecated and its parameters are annotated
"N/A due to the deprecation," so a conformant implementation returns
ErrorCode::UNIMPLEMENTED and never consumes the token there.
40.10.4 KeyMint, Gatekeeper, and RKP Inside a Protected VM¶
Android 17 extends the protected-VM (Microdroid) model to the hardware-backed
security HALs. Instead of a vendor service registered directly with
servicemanager, the KeyMint, Gatekeeper, SecureClock, SharedSecret, and
Remotely Provisioned Component interfaces can be hosted inside a security VM and
reached through android.os.IAccessor proxies. servicemanager resolves the
client-visible name to an IAccessor that forwards binder traffic into the VM.
system/sepolicy/private/service.te defines a dedicated SELinux type for each
accessor so the policy can keep the in-VM services distinct from the
host-registered ones:
type accessor_trusty_keymint_comm_service, service_manager_type;
type accessor_trusty_gatekeeper_service, service_manager_type;
type accessor_trusty_hwcrypto_sharedsecret_service, service_manager_type;
type accessor_trusty_keymint_provisioning_service, service_manager_type;
type accessor_trusty_keymint_provisioning_thal_service, service_manager_type;
type accessor_trusty_keymint_remotelyprovisionedcomponent_service, service_manager_type;
type accessor_trusty_keymint_secureclock_service, service_manager_type;
system/sepolicy/private/service_contexts binds the IAccessor-namespaced
names to those types, for example:
android.os.IAccessor/ICommService/security_vm_keymint u:object_r:accessor_trusty_keymint_comm_service:s0
android.os.IAccessor/IGatekeeper/security_vm_gatekeeper u:object_r:accessor_trusty_gatekeeper_service:s0
android.os.IAccessor/IRemotelyProvisionedComponent/security_vm_keymint u:object_r:accessor_trusty_keymint_remotelyprovisionedcomponent_service:s0
android.os.IAccessor/ISecureClock/security_vm_keymint u:object_r:accessor_trusty_keymint_secureclock_service:s0
android.os.IAccessor/IProvisioning/security_vm_keymint u:object_r:accessor_trusty_keymint_provisioning_thal_service:s0
The system-side KeyMint HAL is granted find on its accessor in
system/sepolicy/private/hal_keymint_system.te, and per-interface accessor
permissions were added across the keymint-in-vm policy ("keymint-in-vm: Add
permissions for IRemotelyProvisionedComponent accessor," and the equivalents for
ISecureClock, IProvisioning, ISharedSecret, and `IGatekeeper"). The
result is that the entire root-of-trust HAL surface can be isolated inside a
protected VM while the framework keeps talking to it through ordinary binder
names.
The accessor indirection that makes this work is shown below.
flowchart LR
KS["Keystore2 / system_server"] --> SM["servicemanager"]
SM --> ACC["IAccessor proxy<br/>(accessor_trusty_keymint_*)"]
ACC --> VM["Security VM (Microdroid)"]
VM --> KM["KeyMint TA"]
VM --> RKP["IRemotelyProvisionedComponent"]
VM --> SC["ISecureClock"]
VM --> GK["IGatekeeper"]
40.10.5 Remote Key Provisioning Hardening¶
Several RKP changes in Android 17 tighten when and how Keystore2 reaches the provisioning daemon (RKPD):
-
RKP is gated on a hostname property.
RemProvState::is_rkp_enabledinsystem/security/keystore2/src/remote_provisioning.rsnow treats RKP as enabled only whenremote_provisioning.hostnameis set to a non-empty value. On an RKP-only device with no hostname configured, key generation fails withResponseCode::OUT_OF_KEYS_PERMANENT_ERRORinstead of silently falling back, which surfaces a misconfiguration early rather than handing out unattested keys. -
ML-DSA counts as an asymmetric algorithm for RKP.
is_asymmetric_keymatchesAlgorithm::RSA,Algorithm::EC, and nowAlgorithm::ML_DSA, so post-quantum keys flow through the same remote attestation path as classical asymmetric keys. -
Concurrent RKPD calls are capped.
system/security/keystore2/rkpd_client/src/lib.rsdefinesRKP_MAX_CONCURRENT_OPERATIONS = 15and guards each call with aConcurrentOperationRAII counter; exceeding the cap returnsTooManyConcurrentOperationsrather than starving the rest of the system's threads on the network round-trip.
flowchart TD
A["generateKey (asymmetric, app domain)"] --> B{"is_rkp_enabled()<br/>hostname set?"}
B -->|No, RKP-only| C["OUT_OF_KEYS_PERMANENT_ERROR"]
B -->|No, hybrid| D["Fall back to factory key"]
B -->|Yes| E{"under concurrency cap?"}
E -->|No| F["TooManyConcurrentOperations"]
E -->|Yes| G["Fetch key + cert chain from RKPD"]
40.10.6 Attestation Certificate Post-Processing¶
Keystore2 gained an optional hook to rewrite the RKP attestation certificate
chain after KeyMint produces it. When the system property
remote_provisioning.use_cert_processor is true,
KeystoreSecurityLevel::generate_key in
system/security/keystore2/src/security_level.rs routes the freshly generated
chain through process_certificate_chain
(system/security/keystore2/postprocessor_client/src/lib.rs) instead of
storing the raw chain. That helper connects to the lazily started binder
service rkp_cert_processor.service, with a 5-second timeout; if the connection
ever times out, an AtomicBool latches the failure so the post-processor is not
retried until the next reboot. Critically, any failure falls back to the
original certificate chain, so post-processing can never block key generation.
The post-processor service itself lives in
packages/services/DroidfoodAttestationFixer. It builds a Rust binary,
rkp_cert_processor, that registers a lazy AIDL service implementing
IKeystoreCertificatePostProcessor (defined in
system/security/keystore2/aidl/android/security/postprocessor/). Its
processKeystoreCertificates implementation in
packages/services/DroidfoodAttestationFixer/src/lib.rs base64-encodes the leaf
and remaining chain, sends them to a backend over a small libcurl bridge, and
returns the overwritten chain (falling back to the original on any decode or
server error). The whole package is flag-gated: its
droidfood_attestation_flags.aconfig declares
droidfood_attestation_bringup (a is_fixed_read_only flag in the
android.security.postprocessor package), and the service is installed to
system_ext and started disabled / oneshot from
packages/services/DroidfoodAttestationFixer/droidfoodattestation.rc, so it
runs only when explicitly enabled for the Droidfood (internal dogfood) program.
The post-processing flow, including its fail-safe fallback, is shown below.
flowchart TD
A["generate_key produces RKP chain"] --> B{"use_cert_processor property?"}
B -->|No| C["Store raw concatenated chain"]
B -->|Yes| D["process_certificate_chain"]
D --> E{"connect to rkp_cert_processor.service<br/>within 5s?"}
E -->|No| F["Latch failure, use original chain"]
E -->|Yes| G["processKeystoreCertificates over libcurl"]
G --> H{"server returned valid chain?"}
H -->|No| F
H -->|Yes| I["Replace leaf + remaining chain"]
40.10.7 HPKE Hybrid Public-Key Encryption Through JCA¶
Hybrid Public Key Encryption (HPKE, RFC 9180) combines a KEM (key
encapsulation), a KDF, and an AEAD into one scheme: a sender encapsulates a
fresh symmetric key to the recipient's public key and uses it to seal a
message, so the recipient needs only its private key to open it. BoringSSL has
implemented HPKE for a while and Conscrypt exposed it internally, but Android 17
promotes it to a public API under android.crypto.hpke, gated by the
com.android.libcore.hpke_public_api aconfig flag
(libcore/libcore.aconfig).
The public surface lives in libcore at
libcore/luni/src/main/java/android/crypto/hpke/. An app does not talk to a JCA
Cipher; instead the entry point is Hpke, which resolves an implementation
through the JCA Provider mechanism under a custom service type, ConscryptHpke:
// app code
Hpke hpke = Hpke.getInstance(
"DHKEM_X25519_HKDF_SHA256/HKDF_SHA256/AES_128_GCM");
Sender sender = new Sender.Builder(hpke, recipientPublicKey).build();
byte[] enc = sender.getEncapsulated(); // send to recipient
byte[] ct = sender.seal(plaintext, aad);
The suite name is a KEM/KDF/AEAD triple. The three components are named with
AlgorithmParameterSpec subclasses, each a java.security.spec.NamedParameterSpec:
KemParameterSpec (for example DHKEM_X25519_HKDF_SHA256), KdfParameterSpec
(HKDF_SHA256), and AeadParameterSpec, whose constants are
AeadParameterSpec.AES_128_GCM, AES_256_GCM, and CHACHA20POLY1305.
The provider-facing contract is HpkeSpi
(libcore/luni/src/main/java/android/crypto/hpke/HpkeSpi.java), the
Service-Provider-Interface a crypto provider implements to supply HPKE. Its
methods mirror the RFC operations:
void engineInitSender(PublicKey recipientKey, byte[] info,
PrivateKey senderKey, byte[] psk, byte[] psk_id);
void engineInitRecipient(byte[] encapsulated, PrivateKey recipientKey,
byte[] info, PublicKey senderKey, byte[] psk, byte[] psk_id);
byte[] engineSeal(byte[] plaintext, byte[] aad);
byte[] engineOpen(byte[] ciphertext, byte[] aad);
byte[] engineExport(int length, byte[] context);
byte[] getEncapsulated();
Conscrypt registers the implementations in its provider. OpenSSLProvider
(external/conscrypt/repackaged/common/src/main/java/com/android/org/conscrypt/OpenSSLProvider.java)
puts a ConscryptHpke.<suite> entry for each supported KEM/KDF/AEAD triple,
choosing the adapter class that bridges to android.crypto.hpke.HpkeSpi when
that public interface is present on the platform:
String baseClass = classExists("android.crypto.hpke.HpkeSpi")
? PREFIX + "AndroidHpkeSpi" : PREFIX + "HpkeImpl";
put("ConscryptHpke.DHKEM_X25519_HKDF_SHA256/HKDF_SHA256/AES_128_GCM",
baseClass + "$X25519_AES_128");
AndroidHpkeSpi
(external/conscrypt/repackaged/platform/src/main/java/com/android/org/conscrypt/AndroidHpkeSpi.java)
adapts the public HpkeSpi to Conscrypt's internal SPI, and HpkeImpl
(.../common/src/main/java/com/android/org/conscrypt/HpkeImpl.java) drives
BoringSSL through NativeCrypto.EVP_HPKE_CTX_*. So Hpke.getInstance() ->
Provider.getService("ConscryptHpke", suite) -> AndroidHpkeSpi ->
HpkeImpl -> BoringSSL. The current implementation supports the base HPKE mode;
the PSK and authenticated modes are not yet wired through.
40.10.8 Lock-Screen Rate-Limiting Rename and Recovery Shortlink¶
Two smaller lock-screen changes land in frameworks/base. First, the method
that reports how long primary authentication is throttled was renamed:
LockPatternUtils#getLockoutAttemptDeadline(int) became getLockoutEndTime(int),
and it now returns a java.time.Duration instead of a raw deadline timestamp.
The value is the time since boot at which the user may attempt PIN, pattern, or
password again, or Duration.ZERO when no lockout is in effect
(frameworks/base/core/java/com/android/internal/widget/LockPatternUtils.java,
lines 1187-1197). The result is served through a PropertyInvalidatedCache and
backed by ParcelDuration getLockoutEndTime(int userId) on the lock-settings
binder interface (ILockSettings.aidl, line 76).
Second, a new string resource config_lockscreenLockoutShortlink (default
g.co/android/unlock) gives the bouncer a recovery URL to show once a device is
locked out (frameworks/base/core/res/res/values/config.xml, line 1552, with
its java-symbol in symbols.xml). SystemUI substitutes it into the locked-out
bouncer messages when the android.security.Flags.lockscreenTimeoutShortlink
aconfig flag is set
(frameworks/base/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerMessageStrings.kt),
so a user who has forgotten the credential is pointed at the account-recovery
flow rather than only being told to wait.
40.10.9 SELinux memfd Class Compatibility¶
Android 17 expects the SELinux policy to treat anonymous memory file descriptors
as their own object class. The kernel and libsepol carry a policy capability
memfd_class (POLICYDB_CAP_MEMFD_CLASS in
external/selinux/libsepol/src/polcaps.c); when it is enabled, the kernel labels
memfd_create() descriptors with the memfd_file class instead of folding them
into the generic file class. AOSP's userspace policy already defines that class
(class memfd_file in system/sepolicy/private/security_classes) and relies on
it throughout: domain.te grants every domain memfd_file access to its own
descriptors and locks down ioctl on others, and media, sensor, allocator, and
zygote domains pass memfd_file descriptors across IPC
(system/sepolicy/private/domain.te, app.te, mediaserver.te).
Because those allow rules name the memfd_file class explicitly, a device whose
kernel does not enable the memfd_class capability would see those descriptors
labeled file, the memfd_file rules would never match, and the affected IPC
would be denied. Devices must therefore enable the memfd_class policy
capability and support the memfd_file class so the shipped policy applies as
written.
40.10.10 Per-App Keystore Key Limit (API 37)¶
Android 17 caps how many Keystore keys a single app (UID) may hold, so runaway or
abusive key creation cannot bloat the keystore database. keystore2 enforces a
fallback ceiling for every app plus a tighter ceiling for apps targeting API 37:
// system/security/keystore2/src/security_level.rs
/// The fallback limit on the number of keys per app. All apps must stay within this limit.
const DEFAULT_PER_UID_KEY_LIMIT: i32 = 200_000; // line 79
const API_37_PER_UID_KEY_LIMIT: i32 = 50_000; // line 82
The check is gated by the keystore2 aconfig flag limit_keys_per_uid
(security_level.rs:563): when it is on, key generation for an app targeting
Android 17 fails once the app's key count reaches API_37_PER_UID_KEY_LIMIT (the
comparison at line 584), surfacing as a Keystore error to the caller; apps below
the target keep the looser 200,000 fallback. keystore2 also tracks per-UID key
counts for diagnostics -- dumpsys prints the worst offenders via
KEYS_PER_UID_MAX_UIDS / KEYS_PER_UID_MIN_KEY_COUNT
(system/security/keystore2/src/maintenance.rs:436-440) -- so an OEM can spot apps
approaching the cap before enforcement bites.
40.11 Security Posture and Version Evolution¶
This section steps back from individual subsystems to look at how Android keeps its security current: updatable components, patch levels, and the long arc of attack-surface reduction that culminates in the Android 17 features above.
40.11.1 Security Updates and Mainline Modules¶
Starting with Android 10, security-critical components can be updated independently of full OS updates through Project Mainline:
| Module | Security Role |
|---|---|
| Conscrypt | TLS implementation (certificate validation, cipher suites) |
| DNS Resolver | Private DNS (DoT/DoH) |
| Media Codecs | Prevents media-based exploits |
| Networking | Network stack security patches |
| Permission Controller | Permission management and privacy |
| DocumentsUI | Prevents file manager exploits |
| tethering | Hotspot/tethering security |
These modules are updated via the Play Store as APEX packages, allowing Google to push security fixes without waiting for OEM/carrier approval.
40.11.2 Monthly Security Patch Levels¶
Android uses two security patch levels:
- Platform SPL (
ro.build.version.security_patch): Patches to the Android framework, system libraries, and core. - Vendor SPL (
ro.vendor.build.security_patch): Patches to vendor components, HALs, and kernel.
Both are dates (e.g., 2026-03-05). CTS verifies that the device's
patch level matches its claimed security patches.
The Keystore attestation certificate includes the patch level, allowing relying parties to require a minimum patch level before trusting the device.
40.11.3 Attack Surface Reduction Over Time¶
Each Android version reduces the attack surface:
| Version | Key Security Improvements |
|---|---|
| 5.0 | SELinux enforcing, FDE |
| 6.0 | Runtime permissions, Verified Boot v1 |
| 7.0 | FBE, Network Security Config, Verified Boot v2 (AVB) |
| 8.0 | Treble HAL isolation, seccomp for all apps |
| 9.0 | StrongBox, BiometricPrompt, cleartext default off |
| 10 | Scoped storage, FBE mandatory, mount namespace per app |
| 11 | Scoped storage enforced, one-time permissions |
| 12 | KeyMint AIDL, Remote Key Provisioning |
| 13 | SDK Sandbox, photo picker |
| 14 | Credential Manager, improved passkey support |
| 15 | Per-app locale, tighter intent restrictions |
| 16 | Advanced Protection mode, Identity Credential maturation |
| 17 | ML-DSA post-quantum keys, KeyMint/Gatekeeper in protected VMs, RKP hardening |
40.11.4 Security Architecture Principles¶
Several cross-cutting principles emerge from the code:
-
No single point of failure -- every security mechanism is designed to be useful even if other mechanisms are bypassed. SELinux blocks access even if DAC permissions are wrong. Encryption protects data even if filesystem permissions are bypassed. Verified Boot detects tampering even if the attacker has root.
-
Hardware-anchored trust -- the most sensitive operations (key storage, authentication verification, boot verification) are anchored in hardware that software cannot modify. The TEE, StrongBox, and boot ROM fuses provide guarantees that no amount of software compromise can violate.
-
Principle of least authority -- every component runs with the minimum privileges needed. Apps start with no permissions. HALs are confined to their specific domain. Even system services are restricted by SELinux neverallow rules.
-
Defense in depth with independent layers -- the sandbox is enforced by UID isolation AND SELinux AND seccomp AND mount namespaces. An attacker must bypass ALL of these simultaneously.
-
Progressive tightening -- each Android version tightens restrictions for apps targeting the new SDK level while maintaining backward compatibility for older apps. This is visible in the versioned untrusted_app domains and the network security config defaults.
-
Open source verification -- all security-critical code (SELinux policy, AVB, Keystore2, biometric HALs) is open source, enabling independent audit and verification.
40.11.5 Security Testing in AOSP¶
AOSP includes extensive security tests:
| Test Suite | Path | Tests |
|---|---|---|
| SELinux CTS | system/sepolicy/tests/ |
Neverallow validation, context correctness |
| Keystore VTS | hardware/interfaces/security/keymint/aidl/vts/ |
HAL conformance |
| ML-DSA VTS | hardware/interfaces/security/keymint/aidl/vts/functional/MlDsaTest.cpp |
Post-quantum key conformance |
| Biometric VTS | hardware/interfaces/biometrics/fingerprint/aidl/vts/ |
HAL conformance |
| AVB tests | external/avb/test/ |
Image verification, signing |
| Keystore2 unit tests | system/security/keystore2/src/*/tests.rs |
Rust unit tests |
| Treble sepolicy tests | system/sepolicy/treble_sepolicy_tests_for_release/ |
Vendor isolation |
Running the SELinux tests:
# Build the sepolicy tests
mmm system/sepolicy/tests
# Run treble sepolicy tests
python3 system/sepolicy/tests/treble_tests.py \
-l system/sepolicy/prebuilts/api/<api>/ \
-f <compiled_policy>
Running Keystore2 tests:
For deeper background, the Android Security Bulletin tracks monthly patches and
CVEs, the Android CDD lists mandatory security requirements, and the in-tree
READMEs (external/avb/README.md, system/sepolicy/README.md, and the
Keystore2 design notes under system/security/keystore2/) document each
subsystem.
40.12 AuthGraph: Authenticated Key Exchange Between Secure Components¶
Several of the subsystems above establish an encrypted, mutually authenticated
channel between two security domains that do not share a binder transport --
for example, a client running in a protected VM and the SecretKeeper service in
a TEE. The protocol that bootstraps that channel is AuthGraph, an authenticated
key-exchange (AKE) protocol with an AIDL contract,
IAuthGraphKeyExchange (hardware/interfaces/security/authgraph/aidl/android/hardware/security/authgraph/IAuthGraphKeyExchange.aidl),
and a roughly 7.5K-line Rust reference implementation under system/authgraph/.
40.12.1 What AuthGraph Provides¶
AuthGraph negotiates a pair of symmetric session keys -- one per direction --
between two parties named P1 (the source, typically the client) and P2 (the
sink, typically the service). Each party must already hold two things,
described in the AIDL header: a persistent identity (a signing key whose
public half is carried in a self-signed or DICE certificate chain) and a
per-boot key (an in-memory symmetric key with the lifetime of one boot).
The output is two 256-bit AES keys plus a shared session_id, after which both
sides can exchange encrypted, replay-resistant messages.
Two design choices recur throughout the protocol. First, derived keys are never
returned in the clear: they are wrapped in arcs -- CBOR/COSE blobs encrypted
under the holder's per-boot key, with protected headers that record the peer
identity, the session id, and whether authentication has completed. An arc both
hides the key from the normal world and lets a party stay nearly stateless
between calls. Second, mutual authentication is bound to the key material: each
party signs the session_id (an HMAC over both nonces) with its persistent
signing key, and the peer verifies that signature against the certificate chain
in the peer's identity, so a man-in-the-middle that cannot produce a valid
signature is rejected.
40.12.2 The Four-Method Handshake¶
The interface defines four methods. Three run during the handshake -- create
and finish on P1, init on P2 -- and authenticationComplete lets P2 finish
out-of-band once it has seen P1's signature. The shared secret itself is an
ephemeral ECDH agreement on NIST curve P-256: each side generates a fresh P-256
key pair and a 16-byte nonce, and the Diffie-Hellman result Z is run through
HKDF to derive the session keys.
sequenceDiagram
participant P1 as "P1 source (client)"
participant P2 as "P2 sink (service)"
P1->>P1: "create(): ephemeral P-256 key + nonce"
P1->>P2: "init(peerPubKey, peerId, peerNonce, peerVersion)"
Note over P2: "ECDH -> Z, HKDF -> 2 session keys + MAC key,<br/>compute session_id, sign it"
P2-->>P1: "KeInitResult (P2 pubkey, nonce, shared-key arcs, session_id, signature)"
P1->>P1: "finish(): ECDH -> Z, derive keys, verify P2 signature, sign session_id"
P1-->>P2: "authenticationComplete(P1 signature, sharedKeys)"
Note over P2: "verify P1 signature, mark arcs authentication_complete = true"
The flow follows the AIDL documentation step by step:
create()runs on P1. It produces aSessionInitiationInfo: an ephemeral P-256 public key (with the matching private key sealed in an arc under P1's per-boot key), a 16-byte nonce, P1's persistent identity, and the highest protocol (AIDL) version P1 supports.init(peerPubKey, peerId, peerNonce, peerVersion)runs on P2. P2 generates its own ephemeral P-256 key pair and nonce, computes the ECDH secretZ, and derives a cryptographic secret using the SHA-256 digest of a CBORsalt_input(which binds both versions, both public keys, both nonces, and both certificate chains) as the HKDF salt. From that it derives two AES-256 keys -- contextsb"KE_ENCRYPTION_KEY_SOURCE_TO_SINK"andb"KE_ENCRYPTION_KEY_SINK_TO_SOURCE"-- and an HMAC key (b"KE_HMAC_KEY"). It HMACs the two nonces to form thesession_id, signs it with its identity key, and returns everything as aKeInitResult, with the two session keys handed back as arcs markedauthentication_complete = false.finish(peerPubKey, peerId, peerSignature, peerNonce, peerVersion, ownKey)runs back on P1. P1 repeats the same ECDH/HKDF derivation, recomputes thesession_id, and verifies P2's signature over it (returningINVALID_SIGNATUREon failure). On success P1 re-wraps its own copies of the two session keys as arcs markedauthentication_complete = trueand signs thesession_idwith its identity.authenticationComplete(peerSignature, sharedKeys)runs on P2. P2 takes P1's signature fromfinish, verifies it against thesession_idrecorded in the shared-key arcs, and flips its own arcs toauthentication_complete = true, completing the mutual authentication.
Version negotiation is built in: P2 returns a negotiated version no higher than
P1's advertised version, and finish rejects a peerVersion greater than the
one P1 offered in create with INCOMPATIBLE_PROTOCOL_VERSION.
40.12.3 The Reference Implementation¶
The Rust crates under system/authgraph/ factor the protocol so a TEE vendor
can reuse the protocol logic while swapping in its own primitives:
system/authgraph/core/holds the cryptography-agnostic protocol engine.system/authgraph/core/src/keyexchange.rsimplements thecreate/init/finish/authenticationCompletestate machine and defines the HKDF context strings shown above;arc.rshandles arc sealing,key.rsthe key and identity types, andtraits.rstheDevice,EcDh,Hkdf, andSha256abstractions a backend must supply. The crate is#![no_std]so it can run inside a secure-world TA.system/authgraph/boringssl/provides a default BoringSSL-backed implementation of those crypto traits; a partner can substitute its own AES-GCM, ECDH, and RNG instead.system/authgraph/hal/is the glue that exposes the engine as theIAuthGraphKeyExchangebinder service, converting between the AIDL types and the internalauthgraph_coretypes.system/authgraph/wire/defines the CBOR wire types and error codes shared across the crates.
40.12.4 Who Uses AuthGraph¶
AuthGraph is not invoked directly by apps; it underpins higher-level secure services that need a channel into a different security domain:
- SecretKeeper. The SecretKeeper HAL covered in section 40.5.7 stores pVM
secrets in a higher-privilege domain, so its request/response CBOR protocol
must be encrypted.
ISecretkeeper.aidl(hardware/interfaces/security/secretkeeper/aidl/android/hardware/security/secretkeeper/ISecretkeeper.aidl) importsIAuthGraphKeyExchangeand exposesgetAuthGraphKe(); the pVM client acts as P1, SecretKeeper as P2, and the resulting session keys protect every subsequent SecretManagement message. Its reference implementation undersystem/secretkeeper/reuses thesystem/authgraph/crates for exactly this step. - The SEE AuthMgr. The Secure Execution Environment authorization manager,
IAuthMgrAuthorization(hardware/interfaces/security/see/authmgr/aidl/android/hardware/security/see/authmgr/IAuthMgrAuthorization.aidl), authenticates and authorizes a pVM client before it may reach trusted HALs hosted in a TEE -- the in-VM root-of-trust model from section 40.10.4. It does not run the AuthGraph AKE handshake. Instead it authenticates the AuthMgr frontend (in the pVM) to the AuthMgr backend (in the TEE) via a DICE-based challenge-response:initAuthenticationreturns a 32-byte challenge, andcompleteAuthenticationverifies the frontend's signature over aSignedConnectionRequest(which binds in that challenge) against a stored DICE policy, validating the frontend's DICE certificate chain and enforcing rollback protection. What it reuses from AuthGraph is the DICE certificate-chain and key types --authgraph_core::keyitems such asCertChain,DiceChainEntry,EcSignKey,EcVerifyKey,InstanceIdentifier, andPolicy, plus theEcDsa/Rngtraits -- not the AuthGraph create/init/finish key-exchange, shared-key derivation, or session contexts.
These two consumers use AuthGraph differently. SecretKeeper runs the full, vendor-pluggable, DICE-aware AKE to let two mutually distrustful Android security domains agree on session keys without a shared transport or a pre-shared secret. The SEE AuthMgr borrows only AuthGraph's DICE certificate-chain and key types for its own DICE-based authentication protocol.
40.13 external/keyattestation: The Relying-Party Verifier Library¶
Section 40.4.7 covers the producer side of key attestation: a key is
generated inside secure hardware, and KeyMint emits a certificate chain whose
leaf carries a KeyDescription extension describing the key's properties and
the device's verified-boot state. The relying party then has to verify that
chain: a server, an enterprise enrollment backend, or another app validates the
chain back to a trusted root, parses the attestation extension, and decides
whether to trust the key. Historically each verifier reimplemented that logic,
often imperfectly.
external/keyattestation is the AOSP home for that verifier side. It is a
small Kotlin library (java_library named keyattestation, declared in
external/keyattestation/Android.bp) that parses and validates Android key
attestation certificate chains on behalf of a relying party. The entry point
is the Verifier class
(external/keyattestation/src/main/kotlin/Verifier.kt), constructed with three
caller-supplied sources (a set of trust anchors, a set of revoked certificate
serial numbers, and a time source) and exposing a single verify(chain)
method:
val verifier = Verifier(
{ setOf(TrustAnchor(rootCertificate, null)) }, // trust anchors
{ setOf<String>() }, // revoked serials
{ Instant.now() }, // time source
)
val result = verifier.verify(certificateChain)
verify returns a sealed VerificationResult: Success (carrying the
attested public key, the challenge, the security level, the verified-boot
state, and device identity), or one of several failure states
(PathValidationFailure, ChainParsingFailure, ChallengeMismatch,
ExtensionParsingFailure, ExtensionConstraintViolation). Beyond plain path
validation it enforces attestation-specific invariants: the key's origin must
be GENERATED, and the attestation security level must equal the KeyMint
security level.
Two design choices shape the library. Validation runs through a custom JCA
provider instead of the stock PKIX validator. KeyAttestationProvider
(external/keyattestation/src/main/kotlin/provider/KeyAttestationProvider.kt)
registers a CertPathValidator algorithm named "KeyAttestation", backed by
KeyAttestationCertPathValidator
(external/keyattestation/src/main/kotlin/provider/KeyAttestationCertPathValidator.kt).
That class exists because older devices emit chains that do not fully conform
to RFC 5280 and so cannot be validated by the JDK's built-in
PKIXCertPathValidator; the custom validator is deliberately more permissive
about those chains. Revocation is handled by a PKIXRevocationChecker
subclass, RevocationChecker
(external/keyattestation/src/main/kotlin/provider/RevocationChecker.kt), which
fails any certificate whose hex serial appears in the caller-supplied revoked
set. The other choice is what the library leaves out: it ships only the
verification logic and a set of well-known roots
(external/keyattestation/roots.json). Fetching the live revocation list
(from the Android attestation status endpoint) and supplying the trust anchors
are the caller's job, which keeps the library free of network and policy
assumptions.
This is the verifier counterpart to the KeyMint producer path. KeyMint mints
the chain inside the TEE or StrongBox; keyattestation is what a relying party
links in to check that chain. The module is visible to
frameworks/base/core/java and to vendor subpackages (per its Android.bp
visibility list), so several callers share one implementation instead of each
writing its own.
40.14 Weaver: Throttle-Resistant Secret Slots¶
Gatekeeper (section 40.6.1) verifies a knowledge factor inside the TEE and throttles guessing. Weaver is the storage primitive that backs that throttling and ties the credential to file-based encryption. It provides a fixed array of slots, each holding a key-value pair, where the value can be read back only by presenting the correct key. The lockscreen flow stores a synthetic-password-derived secret as a Weaver value, keyed by a secret derived from the user's credential, so that the encryption key material is released only when the credential is presented and the per-slot throttle permits it.
The HAL contract lives in
hardware/interfaces/weaver/aidl/android/hardware/weaver/IWeaver.aidl. It is
deliberately small: getConfig() reports the slot count and key/value sizes
(recommended at least 64 slots and 16-byte keys/values); write() overwrites a
slot idempotently; read(slotId, key) returns a WeaverReadResponse whose
WeaverReadStatus is OK (with the value), INCORRECT_KEY, THROTTLE, or
FAILED; and getTimeout(slotId) reports the remaining throttle. The AIDL
spells out the throttle rules the implementation must honor: throttling is per
slot, the failure count must be persisted across reboots, a throttled read must
reveal nothing and must not change the failure count, and key comparison must
be constant-time. One rule is easy to miss: the failure count must be
incremented before the key is checked and reset to zero only on success, so
an attacker cannot test a key without the attempt being counted.
system/weaver is the Rust reference TA and HAL service implementing that
contract (it is not the HAL interface itself, which is under
hardware/interfaces/weaver). The core logic is WeaverTa in
system/weaver/ta/src/lib.rs, which runs inside the secure environment and
processes CBOR-encoded requests forwarded by the HAL service in
system/weaver/hal/src/lib.rs. Each Slot stores its slot_key,
slot_value, a failure_counter, and a last_checked_timestamp. The
throttle policy is the TIMEOUT_TABLE in that file: failures 0-4 incur no
delay, then the timeout grows steeply (1 minute at 5 failures, 5/15/30/90
minutes, then hours, days, and ultimately years), and once the table is
exhausted further attempts return a timeout of i64::MAX. Slots are required
to persist across boots and across an untrusted factory reset (with the value
cleared), so the throttle cannot be reset by rebooting. This is the
hardware-backed counterpart to the in-TEE throttling that section 40.6.1
describes for Gatekeeper.
flowchart TD
LS["LockSettingsService<br/>(normal world)"] -->|"read(slotId, key)"| HAL["Weaver HAL service<br/>(system/weaver/hal)"]
HAL -->|"CBOR request"| TA["WeaverTa<br/>(system/weaver/ta, secure env)"]
TA -->|"check throttle, then<br/>compare key constant-time"| SLOT["Persistent slot<br/>(key, value, failure_counter)"]
SLOT -->|"OK + value, or<br/>INCORRECT_KEY / THROTTLE + timeout"| HAL
HAL -->|"WeaverReadResponse"| LS
The newest addition to the contract is warmUp(), a oneway hint added in
Weaver AIDL v3 (in hardware/interfaces/weaver/aidl, not in system/weaver).
It conveys that a read or write will probably happen within a few seconds (for
example when the user starts entering their PIN), so an implementation whose
secure hardware sits in a low-power state can begin transitioning to a ready
state asynchronously, cutting the latency of the upcoming credential check. It
is purely advisory: reads and writes must still work without a preceding
warmUp(), there is no matching cool-down call (implementations re-enter low
power after an idle timeout, recommended at five seconds), and the method may
be a no-op where it does not apply.
40.15 Keystore Certificate Post-Processor¶
Section 40.4.7 describes the attestation certificate chain Keystore returns for
a hardware-backed key, and section 40.13 covers external/keyattestation, the
library a relying party uses to verify that chain. Android 17 adds a hook on
the issuing side: after Keystore has assembled the chain, it can hand the
chain to a registered service that returns a replacement chain. The platform
piece is an AIDL contract plus a service lookup; the actual post-processing is
left to whatever service is registered under a fixed name.
The contract is IKeystoreCertificatePostProcessor, in package
android.security.postprocessor
(system/security/keystore2/aidl/android/security/postprocessor/IKeystoreCertificatePostProcessor.aidl).
It has one method, processKeystoreCertificates(in CertificateChain), which
takes a CertificateChain (a leafCertificate plus a remainingChain of the
concatenated DER-encoded intermediates and root) and returns a replacement
CertificateChain. An implementing service derives from the generated
BnKeystoreCertificatePostProcessor.
Keystore actually invokes this hook, but only on a narrow path and only when a
runtime flag is set. The call lives in
system/security/keystore2/src/security_level.rs, in the branch that handles a
key attested with an RKP-provisioned attestation key
(AttestationKeyInfo::RkpdProvisioned). That branch checks the system property
remote_provisioning.use_cert_processor (default false); when it is set, it
calls process_certificate_chain from the postprocessor_client helper crate
(system/security/keystore2/postprocessor_client/src/lib.rs) instead of just
appending the RKP chain to the result. Keystore does not bind a post-processor
at startup or for the common attestation paths; it looks one up by name only at
this point and only behind the flag.
The helper crate is where the lookup happens. process_certificate_chain
packs the leaf and remaining certificates into a CertificateChain, then calls
binder::wait_for_interface("rkp_cert_processor.service") to find the
registered post-processor and invokes processKeystoreCertificates on it. The
service name is hard-coded, the lookup runs on a worker thread with a 5-second
timeout, and the result is fail-open: on success the returned chain replaces the
original, and on any error (timeout, missing service, or a service-specific
error) the helper logs a warning and falls back to the original Keystore chain.
A timeout latches a process-wide flag so Keystore stops retrying the lookup
until the next reboot. So the hook is a registered-service contract that
Keystore consults by name, not a chain of post-processors or a callback the
caller supplies.
flowchart TD
GEN["generate_key<br/>(RkpdProvisioned branch,<br/>security_level.rs)"] -->|"remote_provisioning.use_cert_processor set?"| CHECK{"flag on?"}
CHECK -->|"no"| APPEND["append RKP chain to result<br/>(original behavior)"]
CHECK -->|"yes"| HELPER["process_certificate_chain<br/>(postprocessor_client)"]
HELPER -->|"wait_for_interface<br/>(rkp_cert_processor.service)"| SVC["registered post-processor<br/>(BnKeystoreCertificatePostProcessor)"]
SVC -->|"processKeystoreCertificates<br/>(CertificateChain)"| HELPER
HELPER -->|"success: replacement chain"| RESULT["result.certificateChain"]
HELPER -->|"timeout / error: fall back"| RESULT
40.15.1 DroidfoodAttestationFixer: A Flag-Gated Reference Consumer¶
AOSP ships one consumer of this hook, packages/services/DroidfoodAttestationFixer,
a Rust service that builds the binary rkp_cert_processor. It is meant for
dogfood and test devices whose factory-provisioned attestation chain needs to be
fixed up, not for production. main.rs registers the implementation under the
expected name with binder::register_lazy_service("rkp_cert_processor.service", ...),
so it starts on demand when Keystore looks the service up. lib.rs implements
processKeystoreCertificates by base64-encoding the existing leaf and chain,
POSTing them to a server over a small libcurl-backed C++ client (src/curl),
and returning the server's replacement chain; on a decode or server error it
returns a service-specific error, which leaves Keystore on the original chain.
The build wires it as a system_ext component (Android.bp): the
rust_binary is system_ext_specific, and its init script
droidfoodattestation.rc installs a service that runs /system_ext/bin/rkp_cert_processor,
declares interface aidl rkp_cert_processor.service, and is oneshot and
disabled (started lazily by the service manager rather than at boot). The
whole package is gated by an aconfig flag,
android.security.postprocessor.droidfood_attestation_bringup
(droidfood_attestation_flags.aconfig, namespace hardware_backed_security,
bug 361877215), declared is_fixed_read_only so it is fixed at build time and
container system_ext. The flag controls the package rollout; the
remote_provisioning.use_cert_processor property controls whether Keystore
attempts the lookup at all. DroidfoodAttestationFixer is one reference
implementation of the post-processor contract; the reusable platform addition is
the IKeystoreCertificatePostProcessor AIDL and the named service lookup in
Keystore, which any vendor service could implement instead.
40.16 Try It¶
This section provides hands-on exercises for exploring Android's security subsystems.
Exercise 40.1: Inspect SELinux Policy¶
Build the sepolicy and inspect its contents:
# In the AOSP source tree
cd system/sepolicy
# List all types defined in public policy
grep '^type ' public/*.te | sort
# Count neverallow rules
grep -c 'neverallow' private/*.te
# View the domain assigned to a specific app
grep 'untrusted_app' private/seapp_contexts
On a running device:
# Check SELinux mode
adb shell getenforce
# View the SELinux context of a running process
adb shell ps -eZ | grep com.example.myapp
# View recent SELinux denials
adb shell dmesg | grep 'avc: denied'
# Check file contexts
adb shell ls -Z /data/data/com.example.myapp/
Exercise 40.2: Examine Verified Boot State¶
# Check the verified boot state from userspace
adb shell getprop ro.boot.verifiedbootstate
# Output: green (locked, verified), yellow (locked, custom key),
# orange (unlocked), red (verification failure)
# View the vbmeta digest
adb shell getprop ro.boot.vbmeta.digest
# Check device lock state
adb shell getprop ro.boot.vbmeta.device_state
# Examine AVB metadata on a partition image
avbtool info_image --image boot.img
Exercise 40.3: Explore Keystore Keys¶
# List all Keystore aliases for the current user
adb shell cmd keystore2 list
# Generate a test key
# (In an Android app)
val keyGen = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
keyGen.initialize(
KeyGenParameterSpec.Builder("test_key",
KeyProperties.PURPOSE_SIGN)
.setDigests(KeyProperties.DIGEST_SHA256)
.build())
val keyPair = keyGen.generateKeyPair()
# Check the security level of the key
adb shell dumpsys keystore2
Exercise 40.4: Verify App Sandbox Isolation¶
# Check the UID of a running app
adb shell ps -o USER,PID,NAME | grep com.example
# Try to access another app's data (should fail)
adb shell run-as com.example.app1 ls /data/data/com.example.app2/
# Output: ls: /data/data/com.example.app2/: Permission denied
# View seccomp filter status
adb shell cat /proc/<pid>/status | grep Seccomp
# Output: Seccomp: 2 (2 = filter mode)
# View the SELinux domain
adb shell cat /proc/<pid>/attr/current
# Output: u:r:untrusted_app:s0:c42,c256,c512,c768
Exercise 40.5: Inspect Encryption Status¶
# Check FBE status
adb shell getprop ro.crypto.state
# Output: encrypted
# Check which encryption type is in use
adb shell getprop ro.crypto.type
# Output: file
# List DE and CE directories
adb shell ls /data/system_de/0/
adb shell ls /data/system_ce/0/
# Check dm-default-key (metadata encryption)
adb shell dmctl table userdata
Exercise 40.6: Test Network Security Config¶
Create a test app with network security config:
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
<domain-config>
<domain>httpbin.org</domain>
<pin-set>
<pin digest="SHA-256">
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
</pin>
</pin-set>
</domain-config>
</network-security-config>
<!-- AndroidManifest.xml -->
<application
android:networkSecurityConfig="@xml/network_security_config"
... >
Then test:
# HTTP connection should fail (cleartext blocked)
# HTTPS to httpbin.org should fail (pin mismatch)
# HTTPS to other hosts should succeed
Exercise 40.7: Examine Trusty Services¶
# On a Trusty-enabled device, check for the Trusty IPC device
adb shell ls -la /dev/trusty-ipc-dev0
# View running Trusty HAL services
adb shell dumpsys -l | grep -i trusty
# Check KeyMint HAL instance
adb shell service check android.hardware.security.keymint.IKeyMintDevice/default
# View Gatekeeper HAL
adb shell service check android.hardware.gatekeeper.IGatekeeper/default
Exercise 40.8: Audit SELinux Policy Changes¶
Practice the audit2allow workflow:
# 1. Capture denials
adb shell dmesg | grep 'avc: denied' > /tmp/denials.txt
# 2. Generate candidate allow rules
audit2allow -i /tmp/denials.txt
# 3. NEVER blindly apply! Instead:
# - Determine if the access is legitimate
# - If so, create a specific type for the target
# - Write a minimal allow rule
# - Add appropriate neverallow tests
# 4. Rebuild sepolicy
cd system/sepolicy
mmm .
Exercise 40.9: Trace a Key Generation through the Stack¶
Use system tracing to follow a key generation from app to TEE:
# Enable tracing
adb shell atrace --async_start -b 65536 aidl hal
# In your app: generate a KeyStore key
# Collect trace
adb shell atrace --async_stop -z > /tmp/trace.html
# Or use perfetto for more detailed tracing
adb shell perfetto --txt -c - <<EOF
buffers { size_kb: 65536 }
data_sources {
config {
name: "android.gpu.memory"
}
}
data_sources {
config {
name: "android.surfaceflinger.frametimeline"
}
}
duration_ms: 10000
EOF
Exercise 40.10: Build and Flash Custom AVB Keys¶
For development purposes only, on an unlocked device:
# Generate a new RSA 4096 key pair
openssl genrsa -out custom_key.pem 4096
# Extract the public key for embedding
avbtool extract_public_key --key custom_key.pem --output custom_key.bin
# Sign a boot image
avbtool add_hash_footer \
--image boot.img \
--partition_size $((64 * 1024 * 1024)) \
--partition_name boot \
--key custom_key.pem \
--algorithm SHA256_RSA4096
# Sign the vbmeta image
avbtool make_vbmeta_image \
--output vbmeta.img \
--key custom_key.pem \
--algorithm SHA256_RSA4096 \
--include_descriptors_from_image boot.img
# Flash (requires unlocked bootloader)
fastboot flash vbmeta vbmeta.img
fastboot flash boot boot.img
Exercise 40.11: Write a Custom SELinux Policy for a New Daemon¶
This exercise walks through creating SELinux policy from scratch for a
hypothetical new system daemon called my_daemon.
Step 1: Create the type definition (system/sepolicy/public/my_daemon.te):
# Type for the my_daemon process
type my_daemon, domain;
# Type for the executable
type my_daemon_exec, exec_type, file_type, system_file_type;
Step 2: Create the private policy (system/sepolicy/private/my_daemon.te):
# Domain transition from init
init_daemon_domain(my_daemon)
# Allow access to its own data directory
type my_daemon_data_file, file_type, data_file_type;
allow my_daemon my_daemon_data_file:dir create_dir_perms;
allow my_daemon my_daemon_data_file:file create_file_perms;
# Allow binder communication with system_server
binder_use(my_daemon)
binder_call(my_daemon, system_server)
# Allow reading system properties
get_prop(my_daemon, system_prop)
# Allow network access (if needed)
net_domain(my_daemon)
Step 3: Add file_contexts:
/system/bin/my_daemon u:object_r:my_daemon_exec:s0
/data/misc/my_daemon(/.*)? u:object_r:my_daemon_data_file:s0
Step 4: Add service_contexts (if registering a binder service):
Step 5: Build and verify:
Exercise 40.12: Analyze the Authentication Flow¶
Trace the complete flow from screen unlock to key availability:
# Enable verbose Keystore logging
adb shell setprop log.tag.keystore2 VERBOSE
# Monitor the authentication flow
adb logcat -s keystore2:* GateKeeper:* Fingerprint:*
# Lock and unlock the device, then search for:
# - GateKeeper: verify() calls
# - HardwareAuthToken generation
# - Keystore2: addAuthToken() calls
# - Super key unlocking
Exercise 40.13: Measure the Security Surface¶
Quantify the security-relevant code:
# Count SELinux rules
grep -c 'allow ' system/sepolicy/private/*.te
grep -c 'neverallow' system/sepolicy/private/*.te
# Count Keystore2 source lines
wc -l system/security/keystore2/src/*.rs
# List all AIDL security interfaces
find hardware/interfaces/security -name "*.aidl" | wc -l
find hardware/interfaces/biometrics -name "*.aidl" | wc -l
# Count AVB source lines
wc -l external/avb/libavb/*.c external/avb/libavb/*.h
Exercise 40.14: Examine Verified Boot on a Real Device¶
# Dump the full vbmeta information
adb pull /dev/block/by-name/vbmeta vbmeta.img
avbtool info_image --image vbmeta.img
# The output will show:
# - Algorithm: SHA256_RSA4096 (or similar)
# - Rollback index: <value>
# - Release string: "avbtool ..."
# - Descriptors:
# - Hash descriptor for boot
# - Hashtree descriptor for system
# - Chain partition descriptor for vendor
# Check the dm-verity status at runtime
adb shell cat /proc/device-mapper/verity/status
# Or on newer kernels:
adb shell dmctl status system
Exercise 40.15: Explore Hardware-Backed Key Properties¶
// In an Android app, generate a key and inspect its properties
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
"test_hwbound_key",
KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
.setDigests(KeyProperties.DIGEST_SHA256)
.setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1"))
.setIsStrongBoxBacked(true) // Request StrongBox
.setUserAuthenticationRequired(true)
.setUserAuthenticationValidityDurationSeconds(30)
.setAttestationChallenge("my_challenge".getBytes())
.build();
KeyPairGenerator kpg = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
kpg.initialize(spec);
KeyPair kp = kpg.generateKeyPair();
// Inspect the attestation certificate chain
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
Certificate[] chain = ks.getCertificateChain("test_hwbound_key");
// chain[0] = attestation cert (contains key properties in extension)
// chain[1..n] = intermediate + root certs
// Parse the attestation extension (OID 1.3.6.1.4.1.11129.2.1.17)
// to read:
// - Security level (TEE or StrongBox)
// - Verified boot state
// - OS version and patch level
// - Key properties (algorithm, purpose, auth requirements)
Summary¶
Android's security is not a single feature but an interlocking set of mechanisms, each providing defense even when neighboring layers are compromised:
| Layer | Mechanism | Protects Against |
|---|---|---|
| Hardware | Root of trust, TEE, StrongBox | Physical attacks, key extraction |
| Boot | AVB, dm-verity | OS tampering, rollback attacks |
| Kernel | SELinux, seccomp, namespaces | Privilege escalation, policy bypass |
| Storage | FBE, metadata encryption | Data theft from powered-off device |
| Framework | Keystore2, permissions, app sandbox | App-to-app attacks, unauthorized access |
| Network | NSC, certificate pinning, CT | Man-in-the-middle, traffic interception |
| User auth | Gatekeeper, biometrics | Unauthorized physical access |
The key insight is that these layers are not alternatives -- they are cumulative. An attacker must defeat all of them simultaneously to fully compromise a device. Each layer assumes the layer below it might be partially compromised and provides independent protection. Android 17 extends the same philosophy to new fronts: post-quantum signatures hedge against future cryptographic breaks, and moving the root-of-trust HALs into protected VMs shrinks the host attack surface that can reach them.
Key Source Files Reference¶
| Path | Component | Language |
|---|---|---|
system/sepolicy/ |
SELinux policy | SELinux TE |
system/sepolicy/public/ |
Exported types & attributes | SELinux TE |
system/sepolicy/private/ |
Platform-private policy | SELinux TE |
system/sepolicy/vendor/ |
Vendor HAL policy | SELinux TE |
external/avb/ |
Android Verified Boot | C |
external/avb/libavb/ |
Core AVB library | C |
external/avb/avbtool.py |
AVB build tool | Python |
system/security/keystore2/ |
Keystore2 service | Rust |
system/security/keystore2/src/ |
Keystore2 core modules | Rust |
system/security/keystore2/aidl/ |
Keystore2 AIDL interface | AIDL |
system/core/trusty/ |
Trusty HAL integrations | Rust/C++ |
system/core/trusty/keymint/ |
KeyMint HAL for Trusty | Rust |
system/core/trusty/gatekeeper/ |
Gatekeeper HAL for Trusty | C++ |
system/core/trusty/confirmationui/ |
Confirmation UI for Trusty | C++ |
system/core/trusty/secretkeeper/ |
SecretKeeper HAL | Rust |
system/core/trusty/confirmationui/ |
Trusty ConfirmationUI HAL (links libteeui) | C++ |
system/teeui/libteeui/ |
Protected Confirmation rendering engine | C++ |
system/nvram/ |
Access-controlled NVRAM reference HAL | C++ |
hardware/interfaces/security/authgraph/aidl/ |
AuthGraph AKE HAL interface | AIDL |
system/authgraph/ |
AuthGraph AKE reference implementation | Rust |
system/authgraph/core/src/keyexchange.rs |
AuthGraph handshake state machine | Rust |
system/secretkeeper/ |
SecretKeeper reference implementation | Rust |
hardware/interfaces/security/see/authmgr/aidl/ |
SEE AuthMgr authorization HAL | AIDL |
system/core/trusty/libtrusty/ |
IPC library for Trusty | C |
trusty/ |
Trusty TEE OS | C |
trusty/kernel/ |
Trusty kernel (Little Kernel) | C |
trusty/user/ |
Trusty userspace TAs | C |
system/vold/ |
Volume daemon (encryption) | C++ |
system/vold/FsCrypt.cpp |
File-Based Encryption | C++ |
system/vold/MetadataCrypt.cpp |
Metadata encryption | C++ |
system/vold/KeyStorage.cpp |
Key storage and wrapping | C++ |
hardware/interfaces/biometrics/ |
Biometric HALs | AIDL/C++ |
hardware/interfaces/biometrics/fingerprint/aidl/ |
Fingerprint HAL | AIDL |
hardware/interfaces/biometrics/face/aidl/ |
Face HAL | AIDL |
hardware/interfaces/security/keymint/aidl/ |
KeyMint HAL | AIDL |
system/keymint/common/src/crypto/mldsa.rs |
ML-DSA post-quantum keys (Android 17) | Rust |
system/keymint/ta/src/clock.rs |
KeyMint-hosted secure clock | Rust |
system/security/keystore2/src/remote_provisioning.rs |
RKP routing and gating | Rust |
system/security/keystore2/postprocessor_client/ |
Attestation cert post-processing client | Rust |
packages/services/DroidfoodAttestationFixer/ |
RKP cert post-processor service | Rust/C++ |
frameworks/base/packages/NetworkSecurityConfig/ |
Network security | Java |