mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-12423] Migrate Cipher Decryption to Use SDK (#14206)
* Created mappings for client domain object to SDK * Add abstract decrypt observable * Added todo for future consideration * Added implementation to cipher service * Added adapter and unit tests * Created cipher encryption abstraction and service * Register cipher encryption service * Added tests for the cipher encryption service * changed signature * Updated feature flag name * added new function to be used for decrypting ciphers * Added new encryptedKey field * added new function to be used for decrypting ciphers * Manually set fields * Added encrypted key in attachment view * Fixed test * Updated references to use decrypt with feature flag * Added dependency * updated package.json * lint fix * fixed tests * Fixed small mapping issues * Fixed test * Added function to decrypt fido2 key value * Added function to decrypt fido2 key value and updated test * updated to use sdk function without prociding the key * updated localdata sdk type change * decrypt attachment content using sdk * Fixed dependency issues * updated package.json * Refactored service to handle getting decrypted buffer using the legacy and sdk implementations * updated services and component to use refactored version * Updated decryptCiphersWithSdk to use decryptManyLegacy for batch decryption, ensuring the SDK is only called once per batch * Fixed merge conflicts * Fixed merge conflicts * Fixed merge conflicts * Fixed lint issues * Moved getDecryptedAttachmentBuffer to cipher service * Moved getDecryptedAttachmentBuffer to cipher service * ensure CipherView properties are null instead of undefined * Fixed test * ensure AttachmentView properties are null instead of undefined * Linked ticket in comment * removed unused orgKey
This commit is contained in:
@@ -894,9 +894,7 @@ export default class NotificationBackground {
|
|||||||
private async getDecryptedCipherById(cipherId: string, userId: UserId) {
|
private async getDecryptedCipherById(cipherId: string, userId: UserId) {
|
||||||
const cipher = await this.cipherService.get(cipherId, userId);
|
const cipher = await this.cipherService.get(cipherId, userId);
|
||||||
if (cipher != null && cipher.type === CipherType.Login) {
|
if (cipher != null && cipher.type === CipherType.Login) {
|
||||||
return await cipher.decrypt(
|
return await this.cipherService.decrypt(cipher, userId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, userId),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,9 +216,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
this.ciphers = await Promise.all(
|
this.ciphers = await Promise.all(
|
||||||
message.cipherIds.map(async (cipherId) => {
|
message.cipherIds.map(async (cipherId) => {
|
||||||
const cipher = await this.cipherService.get(cipherId, activeUserId);
|
const cipher = await this.cipherService.get(cipherId, activeUserId);
|
||||||
return cipher.decrypt(
|
return this.cipherService.decrypt(cipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -237,9 +235,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
this.ciphers = await Promise.all(
|
this.ciphers = await Promise.all(
|
||||||
message.existingCipherIds.map(async (cipherId) => {
|
message.existingCipherIds.map(async (cipherId) => {
|
||||||
const cipher = await this.cipherService.get(cipherId, activeUserId);
|
const cipher = await this.cipherService.get(cipherId, activeUserId);
|
||||||
return cipher.decrypt(
|
return this.cipherService.decrypt(cipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-st
|
|||||||
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";
|
||||||
|
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
|
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
|
||||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||||
@@ -199,6 +200,7 @@ import {
|
|||||||
DefaultCipherAuthorizationService,
|
DefaultCipherAuthorizationService,
|
||||||
} from "@bitwarden/common/vault/services/cipher-authorization.service";
|
} from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
||||||
|
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service";
|
||||||
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
|
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
|
||||||
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
|
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
|
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
|
||||||
@@ -408,6 +410,7 @@ export default class MainBackground {
|
|||||||
endUserNotificationService: EndUserNotificationService;
|
endUserNotificationService: EndUserNotificationService;
|
||||||
inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
|
inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
|
||||||
taskService: TaskService;
|
taskService: TaskService;
|
||||||
|
cipherEncryptionService: CipherEncryptionService;
|
||||||
|
|
||||||
ipcContentScriptManagerService: IpcContentScriptManagerService;
|
ipcContentScriptManagerService: IpcContentScriptManagerService;
|
||||||
ipcService: IpcService;
|
ipcService: IpcService;
|
||||||
@@ -856,6 +859,11 @@ export default class MainBackground {
|
|||||||
|
|
||||||
this.bulkEncryptService = new FallbackBulkEncryptService(this.encryptService);
|
this.bulkEncryptService = new FallbackBulkEncryptService(this.encryptService);
|
||||||
|
|
||||||
|
this.cipherEncryptionService = new DefaultCipherEncryptionService(
|
||||||
|
this.sdkService,
|
||||||
|
this.logService,
|
||||||
|
);
|
||||||
|
|
||||||
this.cipherService = new CipherService(
|
this.cipherService = new CipherService(
|
||||||
this.keyService,
|
this.keyService,
|
||||||
this.domainSettingsService,
|
this.domainSettingsService,
|
||||||
@@ -871,6 +879,7 @@ export default class MainBackground {
|
|||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
this.accountService,
|
this.accountService,
|
||||||
this.logService,
|
this.logService,
|
||||||
|
this.cipherEncryptionService,
|
||||||
);
|
);
|
||||||
this.folderService = new FolderService(
|
this.folderService = new FolderService(
|
||||||
this.keyService,
|
this.keyService,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { CollectionService } from "@bitwarden/admin-console/common";
|
|||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
import { OrgKey, UserKey } from "@bitwarden/common/types/key";
|
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import {
|
import {
|
||||||
@@ -66,11 +65,7 @@ export class AssignCollections {
|
|||||||
route.queryParams.pipe(
|
route.queryParams.pipe(
|
||||||
switchMap(async ({ cipherId }) => {
|
switchMap(async ({ cipherId }) => {
|
||||||
const cipherDomain = await this.cipherService.get(cipherId, userId);
|
const cipherDomain = await this.cipherService.get(cipherId, userId);
|
||||||
const key: UserKey | OrgKey = await this.cipherService.getKeyForCipherKeyDecryption(
|
return await this.cipherService.decrypt(cipherDomain, userId);
|
||||||
cipherDomain,
|
|
||||||
userId,
|
|
||||||
);
|
|
||||||
return cipherDomain.decrypt(key);
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ describe("OpenAttachmentsComponent", () => {
|
|||||||
useValue: {
|
useValue: {
|
||||||
get: getCipher,
|
get: getCipher,
|
||||||
getKeyForCipherKeyDecryption: () => Promise.resolve(null),
|
getKeyForCipherKeyDecryption: () => Promise.resolve(null),
|
||||||
|
decrypt: jest.fn().mockResolvedValue(cipherView),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -81,9 +81,7 @@ export class OpenAttachmentsComponent implements OnInit {
|
|||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
);
|
);
|
||||||
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
|
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
|
||||||
const cipher = await cipherDomain.decrypt(
|
const cipher = await this.cipherService.decrypt(cipherDomain, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!cipher.organizationId) {
|
if (!cipher.organizationId) {
|
||||||
this.cipherIsAPartOfFreeOrg = false;
|
this.cipherIsAPartOfFreeOrg = false;
|
||||||
|
|||||||
@@ -69,8 +69,6 @@ export class PasswordHistoryV2Component implements OnInit {
|
|||||||
const activeUserId = activeAccount.id as UserId;
|
const activeUserId = activeAccount.id as UserId;
|
||||||
|
|
||||||
const cipher = await this.cipherService.get(cipherId, activeUserId);
|
const cipher = await this.cipherService.get(cipherId, activeUserId);
|
||||||
this.cipher = await cipher.decrypt(
|
this.cipher = await this.cipherService.decrypt(cipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ describe("ViewV2Component", () => {
|
|||||||
getKeyForCipherKeyDecryption: jest.fn().mockResolvedValue({}),
|
getKeyForCipherKeyDecryption: jest.fn().mockResolvedValue({}),
|
||||||
deleteWithServer: jest.fn().mockResolvedValue(undefined),
|
deleteWithServer: jest.fn().mockResolvedValue(undefined),
|
||||||
softDeleteWithServer: jest.fn().mockResolvedValue(undefined),
|
softDeleteWithServer: jest.fn().mockResolvedValue(undefined),
|
||||||
|
decrypt: jest.fn().mockResolvedValue(mockCipher),
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|||||||
@@ -203,9 +203,7 @@ export class ViewV2Component {
|
|||||||
|
|
||||||
async getCipherData(id: string, userId: UserId) {
|
async getCipherData(id: string, userId: UserId) {
|
||||||
const cipher = await this.cipherService.get(id, userId);
|
const cipher = await this.cipherService.get(id, userId);
|
||||||
return await cipher.decrypt(
|
return await this.cipherService.decrypt(cipher, userId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, userId),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async editCipher() {
|
async editCipher() {
|
||||||
|
|||||||
@@ -59,15 +59,11 @@ export class ShareCommand {
|
|||||||
return Response.badRequest("This item already belongs to an organization.");
|
return Response.badRequest("This item already belongs to an organization.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const cipherView = await cipher.decrypt(
|
const cipherView = await this.cipherService.decrypt(cipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await this.cipherService.shareWithServer(cipherView, organizationId, req, activeUserId);
|
await this.cipherService.shareWithServer(cipherView, organizationId, req, activeUserId);
|
||||||
const updatedCipher = await this.cipherService.get(cipher.id, activeUserId);
|
const updatedCipher = await this.cipherService.get(cipher.id, activeUserId);
|
||||||
const decCipher = await updatedCipher.decrypt(
|
const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
|
|
||||||
);
|
|
||||||
const res = new CipherResponse(decCipher);
|
const res = new CipherResponse(decCipher);
|
||||||
return Response.success(res);
|
return Response.success(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -90,9 +90,7 @@ export class EditCommand {
|
|||||||
return Response.notFound();
|
return Response.notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
let cipherView = await cipher.decrypt(
|
let cipherView = await this.cipherService.decrypt(cipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
|
||||||
);
|
|
||||||
if (cipherView.isDeleted) {
|
if (cipherView.isDeleted) {
|
||||||
return Response.badRequest("You may not edit a deleted item. Use the restore command first.");
|
return Response.badRequest("You may not edit a deleted item. Use the restore command first.");
|
||||||
}
|
}
|
||||||
@@ -100,9 +98,7 @@ export class EditCommand {
|
|||||||
const encCipher = await this.cipherService.encrypt(cipherView, activeUserId);
|
const encCipher = await this.cipherService.encrypt(cipherView, activeUserId);
|
||||||
try {
|
try {
|
||||||
const updatedCipher = await this.cipherService.updateWithServer(encCipher);
|
const updatedCipher = await this.cipherService.updateWithServer(encCipher);
|
||||||
const decCipher = await updatedCipher.decrypt(
|
const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
|
|
||||||
);
|
|
||||||
const res = new CipherResponse(decCipher);
|
const res = new CipherResponse(decCipher);
|
||||||
return Response.success(res);
|
return Response.success(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -132,12 +128,7 @@ export class EditCommand {
|
|||||||
cipher,
|
cipher,
|
||||||
activeUserId,
|
activeUserId,
|
||||||
);
|
);
|
||||||
const decCipher = await updatedCipher.decrypt(
|
const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(
|
|
||||||
updatedCipher,
|
|
||||||
await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const res = new CipherResponse(decCipher);
|
const res = new CipherResponse(decCipher);
|
||||||
return Response.success(res);
|
return Response.success(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -116,9 +116,7 @@ export class GetCommand extends DownloadCommand {
|
|||||||
if (Utils.isGuid(id)) {
|
if (Utils.isGuid(id)) {
|
||||||
const cipher = await this.cipherService.get(id, activeUserId);
|
const cipher = await this.cipherService.get(id, activeUserId);
|
||||||
if (cipher != null) {
|
if (cipher != null) {
|
||||||
decCipher = await cipher.decrypt(
|
decCipher = await this.cipherService.decrypt(cipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if (id.trim() !== "") {
|
} else if (id.trim() !== "") {
|
||||||
let ciphers = await this.cipherService.getAllDecrypted(activeUserId);
|
let ciphers = await this.cipherService.getAllDecrypted(activeUserId);
|
||||||
|
|||||||
@@ -139,12 +139,14 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s
|
|||||||
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
|
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 { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||||
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
import {
|
import {
|
||||||
CipherAuthorizationService,
|
CipherAuthorizationService,
|
||||||
DefaultCipherAuthorizationService,
|
DefaultCipherAuthorizationService,
|
||||||
} from "@bitwarden/common/vault/services/cipher-authorization.service";
|
} from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
||||||
|
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service";
|
||||||
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
|
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
|
||||||
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
|
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
|
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
|
||||||
@@ -284,6 +286,7 @@ export class ServiceContainer {
|
|||||||
ssoUrlService: SsoUrlService;
|
ssoUrlService: SsoUrlService;
|
||||||
masterPasswordApiService: MasterPasswordApiServiceAbstraction;
|
masterPasswordApiService: MasterPasswordApiServiceAbstraction;
|
||||||
bulkEncryptService: FallbackBulkEncryptService;
|
bulkEncryptService: FallbackBulkEncryptService;
|
||||||
|
cipherEncryptionService: CipherEncryptionService;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
let p = null;
|
let p = null;
|
||||||
@@ -679,6 +682,11 @@ export class ServiceContainer {
|
|||||||
this.accountService,
|
this.accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.cipherEncryptionService = new DefaultCipherEncryptionService(
|
||||||
|
this.sdkService,
|
||||||
|
this.logService,
|
||||||
|
);
|
||||||
|
|
||||||
this.cipherService = new CipherService(
|
this.cipherService = new CipherService(
|
||||||
this.keyService,
|
this.keyService,
|
||||||
this.domainSettingsService,
|
this.domainSettingsService,
|
||||||
@@ -694,6 +702,7 @@ export class ServiceContainer {
|
|||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
this.accountService,
|
this.accountService,
|
||||||
this.logService,
|
this.logService,
|
||||||
|
this.cipherEncryptionService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.folderService = new FolderService(
|
this.folderService = new FolderService(
|
||||||
|
|||||||
@@ -93,9 +93,7 @@ export class CreateCommand {
|
|||||||
const cipher = await this.cipherService.encrypt(CipherExport.toView(req), activeUserId);
|
const cipher = await this.cipherService.encrypt(CipherExport.toView(req), activeUserId);
|
||||||
try {
|
try {
|
||||||
const newCipher = await this.cipherService.createWithServer(cipher);
|
const newCipher = await this.cipherService.createWithServer(cipher);
|
||||||
const decCipher = await newCipher.decrypt(
|
const decCipher = await this.cipherService.decrypt(newCipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(newCipher, activeUserId),
|
|
||||||
);
|
|
||||||
const res = new CipherResponse(decCipher);
|
const res = new CipherResponse(decCipher);
|
||||||
return Response.success(res);
|
return Response.success(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -162,9 +160,7 @@ export class CreateCommand {
|
|||||||
new Uint8Array(fileBuf).buffer,
|
new Uint8Array(fileBuf).buffer,
|
||||||
activeUserId,
|
activeUserId,
|
||||||
);
|
);
|
||||||
const decCipher = await updatedCipher.decrypt(
|
const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
|
|
||||||
);
|
|
||||||
return Response.success(new CipherResponse(decCipher));
|
return Response.success(new CipherResponse(decCipher));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.error(e);
|
return Response.error(e);
|
||||||
|
|||||||
@@ -199,9 +199,7 @@ export class DesktopAutofillService implements OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const decrypted = await cipher.decrypt(
|
const decrypted = await this.cipherService.decrypt(cipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
|
||||||
);
|
|
||||||
|
|
||||||
const fido2Credential = decrypted.login.fido2Credentials?.[0];
|
const fido2Credential = decrypted.login.fido2Credentials?.[0];
|
||||||
if (!fido2Credential) {
|
if (!fido2Credential) {
|
||||||
|
|||||||
@@ -207,9 +207,7 @@ export class EncryptedMessageHandlerService {
|
|||||||
return { status: "failure" };
|
return { status: "failure" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const cipherView = await cipher.decrypt(
|
const cipherView = await this.cipherService.decrypt(cipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
|
||||||
);
|
|
||||||
cipherView.name = credentialUpdatePayload.name;
|
cipherView.name = credentialUpdatePayload.name;
|
||||||
cipherView.login.password = credentialUpdatePayload.password;
|
cipherView.login.password = credentialUpdatePayload.password;
|
||||||
cipherView.login.username = credentialUpdatePayload.userName;
|
cipherView.login.username = credentialUpdatePayload.userName;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
@@ -33,6 +34,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
|
|||||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
toastService: ToastService,
|
toastService: ToastService,
|
||||||
|
configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
cipherService,
|
cipherService,
|
||||||
@@ -49,6 +51,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
|
|||||||
billingAccountProfileStateService,
|
billingAccountProfileStateService,
|
||||||
accountService,
|
accountService,
|
||||||
toastService,
|
toastService,
|
||||||
|
configService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
|
|||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
toastService: ToastService,
|
toastService: ToastService,
|
||||||
cipherAuthorizationService: CipherAuthorizationService,
|
cipherAuthorizationService: CipherAuthorizationService,
|
||||||
private configService: ConfigService,
|
configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
cipherService,
|
cipherService,
|
||||||
@@ -100,6 +100,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
|
|||||||
billingAccountProfileStateService,
|
billingAccountProfileStateService,
|
||||||
toastService,
|
toastService,
|
||||||
cipherAuthorizationService,
|
cipherAuthorizationService,
|
||||||
|
configService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -481,9 +481,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
|||||||
activeUserId,
|
activeUserId,
|
||||||
);
|
);
|
||||||
|
|
||||||
updatedCipherView = await updatedCipher.decrypt(
|
updatedCipherView = await this.cipherService.decrypt(updatedCipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cipherFormComponent.patchCipher((currentCipher) => {
|
this.cipherFormComponent.patchCipher((currentCipher) => {
|
||||||
@@ -520,9 +518,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
return await config.originalCipher.decrypt(
|
return await this.cipherService.decrypt(config.originalCipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(config.originalCipher, activeUserId),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateTitle() {
|
private updateTitle() {
|
||||||
|
|||||||
@@ -50,9 +50,7 @@ export class CollectionsComponent implements OnInit {
|
|||||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
this.cipherDomain = await this.loadCipher(activeUserId);
|
this.cipherDomain = await this.loadCipher(activeUserId);
|
||||||
this.collectionIds = this.loadCipherCollections();
|
this.collectionIds = this.loadCipherCollections();
|
||||||
this.cipher = await this.cipherDomain.decrypt(
|
this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
|
|
||||||
);
|
|
||||||
this.collections = await this.loadCollections();
|
this.collections = await this.loadCollections();
|
||||||
|
|
||||||
this.collections.forEach((c) => ((c as any).checked = false));
|
this.collections.forEach((c) => ((c as any).checked = false));
|
||||||
|
|||||||
@@ -76,9 +76,7 @@ export class ShareComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
|
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
|
||||||
this.cipher = await cipherDomain.decrypt(
|
this.cipher = await this.cipherService.decrypt(cipherDomain, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
filterCollections() {
|
filterCollections() {
|
||||||
@@ -105,9 +103,7 @@ export class ShareComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
|
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
|
||||||
const cipherView = await cipherDomain.decrypt(
|
const cipherView = await this.cipherService.decrypt(cipherDomain, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
|
|
||||||
);
|
|
||||||
const orgs = await firstValueFrom(this.organizations$);
|
const orgs = await firstValueFrom(this.organizations$);
|
||||||
const orgName =
|
const orgName =
|
||||||
orgs.find((o) => o.id === this.organizationId)?.name ?? this.i18nService.t("organization");
|
orgs.find((o) => o.id === this.organizationId)?.name ?? this.i18nService.t("organization");
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ import {
|
|||||||
InternalSendService,
|
InternalSendService,
|
||||||
SendService as SendServiceAbstraction,
|
SendService as SendServiceAbstraction,
|
||||||
} from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
} from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||||
|
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
|
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
|
||||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||||
@@ -281,6 +282,7 @@ import {
|
|||||||
DefaultCipherAuthorizationService,
|
DefaultCipherAuthorizationService,
|
||||||
} from "@bitwarden/common/vault/services/cipher-authorization.service";
|
} from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
||||||
|
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service";
|
||||||
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
|
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
|
||||||
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
|
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
|
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
|
||||||
@@ -509,6 +511,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
stateProvider: StateProvider,
|
stateProvider: StateProvider,
|
||||||
accountService: AccountServiceAbstraction,
|
accountService: AccountServiceAbstraction,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
|
cipherEncryptionService: CipherEncryptionService,
|
||||||
) =>
|
) =>
|
||||||
new CipherService(
|
new CipherService(
|
||||||
keyService,
|
keyService,
|
||||||
@@ -525,6 +528,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
stateProvider,
|
stateProvider,
|
||||||
accountService,
|
accountService,
|
||||||
logService,
|
logService,
|
||||||
|
cipherEncryptionService,
|
||||||
),
|
),
|
||||||
deps: [
|
deps: [
|
||||||
KeyService,
|
KeyService,
|
||||||
@@ -541,6 +545,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
StateProvider,
|
StateProvider,
|
||||||
AccountServiceAbstraction,
|
AccountServiceAbstraction,
|
||||||
LogService,
|
LogService,
|
||||||
|
CipherEncryptionService,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
@@ -1528,6 +1533,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: MasterPasswordApiService,
|
useClass: MasterPasswordApiService,
|
||||||
deps: [ApiServiceAbstraction, LogService],
|
deps: [ApiServiceAbstraction, LogService],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: CipherEncryptionService,
|
||||||
|
useClass: DefaultCipherEncryptionService,
|
||||||
|
deps: [SdkService, LogService],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -269,9 +269,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||||||
if (this.cipher == null) {
|
if (this.cipher == null) {
|
||||||
if (this.editMode) {
|
if (this.editMode) {
|
||||||
const cipher = await this.loadCipher(activeUserId);
|
const cipher = await this.loadCipher(activeUserId);
|
||||||
this.cipher = await cipher.decrypt(
|
this.cipher = await this.cipherService.decrypt(cipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Adjust Cipher Name if Cloning
|
// Adjust Cipher Name if Cloning
|
||||||
if (this.cloneMode) {
|
if (this.cloneMode) {
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
|||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
|
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
|
||||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
@@ -56,6 +56,7 @@ export class AttachmentsComponent implements OnInit {
|
|||||||
protected billingAccountProfileStateService: BillingAccountProfileStateService,
|
protected billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
protected accountService: AccountService,
|
protected accountService: AccountService,
|
||||||
protected toastService: ToastService,
|
protected toastService: ToastService,
|
||||||
|
protected configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -88,9 +89,7 @@ export class AttachmentsComponent implements OnInit {
|
|||||||
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||||
this.formPromise = this.saveCipherAttachment(files[0], activeUserId);
|
this.formPromise = this.saveCipherAttachment(files[0], activeUserId);
|
||||||
this.cipherDomain = await this.formPromise;
|
this.cipherDomain = await this.formPromise;
|
||||||
this.cipher = await this.cipherDomain.decrypt(
|
this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
|
|
||||||
);
|
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
title: null,
|
title: null,
|
||||||
@@ -130,9 +129,7 @@ export class AttachmentsComponent implements OnInit {
|
|||||||
const updatedCipher = await this.deletePromises[attachment.id];
|
const updatedCipher = await this.deletePromises[attachment.id];
|
||||||
|
|
||||||
const cipher = new Cipher(updatedCipher);
|
const cipher = new Cipher(updatedCipher);
|
||||||
this.cipher = await cipher.decrypt(
|
this.cipher = await this.cipherService.decrypt(cipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
@@ -197,12 +194,14 @@ export class AttachmentsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
const key =
|
const decBuf = await this.cipherService.getDecryptedAttachmentBuffer(
|
||||||
attachment.key != null
|
this.cipherDomain.id as CipherId,
|
||||||
? attachment.key
|
attachment,
|
||||||
: await this.keyService.getOrgKey(this.cipher.organizationId);
|
response,
|
||||||
const decBuf = await this.encryptService.decryptFileData(encBuf, key);
|
activeUserId,
|
||||||
|
);
|
||||||
|
|
||||||
this.fileDownloadService.download({
|
this.fileDownloadService.download({
|
||||||
fileName: attachment.fileName,
|
fileName: attachment.fileName,
|
||||||
blobData: decBuf,
|
blobData: decBuf,
|
||||||
@@ -228,9 +227,7 @@ export class AttachmentsComponent implements OnInit {
|
|||||||
protected async init() {
|
protected async init() {
|
||||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
this.cipherDomain = await this.loadCipher(activeUserId);
|
this.cipherDomain = await this.loadCipher(activeUserId);
|
||||||
this.cipher = await this.cipherDomain.decrypt(
|
this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
|
|
||||||
);
|
|
||||||
|
|
||||||
const canAccessPremium = await firstValueFrom(
|
const canAccessPremium = await firstValueFrom(
|
||||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
|
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
|
||||||
@@ -276,15 +273,17 @@ export class AttachmentsComponent implements OnInit {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 2. Resave
|
// 2. Resave
|
||||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
|
||||||
const key =
|
|
||||||
attachment.key != null
|
|
||||||
? attachment.key
|
|
||||||
: await this.keyService.getOrgKey(this.cipher.organizationId);
|
|
||||||
const decBuf = await this.encryptService.decryptFileData(encBuf, key);
|
|
||||||
const activeUserId = await firstValueFrom(
|
const activeUserId = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(getUserId),
|
this.accountService.activeAccount$.pipe(getUserId),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const decBuf = await this.cipherService.getDecryptedAttachmentBuffer(
|
||||||
|
this.cipherDomain.id as CipherId,
|
||||||
|
attachment,
|
||||||
|
response,
|
||||||
|
activeUserId,
|
||||||
|
);
|
||||||
|
|
||||||
this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer(
|
this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer(
|
||||||
this.cipherDomain,
|
this.cipherDomain,
|
||||||
attachment.fileName,
|
attachment.fileName,
|
||||||
@@ -292,9 +291,7 @@ export class AttachmentsComponent implements OnInit {
|
|||||||
activeUserId,
|
activeUserId,
|
||||||
admin,
|
admin,
|
||||||
);
|
);
|
||||||
this.cipher = await this.cipherDomain.decrypt(
|
this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. Delete old
|
// 3. Delete old
|
||||||
this.deletePromises[attachment.id] = this.deleteCipherAttachment(
|
this.deletePromises[attachment.id] = this.deleteCipherAttachment(
|
||||||
|
|||||||
@@ -42,9 +42,7 @@ export class PasswordHistoryComponent implements OnInit {
|
|||||||
protected async init() {
|
protected async init() {
|
||||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
const cipher = await this.cipherService.get(this.cipherId, activeUserId);
|
const cipher = await this.cipherService.get(this.cipherId, activeUserId);
|
||||||
const decCipher = await cipher.decrypt(
|
const decCipher = await this.cipherService.decrypt(cipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
|
||||||
);
|
|
||||||
this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory;
|
this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ import { EventType } from "@bitwarden/common/enums";
|
|||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
import { CipherId, CollectionId, UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CollectionId, UserId } from "@bitwarden/common/types/guid";
|
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||||
@@ -137,6 +137,7 @@ export class ViewComponent implements OnDestroy, OnInit {
|
|||||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
protected toastService: ToastService,
|
protected toastService: ToastService,
|
||||||
private cipherAuthorizationService: CipherAuthorizationService,
|
private cipherAuthorizationService: CipherAuthorizationService,
|
||||||
|
protected configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -458,19 +459,19 @@ export class ViewComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
const key =
|
const decBuf = await this.cipherService.getDecryptedAttachmentBuffer(
|
||||||
attachment.key != null
|
this.cipher.id as CipherId,
|
||||||
? attachment.key
|
attachment,
|
||||||
: await this.keyService.getOrgKey(this.cipher.organizationId);
|
response,
|
||||||
const decBuf = await this.encryptService.decryptFileData(encBuf, key);
|
activeUserId,
|
||||||
|
);
|
||||||
|
|
||||||
this.fileDownloadService.download({
|
this.fileDownloadService.download({
|
||||||
fileName: attachment.fileName,
|
fileName: attachment.fileName,
|
||||||
blobData: decBuf,
|
blobData: decBuf,
|
||||||
});
|
});
|
||||||
// FIXME: Remove when updating file. Eslint update
|
} catch {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
} catch (e) {
|
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: null,
|
title: null,
|
||||||
|
|||||||
@@ -64,6 +64,20 @@ export function makeSymmetricCryptoKey<T extends SymmetricCryptoKey>(
|
|||||||
*/
|
*/
|
||||||
export const mockFromJson = (stub: any) => (stub + "_fromJSON") as any;
|
export const mockFromJson = (stub: any) => (stub + "_fromJSON") as any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use to mock a return value of a static fromSdk method.
|
||||||
|
*/
|
||||||
|
export const mockFromSdk = (stub: any) => {
|
||||||
|
if (typeof stub === "object") {
|
||||||
|
return {
|
||||||
|
...stub,
|
||||||
|
__fromSdk: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${stub}_fromSdk`;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks the emissions of the given observable.
|
* Tracks the emissions of the given observable.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export enum FeatureFlag {
|
|||||||
PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge",
|
PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge",
|
||||||
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
|
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
|
||||||
SecurityTasks = "security-tasks",
|
SecurityTasks = "security-tasks",
|
||||||
|
PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk",
|
||||||
CipherKeyEncryption = "cipher-key-encryption",
|
CipherKeyEncryption = "cipher-key-encryption",
|
||||||
PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms",
|
PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms",
|
||||||
EndUserNotifications = "pm-10609-end-user-notifications",
|
EndUserNotifications = "pm-10609-end-user-notifications",
|
||||||
@@ -111,6 +112,7 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||||
[FeatureFlag.PM18520_UpdateDesktopCipherForm]: FALSE,
|
[FeatureFlag.PM18520_UpdateDesktopCipherForm]: FALSE,
|
||||||
[FeatureFlag.EndUserNotifications]: FALSE,
|
[FeatureFlag.EndUserNotifications]: FALSE,
|
||||||
|
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
|
||||||
|
|
||||||
/* Auth */
|
/* Auth */
|
||||||
[FeatureFlag.PM9115_TwoFactorExtensionDataPersistence]: FALSE,
|
[FeatureFlag.PM9115_TwoFactorExtensionDataPersistence]: FALSE,
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ describe("FidoAuthenticatorService", () => {
|
|||||||
id === excludedCipher.id ? ({ decrypt: () => excludedCipher } as any) : undefined,
|
id === excludedCipher.id ? ({ decrypt: () => excludedCipher } as any) : undefined,
|
||||||
);
|
);
|
||||||
cipherService.getAllDecrypted.mockResolvedValue([excludedCipher]);
|
cipherService.getAllDecrypted.mockResolvedValue([excludedCipher]);
|
||||||
|
cipherService.decrypt.mockResolvedValue(excludedCipher);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -220,6 +221,7 @@ describe("FidoAuthenticatorService", () => {
|
|||||||
id === existingCipher.id ? ({ decrypt: () => existingCipher } as any) : undefined,
|
id === existingCipher.id ? ({ decrypt: () => existingCipher } as any) : undefined,
|
||||||
);
|
);
|
||||||
cipherService.getAllDecrypted.mockResolvedValue([existingCipher]);
|
cipherService.getAllDecrypted.mockResolvedValue([existingCipher]);
|
||||||
|
cipherService.decrypt.mockResolvedValue(existingCipher);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -306,6 +308,11 @@ describe("FidoAuthenticatorService", () => {
|
|||||||
const encryptedCipher = { ...existingCipher, reprompt: CipherRepromptType.Password };
|
const encryptedCipher = { ...existingCipher, reprompt: CipherRepromptType.Password };
|
||||||
cipherService.get.mockResolvedValue(encryptedCipher as unknown as Cipher);
|
cipherService.get.mockResolvedValue(encryptedCipher as unknown as Cipher);
|
||||||
|
|
||||||
|
cipherService.decrypt.mockResolvedValue({
|
||||||
|
...existingCipher,
|
||||||
|
reprompt: CipherRepromptType.Password,
|
||||||
|
} as unknown as CipherView);
|
||||||
|
|
||||||
const result = async () => await authenticator.makeCredential(params, windowReference);
|
const result = async () => await authenticator.makeCredential(params, windowReference);
|
||||||
|
|
||||||
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
|
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
|
||||||
@@ -347,6 +354,7 @@ describe("FidoAuthenticatorService", () => {
|
|||||||
cipherId === cipher.id ? ({ decrypt: () => cipher } as any) : undefined,
|
cipherId === cipher.id ? ({ decrypt: () => cipher } as any) : undefined,
|
||||||
);
|
);
|
||||||
cipherService.getAllDecrypted.mockResolvedValue([await cipher]);
|
cipherService.getAllDecrypted.mockResolvedValue([await cipher]);
|
||||||
|
cipherService.decrypt.mockResolvedValue(cipher);
|
||||||
cipherService.encrypt.mockImplementation(async (cipher) => {
|
cipherService.encrypt.mockImplementation(async (cipher) => {
|
||||||
cipher.login.fido2Credentials[0].credentialId = credentialId; // Replace id for testability
|
cipher.login.fido2Credentials[0].credentialId = credentialId; // Replace id for testability
|
||||||
return {} as any;
|
return {} as any;
|
||||||
|
|||||||
@@ -151,9 +151,7 @@ export class Fido2AuthenticatorService<ParentWindowReference>
|
|||||||
);
|
);
|
||||||
const encrypted = await this.cipherService.get(cipherId, activeUserId);
|
const encrypted = await this.cipherService.get(cipherId, activeUserId);
|
||||||
|
|
||||||
cipher = await encrypted.decrypt(
|
cipher = await this.cipherService.decrypt(encrypted, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(encrypted, activeUserId),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!userVerified &&
|
!userVerified &&
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
|
import { Cipher } from "../models/domain/cipher";
|
||||||
|
import { AttachmentView } from "../models/view/attachment.view";
|
||||||
|
import { CipherView } from "../models/view/cipher.view";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service responsible for encrypting and decrypting ciphers.
|
||||||
|
*/
|
||||||
|
export abstract class CipherEncryptionService {
|
||||||
|
/**
|
||||||
|
* Decrypts a cipher using the SDK for the given userId.
|
||||||
|
*
|
||||||
|
* @param cipher The encrypted cipher object
|
||||||
|
* @param userId The user ID whose key will be used for decryption
|
||||||
|
*
|
||||||
|
* @returns A promise that resolves to the decrypted cipher view
|
||||||
|
*/
|
||||||
|
abstract decrypt(cipher: Cipher, userId: UserId): Promise<CipherView>;
|
||||||
|
/**
|
||||||
|
* Decrypts many ciphers using the SDK for the given userId.
|
||||||
|
*
|
||||||
|
* For bulk decryption, prefer using `decryptMany`, which returns a more efficient
|
||||||
|
* `CipherListView` object.
|
||||||
|
*
|
||||||
|
* @param ciphers The encrypted cipher objects
|
||||||
|
* @param userId The user ID whose key will be used for decryption
|
||||||
|
*
|
||||||
|
* @deprecated Use `decryptMany` for bulk decryption instead.
|
||||||
|
*
|
||||||
|
* @returns A promise that resolves to an array of decrypted cipher views
|
||||||
|
*/
|
||||||
|
abstract decryptManyLegacy(ciphers: Cipher[], userId: UserId): Promise<CipherView[]>;
|
||||||
|
/**
|
||||||
|
* Decrypts many ciphers using the SDK for the given userId.
|
||||||
|
*
|
||||||
|
* @param ciphers The encrypted cipher objects
|
||||||
|
* @param userId The user ID whose key will be used for decryption
|
||||||
|
*
|
||||||
|
* @returns A promise that resolves to an array of decrypted cipher list views
|
||||||
|
*/
|
||||||
|
abstract decryptMany(ciphers: Cipher[], userId: UserId): Promise<CipherListView[]>;
|
||||||
|
/**
|
||||||
|
* Decrypts an attachment's content from a response object.
|
||||||
|
*
|
||||||
|
* @param cipher The encrypted cipher object that owns the attachment
|
||||||
|
* @param attachment The attachment view object
|
||||||
|
* @param encryptedContent The encrypted content of the attachment
|
||||||
|
* @param userId The user ID whose key will be used for decryption
|
||||||
|
*
|
||||||
|
* @returns A promise that resolves to the decrypted content
|
||||||
|
*/
|
||||||
|
abstract decryptAttachmentContent(
|
||||||
|
cipher: Cipher,
|
||||||
|
attachment: AttachmentView,
|
||||||
|
encryptedContent: Uint8Array,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<Uint8Array>;
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import { LocalData } from "../models/data/local.data";
|
|||||||
import { Cipher } from "../models/domain/cipher";
|
import { Cipher } from "../models/domain/cipher";
|
||||||
import { Field } from "../models/domain/field";
|
import { Field } from "../models/domain/field";
|
||||||
import { CipherWithIdRequest } from "../models/request/cipher-with-id.request";
|
import { CipherWithIdRequest } from "../models/request/cipher-with-id.request";
|
||||||
|
import { AttachmentView } from "../models/view/attachment.view";
|
||||||
import { CipherView } from "../models/view/cipher.view";
|
import { CipherView } from "../models/view/cipher.view";
|
||||||
import { FieldView } from "../models/view/field.view";
|
import { FieldView } from "../models/view/field.view";
|
||||||
import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
|
import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
|
||||||
@@ -215,4 +216,28 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
|||||||
): Promise<CipherWithIdRequest[]>;
|
): Promise<CipherWithIdRequest[]>;
|
||||||
abstract getNextCardCipher(userId: UserId): Promise<CipherView>;
|
abstract getNextCardCipher(userId: UserId): Promise<CipherView>;
|
||||||
abstract getNextIdentityCipher(userId: UserId): Promise<CipherView>;
|
abstract getNextIdentityCipher(userId: UserId): Promise<CipherView>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts a cipher using either the SDK or the legacy method based on the feature flag.
|
||||||
|
* @param cipher The cipher to decrypt.
|
||||||
|
* @param userId The user ID to use for decryption.
|
||||||
|
* @returns A promise that resolves to the decrypted cipher view.
|
||||||
|
*/
|
||||||
|
abstract decrypt(cipher: Cipher, userId: UserId): Promise<CipherView>;
|
||||||
|
/**
|
||||||
|
* Decrypts an attachment's content from a response object.
|
||||||
|
*
|
||||||
|
* @param cipherId The ID of the cipher that owns the attachment
|
||||||
|
* @param attachment The attachment view object
|
||||||
|
* @param response The response object containing the encrypted content
|
||||||
|
* @param userId The user ID whose key will be used for decryption
|
||||||
|
*
|
||||||
|
* @returns A promise that resolves to the decrypted content
|
||||||
|
*/
|
||||||
|
abstract getDecryptedAttachmentBuffer(
|
||||||
|
cipherId: CipherId,
|
||||||
|
attachment: AttachmentView,
|
||||||
|
response: Response,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<Uint8Array | null>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { CipherPermissions as SdkCipherPermissions } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { BaseResponse } from "../../../models/response/base.response";
|
import { BaseResponse } from "../../../models/response/base.response";
|
||||||
|
|
||||||
export class CipherPermissionsApi extends BaseResponse {
|
export class CipherPermissionsApi extends BaseResponse {
|
||||||
@@ -18,4 +20,19 @@ export class CipherPermissionsApi extends BaseResponse {
|
|||||||
static fromJSON(obj: Jsonify<CipherPermissionsApi>) {
|
static fromJSON(obj: Jsonify<CipherPermissionsApi>) {
|
||||||
return Object.assign(new CipherPermissionsApi(), obj);
|
return Object.assign(new CipherPermissionsApi(), obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the SDK CipherPermissionsApi to a CipherPermissionsApi.
|
||||||
|
*/
|
||||||
|
static fromSdkCipherPermissions(obj: SdkCipherPermissions): CipherPermissionsApi | undefined {
|
||||||
|
if (!obj) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions = new CipherPermissionsApi();
|
||||||
|
permissions.delete = obj.delete;
|
||||||
|
permissions.restore = obj.restore;
|
||||||
|
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export class CipherData {
|
|||||||
passwordHistory?: PasswordHistoryData[];
|
passwordHistory?: PasswordHistoryData[];
|
||||||
collectionIds?: string[];
|
collectionIds?: string[];
|
||||||
creationDate: string;
|
creationDate: string;
|
||||||
deletedDate: string;
|
deletedDate: string | null;
|
||||||
reprompt: CipherRepromptType;
|
reprompt: CipherRepromptType;
|
||||||
key: string;
|
key: string;
|
||||||
|
|
||||||
|
|||||||
@@ -153,4 +153,21 @@ describe("Attachment", () => {
|
|||||||
expect(Attachment.fromJSON(null)).toBeNull();
|
expect(Attachment.fromJSON(null)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("toSdkAttachment", () => {
|
||||||
|
it("should map to SDK Attachment", () => {
|
||||||
|
const attachment = new Attachment(data);
|
||||||
|
|
||||||
|
const sdkAttachment = attachment.toSdkAttachment();
|
||||||
|
|
||||||
|
expect(sdkAttachment).toEqual({
|
||||||
|
id: "id",
|
||||||
|
url: "url",
|
||||||
|
size: "1100",
|
||||||
|
sizeName: "1.1 KB",
|
||||||
|
fileName: "fileName",
|
||||||
|
key: "key",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { Attachment as SdkAttachment } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { Utils } from "../../../platform/misc/utils";
|
import { Utils } from "../../../platform/misc/utils";
|
||||||
import Domain from "../../../platform/models/domain/domain-base";
|
import Domain from "../../../platform/models/domain/domain-base";
|
||||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||||
@@ -113,4 +115,20 @@ export class Attachment extends Domain {
|
|||||||
fileName,
|
fileName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps to SDK Attachment
|
||||||
|
*
|
||||||
|
* @returns {SdkAttachment} - The SDK Attachment object
|
||||||
|
*/
|
||||||
|
toSdkAttachment(): SdkAttachment {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
url: this.url,
|
||||||
|
size: this.size,
|
||||||
|
sizeName: this.sizeName,
|
||||||
|
fileName: this.fileName?.toJSON(),
|
||||||
|
key: this.key?.toJSON(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,4 +99,21 @@ describe("Card", () => {
|
|||||||
expect(Card.fromJSON(null)).toBeNull();
|
expect(Card.fromJSON(null)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("toSdkCard", () => {
|
||||||
|
it("should map to SDK Card", () => {
|
||||||
|
const card = new Card(data);
|
||||||
|
|
||||||
|
const sdkCard = card.toSdkCard();
|
||||||
|
|
||||||
|
expect(sdkCard).toEqual({
|
||||||
|
cardholderName: "encHolder",
|
||||||
|
brand: "encBrand",
|
||||||
|
number: "encNumber",
|
||||||
|
expMonth: "encMonth",
|
||||||
|
expYear: "encYear",
|
||||||
|
code: "encCode",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { Card as SdkCard } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import Domain from "../../../platform/models/domain/domain-base";
|
import Domain from "../../../platform/models/domain/domain-base";
|
||||||
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";
|
||||||
@@ -85,4 +87,20 @@ export class Card extends Domain {
|
|||||||
code,
|
code,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps Card to SDK format.
|
||||||
|
*
|
||||||
|
* @returns {SdkCard} The SDK card object.
|
||||||
|
*/
|
||||||
|
toSdkCard(): SdkCard {
|
||||||
|
return {
|
||||||
|
cardholderName: this.cardholderName?.toJSON(),
|
||||||
|
brand: this.brand?.toJSON(),
|
||||||
|
number: this.number?.toJSON(),
|
||||||
|
expMonth: this.expMonth?.toJSON(),
|
||||||
|
expYear: this.expYear?.toJSON(),
|
||||||
|
code: this.code?.toJSON(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ import { Jsonify } from "type-fest";
|
|||||||
|
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
import {
|
||||||
|
CipherType as SdkCipherType,
|
||||||
|
UriMatchType,
|
||||||
|
CipherRepromptType as SdkCipherRepromptType,
|
||||||
|
LoginLinkedIdType,
|
||||||
|
} from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils";
|
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils";
|
||||||
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
|
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
|
||||||
@@ -12,7 +18,7 @@ import { ContainerService } from "../../../platform/services/container.service";
|
|||||||
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
|
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { CipherService } from "../../abstractions/cipher.service";
|
import { CipherService } from "../../abstractions/cipher.service";
|
||||||
import { FieldType, SecureNoteType } from "../../enums";
|
import { FieldType, LoginLinkedId, SecureNoteType } from "../../enums";
|
||||||
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
||||||
import { CipherType } from "../../enums/cipher-type";
|
import { CipherType } from "../../enums/cipher-type";
|
||||||
import { CipherData } from "../../models/data/cipher.data";
|
import { CipherData } from "../../models/data/cipher.data";
|
||||||
@@ -770,6 +776,165 @@ describe("Cipher DTO", () => {
|
|||||||
expect(Cipher.fromJSON(null)).toBeNull();
|
expect(Cipher.fromJSON(null)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("toSdkCipher", () => {
|
||||||
|
it("should map to SDK Cipher", () => {
|
||||||
|
const lastUsedDate = new Date("2025-04-15T12:00:00.000Z").getTime();
|
||||||
|
const lastLaunched = new Date("2025-04-15T12:00:00.000Z").getTime();
|
||||||
|
|
||||||
|
const cipherData: CipherData = {
|
||||||
|
id: "id",
|
||||||
|
organizationId: "orgId",
|
||||||
|
folderId: "folderId",
|
||||||
|
edit: true,
|
||||||
|
permissions: new CipherPermissionsApi(),
|
||||||
|
viewPassword: true,
|
||||||
|
organizationUseTotp: true,
|
||||||
|
favorite: false,
|
||||||
|
revisionDate: "2022-01-31T12:00:00.000Z",
|
||||||
|
type: CipherType.Login,
|
||||||
|
name: "EncryptedString",
|
||||||
|
notes: "EncryptedString",
|
||||||
|
creationDate: "2022-01-01T12:00:00.000Z",
|
||||||
|
deletedDate: null,
|
||||||
|
reprompt: CipherRepromptType.None,
|
||||||
|
key: "EncryptedString",
|
||||||
|
login: {
|
||||||
|
uris: [
|
||||||
|
{
|
||||||
|
uri: "EncryptedString",
|
||||||
|
uriChecksum: "EncryptedString",
|
||||||
|
match: UriMatchStrategy.Domain,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
username: "EncryptedString",
|
||||||
|
password: "EncryptedString",
|
||||||
|
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
|
||||||
|
totp: "EncryptedString",
|
||||||
|
autofillOnPageLoad: false,
|
||||||
|
},
|
||||||
|
passwordHistory: [
|
||||||
|
{ password: "EncryptedString", lastUsedDate: "2022-01-31T12:00:00.000Z" },
|
||||||
|
],
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
url: "url",
|
||||||
|
size: "1100",
|
||||||
|
sizeName: "1.1 KB",
|
||||||
|
fileName: "file",
|
||||||
|
key: "EncKey",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a2",
|
||||||
|
url: "url",
|
||||||
|
size: "1100",
|
||||||
|
sizeName: "1.1 KB",
|
||||||
|
fileName: "file",
|
||||||
|
key: "EncKey",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "EncryptedString",
|
||||||
|
value: "EncryptedString",
|
||||||
|
type: FieldType.Linked,
|
||||||
|
linkedId: LoginLinkedId.Username,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EncryptedString",
|
||||||
|
value: "EncryptedString",
|
||||||
|
type: FieldType.Linked,
|
||||||
|
linkedId: LoginLinkedId.Password,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const cipher = new Cipher(cipherData, { lastUsedDate, lastLaunched });
|
||||||
|
const sdkCipher = cipher.toSdkCipher();
|
||||||
|
|
||||||
|
expect(sdkCipher).toEqual({
|
||||||
|
id: "id",
|
||||||
|
organizationId: "orgId",
|
||||||
|
folderId: "folderId",
|
||||||
|
collectionIds: [],
|
||||||
|
key: "EncryptedString",
|
||||||
|
name: "EncryptedString",
|
||||||
|
notes: "EncryptedString",
|
||||||
|
type: SdkCipherType.Login,
|
||||||
|
login: {
|
||||||
|
username: "EncryptedString",
|
||||||
|
password: "EncryptedString",
|
||||||
|
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
|
||||||
|
uris: [
|
||||||
|
{
|
||||||
|
uri: "EncryptedString",
|
||||||
|
uriChecksum: "EncryptedString",
|
||||||
|
match: UriMatchType.Domain,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
totp: "EncryptedString",
|
||||||
|
autofillOnPageLoad: false,
|
||||||
|
fido2Credentials: undefined,
|
||||||
|
},
|
||||||
|
identity: undefined,
|
||||||
|
card: undefined,
|
||||||
|
secureNote: undefined,
|
||||||
|
sshKey: undefined,
|
||||||
|
favorite: false,
|
||||||
|
reprompt: SdkCipherRepromptType.None,
|
||||||
|
organizationUseTotp: true,
|
||||||
|
edit: true,
|
||||||
|
permissions: new CipherPermissionsApi(),
|
||||||
|
viewPassword: true,
|
||||||
|
localData: {
|
||||||
|
lastUsedDate: "2025-04-15T12:00:00.000Z",
|
||||||
|
lastLaunched: "2025-04-15T12:00:00.000Z",
|
||||||
|
},
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
url: "url",
|
||||||
|
size: "1100",
|
||||||
|
sizeName: "1.1 KB",
|
||||||
|
fileName: "file",
|
||||||
|
key: "EncKey",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a2",
|
||||||
|
url: "url",
|
||||||
|
size: "1100",
|
||||||
|
sizeName: "1.1 KB",
|
||||||
|
fileName: "file",
|
||||||
|
key: "EncKey",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "EncryptedString",
|
||||||
|
value: "EncryptedString",
|
||||||
|
type: FieldType.Linked,
|
||||||
|
linkedId: LoginLinkedIdType.Username,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EncryptedString",
|
||||||
|
value: "EncryptedString",
|
||||||
|
type: FieldType.Linked,
|
||||||
|
linkedId: LoginLinkedIdType.Password,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
passwordHistory: [
|
||||||
|
{
|
||||||
|
password: "EncryptedString",
|
||||||
|
lastUsedDate: "2022-01-31T12:00:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
creationDate: "2022-01-01T12:00:00.000Z",
|
||||||
|
deletedDate: undefined,
|
||||||
|
revisionDate: "2022-01-31T12:00:00.000Z",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockUserId = "TestUserId" as UserId;
|
const mockUserId = "TestUserId" as UserId;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { Cipher as SdkCipher } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||||
import { Utils } from "../../../platform/misc/utils";
|
import { Utils } from "../../../platform/misc/utils";
|
||||||
import Domain from "../../../platform/models/domain/domain-base";
|
import Domain from "../../../platform/models/domain/domain-base";
|
||||||
@@ -330,4 +332,72 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
|||||||
|
|
||||||
return domain;
|
return domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps Cipher to SDK format.
|
||||||
|
*
|
||||||
|
* @returns {SdkCipher} The SDK cipher object.
|
||||||
|
*/
|
||||||
|
toSdkCipher(): SdkCipher {
|
||||||
|
const sdkCipher: SdkCipher = {
|
||||||
|
id: this.id,
|
||||||
|
organizationId: this.organizationId,
|
||||||
|
folderId: this.folderId,
|
||||||
|
collectionIds: this.collectionIds || [],
|
||||||
|
key: this.key?.toJSON(),
|
||||||
|
name: this.name.toJSON(),
|
||||||
|
notes: this.notes?.toJSON(),
|
||||||
|
type: this.type,
|
||||||
|
favorite: this.favorite,
|
||||||
|
organizationUseTotp: this.organizationUseTotp,
|
||||||
|
edit: this.edit,
|
||||||
|
permissions: this.permissions,
|
||||||
|
viewPassword: this.viewPassword,
|
||||||
|
localData: this.localData
|
||||||
|
? {
|
||||||
|
lastUsedDate: this.localData.lastUsedDate
|
||||||
|
? new Date(this.localData.lastUsedDate).toISOString()
|
||||||
|
: undefined,
|
||||||
|
lastLaunched: this.localData.lastLaunched
|
||||||
|
? new Date(this.localData.lastLaunched).toISOString()
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
attachments: this.attachments?.map((a) => a.toSdkAttachment()),
|
||||||
|
fields: this.fields?.map((f) => f.toSdkField()),
|
||||||
|
passwordHistory: this.passwordHistory?.map((ph) => ph.toSdkPasswordHistory()),
|
||||||
|
revisionDate: this.revisionDate?.toISOString(),
|
||||||
|
creationDate: this.creationDate?.toISOString(),
|
||||||
|
deletedDate: this.deletedDate?.toISOString(),
|
||||||
|
reprompt: this.reprompt,
|
||||||
|
// Initialize all cipher-type-specific properties as undefined
|
||||||
|
login: undefined,
|
||||||
|
identity: undefined,
|
||||||
|
card: undefined,
|
||||||
|
secureNote: undefined,
|
||||||
|
sshKey: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (this.type) {
|
||||||
|
case CipherType.Login:
|
||||||
|
sdkCipher.login = this.login.toSdkLogin();
|
||||||
|
break;
|
||||||
|
case CipherType.SecureNote:
|
||||||
|
sdkCipher.secureNote = this.secureNote.toSdkSecureNote();
|
||||||
|
break;
|
||||||
|
case CipherType.Card:
|
||||||
|
sdkCipher.card = this.card.toSdkCard();
|
||||||
|
break;
|
||||||
|
case CipherType.Identity:
|
||||||
|
sdkCipher.identity = this.identity.toSdkIdentity();
|
||||||
|
break;
|
||||||
|
case CipherType.SshKey:
|
||||||
|
sdkCipher.sshKey = this.sshKey.toSdkSshKey();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdkCipher;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,45 @@ describe("Fido2Credential", () => {
|
|||||||
expect(Fido2Credential.fromJSON(null)).toBeNull();
|
expect(Fido2Credential.fromJSON(null)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("SDK Fido2Credential Mapping", () => {
|
||||||
|
it("should map to SDK Fido2Credential", () => {
|
||||||
|
const data: Fido2CredentialData = {
|
||||||
|
credentialId: "credentialId",
|
||||||
|
keyType: "public-key",
|
||||||
|
keyAlgorithm: "ECDSA",
|
||||||
|
keyCurve: "P-256",
|
||||||
|
keyValue: "keyValue",
|
||||||
|
rpId: "rpId",
|
||||||
|
userHandle: "userHandle",
|
||||||
|
userName: "userName",
|
||||||
|
counter: "2",
|
||||||
|
rpName: "rpName",
|
||||||
|
userDisplayName: "userDisplayName",
|
||||||
|
discoverable: "discoverable",
|
||||||
|
creationDate: mockDate.toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const credential = new Fido2Credential(data);
|
||||||
|
const sdkCredential = credential.toSdkFido2Credential();
|
||||||
|
|
||||||
|
expect(sdkCredential).toEqual({
|
||||||
|
credentialId: "credentialId",
|
||||||
|
keyType: "public-key",
|
||||||
|
keyAlgorithm: "ECDSA",
|
||||||
|
keyCurve: "P-256",
|
||||||
|
keyValue: "keyValue",
|
||||||
|
rpId: "rpId",
|
||||||
|
userHandle: "userHandle",
|
||||||
|
userName: "userName",
|
||||||
|
counter: "2",
|
||||||
|
rpName: "rpName",
|
||||||
|
userDisplayName: "userDisplayName",
|
||||||
|
discoverable: "discoverable",
|
||||||
|
creationDate: mockDate.toISOString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function createEncryptedEncString(s: string): EncString {
|
function createEncryptedEncString(s: string): EncString {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { Fido2Credential as SdkFido2Credential } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import Domain from "../../../platform/models/domain/domain-base";
|
import Domain from "../../../platform/models/domain/domain-base";
|
||||||
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";
|
||||||
@@ -148,4 +150,27 @@ export class Fido2Credential extends Domain {
|
|||||||
creationDate,
|
creationDate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps Fido2Credential to SDK format.
|
||||||
|
*
|
||||||
|
* @returns {SdkFido2Credential} The SDK Fido2Credential object.
|
||||||
|
*/
|
||||||
|
toSdkFido2Credential(): SdkFido2Credential {
|
||||||
|
return {
|
||||||
|
credentialId: this.credentialId?.toJSON(),
|
||||||
|
keyType: this.keyType.toJSON(),
|
||||||
|
keyAlgorithm: this.keyAlgorithm.toJSON(),
|
||||||
|
keyCurve: this.keyCurve.toJSON(),
|
||||||
|
keyValue: this.keyValue.toJSON(),
|
||||||
|
rpId: this.rpId.toJSON(),
|
||||||
|
userHandle: this.userHandle.toJSON(),
|
||||||
|
userName: this.userName.toJSON(),
|
||||||
|
counter: this.counter.toJSON(),
|
||||||
|
rpName: this.rpName?.toJSON(),
|
||||||
|
userDisplayName: this.userDisplayName?.toJSON(),
|
||||||
|
discoverable: this.discoverable?.toJSON(),
|
||||||
|
creationDate: this.creationDate.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { mockEnc, mockFromJson } from "../../../../spec";
|
import { mockEnc, mockFromJson } from "../../../../spec";
|
||||||
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
|
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
|
||||||
import { FieldType } from "../../enums";
|
import { CardLinkedId, FieldType, IdentityLinkedId, LoginLinkedId } from "../../enums";
|
||||||
import { FieldData } from "../../models/data/field.data";
|
import { FieldData } from "../../models/data/field.data";
|
||||||
import { Field } from "../../models/domain/field";
|
import { Field } from "../../models/domain/field";
|
||||||
|
|
||||||
@@ -82,4 +82,26 @@ describe("Field", () => {
|
|||||||
expect(Field.fromJSON(null)).toBeNull();
|
expect(Field.fromJSON(null)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("SDK Field Mapping", () => {
|
||||||
|
it("should map to SDK Field", () => {
|
||||||
|
// Test Login LinkedId
|
||||||
|
const loginField = new Field(data);
|
||||||
|
loginField.type = FieldType.Linked;
|
||||||
|
loginField.linkedId = LoginLinkedId.Username;
|
||||||
|
expect(loginField.toSdkField().linkedId).toBe(100);
|
||||||
|
|
||||||
|
// Test Card LinkedId
|
||||||
|
const cardField = new Field(data);
|
||||||
|
cardField.type = FieldType.Linked;
|
||||||
|
cardField.linkedId = CardLinkedId.Number;
|
||||||
|
expect(cardField.toSdkField().linkedId).toBe(305);
|
||||||
|
|
||||||
|
// Test Identity LinkedId
|
||||||
|
const identityField = new Field(data);
|
||||||
|
identityField.type = FieldType.Linked;
|
||||||
|
identityField.linkedId = IdentityLinkedId.LicenseNumber;
|
||||||
|
expect(identityField.toSdkField().linkedId).toBe(415);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { Field as SdkField, LinkedIdType as SdkLinkedIdType } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import Domain from "../../../platform/models/domain/domain-base";
|
import Domain from "../../../platform/models/domain/domain-base";
|
||||||
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";
|
||||||
@@ -73,4 +75,19 @@ export class Field extends Domain {
|
|||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps Field to SDK format.
|
||||||
|
*
|
||||||
|
* @returns {SdkField} The SDK field object.
|
||||||
|
*/
|
||||||
|
toSdkField(): SdkField {
|
||||||
|
return {
|
||||||
|
name: this.name?.toJSON(),
|
||||||
|
value: this.value?.toJSON(),
|
||||||
|
type: this.type,
|
||||||
|
// Safe type cast: client and SDK LinkedIdType enums have identical values
|
||||||
|
linkedId: this.linkedId as unknown as SdkLinkedIdType,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,4 +184,32 @@ describe("Identity", () => {
|
|||||||
expect(Identity.fromJSON(null)).toBeNull();
|
expect(Identity.fromJSON(null)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("toSdkIdentity", () => {
|
||||||
|
it("returns the correct SDK Identity object", () => {
|
||||||
|
const identity = new Identity(data);
|
||||||
|
const sdkIdentity = identity.toSdkIdentity();
|
||||||
|
|
||||||
|
expect(sdkIdentity).toEqual({
|
||||||
|
title: "enctitle",
|
||||||
|
firstName: "encfirstName",
|
||||||
|
middleName: "encmiddleName",
|
||||||
|
lastName: "enclastName",
|
||||||
|
address1: "encaddress1",
|
||||||
|
address2: "encaddress2",
|
||||||
|
address3: "encaddress3",
|
||||||
|
city: "enccity",
|
||||||
|
state: "encstate",
|
||||||
|
postalCode: "encpostalCode",
|
||||||
|
country: "enccountry",
|
||||||
|
company: "enccompany",
|
||||||
|
email: "encemail",
|
||||||
|
phone: "encphone",
|
||||||
|
ssn: "encssn",
|
||||||
|
username: "encusername",
|
||||||
|
passportNumber: "encpassportNumber",
|
||||||
|
licenseNumber: "enclicenseNumber",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { Identity as SdkIdentity } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import Domain from "../../../platform/models/domain/domain-base";
|
import Domain from "../../../platform/models/domain/domain-base";
|
||||||
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";
|
||||||
@@ -165,4 +167,32 @@ export class Identity extends Domain {
|
|||||||
licenseNumber,
|
licenseNumber,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps Identity to SDK format.
|
||||||
|
*
|
||||||
|
* @returns {SdkIdentity} The SDK identity object.
|
||||||
|
*/
|
||||||
|
toSdkIdentity(): SdkIdentity {
|
||||||
|
return {
|
||||||
|
title: this.title?.toJSON(),
|
||||||
|
firstName: this.firstName?.toJSON(),
|
||||||
|
middleName: this.middleName?.toJSON(),
|
||||||
|
lastName: this.lastName?.toJSON(),
|
||||||
|
address1: this.address1?.toJSON(),
|
||||||
|
address2: this.address2?.toJSON(),
|
||||||
|
address3: this.address3?.toJSON(),
|
||||||
|
city: this.city?.toJSON(),
|
||||||
|
state: this.state?.toJSON(),
|
||||||
|
postalCode: this.postalCode?.toJSON(),
|
||||||
|
country: this.country?.toJSON(),
|
||||||
|
company: this.company?.toJSON(),
|
||||||
|
email: this.email?.toJSON(),
|
||||||
|
phone: this.phone?.toJSON(),
|
||||||
|
ssn: this.ssn?.toJSON(),
|
||||||
|
username: this.username?.toJSON(),
|
||||||
|
passportNumber: this.passportNumber?.toJSON(),
|
||||||
|
licenseNumber: this.licenseNumber?.toJSON(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { MockProxy, mock } from "jest-mock-extended";
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { UriMatchType } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { mockEnc, mockFromJson } from "../../../../spec";
|
import { mockEnc, mockFromJson } from "../../../../spec";
|
||||||
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
|
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
|
||||||
import { UriMatchStrategy } from "../../../models/domain/domain-service";
|
import { UriMatchStrategy } from "../../../models/domain/domain-service";
|
||||||
@@ -118,4 +120,17 @@ describe("LoginUri", () => {
|
|||||||
expect(LoginUri.fromJSON(null)).toBeNull();
|
expect(LoginUri.fromJSON(null)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("SDK Login Uri Mapping", () => {
|
||||||
|
it("should map to SDK login uri", () => {
|
||||||
|
const loginUri = new LoginUri(data);
|
||||||
|
const sdkLoginUri = loginUri.toSdkLoginUri();
|
||||||
|
|
||||||
|
expect(sdkLoginUri).toEqual({
|
||||||
|
uri: "encUri",
|
||||||
|
uriChecksum: "encUriChecksum",
|
||||||
|
match: UriMatchType.Domain,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { LoginUri as SdkLoginUri } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
||||||
import { Utils } from "../../../platform/misc/utils";
|
import { Utils } from "../../../platform/misc/utils";
|
||||||
import Domain from "../../../platform/models/domain/domain-base";
|
import Domain from "../../../platform/models/domain/domain-base";
|
||||||
@@ -87,4 +89,17 @@ export class LoginUri extends Domain {
|
|||||||
uriChecksum,
|
uriChecksum,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps LoginUri to SDK format.
|
||||||
|
*
|
||||||
|
* @returns {SdkLoginUri} The SDK login uri object.
|
||||||
|
*/
|
||||||
|
toSdkLoginUri(): SdkLoginUri {
|
||||||
|
return {
|
||||||
|
uri: this.uri.toJSON(),
|
||||||
|
uriChecksum: this.uriChecksum.toJSON(),
|
||||||
|
match: this.match,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,6 +202,54 @@ describe("Login DTO", () => {
|
|||||||
expect(Login.fromJSON(null)).toBeNull();
|
expect(Login.fromJSON(null)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("toSdkLogin", () => {
|
||||||
|
it("should map to SDK login", () => {
|
||||||
|
const data: LoginData = {
|
||||||
|
uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchStrategy.Domain }],
|
||||||
|
username: "username",
|
||||||
|
password: "password",
|
||||||
|
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
|
||||||
|
totp: "123",
|
||||||
|
autofillOnPageLoad: false,
|
||||||
|
fido2Credentials: [initializeFido2Credential(new Fido2CredentialData())],
|
||||||
|
};
|
||||||
|
const login = new Login(data);
|
||||||
|
const sdkLogin = login.toSdkLogin();
|
||||||
|
|
||||||
|
expect(sdkLogin).toEqual({
|
||||||
|
username: "username",
|
||||||
|
password: "password",
|
||||||
|
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
|
||||||
|
uris: [
|
||||||
|
{
|
||||||
|
match: 0,
|
||||||
|
uri: "uri",
|
||||||
|
uriChecksum: "checksum",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
totp: "123",
|
||||||
|
autofillOnPageLoad: false,
|
||||||
|
fido2Credentials: [
|
||||||
|
{
|
||||||
|
credentialId: "credentialId",
|
||||||
|
keyType: "public-key",
|
||||||
|
keyAlgorithm: "ECDSA",
|
||||||
|
keyCurve: "P-256",
|
||||||
|
keyValue: "keyValue",
|
||||||
|
rpId: "rpId",
|
||||||
|
userHandle: "userHandle",
|
||||||
|
userName: "userName",
|
||||||
|
counter: "counter",
|
||||||
|
rpName: "rpName",
|
||||||
|
userDisplayName: "userDisplayName",
|
||||||
|
discoverable: "discoverable",
|
||||||
|
creationDate: "2023-01-01T12:00:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
type Fido2CredentialLike = Fido2CredentialData | Fido2CredentialView | Fido2CredentialApi;
|
type Fido2CredentialLike = Fido2CredentialData | Fido2CredentialView | Fido2CredentialApi;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { Login as SdkLogin } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import Domain from "../../../platform/models/domain/domain-base";
|
import Domain from "../../../platform/models/domain/domain-base";
|
||||||
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";
|
||||||
@@ -144,4 +146,21 @@ export class Login extends Domain {
|
|||||||
fido2Credentials,
|
fido2Credentials,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps Login to SDK format.
|
||||||
|
*
|
||||||
|
* @returns {SdkLogin} The SDK login object.
|
||||||
|
*/
|
||||||
|
toSdkLogin(): SdkLogin {
|
||||||
|
return {
|
||||||
|
uris: this.uris?.map((u) => u.toSdkLoginUri()),
|
||||||
|
username: this.username?.toJSON(),
|
||||||
|
password: this.password?.toJSON(),
|
||||||
|
passwordRevisionDate: this.passwordRevisionDate?.toISOString(),
|
||||||
|
totp: this.totp?.toJSON(),
|
||||||
|
autofillOnPageLoad: this.autofillOnPageLoad,
|
||||||
|
fido2Credentials: this.fido2Credentials?.map((f) => f.toSdkFido2Credential()),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,4 +70,17 @@ describe("Password", () => {
|
|||||||
expect(Password.fromJSON(null)).toBeNull();
|
expect(Password.fromJSON(null)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("toSdkPasswordHistory", () => {
|
||||||
|
it("returns the correct SDK PasswordHistory object", () => {
|
||||||
|
const password = new Password(data);
|
||||||
|
|
||||||
|
const sdkPasswordHistory = password.toSdkPasswordHistory();
|
||||||
|
|
||||||
|
expect(sdkPasswordHistory).toEqual({
|
||||||
|
password: "encPassword",
|
||||||
|
lastUsedDate: new Date("2022-01-31T12:00:00.000Z").toISOString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { PasswordHistory } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import Domain from "../../../platform/models/domain/domain-base";
|
import Domain from "../../../platform/models/domain/domain-base";
|
||||||
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";
|
||||||
@@ -57,4 +59,16 @@ export class Password extends Domain {
|
|||||||
lastUsedDate,
|
lastUsedDate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps Password to SDK format.
|
||||||
|
*
|
||||||
|
* @returns {PasswordHistory} The SDK password history object.
|
||||||
|
*/
|
||||||
|
toSdkPasswordHistory(): PasswordHistory {
|
||||||
|
return {
|
||||||
|
password: this.password.toJSON(),
|
||||||
|
lastUsedDate: this.lastUsedDate.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,4 +50,17 @@ describe("SecureNote", () => {
|
|||||||
expect(SecureNote.fromJSON(null)).toBeNull();
|
expect(SecureNote.fromJSON(null)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("toSdkSecureNote", () => {
|
||||||
|
it("returns the correct SDK SecureNote object", () => {
|
||||||
|
const secureNote = new SecureNote();
|
||||||
|
secureNote.type = SecureNoteType.Generic;
|
||||||
|
|
||||||
|
const sdkSecureNote = secureNote.toSdkSecureNote();
|
||||||
|
|
||||||
|
expect(sdkSecureNote).toEqual({
|
||||||
|
type: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { SecureNote as SdkSecureNote } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import Domain from "../../../platform/models/domain/domain-base";
|
import Domain from "../../../platform/models/domain/domain-base";
|
||||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||||
import { SecureNoteType } from "../../enums";
|
import { SecureNoteType } from "../../enums";
|
||||||
@@ -41,4 +43,15 @@ export class SecureNote extends Domain {
|
|||||||
|
|
||||||
return Object.assign(new SecureNote(), obj);
|
return Object.assign(new SecureNote(), obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps Secure note to SDK format.
|
||||||
|
*
|
||||||
|
* @returns {SdkSecureNote} The SDK secure note object.
|
||||||
|
*/
|
||||||
|
toSdkSecureNote(): SdkSecureNote {
|
||||||
|
return {
|
||||||
|
type: this.type,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,4 +64,17 @@ describe("Sshkey", () => {
|
|||||||
expect(SshKey.fromJSON(null)).toBeNull();
|
expect(SshKey.fromJSON(null)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("toSdkSshKey", () => {
|
||||||
|
it("returns the correct SDK SshKey object", () => {
|
||||||
|
const sshKey = new SshKey(data);
|
||||||
|
const sdkSshKey = sshKey.toSdkSshKey();
|
||||||
|
|
||||||
|
expect(sdkSshKey).toEqual({
|
||||||
|
privateKey: "privateKey",
|
||||||
|
publicKey: "publicKey",
|
||||||
|
fingerprint: "keyFingerprint",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { SshKey as SdkSshKey } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import Domain from "../../../platform/models/domain/domain-base";
|
import Domain from "../../../platform/models/domain/domain-base";
|
||||||
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";
|
||||||
@@ -70,4 +72,17 @@ export class SshKey extends Domain {
|
|||||||
keyFingerprint,
|
keyFingerprint,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps SSH key to SDK format.
|
||||||
|
*
|
||||||
|
* @returns {SdkSshKey} The SDK SSH key object.
|
||||||
|
*/
|
||||||
|
toSdkSshKey(): SdkSshKey {
|
||||||
|
return {
|
||||||
|
privateKey: this.privateKey.toJSON(),
|
||||||
|
publicKey: this.publicKey.toJSON(),
|
||||||
|
fingerprint: this.keyFingerprint.toJSON(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import { AttachmentView as SdkAttachmentView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { mockFromJson } from "../../../../spec";
|
import { mockFromJson } from "../../../../spec";
|
||||||
|
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 { AttachmentView } from "./attachment.view";
|
import { AttachmentView } from "./attachment.view";
|
||||||
@@ -15,4 +18,56 @@ describe("AttachmentView", () => {
|
|||||||
|
|
||||||
expect(actual.key).toEqual("encKeyB64_fromJSON");
|
expect(actual.key).toEqual("encKeyB64_fromJSON");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("fromSdkAttachmentView", () => {
|
||||||
|
it("should return undefined when the input is null", () => {
|
||||||
|
const result = AttachmentView.fromSdkAttachmentView(null as unknown as any);
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return an AttachmentView from an SdkAttachmentView", () => {
|
||||||
|
const sdkAttachmentView = {
|
||||||
|
id: "id",
|
||||||
|
url: "url",
|
||||||
|
size: "size",
|
||||||
|
sizeName: "sizeName",
|
||||||
|
fileName: "fileName",
|
||||||
|
key: "encKeyB64_fromString",
|
||||||
|
} as SdkAttachmentView;
|
||||||
|
|
||||||
|
const result = AttachmentView.fromSdkAttachmentView(sdkAttachmentView);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
id: "id",
|
||||||
|
url: "url",
|
||||||
|
size: "size",
|
||||||
|
sizeName: "sizeName",
|
||||||
|
fileName: "fileName",
|
||||||
|
key: null,
|
||||||
|
encryptedKey: new EncString(sdkAttachmentView.key as string),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("toSdkAttachmentView", () => {
|
||||||
|
it("should convert AttachmentView to SdkAttachmentView", () => {
|
||||||
|
const attachmentView = new AttachmentView();
|
||||||
|
attachmentView.id = "id";
|
||||||
|
attachmentView.url = "url";
|
||||||
|
attachmentView.size = "size";
|
||||||
|
attachmentView.sizeName = "sizeName";
|
||||||
|
attachmentView.fileName = "fileName";
|
||||||
|
attachmentView.encryptedKey = new EncString("encKeyB64");
|
||||||
|
|
||||||
|
const result = attachmentView.toSdkAttachmentView();
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: "id",
|
||||||
|
url: "url",
|
||||||
|
size: "size",
|
||||||
|
sizeName: "sizeName",
|
||||||
|
fileName: "fileName",
|
||||||
|
key: "encKeyB64",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { AttachmentView as SdkAttachmentView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { View } from "../../../models/view/view";
|
import { View } from "../../../models/view/view";
|
||||||
|
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 { Attachment } from "../domain/attachment";
|
import { Attachment } from "../domain/attachment";
|
||||||
|
|
||||||
@@ -13,6 +16,10 @@ export class AttachmentView implements View {
|
|||||||
sizeName: string = null;
|
sizeName: string = null;
|
||||||
fileName: string = null;
|
fileName: string = null;
|
||||||
key: SymmetricCryptoKey = null;
|
key: SymmetricCryptoKey = null;
|
||||||
|
/**
|
||||||
|
* The SDK returns an encrypted key for the attachment.
|
||||||
|
*/
|
||||||
|
encryptedKey: EncString | undefined;
|
||||||
|
|
||||||
constructor(a?: Attachment) {
|
constructor(a?: Attachment) {
|
||||||
if (!a) {
|
if (!a) {
|
||||||
@@ -40,4 +47,37 @@ export class AttachmentView implements View {
|
|||||||
const key = obj.key == null ? null : SymmetricCryptoKey.fromJSON(obj.key);
|
const key = obj.key == null ? null : SymmetricCryptoKey.fromJSON(obj.key);
|
||||||
return Object.assign(new AttachmentView(), obj, { key: key });
|
return Object.assign(new AttachmentView(), obj, { key: key });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the AttachmentView to a SDK AttachmentView.
|
||||||
|
*/
|
||||||
|
toSdkAttachmentView(): SdkAttachmentView {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
url: this.url,
|
||||||
|
size: this.size,
|
||||||
|
sizeName: this.sizeName,
|
||||||
|
fileName: this.fileName,
|
||||||
|
key: this.encryptedKey?.toJSON(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the SDK AttachmentView to a AttachmentView.
|
||||||
|
*/
|
||||||
|
static fromSdkAttachmentView(obj: SdkAttachmentView): AttachmentView | undefined {
|
||||||
|
if (!obj) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = new AttachmentView();
|
||||||
|
view.id = obj.id ?? null;
|
||||||
|
view.url = obj.url ?? null;
|
||||||
|
view.size = obj.size ?? null;
|
||||||
|
view.sizeName = obj.sizeName ?? null;
|
||||||
|
view.fileName = obj.fileName ?? null;
|
||||||
|
view.encryptedKey = new EncString(obj.key);
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { CardView as SdkCardView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { normalizeExpiryYearFormat } from "../../../autofill/utils";
|
import { normalizeExpiryYearFormat } from "../../../autofill/utils";
|
||||||
import { CardLinkedId as LinkedId } from "../../enums";
|
import { CardLinkedId as LinkedId } from "../../enums";
|
||||||
import { linkedFieldOption } from "../../linked-field-option.decorator";
|
import { linkedFieldOption } from "../../linked-field-option.decorator";
|
||||||
@@ -146,4 +148,15 @@ export class CardView extends ItemView {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an SDK CardView to a CardView.
|
||||||
|
*/
|
||||||
|
static fromSdkCardView(obj: SdkCardView): CardView | undefined {
|
||||||
|
if (obj == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.assign(new CardView(), obj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
import { mockFromJson } from "../../../../spec";
|
import {
|
||||||
|
CipherView as SdkCipherView,
|
||||||
|
CipherType as SdkCipherType,
|
||||||
|
CipherRepromptType as SdkCipherRepromptType,
|
||||||
|
AttachmentView as SdkAttachmentView,
|
||||||
|
LoginUriView as SdkLoginUriView,
|
||||||
|
LoginView as SdkLoginView,
|
||||||
|
FieldView as SdkFieldView,
|
||||||
|
FieldType as SdkFieldType,
|
||||||
|
} from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
|
import { mockFromJson, mockFromSdk } from "../../../../spec";
|
||||||
|
import { CipherRepromptType } from "../../enums";
|
||||||
import { CipherType } from "../../enums/cipher-type";
|
import { CipherType } from "../../enums/cipher-type";
|
||||||
|
|
||||||
import { AttachmentView } from "./attachment.view";
|
import { AttachmentView } from "./attachment.view";
|
||||||
@@ -9,6 +21,7 @@ import { IdentityView } from "./identity.view";
|
|||||||
import { LoginView } from "./login.view";
|
import { LoginView } from "./login.view";
|
||||||
import { PasswordHistoryView } from "./password-history.view";
|
import { PasswordHistoryView } from "./password-history.view";
|
||||||
import { SecureNoteView } from "./secure-note.view";
|
import { SecureNoteView } from "./secure-note.view";
|
||||||
|
import { SshKeyView } from "./ssh-key.view";
|
||||||
|
|
||||||
jest.mock("../../models/view/login.view");
|
jest.mock("../../models/view/login.view");
|
||||||
jest.mock("../../models/view/attachment.view");
|
jest.mock("../../models/view/attachment.view");
|
||||||
@@ -73,4 +86,121 @@ describe("CipherView", () => {
|
|||||||
expect(actual).toMatchObject(expected);
|
expect(actual).toMatchObject(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("fromSdkCipherView", () => {
|
||||||
|
let sdkCipherView: SdkCipherView;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(CardView, "fromSdkCardView").mockImplementation(mockFromSdk);
|
||||||
|
jest.spyOn(IdentityView, "fromSdkIdentityView").mockImplementation(mockFromSdk);
|
||||||
|
jest.spyOn(LoginView, "fromSdkLoginView").mockImplementation(mockFromSdk);
|
||||||
|
jest.spyOn(SecureNoteView, "fromSdkSecureNoteView").mockImplementation(mockFromSdk);
|
||||||
|
jest.spyOn(SshKeyView, "fromSdkSshKeyView").mockImplementation(mockFromSdk);
|
||||||
|
jest.spyOn(AttachmentView, "fromSdkAttachmentView").mockImplementation(mockFromSdk);
|
||||||
|
jest.spyOn(FieldView, "fromSdkFieldView").mockImplementation(mockFromSdk);
|
||||||
|
|
||||||
|
sdkCipherView = {
|
||||||
|
id: "id",
|
||||||
|
organizationId: "orgId",
|
||||||
|
folderId: "folderId",
|
||||||
|
collectionIds: ["collectionId"],
|
||||||
|
key: undefined,
|
||||||
|
name: "name",
|
||||||
|
notes: undefined,
|
||||||
|
type: SdkCipherType.Login,
|
||||||
|
favorite: true,
|
||||||
|
edit: true,
|
||||||
|
reprompt: SdkCipherRepromptType.None,
|
||||||
|
organizationUseTotp: false,
|
||||||
|
viewPassword: true,
|
||||||
|
localData: undefined,
|
||||||
|
permissions: undefined,
|
||||||
|
attachments: [{ id: "attachmentId", url: "attachmentUrl" } as SdkAttachmentView],
|
||||||
|
login: {
|
||||||
|
username: "username",
|
||||||
|
password: "password",
|
||||||
|
uris: [{ uri: "bitwarden.com" } as SdkLoginUriView],
|
||||||
|
totp: "totp",
|
||||||
|
autofillOnPageLoad: true,
|
||||||
|
} as SdkLoginView,
|
||||||
|
identity: undefined,
|
||||||
|
card: undefined,
|
||||||
|
secureNote: undefined,
|
||||||
|
sshKey: undefined,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "fieldName",
|
||||||
|
value: "fieldValue",
|
||||||
|
type: SdkFieldType.Linked,
|
||||||
|
linkedId: 100,
|
||||||
|
} as SdkFieldView,
|
||||||
|
],
|
||||||
|
passwordHistory: undefined,
|
||||||
|
creationDate: "2022-01-01T12:00:00.000Z",
|
||||||
|
revisionDate: "2022-01-02T12:00:00.000Z",
|
||||||
|
deletedDate: undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined when input is null", () => {
|
||||||
|
expect(CipherView.fromSdkCipherView(null as unknown as SdkCipherView)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps properties correctly", () => {
|
||||||
|
const result = CipherView.fromSdkCipherView(sdkCipherView);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
id: "id",
|
||||||
|
organizationId: "orgId",
|
||||||
|
folderId: "folderId",
|
||||||
|
collectionIds: ["collectionId"],
|
||||||
|
name: "name",
|
||||||
|
notes: null,
|
||||||
|
type: CipherType.Login,
|
||||||
|
favorite: true,
|
||||||
|
edit: true,
|
||||||
|
reprompt: CipherRepromptType.None,
|
||||||
|
organizationUseTotp: false,
|
||||||
|
viewPassword: true,
|
||||||
|
localData: undefined,
|
||||||
|
permissions: undefined,
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
id: "attachmentId",
|
||||||
|
url: "attachmentUrl",
|
||||||
|
__fromSdk: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
login: {
|
||||||
|
username: "username",
|
||||||
|
password: "password",
|
||||||
|
uris: [
|
||||||
|
{
|
||||||
|
uri: "bitwarden.com",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
totp: "totp",
|
||||||
|
autofillOnPageLoad: true,
|
||||||
|
__fromSdk: true,
|
||||||
|
},
|
||||||
|
identity: new IdentityView(),
|
||||||
|
card: new CardView(),
|
||||||
|
secureNote: new SecureNoteView(),
|
||||||
|
sshKey: new SshKeyView(),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "fieldName",
|
||||||
|
value: "fieldValue",
|
||||||
|
type: SdkFieldType.Linked,
|
||||||
|
linkedId: 100,
|
||||||
|
__fromSdk: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
passwordHistory: null,
|
||||||
|
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||||
|
revisionDate: new Date("2022-01-02T12:00:00.000Z"),
|
||||||
|
deletedDate: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
|
import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { View } from "../../../models/view/view";
|
import { View } from "../../../models/view/view";
|
||||||
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
|
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
|
||||||
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
|
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
|
||||||
@@ -110,7 +112,7 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
get hasOldAttachments(): boolean {
|
get hasOldAttachments(): boolean {
|
||||||
if (this.hasAttachments) {
|
if (this.hasAttachments) {
|
||||||
for (let i = 0; i < this.attachments.length; i++) {
|
for (let i = 0; i < this.attachments.length; i++) {
|
||||||
if (this.attachments[i].key == null) {
|
if (this.attachments[i].key == null && this.attachments[i].encryptedKey == null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,4 +224,68 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a CipherView from the SDK CipherView.
|
||||||
|
*/
|
||||||
|
static fromSdkCipherView(obj: SdkCipherView): CipherView | undefined {
|
||||||
|
if (obj == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipherView = new CipherView();
|
||||||
|
cipherView.id = obj.id ?? null;
|
||||||
|
cipherView.organizationId = obj.organizationId ?? null;
|
||||||
|
cipherView.folderId = obj.folderId ?? null;
|
||||||
|
cipherView.name = obj.name;
|
||||||
|
cipherView.notes = obj.notes ?? null;
|
||||||
|
cipherView.type = obj.type;
|
||||||
|
cipherView.favorite = obj.favorite;
|
||||||
|
cipherView.organizationUseTotp = obj.organizationUseTotp;
|
||||||
|
cipherView.permissions = CipherPermissionsApi.fromSdkCipherPermissions(obj.permissions);
|
||||||
|
cipherView.edit = obj.edit;
|
||||||
|
cipherView.viewPassword = obj.viewPassword;
|
||||||
|
cipherView.localData = obj.localData
|
||||||
|
? {
|
||||||
|
lastUsedDate: obj.localData.lastUsedDate
|
||||||
|
? new Date(obj.localData.lastUsedDate).getTime()
|
||||||
|
: undefined,
|
||||||
|
lastLaunched: obj.localData.lastLaunched
|
||||||
|
? new Date(obj.localData.lastLaunched).getTime()
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
cipherView.attachments =
|
||||||
|
obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)) ?? null;
|
||||||
|
cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)) ?? null;
|
||||||
|
cipherView.passwordHistory =
|
||||||
|
obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)) ?? null;
|
||||||
|
cipherView.collectionIds = obj.collectionIds ?? null;
|
||||||
|
cipherView.revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
||||||
|
cipherView.creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
|
||||||
|
cipherView.deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
|
||||||
|
cipherView.reprompt = obj.reprompt ?? CipherRepromptType.None;
|
||||||
|
|
||||||
|
switch (obj.type) {
|
||||||
|
case CipherType.Card:
|
||||||
|
cipherView.card = CardView.fromSdkCardView(obj.card);
|
||||||
|
break;
|
||||||
|
case CipherType.Identity:
|
||||||
|
cipherView.identity = IdentityView.fromSdkIdentityView(obj.identity);
|
||||||
|
break;
|
||||||
|
case CipherType.Login:
|
||||||
|
cipherView.login = LoginView.fromSdkLoginView(obj.login);
|
||||||
|
break;
|
||||||
|
case CipherType.SecureNote:
|
||||||
|
cipherView.secureNote = SecureNoteView.fromSdkSecureNoteView(obj.secureNote);
|
||||||
|
break;
|
||||||
|
case CipherType.SshKey:
|
||||||
|
cipherView.sshKey = SshKeyView.fromSdkSshKeyView(obj.sshKey);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cipherView;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { Fido2CredentialView as SdkFido2CredentialView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { ItemView } from "./item.view";
|
import { ItemView } from "./item.view";
|
||||||
|
|
||||||
export class Fido2CredentialView extends ItemView {
|
export class Fido2CredentialView extends ItemView {
|
||||||
@@ -29,4 +31,29 @@ export class Fido2CredentialView extends ItemView {
|
|||||||
creationDate,
|
creationDate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the SDK Fido2CredentialView to a Fido2CredentialView.
|
||||||
|
*/
|
||||||
|
static fromSdkFido2CredentialView(obj: SdkFido2CredentialView): Fido2CredentialView | undefined {
|
||||||
|
if (!obj) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = new Fido2CredentialView();
|
||||||
|
view.credentialId = obj.credentialId;
|
||||||
|
view.keyType = obj.keyType as "public-key";
|
||||||
|
view.keyAlgorithm = obj.keyAlgorithm as "ECDSA";
|
||||||
|
view.keyCurve = obj.keyCurve as "P-256";
|
||||||
|
view.rpId = obj.rpId;
|
||||||
|
view.userHandle = obj.userHandle;
|
||||||
|
view.userName = obj.userName;
|
||||||
|
view.counter = parseInt(obj.counter);
|
||||||
|
view.rpName = obj.rpName;
|
||||||
|
view.userDisplayName = obj.userDisplayName;
|
||||||
|
view.discoverable = obj.discoverable?.toLowerCase() === "true" ? true : false;
|
||||||
|
view.creationDate = obj.creationDate ? new Date(obj.creationDate) : null;
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { FieldView as SdkFieldView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { View } from "../../../models/view/view";
|
import { View } from "../../../models/view/view";
|
||||||
import { FieldType, LinkedIdType } from "../../enums";
|
import { FieldType, LinkedIdType } from "../../enums";
|
||||||
import { Field } from "../domain/field";
|
import { Field } from "../domain/field";
|
||||||
@@ -31,4 +33,21 @@ export class FieldView implements View {
|
|||||||
static fromJSON(obj: Partial<Jsonify<FieldView>>): FieldView {
|
static fromJSON(obj: Partial<Jsonify<FieldView>>): FieldView {
|
||||||
return Object.assign(new FieldView(), obj);
|
return Object.assign(new FieldView(), obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the SDK FieldView to a FieldView.
|
||||||
|
*/
|
||||||
|
static fromSdkFieldView(obj: SdkFieldView): FieldView | undefined {
|
||||||
|
if (!obj) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = new FieldView();
|
||||||
|
view.name = obj.name;
|
||||||
|
view.value = obj.value;
|
||||||
|
view.type = obj.type;
|
||||||
|
view.linkedId = obj.linkedId as unknown as LinkedIdType;
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { IdentityView as SdkIdentityView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { Utils } from "../../../platform/misc/utils";
|
import { Utils } from "../../../platform/misc/utils";
|
||||||
import { IdentityLinkedId as LinkedId } from "../../enums";
|
import { IdentityLinkedId as LinkedId } from "../../enums";
|
||||||
import { linkedFieldOption } from "../../linked-field-option.decorator";
|
import { linkedFieldOption } from "../../linked-field-option.decorator";
|
||||||
@@ -158,4 +160,15 @@ export class IdentityView extends ItemView {
|
|||||||
static fromJSON(obj: Partial<Jsonify<IdentityView>>): IdentityView {
|
static fromJSON(obj: Partial<Jsonify<IdentityView>>): IdentityView {
|
||||||
return Object.assign(new IdentityView(), obj);
|
return Object.assign(new IdentityView(), obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the SDK IdentityView to an IdentityView.
|
||||||
|
*/
|
||||||
|
static fromSdkIdentityView(obj: SdkIdentityView): IdentityView | undefined {
|
||||||
|
if (obj == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.assign(new IdentityView(), obj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { LoginUriView as SdkLoginUriView, UriMatchType } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
||||||
import { Utils } from "../../../platform/misc/utils";
|
import { Utils } from "../../../platform/misc/utils";
|
||||||
|
|
||||||
@@ -184,6 +186,26 @@ describe("LoginUriView", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("fromSdkLoginUriView", () => {
|
||||||
|
it("should return undefined when the input is null", () => {
|
||||||
|
const result = LoginUriView.fromSdkLoginUriView(null as unknown as SdkLoginUriView);
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a LoginUriView from a SdkLoginUriView", () => {
|
||||||
|
const sdkLoginUriView = {
|
||||||
|
uri: "https://example.com",
|
||||||
|
match: UriMatchType.Host,
|
||||||
|
} as SdkLoginUriView;
|
||||||
|
|
||||||
|
const loginUriView = LoginUriView.fromSdkLoginUriView(sdkLoginUriView);
|
||||||
|
|
||||||
|
expect(loginUriView).toBeInstanceOf(LoginUriView);
|
||||||
|
expect(loginUriView!.uri).toBe(sdkLoginUriView.uri);
|
||||||
|
expect(loginUriView!.match).toBe(sdkLoginUriView.match);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function uriFactory(match: UriMatchStrategySetting, uri: string) {
|
function uriFactory(match: UriMatchStrategySetting, uri: string) {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { LoginUriView as SdkLoginUriView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
||||||
import { View } from "../../../models/view/view";
|
import { View } from "../../../models/view/view";
|
||||||
import { SafeUrls } from "../../../platform/misc/safe-urls";
|
import { SafeUrls } from "../../../platform/misc/safe-urls";
|
||||||
@@ -112,6 +114,21 @@ export class LoginUriView implements View {
|
|||||||
return Object.assign(new LoginUriView(), obj);
|
return Object.assign(new LoginUriView(), obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a LoginUriView object from the SDK to a LoginUriView object.
|
||||||
|
*/
|
||||||
|
static fromSdkLoginUriView(obj: SdkLoginUriView): LoginUriView | undefined {
|
||||||
|
if (obj == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = new LoginUriView();
|
||||||
|
view.uri = obj.uri;
|
||||||
|
view.match = obj.match;
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
matchesUri(
|
matchesUri(
|
||||||
targetUri: string,
|
targetUri: string,
|
||||||
equivalentDomains: Set<string>,
|
equivalentDomains: Set<string>,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { mockFromJson } from "../../../../spec";
|
import { LoginView as SdkLoginView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
|
import { mockFromJson, mockFromSdk } from "../../../../spec";
|
||||||
|
|
||||||
import { LoginUriView } from "./login-uri.view";
|
import { LoginUriView } from "./login-uri.view";
|
||||||
import { LoginView } from "./login.view";
|
import { LoginView } from "./login.view";
|
||||||
@@ -25,4 +27,35 @@ describe("LoginView", () => {
|
|||||||
uris: ["uri1_fromJSON", "uri2_fromJSON", "uri3_fromJSON"],
|
uris: ["uri1_fromJSON", "uri2_fromJSON", "uri3_fromJSON"],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("fromSdkLoginView", () => {
|
||||||
|
it("should return undefined when the input is null", () => {
|
||||||
|
const result = LoginView.fromSdkLoginView(null as unknown as SdkLoginView);
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a LoginView from an SdkLoginView", () => {
|
||||||
|
jest.spyOn(LoginUriView, "fromSdkLoginUriView").mockImplementation(mockFromSdk);
|
||||||
|
|
||||||
|
const sdkLoginView = {
|
||||||
|
username: "username",
|
||||||
|
password: "password",
|
||||||
|
passwordRevisionDate: "2025-01-01T01:06:40.441Z",
|
||||||
|
uris: [{ uri: "bitwarden.com" } as any],
|
||||||
|
totp: "totp",
|
||||||
|
autofillOnPageLoad: true,
|
||||||
|
} as SdkLoginView;
|
||||||
|
|
||||||
|
const result = LoginView.fromSdkLoginView(sdkLoginView);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
username: "username",
|
||||||
|
password: "password",
|
||||||
|
passwordRevisionDate: new Date("2025-01-01T01:06:40.441Z"),
|
||||||
|
uris: [expect.objectContaining({ uri: "bitwarden.com", __fromSdk: true })],
|
||||||
|
totp: "totp",
|
||||||
|
autofillOnPageLoad: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
|
import { LoginView as SdkLoginView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
||||||
import { Utils } from "../../../platform/misc/utils";
|
import { Utils } from "../../../platform/misc/utils";
|
||||||
import { DeepJsonify } from "../../../types/deep-jsonify";
|
import { DeepJsonify } from "../../../types/deep-jsonify";
|
||||||
@@ -100,4 +102,27 @@ export class LoginView extends ItemView {
|
|||||||
fido2Credentials,
|
fido2Credentials,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the SDK LoginView to a LoginView.
|
||||||
|
*
|
||||||
|
* Note: FIDO2 credentials remain encrypted at this stage.
|
||||||
|
* Unlike other fields that are decrypted as part of the LoginView, the SDK maintains
|
||||||
|
* the FIDO2 credentials in encrypted form. We can decrypt them later using a separate
|
||||||
|
* call to client.vault().ciphers().decrypt_fido2_credentials().
|
||||||
|
*/
|
||||||
|
static fromSdkLoginView(obj: SdkLoginView): LoginView | undefined {
|
||||||
|
if (obj == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordRevisionDate =
|
||||||
|
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
|
||||||
|
const uris = obj.uris?.map((uri) => LoginUriView.fromSdkLoginUriView(uri));
|
||||||
|
|
||||||
|
return Object.assign(new LoginView(), obj, {
|
||||||
|
passwordRevisionDate,
|
||||||
|
uris,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { PasswordHistoryView as SdkPasswordHistoryView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { PasswordHistoryView } from "./password-history.view";
|
import { PasswordHistoryView } from "./password-history.view";
|
||||||
|
|
||||||
describe("PasswordHistoryView", () => {
|
describe("PasswordHistoryView", () => {
|
||||||
@@ -10,4 +12,25 @@ describe("PasswordHistoryView", () => {
|
|||||||
|
|
||||||
expect(actual.lastUsedDate).toEqual(lastUsedDate);
|
expect(actual.lastUsedDate).toEqual(lastUsedDate);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("fromSdkPasswordHistoryView", () => {
|
||||||
|
it("should return undefined when the input is null", () => {
|
||||||
|
const result = PasswordHistoryView.fromSdkPasswordHistoryView(null as unknown as any);
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a PasswordHistoryView from an SdkPasswordHistoryView", () => {
|
||||||
|
const sdkPasswordHistoryView = {
|
||||||
|
password: "password",
|
||||||
|
lastUsedDate: "2023-10-01T00:00:00Z",
|
||||||
|
} as SdkPasswordHistoryView;
|
||||||
|
|
||||||
|
const result = PasswordHistoryView.fromSdkPasswordHistoryView(sdkPasswordHistoryView);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
password: "password",
|
||||||
|
lastUsedDate: new Date("2023-10-01T00:00:00Z"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { PasswordHistoryView as SdkPasswordHistoryView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { View } from "../../../models/view/view";
|
import { View } from "../../../models/view/view";
|
||||||
import { Password } from "../domain/password";
|
import { Password } from "../domain/password";
|
||||||
|
|
||||||
@@ -24,4 +26,19 @@ export class PasswordHistoryView implements View {
|
|||||||
lastUsedDate: lastUsedDate,
|
lastUsedDate: lastUsedDate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the SDK PasswordHistoryView to a PasswordHistoryView.
|
||||||
|
*/
|
||||||
|
static fromSdkPasswordHistoryView(obj: SdkPasswordHistoryView): PasswordHistoryView | undefined {
|
||||||
|
if (!obj) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = new PasswordHistoryView();
|
||||||
|
view.password = obj.password;
|
||||||
|
view.lastUsedDate = obj.lastUsedDate == null ? null : new Date(obj.lastUsedDate);
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { SecureNoteView as SdkSecureNoteView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { SecureNoteType } from "../../enums";
|
import { SecureNoteType } from "../../enums";
|
||||||
import { SecureNote } from "../domain/secure-note";
|
import { SecureNote } from "../domain/secure-note";
|
||||||
|
|
||||||
@@ -26,4 +28,15 @@ export class SecureNoteView extends ItemView {
|
|||||||
static fromJSON(obj: Partial<Jsonify<SecureNoteView>>): SecureNoteView {
|
static fromJSON(obj: Partial<Jsonify<SecureNoteView>>): SecureNoteView {
|
||||||
return Object.assign(new SecureNoteView(), obj);
|
return Object.assign(new SecureNoteView(), obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the SDK SecureNoteView to a SecureNoteView.
|
||||||
|
*/
|
||||||
|
static fromSdkSecureNoteView(obj: SdkSecureNoteView): SecureNoteView | undefined {
|
||||||
|
if (!obj) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.assign(new SecureNoteView(), obj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { SshKeyView as SdkSshKeyView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { SshKey } from "../domain/ssh-key";
|
import { SshKey } from "../domain/ssh-key";
|
||||||
|
|
||||||
import { ItemView } from "./item.view";
|
import { ItemView } from "./item.view";
|
||||||
@@ -44,4 +46,19 @@ export class SshKeyView extends ItemView {
|
|||||||
static fromJSON(obj: Partial<Jsonify<SshKeyView>>): SshKeyView {
|
static fromJSON(obj: Partial<Jsonify<SshKeyView>>): SshKeyView {
|
||||||
return Object.assign(new SshKeyView(), obj);
|
return Object.assign(new SshKeyView(), obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the SDK SshKeyView to a SshKeyView.
|
||||||
|
*/
|
||||||
|
static fromSdkSshKeyView(obj: SdkSshKeyView): SshKeyView | undefined {
|
||||||
|
if (!obj) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyFingerprint = obj.fingerprint;
|
||||||
|
|
||||||
|
return Object.assign(new SshKeyView(), obj, {
|
||||||
|
keyFingerprint,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { CipherDecryptionKeys, KeyService } from "@bitwarden/key-management";
|
|||||||
|
|
||||||
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||||
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
||||||
import { makeStaticByteArray } from "../../../spec/utils";
|
import { makeStaticByteArray, makeSymmetricCryptoKey } from "../../../spec/utils";
|
||||||
import { ApiService } from "../../abstractions/api.service";
|
import { ApiService } from "../../abstractions/api.service";
|
||||||
import { SearchService } from "../../abstractions/search.service";
|
import { SearchService } from "../../abstractions/search.service";
|
||||||
import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service";
|
import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service";
|
||||||
@@ -24,6 +24,7 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt
|
|||||||
import { ContainerService } from "../../platform/services/container.service";
|
import { ContainerService } from "../../platform/services/container.service";
|
||||||
import { CipherId, UserId } from "../../types/guid";
|
import { CipherId, UserId } from "../../types/guid";
|
||||||
import { CipherKey, OrgKey, UserKey } from "../../types/key";
|
import { CipherKey, OrgKey, UserKey } from "../../types/key";
|
||||||
|
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
|
||||||
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
||||||
import { FieldType } from "../enums";
|
import { FieldType } from "../enums";
|
||||||
import { CipherRepromptType } from "../enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "../enums/cipher-reprompt-type";
|
||||||
@@ -34,6 +35,7 @@ import { Cipher } from "../models/domain/cipher";
|
|||||||
import { CipherCreateRequest } from "../models/request/cipher-create.request";
|
import { CipherCreateRequest } from "../models/request/cipher-create.request";
|
||||||
import { CipherPartialRequest } from "../models/request/cipher-partial.request";
|
import { CipherPartialRequest } from "../models/request/cipher-partial.request";
|
||||||
import { CipherRequest } from "../models/request/cipher.request";
|
import { CipherRequest } from "../models/request/cipher.request";
|
||||||
|
import { AttachmentView } from "../models/view/attachment.view";
|
||||||
import { CipherView } from "../models/view/cipher.view";
|
import { CipherView } from "../models/view/cipher.view";
|
||||||
import { LoginUriView } from "../models/view/login-uri.view";
|
import { LoginUriView } from "../models/view/login-uri.view";
|
||||||
|
|
||||||
@@ -124,6 +126,7 @@ describe("Cipher Service", () => {
|
|||||||
accountService = mockAccountServiceWith(mockUserId);
|
accountService = mockAccountServiceWith(mockUserId);
|
||||||
const logService = mock<LogService>();
|
const logService = mock<LogService>();
|
||||||
const stateProvider = new FakeStateProvider(accountService);
|
const stateProvider = new FakeStateProvider(accountService);
|
||||||
|
const cipherEncryptionService = mock<CipherEncryptionService>();
|
||||||
|
|
||||||
const userId = "TestUserId" as UserId;
|
const userId = "TestUserId" as UserId;
|
||||||
|
|
||||||
@@ -151,6 +154,7 @@ describe("Cipher Service", () => {
|
|||||||
stateProvider,
|
stateProvider,
|
||||||
accountService,
|
accountService,
|
||||||
logService,
|
logService,
|
||||||
|
cipherEncryptionService,
|
||||||
);
|
);
|
||||||
|
|
||||||
cipherObj = new Cipher(cipherData);
|
cipherObj = new Cipher(cipherData);
|
||||||
@@ -478,4 +482,85 @@ describe("Cipher Service", () => {
|
|||||||
).rejects.toThrow("Cannot rotate ciphers when decryption failures are present");
|
).rejects.toThrow("Cannot rotate ciphers when decryption failures are present");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("decrypt", () => {
|
||||||
|
it("should call decrypt method of CipherEncryptionService when feature flag is true", async () => {
|
||||||
|
configService.getFeatureFlag.mockResolvedValue(true);
|
||||||
|
cipherEncryptionService.decrypt.mockResolvedValue(new CipherView(cipherObj));
|
||||||
|
|
||||||
|
const result = await cipherService.decrypt(cipherObj, userId);
|
||||||
|
|
||||||
|
expect(result).toEqual(new CipherView(cipherObj));
|
||||||
|
expect(cipherEncryptionService.decrypt).toHaveBeenCalledWith(cipherObj, userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call legacy decrypt when feature flag is false", async () => {
|
||||||
|
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey;
|
||||||
|
configService.getFeatureFlag.mockResolvedValue(false);
|
||||||
|
cipherService.getKeyForCipherKeyDecryption = jest.fn().mockResolvedValue(mockUserKey);
|
||||||
|
encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32));
|
||||||
|
jest.spyOn(cipherObj, "decrypt").mockResolvedValue(new CipherView(cipherObj));
|
||||||
|
|
||||||
|
const result = await cipherService.decrypt(cipherObj, userId);
|
||||||
|
|
||||||
|
expect(result).toEqual(new CipherView(cipherObj));
|
||||||
|
expect(cipherObj.decrypt).toHaveBeenCalledWith(mockUserKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getDecryptedAttachmentBuffer", () => {
|
||||||
|
const mockEncryptedContent = new Uint8Array([1, 2, 3]);
|
||||||
|
const mockDecryptedContent = new Uint8Array([4, 5, 6]);
|
||||||
|
|
||||||
|
it("should use SDK when feature flag is enabled", async () => {
|
||||||
|
const cipher = new Cipher(cipherData);
|
||||||
|
const attachment = new AttachmentView(cipher.attachments![0]);
|
||||||
|
configService.getFeatureFlag.mockResolvedValue(true);
|
||||||
|
|
||||||
|
jest.spyOn(cipherService, "ciphers$").mockReturnValue(of({ [cipher.id]: cipherData }));
|
||||||
|
cipherEncryptionService.decryptAttachmentContent.mockResolvedValue(mockDecryptedContent);
|
||||||
|
const mockResponse = {
|
||||||
|
arrayBuffer: jest.fn().mockResolvedValue(mockEncryptedContent.buffer),
|
||||||
|
} as unknown as Response;
|
||||||
|
|
||||||
|
const result = await cipherService.getDecryptedAttachmentBuffer(
|
||||||
|
cipher.id as CipherId,
|
||||||
|
attachment,
|
||||||
|
mockResponse,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockDecryptedContent);
|
||||||
|
expect(cipherEncryptionService.decryptAttachmentContent).toHaveBeenCalledWith(
|
||||||
|
cipher,
|
||||||
|
attachment,
|
||||||
|
mockEncryptedContent,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use legacy decryption when feature flag is enabled", async () => {
|
||||||
|
configService.getFeatureFlag.mockResolvedValue(false);
|
||||||
|
const cipher = new Cipher(cipherData);
|
||||||
|
const attachment = new AttachmentView(cipher.attachments![0]);
|
||||||
|
attachment.key = makeSymmetricCryptoKey(64);
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
arrayBuffer: jest.fn().mockResolvedValue(mockEncryptedContent.buffer),
|
||||||
|
} as unknown as Response;
|
||||||
|
const mockEncBuf = {} as EncArrayBuffer;
|
||||||
|
EncArrayBuffer.fromResponse = jest.fn().mockResolvedValue(mockEncBuf);
|
||||||
|
encryptService.decryptFileData.mockResolvedValue(mockDecryptedContent);
|
||||||
|
|
||||||
|
const result = await cipherService.getDecryptedAttachmentBuffer(
|
||||||
|
cipher.id as CipherId,
|
||||||
|
attachment,
|
||||||
|
mockResponse,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockDecryptedContent);
|
||||||
|
expect(encryptService.decryptFileData).toHaveBeenCalledWith(mockEncBuf, attachment.key);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt
|
|||||||
import { StateProvider } from "../../platform/state";
|
import { StateProvider } from "../../platform/state";
|
||||||
import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid";
|
import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid";
|
||||||
import { OrgKey, UserKey } from "../../types/key";
|
import { OrgKey, UserKey } from "../../types/key";
|
||||||
import { perUserCache$ } from "../../vault/utils/observable-utilities";
|
import { filterOutNullish, perUserCache$ } from "../../vault/utils/observable-utilities";
|
||||||
|
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
|
||||||
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
|
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
|
||||||
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
||||||
import { FieldType } from "../enums";
|
import { FieldType } from "../enums";
|
||||||
@@ -103,6 +104,7 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
private stateProvider: StateProvider,
|
private stateProvider: StateProvider,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
|
private cipherEncryptionService: CipherEncryptionService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
localData$(userId: UserId): Observable<Record<CipherId, LocalData>> {
|
localData$(userId: UserId): Observable<Record<CipherId, LocalData>> {
|
||||||
@@ -424,13 +426,21 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
ciphers: Cipher[],
|
ciphers: Cipher[],
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
): Promise<[CipherView[], CipherView[]] | null> {
|
): Promise<[CipherView[], CipherView[]] | null> {
|
||||||
const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true));
|
if (await this.configService.getFeatureFlag(FeatureFlag.PM19941MigrateCipherDomainToSdk)) {
|
||||||
|
const decryptStartTime = new Date().getTime();
|
||||||
|
const decrypted = await this.decryptCiphersWithSdk(ciphers, userId);
|
||||||
|
this.logService.info(
|
||||||
|
`[CipherService] Decrypting ${decrypted.length} ciphers took ${new Date().getTime() - decryptStartTime}ms`,
|
||||||
|
);
|
||||||
|
// With SDK, failed ciphers are not returned
|
||||||
|
return [decrypted, []];
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true));
|
||||||
if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) {
|
if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) {
|
||||||
// return early if there are no keys to decrypt with
|
// return early if there are no keys to decrypt with
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group ciphers by orgId or under 'null' for the user's ciphers
|
// Group ciphers by orgId or under 'null' for the user's ciphers
|
||||||
const grouped = ciphers.reduce(
|
const grouped = ciphers.reduce(
|
||||||
(agg, c) => {
|
(agg, c) => {
|
||||||
@@ -440,7 +450,6 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
},
|
},
|
||||||
{} as Record<string, Cipher[]>,
|
{} as Record<string, Cipher[]>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const decryptStartTime = new Date().getTime();
|
const decryptStartTime = new Date().getTime();
|
||||||
const allCipherViews = (
|
const allCipherViews = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@@ -464,7 +473,6 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
this.logService.info(
|
this.logService.info(
|
||||||
`[CipherService] Decrypting ${allCipherViews.length} ciphers took ${new Date().getTime() - decryptStartTime}ms`,
|
`[CipherService] Decrypting ${allCipherViews.length} ciphers took ${new Date().getTime() - decryptStartTime}ms`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Split ciphers into two arrays, one for successfully decrypted ciphers and one for ciphers that failed to decrypt
|
// Split ciphers into two arrays, one for successfully decrypted ciphers and one for ciphers that failed to decrypt
|
||||||
return allCipherViews.reduce(
|
return allCipherViews.reduce(
|
||||||
(acc, c) => {
|
(acc, c) => {
|
||||||
@@ -479,6 +487,21 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts a cipher using either the SDK or the legacy method based on the feature flag.
|
||||||
|
* @param cipher The cipher to decrypt.
|
||||||
|
* @param userId The user ID to use for decryption.
|
||||||
|
* @returns A promise that resolves to the decrypted cipher view.
|
||||||
|
*/
|
||||||
|
async decrypt(cipher: Cipher, userId: UserId): Promise<CipherView> {
|
||||||
|
if (await this.configService.getFeatureFlag(FeatureFlag.PM19941MigrateCipherDomainToSdk)) {
|
||||||
|
return await this.cipherEncryptionService.decrypt(cipher, userId);
|
||||||
|
} else {
|
||||||
|
const encKey = await this.getKeyForCipherKeyDecryption(cipher, userId);
|
||||||
|
return await cipher.decrypt(encKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async reindexCiphers(userId: UserId) {
|
private async reindexCiphers(userId: UserId) {
|
||||||
const reindexRequired =
|
const reindexRequired =
|
||||||
this.searchService != null &&
|
this.searchService != null &&
|
||||||
@@ -895,7 +918,7 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
//then we rollback to using the user key as the main key of encryption of the item
|
//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
|
//in order to keep item and it's attachments with the same encryption level
|
||||||
if (cipher.key != null && !cipherKeyEncryptionEnabled) {
|
if (cipher.key != null && !cipherKeyEncryptionEnabled) {
|
||||||
const model = await cipher.decrypt(await this.getKeyForCipherKeyDecryption(cipher, userId));
|
const model = await this.decrypt(cipher, userId);
|
||||||
cipher = await this.encrypt(model, userId);
|
cipher = await this.encrypt(model, userId);
|
||||||
await this.updateWithServer(cipher);
|
await this.updateWithServer(cipher);
|
||||||
}
|
}
|
||||||
@@ -1381,6 +1404,43 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
return encryptedCiphers;
|
return encryptedCiphers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDecryptedAttachmentBuffer(
|
||||||
|
cipherId: CipherId,
|
||||||
|
attachment: AttachmentView,
|
||||||
|
response: Response,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
const useSdkDecryption = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.PM19941MigrateCipherDomainToSdk,
|
||||||
|
);
|
||||||
|
|
||||||
|
const cipherDomain = await firstValueFrom(
|
||||||
|
this.ciphers$(userId).pipe(map((ciphersData) => new Cipher(ciphersData[cipherId]))),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (useSdkDecryption) {
|
||||||
|
const encryptedContent = await response.arrayBuffer();
|
||||||
|
return this.cipherEncryptionService.decryptAttachmentContent(
|
||||||
|
cipherDomain,
|
||||||
|
attachment,
|
||||||
|
new Uint8Array(encryptedContent),
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||||
|
const key =
|
||||||
|
attachment.key != null
|
||||||
|
? attachment.key
|
||||||
|
: await firstValueFrom(
|
||||||
|
this.keyService.orgKeys$(userId).pipe(
|
||||||
|
filterOutNullish(),
|
||||||
|
map((orgKeys) => orgKeys[cipherDomain.organizationId as OrganizationId] as OrgKey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return await this.encryptService.decryptFileData(encBuf, key);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns a SingleUserState
|
* @returns a SingleUserState
|
||||||
*/
|
*/
|
||||||
@@ -1430,9 +1490,7 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
originalCipher: Cipher,
|
originalCipher: Cipher,
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const existingCipher = await originalCipher.decrypt(
|
const existingCipher = await this.decrypt(originalCipher, userId);
|
||||||
await this.getKeyForCipherKeyDecryption(originalCipher, userId),
|
|
||||||
);
|
|
||||||
model.passwordHistory = existingCipher.passwordHistory || [];
|
model.passwordHistory = existingCipher.passwordHistory || [];
|
||||||
if (model.type === CipherType.Login && existingCipher.type === CipherType.Login) {
|
if (model.type === CipherType.Login && existingCipher.type === CipherType.Login) {
|
||||||
if (
|
if (
|
||||||
@@ -1852,4 +1910,17 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
);
|
);
|
||||||
return featureEnabled && meetsServerVersion;
|
return featureEnabled && meetsServerVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the provided ciphers using the SDK.
|
||||||
|
* @param ciphers The ciphers to decrypt.
|
||||||
|
* @param userId The user ID to use for decryption.
|
||||||
|
* @returns The decrypted ciphers.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async decryptCiphersWithSdk(ciphers: Cipher[], userId: UserId): Promise<CipherView[]> {
|
||||||
|
const decryptedViews = await this.cipherEncryptionService.decryptManyLegacy(ciphers, userId);
|
||||||
|
|
||||||
|
return decryptedViews.sort(this.getLocaleSortingFunction());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,334 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Fido2Credential,
|
||||||
|
Cipher as SdkCipher,
|
||||||
|
CipherType as SdkCipherType,
|
||||||
|
CipherView as SdkCipherView,
|
||||||
|
CipherListView,
|
||||||
|
Attachment as SdkAttachment,
|
||||||
|
} from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
|
import { mockEnc } from "../../../spec";
|
||||||
|
import { UriMatchStrategy } from "../../models/domain/domain-service";
|
||||||
|
import { LogService } from "../../platform/abstractions/log.service";
|
||||||
|
import { SdkService } from "../../platform/abstractions/sdk/sdk.service";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
|
import { CipherRepromptType, CipherType } from "../enums";
|
||||||
|
import { CipherPermissionsApi } from "../models/api/cipher-permissions.api";
|
||||||
|
import { CipherData } from "../models/data/cipher.data";
|
||||||
|
import { Cipher } from "../models/domain/cipher";
|
||||||
|
import { AttachmentView } from "../models/view/attachment.view";
|
||||||
|
import { CipherView } from "../models/view/cipher.view";
|
||||||
|
import { Fido2CredentialView } from "../models/view/fido2-credential.view";
|
||||||
|
|
||||||
|
import { DefaultCipherEncryptionService } from "./default-cipher-encryption.service";
|
||||||
|
|
||||||
|
const cipherData: CipherData = {
|
||||||
|
id: "id",
|
||||||
|
organizationId: "orgId",
|
||||||
|
folderId: "folderId",
|
||||||
|
edit: true,
|
||||||
|
viewPassword: true,
|
||||||
|
organizationUseTotp: true,
|
||||||
|
favorite: false,
|
||||||
|
revisionDate: "2022-01-31T12:00:00.000Z",
|
||||||
|
type: CipherType.Login,
|
||||||
|
name: "EncryptedString",
|
||||||
|
notes: "EncryptedString",
|
||||||
|
creationDate: "2022-01-01T12:00:00.000Z",
|
||||||
|
deletedDate: null,
|
||||||
|
permissions: new CipherPermissionsApi(),
|
||||||
|
key: "EncKey",
|
||||||
|
reprompt: CipherRepromptType.None,
|
||||||
|
login: {
|
||||||
|
uris: [
|
||||||
|
{ uri: "EncryptedString", uriChecksum: "EncryptedString", match: UriMatchStrategy.Domain },
|
||||||
|
],
|
||||||
|
username: "EncryptedString",
|
||||||
|
password: "EncryptedString",
|
||||||
|
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
|
||||||
|
totp: "EncryptedString",
|
||||||
|
autofillOnPageLoad: false,
|
||||||
|
},
|
||||||
|
passwordHistory: [{ password: "EncryptedString", lastUsedDate: "2022-01-31T12:00:00.000Z" }],
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
url: "url",
|
||||||
|
size: "1100",
|
||||||
|
sizeName: "1.1 KB",
|
||||||
|
fileName: "file",
|
||||||
|
key: "EncKey",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a2",
|
||||||
|
url: "url",
|
||||||
|
size: "1100",
|
||||||
|
sizeName: "1.1 KB",
|
||||||
|
fileName: "file",
|
||||||
|
key: "EncKey",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("DefaultCipherEncryptionService", () => {
|
||||||
|
let cipherEncryptionService: DefaultCipherEncryptionService;
|
||||||
|
const sdkService = mock<SdkService>();
|
||||||
|
const logService = mock<LogService>();
|
||||||
|
let sdkCipherView: SdkCipherView;
|
||||||
|
|
||||||
|
const mockSdkClient = {
|
||||||
|
vault: jest.fn().mockReturnValue({
|
||||||
|
ciphers: jest.fn().mockReturnValue({
|
||||||
|
decrypt: jest.fn(),
|
||||||
|
decrypt_list: jest.fn(),
|
||||||
|
decrypt_fido2_credentials: jest.fn(),
|
||||||
|
}),
|
||||||
|
attachments: jest.fn().mockReturnValue({
|
||||||
|
decrypt_buffer: jest.fn(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const mockRef = {
|
||||||
|
value: mockSdkClient,
|
||||||
|
[Symbol.dispose]: jest.fn(),
|
||||||
|
};
|
||||||
|
const mockSdk = {
|
||||||
|
take: jest.fn().mockReturnValue(mockRef),
|
||||||
|
};
|
||||||
|
|
||||||
|
const userId = "user-id" as UserId;
|
||||||
|
|
||||||
|
let cipherObj: Cipher;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sdkService.userClient$ = jest.fn((userId: UserId) => of(mockSdk)) as any;
|
||||||
|
cipherEncryptionService = new DefaultCipherEncryptionService(sdkService, logService);
|
||||||
|
cipherObj = new Cipher(cipherData);
|
||||||
|
|
||||||
|
jest.spyOn(cipherObj, "toSdkCipher").mockImplementation(() => {
|
||||||
|
return { id: cipherData.id } as SdkCipher;
|
||||||
|
});
|
||||||
|
|
||||||
|
sdkCipherView = {
|
||||||
|
id: "test-id",
|
||||||
|
type: SdkCipherType.Login,
|
||||||
|
name: "test-name",
|
||||||
|
login: {
|
||||||
|
username: "test-username",
|
||||||
|
password: "test-password",
|
||||||
|
},
|
||||||
|
} as SdkCipherView;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("decrypt", () => {
|
||||||
|
it("should decrypt a cipher successfully", async () => {
|
||||||
|
const expectedCipherView: CipherView = {
|
||||||
|
id: "test-id",
|
||||||
|
type: CipherType.Login,
|
||||||
|
name: "test-name",
|
||||||
|
login: {
|
||||||
|
username: "test-username",
|
||||||
|
password: "test-password",
|
||||||
|
},
|
||||||
|
} as CipherView;
|
||||||
|
|
||||||
|
mockSdkClient.vault().ciphers().decrypt.mockReturnValue(sdkCipherView);
|
||||||
|
jest.spyOn(CipherView, "fromSdkCipherView").mockReturnValue(expectedCipherView);
|
||||||
|
|
||||||
|
const result = await cipherEncryptionService.decrypt(cipherObj, userId);
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedCipherView);
|
||||||
|
expect(cipherObj.toSdkCipher).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockSdkClient.vault().ciphers().decrypt).toHaveBeenCalledWith({ id: cipherData.id });
|
||||||
|
expect(CipherView.fromSdkCipherView).toHaveBeenCalledWith(sdkCipherView);
|
||||||
|
expect(mockSdkClient.vault().ciphers().decrypt_fido2_credentials).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should decrypt FIDO2 credentials if present", async () => {
|
||||||
|
const fido2Credentials = [
|
||||||
|
{
|
||||||
|
credentialId: mockEnc("credentialId"),
|
||||||
|
keyType: mockEnc("keyType"),
|
||||||
|
keyAlgorithm: mockEnc("keyAlgorithm"),
|
||||||
|
keyCurve: mockEnc("keyCurve"),
|
||||||
|
keyValue: mockEnc("keyValue"),
|
||||||
|
rpId: mockEnc("rpId"),
|
||||||
|
userHandle: mockEnc("userHandle"),
|
||||||
|
userName: mockEnc("userName"),
|
||||||
|
counter: mockEnc("2"),
|
||||||
|
rpName: mockEnc("rpName"),
|
||||||
|
userDisplayName: mockEnc("userDisplayName"),
|
||||||
|
discoverable: mockEnc("true"),
|
||||||
|
creationDate: new Date("2023-01-01T12:00:00.000Z"),
|
||||||
|
},
|
||||||
|
] as unknown as Fido2Credential[];
|
||||||
|
|
||||||
|
sdkCipherView.login!.fido2Credentials = fido2Credentials;
|
||||||
|
|
||||||
|
const expectedCipherView: CipherView = {
|
||||||
|
id: "test-id",
|
||||||
|
type: CipherType.Login,
|
||||||
|
name: "test-name",
|
||||||
|
login: {
|
||||||
|
username: "test-username",
|
||||||
|
password: "test-password",
|
||||||
|
fido2Credentials: [],
|
||||||
|
},
|
||||||
|
} as unknown as CipherView;
|
||||||
|
|
||||||
|
const fido2CredentialView: Fido2CredentialView = {
|
||||||
|
credentialId: "credentialId",
|
||||||
|
keyType: "keyType",
|
||||||
|
keyAlgorithm: "keyAlgorithm",
|
||||||
|
keyCurve: "keyCurve",
|
||||||
|
keyValue: "decrypted-key-value",
|
||||||
|
rpId: "rpId",
|
||||||
|
userHandle: "userHandle",
|
||||||
|
userName: "userName",
|
||||||
|
counter: 2,
|
||||||
|
rpName: "rpName",
|
||||||
|
userDisplayName: "userDisplayName",
|
||||||
|
discoverable: true,
|
||||||
|
creationDate: new Date("2023-01-01T12:00:00.000Z"),
|
||||||
|
} as unknown as Fido2CredentialView;
|
||||||
|
|
||||||
|
mockSdkClient.vault().ciphers().decrypt.mockReturnValue(sdkCipherView);
|
||||||
|
mockSdkClient.vault().ciphers().decrypt_fido2_credentials.mockReturnValue(fido2Credentials);
|
||||||
|
mockSdkClient.vault().ciphers().decrypt_fido2_private_key = jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue("decrypted-key-value");
|
||||||
|
|
||||||
|
jest.spyOn(CipherView, "fromSdkCipherView").mockReturnValue(expectedCipherView);
|
||||||
|
jest
|
||||||
|
.spyOn(Fido2CredentialView, "fromSdkFido2CredentialView")
|
||||||
|
.mockReturnValueOnce(fido2CredentialView);
|
||||||
|
|
||||||
|
const result = await cipherEncryptionService.decrypt(cipherObj, userId);
|
||||||
|
|
||||||
|
expect(result).toBe(expectedCipherView);
|
||||||
|
expect(result.login?.fido2Credentials).toEqual([fido2CredentialView]);
|
||||||
|
expect(mockSdkClient.vault().ciphers().decrypt_fido2_credentials).toHaveBeenCalledWith(
|
||||||
|
sdkCipherView,
|
||||||
|
);
|
||||||
|
expect(mockSdkClient.vault().ciphers().decrypt_fido2_private_key).toHaveBeenCalledWith(
|
||||||
|
sdkCipherView,
|
||||||
|
);
|
||||||
|
expect(Fido2CredentialView.fromSdkFido2CredentialView).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("decryptManyLegacy", () => {
|
||||||
|
it("should decrypt multiple ciphers successfully", async () => {
|
||||||
|
const ciphers = [new Cipher(cipherData), new Cipher(cipherData)];
|
||||||
|
|
||||||
|
const expectedViews = [
|
||||||
|
{
|
||||||
|
id: "test-id-1",
|
||||||
|
name: "test-name-1",
|
||||||
|
} as CipherView,
|
||||||
|
{
|
||||||
|
id: "test-id-2",
|
||||||
|
name: "test-name-2",
|
||||||
|
} as CipherView,
|
||||||
|
];
|
||||||
|
|
||||||
|
mockSdkClient
|
||||||
|
.vault()
|
||||||
|
.ciphers()
|
||||||
|
.decrypt.mockReturnValueOnce({ id: "test-id-1", name: "test-name-1" } as SdkCipherView)
|
||||||
|
.mockReturnValueOnce({ id: "test-id-2", name: "test-name-2" } as SdkCipherView);
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(CipherView, "fromSdkCipherView")
|
||||||
|
.mockReturnValueOnce(expectedViews[0])
|
||||||
|
.mockReturnValueOnce(expectedViews[1]);
|
||||||
|
|
||||||
|
const result = await cipherEncryptionService.decryptManyLegacy(ciphers, userId);
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedViews);
|
||||||
|
expect(mockSdkClient.vault().ciphers().decrypt).toHaveBeenCalledTimes(2);
|
||||||
|
expect(CipherView.fromSdkCipherView).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw EmptyError when SDK is not available", async () => {
|
||||||
|
sdkService.userClient$ = jest.fn().mockReturnValue(of(null)) as any;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
cipherEncryptionService.decryptManyLegacy([cipherObj], userId),
|
||||||
|
).rejects.toThrow();
|
||||||
|
|
||||||
|
expect(logService.error).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("Failed to decrypt ciphers"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("decryptMany", () => {
|
||||||
|
it("should decrypt multiple ciphers to list views", async () => {
|
||||||
|
const ciphers = [new Cipher(cipherData), new Cipher(cipherData)];
|
||||||
|
|
||||||
|
const expectedListViews = [
|
||||||
|
{ id: "list1", name: "List 1" } as CipherListView,
|
||||||
|
{ id: "list2", name: "List 2" } as CipherListView,
|
||||||
|
];
|
||||||
|
|
||||||
|
mockSdkClient.vault().ciphers().decrypt_list.mockReturnValue(expectedListViews);
|
||||||
|
|
||||||
|
const result = await cipherEncryptionService.decryptMany(ciphers, userId);
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedListViews);
|
||||||
|
expect(mockSdkClient.vault().ciphers().decrypt_list).toHaveBeenCalledWith(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ id: cipherData.id }),
|
||||||
|
expect.objectContaining({ id: cipherData.id }),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw EmptyError when SDK is not available", async () => {
|
||||||
|
sdkService.userClient$ = jest.fn().mockReturnValue(of(null)) as any;
|
||||||
|
|
||||||
|
await expect(cipherEncryptionService.decryptMany([cipherObj], userId)).rejects.toThrow();
|
||||||
|
|
||||||
|
expect(logService.error).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("Failed to decrypt cipher list"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("decryptAttachmentContent", () => {
|
||||||
|
it("should decrypt attachment content successfully", async () => {
|
||||||
|
const cipher = new Cipher(cipherData);
|
||||||
|
const attachment = new AttachmentView(cipher.attachments![0]);
|
||||||
|
const encryptedContent = new Uint8Array([1, 2, 3, 4]);
|
||||||
|
const expectedDecryptedContent = new Uint8Array([5, 6, 7, 8]);
|
||||||
|
|
||||||
|
jest.spyOn(cipher, "toSdkCipher").mockReturnValue({ id: "id" } as SdkCipher);
|
||||||
|
jest.spyOn(attachment, "toSdkAttachmentView").mockReturnValue({ id: "a1" } as SdkAttachment);
|
||||||
|
mockSdkClient.vault().attachments().decrypt_buffer.mockReturnValue(expectedDecryptedContent);
|
||||||
|
|
||||||
|
const result = await cipherEncryptionService.decryptAttachmentContent(
|
||||||
|
cipher,
|
||||||
|
attachment,
|
||||||
|
encryptedContent,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedDecryptedContent);
|
||||||
|
expect(cipher.toSdkCipher).toHaveBeenCalled();
|
||||||
|
expect(attachment.toSdkAttachmentView).toHaveBeenCalled();
|
||||||
|
expect(mockSdkClient.vault().attachments().decrypt_buffer).toHaveBeenCalledWith(
|
||||||
|
{ id: "id" },
|
||||||
|
{ id: "a1" },
|
||||||
|
encryptedContent,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import { EMPTY, catchError, firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
|
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
|
import { LogService } from "../../platform/abstractions/log.service";
|
||||||
|
import { SdkService } from "../../platform/abstractions/sdk/sdk.service";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
|
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
|
||||||
|
import { CipherType } from "../enums";
|
||||||
|
import { Cipher } from "../models/domain/cipher";
|
||||||
|
import { AttachmentView } from "../models/view/attachment.view";
|
||||||
|
import { CipherView } from "../models/view/cipher.view";
|
||||||
|
import { Fido2CredentialView } from "../models/view/fido2-credential.view";
|
||||||
|
|
||||||
|
export class DefaultCipherEncryptionService implements CipherEncryptionService {
|
||||||
|
constructor(
|
||||||
|
private sdkService: SdkService,
|
||||||
|
private logService: LogService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async decrypt(cipher: Cipher, userId: UserId): Promise<CipherView> {
|
||||||
|
return firstValueFrom(
|
||||||
|
this.sdkService.userClient$(userId).pipe(
|
||||||
|
map((sdk) => {
|
||||||
|
if (!sdk) {
|
||||||
|
throw new Error("SDK not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
using ref = sdk.take();
|
||||||
|
const sdkCipherView = ref.value.vault().ciphers().decrypt(cipher.toSdkCipher());
|
||||||
|
|
||||||
|
const clientCipherView = CipherView.fromSdkCipherView(sdkCipherView)!;
|
||||||
|
|
||||||
|
// Decrypt Fido2 credentials if available
|
||||||
|
if (
|
||||||
|
clientCipherView.type === CipherType.Login &&
|
||||||
|
sdkCipherView.login?.fido2Credentials?.length
|
||||||
|
) {
|
||||||
|
const fido2CredentialViews = ref.value
|
||||||
|
.vault()
|
||||||
|
.ciphers()
|
||||||
|
.decrypt_fido2_credentials(sdkCipherView);
|
||||||
|
|
||||||
|
// TEMPORARY: Manually decrypt the keyValue for Fido2 credentials
|
||||||
|
// since we don't currently use the SDK for Fido2 Authentication.
|
||||||
|
const decryptedKeyValue = ref.value
|
||||||
|
.vault()
|
||||||
|
.ciphers()
|
||||||
|
.decrypt_fido2_private_key(sdkCipherView);
|
||||||
|
|
||||||
|
clientCipherView.login.fido2Credentials = fido2CredentialViews
|
||||||
|
.map((f) => {
|
||||||
|
const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...view,
|
||||||
|
keyValue: decryptedKeyValue,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((view): view is Fido2CredentialView => view !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientCipherView;
|
||||||
|
}),
|
||||||
|
catchError((error: unknown) => {
|
||||||
|
this.logService.error(`Failed to decrypt cipher ${error}`);
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptManyLegacy(ciphers: Cipher[], userId: UserId): Promise<CipherView[]> {
|
||||||
|
return firstValueFrom(
|
||||||
|
this.sdkService.userClient$(userId).pipe(
|
||||||
|
map((sdk) => {
|
||||||
|
if (!sdk) {
|
||||||
|
throw new Error("SDK not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
using ref = sdk.take();
|
||||||
|
|
||||||
|
return ciphers.map((cipher) => {
|
||||||
|
const sdkCipherView = ref.value.vault().ciphers().decrypt(cipher.toSdkCipher());
|
||||||
|
const clientCipherView = CipherView.fromSdkCipherView(sdkCipherView)!;
|
||||||
|
|
||||||
|
// Handle FIDO2 credentials if present
|
||||||
|
if (
|
||||||
|
clientCipherView.type === CipherType.Login &&
|
||||||
|
sdkCipherView.login?.fido2Credentials?.length
|
||||||
|
) {
|
||||||
|
const fido2CredentialViews = ref.value
|
||||||
|
.vault()
|
||||||
|
.ciphers()
|
||||||
|
.decrypt_fido2_credentials(sdkCipherView);
|
||||||
|
|
||||||
|
// TODO (PM-21259): Remove manual keyValue decryption for FIDO2 credentials.
|
||||||
|
// This is a temporary workaround until we can use the SDK for FIDO2 authentication.
|
||||||
|
const decryptedKeyValue = ref.value
|
||||||
|
.vault()
|
||||||
|
.ciphers()
|
||||||
|
.decrypt_fido2_private_key(sdkCipherView);
|
||||||
|
|
||||||
|
clientCipherView.login.fido2Credentials = fido2CredentialViews
|
||||||
|
.map((f) => {
|
||||||
|
const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!;
|
||||||
|
return {
|
||||||
|
...view,
|
||||||
|
keyValue: decryptedKeyValue,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((view): view is Fido2CredentialView => view !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientCipherView;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
catchError((error: unknown) => {
|
||||||
|
this.logService.error(`Failed to decrypt ciphers: ${error}`);
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptMany(ciphers: Cipher[], userId: UserId): Promise<CipherListView[]> {
|
||||||
|
return firstValueFrom(
|
||||||
|
this.sdkService.userClient$(userId).pipe(
|
||||||
|
map((sdk) => {
|
||||||
|
if (!sdk) {
|
||||||
|
throw new Error("SDK is undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
using ref = sdk.take();
|
||||||
|
|
||||||
|
return ref.value
|
||||||
|
.vault()
|
||||||
|
.ciphers()
|
||||||
|
.decrypt_list(ciphers.map((cipher) => cipher.toSdkCipher()));
|
||||||
|
}),
|
||||||
|
catchError((error: unknown) => {
|
||||||
|
this.logService.error(`Failed to decrypt cipher list: ${error}`);
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts an attachment's content from a response object.
|
||||||
|
*
|
||||||
|
* @param cipher The encrypted cipher object that owns the attachment
|
||||||
|
* @param attachment The encrypted attachment object
|
||||||
|
* @param encryptedContent The encrypted content as a Uint8Array
|
||||||
|
* @param userId The user ID whose key will be used for decryption
|
||||||
|
*
|
||||||
|
* @returns A promise that resolves to the decrypted content
|
||||||
|
*/
|
||||||
|
async decryptAttachmentContent(
|
||||||
|
cipher: Cipher,
|
||||||
|
attachment: AttachmentView,
|
||||||
|
encryptedContent: Uint8Array,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
return firstValueFrom(
|
||||||
|
this.sdkService.userClient$(userId).pipe(
|
||||||
|
map((sdk) => {
|
||||||
|
if (!sdk) {
|
||||||
|
throw new Error("SDK is undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
using ref = sdk.take();
|
||||||
|
|
||||||
|
return ref.value
|
||||||
|
.vault()
|
||||||
|
.attachments()
|
||||||
|
.decrypt_buffer(
|
||||||
|
cipher.toSdkCipher(),
|
||||||
|
attachment.toSdkAttachmentView(),
|
||||||
|
encryptedContent,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
catchError((error: unknown) => {
|
||||||
|
this.logService.error(`Failed to decrypt cipher buffer: ${error}`);
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -118,9 +118,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
|
|||||||
const activeUserId = await firstValueFrom(
|
const activeUserId = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
);
|
);
|
||||||
const view = await cipher.decrypt(
|
const view = await this.cipherService.decrypt(cipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
|
||||||
);
|
|
||||||
this.cleanupCipher(view);
|
this.cleanupCipher(view);
|
||||||
this.result.ciphers.push(view);
|
this.result.ciphers.push(view);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
|
|||||||
import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export";
|
import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
@@ -172,6 +172,8 @@ describe("VaultExportService", () => {
|
|||||||
let apiService: MockProxy<ApiService>;
|
let apiService: MockProxy<ApiService>;
|
||||||
let fetchMock: jest.Mock;
|
let fetchMock: jest.Mock;
|
||||||
|
|
||||||
|
const userId = "" as UserId;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cryptoFunctionService = mock<CryptoFunctionService>();
|
cryptoFunctionService = mock<CryptoFunctionService>();
|
||||||
cipherService = mock<CipherService>();
|
cipherService = mock<CipherService>();
|
||||||
@@ -185,7 +187,6 @@ describe("VaultExportService", () => {
|
|||||||
|
|
||||||
keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any));
|
keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any));
|
||||||
|
|
||||||
const userId = "" as UserId;
|
|
||||||
const accountInfo: AccountInfo = {
|
const accountInfo: AccountInfo = {
|
||||||
email: "",
|
email: "",
|
||||||
emailVerified: true,
|
emailVerified: true,
|
||||||
@@ -338,7 +339,9 @@ describe("VaultExportService", () => {
|
|||||||
|
|
||||||
cipherService.getAllDecrypted.mockResolvedValue([cipherView]);
|
cipherService.getAllDecrypted.mockResolvedValue([cipherView]);
|
||||||
folderService.getAllDecryptedFromState.mockResolvedValue([]);
|
folderService.getAllDecryptedFromState.mockResolvedValue([]);
|
||||||
encryptService.decryptFileData.mockResolvedValue(new Uint8Array(255));
|
cipherService.getDecryptedAttachmentBuffer.mockRejectedValue(
|
||||||
|
new Error("Error decrypting attachment"),
|
||||||
|
);
|
||||||
|
|
||||||
global.fetch = jest.fn(() =>
|
global.fetch = jest.fn(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
@@ -356,13 +359,17 @@ describe("VaultExportService", () => {
|
|||||||
it("contains attachments with folders", async () => {
|
it("contains attachments with folders", async () => {
|
||||||
const cipherData = new CipherData();
|
const cipherData = new CipherData();
|
||||||
cipherData.id = "mock-id";
|
cipherData.id = "mock-id";
|
||||||
|
const cipherRecord: Record<CipherId, CipherData> = {
|
||||||
|
["mock-id" as CipherId]: cipherData,
|
||||||
|
};
|
||||||
const cipherView = new CipherView(new Cipher(cipherData));
|
const cipherView = new CipherView(new Cipher(cipherData));
|
||||||
const attachmentView = new AttachmentView(new Attachment(new AttachmentData()));
|
const attachmentView = new AttachmentView(new Attachment(new AttachmentData()));
|
||||||
attachmentView.fileName = "mock-file-name";
|
attachmentView.fileName = "mock-file-name";
|
||||||
cipherView.attachments = [attachmentView];
|
cipherView.attachments = [attachmentView];
|
||||||
|
cipherService.ciphers$.mockReturnValue(of(cipherRecord));
|
||||||
cipherService.getAllDecrypted.mockResolvedValue([cipherView]);
|
cipherService.getAllDecrypted.mockResolvedValue([cipherView]);
|
||||||
folderService.getAllDecryptedFromState.mockResolvedValue([]);
|
folderService.getAllDecryptedFromState.mockResolvedValue([]);
|
||||||
encryptService.decryptFileData.mockResolvedValue(new Uint8Array(255));
|
cipherService.getDecryptedAttachmentBuffer.mockResolvedValue(new Uint8Array(255));
|
||||||
global.fetch = jest.fn(() =>
|
global.fetch = jest.fn(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -12,14 +12,12 @@ import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/a
|
|||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export";
|
import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
import { Folder } from "@bitwarden/common/vault/models/domain/folder";
|
import { Folder } from "@bitwarden/common/vault/models/domain/folder";
|
||||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||||
@@ -118,8 +116,19 @@ export class IndividualVaultExportService
|
|||||||
const cipherFolder = attachmentsFolder.folder(cipher.id);
|
const cipherFolder = attachmentsFolder.folder(cipher.id);
|
||||||
for (const attachment of cipher.attachments) {
|
for (const attachment of cipher.attachments) {
|
||||||
const response = await this.downloadAttachment(cipher.id, attachment.id);
|
const response = await this.downloadAttachment(cipher.id, attachment.id);
|
||||||
const decBuf = await this.decryptAttachment(cipher, attachment, response);
|
|
||||||
cipherFolder.file(attachment.fileName, decBuf);
|
try {
|
||||||
|
const decBuf = await this.cipherService.getDecryptedAttachmentBuffer(
|
||||||
|
cipher.id as CipherId,
|
||||||
|
attachment,
|
||||||
|
response,
|
||||||
|
activeUserId,
|
||||||
|
);
|
||||||
|
|
||||||
|
cipherFolder.file(attachment.fileName, decBuf);
|
||||||
|
} catch {
|
||||||
|
throw new Error("Error decrypting attachment");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,23 +155,6 @@ export class IndividualVaultExportService
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async decryptAttachment(
|
|
||||||
cipher: CipherView,
|
|
||||||
attachment: AttachmentView,
|
|
||||||
response: Response,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
|
||||||
const key =
|
|
||||||
attachment.key != null
|
|
||||||
? attachment.key
|
|
||||||
: await this.keyService.getOrgKey(cipher.organizationId);
|
|
||||||
return await this.encryptService.decryptFileData(encBuf, key);
|
|
||||||
} catch {
|
|
||||||
throw new Error("Error decrypting attachment");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getDecryptedExport(
|
private async getDecryptedExport(
|
||||||
activeUserId: UserId,
|
activeUserId: UserId,
|
||||||
format: "json" | "csv",
|
format: "json" | "csv",
|
||||||
|
|||||||
@@ -155,12 +155,9 @@ export class OrganizationVaultExportService
|
|||||||
.forEach(async (c) => {
|
.forEach(async (c) => {
|
||||||
const cipher = new Cipher(new CipherData(c));
|
const cipher = new Cipher(new CipherData(c));
|
||||||
exportPromises.push(
|
exportPromises.push(
|
||||||
this.cipherService
|
this.cipherService.decrypt(cipher, activeUserId).then((decCipher) => {
|
||||||
.getKeyForCipherKeyDecryption(cipher, activeUserId)
|
decCiphers.push(decCipher);
|
||||||
.then((key) => cipher.decrypt(key))
|
}),
|
||||||
.then((decCipher) => {
|
|
||||||
decCiphers.push(decCipher);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ describe("CipherAttachmentsComponent", () => {
|
|||||||
get: cipherServiceGet,
|
get: cipherServiceGet,
|
||||||
saveAttachmentWithServer,
|
saveAttachmentWithServer,
|
||||||
getKeyForCipherKeyDecryption: () => Promise.resolve(null),
|
getKeyForCipherKeyDecryption: () => Promise.resolve(null),
|
||||||
|
decrypt: jest.fn().mockResolvedValue(cipherView),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -137,9 +137,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
|||||||
this.organization = await this.getOrganization();
|
this.organization = await this.getOrganization();
|
||||||
this.cipherDomain = await this.getCipher(this.cipherId);
|
this.cipherDomain = await this.getCipher(this.cipherId);
|
||||||
|
|
||||||
this.cipher = await this.cipherDomain.decrypt(
|
this.cipher = await this.cipherService.decrypt(this.cipherDomain, this.activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, this.activeUserId),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update the initial state of the submit button
|
// Update the initial state of the submit button
|
||||||
if (this.submitBtn) {
|
if (this.submitBtn) {
|
||||||
@@ -210,9 +208,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// re-decrypt the cipher to update the attachments
|
// re-decrypt the cipher to update the attachments
|
||||||
this.cipher = await this.cipherDomain.decrypt(
|
this.cipher = await this.cipherService.decrypt(this.cipherDomain, this.activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, this.activeUserId),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset reactive form and input element
|
// Reset reactive form and input element
|
||||||
this.fileInput.nativeElement.value = "";
|
this.fileInput.nativeElement.value = "";
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { inject, Injectable } from "@angular/core";
|
import { inject, Injectable } from "@angular/core";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@@ -21,13 +20,10 @@ function isSetEqual(a: Set<string>, b: Set<string>) {
|
|||||||
export class DefaultCipherFormService implements CipherFormService {
|
export class DefaultCipherFormService implements CipherFormService {
|
||||||
private cipherService: CipherService = inject(CipherService);
|
private cipherService: CipherService = inject(CipherService);
|
||||||
private accountService: AccountService = inject(AccountService);
|
private accountService: AccountService = inject(AccountService);
|
||||||
private apiService: ApiService = inject(ApiService);
|
|
||||||
|
|
||||||
async decryptCipher(cipher: Cipher): Promise<CipherView> {
|
async decryptCipher(cipher: Cipher): Promise<CipherView> {
|
||||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
return await cipher.decrypt(
|
return await this.cipherService.decrypt(cipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveCipher(cipher: CipherView, config: CipherFormConfig): Promise<CipherView> {
|
async saveCipher(cipher: CipherView, config: CipherFormConfig): Promise<CipherView> {
|
||||||
@@ -46,9 +42,7 @@ export class DefaultCipherFormService implements CipherFormService {
|
|||||||
// Creating a new cipher
|
// Creating a new cipher
|
||||||
if (cipher.id == null) {
|
if (cipher.id == null) {
|
||||||
savedCipher = await this.cipherService.createWithServer(encryptedCipher, config.admin);
|
savedCipher = await this.cipherService.createWithServer(encryptedCipher, config.admin);
|
||||||
return await savedCipher.decrypt(
|
return await this.cipherService.decrypt(savedCipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(savedCipher, activeUserId),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.originalCipher == null) {
|
if (config.originalCipher == null) {
|
||||||
@@ -100,8 +94,6 @@ export class DefaultCipherFormService implements CipherFormService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await savedCipher.decrypt(
|
return await this.cipherService.decrypt(savedCipher, activeUserId);
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(savedCipher, activeUserId),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,16 @@ import { BehaviorSubject } from "rxjs";
|
|||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { ToastService } from "@bitwarden/components";
|
import { ToastService } from "@bitwarden/components";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
|
||||||
|
|
||||||
import { PasswordRepromptService } from "../../services/password-reprompt.service";
|
import { PasswordRepromptService } from "../../services/password-reprompt.service";
|
||||||
|
|
||||||
@@ -51,6 +52,21 @@ describe("DownloadAttachmentComponent", () => {
|
|||||||
},
|
},
|
||||||
} as CipherView;
|
} as CipherView;
|
||||||
|
|
||||||
|
const ciphers$ = new BehaviorSubject({
|
||||||
|
"5555-444-3333": {
|
||||||
|
id: "5555-444-3333",
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
id: "222-3333-4444",
|
||||||
|
fileName: "encrypted-filename",
|
||||||
|
key: "encrypted-key",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const getFeatureFlag = jest.fn().mockResolvedValue(false);
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
showToast.mockClear();
|
showToast.mockClear();
|
||||||
getAttachmentData.mockClear();
|
getAttachmentData.mockClear();
|
||||||
@@ -60,13 +76,22 @@ describe("DownloadAttachmentComponent", () => {
|
|||||||
imports: [DownloadAttachmentComponent],
|
imports: [DownloadAttachmentComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: EncryptService, useValue: mock<EncryptService>() },
|
{ provide: EncryptService, useValue: mock<EncryptService>() },
|
||||||
{ provide: KeyService, useValue: mock<KeyService>() },
|
|
||||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||||
{ provide: StateProvider, useValue: { activeUserId$ } },
|
{ provide: StateProvider, useValue: { activeUserId$ } },
|
||||||
{ provide: ToastService, useValue: { showToast } },
|
{ provide: ToastService, useValue: { showToast } },
|
||||||
{ provide: ApiService, useValue: { getAttachmentData } },
|
{ provide: ApiService, useValue: { getAttachmentData } },
|
||||||
{ provide: FileDownloadService, useValue: { download } },
|
{ provide: FileDownloadService, useValue: { download } },
|
||||||
{ provide: PasswordRepromptService, useValue: mock<PasswordRepromptService>() },
|
{ provide: PasswordRepromptService, useValue: mock<PasswordRepromptService>() },
|
||||||
|
{
|
||||||
|
provide: ConfigService,
|
||||||
|
useValue: {
|
||||||
|
getFeatureFlag,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: CipherService,
|
||||||
|
useValue: { ciphers$: () => ciphers$, getDecryptedAttachmentBuffer: jest.fn() },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
@@ -128,10 +153,12 @@ describe("DownloadAttachmentComponent", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows an error toast when EncArrayBuffer fails", async () => {
|
it("shows an error toast when getDecryptedAttachmentBuffer fails", async () => {
|
||||||
getAttachmentData.mockResolvedValue({ url: "https://www.downloadattachement.com" });
|
getAttachmentData.mockResolvedValue({ url: "https://www.downloadattachement.com" });
|
||||||
fetchMock.mockResolvedValue({ status: 200 });
|
fetchMock.mockResolvedValue({ status: 200 });
|
||||||
EncArrayBuffer.fromResponse = jest.fn().mockRejectedValue({});
|
|
||||||
|
const cipherService = TestBed.inject(CipherService) as jest.Mocked<CipherService>;
|
||||||
|
cipherService.getDecryptedAttachmentBuffer.mockRejectedValue(new Error());
|
||||||
|
|
||||||
await component.download();
|
await component.download();
|
||||||
|
|
||||||
|
|||||||
@@ -2,23 +2,19 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, Input } from "@angular/core";
|
import { Component, Input } from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { firstValueFrom } from "rxjs";
|
||||||
import { NEVER, switchMap } from "rxjs";
|
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
import { EmergencyAccessId, OrganizationId } from "@bitwarden/common/types/guid";
|
import { CipherId, EmergencyAccessId } from "@bitwarden/common/types/guid";
|
||||||
import { OrgKey } from "@bitwarden/common/types/key";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { AsyncActionsModule, IconButtonModule, ToastService } from "@bitwarden/components";
|
import { AsyncActionsModule, IconButtonModule, ToastService } from "@bitwarden/components";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -42,29 +38,14 @@ export class DownloadAttachmentComponent {
|
|||||||
/** When owners/admins can mange all items and when accessing from the admin console, use the admin endpoint */
|
/** When owners/admins can mange all items and when accessing from the admin console, use the admin endpoint */
|
||||||
@Input() admin?: boolean = false;
|
@Input() admin?: boolean = false;
|
||||||
|
|
||||||
/** The organization key if the cipher is associated with one */
|
|
||||||
private orgKey: OrgKey | null = null;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private fileDownloadService: FileDownloadService,
|
private fileDownloadService: FileDownloadService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private encryptService: EncryptService,
|
|
||||||
private stateProvider: StateProvider,
|
private stateProvider: StateProvider,
|
||||||
private keyService: KeyService,
|
private cipherService: CipherService,
|
||||||
) {
|
) {}
|
||||||
this.stateProvider.activeUserId$
|
|
||||||
.pipe(
|
|
||||||
switchMap((userId) => (userId !== null ? this.keyService.orgKeys$(userId) : NEVER)),
|
|
||||||
takeUntilDestroyed(),
|
|
||||||
)
|
|
||||||
.subscribe((data: Record<OrganizationId, OrgKey> | null) => {
|
|
||||||
if (data) {
|
|
||||||
this.orgKey = data[this.cipher.organizationId as OrganizationId];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Download the attachment */
|
/** Download the attachment */
|
||||||
download = async () => {
|
download = async () => {
|
||||||
@@ -100,9 +81,15 @@ export class DownloadAttachmentComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||||
const key = this.attachment.key != null ? this.attachment.key : this.orgKey;
|
|
||||||
const decBuf = await this.encryptService.decryptFileData(encBuf, key);
|
const decBuf = await this.cipherService.getDecryptedAttachmentBuffer(
|
||||||
|
this.cipher.id as CipherId,
|
||||||
|
this.attachment,
|
||||||
|
response,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
|
||||||
this.fileDownloadService.download({
|
this.fileDownloadService.download({
|
||||||
fileName: this.attachment.fileName,
|
fileName: this.attachment.fileName,
|
||||||
blobData: decBuf,
|
blobData: decBuf,
|
||||||
|
|||||||
Reference in New Issue
Block a user