1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

Move CLI to apps/cli

This commit is contained in:
Hinton
2022-05-25 10:56:29 +02:00
parent f6c454c970
commit 980429f4bd
103 changed files with 2 additions and 2 deletions

380
apps/cli/src/bw.ts Normal file
View File

@@ -0,0 +1,380 @@
import * as fs from "fs";
import * as path from "path";
import * as program from "commander";
import * as jsdom from "jsdom";
import { ClientType } from "jslib-common/enums/clientType";
import { KeySuffixOptions } from "jslib-common/enums/keySuffixOptions";
import { LogLevelType } from "jslib-common/enums/logLevelType";
import { StateFactory } from "jslib-common/factories/stateFactory";
import { Account } from "jslib-common/models/domain/account";
import { GlobalState } from "jslib-common/models/domain/globalState";
import { AppIdService } from "jslib-common/services/appId.service";
import { AuditService } from "jslib-common/services/audit.service";
import { AuthService } from "jslib-common/services/auth.service";
import { CipherService } from "jslib-common/services/cipher.service";
import { CollectionService } from "jslib-common/services/collection.service";
import { ContainerService } from "jslib-common/services/container.service";
import { CryptoService } from "jslib-common/services/crypto.service";
import { EnvironmentService } from "jslib-common/services/environment.service";
import { ExportService } from "jslib-common/services/export.service";
import { FileUploadService } from "jslib-common/services/fileUpload.service";
import { FolderService } from "jslib-common/services/folder.service";
import { ImportService } from "jslib-common/services/import.service";
import { KeyConnectorService } from "jslib-common/services/keyConnector.service";
import { NoopMessagingService } from "jslib-common/services/noopMessaging.service";
import { OrganizationService } from "jslib-common/services/organization.service";
import { PasswordGenerationService } from "jslib-common/services/passwordGeneration.service";
import { PolicyService } from "jslib-common/services/policy.service";
import { ProviderService } from "jslib-common/services/provider.service";
import { SearchService } from "jslib-common/services/search.service";
import { SendService } from "jslib-common/services/send.service";
import { SettingsService } from "jslib-common/services/settings.service";
import { StateService } from "jslib-common/services/state.service";
import { StateMigrationService } from "jslib-common/services/stateMigration.service";
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 { TwoFactorService } from "jslib-common/services/twoFactor.service";
import { UserVerificationService } from "jslib-common/services/userVerification.service";
import { VaultTimeoutService } from "jslib-common/services/vaultTimeout.service";
import { CliPlatformUtilsService } from "jslib-node/cli/services/cliPlatformUtils.service";
import { ConsoleLogService } from "jslib-node/cli/services/consoleLog.service";
import { NodeApiService } from "jslib-node/services/nodeApi.service";
import { NodeCryptoFunctionService } from "jslib-node/services/nodeCryptoFunction.service";
import { Program } from "./program";
import { SendProgram } from "./send.program";
import { I18nService } from "./services/i18n.service";
import { LowdbStorageService } from "./services/lowdbStorage.service";
import { NodeEnvSecureStorageService } from "./services/nodeEnvSecureStorage.service";
import { VaultProgram } from "./vault.program";
// Polyfills
(global as any).DOMParser = new jsdom.JSDOM().window.DOMParser;
// eslint-disable-next-line
const packageJson = require("../package.json");
export class Main {
messagingService: NoopMessagingService;
storageService: LowdbStorageService;
secureStorageService: NodeEnvSecureStorageService;
i18nService: I18nService;
platformUtilsService: CliPlatformUtilsService;
cryptoService: CryptoService;
tokenService: TokenService;
appIdService: AppIdService;
apiService: NodeApiService;
environmentService: EnvironmentService;
settingsService: SettingsService;
cipherService: CipherService;
folderService: FolderService;
collectionService: CollectionService;
vaultTimeoutService: VaultTimeoutService;
syncService: SyncService;
passwordGenerationService: PasswordGenerationService;
totpService: TotpService;
containerService: ContainerService;
auditService: AuditService;
importService: ImportService;
exportService: ExportService;
searchService: SearchService;
cryptoFunctionService: NodeCryptoFunctionService;
authService: AuthService;
policyService: PolicyService;
program: Program;
vaultProgram: VaultProgram;
sendProgram: SendProgram;
logService: ConsoleLogService;
sendService: SendService;
fileUploadService: FileUploadService;
keyConnectorService: KeyConnectorService;
userVerificationService: UserVerificationService;
stateService: StateService;
stateMigrationService: StateMigrationService;
organizationService: OrganizationService;
providerService: ProviderService;
twoFactorService: TwoFactorService;
constructor() {
let p = null;
const relativeDataDir = path.join(path.dirname(process.execPath), "bw-data");
if (fs.existsSync(relativeDataDir)) {
p = relativeDataDir;
} else if (process.env.BITWARDENCLI_APPDATA_DIR) {
p = path.resolve(process.env.BITWARDENCLI_APPDATA_DIR);
} else if (process.platform === "darwin") {
p = path.join(process.env.HOME, "Library/Application Support/Bitwarden CLI");
} else if (process.platform === "win32") {
p = path.join(process.env.APPDATA, "Bitwarden CLI");
} else if (process.env.XDG_CONFIG_HOME) {
p = path.join(process.env.XDG_CONFIG_HOME, "Bitwarden CLI");
} else {
p = path.join(process.env.HOME, ".config/Bitwarden CLI");
}
this.i18nService = new I18nService("en", "./locales");
this.platformUtilsService = new CliPlatformUtilsService(ClientType.Cli, packageJson);
this.logService = new ConsoleLogService(
this.platformUtilsService.isDev(),
(level) => process.env.BITWARDENCLI_DEBUG !== "true" && level <= LogLevelType.Info
);
this.cryptoFunctionService = new NodeCryptoFunctionService();
this.storageService = new LowdbStorageService(this.logService, null, p, false, true);
this.secureStorageService = new NodeEnvSecureStorageService(
this.storageService,
this.logService,
() => this.cryptoService
);
this.stateMigrationService = new StateMigrationService(
this.storageService,
this.secureStorageService,
new StateFactory(GlobalState, Account)
);
this.stateService = new StateService(
this.storageService,
this.secureStorageService,
this.logService,
this.stateMigrationService,
new StateFactory(GlobalState, Account)
);
this.cryptoService = new CryptoService(
this.cryptoFunctionService,
this.platformUtilsService,
this.logService,
this.stateService
);
this.appIdService = new AppIdService(this.storageService);
this.tokenService = new TokenService(this.stateService);
this.messagingService = new NoopMessagingService();
this.environmentService = new EnvironmentService(this.stateService);
const customUserAgent =
"Bitwarden_CLI/" +
this.platformUtilsService.getApplicationVersionSync() +
" (" +
this.platformUtilsService.getDeviceString().toUpperCase() +
")";
this.apiService = new NodeApiService(
this.tokenService,
this.platformUtilsService,
this.environmentService,
this.appIdService,
async (expired: boolean) => await this.logout(),
customUserAgent
);
this.containerService = new ContainerService(this.cryptoService);
this.settingsService = new SettingsService(this.stateService);
this.fileUploadService = new FileUploadService(this.logService, this.apiService);
this.cipherService = new CipherService(
this.cryptoService,
this.settingsService,
this.apiService,
this.fileUploadService,
this.i18nService,
null,
this.logService,
this.stateService
);
this.folderService = new FolderService(
this.cryptoService,
this.apiService,
this.i18nService,
this.cipherService,
this.stateService
);
this.collectionService = new CollectionService(
this.cryptoService,
this.i18nService,
this.stateService
);
this.searchService = new SearchService(this.cipherService, this.logService, this.i18nService);
this.providerService = new ProviderService(this.stateService);
this.organizationService = new OrganizationService(this.stateService);
this.policyService = new PolicyService(
this.stateService,
this.organizationService,
this.apiService
);
this.sendService = new SendService(
this.cryptoService,
this.apiService,
this.fileUploadService,
this.i18nService,
this.cryptoFunctionService,
this.stateService
);
this.keyConnectorService = new KeyConnectorService(
this.stateService,
this.cryptoService,
this.apiService,
this.tokenService,
this.logService,
this.organizationService,
this.cryptoFunctionService
);
this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService);
this.authService = new AuthService(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.keyConnectorService,
this.environmentService,
this.stateService,
this.twoFactorService,
this.i18nService
);
const lockedCallback = async () =>
await this.cryptoService.clearStoredKey(KeySuffixOptions.Auto);
this.vaultTimeoutService = new VaultTimeoutService(
this.cipherService,
this.folderService,
this.collectionService,
this.cryptoService,
this.platformUtilsService,
this.messagingService,
this.searchService,
this.tokenService,
this.policyService,
this.keyConnectorService,
this.stateService,
this.authService,
lockedCallback,
null
);
this.syncService = new SyncService(
this.apiService,
this.settingsService,
this.folderService,
this.cipherService,
this.cryptoService,
this.collectionService,
this.messagingService,
this.policyService,
this.sendService,
this.logService,
this.keyConnectorService,
this.stateService,
this.organizationService,
this.providerService,
async (expired: boolean) => await this.logout()
);
this.passwordGenerationService = new PasswordGenerationService(
this.cryptoService,
this.policyService,
this.stateService
);
this.totpService = new TotpService(
this.cryptoFunctionService,
this.logService,
this.stateService
);
this.importService = new ImportService(
this.cipherService,
this.folderService,
this.apiService,
this.i18nService,
this.collectionService,
this.platformUtilsService,
this.cryptoService
);
this.exportService = new ExportService(
this.folderService,
this.cipherService,
this.apiService,
this.cryptoService,
this.cryptoFunctionService
);
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
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() {
await this.init();
await this.program.register();
await this.vaultProgram.register();
await this.sendProgram.register();
program.parse(process.argv);
if (process.argv.slice(2).length === 0) {
program.outputHelp();
}
}
async logout() {
this.authService.logOut(() => {
/* Do nothing */
});
const userId = await this.stateService.getUserId();
await Promise.all([
this.syncService.setLastSync(new Date(0)),
this.cryptoService.clearKeys(),
this.settingsService.clear(userId),
this.cipherService.clear(userId),
this.folderService.clear(userId),
this.collectionService.clear(userId),
this.policyService.clear(userId),
this.passwordGenerationService.clear(),
]);
await this.stateService.clean();
process.env.BW_SESSION = null;
}
private async init() {
await this.storageService.init();
await this.stateService.init();
this.containerService.attachToWindow(global);
await this.environmentService.setUrlsFromStorage();
const locale = await this.stateService.getLocale();
await this.i18nService.init(locale);
this.twoFactorService.init();
const installedVersion = await this.stateService.getInstalledVersion();
const currentVersion = await this.platformUtilsService.getApplicationVersion();
if (installedVersion == null || installedVersion !== currentVersion) {
await this.stateService.setInstalledVersion(currentVersion);
}
}
}
const main = new Main();
main.run();

View File

@@ -0,0 +1,119 @@
import * as program from "commander";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
interface IOption {
long?: string;
short?: string;
description: string;
}
interface ICommand {
commands?: ICommand[];
options?: IOption[];
_name: string;
_description: string;
}
const validShells = ["zsh"];
export class CompletionCommand {
async run(options: program.OptionValues) {
const shell: typeof validShells[number] = options.shell;
if (!shell) {
return Response.badRequest("`shell` option was not provided.");
}
if (!validShells.includes(shell)) {
return Response.badRequest("Unsupported shell.");
}
let content = "";
if (shell === "zsh") {
content = this.zshCompletion("bw", program as any as ICommand).render();
}
const res = new MessageResponse(content, null);
return Response.success(res);
}
private zshCompletion(rootName: string, rootCommand: ICommand) {
return {
render: () => {
return [
`#compdef _${rootName} ${rootName}`,
"",
this.renderCommandBlock(rootName, rootCommand),
].join("\n");
},
};
}
private renderCommandBlock(name: string, command: ICommand): string {
const { commands = [], options = [] } = command;
const hasOptions = options.length > 0;
const hasCommands = commands.length > 0;
const args = options
.map(({ long, short, description }) => {
const aliases = [short, long].filter(Boolean);
const opts = aliases.join(",");
const desc = `[${description.replace(`'`, `'"'"'`)}]`;
return aliases.length > 1
? `'(${aliases.join(" ")})'{${opts}}'${desc}'`
: `'${opts}${desc}'`;
})
.concat(
`'(-h --help)'{-h,--help}'[output usage information]'`,
hasCommands ? '"1: :->cmnds"' : null,
'"*::arg:->args"'
)
.filter(Boolean);
const commandBlockFunctionParts = [];
if (hasCommands) {
commandBlockFunctionParts.push("local -a commands");
}
if (hasOptions) {
commandBlockFunctionParts.push(`_arguments -C \\\n ${args.join(` \\\n `)}`);
}
if (hasCommands) {
commandBlockFunctionParts.push(
`case $state in
cmnds)
commands=(
${commands
.map(({ _name, _description }) => `"${_name}:${_description}"`)
.join("\n ")}
)
_describe "command" commands
;;
esac
case "$words[1]" in
${commands
.map(({ _name }) => [`${_name})`, `_${name}_${_name}`, ";;"].join("\n "))
.join("\n ")}
esac`
);
}
const commandBlocParts = [
`function _${name} {\n ${commandBlockFunctionParts.join("\n\n ")}\n}`,
];
if (hasCommands) {
commandBlocParts.push(
commands.map((c) => this.renderCommandBlock(`${name}_${c._name}`, c)).join("\n\n")
);
}
return commandBlocParts.join("\n\n");
}
}

View File

@@ -0,0 +1,53 @@
import * as program from "commander";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
export class ConfigCommand {
constructor(private environmentService: EnvironmentService) {}
async run(setting: string, value: string, options: program.OptionValues): Promise<Response> {
setting = setting.toLowerCase();
switch (setting) {
case "server":
return await this.getOrSetServer(value, options);
default:
return Response.badRequest("Unknown setting.");
}
}
private async getOrSetServer(url: string, options: program.OptionValues): Promise<Response> {
if (
(url == null || url.trim() === "") &&
!options.webVault &&
!options.api &&
!options.identity &&
!options.icons &&
!options.notifications &&
!options.events
) {
const stringRes = new StringResponse(
this.environmentService.hasBaseUrl()
? this.environmentService.getUrls().base
: "https://bitwarden.com"
);
return Response.success(stringRes);
}
url = url === "null" || url === "bitwarden.com" || url === "https://bitwarden.com" ? null : url;
await this.environmentService.setUrls({
base: url,
webVault: options.webVault || null,
api: options.api || null,
identity: options.identity || null,
icons: options.icons || null,
notifications: options.notifications || null,
events: options.events || null,
keyConnector: options.keyConnector || null,
});
const res = new MessageResponse("Saved setting `config`.", null);
return Response.success(res);
}
}

View File

@@ -0,0 +1,62 @@
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { Utils } from "jslib-common/misc/utils";
import { OrganizationUserConfirmRequest } from "jslib-common/models/request/organizationUserConfirmRequest";
import { Response } from "jslib-node/cli/models/response";
export class ConfirmCommand {
constructor(private apiService: ApiService, private cryptoService: CryptoService) {}
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
if (id != null) {
id = id.toLowerCase();
}
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) {
case "org-member":
return await this.confirmOrganizationMember(id, normalizedOptions);
default:
return Response.badRequest("Unknown object.");
}
}
private async confirmOrganizationMember(id: string, options: Options) {
if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("--organizationid <organizationid> required.");
}
if (!Utils.isGuid(id)) {
return Response.badRequest("`" + id + "` is not a GUID.");
}
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
}
try {
const orgKey = await this.cryptoService.getOrgKey(options.organizationId);
if (orgKey == null) {
throw new Error("No encryption key for this organization.");
}
const orgUser = await this.apiService.getOrganizationUser(options.organizationId, id);
if (orgUser == null) {
throw new Error("Member id does not exist for this organization.");
}
const publicKeyResponse = await this.apiService.getUserPublicKey(orgUser.userId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
const req = new OrganizationUserConfirmRequest();
req.key = key.encryptedString;
await this.apiService.postOrganizationUserConfirm(options.organizationId, id, req);
return Response.success();
} catch (e) {
return Response.error(e);
}
}
}
class Options {
organizationId: string;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId;
}
}

View File

@@ -0,0 +1,84 @@
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<void>
) {}
async run(): Promise<Response> {
// 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);
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.");
}
}
}

View File

