From 8b650666c593efa19ee54ef7360321de63efe0e2 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 21 Jan 2022 06:03:37 +1000 Subject: [PATCH] Fix migration to Key Connector (#452) * Move Key Connector check to subclass * Move authService.logout call to main program * Move Key Connector migration check to unlock command * Use get/setConvertAccountRequired flag * Move Key Connector convert to own command, set usesKeyConnector after conversion * Remove KC conversion check from syncCommand, fix callback * Make class service private * Fix naming convention * Update jslib and deps --- jslib | 2 +- src/bw.ts | 8 +- src/commands/convertToKeyConnector.command.ts | 85 +++++++++++++++++++ src/commands/login.command.ts | 11 ++- src/commands/serve.command.ts | 6 +- src/commands/unlock.command.ts | 62 ++++++++++---- src/program.ts | 14 ++- 7 files changed, 159 insertions(+), 29 deletions(-) create mode 100644 src/commands/convertToKeyConnector.command.ts diff --git a/jslib b/jslib index cc285e5ea7..11e7133aef 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit cc285e5ea729795812d8f1f66f622b67c11efb4a +Subproject commit 11e7133aefa4443e07febde9b3add25539ec2287 diff --git a/src/bw.ts b/src/bw.ts index 6592984c2b..39b19fb149 100644 --- a/src/bw.ts +++ b/src/bw.ts @@ -50,6 +50,8 @@ import { Program } from "./program"; import { SendProgram } from "./send.program"; import { VaultProgram } from "./vault.program"; +import { Account, AccountFactory } from "jslib-common/models/domain/account"; + // Polyfills (global as any).DOMParser = new jsdom.JSDOM().window.DOMParser; @@ -136,7 +138,8 @@ export class Main { this.storageService, this.secureStorageService, this.logService, - this.stateMigrationService + this.stateMigrationService, + new AccountFactory(Account) ); this.cryptoService = new CryptoService( @@ -328,6 +331,9 @@ export class Main { } async logout() { + this.authService.logOut(() => { + /* Do nothing */ + }); const userId = await this.stateService.getUserId(); await Promise.all([ this.syncService.setLastSync(new Date(0)), diff --git a/src/commands/convertToKeyConnector.command.ts b/src/commands/convertToKeyConnector.command.ts new file mode 100644 index 0000000000..f7f61eb540 --- /dev/null +++ b/src/commands/convertToKeyConnector.command.ts @@ -0,0 +1,85 @@ +import * as inquirer from "inquirer"; + +import { ApiService } from "jslib-common/abstractions/api.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; +import { SyncService } from "jslib-common/abstractions/sync.service"; + +import { Response } from "jslib-node/cli/models/response"; +import { MessageResponse } from "jslib-node/cli/models/response/messageResponse"; + +export class ConvertToKeyConnectorCommand { + constructor( + private apiService: ApiService, + private keyConnectorService: KeyConnectorService, + private environmentService: EnvironmentService, + private syncService: SyncService, + private logout: () => Promise + ) {} + + async run(): Promise { + // If no interaction available, alert user to use web vault + const canInteract = process.env.BW_NOINTERACTION !== "true"; + if (!canInteract) { + await this.logout(); + return Response.error( + new MessageResponse( + "An organization you are a member of is using Key Connector. " + + "In order to access the vault, you must opt-in to Key Connector now via the web vault. You have been logged out.", + null + ) + ); + } + + const organization = await this.keyConnectorService.getManagingOrganization(); + + const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ + type: "list", + name: "convert", + message: + organization.name + + " is using a self-hosted key server. A master password is no longer required to log in for members of this organization. ", + choices: [ + { + name: "Remove master password and unlock", + value: "remove", + }, + { + name: "Leave organization and unlock", + value: "leave", + }, + { + name: "Log out", + value: "exit", + }, + ], + }); + + if (answer.convert === "remove") { + try { + await this.keyConnectorService.migrateUser(); + } catch (e) { + await this.logout(); + throw e; + } + + await this.keyConnectorService.removeConvertAccountRequired(); + await this.keyConnectorService.setUsesKeyConnector(true); + + // Update environment URL - required for api key login + const urls = this.environmentService.getUrls(); + urls.keyConnector = organization.keyConnectorUrl; + await this.environmentService.setUrls(urls, true); + + return Response.success(); + } else if (answer.convert === "leave") { + await this.apiService.postLeaveOrganization(organization.id); + await this.keyConnectorService.removeConvertAccountRequired(); + await this.syncService.fullSync(true); + return Response.success(); + } else { + await this.logout(); + return Response.error("You have been logged out."); + } + } +} diff --git a/src/commands/login.command.ts b/src/commands/login.command.ts index 273099d46f..6a4571f22a 100644 --- a/src/commands/login.command.ts +++ b/src/commands/login.command.ts @@ -1,5 +1,4 @@ import * as program from "commander"; -import * as inquirer from "inquirer"; import { ApiService } from "jslib-common/abstractions/api.service"; import { AuthService } from "jslib-common/abstractions/auth.service"; @@ -27,7 +26,6 @@ export class LoginCommand extends BaseLoginCommand { authService: AuthService, apiService: ApiService, cryptoFunctionService: CryptoFunctionService, - syncService: SyncService, i18nService: I18nService, environmentService: EnvironmentService, passwordGenerationService: PasswordGenerationService, @@ -35,7 +33,8 @@ export class LoginCommand extends BaseLoginCommand { stateService: StateService, cryptoService: CryptoService, policyService: PolicyService, - keyConnectorService: KeyConnectorService, + private syncService: SyncService, + private keyConnectorService: KeyConnectorService, private logoutCallback: () => Promise ) { super( @@ -49,9 +48,7 @@ export class LoginCommand extends BaseLoginCommand { stateService, cryptoService, policyService, - "cli", - syncService, - keyConnectorService + "cli" ); this.logout = this.logoutCallback; this.validatedParams = async () => { @@ -59,6 +56,8 @@ export class LoginCommand extends BaseLoginCommand { process.env.BW_SESSION = Utils.fromBufferToB64(key); }; this.success = async () => { + await this.syncService.fullSync(true); + const usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector(); if ( diff --git a/src/commands/serve.command.ts b/src/commands/serve.command.ts index 67d79d3d47..9d692aab37 100644 --- a/src/commands/serve.command.ts +++ b/src/commands/serve.command.ts @@ -107,7 +107,11 @@ export class ServeCommand { this.main.stateService, this.main.cryptoFunctionService, this.main.apiService, - this.main.logService + this.main.logService, + this.main.keyConnectorService, + this.main.environmentService, + this.main.syncService, + async () => await this.main.logout() ); this.sendCreateCommand = new SendCreateCommand( diff --git a/src/commands/unlock.command.ts b/src/commands/unlock.command.ts index 501855190d..9d527ad676 100644 --- a/src/commands/unlock.command.ts +++ b/src/commands/unlock.command.ts @@ -3,7 +3,10 @@ import * as inquirer from "inquirer"; import { ApiService } from "jslib-common/abstractions/api.service"; import { CryptoService } from "jslib-common/abstractions/crypto.service"; import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; import { StateService } from "jslib-common/abstractions/state.service"; +import { SyncService } from "jslib-common/abstractions/sync.service"; import { Response } from "jslib-node/cli/models/response"; import { MessageResponse } from "jslib-node/cli/models/response/messageResponse"; @@ -16,13 +19,19 @@ import { HashPurpose } from "jslib-common/enums/hashPurpose"; import { NodeUtils } from "jslib-common/misc/nodeUtils"; import { ConsoleLogService } from "jslib-common/services/consoleLog.service"; +import { ConvertToKeyConnectorCommand } from "./convertToKeyConnector.command"; + export class UnlockCommand { constructor( private cryptoService: CryptoService, private stateService: StateService, private cryptoFunctionService: CryptoFunctionService, private apiService: ApiService, - private logService: ConsoleLogService + private logService: ConsoleLogService, + private keyConnectorService: KeyConnectorService, + private environmentService: EnvironmentService, + private syncService: SyncService, + private logout: () => Promise ) {} async run(password: string, cmdOptions: Record) { @@ -92,22 +101,22 @@ export class UnlockCommand { if (passwordValid) { await this.cryptoService.setKey(key); - const res = new MessageResponse( - "Your vault is now unlocked!", - "\n" + - "To unlock your vault, set your session key to the `BW_SESSION` environment variable. ex:\n" + - '$ export BW_SESSION="' + - process.env.BW_SESSION + - '"\n' + - '> $env:BW_SESSION="' + - process.env.BW_SESSION + - '"\n\n' + - "You can also pass the session key to any command with the `--session` option. ex:\n" + - "$ bw list items --session " + - process.env.BW_SESSION - ); - res.raw = process.env.BW_SESSION; - return Response.success(res); + + if (await this.keyConnectorService.getConvertAccountRequired()) { + const convertToKeyConnectorCommand = new ConvertToKeyConnectorCommand( + this.apiService, + this.keyConnectorService, + this.environmentService, + this.syncService, + this.logout + ); + const convertResponse = await convertToKeyConnectorCommand.run(); + if (!convertResponse.success) { + return convertResponse; + } + } + + return this.successResponse(); } else { return Response.error("Invalid master password."); } @@ -117,6 +126,25 @@ export class UnlockCommand { const key = await this.cryptoFunctionService.randomBytes(64); process.env.BW_SESSION = Utils.fromBufferToB64(key); } + + private async successResponse() { + const res = new MessageResponse( + "Your vault is now unlocked!", + "\n" + + "To unlock your vault, set your session key to the `BW_SESSION` environment variable. ex:\n" + + '$ export BW_SESSION="' + + process.env.BW_SESSION + + '"\n' + + '> $env:BW_SESSION="' + + process.env.BW_SESSION + + '"\n\n' + + "You can also pass the session key to any command with the `--session` option. ex:\n" + + "$ bw list items --session " + + process.env.BW_SESSION + ); + res.raw = process.env.BW_SESSION; + return Response.success(res); + } } class Options { diff --git a/src/program.ts b/src/program.ts index 8948fc3a28..2fa4b04a11 100644 --- a/src/program.ts +++ b/src/program.ts @@ -149,7 +149,6 @@ export class Program extends BaseProgram { this.main.authService, this.main.apiService, this.main.cryptoFunctionService, - this.main.syncService, this.main.i18nService, this.main.environmentService, this.main.passwordGenerationService, @@ -157,6 +156,7 @@ export class Program extends BaseProgram { this.main.stateService, this.main.cryptoService, this.main.policyService, + this.main.syncService, this.main.keyConnectorService, async () => await this.main.logout() ); @@ -257,7 +257,11 @@ export class Program extends BaseProgram { this.main.stateService, this.main.cryptoFunctionService, this.main.apiService, - this.main.logService + this.main.logService, + this.main.keyConnectorService, + this.main.environmentService, + this.main.syncService, + async () => await this.main.logout() ); const response = await command.run(password, cmd); this.processResponse(response); @@ -511,7 +515,11 @@ export class Program extends BaseProgram { this.main.stateService, this.main.cryptoFunctionService, this.main.apiService, - this.main.logService + this.main.logService, + this.main.keyConnectorService, + this.main.environmentService, + this.main.syncService, + this.main.logout ); const response = await command.run(null, null); if (!response.success) {