bitwarden-estensione-browser/apps/browser/src/background/notification.background.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

450 lines
15 KiB
TypeScript
Raw Normal View History

2022-06-14 17:10:53 +02:00
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/abstractions/folder.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy.service";
import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus";
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { PolicyType } from "@bitwarden/common/enums/policyType";
import { Utils } from "@bitwarden/common/misc/utils";
import { CipherView } from "@bitwarden/common/models/view/cipherView";
import { LoginUriView } from "@bitwarden/common/models/view/loginUriView";
import { LoginView } from "@bitwarden/common/models/view/loginView";
import { BrowserApi } from "../browser/browserApi";
2022-02-24 18:14:04 +01:00
import { AutofillService } from "../services/abstractions/autofill.service";
import { StateService } from "../services/abstractions/state.service";
import AddChangePasswordQueueMessage from "./models/addChangePasswordQueueMessage";
import AddLoginQueueMessage from "./models/addLoginQueueMessage";
import AddLoginRuntimeMessage from "./models/addLoginRuntimeMessage";
import ChangePasswordRuntimeMessage from "./models/changePasswordRuntimeMessage";
import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNotificationsItem";
import { NotificationQueueMessageType } from "./models/notificationQueueMessageType";
export default class NotificationBackground {
private notificationQueue: (AddLoginQueueMessage | AddChangePasswordQueueMessage)[] = [];
2021-12-21 15:43:35 +01:00
constructor(
private autofillService: AutofillService,
private cipherService: CipherService,
private authService: AuthService,
private policyService: PolicyService,
private folderService: FolderService,
[Account Switching] Base changes for account switching (#2250) * Pull in jslib * Create new state models * Create browser specific stateService * Remove registration deprecated services, register stateService * Replace usage of deprecated services (user, constants) * Add missing properties to BrowserGroupingsComponentState * Remove StorageService from initFactory * Clear the correct state * Add null check when restoring send-grouping state * add remember email * Initialize stateservice in services.module * Fix 'lock now' not working * Comment to remove setting defaults on install * Pull jslib * Remove setting defaults on install * Bump jslib * Pass the current userId to services when logging out * Bump jslib * Override vaultTimeout default on account addition * Pull latest jslib * Retrieve vaultTimeout from stateService * Record activity per Account * Add userId to logout and add fallback if not present * Register AccountFactory * Pass userId in messages * Base changes for account switching di fixes (#2280) * [bug] Null checks on Account init * [bug] Use same stateService instance for all operations We override the stateService in browser, but currently don't pull the background service into popup and allow jslib to create its own instance of the base StateService for jslib services. This causes a split in in memory state between the three isntances that results in many errors, namely locking not working. * [chore] Update jslib * Pull in jslib * Pull in jslib * Pull in latest jslib to multiple stateservice inits * Check vault states before executing processReload * Adjust iterator * Update native messaging to include the userId (#2290) * Re-Add UserVerificationService * Fix email not being remembered by base component * Improve readability of reloadProcess * Removed unneeded null check * Fix constructor dependency (stateService) * Added missing await * Simplify dependency registration * Fixed typos * Reverted back to simple loop * Use vaultTimeoutService to retrieve Timeout Co-authored-by: Addison Beck <abeck@bitwarden.com> Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>
2022-01-27 22:22:51 +01:00
private stateService: StateService
2021-12-21 15:43:35 +01:00
) {}
async init() {
if (chrome.runtime == null) {
2021-12-21 15:43:35 +01:00
return;
}
2021-10-18 16:34:14 +02:00
BrowserApi.messageListener(
2021-10-18 16:31:59 +02:00
"notification.background",
async (msg: any, sender: chrome.runtime.MessageSender) => {
2021-10-18 16:34:14 +02:00
await this.processMessage(msg, sender);
2021-12-21 15:43:35 +01:00
}
);
2021-10-18 16:31:59 +02:00
this.cleanupNotificationQueue();
2021-12-21 15:43:35 +01:00
}
2021-10-18 16:34:14 +02:00
async processMessage(msg: any, sender: chrome.runtime.MessageSender) {
2021-10-15 15:32:56 +02:00
switch (msg.command) {
2021-10-18 16:31:59 +02:00
case "unlockCompleted":
if (msg.data.target !== "notification.background") {
return;
}
2021-10-18 16:34:14 +02:00
await this.processMessage(msg.data.commandToRetry.msg, msg.data.commandToRetry.sender);
break;
case "bgGetDataForTab":
await this.getDataForTab(sender.tab, msg.responseCommand);
break;
2021-10-15 15:32:56 +02:00
case "bgCloseNotificationBar":
await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar");
break;
case "bgAdjustNotificationBar":
await BrowserApi.tabSendMessageData(sender.tab, "adjustNotificationBar", msg.data);
break;
case "bgAddLogin":
await this.addLogin(msg.login, sender.tab);
break;
case "bgChangedPassword":
await this.changedPassword(msg.data, sender.tab);
break;
case "bgAddClose":
case "bgChangeClose":
this.removeTabFromNotificationQueue(sender.tab);
break;
case "bgAddSave":
case "bgChangeSave":
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
const retryMessage: LockedVaultPendingNotificationsItem = {
commandToRetry: {
msg: msg,
sender: sender,
},
target: "notification.background",
2021-10-15 15:32:56 +02:00
};
await BrowserApi.tabSendMessageData(
sender.tab,
"addToLockedVaultPendingNotifications",
retryMessage
2021-12-21 15:43:35 +01:00
);
2021-10-15 15:32:56 +02:00
await BrowserApi.tabSendMessageData(sender.tab, "promptForLogin");
return;
}
await this.saveOrUpdateCredentials(sender.tab, msg.folder);
2021-12-21 15:43:35 +01:00
break;
case "bgNeverSave":
await this.saveNever(sender.tab);
2021-12-21 15:43:35 +01:00
break;
case "collectPageDetailsResponse":
switch (msg.sender) {
2022-02-24 18:14:04 +01:00
case "notificationBar": {
const forms = this.autofillService.getFormsWithPasswordFields(msg.details);
await BrowserApi.tabSendMessageData(msg.tab, "notificationBarPageDetails", {
details: msg.details,
2021-10-15 15:32:56 +02:00
forms: forms,
2021-12-21 15:43:35 +01:00
});
break;
2022-02-24 18:14:04 +01:00
}
2021-12-21 15:43:35 +01:00
default:
break;
}
2021-12-21 15:43:35 +01:00
break;
default:
break;
}
}
async checkNotificationQueue(tab: chrome.tabs.Tab = null): Promise<void> {
if (this.notificationQueue.length === 0) {
return;
}
if (tab != null) {
this.doNotificationQueueCheck(tab);
return;
}
const currentTab = await BrowserApi.getTabFromCurrentWindow();
if (currentTab != null) {
this.doNotificationQueueCheck(currentTab);
}
2021-12-21 15:43:35 +01:00
}
private cleanupNotificationQueue() {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
if (this.notificationQueue[i].expires < new Date()) {
this.notificationQueue.splice(i, 1);
2021-12-21 15:43:35 +01:00
}
}
setTimeout(() => this.cleanupNotificationQueue(), 2 * 60 * 1000); // check every 2 minutes
}
private doNotificationQueueCheck(tab: chrome.tabs.Tab): void {
if (tab == null) {
return;
}
const tabDomain = Utils.getDomain(tab.url);
if (tabDomain == null) {
return;
}
for (let i = 0; i < this.notificationQueue.length; i++) {
if (
this.notificationQueue[i].tabId !== tab.id ||
this.notificationQueue[i].domain !== tabDomain
2021-12-21 15:43:35 +01:00
) {
continue;
2021-12-21 15:43:35 +01:00
}
if (this.notificationQueue[i].type === NotificationQueueMessageType.AddLogin) {
BrowserApi.tabSendMessageData(tab, "openNotificationBar", {
2021-12-21 15:43:35 +01:00
type: "add",
typeData: {
isVaultLocked: this.notificationQueue[i].wasVaultLocked,
2021-12-21 15:43:35 +01:00
},
});
} else if (this.notificationQueue[i].type === NotificationQueueMessageType.ChangePassword) {
BrowserApi.tabSendMessageData(tab, "openNotificationBar", {
type: "change",
typeData: {
isVaultLocked: this.notificationQueue[i].wasVaultLocked,
2021-12-21 15:43:35 +01:00
},
});
}
2021-12-21 15:43:35 +01:00
break;
}
2021-12-21 15:43:35 +01:00
}
private removeTabFromNotificationQueue(tab: chrome.tabs.Tab) {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
if (this.notificationQueue[i].tabId === tab.id) {
this.notificationQueue.splice(i, 1);
2021-12-21 15:43:35 +01:00
}
}
2021-12-21 15:43:35 +01:00
}
private async addLogin(loginInfo: AddLoginRuntimeMessage, tab: chrome.tabs.Tab) {
const authStatus = await this.authService.getAuthStatus();
if (authStatus === AuthenticationStatus.LoggedOut) {
return;
}
const loginDomain = Utils.getDomain(loginInfo.url);
if (loginDomain == null) {
return;
}
let normalizedUsername = loginInfo.username;
if (normalizedUsername != null) {
normalizedUsername = normalizedUsername.toLowerCase();
}
[Account Switching] Base changes for account switching (#2250) * Pull in jslib * Create new state models * Create browser specific stateService * Remove registration deprecated services, register stateService * Replace usage of deprecated services (user, constants) * Add missing properties to BrowserGroupingsComponentState * Remove StorageService from initFactory * Clear the correct state * Add null check when restoring send-grouping state * add remember email * Initialize stateservice in services.module * Fix 'lock now' not working * Comment to remove setting defaults on install * Pull jslib * Remove setting defaults on install * Bump jslib * Pass the current userId to services when logging out * Bump jslib * Override vaultTimeout default on account addition * Pull latest jslib * Retrieve vaultTimeout from stateService * Record activity per Account * Add userId to logout and add fallback if not present * Register AccountFactory * Pass userId in messages * Base changes for account switching di fixes (#2280) * [bug] Null checks on Account init * [bug] Use same stateService instance for all operations We override the stateService in browser, but currently don't pull the background service into popup and allow jslib to create its own instance of the base StateService for jslib services. This causes a split in in memory state between the three isntances that results in many errors, namely locking not working. * [chore] Update jslib * Pull in jslib * Pull in jslib * Pull in latest jslib to multiple stateservice inits * Check vault states before executing processReload * Adjust iterator * Update native messaging to include the userId (#2290) * Re-Add UserVerificationService * Fix email not being remembered by base component * Improve readability of reloadProcess * Removed unneeded null check * Fix constructor dependency (stateService) * Added missing await * Simplify dependency registration * Fixed typos * Reverted back to simple loop * Use vaultTimeoutService to retrieve Timeout Co-authored-by: Addison Beck <abeck@bitwarden.com> Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>
2022-01-27 22:22:51 +01:00
const disabledAddLogin = await this.stateService.getDisableAddLoginNotification();
if (authStatus === AuthenticationStatus.Locked) {
if (disabledAddLogin) {
return;
}
if (!(await this.allowPersonalOwnership())) {
return;
}
this.pushAddLoginToQueue(loginDomain, loginInfo, tab, true);
return;
}
const ciphers = await this.cipherService.getAllDecryptedForUrl(loginInfo.url);
const usernameMatches = ciphers.filter(
(c) => c.login.username != null && c.login.username.toLowerCase() === normalizedUsername
2021-12-21 15:43:35 +01:00
);
if (usernameMatches.length === 0) {
if (disabledAddLogin) {
2021-12-21 15:43:35 +01:00
return;
}
2021-11-02 10:09:44 +01:00
if (!(await this.allowPersonalOwnership())) {
2021-12-21 15:43:35 +01:00
return;
}
this.pushAddLoginToQueue(loginDomain, loginInfo, tab);
} else if (
usernameMatches.length === 1 &&
usernameMatches[0].login.password !== loginInfo.password
2021-12-21 15:43:35 +01:00
) {
[Account Switching] Base changes for account switching (#2250) * Pull in jslib * Create new state models * Create browser specific stateService * Remove registration deprecated services, register stateService * Replace usage of deprecated services (user, constants) * Add missing properties to BrowserGroupingsComponentState * Remove StorageService from initFactory * Clear the correct state * Add null check when restoring send-grouping state * add remember email * Initialize stateservice in services.module * Fix 'lock now' not working * Comment to remove setting defaults on install * Pull jslib * Remove setting defaults on install * Bump jslib * Pass the current userId to services when logging out * Bump jslib * Override vaultTimeout default on account addition * Pull latest jslib * Retrieve vaultTimeout from stateService * Record activity per Account * Add userId to logout and add fallback if not present * Register AccountFactory * Pass userId in messages * Base changes for account switching di fixes (#2280) * [bug] Null checks on Account init * [bug] Use same stateService instance for all operations We override the stateService in browser, but currently don't pull the background service into popup and allow jslib to create its own instance of the base StateService for jslib services. This causes a split in in memory state between the three isntances that results in many errors, namely locking not working. * [chore] Update jslib * Pull in jslib * Pull in jslib * Pull in latest jslib to multiple stateservice inits * Check vault states before executing processReload * Adjust iterator * Update native messaging to include the userId (#2290) * Re-Add UserVerificationService * Fix email not being remembered by base component * Improve readability of reloadProcess * Removed unneeded null check * Fix constructor dependency (stateService) * Added missing await * Simplify dependency registration * Fixed typos * Reverted back to simple loop * Use vaultTimeoutService to retrieve Timeout Co-authored-by: Addison Beck <abeck@bitwarden.com> Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>
2022-01-27 22:22:51 +01:00
const disabledChangePassword =
await this.stateService.getDisableChangedPasswordNotification();
if (disabledChangePassword) {
2021-12-21 15:43:35 +01:00
return;
}
this.pushChangePasswordToQueue(usernameMatches[0].id, loginDomain, loginInfo.password, tab);
2021-12-21 15:43:35 +01:00
}
}
private async pushAddLoginToQueue(
loginDomain: string,
loginInfo: AddLoginRuntimeMessage,
tab: chrome.tabs.Tab,
2022-02-24 18:14:04 +01:00
isVaultLocked = false
2021-12-21 15:43:35 +01:00
) {
// remove any old messages for this tab
this.removeTabFromNotificationQueue(tab);
const message: AddLoginQueueMessage = {
type: NotificationQueueMessageType.AddLogin,
username: loginInfo.username,
password: loginInfo.password,
domain: loginDomain,
uri: loginInfo.url,
tabId: tab.id,
expires: new Date(new Date().getTime() + 5 * 60000), // 5 minutes
wasVaultLocked: isVaultLocked,
};
this.notificationQueue.push(message);
await this.checkNotificationQueue(tab);
2021-12-21 15:43:35 +01:00
}
private async changedPassword(changeData: ChangePasswordRuntimeMessage, tab: chrome.tabs.Tab) {
const loginDomain = Utils.getDomain(changeData.url);
if (loginDomain == null) {
2021-12-21 15:43:35 +01:00
return;
}
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
this.pushChangePasswordToQueue(null, loginDomain, changeData.newPassword, tab, true);
return;
}
let id: string = null;
const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url);
if (changeData.currentPassword != null) {
const passwordMatches = ciphers.filter(
(c) => c.login.password === changeData.currentPassword
);
if (passwordMatches.length === 1) {
id = passwordMatches[0].id;
}
} else if (ciphers.length === 1) {
id = ciphers[0].id;
2021-12-21 15:43:35 +01:00
}
if (id != null) {
this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, tab);
2021-12-21 15:43:35 +01:00
}
}
private async pushChangePasswordToQueue(
cipherId: string,
loginDomain: string,
newPassword: string,
tab: chrome.tabs.Tab,
2022-02-24 18:14:04 +01:00
isVaultLocked = false
2021-12-21 15:43:35 +01:00
) {
// remove any old messages for this tab
this.removeTabFromNotificationQueue(tab);
const message: AddChangePasswordQueueMessage = {
type: NotificationQueueMessageType.ChangePassword,
cipherId: cipherId,
newPassword: newPassword,
domain: loginDomain,
tabId: tab.id,
expires: new Date(new Date().getTime() + 5 * 60000), // 5 minutes
wasVaultLocked: isVaultLocked,
2021-12-21 15:43:35 +01:00
};
this.notificationQueue.push(message);
await this.checkNotificationQueue(tab);
2021-12-21 15:43:35 +01:00
}
private async saveOrUpdateCredentials(tab: chrome.tabs.Tab, folderId?: string) {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
const queueMessage = this.notificationQueue[i];
2021-12-21 15:43:35 +01:00
if (
queueMessage.tabId !== tab.id ||
(queueMessage.type !== NotificationQueueMessageType.AddLogin &&
queueMessage.type !== NotificationQueueMessageType.ChangePassword)
2021-12-21 15:43:35 +01:00
) {
continue;
2021-12-21 15:43:35 +01:00
}
const tabDomain = Utils.getDomain(tab.url);
if (tabDomain != null && tabDomain !== queueMessage.domain) {
continue;
2021-12-21 15:43:35 +01:00
}
this.notificationQueue.splice(i, 1);
BrowserApi.tabSendMessageData(tab, "closeNotificationBar");
2021-12-21 15:43:35 +01:00
if (queueMessage.type === NotificationQueueMessageType.ChangePassword) {
const changePasswordMessage = queueMessage as AddChangePasswordQueueMessage;
const cipher = await this.getDecryptedCipherById(changePasswordMessage.cipherId);
if (cipher == null) {
2021-12-21 15:43:35 +01:00
return;
}
await this.updateCipher(cipher, changePasswordMessage.newPassword);
2021-12-21 15:43:35 +01:00
return;
}
if (queueMessage.type === NotificationQueueMessageType.AddLogin) {
if (!queueMessage.wasVaultLocked) {
await this.createNewCipher(queueMessage as AddLoginQueueMessage, folderId);
BrowserApi.tabSendMessageData(tab, "addedCipher");
return;
}
2021-12-21 15:43:35 +01:00
// If the vault was locked, check if a cipher needs updating instead of creating a new one
const addLoginMessage = queueMessage as AddLoginQueueMessage;
const ciphers = await this.cipherService.getAllDecryptedForUrl(addLoginMessage.uri);
const usernameMatches = ciphers.filter(
(c) =>
c.login.username != null && c.login.username.toLowerCase() === addLoginMessage.username
2021-12-21 15:43:35 +01:00
);
if (usernameMatches.length >= 1) {
await this.updateCipher(usernameMatches[0], addLoginMessage.password);
2021-12-21 15:43:35 +01:00
return;
}
await this.createNewCipher(addLoginMessage, folderId);
BrowserApi.tabSendMessageData(tab, "addedCipher");
}
}
2021-12-21 15:43:35 +01:00
}
private async createNewCipher(queueMessage: AddLoginQueueMessage, folderId: string) {
const loginModel = new LoginView();
const loginUri = new LoginUriView();
loginUri.uri = queueMessage.uri;
loginModel.uris = [loginUri];
loginModel.username = queueMessage.username;
loginModel.password = queueMessage.password;
const model = new CipherView();
model.name = Utils.getHostname(queueMessage.uri) || queueMessage.domain;
model.name = model.name.replace(/^www\./, "");
model.type = CipherType.Login;
model.login = loginModel;
2021-12-21 15:43:35 +01:00
if (!Utils.isNullOrWhitespace(folderId)) {
const folders = await this.folderService.getAllDecrypted();
if (folders.some((x) => x.id === folderId)) {
model.folderId = folderId;
}
}
const cipher = await this.cipherService.encrypt(model);
await this.cipherService.saveWithServer(cipher);
}
private async getDecryptedCipherById(cipherId: string) {
const cipher = await this.cipherService.get(cipherId);
if (cipher != null && cipher.type === CipherType.Login) {
return await cipher.decrypt();
}
return null;
2021-12-21 15:43:35 +01:00
}
private async updateCipher(cipher: CipherView, newPassword: string) {
if (cipher != null && cipher.type === CipherType.Login) {
cipher.login.password = newPassword;
const newCipher = await this.cipherService.encrypt(cipher);
await this.cipherService.saveWithServer(newCipher);
}
2021-12-21 15:43:35 +01:00
}
private async saveNever(tab: chrome.tabs.Tab) {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
const queueMessage = this.notificationQueue[i];
2021-12-21 15:43:35 +01:00
if (
queueMessage.tabId !== tab.id ||
queueMessage.type !== NotificationQueueMessageType.AddLogin
2021-12-21 15:43:35 +01:00
) {
continue;
2021-12-21 15:43:35 +01:00
}
const tabDomain = Utils.getDomain(tab.url);
if (tabDomain != null && tabDomain !== queueMessage.domain) {
continue;
2021-12-21 15:43:35 +01:00
}
this.notificationQueue.splice(i, 1);
BrowserApi.tabSendMessageData(tab, "closeNotificationBar");
2021-12-21 15:43:35 +01:00
const hostname = Utils.getHostname(tab.url);
await this.cipherService.saveNeverDomain(hostname);
}
2021-12-21 15:43:35 +01:00
}
private async getDataForTab(tab: chrome.tabs.Tab, responseCommand: string) {
const responseData: any = {};
if (responseCommand === "notificationBarGetFoldersList") {
responseData.folders = await this.folderService.getAllDecrypted();
}
await BrowserApi.tabSendMessageData(tab, responseCommand, responseData);
}
private async allowPersonalOwnership(): Promise<boolean> {
return !(await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership));
}
}