1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 22:03:36 +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

@@ -146,6 +146,7 @@ import {
} from "@bitwarden/common/tools/password-strength"; } from "@bitwarden/common/tools/password-strength";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service";
import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { SendService } from "@bitwarden/common/tools/send/services/send.service";
import { InternalSendService as InternalSendServiceAbstraction } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { InternalSendService as InternalSendServiceAbstraction } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
@@ -276,6 +277,7 @@ export default class MainBackground {
eventUploadService: EventUploadServiceAbstraction; eventUploadService: EventUploadServiceAbstraction;
policyService: InternalPolicyServiceAbstraction; policyService: InternalPolicyServiceAbstraction;
sendService: InternalSendServiceAbstraction; sendService: InternalSendServiceAbstraction;
sendStateProvider: SendStateProvider;
fileUploadService: FileUploadServiceAbstraction; fileUploadService: FileUploadServiceAbstraction;
cipherFileUploadService: CipherFileUploadServiceAbstraction; cipherFileUploadService: CipherFileUploadServiceAbstraction;
organizationService: InternalOrganizationServiceAbstraction; organizationService: InternalOrganizationServiceAbstraction;
@@ -707,11 +709,14 @@ export default class MainBackground {
logoutCallback, logoutCallback,
); );
this.containerService = new ContainerService(this.cryptoService, this.encryptService); this.containerService = new ContainerService(this.cryptoService, this.encryptService);
this.sendStateProvider = new SendStateProvider(this.stateProvider);
this.sendService = new SendService( this.sendService = new SendService(
this.cryptoService, this.cryptoService,
this.i18nService, this.i18nService,
this.keyGenerationService, this.keyGenerationService,
this.stateService, this.sendStateProvider,
this.encryptService,
); );
this.sendApiService = new SendApiService( this.sendApiService = new SendApiService(
this.apiService, this.apiService,

View File

@@ -5,6 +5,10 @@ import {
CryptoServiceInitOptions, CryptoServiceInitOptions,
cryptoServiceFactory, cryptoServiceFactory,
} from "../../platform/background/service-factories/crypto-service.factory"; } from "../../platform/background/service-factories/crypto-service.factory";
import {
EncryptServiceInitOptions,
encryptServiceFactory,
} from "../../platform/background/service-factories/encrypt-service.factory";
import { import {
FactoryOptions, FactoryOptions,
CachedServices, CachedServices,
@@ -18,10 +22,11 @@ import {
KeyGenerationServiceInitOptions, KeyGenerationServiceInitOptions,
keyGenerationServiceFactory, keyGenerationServiceFactory,
} from "../../platform/background/service-factories/key-generation-service.factory"; } from "../../platform/background/service-factories/key-generation-service.factory";
import { import {
stateServiceFactory, SendStateProviderInitOptions,
StateServiceInitOptions, sendStateProviderFactory,
} from "../../platform/background/service-factories/state-service.factory"; } from "./send-state-provider.factory";
type SendServiceFactoryOptions = FactoryOptions; type SendServiceFactoryOptions = FactoryOptions;
@@ -29,7 +34,8 @@ export type SendServiceInitOptions = SendServiceFactoryOptions &
CryptoServiceInitOptions & CryptoServiceInitOptions &
I18nServiceInitOptions & I18nServiceInitOptions &
KeyGenerationServiceInitOptions & KeyGenerationServiceInitOptions &
StateServiceInitOptions; SendStateProviderInitOptions &
EncryptServiceInitOptions;
export function sendServiceFactory( export function sendServiceFactory(
cache: { sendService?: InternalSendService } & CachedServices, cache: { sendService?: InternalSendService } & CachedServices,
@@ -44,7 +50,8 @@ export function sendServiceFactory(
await cryptoServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts), await i18nServiceFactory(cache, opts),
await keyGenerationServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts),
await stateServiceFactory(cache, opts), await sendStateProviderFactory(cache, opts),
await encryptServiceFactory(cache, opts),
), ),
); );
} }

View File

@@ -0,0 +1,28 @@
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
import {
CachedServices,
FactoryOptions,
factory,
} from "../../platform/background/service-factories/factory-options";
import {
StateProviderInitOptions,
stateProviderFactory,
} from "../../platform/background/service-factories/state-provider.factory";
type SendStateProviderFactoryOptions = FactoryOptions;
export type SendStateProviderInitOptions = SendStateProviderFactoryOptions &
StateProviderInitOptions;
export function sendStateProviderFactory(
cache: { sendStateProvider?: SendStateProvider } & CachedServices,
opts: SendStateProviderInitOptions,
): Promise<SendStateProvider> {
return factory(
cache,
"sendStateProvider",
opts,
async () => new SendStateProvider(await stateProviderFactory(cache, opts)),
);
}

View File

