diff options
15 files changed, 798 insertions, 193 deletions
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java index 143d147ab4e7..d6e36914ac6c 100644 --- a/core/java/android/app/NotificationChannel.java +++ b/core/java/android/app/NotificationChannel.java @@ -15,11 +15,6 @@ */ package android.app; -import org.json.JSONException; -import org.json.JSONObject; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlSerializer; - import android.annotation.SystemApi; import android.app.NotificationManager.Importance; import android.content.Intent; @@ -31,6 +26,11 @@ import android.provider.Settings; import android.service.notification.NotificationListenerService; import android.text.TextUtils; +import org.json.JSONException; +import org.json.JSONObject; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlSerializer; + import java.io.IOException; import java.util.Arrays; @@ -743,7 +743,7 @@ public final class NotificationChannel implements Parcelable { private static String longArrayToString(long[] values) { StringBuffer sb = new StringBuffer(); - if (values != null) { + if (values != null && values.length > 0) { for (int i = 0; i < values.length - 1; i++) { sb.append(values[i]).append(DELIMITER); } diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index f21545fe8636..04a826514a83 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -1387,7 +1387,7 @@ public class Editor { if (mTextActionMode != null) { switch (event.getActionMasked()) { case MotionEvent.ACTION_MOVE: - hideFloatingToolbar(); + hideFloatingToolbar(ActionMode.DEFAULT_HIDE_DURATION); break; case MotionEvent.ACTION_UP: // fall through case MotionEvent.ACTION_CANCEL: @@ -1396,10 +1396,10 @@ public class Editor { } } - private void hideFloatingToolbar() { + void hideFloatingToolbar(int duration) { if (mTextActionMode != null) { mTextView.removeCallbacks(mShowFloatingToolbar); - mTextActionMode.hide(ActionMode.DEFAULT_HIDE_DURATION); + mTextActionMode.hide(duration); } } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 9a924890fcd7..d277616b0f4f 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -374,6 +374,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private static final int KEY_DOWN_HANDLED_BY_KEY_LISTENER = 1; private static final int KEY_DOWN_HANDLED_BY_MOVEMENT_METHOD = 2; + private static final int FLOATING_TOOLBAR_SELECT_ALL_REFRESH_DELAY = 500; + // System wide time for last cut, copy or text changed action. static long sLastCutCopyOrTextChangedTime; @@ -11138,6 +11140,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } boolean selectAllText() { + if (mEditor != null) { + // Hide the toolbar before changing the selection to avoid flickering. + mEditor.hideFloatingToolbar(FLOATING_TOOLBAR_SELECT_ALL_REFRESH_DELAY); + } final int length = mText.length(); Selection.setSelection((Spannable) mText, 0, length); return length > 0; diff --git a/core/java/com/android/internal/view/FloatingActionMode.java b/core/java/com/android/internal/view/FloatingActionMode.java index ff211b6c2d62..497e7b08d881 100644 --- a/core/java/com/android/internal/view/FloatingActionMode.java +++ b/core/java/com/android/internal/view/FloatingActionMode.java @@ -295,6 +295,8 @@ public final class FloatingActionMode extends ActionMode { */ private static final class FloatingToolbarVisibilityHelper { + private static final long MIN_SHOW_DURATION_FOR_MOVE_HIDE = 500; + private final FloatingToolbar mToolbar; private boolean mHideRequested; @@ -304,6 +306,8 @@ public final class FloatingActionMode extends ActionMode { private boolean mActive; + private long mLastShowTime; + public FloatingToolbarVisibilityHelper(FloatingToolbar toolbar) { mToolbar = Preconditions.checkNotNull(toolbar); } @@ -327,7 +331,13 @@ public final class FloatingActionMode extends ActionMode { } public void setMoving(boolean moving) { - mMoving = moving; + // Avoid unintended flickering by allowing the toolbar to show long enough before + // triggering the 'moving' flag - which signals a hide. + final boolean showingLongEnough = + System.currentTimeMillis() - mLastShowTime > MIN_SHOW_DURATION_FOR_MOVE_HIDE; + if (!moving || showingLongEnough) { + mMoving = moving; + } } public void setOutOfBounds(boolean outOfBounds) { @@ -347,6 +357,7 @@ public final class FloatingActionMode extends ActionMode { mToolbar.hide(); } else { mToolbar.show(); + mLastShowTime = System.currentTimeMillis(); } } } diff --git a/legacy-test/jarjar-rules.txt b/legacy-test/jarjar-rules.txt index 9077e6f8ba26..fd8555c8931c 100644 --- a/legacy-test/jarjar-rules.txt +++ b/legacy-test/jarjar-rules.txt @@ -1,2 +1,3 @@ rule junit.** repackaged.junit.@1 rule android.test.** repackaged.android.test.@1 +rule com.android.internal.util.** repackaged.com.android.internal.util.@1 diff --git a/services/backup/java/com/android/server/backup/BackupPasswordManager.java b/services/backup/java/com/android/server/backup/BackupPasswordManager.java new file mode 100644 index 000000000000..ee7651b0a087 --- /dev/null +++ b/services/backup/java/com/android/server/backup/BackupPasswordManager.java @@ -0,0 +1,307 @@ +/* + * 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); + } + } +} diff --git a/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java b/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java index 7e28f610e565..674e9725cc8a 100644 --- a/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java +++ b/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java @@ -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. diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java index 007d9309c188..804e92c88eb7 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java +++ b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java @@ -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; diff --git a/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java index b1d6afcbd63c..62ae065be1ac 100644 --- a/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java +++ b/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java @@ -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; diff --git a/services/backup/java/com/android/server/backup/utils/DataStreamCodec.java b/services/backup/java/com/android/server/backup/utils/DataStreamCodec.java new file mode 100644 index 000000000000..b1e226d5999c --- /dev/null +++ b/services/backup/java/com/android/server/backup/utils/DataStreamCodec.java @@ -0,0 +1,40 @@ +/* + * 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.utils; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * Implements how to serialize a {@code T} to a {@link DataOutputStream} and how to deserialize a + * {@code T} from a {@link DataInputStream}. + * + * @param <T> Type of object to be serialized / deserialized. + */ +public interface DataStreamCodec<T> { + /** + * Serializes {@code t} to {@code dataOutputStream}. + */ + void serialize(T t, DataOutputStream dataOutputStream) throws IOException; + + /** + * Deserializes {@code t} from {@code dataInputStream}. + */ + T deserialize(DataInputStream dataInputStream) throws IOException; +} + diff --git a/services/backup/java/com/android/server/backup/utils/DataStreamFileCodec.java b/services/backup/java/com/android/server/backup/utils/DataStreamFileCodec.java new file mode 100644 index 000000000000..7753b0370279 --- /dev/null +++ b/services/backup/java/com/android/server/backup/utils/DataStreamFileCodec.java @@ -0,0 +1,78 @@ +/* + * 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.utils; + +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Provides an interface for serializing an object to a file and deserializing it back again. + * + * <p>Serialization logic is implemented as a {@link DataStreamCodec}. + * + * @param <T> The type of object to serialize / deserialize. + */ +public final class DataStreamFileCodec<T> { + private final File mFile; + private final DataStreamCodec<T> mCodec; + + /** + * Constructs an instance to serialize to or deserialize from the given file, with the given + * serialization / deserialization strategy. + */ + public DataStreamFileCodec(File file, DataStreamCodec<T> codec) { + mFile = file; + mCodec = codec; + } + + /** + * Deserializes a {@code T} from the file, automatically closing input streams. + * + * @return The deserialized object. + * @throws IOException if an IO error occurred. + */ + public T deserialize() throws IOException { + try ( + FileInputStream fileInputStream = new FileInputStream(mFile); + DataInputStream dataInputStream = new DataInputStream(fileInputStream) + ) { + return mCodec.deserialize(dataInputStream); + } + } + + /** + * Serializes {@code t} to the file, automatically flushing and closing output streams. + * + * @param t The object to serialize. + * @throws IOException if an IO error occurs. + */ + public void serialize(T t) throws IOException { + try ( + FileOutputStream fileOutputStream = new FileOutputStream(mFile); + BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream); + DataOutputStream dataOutputStream = new DataOutputStream(bufferedOutputStream) + ) { + mCodec.serialize(t, dataOutputStream); + dataOutputStream.flush(); + } + } +} diff --git a/services/backup/java/com/android/server/backup/utils/PasswordUtils.java b/services/backup/java/com/android/server/backup/utils/PasswordUtils.java index 12fc927315c2..9c5e28393a53 100644 --- a/services/backup/java/com/android/server/backup/utils/PasswordUtils.java +++ b/services/backup/java/com/android/server/backup/utils/PasswordUtils.java @@ -123,8 +123,7 @@ public class PasswordUtils { int rounds) { try { SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm); - KeySpec - ks = new PBEKeySpec(pwArray, salt, rounds, PBKDF2_KEY_SIZE); + KeySpec ks = new PBEKeySpec(pwArray, salt, rounds, PBKDF2_KEY_SIZE); return keyFactory.generateSecret(ks); } catch (InvalidKeySpecException e) { Slog.e(TAG, "Invalid key spec for PBKDF2!"); diff --git a/services/tests/notification/src/com/android/server/notification/NotificationChannelTest.java b/services/tests/notification/src/com/android/server/notification/NotificationChannelTest.java index 3007cb1755e2..f457f6a550c1 100644 --- a/services/tests/notification/src/com/android/server/notification/NotificationChannelTest.java +++ b/services/tests/notification/src/com/android/server/notification/NotificationChannelTest.java @@ -25,8 +25,14 @@ import android.os.Parcel; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; +import com.android.internal.util.FastXmlSerializer; + import org.junit.Test; import org.junit.runner.RunWith; +import org.xmlpull.v1.XmlSerializer; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; @SmallTest @RunWith(AndroidJUnit4.class) @@ -50,4 +56,15 @@ public class NotificationChannelTest extends NotificationTestCase { channel.setBlockableSystem(true); assertEquals(true, channel.isBlockableSystem()); } + + @Test + public void testEmptyVibration_noException() throws Exception { + NotificationChannel channel = new NotificationChannel("a", "ab", IMPORTANCE_DEFAULT); + channel.setVibrationPattern(new long[0]); + + XmlSerializer serializer = new FastXmlSerializer(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + serializer.setOutput(new BufferedOutputStream(baos), "utf-8"); + channel.writeXml(serializer); + } } diff --git a/services/tests/servicestests/src/com/android/server/backup/BackupPasswordManagerTest.java b/services/tests/servicestests/src/com/android/server/backup/BackupPasswordManagerTest.java new file mode 100644 index 000000000000..04c02510cb3d --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/backup/BackupPasswordManagerTest.java @@ -0,0 +1,232 @@ +/* + * 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 static com.android.server.testutis.TestUtils.assertExpectException; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; + +import android.content.Context; +import android.platform.test.annotations.Presubmit; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import com.android.server.backup.utils.PasswordUtils; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.security.SecureRandom; + +@SmallTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class BackupPasswordManagerTest { + private static final String PASSWORD_VERSION_FILE_NAME = "pwversion"; + private static final String PASSWORD_HASH_FILE_NAME = "pwhash"; + private static final String V1_HASH_ALGORITHM = "PBKDF2WithHmacSHA1And8bit"; + + @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + + @Mock private Context mContext; + + private File mStateFolder; + private BackupPasswordManager mPasswordManager; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mStateFolder = mTemporaryFolder.newFolder(); + mPasswordManager = new BackupPasswordManager(mContext, mStateFolder, new SecureRandom()); + } + + @Test + public void hasBackupPassword_isFalseIfFileDoesNotExist() { + assertThat(mPasswordManager.hasBackupPassword()).isFalse(); + } + + @Test + public void hasBackupPassword_isTrueIfFileExists() throws Exception { + mPasswordManager.setBackupPassword(null, "password1234"); + assertThat(mPasswordManager.hasBackupPassword()).isTrue(); + } + + @Test + public void hasBackupPassword_throwsSecurityExceptionIfLacksPermission() { + setDoesNotHavePermission(); + + assertExpectException( + SecurityException.class, + /* expectedExceptionMessageRegex */ null, + () -> mPasswordManager.hasBackupPassword()); + } + + @Test + public void backupPasswordMatches_isTrueIfNoPassword() { + assertThat(mPasswordManager.backupPasswordMatches("anything")).isTrue(); + } + + @Test + public void backupPasswordMatches_isTrueForSamePassword() { + String password = "password1234"; + mPasswordManager.setBackupPassword(null, password); + assertThat(mPasswordManager.backupPasswordMatches(password)).isTrue(); + } + + @Test + public void backupPasswordMatches_isFalseForDifferentPassword() { + mPasswordManager.setBackupPassword(null, "shiba"); + assertThat(mPasswordManager.backupPasswordMatches("corgi")).isFalse(); + } + + @Test + public void backupPasswordMatches_worksForV1HashIfVersionIsV1() throws Exception { + String password = "corgi\uFFFF"; + writePasswordVersionToFile(1); + writeV1HashToFile(password, saltFixture()); + + // Reconstruct so it reloads from filesystem + mPasswordManager = new BackupPasswordManager(mContext, mStateFolder, new SecureRandom()); + + assertThat(mPasswordManager.backupPasswordMatches(password)).isTrue(); + } + + @Test + public void backupPasswordMatches_failsForV1HashIfVersionIsV2() throws Exception { + // The algorithms produce identical hashes except if the password contains higher-order + // unicode. See + // https://android-developers.googleblog.com/2013/12/changes-to-secretkeyfactory-api-in.html + String password = "corgi\uFFFF"; + writePasswordVersionToFile(2); + writeV1HashToFile(password, saltFixture()); + + // Reconstruct so it reloads from filesystem + mPasswordManager = new BackupPasswordManager(mContext, mStateFolder, new SecureRandom()); + + assertThat(mPasswordManager.backupPasswordMatches(password)).isFalse(); + } + + @Test + public void backupPasswordMatches_throwsSecurityExceptionIfLacksPermission() { + setDoesNotHavePermission(); + + assertExpectException( + SecurityException.class, + /* expectedExceptionMessageRegex */ null, + () -> mPasswordManager.backupPasswordMatches("password123")); + } + + @Test + public void setBackupPassword_persistsPasswordToFile() { + String password = "shiba"; + + mPasswordManager.setBackupPassword(null, password); + + BackupPasswordManager newManager = new BackupPasswordManager( + mContext, mStateFolder, new SecureRandom()); + assertThat(newManager.backupPasswordMatches(password)).isTrue(); + } + + @Test + public void setBackupPassword_failsIfCurrentPasswordIsWrong() { + String secondPassword = "second password"; + mPasswordManager.setBackupPassword(null, "first password"); + + boolean result = mPasswordManager.setBackupPassword( + "incorrect pass", secondPassword); + + BackupPasswordManager newManager = new BackupPasswordManager( + mContext, mStateFolder, new SecureRandom()); + assertThat(result).isFalse(); + assertThat(newManager.backupPasswordMatches(secondPassword)).isFalse(); + } + + @Test + public void setBackupPassword_throwsSecurityExceptionIfLacksPermission() { + setDoesNotHavePermission(); + + assertExpectException( + SecurityException.class, + /* expectedExceptionMessageRegex */ null, + () -> mPasswordManager.setBackupPassword( + "password123", "password111")); + } + + private byte[] saltFixture() { + byte[] bytes = new byte[64]; + for (int i = 0; i < 64; i++) { + bytes[i] = (byte) i; + } + return bytes; + } + + private void setDoesNotHavePermission() { + doThrow(new SecurityException()).when(mContext) + .enforceCallingOrSelfPermission(anyString(), anyString()); + } + + private void writeV1HashToFile(String password, byte[] salt) throws Exception { + String hash = PasswordUtils.buildPasswordHash( + V1_HASH_ALGORITHM, password, salt, PasswordUtils.PBKDF2_HASH_ROUNDS); + writeHashAndSaltToFile(hash, salt); + } + + private void writeHashAndSaltToFile(String hash, byte[] salt) throws Exception { + FileOutputStream fos = null; + DataOutputStream dos = null; + + try { + File passwordHash = new File(mStateFolder, PASSWORD_HASH_FILE_NAME); + fos = new FileOutputStream(passwordHash); + dos = new DataOutputStream(fos); + dos.writeInt(salt.length); + dos.write(salt); + dos.writeUTF(hash); + dos.flush(); + } finally { + if (dos != null) dos.close(); + if (fos != null) fos.close(); + } + } + + private void writePasswordVersionToFile(int version) throws Exception { + FileOutputStream fos = null; + DataOutputStream dos = null; + + try { + File passwordVersion = new File(mStateFolder, PASSWORD_VERSION_FILE_NAME); + fos = new FileOutputStream(passwordVersion); + dos = new DataOutputStream(fos); + dos.writeInt(version); + dos.flush(); + } finally { + if (dos != null) dos.close(); + if (fos != null) fos.close(); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/backup/utils/DataStreamFileCodecTest.java b/services/tests/servicestests/src/com/android/server/backup/utils/DataStreamFileCodecTest.java new file mode 100644 index 000000000000..bfb95c1a3eda --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/backup/utils/DataStreamFileCodecTest.java @@ -0,0 +1,86 @@ +/* + * 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.utils; + +import static com.google.common.truth.Truth.assertThat; + +import android.platform.test.annotations.Presubmit; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; + +@SmallTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public final class DataStreamFileCodecTest { + @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + + @Test + public void serialize_writesToTheFile() throws Exception { + File unicornFile = mTemporaryFolder.newFile(); + + DataStreamFileCodec<MythicalCreature> mythicalCreatureCodec = new DataStreamFileCodec<>( + unicornFile, new MythicalCreatureDataStreamCodec()); + MythicalCreature unicorn = new MythicalCreature( + 10000, "Unicorn"); + mythicalCreatureCodec.serialize(unicorn); + + DataStreamFileCodec<MythicalCreature> newCodecWithSameFile = new DataStreamFileCodec<>( + unicornFile, new MythicalCreatureDataStreamCodec()); + MythicalCreature deserializedUnicorn = newCodecWithSameFile.deserialize(); + + assertThat(deserializedUnicorn.averageLifespanInYears) + .isEqualTo(unicorn.averageLifespanInYears); + assertThat(deserializedUnicorn.name).isEqualTo(unicorn.name); + } + + private static class MythicalCreature { + int averageLifespanInYears; + String name; + + MythicalCreature(int averageLifespanInYears, String name) { + this.averageLifespanInYears = averageLifespanInYears; + this.name = name; + } + } + + private static class MythicalCreatureDataStreamCodec implements + DataStreamCodec<MythicalCreature> { + @Override + public void serialize(MythicalCreature mythicalCreature, + DataOutputStream dataOutputStream) throws IOException { + dataOutputStream.writeInt(mythicalCreature.averageLifespanInYears); + dataOutputStream.writeUTF(mythicalCreature.name); + } + + @Override + public MythicalCreature deserialize(DataInputStream dataInputStream) + throws IOException { + int years = dataInputStream.readInt(); + String name = dataInputStream.readUTF(); + return new MythicalCreature(years, name); + } + } +} |