1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-21 20:04:02 +00:00

PM-919 rewrite to implement receive

This commit is contained in:
voommen-livefront
2025-03-18 10:02:05 -05:00
parent e737731270
commit 46c024857d
7 changed files with 166 additions and 43 deletions

View File

@@ -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";
}
}
}

View File

@@ -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<string, any>): Promise<Response> {

View File

@@ -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,

View File

@@ -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<SendView | SendView[]> {
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<Response | SendAccessView> {
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);
}
}
}

View File

@@ -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<Response> {
@@ -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,

View File

@@ -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) => {

View File

@@ -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);