/*
 * 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.fullbackup;

import static com.android.server.backup.BackupPasswordManager.PBKDF_CURRENT;
import static com.android.server.backup.BackupManagerService.BACKUP_FILE_HEADER_MAGIC;
import static com.android.server.backup.BackupManagerService.BACKUP_FILE_VERSION;
import static com.android.server.backup.BackupManagerService.DEBUG;
import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
import static com.android.server.backup.BackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
import static com.android.server.backup.BackupManagerService.TAG;

import android.app.backup.IFullBackupRestoreObserver;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Slog;

import com.android.server.AppWidgetBackupBridge;
import com.android.server.backup.BackupRestoreTask;
import com.android.server.backup.KeyValueAdbBackupEngine;
import com.android.server.backup.BackupManagerService;
import com.android.server.backup.utils.AppBackupUtils;
import com.android.server.backup.utils.PasswordUtils;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;

import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

/**
 * Full backup task variant used for adb backup.
 */
public class PerformAdbBackupTask extends FullBackupTask implements BackupRestoreTask {

    private BackupManagerService backupManagerService;
    FullBackupEngine mBackupEngine;
    final AtomicBoolean mLatch;

    ParcelFileDescriptor mOutputFile;
    DeflaterOutputStream mDeflater;
    boolean mIncludeApks;
    boolean mIncludeObbs;
    boolean mIncludeShared;
    boolean mDoWidgets;
    boolean mAllApps;
    boolean mIncludeSystem;
    boolean mCompress;
    boolean mKeyValue;
    ArrayList<String> mPackages;
    PackageInfo mCurrentTarget;
    String mCurrentPassword;
    String mEncryptPassword;
    private final int mCurrentOpToken;

    public PerformAdbBackupTask(BackupManagerService backupManagerService,
            ParcelFileDescriptor fd, IFullBackupRestoreObserver observer,
            boolean includeApks, boolean includeObbs, boolean includeShared, boolean doWidgets,
            String curPassword, String encryptPassword, boolean doAllApps, boolean doSystem,
            boolean doCompress, boolean doKeyValue, String[] packages, AtomicBoolean latch) {
        super(observer);
        this.backupManagerService = backupManagerService;
        mCurrentOpToken = backupManagerService.generateRandomIntegerToken();
        mLatch = latch;

        mOutputFile = fd;
        mIncludeApks = includeApks;
        mIncludeObbs = includeObbs;
        mIncludeShared = includeShared;
        mDoWidgets = doWidgets;
        mAllApps = doAllApps;
        mIncludeSystem = doSystem;
        mPackages = (packages == null)
                ? new ArrayList<String>()
                : new ArrayList<>(Arrays.asList(packages));
        mCurrentPassword = curPassword;
        // when backing up, if there is a current backup password, we require that
        // the user use a nonempty encryption password as well.  if one is supplied
        // in the UI we use that, but if the UI was left empty we fall back to the
        // current backup password (which was supplied by the user as well).
        if (encryptPassword == null || "".equals(encryptPassword)) {
            mEncryptPassword = curPassword;
        } else {
            mEncryptPassword = encryptPassword;
        }
        if (MORE_DEBUG) {
            Slog.w(TAG, "Encrypting backup with passphrase=" + mEncryptPassword);
        }
        mCompress = doCompress;
        mKeyValue = doKeyValue;
    }

    void addPackagesToSet(TreeMap<String, PackageInfo> set, List<String> pkgNames) {
        for (String pkgName : pkgNames) {
            if (!set.containsKey(pkgName)) {
                try {
                    PackageInfo info = backupManagerService.getPackageManager().getPackageInfo(
                            pkgName,
                            PackageManager.GET_SIGNATURES);
                    set.put(pkgName, info);
                } catch (NameNotFoundException e) {
                    Slog.w(TAG, "Unknown package " + pkgName + ", skipping");
                }
            }
        }
    }

