Merge "Adapt to underlying changes in the PBKDF2 implementation" into klp-dev

This commit is contained in:
Christopher Tate
2014-03-05 00:51:32 +00:00
committed by Android (Google) Code Review
4 changed files with 188 additions and 84 deletions

View File

@ -82,7 +82,7 @@ import android.util.StringBuilderPrinter;
import com.android.internal.backup.BackupConstants; import com.android.internal.backup.BackupConstants;
import com.android.internal.backup.IBackupTransport; import com.android.internal.backup.IBackupTransport;
import com.android.internal.backup.IObbBackupService; import com.android.internal.backup.IObbBackupService;
import com.android.internal.backup.LocalTransport; import com.android.server.EventLogTags;
import com.android.server.PackageManagerBackupAgent.Metadata; import com.android.server.PackageManagerBackupAgent.Metadata;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
@ -140,11 +140,16 @@ class BackupManagerService extends IBackupManager.Stub {
private static final boolean DEBUG = true; private static final boolean DEBUG = true;
private static final boolean MORE_DEBUG = false; private static final boolean MORE_DEBUG = false;
// Historical and current algorithm names
static final String PBKDF_CURRENT = "PBKDF2WithHmacSHA1";
static final String PBKDF_FALLBACK = "PBKDF2WithHmacSHA1And8bit";
// Name and current contents version of the full-backup manifest file // Name and current contents version of the full-backup manifest file
static final String BACKUP_MANIFEST_FILENAME = "_manifest"; static final String BACKUP_MANIFEST_FILENAME = "_manifest";
static final int BACKUP_MANIFEST_VERSION = 1; static final int BACKUP_MANIFEST_VERSION = 1;
static final String BACKUP_FILE_HEADER_MAGIC = "ANDROID BACKUP\n"; static final String BACKUP_FILE_HEADER_MAGIC = "ANDROID BACKUP\n";
static final int BACKUP_FILE_VERSION = 1; static final int BACKUP_FILE_VERSION = 2;
static final int BACKUP_PW_FILE_VERSION = 2;
static final boolean COMPRESS_FULL_BACKUPS = true; // should be true in production static final boolean COMPRESS_FULL_BACKUPS = true; // should be true in production
static final String SHARED_BACKUP_AGENT_PACKAGE = "com.android.sharedstoragebackup"; static final String SHARED_BACKUP_AGENT_PACKAGE = "com.android.sharedstoragebackup";
@ -450,6 +455,8 @@ class BackupManagerService extends IBackupManager.Stub {
private final SecureRandom mRng = new SecureRandom(); private final SecureRandom mRng = new SecureRandom();
private String mPasswordHash; private String mPasswordHash;
private File mPasswordHashFile; private File mPasswordHashFile;
private int mPasswordVersion;
private File mPasswordVersionFile;
private byte[] mPasswordSalt; private byte[] mPasswordSalt;
// Configuration of PBKDF2 that we use for generating pw hashes and intermediate keys // Configuration of PBKDF2 that we use for generating pw hashes and intermediate keys
@ -810,6 +817,27 @@ class BackupManagerService extends IBackupManager.Stub {
} }
mDataDir = Environment.getDownloadCacheDirectory(); mDataDir = Environment.getDownloadCacheDirectory();
mPasswordVersion = 1; // unless we hear otherwise
mPasswordVersionFile = new File(mBaseStateDir, "pwversion");
if (mPasswordVersionFile.exists()) {
FileInputStream fin = null;
DataInputStream in = null;
try {
fin = new FileInputStream(mPasswordVersionFile);
in = new DataInputStream(fin);
mPasswordVersion = in.readInt();
} catch (IOException e) {
Slog.e(TAG, "Unable to read backup pw version");
} finally {
try {
if (in != null) in.close();
if (fin != null) fin.close();
} catch (IOException e) {
Slog.w(TAG, "Error closing pw version files");
}
}
}
mPasswordHashFile = new File(mBaseStateDir, "pwhash"); mPasswordHashFile = new File(mBaseStateDir, "pwhash");
if (mPasswordHashFile.exists()) { if (mPasswordHashFile.exists()) {
FileInputStream fin = null; FileInputStream fin = null;
@ -1110,13 +1138,13 @@ class BackupManagerService extends IBackupManager.Stub {
} }
} }
private SecretKey buildPasswordKey(String pw, byte[] salt, int rounds) { private SecretKey buildPasswordKey(String algorithm, String pw, byte[] salt, int rounds) {
return buildCharArrayKey(pw.toCharArray(), salt, rounds); return buildCharArrayKey(algorithm, pw.toCharArray(), salt, rounds);
} }
private SecretKey buildCharArrayKey(char[] pwArray, byte[] salt, int rounds) { private SecretKey buildCharArrayKey(String algorithm, char[] pwArray, byte[] salt, int rounds) {
try { try {
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm);
KeySpec ks = new PBEKeySpec(pwArray, salt, rounds, PBKDF2_KEY_SIZE); KeySpec ks = new PBEKeySpec(pwArray, salt, rounds, PBKDF2_KEY_SIZE);
return keyFactory.generateSecret(ks); return keyFactory.generateSecret(ks);
} catch (InvalidKeySpecException e) { } catch (InvalidKeySpecException e) {
@ -1127,8 +1155,8 @@ class BackupManagerService extends IBackupManager.Stub {
return null; return null;
} }
private String buildPasswordHash(String pw, byte[] salt, int rounds) { private String buildPasswordHash(String algorithm, String pw, byte[] salt, int rounds) {
SecretKey key = buildPasswordKey(pw, salt, rounds); SecretKey key = buildPasswordKey(algorithm, pw, salt, rounds);
if (key != null) { if (key != null) {
return byteArrayToHex(key.getEncoded()); return byteArrayToHex(key.getEncoded());
} }
@ -1156,13 +1184,13 @@ class BackupManagerService extends IBackupManager.Stub {
return result; return result;
} }
private byte[] makeKeyChecksum(byte[] pwBytes, byte[] salt, int rounds) { private byte[] makeKeyChecksum(String algorithm, byte[] pwBytes, byte[] salt, int rounds) {
char[] mkAsChar = new char[pwBytes.length]; char[] mkAsChar = new char[pwBytes.length];
for (int i = 0; i < pwBytes.length; i++) { for (int i = 0; i < pwBytes.length; i++) {
mkAsChar[i] = (char) pwBytes[i]; mkAsChar[i] = (char) pwBytes[i];
} }
Key checksum = buildCharArrayKey(mkAsChar, salt, rounds); Key checksum = buildCharArrayKey(algorithm, mkAsChar, salt, rounds);
return checksum.getEncoded(); return checksum.getEncoded();
} }
@ -1174,7 +1202,7 @@ class BackupManagerService extends IBackupManager.Stub {
} }
// Backup password management // Backup password management
boolean passwordMatchesSaved(String candidatePw, int rounds) { boolean passwordMatchesSaved(String algorithm, String candidatePw, int rounds) {
// First, on an encrypted device we require matching the device pw // First, on an encrypted device we require matching the device pw
final boolean isEncrypted; final boolean isEncrypted;
try { try {
@ -1217,7 +1245,7 @@ class BackupManagerService extends IBackupManager.Stub {
} else { } else {
// hash the stated current pw and compare to the stored one // hash the stated current pw and compare to the stored one
if (candidatePw != null && candidatePw.length() > 0) { if (candidatePw != null && candidatePw.length() > 0) {
String currentPwHash = buildPasswordHash(candidatePw, mPasswordSalt, rounds); String currentPwHash = buildPasswordHash(algorithm, candidatePw, mPasswordSalt, rounds);
if (mPasswordHash.equalsIgnoreCase(currentPwHash)) { if (mPasswordHash.equalsIgnoreCase(currentPwHash)) {
// candidate hash matches the stored hash -- the password matches // candidate hash matches the stored hash -- the password matches
return true; return true;
@ -1232,11 +1260,37 @@ class BackupManagerService extends IBackupManager.Stub {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
"setBackupPassword"); "setBackupPassword");
// If the supplied pw doesn't hash to the the saved one, fail // When processing v1 passwords we may need to try two different PBKDF2 checksum regimes
if (!passwordMatchesSaved(currentPw, PBKDF2_HASH_ROUNDS)) { final boolean pbkdf2Fallback = (mPasswordVersion < BACKUP_PW_FILE_VERSION);
// If the supplied pw doesn't hash to the the saved one, fail. The password
// might be caught in the legacy crypto mismatch; verify that too.
if (!passwordMatchesSaved(PBKDF_CURRENT, currentPw, PBKDF2_HASH_ROUNDS)
&& !(pbkdf2Fallback && passwordMatchesSaved(PBKDF_FALLBACK,
currentPw, PBKDF2_HASH_ROUNDS))) {
return false; return false;
} }
// Snap up to current on the pw file version
mPasswordVersion = BACKUP_PW_FILE_VERSION;
FileOutputStream pwFout = null;
DataOutputStream pwOut = null;
try {
pwFout = new FileOutputStream(mPasswordVersionFile);
pwOut = new DataOutputStream(pwFout);
pwOut.writeInt(mPasswordVersion);
} catch (IOException e) {
Slog.e(TAG, "Unable to write backup pw version; password not changed");
return false;
} finally {
try {
if (pwOut != null) pwOut.close();
if (pwFout != null) pwFout.close();
} catch (IOException e) {
Slog.w(TAG, "Unable to close pw version record");
}
}
// Clearing the password is okay // Clearing the password is okay
if (newPw == null || newPw.isEmpty()) { if (newPw == null || newPw.isEmpty()) {
if (mPasswordHashFile.exists()) { if (mPasswordHashFile.exists()) {
@ -1254,7 +1308,7 @@ class BackupManagerService extends IBackupManager.Stub {
try { try {
// Okay, build the hash of the new backup password // Okay, build the hash of the new backup password
byte[] salt = randomBytes(PBKDF2_SALT_SIZE); byte[] salt = randomBytes(PBKDF2_SALT_SIZE);
String newPwHash = buildPasswordHash(newPw, salt, PBKDF2_HASH_ROUNDS); String newPwHash = buildPasswordHash(PBKDF_CURRENT, newPw, salt, PBKDF2_HASH_ROUNDS);
OutputStream pwf = null, buffer = null; OutputStream pwf = null, buffer = null;
DataOutputStream out = null; DataOutputStream out = null;
@ -1297,6 +1351,19 @@ class BackupManagerService extends IBackupManager.Stub {
} }
} }
private boolean backupPasswordMatches(String currentPw) {
if (hasBackupPassword()) {
final boolean pbkdf2Fallback = (mPasswordVersion < BACKUP_PW_FILE_VERSION);
if (!passwordMatchesSaved(PBKDF_CURRENT, currentPw, PBKDF2_HASH_ROUNDS)
&& !(pbkdf2Fallback && passwordMatchesSaved(PBKDF_FALLBACK,
currentPw, PBKDF2_HASH_ROUNDS))) {
if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting");
return false;
}
}
return true;
}
// Maintain persistent state around whether need to do an initialize operation. // Maintain persistent state around whether need to do an initialize operation.
// Must be called with the queue lock held. // Must be called with the queue lock held.
void recordInitPendingLocked(boolean isPending, String transportName) { void recordInitPendingLocked(boolean isPending, String transportName) {
@ -2717,11 +2784,9 @@ class BackupManagerService extends IBackupManager.Stub {
// Verify that the given password matches the currently-active // Verify that the given password matches the currently-active
// backup password, if any // backup password, if any
if (hasBackupPassword()) { if (!backupPasswordMatches(mCurrentPassword)) {
if (!passwordMatchesSaved(mCurrentPassword, PBKDF2_HASH_ROUNDS)) { if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting");
if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting"); return;
return;
}
} }
// Write the global file header. All strings are UTF-8 encoded; lines end // Write the global file header. All strings are UTF-8 encoded; lines end
@ -2729,7 +2794,7 @@ class BackupManagerService extends IBackupManager.Stub {
// final '\n'. // final '\n'.
// //
// line 1: "ANDROID BACKUP" // line 1: "ANDROID BACKUP"
// line 2: backup file format version, currently "1" // line 2: backup file format version, currently "2"
// line 3: compressed? "0" if not compressed, "1" if compressed. // line 3: compressed? "0" if not compressed, "1" if compressed.
// line 4: name of encryption algorithm [currently only "none" or "AES-256"] // line 4: name of encryption algorithm [currently only "none" or "AES-256"]
// //
@ -2837,7 +2902,7 @@ class BackupManagerService extends IBackupManager.Stub {
OutputStream ofstream) throws Exception { OutputStream ofstream) throws Exception {
// User key will be used to encrypt the master key. // User key will be used to encrypt the master key.
byte[] newUserSalt = randomBytes(PBKDF2_SALT_SIZE); byte[] newUserSalt = randomBytes(PBKDF2_SALT_SIZE);
SecretKey userKey = buildPasswordKey(mEncryptPassword, newUserSalt, SecretKey userKey = buildPasswordKey(PBKDF_CURRENT, mEncryptPassword, newUserSalt,
PBKDF2_HASH_ROUNDS); PBKDF2_HASH_ROUNDS);
// the master key is random for each backup // the master key is random for each backup
@ -2884,7 +2949,7 @@ class BackupManagerService extends IBackupManager.Stub {
// stated number of PBKDF2 rounds // stated number of PBKDF2 rounds
IV = c.getIV(); IV = c.getIV();
byte[] mk = masterKeySpec.getEncoded(); byte[] mk = masterKeySpec.getEncoded();
byte[] checksum = makeKeyChecksum(masterKeySpec.getEncoded(), byte[] checksum = makeKeyChecksum(PBKDF_CURRENT, masterKeySpec.getEncoded(),
checksumSalt, PBKDF2_HASH_ROUNDS); checksumSalt, PBKDF2_HASH_ROUNDS);
ByteArrayOutputStream blob = new ByteArrayOutputStream(IV.length + mk.length ByteArrayOutputStream blob = new ByteArrayOutputStream(IV.length + mk.length
@ -3227,11 +3292,9 @@ class BackupManagerService extends IBackupManager.Stub {
FileInputStream rawInStream = null; FileInputStream rawInStream = null;
DataInputStream rawDataIn = null; DataInputStream rawDataIn = null;
try { try {
if (hasBackupPassword()) { if (!backupPasswordMatches(mCurrentPassword)) {
if (!passwordMatchesSaved(mCurrentPassword, PBKDF2_HASH_ROUNDS)) { if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting");
if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting"); return;
return;
}
} }
mBytes = 0; mBytes = 0;
@ -3252,8 +3315,12 @@ class BackupManagerService extends IBackupManager.Stub {
if (Arrays.equals(magicBytes, streamHeader)) { if (Arrays.equals(magicBytes, streamHeader)) {
// okay, header looks good. now parse out the rest of the fields. // okay, header looks good. now parse out the rest of the fields.
String s = readHeaderLine(rawInStream); String s = readHeaderLine(rawInStream);
if (Integer.parseInt(s) == BACKUP_FILE_VERSION) { final int archiveVersion = Integer.parseInt(s);
// okay, it's a version we recognize if (archiveVersion <= BACKUP_FILE_VERSION) {
// okay, it's a version we recognize. if it's version 1, we may need
// to try two different PBKDF2 regimes to compare checksums.
final boolean pbkdf2Fallback = (archiveVersion == 1);
s = readHeaderLine(rawInStream); s = readHeaderLine(rawInStream);
compressed = (Integer.parseInt(s) != 0); compressed = (Integer.parseInt(s) != 0);
s = readHeaderLine(rawInStream); s = readHeaderLine(rawInStream);
@ -3261,7 +3328,8 @@ class BackupManagerService extends IBackupManager.Stub {
// no more header to parse; we're good to go // no more header to parse; we're good to go
okay = true; okay = true;
} else if (mDecryptPassword != null && mDecryptPassword.length() > 0) { } else if (mDecryptPassword != null && mDecryptPassword.length() > 0) {
preCompressStream = decodeAesHeaderAndInitialize(s, rawInStream); preCompressStream = decodeAesHeaderAndInitialize(s, pbkdf2Fallback,
rawInStream);
if (preCompressStream != null) { if (preCompressStream != null) {
okay = true; okay = true;
} }
@ -3321,7 +3389,71 @@ class BackupManagerService extends IBackupManager.Stub {
return buffer.toString(); return buffer.toString();
} }
InputStream decodeAesHeaderAndInitialize(String encryptionName, InputStream rawInStream) { InputStream attemptMasterKeyDecryption(String algorithm, byte[] userSalt, byte[] ckSalt,
int rounds, String userIvHex, String masterKeyBlobHex, InputStream rawInStream,
boolean doLog) {
InputStream result = null;
try {
Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKey userKey = buildPasswordKey(algorithm, mDecryptPassword, userSalt,
rounds);
byte[] IV = hexToByteArray(userIvHex);
IvParameterSpec ivSpec = new IvParameterSpec(IV);
c.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(userKey.getEncoded(), "AES"),
ivSpec);
byte[] mkCipher = hexToByteArray(masterKeyBlobHex);
byte[] mkBlob = c.doFinal(mkCipher);
// first, the master key IV
int offset = 0;
int len = mkBlob[offset++];
IV = Arrays.copyOfRange(mkBlob, offset, offset + len);
offset += len;
// then the master key itself
len = mkBlob[offset++];
byte[] mk = Arrays.copyOfRange(mkBlob,
offset, offset + len);
offset += len;
// and finally the master key checksum hash
len = mkBlob[offset++];
byte[] mkChecksum = Arrays.copyOfRange(mkBlob,
offset, offset + len);
// now validate the decrypted master key against the checksum
byte[] calculatedCk = makeKeyChecksum(algorithm, mk, ckSalt, rounds);
if (Arrays.equals(calculatedCk, mkChecksum)) {
ivSpec = new IvParameterSpec(IV);
c.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(mk, "AES"),
ivSpec);
// Only if all of the above worked properly will 'result' be assigned
result = new CipherInputStream(rawInStream, c);
} else if (doLog) Slog.w(TAG, "Incorrect password");
} catch (InvalidAlgorithmParameterException e) {
if (doLog) Slog.e(TAG, "Needed parameter spec unavailable!", e);
} catch (BadPaddingException e) {
// This case frequently occurs when the wrong password is used to decrypt
// the master key. Use the identical "incorrect password" log text as is
// used in the checksum failure log in order to avoid providing additional
// information to an attacker.
if (doLog) Slog.w(TAG, "Incorrect password");
} catch (IllegalBlockSizeException e) {
if (doLog) Slog.w(TAG, "Invalid block size in master key");
} catch (NoSuchAlgorithmException e) {
if (doLog) Slog.e(TAG, "Needed decryption algorithm unavailable!");
} catch (NoSuchPaddingException e) {
if (doLog) Slog.e(TAG, "Needed padding mechanism unavailable!");
} catch (InvalidKeyException e) {
if (doLog) Slog.w(TAG, "Illegal password; aborting");
}
return result;
}
InputStream decodeAesHeaderAndInitialize(String encryptionName, boolean pbkdf2Fallback,
InputStream rawInStream) {
InputStream result = null; InputStream result = null;
try { try {
if (encryptionName.equals(ENCRYPTION_ALGORITHM_NAME)) { if (encryptionName.equals(ENCRYPTION_ALGORITHM_NAME)) {
@ -3338,59 +3470,13 @@ class BackupManagerService extends IBackupManager.Stub {
String masterKeyBlobHex = readHeaderLine(rawInStream); // 9 String masterKeyBlobHex = readHeaderLine(rawInStream); // 9
// decrypt the master key blob // decrypt the master key blob
Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); result = attemptMasterKeyDecryption(PBKDF_CURRENT, userSalt, ckSalt,
SecretKey userKey = buildPasswordKey(mDecryptPassword, userSalt, rounds, userIvHex, masterKeyBlobHex, rawInStream, false);
rounds); if (result == null && pbkdf2Fallback) {
byte[] IV = hexToByteArray(userIvHex); result = attemptMasterKeyDecryption(PBKDF_FALLBACK, userSalt, ckSalt,
IvParameterSpec ivSpec = new IvParameterSpec(IV); rounds, userIvHex, masterKeyBlobHex, rawInStream, true);
c.init(Cipher.DECRYPT_MODE, }
new SecretKeySpec(userKey.getEncoded(), "AES"),
ivSpec);
byte[] mkCipher = hexToByteArray(masterKeyBlobHex);
byte[] mkBlob = c.doFinal(mkCipher);
// first, the master key IV
int offset = 0;
int len = mkBlob[offset++];
IV = Arrays.copyOfRange(mkBlob, offset, offset + len);
offset += len;
// then the master key itself
len = mkBlob[offset++];
byte[] mk = Arrays.copyOfRange(mkBlob,
offset, offset + len);
offset += len;
// and finally the master key checksum hash
len = mkBlob[offset++];
byte[] mkChecksum = Arrays.copyOfRange(mkBlob,
offset, offset + len);
// now validate the decrypted master key against the checksum
byte[] calculatedCk = makeKeyChecksum(mk, ckSalt, rounds);
if (Arrays.equals(calculatedCk, mkChecksum)) {
ivSpec = new IvParameterSpec(IV);
c.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(mk, "AES"),
ivSpec);
// Only if all of the above worked properly will 'result' be assigned
result = new CipherInputStream(rawInStream, c);
} else Slog.w(TAG, "Incorrect password");
} else Slog.w(TAG, "Unsupported encryption method: " + encryptionName); } else Slog.w(TAG, "Unsupported encryption method: " + encryptionName);
} catch (InvalidAlgorithmParameterException e) {
Slog.e(TAG, "Needed parameter spec unavailable!", e);
} catch (BadPaddingException e) {
// This case frequently occurs when the wrong password is used to decrypt
// the master key. Use the identical "incorrect password" log text as is
// used in the checksum failure log in order to avoid providing additional
// information to an attacker.
Slog.w(TAG, "Incorrect password");
} catch (IllegalBlockSizeException e) {
Slog.w(TAG, "Invalid block size in master key");
} catch (NoSuchAlgorithmException e) {
Slog.e(TAG, "Needed decryption algorithm unavailable!");
} catch (NoSuchPaddingException e) {
Slog.e(TAG, "Needed padding mechanism unavailable!");
} catch (InvalidKeyException e) {
Slog.w(TAG, "Illegal password; aborting");
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
Slog.w(TAG, "Can't parse restore data header"); Slog.w(TAG, "Can't parse restore data header");
} catch (IOException e) { } catch (IOException e) {

View File

@ -0,0 +1,18 @@
The file "jbmr2-encrypted-settings-abcd.ab" in this directory is an encrypted
"adb backup" archive of the settings provider package. It was generated on a
Nexus 4 running Android 4.3 (API 18), and so predates the Android 4.4 changes
to the PBKDF2 implementation. The archive's encryption password, entered on-screen,
is "abcd" (with no quotation marks).
'adb restore' decrypts and applies the restored archive successfully on a device
running Android 4.3, but fails to restore correctly on a device running Android 4.4,
reporting an invalid password in logcat. This is the situation reported in bug
<https://code.google.com/p/android/issues/detail?id=63880>.
The file "kk-fixed-encrypted-settings-abcd.ab" is a similar encrypted "adb backup"
archive, using the same key, generated on a Nexus 4 running Android 4.4 with a fix
to this bug in place. This archive should be successfully restorable on any
version of Android which incorporates the fix.
These archives can be used as an ongoing test to verify that historical encrypted
archives from various points in Android's history can be successfully restored.