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 17package com.android.server.backup; 18 19import static com.android.server.testutis.TestUtils.assertExpectException; 20 21import static com.google.common.truth.Truth.assertThat; 22 23import static org.mockito.ArgumentMatchers.anyString; 24import static org.mockito.Mockito.doThrow; 25 26import android.content.Context; 27import android.platform.test.annotations.Presubmit; 28import android.support.test.filters.SmallTest; 29import android.support.test.runner.AndroidJUnit4; 30 31import com.android.server.backup.utils.PasswordUtils; 32 33import org.junit.Before; 34import org.junit.Rule; 35import org.junit.Test; 36import org.junit.rules.TemporaryFolder; 37import org.junit.runner.RunWith; 38import org.mockito.Mock; 39import org.mockito.MockitoAnnotations; 40 41import java.io.DataOutputStream; 42import java.io.File; 43import java.io.FileOutputStream; 44import java.security.SecureRandom; 45 46@SmallTest 47@Presubmit 48@RunWith(AndroidJUnit4.class) 49public class BackupPasswordManagerTest { 50 private static final String PASSWORD_VERSION_FILE_NAME = "pwversion"; 51 private static final String PASSWORD_HASH_FILE_NAME = "pwhash"; 52 private static final String V1_HASH_ALGORITHM = "PBKDF2WithHmacSHA1And8bit"; 53 54 @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); 55 56 @Mock private Context mContext; 57 58 private File mStateFolder; 59 private BackupPasswordManager mPasswordManager; 60 61 @Before 62 public void setUp() throws Exception { 63 MockitoAnnotations.initMocks(this); 64 mStateFolder = mTemporaryFolder.newFolder(); 65 mPasswordManager = new BackupPasswordManager(mContext, mStateFolder, new SecureRandom()); 66 } 67 68 @Test 69 public void hasBackupPassword_isFalseIfFileDoesNotExist() { 70 assertThat(mPasswordManager.hasBackupPassword()).isFalse(); 71 } 72 73 @Test 74 public void hasBackupPassword_isTrueIfFileExists() throws Exception { 75 mPasswordManager.setBackupPassword(null, "password1234"); 76 assertThat(mPasswordManager.hasBackupPassword()).isTrue(); 77 } 78 79 @Test 80 public void hasBackupPassword_throwsSecurityExceptionIfLacksPermission() { 81 setDoesNotHavePermission(); 82 83 assertExpectException( 84 SecurityException.class, 85 /* expectedExceptionMessageRegex */ null, 86 () -> mPasswordManager.hasBackupPassword()); 87 } 88 89 @Test 90 public void backupPasswordMatches_isTrueIfNoPassword() { 91 assertThat(mPasswordManager.backupPasswordMatches("anything")).isTrue(); 92 } 93 94 @Test 95 public void backupPasswordMatches_isTrueForSamePassword() { 96 String password = "password1234"; 97 mPasswordManager.setBackupPassword(null, password); 98 assertThat(mPasswordManager.backupPasswordMatches(password)).isTrue(); 99 } 100 101 @Test 102 public void backupPasswordMatches_isFalseForDifferentPassword() { 103 mPasswordManager.setBackupPassword(null, "shiba"); 104 assertThat(mPasswordManager.backupPasswordMatches("corgi")).isFalse(); 105 } 106 107 @Test 108 public void backupPasswordMatches_worksForV1HashIfVersionIsV1() throws Exception { 109 String password = "corgi\uFFFF"; 110 writePasswordVersionToFile(1); 111 writeV1HashToFile(password, saltFixture()); 112 113 // Reconstruct so it reloads from filesystem 114 mPasswordManager = new BackupPasswordManager(mContext, mStateFolder, new SecureRandom()); 115 116 assertThat(mPasswordManager.backupPasswordMatches(password)).isTrue(); 117 } 118 119 @Test 120 public void backupPasswordMatches_failsForV1HashIfVersionIsV2() throws Exception { 121 // The algorithms produce identical hashes except if the password contains higher-order 122 // unicode. See 123 // https://android-developers.googleblog.com/2013/12/changes-to-secretkeyfactory-api-in.html 124 String password = "corgi\uFFFF"; 125 writePasswordVersionToFile(2); 126 writeV1HashToFile(password, saltFixture()); 127 128 // Reconstruct so it reloads from filesystem 129 mPasswordManager = new BackupPasswordManager(mContext, mStateFolder, new SecureRandom()); 130 131 assertThat(mPasswordManager.backupPasswordMatches(password)).isFalse(); 132 } 133 134 @Test 135 public void backupPasswordMatches_throwsSecurityExceptionIfLacksPermission() { 136 setDoesNotHavePermission(); 137 138 assertExpectException( 139 SecurityException.class, 140 /* expectedExceptionMessageRegex */ null, 141 () -> mPasswordManager.backupPasswordMatches("password123")); 142 } 143 144 @Test 145 public void setBackupPassword_persistsPasswordToFile() { 146 String password = "shiba"; 147 148 mPasswordManager.setBackupPassword(null, password); 149 150 BackupPasswordManager newManager = new BackupPasswordManager( 151 mContext, mStateFolder, new SecureRandom()); 152 assertThat(newManager.backupPasswordMatches(password)).isTrue(); 153 } 154 155 @Test 156 public void setBackupPassword_failsIfCurrentPasswordIsWrong() { 157 String secondPassword = "second password"; 158 mPasswordManager.setBackupPassword(null, "first password"); 159 160 boolean result = mPasswordManager.setBackupPassword( 161 "incorrect pass", secondPassword); 162 163 BackupPasswordManager newManager = new BackupPasswordManager( 164 mContext, mStateFolder, new SecureRandom()); 165 assertThat(result).isFalse(); 166 assertThat(newManager.backupPasswordMatches(secondPassword)).isFalse(); 167 } 168 169 @Test 170 public void setBackupPassword_throwsSecurityExceptionIfLacksPermission() { 171 setDoesNotHavePermission(); 172 173 assertExpectException( 174 SecurityException.class, 175 /* expectedExceptionMessageRegex */ null, 176 () -> mPasswordManager.setBackupPassword( 177 "password123", "password111")); 178 } 179 180 private byte[] saltFixture() { 181 byte[] bytes = new byte[64]; 182 for (int i = 0; i < 64; i++) { 183 bytes[i] = (byte) i; 184 } 185 return bytes; 186 } 187 188 private void setDoesNotHavePermission() { 189 doThrow(new SecurityException()).when(mContext) 190 .enforceCallingOrSelfPermission(anyString(), anyString()); 191 } 192 193 private void writeV1HashToFile(String password, byte[] salt) throws Exception { 194 String hash = PasswordUtils.buildPasswordHash( 195 V1_HASH_ALGORITHM, password, salt, PasswordUtils.PBKDF2_HASH_ROUNDS); 196 writeHashAndSaltToFile(hash, salt); 197 } 198 199 private void writeHashAndSaltToFile(String hash, byte[] salt) throws Exception { 200 FileOutputStream fos = null; 201 DataOutputStream dos = null; 202 203 try { 204 File passwordHash = new File(mStateFolder, PASSWORD_HASH_FILE_NAME); 205 fos = new FileOutputStream(passwordHash); 206 dos = new DataOutputStream(fos); 207 dos.writeInt(salt.length); 208 dos.write(salt); 209 dos.writeUTF(hash); 210 dos.flush(); 211 } finally { 212 if (dos != null) dos.close(); 213 if (fos != null) fos.close(); 214 } 215 } 216 217 private void writePasswordVersionToFile(int version) throws Exception { 218 FileOutputStream fos = null; 219 DataOutputStream dos = null; 220 221 try { 222 File passwordVersion = new File(mStateFolder, PASSWORD_VERSION_FILE_NAME); 223 fos = new FileOutputStream(passwordVersion); 224 dos = new DataOutputStream(fos); 225 dos.writeInt(version); 226 dos.flush(); 227 } finally { 228 if (dos != null) dos.close(); 229 if (fos != null) fos.close(); 230 } 231 } 232} 233