mirror of
https://github.com/bitwarden/browser.git
synced 2024-10-06 05:28:51 +02:00
Feature/password protected export (#446)
* Update jslib
* Bumped version to 1.20.0 (#421)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
(cherry picked from commit 3e4aa8e476
)
* password protected export
* Run Prettier
* Add importer to list of known file types
* Improve launch.json settings
* Turn on import from password protected file
* Run prettier
* Fix webpack source map path change
* Update getPassword helper to use new options class
* Prettier
* Add client type
* Remove master password requirement for export
Alter password optional argument to indicating the file should be password protected rather than account protected
* update jslib
* Handle passwordProtected automagically
* Remove passwordproteted type from import command
* Update src/utils.ts
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
* Update src/vault.program.ts
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
* Use new util method
* remove password protected format
* Update jslib
* Clarify export command
* Run prettier
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Gibson <gibson.matt10@gmail.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
parent
8e65d3e8d2
commit
323c3ee04a
@ -1,6 +1,7 @@
|
||||
# Build directories
|
||||
build
|
||||
dist
|
||||
coverage
|
||||
|
||||
jslib
|
||||
|
||||
|
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
@ -8,6 +8,17 @@
|
||||
"protocol": "inspector",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"program": "${workspaceFolder}/build/bw.js",
|
||||
"env": {
|
||||
"BW_SESSION": "fPZb0J+1NBzQ+HB512pLhSIIt2aRoOjqs6SrbxbTHVcsZdFk1cthzjBIMqBa2X7fjOOA3VU0bnR42fYeuWj2Vw=="
|
||||
},
|
||||
"sourceMapPathOverrides": {
|
||||
"meteor://💻app/*": "${workspaceFolder}/*",
|
||||
"webpack:///./~/*": "${workspaceFolder}/node_modules/*",
|
||||
"webpack://?:*/*": "${workspaceFolder}/*",
|
||||
"webpack://@bitwarden/cli/*": "${workspaceFolder}/*"
|
||||
},
|
||||
"smartStep": true,
|
||||
"console": "integratedTerminal",
|
||||
"args": ["login", "sdfsd@sdfdf.com", "ddddddd"]
|
||||
}
|
||||
]
|
||||
|
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"debug.javascript.terminalOptions": {
|
||||
"sourceMapPathOverrides": {
|
||||
"meteor://💻app/*": "${workspaceFolder}/*",
|
||||
"webpack:///./~/*": "${workspaceFolder}/node_modules/*",
|
||||
"webpack://?:*/*": "${workspaceFolder}/*",
|
||||
"webpack://@bitwarden/cli/*": "${workspaceFolder}/*"
|
||||
}
|
||||
}
|
||||
}
|
2
jslib
2
jslib
@ -1 +1 @@
|
||||
Subproject commit a6092916d80424b8bf4d34e321a0b58f15c7519d
|
||||
Subproject commit 78b5f1504208931e17dbfd447331447b6fc4ca1f
|
@ -1,29 +1,21 @@
|
||||
import * as program from "commander";
|
||||
import * as inquirer from "inquirer";
|
||||
|
||||
import { ExportService } from "jslib-common/abstractions/export.service";
|
||||
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
|
||||
import { ExportFormat, ExportService } from "jslib-common/abstractions/export.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 exportService: ExportService,
|
||||
private policyService: PolicyService,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
private userVerificationService: UserVerificationService
|
||||
) {}
|
||||
constructor(private exportService: ExportService, private policyService: PolicyService) {}
|
||||
|
||||
async run(password: string, options: program.OptionValues): Promise<Response> {
|
||||
async run(options: program.OptionValues): Promise<Response> {
|
||||
if (
|
||||
options.organizationid == null &&
|
||||
(await this.policyService.policyAppliesToUser(PolicyType.DisablePersonalVaultExport))
|
||||
@ -33,44 +25,39 @@ export class ExportCommand {
|
||||
);
|
||||
}
|
||||
|
||||
const canInteract = process.env.BW_NOINTERACTION !== "true";
|
||||
if (!canInteract) {
|
||||
return Response.badRequest(
|
||||
"User verification is required. Try running this command again in interactive mode."
|
||||
);
|
||||
}
|
||||
const format = options.format ?? "csv";
|
||||
|
||||
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);
|
||||
format === "encrypted_json"
|
||||
? await this.getProtectedExport(options.password, options.organizationid)
|
||||
: await this.getUnprotectedExport(format, options.organizationid);
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
return await this.saveFile(exportContent, options, format);
|
||||
}
|
||||
|
||||
async saveFile(
|
||||
private async getProtectedExport(passwordOption: string | boolean, organizationId?: string) {
|
||||
const password = await this.promptPassword(passwordOption);
|
||||
return password == null
|
||||
? await this.exportService.getExport("encrypted_json", organizationId)
|
||||
: await this.exportService.getPasswordProtectedExport(password, organizationId);
|
||||
}
|
||||
|
||||
private async getUnprotectedExport(format: ExportFormat, organizationId?: string) {
|
||||
return this.exportService.getExport(format, organizationId);
|
||||
}
|
||||
|
||||
private async saveFile(
|
||||
exportContent: string,
|
||||
options: program.OptionValues,
|
||||
format: string
|
||||
format: ExportFormat
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const fileName = this.getFileName(format, options.organizationid != null ? "org" : null);
|
||||
@ -80,7 +67,7 @@ export class ExportCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private getFileName(format: string, prefix?: string) {
|
||||
private getFileName(format: ExportFormat, prefix?: string) {
|
||||
if (format === "encrypted_json") {
|
||||
if (prefix == null) {
|
||||
prefix = "encrypted";
|
||||
@ -92,35 +79,22 @@ export class ExportCommand {
|
||||
return this.exportService.getFileName(prefix, format);
|
||||
}
|
||||
|
||||
private async verifyMasterPassword(password: string) {
|
||||
if (password == null || password === "") {
|
||||
private async promptPassword(password: string | boolean) {
|
||||
// boolean => flag set with no value, we need to prompt for password
|
||||
// string => flag set with value, use this value for password
|
||||
// undefined/null/false => account protect, not password, no password needed
|
||||
if (typeof password === "string") {
|
||||
return password;
|
||||
} else if (password) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "password",
|
||||
name: "password",
|
||||
message: "Master password:",
|
||||
message: "Export file password:",
|
||||
});
|
||||
password = answer.password;
|
||||
return answer.password as string;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
import * as program from "commander";
|
||||
import * as inquirer from "inquirer";
|
||||
|
||||
import { ImportService } from "jslib-common/abstractions/import.service";
|
||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||
import { ImportType } from "jslib-common/enums/importOptions";
|
||||
|
||||
import { ImportType } from "jslib-common/services/import.service";
|
||||
import { Importer } from "jslib-common/importers/importer";
|
||||
|
||||
import { Response } from "jslib-node/cli/models/response";
|
||||
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
|
||||
@ -63,12 +66,11 @@ export class ImportCommand {
|
||||
return Response.badRequest("Import file was empty.");
|
||||
}
|
||||
|
||||
const err = await this.importService.import(importer, contents, organizationId);
|
||||
if (err != null) {
|
||||
return Response.badRequest(err.message);
|
||||
const response = await this.doImport(importer, contents, organizationId);
|
||||
if (response.success) {
|
||||
response.data = new MessageResponse("Imported " + filepath, null);
|
||||
}
|
||||
const res = new MessageResponse("Imported " + filepath, null);
|
||||
return Response.success(res);
|
||||
return response;
|
||||
} catch (err) {
|
||||
return Response.badRequest(err);
|
||||
}
|
||||
@ -86,4 +88,36 @@ export class ImportCommand {
|
||||
res.raw = options;
|
||||
return Response.success(res);
|
||||
}
|
||||
|
||||
private async doImport(
|
||||
importer: Importer,
|
||||
contents: string,
|
||||
organizationId?: string
|
||||
): Promise<Response> {
|
||||
const err = await this.importService.import(importer, contents, organizationId);
|
||||
if (err != null) {
|
||||
if (err.passwordRequired) {
|
||||
importer = this.importService.getImporter(
|
||||
"bitwardenpasswordprotected",
|
||||
organizationId,
|
||||
await this.promptPassword()
|
||||
);
|
||||
return this.doImport(importer, contents, organizationId);
|
||||
}
|
||||
return Response.badRequest(err.message);
|
||||
}
|
||||
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
private async promptPassword() {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "password",
|
||||
name: "password",
|
||||
message: "Import file password:",
|
||||
});
|
||||
return answer.password;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
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";
|
||||
@ -14,9 +12,9 @@ import { MessageResponse } from "jslib-node/cli/models/response/messageResponse"
|
||||
import { SecretVerificationRequest } from "jslib-common/models/request/secretVerificationRequest";
|
||||
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
import { CliUtils } from "../utils";
|
||||
|
||||
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";
|
||||
@ -37,34 +35,12 @@ export class UnlockCommand {
|
||||
async run(password: string, cmdOptions: Record<string, any>) {
|
||||
const canInteract = process.env.BW_NOINTERACTION !== "true";
|
||||
const normalizedOptions = new Options(cmdOptions);
|
||||
if (password == null || password === "") {
|
||||
if (normalizedOptions?.passwordFile) {
|
||||
password = await NodeUtils.readFirstLine(normalizedOptions.passwordFile);
|
||||
} else if (normalizedOptions?.passwordEnv) {
|
||||
if (process.env[normalizedOptions.passwordEnv]) {
|
||||
password = process.env[normalizedOptions.passwordEnv];
|
||||
} else {
|
||||
this.logService.warning(
|
||||
`Warning: Provided passwordenv ${normalizedOptions.passwordEnv} is not set`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const passwordResult = await CliUtils.getPassword(password, normalizedOptions, this.logService);
|
||||
|
||||
if (password == null || password === "") {
|
||||
if (canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "password",
|
||||
name: "password",
|
||||
message: "Master password:",
|
||||
});
|
||||
|
||||
password = answer.password;
|
||||
} else {
|
||||
return Response.badRequest("Master password is required.");
|
||||
}
|
||||
if (passwordResult instanceof Response) {
|
||||
return passwordResult;
|
||||
} else {
|
||||
password = passwordResult;
|
||||
}
|
||||
|
||||
await this.setNewSessionKey();
|
||||
|
@ -14,6 +14,12 @@
|
||||
"noneFolder": {
|
||||
"message": "No Folder"
|
||||
},
|
||||
"importEncKeyError": {
|
||||
"message": "Invalid file password."
|
||||
},
|
||||
"importPasswordRequired": {
|
||||
"message": "File is password protected, please provide a decryption password."
|
||||
},
|
||||
"importFormatError": {
|
||||
"message": "Data is not formatted correctly. Please check your import file and try again."
|
||||
},
|
||||
|
50
src/utils.ts
50
src/utils.ts
@ -1,4 +1,6 @@
|
||||
import * as program from "commander";
|
||||
import * as fs from "fs";
|
||||
import * as inquirer from "inquirer";
|
||||
import * as path from "path";
|
||||
|
||||
import { Response } from "jslib-node/cli/models/response";
|
||||
@ -11,6 +13,9 @@ import { FolderView } from "jslib-common/models/view/folderView";
|
||||
import { NodeUtils } from "jslib-common/misc/nodeUtils";
|
||||
import { FlagName, Flags } from "./flags";
|
||||
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
|
||||
export class CliUtils {
|
||||
static writeLn(s: string, finalLine: boolean = false, error: boolean = false) {
|
||||
const stream = error ? process.stderr : process.stdout;
|
||||
@ -172,6 +177,51 @@ export class CliUtils {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a password from all available sources. In order of priority these are:
|
||||
* * passwordfile
|
||||
* * passwordenv
|
||||
* * user interaction
|
||||
*
|
||||
* Returns password string if successful, Response if not.
|
||||
*/
|
||||
static async getPassword(
|
||||
password: string,
|
||||
options: { passwordFile?: string; passwordEnv?: string },
|
||||
logService?: LogService
|
||||
): Promise<string | Response> {
|
||||
if (Utils.isNullOrEmpty(password)) {
|
||||
if (options?.passwordFile) {
|
||||
password = await NodeUtils.readFirstLine(options.passwordFile);
|
||||
} else if (options?.passwordEnv) {
|
||||
if (process.env[options.passwordEnv]) {
|
||||
password = process.env[options.passwordEnv];
|
||||
} else if (logService) {
|
||||
logService.warning(`Warning: Provided passwordenv ${options.passwordEnv} is not set`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Utils.isNullOrEmpty(password)) {
|
||||
if (process.env.BW_NOINTERACTION !== "true") {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "password",
|
||||
name: "password",
|
||||
message: "Master password:",
|
||||
});
|
||||
|
||||
password = answer.password;
|
||||
} else {
|
||||
return Response.badRequest(
|
||||
"Master password is required. Try again in interactive mode or provide a password file or environment variable."
|
||||
);
|
||||
}
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
static convertBooleanOption(optionValue: any) {
|
||||
return optionValue || optionValue === "" ? true : false;
|
||||
}
|
||||
|
@ -447,17 +447,20 @@ export class VaultProgram extends Program {
|
||||
|
||||
private exportCommand(): program.Command {
|
||||
return new program.Command("export")
|
||||
.arguments("[password]")
|
||||
.description("Export vault data to a CSV or JSON file.", {
|
||||
password: "Optional: Your master password.",
|
||||
})
|
||||
.description("Export vault data to a CSV or JSON file.", {})
|
||||
.option("--output <output>", "Output directory or filename.")
|
||||
.option("--format <format>", "Export file format.")
|
||||
.option(
|
||||
"--password [password]",
|
||||
"Use password to encrypt instead of your Bitwarden account encryption key. Only applies to the encrypted_json format."
|
||||
)
|
||||
.option("--organizationid <organizationid>", "Organization id for an organization.")
|
||||
.on("--help", () => {
|
||||
writeLn("\n Notes:");
|
||||
writeLn("");
|
||||
writeLn(" Valid formats are `csv`, `json`, `encrypted_json`. Default format is `csv`.");
|
||||
writeLn(
|
||||
" Valid formats are `csv`, `json`, and `encrypted_json`. Default format is `csv`."
|
||||
);
|
||||
writeLn("");
|
||||
writeLn(
|
||||
" If --raw option is specified and no output filename or directory is given, the"
|
||||
@ -477,15 +480,10 @@ export class VaultProgram extends Program {
|
||||
);
|
||||
writeLn("", true);
|
||||
})
|
||||
.action(async (password, options) => {
|
||||
.action(async (options) => {
|
||||
await this.exitIfLocked();
|
||||
const command = new ExportCommand(
|
||||
this.main.exportService,
|
||||
this.main.policyService,
|
||||
this.main.keyConnectorService,
|
||||
this.main.userVerificationService
|
||||
);
|
||||
const response = await command.run(password, options);
|
||||
const command = new ExportCommand(this.main.exportService, this.main.policyService);
|
||||
const response = await command.run(options);
|
||||
this.processResponse(response);
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user