chromium/chrome/browser/password_manager/android/test_support/java/src/org/chromium/chrome/browser/password_manager/FakePasswordStoreAndroidBackend.java

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.password_manager;

import android.accounts.Account;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.Callback;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.SequencedTaskRunner;
import org.chromium.base.task.TaskTraits;
import org.chromium.components.password_manager.core.browser.proto.ListAffiliatedPasswordsResult;
import org.chromium.components.password_manager.core.browser.proto.ListAffiliatedPasswordsResult.AffiliatedPassword;
import org.chromium.components.password_manager.core.browser.proto.ListPasswordsResult;
import org.chromium.components.password_manager.core.browser.proto.ListPasswordsWithUiInfoResult;
import org.chromium.components.password_manager.core.browser.proto.ListPasswordsWithUiInfoResult.PasswordWithUiInfo;
import org.chromium.components.password_manager.core.browser.proto.PasswordWithLocalData;
import org.chromium.components.sync.protocol.PasswordSpecificsData;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;

/** Fake {@link PasswordStoreAndroidBackend} to be used in integration tests. */
public class FakePasswordStoreAndroidBackend implements PasswordStoreAndroidBackend {
    private final Map<Account, List<PasswordWithLocalData>> mSavedPasswords = new HashMap<>();
    private SequencedTaskRunner mTaskRunner =
            PostTask.createSequencedTaskRunner(TaskTraits.UI_USER_BLOCKING);

    public static final Account sLocalDefaultAccount = new Account("Test user", "Local");

    public FakePasswordStoreAndroidBackend() {
        mSavedPasswords.put(sLocalDefaultAccount, new LinkedList<>());
    }

    public void setSyncingAccount(Account syncingAccount) {
        mSavedPasswords.put(syncingAccount, new LinkedList<>());
    }

    @Override
    public void getAllLogins(
            Optional<Account> syncingAccount,
            Callback<byte[]> loginsReply,
            Callback<Exception> failureCallback) {
        mTaskRunner.postTask(
                () -> {
                    Account account = getAccountOrFail(syncingAccount, failureCallback);
                    if (account == null) return;
                    ListPasswordsResult allLogins =
                            ListPasswordsResult.newBuilder()
                                    .addAllPasswordData(mSavedPasswords.get(account))
                                    .build();
                    loginsReply.onResult(allLogins.toByteArray());
                });
    }

    @Override
    public void getAllLoginsBetween(
            Date createdAfter,
            Date createdBefore,
            Optional<Account> syncingAccount,
            Callback<byte[]> loginsReply,
            Callback<Exception> failureCallback) {
        mTaskRunner.postTask(
                () -> {
                    Account account = getAccountOrFail(syncingAccount, failureCallback);
                    if (account == null) return;
                    ListPasswordsResult allLogins =
                            ListPasswordsResult.newBuilder()
                                    .addAllPasswordData(
                                            filterPasswords(
                                                    mSavedPasswords.get(account),
                                                    pwd ->
                                                            hasDateBetween(
                                                                    pwd,
                                                                    createdAfter,
                                                                    createdBefore)))
                                    .build();
                    loginsReply.onResult(allLogins.toByteArray());
                });
    }

    @Override
    public void getAutofillableLogins(
            Optional<Account> syncingAccount,
            Callback<byte[]> loginsReply,
            Callback<Exception> failureCallback) {
        mTaskRunner.postTask(
                () -> {
                    Account account = getAccountOrFail(syncingAccount, failureCallback);
                    if (account == null) return;
                    ListPasswordsResult allLogins =
                            ListPasswordsResult.newBuilder()
                                    .addAllPasswordData(
                                            filterPasswords(
                                                    mSavedPasswords.get(account),
                                                    pwd ->
                                                            !pwd.getPasswordSpecificsData()
                                                                    .getBlacklisted()))
                                    .build();
                    loginsReply.onResult(allLogins.toByteArray());
                });
    }

