import { AppIdService } from "jslib-common/abstractions/appId.service" ; import { CryptoService } from "jslib-common/abstractions/crypto.service" ; import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service" ; import { I18nService } from "jslib-common/abstractions/i18n.service" ; import { MessagingService } from "jslib-common/abstractions/messaging.service" ; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.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 { Utils } from "jslib-common/misc/utils" ; import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey" ; import { BrowserApi } from "../browser/browserApi" ; import RuntimeBackground from "./runtime.background" ; const MessageValidTimeout = 10 * 1000 ; const EncryptionAlgorithm = "sha1" ; export class NativeMessagingBackground { private connected = false ; private connecting : boolean ; private port : browser . runtime . Port | chrome . runtime . Port ; private resolver : any = null ; private privateKey : ArrayBuffer = null ; private publicKey : ArrayBuffer = null ; private secureSetupResolve : any = null ; private sharedSecret : SymmetricCryptoKey ; private appId : string ; private validatingFingerprint : boolean ; constructor ( private storageService , private cryptoService , private cryptoFunctionService , private vaultTimeoutService , private runtimeBackground , private i18nService , private userService , private messagingService , private appIdService , private platformUtilsService ) { this . storageService . save ( ConstantsService . biometricFingerprintValidated , false ) ; if ( chrome ?. permissions ?. onAdded ) { chrome . permissions . onAdded . addListener ( ( permissions ) => { BrowserApi . reloadExtension ( null ) ; } ) ; } } async connect ( ) { this . appId = await this . appIdService . getAppId ( ) ; this . storageService . save ( ConstantsService . biometricFingerprintValidated , false ) ; return new Promise < void > ( ( resolve , reject ) => { this . port = BrowserApi . connectNative ( "com.8bit.bitwarden" ) ; this . connecting = true ; const connectedCallback = ( ) => { this . connected = true ; this . connecting = false ; resolve ( ) ; } ; if ( this . platformUtilsService . isSafari ( ) ) { connectedCallback ( ) ; } this . port . onMessage . addListener ( async ( message ) => { switch ( message . command ) { case "connected" : connectedCallback ( ) ; break ; case "disconnected" : if ( this . connecting ) { this . messagingService . send ( "showDialog" , { text : this . i18nService . t ( "startDesktopDesc" ) , title : this . i18nService . t ( "startDesktopTitle" ) , confirmText : this . i18nService . t ( "ok" ) , type : "error" , } ) ; reject ( ) ; } this . connected = false ; this . port . disconnect ( ) ; break ; case "setupEncryption" : if ( message . appId !== this . appId ) { return ; } const encrypted = Utils . fromB64ToArray ( message . sharedSecret ) ; const decrypted = await this . cryptoFunctionService . rsaDecrypt ( encrypted . buffer , this . privateKey , EncryptionAlgorithm ) ; if ( this . validatingFingerprint ) { this . validatingFingerprint = false ; this . storageService . save ( ConstantsService . biometricFingerprintValidated , true ) ; } this . sharedSecret = new SymmetricCryptoKey ( decrypted ) ; this . secureSetupResolve ( ) ; break ; case "invalidateEncryption" : if ( message . appId !== this . appId ) { return ; } this . sharedSecret = null ; this . privateKey = null ; this . connected = false ; this . messagingService . send ( "showDialog" , { text : this . i18nService . t ( "nativeMessagingInvalidEncryptionDesc" ) , title : this . i18nService . t ( "nativeMessagingInvalidEncryptionTitle" ) , confirmText : this . i18nService . t ( "ok" ) , type : "error" , } ) ; break ; case "verifyFingerprint" : { if ( this . sharedSecret == null ) { this . validatingFingerprint = true ; this . showFingerprintDialog ( ) ; } break ; } case "wrongUserId" : this . showWrongUserDialog ( ) ; default : if ( ! this . platformUtilsService . isSafari ( ) && message . appId !== this . appId ) { return ; } this . onMessage ( message . message ) ; } } ) ; this . port . onDisconnect . addListener ( ( p ) => { let error ; if ( BrowserApi . isWebExtensionsApi ) { error = p . error . message ; } else { error = chrome . runtime . lastError . message ; } if ( error != null ) { this . messagingService . send ( "showDialog" , { text : this . i18nService . t ( "desktopIntegrationDisabledDesc" ) , title : this . i18nService . t ( "desktopIntegrationDisabledTitle" ) , confirmText : this . i18nService . t ( "ok" ) , type : "error" , } ) ; } this . sharedSecret = null ; this . privateKey = null ; this . connected = false ; reject ( ) ; } ) ; } ) ; } showWrongUserDialog ( ) { this . messagingService . send ( "showDialog" , { text : this . i18nService . t ( "nativeMessagingWrongUserDesc" ) , title : this . i18nService . t ( "nativeMessagingWrongUserTitle" ) , confirmText : this . i18nService . t ( "ok" ) , type : "error" , } ) ; } async send ( message ) { if ( ! this . connected ) { await this . connect ( ) ; } if ( this . platformUtilsService . isSafari ( ) ) { this . postMessage ( message ) ; } else { this . postMessage ( { appId : this . appId , message : await this . encryptMessage ( message ) } ) ; } } async encryptMessage ( message ) { if ( this . sharedSecret == null ) { await this . secureCommunication ( ) ; } message . timestamp = Date . now ( ) ; return await this . cryptoService . encrypt ( JSON . stringify ( message ) , this . sharedSecret ) ; } getResponse ( ) { return new Promise ( ( resolve , reject ) => { this . resolver = resolve ; } ) ; } private postMessage ( message ) { try { this . port . postMessage ( message ) ; } catch ( e ) { console . error ( "NativeMessaging port disconnected, disconnecting." ) ; this . sharedSecret = null ; this . privateKey = null ; this . connected = false ; this . messagingService . send ( "showDialog" , { text : this . i18nService . t ( "nativeMessagingInvalidEncryptionDesc" ) , title : this . i18nService . t ( "nativeMessagingInvalidEncryptionTitle" ) , confirmText : this . i18nService . t ( "ok" ) , type : "error" , } ) ; } } private async onMessage ( rawMessage ) { let message = rawMessage ; if ( ! this . platformUtilsService . isSafari ( ) ) { message = JSON . parse ( await this . cryptoService . decryptToUtf8 ( rawMessage , this . sharedSecret ) ) ; } if ( Math . abs ( message . timestamp - Date . now ( ) ) > MessageValidTimeout ) { console . error ( "NativeMessage is to old, ignoring." ) ; return ; } switch ( message . command ) { case "biometricUnlock" : await this . storageService . remove ( ConstantsService . biometricAwaitingAcceptance ) ; if ( message . response === "not enabled" ) { this . messagingService . send ( "showDialog" , { text : this . i18nService . t ( "biometricsNotEnabledDesc" ) , title : this . i18nService . t ( "biometricsNotEnabledTitle" ) , confirmText : this . i18nService . t ( "ok" ) , type : "error" , } ) ; break ; } else if ( message . response === "not supported" ) { this . messagingService . send ( "showDialog" , { text : this . i18nService . t ( "biometricsNotSupportedDesc" ) , title : this . i18nService . t ( "biometricsNotSupportedTitle" ) , confirmText : this . i18nService . t ( "ok" ) , type : "error" , } ) ; break ; } const enabled = await this . storageService . get ( ConstantsService . biometricUnlockKey ) ; if ( enabled === null || enabled === false ) { if ( message . response === "unlocked" ) { await this . storageService . save ( ConstantsService . biometricUnlockKey , true ) ; } break ; } if ( ! this . vaultTimeoutService . biometricLocked ) { break ; } if ( message . response === "unlocked" ) { await this . cryptoService . setKey ( new SymmetricCryptoKey ( Utils . fromB64ToArray ( message . keyB64 ) . buffer ) ) ; try { await this . cryptoService . getFingerprint ( await this . userService . getUserId ( ) ) ; } catch ( e ) { console . error ( "Unable to verify key:" , e ) ; await this . cryptoService . clearKey ( ) ; this . showWrongUserDialog ( ) ; message = false ; break ; } this . vaultTimeoutService . biometricLocked = false ; this . runtimeBackground . processMessage ( { command : "unlocked" } , null , null ) ; } break ; default : console . error ( "NativeMessage, got unknown command: " , message . command ) ; } if ( this . resolver ) { this . resolver ( message ) ; } } private async secureCommunication ( ) { const [ publicKey , privateKey ] = await this . cryptoFunctionService . rsaGenerateKeyPair ( 2048 ) ; this . publicKey = publicKey ; this . privateKey = privateKey ; this . sendUnencrypted ( { command : "setupEncryption" , publicKey : Utils . fromBufferToB64 ( publicKey ) , userId : await this . userService . getUserId ( ) , } ) ; return new Promise ( ( resolve , reject ) => ( this . secureSetupResolve = resolve ) ) ; } private async sendUnencrypted ( message ) { if ( ! this . connected ) { await this . connect ( ) ; } message . timestamp = Date . now ( ) ; this . postMessage ( { appId : this . appId , message : message } ) ; } private async showFingerprintDialog ( ) { const fingerprint = ( await this . cryptoService . getFingerprint ( await this . userService . getUserId ( ) , this . publicKey ) ) . join ( " " ) ; this . messagingService . send ( "showDialog" , { html : `${ this . i18nService . t ( "desktopIntegrationVerificationText" ) }<br><br><strong>${ fingerprint }</strong>` , title : this . i18nService . t ( "desktopSyncVerificationTitle" ) , confirmText : this . i18nService . t ( "ok" ) , type : "warning" , } ) ; } }