Merge pull request #15548 from joefarebrother/android-local-auth-keys

Java: Add query for insecurely generated keys for local authentication.
This commit is contained in:
Joe Farebrother
2024-02-22 14:04:17 +00:00
committed by GitHub
21 changed files with 377 additions and 1 deletions

View File

@@ -1,6 +1,7 @@
/** Definitions for the insecure local authentication query. */
import java
private import semmle.code.java.dataflow.DataFlow
/** A base class that is used as a callback for biometric authentication. */
private class AuthenticationCallbackClass extends Class {
@@ -40,3 +41,24 @@ class AuthenticationSuccessCallback extends Method {
not result = this.getASuperResultUse()
}
}
/** A call that sets a parameter for key generation that is insecure for use with biometric authentication. */
class InsecureBiometricKeyParamCall extends MethodCall {
InsecureBiometricKeyParamCall() {
exists(string name, CompileTimeConstantExpr val |
this.getMethod()
.hasQualifiedName("android.security.keystore", "KeyGenParameterSpec$Builder", name) and
DataFlow::localExprFlow(val, this.getArgument(0)) and
(
name = ["setUserAuthenticationRequired", "setInvalidatedByBiometricEnrollment"] and
val.getBooleanValue() = false
or
name = "setUserAuthenticationValidityDurationSeconds" and
val.getIntValue() != -1
)
)
}
}
/** Holds if the application contains an instance of a key being used for local biometric authentication. */
predicate usesLocalAuth() { exists(AuthenticationSuccessCallback cb | exists(cb.getAResultUse())) }

View File

@@ -0,0 +1,43 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Biometric authentication, such as fingerprint recognition, can be used alongside cryptographic keys stored in the Android <code>KeyStore</code> to protect sensitive parts of the application. However,
when a key generated for this purpose has certain parameters set insecurely, an attacker with physical access can bypass the
authentication check using application hooking tools such as Frida.
</p>
</overview>
<recommendation>
<p>
When generating a key for use with biometric authentication, ensure that the following parameters of <code>KeyGenParameterSpec.Builder</code> are set:
</p>
<ul>
<li><code>setUserAuthenticationRequired</code> should be set to <code>true</code>; otherwise, the key can be used without user authentication.</li>
<li><code>setInvalidatedByBiometricEnrollment</code> should be set to <code>true</code> (the default); otherwise, an attacker can use the key by enrolling additional biometrics on the device.</li>
<li><code>setUserAuthenticationValidityDurationSeconds</code>, if used, should be set to <code>-1</code>; otherwise, non-biometric (less secure) credentials can be used to access the key. We recommend using <code>setUserAuthenticationParameters</code> instead to explicitly set both the timeout and the types of credentials that may be used.</li>
</ul>
</recommendation>
<example>
<p>The following example demonstrates a key that is configured with secure paramaters:</p>
<sample src="AndroidInsecureKeysGood.java"/>
<p>In each of the following cases, a parameter is set insecurely:</p>
<sample src="AndroidInsecureKeysBad.java"/>
</example>
<references>
<li>
WithSecure: <a href="https://labs.withsecure.com/publications/how-secure-is-your-android-keystore-authentication">How Secure is your Android Keystore Authentication?</a>.
</li>
<li>
Android Developers: <a href="https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder">KeyGenParameterSpec.Builder</a>.
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,18 @@
/**
* @name Insecurely generated keys for local authentication
* @description Generation of keys with insecure parameters for local biometric authentication can allow attackers with physical access to bypass authentication checks.
* @kind problem
* @problem.severity warning
* @security-severity 4.4
* @precision medium
* @id java/android/insecure-local-key-gen
* @tags security
* external/cwe/cwe-287
*/
import java
import semmle.code.java.security.AndroidLocalAuthQuery
from InsecureBiometricKeyParamCall call
where usesLocalAuth()
select call, "This key is not secure for biometric authentication."

View File