@@ -0,0 +1,206 @@
import * as fs from "fs";
import * as path from "path";
import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { Utils } from "jslib-common/misc/utils";
import { CipherExport } from "jslib-common/models/export/cipherExport";
import { CollectionExport } from "jslib-common/models/export/collectionExport";
import { FolderExport } from "jslib-common/models/export/folderExport";
import { CollectionRequest } from "jslib-common/models/request/collectionRequest";
import { SelectionReadOnlyRequest } from "jslib-common/models/request/selectionReadOnlyRequest";
import { Response } from "jslib-node/cli/models/response";
import { OrganizationCollectionRequest } from "../models/request/organizationCollectionRequest";
import { CipherResponse } from "../models/response/cipherResponse";
import { FolderResponse } from "../models/response/folderResponse";
import { OrganizationCollectionResponse } from "../models/response/organizationCollectionResponse";
import { CliUtils } from "../utils";
export class CreateCommand {
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private stateService: StateService,
private cryptoService: CryptoService,
private apiService: ApiService
) {}
async run(
object: string,
requestJson: string,
cmdOptions: Record<string, any>,
additionalData: any = null
): Promise<Response> {
let req: any = null;
if (object !== "attachment") {
if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
requestJson = await CliUtils.readStdin();
}
if (requestJson == null || requestJson === "") {
return Response.badRequest("`requestJson` was not provided.");
}
if (typeof requestJson !== "string") {
req = requestJson;
} else {
try {
const reqJson = Buffer.from(requestJson, "base64").toString();
req = JSON.parse(reqJson);
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
}
}
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) {
case "item":
return await this.createCipher(req);
case "attachment":
return await this.createAttachment(normalizedOptions, additionalData);
case "folder":
return await this.createFolder(req);
case "org-collection":
return await this.createOrganizationCollection(req, normalizedOptions);
default:
return Response.badRequest("Unknown object.");
}
}
private async createCipher(req: CipherExport) {
const cipher = await this.cipherService.encrypt(CipherExport.toView(req));
try {
await this.cipherService.saveWithServer(cipher);
const newCipher = await this.cipherService.get(cipher.id);
const decCipher = await newCipher.decrypt();
const res = new CipherResponse(decCipher);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async createAttachment(options: Options, additionalData: any) {
if (options.itemId == null || options.itemId === "") {
return Response.badRequest("`itemid` option is required.");
}
let fileBuf: Buffer = null;
let fileName: string = null;
if (process.env.BW_SERVE === "true") {
fileBuf = additionalData.fileBuffer;
fileName = additionalData.fileName;
} else {
if (options.file == null || options.file === "") {
return Response.badRequest("`file` option is required.");
}
const filePath = path.resolve(options.file);
if (!fs.existsSync(options.file)) {
return Response.badRequest("Cannot find file at " + filePath);
}
fileBuf = fs.readFileSync(filePath);
fileName = path.basename(filePath);
}
if (fileBuf == null) {
return Response.badRequest("File not provided.");
}
if (fileName == null || fileName.trim() === "") {
return Response.badRequest("File name not provided.");
}
const itemId = options.itemId.toLowerCase();
const cipher = await this.cipherService.get(itemId);
if (cipher == null) {
return Response.notFound();
}
if (cipher.organizationId == null && !(await this.stateService.getCanAccessPremium())) {
return Response.error("Premium status is required to use this feature.");
}
const encKey = await this.cryptoService.getEncKey();
if (encKey == null) {
return Response.error(
"You must update your encryption key before you can use this feature. " +
"See https://help.bitwarden.com/article/update-encryption-key/"
);
}
try {
await this.cipherService.saveAttachmentRawWithServer(
cipher,
fileName,
new Uint8Array(fileBuf).buffer
);
const updatedCipher = await this.cipherService.get(cipher.id);
const decCipher = await updatedCipher.decrypt();
return Response.success(new CipherResponse(decCipher));
} catch (e) {
return Response.error(e);
}
}
private async createFolder(req: FolderExport) {
const folder = await this.folderService.encrypt(FolderExport.toView(req));
try {
await this.folderService.saveWithServer(folder);
const newFolder = await this.folderService.get(folder.id);
const decFolder = await newFolder.decrypt();
const res = new FolderResponse(decFolder);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async createOrganizationCollection(req: OrganizationCollectionRequest, options: Options) {
if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("`organizationid` option is required.");
}
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
}
if (options.organizationId !== req.organizationId) {
return Response.badRequest("`organizationid` option does not match request object.");
}
try {
const orgKey = await this.cryptoService.getOrgKey(req.organizationId);
if (orgKey == null) {
throw new Error("No encryption key for this organization.");
}
const groups =
req.groups == null
? null
: req.groups.map((g) => new SelectionReadOnlyRequest(g.id, g.readOnly, g.hidePasswords));
const request = new CollectionRequest();
request.name = (await this.cryptoService.encrypt(req.name, orgKey)).encryptedString;
request.externalId = req.externalId;
request.groups = groups;
const response = await this.apiService.postCollection(req.organizationId, request);
const view = CollectionExport.toView(req);
view.id = response.id;
const res = new OrganizationCollectionResponse(view, groups);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
}
class Options {
itemId: string;
organizationId: string;
file: string;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId;
this.itemId = passedOptions?.itemid || passedOptions?.itemId;
this.file = passedOptions?.file;
}
}

View File

@@ -0,0 +1,131 @@
import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { Utils } from "jslib-common/misc/utils";
import { Response } from "jslib-node/cli/models/response";
import { CliUtils } from "src/utils";
export class DeleteCommand {
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private stateService: StateService,
private apiService: ApiService
) {}
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
if (id != null) {
id = id.toLowerCase();
}
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) {
case "item":
return await this.deleteCipher(id, normalizedOptions);
case "attachment":
return await this.deleteAttachment(id, normalizedOptions);
case "folder":
return await this.deleteFolder(id);
case "org-collection":
return await this.deleteOrganizationCollection(id, normalizedOptions);
default:
return Response.badRequest("Unknown object.");
}
}
private async deleteCipher(id: string, options: Options) {
const cipher = await this.cipherService.get(id);
if (cipher == null) {
return Response.notFound();
}
try {
if (options.permanent) {
await this.cipherService.deleteWithServer(id);
} else {
await this.cipherService.softDeleteWithServer(id);
}
return Response.success();
} catch (e) {
return Response.error(e);
}
}
private async deleteAttachment(id: string, options: Options) {
if (options.itemId == null || options.itemId === "") {
return Response.badRequest("`itemid` option is required.");
}
const itemId = options.itemId.toLowerCase();
const cipher = await this.cipherService.get(itemId);
if (cipher == null) {
return Response.notFound();
}
if (cipher.attachments == null || cipher.attachments.length === 0) {
return Response.error("No attachments available for this item.");
}
const attachments = cipher.attachments.filter((a) => a.id.toLowerCase() === id);
if (attachments.length === 0) {
return Response.error("Attachment `" + id + "` was not found.");
}
if (cipher.organizationId == null && !(await this.stateService.getCanAccessPremium())) {
return Response.error("Premium status is required to use this feature.");
}
try {
await this.cipherService.deleteAttachmentWithServer(cipher.id, attachments[0].id);
return Response.success();
} catch (e) {
return Response.error(e);
}
}
private async deleteFolder(id: string) {
const folder = await this.folderService.get(id);
if (folder == null) {
return Response.notFound();
}
try {
await this.folderService.deleteWithServer(id);
return Response.success();
} catch (e) {
return Response.error(e);
}
}
private async deleteOrganizationCollection(id: string, options: Options) {
if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("`organizationid` options is required.");
}
if (!Utils.isGuid(id)) {
return Response.badRequest("`" + id + "` is not a GUID.");
}
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
}
try {
await this.apiService.deleteCollection(options.organizationId, id);
return Response.success();
} catch (e) {
return Response.error(e);
}
}
}
class Options {
itemId: string;
organizationId: string;
permanent: boolean;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId;
this.itemId = passedOptions?.itemid || passedOptions?.itemId;
this.permanent = CliUtils.convertBooleanOption(passedOptions?.permanent);
}
}

View File

@@ -0,0 +1,43 @@
import * as fet from "node-fetch";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { Response } from "jslib-node/cli/models/response";
import { FileResponse } from "jslib-node/cli/models/response/fileResponse";
import { CliUtils } from "../utils";
export abstract class DownloadCommand {
constructor(protected cryptoService: CryptoService) {}
protected async saveAttachmentToFile(
url: string,
key: SymmetricCryptoKey,
fileName: string,
output?: string
) {
const response = await fet.default(new fet.Request(url, { headers: { cache: "no-cache" } }));
if (response.status !== 200) {
return Response.error(
"A " + response.status + " error occurred while downloading the attachment."
);
}
try {
const buf = await response.arrayBuffer();
const decBuf = await this.cryptoService.decryptFromBytes(buf, key);
if (process.env.BW_SERVE === "true") {
const res = new FileResponse(Buffer.from(decBuf), fileName);
return Response.success(res);
} else {
return await CliUtils.saveResultToFile(Buffer.from(decBuf), output, fileName);
}
} catch (e) {
if (typeof e === "string") {
return Response.error(e);
} else {
return Response.error("An error occurred while saving the attachment.");
}
}
}
}

View File

@@ -0,0 +1,186 @@
import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { Utils } from "jslib-common/misc/utils";
import { CipherExport } from "jslib-common/models/export/cipherExport";
import { CollectionExport } from "jslib-common/models/export/collectionExport";
import { FolderExport } from "jslib-common/models/export/folderExport";
import { CollectionRequest } from "jslib-common/models/request/collectionRequest";
import { SelectionReadOnlyRequest } from "jslib-common/models/request/selectionReadOnlyRequest";
import { Response } from "jslib-node/cli/models/response";
import { OrganizationCollectionRequest } from "../models/request/organizationCollectionRequest";
import { CipherResponse } from "../models/response/cipherResponse";
import { FolderResponse } from "../models/response/folderResponse";
import { OrganizationCollectionResponse } from "../models/response/organizationCollectionResponse";
import { CliUtils } from "../utils";
export class EditCommand {
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private cryptoService: CryptoService,
private apiService: ApiService
) {}
async run(
object: string,
id: string,
requestJson: any,
cmdOptions: Record<string, any>
): Promise<Response> {
if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
requestJson = await CliUtils.readStdin();
}
if (requestJson == null || requestJson === "") {
return Response.badRequest("`requestJson` was not provided.");
}
let req: any = null;
if (typeof requestJson !== "string") {
req = requestJson;
} else {
try {
const reqJson = Buffer.from(requestJson, "base64").toString();
req = JSON.parse(reqJson);
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
}
if (id != null) {
id = id.toLowerCase();
}
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) {
case "item":
return await this.editCipher(id, req);
case "item-collections":
return await this.editCipherCollections(id, req);
case "folder":
return await this.editFolder(id, req);
case "org-collection":
return await this.editOrganizationCollection(id, req, normalizedOptions);
default:
return Response.badRequest("Unknown object.");
}
}
private async editCipher(id: string, req: CipherExport) {
const cipher = await this.cipherService.get(id);
if (cipher == null) {
return Response.notFound();
}
let cipherView = await cipher.decrypt();
if (cipherView.isDeleted) {
return Response.badRequest("You may not edit a deleted item. Use the restore command first.");
}
cipherView = CipherExport.toView(req, cipherView);
const encCipher = await this.cipherService.encrypt(cipherView);
try {
await this.cipherService.saveWithServer(encCipher);
const updatedCipher = await this.cipherService.get(cipher.id);
const decCipher = await updatedCipher.decrypt();
const res = new CipherResponse(decCipher);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async editCipherCollections(id: string, req: string[]) {
const cipher = await this.cipherService.get(id);
if (cipher == null) {
return Response.notFound();
}
if (cipher.organizationId == null) {
return Response.badRequest(
"Item does not belong to an organization. Consider moving it first."
);
}
cipher.collectionIds = req;
try {
await this.cipherService.saveCollectionsWithServer(cipher);
const updatedCipher = await this.cipherService.get(cipher.id);
const decCipher = await updatedCipher.decrypt();
const res = new CipherResponse(decCipher);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async editFolder(id: string, req: FolderExport) {
const folder = await this.folderService.get(id);
if (folder == null) {
return Response.notFound();
}
let folderView = await folder.decrypt();
folderView = FolderExport.toView(req, folderView);
const encFolder = await this.folderService.encrypt(folderView);
try {
await this.folderService.saveWithServer(encFolder);
const updatedFolder = await this.folderService.get(folder.id);
const decFolder = await updatedFolder.decrypt();
const res = new FolderResponse(decFolder);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async editOrganizationCollection(
id: string,
req: OrganizationCollectionRequest,
options: Options
) {
if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("`organizationid` option is required.");
}
if (!Utils.isGuid(id)) {
return Response.badRequest("`" + id + "` is not a GUID.");
}
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
}
if (options.organizationId !== req.organizationId) {
return Response.badRequest("`organizationid` option does not match request object.");
}
try {
const orgKey = await this.cryptoService.getOrgKey(req.organizationId);
if (orgKey == null) {
throw new Error("No encryption key for this organization.");
}
const groups =
req.groups == null
? null
: req.groups.map((g) => new SelectionReadOnlyRequest(g.id, g.readOnly, g.hidePasswords));
const request = new CollectionRequest();
request.name = (await this.cryptoService.encrypt(req.name, orgKey)).encryptedString;
request.externalId = req.externalId;
request.groups = groups;
const response = await this.apiService.putCollection(req.organizationId, id, request);
const view = CollectionExport.toView(req);
view.id = response.id;
const res = new OrganizationCollectionResponse(view, groups);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
}
class Options {
organizationId: string;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId;
}
}

View File

@@ -0,0 +1,16 @@
import { Response } from "jslib-node/cli/models/response";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
import { CliUtils } from "../utils";
export class EncodeCommand {
async run(): Promise<Response> {
if (process.stdin.isTTY) {
return Response.badRequest("No stdin was piped in.");
}
const input = await CliUtils.readStdin();
const b64 = Buffer.from(input, "utf8").toString("base64");
const res = new StringResponse(b64);
return Response.success(res);
}
}

View File

@@ -0,0 +1,97 @@
import * as program from "commander";
import * as inquirer from "inquirer";
import { ExportFormat, ExportService } from "jslib-common/abstractions/export.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { PolicyType } from "jslib-common/enums/policyType";
import { Utils } from "jslib-common/misc/utils";
import { Response } from "jslib-node/cli/models/response";
import { CliUtils } from "../utils";
export class ExportCommand {
constructor(private exportService: ExportService, private policyService: PolicyService) {}
async run(options: program.OptionValues): Promise<Response> {
if (
options.organizationid == null &&
(await this.policyService.policyAppliesToUser(PolicyType.DisablePersonalVaultExport))
) {
return Response.badRequest(
"One or more organization policies prevents you from exporting your personal vault."
);
}
const format = options.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 =
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);
}
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: ExportFormat
): Promise<Response> {
try {
const fileName = this.getFileName(format, options.organizationid != null ? "org" : null);
return await CliUtils.saveResultToFile(exportContent, options.output, fileName);
} catch (e) {
return Response.error(e.toString());
}
}
private getFileName(format: ExportFormat, prefix?: string) {
if (format === "encrypted_json") {
if (prefix == null) {
prefix = "encrypted";
} else {
prefix = "encrypted_" + prefix;
}
format = "json";
}
return this.exportService.getFileName(prefix, format);
}
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: "Export file password:",
});
return answer.password as string;
}
return null;
}
}

View File

@@ -0,0 +1,80 @@
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { Response } from "jslib-node/cli/models/response";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
import { CliUtils } from "../utils";
export class GenerateCommand {
constructor(
private passwordGenerationService: PasswordGenerationService,
private stateService: StateService
) {}
async run(cmdOptions: Record<string, any>): Promise<Response> {
const normalizedOptions = new Options(cmdOptions);
const options = {
uppercase: normalizedOptions.uppercase,
lowercase: normalizedOptions.lowercase,
number: normalizedOptions.number,
special: normalizedOptions.special,
length: normalizedOptions.length,
type: normalizedOptions.type,
wordSeparator: normalizedOptions.separator,
numWords: normalizedOptions.words,
capitalize: normalizedOptions.capitalize,
includeNumber: normalizedOptions.includeNumber,
};
const enforcedOptions = (await this.stateService.getIsAuthenticated())
? (await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(options))[0]
: options;
const password = await this.passwordGenerationService.generatePassword(enforcedOptions);
const res = new StringResponse(password);
return Response.success(res);
}
}
class Options {
uppercase: boolean;
lowercase: boolean;
number: boolean;
special: boolean;
length: number;
type: "passphrase" | "password";
separator: string;
words: number;
capitalize: boolean;
includeNumber: boolean;
constructor(passedOptions: Record<string, any>) {
this.uppercase = CliUtils.convertBooleanOption(passedOptions?.uppercase);
this.lowercase = CliUtils.convertBooleanOption(passedOptions?.lowercase);
this.number = CliUtils.convertBooleanOption(passedOptions?.number);
this.special = CliUtils.convertBooleanOption(passedOptions?.special);
this.capitalize = CliUtils.convertBooleanOption(passedOptions?.capitalize);
this.includeNumber = CliUtils.convertBooleanOption(passedOptions?.includeNumber);
this.length = passedOptions?.length != null ? parseInt(passedOptions?.length, null) : 14;
this.type = passedOptions?.passphrase ? "passphrase" : "password";
this.separator = passedOptions?.separator == null ? "-" : passedOptions.separator + "";
this.words = passedOptions?.words != null ? parseInt(passedOptions.words, null) : 3;
if (!this.uppercase && !this.lowercase && !this.special && !this.number) {
this.lowercase = true;
this.uppercase = true;
this.number = true;
}
if (this.length < 5) {
this.length = 5;
}
if (this.words < 3) {
this.words = 3;
}
if (this.separator === "space") {
this.separator = " ";
} else if (this.separator != null && this.separator.length > 1) {
this.separator = this.separator[0];
}
}
}

View File

