1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +00:00

[PM-5574] sends state provider (#8373)

* Adding the key definitions and tests and initial send state service

* Adding the abstraction and implementing

* Planning comments

* Everything but fixing the send tests

* Moving send tests over to the state provider

* jslib needed name refactor

* removing get/set encrypted sends from web vault state service

* browser send state service factory

* Fixing conflicts

* Removing send service from services module and fixing send service observable

* Commenting the migrator to be clear on why only encrypted

* No need for service factories in browser

* browser send service is no longer needed

* Key def test cases to use toStrictEqual

* Running prettier

* Creating send test data to avoid code duplication

* Adding state provider and account service to send in cli

* Fixing the send service test cases

* Fixing state definition keys

* Moving to observables and implementing encryption service

* Fixing key def tests

* The cli was using the deprecated get method

* The observables init doesn't need to happen in constructor

* Missed commented out code

* If enc key is null get user key

* Service factory fix
This commit is contained in:
Tom
2024-04-02 12:39:06 -04:00
committed by GitHub
parent 9956f020e7
commit a6e178f1e6
25 changed files with 767 additions and 327 deletions

View File

@@ -0,0 +1,21 @@
import { SEND_USER_ENCRYPTED, SEND_USER_DECRYPTED } from "./key-definitions";
import { testSendData, testSendViewData } from "./test-data/send-tests.data";
describe("Key definitions", () => {
describe("SEND_USER_ENCRYPTED", () => {
it("should pass through deserialization", () => {
const result = SEND_USER_ENCRYPTED.deserializer(
JSON.parse(JSON.stringify(testSendData("1", "Test Send Data"))),
);
expect(result).toEqual(testSendData("1", "Test Send Data"));
});
});
describe("SEND_USER_DECRYPTED", () => {
it("should pass through deserialization", () => {
const sendViews = [testSendViewData("1", "Test Send View")];
const result = SEND_USER_DECRYPTED.deserializer(JSON.parse(JSON.stringify(sendViews)));
expect(result).toEqual(sendViews);
});
});
});

View File

@@ -0,0 +1,13 @@
import { KeyDefinition, SEND_DISK, SEND_MEMORY } from "../../../platform/state";
import { SendData } from "../models/data/send.data";
import { SendView } from "../models/view/send.view";
/** Encrypted send state stored on disk */
export const SEND_USER_ENCRYPTED = KeyDefinition.record<SendData>(SEND_DISK, "sendUserEncrypted", {
deserializer: (obj: SendData) => obj,
});
/** Decrypted send state stored in memory */
export const SEND_USER_DECRYPTED = new KeyDefinition<SendView[]>(SEND_MEMORY, "sendUserDecrypted", {
deserializer: (obj) => obj,
});

View File

@@ -0,0 +1,17 @@
import { Observable } from "rxjs";
import { SendData } from "../models/data/send.data";
import { SendView } from "../models/view/send.view";
export abstract class SendStateProvider {
encryptedState$: Observable<Record<string, SendData>>;
decryptedState$: Observable<SendView[]>;
getEncryptedSends: () => Promise<{ [id: string]: SendData }>;
setEncryptedSends: (value: { [id: string]: SendData }) => Promise<void>;
getDecryptedSends: () => Promise<SendView[]>;
setDecryptedSends: (value: SendView[]) => Promise<void>;
}

View File

@@ -0,0 +1,48 @@
import {
FakeAccountService,
FakeStateProvider,
awaitAsync,
mockAccountServiceWith,
} from "../../../../spec";
import { Utils } from "../../../platform/misc/utils";
import { UserId } from "../../../types/guid";
import { SendStateProvider } from "./send-state.provider";
import { testSendData, testSendViewData } from "./test-data/send-tests.data";
describe("Send State Provider", () => {
let stateProvider: FakeStateProvider;
let accountService: FakeAccountService;
let sendStateProvider: SendStateProvider;
const mockUserId = Utils.newGuid() as UserId;
beforeEach(() => {
accountService = mockAccountServiceWith(mockUserId);
stateProvider = new FakeStateProvider(accountService);
sendStateProvider = new SendStateProvider(stateProvider);
});
describe("Encrypted Sends", () => {
it("should return SendData", async () => {
const sendData = { "1": testSendData("1", "Test Send Data") };
await sendStateProvider.setEncryptedSends(sendData);
await awaitAsync();
const actual = await sendStateProvider.getEncryptedSends();
expect(actual).toStrictEqual(sendData);
});
});
describe("Decrypted Sends", () => {
it("should return SendView", async () => {
const state = [testSendViewData("1", "Test")];
await sendStateProvider.setDecryptedSends(state);
await awaitAsync();
const actual = await sendStateProvider.getDecryptedSends();
expect(actual).toStrictEqual(state);
});
});
});

View File

@@ -0,0 +1,47 @@
import { Observable, firstValueFrom } from "rxjs";
import { ActiveUserState, StateProvider } from "../../../platform/state";
import { SendData } from "../models/data/send.data";
import { SendView } from "../models/view/send.view";
import { SEND_USER_DECRYPTED, SEND_USER_ENCRYPTED } from "./key-definitions";
import { SendStateProvider as SendStateProviderAbstraction } from "./send-state.provider.abstraction";
/** State provider for sends */
export class SendStateProvider implements SendStateProviderAbstraction {
/** Observable for the encrypted sends for an active user */
encryptedState$: Observable<Record<string, SendData>>;
/** Observable with the decrypted sends for an active user */
decryptedState$: Observable<SendView[]>;
private activeUserEncryptedState: ActiveUserState<Record<string, SendData>>;
private activeUserDecryptedState: ActiveUserState<SendView[]>;
constructor(protected stateProvider: StateProvider) {
this.activeUserEncryptedState = this.stateProvider.getActive(SEND_USER_ENCRYPTED);
this.encryptedState$ = this.activeUserEncryptedState.state$;
this.activeUserDecryptedState = this.stateProvider.getActive(SEND_USER_DECRYPTED);
this.decryptedState$ = this.activeUserDecryptedState.state$;
}
/** Gets the encrypted sends from state for an active user */
async getEncryptedSends(): Promise<{ [id: string]: SendData }> {
return await firstValueFrom(this.encryptedState$);
}
/** Sets the encrypted send state for an active user */
async setEncryptedSends(value: { [id: string]: SendData }): Promise<void> {
await this.activeUserEncryptedState.update(() => value);
}
/** Gets the decrypted sends from state for the active user */
async getDecryptedSends(): Promise<SendView[]> {
return await firstValueFrom(this.decryptedState$);
}
/** Sets the decrypted send state for an active user */
async setDecryptedSends(value: SendView[]): Promise<void> {
await this.activeUserDecryptedState.update(() => value);
}
}

View File

@@ -18,10 +18,6 @@ export abstract class SendService {
password: string,
key?: SymmetricCryptoKey,
) => Promise<[Send, EncArrayBuffer]>;
/**
* @deprecated Do not call this, use the get$ method
*/
get: (id: string) => Send;
/**
* Provides a send for a determined id
* updates after a change occurs to the send that matches the id
@@ -53,6 +49,5 @@ export abstract class SendService {
export abstract class InternalSendService extends SendService {
upsert: (send: SendData | SendData[]) => Promise<any>;
replace: (sends: { [id: string]: SendData }) => Promise<void>;
clear: (userId: string) => Promise<any>;
delete: (id: string | string[]) => Promise<any>;
}

View File

@@ -1,14 +1,23 @@
import { any, mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { mock } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import {
FakeAccountService,
FakeActiveUserState,
FakeStateProvider,
awaitAsync,
mockAccountServiceWith,
} from "../../../../spec";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { Utils } from "../../../platform/misc/utils";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { ContainerService } from "../../../platform/services/container.service";
import { UserId } from "../../../types/guid";
import { UserKey } from "../../../types/key";
import { SendType } from "../enums/send-type";
import { SendFileApi } from "../models/api/send-file.api";
@@ -16,10 +25,17 @@ import { SendTextApi } from "../models/api/send-text.api";
import { SendFileData } from "../models/data/send-file.data";
import { SendTextData } from "../models/data/send-text.data";
import { SendData } from "../models/data/send.data";
import { Send } from "../models/domain/send";
import { SendView } from "../models/view/send.view";
import { SEND_USER_DECRYPTED, SEND_USER_ENCRYPTED } from "./key-definitions";
import { SendStateProvider } from "./send-state.provider";
import { SendService } from "./send.service";
import {
createSendData,
testSend,
testSendData,
testSendViewData,
} from "./test-data/send-tests.data";
describe("SendService", () => {
const cryptoService = mock<CryptoService>();
@@ -27,56 +43,53 @@ describe("SendService", () => {
const keyGenerationService = mock<KeyGenerationService>();
const encryptService = mock<EncryptService>();
let sendStateProvider: SendStateProvider;
let sendService: SendService;
let stateService: MockProxy<StateService>;
let activeAccount: BehaviorSubject<string>;
let activeAccountUnlocked: BehaviorSubject<boolean>;
let stateProvider: FakeStateProvider;
let encryptedState: FakeActiveUserState<Record<string, SendData>>;
let decryptedState: FakeActiveUserState<SendView[]>;
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
beforeEach(() => {
activeAccount = new BehaviorSubject("123");
activeAccountUnlocked = new BehaviorSubject(true);
accountService = mockAccountServiceWith(mockUserId);
stateProvider = new FakeStateProvider(accountService);
sendStateProvider = new SendStateProvider(stateProvider);
stateService = mock<StateService>();
stateService.activeAccount$ = activeAccount;
stateService.activeAccountUnlocked$ = activeAccountUnlocked;
(window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService);
stateService.getEncryptedSends.calledWith(any()).mockResolvedValue({
"1": sendData("1", "Test Send"),
accountService.activeAccountSubject.next({
id: mockUserId,
email: "email",
name: "name",
status: AuthenticationStatus.Unlocked,
});
stateService.getDecryptedSends
.calledWith(any())
.mockResolvedValue([sendView("1", "Test Send")]);
sendService = new SendService(cryptoService, i18nService, keyGenerationService, stateService);
});
afterEach(() => {
activeAccount.complete();
activeAccountUnlocked.complete();
});
describe("get", () => {
it("exists", async () => {
const result = sendService.get("1");
expect(result).toEqual(send("1", "Test Send"));
// Initial encrypted state
encryptedState = stateProvider.activeUser.getFake(SEND_USER_ENCRYPTED);
encryptedState.nextState({
"1": testSendData("1", "Test Send"),
});
// Initial decrypted state
decryptedState = stateProvider.activeUser.getFake(SEND_USER_DECRYPTED);
decryptedState.nextState([testSendViewData("1", "Test Send")]);
it("does not exist", async () => {
const result = sendService.get("2");
expect(result).toBe(undefined);
});
sendService = new SendService(
cryptoService,
i18nService,
keyGenerationService,
sendStateProvider,
encryptService,
);
});
describe("get$", () => {
it("exists", async () => {
const result = await firstValueFrom(sendService.get$("1"));
expect(result).toEqual(send("1", "Test Send"));
expect(result).toEqual(testSend("1", "Test Send"));
});
it("does not exist", async () => {
@@ -88,14 +101,14 @@ describe("SendService", () => {
it("updated observable", async () => {
const singleSendObservable = sendService.get$("1");
const result = await firstValueFrom(singleSendObservable);
expect(result).toEqual(send("1", "Test Send"));
expect(result).toEqual(testSend("1", "Test Send"));
await sendService.replace({
"1": sendData("1", "Test Send Updated"),
"1": testSendData("1", "Test Send Updated"),
});
const result2 = await firstValueFrom(singleSendObservable);
expect(result2).toEqual(send("1", "Test Send Updated"));
expect(result2).toEqual(testSend("1", "Test Send Updated"));
});
it("reports a change when name changes on a new send", async () => {
@@ -103,13 +116,13 @@ describe("SendService", () => {
sendService.get$("1").subscribe(() => {
changed = true;
});
const sendDataObject = sendData("1", "Test Send 2");
const sendDataObject = testSendData("1", "Test Send 2");
//it is immediately called when subscribed, we need to reset the value
changed = false;
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
expect(changed).toEqual(true);
@@ -120,7 +133,7 @@ describe("SendService", () => {
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
let changed = false;
@@ -134,7 +147,7 @@ describe("SendService", () => {
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
expect(changed).toEqual(true);
@@ -145,7 +158,7 @@ describe("SendService", () => {
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
let changed = false;
@@ -159,7 +172,7 @@ describe("SendService", () => {
sendDataObject.text.text = "new text";
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
expect(changed).toEqual(true);
@@ -170,7 +183,7 @@ describe("SendService", () => {
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
let changed = false;
@@ -184,7 +197,7 @@ describe("SendService", () => {
sendDataObject.text = null;
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
expect(changed).toEqual(true);
@@ -197,7 +210,7 @@ describe("SendService", () => {
}) as SendData;
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
sendDataObject.file = new SendFileData(new SendFileApi({ FileName: "updated name of file" }));
@@ -211,7 +224,7 @@ describe("SendService", () => {
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
expect(changed).toEqual(false);
@@ -222,7 +235,7 @@ describe("SendService", () => {
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
let changed = false;
@@ -236,7 +249,7 @@ describe("SendService", () => {
sendDataObject.key = "newKey";
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
expect(changed).toEqual(true);
@@ -247,7 +260,7 @@ describe("SendService", () => {
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
let changed = false;
@@ -261,7 +274,7 @@ describe("SendService", () => {
sendDataObject.revisionDate = "2025-04-05";
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
expect(changed).toEqual(true);
@@ -272,7 +285,7 @@ describe("SendService", () => {
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
let changed = false;
@@ -286,7 +299,7 @@ describe("SendService", () => {
sendDataObject.name = null;
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
expect(changed).toEqual(true);
@@ -299,7 +312,7 @@ describe("SendService", () => {
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
let changed = false;
@@ -312,7 +325,7 @@ describe("SendService", () => {
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
expect(changed).toEqual(false);
@@ -320,7 +333,7 @@ describe("SendService", () => {
sendDataObject.text.text = "Asdf";
await sendService.replace({
"1": sendDataObject,
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
expect(changed).toEqual(true);
@@ -332,14 +345,14 @@ describe("SendService", () => {
changed = true;
});
const sendDataObject = sendData("1", "Test Send");
const sendDataObject = testSendData("1", "Test Send");
//it is immediately called when subscribed, we need to reset the value
changed = false;
await sendService.replace({
"1": sendDataObject,
"2": sendData("3", "Test Send 3"),
"2": testSendData("3", "Test Send 3"),
});
expect(changed).toEqual(false);
@@ -354,7 +367,7 @@ describe("SendService", () => {
changed = false;
await sendService.replace({
"2": sendData("2", "Test Send 2"),
"2": testSendData("2", "Test Send 2"),
});
expect(changed).toEqual(true);
@@ -366,14 +379,14 @@ describe("SendService", () => {
const send1 = sends[0];
expect(sends).toHaveLength(1);
expect(send1).toEqual(send("1", "Test Send"));
expect(send1).toEqual(testSend("1", "Test Send"));
});
describe("getFromState", () => {
it("exists", async () => {
const result = await sendService.getFromState("1");
expect(result).toEqual(send("1", "Test Send"));
expect(result).toEqual(testSend("1", "Test Send"));
});
it("does not exist", async () => {
const result = await sendService.getFromState("2");
@@ -383,17 +396,17 @@ describe("SendService", () => {
});
it("getAllDecryptedFromState", async () => {
await sendService.getAllDecryptedFromState();
const sends = await sendService.getAllDecryptedFromState();
expect(stateService.getDecryptedSends).toHaveBeenCalledTimes(1);
expect(sends[0]).toMatchObject(testSendViewData("1", "Test Send"));
});
describe("getRotatedKeys", () => {
let encryptedKey: EncString;
beforeEach(() => {
cryptoService.decryptToBytes.mockResolvedValue(new Uint8Array(32));
encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32));
encryptedKey = new EncString("Re-encrypted Send Key");
cryptoService.encrypt.mockResolvedValue(encryptedKey);
encryptService.encrypt.mockResolvedValue(encryptedKey);
});
it("returns re-encrypted user sends", async () => {
@@ -408,6 +421,8 @@ describe("SendService", () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sendService.replace(null);
await awaitAsync();
const newUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey;
const result = await sendService.getRotatedKeys(newUserKey);
@@ -424,114 +439,51 @@ describe("SendService", () => {
// InternalSendService
it("upsert", async () => {
await sendService.upsert(sendData("2", "Test 2"));
await sendService.upsert(testSendData("2", "Test 2"));
expect(await firstValueFrom(sendService.sends$)).toEqual([
send("1", "Test Send"),
send("2", "Test 2"),
testSend("1", "Test Send"),
testSend("2", "Test 2"),
]);
});
it("replace", async () => {
await sendService.replace({ "2": sendData("2", "test 2") });
await sendService.replace({ "2": testSendData("2", "test 2") });
expect(await firstValueFrom(sendService.sends$)).toEqual([send("2", "test 2")]);
expect(await firstValueFrom(sendService.sends$)).toEqual([testSend("2", "test 2")]);
});
it("clear", async () => {
await sendService.clear();
await awaitAsync();
expect(await firstValueFrom(sendService.sends$)).toEqual([]);
});
describe("Delete", () => {
it("Sends count should decrease after delete", async () => {
const sendsBeforeDelete = await firstValueFrom(sendService.sends$);
await sendService.delete(sendsBeforeDelete[0].id);
describe("delete", () => {
it("exists", async () => {
await sendService.delete("1");
expect(stateService.getEncryptedSends).toHaveBeenCalledTimes(2);
expect(stateService.setEncryptedSends).toHaveBeenCalledTimes(1);
const sendsAfterDelete = await firstValueFrom(sendService.sends$);
expect(sendsAfterDelete.length).toBeLessThan(sendsBeforeDelete.length);
});
it("does not exist", async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sendService.delete("1");
it("Intended send should be delete", async () => {
const sendsBeforeDelete = await firstValueFrom(sendService.sends$);
await sendService.delete(sendsBeforeDelete[0].id);
const sendsAfterDelete = await firstValueFrom(sendService.sends$);
expect(sendsAfterDelete[0]).not.toBe(sendsBeforeDelete[0]);
});
expect(stateService.getEncryptedSends).toHaveBeenCalledTimes(2);
it("Deleting on an empty sends array should not throw", async () => {
sendStateProvider.getEncryptedSends = jest.fn().mockResolvedValue(null);
await expect(sendService.delete("2")).resolves.not.toThrow();
});
it("Delete multiple sends", async () => {
await sendService.upsert(testSendData("2", "send data 2"));
await sendService.delete(["1", "2"]);
const sendsAfterDelete = await firstValueFrom(sendService.sends$);
expect(sendsAfterDelete.length).toBe(0);
});
});
// Send object helper functions
function sendData(id: string, name: string) {
const data = new SendData({} as any);
data.id = id;
data.name = name;
data.disabled = false;
data.accessCount = 2;
data.accessId = "1";
data.revisionDate = null;
data.expirationDate = null;
data.deletionDate = null;
data.notes = "Notes!!";
data.key = null;
return data;
}
const defaultSendData: Partial<SendData> = {
id: "1",
name: "Test Send",
accessId: "123",
type: SendType.Text,
notes: "notes!",
file: null,
text: new SendTextData(new SendTextApi({ Text: "send text" })),
key: "key",
maxAccessCount: 12,
accessCount: 2,
revisionDate: "2024-09-04",
expirationDate: "2024-09-04",
deletionDate: "2024-09-04",
password: "password",
disabled: false,
hideEmail: false,
};
function createSendData(value: Partial<SendData> = {}) {
const testSend: any = {};
for (const prop in defaultSendData) {
testSend[prop] = value[prop as keyof SendData] ?? defaultSendData[prop as keyof SendData];
}
return testSend;
}
function sendView(id: string, name: string) {
const data = new SendView({} as any);
data.id = id;
data.name = name;
data.disabled = false;
data.accessCount = 2;
data.accessId = "1";
data.revisionDate = null;
data.expirationDate = null;
data.deletionDate = null;
data.notes = "Notes!!";
data.key = null;
return data;
}
function send(id: string, name: string) {
const data = new Send({} as any);
data.id = id;
data.name = new EncString(name);
data.disabled = false;
data.accessCount = 2;
data.accessId = "1";
data.revisionDate = null;
data.expirationDate = null;
data.deletionDate = null;
data.notes = new EncString("Notes!!");
data.key = null;
return data;
}
});

View File

@@ -1,9 +1,9 @@
import { BehaviorSubject, Observable, concatMap, distinctUntilChanged, map } from "rxjs";
import { Observable, concatMap, distinctUntilChanged, firstValueFrom, map } from "rxjs";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { KdfType } from "../../../platform/enums";
import { Utils } from "../../../platform/misc/utils";
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
@@ -19,48 +19,29 @@ import { SendWithIdRequest } from "../models/request/send-with-id.request";
import { SendView } from "../models/view/send.view";
import { SEND_KDF_ITERATIONS } from "../send-kdf";
import { SendStateProvider } from "./send-state.provider.abstraction";
import { InternalSendService as InternalSendServiceAbstraction } from "./send.service.abstraction";
export class SendService implements InternalSendServiceAbstraction {
readonly sendKeySalt = "bitwarden-send";
readonly sendKeyPurpose = "send";
protected _sends: BehaviorSubject<Send[]> = new BehaviorSubject([]);
protected _sendViews: BehaviorSubject<SendView[]> = new BehaviorSubject([]);
sends$ = this._sends.asObservable();
sendViews$ = this._sendViews.asObservable();
sends$ = this.stateProvider.encryptedState$.pipe(
map((record) => Object.values(record || {}).map((data) => new Send(data))),
);
sendViews$ = this.stateProvider.encryptedState$.pipe(
concatMap((record) =>
this.decryptSends(Object.values(record || {}).map((data) => new Send(data))),
),
);
constructor(
private cryptoService: CryptoService,
private i18nService: I18nService,
private keyGenerationService: KeyGenerationService,
private stateService: StateService,
) {
this.stateService.activeAccountUnlocked$
.pipe(
concatMap(async (unlocked) => {
if (Utils.global.bitwardenContainerService == null) {
return;
}
if (!unlocked) {
this._sends.next([]);
this._sendViews.next([]);
return;
}
const data = await this.stateService.getEncryptedSends();
await this.updateObservables(data);
}),
)
.subscribe();
}
async clearCache(): Promise<void> {
await this._sendViews.next([]);
}
private stateProvider: SendStateProvider,
private encryptService: EncryptService,
) {}
async encrypt(
model: SendView,
@@ -93,12 +74,15 @@ export class SendService implements InternalSendServiceAbstraction {
);
send.password = passwordKey.keyB64;
}
send.key = await this.cryptoService.encrypt(model.key, key);
send.name = await this.cryptoService.encrypt(model.name, model.cryptoKey);
send.notes = await this.cryptoService.encrypt(model.notes, model.cryptoKey);
if (key == null) {
key = await this.cryptoService.getUserKey();
}
send.key = await this.encryptService.encrypt(model.key, key);
send.name = await this.encryptService.encrypt(model.name, model.cryptoKey);
send.notes = await this.encryptService.encrypt(model.notes, model.cryptoKey);
if (send.type === SendType.Text) {
send.text = new SendText();
send.text.text = await this.cryptoService.encrypt(model.text.text, model.cryptoKey);
send.text.text = await this.encryptService.encrypt(model.text.text, model.cryptoKey);
send.text.hidden = model.text.hidden;
} else if (send.type === SendType.File) {
send.file = new SendFile();
@@ -120,11 +104,6 @@ export class SendService implements InternalSendServiceAbstraction {
return [send, fileData];
}
get(id: string): Send {
const sends = this._sends.getValue();
return sends.find((send) => send.id === id);
}
get$(id: string): Observable<Send | undefined> {
return this.sends$.pipe(
distinctUntilChanged((oldSends, newSends) => {
@@ -188,7 +167,7 @@ export class SendService implements InternalSendServiceAbstraction {
}
async getFromState(id: string): Promise<Send> {
const sends = await this.stateService.getEncryptedSends();
const sends = await this.stateProvider.getEncryptedSends();
// eslint-disable-next-line
if (sends == null || !sends.hasOwnProperty(id)) {
return null;
@@ -198,7 +177,7 @@ export class SendService implements InternalSendServiceAbstraction {
}
async getAll(): Promise<Send[]> {
const sends = await this.stateService.getEncryptedSends();
const sends = await this.stateProvider.getEncryptedSends();
const response: Send[] = [];
for (const id in sends) {
// eslint-disable-next-line
@@ -210,7 +189,7 @@ export class SendService implements InternalSendServiceAbstraction {
}
async getAllDecryptedFromState(): Promise<SendView[]> {
let decSends = await this.stateService.getDecryptedSends();
let decSends = await this.stateProvider.getDecryptedSends();
if (decSends != null) {
return decSends;
}
@@ -230,12 +209,12 @@ export class SendService implements InternalSendServiceAbstraction {
await Promise.all(promises);
decSends.sort(Utils.getSortFunction(this.i18nService, "name"));
await this.stateService.setDecryptedSends(decSends);
await this.stateProvider.setDecryptedSends(decSends);
return decSends;
}
async upsert(send: SendData | SendData[]): Promise<any> {
let sends = await this.stateService.getEncryptedSends();
let sends = await this.stateProvider.getEncryptedSends();
if (sends == null) {
sends = {};
}
@@ -252,16 +231,12 @@ export class SendService implements InternalSendServiceAbstraction {
}
async clear(userId?: string): Promise<any> {
if (userId == null || userId == (await this.stateService.getUserId())) {
this._sends.next([]);
this._sendViews.next([]);
}
await this.stateService.setDecryptedSends(null, { userId: userId });
await this.stateService.setEncryptedSends(null, { userId: userId });
await this.stateProvider.setDecryptedSends(null);
await this.stateProvider.setEncryptedSends(null);
}
async delete(id: string | string[]): Promise<any> {
const sends = await this.stateService.getEncryptedSends();
const sends = await this.stateProvider.getEncryptedSends();
if (sends == null) {
return;
}
@@ -281,8 +256,7 @@ export class SendService implements InternalSendServiceAbstraction {
}
async replace(sends: { [id: string]: SendData }): Promise<any> {
await this.updateObservables(sends);
await this.stateService.setEncryptedSends(sends);
await this.stateProvider.setEncryptedSends(sends);
}
async getRotatedKeys(newUserKey: UserKey): Promise<SendWithIdRequest[]> {
@@ -290,14 +264,21 @@ export class SendService implements InternalSendServiceAbstraction {
throw new Error("New user key is required for rotation.");
}
const req = await firstValueFrom(
this.sends$.pipe(concatMap(async (sends) => this.toRotatedKeyRequestMap(sends, newUserKey))),
);
// separate return for easier debugging
return req;
}
private async toRotatedKeyRequestMap(sends: Send[], newUserKey: UserKey) {
const requests = await Promise.all(
this._sends.value.map(async (send) => {
const sendKey = await this.cryptoService.decryptToBytes(send.key);
send.key = await this.cryptoService.encrypt(sendKey, newUserKey);
sends.map(async (send) => {
const sendKey = await this.encryptService.decryptToBytes(send.key, newUserKey);
send.key = await this.encryptService.encrypt(sendKey, newUserKey);
return new SendWithIdRequest(send);
}),
);
// separate return for easier debugging
return requests;
}
@@ -329,18 +310,12 @@ export class SendService implements InternalSendServiceAbstraction {
data: ArrayBuffer,
key: SymmetricCryptoKey,
): Promise<[EncString, EncArrayBuffer]> {
const encFileName = await this.cryptoService.encrypt(fileName, key);
const encFileData = await this.cryptoService.encryptToBytes(new Uint8Array(data), key);
return [encFileName, encFileData];
}
private async updateObservables(sendsMap: { [id: string]: SendData }) {
const sends = Object.values(sendsMap || {}).map((f) => new Send(f));
this._sends.next(sends);
if (await this.cryptoService.hasUserKey()) {
this._sendViews.next(await this.decryptSends(sends));
if (key == null) {
key = await this.cryptoService.getUserKey();
}
const encFileName = await this.encryptService.encrypt(fileName, key);
const encFileData = await this.encryptService.encryptToBytes(new Uint8Array(data), key);
return [encFileName, encFileData];
}
private async decryptSends(sends: Send[]) {

View File

@@ -0,0 +1,79 @@
import { EncString } from "../../../../platform/models/domain/enc-string";
import { SendType } from "../../enums/send-type";
import { SendTextApi } from "../../models/api/send-text.api";
import { SendTextData } from "../../models/data/send-text.data";
import { SendData } from "../../models/data/send.data";
import { Send } from "../../models/domain/send";
import { SendView } from "../../models/view/send.view";
export function testSendViewData(id: string, name: string) {
const data = new SendView({} as any);
data.id = id;
data.name = name;
data.disabled = false;
data.accessCount = 2;
data.accessId = "1";
data.revisionDate = null;
data.expirationDate = null;
data.deletionDate = null;
data.notes = "Notes!!";
data.key = null;
return data;
}
export function createSendData(value: Partial<SendData> = {}) {
const defaultSendData: Partial<SendData> = {
id: "1",
name: "Test Send",
accessId: "123",
type: SendType.Text,
notes: "notes!",
file: null,
text: new SendTextData(new SendTextApi({ Text: "send text" })),
key: "key",
maxAccessCount: 12,
accessCount: 2,
revisionDate: "2024-09-04",
expirationDate: "2024-09-04",
deletionDate: "2024-09-04",
password: "password",
disabled: false,
hideEmail: false,
};
const testSend: any = {};
for (const prop in defaultSendData) {
testSend[prop] = value[prop as keyof SendData] ?? defaultSendData[prop as keyof SendData];
}
return testSend;
}
export function testSendData(id: string, name: string) {
const data = new SendData({} as any);
data.id = id;
data.name = name;
data.disabled = false;
data.accessCount = 2;
data.accessId = "1";
data.revisionDate = null;
data.expirationDate = null;
data.deletionDate = null;
data.notes = "Notes!!";
data.key = null;
return data;
}
export function testSend(id: string, name: string) {
const data = new Send({} as any);
data.id = id;
data.name = new EncString(name);
data.disabled = false;
data.accessCount = 2;
data.accessId = "1";
data.revisionDate = null;
data.expirationDate = null;
data.deletionDate = null;
data.notes = new EncString("Notes!!");
data.key = null;
return data;
}