@@ -0,0 +1,47 @@
private void generateSecretKey() {
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(
"MySecretKey",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
// BAD: User authentication is not required to use this key.
.setUserAuthenticationRequired(false)
.build();
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGenerator.init(keyGenParameterSpec);
keyGenerator.generateKey();
}
private void generateSecretKey() {
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(
"MySecretKey",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
// BAD: An attacker can access this key by enrolling additional biometrics.
.setInvalidatedByBiometricEnrollment(false)
.build();
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGenerator.init(keyGenParameterSpec);
keyGenerator.generateKey();
}
private void generateSecretKey() {
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(
"MySecretKey",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
// BAD: This key can be accessed using non-biometric credentials.
.setUserAuthenticationValidityDurationSeconds(30)
.build();
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGenerator.init(keyGenParameterSpec);
keyGenerator.generateKey();
}

View File

@@ -0,0 +1,16 @@
private void generateSecretKey() {
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(
"MySecretKey",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
// GOOD: Secure parameters are used to generate a key for biometric authentication.
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)
.build();
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGenerator.init(keyGenParameterSpec);
keyGenerator.generateKey();
}

View File

@@ -0,0 +1,4 @@
---
category: newQuery
---
* Added a new query `java/android/insecure-local-key-gen` for finding instances of keys generated for biometric authentication in an insecure way.

View File

@@ -0,0 +1,19 @@
import java
import TestUtilities.InlineExpectationsTest
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.security.AndroidLocalAuthQuery
module InsecureKeysTest implements TestSig {
string getARelevantTag() { result = "insecure-key" }
predicate hasActualResult(Location location, string element, string tag, string value) {
tag = "insecure-key" and
exists(InsecureBiometricKeyParamCall call | usesLocalAuth() |
call.getLocation() = location and
element = call.toString() and
value = ""
)
}
}
import MakeTest<InsecureKeysTest>

View File

@@ -0,0 +1,39 @@
import android.security.keystore.KeyGenParameterSpec;
import android.hardware.biometrics.BiometricPrompt;
import android.security.keystore.KeyProperties;
import javax.crypto.KeyGenerator;
class Test {
void test() {
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder("MySecretKey", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT);
builder.setUserAuthenticationRequired(false); // $insecure-key
builder.setInvalidatedByBiometricEnrollment(false); // $insecure-key
builder.setUserAuthenticationValidityDurationSeconds(30); // $insecure-key
}
private void generateSecretKey() throws Exception {
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(
"MySecretKey",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
// GOOD: Secure parameters are used to generate a key for biometric authentication.
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)
.build();
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGenerator.init(keyGenParameterSpec);
keyGenerator.generateKey();
}
}
class Callback extends BiometricPrompt.AuthenticationCallback {
public static void useKey(BiometricPrompt.CryptoObject key) {}
@Override
public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
useKey(result.getCryptoObject());
}
}

View File

@@ -0,0 +1 @@
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../../stubs/google-android-9.0.0

View File

@@ -0,0 +1,2 @@
testFailures
failures

View File

@@ -0,0 +1,19 @@
import java
import TestUtilities.InlineExpectationsTest
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.security.AndroidLocalAuthQuery
module InsecureKeysTest implements TestSig {
string getARelevantTag() { result = "insecure-key" }
predicate hasActualResult(Location location, string element, string tag, string value) {
tag = "insecure-key" and
exists(InsecureBiometricKeyParamCall call | usesLocalAuth() |
call.getLocation() = location and
element = call.toString() and
value = ""
)
}
}
import MakeTest<InsecureKeysTest>

View File

@@ -0,0 +1,13 @@
import android.security.keystore.KeyGenParameterSpec;
import android.hardware.biometrics.BiometricPrompt;
import android.security.keystore.KeyProperties;
class Test {
void test() {
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder("MySecretKey", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT);
// No alert as there is no use of biometric authentication in this application.
builder.setUserAuthenticationRequired(false);
builder.setInvalidatedByBiometricEnrollment(false);
builder.setUserAuthenticationValidityDurationSeconds(30);
}
}

View File

@@ -0,0 +1 @@
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../../stubs/google-android-9.0.0

View File

@@ -0,0 +1,2 @@
testFailures
failures

View File

@@ -1 +1 @@
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/google-android-9.0.0
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/google-android-9.0.0

View File

@@ -0,0 +1,76 @@
// Generated automatically from android.security.keystore.KeyGenParameterSpec for testing purposes
package android.security.keystore;
import java.math.BigInteger;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Date;
import javax.security.auth.x500.X500Principal;
public class KeyGenParameterSpec implements AlgorithmParameterSpec
{
public AlgorithmParameterSpec getAlgorithmParameterSpec(){ return null; }
public BigInteger getCertificateSerialNumber(){ return null; }
public Date getCertificateNotAfter(){ return null; }
public Date getCertificateNotBefore(){ return null; }
public Date getKeyValidityForConsumptionEnd(){ return null; }
public Date getKeyValidityForOriginationEnd(){ return null; }
public Date getKeyValidityStart(){ return null; }
public String getAttestKeyAlias(){ return null; }
public String getKeystoreAlias(){ return null; }
public String[] getBlockModes(){ return null; }
public String[] getDigests(){ return null; }
public String[] getEncryptionPaddings(){ return null; }
public String[] getSignaturePaddings(){ return null; }
public X500Principal getCertificateSubject(){ return null; }
public boolean isDevicePropertiesAttestationIncluded(){ return false; }
public boolean isDigestsSpecified(){ return false; }
public boolean isInvalidatedByBiometricEnrollment(){ return false; }
public boolean isRandomizedEncryptionRequired(){ return false; }
public boolean isStrongBoxBacked(){ return false; }
public boolean isUnlockedDeviceRequired(){ return false; }
public boolean isUserAuthenticationRequired(){ return false; }
public boolean isUserAuthenticationValidWhileOnBody(){ return false; }
public boolean isUserConfirmationRequired(){ return false; }
public boolean isUserPresenceRequired(){ return false; }
public byte[] getAttestationChallenge(){ return null; }
public int getKeySize(){ return 0; }
public int getMaxUsageCount(){ return 0; }
public int getPurposes(){ return 0; }
public int getUserAuthenticationType(){ return 0; }
public int getUserAuthenticationValidityDurationSeconds(){ return 0; }
static public class Builder
{
protected Builder() {}
public Builder(String p0, int p1){}
public KeyGenParameterSpec build(){ return null; }
public KeyGenParameterSpec.Builder setAlgorithmParameterSpec(AlgorithmParameterSpec p0){ return null; }
public KeyGenParameterSpec.Builder setAttestKeyAlias(String p0){ return null; }
public KeyGenParameterSpec.Builder setAttestationChallenge(byte[] p0){ return null; }
public KeyGenParameterSpec.Builder setBlockModes(String... p0){ return null; }
public KeyGenParameterSpec.Builder setCertificateNotAfter(Date p0){ return null; }
public KeyGenParameterSpec.Builder setCertificateNotBefore(Date p0){ return null; }
public KeyGenParameterSpec.Builder setCertificateSerialNumber(BigInteger p0){ return null; }
public KeyGenParameterSpec.Builder setCertificateSubject(X500Principal p0){ return null; }
public KeyGenParameterSpec.Builder setDevicePropertiesAttestationIncluded(boolean p0){ return null; }
public KeyGenParameterSpec.Builder setDigests(String... p0){ return null; }
public KeyGenParameterSpec.Builder setEncryptionPaddings(String... p0){ return null; }
public KeyGenParameterSpec.Builder setInvalidatedByBiometricEnrollment(boolean p0){ return null; }
public KeyGenParameterSpec.Builder setIsStrongBoxBacked(boolean p0){ return null; }
public KeyGenParameterSpec.Builder setKeySize(int p0){ return null; }
public KeyGenParameterSpec.Builder setKeyValidityEnd(Date p0){ return null; }
public KeyGenParameterSpec.Builder setKeyValidityForConsumptionEnd(Date p0){ return null; }
public KeyGenParameterSpec.Builder setKeyValidityForOriginationEnd(Date p0){ return null; }
public KeyGenParameterSpec.Builder setKeyValidityStart(Date p0){ return null; }
public KeyGenParameterSpec.Builder setMaxUsageCount(int p0){ return null; }
public KeyGenParameterSpec.Builder setRandomizedEncryptionRequired(boolean p0){ return null; }
public KeyGenParameterSpec.Builder setSignaturePaddings(String... p0){ return null; }
public KeyGenParameterSpec.Builder setUnlockedDeviceRequired(boolean p0){ return null; }
public KeyGenParameterSpec.Builder setUserAuthenticationParameters(int p0, int p1){ return null; }
public KeyGenParameterSpec.Builder setUserAuthenticationRequired(boolean p0){ return null; }
public KeyGenParameterSpec.Builder setUserAuthenticationValidWhileOnBody(boolean p0){ return null; }
public KeyGenParameterSpec.Builder setUserAuthenticationValidityDurationSeconds(int p0){ return null; }
public KeyGenParameterSpec.Builder setUserConfirmationRequired(boolean p0){ return null; }
public KeyGenParameterSpec.Builder setUserPresenceRequired(boolean p0){ return null; }
}
}

View File

@@ -0,0 +1,54 @@
// Generated automatically from android.security.keystore.KeyProperties for testing purposes
package android.security.keystore;
abstract public class KeyProperties
{
protected KeyProperties() {}
public static String BLOCK_MODE_CBC = null;
public static String BLOCK_MODE_CTR = null;
public static String BLOCK_MODE_ECB = null;
public static String BLOCK_MODE_GCM = null;
public static String DIGEST_MD5 = null;
public static String DIGEST_NONE = null;
public static String DIGEST_SHA1 = null;
public static String DIGEST_SHA224 = null;
public static String DIGEST_SHA256 = null;
public static String DIGEST_SHA384 = null;
public static String DIGEST_SHA512 = null;
public static String ENCRYPTION_PADDING_NONE = null;
public static String ENCRYPTION_PADDING_PKCS7 = null;
public static String ENCRYPTION_PADDING_RSA_OAEP = null;
public static String ENCRYPTION_PADDING_RSA_PKCS1 = null;
public static String KEY_ALGORITHM_3DES = null;
public static String KEY_ALGORITHM_AES = null;
public static String KEY_ALGORITHM_EC = null;
public static String KEY_ALGORITHM_HMAC_SHA1 = null;
public static String KEY_ALGORITHM_HMAC_SHA224 = null;
public static String KEY_ALGORITHM_HMAC_SHA256 = null;
public static String KEY_ALGORITHM_HMAC_SHA384 = null;
public static String KEY_ALGORITHM_HMAC_SHA512 = null;
public static String KEY_ALGORITHM_RSA = null;
public static String SIGNATURE_PADDING_RSA_PKCS1 = null;
public static String SIGNATURE_PADDING_RSA_PSS = null;
public static int AUTH_BIOMETRIC_STRONG = 0;
public static int AUTH_DEVICE_CREDENTIAL = 0;
public static int ORIGIN_GENERATED = 0;
public static int ORIGIN_IMPORTED = 0;
public static int ORIGIN_SECURELY_IMPORTED = 0;
public static int ORIGIN_UNKNOWN = 0;
public static int PURPOSE_AGREE_KEY = 0;
public static int PURPOSE_ATTEST_KEY = 0;
public static int PURPOSE_DECRYPT = 0;
public static int PURPOSE_ENCRYPT = 0;
public static int PURPOSE_SIGN = 0;
public static int PURPOSE_VERIFY = 0;
public static int PURPOSE_WRAP_KEY = 0;
public static int SECURITY_LEVEL_SOFTWARE = 0;
public static int SECURITY_LEVEL_STRONGBOX = 0;
public static int SECURITY_LEVEL_TRUSTED_ENVIRONMENT = 0;
public static int SECURITY_LEVEL_UNKNOWN = 0;
public static int SECURITY_LEVEL_UNKNOWN_SECURE = 0;
public static int UNRESTRICTED_USAGE_COUNT = 0;
}