@@ -0,0 +1,540 @@
import { ApiService } from "jslib-common/abstractions/api.service";
import { AuditService } from "jslib-common/abstractions/audit.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { SearchService } from "jslib-common/abstractions/search.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { TotpService } from "jslib-common/abstractions/totp.service";
import { CipherType } from "jslib-common/enums/cipherType";
import { SendType } from "jslib-common/enums/sendType";
import { Utils } from "jslib-common/misc/utils";
import { EncString } from "jslib-common/models/domain/encString";
import { Organization } from "jslib-common/models/domain/organization";
import { CardExport } from "jslib-common/models/export/cardExport";
import { CipherExport } from "jslib-common/models/export/cipherExport";
import { CollectionExport } from "jslib-common/models/export/collectionExport";
import { FieldExport } from "jslib-common/models/export/fieldExport";
import { FolderExport } from "jslib-common/models/export/folderExport";
import { IdentityExport } from "jslib-common/models/export/identityExport";
import { LoginExport } from "jslib-common/models/export/loginExport";
import { LoginUriExport } from "jslib-common/models/export/loginUriExport";
import { SecureNoteExport } from "jslib-common/models/export/secureNoteExport";
import { ErrorResponse } from "jslib-common/models/response/errorResponse";
import { CipherView } from "jslib-common/models/view/cipherView";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { FolderView } from "jslib-common/models/view/folderView";
import { Response } from "jslib-node/cli/models/response";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
import { OrganizationCollectionRequest } from "../models/request/organizationCollectionRequest";
import { CipherResponse } from "../models/response/cipherResponse";
import { CollectionResponse } from "../models/response/collectionResponse";
import { FolderResponse } from "../models/response/folderResponse";
import { OrganizationCollectionResponse } from "../models/response/organizationCollectionResponse";
import { OrganizationResponse } from "../models/response/organizationResponse";
import { SendResponse } from "../models/response/sendResponse";
import { TemplateResponse } from "../models/response/templateResponse";
import { SelectionReadOnly } from "../models/selectionReadOnly";
import { CliUtils } from "../utils";
import { DownloadCommand } from "./download.command";
export class GetCommand extends DownloadCommand {
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private collectionService: CollectionService,
private totpService: TotpService,
private auditService: AuditService,
cryptoService: CryptoService,
private stateService: StateService,
private searchService: SearchService,
private apiService: ApiService,
private organizationService: OrganizationService
) {
super(cryptoService);
}
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
if (id != null) {
id = id.toLowerCase();
}
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) {
case "item":
return await this.getCipher(id);
case "username":
return await this.getUsername(id);
case "password":
return await this.getPassword(id);
case "uri":
return await this.getUri(id);
case "totp":
return await this.getTotp(id);
case "notes":
return await this.getNotes(id);
case "exposed":
return await this.getExposed(id);
case "attachment":
return await this.getAttachment(id, normalizedOptions);
case "folder":
return await this.getFolder(id);
case "collection":
return await this.getCollection(id);
case "org-collection":
return await this.getOrganizationCollection(id, normalizedOptions);
case "organization":
return await this.getOrganization(id);
case "template":
return await this.getTemplate(id);
case "fingerprint":
return await this.getFingerprint(id);
default:
return Response.badRequest("Unknown object.");
}
}
private async getCipherView(id: string): Promise<CipherView | CipherView[]> {
let decCipher: CipherView = null;
if (Utils.isGuid(id)) {
const cipher = await this.cipherService.get(id);
if (cipher != null) {
decCipher = await cipher.decrypt();
}
} else if (id.trim() !== "") {
let ciphers = await this.cipherService.getAllDecrypted();
ciphers = this.searchService.searchCiphersBasic(ciphers, id);
if (ciphers.length > 1) {
return ciphers;
}
if (ciphers.length > 0) {
decCipher = ciphers[0];
}
}
return decCipher;
}
private async getCipher(id: string, filter?: (c: CipherView) => boolean) {
let decCipher = await this.getCipherView(id);
if (decCipher == null) {
return Response.notFound();
}
if (Array.isArray(decCipher)) {
if (filter != null) {
decCipher = decCipher.filter(filter);
if (decCipher.length === 1) {
decCipher = decCipher[0];
}
}
if (Array.isArray(decCipher)) {
return Response.multipleResults(decCipher.map((c) => c.id));
}
}
const res = new CipherResponse(decCipher);
return Response.success(res);
}
private async getUsername(id: string) {
const cipherResponse = await this.getCipher(
id,
(c) => c.type === CipherType.Login && !Utils.isNullOrWhitespace(c.login.username)
);
if (!cipherResponse.success) {
return cipherResponse;
}
const cipher = cipherResponse.data as CipherResponse;
if (cipher.type !== CipherType.Login) {
return Response.badRequest("Not a login.");
}
if (Utils.isNullOrWhitespace(cipher.login.username)) {
return Response.error("No username available for this login.");
}
const res = new StringResponse(cipher.login.username);
return Response.success(res);
}
private async getPassword(id: string) {
const cipherResponse = await this.getCipher(
id,
(c) => c.type === CipherType.Login && !Utils.isNullOrWhitespace(c.login.password)
);
if (!cipherResponse.success) {
return cipherResponse;
}
const cipher = cipherResponse.data as CipherResponse;
if (cipher.type !== CipherType.Login) {
return Response.badRequest("Not a login.");
}
if (Utils.isNullOrWhitespace(cipher.login.password)) {
return Response.error("No password available for this login.");
}
const res = new StringResponse(cipher.login.password);
return Response.success(res);
}
private async getUri(id: string) {
const cipherResponse = await this.getCipher(
id,
(c) =>
c.type === CipherType.Login &&
c.login.uris != null &&
c.login.uris.length > 0 &&
c.login.uris[0].uri !== ""
);
if (!cipherResponse.success) {
return cipherResponse;
}
const cipher = cipherResponse.data as CipherResponse;
if (cipher.type !== CipherType.Login) {
return Response.badRequest("Not a login.");
}
if (
cipher.login.uris == null ||
cipher.login.uris.length === 0 ||
cipher.login.uris[0].uri === ""
) {
return Response.error("No uri available for this login.");
}
const res = new StringResponse(cipher.login.uris[0].uri);
return Response.success(res);
}
private async getTotp(id: string) {
const cipherResponse = await this.getCipher(
id,
(c) => c.type === CipherType.Login && !Utils.isNullOrWhitespace(c.login.totp)
);
if (!cipherResponse.success) {
return cipherResponse;
}
const cipher = cipherResponse.data as CipherResponse;
if (cipher.type !== CipherType.Login) {
return Response.badRequest("Not a login.");
}
if (Utils.isNullOrWhitespace(cipher.login.totp)) {
return Response.error("No TOTP available for this login.");
}
const totp = await this.totpService.getCode(cipher.login.totp);
if (totp == null) {
return Response.error("Couldn't generate TOTP code.");
}
const canAccessPremium = await this.stateService.getCanAccessPremium();
if (!canAccessPremium) {
const originalCipher = await this.cipherService.get(cipher.id);
if (
originalCipher == null ||
originalCipher.organizationId == null ||
!originalCipher.organizationUseTotp
) {
return Response.error("Premium status is required to use this feature.");
}
}
const res = new StringResponse(totp);
return Response.success(res);
}
private async getNotes(id: string) {
const cipherResponse = await this.getCipher(id, (c) => !Utils.isNullOrWhitespace(c.notes));
if (!cipherResponse.success) {
return cipherResponse;
}
const cipher = cipherResponse.data as CipherResponse;
if (Utils.isNullOrWhitespace(cipher.notes)) {
return Response.error("No notes available for this item.");
}
const res = new StringResponse(cipher.notes);
return Response.success(res);
}
private async getExposed(id: string) {
const passwordResponse = await this.getPassword(id);
if (!passwordResponse.success) {
return passwordResponse;
}
const exposedNumber = await this.auditService.passwordLeaked(
(passwordResponse.data as StringResponse).data
);
const res = new StringResponse(exposedNumber.toString());
return Response.success(res);
}
private async getAttachment(id: string, options: Options) {
if (options.itemId == null || options.itemId === "") {
return Response.badRequest("--itemid <itemid> required.");
}
const itemId = options.itemId.toLowerCase();
const cipherResponse = await this.getCipher(itemId);
if (!cipherResponse.success) {
return cipherResponse;
}
const cipher = await this.getCipherView(itemId);
if (
cipher == null ||
Array.isArray(cipher) ||
cipher.attachments == null ||
cipher.attachments.length === 0
) {
return Response.error("No attachments available for this item.");
}
let attachments = cipher.attachments.filter(
(a) =>
a.id.toLowerCase() === id ||
(a.fileName != null && a.fileName.toLowerCase().indexOf(id) > -1)
);
if (attachments.length === 0) {
return Response.error("Attachment `" + id + "` was not found.");
}
const exactMatches = attachments.filter((a) => a.fileName.toLowerCase() === id);
if (exactMatches.length === 1) {
attachments = exactMatches;
}
if (attachments.length > 1) {
return Response.multipleResults(attachments.map((a) => a.id));
}
if (!(await this.stateService.getCanAccessPremium())) {
const originalCipher = await this.cipherService.get(cipher.id);
if (originalCipher == null || originalCipher.organizationId == null) {
return Response.error("Premium status is required to use this feature.");
}
}
let url: string;
try {
const attachmentDownloadResponse = await this.apiService.getAttachmentData(
cipher.id,
attachments[0].id
);
url = attachmentDownloadResponse.url;
} catch (e) {
if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) {
url = attachments[0].url;
} else if (e instanceof ErrorResponse) {
throw new Error((e as ErrorResponse).getSingleMessage());
} else {
throw e;
}
}
const key =
attachments[0].key != null
? attachments[0].key
: await this.cryptoService.getOrgKey(cipher.organizationId);
return await this.saveAttachmentToFile(url, key, attachments[0].fileName, options.output);
}
private async getFolder(id: string) {
let decFolder: FolderView = null;
if (Utils.isGuid(id)) {
const folder = await this.folderService.get(id);
if (folder != null) {
decFolder = await folder.decrypt();
}
} else if (id.trim() !== "") {
let folders = await this.folderService.getAllDecrypted();
folders = CliUtils.searchFolders(folders, id);
if (folders.length > 1) {
return Response.multipleResults(folders.map((f) => f.id));
}
if (folders.length > 0) {
decFolder = folders[0];
}
}
if (decFolder == null) {
return Response.notFound();
}
const res = new FolderResponse(decFolder);
return Response.success(res);
}
private async getCollection(id: string) {
let decCollection: CollectionView = null;
if (Utils.isGuid(id)) {
const collection = await this.collectionService.get(id);
if (collection != null) {
decCollection = await collection.decrypt();
}
} else if (id.trim() !== "") {
let collections = await this.collectionService.getAllDecrypted();
collections = CliUtils.searchCollections(collections, id);
if (collections.length > 1) {
return Response.multipleResults(collections.map((c) => c.id));
}
if (collections.length > 0) {
decCollection = collections[0];
}
}
if (decCollection == null) {
return Response.notFound();
}
const res = new CollectionResponse(decCollection);
return Response.success(res);
}
private async getOrganizationCollection(id: string, options: Options) {
if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("`organizationid` option is required.");
}
if (!Utils.isGuid(id)) {
return Response.badRequest("`" + id + "` is not a GUID.");
}
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
}
try {
const orgKey = await this.cryptoService.getOrgKey(options.organizationId);
if (orgKey == null) {
throw new Error("No encryption key for this organization.");
}
const response = await this.apiService.getCollectionDetails(options.organizationId, id);
const decCollection = new CollectionView(response);
decCollection.name = await this.cryptoService.decryptToUtf8(
new EncString(response.name),
orgKey
);
const groups =
response.groups == null
? null
: response.groups.map((g) => new SelectionReadOnly(g.id, g.readOnly, g.hidePasswords));
const res = new OrganizationCollectionResponse(decCollection, groups);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async getOrganization(id: string) {
let org: Organization = null;
if (Utils.isGuid(id)) {
org = await this.organizationService.get(id);
} else if (id.trim() !== "") {
let orgs = await this.organizationService.getAll();
orgs = CliUtils.searchOrganizations(orgs, id);
if (orgs.length > 1) {
return Response.multipleResults(orgs.map((c) => c.id));
}
if (orgs.length > 0) {
org = orgs[0];
}
}
if (org == null) {
return Response.notFound();
}
const res = new OrganizationResponse(org);
return Response.success(res);
}
private async getTemplate(id: string) {
let template: any = null;
switch (id.toLowerCase()) {
case "item":
template = CipherExport.template();
break;
case "item.field":
template = FieldExport.template();
break;
case "item.login":
template = LoginExport.template();
break;
case "item.login.uri":
template = LoginUriExport.template();
break;
case "item.card":
template = CardExport.template();
break;
case "item.identity":
template = IdentityExport.template();
break;
case "item.securenote":
template = SecureNoteExport.template();
break;
case "folder":
template = FolderExport.template();
break;
case "collection":
template = CollectionExport.template();
break;
case "item-collections":
template = ["collection-id1", "collection-id2"];
break;
case "org-collection":
template = OrganizationCollectionRequest.template();
break;
case "send.text":
template = SendResponse.template(SendType.Text);
break;
case "send.file":
template = SendResponse.template(SendType.File);
break;
default:
return Response.badRequest("Unknown template object.");
}
const res = new TemplateResponse(template);
return Response.success(res);
}
private async getFingerprint(id: string) {
let fingerprint: string[] = null;
if (id === "me") {
fingerprint = await this.cryptoService.getFingerprint(await this.stateService.getUserId());
} else if (Utils.isGuid(id)) {
try {
const response = await this.apiService.getUserPublicKey(id);
const pubKey = Utils.fromB64ToArray(response.publicKey);
fingerprint = await this.cryptoService.getFingerprint(id, pubKey.buffer);
} catch {
// eslint-disable-next-line
}
}
if (fingerprint == null) {
return Response.notFound();
}
const res = new StringResponse(fingerprint.join("-"));
return Response.success(res);
}
}
class Options {
itemId: string;
organizationId: string;
output: string;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId;
this.itemId = passedOptions?.itemid || passedOptions?.itemId;
this.output = passedOptions?.output;
}
}

View File

@@ -0,0 +1,127 @@
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 { Importer } from "jslib-common/importers/importer";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { CliUtils } from "../utils";
export class ImportCommand {
constructor(
private importService: ImportService,
private organizationService: OrganizationService
) {}
async run(
format: ImportType,
filepath: string,
options: program.OptionValues
): Promise<Response> {
const organizationId = options.organizationid;
if (organizationId != null) {
const organization = await this.organizationService.get(organizationId);
if (organization == null) {
return Response.badRequest(
`You do not belong to an organization with the ID of ${organizationId}. Check the organization ID and sync your vault.`
);
}
if (!organization.canAccessImportExport) {
return Response.badRequest(
"You are not authorized to import into the provided organization."
);
}
}
if (options.formats || false) {
return await this.list();
} else {
return await this.import(format, filepath, organizationId);
}
}
private async import(format: ImportType, filepath: string, organizationId: string) {
if (format == null) {
return Response.badRequest("`format` was not provided.");
}
if (filepath == null || filepath === "") {
return Response.badRequest("`filepath` was not provided.");
}
const importer = await this.importService.getImporter(format, organizationId);
if (importer === null) {
return Response.badRequest("Proper importer type required.");
}
try {
let contents;
if (format === "1password1pux") {
contents = await CliUtils.extract1PuxContent(filepath);
} else {
contents = await CliUtils.readFile(filepath);
}
if (contents === null || contents === "") {
return Response.badRequest("Import file was empty.");
}
const response = await this.doImport(importer, contents, organizationId);
if (response.success) {
response.data = new MessageResponse("Imported " + filepath, null);
}
return response;
} catch (err) {
return Response.badRequest(err);
}
}
private async list() {
const options = this.importService
.getImportOptions()
.sort((a, b) => {
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
})
.map((option) => option.id)
.join("\n");
const res = new MessageResponse("Supported input formats:", options);
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;
}
}

View File

@@ -0,0 +1,252 @@
import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { SearchService } from "jslib-common/abstractions/search.service";
import { Utils } from "jslib-common/misc/utils";
import { CollectionData } from "jslib-common/models/data/collectionData";
import { Collection } from "jslib-common/models/domain/collection";
import {
CollectionDetailsResponse as ApiCollectionDetailsResponse,
CollectionResponse as ApiCollectionResponse,
} from "jslib-common/models/response/collectionResponse";
import { ListResponse as ApiListResponse } from "jslib-common/models/response/listResponse";
import { CipherView } from "jslib-common/models/view/cipherView";
import { Response } from "jslib-node/cli/models/response";
import { ListResponse } from "jslib-node/cli/models/response/listResponse";
import { CipherResponse } from "../models/response/cipherResponse";
import { CollectionResponse } from "../models/response/collectionResponse";
import { FolderResponse } from "../models/response/folderResponse";
import { OrganizationResponse } from "../models/response/organizationResponse";
import { OrganizationUserResponse } from "../models/response/organizationUserResponse";
import { CliUtils } from "../utils";
export class ListCommand {
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private collectionService: CollectionService,
private organizationService: OrganizationService,
private searchService: SearchService,
private apiService: ApiService
) {}
async run(object: string, cmdOptions: Record<string, any>): Promise<Response> {
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) {
case "items":
return await this.listCiphers(normalizedOptions);
case "folders":
return await this.listFolders(normalizedOptions);
case "collections":
return await this.listCollections(normalizedOptions);
case "org-collections":
return await this.listOrganizationCollections(normalizedOptions);
case "org-members":
return await this.listOrganizationMembers(normalizedOptions);
case "organizations":
return await this.listOrganizations(normalizedOptions);
default:
return Response.badRequest("Unknown object.");
}
}
private async listCiphers(options: Options) {
let ciphers: CipherView[];
options.trash = options.trash || false;
if (options.url != null && options.url.trim() !== "") {
ciphers = await this.cipherService.getAllDecryptedForUrl(options.url);
} else {
ciphers = await this.cipherService.getAllDecrypted();
}
if (
options.folderId != null ||
options.collectionId != null ||
options.organizationId != null
) {
ciphers = ciphers.filter((c) => {
if (options.trash !== c.isDeleted) {
return false;
}
if (options.folderId != null) {
if (options.folderId === "notnull" && c.folderId != null) {
return true;
}
const folderId = options.folderId === "null" ? null : options.folderId;
if (folderId === c.folderId) {
return true;
}
}
if (options.organizationId != null) {
if (options.organizationId === "notnull" && c.organizationId != null) {
return true;
}
const organizationId = options.organizationId === "null" ? null : options.organizationId;
if (organizationId === c.organizationId) {
return true;
}
}
if (options.collectionId != null) {
if (
options.collectionId === "notnull" &&
c.collectionIds != null &&
c.collectionIds.length > 0
) {
return true;
}
const collectionId = options.collectionId === "null" ? null : options.collectionId;
if (collectionId == null && (c.collectionIds == null || c.collectionIds.length === 0)) {
return true;
}
if (
collectionId != null &&
c.collectionIds != null &&
c.collectionIds.indexOf(collectionId) > -1
) {
return true;
}
}
return false;
});
} else if (options.search == null || options.search.trim() === "") {
ciphers = ciphers.filter((c) => options.trash === c.isDeleted);
}
if (options.search != null && options.search.trim() !== "") {
ciphers = this.searchService.searchCiphersBasic(ciphers, options.search, options.trash);
}
const res = new ListResponse(ciphers.map((o) => new CipherResponse(o)));
return Response.success(res);
}
private async listFolders(options: Options) {
let folders = await this.folderService.getAllDecrypted();
if (options.search != null && options.search.trim() !== "") {
folders = CliUtils.searchFolders(folders, options.search);
}
const res = new ListResponse(folders.map((o) => new FolderResponse(o)));
return Response.success(res);
}
private async listCollections(options: Options) {
let collections = await this.collectionService.getAllDecrypted();
if (options.organizationId != null) {
collections = collections.filter((c) => {
if (options.organizationId === c.organizationId) {
return true;
}
return false;
});
}
if (options.search != null && options.search.trim() !== "") {
collections = CliUtils.searchCollections(collections, options.search);
}
const res = new ListResponse(collections.map((o) => new CollectionResponse(o)));
return Response.success(res);
}
private async listOrganizationCollections(options: Options) {
if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("`organizationid` option is required.");
}
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
}
const organization = await this.organizationService.get(options.organizationId);
if (organization == null) {
return Response.error("Organization not found.");
}
try {
let response: ApiListResponse<ApiCollectionResponse>;
if (organization.canViewAllCollections) {
response = await this.apiService.getCollections(options.organizationId);
} else {
response = await this.apiService.getUserCollections();
}
const collections = response.data
.filter((c) => c.organizationId === options.organizationId)
.map((r) => new Collection(new CollectionData(r as ApiCollectionDetailsResponse)));
let decCollections = await this.collectionService.decryptMany(collections);
if (options.search != null && options.search.trim() !== "") {
decCollections = CliUtils.searchCollections(decCollections, options.search);
}
const res = new ListResponse(decCollections.map((o) => new CollectionResponse(o)));
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async listOrganizationMembers(options: Options) {
if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("`organizationid` option is required.");
}
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
}
const organization = await this.organizationService.get(options.organizationId);
if (organization == null) {
return Response.error("Organization not found.");
}
try {
const response = await this.apiService.getOrganizationUsers(options.organizationId);
const res = new ListResponse(
response.data.map((r) => {
const u = new OrganizationUserResponse();
u.email = r.email;
u.name = r.name;
u.id = r.id;
u.status = r.status;
u.type = r.type;
u.twoFactorEnabled = r.twoFactorEnabled;
return u;
})
);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async listOrganizations(options: Options) {
let organizations = await this.organizationService.getAll();
if (options.search != null && options.search.trim() !== "") {
organizations = CliUtils.searchOrganizations(organizations, options.search);
}
const res = new ListResponse(organizations.map((o) => new OrganizationResponse(o)));
return Response.success(res);
}
}
class Options {
organizationId: string;
collectionId: string;
folderId: string;
search: string;
url: string;
trash: boolean;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId;
this.collectionId = passedOptions?.collectionid || passedOptions?.collectionId;
this.folderId = passedOptions?.folderid || passedOptions?.folderId;
this.search = passedOptions?.search;
this.url = passedOptions?.url;
this.trash = CliUtils.convertBooleanOption(passedOptions?.trash);
}
}

