diff --git a/jslib b/jslib index 72bf18f3690..cea09a22e53 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 72bf18f369068d36767794bdc0ca377f734cf373 +Subproject commit cea09a22e533ef3598bb497ba0503c2fcd5b2dc1 diff --git a/package-lock.json b/package-lock.json index fd6c462d7d9..d907f23b218 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1784,9 +1784,9 @@ "dev": true }, "bl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.0.tgz", - "integrity": "sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", + "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", "requires": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -1894,6 +1894,11 @@ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "dev": true }, + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" + }, "browser-resolve": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", diff --git a/package.json b/package.json index 113bccba15a..764d5e0c48a 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "angular2-toaster": "8.0.0", "angulartics2": "9.1.0", "big-integer": "1.6.36", + "browser-process-hrtime": "1.0.0", "core-js": "2.6.2", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", "font-awesome": "4.7.0", diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 946dba4b988..cdcbd647369 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -606,6 +606,9 @@ "message": "WARNING", "description": "WARNING (should stay in capitalized letters if the language permits)" }, + "confirmVaultExport": { + "message": "Confirm Vault Export" + }, "exportWarningDesc": { "message": "This export contains your vault data in an unencrypted format. You should not store or send the exported file over unsecure channels (such as email). Delete it immediately after you are done using it." }, @@ -1381,6 +1384,9 @@ "privacyPolicy": { "message": "Privacy Policy" }, + "hintEqualsPassword": { + "message": "Your password hint cannot be the same as your password." + }, "ok": { "message": "Ok" }, @@ -1412,9 +1418,24 @@ "message": "Desktop application invalidated the secure communication channel. Please retry this operation" }, "nativeMessagingInvalidEncryptionTitle": { - "message": "Desktop communication interupted" + "message": "Desktop communication interrupted" + }, + "biometricsNotEnabledTitle": { + "message": "Biometrics not enabled" + }, + "biometricsNotEnabledDesc": { + "message": "Browser biometrics requires desktop biometric to be enabled in the settings first." + }, + "biometricsNotSupportedTitle": { + "message": "Biometrics not supported" + }, + "biometricsNotSupportedDesc": { + "message": "Browser biometrics is not supported on this device." }, "personalOwnershipSubmitError": { "message": "Due to an Enterprise Policy, you are restricted from saving items to your personal vault. Change the Ownership option to an organization and choose from available Collections." + }, + "personalOwnershipPolicyInEffect": { + "message": "An organization policy is affecting your ownership options." } } diff --git a/src/background/main.background.ts b/src/background/main.background.ts index c35cb0cabd0..82d1f739a6a 100644 --- a/src/background/main.background.ts +++ b/src/background/main.background.ts @@ -21,6 +21,7 @@ import { UserService, VaultTimeoutService, } from 'jslib/services'; +import { ConsoleLogService } from 'jslib/services/consoleLog.service'; import { EventService } from 'jslib/services/event.service'; import { ExportService } from 'jslib/services/export.service'; import { NotificationsService } from 'jslib/services/notifications.service'; @@ -93,6 +94,7 @@ export default class MainBackground { i18nService: I18nServiceAbstraction; platformUtilsService: PlatformUtilsServiceAbstraction; constantsService: ConstantsService; + consoleLogService: ConsoleLogService; cryptoService: CryptoServiceAbstraction; cryptoFunctionService: CryptoFunctionServiceAbstraction; tokenService: TokenServiceAbstraction; @@ -169,8 +171,9 @@ export default class MainBackground { this.secureStorageService = new BrowserStorageService(); this.i18nService = new I18nService(BrowserApi.getUILanguage(window)); this.cryptoFunctionService = new WebCryptoFunctionService(window, this.platformUtilsService); + this.consoleLogService = new ConsoleLogService(false); this.cryptoService = new CryptoService(this.storageService, this.secureStorageService, - this.cryptoFunctionService, this.platformUtilsService); + this.cryptoFunctionService, this.platformUtilsService, this.consoleLogService); this.tokenService = new TokenService(this.storageService); this.appIdService = new AppIdService(this.storageService); this.apiService = new ApiService(this.tokenService, this.platformUtilsService, @@ -178,7 +181,7 @@ export default class MainBackground { this.userService = new UserService(this.tokenService, this.storageService); this.authService = new AuthService(this.cryptoService, this.apiService, this.userService, this.tokenService, this.appIdService, this.i18nService, this.platformUtilsService, - this.messagingService, this.vaultTimeoutService); + this.messagingService, this.vaultTimeoutService, this.consoleLogService); this.settingsService = new SettingsService(this.userService, this.storageService); this.cipherService = new CipherService(this.cryptoService, this.userService, this.settingsService, this.apiService, this.storageService, this.i18nService, () => this.searchService); @@ -186,7 +189,7 @@ export default class MainBackground { this.storageService, this.i18nService, this.cipherService); this.collectionService = new CollectionService(this.cryptoService, this.userService, this.storageService, this.i18nService); - this.searchService = new SearchService(this.cipherService); + this.searchService = new SearchService(this.cipherService, this.consoleLogService); this.sendService = new SendService(this.cryptoService, this.userService, this.apiService, this.storageService, this.i18nService, this.cryptoFunctionService); this.stateService = new StateService(); @@ -220,7 +223,7 @@ export default class MainBackground { this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); this.exportService = new ExportService(this.folderService, this.cipherService, this.apiService); this.notificationsService = new NotificationsService(this.userService, this.syncService, this.appIdService, - this.apiService, this.vaultTimeoutService, () => this.logout(true)); + this.apiService, this.vaultTimeoutService, () => this.logout(true), this.consoleLogService); this.environmentService = new EnvironmentService(this.apiService, this.storageService, this.notificationsService); this.analytics = new Analytics(window, () => BrowserApi.gaFilter(), this.platformUtilsService, @@ -245,7 +248,7 @@ export default class MainBackground { this.analytics, this.notificationsService, this.systemService, this.vaultTimeoutService, this.environmentService, this.policyService, this.userService); this.nativeMessagingBackground = new NativeMessagingBackground(this.storageService, this.cryptoService, this.cryptoFunctionService, - this.vaultTimeoutService, this.runtimeBackground, this.i18nService, this.userService, this.messagingService); + this.vaultTimeoutService, this.runtimeBackground, this.i18nService, this.userService, this.messagingService, this.appIdService); this.commandsBackground = new CommandsBackground(this, this.passwordGenerationService, this.platformUtilsService, this.analytics, this.vaultTimeoutService); diff --git a/src/background/nativeMessaging.background.ts b/src/background/nativeMessaging.background.ts index 88397c05389..ddd9317da83 100644 --- a/src/background/nativeMessaging.background.ts +++ b/src/background/nativeMessaging.background.ts @@ -1,4 +1,5 @@ import { ConstantsService } from 'jslib/services/constants.service'; +import { AppIdService } from 'jslib/abstractions/appId.service'; import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service'; import { CryptoService } from 'jslib/abstractions/crypto.service'; import { I18nService } from 'jslib/abstractions/i18n.service'; @@ -23,15 +24,19 @@ export class NativeMessagingBackground { private resolver: any = null; private privateKey: ArrayBuffer = null; + private publicKey: ArrayBuffer = null; private secureSetupResolve: any = null; private sharedSecret: SymmetricCryptoKey; + private appId: string; constructor(private storageService: StorageService, private cryptoService: CryptoService, private cryptoFunctionService: CryptoFunctionService, private vaultTimeoutService: VaultTimeoutService, private runtimeBackground: RuntimeBackground, private i18nService: I18nService, private userService: UserService, - private messagingService: MessagingService) {} + private messagingService: MessagingService, private appIdService: AppIdService) {} async connect() { + this.appId = await this.appIdService.getAppId(); + return new Promise((resolve, reject) => { this.port = BrowserApi.connectNative('com.8bit.bitwarden'); @@ -58,6 +63,11 @@ export class NativeMessagingBackground { this.port.disconnect(); break; case 'setupEncryption': + // Ignore since it belongs to another device + if (message.appId !== this.appId) { + return; + } + const encrypted = Utils.fromB64ToArray(message.sharedSecret); const decrypted = await this.cryptoFunctionService.rsaDecrypt(encrypted.buffer, this.privateKey, EncryptionAlgorithm); @@ -65,6 +75,11 @@ export class NativeMessagingBackground { this.secureSetupResolve(); break; case 'invalidateEncryption': + // Ignore since it belongs to another device + if (message.appId !== this.appId) { + return; + } + this.sharedSecret = null; this.privateKey = null; this.connected = false; @@ -75,8 +90,20 @@ export class NativeMessagingBackground { confirmText: this.i18nService.t('ok'), type: 'error', }); + break; + case 'verifyFingerprint': { + if (this.sharedSecret == null) { + this.showFingerprintDialog(); + } + break; + } default: - this.onMessage(message); + // Ignore since it belongs to another device + if (message.appId !== this.appId) { + return; + } + + this.onMessage(message.message); } }); @@ -88,9 +115,7 @@ export class NativeMessagingBackground { error = chrome.runtime.lastError.message; } - if (error === 'Specified native messaging host not found.' || - error === 'Access to the specified native messaging host is forbidden.' || - error === 'An unexpected error occurred') { + if (error != null) { this.messagingService.send('showDialog', { text: this.i18nService.t('desktopIntegrationDisabledDesc'), title: this.i18nService.t('desktopIntegrationDisabledTitle'), @@ -118,7 +143,7 @@ export class NativeMessagingBackground { message.timestamp = Date.now(); const encrypted = await this.cryptoService.encrypt(JSON.stringify(message), this.sharedSecret); - this.port.postMessage(encrypted); + this.postMessage({appId: this.appId, message: encrypted}); } getResponse(): Promise { @@ -127,6 +152,27 @@ export class NativeMessagingBackground { }); } + private postMessage(message: any) { + // Wrap in try-catch to when the port disconnected without triggering `onDisconnect`. + try { + this.port.postMessage(message); + } catch (e) { + // tslint:disable-next-line + 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: any) { const message = JSON.parse(await this.cryptoService.decryptToUtf8(rawMessage, this.sharedSecret)); @@ -140,6 +186,24 @@ export class NativeMessagingBackground { 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') { @@ -171,17 +235,10 @@ export class NativeMessagingBackground { 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)}); - const fingerprint = (await this.cryptoService.getFingerprint(await this.userService.getUserId(), publicKey)).join(' '); - - this.messagingService.send('showDialog', { - html: `${this.i18nService.t('desktopIntegrationVerificationText')}