    @Override
    public void getLoginsForSignonRealm(
            String signonRealm,
            Optional<Account> syncingAccount,
            Callback<byte[]> loginsReply,
            Callback<Exception> failureCallback) {
        mTaskRunner.postTask(
                () -> {
                    Account account = getAccountOrFail(syncingAccount, failureCallback);
                    if (account == null) return;
                    ListPasswordsResult allLogins =
                            ListPasswordsResult.newBuilder()
                                    .addAllPasswordData(
                                            filterPasswords(
                                                    mSavedPasswords.get(account),
                                                    pwd -> hasSignonRealm(pwd, signonRealm)))
                                    .build();
                    loginsReply.onResult(allLogins.toByteArray());
                });
    }

    @Override
    public void getAffiliatedLoginsForSignonRealm(
            String signonRealm,
            Optional<Account> syncingAccount,
            Callback<byte[]> loginsReply,
            Callback<Exception> failureCallback) {
        mTaskRunner.postTask(
                () -> {
                    Account account = getAccountOrFail(syncingAccount, failureCallback);
                    if (account == null) return;
                    List<PasswordWithLocalData> filteredPasswords =
                            filterPasswords(
                                    mSavedPasswords.get(account),
                                    pwd -> hasSignonRealm(pwd, signonRealm));
                    List<AffiliatedPassword> affiliatedPasswords = new ArrayList<>();
                    for (PasswordWithLocalData password : filteredPasswords) {
                        affiliatedPasswords.add(
                                AffiliatedPassword.newBuilder().setPasswordData(password).build());
                    }

                    ListAffiliatedPasswordsResult allAffiliatedLogins =
                            ListAffiliatedPasswordsResult.newBuilder()
                                    .addAllAffiliatedPasswords(affiliatedPasswords)
                                    .build();
                    loginsReply.onResult(allAffiliatedLogins.toByteArray());
                });
    }

    @Override
    public void addLogin(
            byte[] pwdWithLocalData,
            Optional<Account> syncingAccount,
            Runnable successCallback,
            Callback<Exception> failureCallback) {
        // In the production both addLogin and updateLogin act the same: they add if it's a new
        // credential and update if the credential with the same key already exists in the database.
        updateLogin(pwdWithLocalData, syncingAccount, successCallback, failureCallback);
    }

    @Override
    public void updateLogin(
            byte[] pwdWithLocalData,
            Optional<Account> syncingAccount,
            Runnable successCallback,
            Callback<Exception> failureCallback) {
        mTaskRunner.postTask(
                () -> {
                    PasswordWithLocalData parsedPassword =
                            parsePwdWithLocalDataOrFail(pwdWithLocalData, failureCallback);
                    if (parsedPassword == null) return;
                    Account account = getAccountOrFail(syncingAccount, failureCallback);
                    if (account == null) return;
                    assert parsedPassword.getPasswordSpecificsData().hasSignonRealm();
                    List<PasswordWithLocalData> accountPasswords = mSavedPasswords.get(account);
                    List<PasswordWithLocalData> loginsWithSameUsrAndOrigin =
                            filterPasswords(
                                    accountPasswords, pwd -> hasSameUniqueKey(pwd, parsedPassword));
                    accountPasswords.removeAll(loginsWithSameUsrAndOrigin);
                    assert !containsPasswordWithSameUniqueKey(
                            mSavedPasswords.get(account), parsedPassword);
                    accountPasswords.add(parsedPassword);
                    successCallback.run();
                });
    }

    @Override
    public void removeLogin(
            byte[] pwdSpecificsData,
            Optional<Account> syncingAccount,
            Runnable successCallback,
            Callback<Exception> failureCallback) {
        mTaskRunner.postTask(
                () -> {
                    PasswordSpecificsData parsedPassword =
                            parsePwdSpecificDataOrFail(pwdSpecificsData, failureCallback);
                    if (parsedPassword == null) return;
                    Account account = getAccountOrFail(syncingAccount, failureCallback);
                    if (account == null) return;
                    List<PasswordWithLocalData> pwdsToRemove =
                            filterPasswords(
                                    mSavedPasswords.get(account),
                                    p ->
                                            hasSameUniqueKey(
                                                    parsedPassword, p.getPasswordSpecificsData()));
                    mSavedPasswords.get(account).removeAll(pwdsToRemove);
                    successCallback.run();
                });
    }