View File

@@ -0,0 +1,14 @@
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
export class LockCommand {
constructor(private vaultTimeoutService: VaultTimeoutService) {}
async run() {
await this.vaultTimeoutService.lock();
process.env.BW_SESSION = null;
const res = new MessageResponse("Your vault is locked.", null);
return Response.success(res);
}
}

View File

@@ -0,0 +1,99 @@
import * as program from "commander";
import { ApiService } from "jslib-common/abstractions/api.service";
import { AuthService } from "jslib-common/abstractions/auth.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 { I18nService } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
import { Utils } from "jslib-common/misc/utils";
import { LoginCommand as BaseLoginCommand } from "jslib-node/cli/commands/login.command";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
export class LoginCommand extends BaseLoginCommand {
private options: program.OptionValues;
constructor(
authService: AuthService,
apiService: ApiService,
cryptoFunctionService: CryptoFunctionService,
i18nService: I18nService,
environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService,
stateService: StateService,
cryptoService: CryptoService,
policyService: PolicyService,
twoFactorService: TwoFactorService,
private syncService: SyncService,
private keyConnectorService: KeyConnectorService,
private logoutCallback: () => Promise<void>
) {
super(
authService,
apiService,
i18nService,
environmentService,
passwordGenerationService,
cryptoFunctionService,
platformUtilsService,
stateService,
cryptoService,
policyService,
twoFactorService,
"cli"
);
this.logout = this.logoutCallback;
this.validatedParams = async () => {
const key = await cryptoFunctionService.randomBytes(64);
process.env.BW_SESSION = Utils.fromBufferToB64(key);
};
this.success = async () => {
await this.syncService.fullSync(true);
const usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector();
if (
(this.options.sso != null || this.options.apikey != null) &&
this.canInteract &&
!usesKeyConnector
) {
const res = new MessageResponse(
"You are logged in!",
"\n" + "To unlock your vault, use the `unlock` command. ex:\n" + "$ bw unlock"
);
return res;
} else {
const res = new MessageResponse(
"You are logged in!",
"\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 res;
}
};
}
run(email: string, password: string, options: program.OptionValues) {
this.options = options;
this.email = email;
return super.run(email, password, options);
}
}

View File

@@ -0,0 +1,36 @@
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { Response } from "jslib-node/cli/models/response";
export class RestoreCommand {
constructor(private cipherService: CipherService) {}
async run(object: string, id: string): Promise<Response> {
if (id != null) {
id = id.toLowerCase();
}
switch (object.toLowerCase()) {
case "item":
return await this.restoreCipher(id);
default:
return Response.badRequest("Unknown object.");
}
}
private async restoreCipher(id: string) {
const cipher = await this.cipherService.get(id);
if (cipher == null) {
return Response.notFound();
}
if (cipher.deletedDate == null) {
return Response.badRequest("Cipher is not in trash.");
}
try {
await this.cipherService.restoreWithServer(id);
return Response.success();
} catch (e) {
return Response.error(e);
}
}
}

View File

@@ -0,0 +1,149 @@
import * as fs from "fs";
import * as path from "path";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { SendService } from "jslib-common/abstractions/send.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SendType } from "jslib-common/enums/sendType";
import { NodeUtils } from "jslib-common/misc/nodeUtils";
import { Response } from "jslib-node/cli/models/response";
import { SendResponse } from "../../models/response/sendResponse";
import { SendTextResponse } from "../../models/response/sendTextResponse";
import { CliUtils } from "../../utils";
export class SendCreateCommand {
constructor(
private sendService: SendService,
private stateService: StateService,
private environmentService: EnvironmentService
) {}
async run(requestJson: any, cmdOptions: Record<string, any>) {
let req: any = null;
if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
requestJson = await CliUtils.readStdin();
}
if (requestJson == null || requestJson === "") {
return Response.badRequest("`requestJson` was not provided.");
}
if (typeof requestJson !== "string") {
req = requestJson;
req.deletionDate = req.deletionDate == null ? null : new Date(req.deletionDate);
req.expirationDate = req.expirationDate == null ? null : new Date(req.expirationDate);
} else {
try {
const reqJson = Buffer.from(requestJson, "base64").toString();
req = SendResponse.fromJson(reqJson);
if (req == null) {
throw new Error("Null request");
}
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
}
if (
req.deletionDate == null ||
isNaN(new Date(req.deletionDate).getTime()) ||
new Date(req.deletionDate) <= new Date()
) {
return Response.badRequest("Must specify a valid deletion date after the current time");
}
if (req.expirationDate != null && isNaN(new Date(req.expirationDate).getTime())) {
return Response.badRequest("Unable to parse expirationDate: " + req.expirationDate);
}
const normalizedOptions = new Options(cmdOptions);
return this.createSend(req, normalizedOptions);
}
private async createSend(req: SendResponse, options: Options) {
const filePath = req.file?.fileName ?? options.file;
const text = req.text?.text ?? options.text;
const hidden = req.text?.hidden ?? options.hidden;
const password = req.password ?? options.password;
const maxAccessCount = req.maxAccessCount ?? options.maxAccessCount;
req.key = null;
req.maxAccessCount = maxAccessCount;
switch (req.type) {
case SendType.File:
if (process.env.BW_SERVE === "true") {
return Response.error(
"Creating a file-based Send is unsupported through the `serve` command at this time."
);
}
if (!(await this.stateService.getCanAccessPremium())) {
return Response.error("Premium status is required to use this feature.");
}
if (filePath == null) {
return Response.badRequest(
"Must specify a file to Send either with the --file option or in the request JSON."
);
}
req.file.fileName = path.basename(filePath);
break;
case SendType.Text:
if (text == null) {
return Response.badRequest(
"Must specify text content to Send either with the --text option or in the request JSON."
);
}
req.text = new SendTextResponse();
req.text.text = text;
req.text.hidden = hidden;
break;
default:
return Response.badRequest(
"Unknown Send type " + SendType[req.type] + ". Valid types are: file, text"
);
}
try {
let fileBuffer: ArrayBuffer = null;
if (req.type === SendType.File) {
fileBuffer = NodeUtils.bufferToArrayBuffer(fs.readFileSync(filePath));
}
const sendView = SendResponse.toView(req);
const [encSend, fileData] = await this.sendService.encrypt(sendView, fileBuffer, password);
// Add dates from template
encSend.deletionDate = sendView.deletionDate;
encSend.expirationDate = sendView.expirationDate;
await this.sendService.saveWithServer([encSend, fileData]);
const newSend = await this.sendService.get(encSend.id);
const decSend = await newSend.decrypt();
const res = new SendResponse(decSend, this.environmentService.getWebVaultUrl());
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
}
class Options {
file: string;
text: string;
maxAccessCount: number;
password: string;
hidden: boolean;
constructor(passedOptions: Record<string, any>) {
this.file = passedOptions?.file;
this.text = passedOptions?.text;
this.password = passedOptions?.password;
this.hidden = CliUtils.convertBooleanOption(passedOptions?.hidden);
this.maxAccessCount =
passedOptions?.maxAccessCount != null ? parseInt(passedOptions.maxAccessCount, null) : null;
}
}

View File

@@ -0,0 +1,21 @@
import { SendService } from "jslib-common/abstractions/send.service";
import { Response } from "jslib-node/cli/models/response";
export class SendDeleteCommand {
constructor(private sendService: SendService) {}
async run(id: string) {
const send = await this.sendService.get(id);
if (send == null) {
return Response.notFound();
}
try {
await this.sendService.deleteWithServer(id);
return Response.success();
} catch (e) {
return Response.error(e);
}
}
}

View File

@@ -0,0 +1,90 @@
import { SendService } from "jslib-common/abstractions/send.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SendType } from "jslib-common/enums/sendType";
import { Response } from "jslib-node/cli/models/response";
import { SendResponse } from "../../models/response/sendResponse";
import { CliUtils } from "../../utils";
import { SendGetCommand } from "./get.command";
export class SendEditCommand {
constructor(
private sendService: SendService,
private stateService: StateService,
private getCommand: SendGetCommand
) {}
async run(requestJson: string, cmdOptions: Record<string, any>): Promise<Response> {
if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
requestJson = await CliUtils.readStdin();
}
if (requestJson == null || requestJson === "") {
return Response.badRequest("`requestJson` was not provided.");
}
let req: SendResponse = null;
if (typeof requestJson !== "string") {
req = requestJson;
req.deletionDate = req.deletionDate == null ? null : new Date(req.deletionDate);
req.expirationDate = req.expirationDate == null ? null : new Date(req.expirationDate);
} else {
try {
const reqJson = Buffer.from(requestJson, "base64").toString();
req = SendResponse.fromJson(reqJson);
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
}
const normalizedOptions = new Options(cmdOptions);
req.id = normalizedOptions.itemId || req.id;
if (req.id != null) {
req.id = req.id.toLowerCase();
}
const send = await this.sendService.get(req.id);
if (send == null) {
return Response.notFound();
}
if (send.type !== req.type) {
return Response.badRequest("Cannot change a Send's type");
}
if (send.type === SendType.File && !(await this.stateService.getCanAccessPremium())) {
return Response.error("Premium status is required to use this feature.");
}
let sendView = await send.decrypt();
sendView = SendResponse.toView(req, sendView);
if (typeof req.password !== "string" || req.password === "") {
req.password = null;
}
try {
const [encSend, encFileData] = await this.sendService.encrypt(sendView, null, req.password);
// Add dates from template
encSend.deletionDate = sendView.deletionDate;
encSend.expirationDate = sendView.expirationDate;
await this.sendService.saveWithServer([encSend, encFileData]);
} catch (e) {
return Response.error(e);
}
return await this.getCommand.run(send.id, {});
}
}
class Options {
itemId: string;
constructor(passedOptions: Record<string, any>) {
this.itemId = passedOptions?.itemId || passedOptions?.itemid;
}
}

View File

@@ -0,0 +1,83 @@
import * as program from "commander";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { SearchService } from "jslib-common/abstractions/search.service";
import { SendService } from "jslib-common/abstractions/send.service";
import { Utils } from "jslib-common/misc/utils";
import { SendView } from "jslib-common/models/view/sendView";
import { Response } from "jslib-node/cli/models/response";
import { SendResponse } from "../../models/response/sendResponse";
import { DownloadCommand } from "../download.command";
export class SendGetCommand extends DownloadCommand {
constructor(
private sendService: SendService,
private environmentService: EnvironmentService,
private searchService: SearchService,
cryptoService: CryptoService
) {
super(cryptoService);
}
async run(id: string, options: program.OptionValues) {
const serveCommand = process.env.BW_SERVE === "true";
if (serveCommand && !Utils.isGuid(id)) {
return Response.badRequest("`" + id + "` is not a GUID.");
}
let sends = await this.getSendView(id);
if (sends == null) {
return Response.notFound();
}
const webVaultUrl = this.environmentService.getWebVaultUrl();
let filter = (s: SendView) => true;
let selector = async (s: SendView): Promise<Response> =>
Response.success(new SendResponse(s, webVaultUrl));
if (!serveCommand && options?.text != null) {
filter = (s) => {
return filter(s) && s.text != null;
};
selector = async (s) => {
// Write to stdout and response success so we get the text string only to stdout
process.stdout.write(s.text.text);
return Response.success();
};
}
if (Array.isArray(sends)) {
if (filter != null) {
sends = sends.filter(filter);
}
if (sends.length > 1) {
return Response.multipleResults(sends.map((s) => s.id));
}
if (sends.length > 0) {
return selector(sends[0]);
} else {
return Response.notFound();
}
}
return selector(sends);
}
private async getSendView(id: string): Promise<SendView | SendView[]> {
if (Utils.isGuid(id)) {
const send = await this.sendService.get(id);
if (send != null) {
return await send.decrypt();
}
} else if (id.trim() !== "") {
let sends = await this.sendService.getAllDecrypted();
sends = this.searchService.searchSends(sends, id);
if (sends.length > 1) {
return sends;
} else if (sends.length > 0) {
return sends[0];
}
}
}
}

View File

@@ -0,0 +1,36 @@
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { SearchService } from "jslib-common/abstractions/search.service";
import { SendService } from "jslib-common/abstractions/send.service";
import { Response } from "jslib-node/cli/models/response";
import { ListResponse } from "jslib-node/cli/models/response/listResponse";
import { SendResponse } from "../..//models/response/sendResponse";
export class SendListCommand {
constructor(
private sendService: SendService,
private environmentService: EnvironmentService,
private searchService: SearchService
) {}
async run(cmdOptions: Record<string, any>): Promise<Response> {
let sends = await this.sendService.getAllDecrypted();
const normalizedOptions = new Options(cmdOptions);
if (normalizedOptions.search != null && normalizedOptions.search.trim() !== "") {
sends = this.searchService.searchSends(sends, normalizedOptions.search);
}
const webVaultUrl = this.environmentService.getWebVaultUrl();
const res = new ListResponse(sends.map((s) => new SendResponse(s, webVaultUrl)));
return Response.success(res);
}
}
class Options {
search: string;
constructor(passedOptions: Record<string, any>) {
this.search = passedOptions?.search;
}
}

View File

@@ -0,0 +1,170 @@
import * as program from "commander";
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 { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SendType } from "jslib-common/enums/sendType";
import { NodeUtils } from "jslib-common/misc/nodeUtils";
import { Utils } from "jslib-common/misc/utils";
import { SendAccess } from "jslib-common/models/domain/sendAccess";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { SendAccessRequest } from "jslib-common/models/request/sendAccessRequest";
import { ErrorResponse } from "jslib-common/models/response/errorResponse";
import { SendAccessView } from "jslib-common/models/view/sendAccessView";
import { Response } from "jslib-node/cli/models/response";
import { SendAccessResponse } from "../../models/response/sendAccessResponse";
import { DownloadCommand } from "../download.command";
export class SendReceiveCommand extends DownloadCommand {
private canInteract: boolean;
private decKey: SymmetricCryptoKey;
private sendAccessRequest: SendAccessRequest;
constructor(
private apiService: ApiService,
cryptoService: CryptoService,
private cryptoFunctionService: CryptoFunctionService,
private platformUtilsService: PlatformUtilsService,
private environmentService: EnvironmentService
) {
super(cryptoService);
}
async run(url: string, options: program.OptionValues): Promise<Response> {
this.canInteract = process.env.BW_NOINTERACTION !== "true";
let urlObject: URL;
try {
urlObject = new URL(url);
} catch (e) {
return Response.badRequest("Failed to parse the provided Send url");
}
const apiUrl = this.getApiUrl(urlObject);
const [id, key] = this.getIdAndKey(urlObject);
if (Utils.isNullOrWhitespace(id) || Utils.isNullOrWhitespace(key)) {
return Response.badRequest("Failed to parse url, the url provided is not a valid Send url");
}
const keyArray = Utils.fromUrlB64ToArray(key);
this.sendAccessRequest = new SendAccessRequest();
let password = options.password;
if (password == null || password === "") {
if (options.passwordfile) {
password = await NodeUtils.readFirstLine(options.passwordfile);
} else if (options.passwordenv && process.env[options.passwordenv]) {
password = process.env[options.passwordenv];
}
}
if (password != null && password !== "") {
this.sendAccessRequest.password = await this.getUnlockedPassword(password, keyArray);
}
const response = await this.sendRequest(apiUrl, id, keyArray);
if (response instanceof Response) {
// Error scenario
return response;
}
if (options.obj != null) {
return Response.success(new SendAccessResponse(response));
}
switch (response.type) {
case SendType.Text:
// Write to stdout and response success so we get the text string only to stdout
process.stdout.write(response?.text?.text);
return Response.success();
case SendType.File: {
const downloadData = await this.apiService.getSendFileDownloadData(
response,
this.sendAccessRequest,
apiUrl
);
return await this.saveAttachmentToFile(
downloadData.url,
this.decKey,
response?.file?.fileName,
options.output
);
}
default:
return Response.success(new SendAccessResponse(response));
}
}
private getIdAndKey(url: URL): [string, string] {
const result = url.hash.slice(1).split("/").slice(-2);
return [result[0], result[1]];
}
private getApiUrl(url: URL) {
const urls = this.environmentService.getUrls();
if (url.origin === "https://send.bitwarden.com") {
return "https://vault.bitwarden.com/api";
} else if (url.origin === urls.api) {
return url.origin;
} else if (this.platformUtilsService.isDev() && url.origin === urls.webVault) {
return urls.api;
} else {
return url.origin + "/api";
}
}
private async getUnlockedPassword(password: string, keyArray: ArrayBuffer) {
const passwordHash = await this.cryptoFunctionService.pbkdf2(
password,
keyArray,
"sha256",
100000
);
return Utils.fromBufferToB64(passwordHash);
}
private async sendRequest(
url: string,
id: string,
key: ArrayBuffer
): Promise<Response | SendAccessView> {
try {
const sendResponse = await this.apiService.postSendAccess(id, this.sendAccessRequest, url);
const sendAccess = new SendAccess(sendResponse);
this.decKey = await this.cryptoService.makeSendKey(key);
return await sendAccess.decrypt(this.decKey);
} catch (e) {
if (e instanceof ErrorResponse) {
if (e.statusCode === 401) {
if (this.canInteract) {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "password",
name: "password",
message: "Send password:",
});
// reattempt with new password
this.sendAccessRequest.password = await this.getUnlockedPassword(answer.password, key);
return await this.sendRequest(url, id, key);
}
return Response.badRequest("Incorrect or missing password");
} else if (e.statusCode === 405) {
return Response.badRequest("Bad Request");
} else if (e.statusCode === 404) {
return Response.notFound();
}
}
return Response.error(e);
}
}
}

View File

@@ -0,0 +1,21 @@
import { SendService } from "jslib-common/abstractions/send.service";
import { Response } from "jslib-node/cli/models/response";
import { SendResponse } from "../../models/response/sendResponse";
export class SendRemovePasswordCommand {
constructor(private sendService: SendService) {}
async run(id: string) {
try {
await this.sendService.removePasswordWithServer(id);
const updatedSend = await this.sendService.get(id);
const decSend = await updatedSend.decrypt();
const res = new SendResponse(decSend);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
}

View File

@@ -0,0 +1,394 @@
import * as koaMulter from "@koa/multer";
import * as koaRouter from "@koa/router";
import * as program from "commander";
import * as koa from "koa";
import * as koaBodyParser from "koa-bodyparser";
import * as koaJson from "koa-json";
import { KeySuffixOptions } from "jslib-common/enums/keySuffixOptions";
import { Response } from "jslib-node/cli/models/response";
import { FileResponse } from "jslib-node/cli/models/response/fileResponse";
import { Main } from "../bw";
import { ConfirmCommand } from "./confirm.command";
import { CreateCommand } from "./create.command";
import { DeleteCommand } from "./delete.command";
import { EditCommand } from "./edit.command";
import { GenerateCommand } from "./generate.command";
import { GetCommand } from "./get.command";
import { ListCommand } from "./list.command";
import { LockCommand } from "./lock.command";
import { RestoreCommand } from "./restore.command";
import { SendCreateCommand } from "./send/create.command";
import { SendDeleteCommand } from "./send/delete.command";
import { SendEditCommand } from "./send/edit.command";
import { SendGetCommand } from "./send/get.command";
import { SendListCommand } from "./send/list.command";
import { SendRemovePasswordCommand } from "./send/removePassword.command";
import { ShareCommand } from "./share.command";
import { StatusCommand } from "./status.command";
import { SyncCommand } from "./sync.command";
import { UnlockCommand } from "./unlock.command";
export class ServeCommand {
private listCommand: ListCommand;
private getCommand: GetCommand;
private createCommand: CreateCommand;
private editCommand: EditCommand;
private generateCommand: GenerateCommand;
private shareCommand: ShareCommand;
private statusCommand: StatusCommand;
private syncCommand: SyncCommand;
private deleteCommand: DeleteCommand;
private confirmCommand: ConfirmCommand;
private restoreCommand: RestoreCommand;
private lockCommand: LockCommand;
private unlockCommand: UnlockCommand;
private sendCreateCommand: SendCreateCommand;
private sendDeleteCommand: SendDeleteCommand;
private sendEditCommand: SendEditCommand;
private sendGetCommand: SendGetCommand;
private sendListCommand: SendListCommand;
private sendRemovePasswordCommand: SendRemovePasswordCommand;
constructor(protected main: Main) {
this.getCommand = new GetCommand(
this.main.cipherService,
this.main.folderService,
this.main.collectionService,
this.main.totpService,
this.main.auditService,
this.main.cryptoService,
this.main.stateService,
this.main.searchService,
this.main.apiService,
this.main.organizationService
);
this.listCommand = new ListCommand(
this.main.cipherService,
this.main.folderService,
this.main.collectionService,
this.main.organizationService,
this.main.searchService,
this.main.apiService
);
this.createCommand = new CreateCommand(
this.main.cipherService,
this.main.folderService,
this.main.stateService,
this.main.cryptoService,
this.main.apiService
);
this.editCommand = new EditCommand(
this.main.cipherService,
this.main.folderService,
this.main.cryptoService,
this.main.apiService
);
this.generateCommand = new GenerateCommand(
this.main.passwordGenerationService,
this.main.stateService
);
this.syncCommand = new SyncCommand(this.main.syncService);
this.statusCommand = new StatusCommand(
this.main.environmentService,
this.main.syncService,
this.main.stateService,
this.main.authService
);
this.deleteCommand = new DeleteCommand(
this.main.cipherService,
this.main.folderService,
this.main.stateService,
this.main.apiService
);
this.confirmCommand = new ConfirmCommand(this.main.apiService, this.main.cryptoService);
this.restoreCommand = new RestoreCommand(this.main.cipherService);
this.shareCommand = new ShareCommand(this.main.cipherService);
this.lockCommand = new LockCommand(this.main.vaultTimeoutService);
this.unlockCommand = new UnlockCommand(
this.main.cryptoService,
this.main.stateService,
this.main.cryptoFunctionService,
this.main.apiService,
this.main.logService,
this.main.keyConnectorService,
this.main.environmentService,
this.main.syncService,
async () => await this.main.logout()
);
this.sendCreateCommand = new SendCreateCommand(
this.main.sendService,
this.main.stateService,
this.main.environmentService
);
this.sendDeleteCommand = new SendDeleteCommand(this.main.sendService);
this.sendGetCommand = new SendGetCommand(
this.main.sendService,
this.main.environmentService,
this.main.searchService,
this.main.cryptoService
);
this.sendEditCommand = new SendEditCommand(
this.main.sendService,
this.main.stateService,
this.sendGetCommand
);
this.sendListCommand = new SendListCommand(
this.main.sendService,
this.main.environmentService,
this.main.searchService
);
this.sendRemovePasswordCommand = new SendRemovePasswordCommand(this.main.sendService);
}
async run(options: program.OptionValues) {
const port = options.port || 8087;
const hostname = options.hostname || "localhost";
const server = new koa();
const router = new koaRouter();
process.env.BW_SERVE = "true";
process.env.BW_NOINTERACTION = "true";
server.use(koaBodyParser()).use(koaJson({ pretty: false, param: "pretty" }));
router.get("/generate", async (ctx, next) => {
const response = await this.generateCommand.run(ctx.request.query);
this.processResponse(ctx.response, response);
await next();
});
router.get("/status", async (ctx, next) => {
const response = await this.statusCommand.run();
this.processResponse(ctx.response, response);
await next();
});
router.get("/list/object/:object", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
let response: Response = null;
if (ctx.params.object === "send") {
response = await this.sendListCommand.run(ctx.request.query);
} else {
response = await this.listCommand.run(ctx.params.object, ctx.request.query);
}
this.processResponse(ctx.response, response);
await next();
});
router.get("/send/list", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
const response = await this.sendListCommand.run(ctx.request.query);
this.processResponse(ctx.response, response);
await next();
});
router.post("/sync", async (ctx, next) => {
const response = await this.syncCommand.run(ctx.request.query);
this.processResponse(ctx.response, response);
await next();
});
router.post("/lock", async (ctx, next) => {
const response = await this.lockCommand.run();
this.processResponse(ctx.response, response);
await next();
});
router.post("/unlock", async (ctx, next) => {
const response = await this.unlockCommand.run(
ctx.request.body.password == null ? null : (ctx.request.body.password as string),
ctx.request.query
);
this.processResponse(ctx.response, response);
await next();
});
router.post("/confirm/:object/:id", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
const response = await this.confirmCommand.run(
ctx.params.object,
ctx.params.id,
ctx.request.query
);
this.processResponse(ctx.response, response);
await next();
});
router.post("/restore/:object/:id", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
const response = await this.restoreCommand.run(ctx.params.object, ctx.params.id);
this.processResponse(ctx.response, response);
await next();
});
router.post("/move/:id/:organizationId", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
const response = await this.shareCommand.run(
ctx.params.id,
ctx.params.organizationId,
ctx.request.body // TODO: Check the format of this body for an array of collection ids
);
this.processResponse(ctx.response, response);
await next();
});
router.post("/attachment", koaMulter().single("file"), async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
const response = await this.createCommand.run(
"attachment",
ctx.request.body,
ctx.request.query,
{
fileBuffer: ctx.request.file.buffer,
fileName: ctx.request.file.originalname,
}
);
this.processResponse(ctx.response, response);
await next();
});
router.post("/send/:id/remove-password", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
const response = await this.sendRemovePasswordCommand.run(ctx.params.id);
this.processResponse(ctx.response, response);
await next();
});
router.post("/object/:object", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
let response: Response = null;
if (ctx.params.object === "send") {
response = await this.sendCreateCommand.run(ctx.request.body, ctx.request.query);
} else {
response = await this.createCommand.run(
ctx.params.object,
ctx.request.body,
ctx.request.query
);
}
this.processResponse(ctx.response, response);
await next();
});
router.put("/object/:object/:id", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
let response: Response = null;
if (ctx.params.object === "send") {
ctx.request.body.id = ctx.params.id;
response = await this.sendEditCommand.run(ctx.request.body, ctx.request.query);
} else {
response = await this.editCommand.run(
ctx.params.object,
ctx.params.id,
ctx.request.body,
ctx.request.query
);
}
this.processResponse(ctx.response, response);
await next();
});
router.get("/object/:object/:id", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
let response: Response = null;
if (ctx.params.object === "send") {
response = await this.sendGetCommand.run(ctx.params.id, null);
} else {
response = await this.getCommand.run(ctx.params.object, ctx.params.id, ctx.request.query);
}
this.processResponse(ctx.response, response);
await next();
});
router.delete("/object/:object/:id", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
let response: Response = null;
if (ctx.params.object === "send") {
response = await this.sendDeleteCommand.run(ctx.params.id);
} else {
response = await this.deleteCommand.run(
ctx.params.object,
ctx.params.id,
ctx.request.query
);
}
this.processResponse(ctx.response, response);
await next();
});
server
.use(router.routes())
.use(router.allowedMethods())
.listen(port, hostname === "all" ? null : hostname, () => {
this.main.logService.info("Listening on " + hostname + ":" + port);
});
}
private processResponse(res: koa.Response, commandResponse: Response) {
if (!commandResponse.success) {
res.status = 400;
}
if (commandResponse.data instanceof FileResponse) {
res.body = commandResponse.data.data;
res.attachment(commandResponse.data.fileName);
res.set("Content-Type", "application/octet-stream");
res.set("Content-Length", commandResponse.data.data.length.toString());
} else {
res.body = commandResponse;
}
}
private async errorIfLocked(res: koa.Response) {
const authed = await this.main.stateService.getIsAuthenticated();
if (!authed) {
this.processResponse(res, Response.error("You are not logged in."));
return true;
}
if (await this.main.cryptoService.hasKeyInMemory()) {
return false;
} else if (await this.main.cryptoService.hasKeyStored(KeySuffixOptions.Auto)) {
// load key into memory
await this.main.cryptoService.getKey();
return false;
}
this.processResponse(res, Response.error("Vault is locked."));
return true;
}
}