@@ -106,6 +106,7 @@ import {
PasswordStrengthServiceAbstraction, PasswordStrengthServiceAbstraction,
} from "@bitwarden/common/tools/password-strength"; } from "@bitwarden/common/tools/password-strength";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service";
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { SendService } from "@bitwarden/common/tools/send/services/send.service";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
@@ -194,6 +195,7 @@ export class Main {
sendProgram: SendProgram; sendProgram: SendProgram;
logService: ConsoleLogService; logService: ConsoleLogService;
sendService: SendService; sendService: SendService;
sendStateProvider: SendStateProvider;
fileUploadService: FileUploadService; fileUploadService: FileUploadService;
cipherFileUploadService: CipherFileUploadService; cipherFileUploadService: CipherFileUploadService;
keyConnectorService: KeyConnectorService; keyConnectorService: KeyConnectorService;
@@ -388,11 +390,14 @@ export class Main {
this.fileUploadService = new FileUploadService(this.logService); this.fileUploadService = new FileUploadService(this.logService);
this.sendStateProvider = new SendStateProvider(this.stateProvider);
this.sendService = new SendService( this.sendService = new SendService(
this.cryptoService, this.cryptoService,
this.i18nService, this.i18nService,
this.keyGenerationService, this.keyGenerationService,
this.stateService, this.sendStateProvider,
this.encryptService,
); );
this.cipherFileUploadService = new CipherFileUploadService( this.cipherFileUploadService = new CipherFileUploadService(

View File

@@ -18,7 +18,7 @@ export class SendRemovePasswordCommand {
try { try {
await this.sendApiService.removePassword(id); await this.sendApiService.removePassword(id);
const updatedSend = await this.sendService.get(id); const updatedSend = await firstValueFrom(this.sendService.get$(id));
const decSend = await updatedSend.decrypt(); const decSend = await updatedSend.decrypt();
const env = await firstValueFrom(this.environmentService.environment$); const env = await firstValueFrom(this.environmentService.environment$);
const webVaultUrl = env.getWebVaultUrl(); const webVaultUrl = env.getWebVaultUrl();

View File

@@ -18,7 +18,6 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
import { SendData } from "@bitwarden/common/tools/send/models/data/send.data";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
import { Account } from "./account"; import { Account } from "./account";
@@ -71,19 +70,6 @@ export class StateService extends BaseStateService<GlobalState, Account> {
return await super.setEncryptedCiphers(value, options); return await super.setEncryptedCiphers(value, options);
} }
async getEncryptedSends(options?: StorageOptions): Promise<{ [id: string]: SendData }> {
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
return await super.getEncryptedSends(options);
}
async setEncryptedSends(
value: { [id: string]: SendData },
options?: StorageOptions,
): Promise<void> {
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
return await super.setEncryptedSends(value, options);
}
override async getLastSync(options?: StorageOptions): Promise<string> { override async getLastSync(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
return await super.getLastSync(options); return await super.getLastSync(options);

View File

@@ -191,6 +191,8 @@ import {
} from "@bitwarden/common/tools/password-strength"; } from "@bitwarden/common/tools/password-strength";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service";
import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendStateProvider as SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
import { SendStateProvider as SendStateProviderAbstraction } from "@bitwarden/common/tools/send/services/send-state.provider.abstraction";
import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { SendService } from "@bitwarden/common/tools/send/services/send.service";
import { import {
InternalSendService, InternalSendService,
@@ -567,9 +569,15 @@ const safeProviders: SafeProvider[] = [
CryptoServiceAbstraction, CryptoServiceAbstraction,
I18nServiceAbstraction, I18nServiceAbstraction,
KeyGenerationServiceAbstraction, KeyGenerationServiceAbstraction,
StateServiceAbstraction, SendStateProviderAbstraction,
EncryptService,
], ],
}), }),
safeProvider({
provide: SendStateProviderAbstraction,
useClass: SendStateProvider,
deps: [StateProvider],
}),
safeProvider({ safeProvider({
provide: SendApiServiceAbstraction, provide: SendApiServiceAbstraction,
useClass: SendApiService, useClass: SendApiService,

View File

@@ -1,5 +1,5 @@
import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Subject, firstValueFrom, takeUntil } from "rxjs"; import { Subject, firstValueFrom, mergeMap, takeUntil } from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@@ -77,9 +77,15 @@ export class SendComponent implements OnInit, OnDestroy {
async load(filter: (send: SendView) => boolean = null) { async load(filter: (send: SendView) => boolean = null) {
this.loading = true; this.loading = true;
this.sendService.sendViews$.pipe(takeUntil(this.destroy$)).subscribe((sends) => { this.sendService.sendViews$
this.sends = sends; .pipe(
}); mergeMap(async (sends) => {
this.sends = sends;
await this.search(null);
}),
takeUntil(this.destroy$),
)
.subscribe();
if (this.onSuccessfulLoad != null) { if (this.onSuccessfulLoad != null) {
await this.onSuccessfulLoad(); await this.onSuccessfulLoad();
} else { } else {

View File

@@ -7,8 +7,6 @@ import { BiometricKey } from "../../auth/types/biometric-key";
import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratorOptions } from "../../tools/generator/generator-options";
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
import { UsernameGeneratorOptions } from "../../tools/generator/username"; import { UsernameGeneratorOptions } from "../../tools/generator/username";
import { SendData } from "../../tools/send/models/data/send.data";
import { SendView } from "../../tools/send/models/view/send.view";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { MasterKey } from "../../types/key"; import { MasterKey } from "../../types/key";
import { CipherData } from "../../vault/models/data/cipher.data"; import { CipherData } from "../../vault/models/data/cipher.data";
@@ -151,14 +149,6 @@ export abstract class StateService<T extends Account = Account> {
* @deprecated For migration purposes only, use setDecryptedUserKeyPin instead * @deprecated For migration purposes only, use setDecryptedUserKeyPin instead
*/ */
setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>; setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this directly, use SendService
*/
getDecryptedSends: (options?: StorageOptions) => Promise<SendView[]>;
/**
* @deprecated Do not call this directly, use SendService
*/
setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>;
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>; getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>; setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
getAdminAuthRequest: (options?: StorageOptions) => Promise<AdminAuthRequestStorable | null>; getAdminAuthRequest: (options?: StorageOptions) => Promise<AdminAuthRequestStorable | null>;
@@ -197,14 +187,6 @@ export abstract class StateService<T extends Account = Account> {
* @deprecated For migration purposes only, use setEncryptedUserKeyPin instead * @deprecated For migration purposes only, use setEncryptedUserKeyPin instead
*/ */
setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>; setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this directly, use SendService
*/
getEncryptedSends: (options?: StorageOptions) => Promise<{ [id: string]: SendData }>;
/**
* @deprecated Do not call this directly, use SendService
*/
setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise<void>;
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>; getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>; setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>;
getForceSetPasswordReason: (options?: StorageOptions) => Promise<ForceSetPasswordReason>; getForceSetPasswordReason: (options?: StorageOptions) => Promise<ForceSetPasswordReason>;

View File

@@ -9,8 +9,6 @@ import {
PasswordGeneratorOptions, PasswordGeneratorOptions,
} from "../../../tools/generator/password"; } from "../../../tools/generator/password";
import { UsernameGeneratorOptions } from "../../../tools/generator/username/username-generation-options"; import { UsernameGeneratorOptions } from "../../../tools/generator/username/username-generation-options";
import { SendData } from "../../../tools/send/models/data/send.data";
import { SendView } from "../../../tools/send/models/view/send.view";
import { DeepJsonify } from "../../../types/deep-jsonify"; import { DeepJsonify } from "../../../types/deep-jsonify";
import { MasterKey } from "../../../types/key"; import { MasterKey } from "../../../types/key";
import { CipherData } from "../../../vault/models/data/cipher.data"; import { CipherData } from "../../../vault/models/data/cipher.data";
@@ -71,7 +69,6 @@ export class AccountData {
CipherView CipherView
>(); >();
localData?: any; localData?: any;
sends?: DataEncryptionPair<SendData, SendView> = new DataEncryptionPair<SendData, SendView>();
passwordGenerationHistory?: EncryptionPair< passwordGenerationHistory?: EncryptionPair<
GeneratedPasswordHistory[], GeneratedPasswordHistory[],
GeneratedPasswordHistory[] GeneratedPasswordHistory[]

View File

@@ -11,8 +11,6 @@ import { BiometricKey } from "../../auth/types/biometric-key";
import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratorOptions } from "../../tools/generator/generator-options";
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
import { UsernameGeneratorOptions } from "../../tools/generator/username"; import { UsernameGeneratorOptions } from "../../tools/generator/username";
import { SendData } from "../../tools/send/models/data/send.data";
import { SendView } from "../../tools/send/models/view/send.view";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { MasterKey } from "../../types/key"; import { MasterKey } from "../../types/key";
import { CipherData } from "../../vault/models/data/cipher.data"; import { CipherData } from "../../vault/models/data/cipher.data";
@@ -614,24 +612,6 @@ export class StateService<
); );
} }
@withPrototypeForArrayMembers(SendView)
async getDecryptedSends(options?: StorageOptions): Promise<SendView[]> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.data?.sends?.decrypted;
}
async setDecryptedSends(value: SendView[], options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
account.data.sends.decrypted = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
}
async getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string> { async getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) { if (options?.userId == null) {
@@ -825,27 +805,6 @@ export class StateService<
); );
} }
@withPrototypeForObjectValues(SendData)
async getEncryptedSends(options?: StorageOptions): Promise<{ [id: string]: SendData }> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()))
)?.data?.sends.encrypted;
}
async setEncryptedSends(
value: { [id: string]: SendData },
options?: StorageOptions,
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()),
);
account.data.sends.encrypted = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()),
);
}
async getEverBeenUnlocked(options?: StorageOptions): Promise<boolean> { async getEverBeenUnlocked(options?: StorageOptions): Promise<boolean> {
return ( return (
(await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))) (await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())))

