1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +00:00

[SG-520] Native messaging handler (#3566)

* [SG-523] Base test runner app for native messages (#3269)

* Base test runner app for native messages

* Remove default test script

* Add case for canceled status

* Modify to allow usage of libs crypto services and functions

* Small adjustments

* Handshake request (#3277)

* Handshake request

* Fix capitalization

* Update info text

* lock node-ipc to 9.2.1

* [SG-569] Native Messaging settings bug (#3285)

* Fix bug where updating setting wasn't starting the native messaging listener

* Update test runner error message

* [SG-532] Implement Status command in Native Messaging Service (#3310)

* Status command start

* Refactor ipc test service and add status command

* fixed linter errors

* Move types into a model file

* Cleanup and comments

* Fix auth status condition

* Remove .vscode settings file. Fix this in a separate work item

* Add active field to status response

* Extract native messaging types into their own files

* Remove experimental decorators

* Turn off no console lint rule for the test runner

* Casing fix

* Models import casing fixes

* Remove in progress file (merge error)

* Move models to their own folder and add index.ts

* Remove file that got un-deleted

* Remove file that will be added in separate command

* Fix imports that got borked

* [SG-533] Implement bw-credential-retrieval (#3334)

* Status command start

* Refactor ipc test service and add status command

* fixed linter errors

* Move types into a model file

* Cleanup and comments

* Fix auth status condition

* Remove .vscode settings file. Fix this in a separate work item

* Implement bw-credential-retrieval

* Add active field to status response

* Extract native messaging types into their own files

* Remove experimental decorators

* Turn off no console lint rule for the test runner

* Casing fix

* Models import casing fixes

* Add error handling for passing a bad public key to handshake

* [SG-534] and [SG-535] Implement Credential Create and Update commands (#3342)

* Status command start

* Refactor ipc test service and add status command

* fixed linter errors

* Move types into a model file

* Cleanup and comments

* Fix auth status condition

* Remove .vscode settings file. Fix this in a separate work item

* Implement bw-credential-retrieval

* Add active field to status response

* Add bw-credential-create

* Better response handling in test runner

* Extract native messaging types into their own files

* Remove experimental decorators

* Turn off no console lint rule for the test runner

* Casing fix

* Models import casing fixes

* bw-cipher-create move type into its own file

* Use LogUtils for all logging

* Implement bw-credential-update

* Give naming conventions for types

* Rename file correctly

* Update handleEncyptedMessage with EncString changes

* [SG-626] Fix Desktop app not showing updated credentials from native messages (#3380)

* Add MessagingService to send messages on login create and update

* Add `not-active-user` error to create and update and other refactors

* [SG-536] Implement bw-generate-password (#3370)

* implement bw-generate-password

* Fix merge conflict resolution errors

* Update apps/desktop/native-messaging-test-runner/src/bw-generate-password.ts

Co-authored-by: Addison Beck <addisonbeck1@gmail.com>

* Logging improvements

* Add NativeMessagingVersion enum

* Add version check in NativeMessagingHandler

Co-authored-by: Addison Beck <addisonbeck1@gmail.com>

* Refactor account status checks and check for locked state in generate command (#3461)

* Add feawture flag to show/hide ddg setting (#3506)

* [SG-649] Add confirmation dialog and tweak shared key retrieval  (#3451)

* Add confirmation dialog when completing handshake

* Copy updates for dialog

* HandshakeResponse type fixes

* Add longer timeout for handshake command

* [SG-663] RefactorNativeMessagingHandlerService and strengthen typing (#3551)

* NativeMessageHandlerService refactor and additional types

* Return empty array if no uri to retrieve command

* Move commands from test runner into a separate folder

* Fix bug where confirmation dialog messes with styling

* Enable DDG feature

* Fix generated password not saving to history

* Take credentialId as parameter to update

* Add applicationName to handshake payload

* Add warning text to confirmation modal

Co-authored-by: Addison Beck <addisonbeck1@gmail.com>
This commit is contained in:
Robyn MacCallum
2022-09-23 15:47:17 -04:00
committed by GitHub
parent 32eac70c82
commit f4e61d1cec
57 changed files with 2386 additions and 27 deletions

View File

@@ -309,6 +309,23 @@
</div>
<small class="help-block">{{ "enableBrowserIntegrationDesc" | i18n }}</small>
</div>
<div class="form-group" *ngIf="showDuckDuckGoIntegrationOption">
<div class="checkbox">
<label for="enableDuckDuckGoBrowserIntegration">
<input
id="enableDuckDuckGoBrowserIntegration"
type="checkbox"
name="enableDuckDuckGoBrowserIntegration"
[(ngModel)]="enableDuckDuckGoBrowserIntegration"
(change)="saveDdgBrowserIntegration()"
/>
{{ "enableDuckDuckGoBrowserIntegration" | i18n }}
</label>
</div>
<small class="help-block">{{
"enableDuckDuckGoBrowserIntegrationDesc" | i18n
}}</small>
</div>
<div class="form-group">
<div class="checkbox">
<label for="enableBrowserIntegrationFingerprint">

View File

@@ -16,6 +16,7 @@ import { ThemeType } from "@bitwarden/common/enums/themeType";
import { Utils } from "@bitwarden/common/misc/utils";
import { isWindowsStore } from "@bitwarden/electron/utils";
import { flagEnabled } from "../../flags";
import { SetPinComponent } from "../components/set-pin.component";
@Component({
@@ -28,6 +29,7 @@ export class SettingsComponent implements OnInit {
pin: boolean = null;
enableFavicons = false;
enableBrowserIntegration = false;
enableDuckDuckGoBrowserIntegration = false;
enableBrowserIntegrationFingerprint = false;
enableMinToTray = false;
enableCloseToTray = false;
@@ -51,6 +53,7 @@ export class SettingsComponent implements OnInit {
showAlwaysShowDock = false;
openAtLogin: boolean;
requireEnableTray = false;
showDuckDuckGoIntegrationOption = false;
enableTrayText: string;
enableTrayDescText: string;
@@ -102,6 +105,9 @@ export class SettingsComponent implements OnInit {
this.startToTrayText = this.i18nService.t(startToTrayKey);
this.startToTrayDescText = this.i18nService.t(startToTrayKey + "Desc");
// DuckDuckGo browser is only for macos initially
this.showDuckDuckGoIntegrationOption = flagEnabled("showDDGSetting") && isMac;
this.vaultTimeouts = [
// { name: i18nService.t('immediately'), value: 0 },
{ name: i18nService.t("oneMinute"), value: 1 },
@@ -188,6 +194,8 @@ export class SettingsComponent implements OnInit {
// Account preferences
this.enableFavicons = !(await this.stateService.getDisableFavicon());
this.enableBrowserIntegration = await this.stateService.getEnableBrowserIntegration();
this.enableDuckDuckGoBrowserIntegration =
await this.stateService.getEnableDuckDuckGoBrowserIntegration();
this.enableBrowserIntegrationFingerprint =
await this.stateService.getEnableBrowserIntegrationFingerprint();
this.clearClipboard = await this.stateService.getClearClipboard();
@@ -432,6 +440,22 @@ export class SettingsComponent implements OnInit {
}
}
async saveDdgBrowserIntegration() {
await this.stateService.setEnableDuckDuckGoBrowserIntegration(
this.enableDuckDuckGoBrowserIntegration
);
if (!this.enableBrowserIntegration) {
await this.stateService.setDuckDuckGoSharedKey(null);
}
this.messagingService.send(
this.enableDuckDuckGoBrowserIntegration
? "enableDuckDuckGoBrowserIntegration"
: "disableDuckDuckGoBrowserIntegration"
);
}
async saveBrowserIntegrationFingerprint() {
await this.stateService.setEnableBrowserIntegrationFingerprint(
this.enableBrowserIntegrationFingerprint

View File

@@ -12,7 +12,9 @@ import {
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service";
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/abstractions/auth.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/abstractions/broadcaster.service";
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/abstractions/cipher.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service";
@@ -22,8 +24,10 @@ import {
LogService as LogServiceAbstraction,
} from "@bitwarden/common/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service";
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService as StateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
import { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/abstractions/stateMigration.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
@@ -41,7 +45,9 @@ import { ElectronRendererSecureStorageService } from "@bitwarden/electron/servic
import { ElectronRendererStorageService } from "@bitwarden/electron/services/electronRendererStorage.service";
import { Account } from "../../models/account";
import { EncryptedMessageHandlerService } from "../../services/encryptedMessageHandlerService";
import { I18nService } from "../../services/i18n.service";
import { NativeMessageHandlerService } from "../../services/nativeMessageHandler.service";
import { NativeMessagingService } from "../../services/nativeMessaging.service";
import { PasswordRepromptService } from "../../services/passwordReprompt.service";
import { StateService } from "../../services/state.service";
@@ -147,6 +153,28 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
provide: AbstractThemingService,
useClass: DesktopThemingService,
},
{
provide: EncryptedMessageHandlerService,
deps: [
StateServiceAbstraction,
AuthServiceAbstraction,
CipherServiceAbstraction,
PolicyServiceAbstraction,
MessagingServiceAbstraction,
PasswordGenerationServiceAbstraction,
],
},
{
provide: NativeMessageHandlerService,
deps: [
StateServiceAbstraction,
CryptoServiceAbstraction,
CryptoFunctionServiceAbstraction,
MessagingServiceAbstraction,
I18nServiceAbstraction,
EncryptedMessageHandlerService,
],
},
],
})
export class ServicesModule {}

View File

@@ -8,7 +8,9 @@ import {
// required to avoid linting errors when there are no flags
/* eslint-disable-next-line @typescript-eslint/ban-types */
export type Flags = {} & SharedFlags;
export type Flags = {
showDDGSetting?: boolean;
} & SharedFlags;
// required to avoid linting errors when there are no flags
/* eslint-disable-next-line @typescript-eslint/ban-types */

View File

@@ -1570,6 +1570,12 @@
"enableBrowserIntegrationDesc": {
"message": "Used for biometrics in browser."
},
"enableDuckDuckGoBrowserIntegration": {
"message": "Allow DuckDuckGo browser integration"
},
"enableDuckDuckGoBrowserIntegrationDesc": {
"message": "Use your Bitwarden vault when browsing with DuckDuckGo."
},
"browserIntegrationUnsupportedTitle": {
"message": "Browser integration not supported"
},
@@ -1597,6 +1603,21 @@
"verifyBrowserDesc": {
"message": "Please ensure the shown fingerprint is identical to the fingerprint showed in the browser extension."
},
"verifyNativeMessagingConnectionTitle": {
"message": "$APPID$ wants to connect to Bitwarden",
"placeholders": {
"appid": {
"content": "$1",
"example": "My App"
}
}
},
"verifyNativeMessagingConnectionDesc": {
"message": "Would you like to approve this request?"
},
"verifyNativeMessagingConnectionWarning": {
"message": "If you did not initiate this request, do not approve it."
},
"biometricsNotEnabledTitle": {
"message": "Biometrics not enabled"
},
@@ -1985,15 +2006,15 @@
"organizationIsDisabled": {
"message": "Organization is disabled."
},
"disabledOrganizationFilterError" : {
"message" : "Items in disabled Organizations cannot be accessed. Contact your Organization owner for assistance."
"disabledOrganizationFilterError": {
"message": "Items in disabled Organizations cannot be accessed. Contact your Organization owner for assistance."
},
"neverLockWarning": {
"message": "Are you sure you want to use the \"Never\" option? Setting your lock options to \"Never\" stores your vault's encryption key on your device. If you use this option you should ensure that you keep your device properly protected."
},
"cardBrandMir": {
"message": "Mir"
},
},
"vault": {
"message": "Vault"
}

View File

@@ -171,7 +171,10 @@ export class Main {
await this.biometricMain.init();
}
if (await this.stateService.getEnableBrowserIntegration()) {
if (
(await this.stateService.getEnableBrowserIntegration()) ||
(await this.stateService.getEnableDuckDuckGoBrowserIntegration())
) {
this.nativeMessagingMain.listen();
}

View File

@@ -68,10 +68,18 @@ export class MessagingMain {
this.main.nativeMessagingMain.generateManifests();
this.main.nativeMessagingMain.listen();
break;
case "enableDuckDuckGoBrowserIntegration":
this.main.nativeMessagingMain.generateDdgManifests();
this.main.nativeMessagingMain.listen();
break;
case "disableBrowserIntegration":
this.main.nativeMessagingMain.removeManifests();
this.main.nativeMessagingMain.stop();
break;
case "disableDuckDuckGoBrowserIntegration":
this.main.nativeMessagingMain.removeDdgManifests();
this.main.nativeMessagingMain.stop();
break;
default:
break;
}

View File

@@ -163,6 +163,27 @@ export class NativeMessagingMain {
}
}
generateDdgManifests() {
const manifest = {
name: "com.8bit.bitwarden",
description: "Bitwarden desktop <-> DuckDuckGo bridge",
path: this.binaryPath(),
type: "stdio",
};
switch (process.platform) {
case "darwin": {
/* eslint-disable-next-line no-useless-escape */
const path = `${this.homedir()}/Library/Containers/com.duckduckgo.macos.browser/Data/Library/Application\ Support/NativeMessagingHosts/com.8bit.bitwarden.json`;
this.writeManifest(path, manifest).catch((e) =>
this.logService.error(`Error writing manifest for DuckDuckGo. ${e}`)
);
break;
}
default:
break;
}
}
removeManifests() {
switch (process.platform) {
case "win32":
@@ -217,6 +238,21 @@ export class NativeMessagingMain {
}
}
removeDdgManifests() {
switch (process.platform) {
case "darwin": {
/* eslint-disable-next-line no-useless-escape */
const path = `${this.homedir()}/Library/Containers/com.duckduckgo.macos.browser/Data/Library/Application\ Support/NativeMessagingHosts/com.8bit.bitwarden.json`;
if (existsSync(path)) {
fs.unlink(path);
}
break;
}
default:
break;
}
}
private getDarwinNMHS() {
/* eslint-disable no-useless-escape */
return {

View File

@@ -0,0 +1,6 @@
import { EncryptedCommand } from "./encryptedCommand";
export type DecryptedCommandData = {
command: EncryptedCommand;
payload?: any;
};

View File

@@ -0,0 +1,6 @@
export type EncryptedCommand =
| "bw-status"
| "bw-credential-retrieval"
| "bw-credential-create"
| "bw-credential-update"
| "bw-generate-password";

View File

@@ -0,0 +1,8 @@
import { EncString } from "@bitwarden/common/models/domain/encString";
import { MessageCommon } from "./messageCommon";
export type EncryptedMessage = MessageCommon & {
// Will decrypt to a DecryptedCommandData object
encryptedCommand: EncString;
};

View File

@@ -0,0 +1,7 @@
export type CredentialCreatePayload = {
userId: string;
userName: string;
password: string;
name: string;
uri: string;
};

View File

@@ -0,0 +1,4 @@
export type CredentialRetrievePayload = {
userId: string;
uri: string;
};

View File

@@ -0,0 +1,8 @@
export type CredentialUpdatePayload = {
userId: string;
userName: string;
password: string;
name: string;
uri: string;
credentialId: string;
};

View File

@@ -0,0 +1,3 @@
export type PasswordGeneratePayload = {
userId: string;
};

View File

@@ -0,0 +1,7 @@
import { EncString } from "@bitwarden/common/models/domain/encString";
import { MessageCommon } from "./messageCommon";
export type EncryptedMessageResponse = MessageCommon & {
encryptedPayload: EncString;
};

View File

@@ -0,0 +1,6 @@
export type AccountStatusResponse = {
id: string;
email: string;
status: "locked" | "unlocked";
active: boolean;
};

View File

@@ -0,0 +1,3 @@
export type CannotDecryptErrorResponse = {
error: "cannot-decrypt";
};

View File

@@ -0,0 +1,7 @@
export type CipherResponse = {
userId: string;
credentialId: string;
userName: string;
password: string;
name: string;
};

View File

@@ -0,0 +1,16 @@
import { AccountStatusResponse } from "./accountStatusResponse";
import { CannotDecryptErrorResponse } from "./cannotDecryptErrorResponse";
import { CipherResponse } from "./cipherResponse";
import { FailureStatusResponse } from "./failureStatusResponse";
import { GenerateResponse } from "./generateResponse";
import { SuccessStatusResponse } from "./successStatusResponse";
import { UserStatusErrorResponse } from "./userStatusErrorResponse";
export type EncyptedMessageResponse =
| AccountStatusResponse[]
| CannotDecryptErrorResponse
| CipherResponse[]
| FailureStatusResponse
| GenerateResponse
| SuccessStatusResponse
| UserStatusErrorResponse;

View File

@@ -0,0 +1,3 @@
export type FailureStatusResponse = {
status: "failure";
};

View File

@@ -0,0 +1,3 @@
export type GenerateResponse = {
password: string;
};

View File

@@ -0,0 +1,3 @@
export type SuccessStatusResponse = {
status: "success";
};

View File

@@ -0,0 +1,3 @@
export type UserStatusErrorResponse = {
error: "locked" | "not-active-user";
};

View File

@@ -0,0 +1,25 @@
export * from "./encryptedMessagePayloads/credentialCreatePayload";
export * from "./encryptedMessagePayloads/credentialRetrievePayload";
export * from "./encryptedMessagePayloads/credentialUpdatePayload";
export * from "./encryptedMessagePayloads/passwordGeneratePayload";
export * from "./encryptedMessageResponses/accountStatusResponse";
export * from "./encryptedMessageResponses/cannotDecryptErrorResponse";
export * from "./encryptedMessageResponses/cipherResponse";
export * from "./encryptedMessageResponses/encryptedMessageResponse";
export * from "./encryptedMessageResponses/failureStatusResponse";
export * from "./encryptedMessageResponses/generateResponse";
export * from "./encryptedMessageResponses/successStatusResponse";
export * from "./encryptedMessageResponses/userStatusErrorResponse";
export * from "./decryptedCommandData";
export * from "./encryptedCommand";
export * from "./encryptedMessage";
export * from "./encryptedMessageResponse";
export * from "./legacyMessage";
export * from "./legacyMessageWrapper";
export * from "./message";
export * from "./messageCommon";
export * from "./unencryptedCommand";
export * from "./unencryptedMessage";
export * from "./unencryptedMessageResponse";

View File

@@ -0,0 +1,8 @@
export type LegacyMessage = {
command: string;
userId?: string;
timestamp?: number;
publicKey?: string;
};

View File

@@ -0,0 +1,8 @@
import { EncString } from "@bitwarden/common/models/domain/encString";
import { LegacyMessage } from "./legacyMessage";
export type LegacyMessageWrapper = {
message: LegacyMessage | EncString;
appId: string;
};

View File

@@ -0,0 +1,4 @@
import { EncryptedMessage } from "./encryptedMessage";
import { UnencryptedMessage } from "./unencryptedMessage";
export type Message = UnencryptedMessage | EncryptedMessage;

View File

@@ -0,0 +1,4 @@
export interface MessageCommon {
version: number;
messageId: string;
}

View File

@@ -0,0 +1 @@
export type UnencryptedCommand = "bw-handshake";

View File

@@ -0,0 +1,10 @@
import { MessageCommon } from "./messageCommon";
import { UnencryptedCommand } from "./unencryptedCommand";
export type UnencryptedMessage = MessageCommon & {
command: UnencryptedCommand;
payload: {
publicKey: string;
applicationName: string;
};
};

View File

@@ -0,0 +1,16 @@
import { MessageCommon } from "./messageCommon";
export type UnencryptedMessageResponse = MessageCommon &
(
| {
payload: {
status: "success";
sharedKey: string;
};
}
| {
payload: {
error: "canceled" | "locked" | "cannot-decrypt" | "version-discrepancy";
};
}
);

View File

@@ -0,0 +1,228 @@
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus";
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { PolicyType } from "@bitwarden/common/enums/policyType";
import { CipherView } from "@bitwarden/common/models/view/cipherView";
import { LoginUriView } from "@bitwarden/common/models/view/loginUriView";
import { LoginView } from "@bitwarden/common/models/view/loginView";
import { DecryptedCommandData } from "src/models/nativeMessaging/decryptedCommandData";
import { CredentialCreatePayload } from "src/models/nativeMessaging/encryptedMessagePayloads/credentialCreatePayload";
import { CredentialRetrievePayload } from "src/models/nativeMessaging/encryptedMessagePayloads/credentialRetrievePayload";
import { CredentialUpdatePayload } from "src/models/nativeMessaging/encryptedMessagePayloads/credentialUpdatePayload";
import { PasswordGeneratePayload } from "src/models/nativeMessaging/encryptedMessagePayloads/passwordGeneratePayload";
import { AccountStatusResponse } from "src/models/nativeMessaging/encryptedMessageResponses/accountStatusResponse";
import { CipherResponse } from "src/models/nativeMessaging/encryptedMessageResponses/cipherResponse";
import { EncyptedMessageResponse } from "src/models/nativeMessaging/encryptedMessageResponses/encryptedMessageResponse";
import { FailureStatusResponse } from "src/models/nativeMessaging/encryptedMessageResponses/failureStatusResponse";
import { GenerateResponse } from "src/models/nativeMessaging/encryptedMessageResponses/generateResponse";
import { SuccessStatusResponse } from "src/models/nativeMessaging/encryptedMessageResponses/successStatusResponse";
import { UserStatusErrorResponse } from "src/models/nativeMessaging/encryptedMessageResponses/userStatusErrorResponse";
import { StateService } from "./state.service";
export class EncryptedMessageHandlerService {
constructor(
private stateService: StateService,
private authService: AuthService,
private cipherService: CipherService,
private policyService: PolicyService,
private messagingService: MessagingService,
private passwordGenerationService: PasswordGenerationService
) {}
async responseDataForCommand(
commandData: DecryptedCommandData
): Promise<EncyptedMessageResponse> {
const { command, payload } = commandData;
switch (command) {
case "bw-status": {
return await this.statusCommandHandler();
}
case "bw-credential-retrieval": {
return await this.credentialretreivalCommandHandler(payload as CredentialRetrievePayload);
}
case "bw-credential-create": {
return await this.credentialCreateCommandHandler(payload as CredentialCreatePayload);
}
case "bw-credential-update": {
return await this.credentialUpdateCommandHandler(payload as CredentialUpdatePayload);
}
case "bw-generate-password": {
return await this.generateCommandHandler(payload as PasswordGeneratePayload);
}
default:
return {
error: "cannot-decrypt",
};
}
}
private async checkUserStatus(userId: string): Promise<string> {
const activeUserId = await this.stateService.getUserId();
if (userId !== activeUserId) {
return "not-active-user";
}
const authStatus = await this.authService.getAuthStatus(activeUserId);
if (authStatus !== AuthenticationStatus.Unlocked) {
return "locked";
}
return "valid";
}
private async statusCommandHandler(): Promise<AccountStatusResponse[]> {
const accounts = this.stateService.accounts.getValue();
const activeUserId = await this.stateService.getUserId();
if (!accounts || !Object.keys(accounts)) {
return [];
}
return Promise.all(
Object.keys(accounts).map(async (userId) => {
const authStatus = await this.authService.getAuthStatus(userId);
const email = await this.stateService.getEmail({ userId });
return {
id: userId,
email,
status: authStatus === AuthenticationStatus.Unlocked ? "unlocked" : "locked",
active: userId === activeUserId,
};
})
);
}
private async credentialretreivalCommandHandler(
payload: CredentialRetrievePayload
): Promise<CipherResponse[] | UserStatusErrorResponse> {
if (payload.uri == null) {
return [];
}
const ciphersResponse: CipherResponse[] = [];
const activeUserId = await this.stateService.getUserId();
const authStatus = await this.authService.getAuthStatus(activeUserId);
if (authStatus !== AuthenticationStatus.Unlocked) {
return { error: "locked" };
}
const ciphers = await this.cipherService.getAllDecryptedForUrl(payload.uri);
ciphers.sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
ciphers.forEach((c) => {
ciphersResponse.push({
userId: activeUserId,
credentialId: c.id,
userName: c.login.username,
password: c.login.password,
name: c.name,
} as CipherResponse);
});
return ciphersResponse;
}
private async credentialCreateCommandHandler(
payload: CredentialCreatePayload
): Promise<SuccessStatusResponse | FailureStatusResponse | UserStatusErrorResponse> {
const userStatus = await this.checkUserStatus(payload.userId);
if (userStatus !== "valid") {
return { error: userStatus } as UserStatusErrorResponse;
}
const credentialCreatePayload = payload as CredentialCreatePayload;
if (
credentialCreatePayload.name == null ||
(await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership))
) {
return { status: "failure" };
}
const cipherView = new CipherView();
cipherView.type = CipherType.Login;
cipherView.name = payload.name;
cipherView.login = new LoginView();
cipherView.login.password = credentialCreatePayload.password;
cipherView.login.username = credentialCreatePayload.userName;
cipherView.login.uris = [new LoginUriView()];
cipherView.login.uris[0].uri = credentialCreatePayload.uri;
try {
const encrypted = await this.cipherService.encrypt(cipherView);
await this.cipherService.saveWithServer(encrypted);
// Notify other clients of new login
await this.messagingService.send("addedCipher");
// Refresh Desktop ciphers list
await this.messagingService.send("refreshCiphers");
return { status: "success" };
} catch (error) {
return { status: "failure" };
}
}
private async credentialUpdateCommandHandler(
payload: CredentialUpdatePayload
): Promise<SuccessStatusResponse | FailureStatusResponse | UserStatusErrorResponse> {
const userStatus = await this.checkUserStatus(payload.userId);
if (userStatus !== "valid") {
return { error: userStatus } as UserStatusErrorResponse;
}
const credentialUpdatePayload = payload as CredentialUpdatePayload;
if (credentialUpdatePayload.name == null) {
return { status: "failure" };
}
try {
const cipher = await this.cipherService.get(credentialUpdatePayload.credentialId);
if (cipher === null) {
return { status: "failure" };
}
const cipherView = await cipher.decrypt();
cipherView.name = credentialUpdatePayload.name;
cipherView.login.password = credentialUpdatePayload.password;
cipherView.login.username = credentialUpdatePayload.userName;
cipherView.login.uris[0].uri = credentialUpdatePayload.uri;
const encrypted = await this.cipherService.encrypt(cipherView);
await this.cipherService.saveWithServer(encrypted);
// Notify other clients of update
await this.messagingService.send("editedCipher");
// Refresh Desktop ciphers list
await this.messagingService.send("refreshCiphers");
return { status: "success" };
} catch (error) {
return { status: "failure" };
}
}
private async generateCommandHandler(
payload: PasswordGeneratePayload
): Promise<GenerateResponse | UserStatusErrorResponse> {
const userStatus = await this.checkUserStatus(payload.userId);
if (userStatus !== "valid") {
return { error: userStatus } as UserStatusErrorResponse;
}
const options = (await this.passwordGenerationService.getOptions())[0];
const generatedValue = await this.passwordGenerationService.generatePassword(options);
await this.passwordGenerationService.addHistory(generatedValue);
return { password: generatedValue };
}
}

View File

@@ -0,0 +1,221 @@
import { Injectable } from "@angular/core";
import { ipcRenderer } from "electron";
import Swal from "sweetalert2";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
import { Utils } from "@bitwarden/common/misc/utils";
import { EncString } from "@bitwarden/common/models/domain/encString";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
import { StateService } from "@bitwarden/common/services/state.service";
import { DecryptedCommandData } from "src/models/nativeMessaging/decryptedCommandData";
import { EncryptedMessage } from "src/models/nativeMessaging/encryptedMessage";
import { EncryptedMessageResponse } from "src/models/nativeMessaging/encryptedMessageResponse";
import { Message } from "src/models/nativeMessaging/message";
import { UnencryptedMessage } from "src/models/nativeMessaging/unencryptedMessage";
import { UnencryptedMessageResponse } from "src/models/nativeMessaging/unencryptedMessageResponse";
import { EncryptedMessageHandlerService } from "./encryptedMessageHandlerService";
const EncryptionAlgorithm = "sha1";
@Injectable()
export class NativeMessageHandlerService {
private ddgSharedSecret: SymmetricCryptoKey;
constructor(
private stateService: StateService,
private cryptoService: CryptoService,
private cryptoFunctionService: CryptoFunctionService,
private messagingService: MessagingService,
private i18nService: I18nService,
private encryptedMessageHandlerService: EncryptedMessageHandlerService
) {}
async handleMessage(message: Message) {
const decryptedCommand = message as UnencryptedMessage;
if (message.version != NativeMessagingVersion.Latest) {
this.sendResponse({
messageId: message.messageId,
version: NativeMessagingVersion.Latest,
payload: {
error: "version-discrepancy",
},
});
} else {
if (decryptedCommand.command === "bw-handshake") {
await this.handleDecryptedMessage(decryptedCommand);
} else {
await this.handleEncryptedMessage(message as EncryptedMessage);
}
}
}
private async handleDecryptedMessage(message: UnencryptedMessage) {
const { messageId, payload } = message;
const { publicKey, applicationName } = payload;
if (!publicKey) {
this.sendResponse({
messageId: messageId,
version: NativeMessagingVersion.Latest,
payload: {
error: "cannot-decrypt",
},
});
return;
}
try {
const remotePublicKey = Utils.fromB64ToArray(publicKey).buffer;
const ddgEnabled = await this.stateService.getEnableDuckDuckGoBrowserIntegration();
if (!ddgEnabled) {
this.sendResponse({
messageId: messageId,
version: NativeMessagingVersion.Latest,
payload: {
error: "canceled",
},
});
return;
}
// Ask for confirmation from user
this.messagingService.send("setFocus");
const submitted = await Swal.fire({
heightAuto: false,
titleText: this.i18nService.t("verifyNativeMessagingConnectionTitle", applicationName),
html: `${this.i18nService.t("verifyNativeMessagingConnectionDesc")}<br>${this.i18nService.t(
"verifyNativeMessagingConnectionWarning"
)}`,
showCancelButton: true,
cancelButtonText: this.i18nService.t("no"),
showConfirmButton: true,
confirmButtonText: this.i18nService.t("yes"),
allowOutsideClick: false,
focusCancel: true,
});
if (submitted.value !== true) {
this.sendResponse({
messageId: messageId,
version: NativeMessagingVersion.Latest,
payload: {
error: "canceled",
},
});
return;
}
const secret = await this.cryptoFunctionService.randomBytes(64);
this.ddgSharedSecret = new SymmetricCryptoKey(secret);
const sharedKeyB64 = new SymmetricCryptoKey(secret).toJSON().keyB64;
await this.stateService.setDuckDuckGoSharedKey(sharedKeyB64);
const encryptedSecret = await this.cryptoFunctionService.rsaEncrypt(
secret,
remotePublicKey,
EncryptionAlgorithm
);
this.sendResponse({
messageId: messageId,
version: NativeMessagingVersion.Latest,
payload: {
status: "success",
sharedKey: Utils.fromBufferToB64(encryptedSecret),
},
});
} catch (error) {
this.sendResponse({
messageId: messageId,
version: NativeMessagingVersion.Latest,
payload: {
error: "cannot-decrypt",
},
});
}
}
private async handleEncryptedMessage(message: EncryptedMessage) {
message.encryptedCommand = EncString.fromJSON(message.encryptedCommand.toString());
const decryptedCommandData = await this.decryptPayload(message);
const { command } = decryptedCommandData;
try {
const responseData = await this.encryptedMessageHandlerService.responseDataForCommand(
decryptedCommandData
);
await this.sendEncryptedResponse(message, { command, payload: responseData });
} catch (error) {
this.sendEncryptedResponse(message, { command, payload: {} });
}
}
private async encryptPayload(
payload: DecryptedCommandData,
key: SymmetricCryptoKey
): Promise<EncString> {
return await this.cryptoService.encrypt(JSON.stringify(payload), key);
}
private async decryptPayload(message: EncryptedMessage): Promise<DecryptedCommandData> {
if (!this.ddgSharedSecret) {
const storedKey = await this.stateService.getDuckDuckGoSharedKey();
if (storedKey == null) {
this.sendResponse({
messageId: message.messageId,
version: NativeMessagingVersion.Latest,
payload: {
error: "cannot-decrypt",
},
});
return;
}
this.ddgSharedSecret = SymmetricCryptoKey.fromJSON({ keyB64: storedKey });
}
return JSON.parse(
await this.cryptoService.decryptToUtf8(
message.encryptedCommand as EncString,
this.ddgSharedSecret
)
);
}
private async sendEncryptedResponse(
originalMessage: EncryptedMessage,
response: DecryptedCommandData
) {
if (!this.ddgSharedSecret) {
this.sendResponse({
messageId: originalMessage.messageId,
version: NativeMessagingVersion.Latest,
payload: {
error: "cannot-decrypt",
},
});
return;
}
const encryptedPayload = await this.encryptPayload(response, this.ddgSharedSecret);
this.sendResponse({
messageId: originalMessage.messageId,
version: NativeMessagingVersion.Latest,
encryptedPayload,
});
}
private sendResponse(response: EncryptedMessageResponse | UnencryptedMessageResponse) {
ipcRenderer.send("nativeMessagingReply", response);
}
}

View File

@@ -14,23 +14,15 @@ import { Utils } from "@bitwarden/common/misc/utils";
import { EncString } from "@bitwarden/common/models/domain/encString";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
import { LegacyMessage } from "src/models/nativeMessaging/legacyMessage";
import { LegacyMessageWrapper } from "src/models/nativeMessaging/legacyMessageWrapper";
import { Message } from "src/models/nativeMessaging/message";
import { NativeMessageHandlerService } from "./nativeMessageHandler.service";
const MessageValidTimeout = 10 * 1000;
const EncryptionAlgorithm = "sha1";
type Message = {
command: string;
userId?: string;
timestamp?: number;
publicKey?: string;
};
type OuterMessage = {
message: Message | EncString;
appId: string;
};
@Injectable()
export class NativeMessagingService {
private sharedSecrets = new Map<string, SymmetricCryptoKey>();
@@ -42,7 +34,8 @@ export class NativeMessagingService {
private logService: LogService,
private i18nService: I18nService,
private messagingService: MessagingService,
private stateService: StateService
private stateService: StateService,
private nativeMessageHandler: NativeMessageHandlerService
) {}
init() {
@@ -51,15 +44,20 @@ export class NativeMessagingService {
});
}
private async messageHandler(msg: OuterMessage) {
const appId = msg.appId;
const rawMessage = msg.message;
private async messageHandler(msg: LegacyMessageWrapper | Message) {
const outerMessage = msg as Message;
if (outerMessage.version) {
this.nativeMessageHandler.handleMessage(outerMessage);
return;
}
const { appId, message: rawMessage } = msg as LegacyMessageWrapper;
// Request to setup secure encryption
if ("command" in rawMessage && rawMessage.command === "setupEncryption") {
const remotePublicKey = Utils.fromB64ToArray(rawMessage.publicKey).buffer;
// Valudate the UserId to ensure we are logged into the same account.
// Validate the UserId to ensure we are logged into the same account.
const userIds = Object.keys(this.stateService.accounts.getValue());
if (!userIds.includes(rawMessage.userId)) {
ipcRenderer.send("nativeMessagingReply", { command: "wrongUserId", appId: appId });
@@ -103,7 +101,7 @@ export class NativeMessagingService {
return;
}
const message: Message = JSON.parse(
const message: LegacyMessage = JSON.parse(
await this.cryptoService.decryptToUtf8(rawMessage as EncString, this.sharedSecrets.get(appId))
);