View File

@@ -0,0 +1,59 @@
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { Response } from "jslib-node/cli/models/response";
import { CipherResponse } from "../models/response/cipherResponse";
import { CliUtils } from "../utils";
export class ShareCommand {
constructor(private cipherService: CipherService) {}
async run(id: string, organizationId: string, requestJson: string): Promise<Response> {
if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
requestJson = await CliUtils.readStdin();
}
if (requestJson == null || requestJson === "") {
return Response.badRequest("`requestJson` was not provided.");
}
let req: string[] = [];
if (typeof requestJson !== "string") {
req = requestJson;
} else {
try {
const reqJson = Buffer.from(requestJson, "base64").toString();
req = JSON.parse(reqJson);
if (req == null || req.length === 0) {
return Response.badRequest("You must provide at least one collection id for this item.");
}
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
}
if (id != null) {
id = id.toLowerCase();
}
if (organizationId != null) {
organizationId = organizationId.toLowerCase();
}
const cipher = await this.cipherService.get(id);
if (cipher == null) {
return Response.notFound();
}
if (cipher.organizationId != null) {
return Response.badRequest("This item already belongs to an organization.");
}
const cipherView = await cipher.decrypt();
try {
await this.cipherService.shareWithServer(cipherView, organizationId, req);
const updatedCipher = await this.cipherService.get(cipher.id);
const decCipher = await updatedCipher.decrypt();
const res = new CipherResponse(decCipher);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
}

View File

@@ -0,0 +1,54 @@
import { AuthService } from "jslib-common/abstractions/auth.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
import { Response } from "jslib-node/cli/models/response";
import { TemplateResponse } from "../models/response/templateResponse";
export class StatusCommand {
constructor(
private envService: EnvironmentService,
private syncService: SyncService,
private stateService: StateService,
private authService: AuthService
) {}
async run(): Promise<Response> {
try {
const baseUrl = this.baseUrl();
const status = await this.status();
const lastSync = await this.syncService.getLastSync();
const userId = await this.stateService.getUserId();
const email = await this.stateService.getEmail();
return Response.success(
new TemplateResponse({
serverUrl: baseUrl,
lastSync: lastSync,
userEmail: email,
userId: userId,
status: status,
})
);
} catch (e) {
return Response.error(e);
}
}
private baseUrl(): string {
return this.envService.getUrls().base;
}
private async status(): Promise<"unauthenticated" | "locked" | "unlocked"> {
const authStatus = await this.authService.getAuthStatus();
if (authStatus === AuthenticationStatus.Unlocked) {
return "unlocked";
} else if (authStatus === AuthenticationStatus.Locked) {
return "locked";
} else {
return "unauthenticated";
}
}
}

View File

@@ -0,0 +1,41 @@
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";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
import { CliUtils } from "src/utils";
export class SyncCommand {
constructor(private syncService: SyncService) {}
async run(cmdOptions: Record<string, any>): Promise<Response> {
const normalizedOptions = new Options(cmdOptions);
if (normalizedOptions.last) {
return await this.getLastSync();
}
try {
await this.syncService.fullSync(normalizedOptions.force, true);
const res = new MessageResponse("Syncing complete.", null);
return Response.success(res);
} catch (e) {
return Response.error("Syncing failed: " + e.toString());
}
}
private async getLastSync() {
const lastSyncDate = await this.syncService.getLastSync();
const res = new StringResponse(lastSyncDate == null ? null : lastSyncDate.toISOString());
return Response.success(res);
}
}
class Options {
last: boolean;
force: boolean;
constructor(passedOptions: Record<string, any>) {
this.last = CliUtils.convertBooleanOption(passedOptions?.last);
this.force = CliUtils.convertBooleanOption(passedOptions?.force);
}
}

View File

@@ -0,0 +1,132 @@
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 { HashPurpose } from "jslib-common/enums/hashPurpose";
import { Utils } from "jslib-common/misc/utils";
import { SecretVerificationRequest } from "jslib-common/models/request/secretVerificationRequest";
import { ConsoleLogService } from "jslib-common/services/consoleLog.service";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { CliUtils } from "../utils";
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 keyConnectorService: KeyConnectorService,
private environmentService: EnvironmentService,
private syncService: SyncService,
private logout: () => Promise<void>
) {}
async run(password: string, cmdOptions: Record<string, any>) {
const normalizedOptions = new Options(cmdOptions);
const passwordResult = await CliUtils.getPassword(password, normalizedOptions, this.logService);
if (passwordResult instanceof Response) {
return passwordResult;
} else {
password = passwordResult;
}
await this.setNewSessionKey();
const email = await this.stateService.getEmail();
const kdf = await this.stateService.getKdfType();
const kdfIterations = await this.stateService.getKdfIterations();
const key = await this.cryptoService.makeKey(password, email, kdf, kdfIterations);
const storedKeyHash = await this.cryptoService.getKeyHash();
let passwordValid = false;
if (key != null) {
if (storedKeyHash != null) {
passwordValid = await this.cryptoService.compareAndUpdateKeyHash(password, key);
} else {
const serverKeyHash = await this.cryptoService.hashPassword(
password,
key,
HashPurpose.ServerAuthorization
);
const request = new SecretVerificationRequest();
request.masterPasswordHash = serverKeyHash;
try {
await this.apiService.postAccountVerifyPassword(request);
passwordValid = true;
const localKeyHash = await this.cryptoService.hashPassword(
password,
key,
HashPurpose.LocalAuthorization
);
await this.cryptoService.setKeyHash(localKeyHash);
} catch {
// Ignore
}
}
}
if (passwordValid) {
await this.cryptoService.setKey(key);
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.");
}
}
private async setNewSessionKey() {
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 {
passwordEnv: string;
passwordFile: string;
constructor(passedOptions: Record<string, any>) {
this.passwordEnv = passedOptions?.passwordenv || passedOptions?.passwordEnv;
this.passwordFile = passedOptions?.passwordfile || passedOptions?.passwordFile;
}
}

5
apps/cli/src/flags.ts Normal file
View File

@@ -0,0 +1,5 @@
export type Flags = {
serve?: boolean;
};
export type FlagName = keyof Flags;

View File

@@ -0,0 +1,44 @@
{
"bitwarden": {
"message": "Bitwarden"
},
"authenticatorAppTitle": {
"message": "Authenticator App"
},
"yubiKeyTitle": {
"message": "YubiKey OTP Security Key"
},
"emailTitle": {
"message": "Email"
},
"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."
},
"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."
},
"sessionTimeout": {
"message": "Your session has timed out. Please go back and try logging in again."
}
}

View File

@@ -0,0 +1,16 @@
import { CollectionExport } from "jslib-common/models/export/collectionExport";
import { SelectionReadOnly } from "../selectionReadOnly";
export class OrganizationCollectionRequest extends CollectionExport {
static template(): OrganizationCollectionRequest {
const req = new OrganizationCollectionRequest();
req.organizationId = "00000000-0000-0000-0000-000000000000";
req.name = "Collection name";
req.externalId = null;
req.groups = [SelectionReadOnly.template(), SelectionReadOnly.template()];
return req;
}
groups: SelectionReadOnly[];
}

View File

@@ -0,0 +1,17 @@
import { AttachmentView } from "jslib-common/models/view/attachmentView";
export class AttachmentResponse {
id: string;
fileName: string;
size: string;
sizeName: string;
url: string;
constructor(o: AttachmentView) {
this.id = o.id;
this.fileName = o.fileName;
this.size = o.size;
this.sizeName = o.sizeName;
this.url = o.url;
}
}

View File

@@ -0,0 +1,33 @@
import { CipherType } from "jslib-common/enums/cipherType";
import { CipherWithIdExport } from "jslib-common/models/export/cipherWithIdsExport";
import { CipherView } from "jslib-common/models/view/cipherView";
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
import { AttachmentResponse } from "./attachmentResponse";
import { LoginResponse } from "./loginResponse";
import { PasswordHistoryResponse } from "./passwordHistoryResponse";
export class CipherResponse extends CipherWithIdExport implements BaseResponse {
object: string;
attachments: AttachmentResponse[];
revisionDate: Date;
deletedDate: Date;
passwordHistory: PasswordHistoryResponse[];
constructor(o: CipherView) {
super();
this.object = "item";
this.build(o);
if (o.attachments != null) {
this.attachments = o.attachments.map((a) => new AttachmentResponse(a));
}
this.revisionDate = o.revisionDate;
this.deletedDate = o.deletedDate;
if (o.passwordHistory != null) {
this.passwordHistory = o.passwordHistory.map((h) => new PasswordHistoryResponse(h));
}
if (o.type === CipherType.Login && o.login != null) {
this.login = new LoginResponse(o.login);
}
}
}

View File