View File

@@ -102,6 +102,10 @@ export const SM_ONBOARDING_DISK = new StateDefinition("smOnboarding", "disk", {
export const GENERATOR_DISK = new StateDefinition("generator", "disk"); export const GENERATOR_DISK = new StateDefinition("generator", "disk");
export const GENERATOR_MEMORY = new StateDefinition("generator", "memory"); export const GENERATOR_MEMORY = new StateDefinition("generator", "memory");
export const EVENT_COLLECTION_DISK = new StateDefinition("eventCollection", "disk"); export const EVENT_COLLECTION_DISK = new StateDefinition("eventCollection", "disk");
export const SEND_DISK = new StateDefinition("encryptedSend", "disk", {
web: "memory",
});
export const SEND_MEMORY = new StateDefinition("decryptedSend", "memory");
// Vault // Vault

View File

@@ -50,6 +50,7 @@ import { KeyConnectorMigrator } from "./migrations/50-move-key-connector-to-stat
import { RememberedEmailMigrator } from "./migrations/51-move-remembered-email-to-state-providers"; import { RememberedEmailMigrator } from "./migrations/51-move-remembered-email-to-state-providers";
import { DeleteInstalledVersion } from "./migrations/52-delete-installed-version"; import { DeleteInstalledVersion } from "./migrations/52-delete-installed-version";
import { DeviceTrustCryptoServiceStateProviderMigrator } from "./migrations/53-migrate-device-trust-crypto-svc-to-state-providers"; import { DeviceTrustCryptoServiceStateProviderMigrator } from "./migrations/53-migrate-device-trust-crypto-svc-to-state-providers";
import { SendMigrator } from "./migrations/54-move-encrypted-sends";
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
@@ -57,7 +58,8 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
import { MinVersionMigrator } from "./migrations/min-version"; import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 3; export const MIN_VERSION = 3;
export const CURRENT_VERSION = 53; export const CURRENT_VERSION = 54;
export type MinVersion = typeof MIN_VERSION; export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() { export function createMigrationBuilder() {
@@ -112,7 +114,8 @@ export function createMigrationBuilder() {
.with(KeyConnectorMigrator, 49, 50) .with(KeyConnectorMigrator, 49, 50)
.with(RememberedEmailMigrator, 50, 51) .with(RememberedEmailMigrator, 50, 51)
.with(DeleteInstalledVersion, 51, 52) .with(DeleteInstalledVersion, 51, 52)
.with(DeviceTrustCryptoServiceStateProviderMigrator, 52, CURRENT_VERSION); .with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53)
.with(SendMigrator, 53, 54);
} }
export async function currentVersion( export async function currentVersion(

View File

@@ -0,0 +1,236 @@
import { MockProxy, any } from "jest-mock-extended";
import { MigrationHelper } from "../migration-helper";
import { mockMigrationHelper } from "../migration-helper.spec";
import { SendMigrator } from "./54-move-encrypted-sends";
function exampleJSON() {
return {
global: {
otherStuff: "otherStuff1",
},
authenticatedAccounts: ["user-1", "user-2"],
"user-1": {
data: {
sends: {
encrypted: {
"2ebadc23-e101-471b-bf2d-b125015337a0": {
id: "2ebadc23-e101-471b-bf2d-b125015337a0",
accessId: "I9y6LgHhG0e_LbElAVM3oA",
deletionDate: "2024-03-07T20:35:03Z",
disabled: false,
hideEmail: false,
key: "2.sR07sf4f18Rw6YQH9R/fPw==|DlLIYdTlFBktHVEJixqrOZmW/dTDGmZ+9iVftYkRh4s=|2mXH2fKgtItEMi8rcP1ykkVwRbxztw5MGboBwRl/kKM=",
name: "2.A0wIvDbyzuh6AjgFtv2gqQ==|D0FymzfCdYJQcAk5MARfjg==|2g52y7e/33A7Bafaaoy3Yvae7vxbIxoABZdZeoZuyg4=",
text: {
hidden: false,
text: "2.MkcPiJUnNfpcyETsoH3b8g==|/oHZ5g6pmcerXAJidP9sXg==|JDhd1Blsxm/ubp2AAggHZr6gZhyW4UYwZkF5rxlO6X0=",
},
type: 0,
},
"3b31c20d-b783-4912-9170-b12501555398": {
id: "3b31c20d-b783-4912-9170-b12501555398",
accessId: "DcIxO4O3EkmRcLElAVVTmA",
deletionDate: "2024-03-07T20:42:43Z",
disabled: false,
hideEmail: false,
key: "2.366XwLCi7RJnXuAvpsEVNw==|XfLoSsdOIYsHfcSMmv+7VJY97bKfS3fjpbq3ez+KCdk=|iTJxf4Pc3ub6hTFXGeU8NpUV3KxnuxzaHuNoFo/I6Vs=",
name: "2.uJ2FoouFJr/SR9gv3jYY/Q==|ksVre4/YqwY/XOtPyIfIJw==|/LVT842LJgyAchl7NffogXkrmCFwOEHX9NFd0zgLqKo=",
text: {
hidden: false,
text: "2.zBeOzMKtjnP5YI5lJWQTWA==|vxrGt4GKtydhrqaW35b/jw==|36Jtg172awn9YsgfzNs4pJ/OpA59NBnUkLNt6lg7Zw8=",
},
type: 0,
},
},
},
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
},
"user-2": {
data: {
otherStuff: "otherStuff4",
},
otherStuff: "otherStuff5",
},
};
}
function rollbackJSON() {
return {
"user_user-1_send_sends": {
"2ebadc23-e101-471b-bf2d-b125015337a0": {
id: "2ebadc23-e101-471b-bf2d-b125015337a0",
accessId: "I9y6LgHhG0e_LbElAVM3oA",
deletionDate: "2024-03-07T20:35:03Z",
disabled: false,
hideEmail: false,
key: "2.sR07sf4f18Rw6YQH9R/fPw==|DlLIYdTlFBktHVEJixqrOZmW/dTDGmZ+9iVftYkRh4s=|2mXH2fKgtItEMi8rcP1ykkVwRbxztw5MGboBwRl/kKM=",
name: "2.A0wIvDbyzuh6AjgFtv2gqQ==|D0FymzfCdYJQcAk5MARfjg==|2g52y7e/33A7Bafaaoy3Yvae7vxbIxoABZdZeoZuyg4=",
text: {
hidden: false,
text: "2.MkcPiJUnNfpcyETsoH3b8g==|/oHZ5g6pmcerXAJidP9sXg==|JDhd1Blsxm/ubp2AAggHZr6gZhyW4UYwZkF5rxlO6X0=",
},
type: 0,
},
"3b31c20d-b783-4912-9170-b12501555398": {
id: "3b31c20d-b783-4912-9170-b12501555398",
accessId: "DcIxO4O3EkmRcLElAVVTmA",
deletionDate: "2024-03-07T20:42:43Z",
disabled: false,
hideEmail: false,
key: "2.366XwLCi7RJnXuAvpsEVNw==|XfLoSsdOIYsHfcSMmv+7VJY97bKfS3fjpbq3ez+KCdk=|iTJxf4Pc3ub6hTFXGeU8NpUV3KxnuxzaHuNoFo/I6Vs=",
name: "2.uJ2FoouFJr/SR9gv3jYY/Q==|ksVre4/YqwY/XOtPyIfIJw==|/LVT842LJgyAchl7NffogXkrmCFwOEHX9NFd0zgLqKo=",
text: {
hidden: false,
text: "2.zBeOzMKtjnP5YI5lJWQTWA==|vxrGt4GKtydhrqaW35b/jw==|36Jtg172awn9YsgfzNs4pJ/OpA59NBnUkLNt6lg7Zw8=",
},
type: 0,
},
},
"user_user-2_send_data": null as any,
global: {
otherStuff: "otherStuff1",
},
authenticatedAccounts: ["user-1", "user-2"],
"user-1": {
data: {
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
},
"user-2": {
data: {
otherStuff: "otherStuff4",
},
otherStuff: "otherStuff5",
},
};
}
describe("SendMigrator", () => {
let helper: MockProxy<MigrationHelper>;
let sut: SendMigrator;
const keyDefinitionLike = {
stateDefinition: {
name: "send",
},
key: "sends",
};
describe("migrate", () => {
beforeEach(() => {
helper = mockMigrationHelper(exampleJSON(), 53);
sut = new SendMigrator(53, 54);
});
it("should remove encrypted sends from all accounts", async () => {
await sut.migrate(helper);
expect(helper.set).toHaveBeenCalledWith("user-1", {
data: {
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
});
});
it("should set encrypted sends for each account", async () => {
await sut.migrate(helper);
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, {
"2ebadc23-e101-471b-bf2d-b125015337a0": {
id: "2ebadc23-e101-471b-bf2d-b125015337a0",
accessId: "I9y6LgHhG0e_LbElAVM3oA",
deletionDate: "2024-03-07T20:35:03Z",
disabled: false,
hideEmail: false,
key: "2.sR07sf4f18Rw6YQH9R/fPw==|DlLIYdTlFBktHVEJixqrOZmW/dTDGmZ+9iVftYkRh4s=|2mXH2fKgtItEMi8rcP1ykkVwRbxztw5MGboBwRl/kKM=",
name: "2.A0wIvDbyzuh6AjgFtv2gqQ==|D0FymzfCdYJQcAk5MARfjg==|2g52y7e/33A7Bafaaoy3Yvae7vxbIxoABZdZeoZuyg4=",
text: {
hidden: false,
text: "2.MkcPiJUnNfpcyETsoH3b8g==|/oHZ5g6pmcerXAJidP9sXg==|JDhd1Blsxm/ubp2AAggHZr6gZhyW4UYwZkF5rxlO6X0=",
},
type: 0,
},
"3b31c20d-b783-4912-9170-b12501555398": {
id: "3b31c20d-b783-4912-9170-b12501555398",
accessId: "DcIxO4O3EkmRcLElAVVTmA",
deletionDate: "2024-03-07T20:42:43Z",
disabled: false,
hideEmail: false,
key: "2.366XwLCi7RJnXuAvpsEVNw==|XfLoSsdOIYsHfcSMmv+7VJY97bKfS3fjpbq3ez+KCdk=|iTJxf4Pc3ub6hTFXGeU8NpUV3KxnuxzaHuNoFo/I6Vs=",
name: "2.uJ2FoouFJr/SR9gv3jYY/Q==|ksVre4/YqwY/XOtPyIfIJw==|/LVT842LJgyAchl7NffogXkrmCFwOEHX9NFd0zgLqKo=",
text: {
hidden: false,
text: "2.zBeOzMKtjnP5YI5lJWQTWA==|vxrGt4GKtydhrqaW35b/jw==|36Jtg172awn9YsgfzNs4pJ/OpA59NBnUkLNt6lg7Zw8=",
},
type: 0,
},
});
});
});
describe("rollback", () => {
beforeEach(() => {
helper = mockMigrationHelper(rollbackJSON(), 54);
sut = new SendMigrator(53, 54);
});
it.each(["user-1", "user-2"])("should null out new values", async (userId) => {
await sut.rollback(helper);
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
});
it("should add encrypted send values back to accounts", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalled();
expect(helper.set).toHaveBeenCalledWith("user-1", {
data: {
sends: {
encrypted: {
"2ebadc23-e101-471b-bf2d-b125015337a0": {
id: "2ebadc23-e101-471b-bf2d-b125015337a0",
accessId: "I9y6LgHhG0e_LbElAVM3oA",
deletionDate: "2024-03-07T20:35:03Z",
disabled: false,
hideEmail: false,
key: "2.sR07sf4f18Rw6YQH9R/fPw==|DlLIYdTlFBktHVEJixqrOZmW/dTDGmZ+9iVftYkRh4s=|2mXH2fKgtItEMi8rcP1ykkVwRbxztw5MGboBwRl/kKM=",
name: "2.A0wIvDbyzuh6AjgFtv2gqQ==|D0FymzfCdYJQcAk5MARfjg==|2g52y7e/33A7Bafaaoy3Yvae7vxbIxoABZdZeoZuyg4=",
text: {
hidden: false,
text: "2.MkcPiJUnNfpcyETsoH3b8g==|/oHZ5g6pmcerXAJidP9sXg==|JDhd1Blsxm/ubp2AAggHZr6gZhyW4UYwZkF5rxlO6X0=",
},
type: 0,
},
"3b31c20d-b783-4912-9170-b12501555398": {
id: "3b31c20d-b783-4912-9170-b12501555398",
accessId: "DcIxO4O3EkmRcLElAVVTmA",
deletionDate: "2024-03-07T20:42:43Z",
disabled: false,
hideEmail: false,
key: "2.366XwLCi7RJnXuAvpsEVNw==|XfLoSsdOIYsHfcSMmv+7VJY97bKfS3fjpbq3ez+KCdk=|iTJxf4Pc3ub6hTFXGeU8NpUV3KxnuxzaHuNoFo/I6Vs=",
name: "2.uJ2FoouFJr/SR9gv3jYY/Q==|ksVre4/YqwY/XOtPyIfIJw==|/LVT842LJgyAchl7NffogXkrmCFwOEHX9NFd0zgLqKo=",
text: {
hidden: false,
text: "2.zBeOzMKtjnP5YI5lJWQTWA==|vxrGt4GKtydhrqaW35b/jw==|36Jtg172awn9YsgfzNs4pJ/OpA59NBnUkLNt6lg7Zw8=",
},
type: 0,
},
},
},
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
});
});
it("should not try to restore values to missing accounts", async () => {
await sut.rollback(helper);
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
});
});
});

View File

@@ -0,0 +1,67 @@
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
import { Migrator } from "../migrator";
export enum SendType {
Text = 0,
File = 1,
}
type SendData = {
id: string;
accessId: string;
};
type ExpectedSendState = {
data?: {
sends?: {
encrypted?: Record<string, SendData>;
};
};
};
const ENCRYPTED_SENDS: KeyDefinitionLike = {
stateDefinition: {
name: "send",
},
key: "sends",
};
/**
* Only encrypted sends are stored on disk. Only the encrypted items need to be
* migrated from the previous sends state data.
*/
export class SendMigrator extends Migrator<53, 54> {
async migrate(helper: MigrationHelper): Promise<void> {
const accounts = await helper.getAccounts<ExpectedSendState>();
async function migrateAccount(userId: string, account: ExpectedSendState): Promise<void> {
const value = account?.data?.sends?.encrypted;
if (value != null) {
await helper.setToUser(userId, ENCRYPTED_SENDS, value);
delete account.data.sends;
await helper.set(userId, account);
}
}
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
}
async rollback(helper: MigrationHelper): Promise<void> {
const accounts = await helper.getAccounts<ExpectedSendState>();
async function rollbackAccount(userId: string, account: ExpectedSendState): Promise<void> {
const value = await helper.getFromUser(userId, ENCRYPTED_SENDS);
if (account) {
account.data = Object.assign(account.data ?? {}, {
sends: {
encrypted: value,
},
});
await helper.set(userId, account);
}
await helper.setToUser(userId, ENCRYPTED_SENDS, null);
}
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
}
}

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

View File

@@ -1,14 +1,23 @@
import { any, mock, MockProxy } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom } from "rxjs"; 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 { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { I18nService } from "../../../platform/abstractions/i18n.service"; import { I18nService } from "../../../platform/abstractions/i18n.service";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.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 { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { ContainerService } from "../../../platform/services/container.service"; import { ContainerService } from "../../../platform/services/container.service";
import { UserId } from "../../../types/guid";
import { UserKey } from "../../../types/key"; import { UserKey } from "../../../types/key";
import { SendType } from "../enums/send-type"; import { SendType } from "../enums/send-type";
import { SendFileApi } from "../models/api/send-file.api"; 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 { SendFileData } from "../models/data/send-file.data";
import { SendTextData } from "../models/data/send-text.data"; import { SendTextData } from "../models/data/send-text.data";
import { SendData } from "../models/data/send.data"; import { SendData } from "../models/data/send.data";
import { Send } from "../models/domain/send";
import { SendView } from "../models/view/send.view"; 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 { SendService } from "./send.service";
import {
createSendData,
testSend,
testSendData,
testSendViewData,
} from "./test-data/send-tests.data";
describe("SendService", () => { describe("SendService", () => {
const cryptoService = mock<CryptoService>(); const cryptoService = mock<CryptoService>();
@@ -27,56 +43,53 @@ describe("SendService", () => {
const keyGenerationService = mock<KeyGenerationService>(); const keyGenerationService = mock<KeyGenerationService>();
const encryptService = mock<EncryptService>(); const encryptService = mock<EncryptService>();
let sendStateProvider: SendStateProvider;
let sendService: SendService; let sendService: SendService;
let stateService: MockProxy<StateService>; let stateProvider: FakeStateProvider;
let activeAccount: BehaviorSubject<string>; let encryptedState: FakeActiveUserState<Record<string, SendData>>;
let activeAccountUnlocked: BehaviorSubject<boolean>; let decryptedState: FakeActiveUserState<SendView[]>;
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
beforeEach(() => { beforeEach(() => {
activeAccount = new BehaviorSubject("123"); accountService = mockAccountServiceWith(mockUserId);
activeAccountUnlocked = new BehaviorSubject(true); 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); (window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService);
stateService.getEncryptedSends.calledWith(any()).mockResolvedValue({ accountService.activeAccountSubject.next({
"1": sendData("1", "Test Send"), id: mockUserId,
email: "email",
name: "name",
status: AuthenticationStatus.Unlocked,
}); });
stateService.getDecryptedSends // Initial encrypted state
.calledWith(any()) encryptedState = stateProvider.activeUser.getFake(SEND_USER_ENCRYPTED);
.mockResolvedValue([sendView("1", "Test Send")]); encryptedState.nextState({
"1": testSendData("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 decrypted state
decryptedState = stateProvider.activeUser.getFake(SEND_USER_DECRYPTED);
decryptedState.nextState([testSendViewData("1", "Test Send")]);
it("does not exist", async () => { sendService = new SendService(
const result = sendService.get("2"); cryptoService,
i18nService,
expect(result).toBe(undefined); keyGenerationService,
}); sendStateProvider,
encryptService,
);
}); });
describe("get$", () => { describe("get$", () => {
it("exists", async () => { it("exists", async () => {
const result = await firstValueFrom(sendService.get$("1")); 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 () => { it("does not exist", async () => {
@@ -88,14 +101,14 @@ describe("SendService", () => {
it("updated observable", async () => { it("updated observable", async () => {
const singleSendObservable = sendService.get$("1"); const singleSendObservable = sendService.get$("1");
const result = await firstValueFrom(singleSendObservable); const result = await firstValueFrom(singleSendObservable);
expect(result).toEqual(send("1", "Test Send")); expect(result).toEqual(testSend("1", "Test Send"));
await sendService.replace({ await sendService.replace({
"1": sendData("1", "Test Send Updated"), "1": testSendData("1", "Test Send Updated"),
}); });
const result2 = await firstValueFrom(singleSendObservable); 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 () => { it("reports a change when name changes on a new send", async () => {
@@ -103,13 +116,13 @@ describe("SendService", () => {
sendService.get$("1").subscribe(() => { sendService.get$("1").subscribe(() => {
changed = true; 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 //it is immediately called when subscribed, we need to reset the value
changed = false; changed = false;
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
expect(changed).toEqual(true); expect(changed).toEqual(true);
@@ -120,7 +133,7 @@ describe("SendService", () => {
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
let changed = false; let changed = false;
@@ -134,7 +147,7 @@ describe("SendService", () => {
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
expect(changed).toEqual(true); expect(changed).toEqual(true);
@@ -145,7 +158,7 @@ describe("SendService", () => {
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
let changed = false; let changed = false;
@@ -159,7 +172,7 @@ describe("SendService", () => {
sendDataObject.text.text = "new text"; sendDataObject.text.text = "new text";
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
expect(changed).toEqual(true); expect(changed).toEqual(true);
@@ -170,7 +183,7 @@ describe("SendService", () => {
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
let changed = false; let changed = false;
@@ -184,7 +197,7 @@ describe("SendService", () => {
sendDataObject.text = null; sendDataObject.text = null;
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
expect(changed).toEqual(true); expect(changed).toEqual(true);
@@ -197,7 +210,7 @@ describe("SendService", () => {
}) as SendData; }) as SendData;
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "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" })); sendDataObject.file = new SendFileData(new SendFileApi({ FileName: "updated name of file" }));
@@ -211,7 +224,7 @@ describe("SendService", () => {
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
expect(changed).toEqual(false); expect(changed).toEqual(false);
@@ -222,7 +235,7 @@ describe("SendService", () => {
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
let changed = false; let changed = false;
@@ -236,7 +249,7 @@ describe("SendService", () => {
sendDataObject.key = "newKey"; sendDataObject.key = "newKey";
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
expect(changed).toEqual(true); expect(changed).toEqual(true);
@@ -247,7 +260,7 @@ describe("SendService", () => {
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
let changed = false; let changed = false;
@@ -261,7 +274,7 @@ describe("SendService", () => {
sendDataObject.revisionDate = "2025-04-05"; sendDataObject.revisionDate = "2025-04-05";
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
expect(changed).toEqual(true); expect(changed).toEqual(true);
@@ -272,7 +285,7 @@ describe("SendService", () => {
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
let changed = false; let changed = false;
@@ -286,7 +299,7 @@ describe("SendService", () => {
sendDataObject.name = null; sendDataObject.name = null;
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
expect(changed).toEqual(true); expect(changed).toEqual(true);
@@ -299,7 +312,7 @@ describe("SendService", () => {
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
let changed = false; let changed = false;
@@ -312,7 +325,7 @@ describe("SendService", () => {
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
expect(changed).toEqual(false); expect(changed).toEqual(false);
@@ -320,7 +333,7 @@ describe("SendService", () => {
sendDataObject.text.text = "Asdf"; sendDataObject.text.text = "Asdf";
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
expect(changed).toEqual(true); expect(changed).toEqual(true);
@@ -332,14 +345,14 @@ describe("SendService", () => {
changed = true; 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 //it is immediately called when subscribed, we need to reset the value
changed = false; changed = false;
await sendService.replace({ await sendService.replace({
"1": sendDataObject, "1": sendDataObject,
"2": sendData("3", "Test Send 3"), "2": testSendData("3", "Test Send 3"),
}); });
expect(changed).toEqual(false); expect(changed).toEqual(false);
@@ -354,7 +367,7 @@ describe("SendService", () => {
changed = false; changed = false;
await sendService.replace({ await sendService.replace({
"2": sendData("2", "Test Send 2"), "2": testSendData("2", "Test Send 2"),
}); });
expect(changed).toEqual(true); expect(changed).toEqual(true);
@@ -366,14 +379,14 @@ describe("SendService", () => {
const send1 = sends[0]; const send1 = sends[0];
expect(sends).toHaveLength(1); expect(sends).toHaveLength(1);
expect(send1).toEqual(send("1", "Test Send")); expect(send1).toEqual(testSend("1", "Test Send"));
}); });
describe("getFromState", () => { describe("getFromState", () => {
it("exists", async () => { it("exists", async () => {
const result = await sendService.getFromState("1"); 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 () => { it("does not exist", async () => {
const result = await sendService.getFromState("2"); const result = await sendService.getFromState("2");
@@ -383,17 +396,17 @@ describe("SendService", () => {
}); });
it("getAllDecryptedFromState", async () => { 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", () => { describe("getRotatedKeys", () => {
let encryptedKey: EncString; let encryptedKey: EncString;
beforeEach(() => { beforeEach(() => {
cryptoService.decryptToBytes.mockResolvedValue(new Uint8Array(32)); encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32));
encryptedKey = new EncString("Re-encrypted Send Key"); encryptedKey = new EncString("Re-encrypted Send Key");
cryptoService.encrypt.mockResolvedValue(encryptedKey); encryptService.encrypt.mockResolvedValue(encryptedKey);
}); });
it("returns re-encrypted user sends", async () => { it("returns re-encrypted user sends", async () => {
@@ -408,6 +421,8 @@ describe("SendService", () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
sendService.replace(null); sendService.replace(null);
await awaitAsync();
const newUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey; const newUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey;
const result = await sendService.getRotatedKeys(newUserKey); const result = await sendService.getRotatedKeys(newUserKey);
@@ -424,114 +439,51 @@ describe("SendService", () => {
// InternalSendService // InternalSendService
it("upsert", async () => { it("upsert", async () => {
await sendService.upsert(sendData("2", "Test 2")); await sendService.upsert(testSendData("2", "Test 2"));
expect(await firstValueFrom(sendService.sends$)).toEqual([ expect(await firstValueFrom(sendService.sends$)).toEqual([
send("1", "Test Send"), testSend("1", "Test Send"),
send("2", "Test 2"), testSend("2", "Test 2"),
]); ]);
}); });
it("replace", async () => { 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 () => { it("clear", async () => {
await sendService.clear(); await sendService.clear();
await awaitAsync();
expect(await firstValueFrom(sendService.sends$)).toEqual([]); 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", () => { const sendsAfterDelete = await firstValueFrom(sendService.sends$);
it("exists", async () => { expect(sendsAfterDelete.length).toBeLessThan(sendsBeforeDelete.length);
await sendService.delete("1");
expect(stateService.getEncryptedSends).toHaveBeenCalledTimes(2);
expect(stateService.setEncryptedSends).toHaveBeenCalledTimes(1);
}); });
it("does not exist", async () => { it("Intended send should be delete", async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. const sendsBeforeDelete = await firstValueFrom(sendService.sends$);
// eslint-disable-next-line @typescript-eslint/no-floating-promises await sendService.delete(sendsBeforeDelete[0].id);
sendService.delete("1"); 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 { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { I18nService } from "../../../platform/abstractions/i18n.service"; import { I18nService } from "../../../platform/abstractions/i18n.service";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { KdfType } from "../../../platform/enums"; import { KdfType } from "../../../platform/enums";
import { Utils } from "../../../platform/misc/utils"; import { Utils } from "../../../platform/misc/utils";
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; 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 { SendView } from "../models/view/send.view";
import { SEND_KDF_ITERATIONS } from "../send-kdf"; import { SEND_KDF_ITERATIONS } from "../send-kdf";
import { SendStateProvider } from "./send-state.provider.abstraction";
import { InternalSendService as InternalSendServiceAbstraction } from "./send.service.abstraction"; import { InternalSendService as InternalSendServiceAbstraction } from "./send.service.abstraction";
export class SendService implements InternalSendServiceAbstraction { export class SendService implements InternalSendServiceAbstraction {
readonly sendKeySalt = "bitwarden-send"; readonly sendKeySalt = "bitwarden-send";
readonly sendKeyPurpose = "send"; readonly sendKeyPurpose = "send";
protected _sends: BehaviorSubject<Send[]> = new BehaviorSubject([]); sends$ = this.stateProvider.encryptedState$.pipe(
protected _sendViews: BehaviorSubject<SendView[]> = new BehaviorSubject([]); map((record) => Object.values(record || {}).map((data) => new Send(data))),
);
sends$ = this._sends.asObservable(); sendViews$ = this.stateProvider.encryptedState$.pipe(
sendViews$ = this._sendViews.asObservable(); concatMap((record) =>
this.decryptSends(Object.values(record || {}).map((data) => new Send(data))),
),
);
constructor( constructor(
private cryptoService: CryptoService, private cryptoService: CryptoService,
private i18nService: I18nService, private i18nService: I18nService,
private keyGenerationService: KeyGenerationService, private keyGenerationService: KeyGenerationService,
private stateService: StateService, private stateProvider: SendStateProvider,
) { private encryptService: EncryptService,
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([]);
}
async encrypt( async encrypt(
model: SendView, model: SendView,
@@ -93,12 +74,15 @@ export class SendService implements InternalSendServiceAbstraction {
); );
send.password = passwordKey.keyB64; send.password = passwordKey.keyB64;
} }
send.key = await this.cryptoService.encrypt(model.key, key); if (key == null) {
send.name = await this.cryptoService.encrypt(model.name, model.cryptoKey); key = await this.cryptoService.getUserKey();
send.notes = await this.cryptoService.encrypt(model.notes, model.cryptoKey); }
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) { if (send.type === SendType.Text) {
send.text = new SendText(); 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; send.text.hidden = model.text.hidden;
} else if (send.type === SendType.File) { } else if (send.type === SendType.File) {
send.file = new SendFile(); send.file = new SendFile();
@@ -120,11 +104,6 @@ export class SendService implements InternalSendServiceAbstraction {
return [send, fileData]; 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> { get$(id: string): Observable<Send | undefined> {
return this.sends$.pipe( return this.sends$.pipe(
distinctUntilChanged((oldSends, newSends) => { distinctUntilChanged((oldSends, newSends) => {
@@ -188,7 +167,7 @@ export class SendService implements InternalSendServiceAbstraction {
} }
async getFromState(id: string): Promise<Send> { async getFromState(id: string): Promise<Send> {
const sends = await this.stateService.getEncryptedSends(); const sends = await this.stateProvider.getEncryptedSends();
// eslint-disable-next-line // eslint-disable-next-line
if (sends == null || !sends.hasOwnProperty(id)) { if (sends == null || !sends.hasOwnProperty(id)) {
return null; return null;
@@ -198,7 +177,7 @@ export class SendService implements InternalSendServiceAbstraction {
} }
async getAll(): Promise<Send[]> { async getAll(): Promise<Send[]> {
const sends = await this.stateService.getEncryptedSends(); const sends = await this.stateProvider.getEncryptedSends();
const response: Send[] = []; const response: Send[] = [];
for (const id in sends) { for (const id in sends) {
// eslint-disable-next-line // eslint-disable-next-line
@@ -210,7 +189,7 @@ export class SendService implements InternalSendServiceAbstraction {
} }
async getAllDecryptedFromState(): Promise<SendView[]> { async getAllDecryptedFromState(): Promise<SendView[]> {
let decSends = await this.stateService.getDecryptedSends(); let decSends = await this.stateProvider.getDecryptedSends();
if (decSends != null) { if (decSends != null) {
return decSends; return decSends;
} }
@@ -230,12 +209,12 @@ export class SendService implements InternalSendServiceAbstraction {
await Promise.all(promises); await Promise.all(promises);
decSends.sort(Utils.getSortFunction(this.i18nService, "name")); decSends.sort(Utils.getSortFunction(this.i18nService, "name"));
await this.stateService.setDecryptedSends(decSends); await this.stateProvider.setDecryptedSends(decSends);
return decSends; return decSends;
} }
async upsert(send: SendData | SendData[]): Promise<any> { async upsert(send: SendData | SendData[]): Promise<any> {
let sends = await this.stateService.getEncryptedSends(); let sends = await this.stateProvider.getEncryptedSends();
if (sends == null) { if (sends == null) {
sends = {}; sends = {};
} }
@@ -252,16 +231,12 @@ export class SendService implements InternalSendServiceAbstraction {
} }
async clear(userId?: string): Promise<any> { async clear(userId?: string): Promise<any> {
if (userId == null || userId == (await this.stateService.getUserId())) { await this.stateProvider.setDecryptedSends(null);
this._sends.next([]); await this.stateProvider.setEncryptedSends(null);
this._sendViews.next([]);
}
await this.stateService.setDecryptedSends(null, { userId: userId });
await this.stateService.setEncryptedSends(null, { userId: userId });
} }
async delete(id: string | string[]): Promise<any> { async delete(id: string | string[]): Promise<any> {
const sends = await this.stateService.getEncryptedSends(); const sends = await this.stateProvider.getEncryptedSends();
if (sends == null) { if (sends == null) {
return; return;
} }
@@ -281,8 +256,7 @@ export class SendService implements InternalSendServiceAbstraction {
} }
async replace(sends: { [id: string]: SendData }): Promise<any> { async replace(sends: { [id: string]: SendData }): Promise<any> {
await this.updateObservables(sends); await this.stateProvider.setEncryptedSends(sends);
await this.stateService.setEncryptedSends(sends);
} }
async getRotatedKeys(newUserKey: UserKey): Promise<SendWithIdRequest[]> { 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."); 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( const requests = await Promise.all(
this._sends.value.map(async (send) => { sends.map(async (send) => {
const sendKey = await this.cryptoService.decryptToBytes(send.key); const sendKey = await this.encryptService.decryptToBytes(send.key, newUserKey);
send.key = await this.cryptoService.encrypt(sendKey, newUserKey); send.key = await this.encryptService.encrypt(sendKey, newUserKey);
return new SendWithIdRequest(send); return new SendWithIdRequest(send);
}), }),
); );
// separate return for easier debugging
return requests; return requests;
} }
@@ -329,18 +310,12 @@ export class SendService implements InternalSendServiceAbstraction {
data: ArrayBuffer, data: ArrayBuffer,
key: SymmetricCryptoKey, key: SymmetricCryptoKey,
): Promise<[EncString, EncArrayBuffer]> { ): Promise<[EncString, EncArrayBuffer]> {
const encFileName = await this.cryptoService.encrypt(fileName, key); if (key == null) {
const encFileData = await this.cryptoService.encryptToBytes(new Uint8Array(data), key); key = await this.cryptoService.getUserKey();
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));
} }
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[]) { 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;
}

View File

@@ -244,7 +244,7 @@ export class SyncService implements SyncServiceAbstraction {
this.syncStarted(); this.syncStarted();
if (await this.stateService.getIsAuthenticated()) { if (await this.stateService.getIsAuthenticated()) {
try { try {
const localSend = this.sendService.get(notification.id); const localSend = await firstValueFrom(this.sendService.get$(notification.id));
if ( if (
(!isEdit && localSend == null) || (!isEdit && localSend == null) ||
(isEdit && localSend != null && localSend.revisionDate < notification.revisionDate) (isEdit && localSend != null && localSend.revisionDate < notification.revisionDate)