1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-16 00:24:52 +00:00

Merge branch 'main' into PM-7853-Clients-Hide-Send-from-navigation-when-user-is-subject-to-the-disable-Send-policy

This commit is contained in:
bmbitwarden
2025-12-29 16:18:39 -05:00
committed by GitHub
7 changed files with 371 additions and 208 deletions

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Message, MessageTypes } from "./message";
const SENDER = "bitwarden-webauthn";
@@ -25,7 +23,7 @@ type Handler = (
* handling aborts and exceptions across separate execution contexts.
*/
export class Messenger {
private messageEventListener: (event: MessageEvent<MessageWithMetadata>) => void | null = null;
private messageEventListener: ((event: MessageEvent<MessageWithMetadata>) => void) | null = null;
private onDestroy = new EventTarget();
/**
@@ -60,6 +58,12 @@ export class Messenger {
this.broadcastChannel.addEventListener(this.messageEventListener);
}
private stripMetadata({ SENDER, senderId, ...message }: MessageWithMetadata): Message {
void SENDER;
void senderId;
return message;
}
/**
* Sends a request to the content script and returns the response.
* AbortController signals will be forwarded to the content script.
@@ -74,7 +78,9 @@ export class Messenger {
try {
const promise = new Promise<Message>((resolve) => {
localPort.onmessage = (event: MessageEvent<MessageWithMetadata>) => resolve(event.data);
localPort.onmessage = (event: MessageEvent<MessageWithMetadata>) => {
resolve(this.stripMetadata(event.data));
};
});
const abortListener = () =>
@@ -129,7 +135,9 @@ export class Messenger {
try {
const handlerResponse = await this.handler(message, abortController);
port.postMessage({ ...handlerResponse, SENDER });
if (handlerResponse !== undefined) {
port.postMessage({ ...handlerResponse, SENDER });
}
} catch (error) {
port.postMessage({
SENDER,

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { EVENTS } from "@bitwarden/common/autofill/constants";
import { BrowserApi } from "../../../../platform/browser/browser-api";
@@ -84,11 +82,15 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
}
const { type, typeData, params } = message.data;
if (!typeData) {
return;
}
if (this.currentNotificationBarType && type !== this.currentNotificationBarType) {
this.closeNotificationBar();
}
const initData = {
const initData: NotificationBarIframeInitData = {
type: type as NotificationType,
isVaultLocked: typeData.isVaultLocked,
theme: typeData.theme,
@@ -116,7 +118,9 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
const closedByUser =
typeof message.data?.closedByUser === "boolean" ? message.data.closedByUser : true;
if (message.data?.fadeOutNotification) {
setElementStyles(this.notificationBarIframeElement, { opacity: "0" }, true);
if (this.notificationBarIframeElement) {
setElementStyles(this.notificationBarIframeElement, { opacity: "0" }, true);
}
globalThis.setTimeout(() => this.closeNotificationBar(closedByUser), 150);
return;
}
@@ -166,7 +170,9 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
this.createNotificationBarElement();
this.setupInitNotificationBarMessageListener(initData);
globalThis.document.body.appendChild(this.notificationBarRootElement);
if (this.notificationBarRootElement) {
globalThis.document.body.appendChild(this.notificationBarRootElement);
}
}
}
@@ -179,7 +185,7 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
const isNotificationFresh =
initData.launchTimestamp && Date.now() - initData.launchTimestamp < 250;
this.currentNotificationBarType = initData.type;
this.currentNotificationBarType = initData.type ?? null;
this.notificationBarIframeElement = globalThis.document.createElement("iframe");
this.notificationBarIframeElement.id = "bit-notification-bar-iframe";
const parentOrigin = globalThis.location.origin;
@@ -206,11 +212,13 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
* This will animate the notification bar into view.
*/
private handleNotificationBarIframeOnLoad = () => {
setElementStyles(
this.notificationBarIframeElement,
{ transform: "translateX(0)", opacity: "1" },
true,
);
if (this.notificationBarIframeElement) {
setElementStyles(
this.notificationBarIframeElement,
{ transform: "translateX(0)", opacity: "1" },
true,
);
}
this.notificationBarIframeElement?.removeEventListener(
EVENTS.LOAD,
@@ -252,6 +260,7 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
const handleInitNotificationBarMessage = (event: MessageEvent) => {
const { source, data } = event;
if (
!this.notificationBarIframeElement?.contentWindow ||
source !== this.notificationBarIframeElement.contentWindow ||
data?.command !== "initNotificationBar"
) {
@@ -282,13 +291,14 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
return;
}
this.notificationBarIframeElement.remove();
this.notificationBarIframeElement?.remove();
this.notificationBarIframeElement = null;
this.notificationBarElement.remove();
this.notificationBarElement?.remove();
this.notificationBarElement = null;
this.notificationBarShadowRoot = null;
this.notificationBarRootElement.remove();
this.notificationBarRootElement?.remove();
this.notificationBarRootElement = null;
const removableNotificationTypes = new Set([
@@ -297,7 +307,11 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
NotificationTypes.AtRiskPassword,
] as NotificationType[]);
if (closedByUserAction && removableNotificationTypes.has(this.currentNotificationBarType)) {
if (
closedByUserAction &&
this.currentNotificationBarType &&
removableNotificationTypes.has(this.currentNotificationBarType)
) {
void sendExtensionMessage("bgRemoveTabFromNotificationQueue");
}
@@ -310,7 +324,7 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
* @param message - The message to send to the notification bar iframe.
*/
private sendMessageToNotificationBarIframe(message: Record<string, any>) {
if (this.notificationBarIframeElement) {
if (this.notificationBarIframeElement?.contentWindow) {
this.notificationBarIframeElement.contentWindow.postMessage(message, this.extensionOrigin);
}
}

View File

@@ -0,0 +1,201 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { filter, firstValueFrom } from "rxjs";
// 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
import { Collection } from "@bitwarden/admin-console/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import {
CipherWithIdExport,
CollectionWithIdExport,
FolderWithIdExport,
} from "@bitwarden/common/models/export";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { OrgKey, UserKey } from "@bitwarden/common/types/key";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { KeyService } from "@bitwarden/key-management";
import { UserId } from "@bitwarden/user-core";
import {
BitwardenEncryptedIndividualJsonExport,
BitwardenEncryptedJsonExport,
BitwardenEncryptedOrgJsonExport,
BitwardenJsonExport,
BitwardenPasswordProtectedFileFormat,
isOrgEncrypted,
isPasswordProtected,
isUnencrypted,
} from "@bitwarden/vault-export-core";
import { ImportResult } from "../../models/import-result";
import { Importer } from "../importer";
import { BitwardenJsonImporter } from "./bitwarden-json-importer";
export class BitwardenEncryptedJsonImporter extends BitwardenJsonImporter implements Importer {
constructor(
protected keyService: KeyService,
protected encryptService: EncryptService,
protected i18nService: I18nService,
private cipherService: CipherService,
private accountService: AccountService,
) {
super();
}
async parse(data: string): Promise<ImportResult> {
const results: BitwardenPasswordProtectedFileFormat | BitwardenJsonExport = JSON.parse(data);
if (isPasswordProtected(results)) {
throw new Error(
"Data is password-protected. Use BitwardenPasswordProtectedImporter instead.",
);
}
if (results == null || results.items == null) {
const result = new ImportResult();
result.success = false;
return result;
}
if (isUnencrypted(results)) {
return super.parse(data);
}
return await this.parseEncrypted(results);
}
private async parseEncrypted(data: BitwardenEncryptedJsonExport): Promise<ImportResult> {
const result = new ImportResult();
const account = await firstValueFrom(this.accountService.activeAccount$);
if (this.isNullOrWhitespace(data.encKeyValidation_DO_NOT_EDIT)) {
result.success = false;
result.errorMessage = this.i18nService.t("importEncKeyError");
return result;
}
const orgKeys = await firstValueFrom(this.keyService.orgKeys$(account.id));
let keyForDecryption: OrgKey | UserKey | null | undefined = orgKeys?.[this.organizationId];
if (!keyForDecryption) {
keyForDecryption = await firstValueFrom(this.keyService.userKey$(account.id));
}
if (!keyForDecryption) {
result.success = false;
result.errorMessage = this.i18nService.t("importEncKeyError");
return result;
}
const encKeyValidation = new EncString(data.encKeyValidation_DO_NOT_EDIT);
try {
await this.encryptService.decryptString(encKeyValidation, keyForDecryption);
} catch {
result.success = false;
result.errorMessage = this.i18nService.t("importEncKeyError");
return result;
}
let groupingsMap: Map<string, number> | null = null;
if (isOrgEncrypted(data)) {
groupingsMap = await this.parseEncryptedCollections(account.id, data, result);
} else {
groupingsMap = await this.parseEncryptedFolders(account.id, data, result);
}
for (const c of data.items) {
const cipher = CipherWithIdExport.toDomain(c);
// reset ids in case they were set for some reason
cipher.id = null;
cipher.organizationId = this.organizationId;
cipher.collectionIds = null;
// make sure password history is limited
if (cipher.passwordHistory != null && cipher.passwordHistory.length > 5) {
cipher.passwordHistory = cipher.passwordHistory.slice(0, 5);
}
if (!this.organization && c.folderId != null && groupingsMap.has(c.folderId)) {
result.folderRelationships.push([result.ciphers.length, groupingsMap.get(c.folderId)]);
} else if (this.organization && c.collectionIds != null) {
c.collectionIds.forEach((cId) => {
if (groupingsMap.has(cId)) {
result.collectionRelationships.push([result.ciphers.length, groupingsMap.get(cId)]);
}
});
}
const view = await this.cipherService.decrypt(cipher, account.id);
this.cleanupCipher(view);
result.ciphers.push(view);
}
result.success = true;
return result;
}
private async parseEncryptedFolders(
userId: UserId,
data: BitwardenEncryptedIndividualJsonExport,
importResult: ImportResult,
): Promise<Map<string, number>> {
const groupingsMap = new Map<string, number>();
if (data.folders == null) {
return groupingsMap;
}
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
for (const f of data.folders) {
let folderView: FolderView;
const folder = FolderWithIdExport.toDomain(f);
if (folder != null) {
folderView = await folder.decrypt(userKey);
}
if (folderView != null) {
groupingsMap.set(f.id, importResult.folders.length);
importResult.folders.push(folderView);
}
}
return groupingsMap;
}
private async parseEncryptedCollections(
userId: UserId,
data: BitwardenEncryptedOrgJsonExport,
importResult: ImportResult,
): Promise<Map<string, number>> {
const groupingsMap = new Map<string, number>();
if (data.collections == null) {
return groupingsMap;
}
const orgKeys = await firstValueFrom(
this.keyService.orgKeys$(userId).pipe(filter((orgKeys) => orgKeys != null)),
);
for (const c of data.collections) {
const collection = CollectionWithIdExport.toDomain(
c,
new Collection({
id: c.id,
name: new EncString(c.name),
organizationId: this.organizationId,
}),
);
const orgKey = orgKeys[c.organizationId];
const collectionView = await collection.decrypt(orgKey, this.encryptService);
if (collectionView != null) {
groupingsMap.set(c.id, importResult.collections.length);
importResult.collections.push(collectionView);
}
}
return groupingsMap;
}
}

View File

@@ -1,30 +1,17 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { filter, firstValueFrom } from "rxjs";
// 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
import { Collection, CollectionView } from "@bitwarden/admin-console/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import {
CipherWithIdExport,
CollectionWithIdExport,
FolderWithIdExport,
} from "@bitwarden/common/models/export";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { KeyService } from "@bitwarden/key-management";
import {
BitwardenEncryptedIndividualJsonExport,
BitwardenEncryptedOrgJsonExport,
BitwardenJsonExport,
BitwardenUnEncryptedIndividualJsonExport,
BitwardenUnEncryptedJsonExport,
BitwardenUnEncryptedOrgJsonExport,
isOrgUnEncrypted,
isUnencrypted,
} from "@bitwarden/vault-export-core";
import { ImportResult } from "../../models/import-result";
@@ -32,103 +19,30 @@ import { BaseImporter } from "../base-importer";
import { Importer } from "../importer";
export class BitwardenJsonImporter extends BaseImporter implements Importer {
private result: ImportResult;
protected constructor(
protected keyService: KeyService,
protected encryptService: EncryptService,
protected i18nService: I18nService,
protected cipherService: CipherService,
protected accountService: AccountService,
) {
protected constructor() {
super();
}
async parse(data: string): Promise<ImportResult> {
const account = await firstValueFrom(this.accountService.activeAccount$);
this.result = new ImportResult();
const results: BitwardenJsonExport = JSON.parse(data);
if (results == null || results.items == null) {
this.result.success = false;
return this.result;
const result = new ImportResult();
result.success = false;
return result;
}
if (results.encrypted) {
await this.parseEncrypted(results as any, account.id);
} else {
await this.parseDecrypted(results as any, account.id);
if (!isUnencrypted(results)) {
throw new Error("Data is encrypted. Use BitwardenEncryptedJsonImporter instead.");
}
return this.result;
return await this.parseDecrypted(results);
}
private async parseEncrypted(
results: BitwardenEncryptedIndividualJsonExport | BitwardenEncryptedOrgJsonExport,
userId: UserId,
) {
if (results.encKeyValidation_DO_NOT_EDIT != null) {
const orgKeys = await firstValueFrom(this.keyService.orgKeys$(userId));
let keyForDecryption: SymmetricCryptoKey = orgKeys?.[this.organizationId];
if (keyForDecryption == null) {
keyForDecryption = await firstValueFrom(this.keyService.userKey$(userId));
}
const encKeyValidation = new EncString(results.encKeyValidation_DO_NOT_EDIT);
try {
await this.encryptService.decryptString(encKeyValidation, keyForDecryption);
} catch {
this.result.success = false;
this.result.errorMessage = this.i18nService.t("importEncKeyError");
return;
}
}
private async parseDecrypted(results: BitwardenUnEncryptedJsonExport): Promise<ImportResult> {
const importResult = new ImportResult();
const groupingsMap = this.organization
? await this.parseCollections(results as BitwardenEncryptedOrgJsonExport, userId)
: await this.parseFolders(results as BitwardenEncryptedIndividualJsonExport, userId);
for (const c of results.items) {
const cipher = CipherWithIdExport.toDomain(c);
// reset ids in case they were set for some reason
cipher.id = null;
cipher.organizationId = this.organizationId;
cipher.collectionIds = null;
// make sure password history is limited
if (cipher.passwordHistory != null && cipher.passwordHistory.length > 5) {
cipher.passwordHistory = cipher.passwordHistory.slice(0, 5);
}
if (!this.organization && c.folderId != null && groupingsMap.has(c.folderId)) {
this.result.folderRelationships.push([
this.result.ciphers.length,
groupingsMap.get(c.folderId),
]);
} else if (this.organization && c.collectionIds != null) {
c.collectionIds.forEach((cId) => {
if (groupingsMap.has(cId)) {
this.result.collectionRelationships.push([
this.result.ciphers.length,
groupingsMap.get(cId),
]);
}
});
}
const view = await this.cipherService.decrypt(cipher, userId);
this.cleanupCipher(view);
this.result.ciphers.push(view);
}
this.result.success = true;
}
private async parseDecrypted(
results: BitwardenUnEncryptedIndividualJsonExport | BitwardenUnEncryptedOrgJsonExport,
userId: UserId,
) {
const groupingsMap = this.organization
? await this.parseCollections(results as BitwardenUnEncryptedOrgJsonExport, userId)
: await this.parseFolders(results as BitwardenUnEncryptedIndividualJsonExport, userId);
const groupingsMap = isOrgUnEncrypted(results)
? await this.parseCollections(results, importResult)
: await this.parseFolders(results, importResult);
results.items.forEach((c) => {
const cipher = CipherWithIdExport.toView(c);
@@ -143,15 +57,15 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
}
if (!this.organization && c.folderId != null && groupingsMap.has(c.folderId)) {
this.result.folderRelationships.push([
this.result.ciphers.length,
importResult.folderRelationships.push([
importResult.ciphers.length,
groupingsMap.get(c.folderId),
]);
} else if (this.organization && c.collectionIds != null) {
c.collectionIds.forEach((cId) => {
if (groupingsMap.has(cId)) {
this.result.collectionRelationships.push([
this.result.ciphers.length,
importResult.collectionRelationships.push([
importResult.ciphers.length,
groupingsMap.get(cId),
]);
}
@@ -159,79 +73,48 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
}
this.cleanupCipher(cipher);
this.result.ciphers.push(cipher);
importResult.ciphers.push(cipher);
});
this.result.success = true;
importResult.success = true;
return importResult;
}
private async parseFolders(
data: BitwardenUnEncryptedIndividualJsonExport | BitwardenEncryptedIndividualJsonExport,
userId: UserId,
): Promise<Map<string, number>> | null {
data: BitwardenUnEncryptedIndividualJsonExport,
importResult: ImportResult,
): Promise<Map<string, number>> {
const groupingsMap = new Map<string, number>();
if (data.folders == null) {
return null;
return groupingsMap;
}
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
const groupingsMap = new Map<string, number>();
for (const f of data.folders) {
let folderView: FolderView;
if (data.encrypted) {
const folder = FolderWithIdExport.toDomain(f);
if (folder != null) {
folderView = await folder.decrypt(userKey);
}
} else {
folderView = FolderWithIdExport.toView(f);
}
const folderView = FolderWithIdExport.toView(f);
if (folderView != null) {
groupingsMap.set(f.id, this.result.folders.length);
this.result.folders.push(folderView);
groupingsMap.set(f.id, importResult.folders.length);
importResult.folders.push(folderView);
}
}
return groupingsMap;
}
private async parseCollections(
data: BitwardenUnEncryptedOrgJsonExport | BitwardenEncryptedOrgJsonExport,
userId: UserId,
): Promise<Map<string, number>> | null {
data: BitwardenUnEncryptedOrgJsonExport,
importResult: ImportResult,
): Promise<Map<string, number>> {
const groupingsMap = new Map<string, number>();
if (data.collections == null) {
return null;
return groupingsMap;
}
const orgKeys = await firstValueFrom(
this.keyService.orgKeys$(userId).pipe(filter((orgKeys) => orgKeys != null)),
);
const groupingsMap = new Map<string, number>();
for (const c of data.collections) {
let collectionView: CollectionView;
if (data.encrypted) {
const collection = CollectionWithIdExport.toDomain(
c,
new Collection({
id: c.id,
name: new EncString(c.name),
organizationId: this.organizationId,
}),
);
const orgKey = orgKeys[c.organizationId];
collectionView = await collection.decrypt(orgKey, this.encryptService);
} else {
collectionView = CollectionWithIdExport.toView(c);
collectionView.organizationId = null;
}
const collectionView = CollectionWithIdExport.toView(c);
collectionView.organizationId = null;
if (collectionView != null) {
groupingsMap.set(c.id, this.result.collections.length);
this.result.collections.push(collectionView);
groupingsMap.set(c.id, importResult.collections.length);
importResult.collections.push(collectionView);
}
}
return groupingsMap;

View File

@@ -16,6 +16,7 @@ import { UserId } from "@bitwarden/user-core";
import { emptyAccountEncrypted } from "../spec-data/bitwarden-json/account-encrypted.json";
import { emptyUnencryptedExport } from "../spec-data/bitwarden-json/unencrypted.json";
import { BitwardenEncryptedJsonImporter } from "./bitwarden-encrypted-json-importer";
import { BitwardenJsonImporter } from "./bitwarden-json-importer";
import { BitwardenPasswordProtectedImporter } from "./bitwarden-password-protected-importer";
@@ -92,7 +93,7 @@ describe("BitwardenPasswordProtectedImporter", () => {
describe("Account encrypted", () => {
beforeAll(() => {
jest.spyOn(BitwardenJsonImporter.prototype, "parse");
jest.spyOn(BitwardenEncryptedJsonImporter.prototype, "parse");
});
beforeEach(() => {
@@ -114,9 +115,11 @@ describe("BitwardenPasswordProtectedImporter", () => {
);
});
it("Should call BitwardenJsonImporter", async () => {
expect((await importer.parse(emptyAccountEncrypted)).success).toEqual(true);
expect(BitwardenJsonImporter.prototype.parse).toHaveBeenCalledWith(emptyAccountEncrypted);
it("Should call BitwardenEncryptedJsonImporter", async () => {
expect((await importer.parse(emptyAccountEncrypted)).success).toEqual(false);
expect(BitwardenEncryptedJsonImporter.prototype.parse).toHaveBeenCalledWith(
emptyAccountEncrypted,
);
});
});

View File

@@ -14,14 +14,21 @@ import {
KeyService,
KdfType,
} from "@bitwarden/key-management";
import { BitwardenPasswordProtectedFileFormat } from "@bitwarden/vault-export-core";
import {
BitwardenJsonExport,
BitwardenPasswordProtectedFileFormat,
isPasswordProtected,
} from "@bitwarden/vault-export-core";
import { ImportResult } from "../../models/import-result";
import { Importer } from "../importer";
import { BitwardenJsonImporter } from "./bitwarden-json-importer";
import { BitwardenEncryptedJsonImporter } from "./bitwarden-encrypted-json-importer";
export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter implements Importer {
export class BitwardenPasswordProtectedImporter
extends BitwardenEncryptedJsonImporter
implements Importer
{
private key: SymmetricCryptoKey;
constructor(
@@ -38,20 +45,14 @@ export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter im
async parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const parsedData: BitwardenPasswordProtectedFileFormat = JSON.parse(data);
const parsedData: BitwardenPasswordProtectedFileFormat | BitwardenJsonExport = JSON.parse(data);
if (!parsedData) {
result.success = false;
return result;
}
// File is unencrypted
if (!parsedData?.encrypted) {
return await super.parse(data);
}
// File is account-encrypted
if (!parsedData?.passwordProtected) {
if (!isPasswordProtected(parsedData)) {
return await super.parse(data);
}

View File

@@ -5,42 +5,48 @@ import {
} from "@bitwarden/common/models/export";
// Base
export type BitwardenJsonExport = {
encrypted: boolean;
items: CipherWithIdExport[];
};
export type BitwardenJsonExport = BitwardenUnEncryptedJsonExport | BitwardenEncryptedJsonExport;
// Decrypted
export type BitwardenUnEncryptedJsonExport = BitwardenJsonExport & {
encrypted: false;
};
export type BitwardenUnEncryptedJsonExport =
| BitwardenUnEncryptedIndividualJsonExport
| BitwardenUnEncryptedOrgJsonExport;
export type BitwardenUnEncryptedIndividualJsonExport = BitwardenUnEncryptedJsonExport & {
export type BitwardenUnEncryptedIndividualJsonExport = {
encrypted: false;
items: CipherWithIdExport[];
folders: FolderWithIdExport[];
};
export type BitwardenUnEncryptedOrgJsonExport = BitwardenUnEncryptedJsonExport & {
export type BitwardenUnEncryptedOrgJsonExport = {
encrypted: false;
items: CipherWithIdExport[];
collections: CollectionWithIdExport[];
};
// Account-encrypted
export type BitwardenEncryptedJsonExport = BitwardenJsonExport & {
export type BitwardenEncryptedJsonExport =
| BitwardenEncryptedIndividualJsonExport
| BitwardenEncryptedOrgJsonExport;
export type BitwardenEncryptedIndividualJsonExport = {
encrypted: true;
encKeyValidation_DO_NOT_EDIT: string;
};
export type BitwardenEncryptedIndividualJsonExport = BitwardenEncryptedJsonExport & {
items: CipherWithIdExport[];
folders: FolderWithIdExport[];
};
export type BitwardenEncryptedOrgJsonExport = BitwardenEncryptedJsonExport & {
export type BitwardenEncryptedOrgJsonExport = {
encrypted: true;
encKeyValidation_DO_NOT_EDIT: string;
items: CipherWithIdExport[];
collections: CollectionWithIdExport[];
};
// Password-protected
export type BitwardenPasswordProtectedFileFormat = {
encrypted: boolean;
passwordProtected: boolean;
encrypted: true;
passwordProtected: true;
salt: string;
kdfIterations: number;
kdfMemory?: number;
@@ -49,3 +55,50 @@ export type BitwardenPasswordProtectedFileFormat = {
encKeyValidation_DO_NOT_EDIT: string;
data: string;
};
// Unencrypted type guards
export function isUnencrypted(
data: BitwardenJsonExport | null | undefined,
): data is BitwardenUnEncryptedJsonExport {
return data != null && (data as { encrypted?: unknown }).encrypted !== true;
}
export function isIndividualUnEncrypted(
data: BitwardenJsonExport | null | undefined,
): data is BitwardenUnEncryptedIndividualJsonExport {
return isUnencrypted(data) && (data as { folders?: unknown }).folders != null;
}
export function isOrgUnEncrypted(
data: BitwardenJsonExport | null | undefined,
): data is BitwardenUnEncryptedOrgJsonExport {
return isUnencrypted(data) && (data as { collections?: unknown }).collections != null;
}
// Encrypted type guards
export function isEncrypted(
data: BitwardenJsonExport | null | undefined,
): data is BitwardenEncryptedJsonExport {
return data != null && (data as { encrypted?: unknown }).encrypted === true;
}
export function isPasswordProtected(
data: BitwardenPasswordProtectedFileFormat | BitwardenJsonExport | null | undefined,
): data is BitwardenPasswordProtectedFileFormat {
return (
data != null &&
(data as { encrypted?: unknown }).encrypted === true &&
(data as { passwordProtected?: unknown }).passwordProtected === true
);
}
export function isIndividualEncrypted(
data: BitwardenJsonExport | null | undefined,
): data is BitwardenEncryptedIndividualJsonExport {
return isEncrypted(data) && (data as { folders?: unknown }).folders != null;
}
export function isOrgEncrypted(
data: BitwardenJsonExport | null | undefined,
): data is BitwardenEncryptedOrgJsonExport {
return isEncrypted(data) && (data as { collections?: unknown }).collections != null;
}