blob: 01b40fbff201c168f47f4d8fa095b52326ba399d [file] [log] [blame]
/*
* 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.restore;
import static com.android.server.backup.BackupManagerService.DEBUG;
import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
import static com.android.server.backup.BackupManagerService.TAG;
import static com.android.server.backup.BackupPasswordManager.PBKDF_CURRENT;
import static com.android.server.backup.BackupPasswordManager.PBKDF_FALLBACK;
import static com.android.server.backup.UserBackupManagerService.BACKUP_FILE_HEADER_MAGIC;
import static com.android.server.backup.UserBackupManagerService.BACKUP_FILE_VERSION;
import android.app.backup.IFullBackupRestoreObserver;
import android.os.ParcelFileDescriptor;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.backup.UserBackupManagerService;
import com.android.server.backup.fullbackup.FullBackupObbConnection;
import com.android.server.backup.utils.FullBackupRestoreObserverUtils;
import com.android.server.backup.utils.PasswordUtils;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.InflaterInputStream;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class PerformAdbRestoreTask implements Runnable {
private final UserBackupManagerService mBackupManagerService;
private final ParcelFileDescriptor mInputFile;
private final String mCurrentPassword;
private final String mDecryptPassword;
private final AtomicBoolean mLatchObject;
private final FullBackupObbConnection mObbConnection;
private IFullBackupRestoreObserver mObserver;
public PerformAdbRestoreTask(UserBackupManagerService backupManagerService,
ParcelFileDescriptor fd, String curPassword, String decryptPassword,
IFullBackupRestoreObserver observer, AtomicBoolean latch) {
this.mBackupManagerService = backupManagerService;
mInputFile = fd;
mCurrentPassword = curPassword;
mDecryptPassword = decryptPassword;
mObserver = observer;
mLatchObject = latch;
mObbConnection = new FullBackupObbConnection(backupManagerService);
}
@Override
public void run() {
Slog.i(TAG, "--- Performing full-dataset restore ---");
mObbConnection.establish();
mObserver = FullBackupRestoreObserverUtils.sendStartRestore(mObserver);
FileInputStream rawInStream = null;
try {
if (!mBackupManagerService.backupPasswordMatches(mCurrentPassword)) {
if (DEBUG) {
Slog.w(TAG, "Backup password mismatch; aborting");
}
return;
}
rawInStream = new FileInputStream(mInputFile.getFileDescriptor());
InputStream tarInputStream = parseBackupFileHeaderAndReturnTarStream(rawInStream,
mDecryptPassword);
if (tarInputStream == null) {
// There was an error reading the backup file, which is already handled and logged.
// Just abort.
return;
}
FullRestoreEngine mEngine = new FullRestoreEngine(mBackupManagerService, null,
mObserver, null, null, true, 0 /*unused*/, true);
FullRestoreEngineThread mEngineThread = new FullRestoreEngineThread(mEngine,
tarInputStream);
mEngineThread.run();
if (MORE_DEBUG) {
Slog.v(TAG, "Done consuming input tarfile.");
}
} catch (Exception e) {
Slog.e(TAG, "Unable to read restore input");
} finally {
try {
if (rawInStream != null) {
rawInStream.close();
}
mInputFile.close();
} catch (IOException e) {
Slog.w(TAG, "Close of restore data pipe threw", e);
/* nothing we can do about this */
}
synchronized (mLatchObject) {
mLatchObject.set(true);
mLatchObject.notifyAll();
}
mObbConnection.tearDown();
mObserver = FullBackupRestoreObserverUtils.sendEndRestore(mObserver);
Slog.d(TAG, "Full restore pass complete.");
mBackupManagerService.getWakelock().release();
}
}
private static void readFullyOrThrow(InputStream in, byte[] buffer) throws IOException {
int offset = 0;
while (offset < buffer.length) {
int bytesRead = in.read(buffer, offset, buffer.length - offset);
if (bytesRead <= 0) {
throw new IOException("Couldn't fully read data");
}
offset += bytesRead;
}
}
@VisibleForTesting
public static InputStream parseBackupFileHeaderAndReturnTarStream(
InputStream rawInputStream,
String decryptPassword)
throws IOException {
// First, parse out the unencrypted/uncompressed header
boolean compressed = false;
InputStream preCompressStream = rawInputStream;
boolean okay = false;
final int headerLen = BACKUP_FILE_HEADER_MAGIC.length();
byte[] streamHeader = new byte[headerLen];
readFullyOrThrow(rawInputStream, streamHeader);
byte[] magicBytes = BACKUP_FILE_HEADER_MAGIC.getBytes(
"UTF-8");
if (Arrays.equals(magicBytes, streamHeader)) {
// okay, header looks good. now parse out the rest of the fields.
String s = readHeaderLine(rawInputStream);
final int archiveVersion = Integer.parseInt(s);
if (archiveVersion <= BACKUP_FILE_VERSION) {
// okay, it's a version we recognize. if it's version 1, we may need
// to try two different PBKDF2 regimes to compare checksums.
final boolean pbkdf2Fallback = (archiveVersion == 1);
s = readHeaderLine(rawInputStream);
compressed = (Integer.parseInt(s) != 0);
s = readHeaderLine(rawInputStream);
if (s.equals("none")) {
// no more header to parse; we're good to go
okay = true;
} else if (decryptPassword != null && decryptPassword.length() > 0) {
preCompressStream = decodeAesHeaderAndInitialize(
decryptPassword, s, pbkdf2Fallback,
rawInputStream);
if (preCompressStream != null) {
okay = true;
}
} else {
Slog.w(TAG, "Archive is encrypted but no password given");
}
} else {
Slog.w(TAG, "Wrong header version: " + s);
}
} else {
Slog.w(TAG, "Didn't read the right header magic");
}
if (!okay) {
Slog.w(TAG, "Invalid restore data; aborting.");
return null;
}
// okay, use the right stream layer based on compression
return compressed ? new InflaterInputStream(preCompressStream) : preCompressStream;
}
private static String readHeaderLine(InputStream in) throws IOException {
int c;
StringBuilder buffer = new StringBuilder(80);
while ((c = in.read()) >= 0) {
if (c == '\n') {
break; // consume and discard the newlines
}
buffer.append((char) c);
}
return buffer.toString();
}
private static InputStream attemptMasterKeyDecryption(String decryptPassword, String algorithm,
byte[] userSalt, byte[] ckSalt,
int rounds, String userIvHex, String masterKeyBlobHex, InputStream rawInStream,
boolean doLog) {
InputStream result = null;
try {
Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKey userKey = PasswordUtils
.buildPasswordKey(algorithm, decryptPassword, userSalt,
rounds);
byte[] IV = PasswordUtils.hexToByteArray(userIvHex);
IvParameterSpec ivSpec = new IvParameterSpec(IV);
c.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(userKey.getEncoded(), "AES"),
ivSpec);
byte[] mkCipher = PasswordUtils.hexToByteArray(masterKeyBlobHex);
byte[] mkBlob = c.doFinal(mkCipher);
// first, the master key IV
int offset = 0;
int len = mkBlob[offset++];
IV = Arrays.copyOfRange(mkBlob, offset, offset + len);
offset += len;
// then the master key itself
len = mkBlob[offset++];
byte[] mk = Arrays.copyOfRange(mkBlob,
offset, offset + len);
offset += len;
// and finally the master key checksum hash
len = mkBlob[offset++];
byte[] mkChecksum = Arrays.copyOfRange(mkBlob,
offset, offset + len);
// now validate the decrypted master key against the checksum
byte[] calculatedCk = PasswordUtils.makeKeyChecksum(algorithm, mk, ckSalt,
rounds);
if (Arrays.equals(calculatedCk, mkChecksum)) {
ivSpec = new IvParameterSpec(IV);
c.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(mk, "AES"),
ivSpec);
// Only if all of the above worked properly will 'result' be assigned
result = new CipherInputStream(rawInStream, c);
} else if (doLog) {
Slog.w(TAG, "Incorrect password");
}
} catch (InvalidAlgorithmParameterException e) {
if (doLog) {
Slog.e(TAG, "Needed parameter spec unavailable!", e);
}
} catch (BadPaddingException e) {
// This case frequently occurs when the wrong password is used to decrypt
// the master key. Use the identical "incorrect password" log text as is
// used in the checksum failure log in order to avoid providing additional
// information to an attacker.
if (doLog) {
Slog.w(TAG, "Incorrect password");
}
} catch (IllegalBlockSizeException e) {
if (doLog) {
Slog.w(TAG, "Invalid block size in master key");
}
} catch (NoSuchAlgorithmException e) {
if (doLog) {
Slog.e(TAG, "Needed decryption algorithm unavailable!");
}
} catch (NoSuchPaddingException e) {
if (doLog) {
Slog.e(TAG, "Needed padding mechanism unavailable!");
}
} catch (InvalidKeyException e) {
if (doLog) {
Slog.w(TAG, "Illegal password; aborting");
}
}
return result;
}
private static InputStream decodeAesHeaderAndInitialize(String decryptPassword,
String encryptionName,
boolean pbkdf2Fallback,
InputStream rawInStream) {
InputStream result = null;
try {
if (encryptionName.equals(PasswordUtils.ENCRYPTION_ALGORITHM_NAME)) {
String userSaltHex = readHeaderLine(rawInStream); // 5
byte[] userSalt = PasswordUtils.hexToByteArray(userSaltHex);
String ckSaltHex = readHeaderLine(rawInStream); // 6
byte[] ckSalt = PasswordUtils.hexToByteArray(ckSaltHex);
int rounds = Integer.parseInt(readHeaderLine(rawInStream)); // 7
String userIvHex = readHeaderLine(rawInStream); // 8
String masterKeyBlobHex = readHeaderLine(rawInStream); // 9
// decrypt the master key blob
result = attemptMasterKeyDecryption(decryptPassword, PBKDF_CURRENT,
userSalt, ckSalt, rounds, userIvHex, masterKeyBlobHex, rawInStream, false);
if (result == null && pbkdf2Fallback) {
result = attemptMasterKeyDecryption(
decryptPassword, PBKDF_FALLBACK, userSalt, ckSalt,
rounds, userIvHex, masterKeyBlobHex, rawInStream, true);
}
} else {
Slog.w(TAG, "Unsupported encryption method: " + encryptionName);
}
} catch (NumberFormatException e) {
Slog.w(TAG, "Can't parse restore data header");
} catch (IOException e) {
Slog.w(TAG, "Can't read input header");
}
return result;
}
}