@@ -0,0 +1,13 @@
import { CollectionWithIdExport } from "jslib-common/models/export/collectionWithIdExport";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
export class CollectionResponse extends CollectionWithIdExport implements BaseResponse {
object: string;
constructor(o: CollectionView) {
super();
this.object = "collection";
this.build(o);
}
}

View File

@@ -0,0 +1,13 @@
import { FolderWithIdExport } from "jslib-common/models/export/folderWithIdExport";
import { FolderView } from "jslib-common/models/view/folderView";
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
export class FolderResponse extends FolderWithIdExport implements BaseResponse {
object: string;
constructor(o: FolderView) {
super();
this.object = "folder";
this.build(o);
}
}

View File

@@ -0,0 +1,11 @@
import { LoginExport } from "jslib-common/models/export/loginExport";
import { LoginView } from "jslib-common/models/view/loginView";
export class LoginResponse extends LoginExport {
passwordRevisionDate: Date;
constructor(o: LoginView) {
super(o);
this.passwordRevisionDate = o.passwordRevisionDate != null ? o.passwordRevisionDate : null;
}
}

View File

@@ -0,0 +1,15 @@
import { CollectionView } from "jslib-common/models/view/collectionView";
import { SelectionReadOnly } from "../selectionReadOnly";
import { CollectionResponse } from "./collectionResponse";
export class OrganizationCollectionResponse extends CollectionResponse {
groups: SelectionReadOnly[];
constructor(o: CollectionView, groups: SelectionReadOnly[]) {
super(o);
this.object = "org-collection";
this.groups = groups;
}
}

View File

@@ -0,0 +1,22 @@
import { OrganizationUserStatusType } from "jslib-common/enums/organizationUserStatusType";
import { OrganizationUserType } from "jslib-common/enums/organizationUserType";
import { Organization } from "jslib-common/models/domain/organization";
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
export class OrganizationResponse implements BaseResponse {
object: string;
id: string;
name: string;
status: OrganizationUserStatusType;
type: OrganizationUserType;
enabled: boolean;
constructor(o: Organization) {
this.object = "organization";
this.id = o.id;
this.name = o.name;
this.status = o.status;
this.type = o.type;
this.enabled = o.enabled;
}
}

View File

@@ -0,0 +1,17 @@
import { OrganizationUserStatusType } from "jslib-common/enums/organizationUserStatusType";
import { OrganizationUserType } from "jslib-common/enums/organizationUserType";
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
export class OrganizationUserResponse implements BaseResponse {
object: string;
id: string;
email: string;
name: string;
status: OrganizationUserStatusType;
type: OrganizationUserType;
twoFactorEnabled: boolean;
constructor() {
this.object = "org-member";
}
}

View File

@@ -0,0 +1,11 @@
import { PasswordHistoryView } from "jslib-common/models/view/passwordHistoryView";
export class PasswordHistoryResponse {
lastUsedDate: Date;
password: string;
constructor(o: PasswordHistoryView) {
this.lastUsedDate = o.lastUsedDate;
this.password = o.password;
}
}

View File

@@ -0,0 +1,40 @@
import { SendType } from "jslib-common/enums/sendType";
import { SendAccessView } from "jslib-common/models/view/sendAccessView";
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
import { SendFileResponse } from "./sendFileResponse";
import { SendTextResponse } from "./sendTextResponse";
export class SendAccessResponse implements BaseResponse {
static template(): SendAccessResponse {
const req = new SendAccessResponse();
req.name = "Send name";
req.type = SendType.Text;
req.text = null;
req.file = null;
return req;
}
object = "send-access";
id: string;
name: string;
type: SendType;
text: SendTextResponse;
file: SendFileResponse;
constructor(o?: SendAccessView) {
if (o == null) {
return;
}
this.id = o.id;
this.name = o.name;
this.type = o.type;
if (o.type === SendType.Text && o.text != null) {
this.text = new SendTextResponse(o.text);
}
if (o.type === SendType.File && o.file != null) {
this.file = new SendFileResponse(o.file);
}
}
}

View File

@@ -0,0 +1,36 @@
import { SendFileView } from "jslib-common/models/view/sendFileView";
export class SendFileResponse {
static template(fileName = "file attachment location"): SendFileResponse {
const req = new SendFileResponse();
req.fileName = fileName;
return req;
}
static toView(file: SendFileResponse, view = new SendFileView()) {
if (file == null) {
return null;
}
view.id = file.id;
view.size = file.size;
view.sizeName = file.sizeName;
view.fileName = file.fileName;
return view;
}
id: string;
size: string;
sizeName: string;
fileName: string;
constructor(o?: SendFileView) {
if (o == null) {
return;
}
this.id = o.id;
this.size = o.size;
this.sizeName = o.sizeName;
this.fileName = o.fileName;
}
}

View File

@@ -0,0 +1,123 @@
import { SendType } from "jslib-common/enums/sendType";
import { Utils } from "jslib-common/misc/utils";
import { SendView } from "jslib-common/models/view/sendView";
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
import { SendFileResponse } from "./sendFileResponse";
import { SendTextResponse } from "./sendTextResponse";
const dateProperties: string[] = [
Utils.nameOf<SendResponse>("deletionDate"),
Utils.nameOf<SendResponse>("expirationDate"),
];
export class SendResponse implements BaseResponse {
static template(sendType?: SendType, deleteInDays = 7): SendResponse {
const req = new SendResponse();
req.name = "Send name";
req.notes = "Some notes about this send.";
req.type = sendType === SendType.File ? SendType.File : SendType.Text;
req.text = sendType === SendType.Text ? SendTextResponse.template() : null;
req.file = sendType === SendType.File ? SendFileResponse.template() : null;
req.maxAccessCount = null;
req.deletionDate = this.getStandardDeletionDate(deleteInDays);
req.expirationDate = null;
req.password = null;
req.disabled = false;
req.hideEmail = false;
return req;
}
static toView(send: SendResponse, view = new SendView()): SendView {
if (send == null) {
return null;
}
view.id = send.id;
view.accessId = send.accessId;
view.name = send.name;
view.notes = send.notes;
view.key = send.key == null ? null : Utils.fromB64ToArray(send.key);
view.type = send.type;
view.file = SendFileResponse.toView(send.file);
view.text = SendTextResponse.toView(send.text);
view.maxAccessCount = send.maxAccessCount;
view.accessCount = send.accessCount;
view.revisionDate = send.revisionDate;
view.deletionDate = send.deletionDate;
view.expirationDate = send.expirationDate;
view.password = send.password;
view.disabled = send.disabled;
view.hideEmail = send.hideEmail;
return view;
}
static fromJson(json: string) {
return JSON.parse(json, (key, value) => {
if (dateProperties.includes(key)) {
return value == null ? null : new Date(value);
}
return value;
});
}
private static getStandardDeletionDate(days: number) {
const d = new Date();
d.setTime(d.getTime() + days * 86400000); // ms per day
return d;
}
object = "send";
id: string;
accessId: string;
accessUrl: string;
name: string;
notes: string;
key: string;
type: SendType;
text: SendTextResponse;
file: SendFileResponse;
maxAccessCount?: number;
accessCount: number;
revisionDate: Date;
deletionDate: Date;
expirationDate: Date;
password: string;
passwordSet: boolean;
disabled: boolean;
hideEmail: boolean;
constructor(o?: SendView, webVaultUrl?: string) {
if (o == null) {
return;
}
this.id = o.id;
this.accessId = o.accessId;
let sendLinkBaseUrl = webVaultUrl;
if (sendLinkBaseUrl == null) {
sendLinkBaseUrl = "https://send.bitwarden.com/#";
} else {
sendLinkBaseUrl += "/#/send/";
}
this.accessUrl = sendLinkBaseUrl + this.accessId + "/" + o.urlB64Key;
this.name = o.name;
this.notes = o.notes;
this.key = Utils.fromBufferToB64(o.key);
this.type = o.type;
this.maxAccessCount = o.maxAccessCount;
this.accessCount = o.accessCount;
this.revisionDate = o.revisionDate;
this.deletionDate = o.deletionDate;
this.expirationDate = o.expirationDate;
this.passwordSet = o.password != null;
this.disabled = o.disabled;
this.hideEmail = o.hideEmail;
if (o.type === SendType.Text && o.text != null) {
this.text = new SendTextResponse(o.text);
}
if (o.type === SendType.File && o.file != null) {
this.file = new SendFileResponse(o.file);
}
}
}

View File

@@ -0,0 +1,30 @@
import { SendTextView } from "jslib-common/models/view/sendTextView";
export class SendTextResponse {
static template(text = "Text contained in the send.", hidden = false): SendTextResponse {
const req = new SendTextResponse();
req.text = text;
req.hidden = hidden;
return req;
}
static toView(text: SendTextResponse, view = new SendTextView()) {
if (text == null) {
return null;
}
view.text = text.text;
view.hidden = text.hidden;
return view;
}
text: string;
hidden: boolean;
constructor(o?: SendTextView) {
if (o == null) {
return;
}
this.text = o.text;
this.hidden = o.hidden;
}
}

View File

@@ -0,0 +1,11 @@
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
export class TemplateResponse implements BaseResponse {
object: string;
template: any;
constructor(template: any) {
this.object = "template";
this.template = template;
}
}

View File

@@ -0,0 +1,15 @@
export class SelectionReadOnly {
static template(): SelectionReadOnly {
return new SelectionReadOnly("00000000-0000-0000-0000-000000000000", false, false);
}
id: string;
readOnly: boolean;
hidePasswords: boolean;
constructor(id: string, readOnly: boolean, hidePasswords: boolean) {
this.id = id;
this.readOnly = readOnly;
this.hidePasswords = hidePasswords || false;
}
}

546
apps/cli/src/program.ts Normal file
View File

@@ -0,0 +1,546 @@
import * as chalk from "chalk";
import * as program from "commander";
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
import { KeySuffixOptions } from "jslib-common/enums/keySuffixOptions";
import { BaseProgram } from "jslib-node/cli/baseProgram";
import { LogoutCommand } from "jslib-node/cli/commands/logout.command";
import { UpdateCommand } from "jslib-node/cli/commands/update.command";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { Main } from "./bw";
import { CompletionCommand } from "./commands/completion.command";
import { ConfigCommand } from "./commands/config.command";
import { EncodeCommand } from "./commands/encode.command";
import { GenerateCommand } from "./commands/generate.command";
import { LockCommand } from "./commands/lock.command";
import { LoginCommand } from "./commands/login.command";
import { ServeCommand } from "./commands/serve.command";
import { StatusCommand } from "./commands/status.command";
import { SyncCommand } from "./commands/sync.command";
import { UnlockCommand } from "./commands/unlock.command";
import { TemplateResponse } from "./models/response/templateResponse";
import { CliUtils } from "./utils";
const writeLn = CliUtils.writeLn;
export class Program extends BaseProgram {
constructor(protected main: Main) {
super(main.stateService, writeLn);
}
async register() {
program
.option("--pretty", "Format output. JSON is tabbed with two spaces.")
.option("--raw", "Return raw output instead of a descriptive message.")
.option("--response", "Return a JSON formatted version of response output.")
.option("--cleanexit", "Exit with a success exit code (0) unless an error is thrown.")
.option("--quiet", "Don't return anything to stdout.")
.option("--nointeraction", "Do not prompt for interactive user input.")
.option("--session <session>", "Pass session key instead of reading from env.")
.version(await this.main.platformUtilsService.getApplicationVersion(), "-v, --version");
program.on("option:pretty", () => {
process.env.BW_PRETTY = "true";
});
program.on("option:raw", () => {
process.env.BW_RAW = "true";
});
program.on("option:quiet", () => {
process.env.BW_QUIET = "true";
});
program.on("option:response", () => {
process.env.BW_RESPONSE = "true";
});
program.on("option:cleanexit", () => {
process.env.BW_CLEANEXIT = "true";
});
program.on("option:nointeraction", () => {
process.env.BW_NOINTERACTION = "true";
});
program.on("option:session", (key) => {
process.env.BW_SESSION = key;
});
program.on("command:*", () => {
writeLn(chalk.redBright("Invalid command: " + program.args.join(" ")), false, true);
writeLn("See --help for a list of available commands.", true, true);
process.exitCode = 1;
});
program.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw login");
writeLn(" bw lock");
writeLn(" bw unlock myPassword321");
writeLn(" bw list --help");
writeLn(" bw list items --search google");
writeLn(" bw get item 99ee88d2-6046-4ea7-92c2-acac464b1412");
writeLn(" bw get password google.com");
writeLn(' echo \'{"name":"My Folder"}\' | bw encode');
writeLn(" bw create folder eyJuYW1lIjoiTXkgRm9sZGVyIn0K");
writeLn(
" bw edit folder c7c7b60b-9c61-40f2-8ccd-36c49595ed72 eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg=="
);
writeLn(" bw delete item 99ee88d2-6046-4ea7-92c2-acac464b1412");
writeLn(" bw generate -lusn --length 18");
writeLn(" bw config server https://bitwarden.example.com");
writeLn(" bw send -f ./file.ext");
writeLn(' bw send "text to send"');
writeLn(' echo "text to send" | bw send');
writeLn(
" bw receive https://vault.bitwarden.com/#/send/rg3iuoS_Akm2gqy6ADRHmg/Ht7dYjsqjmgqUM3rjzZDSQ"
);
writeLn("", true);
});
program
.command("login [email] [password]")
.description("Log into a user account.")
.option("--method <method>", "Two-step login method.")
.option("--code <code>", "Two-step login code.")
.option("--sso", "Log in with Single-Sign On.")
.option("--apikey", "Log in with an Api Key.")
.option("--passwordenv <passwordenv>", "Environment variable storing your password")
.option(
"--passwordfile <passwordfile>",
"Path to a file containing your password as its first line"
)
.option("--check", "Check login status.", async () => {
const authed = await this.main.stateService.getIsAuthenticated();
if (authed) {
const res = new MessageResponse("You are logged in!", null);
this.processResponse(Response.success(res), true);
}
this.processResponse(Response.error("You are not logged in."), true);
})
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" See docs for valid `method` enum values.");
writeLn("");
writeLn(" Pass `--raw` option to only return the session key.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw login");
writeLn(" bw login john@example.com myPassword321 --raw");
writeLn(" bw login john@example.com myPassword321 --method 1 --code 249213");
writeLn(" bw login --sso");
writeLn("", true);
})
.action(async (email: string, password: string, options: program.OptionValues) => {
if (!options.check) {
await this.exitIfAuthed();
const command = new LoginCommand(
this.main.authService,
this.main.apiService,
this.main.cryptoFunctionService,
this.main.i18nService,
this.main.environmentService,
this.main.passwordGenerationService,
this.main.platformUtilsService,
this.main.stateService,
this.main.cryptoService,
this.main.policyService,
this.main.twoFactorService,
this.main.syncService,
this.main.keyConnectorService,
async () => await this.main.logout()
);
const response = await command.run(email, password, options);
this.processResponse(response);
}
});
program
.command("logout")
.description("Log out of the current user account.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw logout");
writeLn("", true);
})
.action(async (cmd) => {
await this.exitIfNotAuthed();
const command = new LogoutCommand(
this.main.authService,
this.main.i18nService,
async () => await this.main.logout()
);
const response = await command.run();
this.processResponse(response);
});
program
.command("lock")
.description("Lock the vault and destroy active session keys.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw lock");
writeLn("", true);
})
.action(async (cmd) => {
await this.exitIfNotAuthed();
if (await this.main.keyConnectorService.getUsesKeyConnector()) {
const logoutCommand = new LogoutCommand(
this.main.authService,
this.main.i18nService,
async () => await this.main.logout()
);
await logoutCommand.run();
this.processResponse(
Response.error(
"You cannot lock your vault because you are using Key Connector. " +
"To protect your vault, you have been logged out."
),
true
);
return;
}
const command = new LockCommand(this.main.vaultTimeoutService);
const response = await command.run();
this.processResponse(response);
});
program
.command("unlock [password]")
.description("Unlock the vault and return a new session key.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" After unlocking, any previous session keys will no longer be valid.");
writeLn("");
writeLn(" Pass `--raw` option to only return the session key.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw unlock");
writeLn(" bw unlock myPassword321");
writeLn(" bw unlock myPassword321 --raw");
writeLn("", true);
})
.option("--check", "Check lock status.", async () => {
await this.exitIfNotAuthed();
const authStatus = await this.main.authService.getAuthStatus();
if (authStatus === AuthenticationStatus.Unlocked) {
const res = new MessageResponse("Vault is unlocked!", null);
this.processResponse(Response.success(res), true);
} else {
this.processResponse(Response.error("Vault is locked."), true);
}
})
.option("--passwordenv <passwordenv>", "Environment variable storing your password")
.option(
"--passwordfile <passwordfile>",
"Path to a file containing your password as its first line"
)
.action(async (password, cmd) => {
if (!cmd.check) {
await this.exitIfNotAuthed();
const command = new UnlockCommand(
this.main.cryptoService,
this.main.stateService,
this.main.cryptoFunctionService,
this.main.apiService,
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);
}
});
program
.command("sync")
.description("Pull the latest vault data from server.")
.option("-f, --force", "Force a full sync.")
.option("--last", "Get the last sync date.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw sync");
writeLn(" bw sync -f");
writeLn(" bw sync --last");
writeLn("", true);
})
.action(async (cmd) => {
await this.exitIfLocked();
const command = new SyncCommand(this.main.syncService);
const response = await command.run(cmd);
this.processResponse(response);
});
program
.command("generate")
.description("Generate a password/passphrase.")
.option("-u, --uppercase", "Include uppercase characters.")
.option("-l, --lowercase", "Include lowercase characters.")
.option("-n, --number", "Include numeric characters.")
.option("-s, --special", "Include special characters.")
.option("-p, --passphrase", "Generate a passphrase.")
.option("--length <length>", "Length of the password.")
.option("--words <words>", "Number of words.")
.option("--separator <separator>", "Word separator.")
.option("-c, --capitalize", "Title case passphrase.")
.option("--includeNumber", "Passphrase includes number.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" Default options are `-uln --length 14`.");
writeLn("");
writeLn(" Minimum `length` is 5.");
writeLn("");
writeLn(" Minimum `words` is 3.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw generate");
writeLn(" bw generate -u -l --length 18");
writeLn(" bw generate -ulns --length 25");
writeLn(" bw generate -ul");
writeLn(" bw generate -p --separator _");
writeLn(" bw generate -p --words 5 --separator space");
writeLn("", true);
})
.action(async (options) => {
const command = new GenerateCommand(
this.main.passwordGenerationService,
this.main.stateService
);
const response = await command.run(options);
this.processResponse(response);
});
program
.command("encode")
.description("Base 64 encode stdin.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" Use to create `encodedJson` for `create` and `edit` commands.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(' echo \'{"name":"My Folder"}\' | bw encode');
writeLn("", true);
})
.action(async () => {
const command = new EncodeCommand();
const response = await command.run();
this.processResponse(response);
});
program
.command("config <setting> [value]")
.description("Configure CLI settings.")
.option(
"--web-vault <url>",
"Provides a custom web vault URL that differs from the base URL."
)
.option("--api <url>", "Provides a custom API URL that differs from the base URL.")
.option("--identity <url>", "Provides a custom identity URL that differs from the base URL.")
.option(
"--icons <url>",
"Provides a custom icons service URL that differs from the base URL."
)
.option(
"--notifications <url>",
"Provides a custom notifications URL that differs from the base URL."
)
.option("--events <url>", "Provides a custom events URL that differs from the base URL.")
.option("--key-connector <url>", "Provides the URL for your Key Connector server.")
.on("--help", () => {
writeLn("\n Settings:");
writeLn("");
writeLn(" server - On-premises hosted installation URL.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw config server");
writeLn(" bw config server https://bw.company.com");
writeLn(" bw config server bitwarden.com");
writeLn(
" bw config server --api http://localhost:4000 --identity http://localhost:33656"
);
writeLn("", true);
})
.action(async (setting, value, options) => {
const command = new ConfigCommand(this.main.environmentService);
const response = await command.run(setting, value, options);
this.processResponse(response);
});
program
.command("update")
.description("Check for updates.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" Returns the URL to download the newest version of this CLI tool.");
writeLn("");
writeLn(" Use the `--raw` option to return only the download URL for the update.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw update");
writeLn(" bw update --raw");
writeLn("", true);
})
.action(async () => {
const command = new UpdateCommand(
this.main.platformUtilsService,
this.main.i18nService,
"cli",
"bw",
true
);
const response = await command.run();
this.processResponse(response);
});
program
.command("completion")
.description("Generate shell completions.")
.option("--shell <shell>", "Shell to generate completions for.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" Valid shells are `zsh`.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw completion --shell zsh");
writeLn("", true);
})
.action(async (options: program.OptionValues, cmd: program.Command) => {
const command = new CompletionCommand();
const response = await command.run(options);
this.processResponse(response);
});
program
.command("status")
.description("Show server, last sync, user information, and vault status.")
.on("--help", () => {
writeLn("");
writeLn("");
writeLn(" Example return value:");
writeLn("");
writeLn(" {");
writeLn(' "serverUrl": "https://bitwarden.example.com",');
writeLn(' "lastSync": "2020-06-16T06:33:51.419Z",');
writeLn(' "userEmail": "user@example.com,');
writeLn(' "userId": "00000000-0000-0000-0000-000000000000",');
writeLn(' "status": "locked"');
writeLn(" }");
writeLn("");
writeLn(" Notes:");
writeLn("");
writeLn(" `status` is one of:");
writeLn(" - `unauthenticated` when you are not logged in");
writeLn(" - `locked` when you are logged in and the vault is locked");
writeLn(" - `unlocked` when you are logged in and the vault is unlocked");
writeLn("", true);
})
.action(async () => {
const command = new StatusCommand(
this.main.environmentService,
this.main.syncService,
this.main.stateService,
this.main.authService
);
const response = await command.run();
this.processResponse(response);
});
if (CliUtils.flagEnabled("serve")) {
program
.command("serve")
.description("Start a RESTful API webserver.")
.option("--hostname <hostname>", "The hostname to bind your API webserver to.")
.option("--port <port>", "The port to run your API webserver on.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" Default hostname is `localhost`.");
writeLn(" Use hostname `all` for no hostname binding.");
writeLn(" Default port is `8087`.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw serve");
writeLn(" bw serve --port 8080");
writeLn(" bw serve --hostname bwapi.mydomain.com --port 80");
writeLn("", true);
})
.action(async (cmd) => {
await this.exitIfNotAuthed();
const command = new ServeCommand(this.main);
await command.run(cmd);
});
}
}
protected processResponse(response: Response, exitImmediately = false) {
super.processResponse(response, exitImmediately, () => {
if (response.data.object === "template") {
return this.getJson((response.data as TemplateResponse).template);
}
return null;
});
}
protected async exitIfLocked() {
await this.exitIfNotAuthed();
if (await this.main.cryptoService.hasKeyInMemory()) {
return;
} else if (await this.main.cryptoService.hasKeyStored(KeySuffixOptions.Auto)) {
// load key into memory
await this.main.cryptoService.getKey();
} else if (process.env.BW_NOINTERACTION !== "true") {
// must unlock
if (await this.main.keyConnectorService.getUsesKeyConnector()) {
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.stateService,
this.main.cryptoFunctionService,
this.main.apiService,
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) {
this.processResponse(response, true);
}
}
} else {
this.processResponse(Response.error("Vault is locked."), true);
}
}
}

