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:
380
apps/cli/src/bw.ts
Normal file
380
apps/cli/src/bw.ts
Normal 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();
|
||||
119
apps/cli/src/commands/completion.command.ts
Normal file
119
apps/cli/src/commands/completion.command.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
53
apps/cli/src/commands/config.command.ts
Normal file
53
apps/cli/src/commands/config.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
62
apps/cli/src/commands/confirm.command.ts
Normal file
62
apps/cli/src/commands/confirm.command.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
84
apps/cli/src/commands/convertToKeyConnector.command.ts
Normal file
84
apps/cli/src/commands/convertToKeyConnector.command.ts
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
206
apps/cli/src/commands/create.command.ts
Normal file
206
apps/cli/src/commands/create.command.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
131
apps/cli/src/commands/delete.command.ts
Normal file
131
apps/cli/src/commands/delete.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
43
apps/cli/src/commands/download.command.ts
Normal file
43
apps/cli/src/commands/download.command.ts
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
186
apps/cli/src/commands/edit.command.ts
Normal file
186
apps/cli/src/commands/edit.command.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
16
apps/cli/src/commands/encode.command.ts
Normal file
16
apps/cli/src/commands/encode.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
97
apps/cli/src/commands/export.command.ts
Normal file
97
apps/cli/src/commands/export.command.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
80
apps/cli/src/commands/generate.command.ts
Normal file
80
apps/cli/src/commands/generate.command.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
540
apps/cli/src/commands/get.command.ts
Normal file
540
apps/cli/src/commands/get.command.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
127
apps/cli/src/commands/import.command.ts
Normal file
127
apps/cli/src/commands/import.command.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
252
apps/cli/src/commands/list.command.ts
Normal file
252
apps/cli/src/commands/list.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
14
apps/cli/src/commands/lock.command.ts
Normal file
14
apps/cli/src/commands/lock.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
99
apps/cli/src/commands/login.command.ts
Normal file
99
apps/cli/src/commands/login.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
36
apps/cli/src/commands/restore.command.ts
Normal file
36
apps/cli/src/commands/restore.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
149
apps/cli/src/commands/send/create.command.ts
Normal file
149
apps/cli/src/commands/send/create.command.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
21
apps/cli/src/commands/send/delete.command.ts
Normal file
21
apps/cli/src/commands/send/delete.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
90
apps/cli/src/commands/send/edit.command.ts
Normal file
90
apps/cli/src/commands/send/edit.command.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
83
apps/cli/src/commands/send/get.command.ts
Normal file
83
apps/cli/src/commands/send/get.command.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
apps/cli/src/commands/send/list.command.ts
Normal file
36
apps/cli/src/commands/send/list.command.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
170
apps/cli/src/commands/send/receive.command.ts
Normal file
170
apps/cli/src/commands/send/receive.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
apps/cli/src/commands/send/removePassword.command.ts
Normal file
21
apps/cli/src/commands/send/removePassword.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
394
apps/cli/src/commands/serve.command.ts
Normal file
394
apps/cli/src/commands/serve.command.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
59
apps/cli/src/commands/share.command.ts
Normal file
59
apps/cli/src/commands/share.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
54
apps/cli/src/commands/status.command.ts
Normal file
54
apps/cli/src/commands/status.command.ts
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
41
apps/cli/src/commands/sync.command.ts
Normal file
41
apps/cli/src/commands/sync.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
132
apps/cli/src/commands/unlock.command.ts
Normal file
132
apps/cli/src/commands/unlock.command.ts
Normal 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
5
apps/cli/src/flags.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type Flags = {
|
||||
serve?: boolean;
|
||||
};
|
||||
|
||||
export type FlagName = keyof Flags;
|
||||
44
apps/cli/src/locales/en/messages.json
Normal file
44
apps/cli/src/locales/en/messages.json
Normal 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."
|
||||
}
|
||||
}
|
||||
16
apps/cli/src/models/request/organizationCollectionRequest.ts
Normal file
16
apps/cli/src/models/request/organizationCollectionRequest.ts
Normal 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[];
|
||||
}
|
||||
17
apps/cli/src/models/response/attachmentResponse.ts
Normal file
17
apps/cli/src/models/response/attachmentResponse.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
33
apps/cli/src/models/response/cipherResponse.ts
Normal file
33
apps/cli/src/models/response/cipherResponse.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
apps/cli/src/models/response/collectionResponse.ts
Normal file
13
apps/cli/src/models/response/collectionResponse.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
13
apps/cli/src/models/response/folderResponse.ts
Normal file
13
apps/cli/src/models/response/folderResponse.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
11
apps/cli/src/models/response/loginResponse.ts
Normal file
11
apps/cli/src/models/response/loginResponse.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
22
apps/cli/src/models/response/organizationResponse.ts
Normal file
22
apps/cli/src/models/response/organizationResponse.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
17
apps/cli/src/models/response/organizationUserResponse.ts
Normal file
17
apps/cli/src/models/response/organizationUserResponse.ts
Normal 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";
|
||||
}
|
||||
}
|
||||
11
apps/cli/src/models/response/passwordHistoryResponse.ts
Normal file
11
apps/cli/src/models/response/passwordHistoryResponse.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
40
apps/cli/src/models/response/sendAccessResponse.ts
Normal file
40
apps/cli/src/models/response/sendAccessResponse.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
apps/cli/src/models/response/sendFileResponse.ts
Normal file
36
apps/cli/src/models/response/sendFileResponse.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
123
apps/cli/src/models/response/sendResponse.ts
Normal file
123
apps/cli/src/models/response/sendResponse.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
apps/cli/src/models/response/sendTextResponse.ts
Normal file
30
apps/cli/src/models/response/sendTextResponse.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
11
apps/cli/src/models/response/templateResponse.ts
Normal file
11
apps/cli/src/models/response/templateResponse.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
15
apps/cli/src/models/selectionReadOnly.ts
Normal file
15
apps/cli/src/models/selectionReadOnly.ts
Normal 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
546
apps/cli/src/program.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
334
apps/cli/src/send.program.ts
Normal file
334
apps/cli/src/send.program.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
20
apps/cli/src/services/i18n.service.ts
Normal file
20
apps/cli/src/services/i18n.service.ts
Normal 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"];
|
||||
}
|
||||
}
|
||||
40
apps/cli/src/services/lowdbStorage.service.ts
Normal file
40
apps/cli/src/services/lowdbStorage.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
103
apps/cli/src/services/nodeEnvSecureStorage.service.ts
Normal file
103
apps/cli/src/services/nodeEnvSecureStorage.service.ts
Normal 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
270
apps/cli/src/utils.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
486
apps/cli/src/vault.program.ts
Normal file
486
apps/cli/src/vault.program.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user