    private OutputStream emitAesBackupHeader(StringBuilder headerbuf,
            OutputStream ofstream) throws Exception {
        // User key will be used to encrypt the master key.
        byte[] newUserSalt = backupManagerService
                .randomBytes(PasswordUtils.PBKDF2_SALT_SIZE);
        SecretKey userKey = PasswordUtils
                .buildPasswordKey(PBKDF_CURRENT, mEncryptPassword,
                        newUserSalt,
                        PasswordUtils.PBKDF2_HASH_ROUNDS);

        // the master key is random for each backup
        byte[] masterPw = new byte[256 / 8];
        backupManagerService.getRng().nextBytes(masterPw);
        byte[] checksumSalt = backupManagerService
                .randomBytes(PasswordUtils.PBKDF2_SALT_SIZE);

        // primary encryption of the datastream with the random key
        Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKeySpec masterKeySpec = new SecretKeySpec(masterPw, "AES");
        c.init(Cipher.ENCRYPT_MODE, masterKeySpec);
        OutputStream finalOutput = new CipherOutputStream(ofstream, c);

        // line 4: name of encryption algorithm
        headerbuf.append(PasswordUtils.ENCRYPTION_ALGORITHM_NAME);
        headerbuf.append('\n');
        // line 5: user password salt [hex]
        headerbuf.append(PasswordUtils.byteArrayToHex(newUserSalt));
        headerbuf.append('\n');
        // line 6: master key checksum salt [hex]
        headerbuf.append(PasswordUtils.byteArrayToHex(checksumSalt));
        headerbuf.append('\n');
        // line 7: number of PBKDF2 rounds used [decimal]
        headerbuf.append(PasswordUtils.PBKDF2_HASH_ROUNDS);
        headerbuf.append('\n');

        // line 8: IV of the user key [hex]
        Cipher mkC = Cipher.getInstance("AES/CBC/PKCS5Padding");
        mkC.init(Cipher.ENCRYPT_MODE, userKey);

        byte[] IV = mkC.getIV();
        headerbuf.append(PasswordUtils.byteArrayToHex(IV));
        headerbuf.append('\n');

        // line 9: master IV + key blob, encrypted by the user key [hex].  Blob format:
        //    [byte] IV length = Niv
        //    [array of Niv bytes] IV itself
        //    [byte] master key length = Nmk
        //    [array of Nmk bytes] master key itself
        //    [byte] MK checksum hash length = Nck
        //    [array of Nck bytes] master key checksum hash
        //
        // The checksum is the (master key + checksum salt), run through the
        // stated number of PBKDF2 rounds
        IV = c.getIV();
        byte[] mk = masterKeySpec.getEncoded();
        byte[] checksum = PasswordUtils
                .makeKeyChecksum(PBKDF_CURRENT,
                        masterKeySpec.getEncoded(),
                        checksumSalt, PasswordUtils.PBKDF2_HASH_ROUNDS);

        ByteArrayOutputStream blob = new ByteArrayOutputStream(IV.length + mk.length
                + checksum.length + 3);
        DataOutputStream mkOut = new DataOutputStream(blob);
        mkOut.writeByte(IV.length);
        mkOut.write(IV);
        mkOut.writeByte(mk.length);
        mkOut.write(mk);
        mkOut.writeByte(checksum.length);
        mkOut.write(checksum);
        mkOut.flush();
        byte[] encryptedMk = mkC.doFinal(blob.toByteArray());
        headerbuf.append(PasswordUtils.byteArrayToHex(encryptedMk));
        headerbuf.append('\n');

        return finalOutput;
    }

    private void finalizeBackup(OutputStream out) {
        try {
            // A standard 'tar' EOF sequence: two 512-byte blocks of all zeroes.
            byte[] eof = new byte[512 * 2]; // newly allocated == zero filled
            out.write(eof);
        } catch (IOException e) {
            Slog.w(TAG, "Error attempting to finalize backup stream");
        }
    }

    @Override
    public void run() {
        String includeKeyValue = mKeyValue ? ", including key-value backups" : "";
        Slog.i(TAG, "--- Performing adb backup" + includeKeyValue + " ---");

        TreeMap<String, PackageInfo> packagesToBackup = new TreeMap<>();
        FullBackupObbConnection obbConnection = new FullBackupObbConnection(
                backupManagerService);
        obbConnection.establish();  // we'll want this later

        sendStartBackup();
        PackageManager pm = backupManagerService.getPackageManager();

        // doAllApps supersedes the package set if any
        if (mAllApps) {
            List<PackageInfo> allPackages = pm.getInstalledPackages(PackageManager.GET_SIGNATURES);
            for (int i = 0; i < allPackages.size(); i++) {
                PackageInfo pkg = allPackages.get(i);
                // Exclude system apps if we've been asked to do so
                if (mIncludeSystem == true
                        || ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0)) {
                    packagesToBackup.put(pkg.packageName, pkg);
                }
            }
        }

