Keeper Password Manager & Digital Vault: security review

October 2nd, 2014 by Vladimir Katalov
Category: «Cryptography», «Industry News», «Security», «Software»

Introduction

Two years ago, ElcomSoft analyzed some 17 password management applications for mobile platforms only to discover that no single app was able to deliver the claimed level of protection. The majority of the apps relied upon proprietary encryption models rather than utilizing iOS exemplary security model. As a result, most applications were either plain insecure or provided insufficient security levels, allowing a competent intruder to break into the encrypted data in a matter of hours, if not minutes. Full report (PDF) is available here.

Today, we need stronger security more than ever. Was the urge for stronger security recognized by software makers, or are they still using the same inefficient techniques? In order to find out, we decided to re-test some of the previously analyzed products. Keeper® Password Manager & Digital Vault will the first subject for dissection.

Back in 2012, we weren’t much impressed by security in any of the apps we analyzed. Two years later, Keeper developers claimed they’ve successfully implemented the suggestions we made during the last analysis. The developers claim to have used 256-bit AES encryption, PBKDF2 key generation, BCrypt, and SHA-1 among other things. Let’s see if these improvements lead to stronger security.

Description

Keeper® Password Manager & Digital Vault 8.3 (or simply “Keeper”) is a password management app for iOS enabling secure storage of credentials, files and pictures. If you have more than one Apple device, Keeper will automatically synchronize between the different mobile devices. The tool comes with the ability to make and restore backups from the cloud, and allows synchronizing its database with a desktop computer over Wi-Fi. The optional security feature allows erasing secured content automatically after 5 failed login attempts.

User authentication is performed either by using a master password or with Touch ID (if available). The user can enable two-factor authentication to enforce additional checks when activating the application on a new device.

The app is distributed free of charge, and offers in-app purchases. Paid services include backup subscription and additional cloud storage.

Analysis

At the first launch, the app will ask to create an account (tied to an e-mail address) and to specify the master-password. In our lab, we used P@ssw0rd as our master password. We then stored some arbitrary credentials:

1 2

While analyzing the application, we discovered that all data is stored in the keeper.sql database in the /var/mobile/Containers/<AppID> folder. The database contains two tables,settingandpassword. The first table stores application settings, while the second table is used to store user credentials. As you can see, all user data is encrypted:

3

4

a) Master password encryption algorithm. The app stores its master password by using the KeeperUtil class that has a single method (saveMasterPassword:) which is called when the user specifies a new master password:Let’s first look at how the app handles its master password, as the tool’s security model is highly dependent on how securely the master password is stored.

void __cdecl -[KeeperUtilsaveMasterPassword:](structKeeperUtil *self, SEL a2, id a3)
{
<...>
v3 = a3;
v4 = objc_msgSend(&OBJC_CLASS___UIApplication, "sharedApplication");
v5 = objc_msgSend(v4, "delegate");
if ( v3 && !((unsigned int)objc_msgSend(v3, "isEqualToString:", &stru_210FFC) & 0xFF) )
{
objc_msgSend(v5, "setPreviousMasterPassword:", v3);
v8 = objc_msgSend(&OBJC_CLASS___KeeperAppSingleton, "sharedKeeperApp");
objc_msgSend(v8, "setMasterPassword:", v3);
v9 = objc_msgSend(&OBJC_CLASS___Hash, "hashPlainString:withOriginalHash:", v3, 0);
v10 = objc_msgSend(v9, "copy");
objc_msgSend(v5, "setHashedMasterPassword:", v10);
v11 = objc_msgSend(&OBJC_CLASS___Hash, "getSHA1HashBytes:", v3);
objc_msgSend(v5, "setHashedMasterPasswordSHA1:", v11);
       <...>
}
<...>
}

The v3 variable contains the original master password as entered by the user (in plaintext). If the password specified is not empty and if the password is not equal to the value of the stru_210FFC string, then the new master password is assigned to variables KeeperAppSingleton->_masterPassword and iKeeperAppDelegate->_previousMasterPassword (still unencrypted).

