1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-27 01:53:23 +00:00

Merge remote-tracking branch 'origin/main' into playwright

This commit is contained in:
Matt Gibson
2026-01-26 12:57:05 -08:00
1790 changed files with 150488 additions and 32025 deletions

View File

@@ -195,6 +195,8 @@ export class ImportChromeComponent implements OnInit, OnDestroy {
return "Brave";
} else if (format === "vivaldicsv") {
return "Vivaldi";
} else if (format === "arccsv") {
return "Arc";
}
return "Chrome";
}

View File

@@ -21,7 +21,7 @@
<ng-container bitDialogFooter>
<button bitButton buttonType="primary" type="submit">
<span>{{ "importData" | i18n }}</span>
<span>{{ "importVerb" | i18n }}</span>
</button>
<button bitButton bitDialogClose buttonType="secondary" type="button">
<span>{{ "cancel" | i18n }}</span>

View File

@@ -29,15 +29,15 @@ import { combineLatestWith, filter, map, switchMap, takeUntil } from "rxjs/opera
// 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 {
CollectionService,
CollectionTypes,
CollectionView,
} from "@bitwarden/admin-console/common";
import { CollectionService } from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import {
CollectionView,
CollectionTypes,
} from "@bitwarden/common/admin-console/models/collections";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";

View File

@@ -7,8 +7,8 @@ import { safeProvider, SafeProvider } from "@bitwarden/angular/platform/utils/sa
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -84,7 +84,7 @@ export const ImporterProviders: SafeProvider[] = [
CollectionService,
KeyService,
EncryptService,
PinServiceAbstraction,
KeyGenerationService,
AccountService,
RestrictedItemTypesService,
],

View File

@@ -0,0 +1,139 @@
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { ArcCsvImporter } from "./arc-csv-importer";
import { data as missingNameAndUrlData } from "./spec-data/arc-csv/missing-name-and-url-data.csv";
import { data as missingNameWithUrlData } from "./spec-data/arc-csv/missing-name-with-url-data.csv";
import { data as passwordWithNoteData } from "./spec-data/arc-csv/password-with-note-data.csv";
import { data as simplePasswordData } from "./spec-data/arc-csv/simple-password-data.csv";
import { data as subdomainData } from "./spec-data/arc-csv/subdomain-data.csv";
import { data as urlWithWwwData } from "./spec-data/arc-csv/url-with-www-data.csv";
const CipherData = [
{
title: "should parse password",
csv: simplePasswordData,
expected: Object.assign(new CipherView(), {
name: "example.com",
login: Object.assign(new LoginView(), {
username: "user@example.com",
password: "password123",
uris: [
Object.assign(new LoginUriView(), {
uri: "https://example.com/",
}),
],
}),
notes: null,
type: 1,
}),
},
{
title: "should parse password with note",
csv: passwordWithNoteData,
expected: Object.assign(new CipherView(), {
name: "example.com",
login: Object.assign(new LoginView(), {
username: "user@example.com",
password: "password123",
uris: [
Object.assign(new LoginUriView(), {
uri: "https://example.com/",
}),
],
}),
notes: "This is a test note",
type: 1,
}),
},
{
title: "should strip www. prefix from name",
csv: urlWithWwwData,
expected: Object.assign(new CipherView(), {
name: "example.com",
login: Object.assign(new LoginView(), {
username: "user@example.com",
password: "password123",
uris: [
Object.assign(new LoginUriView(), {
uri: "https://www.example.com/",
}),
],
}),
notes: null,
type: 1,
}),
},
{
title: "should extract name from URL when name is missing",
csv: missingNameWithUrlData,
expected: Object.assign(new CipherView(), {
name: "example.com",
login: Object.assign(new LoginView(), {
username: "user@example.com",
password: "password123",
uris: [
Object.assign(new LoginUriView(), {
uri: "https://example.com/login",
}),
],
}),
notes: null,
type: 1,
}),
},
{
title: "should use -- as name when both name and URL are missing",
csv: missingNameAndUrlData,
expected: Object.assign(new CipherView(), {
name: "--",
login: Object.assign(new LoginView(), {
username: null,
password: "password123",
uris: null,
}),
notes: null,
type: 1,
}),
},
{
title: "should preserve subdomain in name",
csv: subdomainData,
expected: Object.assign(new CipherView(), {
name: "login.example.com",
login: Object.assign(new LoginView(), {
username: "user@example.com",
password: "password123",
uris: [
Object.assign(new LoginUriView(), {
uri: "https://login.example.com/auth",
}),
],
}),
notes: null,
type: 1,
}),
},
];
describe("Arc CSV Importer", () => {
CipherData.forEach((data) => {
it(data.title, async () => {
jest.useFakeTimers().setSystemTime(data.expected.creationDate);
const importer = new ArcCsvImporter();
const result = await importer.parse(data.csv);
expect(result != null).toBe(true);
expect(result.ciphers.length).toBeGreaterThan(0);
const cipher = result.ciphers.shift();
let property: keyof typeof data.expected;
for (property in data.expected) {
if (Object.prototype.hasOwnProperty.call(data.expected, property)) {
expect(Object.prototype.hasOwnProperty.call(cipher, property)).toBe(true);
expect(cipher[property]).toEqual(data.expected[property]);
}
}
});
});
});

