1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 06:43:35 +00:00

Merge branch 'master' of github.com:bitwarden/browser into feature/safari-webext

# Conflicts:
#	src/browser/safariApp.ts
#	src/safari/safari/SafariExtensionViewController.swift
#	src/services/browserPlatformUtils.service.ts
This commit is contained in:
Hinton
2021-01-13 14:21:45 +01:00
21 changed files with 202 additions and 71 deletions

2
jslib

Submodule jslib updated: 72bf18f369...cea09a22e5

11
package-lock.json generated
View File

@@ -1784,9 +1784,9 @@
"dev": true "dev": true
}, },
"bl": { "bl": {
"version": "2.2.0", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/bl/-/bl-2.2.0.tgz", "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz",
"integrity": "sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==", "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==",
"requires": { "requires": {
"readable-stream": "^2.3.5", "readable-stream": "^2.3.5",
"safe-buffer": "^5.1.1" "safe-buffer": "^5.1.1"
@@ -1894,6 +1894,11 @@
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
"dev": true "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": { "browser-resolve": {
"version": "1.11.3", "version": "1.11.3",
"resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz",

View File

@@ -94,6 +94,7 @@
"angular2-toaster": "8.0.0", "angular2-toaster": "8.0.0",
"angulartics2": "9.1.0", "angulartics2": "9.1.0",
"big-integer": "1.6.36", "big-integer": "1.6.36",
"browser-process-hrtime": "1.0.0",
"core-js": "2.6.2", "core-js": "2.6.2",
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",

View File

@@ -606,6 +606,9 @@
"message": "WARNING", "message": "WARNING",
"description": "WARNING (should stay in capitalized letters if the language permits)" "description": "WARNING (should stay in capitalized letters if the language permits)"
}, },
"confirmVaultExport": {
"message": "Confirm Vault Export"
},
"exportWarningDesc": { "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." "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": { "privacyPolicy": {
"message": "Privacy Policy" "message": "Privacy Policy"
}, },
"hintEqualsPassword": {
"message": "Your password hint cannot be the same as your password."
},
"ok": { "ok": {
"message": "Ok" "message": "Ok"
}, },
@@ -1412,9 +1418,24 @@
"message": "Desktop application invalidated the secure communication channel. Please retry this operation" "message": "Desktop application invalidated the secure communication channel. Please retry this operation"
}, },
"nativeMessagingInvalidEncryptionTitle": { "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": { "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." "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."
} }
} }

View File

@@ -21,6 +21,7 @@ import {
UserService, UserService,
VaultTimeoutService, VaultTimeoutService,
} from 'jslib/services'; } from 'jslib/services';
import { ConsoleLogService } from 'jslib/services/consoleLog.service';
import { EventService } from 'jslib/services/event.service'; import { EventService } from 'jslib/services/event.service';
import { ExportService } from 'jslib/services/export.service'; import { ExportService } from 'jslib/services/export.service';
import { NotificationsService } from 'jslib/services/notifications.service'; import { NotificationsService } from 'jslib/services/notifications.service';
@@ -93,6 +94,7 @@ export default class MainBackground {
i18nService: I18nServiceAbstraction; i18nService: I18nServiceAbstraction;
platformUtilsService: PlatformUtilsServiceAbstraction; platformUtilsService: PlatformUtilsServiceAbstraction;
constantsService: ConstantsService; constantsService: ConstantsService;
consoleLogService: ConsoleLogService;
cryptoService: CryptoServiceAbstraction; cryptoService: CryptoServiceAbstraction;
cryptoFunctionService: CryptoFunctionServiceAbstraction; cryptoFunctionService: CryptoFunctionServiceAbstraction;
tokenService: TokenServiceAbstraction; tokenService: TokenServiceAbstraction;
@@ -169,8 +171,9 @@ export default class MainBackground {
this.secureStorageService = new BrowserStorageService(); this.secureStorageService = new BrowserStorageService();
this.i18nService = new I18nService(BrowserApi.getUILanguage(window)); this.i18nService = new I18nService(BrowserApi.getUILanguage(window));
this.cryptoFunctionService = new WebCryptoFunctionService(window, this.platformUtilsService); this.cryptoFunctionService = new WebCryptoFunctionService(window, this.platformUtilsService);
this.consoleLogService = new ConsoleLogService(false);
this.cryptoService = new CryptoService(this.storageService, this.secureStorageService, 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.tokenService = new TokenService(this.storageService);
this.appIdService = new AppIdService(this.storageService); this.appIdService = new AppIdService(this.storageService);
this.apiService = new ApiService(this.tokenService, this.platformUtilsService, 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.userService = new UserService(this.tokenService, this.storageService);
this.authService = new AuthService(this.cryptoService, this.apiService, this.userService, this.authService = new AuthService(this.cryptoService, this.apiService, this.userService,
this.tokenService, this.appIdService, this.i18nService, this.platformUtilsService, 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.settingsService = new SettingsService(this.userService, this.storageService);
this.cipherService = new CipherService(this.cryptoService, this.userService, this.settingsService, this.cipherService = new CipherService(this.cryptoService, this.userService, this.settingsService,
this.apiService, this.storageService, this.i18nService, () => this.searchService); this.apiService, this.storageService, this.i18nService, () => this.searchService);
@@ -186,7 +189,7 @@ export default class MainBackground {
this.storageService, this.i18nService, this.cipherService); this.storageService, this.i18nService, this.cipherService);
this.collectionService = new CollectionService(this.cryptoService, this.userService, this.storageService, this.collectionService = new CollectionService(this.cryptoService, this.userService, this.storageService,
this.i18nService); 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.sendService = new SendService(this.cryptoService, this.userService, this.apiService, this.storageService,
this.i18nService, this.cryptoFunctionService); this.i18nService, this.cryptoFunctionService);
this.stateService = new StateService(); this.stateService = new StateService();
@@ -220,7 +223,7 @@ export default class MainBackground {
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
this.exportService = new ExportService(this.folderService, this.cipherService, this.apiService); this.exportService = new ExportService(this.folderService, this.cipherService, this.apiService);
this.notificationsService = new NotificationsService(this.userService, this.syncService, this.appIdService, 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.environmentService = new EnvironmentService(this.apiService, this.storageService,
this.notificationsService); this.notificationsService);
this.analytics = new Analytics(window, () => BrowserApi.gaFilter(), this.platformUtilsService, 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.analytics, this.notificationsService, this.systemService, this.vaultTimeoutService,
this.environmentService, this.policyService, this.userService); this.environmentService, this.policyService, this.userService);
this.nativeMessagingBackground = new NativeMessagingBackground(this.storageService, this.cryptoService, this.cryptoFunctionService, 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.commandsBackground = new CommandsBackground(this, this.passwordGenerationService,
this.platformUtilsService, this.analytics, this.vaultTimeoutService); this.platformUtilsService, this.analytics, this.vaultTimeoutService);

View File

@@ -1,4 +1,5 @@
import { ConstantsService } from 'jslib/services/constants.service'; import { ConstantsService } from 'jslib/services/constants.service';
import { AppIdService } from 'jslib/abstractions/appId.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service'; import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { CryptoService } from 'jslib/abstractions/crypto.service'; import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service'; import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -23,15 +24,19 @@ export class NativeMessagingBackground {
private resolver: any = null; private resolver: any = null;
private privateKey: ArrayBuffer = null; private privateKey: ArrayBuffer = null;
private publicKey: ArrayBuffer = null;
private secureSetupResolve: any = null; private secureSetupResolve: any = null;
private sharedSecret: SymmetricCryptoKey; private sharedSecret: SymmetricCryptoKey;
private appId: string;
constructor(private storageService: StorageService, private cryptoService: CryptoService, constructor(private storageService: StorageService, private cryptoService: CryptoService,
private cryptoFunctionService: CryptoFunctionService, private vaultTimeoutService: VaultTimeoutService, private cryptoFunctionService: CryptoFunctionService, private vaultTimeoutService: VaultTimeoutService,
private runtimeBackground: RuntimeBackground, private i18nService: I18nService, private userService: UserService, private runtimeBackground: RuntimeBackground, private i18nService: I18nService, private userService: UserService,
private messagingService: MessagingService) {} private messagingService: MessagingService, private appIdService: AppIdService) {}
async connect() { async connect() {
this.appId = await this.appIdService.getAppId();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.port = BrowserApi.connectNative('com.8bit.bitwarden'); this.port = BrowserApi.connectNative('com.8bit.bitwarden');
@@ -58,6 +63,11 @@ export class NativeMessagingBackground {
this.port.disconnect(); this.port.disconnect();
break; break;
case 'setupEncryption': case 'setupEncryption':
// Ignore since it belongs to another device
if (message.appId !== this.appId) {
return;
}
const encrypted = Utils.fromB64ToArray(message.sharedSecret); const encrypted = Utils.fromB64ToArray(message.sharedSecret);
const decrypted = await this.cryptoFunctionService.rsaDecrypt(encrypted.buffer, this.privateKey, EncryptionAlgorithm); const decrypted = await this.cryptoFunctionService.rsaDecrypt(encrypted.buffer, this.privateKey, EncryptionAlgorithm);
@@ -65,6 +75,11 @@ export class NativeMessagingBackground {
this.secureSetupResolve(); this.secureSetupResolve();
break; break;
case 'invalidateEncryption': case 'invalidateEncryption':
// Ignore since it belongs to another device
if (message.appId !== this.appId) {
return;
}
this.sharedSecret = null; this.sharedSecret = null;
this.privateKey = null; this.privateKey = null;
this.connected = false; this.connected = false;
@@ -75,8 +90,20 @@ export class NativeMessagingBackground {
confirmText: this.i18nService.t('ok'), confirmText: this.i18nService.t('ok'),
type: 'error', type: 'error',
}); });
break;
case 'verifyFingerprint': {
if (this.sharedSecret == null) {
this.showFingerprintDialog();
}
break;
}
default: 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; error = chrome.runtime.lastError.message;
} }
if (error === 'Specified native messaging host not found.' || if (error != null) {
error === 'Access to the specified native messaging host is forbidden.' ||
error === 'An unexpected error occurred') {
this.messagingService.send('showDialog', { this.messagingService.send('showDialog', {
text: this.i18nService.t('desktopIntegrationDisabledDesc'), text: this.i18nService.t('desktopIntegrationDisabledDesc'),
title: this.i18nService.t('desktopIntegrationDisabledTitle'), title: this.i18nService.t('desktopIntegrationDisabledTitle'),
@@ -118,7 +143,7 @@ export class NativeMessagingBackground {
message.timestamp = Date.now(); message.timestamp = Date.now();
const encrypted = await this.cryptoService.encrypt(JSON.stringify(message), this.sharedSecret); const encrypted = await this.cryptoService.encrypt(JSON.stringify(message), this.sharedSecret);
this.port.postMessage(encrypted); this.postMessage({appId: this.appId, message: encrypted});
} }
getResponse(): Promise<any> { getResponse(): Promise<any> {
@@ -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) { private async onMessage(rawMessage: any) {
const message = JSON.parse(await this.cryptoService.decryptToUtf8(rawMessage, this.sharedSecret)); const message = JSON.parse(await this.cryptoService.decryptToUtf8(rawMessage, this.sharedSecret));
@@ -140,6 +186,24 @@ export class NativeMessagingBackground {
case 'biometricUnlock': case 'biometricUnlock':
await this.storageService.remove(ConstantsService.biometricAwaitingAcceptance); 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); const enabled = await this.storageService.get(ConstantsService.biometricUnlockKey);
if (enabled === null || enabled === false) { if (enabled === null || enabled === false) {
if (message.response === 'unlocked') { if (message.response === 'unlocked') {
@@ -171,17 +235,10 @@ export class NativeMessagingBackground {
private async secureCommunication() { private async secureCommunication() {
const [publicKey, privateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); const [publicKey, privateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
this.publicKey = publicKey;
this.privateKey = privateKey; this.privateKey = privateKey;
this.sendUnencrypted({command: 'setupEncryption', publicKey: Utils.fromBufferToB64(publicKey)}); 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')}<br><br><strong>${fingerprint}</strong>`,
title: this.i18nService.t('desktopSyncVerificationTitle'),
confirmText: this.i18nService.t('ok'),
type: 'warning',
});
return new Promise((resolve, reject) => this.secureSetupResolve = resolve); return new Promise((resolve, reject) => this.secureSetupResolve = resolve);
} }
@@ -193,6 +250,17 @@ export class NativeMessagingBackground {
message.timestamp = Date.now(); 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')}<br><br><strong>${fingerprint}</strong>`,
title: this.i18nService.t('desktopSyncVerificationTitle'),
confirmText: this.i18nService.t('ok'),
type: 'warning',
});
} }
} }

View File

@@ -455,7 +455,7 @@ export default class RuntimeBackground {
if (policy.enabled) { if (policy.enabled) {
const org = await this.userService.getOrganization(policy.organizationId); const org = await this.userService.getOrganization(policy.organizationId);
if (org != null && org.enabled && org.usePolicies && !org.isAdmin if (org != null && org.enabled && org.usePolicies && !org.isAdmin
&& org.status == OrganizationUserStatusType.Confirmed) { && org.status === OrganizationUserStatusType.Confirmed) {
return false; return false;
} }
} }

View File

@@ -41,11 +41,11 @@ export class SsoComponent extends BaseSsoComponent {
this.redirectUri = url + '/sso-connector.html'; this.redirectUri = url + '/sso-connector.html';
this.clientId = 'browser'; this.clientId = 'browser';
super.onSuccessfulLogin = () => { super.onSuccessfulLogin = async () => {
await syncService.fullSync(true);
BrowserApi.reloadOpenWindows(); BrowserApi.reloadOpenWindows();
const thisWindow = window.open('', '_self'); const thisWindow = window.open('', '_self');
thisWindow.close(); thisWindow.close();
return syncService.fullSync(true);
}; };
} }
} }

View File

@@ -150,6 +150,9 @@ export const routerTransition = trigger('routerTransition', [
transition('view-cipher => clone-cipher', inSlideUp), transition('view-cipher => clone-cipher', inSlideUp),
transition('clone-cipher => view-cipher, clone-cipher => tabs', outSlideDown), 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('tabs => add-cipher', inSlideUp),
transition('add-cipher => tabs', outSlideDown), 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('add-cipher => generator, edit-cipher => generator, clone-cipher => generator', inSlideUp),
transition('generator => add-cipher, generator => edit-cipher, generator => clone-cipher', outSlideDown), 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('edit-cipher => attachments, edit-cipher => collections', inSlideLeft),
transition('attachments => edit-cipher, collections => edit-cipher', outSlideRight), transition('attachments => edit-cipher, collections => edit-cipher', outSlideRight),

View File

@@ -16,6 +16,11 @@
[ngClass]="{disabled: (!cipher.login.password || !cipher.viewPassword)}"> [ngClass]="{disabled: (!cipher.login.password || !cipher.viewPassword)}">
<i class="fa fa-lg fa-key" aria-hidden="true"></i> <i class="fa fa-lg fa-key" aria-hidden="true"></i>
</span> </span>
<span class="row-btn" appStopClick appStopProp appA11yTitle="{{'copyVerificationCode' | i18n}}"
(click)="copy(cipher, cipher.login.totp, 'verificationCodeTotp', 'TOTP')"
[ngClass]="{disabled: (!displayTotpCopyButton(cipher))}">
<i class="fa fa-lg fa-clock-o" aria-hidden="true"></i>
</span>
</ng-container> </ng-container>
<ng-container *ngIf="cipher.type === cipherType.Card"> <ng-container *ngIf="cipher.type === cipherType.Card">
<span class="row-btn" appStopClick appStopProp appA11yTitle="{{'copyNumber' | i18n}}" <span class="row-btn" appStopClick appStopProp appA11yTitle="{{'copyNumber' | i18n}}"

View File

@@ -18,6 +18,8 @@ import { CipherView } from 'jslib/models/view/cipherView';
import { EventService } from 'jslib/abstractions/event.service'; import { EventService } from 'jslib/abstractions/event.service';
import { I18nService } from 'jslib/abstractions/i18n.service'; import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { TotpService } from 'jslib/abstractions/totp.service';
import { UserService } from 'jslib/abstractions/user.service';
import { PopupUtilsService } from '../services/popup-utils.service'; import { PopupUtilsService } from '../services/popup-utils.service';
@@ -31,10 +33,16 @@ export class ActionButtonsComponent {
@Input() showView = false; @Input() showView = false;
cipherType = CipherType; cipherType = CipherType;
userHasPremiumAccess = false;
constructor(private analytics: Angulartics2, private toasterService: ToasterService, constructor(private analytics: Angulartics2, private toasterService: ToasterService,
private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
private popupUtilsService: PopupUtilsService, private eventService: EventService) { } private popupUtilsService: PopupUtilsService, private eventService: EventService,
private totpService: TotpService, private userService: UserService) { }
async ngOnInit() {
this.userHasPremiumAccess = await this.userService.canAccessPremium();
}
launch() { launch() {
if (this.cipher.type !== CipherType.Login || !this.cipher.login.canLaunch) { if (this.cipher.type !== CipherType.Login || !this.cipher.login.canLaunch) {
@@ -48,9 +56,11 @@ export class ActionButtonsComponent {
} }
} }
copy(cipher: CipherView, value: string, typeI18nKey: string, aType: string) { async copy(cipher: CipherView, value: string, typeI18nKey: string, aType: string) {
if (value == null) { if (value == null || aType === 'TOTP' && !this.displayTotpCopyButton(cipher)) {
return; return;
} else if (value === cipher.login.totp) {
value = await this.totpService.getCode(value);
} }
if (!cipher.viewPassword) { if (!cipher.viewPassword) {
@@ -62,13 +72,18 @@ export class ActionButtonsComponent {
this.toasterService.popAsync('info', null, this.toasterService.popAsync('info', null,
this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey))); this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey)));
if (typeI18nKey === 'password') { if (typeI18nKey === 'password' || typeI18nKey === 'verificationCodeTotp') {
this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, cipher.id); this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, cipher.id);
} else if (typeI18nKey === 'securityCode') { } else if (typeI18nKey === 'securityCode') {
this.eventService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id); this.eventService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id);
} }
} }
displayTotpCopyButton(cipher: CipherView) {
return (cipher?.login?.hasTotp ?? false) &&
(cipher.organizationUseTotp || this.userHasPremiumAccess);
}
view() { view() {
this.onView.emit(this.cipher); this.onView.emit(this.cipher);
} }

View File

@@ -1,10 +1,12 @@
import { CipherService } from 'jslib/abstractions/cipher.service'; import { CipherService } from 'jslib/abstractions/cipher.service';
import { ConsoleLogService } from 'jslib/services/consoleLog.service';
import { SearchService } from 'jslib/services/search.service'; import { SearchService } from 'jslib/services/search.service';
export class PopupSearchService extends SearchService { export class PopupSearchService extends SearchService {
constructor(private mainSearchService: SearchService, cipherService: CipherService) { constructor(private mainSearchService: SearchService, cipherService: CipherService,
super(cipherService); consoleLogService: ConsoleLogService) {
super(cipherService, consoleLogService);
} }
clearIndex() { clearIndex() {

View File

@@ -50,6 +50,7 @@ import { AuthService } from 'jslib/services/auth.service';
import { ConstantsService } from 'jslib/services/constants.service'; import { ConstantsService } from 'jslib/services/constants.service';
import { SearchService } from 'jslib/services/search.service'; import { SearchService } from 'jslib/services/search.service';
import { StateService } from 'jslib/services/state.service'; import { StateService } from 'jslib/services/state.service';
import { ConsoleLogService } from 'jslib/services/consoleLog.service';
import { Analytics } from 'jslib/misc/analytics'; import { Analytics } from 'jslib/misc/analytics';
@@ -69,11 +70,11 @@ export const authService = new AuthService(getBgService<CryptoService>('cryptoSe
getBgService<ApiService>('apiService')(), getBgService<UserService>('userService')(), getBgService<ApiService>('apiService')(), getBgService<UserService>('userService')(),
getBgService<TokenService>('tokenService')(), getBgService<AppIdService>('appIdService')(), getBgService<TokenService>('tokenService')(), getBgService<AppIdService>('appIdService')(),
getBgService<I18nService>('i18nService')(), getBgService<PlatformUtilsService>('platformUtilsService')(), getBgService<I18nService>('i18nService')(), getBgService<PlatformUtilsService>('platformUtilsService')(),
messagingService, getBgService<VaultTimeoutService>('vaultTimeoutService')()); messagingService, getBgService<VaultTimeoutService>('vaultTimeoutService')(), getBgService<ConsoleLogService>('consoleLogService')());
export const searchService = new PopupSearchService(getBgService<SearchService>('searchService')(), export const searchService = new PopupSearchService(getBgService<SearchService>('searchService')(),
getBgService<CipherService>('cipherService')()); getBgService<CipherService>('cipherService')(), getBgService<ConsoleLogService>('consoleLogService')());
export function initFactory(i18nService: I18nService, storageService: StorageService, export function initFactory(platformUtilsService: PlatformUtilsService, i18nService: I18nService, storageService: StorageService,
popupUtilsService: PopupUtilsService): Function { popupUtilsService: PopupUtilsService): Function {
return async () => { return async () => {
if (!popupUtilsService.inPopup(window)) { if (!popupUtilsService.inPopup(window)) {
@@ -90,7 +91,12 @@ export function initFactory(i18nService: I18nService, storageService: StorageSer
let theme = await storageService.get<string>(ConstantsService.themeKey); let theme = await storageService.get<string>(ConstantsService.themeKey);
if (theme == null) { 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('locale_' + i18nService.translationLocale);
window.document.documentElement.classList.add('theme_' + theme); window.document.documentElement.classList.add('theme_' + theme);
@@ -171,7 +177,7 @@ export function initFactory(i18nService: I18nService, storageService: StorageSer
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: initFactory, useFactory: initFactory,
deps: [I18nService, StorageService, PopupUtilsService], deps: [PlatformUtilsService, I18nService, StorageService, PopupUtilsService],
multi: true, multi: true,
}, },
{ {

View File

@@ -41,13 +41,6 @@
</div> </div>
<div class="box-footer"> <div class="box-footer">
<p>{{'exportMasterPassword' | i18n}}</p> <p>{{'exportMasterPassword' | i18n}}</p>
<strong>{{'warning' | i18n}}</strong>:
<span *ngIf="!encryptedFormat">
{{'exportWarningDesc' | i18n}}
</span>
<span *ngIf="encryptedFormat">
{{'encExportWarningDesc' | i18n}}
</span>
</div> </div>
</div> </div>
</content> </content>

View File

@@ -14,6 +14,9 @@
</div> </div>
</header> </header>
<content *ngIf="cipher"> <content *ngIf="cipher">
<app-callout type="info" *ngIf="allowOwnershipOptions() && !allowPersonal">
{{'personalOwnershipPolicyInEffect' | i18n}}
</app-callout>
<div class="box"> <div class="box">
<div class="box-header"> <div class="box-header">
{{'itemInformation' | i18n}} {{'itemInformation' | i18n}}
@@ -374,15 +377,6 @@
</div> </div>
<div class="box list" *ngIf="editMode && !cloneMode"> <div class="box list" *ngIf="editMode && !cloneMode">
<div class="box-content single-line"> <div class="box-content single-line">
<a class="box-content-row" href="#" appStopClick appBlurClick (click)="share()"
*ngIf="!cipher.organizationId">
<div class="row-main text-primary">
<div class="icon text-primary" aria-hidden="true">
<i class="fa fa-share-alt fa-lg fa-fw"></i>
</div>
<span>{{'shareItem' | i18n}}</span>
</div>
</a>
<a class="box-content-row" href="#" appStopClick appBlurClick (click)="delete()" <a class="box-content-row" href="#" appStopClick appBlurClick (click)="delete()"
[appApiAction]="deletePromise" #deleteBtn> [appApiAction]="deletePromise" #deleteBtn>
<div class="row-main text-danger"> <div class="row-main text-danger">

View File

@@ -118,12 +118,6 @@ export class AddEditComponent extends BaseAddEditComponent {
this.router.navigate(['/attachments'], { queryParams: { cipherId: this.cipher.id } }); 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() { editCollections() {
super.editCollections(); super.editCollections();

View File

@@ -41,14 +41,12 @@ export class ShareComponent extends BaseShareComponent {
async submit(): Promise<boolean> { async submit(): Promise<boolean> {
const success = await super.submit(); const success = await super.submit();
if (success) { if (success) {
window.setTimeout(() => { this.cancel();
this.location.back();
}, 200);
} }
return success; return success;
} }
cancel() { cancel() {
this.location.back(); this.router.navigate(['/view-cipher'], { replaceUrl: true, queryParams: { cipherId: this.cipher.id } });
} }
} }

View File

@@ -295,6 +295,14 @@
<span>{{'cloneItem' | i18n}}</span> <span>{{'cloneItem' | i18n}}</span>
</div> </div>
</a> </a>
<a class="box-content-row" href="#" appStopClick appBlurClick (click)="share()" *ngIf="!cipher.organizationId">
<div class="row-main text-primary">
<div class="icon text-primary" aria-hidden="true">
<i class="fa fa-share-alt fa-lg fa-fw"></i>
</div>
<span>{{'shareItem' | i18n}}</span>
</div>
</a>
<a class="box-content-row" href="#" appStopClick appBlurClick (click)="restore()" *ngIf="cipher.isDeleted"> <a class="box-content-row" href="#" appStopClick appBlurClick (click)="restore()" *ngIf="cipher.isDeleted">
<div class="row-main text-primary"> <div class="row-main text-primary">
<div class="icon text-primary" aria-hidden="true"> <div class="icon text-primary" aria-hidden="true">

View File

@@ -129,6 +129,13 @@ export class ViewComponent extends BaseViewComponent {
}); });
} }
share() {
super.share();
if (this.cipher.organizationId == null) {
this.router.navigate(['/share-cipher'], { replaceUrl: true, queryParams: { cipherId: this.cipher.id } });
}
}
async fillCipher() { async fillCipher() {
const didAutofill = await this.doAutofill(); const didAutofill = await this.doAutofill();
if (didAutofill) { if (didAutofill) {

View File

@@ -254,7 +254,7 @@ export default class AutofillService implements AutofillServiceInterface {
} }
} }
const autoFillResponse = await this.doAutoFill({ const totpCode = await this.doAutoFill({
cipher: cipher, cipher: cipher,
pageDetails: pageDetails, pageDetails: pageDetails,
skipTotp: !fromCommand, skipTotp: !fromCommand,
@@ -265,12 +265,12 @@ export default class AutofillService implements AutofillServiceInterface {
fillNewPassword: fromCommand, fillNewPassword: fromCommand,
}); });
// Only update last used index if doAutoFill didn't throw an exception // Update last used index as autofill has succeed
if (fromCommand) { if (fromCommand) {
this.cipherService.updateLastUsedIndexForUrl(tab.url); this.cipherService.updateLastUsedIndexForUrl(tab.url);
} }
return autoFillResponse; return totpCode;
} }
// Helpers // Helpers

View File

@@ -16,6 +16,7 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
private showDialogResolves = new Map<number, { resolve: (value: boolean) => void, date: Date }>(); private showDialogResolves = new Map<number, { resolve: (value: boolean) => void, date: Date }>();
private deviceCache: DeviceType = null; private deviceCache: DeviceType = null;
private analyticsIdCache: string = null; private analyticsIdCache: string = null;
private prefersColorSchemeDark = window.matchMedia('(prefers-color-scheme: dark)');
constructor(private messagingService: MessagingService, constructor(private messagingService: MessagingService,
private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
@@ -303,4 +304,14 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
supportsSecureStorage(): boolean { supportsSecureStorage(): boolean {
return false; return false;
} }
getDefaultSystemTheme() {
return this.prefersColorSchemeDark.matches ? 'dark' : 'light';
}
onDefaultSystemThemeChange(callback: ((theme: 'light' | 'dark') => unknown)) {
this.prefersColorSchemeDark.addListener(({ matches }) => {
callback(matches ? 'dark' : 'light');
});
}
} }