After that, the [HashhashPlainString:withOriginalHash:]method performs the hashing of the master password; zero is passed as the second argument.

id __cdecl +[Hash hashPlainString:withOriginalHash:](struct Hash *self, SEL a2, id a3, id a4)
{
<...>
v4 = self;
v5 = a4;
plainString = objc_retain(a3);
originalHash = (void *)objc_retain(v5);
v8 =originalHash;
if ( originalHash&& !((unsigned int)objc_msgSend(originalHash, "isEmpty") & 0xFF) )
{
<...>
}
else
{
v9 = objc_msgSend(v4, "bcryptPlainString:withSalt:", plainString, 0);
v10 = (void *)objc_retainAutoreleasedReturnValue(v9);
v11 = v10;
v12 = objc_msgSend(v10, "dataUsingEncoding:", 4);
v13 = objc_retainAutoreleasedReturnValue(v12);
v14 = v11;
}
<...>
}

As seen in the above code snippet, the originalHash check is performed first; however, the originalHash has a value of 0, so we follow the else branch. In the else branch, the [HashbcryptPlainString:withSalt:] method is called, where the new master-password is passed as the first argument and zero as the second argument.

Let’s now delve into the [HashbcryptPlainString:withSalt:] method:

id __cdecl +[Hash bcryptPlainString:withSalt:](struct Hash *self, SEL a2, id a3, id a4)
{
<...>
v4 = a4;
plainString = objc_retain(a3);
salt = (void *)objc_retain(v4);
v7 = salt;
if ( !salt || (unsigned int)objc_msgSend(salt, "isEmpty") & 0xFF )
{
v8 = objc_msgSend(&OBJC_CLASS___JFBCrypt, "generateSaltWithNumberOfRounds:", 7);
v9 = objc_retainAutoreleasedReturnValue(v8);
objc_release(v7);
v7 = (void *)v9;
}
v10 = objc_msgSend(&OBJC_CLASS___JFBCrypt, "hashPassword:withSalt:", plainString, v7);
<...>
}

So, if we have no salt (and we don’t in our case), the salt will be generated by the [JFBCryptgenerateSaltWithNumberOfRounds:7] method with 7 rounds. Then JFBCryptgenerateSaltWithNumberOfRounds:7] performs hashing of the master password with salt computed during the previous step. Actually, JFBCrypt is a third-party class. Its source code is available at https://github.com/jayfuerstenberg/JFCommon/blob/master/JFBCrypt.m

The implementation of the [JFBCryptgenerateSaltWithNumberOfRounds:] method:

+ (NSString *) generateSaltWithNumberOfRounds: (SInt32) numberOfRounds {
         NSMutableString *salt = [NSMutableStringstringWithCapacity: BCRYPT_SALT_LEN];
         NSData *randomData = [JFRandomgenerateRandomSignedDataOfLength: BCRYPT_SALT_LEN];
         [salt appendString: @"$2a$"];
         if (numberOfRounds< 10) {
                 [salt appendString: @"0"];
         }
         [salt appendFormat: @"%d", numberOfRounds];
         [salt appendString: @"$"];
         [salt appendString: [JFBCryptencodeData: randomData ofLength: [randomData length]]];
         return salt;
}

Salt has the following format: “$2a$07$X…X”, where “$2a$07$” is a kind of a “signature” while “S…S” is the salt itself. Password hashing is performed salted in 7 rounds using the Blowfish algorithm. The resulting hash is 60 bytes long, and has the following format: “$2a$07$S…SX…X”, where “$2a$07$” is the signature, “S…S” salt, and “X…X” is the hash itself. Note that hashed passwords contain salt “inside” their body; we make use of this fact a bit later.

Let’s now return to the initial method [KeeperUtilsaveMasterPassword:].The hash is assigned to the iKeeperAppDelegate->_hashedMasterPassword variable, and the SHA-1 hash of the masterpassword is assigned to iKeeperAppDelegate->_hashedMasterPasswordSHA1. Finally, the hashed master password is added to the setting table to the enc_pass field (Figure3).

b) Password check. The check is performed in a separate thread by the[LoginControllerloginCheckThread:]method.