View File

@@ -0,0 +1,30 @@
import { ImportResult } from "../models/import-result";
import { BaseImporter } from "./base-importer";
import { Importer } from "./importer";
export class ArcCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = this.parseCsv(data, true);
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
results.forEach((value) => {
const cipher = this.initLoginCipher();
const url = this.getValueOrDefault(value.url);
cipher.name = this.getValueOrDefault(this.nameFromUrl(url) ?? "", "--");
cipher.login.username = this.getValueOrDefault(value.username);
cipher.login.password = this.getValueOrDefault(value.password);
cipher.login.uris = this.makeUriArray(value.url);
cipher.notes = this.getValueOrDefault(value.note);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -2,9 +2,7 @@
// @ts-strict-ignore
import * as papa from "papaparse";
// 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 { CollectionView, Collection } from "@bitwarden/common/admin-console/models/collections";
import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -319,8 +317,6 @@ export abstract class BaseImporter {
}
if (this.isNullOrWhitespace(cipher.notes)) {
cipher.notes = null;
} else {
cipher.notes = cipher.notes.trim();
}
}

View File

@@ -0,0 +1,199 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { filter, firstValueFrom } from "rxjs";
import { Collection } from "@bitwarden/common/admin-console/models/collections";
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,31 +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 { getUserId } from "@bitwarden/common/auth/services/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";
@@ -33,104 +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> {
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);
} else {
await this.parseDecrypted(results as any);
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,
) {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
private async parseDecrypted(results: BitwardenUnEncryptedJsonExport): Promise<ImportResult> {
const importResult = new ImportResult();
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;
}
}
const groupingsMap = this.organization
? await this.parseCollections(userId, results as BitwardenEncryptedOrgJsonExport)
: await this.parseFolders(results as BitwardenEncryptedIndividualJsonExport);
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,
) {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const groupingsMap = this.organization
? await this.parseCollections(userId, results as BitwardenUnEncryptedOrgJsonExport)
: await this.parseFolders(results as BitwardenUnEncryptedIndividualJsonExport);
const groupingsMap = isOrgUnEncrypted(results)
? await this.parseCollections(results, importResult)
: await this.parseFolders(results, importResult);
results.items.forEach((c) => {
const cipher = CipherWithIdExport.toView(c);
@@ -145,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),
]);
}
@@ -161,76 +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,
): 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 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();
}
} 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(
userId: UserId,
data: BitwardenUnEncryptedOrgJsonExport | BitwardenEncryptedOrgJsonExport,
): 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

