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