| /* |
| * 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. |
| * |
| * <p>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. |
| * |
| * <p>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<Integer> getPasswordVersionFileCodec() { |
| return new DataStreamFileCodec<>( |
| new File(mBaseStateDir, PASSWORD_VERSION_FILE_NAME), |
| new PasswordVersionFileCodec()); |
| } |
| |
| private DataStreamFileCodec<BackupPasswordHash> getPasswordHashFileCodec() { |
| return new DataStreamFileCodec<>(getPasswordHashFile(), new PasswordHashFileCodec()); |
| } |
| |
| private File getPasswordHashFile() { |
| return new File(mBaseStateDir, PASSWORD_HASH_FILE_NAME); |
| } |
| |
| /** |
| * Container class for a PBKDF hash and the salt used to create the hash. |
| */ |
| private static final class BackupPasswordHash { |
| public String hash; |
| public byte[] salt; |
| |
| BackupPasswordHash(String hash, byte[] salt) { |
| this.hash = hash; |
| this.salt = salt; |
| } |
| } |
| |
| /** |
| * The password version file contains a single 32-bit integer. |
| */ |
| private static final class PasswordVersionFileCodec implements |
| DataStreamCodec<Integer> { |
| @Override |
| public void serialize(Integer integer, DataOutputStream dataOutputStream) |
| throws IOException { |
| dataOutputStream.write(integer); |
| } |
| |
| @Override |
| public Integer deserialize(DataInputStream dataInputStream) throws IOException { |
| return dataInputStream.readInt(); |
| } |
| } |
| |
| /** |
| * The passwords hash file contains |
| * |
| * <ul> |
| * <li>A 32-bit integer representing the number of bytes in the salt; |
| * <li>The salt bytes; |
| * <li>A UTF-8 string of the hash. |
| * </ul> |
| */ |
| private static final class PasswordHashFileCodec implements |
| DataStreamCodec<BackupPasswordHash> { |
| @Override |
| public void serialize(BackupPasswordHash backupPasswordHash, |
| DataOutputStream dataOutputStream) throws IOException { |
| dataOutputStream.writeInt(backupPasswordHash.salt.length); |
| dataOutputStream.write(backupPasswordHash.salt); |
| dataOutputStream.writeUTF(backupPasswordHash.hash); |
| } |
| |
| @Override |
| public BackupPasswordHash deserialize( |
| DataInputStream dataInputStream) throws IOException { |
| int saltLen = dataInputStream.readInt(); |
| byte[] salt = new byte[saltLen]; |
| dataInputStream.readFully(salt); |
| String hash = dataInputStream.readUTF(); |
| return new BackupPasswordHash(hash, salt); |
| } |
| } |
| } |