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 { 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 { 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 , private autofillService , private cipherService , private storageService , private vaultTimeoutService , private policyService , private folderService , private userService ) { } async init ( ) { if ( chrome . runtime == null ) { return ; } BrowserApi . messageListener ( "notification.background" , async ( msg , sender ) => { await this . processMessage ( msg , sender ) ; } ) ; this . cleanupNotificationQueue ( ) ; } async processMessage ( msg , sender ) { 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 = { 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 = null ) { 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 ) ; } private doNotificationQueueCheck ( tab ) { 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 ) { 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 , tab ) { if ( ! ( await this . userService . isAuthenticated ( ) ) ) { return ; } const loginDomain = Utils . getDomain ( loginInfo . url ) ; if ( loginDomain == null ) { return ; } let normalizedUsername = loginInfo . username ; if ( normalizedUsername != null ) { normalizedUsername = normalizedUsername . toLowerCase ( ) ; } const disabledAddLogin = await this . storageService . get < boolean > ( ConstantsService . disableAddLoginNotificationKey ) ; if ( await this . vaultTimeoutService . isLocked ( ) ) { 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 ) ; if ( usernameMatches . length === 0 ) { 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 < boolean > ( ConstantsService . disableChangedPasswordNotificationKey ) ; if ( disabledChangePassword ) { return ; } this . pushChangePasswordToQueue ( usernameMatches [ 0 ] . id , loginDomain , loginInfo . password , tab ) ; } } private async pushAddLoginToQueue ( loginDomain , loginInfo , tab , isVaultLocked = false ) { this . removeTabFromNotificationQueue ( tab ) ; const message = { 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 ) , wasVaultLocked : isVaultLocked , } ; this . notificationQueue . push ( message ) ; await this . checkNotificationQueue ( tab ) ; } private async changedPassword ( changeData , 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 = 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 , loginDomain , newPassword , tab , isVaultLocked = false ) { this . removeTabFromNotificationQueue ( tab ) ; const message = { type : NotificationQueueMessageType . changePassword , cipherId : cipherId , newPassword : newPassword , domain : loginDomain , tabId : tab . id , expires : new Date ( new Date ( ) . getTime ( ) + 5 * 60000 ) , wasVaultLocked : isVaultLocked , } ; this . notificationQueue . push ( message ) ; await this . checkNotificationQueue ( tab ) ; } private async saveOrUpdateCredentials ( tab , folderId ? ) { 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 ( 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 , folderId ) { 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 ) { const cipher = await this . cipherService . get ( cipherId ) ; if ( cipher != null && cipher . type === CipherType . Login ) { return await cipher . decrypt ( ) ; } return null ; } private async updateCipher ( cipher , newPassword ) { 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 ) { 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 , responseCommand ) { const responseData = { } ; if ( responseCommand === "notificationBarGetFoldersList" ) { responseData . folders = await this . folderService . getAllDecrypted ( ) ; } await BrowserApi . tabSendMessageData ( tab , responseCommand , responseData ) ; } private async allowPersonalOwnership ( ) { return ! ( await this . policyService . policyAppliesToUser ( PolicyType . PersonalOwnership ) ) ; } }