Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4957cfe2 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Pull backup password logic out of BackupManagerService"

parents 267c6b2a 9699fe31
Loading
Loading
Loading
Loading
+307 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }
}
+6 −178
Original line number Diff line number Diff line
@@ -117,13 +117,11 @@ import com.android.server.backup.restore.PerformUnifiedRestoreTask;
import com.android.server.backup.utils.AppBackupUtils;
import com.android.server.backup.utils.BackupManagerMonitorUtils;
import com.android.server.backup.utils.BackupObserverUtils;
import com.android.server.backup.utils.PasswordUtils;
import com.android.server.power.BatterySaverPolicy.ServiceType;

import libcore.io.IoUtils;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
@@ -135,7 +133,6 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.security.SecureRandom;
@@ -169,10 +166,6 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
    // with U+FF00 or higher for system use).
    public static final String KEY_WIDGET_STATE = "\uffed\uffedwidget";

    // Historical and current algorithm names
    public static final String PBKDF_CURRENT = "PBKDF2WithHmacSHA1";
    public static final String PBKDF_FALLBACK = "PBKDF2WithHmacSHA1And8bit";

    // Name and current contents version of the full-backup manifest file
    //
    // Manifest version history:
@@ -190,7 +183,6 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
    // 5 : added support for key-value packages
    public static final int BACKUP_FILE_VERSION = 5;
    public static final String BACKUP_FILE_HEADER_MAGIC = "ANDROID BACKUP\n";
    private static final int BACKUP_PW_FILE_VERSION = 2;
    public static final String BACKUP_METADATA_FILENAME = "_meta";
    public static final int BACKUP_METADATA_VERSION = 1;
    public static final int BACKUP_WIDGET_METADATA_TOKEN = 0x01FFED01;
@@ -283,6 +275,8 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
    private final Object mClearDataLock = new Object();
    private volatile boolean mClearingData;

    private final BackupPasswordManager mBackupPasswordManager;

    @GuardedBy("mPendingRestores")
    private boolean mIsRestoreInProgress;
    @GuardedBy("mPendingRestores")
@@ -632,18 +626,7 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
    private File mJournalDir;
    private File mJournal;

    // Backup password, if any, and the file where it's saved.  What is stored is not the
    // password text itself; it's the result of a PBKDF2 hash with a randomly chosen (but
    // persisted) salt.  Validation is performed by running the challenge text through the
    // same PBKDF2 cycle with the persisted salt; if the resulting derived key string matches
    // the saved hash string, then the challenge text matches the originally supplied
    // password text.
    private final SecureRandom mRng = new SecureRandom();
    private String mPasswordHash;
    private File mPasswordHashFile;
    private int mPasswordVersion;
    private File mPasswordVersionFile;
    private byte[] mPasswordSalt;

    // Keep a log of all the apps we've ever backed up, and what the
    // dataset tokens are for both the current backup dataset and
@@ -745,52 +728,7 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
        // This dir on /cache is managed directly in init.rc
        mDataDir = new File(Environment.getDownloadCacheDirectory(), "backup_stage");

        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");
        if (mPasswordHashFile.exists()) {
            FileInputStream fin = null;
            DataInputStream in = null;
            try {
                fin = new FileInputStream(mPasswordHashFile);
                in = new DataInputStream(new BufferedInputStream(fin));
                // integer length of the salt array, followed by the salt,
                // then the hex pw hash string
                int saltLen = in.readInt();
                byte[] salt = new byte[saltLen];
                in.readFully(salt);
                mPasswordHash = in.readUTF();
                mPasswordSalt = salt;
            } catch (IOException e) {
                Slog.e(TAG, "Unable to read saved backup pw hash");
            } finally {
                try {
                    if (in != null) in.close();
                    if (fin != null) fin.close();
                } catch (IOException e) {
                    Slog.w(TAG, "Unable to close streams");
                }
            }
        }
        mBackupPasswordManager = new BackupPasswordManager(mContext, mBaseStateDir, mRng);

        // Alarm receivers for scheduled backups & initialization operations
        mRunBackupReceiver = new RunBackupReceiver(this);