void __cdecl -[LoginControllerloginCheckThread:](structLoginController *self, SEL a2, id a3)
{
<…>
v3 = self;
v4 = a3;
if ( !self->didLogin
&& !self->didRunSelfDestruct
&& !((unsigned int)objc_msgSend(self->appDelegate, "loggedIn") & 0xFF) )
{
v5 = objc_msgSend(v3, "convertKeypadAlphaToNumeric:", v4);
v6 = objc_msgSend(&OBJC_CLASS___KeeperUtil, "sharedKeeperUtil");
if ( (unsigned __int8)objc_msgSend(v6, "isPasswordCorrect:", v4) == 1 )
{
<…>
}
<…>
 }
}

The task of checking the password is then passed to the [KeeperUtilisPasswordCorrect:] method:

char __cdecl -[KeeperUtilisPasswordCorrect:](structKeeperUtil *self, SEL a2, id a3)
{
<...>

v3 = a3;
v4 = objc_msgSend(&OBJC_CLASS___UIApplication, "sharedApplication");
v5 = objc_msgSend(v4, "delegate");
v6 = (structobjc_object *)objc_msgSend(v5, "hashedMasterPassword");
j__objc_msgSend((structLoginController *)&OBJC_CLASS___Hash, "hashComparePlainString:withHash:", v3, v6, v8, v9);
return result;
}

The iKeeperAppDelegate->_hashedMasterPassword is assigned the value of v6. The password entered by the user is compared with the hashed master password by the [Hash hashComparePlainString:withHash:] method:

char __cdecl +[Hash hashComparePlainString:withHash:](struct Hash *self, SEL a2, id a3, id a4)
{
<...>
v4 = self;
v5 = a4;
plainString = (void *)objc_retain(a3);
masterHash = objc_retain(v5);
if ( plainString&& !((unsigned int)objc_msgSend(plainString, "isEmpty") & 0xFF) )
{
v9 = (void *)objc_retainAutorelease(masterHash);
v22 = v9;
v10 = objc_msgSend(v9, "bytes");
v11 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithUTF8String:", v10);
v12 = objc_retainAutoreleasedReturnValue(v11);
v13 = objc_msgSend(
&OBJC_CLASS___NSPredicate,
"predicateWithFormat:",
CFSTR("SELF MATCHES %@"),
CFSTR("\\$.*?\\$\\d\\d\\$.{53}"));
v14 = (void *)objc_retainAutoreleasedReturnValue(v13);
if ( (unsigned int)objc_msgSend(v14, "evaluateWithObject:", v12) & 0xFF )
{
v8 = (unsigned int)objc_msgSend(v4, "bcryptComparePlainString:withHash:", plainString, v12);
}
else
{
v15 = objc_msgSend(v4, "getMD5HashBytes:", plainString);
v16 = (void *)objc_retainAutoreleasedReturnValue(v15);
v17 = (unsigned int)objc_msgSend(v16, "isEqualToData:", v22);
objc_release(v16);
v18 = objc_msgSend(v4, "getSHA1HashBytes:", plainString);
v19 = (void *)objc_retainAutoreleasedReturnValue(v18);
v20 = objc_msgSend(v19, "isEqualToData:", v22);
objc_release(v19);
if ( v17 )
{
v8 = 1;
}
else
{
v8 = (char)v20;
if ( v20 )
v8 = 1;
}
}
objc_release(v14);
objc_release(v12);
}
else
{
v8 = 0;
}
objc_release(masterHash);
objc_release(plainString);
return v8;
}

The following code snippet actually checks if the variable masterHashcomplies to the “$2a$07${X}” format, where X is repeated exactly 53 times:

v13 = objc_msgSend(
&OBJC_CLASS___NSPredicate,
"predicateWithFormat:",
CFSTR("SELF MATCHES %@"),
CFSTR("\\$.*?\\$\\d\\d\\$.{53}"));
v14 = (void *)objc_retainAutoreleasedReturnValue(v13);
if ( (unsigned int)objc_msgSend(v14, "evaluateWithObject:", v12) & 0xFF )