View File

@@ -0,0 +1,334 @@
import * as fs from "fs";
import * as path from "path";
import * as chalk from "chalk";
import * as program from "commander";
import { SendType } from "jslib-common/enums/sendType";
import { Utils } from "jslib-common/misc/utils";
import { Response } from "jslib-node/cli/models/response";
import { Main } from "./bw";
import { GetCommand } from "./commands/get.command";
import { SendCreateCommand } from "./commands/send/create.command";
import { SendDeleteCommand } from "./commands/send/delete.command";
import { SendEditCommand } from "./commands/send/edit.command";
import { SendGetCommand } from "./commands/send/get.command";
import { SendListCommand } from "./commands/send/list.command";
import { SendReceiveCommand } from "./commands/send/receive.command";
import { SendRemovePasswordCommand } from "./commands/send/removePassword.command";
import { SendFileResponse } from "./models/response/sendFileResponse";
import { SendResponse } from "./models/response/sendResponse";
import { SendTextResponse } from "./models/response/sendTextResponse";
import { Program } from "./program";
import { CliUtils } from "./utils";
const writeLn = CliUtils.writeLn;
export class SendProgram extends Program {
constructor(main: Main) {
super(main);
}
async register() {
program.addCommand(this.sendCommand());
// receive is accessible both at `bw receive` and `bw send receive`
program.addCommand(this.receiveCommand());
}
private sendCommand(): program.Command {
return new program.Command("send")
.arguments("<data>")
.description(
"Work with Bitwarden sends. A Send can be quickly created using this command or subcommands can be used to fine-tune the Send",
{
data: "The data to Send. Specify as a filepath with the --file option",
}
)
.option("-f, --file", "Specifies that <data> is a filepath")
.option(
"-d, --deleteInDays <days>",
"The number of days in the future to set deletion date, defaults to 7",
"7"
)
.option("-a, --maxAccessCount <amount>", "The amount of max possible accesses.")
.option("--hidden", "Hide <data> in web by default. Valid only if --file is not set.")
.option(
"-n, --name <name>",
"The name of the Send. Defaults to a guid for text Sends and the filename for files."
)
.option("--notes <notes>", "Notes to add to the Send.")
.option(
"--fullObject",
"Specifies that the full Send object should be returned rather than just the access url."
)
.addCommand(this.listCommand())
.addCommand(this.templateCommand())
.addCommand(this.getCommand())
.addCommand(this.receiveCommand())
.addCommand(this.createCommand())
.addCommand(this.editCommand())
.addCommand(this.removePasswordCommand())
.addCommand(this.deleteCommand())
.action(async (data: string, options: program.OptionValues) => {
const encodedJson = this.makeSendJson(data, options);
let response: Response;
if (encodedJson instanceof Response) {
response = encodedJson;
} else {
response = await this.runCreate(encodedJson, options);
}
this.processResponse(response);
});
}
private receiveCommand(): program.Command {
return new program.Command("receive")
.arguments("<url>")
.description("Access a Bitwarden Send from a url")
.option("--password <password>", "Password needed to access the Send.")
.option("--passwordenv <passwordenv>", "Environment variable storing the Send's password")
.option(
"--passwordfile <passwordfile>",
"Path to a file containing the Sends password as its first line"
)
.option("--obj", "Return the Send's json object rather than the Send's content")
.option("--output <location>", "Specify a file path to save a File-type Send to")
.on("--help", () => {
writeLn("");
writeLn(
"If a password is required, the provided password is used or the user is prompted."
);
writeLn("", true);
})
.action(async (url: string, options: program.OptionValues) => {
const cmd = new SendReceiveCommand(
this.main.apiService,
this.main.cryptoService,
this.main.cryptoFunctionService,
this.main.platformUtilsService,
this.main.environmentService
);
const response = await cmd.run(url, options);
this.processResponse(response);
});
}
private listCommand(): program.Command {
return new program.Command("list")
.description("List all the Sends owned by you")
.on("--help", () => {
writeLn(chalk("This is in the list command"));
})
.action(async (options: program.OptionValues) => {
await this.exitIfLocked();
const cmd = new SendListCommand(
this.main.sendService,
this.main.environmentService,
this.main.searchService
);
const response = await cmd.run(options);
this.processResponse(response);
});
}
private templateCommand(): program.Command {
return new program.Command("template")
.arguments("<object>")
.description("Get json templates for send objects", {
object: "Valid objects are: send, send.text, send.file",
})
.action(async (object) => {
const cmd = new GetCommand(
this.main.cipherService,
this.main.folderService,
this.main.collectionService,
this.main.totpService,
this.main.auditService,
this.main.cryptoService,
this.main.stateService,
this.main.searchService,
this.main.apiService,
this.main.organizationService
);
const response = await cmd.run("template", object, null);
this.processResponse(response);
});
}
private getCommand(): program.Command {
return new program.Command("get")
.arguments("<id>")
.description("Get Sends owned by you.")
.option("--output <output>", "Output directory or filename for attachment.")
.option("--text", "Specifies to return the text content of a Send")
.on("--help", () => {
writeLn("");
writeLn(" Id:");
writeLn("");
writeLn(" Search term or Send's globally unique `id`.");
writeLn("");
writeLn(" If raw output is specified and no output filename or directory is given for");
writeLn(" an attachment query, the attachment content is written to stdout.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw get send searchText");
writeLn(" bw get send id");
writeLn(" bw get send searchText --text");
writeLn(" bw get send searchText --file");
writeLn(" bw get send searchText --file --output ../Photos/photo.jpg");
writeLn(" bw get send searchText --file --raw");
writeLn("", true);
})
.action(async (id: string, options: program.OptionValues) => {
await this.exitIfLocked();
const cmd = new SendGetCommand(
this.main.sendService,
this.main.environmentService,
this.main.searchService,
this.main.cryptoService
);
const response = await cmd.run(id, options);
this.processResponse(response);
});
}
private createCommand(): program.Command {
return new program.Command("create")
.arguments("[encodedJson]")
.description("create a Send", {
encodedJson: "JSON object to upload. Can also be piped in through stdin.",
})
.option("--file <path>", "file to Send. Can also be specified in parent's JSON.")
.option("--text <text>", "text to Send. Can also be specified in parent's JSON.")
.option("--hidden", "text hidden flag. Valid only with the --text option.")
.option(
"--password <password>",
"optional password to access this Send. Can also be specified in JSON"
)
.on("--help", () => {
writeLn("");
writeLn("Note:");
writeLn(" Options specified in JSON take precedence over command options");
writeLn("", true);
})
.action(
async (
encodedJson: string,
options: program.OptionValues,
args: { parent: program.Command }
) => {
// Work-around to support `--fullObject` option for `send create --fullObject`
// Calling `option('--fullObject', ...)` above won't work due to Commander doesn't like same option
// to be defind on both parent-command and sub-command
const { fullObject = false } = args.parent.opts();
const mergedOptions = {
...options,
fullObject: fullObject,
};
const response = await this.runCreate(encodedJson, mergedOptions);
this.processResponse(response);
}
);
}
private editCommand(): program.Command {
return new program.Command("edit")
.arguments("[encodedJson]")
.description("edit a Send", {
encodedJson:
"Updated JSON object to save. If not provided, encodedJson is read from stdin.",
})
.option("--itemid <itemid>", "Overrides the itemId provided in [encodedJson]")
.on("--help", () => {
writeLn("");
writeLn("Note:");
writeLn(" You cannot update a File-type Send's file. Just delete and remake it");
writeLn("", true);
})
.action(async (encodedJson: string, options: program.OptionValues) => {
await this.exitIfLocked();
const getCmd = new SendGetCommand(
this.main.sendService,
this.main.environmentService,
this.main.searchService,
this.main.cryptoService
);
const cmd = new SendEditCommand(this.main.sendService, this.main.stateService, getCmd);
const response = await cmd.run(encodedJson, options);
this.processResponse(response);
});
}
private deleteCommand(): program.Command {
return new program.Command("delete")
.arguments("<id>")
.description("delete a Send", {
id: "The id of the Send to delete.",
})
.action(async (id: string) => {
await this.exitIfLocked();
const cmd = new SendDeleteCommand(this.main.sendService);
const response = await cmd.run(id);
this.processResponse(response);
});
}
private removePasswordCommand(): program.Command {
return new program.Command("remove-password")
.arguments("<id>")
.description("removes the saved password from a Send.", {
id: "The id of the Send to alter.",
})
.action(async (id: string) => {
await this.exitIfLocked();
const cmd = new SendRemovePasswordCommand(this.main.sendService);
const response = await cmd.run(id);
this.processResponse(response);
});
}
private makeSendJson(data: string, options: program.OptionValues) {
let sendFile = null;
let sendText = null;
let name = Utils.newGuid();
let type = SendType.Text;
if (options.file != null) {
data = path.resolve(data);
if (!fs.existsSync(data)) {
return Response.badRequest("data path does not exist");
}
sendFile = SendFileResponse.template(data);
name = path.basename(data);
type = SendType.File;
} else {
sendText = SendTextResponse.template(data, options.hidden);
}
const template = Utils.assign(SendResponse.template(null, options.deleteInDays), {
name: options.name ?? name,
notes: options.notes,
file: sendFile,
text: sendText,
type: type,
});
return Buffer.from(JSON.stringify(template), "utf8").toString("base64");
}
private async runCreate(encodedJson: string, options: program.OptionValues) {
await this.exitIfLocked();
const cmd = new SendCreateCommand(
this.main.sendService,
this.main.stateService,
this.main.environmentService
);
return await cmd.run(encodedJson, options);
}
}

View File

@@ -0,0 +1,20 @@
import * as fs from "fs";
import * as path from "path";
import { I18nService as BaseI18nService } from "jslib-common/services/i18n.service";
export class I18nService extends BaseI18nService {
constructor(systemLanguage: string, localesDirectory: string) {
super(systemLanguage, localesDirectory, (formattedLocale: string) => {
const filePath = path.join(
__dirname,
this.localesDirectory + "/" + formattedLocale + "/messages.json"
);
const localesJson = fs.readFileSync(filePath, "utf8");
const locales = JSON.parse(localesJson.replace(/^\uFEFF/, "")); // strip the BOM
return Promise.resolve(locales);
});
this.supportedTranslationLocales = ["en"];
}
}

View File

@@ -0,0 +1,40 @@
import * as lock from "proper-lockfile";
import { OperationOptions } from "retry";
import { LogService } from "jslib-common/abstractions/log.service";
import { Utils } from "jslib-common/misc/utils";
import { LowdbStorageService as LowdbStorageServiceBase } from "jslib-node/services/lowdbStorage.service";
const retries: OperationOptions = {
retries: 50,
minTimeout: 100,
maxTimeout: 250,
factor: 2,
};
export class LowdbStorageService extends LowdbStorageServiceBase {
constructor(
logService: LogService,
defaults?: any,
dir?: string,
allowCache = false,
private requireLock = false
) {
super(logService, defaults, dir, allowCache);
}
protected async lockDbFile<T>(action: () => T): Promise<T> {
if (this.requireLock && !Utils.isNullOrWhitespace(this.dataFilePath)) {
this.logService.info("acquiring db file lock");
return await lock.lock(this.dataFilePath, { retries: retries }).then((release) => {
try {
return action();
} finally {
release();
}
});
} else {
return action();
}
}
}

View File

@@ -0,0 +1,103 @@
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { StorageService } from "jslib-common/abstractions/storage.service";
import { Utils } from "jslib-common/misc/utils";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
export class NodeEnvSecureStorageService implements StorageService {
constructor(
private storageService: StorageService,
private logService: LogService,
private cryptoService: () => CryptoService
) {}
async get<T>(key: string): Promise<T> {
const value = await this.storageService.get<string>(this.makeProtectedStorageKey(key));
if (value == null) {
return null;
}
const obj = await this.decrypt(value);
return obj as any;
}
async has(key: string): Promise<boolean> {
return (await this.get(key)) != null;
}
async save(key: string, obj: any): Promise<any> {
if (obj == null) {
return this.remove(key);
}
if (obj !== null && typeof obj !== "string") {
throw new Error("Only string storage is allowed.");
}
const protectedObj = await this.encrypt(obj);
await this.storageService.save(this.makeProtectedStorageKey(key), protectedObj);
}
remove(key: string): Promise<any> {
return this.storageService.remove(this.makeProtectedStorageKey(key));
}
private async encrypt(plainValue: string): Promise<string> {
const sessionKey = this.getSessionKey();
if (sessionKey == null) {
throw new Error("No session key available.");
}
const encValue = await this.cryptoService().encryptToBytes(
Utils.fromB64ToArray(plainValue).buffer,
sessionKey
);
if (encValue == null) {
throw new Error("Value didn't encrypt.");
}
return Utils.fromBufferToB64(encValue.buffer);
}
private async decrypt(encValue: string): Promise<string> {
try {
const sessionKey = this.getSessionKey();
if (sessionKey == null) {
return null;
}
const decValue = await this.cryptoService().decryptFromBytes(
Utils.fromB64ToArray(encValue).buffer,
sessionKey
);
if (decValue == null) {
this.logService.info("Failed to decrypt.");
return null;
}
return Utils.fromBufferToB64(decValue);
} catch (e) {
this.logService.info("Decrypt error.");
return null;
}
}
private getSessionKey() {
try {
if (process.env.BW_SESSION != null) {
const sessionBuffer = Utils.fromB64ToArray(process.env.BW_SESSION).buffer;
if (sessionBuffer != null) {
const sessionKey = new SymmetricCryptoKey(sessionBuffer);
if (sessionBuffer != null) {
return sessionKey;
}
}
}
} catch (e) {
this.logService.info("Session key is invalid.");
}
return null;
}
private makeProtectedStorageKey(key: string) {
return "__PROTECTED__" + key;
}
}

270
apps/cli/src/utils.ts Normal file
View File

