Using the app, I made a test account with a password of P@ssw0rd
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.
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.
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).
I rebuilt and signed the app.
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.
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.
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.
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 print "BLOB1 (salt): ", blob164 print "BLOB2 (iv): ", blob264 print "BLOB3 (ciphertext): ", blob364 print 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") print 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
I got these obviously automated and useless replies:
The local storage looks the same:
The same Python script works without any modification: this problem has not been fixed.