This check must be completed successfully because the hashed master password must strictly adhere to that format. Next, the [HashbcryptComparePlainString:withHash:]method takes over.

If the hashed master password has a different format, the method computes MD5 and SHA-1 hashes of the user-supplied password. If any of those hashed values are equal to the hashed master password, then the check is assumed to be passed. This is probably a left-over measure to ensure compatibility with older versions of the app which used to hash the master password with MD5 without salt. Compatibility mode aside, the stored hash is generated using Bcrypt, a secure hashing algorithm. As we’ll see later on, this hash is not used for the encryption or decryption of user data

And finally, the [Hash bcryptComparePlainString:withHash] method:

char __cdecl +[Hash bcryptComparePlainString:withHash:](struct Hash *self, SEL a2, id a3, id a4)

{
<...>
v4 = a4;
v5 = objc_retain(a3);
v6 = objc_retain(v4);
v7 = objc_msgSend(&OBJC_CLASS___JFBCrypt, "hashPassword:withSalt:", v5, v6);
objc_release(v5);
v8 = (void *)objc_retainAutoreleasedReturnValue(v7);
LOBYTE(v5) = (unsigned int)objc_msgSend(v8, "isEqualToString:", v6);
objc_release(v6);
objc_release(v8);
return v5;
}

At this point, everything is rather straightforward: the user-supplied password is hashed with salt, and if the result is equal to the hash from the database, the authentication has successfully passed. It should be pointed out that the hashed master password is passed to the method as a salt. This is because the hashed master password contains salt in its “body”, and the JBCrypt class automatically extracts salt from the hash and uses it to check the user-supplied password.

Possible vulnerabilities

Sidechannel attacks. When the application is sent to background, Kepper (like many iOS-applications) captures a screenshot and stores it in the /Library/Caches/Snapshots folder. If the application is sent to background at the moment the user has some of their credentials displayed on the screen, the screenshot will obviously reveal these credentials, which can be easily extracted by any iOS file manager (e.g. iExplorer). Interestingly, as we wrote this, a new release just appeared. After analyzing the new version 8.4, we discovered that it no longer susceptible to this type of attacks. Version 8.4 throws up a vault door screen prior to the app being pushed to background or exiting, which eliminates system-level screenshots to contain sensitive information.

Another attack has to do with the ability of the app to quickly copy the password to clipboard. After the application terminates, the password remains in the clipboard, and can be easily extracted from there. Notably, Keeper actually clears the device clipboard when the user logs out explicitly by tapping the “Logout” button. The code below is used to clean up clipboard on logout:

UIPasteboard *pb = [UIPasteboardgeneralPasteboard];
[pbsetValue:@"" forPasteboardType:UIPasteboardNameGeneral];

As a result, making sure to log out every time before closing Keeper or switching to another app is essential.

Runtime attacks. After first successful authentication, the master password is kept in plaintext in the application’smemory (in KeeperAppSingleton->_masterPassword). This variable can be read with the tool called cycript:

Cycript can also extract the hashed master password and SHA-1 hash of the master password (variables iKeeperAppDelegate->hashedMasterPassword and iKeeperAppDelegate->hashedMasterPasswordSHA1respectively) from memory:

6

Conclusion

The current version of Keeper provides a higher security level compared to its previous version. Even if the intruder gains access to the keeper.sql database, the only way to recover the master password is by performing a brute force or dictionary attack. Therefore, using a strong password renders attacks ineffective. Access to keeper.sql can only be gained on jailbroken devices because the database is not stored in the/Application folder, but kept in the /Containers folder. Because of the same reason, the password database is not present in iTunes backups, as Keeper implementsits own proprietary backup mechanisms.

In summary, Keeper appears to have most security issues fixed. Today, its developers are adopting the correct secure methodologies. Apparently, they followed the points outlined in our past analysis, successfully eliminating all of the weaknesses we’ve discovered two years ago. More information about Keeper’s security implementation can be found at https://keepersecurity.com/security

Credits: Ivan Ponurovskiy, iOS Security Researcher at ElcomSoft