mirror of
https://github.com/bitwarden/cli
synced 2025-12-20 10:13:13 +00:00
Add send to cli (#222)
* Add list all sends and filter by search term * Add get send templates * Add AccessUrl to send responses * Add Send to Get command * Add missing command options to login These options are already coded to work in the command, but commander did not know about the options. * Upgrade Commander to 7.0.0 This is needed to enable the subcommand chaining required by Send. This commit also adds get send and send receive functionality. get send will be moved to send get along with send list and any other send commands. * Use api url for send access url * Move send commands to send subcommands * Use webvault access url everywhere Production instances all have api url located at `baseUrl/api`. Receive command will parse the webvault url and alter it to an api url. * Move create and receive commands to send directory * Separate program concerns program holds authentication/general program concerns vault.program holds commands related to the vault send.program holds commands related to Bitwarden Send * Fix up imports and lint items * Add edit command * Use browser-hrtime * Add send examples to help text * Clean up receive help text * correct help text * Add delete command * Code review Cleanup * Scheme on send receive help text * PR review items Move buffer to array buffer to jslib delete with server some formatting fixes * Add remove password command This is the simplest way to enable removing passwords without resorting to weird type parsing of piped in Send JSONs in edit * Default hidden to false like web * Do not allow password updates that aren't strings or are empty * Delete appveyor.yml.flagged-for-delete * Correctly order imports and include tslint rule * fix npm globbing problem https://stackoverflow.com/a/34594501 globs work differently in package.json. Encasing the globs in single quotes expands them in shell rather than in npm * Remove double slash in path * Trigger github rebuild
This commit is contained in:
147
src/commands/send/receive.command.ts
Normal file
147
src/commands/send/receive.command.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import * as program from 'commander';
|
||||
import * as inquirer from 'inquirer';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import { SendAccessRequest } from 'jslib/models/request/sendAccessRequest';
|
||||
import { ErrorResponse } from 'jslib/models/response/errorResponse';
|
||||
import { SendAccessView } from 'jslib/models/view/sendAccessView';
|
||||
|
||||
import { Response } from 'jslib/cli/models/response';
|
||||
|
||||
import { SendAccess } from 'jslib/models/domain/sendAccess';
|
||||
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
|
||||
|
||||
import { SendType } from 'jslib/enums/sendType';
|
||||
|
||||
import { NodeUtils } from 'jslib/misc/nodeUtils';
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
|
||||
import { SendAccessResponse } from '../../models/response/sendAccessResponse';
|
||||
|
||||
import { DownloadCommand } from '../download.command';
|
||||
|
||||
export class SendReceiveCommand extends DownloadCommand {
|
||||
private canInteract: boolean;
|
||||
private decKey: SymmetricCryptoKey;
|
||||
|
||||
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);
|
||||
const request = 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 !== '') {
|
||||
request.password = await this.getUnlockedPassword(password, keyArray);
|
||||
}
|
||||
|
||||
const response = await this.sendRequest(request, 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:
|
||||
return await this.saveAttachmentToFile(response?.file?.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.split('/').slice(2);
|
||||
return [result[0], result[1]];
|
||||
}
|
||||
|
||||
private getApiUrl(url: URL) {
|
||||
if (url.origin === this.apiService.apiBaseUrl) {
|
||||
return url.origin;
|
||||
} else if (this.platformUtilsService.isDev() && url.origin === this.environmentService.getWebVaultUrl()) {
|
||||
return this.apiService.apiBaseUrl;
|
||||
} 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(request: SendAccessRequest, url: string, id: string, key: ArrayBuffer): Promise<Response | SendAccessView> {
|
||||
try {
|
||||
const sendResponse = await this.apiService.postSendAccess(id, request, 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
|
||||
request.password = await this.getUnlockedPassword(answer.password, key);
|
||||
return await this.sendRequest(request, 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();
|
||||
} else {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user