diff --git a/src/background/contextMenus.background.ts b/src/background/contextMenus.background.ts index b0fa7097d2a..e46fe8e3694 100644 --- a/src/background/contextMenus.background.ts +++ b/src/background/contextMenus.background.ts @@ -10,6 +10,7 @@ import { TotpService } from 'jslib-common/abstractions/totp.service'; import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { EventType } from 'jslib-common/enums/eventType'; +import { CipherView } from 'jslib-common/models/view/cipherView'; export default class ContextMenusBackground { private contextMenus: any; @@ -88,7 +89,7 @@ export default class ContextMenusBackground { } } - private async startAutofillPage(cipher: any) { + private async startAutofillPage(cipher: CipherView) { this.main.loginToAutoFill = cipher; const tab = await BrowserApi.getTabFromCurrentWindow(); if (tab == null) { diff --git a/src/background/main.background.ts b/src/background/main.background.ts index f3538028db1..486ef98b3c2 100644 --- a/src/background/main.background.ts +++ b/src/background/main.background.ts @@ -1,6 +1,8 @@ import { CipherRepromptType } from 'jslib-common/enums/cipherRepromptType'; import { CipherType } from 'jslib-common/enums/cipherType'; +import { CipherView } from 'jslib-common/models/view/cipherView'; + import { ApiService } from 'jslib-common/services/api.service'; import { AppIdService } from 'jslib-common/services/appId.service'; import { AuditService } from 'jslib-common/services/audit.service'; @@ -71,6 +73,7 @@ import CommandsBackground from './commands.background'; import ContextMenusBackground from './contextMenus.background'; import IdleBackground from './idle.background'; import { NativeMessagingBackground } from './nativeMessaging.background'; +import NotificationBackground from './notification.background'; import RuntimeBackground from './runtime.background'; import TabsBackground from './tabs.background'; import WebRequestBackground from './webRequest.background'; @@ -125,12 +128,12 @@ export default class MainBackground { onUpdatedRan: boolean; onReplacedRan: boolean; - loginToAutoFill: any = null; - notificationQueue: any[] = []; + loginToAutoFill: CipherView = null; private commandsBackground: CommandsBackground; private contextMenusBackground: ContextMenusBackground; private idleBackground: IdleBackground; + private notificationBackground: NotificationBackground; private runtimeBackground: RuntimeBackground; private tabsBackground: TabsBackground; private webRequestBackground: WebRequestBackground; @@ -237,17 +240,18 @@ export default class MainBackground { opr.sidebarAction : (window as any).chrome.sidebarAction; // Background - this.runtimeBackground = new RuntimeBackground(this, this.autofillService, this.cipherService, + this.runtimeBackground = new RuntimeBackground(this, this.autofillService, this.platformUtilsService as BrowserPlatformUtilsService, this.storageService, this.i18nService, - this.notificationsService, this.systemService, this.vaultTimeoutService, - this.environmentService, this.policyService, this.userService, this.messagingService, this.folderService); + this.notificationsService, this.systemService, this.environmentService, this.messagingService); this.nativeMessagingBackground = new NativeMessagingBackground(this.storageService, this.cryptoService, this.cryptoFunctionService, this.vaultTimeoutService, this.runtimeBackground, this.i18nService, this.userService, this.messagingService, this.appIdService, this.platformUtilsService); this.commandsBackground = new CommandsBackground(this, this.passwordGenerationService, this.platformUtilsService, this.vaultTimeoutService); + this.notificationBackground = new NotificationBackground(this, this.autofillService, this.cipherService, + this.storageService, this.vaultTimeoutService, this.policyService, this.folderService); - this.tabsBackground = new TabsBackground(this); + this.tabsBackground = new TabsBackground(this, this.notificationBackground); this.contextMenusBackground = new ContextMenusBackground(this, this.cipherService, this.passwordGenerationService, this.platformUtilsService, this.vaultTimeoutService, this.eventService, this.totpService); this.idleBackground = new IdleBackground(this.vaultTimeoutService, this.storageService, @@ -276,6 +280,7 @@ export default class MainBackground { await (this.i18nService as I18nService).init(); await (this.eventService as EventService).init(true); await this.runtimeBackground.init(); + await this.notificationBackground.init(); await this.commandsBackground.init(); await this.tabsBackground.init(); @@ -288,7 +293,6 @@ export default class MainBackground { setTimeout(async () => { await this.environmentService.setUrlsFromStorage(); await this.setIcon(); - this.cleanupNotificationQueue(); this.fullSync(true); setTimeout(() => this.notificationsService.init(), 2500); resolve(); @@ -386,22 +390,6 @@ export default class MainBackground { }, options); } - async checkNotificationQueue(tab: any = null): Promise { - if (this.notificationQueue.length === 0) { - return; - } - - if (tab != null) { - this.doNotificationQueueCheck(tab); - return; - } - - const currentTab = await BrowserApi.getTabFromCurrentWindow(); - if (currentTab != null) { - this.doNotificationQueueCheck(currentTab); - } - } - async openPopup() { // Chrome APIs cannot open popup @@ -653,49 +641,6 @@ export default class MainBackground { return title.replace(/&/g, '&&'); } - private cleanupNotificationQueue() { - for (let i = this.notificationQueue.length - 1; i >= 0; i--) { - if (this.notificationQueue[i].expires < new Date()) { - this.notificationQueue.splice(i, 1); - } - } - setTimeout(() => this.cleanupNotificationQueue(), 2 * 60 * 1000); // check every 2 minutes - } - - private doNotificationQueueCheck(tab: any) { - 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) { - continue; - } - - if (this.notificationQueue[i].type === 'addLogin') { - BrowserApi.tabSendMessageData(tab, 'openNotificationBar', { - type: 'add', - typeData: { - isVaultLocked: this.notificationQueue[i].wasVaultLocked, - }, - }); - } else if (this.notificationQueue[i].type === 'changePassword') { - BrowserApi.tabSendMessageData(tab, 'openNotificationBar', { - type: 'change', - typeData: { - isVaultLocked: this.notificationQueue[i].wasVaultLocked, - }, - }); - } - break; - } - } - private async fullSync(override: boolean = false) { const syncInternal = 6 * 60 * 60 * 1000; // 6 hours const lastSync = await this.syncService.getLastSync(); diff --git a/src/background/models/addChangePasswordQueueMessage.ts b/src/background/models/addChangePasswordQueueMessage.ts index ad1efe3da33..9adcc3d5e74 100644 --- a/src/background/models/addChangePasswordQueueMessage.ts +++ b/src/background/models/addChangePasswordQueueMessage.ts @@ -1,9 +1,6 @@ -export default class AddChangePasswordQueueMessage { - type: string; +import NotificationQueueMessage from "./notificationQueueMessage"; + +export default class AddChangePasswordQueueMessage extends NotificationQueueMessage { cipherId: string; newPassword: string; - domain: string; - tabId: string; - expires: Date; - wasVaultLocked: boolean; } diff --git a/src/background/models/addLoginQueueMessage.ts b/src/background/models/addLoginQueueMessage.ts index 246dca03a4c..466c5c6c95e 100644 --- a/src/background/models/addLoginQueueMessage.ts +++ b/src/background/models/addLoginQueueMessage.ts @@ -1,10 +1,7 @@ -export default class AddLoginQueueMessage { - type: string; +import NotificationQueueMessage from "./notificationQueueMessage"; + +export default class AddLoginQueueMessage extends NotificationQueueMessage { username: string; password: string; - domain: string; uri: string; - tabId: string; - expires: Date; - wasVaultLocked: boolean; } diff --git a/src/background/models/addLoginRuntimeMessage.ts b/src/background/models/addLoginRuntimeMessage.ts new file mode 100644 index 00000000000..3426bc595df --- /dev/null +++ b/src/background/models/addLoginRuntimeMessage.ts @@ -0,0 +1,5 @@ +export default class AddLoginRuntimeMessage { + username: string; + password: string; + url: string; +} diff --git a/src/background/models/changePasswordRuntimeMessage.ts b/src/background/models/changePasswordRuntimeMessage.ts new file mode 100644 index 00000000000..8f3f6aa5778 --- /dev/null +++ b/src/background/models/changePasswordRuntimeMessage.ts @@ -0,0 +1,5 @@ +export default class ChangePasswordRuntimeMessage { + currentPassword: string; + newPassword: string; + url: string; +} diff --git a/src/background/models/lockedVaultPendingNotificationsItem.ts b/src/background/models/lockedVaultPendingNotificationsItem.ts new file mode 100644 index 00000000000..dec23139662 --- /dev/null +++ b/src/background/models/lockedVaultPendingNotificationsItem.ts @@ -0,0 +1,7 @@ +export default class LockedVaultPendingNotificationsItem { + commandToRetry: { + msg: any; + sender: chrome.runtime.MessageSender; + } + target: string; +} diff --git a/src/background/models/notificationQueueMessage.ts b/src/background/models/notificationQueueMessage.ts new file mode 100644 index 00000000000..9c031af0292 --- /dev/null +++ b/src/background/models/notificationQueueMessage.ts @@ -0,0 +1,9 @@ +import { NotificationQueueMessageType } from "./NotificationQueueMessageType"; + +export default class NotificationQueueMessage { + type: NotificationQueueMessageType; + domain: string; + tabId: number; + expires: Date; + wasVaultLocked: boolean; +} diff --git a/src/background/models/notificationQueueMessageType.ts b/src/background/models/notificationQueueMessageType.ts new file mode 100644 index 00000000000..5c27572dacd --- /dev/null +++ b/src/background/models/notificationQueueMessageType.ts @@ -0,0 +1,4 @@ +export enum NotificationQueueMessageType { + addLogin = 'addLogin', + changePassword = 'changePassword', +} diff --git a/src/background/notification.background.ts b/src/background/notification.background.ts new file mode 100644 index 00000000000..929da3f040d --- /dev/null +++ b/src/background/notification.background.ts @@ -0,0 +1,403 @@ +import { CipherType } from 'jslib-common/enums/cipherType'; + +import { CipherView } from 'jslib-common/models/view/cipherView'; +import { LoginUriView } from 'jslib-common/models/view/loginUriView'; +import { LoginView } from 'jslib-common/models/view/loginView'; + +import { CipherService } from 'jslib-common/abstractions/cipher.service'; +import { FolderService } from 'jslib-common/abstractions/folder.service'; +import { PolicyService } from 'jslib-common/abstractions/policy.service'; +import { StorageService } from 'jslib-common/abstractions/storage.service'; +import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; +import { ConstantsService } from 'jslib-common/services/constants.service'; +import { AutofillService } from '../services/abstractions/autofill.service'; + +import { BrowserApi } from '../browser/browserApi'; + +import MainBackground from './main.background'; + +import { Utils } from 'jslib-common/misc/utils'; + +import { PolicyType } from 'jslib-common/enums/policyType'; + +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)[] = []; + + constructor(private main: MainBackground, private autofillService: AutofillService, + private cipherService: CipherService, private storageService: StorageService, + private vaultTimeoutService: VaultTimeoutService, private policyService: PolicyService, + private folderService: FolderService) { + } + + async init() { + if (chrome.runtime == null) { + return; + } + + BrowserApi.messageListener('notification.background', async (msg: any, sender: chrome.runtime.MessageSender) => { + await this.processMessage(msg, sender); + }); + + this.cleanupNotificationQueue(); + } + + async processMessage(msg: any, sender: chrome.runtime.MessageSender) { + switch (msg.command) { + case 'unlockCompleted': + if (msg.data.target !== 'notification.background') { + return; + } + await this.processMessage(msg.data.commandToRetry.msg, msg.data.commandToRetry.sender); + break; + case 'bgGetDataForTab': + await this.getDataForTab(sender.tab, msg.responseCommand); + break; + 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.vaultTimeoutService.isLocked()) { + const retryMessage: LockedVaultPendingNotificationsItem = { + commandToRetry: { + msg: msg, + sender: sender, + }, + target: 'notification.background', + }; + await BrowserApi.tabSendMessageData(sender.tab, 'addToLockedVaultPendingNotifications', retryMessage); + await BrowserApi.tabSendMessageData(sender.tab, 'promptForLogin'); + return; + } + await this.saveOrUpdateCredentials(sender.tab, msg.folder); + break; + case 'bgNeverSave': + await this.saveNever(sender.tab); + break; + case 'collectPageDetailsResponse': + switch (msg.sender) { + case 'notificationBar': + const forms = this.autofillService.getFormsWithPasswordFields(msg.details); + await BrowserApi.tabSendMessageData(msg.tab, 'notificationBarPageDetails', { + details: msg.details, + forms: forms, + }); + break; + default: + break; + } + break; + default: + break; + } + } + + async checkNotificationQueue(tab: chrome.tabs.Tab = null): Promise { + if (this.notificationQueue.length === 0) { + return; + } + + if (tab != null) { + this.doNotificationQueueCheck(tab); + return; + } + + const currentTab = await BrowserApi.getTabFromCurrentWindow(); + if (currentTab != null) { + this.doNotificationQueueCheck(currentTab); + } + } + + private cleanupNotificationQueue() { + for (let i = this.notificationQueue.length - 1; i >= 0; i--) { + if (this.notificationQueue[i].expires < new Date()) { + this.notificationQueue.splice(i, 1); + } + } + 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) { + continue; + } + + if (this.notificationQueue[i].type === NotificationQueueMessageType.addLogin) { + BrowserApi.tabSendMessageData(tab, 'openNotificationBar', { + type: 'add', + typeData: { + isVaultLocked: this.notificationQueue[i].wasVaultLocked, + }, + }); + } else if (this.notificationQueue[i].type === NotificationQueueMessageType.changePassword) { + BrowserApi.tabSendMessageData(tab, 'openNotificationBar', { + type: 'change', + typeData: { + isVaultLocked: this.notificationQueue[i].wasVaultLocked, + }, + }); + } + break; + } + } + + 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); + } + } + } + + private async addLogin(loginInfo: AddLoginRuntimeMessage, tab: chrome.tabs.Tab) { + const loginDomain = Utils.getDomain(loginInfo.url); + if (loginDomain == null) { + return; + } + + let normalizedUsername = loginInfo.username; + if (normalizedUsername != null) { + normalizedUsername = normalizedUsername.toLowerCase(); + } + + if (await this.vaultTimeoutService.isLocked()) { + 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); + if (usernameMatches.length === 0) { + const disabledAddLogin = await this.storageService.get( + ConstantsService.disableAddLoginNotificationKey); + if (disabledAddLogin) { + return; + } + + if (!(await this.allowPersonalOwnership())) { + return; + } + + this.pushAddLoginToQueue(loginDomain, loginInfo, tab); + + } else if (usernameMatches.length === 1 && usernameMatches[0].login.password !== loginInfo.password) { + const disabledChangePassword = await this.storageService.get( + ConstantsService.disableChangedPasswordNotificationKey); + if (disabledChangePassword) { + return; + } + this.pushChangePasswordToQueue(usernameMatches[0].id, loginDomain, loginInfo.password, tab); + } + } + + private async pushAddLoginToQueue(loginDomain: string, loginInfo: AddLoginRuntimeMessage, tab: chrome.tabs.Tab, isVaultLocked: boolean = false) { + // 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); + } + + private async changedPassword(changeData: ChangePasswordRuntimeMessage, tab: chrome.tabs.Tab) { + const loginDomain = Utils.getDomain(changeData.url); + if (loginDomain == null) { + return; + } + + if (await this.vaultTimeoutService.isLocked()) { + 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; + } + if (id != null) { + this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, tab); + } + } + + private async pushChangePasswordToQueue(cipherId: string, loginDomain: string, newPassword: string, tab: chrome.tabs.Tab, isVaultLocked: boolean = false) { + // 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, + }; + this.notificationQueue.push(message); + await this.checkNotificationQueue(tab); + } + + private async saveOrUpdateCredentials(tab: chrome.tabs.Tab, folderId?: string) { + for (let i = this.notificationQueue.length - 1; i >= 0; i--) { + const queueMessage = this.notificationQueue[i]; + if (queueMessage.tabId !== tab.id || + (queueMessage.type !== NotificationQueueMessageType.addLogin && queueMessage.type !== NotificationQueueMessageType.changePassword)) { + continue; + } + + const tabDomain = Utils.getDomain(tab.url); + if (tabDomain != null && tabDomain !== queueMessage.domain) { + continue; + } + + this.notificationQueue.splice(i, 1); + BrowserApi.tabSendMessageData(tab, 'closeNotificationBar'); + + if (queueMessage.type === NotificationQueueMessageType.changePassword) { + const message = (queueMessage as AddChangePasswordQueueMessage); + const cipher = await this.getDecryptedCipherById(message.cipherId); + if (cipher == null) { + return; + } + await this.updateCipher(cipher, message.newPassword); + return; + } + + if (!queueMessage.wasVaultLocked) { + await this.createNewCipher(queueMessage as AddLoginQueueMessage, folderId); + } + + // If the vault was locked, check if a cipher needs updating instead of creating a new one + if (queueMessage.type === NotificationQueueMessageType.addLogin && queueMessage.wasVaultLocked === true) { + const message = (queueMessage as AddLoginQueueMessage); + const ciphers = await this.cipherService.getAllDecryptedForUrl(message.uri); + const usernameMatches = ciphers.filter(c => c.login.username != null && + c.login.username.toLowerCase() === message.username); + + if (usernameMatches.length >= 1) { + await this.updateCipher(usernameMatches[0], message.password); + return; + } + + await this.createNewCipher(message, folderId); + } + } + } + + 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; + + 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; + } + + 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); + } + } + + private async saveNever(tab: chrome.tabs.Tab) { + for (let i = this.notificationQueue.length - 1; i >= 0; i--) { + const queueMessage = this.notificationQueue[i]; + if (queueMessage.tabId !== tab.id || queueMessage.type !== NotificationQueueMessageType.addLogin) { + continue; + } + + const tabDomain = Utils.getDomain(tab.url); + if (tabDomain != null && tabDomain !== queueMessage.domain) { + continue; + } + + this.notificationQueue.splice(i, 1); + BrowserApi.tabSendMessageData(tab, 'closeNotificationBar'); + + const hostname = Utils.getHostname(tab.url); + await this.cipherService.saveNeverDomain(hostname); + } + } + + 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 { + return !await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership); + } +} diff --git a/src/background/runtime.background.ts b/src/background/runtime.background.ts index a46d174aac6..71dbe6ec9d5 100644 --- a/src/background/runtime.background.ts +++ b/src/background/runtime.background.ts @@ -1,20 +1,9 @@ -import { CipherType } from 'jslib-common/enums/cipherType'; - -import { CipherView } from 'jslib-common/models/view/cipherView'; -import { LoginUriView } from 'jslib-common/models/view/loginUriView'; -import { LoginView } from 'jslib-common/models/view/loginView'; - -import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { FolderService } from 'jslib-common/abstractions/folder.service'; import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { NotificationsService } from 'jslib-common/abstractions/notifications.service'; -import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { StorageService } from 'jslib-common/abstractions/storage.service'; import { SystemService } from 'jslib-common/abstractions/system.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; -import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { ConstantsService } from 'jslib-common/services/constants.service'; import { AutofillService } from '../services/abstractions/autofill.service'; import BrowserPlatformUtilsService from '../services/browserPlatformUtils.service'; @@ -24,28 +13,19 @@ import { BrowserApi } from '../browser/browserApi'; import MainBackground from './main.background'; import { Utils } from 'jslib-common/misc/utils'; - -import { PolicyType } from 'jslib-common/enums/policyType'; - -import AddChangePasswordQueueMessage from './models/addChangePasswordQueueMessage'; -import AddLoginQueueMessage from './models/addLoginQueueMessage'; +import LockedVaultPendingNotificationsItem from './models/lockedVaultPendingNotificationsItem'; export default class RuntimeBackground { - private runtime: any; private autofillTimeout: any; private pageDetailsToAutoFill: any[] = []; private onInstalledReason: string = null; - - private lockedVaultPendingNotifications: any[] = []; + private lockedVaultPendingNotifications: LockedVaultPendingNotificationsItem[] = []; constructor(private main: MainBackground, private autofillService: AutofillService, - private cipherService: CipherService, private platformUtilsService: BrowserPlatformUtilsService, + private platformUtilsService: BrowserPlatformUtilsService, private storageService: StorageService, private i18nService: I18nService, - private notificationsService: NotificationsService, - private systemService: SystemService, private vaultTimeoutService: VaultTimeoutService, - private environmentService: EnvironmentService, private policyService: PolicyService, - private userService: UserService, private messagingService: MessagingService, - private folderService: FolderService) { + private notificationsService: NotificationsService, private systemService: SystemService, + private environmentService: EnvironmentService, private messagingService: MessagingService) { // onInstalled listener must be wired up before anything else, so we do it in the ctor chrome.runtime.onInstalled.addListener((details: any) => { @@ -59,7 +39,7 @@ export default class RuntimeBackground { } await this.checkOnInstalled(); - BrowserApi.messageListener('runtime.background', async (msg: any, sender: any, sendResponse: any) => { + BrowserApi.messageListener('runtime.background', async (msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) => { await this.processMessage(msg, sender, sendResponse); }); } @@ -68,28 +48,28 @@ export default class RuntimeBackground { switch (msg.command) { case 'loggedIn': case 'unlocked': + let item: LockedVaultPendingNotificationsItem; + + if (this.lockedVaultPendingNotifications.length > 0) { + await BrowserApi.closeLoginTab(); + + item = this.lockedVaultPendingNotifications.pop(); + if (item.commandToRetry.sender?.tab?.id) { + await BrowserApi.focusSpecifiedTab(item.commandToRetry.sender.tab.id); + } + } + await this.main.setIcon(); await this.main.refreshBadgeAndMenu(false); this.notificationsService.updateConnection(msg.command === 'unlocked'); this.systemService.cancelProcessReload(); - if (this.lockedVaultPendingNotifications.length > 0) { - const retryItem = this.lockedVaultPendingNotifications.pop(); - await this.processMessage(retryItem.msg, retryItem.sender, null); - - await BrowserApi.closeLoginTab(); - - if (retryItem?.sender?.tab?.id) { - await BrowserApi.focusSpecifiedTab(retryItem.sender.tab.id); - } + if (item) { + await BrowserApi.tabSendMessageData(item.commandToRetry.sender.tab, 'unlockCompleted', item); } break; case 'addToLockedVaultPendingNotifications': - const retryMessage = { - msg: msg.retryItem, - sender: sender, - }; - this.lockedVaultPendingNotifications.push(retryMessage); + this.lockedVaultPendingNotifications.push(msg.data); break; case 'logout': await this.main.logout(msg.expired); @@ -108,35 +88,9 @@ export default class RuntimeBackground { case 'showDialogResolve': this.platformUtilsService.resolveDialogPromise(msg.dialogId, msg.confirmed); break; - case 'bgGetDataForTab': - await this.getDataForTab(sender.tab, msg.responseCommand); - break; - case 'bgCloseNotificationBar': - await BrowserApi.tabSendMessageData(sender.tab, 'closeNotificationBar'); - break; - case 'bgAdjustNotificationBar': - await BrowserApi.tabSendMessageData(sender.tab, 'adjustNotificationBar', msg.data); - break; case 'bgCollectPageDetails': await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId); 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': - await this.saveOrUpdateCredentials(sender.tab, msg.folder); - break; - case 'bgNeverSave': - await this.saveNever(sender.tab); - break; case 'bgUpdateContextMenu': case 'editedCipher': case 'addedCipher': @@ -148,13 +102,6 @@ export default class RuntimeBackground { break; case 'collectPageDetailsResponse': switch (msg.sender) { - case 'notificationBar': - const forms = this.autofillService.getFormsWithPasswordFields(msg.details); - await BrowserApi.tabSendMessageData(msg.tab, 'notificationBarPageDetails', { - details: msg.details, - forms: forms, - }); - break; case 'autofiller': case 'autofill_cmd': const totpCode = await this.autofillService.doAutoFillActiveTab([{ @@ -237,222 +184,6 @@ export default class RuntimeBackground { this.pageDetailsToAutoFill = []; } - private async saveOrUpdateCredentials(tab: any, folderId?: string) { - for (let i = this.main.notificationQueue.length - 1; i >= 0; i--) { - const queueMessage = this.main.notificationQueue[i]; - if (queueMessage.tabId !== tab.id || - (queueMessage.type !== 'addLogin' && queueMessage.type !== 'changePassword')) { - continue; - } - - const tabDomain = Utils.getDomain(tab.url); - if (tabDomain != null && tabDomain !== queueMessage.domain) { - continue; - } - - this.main.notificationQueue.splice(i, 1); - BrowserApi.tabSendMessageData(tab, 'closeNotificationBar'); - - if (queueMessage.type === 'changePassword') { - const message = (queueMessage as AddChangePasswordQueueMessage); - const cipher = await this.getDecryptedCipherById(message.cipherId); - if (cipher == null) { - return; - } - await this.updateCipher(cipher, message.newPassword); - return; - } - - if (!queueMessage.wasVaultLocked) { - await this.createNewCipher(queueMessage, folderId); - } - - // If the vault was locked, check if a cipher needs updating instead of creating a new one - if (queueMessage.type === 'addLogin' && queueMessage.wasVaultLocked === true) { - const message = (queueMessage as AddLoginQueueMessage); - const ciphers = await this.cipherService.getAllDecryptedForUrl(message.uri); - const usernameMatches = ciphers.filter(c => c.login.username != null && - c.login.username.toLowerCase() === message.username); - - if (usernameMatches.length >= 1) { - await this.updateCipher(usernameMatches[0], message.password); - return; - } - - await this.createNewCipher(message, folderId); - } - } - } - - 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; - - 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; - } - - 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); - } - } - - private async saveNever(tab: any) { - for (let i = this.main.notificationQueue.length - 1; i >= 0; i--) { - const queueMessage = this.main.notificationQueue[i]; - if (queueMessage.tabId !== tab.id || queueMessage.type !== 'addLogin') { - continue; - } - - const tabDomain = Utils.getDomain(tab.url); - if (tabDomain != null && tabDomain !== queueMessage.domain) { - continue; - } - - this.main.notificationQueue.splice(i, 1); - BrowserApi.tabSendMessageData(tab, 'closeNotificationBar'); - - const hostname = Utils.getHostname(tab.url); - await this.cipherService.saveNeverDomain(hostname); - } - } - - private async addLogin(loginInfo: any, tab: any) { - const loginDomain = Utils.getDomain(loginInfo.url); - if (loginDomain == null) { - return; - } - - let normalizedUsername = loginInfo.username; - if (normalizedUsername != null) { - normalizedUsername = normalizedUsername.toLowerCase(); - } - - if (await this.vaultTimeoutService.isLocked()) { - 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); - if (usernameMatches.length === 0) { - const disabledAddLogin = await this.storageService.get( - ConstantsService.disableAddLoginNotificationKey); - if (disabledAddLogin) { - return; - } - - if (!(await this.allowPersonalOwnership())) { - return; - } - - this.pushAddLoginToQueue(loginDomain, loginInfo, tab); - - } else if (usernameMatches.length === 1 && usernameMatches[0].login.password !== loginInfo.password) { - const disabledChangePassword = await this.storageService.get( - ConstantsService.disableChangedPasswordNotificationKey); - if (disabledChangePassword) { - return; - } - this.pushChangePasswordToQueue(usernameMatches[0].id, loginDomain, loginInfo.password, tab); - } - } - - private async pushAddLoginToQueue(loginDomain: string, loginInfo: any, tab: any, isVaultLocked: boolean = false) { - // remove any old messages for this tab - this.removeTabFromNotificationQueue(tab); - const message: AddLoginQueueMessage = { - type: '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.main.notificationQueue.push(message); - await this.main.checkNotificationQueue(tab); - } - - private async changedPassword(changeData: any, tab: any) { - const loginDomain = Utils.getDomain(changeData.url); - if (loginDomain == null) { - return; - } - - if (await this.vaultTimeoutService.isLocked()) { - 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; - } - if (id != null) { - this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, tab); - } - } - - private async pushChangePasswordToQueue(cipherId: string, loginDomain: string, newPassword: string, tab: any, isVaultLocked: boolean = false) { - // remove any old messages for this tab - this.removeTabFromNotificationQueue(tab); - const message: AddChangePasswordQueueMessage = { - type: 'changePassword', - cipherId: cipherId, - newPassword: newPassword, - domain: loginDomain, - tabId: tab.id, - expires: new Date((new Date()).getTime() + 5 * 60000), // 5 minutes - wasVaultLocked: isVaultLocked, - }; - this.main.notificationQueue.push(message); - await this.main.checkNotificationQueue(tab); - } - - private removeTabFromNotificationQueue(tab: any) { - for (let i = this.main.notificationQueue.length - 1; i >= 0; i--) { - if (this.main.notificationQueue[i].tabId === tab.id) { - this.main.notificationQueue.splice(i, 1); - } - } - } - private async checkOnInstalled() { setTimeout(async () => { if (this.onInstalledReason != null) { @@ -479,17 +210,4 @@ export default class RuntimeBackground { await this.storageService.save(ConstantsService.vaultTimeoutActionKey, 'lock'); } } - - private async getDataForTab(tab: any, responseCommand: string) { - const responseData: any = {}; - if (responseCommand === 'notificationBarGetFoldersList') { - responseData.folders = await this.folderService.getAllDecrypted(); - } - - await BrowserApi.tabSendMessageData(tab, responseCommand, responseData); - } - - private async allowPersonalOwnership(): Promise { - return !await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership); - } } diff --git a/src/background/tabs.background.ts b/src/background/tabs.background.ts index 8f4d4e12b1b..7ba7a75a3dd 100644 --- a/src/background/tabs.background.ts +++ b/src/background/tabs.background.ts @@ -1,9 +1,10 @@ import MainBackground from './main.background'; +import NotificationBackground from './notification.background'; export default class TabsBackground { private tabs: any; - constructor(private main: MainBackground) { + constructor(private main: MainBackground, private notificationBackground: NotificationBackground) { this.tabs = chrome.tabs; } @@ -23,7 +24,7 @@ export default class TabsBackground { return; } this.main.onReplacedRan = true; - await this.main.checkNotificationQueue(); + await this.notificationBackground.checkNotificationQueue(); await this.main.refreshBadgeAndMenu(); this.main.messagingService.send('tabReplaced'); this.main.messagingService.send('tabChanged'); @@ -34,7 +35,7 @@ export default class TabsBackground { return; } this.main.onUpdatedRan = true; - await this.main.checkNotificationQueue(); + await this.notificationBackground.checkNotificationQueue(); await this.main.refreshBadgeAndMenu(); this.main.messagingService.send('tabUpdated'); this.main.messagingService.send('tabChanged'); diff --git a/src/browser/browserApi.ts b/src/browser/browserApi.ts index 648394c9bdc..5efb4bc8217 100644 --- a/src/browser/browserApi.ts +++ b/src/browser/browserApi.ts @@ -48,7 +48,7 @@ export class BrowserApi { return null; } - static tabSendMessageData(tab: any, command: string, data: any = null): Promise { + static tabSendMessageData(tab: chrome.tabs.Tab, command: string, data: any = null): Promise { const obj: any = { command: command, }; @@ -60,7 +60,7 @@ export class BrowserApi { return BrowserApi.tabSendMessage(tab, obj); } - static async tabSendMessage(tab: any, obj: any, options: any = null): Promise { + static async tabSendMessage(tab: chrome.tabs.Tab, obj: any, options: chrome.tabs.MessageSendOptions = null): Promise { if (!tab || !tab.id) { return; } @@ -91,8 +91,8 @@ export class BrowserApi { chrome.tabs.create({ url: url, active: active }); } - static messageListener(name: string, callback: (message: any, sender: any, response: any) => void) { - chrome.runtime.onMessage.addListener((msg: any, sender: any, response: any) => { + static messageListener(name: string, callback: (message: any, sender: chrome.runtime.MessageSender, response: any) => void) { + chrome.runtime.onMessage.addListener((msg: any, sender: chrome.runtime.MessageSender, response: any) => { callback(msg, sender, response); }); } diff --git a/src/content/message_handler.ts b/src/content/message_handler.ts index 4358a97945c..e10834a3d6d 100644 --- a/src/content/message_handler.ts +++ b/src/content/message_handler.ts @@ -20,3 +20,11 @@ window.addEventListener('message', event => { }); } }, false); + +const forwardCommands = ['promptForLogin', 'addToLockedVaultPendingNotifications', 'unlockCompleted']; + +chrome.runtime.onMessage.addListener(event => { + if (forwardCommands.includes(event.command)) { + chrome.runtime.sendMessage(event); + } +}); diff --git a/src/content/notificationBar.ts b/src/content/notificationBar.ts index 103b819d6e3..da3f25d7d46 100644 --- a/src/content/notificationBar.ts +++ b/src/content/notificationBar.ts @@ -1,3 +1,6 @@ +import AddLoginRuntimeMessage from 'src/background/models/addLoginRuntimeMessage'; +import ChangePasswordRuntimeMessage from 'src/background/models/changePasswordRuntimeMessage'; + document.addEventListener('DOMContentLoaded', event => { if (window.location.hostname.indexOf('vault.bitwarden.com') > -1) { return; @@ -294,7 +297,7 @@ document.addEventListener('DOMContentLoaded', event => { } const disabledBoth = disabledChangedPasswordNotification && disabledAddLoginNotification; if (!disabledBoth && formData[i].usernameEl != null && formData[i].passwordEl != null) { - const login = { + const login: AddLoginRuntimeMessage = { username: formData[i].usernameEl.value, password: formData[i].passwordEl.value, url: document.URL, @@ -343,13 +346,15 @@ document.addEventListener('DOMContentLoaded', event => { if (newPass != null && curPass != null || (newPassOnly && newPass != null)) { processedForm(form); + + const changePasswordRuntimeMessage: ChangePasswordRuntimeMessage = { + newPassword: newPass, + currentPassword: curPass, + url: document.URL, + }; sendPlatformMessage({ command: 'bgChangedPassword', - data: { - newPassword: newPass, - currentPassword: curPass, - url: document.URL, - }, + data: changePasswordRuntimeMessage, }); break; } diff --git a/src/notification/bar.js b/src/notification/bar.js index 4cad508a057..fec7ed16f45 100644 --- a/src/notification/bar.js +++ b/src/notification/bar.js @@ -70,19 +70,6 @@ document.addEventListener('DOMContentLoaded', () => { command: 'bgAddSave', folder: folderId, }; - - if (isVaultLocked) { - sendPlatformMessage({ - command: 'promptForLogin' - }); - - sendPlatformMessage({ - command: 'addToLockedVaultPendingNotifications', - retryItem: bgAddSaveMessage - }); - return; - } - sendPlatformMessage(bgAddSaveMessage); }); @@ -114,18 +101,6 @@ document.addEventListener('DOMContentLoaded', () => { const bgChangeSaveMessage = { command: 'bgChangeSave' }; - - if (isVaultLocked) { - sendPlatformMessage({ - command: 'promptForLogin' - }); - - sendPlatformMessage({ - command: 'addToLockedVaultPendingNotifications', - retryItem: bgChangeSaveMessage, - }); - return; - } sendPlatformMessage(bgChangeSaveMessage); }); }