        // If we're doing widget state as well, ensure that we have all the involved
        // host & provider packages in the set
        if (mDoWidgets) {
            // TODO: http://b/22388012
            List<String> pkgs =
                    AppWidgetBackupBridge.getWidgetParticipants(UserHandle.USER_SYSTEM);
            if (pkgs != null) {
                if (MORE_DEBUG) {
                    Slog.i(TAG, "Adding widget participants to backup set:");
                    StringBuilder sb = new StringBuilder(128);
                    sb.append("   ");
                    for (String s : pkgs) {
                        sb.append(' ');
                        sb.append(s);
                    }
                    Slog.i(TAG, sb.toString());
                }
                addPackagesToSet(packagesToBackup, pkgs);
            }
        }

        // Now process the command line argument packages, if any. Note that explicitly-
        // named system-partition packages will be included even if includeSystem was
        // set to false.
        if (mPackages != null) {
            addPackagesToSet(packagesToBackup, mPackages);
        }

        // Now we cull any inapplicable / inappropriate packages from the set.  This
        // includes the special shared-storage agent package; we handle that one
        // explicitly at the end of the backup pass. Packages supporting key-value backup are
        // added to their own queue, and handled after packages supporting fullbackup.
        ArrayList<PackageInfo> keyValueBackupQueue = new ArrayList<>();
        Iterator<Entry<String, PackageInfo>> iter = packagesToBackup.entrySet().iterator();
        while (iter.hasNext()) {
            PackageInfo pkg = iter.next().getValue();
            if (!AppBackupUtils.appIsEligibleForBackup(pkg.applicationInfo, pm)
                    || AppBackupUtils.appIsStopped(pkg.applicationInfo)) {
                iter.remove();
                if (DEBUG) {
                    Slog.i(TAG, "Package " + pkg.packageName
                            + " is not eligible for backup, removing.");
                }
            } else if (AppBackupUtils.appIsKeyValueOnly(pkg)) {
                iter.remove();
                if (DEBUG) {
                    Slog.i(TAG, "Package " + pkg.packageName
                            + " is key-value.");
                }
                keyValueBackupQueue.add(pkg);
            }
        }

        // flatten the set of packages now so we can explicitly control the ordering
        ArrayList<PackageInfo> backupQueue =
                new ArrayList<>(packagesToBackup.values());
        FileOutputStream ofstream = new FileOutputStream(mOutputFile.getFileDescriptor());
        OutputStream out = null;

