Home Depot Android App Encryption Flaw

Installing the App

Here's the app I installed, from Google Play.


Using the app, I made a test account with a password of P@ssw0rd

Viewing the Locally Stored Password

Using Android Debug Bridge, I connected to the phone and found the local password storage, as shown below.

Analysis and Recommendations

At this point, the likely conclusion is clear: the Home Depot app is insecure, because it stores the password on the phone using custom encryption. As discussed here, passwords should not be stored on the phone at all. Because users re-use passwords, they are very sensitive information and handling them carelessly is a disservice to your customers. Locally stored passwords could be stolen by malware on the phone, or by simply stealing the phone itself. Instead, a random cookie should be stored on the phone, which is useless at any company other than Home Depot.

This is the #2 most important security vunerability on mobile devices, according to OWASP.

If a developer is forced to store a password locally, there are far better options, such as the Android Keystore. The Android Keystore can be breached on a rooted phone, so the app should test for this, and refuse to run on a rooted phone.

The remainder of this page merely proves this simple point by explicitly reverse-engineering the encryption used by Home Depot, and making a simple Python script that decrypts the password.

Decompiling the Android App

Using apktool, it's easy to decompile the app, as shown below.

The local storage used the label encrypted_password, so searching for that string reveals the encryption code, which is all in a single file named EncryptionUtil.smali.

Reading the Encryption Code

As shown below, this app uses symmetric encryption with the AES/CBC/PKCS5Padding algorithm, and a secret key created with the PBKDF2WithHmacSHA1 algorithm.

These algorithms can be very secure, if used properly, but they are not used correctly in this app.

As shown below, the key derivation starts from the hard-coded password PUBLIC_PASSWORD_PBKDF2.

It also uses a key length of 256 bits (0x100 in hexadecimal), and 1000 rounds of hashing (0x3e8 in hexadecimal).

Modifying the App

To see the encryption process, I added the code below to the app, to put several parameters in the log.

I rebuilt and signed the app.

Running the Modified App

I launched the app and logged in to my test account:

The log showed the parameters. Notice the "salt" value, as outlined in green below.

The local storage shows three blobs of Base64-encoded data, delimited by ] characters, as shown below.

Decoding these blobs in Python shows that the first one is the salt, matching the value shown in the log above.

As will be proven below, blob2 is the IV (Initialization Vector) and blob3 is the ciphertext--the encrypted password.

Calculating the Secret Key from the Salt

The salt is the only random value used by this app, and the secret key uses the PBKDF2 algorithm with its default settings.

As shown below, this is trivial, requiring only a few lines of Python code. The secret key calculated from the salt matches the key shown in the log above.

Decrypting the Password

Similarly, the password is easily decrypted using the secret key and the IV, as shown below.

Python Script to Decrypt encrypted_password

Putting it all together, this script does the complete reversal, using only the locally stored data.
from Crypto.Cipher import AES
from pbkdf2 import PBKDF2
import os
import base64

orig = raw_input("Enter encrypted_password: ")

d1 = orig.find("]")
d2 = orig.find("]", d1+1)

blob164 = orig[:d1]
blob264 = orig[d1+1:d2]
blob364 = orig[d2+1:]

print "BLOB1 (salt):       ", blob164
print "BLOB2 (iv):         ", blob264
print "BLOB3 (ciphertext): ", blob364

salt = blob164.decode("base64")
iv = blob264.decode("base64")
ciphertext = blob364.decode("base64")

secret_key = PBKDF2('PUBLIC_PASSWORD_PBKDF2', salt).read(32)
print "SECRET KEY (from salt): ", secret_key.encode("hex")

cipher = AES.new(secret_key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(ciphertext)

n = len(decrypted)

pw = ''
for i in range(n):
  if decrypted[i] > chr(8):
    pw += decrypted[i]

print "Stored password: ", pw
Here's the script running on a few test cases I produced by logging out and logging in again. As you can see below, it recovered the password correctly each time.

Developer Notification

I notified the developer of this problem on 4-19-17, as shown below.

I got these obviously automated and useless replies:

Re-Test on 6-19-17

I tested this app:

The local storage looks the same:

The same Python script works without any modification: this problem has not been fixed.

Posted 4-19-17 by Sam Bowne
Updated 6-19-17
Home Depot replies added 7-24-17