${fingerprint}`, - title: this.i18nService.t('desktopSyncVerificationTitle'), - confirmText: this.i18nService.t('ok'), - type: 'warning', - }); return new Promise((resolve, reject) => this.secureSetupResolve = resolve); } @@ -193,6 +250,17 @@ export class NativeMessagingBackground { message.timestamp = Date.now(); - this.port.postMessage(message); + 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')}

${fingerprint}`, + title: this.i18nService.t('desktopSyncVerificationTitle'), + confirmText: this.i18nService.t('ok'), + type: 'warning', + }); } } diff --git a/src/background/runtime.background.ts b/src/background/runtime.background.ts index 2ab3185df49..53df4e978c4 100644 --- a/src/background/runtime.background.ts +++ b/src/background/runtime.background.ts @@ -455,7 +455,7 @@ export default class RuntimeBackground { if (policy.enabled) { const org = await this.userService.getOrganization(policy.organizationId); if (org != null && org.enabled && org.usePolicies && !org.isAdmin - && org.status == OrganizationUserStatusType.Confirmed) { + && org.status === OrganizationUserStatusType.Confirmed) { return false; } } diff --git a/src/popup/accounts/sso.component.ts b/src/popup/accounts/sso.component.ts index c88f3d1e600..0867b781a70 100644 --- a/src/popup/accounts/sso.component.ts +++ b/src/popup/accounts/sso.component.ts @@ -41,11 +41,11 @@ export class SsoComponent extends BaseSsoComponent { this.redirectUri = url + '/sso-connector.html'; this.clientId = 'browser'; - super.onSuccessfulLogin = () => { + super.onSuccessfulLogin = async () => { + await syncService.fullSync(true); BrowserApi.reloadOpenWindows(); const thisWindow = window.open('', '_self'); thisWindow.close(); - return syncService.fullSync(true); }; } } diff --git a/src/popup/app-routing.animations.ts b/src/popup/app-routing.animations.ts index 05e3197605e..0c44460d62e 100644 --- a/src/popup/app-routing.animations.ts +++ b/src/popup/app-routing.animations.ts @@ -150,6 +150,9 @@ export const routerTransition = trigger('routerTransition', [ transition('view-cipher => clone-cipher', inSlideUp), transition('clone-cipher => view-cipher, clone-cipher => tabs', outSlideDown), + transition('view-cipher => share-cipher', inSlideUp), + transition('share-cipher => view-cipher', outSlideDown), + transition('tabs => add-cipher', inSlideUp), transition('add-cipher => tabs', outSlideDown), @@ -159,9 +162,6 @@ export const routerTransition = trigger('routerTransition', [ transition('add-cipher => generator, edit-cipher => generator, clone-cipher => generator', inSlideUp), transition('generator => add-cipher, generator => edit-cipher, generator => clone-cipher', outSlideDown), - transition('edit-cipher => share-cipher', inSlideUp), - transition('share-cipher => edit-cipher, share-cipher => view-cipher', outSlideDown), - transition('edit-cipher => attachments, edit-cipher => collections', inSlideLeft), transition('attachments => edit-cipher, collections => edit-cipher', outSlideRight), diff --git a/src/popup/components/action-buttons.component.html b/src/popup/components/action-buttons.component.html index f6c225f51b8..5d81817e9ca 100644 --- a/src/popup/components/action-buttons.component.html +++ b/src/popup/components/action-buttons.component.html @@ -16,6 +16,11 @@ [ngClass]="{disabled: (!cipher.login.password || !cipher.viewPassword)}"> + + + ('cryptoSe getBgService('apiService')(), getBgService('userService')(), getBgService('tokenService')(), getBgService('appIdService')(), getBgService('i18nService')(), getBgService('platformUtilsService')(), - messagingService, getBgService('vaultTimeoutService')()); + messagingService, getBgService('vaultTimeoutService')(), getBgService('consoleLogService')()); export const searchService = new PopupSearchService(getBgService('searchService')(), - getBgService('cipherService')()); + getBgService('cipherService')(), getBgService('consoleLogService')()); -export function initFactory(i18nService: I18nService, storageService: StorageService, +export function initFactory(platformUtilsService: PlatformUtilsService, i18nService: I18nService, storageService: StorageService, popupUtilsService: PopupUtilsService): Function { return async () => { if (!popupUtilsService.inPopup(window)) { @@ -90,7 +91,12 @@ export function initFactory(i18nService: I18nService, storageService: StorageSer let theme = await storageService.get(ConstantsService.themeKey); if (theme == null) { - theme = 'light'; + theme = platformUtilsService.getDefaultSystemTheme(); + + platformUtilsService.onDefaultSystemThemeChange((theme) => { + window.document.documentElement.classList.remove('theme_light', 'theme_dark'); + window.document.documentElement.classList.add('theme_' + theme); + }); } window.document.documentElement.classList.add('locale_' + i18nService.translationLocale); window.document.documentElement.classList.add('theme_' + theme); @@ -171,7 +177,7 @@ export function initFactory(i18nService: I18nService, storageService: StorageSer { provide: APP_INITIALIZER, useFactory: initFactory, - deps: [I18nService, StorageService, PopupUtilsService], + deps: [PlatformUtilsService, I18nService, StorageService, PopupUtilsService], multi: true, }, { diff --git a/src/popup/settings/export.component.html b/src/popup/settings/export.component.html index f4a90062c9f..759fbec53ce 100644 --- a/src/popup/settings/export.component.html +++ b/src/popup/settings/export.component.html @@ -41,13 +41,6 @@ diff --git a/src/popup/vault/add-edit.component.html b/src/popup/vault/add-edit.component.html index 65f344e29b2..a94e8217dac 100644 --- a/src/popup/vault/add-edit.component.html +++ b/src/popup/vault/add-edit.component.html @@ -14,6 +14,9 @@ + + {{'personalOwnershipPolicyInEffect' | i18n}} +
{{'itemInformation' | i18n}} @@ -374,15 +377,6 @@
- -
- - {{'shareItem' | i18n}} -
-
diff --git a/src/popup/vault/add-edit.component.ts b/src/popup/vault/add-edit.component.ts index fdecd141d8b..0a32bdf89a1 100644 --- a/src/popup/vault/add-edit.component.ts +++ b/src/popup/vault/add-edit.component.ts @@ -118,12 +118,6 @@ export class AddEditComponent extends BaseAddEditComponent { this.router.navigate(['/attachments'], { queryParams: { cipherId: this.cipher.id } }); } - share() { - super.share(); - if (this.cipher.organizationId == null) { - this.router.navigate(['/share-cipher'], { queryParams: { cipherId: this.cipher.id } }); - } - } editCollections() { super.editCollections(); diff --git a/src/popup/vault/share.component.ts b/src/popup/vault/share.component.ts index 4fb14ea6c1a..230e27ac86c 100644 --- a/src/popup/vault/share.component.ts +++ b/src/popup/vault/share.component.ts @@ -41,14 +41,12 @@ export class ShareComponent extends BaseShareComponent { async submit(): Promise { const success = await super.submit(); if (success) { - window.setTimeout(() => { - this.location.back(); - }, 200); + this.cancel(); } return success; } cancel() { - this.location.back(); + this.router.navigate(['/view-cipher'], { replaceUrl: true, queryParams: { cipherId: this.cipher.id } }); } } diff --git a/src/popup/vault/view.component.html b/src/popup/vault/view.component.html index 439b3f2bfac..524fc735e9d 100644 --- a/src/popup/vault/view.component.html +++ b/src/popup/vault/view.component.html @@ -295,6 +295,14 @@ {{'cloneItem' | i18n}}
+ +
+ + {{'shareItem' | i18n}} +
+