diff --git a/src/background/main.background.ts b/src/background/main.background.ts index b52b8cda8c4..f3538028db1 100644 --- a/src/background/main.background.ts +++ b/src/background/main.background.ts @@ -374,10 +374,6 @@ export default class MainBackground { return; } - if (await this.vaultTimeoutService.isLocked()) { - return; - } - const options: any = {}; if (frameId != null) { options.frameId = frameId; @@ -680,13 +676,20 @@ export default class MainBackground { 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; diff --git a/src/background/models/addChangePasswordQueueMessage.ts b/src/background/models/addChangePasswordQueueMessage.ts new file mode 100644 index 00000000000..ad1efe3da33 --- /dev/null +++ b/src/background/models/addChangePasswordQueueMessage.ts @@ -0,0 +1,9 @@ +export default class AddChangePasswordQueueMessage { + type: string; + 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 new file mode 100644 index 00000000000..246dca03a4c --- /dev/null +++ b/src/background/models/addLoginQueueMessage.ts @@ -0,0 +1,10 @@ +export default class AddLoginQueueMessage { + type: string; + username: string; + password: string; + domain: string; + uri: string; + tabId: string; + expires: Date; + wasVaultLocked: boolean; +} diff --git a/src/background/runtime.background.ts b/src/background/runtime.background.ts index 3034c66f58b..a46d174aac6 100644 --- a/src/background/runtime.background.ts +++ b/src/background/runtime.background.ts @@ -27,12 +27,17 @@ import { Utils } from 'jslib-common/misc/utils'; import { PolicyType } from 'jslib-common/enums/policyType'; +import AddChangePasswordQueueMessage from './models/addChangePasswordQueueMessage'; +import AddLoginQueueMessage from './models/addLoginQueueMessage'; + export default class RuntimeBackground { private runtime: any; private autofillTimeout: any; private pageDetailsToAutoFill: any[] = []; private onInstalledReason: string = null; + private lockedVaultPendingNotifications: any[] = []; + constructor(private main: MainBackground, private autofillService: AutofillService, private cipherService: CipherService, private platformUtilsService: BrowserPlatformUtilsService, private storageService: StorageService, private i18nService: I18nService, @@ -67,6 +72,24 @@ export default class RuntimeBackground { 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); + } + } + break; + case 'addToLockedVaultPendingNotifications': + const retryMessage = { + msg: msg.retryItem, + sender: sender, + }; + this.lockedVaultPendingNotifications.push(retryMessage); break; case 'logout': await this.main.logout(msg.expired); @@ -79,15 +102,15 @@ export default class RuntimeBackground { case 'openPopup': await this.main.openPopup(); break; + case 'promptForLogin': + await BrowserApi.createNewTab('popup/index.html?uilocation=popout', true, true); + break; case 'showDialogResolve': this.platformUtilsService.resolveDialogPromise(msg.dialogId, msg.confirmed); break; case 'bgGetDataForTab': await this.getDataForTab(sender.tab, msg.responseCommand); break; - case 'bgOpenNotificationBar': - await BrowserApi.tabSendMessageData(sender.tab, 'openNotificationBar', msg.data); - break; case 'bgCloseNotificationBar': await BrowserApi.tabSendMessageData(sender.tab, 'closeNotificationBar'); break; @@ -108,10 +131,8 @@ export default class RuntimeBackground { this.removeTabFromNotificationQueue(sender.tab); break; case 'bgAddSave': - await this.saveAddLogin(sender.tab, msg.folder); - break; case 'bgChangeSave': - await this.saveChangePassword(sender.tab); + await this.saveOrUpdateCredentials(sender.tab, msg.folder); break; case 'bgNeverSave': await this.saveNever(sender.tab); @@ -126,9 +147,6 @@ export default class RuntimeBackground { await this.main.reseedStorage(); break; case 'collectPageDetailsResponse': - if (await this.vaultTimeoutService.isLocked()) { - return; - } switch (msg.sender) { case 'notificationBar': const forms = this.autofillService.getFormsWithPasswordFields(msg.details); @@ -219,14 +237,11 @@ export default class RuntimeBackground { this.pageDetailsToAutoFill = []; } - private async saveAddLogin(tab: any, folderId: string) { - if (await this.vaultTimeoutService.isLocked()) { - return; - } - + 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') { + if (queueMessage.tabId !== tab.id || + (queueMessage.type !== 'addLogin' && queueMessage.type !== 'changePassword')) { continue; } @@ -238,56 +253,74 @@ export default class RuntimeBackground { this.main.notificationQueue.splice(i, 1); BrowserApi.tabSendMessageData(tab, 'closeNotificationBar'); - 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; + 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; } - const cipher = await this.cipherService.encrypt(model); - await this.cipherService.saveWithServer(cipher); + 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 saveChangePassword(tab: any) { - if (await this.vaultTimeoutService.isLocked()) { - return; + 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; + } } - for (let i = this.main.notificationQueue.length - 1; i >= 0; i--) { - const queueMessage = this.main.notificationQueue[i]; - if (queueMessage.tabId !== tab.id || queueMessage.type !== 'changePassword') { - continue; - } + const cipher = await this.cipherService.encrypt(model); + await this.cipherService.saveWithServer(cipher); + } - const tabDomain = Utils.getDomain(tab.url); - if (tabDomain != null && tabDomain !== queueMessage.domain) { - continue; - } + 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; + } - this.main.notificationQueue.splice(i, 1); - BrowserApi.tabSendMessageData(tab, 'closeNotificationBar'); - - const cipher = await this.cipherService.get(queueMessage.cipherId); - if (cipher != null && cipher.type === CipherType.Login) { - const model = await cipher.decrypt(); - model.login.password = queueMessage.newPassword; - const newCipher = await this.cipherService.encrypt(model); - await this.cipherService.saveWithServer(newCipher); - } + 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); } } @@ -312,10 +345,6 @@ export default class RuntimeBackground { } private async addLogin(loginInfo: any, tab: any) { - if (await this.vaultTimeoutService.isLocked()) { - return; - } - const loginDomain = Utils.getDomain(loginInfo.url); if (loginDomain == null) { return; @@ -326,6 +355,11 @@ export default class RuntimeBackground { 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); @@ -340,35 +374,43 @@ export default class RuntimeBackground { return; } - // remove any old messages for this tab - this.removeTabFromNotificationQueue(tab); - this.main.notificationQueue.push({ - type: 'addLogin', - username: loginInfo.username, - password: loginInfo.password, - domain: loginDomain, - uri: loginInfo.url, - tabId: tab.id, - expires: new Date((new Date()).getTime() + 30 * 60000), // 30 minutes - }); - await this.main.checkNotificationQueue(tab); + 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.addChangedPasswordToQueue(usernameMatches[0].id, loginDomain, loginInfo.password, tab); + 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) { - if (await this.vaultTimeoutService.isLocked()) { + const loginDomain = Utils.getDomain(changeData.url); + if (loginDomain == null) { return; } - const loginDomain = Utils.getDomain(changeData.url); - if (loginDomain == null) { + if (await this.vaultTimeoutService.isLocked()) { + this.pushChangePasswordToQueue(null, loginDomain, changeData.newPassword, tab, true); return; } @@ -383,21 +425,23 @@ export default class RuntimeBackground { id = ciphers[0].id; } if (id != null) { - this.addChangedPasswordToQueue(id, loginDomain, changeData.newPassword, tab); + this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, tab); } } - private async addChangedPasswordToQueue(cipherId: string, loginDomain: string, newPassword: string, tab: any) { + private async pushChangePasswordToQueue(cipherId: string, loginDomain: string, newPassword: string, tab: any, isVaultLocked: boolean = false) { // remove any old messages for this tab this.removeTabFromNotificationQueue(tab); - this.main.notificationQueue.push({ + const message: AddChangePasswordQueueMessage = { type: 'changePassword', cipherId: cipherId, newPassword: newPassword, domain: loginDomain, tabId: tab.id, - expires: new Date((new Date()).getTime() + 30 * 60000), // 30 minutes - }); + expires: new Date((new Date()).getTime() + 5 * 60000), // 5 minutes + wasVaultLocked: isVaultLocked, + }; + this.main.notificationQueue.push(message); await this.main.checkNotificationQueue(tab); } @@ -438,29 +482,7 @@ export default class RuntimeBackground { private async getDataForTab(tab: any, responseCommand: string) { const responseData: any = {}; - if (responseCommand === 'notificationBarDataResponse') { - responseData.neverDomains = await this.storageService.get(ConstantsService.neverDomainsKey); - const disableAddLoginFromOptions = await this.storageService.get( - ConstantsService.disableAddLoginNotificationKey); - responseData.disabledAddLoginNotification = disableAddLoginFromOptions || !(await this.allowPersonalOwnership()); - responseData.disabledChangedPasswordNotification = await this.storageService.get( - ConstantsService.disableChangedPasswordNotificationKey); - } else if (responseCommand === 'autofillerAutofillOnPageLoadEnabledResponse') { - responseData.autofillEnabled = await this.storageService.get( - ConstantsService.enableAutoFillOnPageLoadKey); - } else if (responseCommand === 'notificationBarFrameDataResponse') { - responseData.i18n = { - appName: this.i18nService.t('appName'), - close: this.i18nService.t('close'), - yes: this.i18nService.t('yes'), - never: this.i18nService.t('never'), - notificationAddSave: this.i18nService.t('notificationAddSave'), - notificationNeverSave: this.i18nService.t('notificationNeverSave'), - notificationAddDesc: this.i18nService.t('notificationAddDesc'), - notificationChangeSave: this.i18nService.t('notificationChangeSave'), - notificationChangeDesc: this.i18nService.t('notificationChangeDesc'), - }; - } else if (responseCommand === 'notificationBarGetFoldersList') { + if (responseCommand === 'notificationBarGetFoldersList') { responseData.folders = await this.folderService.getAllDecrypted(); } diff --git a/src/browser/browserApi.ts b/src/browser/browserApi.ts index c52325b20cd..648394c9bdc 100644 --- a/src/browser/browserApi.ts +++ b/src/browser/browserApi.ts @@ -11,27 +11,27 @@ export class BrowserApi { static isFirefoxOnAndroid: boolean = navigator.userAgent.indexOf('Firefox/') !== -1 && navigator.userAgent.indexOf('Android') !== -1; - static async getTabFromCurrentWindowId(): Promise { + static async getTabFromCurrentWindowId(): Promise | null { return await BrowserApi.tabsQueryFirst({ active: true, windowId: chrome.windows.WINDOW_ID_CURRENT, }); } - static async getTabFromCurrentWindow(): Promise { + static async getTabFromCurrentWindow(): Promise | null { return await BrowserApi.tabsQueryFirst({ active: true, currentWindow: true, }); } - static async getActiveTabs(): Promise { + static async getActiveTabs(): Promise { return await BrowserApi.tabsQuery({ active: true, }); } - static async tabsQuery(options: any): Promise { + static async tabsQuery(options: chrome.tabs.QueryInfo): Promise { return new Promise(resolve => { chrome.tabs.query(options, (tabs: any[]) => { resolve(tabs); @@ -39,7 +39,7 @@ export class BrowserApi { }); } - static async tabsQueryFirst(options: any): Promise { + static async tabsQueryFirst(options: chrome.tabs.QueryInfo): Promise | null { const tabs = await BrowserApi.tabsQuery(options); if (tabs.length > 0) { return tabs[0]; @@ -97,6 +97,26 @@ export class BrowserApi { }); } + static async closeLoginTab() { + const tabs = await BrowserApi.tabsQuery({ + active: true, + title: 'Bitwarden', + windowType: 'normal', + currentWindow: true, + }); + + if (tabs.length === 0) { + return; + } + + const tabToClose = tabs[tabs.length - 1].id; + chrome.tabs.remove(tabToClose); + } + + static async focusSpecifiedTab(tabId: number) { + chrome.tabs.update(tabId, { active: true, highlighted: true }); + } + static closePopup(win: Window) { if (BrowserApi.isWebExtensionsApi && BrowserApi.isFirefoxOnAndroid) { // Reactivating the active tab dismisses the popup tab. The promise final diff --git a/src/content/notificationBar.ts b/src/content/notificationBar.ts index a3e215d411d..103b819d6e3 100644 --- a/src/content/notificationBar.ts +++ b/src/content/notificationBar.ts @@ -431,23 +431,11 @@ document.addEventListener('DOMContentLoaded', event => { function closeExistingAndOpenBar(type: string, typeData: any) { let barPage = 'notification/bar.html'; switch (type) { - case 'info': - barPage = barPage + '?info=' + typeData.text; - break; - case 'warning': - barPage = barPage + '?warning=' + typeData.text; - break; - case 'error': - barPage = barPage + '?error=' + typeData.text; - break; - case 'success': - barPage = barPage + '?success=' + typeData.text; - break; case 'add': - barPage = barPage + '?add=1'; + barPage = barPage + '?add=1&isVaultLocked=' + typeData.isVaultLocked; break; case 'change': - barPage = barPage + '?change=1'; + barPage = barPage + '?change=1&isVaultLocked=' + typeData.isVaultLocked; break; default: break; diff --git a/src/images/close.png b/src/images/close.png new file mode 100644 index 00000000000..ccbb3b1d5c7 Binary files /dev/null and b/src/images/close.png differ diff --git a/src/manifest.json b/src/manifest.json index 316a943203c..15b78418a6c 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -128,7 +128,10 @@ } }, "web_accessible_resources": [ - "notification/bar.html" + "notification/bar.html", + "images/icon38.png", + "images/icon38_locked.png", + "images/close.png" ], "applications": { "gecko": { diff --git a/src/notification/bar.html b/src/notification/bar.html index b3fc8b9312a..5e8962c1a59 100644 --- a/src/notification/bar.html +++ b/src/notification/bar.html @@ -1,20 +1,22 @@  + Bitwarden +
@@ -33,7 +35,7 @@ -
+ diff --git a/src/notification/bar.js b/src/notification/bar.js index ce504b6e235..4cad508a057 100644 --- a/src/notification/bar.js +++ b/src/notification/bar.js @@ -20,6 +20,14 @@ document.addEventListener('DOMContentLoaded', () => { setTimeout(load, 50); function load() { + const isVaultLocked = getQueryVariable('isVaultLocked') == 'true'; + document.getElementById('logo').src = isVaultLocked + ? chrome.runtime.getURL('images/icon38_locked.png') + : chrome.runtime.getURL('images/icon38.png'); + + document.getElementById('close').src = chrome.runtime.getURL('images/close.png'); + document.getElementById('close').alt = i18n.close; + var closeButton = document.getElementById('close-button'), body = document.querySelector('body'), bodyRect = body.getBoundingClientRect(); @@ -39,7 +47,7 @@ document.addEventListener('DOMContentLoaded', () => { } else { document.querySelector('#template-add .add-save').textContent = i18n.notificationAddSave; document.querySelector('#template-add .never-save').textContent = i18n.notificationNeverSave; - document.querySelector('#template-add .select-folder').style.display = 'initial'; + document.querySelector('#template-add .select-folder').style.display = isVaultLocked ? 'none' : 'initial'; document.querySelector('#template-add .select-folder').setAttribute('aria-label', i18n.folder); document.querySelector('#template-change .change-save').textContent = i18n.notificationChangeSave; } @@ -55,11 +63,27 @@ document.addEventListener('DOMContentLoaded', () => { addButton.addEventListener('click', (e) => { e.preventDefault(); + const folderId = document.querySelector('#template-add-clone .select-folder').value; - sendPlatformMessage({ + + const bgAddSaveMessage = { command: 'bgAddSave', folder: folderId, - }); + }; + + if (isVaultLocked) { + sendPlatformMessage({ + command: 'promptForLogin' + }); + + sendPlatformMessage({ + command: 'addToLockedVaultPendingNotifications', + retryItem: bgAddSaveMessage + }); + return; + } + + sendPlatformMessage(bgAddSaveMessage); }); neverButton.addEventListener('click', (e) => { @@ -69,28 +93,41 @@ document.addEventListener('DOMContentLoaded', () => { }); }); - const responseFoldersCommand = 'notificationBarGetFoldersList'; - chrome.runtime.onMessage.addListener((msg) => { - if (msg.command === responseFoldersCommand && msg.data) { - fillSelectorWithFolders(msg.data.folders); - } - }); - sendPlatformMessage({ - command: 'bgGetDataForTab', - responseCommand: responseFoldersCommand - }); + if (!isVaultLocked) { + const responseFoldersCommand = 'notificationBarGetFoldersList'; + chrome.runtime.onMessage.addListener((msg) => { + if (msg.command === responseFoldersCommand && msg.data) { + fillSelectorWithFolders(msg.data.folders); + } + }); + sendPlatformMessage({ + command: 'bgGetDataForTab', + responseCommand: responseFoldersCommand + }); + } } else if (getQueryVariable('change')) { setContent(document.getElementById('template-change')); var changeButton = document.querySelector('#template-change-clone .change-save'); changeButton.addEventListener('click', (e) => { e.preventDefault(); - sendPlatformMessage({ + + const bgChangeSaveMessage = { command: 'bgChangeSave' - }); + }; + + if (isVaultLocked) { + sendPlatformMessage({ + command: 'promptForLogin' + }); + + sendPlatformMessage({ + command: 'addToLockedVaultPendingNotifications', + retryItem: bgChangeSaveMessage, + }); + return; + } + sendPlatformMessage(bgChangeSaveMessage); }); - } else if (getQueryVariable('info')) { - setContent(document.getElementById('template-alert')); - document.getElementById('template-alert-clone').textContent = getQueryVariable('info'); } closeButton.addEventListener('click', (e) => { diff --git a/src/popup/scss/base.scss b/src/popup/scss/base.scss index 8dd54509f16..2cda55e9b5d 100644 --- a/src/popup/scss/base.scss +++ b/src/popup/scss/base.scss @@ -346,6 +346,16 @@ app-root { } } +@media only screen and (min-width: 601px) { + app-lock header { + padding: 0 calc((100% - 500px) / 2); + } + + app-lock content { + padding: 0 calc((100% - 500px) / 2); + } +} + content { position: absolute; top: 44px;