diff --git a/apps/cli/src/commands/download.command.ts b/apps/cli/src/commands/download.command.ts index 01ef675d2a8..92c5130ccb0 100644 --- a/apps/cli/src/commands/download.command.ts +++ b/apps/cli/src/commands/download.command.ts @@ -1,10 +1,15 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { firstValueFrom } from "rxjs"; + import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; + import { Response } from "../models/response"; import { FileResponse } from "../models/response/file.response"; import { CliUtils } from "../utils"; @@ -20,6 +25,8 @@ export abstract class DownloadCommand { constructor( protected encryptService: EncryptService, protected apiService: ApiService, + protected environmentService: EnvironmentService, + protected platformUtilsService: PlatformUtilsService, ) {} /** @@ -62,4 +69,23 @@ export abstract class DownloadCommand { } } } + + protected getIdAndKey(url: URL): [string, string] { + const result = url.hash.slice(1).split("/").slice(-2); + return [result[0], result[1]]; + } + + protected async getApiUrl(url: URL) { + const env = await firstValueFrom(this.environmentService.environment$); + const urls = env.getUrls(); + if (url.origin === "https://send.bitwarden.com") { + return "https://api.bitwarden.com"; + } 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"; + } + } } diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index 92c3a8baeaf..e74a85474d5 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -24,6 +24,8 @@ import { LoginUriExport } from "@bitwarden/common/models/export/login-uri.export import { LoginExport } from "@bitwarden/common/models/export/login.export"; import { SecureNoteExport } from "@bitwarden/common/models/export/secure-note.export"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; @@ -66,8 +68,10 @@ export class GetCommand extends DownloadCommand { private eventCollectionService: EventCollectionService, private accountProfileService: BillingAccountProfileStateService, private accountService: AccountService, + protected environmentService: EnvironmentService, + protected platformUtilsService: PlatformUtilsService, ) { - super(encryptService, apiService); + super(encryptService, apiService, environmentService, platformUtilsService); } async run(object: string, id: string, cmdOptions: Record): Promise { diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index 2834c71cf6d..0ae0f42f06d 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -66,6 +66,8 @@ export class OssServeConfigurator { this.serviceContainer.eventCollectionService, this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.accountService, + this.serviceContainer.environmentService, + this.serviceContainer.platformUtilsService, ); this.listCommand = new ListCommand( this.serviceContainer.cipherService, diff --git a/apps/cli/src/tools/send/commands/get.command.ts b/apps/cli/src/tools/send/commands/get.command.ts index b8254aec48a..ced0c541d6f 100644 --- a/apps/cli/src/tools/send/commands/get.command.ts +++ b/apps/cli/src/tools/send/commands/get.command.ts @@ -1,39 +1,48 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { OptionValues } from "commander"; +import * as inquirer from "inquirer"; import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { SendAccess } from "@bitwarden/common/tools/send/models/domain/send-access"; +import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request"; +import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { KeyService } from "@bitwarden/key-management"; +import { NodeUtils } from "@bitwarden/node/node-utils"; import { DownloadCommand } from "../../../commands/download.command"; import { Response } from "../../../models/response"; +import { SendAccessResponse } from "../models/send-access.response"; import { SendResponse } from "../models/send.response"; -import { SendReceiveCommand } from "./receive.command"; - export class SendGetCommand extends DownloadCommand { + private decKey: SymmetricCryptoKey; + constructor( private sendService: SendService, - private environmentService: EnvironmentService, + protected environmentService: EnvironmentService, private searchService: SearchService, encryptService: EncryptService, apiService: ApiService, - private platformUtilsService: PlatformUtilsService, + protected platformUtilsService: PlatformUtilsService, private keyService: KeyService, private cryptoFunctionService: CryptoFunctionService, private sendApiService: SendApiService, ) { - super(encryptService, apiService); + super(encryptService, apiService, environmentService, platformUtilsService); } async run(id: string, options: OptionValues) { @@ -79,20 +88,82 @@ export class SendGetCommand extends DownloadCommand { if (options?.file || options?.output || options?.raw) { const sendWithUrl = new SendResponse(sends, webVaultUrl); - const receiveCommand = new SendReceiveCommand( - this.keyService, - this.encryptService, - this.cryptoFunctionService, - this.platformUtilsService, - this.environmentService, - this.sendApiService, - this.apiService, - ); - return await receiveCommand.run(sendWithUrl.accessUrl, options); + + const urlObject = this.createUrlObject(sendWithUrl.accessUrl); + if (urlObject == null) { + return Response.badRequest("Failed to parse the provided Send url"); + } + + const apiUrl = await 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 password = + options.password || + (options.passwordfile && (await NodeUtils.readFirstLine(options.passwordfile))) || + (options.passwordenv && process.env[options.passwordenv]) || + ""; + + const sendAccessRequest = new SendAccessRequest(); + if (password !== "") { + sendAccessRequest.password = await this.getUnlockedPassword(password, keyArray); + } + + const response = await this.sendRequest(apiUrl, id, keyArray, sendAccessRequest); + 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.sendApiService.getSendFileDownloadData( + response, + sendAccessRequest, + apiUrl, + ); + return await this.saveAttachmentToFile( + downloadData.url, + this.decKey, + response?.file?.fileName, + options.output, + ); + } + default: + return Response.success(new SendAccessResponse(response)); + } } return selector(sends); } + private createUrlObject(url: string): URL | null { + try { + return new URL(url); + } catch { + return null; + } + } + + private async getUnlockedPassword(password: string, keyArray: Uint8Array) { + const passwordHash = await this.cryptoFunctionService.pbkdf2( + password, + keyArray, + "sha256", + 100000, + ); + return Utils.fromBufferToB64(passwordHash); + } private async getSendView(id: string): Promise { if (Utils.isGuid(id)) { @@ -110,4 +181,40 @@ export class SendGetCommand extends DownloadCommand { } } } + + private async sendRequest( + url: string, + id: string, + key: Uint8Array, + sendAccessRequest: SendAccessRequest, + ): Promise { + try { + const sendResponse = await this.sendApiService.postSendAccess(id, sendAccessRequest, url); + + const sendAccess = new SendAccess(sendResponse); + this.decKey = await this.keyService.makeSendKey(key); + return await sendAccess.decrypt(this.decKey); + } catch (e) { + if (e instanceof ErrorResponse) { + if (e.statusCode === 401) { + const answer: inquirer.Answers = await inquirer.createPromptModule({ + output: process.stderr, + })({ + type: "password", + name: "password", + message: "Send password:", + }); + + // reattempt with new password + sendAccessRequest.password = await this.getUnlockedPassword(answer.password, key); + return await this.sendRequest(url, id, key, sendAccessRequest); + } else if (e.statusCode === 405) { + return Response.badRequest("Bad Request"); + } else if (e.statusCode === 404) { + return Response.notFound(); + } + } + return Response.error(e); + } + } } diff --git a/apps/cli/src/tools/send/commands/receive.command.ts b/apps/cli/src/tools/send/commands/receive.command.ts index 879d03f6dae..9968dce6f65 100644 --- a/apps/cli/src/tools/send/commands/receive.command.ts +++ b/apps/cli/src/tools/send/commands/receive.command.ts @@ -2,7 +2,6 @@ // @ts-strict-ignore import { OptionValues } from "commander"; import * as inquirer from "inquirer"; -import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; @@ -33,12 +32,12 @@ export class SendReceiveCommand extends DownloadCommand { private keyService: KeyService, encryptService: EncryptService, private cryptoFunctionService: CryptoFunctionService, - private platformUtilsService: PlatformUtilsService, - private environmentService: EnvironmentService, + protected platformUtilsService: PlatformUtilsService, + protected environmentService: EnvironmentService, private sendApiService: SendApiService, apiService: ApiService, ) { - super(encryptService, apiService); + super(encryptService, apiService, environmentService, platformUtilsService); } async run(url: string, options: OptionValues): Promise { @@ -110,25 +109,6 @@ export class SendReceiveCommand extends DownloadCommand { } } - private getIdAndKey(url: URL): [string, string] { - const result = url.hash.slice(1).split("/").slice(-2); - return [result[0], result[1]]; - } - - private async getApiUrl(url: URL) { - const env = await firstValueFrom(this.environmentService.environment$); - const urls = env.getUrls(); - if (url.origin === "https://send.bitwarden.com") { - return "https://api.bitwarden.com"; - } 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: Uint8Array) { const passwordHash = await this.cryptoFunctionService.pbkdf2( password, diff --git a/apps/cli/src/tools/send/send.program.ts b/apps/cli/src/tools/send/send.program.ts index f32fc246928..a9e92933877 100644 --- a/apps/cli/src/tools/send/send.program.ts +++ b/apps/cli/src/tools/send/send.program.ts @@ -153,6 +153,8 @@ export class SendProgram extends BaseProgram { this.serviceContainer.eventCollectionService, this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.accountService, + this.serviceContainer.environmentService, + this.serviceContainer.platformUtilsService, ); const response = await cmd.run("template", object, null); this.processResponse(response); @@ -187,10 +189,10 @@ export class SendProgram extends BaseProgram { writeLn(""); writeLn(" bw send get searchText"); writeLn(" bw send get id"); - writeLn(" bw send get searchText --text"); - writeLn(" bw send get searchText --file"); - writeLn(" bw send get searchText --file --output ../Photos/photo.jpg"); - writeLn(" bw send get searchText --file --raw"); + writeLn(" bw send get id --text"); + writeLn(" bw send get id --file"); + writeLn(" bw send get id --file --output ../Photos/photo.jpg"); + writeLn(" bw send get id --file --raw"); writeLn("", true); }) .action(async (id: string, options: OptionValues) => { diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index d6ef27b1428..25b89f8222e 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -188,6 +188,8 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.eventCollectionService, this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.accountService, + this.serviceContainer.environmentService, + this.serviceContainer.platformUtilsService, ); const response = await command.run(object, id, cmd); this.processResponse(response);