@@ -1146,128 +1084,18 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
        return array;
    }

    private boolean passwordMatchesSaved(String algorithm, String candidatePw, int rounds) {
        if (mPasswordHash == null) {
            // no current password case -- require that 'currentPw' be null or empty
            if (candidatePw == null || "".equals(candidatePw)) {
                return true;
            } // else the non-empty candidate does not match the empty stored pw
        } else {
            // hash the stated current pw and compare to the stored one
            if (candidatePw != null && candidatePw.length() > 0) {
                String currentPwHash = PasswordUtils.buildPasswordHash(algorithm, candidatePw,
                        mPasswordSalt,
                        rounds);
                if (mPasswordHash.equalsIgnoreCase(currentPwHash)) {
                    // candidate hash matches the stored hash -- the password matches
                    return true;
                }
            } // else the stored pw is nonempty but the candidate is empty; no match
        }
        return false;
    }

    @Override
    public boolean setBackupPassword(String currentPw, String newPw) {
        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
                "setBackupPassword");

        // When processing v1 passwords we may need to try two different PBKDF2 checksum regimes
        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, PasswordUtils.PBKDF2_HASH_ROUNDS)
                && !(pbkdf2Fallback && passwordMatchesSaved(PBKDF_FALLBACK,
                currentPw, PasswordUtils.PBKDF2_HASH_ROUNDS))) {
            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
        if (newPw == null || newPw.isEmpty()) {
            if (mPasswordHashFile.exists()) {
                if (!mPasswordHashFile.delete()) {
                    // Unable to delete the old pw file, so fail
                    Slog.e(TAG, "Unable to clear backup password");
                    return false;
                }
            }
            mPasswordHash = null;
            mPasswordSalt = null;
            return true;
        }

        try {
            // Okay, build the hash of the new backup password
            byte[] salt = randomBytes(PasswordUtils.PBKDF2_SALT_SIZE);
            String newPwHash = PasswordUtils.buildPasswordHash(PBKDF_CURRENT, newPw, salt,
                    PasswordUtils.PBKDF2_HASH_ROUNDS);

            OutputStream pwf = null, buffer = null;
            DataOutputStream out = null;
            try {
                pwf = new FileOutputStream(mPasswordHashFile);
                buffer = new BufferedOutputStream(pwf);
                out = new DataOutputStream(buffer);
                // integer length of the salt array, followed by the salt,
                // then the hex pw hash string
                out.writeInt(salt.length);
                out.write(salt);
                out.writeUTF(newPwHash);
                out.flush();
                mPasswordHash = newPwHash;
                mPasswordSalt = salt;
                return true;
            } finally {
                if (out != null) out.close();
                if (buffer != null) buffer.close();
                if (pwf != null) pwf.close();
            }
        } catch (IOException e) {
            Slog.e(TAG, "Unable to set backup password");
        }
        return false;
        return mBackupPasswordManager.setBackupPassword(currentPw, newPw);
    }

    @Override
    public boolean hasBackupPassword() {
        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
                "hasBackupPassword");

        return mPasswordHash != null && mPasswordHash.length() > 0;
        return mBackupPasswordManager.hasBackupPassword();
    }

    public boolean backupPasswordMatches(String currentPw) {
        if (hasBackupPassword()) {
            final boolean pbkdf2Fallback = (mPasswordVersion < BACKUP_PW_FILE_VERSION);
            if (!passwordMatchesSaved(PBKDF_CURRENT, currentPw, PasswordUtils.PBKDF2_HASH_ROUNDS)
                    && !(pbkdf2Fallback && passwordMatchesSaved(PBKDF_FALLBACK,
                    currentPw, PasswordUtils.PBKDF2_HASH_ROUNDS))) {
                if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting");
                return false;
            }
        }
        return true;
        return mBackupPasswordManager.backupPasswordMatches(currentPw);
    }

    // Maintain persistent state around whether need to do an initialize operation.
+1 −1
Original line number Diff line number Diff line
@@ -16,11 +16,11 @@

package com.android.server.backup.fullbackup;

import static com.android.server.backup.BackupPasswordManager.PBKDF_CURRENT;
import static com.android.server.backup.RefactoredBackupManagerService.BACKUP_FILE_HEADER_MAGIC;
import static com.android.server.backup.RefactoredBackupManagerService.BACKUP_FILE_VERSION;
import static com.android.server.backup.RefactoredBackupManagerService.DEBUG;
import static com.android.server.backup.RefactoredBackupManagerService.MORE_DEBUG;
import static com.android.server.backup.RefactoredBackupManagerService.PBKDF_CURRENT;
import static com.android.server.backup.RefactoredBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
import static com.android.server.backup.RefactoredBackupManagerService.TAG;

+2 −2
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.server.backup.restore;

import static com.android.server.backup.BackupPasswordManager.PBKDF_CURRENT;
import static com.android.server.backup.BackupPasswordManager.PBKDF_FALLBACK;
import static com.android.server.backup.RefactoredBackupManagerService.BACKUP_FILE_HEADER_MAGIC;
import static com.android.server.backup.RefactoredBackupManagerService.BACKUP_FILE_VERSION;
import static com.android.server.backup.RefactoredBackupManagerService.BACKUP_MANIFEST_FILENAME;
@@ -23,8 +25,6 @@ import static com.android.server.backup.RefactoredBackupManagerService.BACKUP_ME
import static com.android.server.backup.RefactoredBackupManagerService.DEBUG;
import static com.android.server.backup.RefactoredBackupManagerService.MORE_DEBUG;
import static com.android.server.backup.RefactoredBackupManagerService.OP_TYPE_RESTORE_WAIT;
import static com.android.server.backup.RefactoredBackupManagerService.PBKDF_CURRENT;
import static com.android.server.backup.RefactoredBackupManagerService.PBKDF_FALLBACK;
import static com.android.server.backup.RefactoredBackupManagerService.SETTINGS_PACKAGE;
import static com.android.server.backup.RefactoredBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
import static com.android.server.backup.RefactoredBackupManagerService.TAG;
+40 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading