forked from AOSPA/android_frameworks_base
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial implementation of KeyboxImitationHooks
Squashed: Author: Adithya R <[email protected]> Date: Mon, 23 Sep 2024 17:07:15 +0530 KeyboxImitationHooks: Few minor improvements - Avoid logspam - Bail out if current process is unknown - Print current process name with debug logs Change-Id: I2af79186c9de085c96c6be0f242327f5cfafe34f Co-authored-by: chiteroman <[email protected]> Co-authored-by: Fabian Leutenegger <[email protected]> Change-Id: I163df27cc743f33ae1b30161f65076b744e16754 Signed-off-by: Adithya R <[email protected]>
- Loading branch information
1 parent
26dccd1
commit 5608ba0
Showing
7 changed files
with
418 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} |
114 changes: 114 additions & 0 deletions
114
core/java/com/android/internal/util/KeyProviderManager.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String, String> 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") | ||
}; | ||
} | ||
} | ||
} |
215 changes: 215 additions & 0 deletions
215
core/java/com/android/internal/util/KeyboxImitationHooks.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
Oops, something went wrong.