mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
[PM-22136] Implement SDK cipher encryption (#15337)
* [PM-22136] Update sdk cipher view map to support uknown uuid type * [PM-22136] Add key to CipherView for copying to SdkCipherView for encryption * [PM-22136] Add fromSdk* helpers to Cipher domain objects * [PM-22136] Add toSdk* helpers to Cipher View objects * [PM-22136] Add encrypt() to cipher encryption service * [PM-22136] Add feature flag * [PM-22136] Use new SDK encrypt method when feature flag is enabled * [PM-22136] Filter out null/empty URIs * [PM-22136] Change default value for cipher view arrays to []. See ADR-0014. * [PM-22136] Keep encrypted key value on attachment so that it is passed to the SDK * [PM-22136] Keep encrypted key value on CipherView so that it is passed to the SDK during encryption * [PM-22136] Update failing attachment test * [PM-22136] Update failing importer tests due to new default value for arrays * [PM-22136] Update CipherView.fromJson to handle the prototype of EncString for the cipher key * [PM-22136] Add tickets for followup work * [PM-22136] Use new set_fido2_credentials SDK method instead * [PM-22136] Fix missing prototype when decrypting Fido2Credentials * [PM-22136] Fix test after sdk change * [PM-22136] Update @bitwarden/sdk-internal version * [PM-22136] Fix some strict typing errors * [PM-23348] Migrate move cipher to org to SDK (#15567) * [PM-23348] Add moveToOrganization method to cipher-encryption.service.ts * [PM-23348] Use cipherEncryptionService.moveToOrganization in cipherService shareWithServer and shareManyWithServer methods * [PM-23348] Update cipherFormService to use the shareWithServer() method instead of encrypt() * [PM-23348] Fix typo * [PM-23348] Add missing docs * [PM-22136] Fix EncString import after merge with main
This commit is contained in:
@@ -54,6 +54,7 @@ export enum FeatureFlag {
|
|||||||
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
|
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
|
||||||
PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk",
|
PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk",
|
||||||
PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view",
|
PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view",
|
||||||
|
PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption",
|
||||||
CipherKeyEncryption = "cipher-key-encryption",
|
CipherKeyEncryption = "cipher-key-encryption",
|
||||||
EndUserNotifications = "pm-10609-end-user-notifications",
|
EndUserNotifications = "pm-10609-end-user-notifications",
|
||||||
RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy",
|
RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy",
|
||||||
@@ -103,6 +104,7 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.RemoveCardItemTypePolicy]: FALSE,
|
[FeatureFlag.RemoveCardItemTypePolicy]: FALSE,
|
||||||
[FeatureFlag.PM22134SdkCipherListView]: FALSE,
|
[FeatureFlag.PM22134SdkCipherListView]: FALSE,
|
||||||
[FeatureFlag.PM19315EndUserActivationMvp]: FALSE,
|
[FeatureFlag.PM19315EndUserActivationMvp]: FALSE,
|
||||||
|
[FeatureFlag.PM22136_SdkCipherEncryption]: FALSE,
|
||||||
|
|
||||||
/* Auth */
|
/* Auth */
|
||||||
[FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE,
|
[FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { EncryptionContext } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherListView } from "@bitwarden/sdk-internal";
|
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId, OrganizationId } from "../../types/guid";
|
||||||
import { Cipher } from "../models/domain/cipher";
|
import { Cipher } from "../models/domain/cipher";
|
||||||
import { AttachmentView } from "../models/view/attachment.view";
|
import { AttachmentView } from "../models/view/attachment.view";
|
||||||
import { CipherView } from "../models/view/cipher.view";
|
import { CipherView } from "../models/view/cipher.view";
|
||||||
@@ -9,6 +10,28 @@ import { CipherView } from "../models/view/cipher.view";
|
|||||||
* Service responsible for encrypting and decrypting ciphers.
|
* Service responsible for encrypting and decrypting ciphers.
|
||||||
*/
|
*/
|
||||||
export abstract class CipherEncryptionService {
|
export abstract class CipherEncryptionService {
|
||||||
|
/**
|
||||||
|
* Encrypts a cipher using the SDK for the given userId.
|
||||||
|
* @param model The cipher view to encrypt
|
||||||
|
* @param userId The user ID to initialize the SDK client with
|
||||||
|
*
|
||||||
|
* @returns A promise that resolves to the encryption context, or undefined if encryption fails
|
||||||
|
*/
|
||||||
|
abstract encrypt(model: CipherView, userId: UserId): Promise<EncryptionContext | undefined>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the cipher to the specified organization by re-encrypting its keys with the organization's key.
|
||||||
|
* The cipher.organizationId will be updated to the new organizationId.
|
||||||
|
* @param model The cipher view to move to the organization
|
||||||
|
* @param organizationId The ID of the organization to move the cipher to
|
||||||
|
* @param userId The user ID to initialize the SDK client with
|
||||||
|
*/
|
||||||
|
abstract moveToOrganization(
|
||||||
|
model: CipherView,
|
||||||
|
organizationId: OrganizationId,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<EncryptionContext | undefined>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypts a cipher using the SDK for the given userId.
|
* Decrypts a cipher using the SDK for the given userId.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -120,11 +120,21 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
|||||||
orgAdmin?: boolean,
|
orgAdmin?: boolean,
|
||||||
isNotClone?: boolean,
|
isNotClone?: boolean,
|
||||||
): Promise<Cipher>;
|
): Promise<Cipher>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a cipher to an organization by re-encrypting its keys with the organization's key.
|
||||||
|
* @param cipher The cipher to move
|
||||||
|
* @param organizationId The Id of the organization to move the cipher to
|
||||||
|
* @param collectionIds The collection Ids to assign the cipher to in the organization
|
||||||
|
* @param userId The Id of the user performing the operation
|
||||||
|
* @param originalCipher Optional original cipher that will be used to compare/update password history
|
||||||
|
*/
|
||||||
abstract shareWithServer(
|
abstract shareWithServer(
|
||||||
cipher: CipherView,
|
cipher: CipherView,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
collectionIds: string[],
|
collectionIds: string[],
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
|
originalCipher?: Cipher,
|
||||||
): Promise<Cipher>;
|
): Promise<Cipher>;
|
||||||
abstract shareManyWithServer(
|
abstract shareManyWithServer(
|
||||||
ciphers: CipherView[],
|
ciphers: CipherView[],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { CipherPermissions as SdkCipherPermissions } from "@bitwarden/sdk-intern
|
|||||||
|
|
||||||
import { BaseResponse } from "../../../models/response/base.response";
|
import { BaseResponse } from "../../../models/response/base.response";
|
||||||
|
|
||||||
export class CipherPermissionsApi extends BaseResponse {
|
export class CipherPermissionsApi extends BaseResponse implements SdkCipherPermissions {
|
||||||
delete: boolean = false;
|
delete: boolean = false;
|
||||||
restore: boolean = false;
|
restore: boolean = false;
|
||||||
|
|
||||||
@@ -35,4 +35,11 @@ export class CipherPermissionsApi extends BaseResponse {
|
|||||||
|
|
||||||
return permissions;
|
return permissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the CipherPermissionsApi to an SdkCipherPermissions
|
||||||
|
*/
|
||||||
|
toSdkCipherPermissions(): SdkCipherPermissions {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,45 @@
|
|||||||
|
import {
|
||||||
|
LocalDataView as SdkLocalDataView,
|
||||||
|
LocalData as SdkLocalData,
|
||||||
|
} from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
export type LocalData = {
|
export type LocalData = {
|
||||||
lastUsedDate?: number;
|
lastUsedDate?: number;
|
||||||
lastLaunched?: number;
|
lastLaunched?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the SdkLocalDataView to LocalData
|
||||||
|
* @param localData
|
||||||
|
*/
|
||||||
|
export function fromSdkLocalData(
|
||||||
|
localData: SdkLocalDataView | SdkLocalData | undefined,
|
||||||
|
): LocalData | undefined {
|
||||||
|
if (localData == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
lastUsedDate: localData.lastUsedDate ? new Date(localData.lastUsedDate).getTime() : undefined,
|
||||||
|
lastLaunched: localData.lastLaunched ? new Date(localData.lastLaunched).getTime() : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the LocalData to SdkLocalData
|
||||||
|
* @param localData
|
||||||
|
*/
|
||||||
|
export function toSdkLocalData(
|
||||||
|
localData: LocalData | undefined,
|
||||||
|
): (SdkLocalDataView & SdkLocalData) | undefined {
|
||||||
|
if (localData == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
lastUsedDate: localData.lastUsedDate
|
||||||
|
? new Date(localData.lastUsedDate).toISOString()
|
||||||
|
: undefined,
|
||||||
|
lastLaunched: localData.lastLaunched
|
||||||
|
? new Date(localData.lastLaunched).toISOString()
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ describe("Attachment", () => {
|
|||||||
sizeName: "1.1 KB",
|
sizeName: "1.1 KB",
|
||||||
fileName: "fileName",
|
fileName: "fileName",
|
||||||
key: expect.any(SymmetricCryptoKey),
|
key: expect.any(SymmetricCryptoKey),
|
||||||
|
encryptedKey: attachment.key,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export class Attachment extends Domain {
|
|||||||
|
|
||||||
if (this.key != null) {
|
if (this.key != null) {
|
||||||
view.key = await this.decryptAttachmentKey(orgId, encKey);
|
view.key = await this.decryptAttachmentKey(orgId, encKey);
|
||||||
|
view.encryptedKey = this.key; // Keep the encrypted key for the view
|
||||||
}
|
}
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
@@ -131,4 +132,24 @@ export class Attachment extends Domain {
|
|||||||
key: this.key?.toJSON(),
|
key: this.key?.toJSON(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps an SDK Attachment object to an Attachment
|
||||||
|
* @param obj - The SDK attachment object
|
||||||
|
*/
|
||||||
|
static fromSdkAttachment(obj: SdkAttachment): Attachment | undefined {
|
||||||
|
if (!obj) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachment = new Attachment();
|
||||||
|
attachment.id = obj.id;
|
||||||
|
attachment.url = obj.url;
|
||||||
|
attachment.size = obj.size;
|
||||||
|
attachment.sizeName = obj.sizeName;
|
||||||
|
attachment.fileName = EncString.fromJSON(obj.fileName);
|
||||||
|
attachment.key = EncString.fromJSON(obj.key);
|
||||||
|
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,4 +103,24 @@ export class Card extends Domain {
|
|||||||
code: this.code?.toJSON(),
|
code: this.code?.toJSON(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps an SDK Card object to a Card
|
||||||
|
* @param obj - The SDK Card object
|
||||||
|
*/
|
||||||
|
static fromSdkCard(obj: SdkCard): Card | undefined {
|
||||||
|
if (obj == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const card = new Card();
|
||||||
|
card.cardholderName = EncString.fromJSON(obj.cardholderName);
|
||||||
|
card.brand = EncString.fromJSON(obj.brand);
|
||||||
|
card.number = EncString.fromJSON(obj.number);
|
||||||
|
card.expMonth = EncString.fromJSON(obj.expMonth);
|
||||||
|
card.expYear = EncString.fromJSON(obj.expYear);
|
||||||
|
card.code = EncString.fromJSON(obj.code);
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
UriMatchType,
|
UriMatchType,
|
||||||
CipherRepromptType as SdkCipherRepromptType,
|
CipherRepromptType as SdkCipherRepromptType,
|
||||||
LoginLinkedIdType,
|
LoginLinkedIdType,
|
||||||
|
Cipher as SdkCipher,
|
||||||
} from "@bitwarden/sdk-internal";
|
} from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils";
|
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils";
|
||||||
@@ -206,7 +207,7 @@ describe("Cipher DTO", () => {
|
|||||||
it("Convert", () => {
|
it("Convert", () => {
|
||||||
const cipher = new Cipher(cipherData);
|
const cipher = new Cipher(cipherData);
|
||||||
|
|
||||||
expect(cipher).toEqual({
|
expect(cipher).toMatchObject({
|
||||||
initializerKey: InitializerKey.Cipher,
|
initializerKey: InitializerKey.Cipher,
|
||||||
id: "id",
|
id: "id",
|
||||||
organizationId: "orgId",
|
organizationId: "orgId",
|
||||||
@@ -339,9 +340,9 @@ describe("Cipher DTO", () => {
|
|||||||
edit: true,
|
edit: true,
|
||||||
viewPassword: true,
|
viewPassword: true,
|
||||||
login: loginView,
|
login: loginView,
|
||||||
attachments: null,
|
attachments: [],
|
||||||
fields: null,
|
fields: [],
|
||||||
passwordHistory: null,
|
passwordHistory: [],
|
||||||
collectionIds: undefined,
|
collectionIds: undefined,
|
||||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||||
@@ -462,9 +463,9 @@ describe("Cipher DTO", () => {
|
|||||||
edit: true,
|
edit: true,
|
||||||
viewPassword: true,
|
viewPassword: true,
|
||||||
secureNote: { type: 0 },
|
secureNote: { type: 0 },
|
||||||
attachments: null,
|
attachments: [],
|
||||||
fields: null,
|
fields: [],
|
||||||
passwordHistory: null,
|
passwordHistory: [],
|
||||||
collectionIds: undefined,
|
collectionIds: undefined,
|
||||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||||
@@ -603,9 +604,9 @@ describe("Cipher DTO", () => {
|
|||||||
edit: true,
|
edit: true,
|
||||||
viewPassword: true,
|
viewPassword: true,
|
||||||
card: cardView,
|
card: cardView,
|
||||||
attachments: null,
|
attachments: [],
|
||||||
fields: null,
|
fields: [],
|
||||||
passwordHistory: null,
|
passwordHistory: [],
|
||||||
collectionIds: undefined,
|
collectionIds: undefined,
|
||||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||||
@@ -768,9 +769,9 @@ describe("Cipher DTO", () => {
|
|||||||
edit: true,
|
edit: true,
|
||||||
viewPassword: true,
|
viewPassword: true,
|
||||||
identity: identityView,
|
identity: identityView,
|
||||||
attachments: null,
|
attachments: [],
|
||||||
fields: null,
|
fields: [],
|
||||||
passwordHistory: null,
|
passwordHistory: [],
|
||||||
collectionIds: undefined,
|
collectionIds: undefined,
|
||||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||||
@@ -1001,6 +1002,167 @@ describe("Cipher DTO", () => {
|
|||||||
revisionDate: "2022-01-31T12:00:00.000Z",
|
revisionDate: "2022-01-31T12:00:00.000Z",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should map from SDK Cipher", () => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
const sdkCipher: SdkCipher = {
|
||||||
|
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 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(),
|
||||||
|
collectionIds: [],
|
||||||
|
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 expectedCipher = new Cipher(cipherData, { lastUsedDate, lastLaunched });
|
||||||
|
|
||||||
|
const cipher = Cipher.fromSdkCipher(sdkCipher);
|
||||||
|
|
||||||
|
expect(cipher).toEqual(expectedCipher);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { uuidToString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||||
import { Cipher as SdkCipher } from "@bitwarden/sdk-internal";
|
import { Cipher as SdkCipher } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { EncString } from "../../../key-management/crypto/models/enc-string";
|
import { EncString } from "../../../key-management/crypto/models/enc-string";
|
||||||
@@ -14,7 +15,7 @@ import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
|||||||
import { CipherType } from "../../enums/cipher-type";
|
import { CipherType } from "../../enums/cipher-type";
|
||||||
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
|
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
|
||||||
import { CipherData } from "../data/cipher.data";
|
import { CipherData } from "../data/cipher.data";
|
||||||
import { LocalData } from "../data/local.data";
|
import { LocalData, fromSdkLocalData, toSdkLocalData } from "../data/local.data";
|
||||||
import { AttachmentView } from "../view/attachment.view";
|
import { AttachmentView } from "../view/attachment.view";
|
||||||
import { CipherView } from "../view/cipher.view";
|
import { CipherView } from "../view/cipher.view";
|
||||||
import { FieldView } from "../view/field.view";
|
import { FieldView } from "../view/field.view";
|
||||||
@@ -361,16 +362,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
viewPassword: this.viewPassword ?? true,
|
viewPassword: this.viewPassword ?? true,
|
||||||
localData: this.localData
|
localData: toSdkLocalData(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()),
|
attachments: this.attachments?.map((a) => a.toSdkAttachment()),
|
||||||
fields: this.fields?.map((f) => f.toSdkField()),
|
fields: this.fields?.map((f) => f.toSdkField()),
|
||||||
passwordHistory: this.passwordHistory?.map((ph) => ph.toSdkPasswordHistory()),
|
passwordHistory: this.passwordHistory?.map((ph) => ph.toSdkPasswordHistory()),
|
||||||
@@ -408,4 +400,50 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
|||||||
|
|
||||||
return sdkCipher;
|
return sdkCipher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps an SDK Cipher object to a Cipher
|
||||||
|
* @param sdkCipher - The SDK Cipher object
|
||||||
|
*/
|
||||||
|
static fromSdkCipher(sdkCipher: SdkCipher | null): Cipher | undefined {
|
||||||
|
if (sdkCipher == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipher = new Cipher();
|
||||||
|
|
||||||
|
cipher.id = sdkCipher.id ? uuidToString(sdkCipher.id) : undefined;
|
||||||
|
cipher.organizationId = sdkCipher.organizationId
|
||||||
|
? uuidToString(sdkCipher.organizationId)
|
||||||
|
: undefined;
|
||||||
|
cipher.folderId = sdkCipher.folderId ? uuidToString(sdkCipher.folderId) : undefined;
|
||||||
|
cipher.collectionIds = sdkCipher.collectionIds ? sdkCipher.collectionIds.map(uuidToString) : [];
|
||||||
|
cipher.key = EncString.fromJSON(sdkCipher.key);
|
||||||
|
cipher.name = EncString.fromJSON(sdkCipher.name);
|
||||||
|
cipher.notes = EncString.fromJSON(sdkCipher.notes);
|
||||||
|
cipher.type = sdkCipher.type;
|
||||||
|
cipher.favorite = sdkCipher.favorite;
|
||||||
|
cipher.organizationUseTotp = sdkCipher.organizationUseTotp;
|
||||||
|
cipher.edit = sdkCipher.edit;
|
||||||
|
cipher.permissions = CipherPermissionsApi.fromSdkCipherPermissions(sdkCipher.permissions);
|
||||||
|
cipher.viewPassword = sdkCipher.viewPassword;
|
||||||
|
cipher.localData = fromSdkLocalData(sdkCipher.localData);
|
||||||
|
cipher.attachments = sdkCipher.attachments?.map((a) => Attachment.fromSdkAttachment(a)) ?? [];
|
||||||
|
cipher.fields = sdkCipher.fields?.map((f) => Field.fromSdkField(f)) ?? [];
|
||||||
|
cipher.passwordHistory =
|
||||||
|
sdkCipher.passwordHistory?.map((ph) => Password.fromSdkPasswordHistory(ph)) ?? [];
|
||||||
|
cipher.creationDate = new Date(sdkCipher.creationDate);
|
||||||
|
cipher.revisionDate = new Date(sdkCipher.revisionDate);
|
||||||
|
cipher.deletedDate = sdkCipher.deletedDate ? new Date(sdkCipher.deletedDate) : null;
|
||||||
|
cipher.reprompt = sdkCipher.reprompt;
|
||||||
|
|
||||||
|
// Cipher type specific properties
|
||||||
|
cipher.login = Login.fromSdkLogin(sdkCipher.login);
|
||||||
|
cipher.secureNote = SecureNote.fromSdkSecureNote(sdkCipher.secureNote);
|
||||||
|
cipher.card = Card.fromSdkCard(sdkCipher.card);
|
||||||
|
cipher.identity = Identity.fromSdkIdentity(sdkCipher.identity);
|
||||||
|
cipher.sshKey = SshKey.fromSdkSshKey(sdkCipher.sshKey);
|
||||||
|
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,4 +173,32 @@ export class Fido2Credential extends Domain {
|
|||||||
creationDate: this.creationDate.toISOString(),
|
creationDate: this.creationDate.toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps an SDK Fido2Credential object to a Fido2Credential
|
||||||
|
* @param obj - The SDK Fido2Credential object
|
||||||
|
*/
|
||||||
|
static fromSdkFido2Credential(obj: SdkFido2Credential): Fido2Credential | undefined {
|
||||||
|
if (!obj) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const credential = new Fido2Credential();
|
||||||
|
|
||||||
|
credential.credentialId = EncString.fromJSON(obj.credentialId);
|
||||||
|
credential.keyType = EncString.fromJSON(obj.keyType);
|
||||||
|
credential.keyAlgorithm = EncString.fromJSON(obj.keyAlgorithm);
|
||||||
|
credential.keyCurve = EncString.fromJSON(obj.keyCurve);
|
||||||
|
credential.keyValue = EncString.fromJSON(obj.keyValue);
|
||||||
|
credential.rpId = EncString.fromJSON(obj.rpId);
|
||||||
|
credential.userHandle = EncString.fromJSON(obj.userHandle);
|
||||||
|
credential.userName = EncString.fromJSON(obj.userName);
|
||||||
|
credential.counter = EncString.fromJSON(obj.counter);
|
||||||
|
credential.rpName = EncString.fromJSON(obj.rpName);
|
||||||
|
credential.userDisplayName = EncString.fromJSON(obj.userDisplayName);
|
||||||
|
credential.discoverable = EncString.fromJSON(obj.discoverable);
|
||||||
|
credential.creationDate = new Date(obj.creationDate);
|
||||||
|
|
||||||
|
return credential;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
|
import {
|
||||||
|
Field as SdkField,
|
||||||
|
FieldType,
|
||||||
|
LoginLinkedIdType,
|
||||||
|
CardLinkedIdType,
|
||||||
|
IdentityLinkedIdType,
|
||||||
|
} from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { mockEnc, mockFromJson } from "../../../../spec";
|
import { mockEnc, mockFromJson } from "../../../../spec";
|
||||||
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
|
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
|
||||||
import { CardLinkedId, FieldType, IdentityLinkedId, LoginLinkedId } from "../../enums";
|
import { CardLinkedId, 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";
|
||||||
|
|
||||||
@@ -103,5 +111,34 @@ describe("Field", () => {
|
|||||||
identityField.linkedId = IdentityLinkedId.LicenseNumber;
|
identityField.linkedId = IdentityLinkedId.LicenseNumber;
|
||||||
expect(identityField.toSdkField().linkedId).toBe(415);
|
expect(identityField.toSdkField().linkedId).toBe(415);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should map from SDK Field", () => {
|
||||||
|
// Test Login LinkedId
|
||||||
|
const loginField: SdkField = {
|
||||||
|
name: undefined,
|
||||||
|
value: undefined,
|
||||||
|
type: FieldType.Linked,
|
||||||
|
linkedId: LoginLinkedIdType.Username,
|
||||||
|
};
|
||||||
|
expect(Field.fromSdkField(loginField)!.linkedId).toBe(100);
|
||||||
|
|
||||||
|
// Test Card LinkedId
|
||||||
|
const cardField: SdkField = {
|
||||||
|
name: undefined,
|
||||||
|
value: undefined,
|
||||||
|
type: FieldType.Linked,
|
||||||
|
linkedId: CardLinkedIdType.Number,
|
||||||
|
};
|
||||||
|
expect(Field.fromSdkField(cardField)!.linkedId).toBe(305);
|
||||||
|
|
||||||
|
// Test Identity LinkedId
|
||||||
|
const identityFieldSdkField: SdkField = {
|
||||||
|
name: undefined,
|
||||||
|
value: undefined,
|
||||||
|
type: FieldType.Linked,
|
||||||
|
linkedId: IdentityLinkedIdType.LicenseNumber,
|
||||||
|
};
|
||||||
|
expect(Field.fromSdkField(identityFieldSdkField)!.linkedId).toBe(415);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -90,4 +90,22 @@ export class Field extends Domain {
|
|||||||
linkedId: this.linkedId as unknown as SdkLinkedIdType,
|
linkedId: this.linkedId as unknown as SdkLinkedIdType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps SDK Field to Field
|
||||||
|
* @param obj The SDK Field object to map
|
||||||
|
*/
|
||||||
|
static fromSdkField(obj: SdkField): Field | undefined {
|
||||||
|
if (!obj) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = new Field();
|
||||||
|
field.name = EncString.fromJSON(obj.name);
|
||||||
|
field.value = EncString.fromJSON(obj.value);
|
||||||
|
field.type = obj.type;
|
||||||
|
field.linkedId = obj.linkedId;
|
||||||
|
|
||||||
|
return field;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,4 +195,36 @@ export class Identity extends Domain {
|
|||||||
licenseNumber: this.licenseNumber?.toJSON(),
|
licenseNumber: this.licenseNumber?.toJSON(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps an SDK Identity object to an Identity
|
||||||
|
* @param obj - The SDK Identity object
|
||||||
|
*/
|
||||||
|
static fromSdkIdentity(obj: SdkIdentity): Identity | undefined {
|
||||||
|
if (obj == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity = new Identity();
|
||||||
|
identity.title = EncString.fromJSON(obj.title);
|
||||||
|
identity.firstName = EncString.fromJSON(obj.firstName);
|
||||||
|
identity.middleName = EncString.fromJSON(obj.middleName);
|
||||||
|
identity.lastName = EncString.fromJSON(obj.lastName);
|
||||||
|
identity.address1 = EncString.fromJSON(obj.address1);
|
||||||
|
identity.address2 = EncString.fromJSON(obj.address2);
|
||||||
|
identity.address3 = EncString.fromJSON(obj.address3);
|
||||||
|
identity.city = EncString.fromJSON(obj.city);
|
||||||
|
identity.state = EncString.fromJSON(obj.state);
|
||||||
|
identity.postalCode = EncString.fromJSON(obj.postalCode);
|
||||||
|
identity.country = EncString.fromJSON(obj.country);
|
||||||
|
identity.company = EncString.fromJSON(obj.company);
|
||||||
|
identity.email = EncString.fromJSON(obj.email);
|
||||||
|
identity.phone = EncString.fromJSON(obj.phone);
|
||||||
|
identity.ssn = EncString.fromJSON(obj.ssn);
|
||||||
|
identity.username = EncString.fromJSON(obj.username);
|
||||||
|
identity.passportNumber = EncString.fromJSON(obj.passportNumber);
|
||||||
|
identity.licenseNumber = EncString.fromJSON(obj.licenseNumber);
|
||||||
|
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,4 +102,17 @@ export class LoginUri extends Domain {
|
|||||||
match: this.match,
|
match: this.match,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromSdkLoginUri(obj: SdkLoginUri): LoginUri | undefined {
|
||||||
|
if (obj == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = new LoginUri();
|
||||||
|
view.uri = EncString.fromJSON(obj.uri);
|
||||||
|
view.uriChecksum = obj.uriChecksum ? EncString.fromJSON(obj.uriChecksum) : undefined;
|
||||||
|
view.match = obj.match;
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,4 +163,31 @@ export class Login extends Domain {
|
|||||||
fido2Credentials: this.fido2Credentials?.map((f) => f.toSdkFido2Credential()),
|
fido2Credentials: this.fido2Credentials?.map((f) => f.toSdkFido2Credential()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps an SDK Login object to a Login
|
||||||
|
* @param obj - The SDK Login object
|
||||||
|
*/
|
||||||
|
static fromSdkLogin(obj: SdkLogin): Login | undefined {
|
||||||
|
if (!obj) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = new Login();
|
||||||
|
|
||||||
|
login.uris =
|
||||||
|
obj.uris?.filter((u) => u.uri != null).map((uri) => LoginUri.fromSdkLoginUri(uri)) ?? [];
|
||||||
|
login.username = EncString.fromJSON(obj.username);
|
||||||
|
login.password = EncString.fromJSON(obj.password);
|
||||||
|
login.passwordRevisionDate = obj.passwordRevisionDate
|
||||||
|
? new Date(obj.passwordRevisionDate)
|
||||||
|
: undefined;
|
||||||
|
login.totp = EncString.fromJSON(obj.totp);
|
||||||
|
login.autofillOnPageLoad = obj.autofillOnPageLoad ?? false;
|
||||||
|
login.fido2Credentials = obj.fido2Credentials?.map((f) =>
|
||||||
|
Fido2Credential.fromSdkFido2Credential(f),
|
||||||
|
);
|
||||||
|
|
||||||
|
return login;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,4 +71,20 @@ export class Password extends Domain {
|
|||||||
lastUsedDate: this.lastUsedDate.toISOString(),
|
lastUsedDate: this.lastUsedDate.toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps an SDK PasswordHistory object to a Password
|
||||||
|
* @param obj - The SDK PasswordHistory object
|
||||||
|
*/
|
||||||
|
static fromSdkPasswordHistory(obj: PasswordHistory): Password | undefined {
|
||||||
|
if (!obj) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordHistory = new Password();
|
||||||
|
passwordHistory.password = EncString.fromJSON(obj.password);
|
||||||
|
passwordHistory.lastUsedDate = new Date(obj.lastUsedDate);
|
||||||
|
|
||||||
|
return passwordHistory;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,4 +54,19 @@ export class SecureNote extends Domain {
|
|||||||
type: this.type,
|
type: this.type,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps an SDK SecureNote object to a SecureNote
|
||||||
|
* @param obj - The SDK SecureNote object
|
||||||
|
*/
|
||||||
|
static fromSdkSecureNote(obj: SdkSecureNote): SecureNote | undefined {
|
||||||
|
if (obj == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secureNote = new SecureNote();
|
||||||
|
secureNote.type = obj.type;
|
||||||
|
|
||||||
|
return secureNote;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,4 +85,21 @@ export class SshKey extends Domain {
|
|||||||
fingerprint: this.keyFingerprint.toJSON(),
|
fingerprint: this.keyFingerprint.toJSON(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps an SDK SshKey object to a SshKey
|
||||||
|
* @param obj - The SDK SshKey object
|
||||||
|
*/
|
||||||
|
static fromSdkSshKey(obj: SdkSshKey): SshKey | undefined {
|
||||||
|
if (obj == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sshKey = new SshKey();
|
||||||
|
sshKey.privateKey = EncString.fromJSON(obj.privateKey);
|
||||||
|
sshKey.publicKey = EncString.fromJSON(obj.publicKey);
|
||||||
|
sshKey.keyFingerprint = EncString.fromJSON(obj.fingerprint);
|
||||||
|
|
||||||
|
return sshKey;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { linkedFieldOption } from "../../linked-field-option.decorator";
|
|||||||
|
|
||||||
import { ItemView } from "./item.view";
|
import { ItemView } from "./item.view";
|
||||||
|
|
||||||
export class CardView extends ItemView {
|
export class CardView extends ItemView implements SdkCardView {
|
||||||
@linkedFieldOption(LinkedId.CardholderName, { sortPosition: 0 })
|
@linkedFieldOption(LinkedId.CardholderName, { sortPosition: 0 })
|
||||||
cardholderName: string = null;
|
cardholderName: string = null;
|
||||||
@linkedFieldOption(LinkedId.ExpMonth, { sortPosition: 3, i18nKey: "expirationMonth" })
|
@linkedFieldOption(LinkedId.ExpMonth, { sortPosition: 3, i18nKey: "expirationMonth" })
|
||||||
@@ -168,4 +168,12 @@ export class CardView extends ItemView {
|
|||||||
|
|
||||||
return cardView;
|
return cardView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the CardView to an SDK CardView.
|
||||||
|
* The view implements the SdkView so we can safely return `this`
|
||||||
|
*/
|
||||||
|
toSdkCardView(): SdkCardView {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||||
|
import { CipherPermissionsApi } from "@bitwarden/common/vault/models/api/cipher-permissions.api";
|
||||||
import {
|
import {
|
||||||
CipherView as SdkCipherView,
|
CipherView as SdkCipherView,
|
||||||
CipherType as SdkCipherType,
|
CipherType as SdkCipherType,
|
||||||
@@ -85,6 +89,25 @@ describe("CipherView", () => {
|
|||||||
|
|
||||||
expect(actual).toMatchObject(expected);
|
expect(actual).toMatchObject(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("handle both string and object inputs for the cipher key", () => {
|
||||||
|
const cipherKeyString = "cipherKeyString";
|
||||||
|
const cipherKeyObject = new EncString("cipherKeyObject");
|
||||||
|
|
||||||
|
// Test with string input
|
||||||
|
let actual = CipherView.fromJSON({
|
||||||
|
key: cipherKeyString,
|
||||||
|
});
|
||||||
|
expect(actual.key).toBeInstanceOf(EncString);
|
||||||
|
expect(actual.key?.toJSON()).toBe(cipherKeyString);
|
||||||
|
|
||||||
|
// Test with object input (which can happen when cipher view is stored in an InMemory state provider)
|
||||||
|
actual = CipherView.fromJSON({
|
||||||
|
key: cipherKeyObject,
|
||||||
|
} as Jsonify<CipherView>);
|
||||||
|
expect(actual.key).toBeInstanceOf(EncString);
|
||||||
|
expect(actual.key?.toJSON()).toBe(cipherKeyObject.toJSON());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("fromSdkCipherView", () => {
|
describe("fromSdkCipherView", () => {
|
||||||
@@ -196,11 +219,80 @@ describe("CipherView", () => {
|
|||||||
__fromSdk: true,
|
__fromSdk: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
passwordHistory: null,
|
passwordHistory: [],
|
||||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||||
revisionDate: new Date("2022-01-02T12:00:00.000Z"),
|
revisionDate: new Date("2022-01-02T12:00:00.000Z"),
|
||||||
deletedDate: null,
|
deletedDate: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("toSdkCipherView", () => {
|
||||||
|
it("maps properties correctly", () => {
|
||||||
|
const cipherView = new CipherView();
|
||||||
|
cipherView.id = "0a54d80c-14aa-4ef8-8c3a-7ea99ce5b602";
|
||||||
|
cipherView.organizationId = "000f2a6e-da5e-4726-87ed-1c5c77322c3c";
|
||||||
|
cipherView.folderId = "41b22db4-8e2a-4ed2-b568-f1186c72922f";
|
||||||
|
cipherView.collectionIds = ["b0473506-3c3c-4260-a734-dfaaf833ab6f"];
|
||||||
|
cipherView.key = new EncString("some-key");
|
||||||
|
cipherView.name = "name";
|
||||||
|
cipherView.notes = "notes";
|
||||||
|
cipherView.type = CipherType.Login;
|
||||||
|
cipherView.favorite = true;
|
||||||
|
cipherView.edit = true;
|
||||||
|
cipherView.viewPassword = false;
|
||||||
|
cipherView.reprompt = CipherRepromptType.None;
|
||||||
|
cipherView.organizationUseTotp = false;
|
||||||
|
cipherView.localData = {
|
||||||
|
lastLaunched: new Date("2022-01-01T12:00:00.000Z").getTime(),
|
||||||
|
lastUsedDate: new Date("2022-01-02T12:00:00.000Z").getTime(),
|
||||||
|
};
|
||||||
|
cipherView.permissions = new CipherPermissionsApi();
|
||||||
|
cipherView.permissions.restore = true;
|
||||||
|
cipherView.permissions.delete = true;
|
||||||
|
cipherView.attachments = [];
|
||||||
|
cipherView.fields = [];
|
||||||
|
cipherView.passwordHistory = [];
|
||||||
|
cipherView.login = new LoginView();
|
||||||
|
cipherView.revisionDate = new Date("2022-01-02T12:00:00.000Z");
|
||||||
|
cipherView.creationDate = new Date("2022-01-02T12:00:00.000Z");
|
||||||
|
|
||||||
|
const sdkCipherView = cipherView.toSdkCipherView();
|
||||||
|
|
||||||
|
expect(sdkCipherView).toMatchObject({
|
||||||
|
id: "0a54d80c-14aa-4ef8-8c3a-7ea99ce5b602",
|
||||||
|
organizationId: "000f2a6e-da5e-4726-87ed-1c5c77322c3c",
|
||||||
|
folderId: "41b22db4-8e2a-4ed2-b568-f1186c72922f",
|
||||||
|
collectionIds: ["b0473506-3c3c-4260-a734-dfaaf833ab6f"],
|
||||||
|
key: "some-key",
|
||||||
|
name: "name",
|
||||||
|
notes: "notes",
|
||||||
|
type: SdkCipherType.Login,
|
||||||
|
favorite: true,
|
||||||
|
edit: true,
|
||||||
|
viewPassword: false,
|
||||||
|
reprompt: SdkCipherRepromptType.None,
|
||||||
|
organizationUseTotp: false,
|
||||||
|
localData: {
|
||||||
|
lastLaunched: "2022-01-01T12:00:00.000Z",
|
||||||
|
lastUsedDate: "2022-01-02T12:00:00.000Z",
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
restore: true,
|
||||||
|
delete: true,
|
||||||
|
},
|
||||||
|
deletedDate: undefined,
|
||||||
|
creationDate: "2022-01-02T12:00:00.000Z",
|
||||||
|
revisionDate: "2022-01-02T12:00:00.000Z",
|
||||||
|
attachments: [],
|
||||||
|
passwordHistory: [],
|
||||||
|
login: undefined,
|
||||||
|
identity: undefined,
|
||||||
|
card: undefined,
|
||||||
|
secureNote: undefined,
|
||||||
|
sshKey: undefined,
|
||||||
|
fields: [],
|
||||||
|
} as SdkCipherView);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||||
|
import { uuidToString, asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||||
import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal";
|
import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { View } from "../../../models/view/view";
|
import { View } from "../../../models/view/view";
|
||||||
@@ -9,7 +11,7 @@ import { DeepJsonify } from "../../../types/deep-jsonify";
|
|||||||
import { CipherType, LinkedIdType } from "../../enums";
|
import { CipherType, LinkedIdType } from "../../enums";
|
||||||
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
||||||
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
|
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
|
||||||
import { LocalData } from "../data/local.data";
|
import { LocalData, toSdkLocalData, fromSdkLocalData } from "../data/local.data";
|
||||||
import { Cipher } from "../domain/cipher";
|
import { Cipher } from "../domain/cipher";
|
||||||
|
|
||||||
import { AttachmentView } from "./attachment.view";
|
import { AttachmentView } from "./attachment.view";
|
||||||
@@ -41,14 +43,17 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
card = new CardView();
|
card = new CardView();
|
||||||
secureNote = new SecureNoteView();
|
secureNote = new SecureNoteView();
|
||||||
sshKey = new SshKeyView();
|
sshKey = new SshKeyView();
|
||||||
attachments: AttachmentView[] = null;
|
attachments: AttachmentView[] = [];
|
||||||
fields: FieldView[] = null;
|
fields: FieldView[] = [];
|
||||||
passwordHistory: PasswordHistoryView[] = null;
|
passwordHistory: PasswordHistoryView[] = [];
|
||||||
collectionIds: string[] = null;
|
collectionIds: string[] = null;
|
||||||
revisionDate: Date = null;
|
revisionDate: Date = null;
|
||||||
creationDate: Date = null;
|
creationDate: Date = null;
|
||||||
deletedDate: Date = null;
|
deletedDate: Date = null;
|
||||||
reprompt: CipherRepromptType = CipherRepromptType.None;
|
reprompt: CipherRepromptType = CipherRepromptType.None;
|
||||||
|
// We need a copy of the encrypted key so we can pass it to
|
||||||
|
// the SdkCipherView during encryption
|
||||||
|
key?: EncString;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag to indicate if the cipher decryption failed.
|
* Flag to indicate if the cipher decryption failed.
|
||||||
@@ -76,6 +81,7 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
this.deletedDate = c.deletedDate;
|
this.deletedDate = c.deletedDate;
|
||||||
// Old locally stored ciphers might have reprompt == null. If so set it to None.
|
// Old locally stored ciphers might have reprompt == null. If so set it to None.
|
||||||
this.reprompt = c.reprompt ?? CipherRepromptType.None;
|
this.reprompt = c.reprompt ?? CipherRepromptType.None;
|
||||||
|
this.key = c.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get item() {
|
private get item() {
|
||||||
@@ -194,6 +200,18 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
const attachments = obj.attachments?.map((a: any) => AttachmentView.fromJSON(a));
|
const attachments = obj.attachments?.map((a: any) => AttachmentView.fromJSON(a));
|
||||||
const fields = obj.fields?.map((f: any) => FieldView.fromJSON(f));
|
const fields = obj.fields?.map((f: any) => FieldView.fromJSON(f));
|
||||||
const passwordHistory = obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph));
|
const passwordHistory = obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph));
|
||||||
|
const permissions = CipherPermissionsApi.fromJSON(obj.permissions);
|
||||||
|
let key: EncString | undefined;
|
||||||
|
|
||||||
|
if (obj.key != null) {
|
||||||
|
if (typeof obj.key === "string") {
|
||||||
|
// If the key is a string, we need to parse it as EncString
|
||||||
|
key = EncString.fromJSON(obj.key);
|
||||||
|
} else if ((obj.key as any) instanceof EncString) {
|
||||||
|
// If the key is already an EncString instance, we can use it directly
|
||||||
|
key = obj.key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(view, obj, {
|
Object.assign(view, obj, {
|
||||||
creationDate: creationDate,
|
creationDate: creationDate,
|
||||||
@@ -202,6 +220,8 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
attachments: attachments,
|
attachments: attachments,
|
||||||
fields: fields,
|
fields: fields,
|
||||||
passwordHistory: passwordHistory,
|
passwordHistory: passwordHistory,
|
||||||
|
permissions: permissions,
|
||||||
|
key: key,
|
||||||
});
|
});
|
||||||
|
|
||||||
switch (obj.type) {
|
switch (obj.type) {
|
||||||
@@ -236,9 +256,9 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cipherView = new CipherView();
|
const cipherView = new CipherView();
|
||||||
cipherView.id = obj.id ?? null;
|
cipherView.id = uuidToString(obj.id) ?? null;
|
||||||
cipherView.organizationId = obj.organizationId ?? null;
|
cipherView.organizationId = uuidToString(obj.organizationId) ?? null;
|
||||||
cipherView.folderId = obj.folderId ?? null;
|
cipherView.folderId = uuidToString(obj.folderId) ?? null;
|
||||||
cipherView.name = obj.name;
|
cipherView.name = obj.name;
|
||||||
cipherView.notes = obj.notes ?? null;
|
cipherView.notes = obj.notes ?? null;
|
||||||
cipherView.type = obj.type;
|
cipherView.type = obj.type;
|
||||||
@@ -247,26 +267,18 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
cipherView.permissions = CipherPermissionsApi.fromSdkCipherPermissions(obj.permissions);
|
cipherView.permissions = CipherPermissionsApi.fromSdkCipherPermissions(obj.permissions);
|
||||||
cipherView.edit = obj.edit;
|
cipherView.edit = obj.edit;
|
||||||
cipherView.viewPassword = obj.viewPassword;
|
cipherView.viewPassword = obj.viewPassword;
|
||||||
cipherView.localData = obj.localData
|
cipherView.localData = fromSdkLocalData(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 =
|
cipherView.attachments =
|
||||||
obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)) ?? null;
|
obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)) ?? [];
|
||||||
cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)) ?? null;
|
cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)) ?? [];
|
||||||
cipherView.passwordHistory =
|
cipherView.passwordHistory =
|
||||||
obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)) ?? null;
|
obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)) ?? [];
|
||||||
cipherView.collectionIds = obj.collectionIds ?? null;
|
cipherView.collectionIds = obj.collectionIds?.map((i) => uuidToString(i)) ?? [];
|
||||||
cipherView.revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
cipherView.revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
||||||
cipherView.creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
|
cipherView.creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
|
||||||
cipherView.deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
|
cipherView.deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
|
||||||
cipherView.reprompt = obj.reprompt ?? CipherRepromptType.None;
|
cipherView.reprompt = obj.reprompt ?? CipherRepromptType.None;
|
||||||
|
cipherView.key = EncString.fromJSON(obj.key);
|
||||||
|
|
||||||
switch (obj.type) {
|
switch (obj.type) {
|
||||||
case CipherType.Card:
|
case CipherType.Card:
|
||||||
@@ -290,4 +302,66 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
|
|
||||||
return cipherView;
|
return cipherView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps CipherView to SdkCipherView
|
||||||
|
*
|
||||||
|
* @returns {SdkCipherView} The SDK cipher view object
|
||||||
|
*/
|
||||||
|
toSdkCipherView(): SdkCipherView {
|
||||||
|
const sdkCipherView: SdkCipherView = {
|
||||||
|
id: this.id ? asUuid(this.id) : undefined,
|
||||||
|
organizationId: this.organizationId ? asUuid(this.organizationId) : undefined,
|
||||||
|
folderId: this.folderId ? asUuid(this.folderId) : undefined,
|
||||||
|
name: this.name ?? "",
|
||||||
|
notes: this.notes,
|
||||||
|
type: this.type ?? CipherType.Login,
|
||||||
|
favorite: this.favorite,
|
||||||
|
organizationUseTotp: this.organizationUseTotp,
|
||||||
|
permissions: this.permissions?.toSdkCipherPermissions(),
|
||||||
|
edit: this.edit,
|
||||||
|
viewPassword: this.viewPassword,
|
||||||
|
localData: toSdkLocalData(this.localData),
|
||||||
|
attachments: this.attachments?.map((a) => a.toSdkAttachmentView()),
|
||||||
|
fields: this.fields?.map((f) => f.toSdkFieldView()),
|
||||||
|
passwordHistory: this.passwordHistory?.map((ph) => ph.toSdkPasswordHistoryView()),
|
||||||
|
collectionIds: this.collectionIds?.map((i) => i) ?? [],
|
||||||
|
// Revision and creation dates are non-nullable in SDKCipherView
|
||||||
|
revisionDate: (this.revisionDate ?? new Date()).toISOString(),
|
||||||
|
creationDate: (this.creationDate ?? new Date()).toISOString(),
|
||||||
|
deletedDate: this.deletedDate?.toISOString(),
|
||||||
|
reprompt: this.reprompt ?? CipherRepromptType.None,
|
||||||
|
key: this.key?.toJSON(),
|
||||||
|
// Cipher type specific properties are set in the switch statement below
|
||||||
|
// CipherView initializes each with default constructors (undefined values)
|
||||||
|
// The SDK does not expect those undefined values and will throw exceptions
|
||||||
|
login: undefined,
|
||||||
|
card: undefined,
|
||||||
|
identity: undefined,
|
||||||
|
secureNote: undefined,
|
||||||
|
sshKey: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (this.type) {
|
||||||
|
case CipherType.Card:
|
||||||
|
sdkCipherView.card = this.card.toSdkCardView();
|
||||||
|
break;
|
||||||
|
case CipherType.Identity:
|
||||||
|
sdkCipherView.identity = this.identity.toSdkIdentityView();
|
||||||
|
break;
|
||||||
|
case CipherType.Login:
|
||||||
|
sdkCipherView.login = this.login.toSdkLoginView();
|
||||||
|
break;
|
||||||
|
case CipherType.SecureNote:
|
||||||
|
sdkCipherView.secureNote = this.secureNote.toSdkSecureNoteView();
|
||||||
|
break;
|
||||||
|
case CipherType.SshKey:
|
||||||
|
sdkCipherView.sshKey = this.sshKey.toSdkSshKeyView();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdkCipherView;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
// @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 {
|
||||||
|
Fido2CredentialView as SdkFido2CredentialView,
|
||||||
|
Fido2CredentialFullView,
|
||||||
|
} from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { ItemView } from "./item.view";
|
import { ItemView } from "./item.view";
|
||||||
|
|
||||||
@@ -56,4 +59,22 @@ export class Fido2CredentialView extends ItemView {
|
|||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toSdkFido2CredentialFullView(): Fido2CredentialFullView {
|
||||||
|
return {
|
||||||
|
credentialId: this.credentialId,
|
||||||
|
keyType: this.keyType,
|
||||||
|
keyAlgorithm: this.keyAlgorithm,
|
||||||
|
keyCurve: this.keyCurve,
|
||||||
|
keyValue: this.keyValue,
|
||||||
|
rpId: this.rpId,
|
||||||
|
userHandle: this.userHandle,
|
||||||
|
userName: this.userName,
|
||||||
|
counter: this.counter.toString(),
|
||||||
|
rpName: this.rpName,
|
||||||
|
userDisplayName: this.userDisplayName,
|
||||||
|
discoverable: this.discoverable ? "true" : "false",
|
||||||
|
creationDate: this.creationDate?.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// @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 { FieldView as SdkFieldView, FieldType as SdkFieldType } 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";
|
||||||
@@ -50,4 +50,16 @@ export class FieldView implements View {
|
|||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the FieldView to an SDK FieldView.
|
||||||
|
*/
|
||||||
|
toSdkFieldView(): SdkFieldView {
|
||||||
|
return {
|
||||||
|
name: this.name ?? undefined,
|
||||||
|
value: this.value ?? undefined,
|
||||||
|
type: this.type ?? SdkFieldType.Text,
|
||||||
|
linkedId: this.linkedId ?? undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { linkedFieldOption } from "../../linked-field-option.decorator";
|
|||||||
|
|
||||||
import { ItemView } from "./item.view";
|
import { ItemView } from "./item.view";
|
||||||
|
|
||||||
export class IdentityView extends ItemView {
|
export class IdentityView extends ItemView implements SdkIdentityView {
|
||||||
@linkedFieldOption(LinkedId.Title, { sortPosition: 0 })
|
@linkedFieldOption(LinkedId.Title, { sortPosition: 0 })
|
||||||
title: string = null;
|
title: string = null;
|
||||||
@linkedFieldOption(LinkedId.MiddleName, { sortPosition: 2 })
|
@linkedFieldOption(LinkedId.MiddleName, { sortPosition: 2 })
|
||||||
@@ -192,4 +192,12 @@ export class IdentityView extends ItemView {
|
|||||||
|
|
||||||
return identityView;
|
return identityView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the IdentityView to an SDK IdentityView.
|
||||||
|
* The view implements the SdkView so we can safely return `this`
|
||||||
|
*/
|
||||||
|
toSdkIdentityView(): SdkIdentityView {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,15 @@ export class LoginUriView implements View {
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Converts a LoginUriView object to an SDK LoginUriView object. */
|
||||||
|
toSdkLoginUriView(): SdkLoginUriView {
|
||||||
|
return {
|
||||||
|
uri: this.uri ?? undefined,
|
||||||
|
match: this.match ?? undefined,
|
||||||
|
uriChecksum: undefined, // SDK handles uri checksum generation internally
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
matchesUri(
|
matchesUri(
|
||||||
targetUri: string,
|
targetUri: string,
|
||||||
equivalentDomains: Set<string>,
|
equivalentDomains: Set<string>,
|
||||||
|
|||||||
@@ -124,10 +124,30 @@ export class LoginView extends ItemView {
|
|||||||
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
|
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
|
||||||
loginView.totp = obj.totp ?? null;
|
loginView.totp = obj.totp ?? null;
|
||||||
loginView.autofillOnPageLoad = obj.autofillOnPageLoad ?? null;
|
loginView.autofillOnPageLoad = obj.autofillOnPageLoad ?? null;
|
||||||
loginView.uris = obj.uris?.map((uri) => LoginUriView.fromSdkLoginUriView(uri)) || [];
|
loginView.uris =
|
||||||
|
obj.uris
|
||||||
|
?.filter((uri) => uri.uri != null && uri.uri !== "")
|
||||||
|
.map((uri) => LoginUriView.fromSdkLoginUriView(uri)) || [];
|
||||||
// FIDO2 credentials are not decrypted here, they remain encrypted
|
// FIDO2 credentials are not decrypted here, they remain encrypted
|
||||||
loginView.fido2Credentials = null;
|
loginView.fido2Credentials = null;
|
||||||
|
|
||||||
return loginView;
|
return loginView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the LoginView to an SDK LoginView.
|
||||||
|
*
|
||||||
|
* Note: FIDO2 credentials remain encrypted in the SDK view so they are not included here.
|
||||||
|
*/
|
||||||
|
toSdkLoginView(): SdkLoginView {
|
||||||
|
return {
|
||||||
|
username: this.username,
|
||||||
|
password: this.password,
|
||||||
|
passwordRevisionDate: this.passwordRevisionDate?.toISOString(),
|
||||||
|
totp: this.totp,
|
||||||
|
autofillOnPageLoad: this.autofillOnPageLoad ?? undefined,
|
||||||
|
uris: this.uris?.map((uri) => uri.toSdkLoginUriView()),
|
||||||
|
fido2Credentials: undefined, // FIDO2 credentials are handled separately and remain encrypted
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,4 +33,17 @@ describe("PasswordHistoryView", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("toSdkPasswordHistoryView", () => {
|
||||||
|
it("should return a SdkPasswordHistoryView", () => {
|
||||||
|
const passwordHistoryView = new PasswordHistoryView();
|
||||||
|
passwordHistoryView.password = "password";
|
||||||
|
passwordHistoryView.lastUsedDate = new Date("2023-10-01T00:00:00.000Z");
|
||||||
|
|
||||||
|
expect(passwordHistoryView.toSdkPasswordHistoryView()).toMatchObject({
|
||||||
|
password: "password",
|
||||||
|
lastUsedDate: "2023-10-01T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,4 +41,14 @@ export class PasswordHistoryView implements View {
|
|||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the PasswordHistoryView to an SDK PasswordHistoryView.
|
||||||
|
*/
|
||||||
|
toSdkPasswordHistoryView(): SdkPasswordHistoryView {
|
||||||
|
return {
|
||||||
|
password: this.password ?? "",
|
||||||
|
lastUsedDate: this.lastUsedDate.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { SecureNote } from "../domain/secure-note";
|
|||||||
|
|
||||||
import { ItemView } from "./item.view";
|
import { ItemView } from "./item.view";
|
||||||
|
|
||||||
export class SecureNoteView extends ItemView {
|
export class SecureNoteView extends ItemView implements SdkSecureNoteView {
|
||||||
type: SecureNoteType = null;
|
type: SecureNoteType = null;
|
||||||
|
|
||||||
constructor(n?: SecureNote) {
|
constructor(n?: SecureNote) {
|
||||||
@@ -42,4 +42,12 @@ export class SecureNoteView extends ItemView {
|
|||||||
|
|
||||||
return secureNoteView;
|
return secureNoteView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the SecureNoteView to an SDK SecureNoteView.
|
||||||
|
* The view implements the SdkView so we can safely return `this`
|
||||||
|
*/
|
||||||
|
toSdkSecureNoteView(): SdkSecureNoteView {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,4 +63,15 @@ export class SshKeyView extends ItemView {
|
|||||||
|
|
||||||
return sshKeyView;
|
return sshKeyView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the SshKeyView to an SDK SshKeyView.
|
||||||
|
*/
|
||||||
|
toSdkSshKeyView(): SdkSshKeyView {
|
||||||
|
return {
|
||||||
|
privateKey: this.privateKey,
|
||||||
|
publicKey: this.publicKey,
|
||||||
|
fingerprint: this.keyFingerprint,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { BehaviorSubject, map, of } from "rxjs";
|
import { BehaviorSubject, map, of } from "rxjs";
|
||||||
|
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { CipherResponse } from "@bitwarden/common/vault/models/response/cipher.response";
|
||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { CipherDecryptionKeys, KeyService } from "@bitwarden/key-management";
|
import { CipherDecryptionKeys, KeyService } from "@bitwarden/key-management";
|
||||||
@@ -23,7 +25,7 @@ import { Utils } from "../../platform/misc/utils";
|
|||||||
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
|
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
|
||||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||||
import { ContainerService } from "../../platform/services/container.service";
|
import { ContainerService } from "../../platform/services/container.service";
|
||||||
import { CipherId, UserId } from "../../types/guid";
|
import { CipherId, UserId, OrganizationId, CollectionId } 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 { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
|
||||||
import { EncryptionContext } from "../abstractions/cipher.service";
|
import { EncryptionContext } from "../abstractions/cipher.service";
|
||||||
@@ -108,6 +110,7 @@ describe("Cipher Service", () => {
|
|||||||
const cipherEncryptionService = mock<CipherEncryptionService>();
|
const cipherEncryptionService = mock<CipherEncryptionService>();
|
||||||
|
|
||||||
const userId = "TestUserId" as UserId;
|
const userId = "TestUserId" as UserId;
|
||||||
|
const orgId = "4ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b2" as OrganizationId;
|
||||||
|
|
||||||
let cipherService: CipherService;
|
let cipherService: CipherService;
|
||||||
let encryptionContext: EncryptionContext;
|
let encryptionContext: EncryptionContext;
|
||||||
@@ -155,7 +158,9 @@ describe("Cipher Service", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(false));
|
configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(false));
|
||||||
configService.getFeatureFlag.mockResolvedValue(false);
|
configService.getFeatureFlag
|
||||||
|
.calledWith(FeatureFlag.CipherKeyEncryption)
|
||||||
|
.mockResolvedValue(false);
|
||||||
|
|
||||||
const spy = jest.spyOn(cipherFileUploadService, "upload");
|
const spy = jest.spyOn(cipherFileUploadService, "upload");
|
||||||
|
|
||||||
@@ -270,6 +275,55 @@ describe("Cipher Service", () => {
|
|||||||
jest.spyOn(cipherService as any, "getAutofillOnPageLoadDefault").mockResolvedValue(true);
|
jest.spyOn(cipherService as any, "getAutofillOnPageLoadDefault").mockResolvedValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should call encrypt method of CipherEncryptionService when feature flag is true", async () => {
|
||||||
|
configService.getFeatureFlag
|
||||||
|
.calledWith(FeatureFlag.PM22136_SdkCipherEncryption)
|
||||||
|
.mockResolvedValue(true);
|
||||||
|
cipherEncryptionService.encrypt.mockResolvedValue(encryptionContext);
|
||||||
|
|
||||||
|
const result = await cipherService.encrypt(cipherView, userId);
|
||||||
|
|
||||||
|
expect(result).toEqual(encryptionContext);
|
||||||
|
expect(cipherEncryptionService.encrypt).toHaveBeenCalledWith(cipherView, userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call legacy encrypt when feature flag is false", async () => {
|
||||||
|
configService.getFeatureFlag
|
||||||
|
.calledWith(FeatureFlag.PM22136_SdkCipherEncryption)
|
||||||
|
.mockResolvedValue(false);
|
||||||
|
|
||||||
|
jest.spyOn(cipherService as any, "encryptCipher").mockResolvedValue(encryptionContext.cipher);
|
||||||
|
|
||||||
|
const result = await cipherService.encrypt(cipherView, userId);
|
||||||
|
|
||||||
|
expect(result).toEqual(encryptionContext);
|
||||||
|
expect(cipherEncryptionService.encrypt).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call legacy encrypt when keys are provided", async () => {
|
||||||
|
configService.getFeatureFlag
|
||||||
|
.calledWith(FeatureFlag.PM22136_SdkCipherEncryption)
|
||||||
|
.mockResolvedValue(true);
|
||||||
|
|
||||||
|
jest.spyOn(cipherService as any, "encryptCipher").mockResolvedValue(encryptionContext.cipher);
|
||||||
|
|
||||||
|
const encryptKey = new SymmetricCryptoKey(new Uint8Array(32));
|
||||||
|
const decryptKey = new SymmetricCryptoKey(new Uint8Array(32));
|
||||||
|
|
||||||
|
let result = await cipherService.encrypt(cipherView, userId, encryptKey);
|
||||||
|
|
||||||
|
expect(result).toEqual(encryptionContext);
|
||||||
|
expect(cipherEncryptionService.encrypt).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
result = await cipherService.encrypt(cipherView, userId, undefined, decryptKey);
|
||||||
|
expect(result).toEqual(encryptionContext);
|
||||||
|
expect(cipherEncryptionService.encrypt).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
result = await cipherService.encrypt(cipherView, userId, encryptKey, decryptKey);
|
||||||
|
expect(result).toEqual(encryptionContext);
|
||||||
|
expect(cipherEncryptionService.encrypt).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("should return the encrypting user id", async () => {
|
it("should return the encrypting user id", async () => {
|
||||||
keyService.getOrgKey.mockReturnValue(
|
keyService.getOrgKey.mockReturnValue(
|
||||||
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey),
|
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey),
|
||||||
@@ -310,7 +364,9 @@ describe("Cipher Service", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("is null when feature flag is false", async () => {
|
it("is null when feature flag is false", async () => {
|
||||||
configService.getFeatureFlag.mockResolvedValue(false);
|
configService.getFeatureFlag
|
||||||
|
.calledWith(FeatureFlag.CipherKeyEncryption)
|
||||||
|
.mockResolvedValue(false);
|
||||||
const { cipher } = await cipherService.encrypt(cipherView, userId);
|
const { cipher } = await cipherService.encrypt(cipherView, userId);
|
||||||
|
|
||||||
expect(cipher.key).toBeNull();
|
expect(cipher.key).toBeNull();
|
||||||
@@ -318,7 +374,9 @@ describe("Cipher Service", () => {
|
|||||||
|
|
||||||
describe("when feature flag is true", () => {
|
describe("when feature flag is true", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
configService.getFeatureFlag.mockResolvedValue(true);
|
configService.getFeatureFlag
|
||||||
|
.calledWith(FeatureFlag.CipherKeyEncryption)
|
||||||
|
.mockResolvedValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is null when the cipher is not viewPassword", async () => {
|
it("is null when the cipher is not viewPassword", async () => {
|
||||||
@@ -348,7 +406,9 @@ describe("Cipher Service", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("is not called when feature flag is false", async () => {
|
it("is not called when feature flag is false", async () => {
|
||||||
configService.getFeatureFlag.mockResolvedValue(false);
|
configService.getFeatureFlag
|
||||||
|
.calledWith(FeatureFlag.CipherKeyEncryption)
|
||||||
|
.mockResolvedValue(false);
|
||||||
|
|
||||||
await cipherService.encrypt(cipherView, userId);
|
await cipherService.encrypt(cipherView, userId);
|
||||||
|
|
||||||
@@ -357,7 +417,9 @@ describe("Cipher Service", () => {
|
|||||||
|
|
||||||
describe("when feature flag is true", () => {
|
describe("when feature flag is true", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
configService.getFeatureFlag.mockResolvedValue(true);
|
configService.getFeatureFlag
|
||||||
|
.calledWith(FeatureFlag.CipherKeyEncryption)
|
||||||
|
.mockResolvedValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is called when cipher viewPassword is true", async () => {
|
it("is called when cipher viewPassword is true", async () => {
|
||||||
@@ -401,7 +463,9 @@ describe("Cipher Service", () => {
|
|||||||
let encryptedKey: EncString;
|
let encryptedKey: EncString;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
configService.getFeatureFlag.mockResolvedValue(true);
|
configService.getFeatureFlag
|
||||||
|
.calledWith(FeatureFlag.CipherKeyEncryption)
|
||||||
|
.mockResolvedValue(true);
|
||||||
configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(true));
|
configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(true));
|
||||||
|
|
||||||
searchService.indexedEntityId$.mockReturnValue(of(null));
|
searchService.indexedEntityId$.mockReturnValue(of(null));
|
||||||
@@ -474,7 +538,9 @@ describe("Cipher Service", () => {
|
|||||||
|
|
||||||
describe("decrypt", () => {
|
describe("decrypt", () => {
|
||||||
it("should call decrypt method of CipherEncryptionService when feature flag is true", async () => {
|
it("should call decrypt method of CipherEncryptionService when feature flag is true", async () => {
|
||||||
configService.getFeatureFlag.mockResolvedValue(true);
|
configService.getFeatureFlag
|
||||||
|
.calledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk)
|
||||||
|
.mockResolvedValue(true);
|
||||||
cipherEncryptionService.decrypt.mockResolvedValue(new CipherView(encryptionContext.cipher));
|
cipherEncryptionService.decrypt.mockResolvedValue(new CipherView(encryptionContext.cipher));
|
||||||
|
|
||||||
const result = await cipherService.decrypt(encryptionContext.cipher, userId);
|
const result = await cipherService.decrypt(encryptionContext.cipher, userId);
|
||||||
@@ -488,7 +554,9 @@ describe("Cipher Service", () => {
|
|||||||
|
|
||||||
it("should call legacy decrypt when feature flag is false", async () => {
|
it("should call legacy decrypt when feature flag is false", async () => {
|
||||||
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey;
|
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey;
|
||||||
configService.getFeatureFlag.mockResolvedValue(false);
|
configService.getFeatureFlag
|
||||||
|
.calledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk)
|
||||||
|
.mockResolvedValue(false);
|
||||||
cipherService.getKeyForCipherKeyDecryption = jest.fn().mockResolvedValue(mockUserKey);
|
cipherService.getKeyForCipherKeyDecryption = jest.fn().mockResolvedValue(mockUserKey);
|
||||||
encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32));
|
encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32));
|
||||||
jest
|
jest
|
||||||
@@ -509,7 +577,9 @@ describe("Cipher Service", () => {
|
|||||||
it("should use SDK when feature flag is enabled", async () => {
|
it("should use SDK when feature flag is enabled", async () => {
|
||||||
const cipher = new Cipher(cipherData);
|
const cipher = new Cipher(cipherData);
|
||||||
const attachment = new AttachmentView(cipher.attachments![0]);
|
const attachment = new AttachmentView(cipher.attachments![0]);
|
||||||
configService.getFeatureFlag.mockResolvedValue(true);
|
configService.getFeatureFlag
|
||||||
|
.calledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk)
|
||||||
|
.mockResolvedValue(true);
|
||||||
|
|
||||||
jest.spyOn(cipherService, "ciphers$").mockReturnValue(of({ [cipher.id]: cipherData }));
|
jest.spyOn(cipherService, "ciphers$").mockReturnValue(of({ [cipher.id]: cipherData }));
|
||||||
cipherEncryptionService.decryptAttachmentContent.mockResolvedValue(mockDecryptedContent);
|
cipherEncryptionService.decryptAttachmentContent.mockResolvedValue(mockDecryptedContent);
|
||||||
@@ -534,7 +604,9 @@ describe("Cipher Service", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should use legacy decryption when feature flag is enabled", async () => {
|
it("should use legacy decryption when feature flag is enabled", async () => {
|
||||||
configService.getFeatureFlag.mockResolvedValue(false);
|
configService.getFeatureFlag
|
||||||
|
.calledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk)
|
||||||
|
.mockResolvedValue(false);
|
||||||
const cipher = new Cipher(cipherData);
|
const cipher = new Cipher(cipherData);
|
||||||
const attachment = new AttachmentView(cipher.attachments![0]);
|
const attachment = new AttachmentView(cipher.attachments![0]);
|
||||||
attachment.key = makeSymmetricCryptoKey(64);
|
attachment.key = makeSymmetricCryptoKey(64);
|
||||||
@@ -557,4 +629,77 @@ describe("Cipher Service", () => {
|
|||||||
expect(encryptService.decryptFileData).toHaveBeenCalledWith(mockEncBuf, attachment.key);
|
expect(encryptService.decryptFileData).toHaveBeenCalledWith(mockEncBuf, attachment.key);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("shareWithServer()", () => {
|
||||||
|
it("should use cipherEncryptionService to move the cipher when feature flag enabled", async () => {
|
||||||
|
configService.getFeatureFlag
|
||||||
|
.calledWith(FeatureFlag.PM22136_SdkCipherEncryption)
|
||||||
|
.mockResolvedValue(true);
|
||||||
|
|
||||||
|
apiService.putShareCipher.mockResolvedValue(new CipherResponse(cipherData));
|
||||||
|
|
||||||
|
const expectedCipher = new Cipher(cipherData);
|
||||||
|
expectedCipher.organizationId = orgId;
|
||||||
|
const cipherView = new CipherView(expectedCipher);
|
||||||
|
const collectionIds = ["collection1", "collection2"] as CollectionId[];
|
||||||
|
|
||||||
|
cipherView.organizationId = undefined; // Ensure organizationId is undefined for this test
|
||||||
|
|
||||||
|
cipherEncryptionService.moveToOrganization.mockResolvedValue({
|
||||||
|
cipher: expectedCipher,
|
||||||
|
encryptedFor: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await cipherService.shareWithServer(cipherView, orgId, collectionIds, userId);
|
||||||
|
|
||||||
|
// Expect SDK usage
|
||||||
|
expect(cipherEncryptionService.moveToOrganization).toHaveBeenCalledWith(
|
||||||
|
cipherView,
|
||||||
|
orgId,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
// Expect collectionIds to be assigned
|
||||||
|
expect(apiService.putShareCipher).toHaveBeenCalledWith(
|
||||||
|
cipherView.id,
|
||||||
|
expect.objectContaining({
|
||||||
|
cipher: expect.objectContaining({ organizationId: orgId }),
|
||||||
|
collectionIds: collectionIds,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use legacy encryption when feature flag disabled", async () => {
|
||||||
|
configService.getFeatureFlag
|
||||||
|
.calledWith(FeatureFlag.PM22136_SdkCipherEncryption)
|
||||||
|
.mockResolvedValue(false);
|
||||||
|
|
||||||
|
apiService.putShareCipher.mockResolvedValue(new CipherResponse(cipherData));
|
||||||
|
|
||||||
|
const expectedCipher = new Cipher(cipherData);
|
||||||
|
expectedCipher.organizationId = orgId;
|
||||||
|
const cipherView = new CipherView(expectedCipher);
|
||||||
|
const collectionIds = ["collection1", "collection2"] as CollectionId[];
|
||||||
|
|
||||||
|
cipherView.organizationId = undefined; // Ensure organizationId is undefined for this test
|
||||||
|
|
||||||
|
const oldEncryptSharedSpy = jest
|
||||||
|
.spyOn(cipherService as any, "encryptSharedCipher")
|
||||||
|
.mockResolvedValue({
|
||||||
|
cipher: expectedCipher,
|
||||||
|
encryptedFor: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await cipherService.shareWithServer(cipherView, orgId, collectionIds, userId);
|
||||||
|
|
||||||
|
// Expect no SDK usage
|
||||||
|
expect(cipherEncryptionService.moveToOrganization).not.toHaveBeenCalled();
|
||||||
|
expect(oldEncryptSharedSpy).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
organizationId: orgId,
|
||||||
|
collectionIds: collectionIds,
|
||||||
|
} as unknown as CipherView),
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -231,13 +231,14 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
this.clearCipherViewsForUser$.next(userId);
|
this.clearCipherViewsForUser$.next(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async encrypt(
|
/**
|
||||||
model: CipherView,
|
* Adjusts the cipher history for the given model by updating its history properties based on the original cipher.
|
||||||
userId: UserId,
|
* @param model The cipher model to adjust.
|
||||||
keyForCipherEncryption?: SymmetricCryptoKey,
|
* @param userId The acting userId
|
||||||
keyForCipherKeyDecryption?: SymmetricCryptoKey,
|
* @param originalCipher The original cipher to compare against. If not provided, it will be fetched from the store.
|
||||||
originalCipher: Cipher = null,
|
* @private
|
||||||
): Promise<EncryptionContext> {
|
*/
|
||||||
|
private async adjustCipherHistory(model: CipherView, userId: UserId, originalCipher?: Cipher) {
|
||||||
if (model.id != null) {
|
if (model.id != null) {
|
||||||
if (originalCipher == null) {
|
if (originalCipher == null) {
|
||||||
originalCipher = await this.get(model.id, userId);
|
originalCipher = await this.get(model.id, userId);
|
||||||
@@ -247,6 +248,25 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
}
|
}
|
||||||
this.adjustPasswordHistoryLength(model);
|
this.adjustPasswordHistoryLength(model);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async encrypt(
|
||||||
|
model: CipherView,
|
||||||
|
userId: UserId,
|
||||||
|
keyForCipherEncryption?: SymmetricCryptoKey,
|
||||||
|
keyForCipherKeyDecryption?: SymmetricCryptoKey,
|
||||||
|
originalCipher: Cipher = null,
|
||||||
|
): Promise<EncryptionContext> {
|
||||||
|
await this.adjustCipherHistory(model, userId, originalCipher);
|
||||||
|
|
||||||
|
const sdkEncryptionEnabled =
|
||||||
|
(await this.configService.getFeatureFlag(FeatureFlag.PM22136_SdkCipherEncryption)) &&
|
||||||
|
keyForCipherEncryption == null && // PM-23085 - SDK encryption does not currently support custom keys (e.g. key rotation)
|
||||||
|
keyForCipherKeyDecryption == null; // PM-23348 - Or has explicit methods for re-encrypting ciphers with different keys (e.g. move to org)
|
||||||
|
|
||||||
|
if (sdkEncryptionEnabled) {
|
||||||
|
return await this.cipherEncryptionService.encrypt(model, userId);
|
||||||
|
}
|
||||||
|
|
||||||
const cipher = new Cipher();
|
const cipher = new Cipher();
|
||||||
cipher.id = model.id;
|
cipher.id = model.id;
|
||||||
@@ -854,22 +874,48 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
organizationId: string,
|
organizationId: string,
|
||||||
collectionIds: string[],
|
collectionIds: string[],
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
|
originalCipher?: Cipher,
|
||||||
): Promise<Cipher> {
|
): Promise<Cipher> {
|
||||||
const attachmentPromises: Promise<any>[] = [];
|
const sdkCipherEncryptionEnabled = await this.configService.getFeatureFlag(
|
||||||
if (cipher.attachments != null) {
|
FeatureFlag.PM22136_SdkCipherEncryption,
|
||||||
cipher.attachments.forEach((attachment) => {
|
);
|
||||||
if (attachment.key == null) {
|
|
||||||
attachmentPromises.push(
|
await this.adjustCipherHistory(cipher, userId, originalCipher);
|
||||||
this.shareAttachmentWithServer(attachment, cipher.id, organizationId),
|
|
||||||
);
|
let encCipher: EncryptionContext;
|
||||||
}
|
if (sdkCipherEncryptionEnabled) {
|
||||||
});
|
// The SDK does not expect the cipher to already have an organizationId. It will result in the wrong
|
||||||
}
|
// cipher encryption key being used during the move to organization operation.
|
||||||
await Promise.all(attachmentPromises);
|
if (cipher.organizationId != null) {
|
||||||
|
throw new Error("Cipher is already associated with an organization.");
|
||||||
|
}
|
||||||
|
|
||||||
|
encCipher = await this.cipherEncryptionService.moveToOrganization(
|
||||||
|
cipher,
|
||||||
|
organizationId as OrganizationId,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
encCipher.cipher.collectionIds = collectionIds;
|
||||||
|
} else {
|
||||||
|
// This old attachment logic is safe to remove after it is replaced in PM-22750; which will require fixing
|
||||||
|
// the attachment before sharing.
|
||||||
|
const attachmentPromises: Promise<any>[] = [];
|
||||||
|
if (cipher.attachments != null) {
|
||||||
|
cipher.attachments.forEach((attachment) => {
|
||||||
|
if (attachment.key == null) {
|
||||||
|
attachmentPromises.push(
|
||||||
|
this.shareAttachmentWithServer(attachment, cipher.id, organizationId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await Promise.all(attachmentPromises);
|
||||||
|
|
||||||
|
cipher.organizationId = organizationId;
|
||||||
|
cipher.collectionIds = collectionIds;
|
||||||
|
encCipher = await this.encryptSharedCipher(cipher, userId);
|
||||||
|
}
|
||||||
|
|
||||||
cipher.organizationId = organizationId;
|
|
||||||
cipher.collectionIds = collectionIds;
|
|
||||||
const encCipher = await this.encryptSharedCipher(cipher, userId);
|
|
||||||
const request = new CipherShareRequest(encCipher);
|
const request = new CipherShareRequest(encCipher);
|
||||||
const response = await this.apiService.putShareCipher(cipher.id, request);
|
const response = await this.apiService.putShareCipher(cipher.id, request);
|
||||||
const data = new CipherData(response, collectionIds);
|
const data = new CipherData(response, collectionIds);
|
||||||
@@ -883,16 +929,36 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
collectionIds: string[],
|
collectionIds: string[],
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
) {
|
) {
|
||||||
|
const sdkCipherEncryptionEnabled = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.PM22136_SdkCipherEncryption,
|
||||||
|
);
|
||||||
const promises: Promise<any>[] = [];
|
const promises: Promise<any>[] = [];
|
||||||
const encCiphers: Cipher[] = [];
|
const encCiphers: Cipher[] = [];
|
||||||
for (const cipher of ciphers) {
|
for (const cipher of ciphers) {
|
||||||
cipher.organizationId = organizationId;
|
if (sdkCipherEncryptionEnabled) {
|
||||||
cipher.collectionIds = collectionIds;
|
// The SDK does not expect the cipher to already have an organizationId. It will result in the wrong
|
||||||
promises.push(
|
// cipher encryption key being used during the move to organization operation.
|
||||||
this.encryptSharedCipher(cipher, userId).then((c) => {
|
if (cipher.organizationId != null) {
|
||||||
encCiphers.push(c.cipher);
|
throw new Error("Cipher is already associated with an organization.");
|
||||||
}),
|
}
|
||||||
);
|
|
||||||
|
promises.push(
|
||||||
|
this.cipherEncryptionService
|
||||||
|
.moveToOrganization(cipher, organizationId as OrganizationId, userId)
|
||||||
|
.then((encCipher) => {
|
||||||
|
encCipher.cipher.collectionIds = collectionIds;
|
||||||
|
encCiphers.push(encCipher.cipher);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
cipher.organizationId = organizationId;
|
||||||
|
cipher.collectionIds = collectionIds;
|
||||||
|
promises.push(
|
||||||
|
this.encryptSharedCipher(cipher, userId).then((c) => {
|
||||||
|
encCiphers.push(c.cipher);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
const request = new CipherBulkShareRequest(encCiphers, collectionIds, userId);
|
const request = new CipherBulkShareRequest(encCiphers, collectionIds, userId);
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { of } from "rxjs";
|
import { of } from "rxjs";
|
||||||
|
|
||||||
|
import { Fido2Credential } from "@bitwarden/common/vault/models/domain/fido2-credential";
|
||||||
import {
|
import {
|
||||||
Fido2Credential,
|
Fido2Credential as SdkFido2Credential,
|
||||||
Cipher as SdkCipher,
|
Cipher as SdkCipher,
|
||||||
CipherType as SdkCipherType,
|
CipherType as SdkCipherType,
|
||||||
CipherView as SdkCipherView,
|
CipherView as SdkCipherView,
|
||||||
CipherListView,
|
CipherListView,
|
||||||
AttachmentView as SdkAttachmentView,
|
AttachmentView as SdkAttachmentView,
|
||||||
|
Fido2CredentialFullView,
|
||||||
} from "@bitwarden/sdk-internal";
|
} from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { mockEnc } from "../../../spec";
|
import { mockEnc } from "../../../spec";
|
||||||
import { UriMatchStrategy } from "../../models/domain/domain-service";
|
import { UriMatchStrategy } from "../../models/domain/domain-service";
|
||||||
import { LogService } from "../../platform/abstractions/log.service";
|
import { LogService } from "../../platform/abstractions/log.service";
|
||||||
import { SdkService } from "../../platform/abstractions/sdk/sdk.service";
|
import { SdkService } from "../../platform/abstractions/sdk/sdk.service";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId, CipherId, OrganizationId } from "../../types/guid";
|
||||||
import { CipherRepromptType, CipherType } from "../enums";
|
import { CipherRepromptType, CipherType } from "../enums";
|
||||||
import { CipherPermissionsApi } from "../models/api/cipher-permissions.api";
|
import { CipherPermissionsApi } from "../models/api/cipher-permissions.api";
|
||||||
import { CipherData } from "../models/data/cipher.data";
|
import { CipherData } from "../models/data/cipher.data";
|
||||||
@@ -25,10 +27,15 @@ import { Fido2CredentialView } from "../models/view/fido2-credential.view";
|
|||||||
|
|
||||||
import { DefaultCipherEncryptionService } from "./default-cipher-encryption.service";
|
import { DefaultCipherEncryptionService } from "./default-cipher-encryption.service";
|
||||||
|
|
||||||
|
const cipherId = "bdc4ef23-1116-477e-ae73-247854af58cb" as CipherId;
|
||||||
|
const orgId = "c5e9654f-6cc5-44c4-8e09-3d323522668c" as OrganizationId;
|
||||||
|
const folderId = "a3e9654f-6cc5-44c4-8e09-3d323522668c";
|
||||||
|
const userId = "59fbbb44-8cc8-4279-ab40-afc5f68704f4" as UserId;
|
||||||
|
|
||||||
const cipherData: CipherData = {
|
const cipherData: CipherData = {
|
||||||
id: "id",
|
id: cipherId,
|
||||||
organizationId: "orgId",
|
organizationId: orgId,
|
||||||
folderId: "folderId",
|
folderId: folderId,
|
||||||
edit: true,
|
edit: true,
|
||||||
viewPassword: true,
|
viewPassword: true,
|
||||||
organizationUseTotp: true,
|
organizationUseTotp: true,
|
||||||
@@ -78,13 +85,17 @@ describe("DefaultCipherEncryptionService", () => {
|
|||||||
const sdkService = mock<SdkService>();
|
const sdkService = mock<SdkService>();
|
||||||
const logService = mock<LogService>();
|
const logService = mock<LogService>();
|
||||||
let sdkCipherView: SdkCipherView;
|
let sdkCipherView: SdkCipherView;
|
||||||
|
let sdkCipher: SdkCipher;
|
||||||
|
|
||||||
const mockSdkClient = {
|
const mockSdkClient = {
|
||||||
vault: jest.fn().mockReturnValue({
|
vault: jest.fn().mockReturnValue({
|
||||||
ciphers: jest.fn().mockReturnValue({
|
ciphers: jest.fn().mockReturnValue({
|
||||||
|
encrypt: jest.fn(),
|
||||||
|
set_fido2_credentials: jest.fn(),
|
||||||
decrypt: jest.fn(),
|
decrypt: jest.fn(),
|
||||||
decrypt_list: jest.fn(),
|
decrypt_list: jest.fn(),
|
||||||
decrypt_fido2_credentials: jest.fn(),
|
decrypt_fido2_credentials: jest.fn(),
|
||||||
|
move_to_organization: jest.fn(),
|
||||||
}),
|
}),
|
||||||
attachments: jest.fn().mockReturnValue({
|
attachments: jest.fn().mockReturnValue({
|
||||||
decrypt_buffer: jest.fn(),
|
decrypt_buffer: jest.fn(),
|
||||||
@@ -99,21 +110,25 @@ describe("DefaultCipherEncryptionService", () => {
|
|||||||
take: jest.fn().mockReturnValue(mockRef),
|
take: jest.fn().mockReturnValue(mockRef),
|
||||||
};
|
};
|
||||||
|
|
||||||
const userId = "user-id" as UserId;
|
|
||||||
|
|
||||||
let cipherObj: Cipher;
|
let cipherObj: Cipher;
|
||||||
|
let cipherViewObj: CipherView;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sdkService.userClient$ = jest.fn((userId: UserId) => of(mockSdk)) as any;
|
sdkService.userClient$ = jest.fn((userId: UserId) => of(mockSdk)) as any;
|
||||||
cipherEncryptionService = new DefaultCipherEncryptionService(sdkService, logService);
|
cipherEncryptionService = new DefaultCipherEncryptionService(sdkService, logService);
|
||||||
cipherObj = new Cipher(cipherData);
|
cipherObj = new Cipher(cipherData);
|
||||||
|
cipherViewObj = new CipherView(cipherObj);
|
||||||
|
|
||||||
jest.spyOn(cipherObj, "toSdkCipher").mockImplementation(() => {
|
jest.spyOn(cipherObj, "toSdkCipher").mockImplementation(() => {
|
||||||
return { id: cipherData.id } as SdkCipher;
|
return { id: cipherData.id } as SdkCipher;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.spyOn(cipherViewObj, "toSdkCipherView").mockImplementation(() => {
|
||||||
|
return { id: cipherData.id } as SdkCipherView;
|
||||||
|
});
|
||||||
|
|
||||||
sdkCipherView = {
|
sdkCipherView = {
|
||||||
id: "test-id",
|
id: cipherId as string,
|
||||||
type: SdkCipherType.Login,
|
type: SdkCipherType.Login,
|
||||||
name: "test-name",
|
name: "test-name",
|
||||||
login: {
|
login: {
|
||||||
@@ -121,16 +136,211 @@ describe("DefaultCipherEncryptionService", () => {
|
|||||||
password: "test-password",
|
password: "test-password",
|
||||||
},
|
},
|
||||||
} as SdkCipherView;
|
} as SdkCipherView;
|
||||||
|
|
||||||
|
sdkCipher = {
|
||||||
|
id: cipherId,
|
||||||
|
type: SdkCipherType.Login,
|
||||||
|
name: "encrypted-name",
|
||||||
|
login: {
|
||||||
|
username: "encrypted-username",
|
||||||
|
password: "encrypted-password",
|
||||||
|
},
|
||||||
|
} as unknown as SdkCipher;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("encrypt", () => {
|
||||||
|
it("should encrypt a cipher successfully", async () => {
|
||||||
|
const expectedCipher: Cipher = {
|
||||||
|
id: cipherId as string,
|
||||||
|
type: CipherType.Login,
|
||||||
|
name: "encrypted-name",
|
||||||
|
login: {
|
||||||
|
username: "encrypted-username",
|
||||||
|
password: "encrypted-password",
|
||||||
|
},
|
||||||
|
} as unknown as Cipher;
|
||||||
|
|
||||||
|
mockSdkClient.vault().ciphers().encrypt.mockReturnValue({
|
||||||
|
cipher: sdkCipher,
|
||||||
|
encryptedFor: userId,
|
||||||
|
});
|
||||||
|
jest.spyOn(Cipher, "fromSdkCipher").mockReturnValue(expectedCipher);
|
||||||
|
|
||||||
|
const result = await cipherEncryptionService.encrypt(cipherViewObj, userId);
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result!.cipher).toEqual(expectedCipher);
|
||||||
|
expect(result!.encryptedFor).toBe(userId);
|
||||||
|
expect(cipherViewObj.toSdkCipherView).toHaveBeenCalled();
|
||||||
|
expect(mockSdkClient.vault().ciphers().encrypt).toHaveBeenCalledWith({ id: cipherData.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should encrypt FIDO2 credentials if present", async () => {
|
||||||
|
const fidoCredentialView = new Fido2CredentialView();
|
||||||
|
fidoCredentialView.credentialId = "credentialId";
|
||||||
|
|
||||||
|
cipherViewObj.login.fido2Credentials = [fidoCredentialView];
|
||||||
|
|
||||||
|
jest.spyOn(fidoCredentialView, "toSdkFido2CredentialFullView").mockImplementation(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
credentialId: "credentialId",
|
||||||
|
}) as Fido2CredentialFullView,
|
||||||
|
);
|
||||||
|
jest.spyOn(cipherViewObj, "toSdkCipherView").mockImplementation(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
id: cipherId as string,
|
||||||
|
login: {
|
||||||
|
fido2Credentials: undefined,
|
||||||
|
},
|
||||||
|
}) as unknown as SdkCipherView,
|
||||||
|
);
|
||||||
|
|
||||||
|
mockSdkClient
|
||||||
|
.vault()
|
||||||
|
.ciphers()
|
||||||
|
.set_fido2_credentials.mockReturnValue({
|
||||||
|
id: cipherId as string,
|
||||||
|
login: {
|
||||||
|
fido2Credentials: [
|
||||||
|
{
|
||||||
|
credentialId: "encrypted-credentialId",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mockSdkClient.vault().ciphers().encrypt.mockReturnValue({
|
||||||
|
cipher: sdkCipher,
|
||||||
|
encryptedFor: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
cipherObj.login!.fido2Credentials = [
|
||||||
|
{ credentialId: "encrypted-credentialId" } as unknown as Fido2Credential,
|
||||||
|
];
|
||||||
|
|
||||||
|
jest.spyOn(Cipher, "fromSdkCipher").mockReturnValue(cipherObj);
|
||||||
|
|
||||||
|
const result = await cipherEncryptionService.encrypt(cipherViewObj, userId);
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result!.cipher.login!.fido2Credentials).toHaveLength(1);
|
||||||
|
|
||||||
|
// Ensure set_fido2_credentials was called with correct parameters
|
||||||
|
expect(mockSdkClient.vault().ciphers().set_fido2_credentials).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ id: cipherId }),
|
||||||
|
[{ credentialId: "credentialId" }],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Encrypted fido2 credential should be in the cipher passed to encrypt
|
||||||
|
expect(mockSdkClient.vault().ciphers().encrypt).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: cipherId,
|
||||||
|
login: { fido2Credentials: [{ credentialId: "encrypted-credentialId" }] },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("moveToOrganization", () => {
|
||||||
|
it("should call the sdk method to move a cipher to an organization", async () => {
|
||||||
|
const expectedCipher: Cipher = {
|
||||||
|
id: cipherId as string,
|
||||||
|
type: CipherType.Login,
|
||||||
|
name: "encrypted-name",
|
||||||
|
organizationId: orgId,
|
||||||
|
login: {
|
||||||
|
username: "encrypted-username",
|
||||||
|
password: "encrypted-password",
|
||||||
|
},
|
||||||
|
} as unknown as Cipher;
|
||||||
|
|
||||||
|
mockSdkClient.vault().ciphers().move_to_organization.mockReturnValue({
|
||||||
|
id: cipherId,
|
||||||
|
organizationId: orgId,
|
||||||
|
});
|
||||||
|
mockSdkClient.vault().ciphers().encrypt.mockReturnValue({
|
||||||
|
cipher: sdkCipher,
|
||||||
|
encryptedFor: userId,
|
||||||
|
});
|
||||||
|
jest.spyOn(Cipher, "fromSdkCipher").mockReturnValue(expectedCipher);
|
||||||
|
|
||||||
|
const result = await cipherEncryptionService.moveToOrganization(cipherViewObj, orgId, userId);
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result!.cipher).toEqual(expectedCipher);
|
||||||
|
expect(result!.encryptedFor).toBe(userId);
|
||||||
|
expect(cipherViewObj.toSdkCipherView).toHaveBeenCalled();
|
||||||
|
expect(mockSdkClient.vault().ciphers().move_to_organization).toHaveBeenCalledWith(
|
||||||
|
{ id: cipherData.id },
|
||||||
|
orgId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should re-encrypt any fido2 credentials when moving to an organization", async () => {
|
||||||
|
const mockSdkCredentialView = {
|
||||||
|
username: "username",
|
||||||
|
} as unknown as Fido2CredentialFullView;
|
||||||
|
const mockCredentialView = mock<Fido2CredentialView>();
|
||||||
|
mockCredentialView.toSdkFido2CredentialFullView.mockReturnValue(mockSdkCredentialView);
|
||||||
|
cipherViewObj.login.fido2Credentials = [mockCredentialView];
|
||||||
|
const expectedCipher: Cipher = {
|
||||||
|
id: cipherId as string,
|
||||||
|
type: CipherType.Login,
|
||||||
|
name: "encrypted-name",
|
||||||
|
organizationId: orgId,
|
||||||
|
login: {
|
||||||
|
username: "encrypted-username",
|
||||||
|
password: "encrypted-password",
|
||||||
|
fido2Credentials: [{ username: "encrypted-username" }],
|
||||||
|
},
|
||||||
|
} as unknown as Cipher;
|
||||||
|
|
||||||
|
mockSdkClient
|
||||||
|
.vault()
|
||||||
|
.ciphers()
|
||||||
|
.set_fido2_credentials.mockReturnValue({
|
||||||
|
id: cipherId as string,
|
||||||
|
login: {
|
||||||
|
fido2Credentials: [mockSdkCredentialView],
|
||||||
|
},
|
||||||
|
} as SdkCipherView);
|
||||||
|
mockSdkClient.vault().ciphers().move_to_organization.mockReturnValue({
|
||||||
|
id: cipherId,
|
||||||
|
organizationId: orgId,
|
||||||
|
});
|
||||||
|
mockSdkClient.vault().ciphers().encrypt.mockReturnValue({
|
||||||
|
cipher: sdkCipher,
|
||||||
|
encryptedFor: userId,
|
||||||
|
});
|
||||||
|
jest.spyOn(Cipher, "fromSdkCipher").mockReturnValue(expectedCipher);
|
||||||
|
|
||||||
|
const result = await cipherEncryptionService.moveToOrganization(cipherViewObj, orgId, userId);
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result!.cipher).toEqual(expectedCipher);
|
||||||
|
expect(result!.encryptedFor).toBe(userId);
|
||||||
|
expect(cipherViewObj.toSdkCipherView).toHaveBeenCalled();
|
||||||
|
expect(mockSdkClient.vault().ciphers().set_fido2_credentials).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ id: cipherId }),
|
||||||
|
expect.arrayContaining([mockSdkCredentialView]),
|
||||||
|
);
|
||||||
|
expect(mockSdkClient.vault().ciphers().move_to_organization).toHaveBeenCalledWith(
|
||||||
|
{ id: cipherData.id, login: { fido2Credentials: [mockSdkCredentialView] } },
|
||||||
|
orgId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("decrypt", () => {
|
describe("decrypt", () => {
|
||||||
it("should decrypt a cipher successfully", async () => {
|
it("should decrypt a cipher successfully", async () => {
|
||||||
const expectedCipherView: CipherView = {
|
const expectedCipherView: CipherView = {
|
||||||
id: "test-id",
|
id: cipherId as string,
|
||||||
type: CipherType.Login,
|
type: CipherType.Login,
|
||||||
name: "test-name",
|
name: "test-name",
|
||||||
login: {
|
login: {
|
||||||
@@ -168,12 +378,12 @@ describe("DefaultCipherEncryptionService", () => {
|
|||||||
discoverable: mockEnc("true"),
|
discoverable: mockEnc("true"),
|
||||||
creationDate: new Date("2023-01-01T12:00:00.000Z"),
|
creationDate: new Date("2023-01-01T12:00:00.000Z"),
|
||||||
},
|
},
|
||||||
] as unknown as Fido2Credential[];
|
] as unknown as SdkFido2Credential[];
|
||||||
|
|
||||||
sdkCipherView.login!.fido2Credentials = fido2Credentials;
|
sdkCipherView.login!.fido2Credentials = fido2Credentials;
|
||||||
|
|
||||||
const expectedCipherView: CipherView = {
|
const expectedCipherView: CipherView = {
|
||||||
id: "test-id",
|
id: cipherId,
|
||||||
type: CipherType.Login,
|
type: CipherType.Login,
|
||||||
name: "test-name",
|
name: "test-name",
|
||||||
login: {
|
login: {
|
||||||
@@ -228,13 +438,15 @@ describe("DefaultCipherEncryptionService", () => {
|
|||||||
it("should decrypt multiple ciphers successfully", async () => {
|
it("should decrypt multiple ciphers successfully", async () => {
|
||||||
const ciphers = [new Cipher(cipherData), new Cipher(cipherData)];
|
const ciphers = [new Cipher(cipherData), new Cipher(cipherData)];
|
||||||
|
|
||||||
|
const cipherId2 = "bdc4ef23-2222-477e-ae73-247854af58cb" as CipherId;
|
||||||
|
|
||||||
const expectedViews = [
|
const expectedViews = [
|
||||||
{
|
{
|
||||||
id: "test-id-1",
|
id: cipherId as string,
|
||||||
name: "test-name-1",
|
name: "test-name-1",
|
||||||
} as CipherView,
|
} as CipherView,
|
||||||
{
|
{
|
||||||
id: "test-id-2",
|
id: cipherId2 as string,
|
||||||
name: "test-name-2",
|
name: "test-name-2",
|
||||||
} as CipherView,
|
} as CipherView,
|
||||||
];
|
];
|
||||||
@@ -242,8 +454,11 @@ describe("DefaultCipherEncryptionService", () => {
|
|||||||
mockSdkClient
|
mockSdkClient
|
||||||
.vault()
|
.vault()
|
||||||
.ciphers()
|
.ciphers()
|
||||||
.decrypt.mockReturnValueOnce({ id: "test-id-1", name: "test-name-1" } as SdkCipherView)
|
.decrypt.mockReturnValueOnce({
|
||||||
.mockReturnValueOnce({ id: "test-id-2", name: "test-name-2" } as SdkCipherView);
|
id: cipherId,
|
||||||
|
name: "test-name-1",
|
||||||
|
} as unknown as SdkCipherView)
|
||||||
|
.mockReturnValueOnce({ id: cipherId2, name: "test-name-2" } as unknown as SdkCipherView);
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(CipherView, "fromSdkCipherView")
|
.spyOn(CipherView, "fromSdkCipherView")
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { EMPTY, catchError, firstValueFrom, map } from "rxjs";
|
import { EMPTY, catchError, firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
import { CipherListView } from "@bitwarden/sdk-internal";
|
import { EncryptionContext } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import {
|
||||||
|
CipherListView,
|
||||||
|
BitwardenClient,
|
||||||
|
CipherView as SdkCipherView,
|
||||||
|
} from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { LogService } from "../../platform/abstractions/log.service";
|
import { LogService } from "../../platform/abstractions/log.service";
|
||||||
import { SdkService } from "../../platform/abstractions/sdk/sdk.service";
|
import { SdkService, asUuid } from "../../platform/abstractions/sdk/sdk.service";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId, OrganizationId } from "../../types/guid";
|
||||||
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
|
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
|
||||||
import { CipherType } from "../enums";
|
import { CipherType } from "../enums";
|
||||||
import { Cipher } from "../models/domain/cipher";
|
import { Cipher } from "../models/domain/cipher";
|
||||||
@@ -18,6 +23,67 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
|
|||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
async encrypt(model: CipherView, userId: UserId): Promise<EncryptionContext | undefined> {
|
||||||
|
return firstValueFrom(
|
||||||
|
this.sdkService.userClient$(userId).pipe(
|
||||||
|
map((sdk) => {
|
||||||
|
if (!sdk) {
|
||||||
|
throw new Error("SDK not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
using ref = sdk.take();
|
||||||
|
const sdkCipherView = this.toSdkCipherView(model, ref.value);
|
||||||
|
|
||||||
|
const encryptionContext = ref.value.vault().ciphers().encrypt(sdkCipherView);
|
||||||
|
|
||||||
|
return {
|
||||||
|
cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!,
|
||||||
|
encryptedFor: asUuid<UserId>(encryptionContext.encryptedFor),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
catchError((error: unknown) => {
|
||||||
|
this.logService.error(`Failed to encrypt cipher: ${error}`);
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveToOrganization(
|
||||||
|
model: CipherView,
|
||||||
|
organizationId: OrganizationId,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<EncryptionContext | undefined> {
|
||||||
|
return firstValueFrom(
|
||||||
|
this.sdkService.userClient$(userId).pipe(
|
||||||
|
map((sdk) => {
|
||||||
|
if (!sdk) {
|
||||||
|
throw new Error("SDK not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
using ref = sdk.take();
|
||||||
|
const sdkCipherView = this.toSdkCipherView(model, ref.value);
|
||||||
|
|
||||||
|
const movedCipherView = ref.value
|
||||||
|
.vault()
|
||||||
|
.ciphers()
|
||||||
|
.move_to_organization(sdkCipherView, asUuid(organizationId));
|
||||||
|
|
||||||
|
const encryptionContext = ref.value.vault().ciphers().encrypt(movedCipherView);
|
||||||
|
|
||||||
|
return {
|
||||||
|
cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!,
|
||||||
|
encryptedFor: asUuid<UserId>(encryptionContext.encryptedFor),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
catchError((error: unknown) => {
|
||||||
|
this.logService.error(`Failed to move cipher to organization: ${error}`);
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async decrypt(cipher: Cipher, userId: UserId): Promise<CipherView> {
|
async decrypt(cipher: Cipher, userId: UserId): Promise<CipherView> {
|
||||||
return firstValueFrom(
|
return firstValueFrom(
|
||||||
this.sdkService.userClient$(userId).pipe(
|
this.sdkService.userClient$(userId).pipe(
|
||||||
@@ -51,11 +117,8 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
|
|||||||
clientCipherView.login.fido2Credentials = fido2CredentialViews
|
clientCipherView.login.fido2Credentials = fido2CredentialViews
|
||||||
.map((f) => {
|
.map((f) => {
|
||||||
const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!;
|
const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!;
|
||||||
|
view.keyValue = decryptedKeyValue;
|
||||||
return {
|
return view;
|
||||||
...view,
|
|
||||||
keyValue: decryptedKeyValue,
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
.filter((view): view is Fido2CredentialView => view !== undefined);
|
.filter((view): view is Fido2CredentialView => view !== undefined);
|
||||||
}
|
}
|
||||||
@@ -104,10 +167,8 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
|
|||||||
clientCipherView.login.fido2Credentials = fido2CredentialViews
|
clientCipherView.login.fido2Credentials = fido2CredentialViews
|
||||||
.map((f) => {
|
.map((f) => {
|
||||||
const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!;
|
const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!;
|
||||||
return {
|
view.keyValue = decryptedKeyValue;
|
||||||
...view,
|
return view;
|
||||||
keyValue: decryptedKeyValue,
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
.filter((view): view is Fido2CredentialView => view !== undefined);
|
.filter((view): view is Fido2CredentialView => view !== undefined);
|
||||||
}
|
}
|
||||||
@@ -187,4 +248,25 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to convert a CipherView model to an SDK CipherView. Has special handling for Fido2 credentials
|
||||||
|
* that need to be encrypted before being sent to the SDK.
|
||||||
|
* @param model The CipherView model to convert
|
||||||
|
* @param sdk An instance of SDK client
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private toSdkCipherView(model: CipherView, sdk: BitwardenClient): SdkCipherView {
|
||||||
|
let sdkCipherView = model.toSdkCipherView();
|
||||||
|
|
||||||
|
if (model.type === CipherType.Login && model.login?.hasFido2Credentials) {
|
||||||
|
// Encrypt Fido2 credentials separately
|
||||||
|
const fido2Credentials = model.login.fido2Credentials?.map((f) =>
|
||||||
|
f.toSdkFido2CredentialFullView(),
|
||||||
|
);
|
||||||
|
sdkCipherView = sdk.vault().ciphers().set_fido2_credentials(sdkCipherView, fido2Credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdkCipherView;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -320,12 +320,6 @@ export abstract class BaseImporter {
|
|||||||
} else {
|
} else {
|
||||||
cipher.notes = cipher.notes.trim();
|
cipher.notes = cipher.notes.trim();
|
||||||
}
|
}
|
||||||
if (cipher.fields != null && cipher.fields.length === 0) {
|
|
||||||
cipher.fields = null;
|
|
||||||
}
|
|
||||||
if (cipher.passwordHistory != null && cipher.passwordHistory.length === 0) {
|
|
||||||
cipher.passwordHistory = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected processKvp(
|
protected processKvp(
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ describe("Keeper CSV Importer", () => {
|
|||||||
expect(result != null).toBe(true);
|
expect(result != null).toBe(true);
|
||||||
|
|
||||||
const cipher = result.ciphers.shift();
|
const cipher = result.ciphers.shift();
|
||||||
expect(cipher.fields).toBeNull();
|
expect(cipher.fields.length).toBe(0);
|
||||||
|
|
||||||
const cipher2 = result.ciphers.shift();
|
const cipher2 = result.ciphers.shift();
|
||||||
expect(cipher2.fields.length).toBe(2);
|
expect(cipher2.fields.length).toBe(2);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ describe("Keeper Json Importer", () => {
|
|||||||
expect(cipher3.login.username).toEqual("someUserName");
|
expect(cipher3.login.username).toEqual("someUserName");
|
||||||
expect(cipher3.login.password).toEqual("w4k4k1wergf$^&@#*%2");
|
expect(cipher3.login.password).toEqual("w4k4k1wergf$^&@#*%2");
|
||||||
expect(cipher3.notes).toBeNull();
|
expect(cipher3.notes).toBeNull();
|
||||||
expect(cipher3.fields).toBeNull();
|
expect(cipher3.fields.length).toBe(0);
|
||||||
expect(cipher3.login.uris.length).toEqual(1);
|
expect(cipher3.login.uris.length).toEqual(1);
|
||||||
const uriView3 = cipher3.login.uris.shift();
|
const uriView3 = cipher3.login.uris.shift();
|
||||||
expect(uriView3.uri).toEqual("https://example.com");
|
expect(uriView3.uri).toEqual("https://example.com");
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { firstValueFrom, map } from "rxjs";
|
|||||||
|
|
||||||
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 { UserId } from "@bitwarden/common/types/guid";
|
import { UserId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
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";
|
||||||
@@ -31,21 +31,13 @@ export class DefaultCipherFormService implements CipherFormService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveCipher(cipher: CipherView, config: CipherFormConfig): Promise<CipherView> {
|
async saveCipher(cipher: CipherView, config: CipherFormConfig): Promise<CipherView> {
|
||||||
// Passing the original cipher is important here as it is responsible for appending to password history
|
|
||||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
const encrypted = await this.cipherService.encrypt(
|
|
||||||
cipher,
|
|
||||||
activeUserId,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
config.originalCipher ?? null,
|
|
||||||
);
|
|
||||||
const encryptedCipher = encrypted.cipher;
|
|
||||||
|
|
||||||
let savedCipher: Cipher;
|
let savedCipher: Cipher;
|
||||||
|
|
||||||
// Creating a new cipher
|
// Creating a new cipher
|
||||||
if (cipher.id == null) {
|
if (cipher.id == null) {
|
||||||
|
const encrypted = await this.cipherService.encrypt(cipher, activeUserId);
|
||||||
savedCipher = await this.cipherService.createWithServer(encrypted, config.admin);
|
savedCipher = await this.cipherService.createWithServer(encrypted, config.admin);
|
||||||
return await this.cipherService.decrypt(savedCipher, activeUserId);
|
return await this.cipherService.decrypt(savedCipher, activeUserId);
|
||||||
}
|
}
|
||||||
@@ -61,16 +53,37 @@ export class DefaultCipherFormService implements CipherFormService {
|
|||||||
|
|
||||||
// Call shareWithServer if the owner is changing from a user to an organization
|
// Call shareWithServer if the owner is changing from a user to an organization
|
||||||
if (config.originalCipher.organizationId === null && cipher.organizationId != null) {
|
if (config.originalCipher.organizationId === null && cipher.organizationId != null) {
|
||||||
|
// shareWithServer expects the cipher to have no organizationId set
|
||||||
|
const organizationId = cipher.organizationId as OrganizationId;
|
||||||
|
cipher.organizationId = null;
|
||||||
|
|
||||||
savedCipher = await this.cipherService.shareWithServer(
|
savedCipher = await this.cipherService.shareWithServer(
|
||||||
cipher,
|
cipher,
|
||||||
cipher.organizationId,
|
organizationId,
|
||||||
cipher.collectionIds,
|
cipher.collectionIds,
|
||||||
activeUserId,
|
activeUserId,
|
||||||
|
config.originalCipher,
|
||||||
);
|
);
|
||||||
// If the collectionIds are the same, update the cipher normally
|
// If the collectionIds are the same, update the cipher normally
|
||||||
} else if (isSetEqual(originalCollectionIds, newCollectionIds)) {
|
} else if (isSetEqual(originalCollectionIds, newCollectionIds)) {
|
||||||
|
const encrypted = await this.cipherService.encrypt(
|
||||||
|
cipher,
|
||||||
|
activeUserId,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
config.originalCipher,
|
||||||
|
);
|
||||||
savedCipher = await this.cipherService.updateWithServer(encrypted, config.admin);
|
savedCipher = await this.cipherService.updateWithServer(encrypted, config.admin);
|
||||||
} else {
|
} else {
|
||||||
|
const encrypted = await this.cipherService.encrypt(
|
||||||
|
cipher,
|
||||||
|
activeUserId,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
config.originalCipher,
|
||||||
|
);
|
||||||
|
const encryptedCipher = encrypted.cipher;
|
||||||
|
|
||||||
// Updating a cipher with collection changes is not supported with a single request currently
|
// Updating a cipher with collection changes is not supported with a single request currently
|
||||||
// First update the cipher with the original collectionIds
|
// First update the cipher with the original collectionIds
|
||||||
encryptedCipher.collectionIds = config.originalCipher.collectionIds;
|
encryptedCipher.collectionIds = config.originalCipher.collectionIds;
|
||||||
|
|||||||
@@ -67,12 +67,12 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- CUSTOM FIELDS -->
|
<!-- CUSTOM FIELDS -->
|
||||||
<ng-container *ngIf="cipher.fields">
|
<ng-container *ngIf="cipher.hasFields">
|
||||||
<app-custom-fields-v2 [cipher]="cipher"> </app-custom-fields-v2>
|
<app-custom-fields-v2 [cipher]="cipher"> </app-custom-fields-v2>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- ATTACHMENTS SECTION -->
|
<!-- ATTACHMENTS SECTION -->
|
||||||
<ng-container *ngIf="cipher.attachments">
|
<ng-container *ngIf="cipher.hasAttachments">
|
||||||
<app-attachments-v2-view
|
<app-attachments-v2-view
|
||||||
[emergencyAccessId]="emergencyAccessId"
|
[emergencyAccessId]="emergencyAccessId"
|
||||||
[cipher]="cipher"
|
[cipher]="cipher"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view";
|
||||||
import { ColorPasswordComponent, ColorPasswordModule, ItemModule } from "@bitwarden/components";
|
import { ColorPasswordComponent, ColorPasswordModule, ItemModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { PasswordHistoryViewComponent } from "./password-history-view.component";
|
import { PasswordHistoryViewComponent } from "./password-history-view.component";
|
||||||
@@ -54,8 +55,14 @@ describe("PasswordHistoryViewComponent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("history", () => {
|
describe("history", () => {
|
||||||
const password1 = { password: "bad-password-1", lastUsedDate: new Date("09/13/2004") };
|
const password1 = {
|
||||||
const password2 = { password: "bad-password-2", lastUsedDate: new Date("02/01/2004") };
|
password: "bad-password-1",
|
||||||
|
lastUsedDate: new Date("09/13/2004"),
|
||||||
|
} as PasswordHistoryView;
|
||||||
|
const password2 = {
|
||||||
|
password: "bad-password-2",
|
||||||
|
lastUsedDate: new Date("02/01/2004"),
|
||||||
|
} as PasswordHistoryView;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockCipher.passwordHistory = [password1, password2];
|
mockCipher.passwordHistory = [password1, password2];
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -23,7 +23,7 @@
|
|||||||
"@angular/platform-browser": "19.2.14",
|
"@angular/platform-browser": "19.2.14",
|
||||||
"@angular/platform-browser-dynamic": "19.2.14",
|
"@angular/platform-browser-dynamic": "19.2.14",
|
||||||
"@angular/router": "19.2.14",
|
"@angular/router": "19.2.14",
|
||||||
"@bitwarden/sdk-internal": "0.2.0-main.225",
|
"@bitwarden/sdk-internal": "0.2.0-main.227",
|
||||||
"@electron/fuses": "1.8.0",
|
"@electron/fuses": "1.8.0",
|
||||||
"@emotion/css": "11.13.5",
|
"@emotion/css": "11.13.5",
|
||||||
"@koa/multer": "4.0.0",
|
"@koa/multer": "4.0.0",
|
||||||
@@ -4604,9 +4604,9 @@
|
|||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/@bitwarden/sdk-internal": {
|
"node_modules/@bitwarden/sdk-internal": {
|
||||||
"version": "0.2.0-main.225",
|
"version": "0.2.0-main.227",
|
||||||
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.225.tgz",
|
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.227.tgz",
|
||||||
"integrity": "sha512-bhSFNX584GPJ9wMBYff1d18/Hfj+o+D4E1l3uDLZNXRI9s7w919AQWqJ0xUy1vh8gpkLJovkf64HQGqs0OiQQA==",
|
"integrity": "sha512-afOsl9jwi1qyX/tF4bYP3EWXgc8oMgnCA0hPPh+AJpn7GgoAPCi+WXaJkbBPwRpxZFKEpwt3oLRNTvtkECvFJw==",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"type-fest": "^4.41.0"
|
"type-fest": "^4.41.0"
|
||||||
|
|||||||
@@ -158,7 +158,7 @@
|
|||||||
"@angular/platform-browser": "19.2.14",
|
"@angular/platform-browser": "19.2.14",
|
||||||
"@angular/platform-browser-dynamic": "19.2.14",
|
"@angular/platform-browser-dynamic": "19.2.14",
|
||||||
"@angular/router": "19.2.14",
|
"@angular/router": "19.2.14",
|
||||||
"@bitwarden/sdk-internal": "0.2.0-main.225",
|
"@bitwarden/sdk-internal": "0.2.0-main.227",
|
||||||
"@electron/fuses": "1.8.0",
|
"@electron/fuses": "1.8.0",
|
||||||
"@emotion/css": "11.13.5",
|
"@emotion/css": "11.13.5",
|
||||||
"@koa/multer": "4.0.0",
|
"@koa/multer": "4.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user