1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 06:43:35 +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

@@ -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 };
}
}