    @Override
    public void getAllLoginsWithBrandingInfo(
            Optional<Account> syncingAccount,
            Callback<byte[]> loginsReply,
            Callback<Exception> failureCallback) {
        mTaskRunner.postTask(
                () -> {
                    Account account = getAccountOrFail(syncingAccount, failureCallback);
                    if (account == null) return;

                    List<PasswordWithUiInfo> passwordsWithUiInfo = new ArrayList<>();
                    for (PasswordWithLocalData passwordLocalData : mSavedPasswords.get(account)) {
                        PasswordWithUiInfo passwordWithUiInfo =
                                PasswordWithUiInfo.newBuilder()
                                        .setPasswordData(passwordLocalData)
                                        .build();
                        passwordsWithUiInfo.add(passwordWithUiInfo);
                    }
                    ListPasswordsWithUiInfoResult.Builder allLogins =
                            ListPasswordsWithUiInfoResult.newBuilder()
                                    .addAllPasswordsWithUiInfo(passwordsWithUiInfo);
                    loginsReply.onResult(allLogins.build().toByteArray());
                });
    }

    @VisibleForTesting
    public Map<Account, List<PasswordWithLocalData>> getAllSavedPasswords() {
        return mSavedPasswords;
    }

    private static List<PasswordWithLocalData> filterPasswords(
            List<PasswordWithLocalData> list, Predicate<PasswordWithLocalData> predicate) {
        List<PasswordWithLocalData> filteredList = new ArrayList<>();
        for (PasswordWithLocalData pwd : list) {
            if (predicate.test(pwd)) filteredList.add(pwd);
        }
        return filteredList;
    }

    private Account getAccountOrFail(
            Optional<Account> syncingAccount, Callback<Exception> failureCallback) {
        Account account = syncingAccount.isPresent() ? syncingAccount.get() : sLocalDefaultAccount;
        if (!mSavedPasswords.containsKey(account)) {
            failureCallback.onResult(
                    new BackendException(
                            "Account " + account + " not found.",
                            AndroidBackendErrorType.NO_ACCOUNT));
            return null;
        }
        return account;
    }

    private static @Nullable PasswordWithLocalData parsePwdWithLocalDataOrFail(
            byte[] pwdWithLocalData, Callback<Exception> failureCallback) {
        try {
            return PasswordWithLocalData.parseFrom(pwdWithLocalData);
        } catch (Exception parsingError) {
            failureCallback.onResult(parsingError);
            return null;
        }
    }

    private static @Nullable PasswordSpecificsData parsePwdSpecificDataOrFail(
            byte[] pwdWithLocalData, Callback<Exception> failureCallback) {
        try {
            return PasswordSpecificsData.parseFrom(pwdWithLocalData);
        } catch (Exception parsingError) {
            failureCallback.onResult(parsingError);
            return null;
        }
    }

    private static boolean hasDateBetween(
            PasswordWithLocalData pwd, Date createdAfter, Date createdBefore) {
        return pwd.getPasswordSpecificsData().hasDateCreated()
                && pwd.getPasswordSpecificsData().getDateCreated() >= createdAfter.getTime()
                && pwd.getPasswordSpecificsData().getDateCreated() <= createdBefore.getTime();
    }

    private static boolean hasSignonRealm(PasswordWithLocalData pwd, String signonRealm) {
        return pwd.getPasswordSpecificsData().getSignonRealm().contains(signonRealm);
    }

    private static boolean hasSameUniqueKey(
            PasswordWithLocalData pwd, PasswordWithLocalData parsedPassword) {
        PasswordSpecificsData data1 = pwd.getPasswordSpecificsData();
        PasswordSpecificsData data2 = parsedPassword.getPasswordSpecificsData();
        return hasSameUniqueKey(data1, data2);
    }

    private static boolean hasSameUniqueKey(
            PasswordSpecificsData data1, PasswordSpecificsData data2) {
        return data1.getUsernameElement().equals(data2.getUsernameElement())
                && data1.getUsernameValue().equals(data2.getUsernameValue())
                && data1.getOrigin().equals(data2.getOrigin())
                && data1.getSignonRealm().equals(data2.getSignonRealm())
                && data1.getPasswordElement().equals(data2.getPasswordElement());
    }

    private static boolean containsPasswordWithSameUniqueKey(
            List<PasswordWithLocalData> list, PasswordWithLocalData pwd) {
        for (PasswordWithLocalData p : list) {
            if (hasSameUniqueKey(p, pwd)) return true;
        }
        return false;
    }
}