From 83a32e32e6fb587803649e876f419b0bb0f4ddc5 Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Wed, 24 Aug 2022 09:46:52 -0400 Subject: [PATCH] [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 --- .../native-messaging-test-runner/package.json | 4 +- .../src/bw-credential-create.ts | 45 ++++++++++ .../src/bw-credential-update.ts | 80 +++++++++++++++++ .../src/bw-handshake.ts | 7 +- .../src/bw-status.ts | 15 ++-- .../src/nativeMessageService.ts | 52 ++++++++++++ .../src/app/services/services.module.ts | 2 + .../{ciphersResponse.ts => cipherResponse.ts} | 2 +- .../credentialCreatePayload.ts | 7 ++ .../credentialUpdatePayload.ts | 8 ++ .../src/models/nativeMessaging/index.ts | 4 +- .../services/nativeMessageHandler.service.ts | 85 ++++++++++++++++++- 12 files changed, 293 insertions(+), 18 deletions(-) create mode 100644 apps/desktop/native-messaging-test-runner/src/bw-credential-create.ts create mode 100644 apps/desktop/native-messaging-test-runner/src/bw-credential-update.ts rename apps/desktop/src/models/nativeMessaging/{ciphersResponse.ts => cipherResponse.ts} (75%) create mode 100644 apps/desktop/src/models/nativeMessaging/credentialCreatePayload.ts create mode 100644 apps/desktop/src/models/nativeMessaging/credentialUpdatePayload.ts diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index 9e3da4a73533..d55a900bc487 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -6,7 +6,9 @@ "scripts": { "handshake": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/bw-handshake.js", "status": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/bw-status.js", - "retrieve": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/bw-credential-retrieval.js" + "retrieve": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/bw-credential-retrieval.js", + "create": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/bw-credential-create.js", + "update": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/bw-credential-update.js" }, "author": "Bitwarden Inc. (https://bitwarden.com)", "license": "GPL-3.0", diff --git a/apps/desktop/native-messaging-test-runner/src/bw-credential-create.ts b/apps/desktop/native-messaging-test-runner/src/bw-credential-create.ts new file mode 100644 index 000000000000..5712e0346bf4 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/bw-credential-create.ts @@ -0,0 +1,45 @@ +import "module-alias/register"; + +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; + +import { LogUtils } from "./logUtils"; +import NativeMessageService from "./nativeMessageService"; +import * as config from "./variables"; + +const argv: any = yargs(hideBin(process.argv)).option("name", { + alias: "n", + demand: true, + describe: "Name that the created login will be given", + type: "string", +}).argv; + +const { name } = argv; + +(async () => { + const nativeMessageService = new NativeMessageService(1.0); + // Handshake + LogUtils.logWarning("Sending Handshake"); + const handshakeResponse = await nativeMessageService.sendHandshake(config.testRsaPublicKey); + + if (handshakeResponse.status !== "success") { + LogUtils.logError(" Handshake failed. Status was:", handshakeResponse.status); + nativeMessageService.disconnect(); + return; + } + + LogUtils.logSuccess("Handshake success response"); + const response = await nativeMessageService.credentialCreation(handshakeResponse.sharedKey, name); + + if (response.payload.status === "failure") { + LogUtils.logError("Failure response returned "); + } else if (response.payload.status === "success") { + LogUtils.logSuccess("Success response returned "); + } else if (response.payload.error === "locked") { + LogUtils.logError("Error: vault is locked"); + } else { + LogUtils.logWarning("Other response: ", response); + } + + nativeMessageService.disconnect(); +})(); diff --git a/apps/desktop/native-messaging-test-runner/src/bw-credential-update.ts b/apps/desktop/native-messaging-test-runner/src/bw-credential-update.ts new file mode 100644 index 000000000000..9cd764334bc8 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/bw-credential-update.ts @@ -0,0 +1,80 @@ +import "module-alias/register"; + +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; + +import { LogUtils } from "./logUtils"; +import NativeMessageService from "./nativeMessageService"; +import * as config from "./variables"; + +// Command line arguments +const argv: any = yargs(hideBin(process.argv)) + .option("name", { + alias: "n", + demand: true, + describe: "Name that the updated login will be given", + type: "string", + }) + .option("username", { + alias: "u", + demand: true, + describe: "Username that the login will be given", + type: "string", + }) + .option("password", { + alias: "p", + demand: true, + describe: "Password that the login will be given", + type: "string", + }) + .option("uri", { + demand: true, + describe: "Uri that the login will be given", + type: "string", + }).argv; + +const { name, username, password, uri } = argv; + +(async () => { + const nativeMessageService = new NativeMessageService(1.0); + // Handshake + LogUtils.logWarning("Sending Handshake"); + const handshakeResponse = await nativeMessageService.sendHandshake(config.testRsaPublicKey); + + if (handshakeResponse.status !== "success") { + LogUtils.logError(" Handshake failed. Status was:", handshakeResponse.status); + nativeMessageService.disconnect(); + return; + } + LogUtils.logSuccess("Handshake success response"); + + // Get active account userId + const status = await nativeMessageService.checkStatus(handshakeResponse.sharedKey); + + const activeUser = status.payload.filter((a) => a.active === true && a.status === "unlocked")[0]; + if (activeUser === undefined) { + LogUtils.logError("No active or unlocked user"); + } + LogUtils.logWarning("Active userId: " + activeUser.id); + + const response = await nativeMessageService.credentialUpdate( + handshakeResponse.sharedKey, + name, + password, + username, + uri, + activeUser.id, + // Replace with credentialId you want to update + "2a08b546-fa9d-48cc-ae8e-ae7601207da9" + ); + + if (response.payload.status === "failure") { + LogUtils.logError("Failure response returned "); + } else if (response.payload.status === "success") { + LogUtils.logSuccess("Success response returned "); + } else { + LogUtils.logWarning("Other response: ", response); + } + + nativeMessageService.disconnect(); +})(); diff --git a/apps/desktop/native-messaging-test-runner/src/bw-handshake.ts b/apps/desktop/native-messaging-test-runner/src/bw-handshake.ts index 0068a830e1ad..96fdedcc8bf3 100644 --- a/apps/desktop/native-messaging-test-runner/src/bw-handshake.ts +++ b/apps/desktop/native-messaging-test-runner/src/bw-handshake.ts @@ -1,3 +1,4 @@ +import { LogUtils } from "./logUtils"; import NativeMessageService from "./nativeMessageService"; import * as config from "./variables"; @@ -5,11 +6,11 @@ import * as config from "./variables"; const nativeMessageService = new NativeMessageService(1.0); const response = await nativeMessageService.sendHandshake(config.testRsaPublicKey); - console.log("[Handshake Command]\x1b[32m Received response to handshake request \x1b[0m"); + LogUtils.logSuccess("Received response to handshake request"); if (response.status === "success") { - console.log("[Handshake Command]\x1b[32m Handshake success response \x1b[0m"); + LogUtils.logSuccess("Handshake success response"); } else { - console.log("[Handshake Command]\x1b[31m Handshake failure response \x1b[0m"); + LogUtils.logError("Handshake failure response"); } nativeMessageService.disconnect(); })(); diff --git a/apps/desktop/native-messaging-test-runner/src/bw-status.ts b/apps/desktop/native-messaging-test-runner/src/bw-status.ts index cbd8a4775240..ac8422f087a9 100644 --- a/apps/desktop/native-messaging-test-runner/src/bw-status.ts +++ b/apps/desktop/native-messaging-test-runner/src/bw-status.ts @@ -1,24 +1,23 @@ import "module-alias/register"; +import { LogUtils } from "./logUtils"; import NativeMessageService from "./nativeMessageService"; import * as config from "./variables"; (async () => { const nativeMessageService = new NativeMessageService(1.0); - console.log("[Status Command]\x1b[32m Sending Handshake \x1b[0m"); + LogUtils.logWarning("Sending Handshake"); const handshakeResponse = await nativeMessageService.sendHandshake(config.testRsaPublicKey); - console.log("[Status Command]\x1b[32m Received response to handshake request \x1b[0m"); + LogUtils.logSuccess("Received response to handshake request"); if (handshakeResponse.status !== "success") { - console.log( - `[Status Command]\x1b[31m Handshake failed \x1b[0m Status was: ${handshakeResponse.status}` - ); + LogUtils.logError(" Handshake failed. Status was:", handshakeResponse.status); + nativeMessageService.disconnect(); return; } - console.log("[Status Command]\x1b[32m Handshake success response \x1b[0m"); + LogUtils.logSuccess("Handshake success response"); const status = await nativeMessageService.checkStatus(handshakeResponse.sharedKey); - console.log(`[Status Command] Status output is: `, status); - + LogUtils.logSuccess("Status output is: ", status); nativeMessageService.disconnect(); })(); diff --git a/apps/desktop/native-messaging-test-runner/src/nativeMessageService.ts b/apps/desktop/native-messaging-test-runner/src/nativeMessageService.ts index b38501af1466..4cf08d2a812f 100644 --- a/apps/desktop/native-messaging-test-runner/src/nativeMessageService.ts +++ b/apps/desktop/native-messaging-test-runner/src/nativeMessageService.ts @@ -9,6 +9,8 @@ import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service import { EncryptService } from "@bitwarden/common/services/encrypt.service"; import { NodeCryptoFunctionService } from "@bitwarden/node/services/nodeCryptoFunction.service"; +import { CredentialCreatePayload } from "../../src/models/nativeMessaging/credentialCreatePayload"; +import { CredentialUpdatePayload } from "../../src/models/nativeMessaging/credentialUpdatePayload"; import { DecryptedCommandData } from "../../src/models/nativeMessaging/decryptedCommandData"; import { EncryptedMessage } from "../../src/models/nativeMessaging/encryptedMessage"; import { EncryptedMessageResponse } from "../../src/models/nativeMessaging/encryptedMessageResponse"; @@ -87,6 +89,56 @@ export default class NativeMessageService { return this.decryptResponsePayload(response.encryptedPayload, key); } + async credentialCreation(key: string, name: string): Promise { + const encryptedCommand = await this.encryptCommandData( + { + command: "bw-credential-create", + payload: { + name: name, + userName: "SuperAwesomeUser", + password: "dolhpin", + uri: "google.com", + } as CredentialCreatePayload, + }, + key + ); + const response = await this.sendEncryptedMessage({ + encryptedCommand, + }); + + return this.decryptResponsePayload(response.encryptedPayload, key); + } + + async credentialUpdate( + key: string, + name: string, + password: string, + username: string, + uri: string, + userId: string, + credentialId: string + ): Promise { + const encryptedCommand = await this.encryptCommandData( + { + command: "bw-credential-update", + payload: { + userId: userId, + credentialId: credentialId, + userName: username, + uri: uri, + password: password, + name: name, + } as CredentialUpdatePayload, + }, + key + ); + const response = await this.sendEncryptedMessage({ + encryptedCommand, + }); + + return this.decryptResponsePayload(response.encryptedPayload, key); + } + // Private message sending private async sendEncryptedMessage( diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index f6e1fe462f0d..f4cce95d35d4 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -26,6 +26,7 @@ import { import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service"; import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service"; +import { PolicyService } 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"; @@ -158,6 +159,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK"); CryptoServiceAbstraction, CryptoFunctionServiceAbstraction, CipherService, + PolicyService, ], }, ], diff --git a/apps/desktop/src/models/nativeMessaging/ciphersResponse.ts b/apps/desktop/src/models/nativeMessaging/cipherResponse.ts similarity index 75% rename from apps/desktop/src/models/nativeMessaging/ciphersResponse.ts rename to apps/desktop/src/models/nativeMessaging/cipherResponse.ts index 14bddc9ff58f..8540bb4d0bd4 100644 --- a/apps/desktop/src/models/nativeMessaging/ciphersResponse.ts +++ b/apps/desktop/src/models/nativeMessaging/cipherResponse.ts @@ -1,4 +1,4 @@ -export type CiphersResponse = { +export type CipherResponse = { userId: string; credentialId: string; userName: string; diff --git a/apps/desktop/src/models/nativeMessaging/credentialCreatePayload.ts b/apps/desktop/src/models/nativeMessaging/credentialCreatePayload.ts new file mode 100644 index 000000000000..8c0b2115e29d --- /dev/null +++ b/apps/desktop/src/models/nativeMessaging/credentialCreatePayload.ts @@ -0,0 +1,7 @@ +export type CredentialCreatePayload = { + userId: string; + userName: string; + password: string; + name: string; + uri: string; +}; diff --git a/apps/desktop/src/models/nativeMessaging/credentialUpdatePayload.ts b/apps/desktop/src/models/nativeMessaging/credentialUpdatePayload.ts new file mode 100644 index 000000000000..3f994d695db1 --- /dev/null +++ b/apps/desktop/src/models/nativeMessaging/credentialUpdatePayload.ts @@ -0,0 +1,8 @@ +export type CredentialUpdatePayload = { + userId: string; + userName: string; + password: string; + name: string; + uri: string; + credentialId: string; +}; diff --git a/apps/desktop/src/models/nativeMessaging/index.ts b/apps/desktop/src/models/nativeMessaging/index.ts index 1b8b7e502f9e..d573fc417333 100644 --- a/apps/desktop/src/models/nativeMessaging/index.ts +++ b/apps/desktop/src/models/nativeMessaging/index.ts @@ -1,4 +1,6 @@ -export * from "./ciphersResponse"; +export * from "./cipherResponse"; +export * from "./credentialCreatePayload"; +export * from "./credentialUpdatePayload"; export * from "./decryptedCommandData"; export * from "./encryptedCommand"; export * from "./encryptedMessage"; diff --git a/apps/desktop/src/services/nativeMessageHandler.service.ts b/apps/desktop/src/services/nativeMessageHandler.service.ts index f54f55b6895c..233af9d803c0 100644 --- a/apps/desktop/src/services/nativeMessageHandler.service.ts +++ b/apps/desktop/src/services/nativeMessageHandler.service.ts @@ -4,14 +4,22 @@ import { ipcRenderer } from "electron"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.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 { Utils } from "@bitwarden/common/misc/utils"; import { EncString } from "@bitwarden/common/models/domain/encString"; import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey"; +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 { AuthService } from "@bitwarden/common/services/auth.service"; import { StateService } from "@bitwarden/common/services/state.service"; -import { CiphersResponse } from "src/models/nativeMessaging/ciphersResponse"; +import { CipherResponse } from "src/models/nativeMessaging/cipherResponse"; +import { CredentialCreatePayload } from "src/models/nativeMessaging/credentialCreatePayload"; +import { CredentialUpdatePayload } from "src/models/nativeMessaging/credentialUpdatePayload"; import { DecryptedCommandData } from "src/models/nativeMessaging/decryptedCommandData"; import { EncryptedMessage } from "src/models/nativeMessaging/encryptedMessage"; import { EncryptedMessageResponse } from "src/models/nativeMessaging/encryptedMessageResponse"; @@ -30,7 +38,8 @@ export class NativeMessageHandler { private authService: AuthService, private cryptoService: CryptoService, private cryptoFunctionService: CryptoFunctionService, - private cipherService: CipherService + private cipherService: CipherService, + private policyService: PolicyService ) {} async handleMessage(message: Message) { @@ -146,7 +155,7 @@ export class NativeMessageHandler { return; } - const ciphersResponse: CiphersResponse[] = []; + const ciphersResponse: CipherResponse[] = []; const activeUserId = await this.stateService.getUserId(); const authStatus = await this.authService.getAuthStatus(activeUserId); @@ -164,11 +173,79 @@ export class NativeMessageHandler { userName: c.login.username, password: c.login.password, name: c.name, - } as CiphersResponse); + } as CipherResponse); }); return ciphersResponse; } + case "bw-credential-create": { + const activeUserId = await this.stateService.getUserId(); + const authStatus = await this.authService.getAuthStatus(activeUserId); + + if (authStatus !== AuthenticationStatus.Unlocked) { + return { error: "locked" }; + } + + 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); + + return { status: "success" }; + } catch (error) { + return { status: "failure" }; + } + } + case "bw-credential-update": { + const activeUserId = await this.stateService.getUserId(); + const authStatus = await this.authService.getAuthStatus(activeUserId); + + if (authStatus !== AuthenticationStatus.Unlocked) { + return { error: "locked" }; + } + + 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); + + return { status: "success" }; + } catch (error) { + return { status: "failure" }; + } + } default: return { error: "cannot-decrypt",