diff --git a/core/java/com/android/internal/util/IKeyboxProvider.java b/core/java/com/android/internal/util/IKeyboxProvider.java new file mode 100644 index 000000000000..3463fcce572d --- /dev/null +++ b/core/java/com/android/internal/util/IKeyboxProvider.java @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2024 Paranoid Android + * SPDX-License-Identifier: Apache-2.0 + */ +package com.android.internal.util; + +/** + * Interface for keybox providers. + * + * This interface defines the methods that a keybox provider must implement + * to provide access to EC and RSA keys and certificate chains. + * + * @hide + */ +public interface IKeyboxProvider { + + /** + * Checks if a valid keybox is available. + * + * @return true if a valid keybox is available, false otherwise + * @hide + */ + boolean hasKeybox(); + + /** + * Retrieves the EC private key. + * + * @return the EC private key as a String + * @hide + */ + String getEcPrivateKey(); + + /** + * Retrieves the RSA private key. + * + * @return the RSA private key as a String + * @hide + */ + String getRsaPrivateKey(); + + /** + * Retrieves the EC certificate chain. + * + * @return an array of Strings representing the EC certificate chain + * @hide + */ + String[] getEcCertificateChain(); + + /** + * Retrieves the RSA certificate chain. + * + * @return an array of Strings representing the RSA certificate chain + * @hide + */ + String[] getRsaCertificateChain(); +} diff --git a/core/java/com/android/internal/util/KeyProviderManager.java b/core/java/com/android/internal/util/KeyProviderManager.java new file mode 100644 index 000000000000..449f674d02c3 --- /dev/null +++ b/core/java/com/android/internal/util/KeyProviderManager.java @@ -0,0 +1,114 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Paranoid Android + * SPDX-License-Identifier: Apache-2.0 + */ +package com.android.internal.util; + +import android.app.ActivityThread; +import android.content.Context; +import android.util.Log; + +import com.android.internal.R; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Manager class for handling keybox providers. + * @hide + */ +public final class KeyProviderManager { + + private static final String TAG = "KeyProviderManager"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private static final IKeyboxProvider PROVIDER = new DefaultKeyboxProvider(); + + private KeyProviderManager() { + } + + public static IKeyboxProvider getProvider() { + return PROVIDER; + } + + public static boolean isKeyboxAvailable() { + if (!PropImitationHooks.sEnableKeyboxImitation) { + dlog("Key attestation spoofing is disabled by user"); + return false; + } + return PROVIDER.hasKeybox(); + } + + private static void dlog(String msg) { + if (DEBUG) Log.d(TAG, msg); + } + + private static class DefaultKeyboxProvider implements IKeyboxProvider { + private final Map keyboxData = new HashMap<>(); + + private DefaultKeyboxProvider() { + Context context = getApplicationContext(); + if (context == null) { + Log.e(TAG, "Failed to get application context"); + return; + } + + String[] keybox = context.getResources().getStringArray(R.array.config_certifiedKeybox); + + Arrays.stream(keybox) + .map(entry -> entry.split(":", 2)) + .filter(parts -> parts.length == 2) + .forEach(parts -> keyboxData.put(parts[0], parts[1])); + + if (!hasKeybox()) { + Log.w(TAG, "Incomplete keybox data loaded"); + } + } + + private static Context getApplicationContext() { + try { + return ActivityThread.currentApplication().getApplicationContext(); + } catch (Exception e) { + Log.e(TAG, "Error getting application context", e); + return null; + } + } + + @Override + public boolean hasKeybox() { + return Arrays.asList("EC.PRIV", "EC.CERT_1", "EC.CERT_2", "EC.CERT_3", + "RSA.PRIV", "RSA.CERT_1", "RSA.CERT_2", "RSA.CERT_3") + .stream() + .allMatch(keyboxData::containsKey); + } + + @Override + public String getEcPrivateKey() { + return keyboxData.get("EC.PRIV"); + } + + @Override + public String getRsaPrivateKey() { + return keyboxData.get("RSA.PRIV"); + } + + @Override + public String[] getEcCertificateChain() { + return getCertificateChain("EC"); + } + + @Override + public String[] getRsaCertificateChain() { + return getCertificateChain("RSA"); + } + + private String[] getCertificateChain(String prefix) { + return new String[]{ + keyboxData.get(prefix + ".CERT_1"), + keyboxData.get(prefix + ".CERT_2"), + keyboxData.get(prefix + ".CERT_3") + }; + } + } +} diff --git a/core/java/com/android/internal/util/KeyboxImitationHooks.java b/core/java/com/android/internal/util/KeyboxImitationHooks.java new file mode 100644 index 000000000000..665497765750 --- /dev/null +++ b/core/java/com/android/internal/util/KeyboxImitationHooks.java @@ -0,0 +1,215 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Paranoid Android + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.internal.util; + +import android.app.Application; +import android.security.KeyChain; +import android.security.keystore.KeyProperties; +import android.system.keystore2.KeyEntryResponse; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.org.bouncycastle.asn1.ASN1Boolean; +import com.android.internal.org.bouncycastle.asn1.ASN1Encodable; +import com.android.internal.org.bouncycastle.asn1.ASN1EncodableVector; +import com.android.internal.org.bouncycastle.asn1.ASN1Enumerated; +import com.android.internal.org.bouncycastle.asn1.ASN1ObjectIdentifier; +import com.android.internal.org.bouncycastle.asn1.ASN1OctetString; +import com.android.internal.org.bouncycastle.asn1.ASN1Sequence; +import com.android.internal.org.bouncycastle.asn1.ASN1TaggedObject; +import com.android.internal.org.bouncycastle.asn1.DEROctetString; +import com.android.internal.org.bouncycastle.asn1.DERSequence; +import com.android.internal.org.bouncycastle.asn1.DERTaggedObject; +import com.android.internal.org.bouncycastle.asn1.x509.Extension; +import com.android.internal.org.bouncycastle.cert.X509CertificateHolder; +import com.android.internal.org.bouncycastle.cert.X509v3CertificateBuilder; +import com.android.internal.org.bouncycastle.operator.ContentSigner; +import com.android.internal.org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +import java.io.ByteArrayOutputStream; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.concurrent.ThreadLocalRandom; + +/** + * @hide + */ +public class KeyboxImitationHooks { + + private static final String TAG = "KeyboxImitationHooks"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private static final ASN1ObjectIdentifier KEY_ATTESTATION_OID = new ASN1ObjectIdentifier( + "1.3.6.1.4.1.11129.2.1.17"); + + private static volatile String sProcessName; + + private static PrivateKey parsePrivateKey(String encodedKey, String algorithm) + throws Exception { + byte[] keyBytes = Base64.getDecoder().decode(encodedKey); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + return KeyFactory.getInstance(algorithm).generatePrivate(keySpec); + } + + private static byte[] parseCertificate(String encodedCert) { + return Base64.getDecoder().decode(encodedCert); + } + + private static byte[] getCertificateChain(String algorithm) throws Exception { + IKeyboxProvider provider = KeyProviderManager.getProvider(); + String[] certChain = KeyProperties.KEY_ALGORITHM_EC.equals(algorithm) + ? provider.getEcCertificateChain() + : provider.getRsaCertificateChain(); + + ByteArrayOutputStream certificateStream = new ByteArrayOutputStream(); + for (String cert : certChain) { + certificateStream.write(parseCertificate(cert)); + } + return certificateStream.toByteArray(); + } + + private static PrivateKey getPrivateKey(String algorithm) throws Exception { + IKeyboxProvider provider = KeyProviderManager.getProvider(); + String privateKeyEncoded = KeyProperties.KEY_ALGORITHM_EC.equals(algorithm) + ? provider.getEcPrivateKey() + : provider.getRsaPrivateKey(); + + return parsePrivateKey(privateKeyEncoded, algorithm); + } + + private static X509CertificateHolder getCertificateHolder(String algorithm) throws Exception { + IKeyboxProvider provider = KeyProviderManager.getProvider(); + String certChain = KeyProperties.KEY_ALGORITHM_EC.equals(algorithm) + ? provider.getEcCertificateChain()[0] + : provider.getRsaCertificateChain()[0]; + + return new X509CertificateHolder(parseCertificate(certChain)); + } + + private static byte[] modifyLeafCertificate(X509Certificate leafCertificate, + String keyAlgorithm) throws Exception { + X509CertificateHolder certificateHolder = new X509CertificateHolder( + leafCertificate.getEncoded()); + Extension keyAttestationExtension = certificateHolder.getExtension(KEY_ATTESTATION_OID); + ASN1Sequence keyAttestationSequence = ASN1Sequence.getInstance( + keyAttestationExtension.getExtnValue().getOctets()); + ASN1Encodable[] keyAttestationEncodables = keyAttestationSequence.toArray(); + ASN1Sequence teeEnforcedSequence = (ASN1Sequence) keyAttestationEncodables[7]; + ASN1EncodableVector teeEnforcedVector = new ASN1EncodableVector(); + + ASN1Sequence rootOfTrustSequence = null; + for (ASN1Encodable teeEnforcedEncodable : teeEnforcedSequence) { + ASN1TaggedObject taggedObject = (ASN1TaggedObject) teeEnforcedEncodable; + if (taggedObject.getTagNo() == 704) { + rootOfTrustSequence = (ASN1Sequence) taggedObject.getObject(); + continue; + } + teeEnforcedVector.add(teeEnforcedEncodable); + } + + if (rootOfTrustSequence == null) throw new Exception("Root of trust not found"); + + PrivateKey privateKey = getPrivateKey(keyAlgorithm); + X509CertificateHolder providerCertHolder = getCertificateHolder(keyAlgorithm); + + X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder( + providerCertHolder.getSubject(), + certificateHolder.getSerialNumber(), + certificateHolder.getNotBefore(), + certificateHolder.getNotAfter(), + certificateHolder.getSubject(), + certificateHolder.getSubjectPublicKeyInfo() + ); + + ContentSigner contentSigner = new JcaContentSignerBuilder( + leafCertificate.getSigAlgName()).build(privateKey); + + byte[] verifiedBootKey = new byte[32]; + ThreadLocalRandom.current().nextBytes(verifiedBootKey); + + DEROctetString verifiedBootHash = (DEROctetString) rootOfTrustSequence.getObjectAt(3); + if (verifiedBootHash == null) { + byte[] randomHash = new byte[32]; + ThreadLocalRandom.current().nextBytes(randomHash); + verifiedBootHash = new DEROctetString(randomHash); + } + + ASN1Encodable[] rootOfTrustEncodables = { + new DEROctetString(verifiedBootKey), + ASN1Boolean.TRUE, + new ASN1Enumerated(0), + verifiedBootHash + }; + + ASN1Sequence newRootOfTrustSequence = new DERSequence(rootOfTrustEncodables); + ASN1TaggedObject rootOfTrustTaggedObject = new DERTaggedObject(704, newRootOfTrustSequence); + teeEnforcedVector.add(rootOfTrustTaggedObject); + + ASN1Sequence newTeeEnforcedSequence = new DERSequence(teeEnforcedVector); + keyAttestationEncodables[7] = newTeeEnforcedSequence; + ASN1Sequence newKeyAttestationSequence = new DERSequence(keyAttestationEncodables); + ASN1OctetString newKeyAttestationOctetString = new DEROctetString( + newKeyAttestationSequence); + Extension newKeyAttestationExtension = new Extension(KEY_ATTESTATION_OID, false, + newKeyAttestationOctetString); + + certificateBuilder.addExtension(newKeyAttestationExtension); + + for (ASN1ObjectIdentifier extensionOID : + certificateHolder.getExtensions().getExtensionOIDs()) { + if (KEY_ATTESTATION_OID.getId().equals(extensionOID.getId())) continue; + certificateBuilder.addExtension(certificateHolder.getExtension(extensionOID)); + } + + return certificateBuilder.build(contentSigner).getEncoded(); + } + + public static KeyEntryResponse onGetKeyEntry(KeyEntryResponse response) { + if (response == null || response.metadata == null || response.metadata.certificate == null) + return response; + + final String processName = Application.getProcessName(); + if (TextUtils.isEmpty(processName)) { + Log.e(TAG, "Null process name"); + return response; + } + sProcessName = processName; + + // If no keybox is found, don't continue spoofing + if (!KeyProviderManager.isKeyboxAvailable()) { + dlog("Key attestation spoofing is unavailable"); + return response; + } + + try { + X509Certificate certificate = KeyChain.toCertificate(response.metadata.certificate); + if (certificate.getExtensionValue(KEY_ATTESTATION_OID.getId()) == null) { + dlog("Key attestation OID not found, skipping modification"); + return response; + } + + String keyAlgorithm = certificate.getPublicKey().getAlgorithm(); + response.metadata.certificate = modifyLeafCertificate(certificate, keyAlgorithm); + response.metadata.certificateChain = getCertificateChain(keyAlgorithm); + dlog("Succesfully modified certificate chain"); + } catch (Exception e) { + elog("Error in onGetKeyEntry", e); + } + + return response; + } + + private static void dlog(String msg) { + if (DEBUG) Log.d(TAG, "[" + sProcessName + "] " + msg); + } + + private static void elog(String msg, Exception e) { + Log.e(TAG, "[" + sProcessName + "] " + msg, e); + } +} diff --git a/core/java/com/android/internal/util/PropImitationHooks.java b/core/java/com/android/internal/util/PropImitationHooks.java index 6d780606eed0..55acd081662e 100644 --- a/core/java/com/android/internal/util/PropImitationHooks.java +++ b/core/java/com/android/internal/util/PropImitationHooks.java @@ -38,8 +38,10 @@ public class PropImitationHooks { private static final int FEATURE_GMS_PROP_IMITATION = 1 << 0; private static final int FEATURE_GMS_BLOCK_KEY_ATTESTATION = 1 << 1; + private static final int FEATURE_GMS_KEYBOX_IMITATION = 1 << 2; private static final int FEATURE_ALL = FEATURE_GMS_PROP_IMITATION - | FEATURE_GMS_BLOCK_KEY_ATTESTATION; + | FEATURE_GMS_BLOCK_KEY_ATTESTATION + | FEATURE_GMS_KEYBOX_IMITATION; private static final String PACKAGE_ARCORE = "com.google.ar.core"; private static final String PACKAGE_FINSKY = "com.android.vending"; @@ -71,6 +73,8 @@ public class PropImitationHooks { (sEnabledFeatures & FEATURE_GMS_PROP_IMITATION) != 0; private static final Boolean sEnableKeyAttestationBlock = (sEnabledFeatures & FEATURE_GMS_BLOCK_KEY_ATTESTATION) != 0; + protected static final Boolean sEnableKeyboxImitation = + (sEnabledFeatures & FEATURE_GMS_KEYBOX_IMITATION) != 0; private static volatile JSONObject sCertifiedProps; private static volatile String sStockFp, sNetflixModel; @@ -276,6 +280,12 @@ public static void onEngineGetCertificateChain() { return; } + // If a keybox is found, don't block key attestation + if (KeyProviderManager.isKeyboxAvailable()) { + dlog("Key attestation blocking is disabled because a keybox is defined to spoof"); + return; + } + // Check stack for SafetyNet or Play Integrity if (isCallerSafetyNet() || sIsFinsky) { dlog("Blocked key attestation sIsGms=" + sIsGms + " sIsFinsky=" + sIsFinsky); diff --git a/core/res/res/values/aospa_config.xml b/core/res/res/values/aospa_config.xml index f615f1bad8be..402fdf195977 100644 --- a/core/res/res/values/aospa_config.xml +++ b/core/res/res/values/aospa_config.xml @@ -105,4 +105,20 @@ + + + + + diff --git a/core/res/res/values/aospa_symbols.xml b/core/res/res/values/aospa_symbols.xml index 0682abe900e3..77e704b1ee3c 100644 --- a/core/res/res/values/aospa_symbols.xml +++ b/core/res/res/values/aospa_symbols.xml @@ -71,9 +71,10 @@ - + + diff --git a/keystore/java/android/security/KeyStore2.java b/keystore/java/android/security/KeyStore2.java index dd703f5eefb9..a335d84bd78c 100644 --- a/keystore/java/android/security/KeyStore2.java +++ b/keystore/java/android/security/KeyStore2.java @@ -32,6 +32,8 @@ import android.system.keystore2.ResponseCode; import android.util.Log; +import com.android.internal.util.KeyboxImitationHooks; + import java.util.Calendar; /** @@ -283,7 +285,8 @@ public KeyEntryResponse getKeyEntry(@NonNull KeyDescriptor descriptor) throws KeyStoreException { StrictMode.noteDiskRead(); - return handleRemoteExceptionWithRetry((service) -> service.getKeyEntry(descriptor)); + return KeyboxImitationHooks.onGetKeyEntry( + handleRemoteExceptionWithRetry((service) -> service.getKeyEntry(descriptor))); } /**