Loading services/backup/java/com/android/server/backup/BackupPasswordManager.java 0 → 100644 +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); } } } services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java +6 −178 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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: Loading @@ -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; Loading Loading @@ -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") Loading Loading @@ -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 Loading Loading @@ -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); Loading Loading @@ -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. Loading services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java +1 −1 Original line number Diff line number Diff line Loading @@ -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; Loading services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java +2 −2 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading services/backup/java/com/android/server/backup/utils/DataStreamCodec.java 0 → 100644 +40 −0 File added.Preview size limit exceeded, changes collapsed. Show changes Loading
services/backup/java/com/android/server/backup/BackupPasswordManager.java 0 → 100644 +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); } } }
services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java +6 −178 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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: Loading @@ -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; Loading Loading @@ -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") Loading Loading @@ -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 Loading Loading @@ -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); Loading Loading @@ -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. Loading
services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java +1 −1 Original line number Diff line number Diff line Loading @@ -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; Loading
services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java +2 −2 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading
services/backup/java/com/android/server/backup/utils/DataStreamCodec.java 0 → 100644 +40 −0 File added.Preview size limit exceeded, changes collapsed. Show changes