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; 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 { 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) { if (url.origin === 'https://send.bitwarden.com') { return 'https://vault.bitwarden.com/api'; } else 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(url: string, id: string, key: ArrayBuffer): Promise { 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); } } }