Home | History | Annotate | Download | only in backup
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.server.backup;
     18 
     19 import android.content.Context;
     20 import android.util.Slog;
     21 
     22 import com.android.server.backup.utils.DataStreamFileCodec;
     23 import com.android.server.backup.utils.DataStreamCodec;
     24 import com.android.server.backup.utils.PasswordUtils;
     25 
     26 import java.io.DataInputStream;
     27 import java.io.DataOutputStream;
     28 import java.io.File;
     29 import java.io.IOException;
     30 import java.security.SecureRandom;
     31 
     32 /**
     33  * Manages persisting and verifying backup passwords.
     34  *
     35  * <p>Does not persist the password itself, but persists a PBKDF2 hash with a randomly chosen (also
     36  * persisted) salt. Validation is performed by running the challenge text through the same
     37  * PBKDF2 cycle with the persisted salt, and checking the hashes match.
     38  *
     39  * @see PasswordUtils for the hashing algorithm.
     40  */
     41 public final class BackupPasswordManager {
     42     private static final String TAG = "BackupPasswordManager";
     43     private static final boolean DEBUG = false;
     44 
     45     private static final int BACKUP_PW_FILE_VERSION = 2;
     46     private static final int DEFAULT_PW_FILE_VERSION = 1;
     47 
     48     private static final String PASSWORD_VERSION_FILE_NAME = "pwversion";
     49     private static final String PASSWORD_HASH_FILE_NAME = "pwhash";
     50 
     51     // See https://android-developers.googleblog.com/2013/12/changes-to-secretkeyfactory-api-in.html
     52     public static final String PBKDF_CURRENT = "PBKDF2WithHmacSHA1";
     53     public static final String PBKDF_FALLBACK = "PBKDF2WithHmacSHA1And8bit";
     54 
     55     private final SecureRandom mRng;
     56     private final Context mContext;
     57     private final File mBaseStateDir;
     58 
     59     private String mPasswordHash;
     60     private int mPasswordVersion;
     61     private byte[] mPasswordSalt;
     62 
     63     /**
     64      * Creates an instance enforcing permissions using the {@code context} and persisting password
     65      * data within the {@code baseStateDir}.
     66      *
     67      * @param context The context, for enforcing permissions around setting the password.
     68      * @param baseStateDir A directory within which to persist password data.
     69      * @param secureRandom Random number generator with which to generate password salts.
     70      */
     71     BackupPasswordManager(Context context, File baseStateDir, SecureRandom secureRandom) {
     72         mContext = context;
     73         mRng = secureRandom;
     74         mBaseStateDir = baseStateDir;
     75         loadStateFromFilesystem();
     76     }
     77 
     78     /**
     79      * Returns {@code true} if a password for backup is set.
     80      *
     81      * @throws SecurityException If caller does not have {@link android.Manifest.permission#BACKUP}
     82      *   permission.
     83      */
     84     boolean hasBackupPassword() {
     85         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
     86                 "hasBackupPassword");
     87         return mPasswordHash != null && mPasswordHash.length() > 0;
     88     }
     89 
     90     /**
     91      * Returns {@code true} if {@code password} matches the persisted password.
     92      *
     93      * @throws SecurityException If caller does not have {@link android.Manifest.permission#BACKUP}
     94      *   permission.
     95      */
     96     boolean backupPasswordMatches(String password) {
     97         if (hasBackupPassword() && !passwordMatchesSaved(password)) {
     98             if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting");
     99             return false;
    100         }
    101         return true;
    102     }
    103 
    104     /**
    105      * Sets the new password, given a correct current password.
    106      *
    107      * @throws SecurityException If caller does not have {@link android.Manifest.permission#BACKUP}
    108      *   permission.
    109      * @return {@code true} if has permission to set the password, {@code currentPassword}
    110      *   matches the currently persisted password, and is able to persist {@code newPassword}.
    111      */
    112     boolean setBackupPassword(String currentPassword, String newPassword) {
    113         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
    114                 "setBackupPassword");
    115 
    116         if (!passwordMatchesSaved(currentPassword)) {
    117             return false;
    118         }
    119 
    120         // Snap up to latest password file version.
    121         try {
    122             getPasswordVersionFileCodec().serialize(BACKUP_PW_FILE_VERSION);
    123             mPasswordVersion = BACKUP_PW_FILE_VERSION;
    124         } catch (IOException e) {
    125             Slog.e(TAG, "Unable to write backup pw version; password not changed");
    126             return false;
    127         }
    128 
    129         if (newPassword == null || newPassword.isEmpty()) {
    130             return clearPassword();
    131         }
    132 
    133         try {
    134             byte[] salt = randomSalt();
    135             String newPwHash = PasswordUtils.buildPasswordHash(
    136                     PBKDF_CURRENT, newPassword, salt, PasswordUtils.PBKDF2_HASH_ROUNDS);
    137 
    138             getPasswordHashFileCodec().serialize(new BackupPasswordHash(newPwHash, salt));
    139             mPasswordHash = newPwHash;
    140             mPasswordSalt = salt;
    141             return true;
    142         } catch (IOException e) {
    143             Slog.e(TAG, "Unable to set backup password");
    144         }
    145         return false;
    146     }
    147 
    148     /**
    149      * Returns {@code true} if should try salting using the older PBKDF algorithm.
    150      *
    151      * <p>This is {@code true} for v1 files.
    152      */
    153     private boolean usePbkdf2Fallback() {
    154         return mPasswordVersion < BACKUP_PW_FILE_VERSION;
    155     }
    156 
    157     /**
    158      * Deletes the current backup password.
    159      *
    160      * @return {@code true} if successful.
    161      */
    162     private boolean clearPassword() {
    163         File passwordHashFile = getPasswordHashFile();
    164         if (passwordHashFile.exists() && !passwordHashFile.delete()) {
    165             Slog.e(TAG, "Unable to clear backup password");
    166             return false;
    167         }
    168 
    169         mPasswordHash = null;
    170         mPasswordSalt = null;
    171         return true;
    172     }
    173 
    174     /**
    175      * Sets the password hash, salt, and version in the object from what has been persisted to the
    176      * filesystem.
    177      */
    178     private void loadStateFromFilesystem() {
    179         try {
    180             mPasswordVersion = getPasswordVersionFileCodec().deserialize();
    181         } catch (IOException e) {
    182             Slog.e(TAG, "Unable to read backup pw version");
    183             mPasswordVersion = DEFAULT_PW_FILE_VERSION;
    184         }
    185 
    186         try {
    187             BackupPasswordHash hash = getPasswordHashFileCodec().deserialize();
    188             mPasswordHash = hash.hash;
    189             mPasswordSalt = hash.salt;
    190         } catch (IOException e) {
    191             Slog.e(TAG, "Unable to read saved backup pw hash");
    192         }
    193     }
    194 
    195     /**
    196      * Whether the candidate password matches the current password. If the persisted password is an
    197      * older version, attempts hashing using the older algorithm.
    198      *
    199      * @param candidatePassword The password to try.
    200      * @return {@code true} if the passwords match.
    201      */
    202     private boolean passwordMatchesSaved(String candidatePassword) {
    203         return passwordMatchesSaved(PBKDF_CURRENT, candidatePassword)
    204                 || (usePbkdf2Fallback() && passwordMatchesSaved(PBKDF_FALLBACK, candidatePassword));
    205     }
    206 
    207     /**
    208      * Returns {@code true} if the candidate password is correct.
    209      *
    210      * @param algorithm The algorithm used to hash passwords.
    211      * @param candidatePassword The candidate password to compare to the current password.
    212      * @return {@code true} if the candidate password matched the saved password.
    213      */
    214     private boolean passwordMatchesSaved(String algorithm, String candidatePassword) {
    215         if (mPasswordHash == null) {
    216             return candidatePassword == null || candidatePassword.equals("");
    217         } else if (candidatePassword == null || candidatePassword.length() == 0) {
    218             // The current password is not zero-length, but the candidate password is.
    219             return false;
    220         } else {
    221             String candidatePasswordHash = PasswordUtils.buildPasswordHash(
    222                     algorithm, candidatePassword, mPasswordSalt, PasswordUtils.PBKDF2_HASH_ROUNDS);
    223             return mPasswordHash.equalsIgnoreCase(candidatePasswordHash);
    224         }
    225     }
    226 
    227     private byte[] randomSalt() {
    228         int bitsPerByte = 8;
    229         byte[] array = new byte[PasswordUtils.PBKDF2_SALT_SIZE / bitsPerByte];
    230         mRng.nextBytes(array);
    231         return array;
    232     }
    233 
    234     private DataStreamFileCodec<Integer> getPasswordVersionFileCodec() {
    235         return new DataStreamFileCodec<>(
    236                 new File(mBaseStateDir, PASSWORD_VERSION_FILE_NAME),
    237                 new PasswordVersionFileCodec());
    238     }
    239 
    240     private DataStreamFileCodec<BackupPasswordHash> getPasswordHashFileCodec() {
    241         return new DataStreamFileCodec<>(getPasswordHashFile(), new PasswordHashFileCodec());
    242     }
    243 
    244     private File getPasswordHashFile() {
    245         return new File(mBaseStateDir, PASSWORD_HASH_FILE_NAME);
    246     }
    247 
    248     /**
    249      * Container class for a PBKDF hash and the salt used to create the hash.
    250      */
    251     private static final class BackupPasswordHash {
    252         public String hash;
    253         public byte[] salt;
    254 
    255         BackupPasswordHash(String hash, byte[] salt) {
    256             this.hash = hash;
    257             this.salt = salt;
    258         }
    259     }
    260 
    261     /**
    262      * The password version file contains a single 32-bit integer.
    263      */
    264     private static final class PasswordVersionFileCodec implements
    265             DataStreamCodec<Integer> {
    266         @Override
    267         public void serialize(Integer integer, DataOutputStream dataOutputStream)
    268                 throws IOException {
    269             dataOutputStream.write(integer);
    270         }
    271 
    272         @Override
    273         public Integer deserialize(DataInputStream dataInputStream) throws IOException {
    274             return dataInputStream.readInt();
    275         }
    276     }
    277 
    278     /**
    279      * The passwords hash file contains
    280      *
    281      * <ul>
    282      *     <li>A 32-bit integer representing the number of bytes in the salt;
    283      *     <li>The salt bytes;
    284      *     <li>A UTF-8 string of the hash.
    285      * </ul>
    286      */
    287     private static final class PasswordHashFileCodec implements
    288             DataStreamCodec<BackupPasswordHash> {
    289         @Override
    290         public void serialize(BackupPasswordHash backupPasswordHash,
    291                 DataOutputStream dataOutputStream) throws IOException {
    292             dataOutputStream.writeInt(backupPasswordHash.salt.length);
    293             dataOutputStream.write(backupPasswordHash.salt);
    294             dataOutputStream.writeUTF(backupPasswordHash.hash);
    295         }
    296 
    297         @Override
    298         public BackupPasswordHash deserialize(
    299                 DataInputStream dataInputStream) throws IOException {
    300             int saltLen = dataInputStream.readInt();
    301             byte[] salt = new byte[saltLen];
    302             dataInputStream.readFully(salt);
    303             String hash = dataInputStream.readUTF();
    304             return new BackupPasswordHash(hash, salt);
    305         }
    306     }
    307 }
    308