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:
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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[]
|
||||||
|
|||||||
@@ -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())))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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))]);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
libs/common/src/tools/send/services/key-definitions.spec.ts
Normal file
21
libs/common/src/tools/send/services/key-definitions.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
13
libs/common/src/tools/send/services/key-definitions.ts
Normal file
13
libs/common/src/tools/send/services/key-definitions.ts
Normal 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,
|
||||||
|
});
|
||||||
@@ -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>;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
47
libs/common/src/tools/send/services/send-state.provider.ts
Normal file
47
libs/common/src/tools/send/services/send-state.provider.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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[]) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user