        PackageInfo pkg = null;
        try {
            boolean encrypting = (mEncryptPassword != null && mEncryptPassword.length() > 0);

            // Only allow encrypted backups of encrypted devices
            if (backupManagerService.deviceIsEncrypted() && !encrypting) {
                Slog.e(TAG, "Unencrypted backup of encrypted device; aborting");
                return;
            }

            OutputStream finalOutput = ofstream;

            // Verify that the given password matches the currently-active
            // backup password, if any
            if (!backupManagerService.backupPasswordMatches(mCurrentPassword)) {
                if (DEBUG) {
                    Slog.w(TAG, "Backup password mismatch; aborting");
                }
                return;
            }

            // Write the global file header.  All strings are UTF-8 encoded; lines end
            // with a '\n' byte.  Actual backup data begins immediately following the
            // final '\n'.
            //
            // line 1: "ANDROID BACKUP"
            // line 2: backup file format version, currently "5"
            // line 3: compressed?  "0" if not compressed, "1" if compressed.
            // line 4: name of encryption algorithm [currently only "none" or "AES-256"]
            //
            // When line 4 is not "none", then additional header data follows:
            //
            // line 5: user password salt [hex]
            // line 6: master key checksum salt [hex]
            // line 7: number of PBKDF2 rounds to use (same for user & master) [decimal]
            // line 8: IV of the user key [hex]
            // line 9: master key blob [hex]
            //     IV of the master key, master key itself, master key checksum hash
            //
            // The master key checksum is the master key plus its checksum salt, run through
            // 10k rounds of PBKDF2.  This is used to verify that the user has supplied the
            // correct password for decrypting the archive:  the master key decrypted from
            // the archive using the user-supplied password is also run through PBKDF2 in
            // this way, and if the result does not match the checksum as stored in the
            // archive, then we know that the user-supplied password does not match the
            // archive's.
            StringBuilder headerbuf = new StringBuilder(1024);

            headerbuf.append(BACKUP_FILE_HEADER_MAGIC);
            headerbuf.append(BACKUP_FILE_VERSION); // integer, no trailing \n
            headerbuf.append(mCompress ? "\n1\n" : "\n0\n");

            try {
                // Set up the encryption stage if appropriate, and emit the correct header
                if (encrypting) {
                    finalOutput = emitAesBackupHeader(headerbuf, finalOutput);
                } else {
                    headerbuf.append("none\n");
                }

                byte[] header = headerbuf.toString().getBytes("UTF-8");
                ofstream.write(header);

                // Set up the compression stage feeding into the encryption stage (if any)
                if (mCompress) {
                    Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
                    finalOutput = new DeflaterOutputStream(finalOutput, deflater, true);
                }

                out = finalOutput;
            } catch (Exception e) {
                // Should never happen!
                Slog.e(TAG, "Unable to emit archive header", e);
                return;
            }

            // Shared storage if requested
            if (mIncludeShared) {
                try {
                    pkg = backupManagerService.getPackageManager().getPackageInfo(
                            SHARED_BACKUP_AGENT_PACKAGE, 0);
                    backupQueue.add(pkg);
                } catch (NameNotFoundException e) {
                    Slog.e(TAG, "Unable to find shared-storage backup handler");
                }
            }

            // Now actually run the constructed backup sequence for full backup
            int N = backupQueue.size();
            for (int i = 0; i < N; i++) {
                pkg = backupQueue.get(i);
                if (DEBUG) {
                    Slog.i(TAG, "--- Performing full backup for package " + pkg.packageName
                            + " ---");
                }
                final boolean isSharedStorage =
                        pkg.packageName.equals(
                                SHARED_BACKUP_AGENT_PACKAGE);

                mBackupEngine = new FullBackupEngine(backupManagerService, out,
                        null, pkg, mIncludeApks, this, Long.MAX_VALUE,
                        mCurrentOpToken, /*transportFlags=*/ 0);
                sendOnBackupPackage(isSharedStorage ? "Shared storage" : pkg.packageName);

                // Don't need to check preflight result as there is no preflight hook.
                mCurrentTarget = pkg;
                mBackupEngine.backupOnePackage();

                // after the app's agent runs to handle its private filesystem
                // contents, back up any OBB content it has on its behalf.
                if (mIncludeObbs) {
                    boolean obbOkay = obbConnection.backupObbs(pkg, out);
                    if (!obbOkay) {
                        throw new RuntimeException("Failure writing OBB stack for " + pkg);
                    }
                }
            }
            // And for key-value backup if enabled
            if (mKeyValue) {
                for (PackageInfo keyValuePackage : keyValueBackupQueue) {
                    if (DEBUG) {
                        Slog.i(TAG, "--- Performing key-value backup for package "
                                + keyValuePackage.packageName + " ---");
                    }
                    KeyValueAdbBackupEngine kvBackupEngine =
                            new KeyValueAdbBackupEngine(out, keyValuePackage,
                                    backupManagerService,
                                    backupManagerService.getPackageManager(),
                                    backupManagerService.getBaseStateDir(),
                                    backupManagerService.getDataDir());
                    sendOnBackupPackage(keyValuePackage.packageName);
                    kvBackupEngine.backupOnePackage();
                }
            }

            // Done!
            finalizeBackup(out);
        } catch (RemoteException e) {
            Slog.e(TAG, "App died during full backup");
        } catch (Exception e) {
            Slog.e(TAG, "Internal exception during full backup", e);
        } finally {
            try {
                if (out != null) {
                    out.flush();
                    out.close();
                }
                mOutputFile.close();
            } catch (IOException e) {
                Slog.e(TAG, "IO error closing adb backup file: " + e.getMessage());
            }
            synchronized (mLatch) {
                mLatch.set(true);
                mLatch.notifyAll();
            }
            sendEndBackup();
            obbConnection.tearDown();
            if (DEBUG) {
                Slog.d(TAG, "Full backup pass complete.");
            }
            backupManagerService.getWakelock().release();
        }
    }

    // BackupRestoreTask methods, used for timeout handling
    @Override
    public void execute() {
        // Unused
    }

    @Override
    public void operationComplete(long result) {
        // Unused
    }

    @Override
    public void handleCancel(boolean cancelAll) {
        final PackageInfo target = mCurrentTarget;
        if (DEBUG) {
            Slog.w(TAG, "adb backup cancel of " + target);
        }
        if (target != null) {
            backupManagerService.tearDownAgentAndKill(mCurrentTarget.applicationInfo);
        }
        backupManagerService.removeOperation(mCurrentOpToken);
    }
}
