[Key Connector] QA fixes (#410)

* Fix locked vault message if using key connector

* Add OTP verification on export

* Finish support for OTP on export

* Delete unneeded subclass

* update deps

* Update jslib
This commit is contained in:
Thomas Rittson 2021-11-16 19:42:30 +10:00 committed by GitHub
parent 0814f3aed6
commit 62a3ea5699
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 93 additions and 39 deletions

2
jslib

@ -1 +1 @@
Subproject commit e02e663ce1aed94d42db00dcdb2e42bdd625f0dc Subproject commit 386903f5a9ef42fed709813d40e550ba91c0ee40

View File

@ -36,6 +36,7 @@ import { SyncService } from 'jslib-common/services/sync.service';
import { TokenService } from 'jslib-common/services/token.service'; import { TokenService } from 'jslib-common/services/token.service';
import { TotpService } from 'jslib-common/services/totp.service'; import { TotpService } from 'jslib-common/services/totp.service';
import { UserService } from 'jslib-common/services/user.service'; import { UserService } from 'jslib-common/services/user.service';
import { UserVerificationService } from 'jslib-common/services/userVerification.service';
import { VaultTimeoutService } from 'jslib-common/services/vaultTimeout.service'; import { VaultTimeoutService } from 'jslib-common/services/vaultTimeout.service';
import { LowdbStorageService } from 'jslib-node/services/lowdbStorage.service'; import { LowdbStorageService } from 'jslib-node/services/lowdbStorage.service';
import { NodeApiService } from 'jslib-node/services/nodeApi.service'; import { NodeApiService } from 'jslib-node/services/nodeApi.service';
@ -87,6 +88,7 @@ export class Main {
sendService: SendService; sendService: SendService;
fileUploadService: FileUploadService; fileUploadService: FileUploadService;
keyConnectorService: KeyConnectorService; keyConnectorService: KeyConnectorService;
userVerificationService: UserVerificationService;
constructor() { constructor() {
let p = null; let p = null;
@ -139,7 +141,7 @@ export class Main {
this.sendService = new SendService(this.cryptoService, this.userService, this.apiService, this.fileUploadService, this.sendService = new SendService(this.cryptoService, this.userService, this.apiService, this.fileUploadService,
this.storageService, this.i18nService, this.cryptoFunctionService); this.storageService, this.i18nService, this.cryptoFunctionService);
this.keyConnectorService = new KeyConnectorService(this.storageService, this.userService, this.cryptoService, this.keyConnectorService = new KeyConnectorService(this.storageService, this.userService, this.cryptoService,
this.apiService, this.environmentService, this.tokenService, this.logService); this.apiService, this.tokenService, this.logService);
this.vaultTimeoutService = new VaultTimeoutService(this.cipherService, this.folderService, this.vaultTimeoutService = new VaultTimeoutService(this.cipherService, this.folderService,
this.collectionService, this.cryptoService, this.platformUtilsService, this.storageService, this.collectionService, this.cryptoService, this.platformUtilsService, this.storageService,
this.messagingService, this.searchService, this.userService, this.tokenService, this.policyService, this.messagingService, this.searchService, this.userService, this.tokenService, this.policyService,
@ -164,6 +166,8 @@ export class Main {
this.program = new Program(this); this.program = new Program(this);
this.vaultProgram = new VaultProgram(this); this.vaultProgram = new VaultProgram(this);
this.sendProgram = new SendProgram(this); this.sendProgram = new SendProgram(this);
this.userVerificationService = new UserVerificationService(this.cryptoService, this.i18nService,
this.apiService);
} }
async run() { async run() {

View File

@ -1,21 +1,23 @@
import * as program from 'commander'; import * as program from 'commander';
import * as inquirer from 'inquirer'; import * as inquirer from 'inquirer';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { ExportService } from 'jslib-common/abstractions/export.service'; import { ExportService } from 'jslib-common/abstractions/export.service';
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service';
import { Response } from 'jslib-node/cli/models/response'; import { Response } from 'jslib-node/cli/models/response';
import { PolicyType } from 'jslib-common/enums/policyType'; import { PolicyType } from 'jslib-common/enums/policyType';
import { VerificationType } from 'jslib-common/enums/verificationType';
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from 'jslib-common/misc/utils';
import { CliUtils } from '../utils'; import { CliUtils } from '../utils';
export class ExportCommand { export class ExportCommand {
constructor(private cryptoService: CryptoService, private exportService: ExportService, constructor(private exportService: ExportService, private policyService: PolicyService,
private policyService: PolicyService) { } private keyConnectorService: KeyConnectorService, private userVerificationService: UserVerificationService) { }
async run(password: string, options: program.OptionValues): Promise<Response> { async run(password: string, options: program.OptionValues): Promise<Response> {
if (options.organizationid == null && if (options.organizationid == null &&
@ -24,20 +26,20 @@ export class ExportCommand {
'One or more organization policies prevents you from exporting your personal vault.' 'One or more organization policies prevents you from exporting your personal vault.'
); );
} }
const canInteract = process.env.BW_NOINTERACTION !== 'true'; const canInteract = process.env.BW_NOINTERACTION !== 'true';
if ((password == null || password === '') && canInteract) { if (!canInteract) {
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ return Response.badRequest('User verification is required. Try running this command again in interactive mode.');
type: 'password', }
name: 'password',
message: 'Master password:', try {
}); await this.keyConnectorService.getUsesKeyConnector()
password = answer.password; ? await this.verifyOTP()
} : await this.verifyMasterPassword(password);
if (password == null || password === '') { } catch (e) {
return Response.badRequest('Master password is required.'); return Response.badRequest(e.message);
} }
if (await this.cryptoService.compareAndUpdateKeyHash(password, null)) {
let format = options.format; let format = options.format;
if (format !== 'encrypted_json' && format !== 'json') { if (format !== 'encrypted_json' && format !== 'json') {
format = 'csv'; format = 'csv';
@ -54,9 +56,6 @@ export class ExportCommand {
return Response.error(e); return Response.error(e);
} }
return await this.saveFile(exportContent, options, format); return await this.saveFile(exportContent, options, format);
} else {
return Response.error('Invalid master password.');
}
} }
async saveFile(exportContent: string, options: program.OptionValues, format: string): Promise<Response> { async saveFile(exportContent: string, options: program.OptionValues, format: string): Promise<Response> {
@ -79,4 +78,34 @@ export class ExportCommand {
} }
return this.exportService.getFileName(prefix, format); return this.exportService.getFileName(prefix, format);
} }
private async verifyMasterPassword(password: string) {
if (password == null || password === '') {
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
type: 'password',
name: 'password',
message: 'Master password:',
});
password = answer.password;
}
await this.userVerificationService.verifyUser({
type: VerificationType.MasterPassword,
secret: password,
});
}
private async verifyOTP() {
await this.userVerificationService.requestOTP();
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
type: 'password',
name: 'otp',
message: 'A verification code has been emailed to you.\n Verification code:',
});
await this.userVerificationService.verifyUser({
type: VerificationType.OTP,
secret: answer.otp,
});
}
} }

View File

@ -19,5 +19,17 @@
}, },
"importNothingError": { "importNothingError": {
"message": "Nothing was imported." "message": "Nothing was imported."
},
"verificationCodeRequired": {
"message": "Verification code is required."
},
"invalidVerificationCode": {
"message": "Invalid verification code."
},
"masterPassRequired": {
"message": "Master password is required."
},
"invalidMasterPassword": {
"message": "Invalid master password."
} }
} }

