[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 { TotpService } from 'jslib-common/services/totp.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 { LowdbStorageService } from 'jslib-node/services/lowdbStorage.service';
import { NodeApiService } from 'jslib-node/services/nodeApi.service';
@ -87,6 +88,7 @@ export class Main {
sendService: SendService;
fileUploadService: FileUploadService;
keyConnectorService: KeyConnectorService;
userVerificationService: UserVerificationService;
constructor() {
let p = null;
@ -139,7 +141,7 @@ export class Main {
this.sendService = new SendService(this.cryptoService, this.userService, this.apiService, this.fileUploadService,
this.storageService, this.i18nService, this.cryptoFunctionService);
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.collectionService, this.cryptoService, this.platformUtilsService, this.storageService,
this.messagingService, this.searchService, this.userService, this.tokenService, this.policyService,
@ -164,6 +166,8 @@ export class Main {
this.program = new Program(this);
this.vaultProgram = new VaultProgram(this);
this.sendProgram = new SendProgram(this);
this.userVerificationService = new UserVerificationService(this.cryptoService, this.i18nService,
this.apiService);
}
async run() {

View File

@ -1,21 +1,23 @@
import * as program from 'commander';
import * as inquirer from 'inquirer';
import { CryptoService } from 'jslib-common/abstractions/crypto.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 { UserVerificationService } from 'jslib-common/abstractions/userVerification.service';
import { Response } from 'jslib-node/cli/models/response';
import { PolicyType } from 'jslib-common/enums/policyType';
import { VerificationType } from 'jslib-common/enums/verificationType';
import { Utils } from 'jslib-common/misc/utils';
import { CliUtils } from '../utils';
export class ExportCommand {
constructor(private cryptoService: CryptoService, private exportService: ExportService,
private policyService: PolicyService) { }
constructor(private exportService: ExportService, private policyService: PolicyService,
private keyConnectorService: KeyConnectorService, private userVerificationService: UserVerificationService) { }
async run(password: string, options: program.OptionValues): Promise<Response> {
if (options.organizationid == null &&
@ -24,39 +26,36 @@ export class ExportCommand {
'One or more organization policies prevents you from exporting your personal vault.'
);
}
const canInteract = process.env.BW_NOINTERACTION !== 'true';
if ((password == null || password === '') && canInteract) {
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
type: 'password',
name: 'password',
message: 'Master password:',
});
password = answer.password;
}
if (password == null || password === '') {
return Response.badRequest('Master password is required.');
if (!canInteract) {
return Response.badRequest('User verification is required. Try running this command again in interactive mode.');
}
if (await this.cryptoService.compareAndUpdateKeyHash(password, null)) {
let format = options.format;
if (format !== 'encrypted_json' && format !== 'json') {
format = 'csv';
}
if (options.organizationid != null && !Utils.isGuid(options.organizationid)) {
return Response.error('`' + options.organizationid + '` is not a GUID.');
}
let exportContent: string = null;
try {
exportContent = options.organizationid != null ?
await this.exportService.getOrganizationExport(options.organizationid, format) :
await this.exportService.getExport(format);
} catch (e) {
return Response.error(e);
}
return await this.saveFile(exportContent, options, format);
} else {
return Response.error('Invalid master password.');
try {
await this.keyConnectorService.getUsesKeyConnector()
? await this.verifyOTP()
: await this.verifyMasterPassword(password);
} catch (e) {
return Response.badRequest(e.message);
}
let format = options.format;
if (format !== 'encrypted_json' && format !== 'json') {
format = 'csv';
}
if (options.organizationid != null && !Utils.isGuid(options.organizationid)) {
return Response.error('`' + options.organizationid + '` is not a GUID.');
}
let exportContent: string = null;
try {
exportContent = options.organizationid != null ?
await this.exportService.getOrganizationExport(options.organizationid, format) :
await this.exportService.getExport(format);
} catch (e) {
return Response.error(e);
}
return await this.saveFile(exportContent, options, format);
}
async saveFile(exportContent: string, options: program.OptionValues, format: string): Promise<Response> {
@ -79,4 +78,34 @@ export class ExportCommand {
}
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": {
"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,11 +425,19 @@ export class Program extends BaseProgram {
if (!hasKey) {
const canInteract = process.env.BW_NOINTERACTION !== 'true';
if (canInteract) {
const command = new UnlockCommand(this.main.cryptoService, this.main.userService,
this.main.cryptoFunctionService, this.main.apiService, this.main.logService);
const response = await command.run(null, null);
if (!response.success) {
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,
this.main.cryptoFunctionService, this.main.apiService, this.main.logService);
const response = await command.run(null, null);
if (!response.success) {
this.processResponse(response, true);
}
}
} else {
this.processResponse(Response.error('Vault is locked.'), true);

View File

@ -424,7 +424,8 @@ export class VaultProgram extends Program {
})
.action(async (password, options) => {
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);
this.processResponse(response);
});