@@ -0,0 +1,270 @@
import * as fs from "fs";
import * as path from "path";
import * as inquirer from "inquirer";
import * as JSZip from "jszip";
import { LogService } from "jslib-common/abstractions/log.service";
import { NodeUtils } from "jslib-common/misc/nodeUtils";
import { Utils } from "jslib-common/misc/utils";
import { Organization } from "jslib-common/models/domain/organization";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { FolderView } from "jslib-common/models/view/folderView";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { FlagName, Flags } from "./flags";
export class CliUtils {
static writeLn(s: string, finalLine = false, error = false) {
const stream = error ? process.stderr : process.stdout;
if (finalLine && (process.platform === "win32" || !stream.isTTY)) {
stream.write(s);
} else {
stream.write(s + "\n");
}
}
static readFile(input: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
let p: string = null;
if (input != null && input !== "") {
const osInput = path.join(input);
if (osInput.indexOf(path.sep) === -1) {
p = path.join(process.cwd(), osInput);
} else {
p = osInput;
}
} else {
reject("You must specify a file path.");
}
fs.readFile(p, "utf8", (err, data) => {
if (err != null) {
reject(err.message);
}
resolve(data);
});
});
}
static extract1PuxContent(input: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
let p: string = null;
if (input != null && input !== "") {
const osInput = path.join(input);
if (osInput.indexOf(path.sep) === -1) {
p = path.join(process.cwd(), osInput);
} else {
p = osInput;
}
} else {
reject("You must specify a file path.");
}
fs.readFile(p, function (err, data) {
if (err) {
reject(err);
}
JSZip.loadAsync(data).then(
(zip) => {
resolve(zip.file("export.data").async("string"));
},
(reason) => {
reject(reason);
}
);
});
});
}
/**
* Save the given data to a file and determine the target file if necessary.
* If output is non-empty, it is used as target filename. Otherwise the target filename is
* built from the current working directory and the given defaultFileName.
*
* @param data to be written to the file.
* @param output file to write to or empty to choose automatically.
* @param defaultFileName to use when no explicit output filename is given.
* @return the chosen output file.
*/
static saveFile(data: string | Buffer, output: string, defaultFileName: string) {
let p: string = null;
let mkdir = false;
if (output != null && output !== "") {
const osOutput = path.join(output);
if (osOutput.indexOf(path.sep) === -1) {
p = path.join(process.cwd(), osOutput);
} else {
mkdir = true;
if (osOutput.endsWith(path.sep)) {
p = path.join(osOutput, defaultFileName);
} else {
p = osOutput;
}
}
} else {
p = path.join(process.cwd(), defaultFileName);
}
p = path.resolve(p);
if (mkdir) {
const dir = p.substring(0, p.lastIndexOf(path.sep));
if (!fs.existsSync(dir)) {
NodeUtils.mkdirpSync(dir, "700");
}
}
return new Promise<string>((resolve, reject) => {
fs.writeFile(p, data, { encoding: "utf8", mode: 0o600 }, (err) => {
if (err != null) {
reject("Cannot save file to " + p);
}
resolve(p);
});
});
}
/**
* Process the given data and write it to a file if possible. If the user requested RAW output and
* no output name is given, the file is directly written to stdout. The resulting Response contains
* an otherwise empty message then to prevent writing other information to stdout.
*
* If an output is given or no RAW output is requested, the rules from [saveFile] apply.
*
* @param data to be written to the file or stdout.
* @param output file to write to or empty to choose automatically.
* @param defaultFileName to use when no explicit output filename is given.
* @return an empty [Response] if written to stdout or a [Response] with the chosen output file otherwise.
*/
static async saveResultToFile(data: string | Buffer, output: string, defaultFileName: string) {
if ((output == null || output === "") && process.env.BW_RAW === "true") {
// No output is given and the user expects raw output. Since the command result is about content,
// we directly return the command result to stdout (and suppress further messages).
process.stdout.write(data);
return Response.success();
}
const filePath = await this.saveFile(data, output, defaultFileName);
const res = new MessageResponse("Saved " + filePath, null);
res.raw = filePath;
return Response.success(res);
}
static readStdin(): Promise<string> {
return new Promise((resolve, reject) => {
let input = "";
if (process.stdin.isTTY) {
resolve(input);
return;
}
process.stdin.setEncoding("utf8");
process.stdin.on("readable", () => {
// eslint-disable-next-line
while (true) {
const chunk = process.stdin.read();
if (chunk == null) {
break;
}
input += chunk;
}
});
process.stdin.on("end", () => {
resolve(input);
});
});
}
static searchFolders(folders: FolderView[], search: string) {
search = search.toLowerCase();
return folders.filter((f) => {
if (f.name != null && f.name.toLowerCase().indexOf(search) > -1) {
return true;
}
return false;
});
}
static searchCollections(collections: CollectionView[], search: string) {
search = search.toLowerCase();
return collections.filter((c) => {
if (c.name != null && c.name.toLowerCase().indexOf(search) > -1) {
return true;
}
return false;
});
}
static searchOrganizations(organizations: Organization[], search: string) {
search = search.toLowerCase();
return organizations.filter((o) => {
if (o.name != null && o.name.toLowerCase().indexOf(search) > -1) {
return true;
}
return false;
});
}
/**
* 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;
}
static flagEnabled(flag: FlagName) {
return this.flags[flag] == null || this.flags[flag];
}
private static get flags(): Flags {
const envFlags = process.env.FLAGS;
if (typeof envFlags === "string") {
return JSON.parse(envFlags) as Flags;
} else {
return envFlags as Flags;
}
}
}

View File

@@ -0,0 +1,486 @@
import * as program from "commander";
import { Response } from "jslib-node/cli/models/response";
import { Main } from "./bw";
import { ConfirmCommand } from "./commands/confirm.command";
import { CreateCommand } from "./commands/create.command";
import { DeleteCommand } from "./commands/delete.command";
import { EditCommand } from "./commands/edit.command";
import { ExportCommand } from "./commands/export.command";
import { GetCommand } from "./commands/get.command";
import { ImportCommand } from "./commands/import.command";
import { ListCommand } from "./commands/list.command";
import { RestoreCommand } from "./commands/restore.command";
import { ShareCommand } from "./commands/share.command";
import { Program } from "./program";
import { CliUtils } from "./utils";
const writeLn = CliUtils.writeLn;
export class VaultProgram extends Program {
constructor(protected main: Main) {
super(main);
}
async register() {
program
.addCommand(this.listCommand())
.addCommand(this.getCommand())
.addCommand(this.createCommand())
.addCommand(this.editCommand())
.addCommand(this.deleteCommand())
.addCommand(this.restoreCommand())
.addCommand(this.shareCommand("move", false))
.addCommand(this.confirmCommand())
.addCommand(this.importCommand())
.addCommand(this.exportCommand())
.addCommand(this.shareCommand("share", true));
}
private validateObject(requestedObject: string, validObjects: string[]): boolean {
let success = true;
if (!validObjects.includes(requestedObject)) {
success = false;
this.processResponse(
Response.badRequest(
'Unknown object "' +
requestedObject +
'". Allowed objects are ' +
validObjects.join(", ") +
"."
)
);
}
return success;
}
private listCommand(): program.Command {
const listObjects = [
"items",
"folders",
"collections",
"org-collections",
"org-members",
"organizations",
];
return new program.Command("list")
.arguments("<object>")
.description("List an array of objects from the vault.", {
object: "Valid objects are: " + listObjects.join(", "),
})
.option("--search <search>", "Perform a search on the listed objects.")
.option("--url <url>", "Filter items of type login with a url-match search.")
.option("--folderid <folderid>", "Filter items by folder id.")
.option("--collectionid <collectionid>", "Filter items by collection id.")
.option(
"--organizationid <organizationid>",
"Filter items or collections by organization id."
)
.option("--trash", "Filter items that are deleted and in the trash.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" Combining search with a filter performs a logical AND operation.");
writeLn("");
writeLn(" Combining multiple filters performs a logical OR operation.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw list items");
writeLn(" bw list items --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2");
writeLn(
" bw list items --search google --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2"
);
writeLn(" bw list items --url https://google.com");
writeLn(" bw list items --folderid null");
writeLn(" bw list items --organizationid notnull");
writeLn(
" bw list items --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2 --organizationid notnull"
);
writeLn(" bw list items --trash");
writeLn(" bw list folders --search email");
writeLn(" bw list org-members --organizationid 60556c31-e649-4b5d-8daf-fc1c391a1bf2");
writeLn("", true);
})
.action(async (object, cmd) => {
if (!this.validateObject(object, listObjects)) {
return;
}
await this.exitIfLocked();
const command = new ListCommand(
this.main.cipherService,
this.main.folderService,
this.main.collectionService,
this.main.organizationService,
this.main.searchService,
this.main.apiService
);
const response = await command.run(object, cmd);
this.processResponse(response);
});
}
private getCommand(): program.Command {
const getObjects = [
"item",
"username",
"password",
"uri",
"totp",
"notes",
"exposed",
"attachment",
"folder",
"collection",
"org-collection",
"organization",
"template",
"fingerprint",
"send",
];
return new program.Command("get")
.arguments("<object> <id>")
.description("Get an object from the vault.", {
object: "Valid objects are: " + getObjects.join(", "),
id: "Search term or object's globally unique `id`.",
})
.option("--itemid <itemid>", "Attachment's item id.")
.option("--output <output>", "Output directory or filename for attachment.")
.option("--organizationid <organizationid>", "Organization id for an organization object.")
.on("--help", () => {
writeLn("\n If raw output is specified and no output filename or directory is given for");
writeLn(" an attachment query, the attachment content is written to stdout.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw get item 99ee88d2-6046-4ea7-92c2-acac464b1412");
writeLn(" bw get password https://google.com");
writeLn(" bw get totp google.com");
writeLn(" bw get notes google.com");
writeLn(" bw get exposed yahoo.com");
writeLn(
" bw get attachment b857igwl1dzrs2 --itemid 99ee88d2-6046-4ea7-92c2-acac464b1412 " +
"--output ./photo.jpg"
);
writeLn(
" bw get attachment photo.jpg --itemid 99ee88d2-6046-4ea7-92c2-acac464b1412 --raw"
);
writeLn(" bw get folder email");
writeLn(" bw get template folder");
writeLn("", true);
})
.action(async (object, id, cmd) => {
if (!this.validateObject(object, getObjects)) {
return;
}
await this.exitIfLocked();
const command = new GetCommand(
this.main.cipherService,
this.main.folderService,
this.main.collectionService,
this.main.totpService,
this.main.auditService,
this.main.cryptoService,
this.main.stateService,
this.main.searchService,
this.main.apiService,
this.main.organizationService
);
const response = await command.run(object, id, cmd);
this.processResponse(response);
});
}
private createCommand() {
const createObjects = ["item", "attachment", "folder", "org-collection"];
return new program.Command("create")
.arguments("<object> [encodedJson]")
.description("Create an object in the vault.", {
object: "Valid objects are: " + createObjects.join(", "),
encodedJson: "Encoded json of the object to create. Can also be piped into stdin.",
})
.option("--file <file>", "Path to file for attachment.")
.option("--itemid <itemid>", "Attachment's item id.")
.option("--organizationid <organizationid>", "Organization id for an organization object.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw create folder eyJuYW1lIjoiTXkgRm9sZGVyIn0K");
writeLn(" echo 'eyJuYW1lIjoiTXkgRm9sZGVyIn0K' | bw create folder");
writeLn(
" bw create attachment --file ./myfile.csv " +
"--itemid 16b15b89-65b3-4639-ad2a-95052a6d8f66"
);
writeLn("", true);
})
.action(async (object, encodedJson, cmd) => {
if (!this.validateObject(object, createObjects)) {
return;
}
await this.exitIfLocked();
const command = new CreateCommand(
this.main.cipherService,
this.main.folderService,
this.main.stateService,
this.main.cryptoService,
this.main.apiService
);
const response = await command.run(object, encodedJson, cmd);
this.processResponse(response);
});
}
private editCommand(): program.Command {
const editObjects = ["item", "item-collections", "folder", "org-collection"];
return new program.Command("edit")
.arguments("<object> <id> [encodedJson]")
.description("Edit an object from the vault.", {
object: "Valid objects are: " + editObjects.join(", "),
id: "Object's globally unique `id`.",
encodedJson: "Encoded json of the object to create. Can also be piped into stdin.",
})
.option("--organizationid <organizationid>", "Organization id for an organization object.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(
" bw edit folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02 eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg=="
);
writeLn(
" echo 'eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg==' | " +
"bw edit folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02"
);
writeLn(
" bw edit item-collections 78307355-fd25-416b-88b8-b33fd0e88c82 " +
"WyI5NzQwNTNkMC0zYjMzLTRiOTgtODg2ZS1mZWNmNWM4ZGJhOTYiXQ=="
);
writeLn("", true);
})
.action(async (object, id, encodedJson, cmd) => {
if (!this.validateObject(object, editObjects)) {
return;
}
await this.exitIfLocked();
const command = new EditCommand(
this.main.cipherService,
this.main.folderService,
this.main.cryptoService,
this.main.apiService
);
const response = await command.run(object, id, encodedJson, cmd);
this.processResponse(response);
});
}
private deleteCommand(): program.Command {
const deleteObjects = ["item", "attachment", "folder", "org-collection"];
return new program.Command("delete")
.arguments("<object> <id>")
.description("Delete an object from the vault.", {
object: "Valid objects are: " + deleteObjects.join(", "),
id: "Object's globally unique `id`.",
})
.option("--itemid <itemid>", "Attachment's item id.")
.option("--organizationid <organizationid>", "Organization id for an organization object.")
.option(
"-p, --permanent",
"Permanently deletes the item instead of soft-deleting it (item only)."
)
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw delete item 7063feab-4b10-472e-b64c-785e2b870b92");
writeLn(" bw delete item 89c21cd2-fab0-4f69-8c6e-ab8a0168f69a --permanent");
writeLn(" bw delete folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02");
writeLn(
" bw delete attachment b857igwl1dzrs2 --itemid 310d5ffd-e9a2-4451-af87-ea054dce0f78"
);
writeLn("", true);
})
.action(async (object, id, cmd) => {
if (!this.validateObject(object, deleteObjects)) {
return;
}
await this.exitIfLocked();
const command = new DeleteCommand(
this.main.cipherService,
this.main.folderService,
this.main.stateService,
this.main.apiService
);
const response = await command.run(object, id, cmd);
this.processResponse(response);
});
}
private restoreCommand(): program.Command {
const restoreObjects = ["item"];
return new program.Command("restore")
.arguments("<object> <id>")
.description("Restores an object from the trash.", {
object: "Valid objects are: " + restoreObjects.join(", "),
id: "Object's globally unique `id`.",
})
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw restore item 7063feab-4b10-472e-b64c-785e2b870b92");
writeLn("", true);
})
.action(async (object, id, cmd) => {
if (!this.validateObject(object, restoreObjects)) {
return;
}
await this.exitIfLocked();
const command = new RestoreCommand(this.main.cipherService);
const response = await command.run(object, id);
this.processResponse(response);
});
}
private shareCommand(commandName: string, deprecated: boolean): program.Command {
return new program.Command(commandName)
.arguments("<id> <organizationId> [encodedJson]")
.description((deprecated ? "--DEPRECATED-- " : "") + "Move an item to an organization.", {
id: "Object's globally unique `id`.",
organizationId: "Organization's globally unique `id`.",
encodedJson: "Encoded json of an array of collection ids. Can also be piped into stdin.",
})
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(
" bw " +
commandName +
" 4af958ce-96a7-45d9-beed-1e70fabaa27a " +
"6d82949b-b44d-468a-adae-3f3bacb0ea32 WyI5NzQwNTNkMC0zYjMzLTRiOTgtODg2ZS1mZWNmNWM4ZGJhOTYiXQ=="
);
writeLn(
" echo '[\"974053d0-3b33-4b98-886e-fecf5c8dba96\"]' | bw encode | " +
"bw " +
commandName +
" 4af958ce-96a7-45d9-beed-1e70fabaa27a 6d82949b-b44d-468a-adae-3f3bacb0ea32"
);
if (deprecated) {
writeLn("");
writeLn('--DEPRECATED See "bw move" for the current implementation--');
}
writeLn("", true);
})
.action(async (id, organizationId, encodedJson, cmd) => {
await this.exitIfLocked();
const command = new ShareCommand(this.main.cipherService);
const response = await command.run(id, organizationId, encodedJson);
this.processResponse(response);
});
}
private confirmCommand(): program.Command {
const confirmObjects = ["org-member"];
return new program.Command("confirm")
.arguments("<object> <id>")
.description("Confirm an object to the organization.", {
object: "Valid objects are: " + confirmObjects.join(", "),
id: "Object's globally unique `id`.",
})
.option("--organizationid <organizationid>", "Organization id for an organization object.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(
" bw confirm org-member 7063feab-4b10-472e-b64c-785e2b870b92 " +
"--organizationid 310d5ffd-e9a2-4451-af87-ea054dce0f78"
);
writeLn("", true);
})
.action(async (object, id, cmd) => {
if (!this.validateObject(object, confirmObjects)) {
return;
}
await this.exitIfLocked();
const command = new ConfirmCommand(this.main.apiService, this.main.cryptoService);
const response = await command.run(object, id, cmd);
this.processResponse(response);
});
}
private importCommand(): program.Command {
return new program.Command("import")
.arguments("[format] [input]")
.description("Import vault data from a file.", {
format: "The format of [input]",
input: "Filepath to data to import",
})
.option("--formats", "List formats")
.option("--organizationid <organizationid>", "ID of the organization to import to.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw import --formats");
writeLn(" bw import bitwardencsv ./from/source.csv");
writeLn(" bw import keepass2xml keepass_backup.xml");
writeLn(
" bw import --organizationid cf14adc3-aca5-4573-890a-f6fa231436d9 keepass2xml keepass_backup.xml"
);
})
.action(async (format, filepath, options) => {
await this.exitIfLocked();
const command = new ImportCommand(this.main.importService, this.main.organizationService);
const response = await command.run(format, filepath, options);
this.processResponse(response);
});
}
private exportCommand(): program.Command {
return new program.Command("export")
.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`, and `encrypted_json`. Default format is `csv`."
);
writeLn("");
writeLn(
" If --raw option is specified and no output filename or directory is given, the"
);
writeLn(" result is written to stdout.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw export");
writeLn(" bw --raw export");
writeLn(" bw export myPassword321");
writeLn(" bw export myPassword321 --format json");
writeLn(" bw export --output ./exp/bw.csv");
writeLn(" bw export myPassword321 --output bw.json --format json");
writeLn(
" bw export myPassword321 --organizationid 7063feab-4b10-472e-b64c-785e2b870b92"
);
writeLn("", true);
})
.action(async (options) => {
await this.exitIfLocked();
const command = new ExportCommand(this.main.exportService, this.main.policyService);
const response = await command.run(options);
this.processResponse(response);
});
}
}