/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.server.backup; import android.content.Context; import android.util.Slog; import com.android.server.backup.utils.DataStreamFileCodec; import com.android.server.backup.utils.DataStreamCodec; import com.android.server.backup.utils.PasswordUtils; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.IOException; import java.security.SecureRandom; /** * Manages persisting and verifying backup passwords. * *
Does not persist the password itself, but persists a PBKDF2 hash with a randomly chosen (also * persisted) salt. Validation is performed by running the challenge text through the same * PBKDF2 cycle with the persisted salt, and checking the hashes match. * * @see PasswordUtils for the hashing algorithm. */ public final class BackupPasswordManager { private static final String TAG = "BackupPasswordManager"; private static final boolean DEBUG = false; private static final int BACKUP_PW_FILE_VERSION = 2; private static final int DEFAULT_PW_FILE_VERSION = 1; private static final String PASSWORD_VERSION_FILE_NAME = "pwversion"; private static final String PASSWORD_HASH_FILE_NAME = "pwhash"; // See https://android-developers.googleblog.com/2013/12/changes-to-secretkeyfactory-api-in.html public static final String PBKDF_CURRENT = "PBKDF2WithHmacSHA1"; public static final String PBKDF_FALLBACK = "PBKDF2WithHmacSHA1And8bit"; private final SecureRandom mRng; private final Context mContext; private final File mBaseStateDir; private String mPasswordHash; private int mPasswordVersion; private byte[] mPasswordSalt; /** * Creates an instance enforcing permissions using the {@code context} and persisting password * data within the {@code baseStateDir}. * * @param context The context, for enforcing permissions around setting the password. * @param baseStateDir A directory within which to persist password data. * @param secureRandom Random number generator with which to generate password salts. */ BackupPasswordManager(Context context, File baseStateDir, SecureRandom secureRandom) { mContext = context; mRng = secureRandom; mBaseStateDir = baseStateDir; loadStateFromFilesystem(); } /** * Returns {@code true} if a password for backup is set. * * @throws SecurityException If caller does not have {@link android.Manifest.permission#BACKUP} * permission. */ boolean hasBackupPassword() { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "hasBackupPassword"); return mPasswordHash != null && mPasswordHash.length() > 0; } /** * Returns {@code true} if {@code password} matches the persisted password. * * @throws SecurityException If caller does not have {@link android.Manifest.permission#BACKUP} * permission. */ boolean backupPasswordMatches(String password) { if (hasBackupPassword() && !passwordMatchesSaved(password)) { if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting"); return false; } return true; } /** * Sets the new password, given a correct current password. * * @throws SecurityException If caller does not have {@link android.Manifest.permission#BACKUP} * permission. * @return {@code true} if has permission to set the password, {@code currentPassword} * matches the currently persisted password, and is able to persist {@code newPassword}. */ boolean setBackupPassword(String currentPassword, String newPassword) { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "setBackupPassword"); if (!passwordMatchesSaved(currentPassword)) { return false; } // Snap up to latest password file version. try { getPasswordVersionFileCodec().serialize(BACKUP_PW_FILE_VERSION); mPasswordVersion = BACKUP_PW_FILE_VERSION; } catch (IOException e) { Slog.e(TAG, "Unable to write backup pw version; password not changed"); return false; } if (newPassword == null || newPassword.isEmpty()) { return clearPassword(); } try { byte[] salt = randomSalt(); String newPwHash = PasswordUtils.buildPasswordHash( PBKDF_CURRENT, newPassword, salt, PasswordUtils.PBKDF2_HASH_ROUNDS); getPasswordHashFileCodec().serialize(new BackupPasswordHash(newPwHash, salt)); mPasswordHash = newPwHash; mPasswordSalt = salt; return true; } catch (IOException e) { Slog.e(TAG, "Unable to set backup password"); } return false; } /** * Returns {@code true} if should try salting using the older PBKDF algorithm. * *
This is {@code true} for v1 files.
*/
private boolean usePbkdf2Fallback() {
return mPasswordVersion < BACKUP_PW_FILE_VERSION;
}
/**
* Deletes the current backup password.
*
* @return {@code true} if successful.
*/
private boolean clearPassword() {
File passwordHashFile = getPasswordHashFile();
if (passwordHashFile.exists() && !passwordHashFile.delete()) {
Slog.e(TAG, "Unable to clear backup password");
return false;
}
mPasswordHash = null;
mPasswordSalt = null;
return true;
}
/**
* Sets the password hash, salt, and version in the object from what has been persisted to the
* filesystem.
*/
private void loadStateFromFilesystem() {
try {
mPasswordVersion = getPasswordVersionFileCodec().deserialize();
} catch (IOException e) {
Slog.e(TAG, "Unable to read backup pw version");
mPasswordVersion = DEFAULT_PW_FILE_VERSION;
}
try {
BackupPasswordHash hash = getPasswordHashFileCodec().deserialize();
mPasswordHash = hash.hash;
mPasswordSalt = hash.salt;
} catch (IOException e) {
Slog.e(TAG, "Unable to read saved backup pw hash");
}
}
/**
* Whether the candidate password matches the current password. If the persisted password is an
* older version, attempts hashing using the older algorithm.
*
* @param candidatePassword The password to try.
* @return {@code true} if the passwords match.
*/
private boolean passwordMatchesSaved(String candidatePassword) {
return passwordMatchesSaved(PBKDF_CURRENT, candidatePassword)
|| (usePbkdf2Fallback() && passwordMatchesSaved(PBKDF_FALLBACK, candidatePassword));
}
/**
* Returns {@code true} if the candidate password is correct.
*
* @param algorithm The algorithm used to hash passwords.
* @param candidatePassword The candidate password to compare to the current password.
* @return {@code true} if the candidate password matched the saved password.
*/
private boolean passwordMatchesSaved(String algorithm, String candidatePassword) {
if (mPasswordHash == null) {
return candidatePassword == null || candidatePassword.equals("");
} else if (candidatePassword == null || candidatePassword.length() == 0) {
// The current password is not zero-length, but the candidate password is.
return false;
} else {
String candidatePasswordHash = PasswordUtils.buildPasswordHash(
algorithm, candidatePassword, mPasswordSalt, PasswordUtils.PBKDF2_HASH_ROUNDS);
return mPasswordHash.equalsIgnoreCase(candidatePasswordHash);
}
}
private byte[] randomSalt() {
int bitsPerByte = 8;
byte[] array = new byte[PasswordUtils.PBKDF2_SALT_SIZE / bitsPerByte];
mRng.nextBytes(array);
return array;
}
private DataStreamFileCodec
*
*/
private static final class PasswordHashFileCodec implements
DataStreamCodec