View File

@ -425,12 +425,20 @@ export class Program extends BaseProgram {
if (!hasKey) { if (!hasKey) {
const canInteract = process.env.BW_NOINTERACTION !== 'true'; const canInteract = process.env.BW_NOINTERACTION !== 'true';
if (canInteract) { if (canInteract) {
const usesKeyConnector = await this.main.keyConnectorService.getUsesKeyConnector();
if (usesKeyConnector) {
const response = Response.error('Your vault is locked. You must unlock your vault using your session key.\n' +
'If you do not have your session key, you can get a new one by logging out and logging in again.');
this.processResponse(response, true);
} else {
const command = new UnlockCommand(this.main.cryptoService, this.main.userService, const command = new UnlockCommand(this.main.cryptoService, this.main.userService,
this.main.cryptoFunctionService, this.main.apiService, this.main.logService); this.main.cryptoFunctionService, this.main.apiService, this.main.logService);
const response = await command.run(null, null); const response = await command.run(null, null);
if (!response.success) { if (!response.success) {
this.processResponse(response, true); this.processResponse(response, true);
} }
}
} else { } else {
this.processResponse(Response.error('Vault is locked.'), true); this.processResponse(Response.error('Vault is locked.'), true);
} }

View File

@ -424,7 +424,8 @@ export class VaultProgram extends Program {
}) })
.action(async (password, options) => { .action(async (password, options) => {
await this.exitIfLocked(); await this.exitIfLocked();
const command = new ExportCommand(this.main.cryptoService, this.main.exportService, this.main.policyService); const command = new ExportCommand(this.main.exportService, this.main.policyService,
this.main.keyConnectorService, this.main.userVerificationService);
const response = await command.run(password, options); const response = await command.run(password, options);
this.processResponse(response); this.processResponse(response);
}); });