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