mirror of
https://github.com/bitwarden/browser
synced 2025-12-27 05:33:59 +00:00
[PM-10607] Require userId for getKeyForCipherKeyDecryption (#10509)
* updated cipher service to stop using the deprecated getUserKeyWithLegacySupport and use the version that requires a user id * Added account service mock * fixed cipher test * Fixed test * removed async from encryptCipher * updated encryptSharedCipher to pass userId to the encrypt function * Pass userId to getUserKeyWithLegacySupport on encryptSharedCipher * pass in userid when setting masterKeyEncryptedUserKey * Added activer usedId to new web refresh function
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -36,6 +38,7 @@ export class CollectionsComponent implements OnInit {
|
||||
protected organizationService: OrganizationService,
|
||||
private logService: LogService,
|
||||
private configService: ConfigService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -48,8 +51,11 @@ export class CollectionsComponent implements OnInit {
|
||||
async load() {
|
||||
this.cipherDomain = await this.loadCipher();
|
||||
this.collectionIds = this.loadCipherCollections();
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain),
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
|
||||
);
|
||||
this.collections = await this.loadCollections();
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -36,6 +37,7 @@ export class ShareComponent implements OnInit, OnDestroy {
|
||||
protected cipherService: CipherService,
|
||||
private logService: LogService,
|
||||
protected organizationService: OrganizationService,
|
||||
protected accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -67,8 +69,11 @@ export class ShareComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
const cipherDomain = await this.cipherService.get(this.cipherId);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
this.cipher = await cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain),
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,8 +100,11 @@ export class ShareComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
const cipherDomain = await this.cipherService.get(this.cipherId);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const cipherView = await cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain),
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
|
||||
);
|
||||
const orgs = await firstValueFrom(this.organizations$);
|
||||
const orgName =
|
||||
@@ -104,7 +112,7 @@ export class ShareComponent implements OnInit, OnDestroy {
|
||||
|
||||
try {
|
||||
this.formPromise = this.cipherService
|
||||
.shareWithServer(cipherView, this.organizationId, selectedCollectionIds)
|
||||
.shareWithServer(cipherView, this.organizationId, selectedCollectionIds, activeUserId)
|
||||
.then(async () => {
|
||||
this.onSharedCipher.emit();
|
||||
this.platformUtilsService.showToast(
|
||||
|
||||
@@ -775,6 +775,7 @@ const safeProviders: SafeProvider[] = [
|
||||
CollectionServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
PinServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -800,6 +801,7 @@ const safeProviders: SafeProvider[] = [
|
||||
CryptoFunctionServiceAbstraction,
|
||||
CollectionServiceAbstraction,
|
||||
KdfConfigServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -22,6 +22,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
@@ -250,8 +251,11 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
if (this.cipher == null) {
|
||||
if (this.editMode) {
|
||||
const cipher = await this.loadCipher();
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
this.cipher = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher),
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
|
||||
// Adjust Cipher Name if Cloning
|
||||
@@ -371,7 +375,10 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
this.cipher.id = null;
|
||||
}
|
||||
|
||||
const cipher = await this.encryptCipher();
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const cipher = await this.encryptCipher(activeUserId);
|
||||
try {
|
||||
this.formPromise = this.saveCipher(cipher);
|
||||
await this.formPromise;
|
||||
@@ -664,8 +671,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
return this.cipherService.get(this.cipherId);
|
||||
}
|
||||
|
||||
protected encryptCipher() {
|
||||
return this.cipherService.encrypt(this.cipher);
|
||||
protected encryptCipher(userId: UserId) {
|
||||
return this.cipherService.encrypt(this.cipher, userId);
|
||||
}
|
||||
|
||||
protected saveCipher(cipher: Cipher) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
@@ -11,6 +12,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
@@ -46,6 +48,7 @@ export class AttachmentsComponent implements OnInit {
|
||||
protected fileDownloadService: FileDownloadService,
|
||||
protected dialogService: DialogService,
|
||||
protected billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
protected accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -75,10 +78,13 @@ export class AttachmentsComponent implements OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
this.formPromise = this.saveCipherAttachment(files[0]);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
this.formPromise = this.saveCipherAttachment(files[0], activeUserId);
|
||||
this.cipherDomain = await this.formPromise;
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain),
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
|
||||
);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("attachmentSaved"));
|
||||
this.onUploadedAttachment.emit();
|
||||
@@ -185,8 +191,11 @@ export class AttachmentsComponent implements OnInit {
|
||||
|
||||
protected async init() {
|
||||
this.cipherDomain = await this.loadCipher();
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain),
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
|
||||
);
|
||||
|
||||
const canAccessPremium = await firstValueFrom(
|
||||
@@ -235,14 +244,18 @@ export class AttachmentsComponent implements OnInit {
|
||||
? attachment.key
|
||||
: await this.cryptoService.getOrgKey(this.cipher.organizationId);
|
||||
const decBuf = await this.cryptoService.decryptFromBytes(encBuf, key);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer(
|
||||
this.cipherDomain,
|
||||
attachment.fileName,
|
||||
decBuf,
|
||||
activeUserId,
|
||||
admin,
|
||||
);
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain),
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
|
||||
);
|
||||
|
||||
// 3. Delete old
|
||||
@@ -278,8 +291,8 @@ export class AttachmentsComponent implements OnInit {
|
||||
return this.cipherService.get(this.cipherId);
|
||||
}
|
||||
|
||||
protected saveCipherAttachment(file: File) {
|
||||
return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file);
|
||||
protected saveCipherAttachment(file: File, userId: UserId) {
|
||||
return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file, userId);
|
||||
}
|
||||
|
||||
protected deleteCipherAttachment(attachmentId: string) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -14,6 +16,7 @@ export class PasswordHistoryComponent implements OnInit {
|
||||
protected cipherService: CipherService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected i18nService: I18nService,
|
||||
protected accountService: AccountService,
|
||||
private win: Window,
|
||||
) {}
|
||||
|
||||
@@ -33,8 +36,11 @@ export class PasswordHistoryComponent implements OnInit {
|
||||
|
||||
protected async init() {
|
||||
const cipher = await this.cipherService.get(this.cipherId);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const decCipher = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher),
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory;
|
||||
}
|
||||
|
||||
@@ -9,11 +9,12 @@ import {
|
||||
OnInit,
|
||||
Output,
|
||||
} from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
@@ -100,6 +101,7 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
protected fileDownloadService: FileDownloadService,
|
||||
protected dialogService: DialogService,
|
||||
protected datePipe: DatePipe,
|
||||
protected accountService: AccountService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
) {}
|
||||
|
||||
@@ -129,8 +131,11 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
this.cleanUp();
|
||||
|
||||
const cipher = await this.cipherService.get(this.cipherId);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
this.cipher = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher),
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
this.canAccessPremium = await firstValueFrom(
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { TextEncoder } from "util";
|
||||
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { CipherService } from "../../../vault/abstractions/cipher.service";
|
||||
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
|
||||
import { CipherRepromptType } from "../../../vault/enums/cipher-reprompt-type";
|
||||
@@ -30,10 +33,18 @@ import { guidToRawFormat } from "./guid-utils";
|
||||
const RpId = "bitwarden.com";
|
||||
|
||||
describe("FidoAuthenticatorService", () => {
|
||||
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({
|
||||
id: "testId" as UserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
});
|
||||
|
||||
let cipherService!: MockProxy<CipherService>;
|
||||
let userInterface!: MockProxy<Fido2UserInterfaceService>;
|
||||
let userInterfaceSession!: MockProxy<Fido2UserInterfaceSession>;
|
||||
let syncService!: MockProxy<SyncService>;
|
||||
let accountService!: MockProxy<AccountService>;
|
||||
let authenticator!: Fido2AuthenticatorService;
|
||||
let tab!: chrome.tabs.Tab;
|
||||
|
||||
@@ -43,8 +54,15 @@ describe("FidoAuthenticatorService", () => {
|
||||
userInterfaceSession = mock<Fido2UserInterfaceSession>();
|
||||
userInterface.newSession.mockResolvedValue(userInterfaceSession);
|
||||
syncService = mock<SyncService>();
|
||||
authenticator = new Fido2AuthenticatorService(cipherService, userInterface, syncService);
|
||||
accountService = mock<AccountService>();
|
||||
authenticator = new Fido2AuthenticatorService(
|
||||
cipherService,
|
||||
userInterface,
|
||||
syncService,
|
||||
accountService,
|
||||
);
|
||||
tab = { id: 123, windowId: 456 } as chrome.tabs.Tab;
|
||||
accountService.activeAccount$ = activeAccountSubject;
|
||||
});
|
||||
|
||||
describe("makeCredential", () => {
|
||||
@@ -677,6 +695,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
],
|
||||
}),
|
||||
}),
|
||||
"testId",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { CipherService } from "../../../vault/abstractions/cipher.service";
|
||||
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
|
||||
import { CipherRepromptType } from "../../../vault/enums/cipher-reprompt-type";
|
||||
@@ -42,6 +45,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
private cipherService: CipherService,
|
||||
private userInterface: Fido2UserInterfaceService,
|
||||
private syncService: SyncService,
|
||||
private accountService: AccountService,
|
||||
private logService?: LogService,
|
||||
) {}
|
||||
|
||||
@@ -130,8 +134,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
keyPair = await createKeyPair();
|
||||
pubKeyDer = await crypto.subtle.exportKey("spki", keyPair.publicKey);
|
||||
const encrypted = await this.cipherService.get(cipherId);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
cipher = await encrypted.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(encrypted),
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(encrypted, activeUserId),
|
||||
);
|
||||
|
||||
if (
|
||||
@@ -150,7 +158,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
if (Utils.isNullOrEmpty(cipher.login.username)) {
|
||||
cipher.login.username = fido2Credential.userName;
|
||||
}
|
||||
const reencrypted = await this.cipherService.encrypt(cipher);
|
||||
const reencrypted = await this.cipherService.encrypt(cipher, activeUserId);
|
||||
await this.cipherService.updateWithServer(reencrypted);
|
||||
credentialId = fido2Credential.credentialId;
|
||||
} catch (error) {
|
||||
@@ -277,7 +285,10 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
};
|
||||
|
||||
if (selectedFido2Credential.counter > 0) {
|
||||
const encrypted = await this.cipherService.encrypt(selectedCipher);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const encrypted = await this.cipherService.encrypt(selectedCipher, activeUserId);
|
||||
await this.cipherService.updateWithServer(encrypted);
|
||||
}
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
throw new Error("Stamp has changed");
|
||||
}
|
||||
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, response.id);
|
||||
await this.cryptoService.setPrivateKey(response.privateKey, response.id);
|
||||
await this.cryptoService.setProviderKeys(response.providers, response.id);
|
||||
await this.cryptoService.setOrgKeys(
|
||||
|
||||
@@ -27,6 +27,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
clearCache: (userId?: string) => Promise<void>;
|
||||
encrypt: (
|
||||
model: CipherView,
|
||||
userId: UserId,
|
||||
keyForEncryption?: SymmetricCryptoKey,
|
||||
keyForCipherKeyDecryption?: SymmetricCryptoKey,
|
||||
originalCipher?: Cipher,
|
||||
@@ -83,21 +84,25 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
cipher: CipherView,
|
||||
organizationId: string,
|
||||
collectionIds: string[],
|
||||
userId: UserId,
|
||||
) => Promise<any>;
|
||||
shareManyWithServer: (
|
||||
ciphers: CipherView[],
|
||||
organizationId: string,
|
||||
collectionIds: string[],
|
||||
userId: UserId,
|
||||
) => Promise<any>;
|
||||
saveAttachmentWithServer: (
|
||||
cipher: Cipher,
|
||||
unencryptedFile: any,
|
||||
userId: UserId,
|
||||
admin?: boolean,
|
||||
) => Promise<Cipher>;
|
||||
saveAttachmentRawWithServer: (
|
||||
cipher: Cipher,
|
||||
filename: string,
|
||||
data: ArrayBuffer,
|
||||
userId: UserId,
|
||||
admin?: boolean,
|
||||
) => Promise<Cipher>;
|
||||
/**
|
||||
@@ -147,7 +152,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
) => Promise<any>;
|
||||
restoreWithServer: (id: string, asAdmin?: boolean) => Promise<any>;
|
||||
restoreManyWithServer: (ids: string[], orgId?: string) => Promise<void>;
|
||||
getKeyForCipherKeyDecryption: (cipher: Cipher) => Promise<any>;
|
||||
getKeyForCipherKeyDecryption: (cipher: Cipher, userId: UserId) => Promise<any>;
|
||||
setAddEditCipherInfo: (value: AddEditCipherInfo) => Promise<void>;
|
||||
/**
|
||||
* Returns user ciphers re-encrypted with the new user key.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils";
|
||||
import { UriMatchStrategy } from "../../../models/domain/domain-service";
|
||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||
@@ -247,7 +249,7 @@ describe("Cipher DTO", () => {
|
||||
);
|
||||
|
||||
const cipherView = await cipher.decrypt(
|
||||
await cipherService.getKeyForCipherKeyDecryption(cipher),
|
||||
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),
|
||||
);
|
||||
|
||||
expect(cipherView).toMatchObject({
|
||||
@@ -367,7 +369,7 @@ describe("Cipher DTO", () => {
|
||||
);
|
||||
|
||||
const cipherView = await cipher.decrypt(
|
||||
await cipherService.getKeyForCipherKeyDecryption(cipher),
|
||||
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),
|
||||
);
|
||||
|
||||
expect(cipherView).toMatchObject({
|
||||
@@ -505,7 +507,7 @@ describe("Cipher DTO", () => {
|
||||
);
|
||||
|
||||
const cipherView = await cipher.decrypt(
|
||||
await cipherService.getKeyForCipherKeyDecryption(cipher),
|
||||
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),
|
||||
);
|
||||
|
||||
expect(cipherView).toMatchObject({
|
||||
@@ -667,7 +669,7 @@ describe("Cipher DTO", () => {
|
||||
);
|
||||
|
||||
const cipherView = await cipher.decrypt(
|
||||
await cipherService.getKeyForCipherKeyDecryption(cipher),
|
||||
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),
|
||||
);
|
||||
|
||||
expect(cipherView).toMatchObject({
|
||||
@@ -754,3 +756,5 @@ describe("Cipher DTO", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const mockUserId = "TestUserId" as UserId;
|
||||
|
||||
@@ -121,6 +121,8 @@ describe("Cipher Service", () => {
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
const stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
const userId = "TestUserId" as UserId;
|
||||
|
||||
let cipherService: CipherService;
|
||||
let cipherObj: Cipher;
|
||||
|
||||
@@ -168,7 +170,7 @@ describe("Cipher Service", () => {
|
||||
|
||||
const spy = jest.spyOn(cipherFileUploadService, "upload");
|
||||
|
||||
await cipherService.saveAttachmentRawWithServer(new Cipher(), fileName, fileData);
|
||||
await cipherService.saveAttachmentRawWithServer(new Cipher(), fileName, fileData, userId);
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
@@ -283,7 +285,7 @@ describe("Cipher Service", () => {
|
||||
{ uri: "uri", match: UriMatchStrategy.RegularExpression } as LoginUriView,
|
||||
];
|
||||
|
||||
const domain = await cipherService.encrypt(cipherView);
|
||||
const domain = await cipherService.encrypt(cipherView, userId);
|
||||
|
||||
expect(domain.login.uris).toEqual([
|
||||
{
|
||||
@@ -299,7 +301,7 @@ describe("Cipher Service", () => {
|
||||
it("is null when enableCipherKeyEncryption flag is false", async () => {
|
||||
setEncryptionKeyFlag(false);
|
||||
|
||||
const cipher = await cipherService.encrypt(cipherView);
|
||||
const cipher = await cipherService.encrypt(cipherView, userId);
|
||||
|
||||
expect(cipher.key).toBeNull();
|
||||
});
|
||||
@@ -307,7 +309,7 @@ describe("Cipher Service", () => {
|
||||
it("is defined when enableCipherKeyEncryption flag is true", async () => {
|
||||
setEncryptionKeyFlag(true);
|
||||
|
||||
const cipher = await cipherService.encrypt(cipherView);
|
||||
const cipher = await cipherService.encrypt(cipherView, userId);
|
||||
|
||||
expect(cipher.key).toBeDefined();
|
||||
});
|
||||
@@ -321,7 +323,7 @@ describe("Cipher Service", () => {
|
||||
it("is not called when enableCipherKeyEncryption is false", async () => {
|
||||
setEncryptionKeyFlag(false);
|
||||
|
||||
await cipherService.encrypt(cipherView);
|
||||
await cipherService.encrypt(cipherView, userId);
|
||||
|
||||
expect(cipherService["encryptCipherWithCipherKey"]).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -329,7 +331,7 @@ describe("Cipher Service", () => {
|
||||
it("is called when enableCipherKeyEncryption is true", async () => {
|
||||
setEncryptionKeyFlag(true);
|
||||
|
||||
await cipherService.encrypt(cipherView);
|
||||
await cipherService.encrypt(cipherView, userId);
|
||||
|
||||
expect(cipherService["encryptCipherWithCipherKey"]).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -165,6 +165,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
|
||||
async encrypt(
|
||||
model: CipherView,
|
||||
userId: UserId,
|
||||
keyForEncryption?: SymmetricCryptoKey,
|
||||
keyForCipherKeyDecryption?: SymmetricCryptoKey,
|
||||
originalCipher: Cipher = null,
|
||||
@@ -174,7 +175,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
originalCipher = await this.get(model.id);
|
||||
}
|
||||
if (originalCipher != null) {
|
||||
await this.updateModelfromExistingCipher(model, originalCipher);
|
||||
await this.updateModelfromExistingCipher(model, originalCipher, userId);
|
||||
}
|
||||
this.adjustPasswordHistoryLength(model);
|
||||
}
|
||||
@@ -192,7 +193,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
|
||||
if (await this.getCipherKeyEncryptionEnabled()) {
|
||||
cipher.key = originalCipher?.key ?? null;
|
||||
const userOrOrgKey = await this.getKeyForCipherKeyDecryption(cipher);
|
||||
const userOrOrgKey = await this.getKeyForCipherKeyDecryption(cipher, userId);
|
||||
// The keyForEncryption is only used for encrypting the cipher key, not the cipher itself, since cipher key encryption is enabled.
|
||||
// If the caller has provided a key for cipher key encryption, use it. Otherwise, use the user or org key.
|
||||
keyForEncryption ||= userOrOrgKey;
|
||||
@@ -718,6 +719,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
cipher: CipherView,
|
||||
organizationId: string,
|
||||
collectionIds: string[],
|
||||
userId: UserId,
|
||||
): Promise<any> {
|
||||
const attachmentPromises: Promise<any>[] = [];
|
||||
if (cipher.attachments != null) {
|
||||
@@ -733,7 +735,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
|
||||
cipher.organizationId = organizationId;
|
||||
cipher.collectionIds = collectionIds;
|
||||
const encCipher = await this.encryptSharedCipher(cipher);
|
||||
const encCipher = await this.encryptSharedCipher(cipher, userId);
|
||||
const request = new CipherShareRequest(encCipher);
|
||||
const response = await this.apiService.putShareCipher(cipher.id, request);
|
||||
const data = new CipherData(response, collectionIds);
|
||||
@@ -744,6 +746,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
ciphers: CipherView[],
|
||||
organizationId: string,
|
||||
collectionIds: string[],
|
||||
userId: UserId,
|
||||
): Promise<any> {
|
||||
const promises: Promise<any>[] = [];
|
||||
const encCiphers: Cipher[] = [];
|
||||
@@ -751,7 +754,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
cipher.organizationId = organizationId;
|
||||
cipher.collectionIds = collectionIds;
|
||||
promises.push(
|
||||
this.encryptSharedCipher(cipher).then((c) => {
|
||||
this.encryptSharedCipher(cipher, userId).then((c) => {
|
||||
encCiphers.push(c);
|
||||
}),
|
||||
);
|
||||
@@ -770,7 +773,12 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
await this.upsert(encCiphers.map((c) => c.toCipherData()));
|
||||
}
|
||||
|
||||
saveAttachmentWithServer(cipher: Cipher, unencryptedFile: any, admin = false): Promise<Cipher> {
|
||||
saveAttachmentWithServer(
|
||||
cipher: Cipher,
|
||||
unencryptedFile: any,
|
||||
userId: UserId,
|
||||
admin = false,
|
||||
): Promise<Cipher> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsArrayBuffer(unencryptedFile);
|
||||
@@ -780,6 +788,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
cipher,
|
||||
unencryptedFile.name,
|
||||
evt.target.result,
|
||||
userId,
|
||||
admin,
|
||||
);
|
||||
resolve(cData);
|
||||
@@ -797,9 +806,10 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
cipher: Cipher,
|
||||
filename: string,
|
||||
data: Uint8Array,
|
||||
userId: UserId,
|
||||
admin = false,
|
||||
): Promise<Cipher> {
|
||||
const encKey = await this.getKeyForCipherKeyDecryption(cipher);
|
||||
const encKey = await this.getKeyForCipherKeyDecryption(cipher, userId);
|
||||
const cipherKeyEncryptionEnabled = await this.getCipherKeyEncryptionEnabled();
|
||||
|
||||
const cipherEncKey =
|
||||
@@ -813,8 +823,8 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
//then we rollback to using the user key as the main key of encryption of the item
|
||||
//in order to keep item and it's attachments with the same encryption level
|
||||
if (cipher.key != null && !cipherKeyEncryptionEnabled) {
|
||||
const model = await cipher.decrypt(await this.getKeyForCipherKeyDecryption(cipher));
|
||||
cipher = await this.encrypt(model);
|
||||
const model = await cipher.decrypt(await this.getKeyForCipherKeyDecryption(cipher, userId));
|
||||
cipher = await this.encrypt(model, userId);
|
||||
await this.updateWithServer(cipher);
|
||||
}
|
||||
|
||||
@@ -1209,10 +1219,10 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
await this.restore(restores);
|
||||
}
|
||||
|
||||
async getKeyForCipherKeyDecryption(cipher: Cipher): Promise<UserKey | OrgKey> {
|
||||
async getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise<UserKey | OrgKey> {
|
||||
return (
|
||||
(await this.cryptoService.getOrgKey(cipher.organizationId)) ||
|
||||
((await this.cryptoService.getUserKeyWithLegacySupport()) as UserKey)
|
||||
((await this.cryptoService.getUserKeyWithLegacySupport(userId)) as UserKey)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1247,7 +1257,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
encryptedCiphers = await Promise.all(
|
||||
userCiphers.map(async (cipher) => {
|
||||
const encryptedCipher = await this.encrypt(cipher, newUserKey, originalUserKey);
|
||||
const encryptedCipher = await this.encrypt(cipher, userId, newUserKey, originalUserKey);
|
||||
return new CipherWithIdRequest(encryptedCipher);
|
||||
}),
|
||||
);
|
||||
@@ -1259,17 +1269,18 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
|
||||
// In the case of a cipher that is being shared with an organization, we want to decrypt the
|
||||
// cipher key with the user's key and then re-encrypt it with the organization's key.
|
||||
private async encryptSharedCipher(model: CipherView): Promise<Cipher> {
|
||||
const keyForCipherKeyDecryption = await this.cryptoService.getUserKeyWithLegacySupport();
|
||||
return await this.encrypt(model, null, keyForCipherKeyDecryption);
|
||||
private async encryptSharedCipher(model: CipherView, userId: UserId): Promise<Cipher> {
|
||||
const keyForCipherKeyDecryption = await this.cryptoService.getUserKeyWithLegacySupport(userId);
|
||||
return await this.encrypt(model, userId, null, keyForCipherKeyDecryption);
|
||||
}
|
||||
|
||||
private async updateModelfromExistingCipher(
|
||||
model: CipherView,
|
||||
originalCipher: Cipher,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
const existingCipher = await originalCipher.decrypt(
|
||||
await this.getKeyForCipherKeyDecryption(originalCipher),
|
||||
await this.getKeyForCipherKeyDecryption(originalCipher, userId),
|
||||
);
|
||||
model.passwordHistory = existingCipher.passwordHistory || [];
|
||||
if (model.type === CipherType.Login && existingCipher.type === CipherType.Login) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { KdfType } from "@bitwarden/common/platform/enums";
|
||||
@@ -21,6 +22,7 @@ describe("BitwardenPasswordProtectedImporter", () => {
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let cipherService: MockProxy<CipherService>;
|
||||
let pinService: MockProxy<PinServiceAbstraction>;
|
||||
let accountService: MockProxy<AccountService>;
|
||||
const password = Utils.newGuid();
|
||||
const promptForPassword_callback = async () => {
|
||||
return password;
|
||||
@@ -31,12 +33,14 @@ describe("BitwardenPasswordProtectedImporter", () => {
|
||||
i18nService = mock<I18nService>();
|
||||
cipherService = mock<CipherService>();
|
||||
pinService = mock<PinServiceAbstraction>();
|
||||
accountService = mock<AccountService>();
|
||||
|
||||
importer = new BitwardenPasswordProtectedImporter(
|
||||
cryptoService,
|
||||
i18nService,
|
||||
cipherService,
|
||||
pinService,
|
||||
accountService,
|
||||
promptForPassword_callback,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -89,6 +90,7 @@ const safeProviders: SafeProvider[] = [
|
||||
CollectionService,
|
||||
CryptoService,
|
||||
PinServiceAbstraction,
|
||||
AccountService,
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
CipherWithIdExport,
|
||||
CollectionWithIdExport,
|
||||
@@ -33,6 +34,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
|
||||
protected i18nService: I18nService,
|
||||
protected cipherService: CipherService,
|
||||
protected pinService: PinServiceAbstraction,
|
||||
protected accountService: AccountService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -103,8 +105,11 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
|
||||
});
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const view = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher),
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
this.cleanupCipher(view);
|
||||
this.result.ciphers.push(view);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
Argon2KdfConfig,
|
||||
KdfConfig,
|
||||
@@ -25,9 +26,10 @@ export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter im
|
||||
i18nService: I18nService,
|
||||
cipherService: CipherService,
|
||||
pinService: PinServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
private promptForPassword_callback: () => Promise<string>,
|
||||
) {
|
||||
super(cryptoService, i18nService, cipherService, pinService);
|
||||
super(cryptoService, i18nService, cipherService, pinService, accountService);
|
||||
}
|
||||
|
||||
async parse(data: string): Promise<ImportResult> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
@@ -27,6 +28,7 @@ describe("ImportService", () => {
|
||||
let collectionService: MockProxy<CollectionService>;
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let pinService: MockProxy<PinServiceAbstraction>;
|
||||
let accountService: MockProxy<AccountService>;
|
||||
|
||||
beforeEach(() => {
|
||||
cipherService = mock<CipherService>();
|
||||
@@ -45,6 +47,7 @@ describe("ImportService", () => {
|
||||
collectionService,
|
||||
cryptoService,
|
||||
pinService,
|
||||
accountService,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ImportCiphersRequest } from "@bitwarden/common/models/request/import-ciphers.request";
|
||||
import { ImportOrganizationCiphersRequest } from "@bitwarden/common/models/request/import-organization-ciphers.request";
|
||||
import { KvpRequest } from "@bitwarden/common/models/request/kvp.request";
|
||||
@@ -102,6 +105,7 @@ export class ImportService implements ImportServiceAbstraction {
|
||||
private collectionService: CollectionService,
|
||||
private cryptoService: CryptoService,
|
||||
private pinService: PinServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
getImportOptions(): ImportOption[] {
|
||||
@@ -206,6 +210,7 @@ export class ImportService implements ImportServiceAbstraction {
|
||||
this.i18nService,
|
||||
this.cipherService,
|
||||
this.pinService,
|
||||
this.accountService,
|
||||
promptForPassword_callback,
|
||||
);
|
||||
case "lastpasscsv":
|
||||
@@ -332,8 +337,11 @@ export class ImportService implements ImportServiceAbstraction {
|
||||
|
||||
private async handleIndividualImport(importResult: ImportResult) {
|
||||
const request = new ImportCiphersRequest();
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
for (let i = 0; i < importResult.ciphers.length; i++) {
|
||||
const c = await this.cipherService.encrypt(importResult.ciphers[i]);
|
||||
const c = await this.cipherService.encrypt(importResult.ciphers[i], activeUserId);
|
||||
request.ciphers.push(new CipherRequest(c));
|
||||
}
|
||||
if (importResult.folders != null) {
|
||||
@@ -352,9 +360,12 @@ export class ImportService implements ImportServiceAbstraction {
|
||||
|
||||
private async handleOrganizationalImport(importResult: ImportResult, organizationId: string) {
|
||||
const request = new ImportOrganizationCiphersRequest();
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
for (let i = 0; i < importResult.ciphers.length; i++) {
|
||||
importResult.ciphers[i].organizationId = organizationId;
|
||||
const c = await this.cipherService.encrypt(importResult.ciphers[i]);
|
||||
const c = await this.cipherService.encrypt(importResult.ciphers[i], activeUserId);
|
||||
request.ciphers.push(new CipherRequest(c));
|
||||
}
|
||||
if (importResult.collections != null) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as papa from "papaparse";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
@@ -42,6 +43,7 @@ export class OrganizationVaultExportService
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
private collectionService: CollectionService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
super(pinService, cryptoService, cryptoFunctionService, kdfConfigService);
|
||||
}
|
||||
@@ -87,6 +89,9 @@ export class OrganizationVaultExportService
|
||||
const decCollections: CollectionView[] = [];
|
||||
const decCiphers: CipherView[] = [];
|
||||
const promises = [];
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
promises.push(
|
||||
this.apiService.getOrganizationExport(organizationId).then((exportData) => {
|
||||
@@ -111,7 +116,7 @@ export class OrganizationVaultExportService
|
||||
const cipher = new Cipher(new CipherData(c));
|
||||
exportPromises.push(
|
||||
this.cipherService
|
||||
.getKeyForCipherKeyDecryption(cipher)
|
||||
.getKeyForCipherKeyDecryption(cipher, activeUserId)
|
||||
.then((key) => cipher.decrypt(key))
|
||||
.then((decCipher) => {
|
||||
decCiphers.push(decCipher);
|
||||
|
||||
@@ -3,11 +3,13 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
@@ -15,6 +17,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { ButtonComponent, ToastService } from "@bitwarden/components";
|
||||
import { DownloadAttachmentComponent } from "@bitwarden/vault";
|
||||
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../../../common/spec";
|
||||
|
||||
import { CipherAttachmentsComponent } from "./cipher-attachments.component";
|
||||
import { DeleteAttachmentComponent } from "./delete-attachment/delete-attachment.component";
|
||||
|
||||
@@ -49,6 +53,9 @@ describe("CipherAttachmentsComponent", () => {
|
||||
const cipherServiceGet = jest.fn().mockResolvedValue(cipherDomain);
|
||||
const saveAttachmentWithServer = jest.fn().mockResolvedValue(cipherDomain);
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
beforeEach(async () => {
|
||||
cipherServiceGet.mockClear();
|
||||
showToast.mockClear();
|
||||
@@ -75,6 +82,10 @@ describe("CipherAttachmentsComponent", () => {
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: accountService,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideComponent(CipherAttachmentsComponent, {
|
||||
@@ -219,7 +230,7 @@ describe("CipherAttachmentsComponent", () => {
|
||||
it("calls `saveAttachmentWithServer`", async () => {
|
||||
await component.submit();
|
||||
|
||||
expect(saveAttachmentWithServer).toHaveBeenCalledWith(cipherDomain, file);
|
||||
expect(saveAttachmentWithServer).toHaveBeenCalledWith(cipherDomain, file, mockUserId);
|
||||
});
|
||||
|
||||
it("resets form and input values", async () => {
|
||||
|
||||
@@ -19,11 +19,13 @@ import {
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
@@ -90,6 +92,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
||||
});
|
||||
|
||||
private cipherDomain: Cipher;
|
||||
private activeUserId: UserId;
|
||||
private destroy$ = inject(DestroyRef);
|
||||
|
||||
constructor(
|
||||
@@ -98,6 +101,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
||||
private formBuilder: FormBuilder,
|
||||
private logService: LogService,
|
||||
private toastService: ToastService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.attachmentForm.statusChanges.pipe(takeUntilDestroyed()).subscribe((status) => {
|
||||
if (!this.submitBtn) {
|
||||
@@ -110,8 +114,11 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.cipherDomain = await this.cipherService.get(this.cipherId);
|
||||
this.activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain),
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, this.activeUserId),
|
||||
);
|
||||
|
||||
// Update the initial state of the submit button
|
||||
@@ -178,11 +185,12 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
||||
this.cipherDomain = await this.cipherService.saveAttachmentWithServer(
|
||||
this.cipherDomain,
|
||||
file,
|
||||
this.activeUserId,
|
||||
);
|
||||
|
||||
// re-decrypt the cipher to update the attachments
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain),
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, this.activeUserId),
|
||||
);
|
||||
|
||||
// Reset reactive form and input element
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -14,15 +16,25 @@ function isSetEqual(a: Set<string>, b: Set<string>) {
|
||||
@Injectable()
|
||||
export class DefaultCipherFormService implements CipherFormService {
|
||||
private cipherService: CipherService = inject(CipherService);
|
||||
private accountService: AccountService = inject(AccountService);
|
||||
|
||||
async decryptCipher(cipher: Cipher): Promise<CipherView> {
|
||||
return await cipher.decrypt(await this.cipherService.getKeyForCipherKeyDecryption(cipher));
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
return await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
}
|
||||
|
||||
async saveCipher(cipher: CipherView, config: CipherFormConfig): Promise<CipherView> {
|
||||
// Passing the original cipher is important here as it is responsible for appending to password history
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const encryptedCipher = await this.cipherService.encrypt(
|
||||
cipher,
|
||||
activeUserId,
|
||||
null,
|
||||
null,
|
||||
config.originalCipher ?? null,
|
||||
@@ -34,7 +46,7 @@ export class DefaultCipherFormService implements CipherFormService {
|
||||
if (cipher.id == null) {
|
||||
savedCipher = await this.cipherService.createWithServer(encryptedCipher, config.admin);
|
||||
return await savedCipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(savedCipher),
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(savedCipher, activeUserId),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,7 +80,7 @@ export class DefaultCipherFormService implements CipherFormService {
|
||||
}
|
||||
|
||||
return await savedCipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(savedCipher),
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(savedCipher, activeUserId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Observable,
|
||||
Subject,
|
||||
combineLatest,
|
||||
firstValueFrom,
|
||||
map,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
@@ -25,10 +26,11 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -162,6 +164,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
||||
private get selectedOrgId(): OrganizationId {
|
||||
return this.formGroup.getRawValue().selectedOrg || this.params.organizationId;
|
||||
}
|
||||
private activeUserId: UserId;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
@@ -172,6 +175,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
||||
private collectionService: CollectionService,
|
||||
private formBuilder: FormBuilder,
|
||||
private toastService: ToastService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -179,6 +183,10 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
||||
FeatureFlag.RestrictProviderAccess,
|
||||
);
|
||||
|
||||
this.activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
const onlyPersonalItems = this.params.ciphers.every((c) => c.organizationId == null);
|
||||
|
||||
if (this.selectedOrgId === MY_VAULT_ID || onlyPersonalItems) {
|
||||
@@ -420,6 +428,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
||||
shareableCiphers,
|
||||
organizationId,
|
||||
selectedCollectionIds,
|
||||
this.activeUserId,
|
||||
);
|
||||
|
||||
this.toastService.showToast({
|
||||
@@ -460,7 +469,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
||||
private async updateAssignedCollections(cipherView: CipherView) {
|
||||
const { collections } = this.formGroup.getRawValue();
|
||||
cipherView.collectionIds = collections.map((i) => i.id as CollectionId);
|
||||
const cipher = await this.cipherService.encrypt(cipherView);
|
||||
const cipher = await this.cipherService.encrypt(cipherView, this.activeUserId);
|
||||
await this.cipherService.saveCollectionsWithServer(cipher);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user