@@ -2,10 +2,11 @@ import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { emptyGuid, OrganizationId } from "@bitwarden/common/types/guid";
import { OrgKey, UserKey } from "@bitwarden/common/types/key";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -15,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";
@@ -24,7 +26,7 @@ describe("BitwardenPasswordProtectedImporter", () => {
let encryptService: MockProxy<EncryptService>;
let i18nService: MockProxy<I18nService>;
let cipherService: MockProxy<CipherService>;
let pinService: MockProxy<PinServiceAbstraction>;
let keyGenerationService: MockProxy<KeyGenerationService>;
let accountService: MockProxy<AccountService>;
const password = Utils.newGuid();
const promptForPassword_callback = async () => {
@@ -36,14 +38,15 @@ describe("BitwardenPasswordProtectedImporter", () => {
encryptService = mock<EncryptService>();
i18nService = mock<I18nService>();
cipherService = mock<CipherService>();
pinService = mock<PinServiceAbstraction>();
keyGenerationService = mock<KeyGenerationService>();
accountService = mock<AccountService>();
accountService.activeAccount$ = of({
id: emptyGuid as UserId,
email: "test@example.com",
emailVerified: true,
name: "Test User",
...mockAccountInfoWith({
email: "test@example.com",
name: "Test User",
}),
});
const mockOrgId = emptyGuid as OrganizationId;
@@ -71,7 +74,7 @@ describe("BitwardenPasswordProtectedImporter", () => {
encryptService,
i18nService,
cipherService,
pinService,
keyGenerationService,
accountService,
promptForPassword_callback,
);
@@ -90,30 +93,33 @@ describe("BitwardenPasswordProtectedImporter", () => {
describe("Account encrypted", () => {
beforeAll(() => {
jest.spyOn(BitwardenJsonImporter.prototype, "parse");
jest.spyOn(BitwardenEncryptedJsonImporter.prototype, "parse");
});
beforeEach(() => {
accountService.activeAccount$ = of({
id: emptyGuid as UserId,
email: "test@example.com",
emailVerified: true,
name: "Test User",
...mockAccountInfoWith({
email: "test@example.com",
name: "Test User",
}),
});
importer = new BitwardenPasswordProtectedImporter(
keyService,
encryptService,
i18nService,
cipherService,
pinService,
keyGenerationService,
accountService,
promptForPassword_callback,
);
});
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

@@ -1,9 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -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(
@@ -29,7 +36,7 @@ export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter im
encryptService: EncryptService,
i18nService: I18nService,
cipherService: CipherService,
private pinService: PinServiceAbstraction,
private keyGenerationService: KeyGenerationService,
accountService: AccountService,
private promptForPassword_callback: () => Promise<string>,
) {
@@ -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);
}
@@ -86,7 +87,7 @@ export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter im
? new PBKDF2KdfConfig(jdoc.kdfIterations)
: new Argon2KdfConfig(jdoc.kdfIterations, jdoc.kdfMemory, jdoc.kdfParallelism);
this.key = await this.pinService.makePinKey(password, jdoc.salt, kdfConfig);
this.key = await this.keyGenerationService.deriveVaultExportKey(password, jdoc.salt, kdfConfig);
const encKeyValidation = new EncString(jdoc.encKeyValidation_DO_NOT_EDIT);

View File

@@ -64,7 +64,10 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
}
}
cipher.notes += "\n" + this.getValueOrDefault(item.note, "");
const note = this.getValueOrDefault(item.note, "");
if (note) {
cipher.notes = note.trimEnd();
}
this.convertToNoteIfNeeded(cipher);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);

View File

@@ -1,3 +1,4 @@
export { ArcCsvImporter } from "./arc-csv-importer";
export { AscendoCsvImporter } from "./ascendo-csv-importer";
export { AvastCsvImporter, AvastJsonImporter } from "./avast";
export { AviraCsvImporter } from "./avira-csv-importer";

View File

@@ -21,7 +21,7 @@ export class KeeperCsvImporter extends BaseImporter implements Importer {
const notes = this.getValueOrDefault(value[5]);
if (notes) {
cipher.notes = `${notes}\n`;
cipher.notes = notes.trimEnd();
}
cipher.name = this.getValueOrDefault(value[1], "--");

View File

@@ -50,7 +50,7 @@ export class MykiCsvImporter extends BaseImporter implements Importer {
results.forEach((value) => {
const cipher = this.initLoginCipher();
cipher.name = this.getValueOrDefault(value.nickname, "--");
cipher.notes = this.getValueOrDefault(value.additionalInfo);
cipher.notes = this.getValueOrDefault(value.additionalInfo, "").trimEnd();
if (value.url !== undefined) {
// Accounts
@@ -132,7 +132,7 @@ export class MykiCsvImporter extends BaseImporter implements Importer {
cipher.secureNote = new SecureNoteView();
cipher.type = CipherType.SecureNote;
cipher.secureNote.type = SecureNoteType.Generic;
cipher.notes = this.getValueOrDefault(value.content);
cipher.notes = this.getValueOrDefault(value.content, "").trimEnd();
this.importUnmappedFields(cipher, value, _mappedUserNoteColumns);
} else {

View File

@@ -35,7 +35,7 @@ export class NetwrixPasswordSecureCsvImporter extends BaseImporter implements Im
const notes = this.getValueOrDefault(row.Informationen);
if (notes) {
cipher.notes = `${notes}\n`;
cipher.notes = notes.trimEnd();
}
cipher.name = this.getValueOrDefault(row.Beschreibung, "--");

View File

@@ -97,7 +97,7 @@ export class OnePassword1PuxImporter extends BaseImporter implements Importer {
this.processSections(category, item.details.sections, cipher);
if (!this.isNullOrWhitespace(item.details.notesPlain)) {
cipher.notes = item.details.notesPlain.split(this.newLineRegex).join("\n") + "\n";
cipher.notes = item.details.notesPlain.split(this.newLineRegex).join("\n").trimEnd();
}
this.convertToNoteIfNeeded(cipher);

View File

@@ -1,8 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
// 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 { CollectionView } from "@bitwarden/admin-console/common";
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
import { ImportResult } from "../models/import-result";

View File

@@ -1,6 +1,4 @@
// 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 { CollectionView } from "@bitwarden/admin-console/common";
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
import { ImportResult } from "../models/import-result";

View File

@@ -1,6 +1,4 @@
// 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 { CollectionView } from "@bitwarden/admin-console/common";
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { FieldType, SecureNoteType } from "@bitwarden/common/vault/enums";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
,,,password123,`;

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
,https://example.com/login,user@example.com,password123,`;

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
example.com,https://example.com/,user@example.com,password123,This is a test note`;

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
example.com,https://example.com/,user@example.com,password123,`;

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
login.example.com,https://login.example.com/auth,user@example.com,password123,`;

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
www.example.com,https://www.example.com/,user@example.com,password123,`;

View File

@@ -46,6 +46,7 @@ export const regularImportOptions = [
{ id: "ascendocsv", name: "Ascendo DataVault (csv)" },
{ id: "meldiumcsv", name: "Meldium (csv)" },
{ id: "passkeepcsv", name: "PassKeep (csv)" },
{ id: "arccsv", name: "Arc" },
{ id: "edgecsv", name: "Edge" },
{ id: "operacsv", name: "Opera" },
{ id: "vivaldicsv", name: "Vivaldi" },

View File

@@ -1,8 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
// 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 { CollectionView } from "@bitwarden/admin-console/common";
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";

View File

@@ -1,8 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
// 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 { CollectionView } from "@bitwarden/admin-console/common";
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
import { UserId } from "@bitwarden/user-core";
export abstract class ImportCollectionServiceAbstraction {

View File

@@ -1,9 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
// 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 { CollectionView } from "@bitwarden/admin-console/common";
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { Importer } from "../importers/importer";

View File

@@ -2,14 +2,14 @@ import { mock, MockProxy } from "jest-mock-extended";
// 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 { CollectionService } from "@bitwarden/admin-console/common";
import {
CollectionService,
CollectionTypes,
CollectionView,
} from "@bitwarden/admin-console/common";
CollectionTypes,
} from "@bitwarden/common/admin-console/models/collections";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
@@ -36,7 +36,7 @@ describe("ImportService", () => {
let collectionService: MockProxy<CollectionService>;
let keyService: MockProxy<KeyService>;
let encryptService: MockProxy<EncryptService>;
let pinService: MockProxy<PinServiceAbstraction>;
let keyGenerationService: MockProxy<KeyGenerationService>;
let accountService: MockProxy<AccountService>;
let restrictedItemTypesService: MockProxy<RestrictedItemTypesService>;
@@ -48,7 +48,7 @@ describe("ImportService", () => {
collectionService = mock<CollectionService>();
keyService = mock<KeyService>();
encryptService = mock<EncryptService>();
pinService = mock<PinServiceAbstraction>();
keyGenerationService = mock<KeyGenerationService>();
restrictedItemTypesService = mock<RestrictedItemTypesService>();
importService = new ImportService(
@@ -59,7 +59,7 @@ describe("ImportService", () => {
collectionService,
keyService,
encryptService,
pinService,
keyGenerationService,
accountService,
restrictedItemTypesService,
);

View File

@@ -4,16 +4,15 @@ import { firstValueFrom, map } 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 { CollectionService, CollectionWithIdRequest } from "@bitwarden/admin-console/common";
import {
CollectionService,
CollectionWithIdRequest,
CollectionView,
CollectionTypes,
} from "@bitwarden/admin-console/common";
} from "@bitwarden/common/admin-console/models/collections";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { ImportCiphersRequest } from "@bitwarden/common/models/request/import-ciphers.request";
import { ImportOrganizationCiphersRequest } from "@bitwarden/common/models/request/import-organization-ciphers.request";
import { KvpRequest } from "@bitwarden/common/models/request/kvp.request";
@@ -32,6 +31,7 @@ import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/res
import { KeyService } from "@bitwarden/key-management";
import {
ArcCsvImporter,
AscendoCsvImporter,
AvastCsvImporter,
AvastJsonImporter,
@@ -119,7 +119,7 @@ export class ImportService implements ImportServiceAbstraction {
private collectionService: CollectionService,
private keyService: KeyService,
private encryptService: EncryptService,
private pinService: PinServiceAbstraction,
private keyGenerationService: KeyGenerationService,
private accountService: AccountService,
private restrictedItemTypesService: RestrictedItemTypesService,
) {}
@@ -238,7 +238,7 @@ export class ImportService implements ImportServiceAbstraction {
this.encryptService,
this.i18nService,
this.cipherService,
this.pinService,
this.keyGenerationService,
this.accountService,
promptForPassword_callback,
);
@@ -257,6 +257,8 @@ export class ImportService implements ImportServiceAbstraction {
return new PadlockCsvImporter();
case "keepass2xml":
return new KeePass2XmlImporter();
case "arccsv":
return new ArcCsvImporter();
case "edgecsv":
case "chromecsv":
case "operacsv":
@@ -374,10 +376,13 @@ export class ImportService implements ImportServiceAbstraction {
private async handleIndividualImport(importResult: ImportResult, userId: UserId) {
const request = new ImportCiphersRequest();
for (let i = 0; i < importResult.ciphers.length; i++) {
const c = await this.cipherService.encrypt(importResult.ciphers[i], userId);
request.ciphers.push(new CipherRequest(c));
const encryptedCiphers = await this.cipherService.encryptMany(importResult.ciphers, userId);
for (const encryptedCipher of encryptedCiphers) {
request.ciphers.push(new CipherRequest(encryptedCipher));
}
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
if (importResult.folders != null) {
@@ -400,11 +405,18 @@ export class ImportService implements ImportServiceAbstraction {
userId: UserId,
) {
const request = new ImportOrganizationCiphersRequest();
for (let i = 0; i < importResult.ciphers.length; i++) {
importResult.ciphers[i].organizationId = organizationId;
const c = await this.cipherService.encrypt(importResult.ciphers[i], userId);
request.ciphers.push(new CipherRequest(c));
// Set organization ID on all ciphers before batch encryption
importResult.ciphers.forEach((cipher) => {
cipher.organizationId = organizationId;
});
const encryptedCiphers = await this.cipherService.encryptMany(importResult.ciphers, userId);
for (const encryptedCipher of encryptedCiphers) {
request.ciphers.push(new CipherRequest(encryptedCipher));
}
if (importResult.collections != null) {
for (let i = 0; i < importResult.collections.length; i++) {
importResult.collections[i].organizationId = organizationId;