Testing Cryptography in Android Apps
The primary goal of cryptography is to provide confidentiality, data integrity, and authenticity, even in the face of an attack. Confidentiality is achieved through use of encryption, with the aim of ensuring secrecy of the contents. Data integrity deals with maintaining and ensuring consistency of data and detection of tampering/modification. Authenticity ensures that the data comes from a trusted source. Since this is a testing guide and not a cryptography textbook, the following paragraphs provide only a very limited outline of relevant techniques and their usages in the context of mobile applications.
- Encryption ensures data confidentiality by using special algorithms to convert the plaintext data into cipher text, which does not reveal any information about the original contents. The plaintext data can be restored from the cipher text through decryption. Two main forms of encryption are symmetric (or secret key) and asymmetric (or public key). In general, encryption operations do not protect integrity, but some symmetric encryption modes also feature that protection (see “Testing Sensitive Data Protection” section).
- Symmetric-key encryption algorithms use the same key for both encryption and decryption. It is fast and suitable for bulk data processing. Since everybody who has access to the key is able to decrypt the encrypted content, they require careful key management.
- Public-key (or asymmetric) encryption algorithms operate with two separate keys: the public key and the private key. The public key can be distributed freely, while the private key should not be shared with anyone. A message encrypted with the public key can only be decrypted with the private key. Since asymmetric encryption is several times slower than symmetric operations, it is typically only used to encrypt small amounts of data, such as symmetric keys for bulk encryption.
- Hash functions deterministically map arbitrary pieces of data into fixed-length values. It is typically easy to compute the hash, but difficult (or impossible) to determine the original input based on the hash. Cryptographic hash functions additionally guarantee that even small changes to the input data result in large changes to the resulting hash values. Cryptographic hash functions are used for integrity verification, but do not provide authenticity guarantees.
- Message Authentication Codes, or MACs, combine other cryptographic mechanism, such as symmetric encryption or hashes, with secret keys to provide both integrity and authenticity protection. However, in order to verify a MAC, multiple entities have to share the same secret key, and any of those entities will be able to generate a valid MAC. The most commonly used type of MAC, called HMAC, relies on hash as the underlying cryptographic primitive. As a rule, full name of an HMAC algorithm also includes the name of the underlying hash, e.g. - HMAC-SHA256.
- Signatures combine asymmetric cryptography (i.e. - using a public/private keypair) with hashing to provide integrity and authenticity by encrypting hash of the message with the private key. However, unlike MACs, signatures also provide non-repudiation property, as the private key should remain unique to the data signer.
- Key Derivation Functions, or KDFs, are often confused with password hashing functions. KDFs do have many useful properties for password hashing, but were created with different purposes in mind. In context of mobile applications, it is the password hashing functions that are typically meant for protecting stored passwords.
Two uses of cryptography are covered in other chapters:
- Secure communications. TLS (Transport Layer Security) uses most of the primitives named above, as well a number of others. It is covered in the “Testing Network Communication” chapter.
- Secure storage. Тhis chapter includes high-level considerations for using cryptography for secure data storage, and specific content for secure data storage capabilities will be found in OS-specific data storage chapters.
References
- [1] Password Hashing Competition - https://password-hashing.net/
Testing for Custom Implementations of Cryptography
Overview
The use of non-standard or custom built cryptographic algorithms is dangerous because a determined attacker may be able to break the algorithm and compromise data that has been protected. Implementing cryptographic functions is time consuming, difficult and very likely to fail. Instead well-known algorithms that were already proven to be secure should be used. All mature frameworks and libraries offer cryptographic functions that should also be used when implementing mobile apps.
Static Analysis
Carefully inspect all the cryptographic methods used within the source code, especially those which are directly applied to sensitive data. All cryptographic operations (see the list in the introduction section) should come from the standard providers (for standard APIs for Android and iOS, see cryptography chapters for the respective platforms). Any cryptographic invocations which do not invoke standard routines from known providers should be candidates for closer inspection. Pay close attention to seemingly standard but modified algorithms. Remember that encoding is not encryption! Any appearance of bit manipulation operators like XOR (exclusive OR) might be a good sign to start digging deeper.
Remediation
Do not develop custom cryptographic algorithms, as it is likely they are prone to attacks that are already well-understood by cryptographers. Select a well-vetted algorithm that is currently considered to be strong by experts in the field, and use well-tested implementations.
References
OWASP Mobile Top 10 2016
- M6 - Broken Cryptography
OWASP MASVS
- V3.2: "The app uses proven implementations of cryptographic primitives"
CWE
- CWE-327: Use of a Broken or Risky Cryptographic Algorithm
Info
- [1] Supported Ciphers in KeyStore - https://developer.android.com/training/articles/keystore.html#SupportedCiphers
Testing for Insecure and/or Deprecated Cryptographic Algorithms
Overview
Many cryptographic algorithms and protocols should not be used because they have been shown to have significant weaknesses or are otherwise insufficient for modern security requirements. Previously thought secure algorithms may become insecure over time. It is therefore important to periodically check current best practices and adjust configurations accordingly.
Static Analysis
The source code should be checked that cryptographic algorithms are up to date and in-line with industry standards. This includes, but is not limited to outdated block ciphers (e.g. DES), stream ciphers (e.g. RC4), as well as hash functions (e.g. MD5) and broken random number generators like Dual_EC_DRBG. Please note, that an algorithm that was certified, e.g., by the NIST, can also become insecure over time. A certification does not replace periodic verification of an algorithm's soundness. All of these should be marked as insecure and should not be used and removed from the application code base.
Inspect the source code to identify the instances of cryptographic algorithms throughout the application, and look for known weak ones, such as:
- DES, 3DES[6]
- RC2
- RC4
- BLOWFISH[6]
- MD4
- MD5
- SHA1 and others.
On Android (via Java Cryptography APIs), selecting an algorithm is done by requesting an instance of the Cipher
(or other primitive) by passing a string containing the algorithm name. For example, Cipher cipher = Cipher.getInstance("DES");
. On iOS, algorithms are typically selected using predefined constants defined in CommonCryptor.h, e.g., kCCAlgorithmDES
. Thus, searching the source code for the presence of these algorithm names would indicate that they are used. Note that since the constants on iOS are numeric, an additional check needs to be performed to check whether the algorithm values sent to CCCrypt function map to one of the deprecated/insecure algorithms.
Other uses of cryptography require careful adherence to best practices:
- For encryption, use a strong, modern cipher with the appropriate, secure mode and a strong key. Examples:
- 256-bit key AES in GCM mode (provides both encryption and integrity verification.)
- 4096-bit RSA with OAEP padding.
- 224/256-bit elliptic curve cryptography.
- Do not use known weak algorithms. For example:
- AES in ECB mode is not considered secure, because it leaks information about the structure of the original data.
- Several other AES modes can be weak.
- RSA with 768-bit and weaker keys can be broken. Older PKCS#1 padding leaks information.
- Rely on secure hardware, if available, for storing encryption keys, performing cryptographic operations, etc.
Remediation
Periodically ensure that the cryptography has not become obsolete. Some older algorithms, once thought to require years of computing time, can now be broken in days or hours. This includes MD4, MD5, SHA1, DES, and other algorithms that were once considered as strong. Examples of currently recommended algorithms[1], [2]:
- Confidentiality: AES-GCM-256 or ChaCha20-Poly1305
- Integrity: SHA-256, SHA-384, SHA-512, Blake2
- Digital signature: RSA (3072 bits and higher), ECDSA with NIST P-384
- Key establishment: RSA (3072 bits and higher), DH (3072 bits or higher), ECDH with NIST P-384
References
OWASP Mobile Top 10
- M6 - Broken Cryptography
OWASP MASVS
- V3.3: "The app uses cryptographic primitives that are appropriate for the particular use-case, configured with parameters that adhere to industry best practices"
- V3.4: "The app does not use cryptographic protocols or algorithms that are widely considered depreciated for security purposes"
CWE
- CWE-326: Inadequate Encryption Strength
- CWE-327: Use of a Broken or Risky Cryptographic Algorithm
Info
- [1] Commercial National Security Algorithm Suite and Quantum Computing FAQ - https://cryptome.org/2016/01/CNSA-Suite-and-Quantum-Computing-FAQ.pdf
- [2] NIST Special Publication 800-57 - http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r4.pdf
- [4] NIST recommendations (2016) - https://www.keylength.com/en/4/
- [5] BSI recommendations (2017) - https://www.keylength.com/en/8/
- [6] Sweet32 attack -- https://sweet32.info/
Tools
- QARK - https://github.com/linkedin/qark
- Mobile Security Framework - https://github.com/ajinabraham/Mobile-Security-Framework-MobSF
Verifying the Configuration of Cryptographic Standard Algorithms
Overview
A general rule in app development is that one should never attempt to invent their own cryptography. In mobile apps in particular, any form of crypto should be implemented using existing, robust implementations. In 99% of cases, this simply means using the data storage APIs and cryptographic libraries that come with the mobile OS.
Android cryptography APIs are based on the Java Cryptography Architecture (JCA). JCA separates the interfaces and implementation, making it possible to include several security providers [8] that can implement sets of cryptographic algorithms. Most of the JCA interfaces and classes are defined in the java.security.*
and javax.crypto.*
packages. In addition, there are Android specific packages android.security.*
and android.security.keystore.*
.
The list of providers included in Android varies between versions of Android and the OEM-specific builds. Some provider implementations in older versions are now known to be less secure or vulnerable. Thus, Android applications should not only choose the correct algorithms and provide good configuration, in some cases they should also pay attention to the strength of the implementations in the legacy providers. You can list the set of existing providers as follows:
StringBuilder builder = new StringBuilder();
for (Provider provider : Security.getProviders()) {
builder.append("provider: ")
.append(provider.getName())
.append(" ")
.append(provider.getVersion())
.append("(")
.append(provider.getInfo())
.append(")\n");
}
String providers = builder.toString();
//now display the string on the screen or in the logs for debugging.
Below you can find the output on the Emulator running Android 4.4 with Google Play APIs after the security provider has been patched:
provider: GmsCore_OpenSSL1.0 (Android's OpenSSL-backed security provider)
provider: AndroidOpenSSL1.0 (Android's OpenSSL-backed security provider)
provider: DRLCertFactory1.0 (ASN.1, DER, PkiPath, PKCS7)
provider: BC1.49 (BouncyCastle Security Provider v1.49)
provider: Crypto1.0 (HARMONY (SHA1 digest; SecureRandom; SHA1withDSA signature))
provider: HarmonyJSSE1.0 (Harmony JSSE Provider)
provider: AndroidKeyStore1.0 (Android KeyStore security provider)
For some applications that support older versions of Android, bundling an up-to-date library may be the only option. SpongyCastle (a repackaged version of BouncyCastle) is a common choice in these situations. Repackaging is necessary because BouncyCastle is included in the Android SDK. The latest version of SpongyCastle [6] likely fixes issues encountered in the earlier versions of BouncyCastle [7] that were included in Android. Note that the BouncyCastle libraries packed with Android are often not as complete as their counterparts from the Legion of the BouncyCastle. Lastly: bear in mind that packing large libraries such as SpongyCastle will often lead to a multidexed Android application.
Android SDK provides mechanisms for specifying secure key generation and use. Android 6.0 (Marshmallow, API 23) introduced the KeyGenParameterSpec
class that can be used to ensure the correct key usage in the application.
Here's an example of using AES/CBC/PKCS7Padding on API 23+:
String keyAlias = "MySecretKey";
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(keyAlias,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setRandomizedEncryptionRequired(true)
.build();
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore");
keyGenerator.init(keyGenParameterSpec);
SecretKey secretKey = keyGenerator.generateKey();
The KeyGenParameterSpec
indicates that the key can be used for encryption and decryption, but not for other purposes, such as signing or verifying. It further specifies the block mode (CBC), padding (PKCS7), and explicitly specifies that randomized encryption is required (this is the default.) "AndroidKeyStore"
is the name of the cryptographic service provider used in this example.
GCM is another AES block mode that provides additional security benefits over other, older modes. In addition to being cryptographically more secure, it also provides authentication. When using CBC (and other modes), authentication would need to be performed separately, using HMACs (see the Reverse Engineering chapter). Note that GCM is the only mode of AES that does not support paddings.[3], [5]
Attempting to use the generated key in violation of the above spec would result in a security exception.
Here's an example of using that key to decrypt:
String AES_MODE = KeyProperties.KEY_ALGORITHM_AES
+ "/" + KeyProperties.BLOCK_MODE_CBC
+ "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7;
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
// byte[] input
Key key = keyStore.getKey(keyAlias, null);
Cipher cipher = Cipher.getInstance(AES_MODE);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encryptedBytes = cipher.doFinal(input);
byte[] iv = cipher.getIV();
// save both the iv and the encryptedBytes
Both the IV and the encrypted bytes need to be stored; otherwise decryption is not possible.
Here's how that cipher text would be decrypted. The input
is the encrypted byte array and iv
is the initialization vector from the encryption step:
// byte[] input
// byte[] iv
Key key = keyStore.getKey(AES_KEY_ALIAS, null);
Cipher cipher = Cipher.getInstance(AES_MODE);
IvParameterSpec params = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, key, params);
byte[] result = cipher.doFinal(input);
Since the IV (initialization vector) is randomly generated each time, it should be saved along with the cipher text (encryptedBytes
) in order to decrypt it later.
Prior to Android 6.0, AES key generation was not supported. As a result, many implementations chose to use RSA and generated public-private key pair for asymmetric encryption using KeyPairGeneratorSpec
or used SecureRandom
to generate AES keys.
Here's an example of KeyPairGenerator
and KeyPairGeneratorSpec
used to create the RSA key pair:
Date startDate = Calendar.getInstance().getTime();
Calendar endCalendar = Calendar.getInstance();
endCalendar.add(Calendar.YEAR, 1);
Date endDate = endCalendar.getTime();
KeyPairGeneratorSpec keyPairGeneratorSpec = new KeyPairGeneratorSpec.Builder(context)
.setAlias(RSA_KEY_ALIAS)
.setKeySize(4096)
.setSubject(new X500Principal("CN=" + RSA_KEY_ALIAS))
.setSerialNumber(BigInteger.ONE)
.setStartDate(startDate)
.setEndDate(endDate)
.build();
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA",
"AndroidKeyStore");
keyPairGenerator.initialize(keyPairGeneratorSpec);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
This sample creates the RSA key pair with the 4096-bit key (i.e., modulus size).
Static Analysis
Locate uses of the cryptographic primitives in code. Some of the most frequently used classes and interfaces:
Cipher
Mac
MessageDigest
Signature
Key
,PrivateKey
,PublicKey
,SecretKey
- And a few others in the
java.security.*
andjavax.crypto.*
packages.
Ensure that the best practices outlined in the Cryptography for Mobile Apps chapter are followed.
Remediation
Use cryptographic algorithm configurations that are currently considered strong, such those from NIST1 and BSI2 recommendations.
References
OWASP Mobile Top 10
- M6 - Broken Cryptography
OWASP MASVS
- V3.3: "The app uses cryptographic primitives that are appropriate for the particular use-case, configured with parameters that adhere to industry best practices"
CWE
- CWE-326: Inadequate Encryption Strength
Info
- [1] NIST recommendations (2016) - https://www.keylength.com/en/4/
- [2] BSI recommendations (2017) - https://www.keylength.com/en/8/
- [3] Supported Ciphers in KeyStore - https://developer.android.com/training/articles/keystore.html#SupportedCiphers
- [4] Credential storage enhancements in Android 4.3 (August 21, 2013) - https://nelenkov.blogspot.co.uk/2013/08/credential-storage-enhancements-android-43.html
- [5] Cipher documentation - https://developer.android.com/reference/javax/crypto/Cipher.html
- [6] Spongy Castle - https://rtyley.github.io/spongycastle/
- [7] CVE Details Bouncy Castle - https://www.cvedetails.com/vulnerability-list/vendor_id-7637/Bouncycastle.html
- [8] Provider - https://developer.android.com/reference/java/security/Provider.html
Testing for Hardcoded Cryptographic Keys
Overview
The security of symmetric encryption and keyed hashes (MACs) is highly dependent upon the secrecy of the used secret key. If the secret key is disclosed, the security gained by encryption/MACing is rendered naught. This mandates, that the secret key is protected and should not be stored together with the encrypted data.
Static Analysis
The following checks would be performed against the used source code:
- Ensure that no keys/passwords are hard coded and stored within the source code. Pay special attention to any 'administrative' or backdoor accounts enabled in the source code. Storing fixed salt within application or password hashes may cause problems too.
- Ensure that no obfuscated keys or passwords are in the source code. Obfuscation is easily bypassed by dynamic instrumentation and in principle does not differ from hard coded keys.
- If the application is using two-way SSL (i.e. there is both server and client certificate validated) check if:
- the password to the client certificate is not stored locally, it should be in the Keychain
- the client certificate is not shared among all installations (e.g. hard coded in the app)
- if the app relies on an additional encrypted container stored in app data, ensure how the encryption key is used;
- if key wrapping scheme is used, ensure that the master secret is initialized for each user, or container is re-encrypted with new key;
- check how password change is handled and specifically, if you can use master secret or previous password to decrypt the container.
Mobile operating systems provide a specially protected storage area for secret keys, commonly named key stores or key chains. Those storage areas will not be part of normal backup routines and might even be protected by hardware means. The application should use this special storage locations/mechanisms for all secret keys.
Remediation
Using symmetric encryption with a hardcoded key is never a good idea. Instead, leverage the secure data storage and cryptography facilities offered by Android as described in the "Data Storage" chapter.
References
OWASP Mobile Top 10
- M6 - Broken Cryptography
OWASP MASVS
- V3.1: "The app does not rely on symmetric cryptography with hardcoded keys as a sole method of encryption."
CWE
- CWE-321 - Use of Hard-coded Cryptographic Key
Info
- [1] iOS: Managing Keys, Certificates, and Passwords - https://developer.apple.com/library/content/documentation/Security/Conceptual/cryptoservices/KeyManagementAPIs/KeyManagementAPIs.html
- [2] Android: The Android Keystore System - https://developer.android.com/training/articles/keystore.html
- [3] Android: Hardware-backed Keystore - https://source.android.com/security/keystore/
Testing Random Number Generation
Overview
Cryptography requires secure pseudo random number generation (PRNG). Standard Java classes do not provide sufficient randomness and in fact may make it possible for an attacker to guess the next value that will be generated, and use this guess to impersonate another user or access sensitive information.
In general, SecureRandom
should be used. However, if the Android versions below KitKat are supported, additional care needs to be taken in order to work around the bug in Jelly Bean (Android 4.1-4.3) versions that failed to properly initialize the PRNG[4].
Most developers should instantiate SecureRandom
via the default constructor without any arguments. Other constructors are for more advanced uses and, if used incorrectly, can lead to decreased randomness and security. The PRNG provider backing SecureRandom
uses the /dev/urandom
device file as the source of randomness by default.[5]
Static Analysis
Identify all the instances of random number generators and look for either custom or known insecure java.util.Random
class. This class produces an identical sequence of numbers for each given seed value; consequently, the sequence of numbers is predictable.
The following sample source code shows weak random number generation:
import java.util.Random;
// ...
Random number = new Random(123L);
//...
for (int i = 0; i < 20; i++) {
// Generate another random integer in the range [0, 20]
int n = number.nextInt(21);
System.out.println(n);
}
Identify all instances of SecureRandom
that are not created using the default constructor. Specifying the seed value may reduce randomness.
Dynamic Analysis
Once an attacker is knowing what type of weak pseudo-random number generator (PRNG) is used, it can be trivial to write proof-of-concept to generate the next random value based on previously observed ones, as it was done for Java Random[1]. In case of very weak custom random generators it may be possible to observe the pattern statistically. Although the recommended approach would anyway be to decompile the APK and inspect the algorithm (see Static Analysis).
Remediation
Use a well-vetted algorithm that is currently considered to be strong by experts in the field, and select well-tested implementations with adequate length seeds. Prefer the no-argument constructor of SecureRandom
that uses the system-specified seed value to generate a 128-byte-long random number[2].
In general, if a PRNG is not advertised as being cryptographically secure (e.g. java.util.Random
), then it is probably a statistical PRNG and should not be used in security-sensitive contexts.
Pseudo-random number generators can produce predictable numbers if the generator is known and the seed can be guessed[3]. A 128-bit seed is a good starting point for producing a "random enough" number.
The following sample source code shows the generation of a secure random number:
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;
// ...
public static void main (String args[]) {
SecureRandom number = new SecureRandom();
// Generate 20 integers 0..20
for (int i = 0; i < 20; i++) {
System.out.println(number.nextInt(21));
}
}
References
OWASP MASVS
- V3.6: "All random values are generated using a sufficiently secure random number generator"
OWASP Mobile Top 10 2016
- M6 - Broken Cryptography
CWE
- CWE-330: Use of Insufficiently Random Values
Info
- [1] Predicting the next Math.random() in Java - http://franklinta.com/2014/08/31/predicting-the-next-math-random-in-java/
- [2] Generation of Strong Random Numbers - https://www.securecoding.cert.org/confluence/display/java/MSC02-J.+Generate+strong+random+numbers
- [3] Proper seeding of SecureRandom - https://www.securecoding.cert.org/confluence/display/java/MSC63-J.+Ensure+that+SecureRandom+is+properly+seeded
- [4] Some SecureRandom Thoughts - https://android-developers.googleblog.com/2013/08/some-securerandom-thoughts.html
- [5] N. Elenkov, Android Security Internals, No Starch Press, 2014, Chapter 5.