BEEEP: Add importer for Keeper in json format (#608)
* Add testdata, create types for keeperjson import * Create keeperjson importer and tests * Register, Create instance of keeperjson importer * Move keeperCsvImporter to keeperImporters folder * Fixed import of BaseImporter * Removed unnecessary check for key * Move instantiation of importer into beforeEach * Fixed the second import with a wrong path * Adjust types based on new test export * Add test case for empty notes and custom fields * Implement logic for failed test case * Removed test expectation
This commit is contained in:
parent
4722a287ec
commit
5353cf03b5
|
@ -1,9 +1,9 @@
|
||||||
import { BaseImporter } from "./baseImporter";
|
import { BaseImporter } from "../baseImporter";
|
||||||
import { Importer } from "./importer";
|
import { Importer } from "../importer";
|
||||||
|
|
||||||
import { ImportResult } from "../models/domain/importResult";
|
import { ImportResult } from "../../models/domain/importResult";
|
||||||
|
|
||||||
import { FolderView } from "../models/view/folderView";
|
import { FolderView } from "../../models/view/folderView";
|
||||||
|
|
||||||
export class KeeperCsvImporter extends BaseImporter implements Importer {
|
export class KeeperCsvImporter extends BaseImporter implements Importer {
|
||||||
parse(data: string): Promise<ImportResult> {
|
parse(data: string): Promise<ImportResult> {
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { BaseImporter } from "../baseImporter";
|
||||||
|
import { Importer } from "../importer";
|
||||||
|
|
||||||
|
import { ImportResult } from "../../models/domain/importResult";
|
||||||
|
|
||||||
|
import { KeeperJsonExport, RecordsEntity } from "./types/keeperJsonTypes";
|
||||||
|
|
||||||
|
export class KeeperJsonImporter extends BaseImporter implements Importer {
|
||||||
|
parse(data: string): Promise<ImportResult> {
|
||||||
|
const result = new ImportResult();
|
||||||
|
const keeperExport: KeeperJsonExport = JSON.parse(data);
|
||||||
|
if (keeperExport == null || keeperExport.records == null || keeperExport.records.length === 0) {
|
||||||
|
result.success = false;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
keeperExport.records.forEach((record) => {
|
||||||
|
this.parseFolders(result, record);
|
||||||
|
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.name = record.title;
|
||||||
|
cipher.login.username = record.login;
|
||||||
|
cipher.login.password = record.password;
|
||||||
|
|
||||||
|
cipher.login.uris = this.makeUriArray(record.login_url);
|
||||||
|
cipher.notes = record.notes;
|
||||||
|
|
||||||
|
if (record.custom_fields != null) {
|
||||||
|
let customfieldKeys = Object.keys(record.custom_fields);
|
||||||
|
if (record.custom_fields["TFC:Keeper"] != null) {
|
||||||
|
customfieldKeys = customfieldKeys.filter((item) => item !== "TFC:Keeper");
|
||||||
|
cipher.login.totp = record.custom_fields["TFC:Keeper"];
|
||||||
|
}
|
||||||
|
|
||||||
|
customfieldKeys.forEach((key) => {
|
||||||
|
this.processKvp(cipher, key, record.custom_fields[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.convertToNoteIfNeeded(cipher);
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.organization) {
|
||||||
|
this.moveFoldersToCollections(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseFolders(result: ImportResult, record: RecordsEntity) {
|
||||||
|
if (record.folders == null || record.folders.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
record.folders.forEach((item) => {
|
||||||
|
if (item.folder != null) {
|
||||||
|
this.processFolder(result, item.folder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.shared_folder != null) {
|
||||||
|
this.processFolder(result, item.shared_folder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
export interface KeeperJsonExport {
|
||||||
|
shared_folders?: SharedFoldersEntity[] | null;
|
||||||
|
records?: RecordsEntity[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharedFoldersEntity {
|
||||||
|
path: string;
|
||||||
|
manage_users: boolean;
|
||||||
|
manage_records: boolean;
|
||||||
|
can_edit: boolean;
|
||||||
|
can_share: boolean;
|
||||||
|
permissions?: PermissionsEntity[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PermissionsEntity {
|
||||||
|
uid?: string | null;
|
||||||
|
manage_users: boolean;
|
||||||
|
manage_records: boolean;
|
||||||
|
name?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecordsEntity {
|
||||||
|
title: string;
|
||||||
|
login: string;
|
||||||
|
password: string;
|
||||||
|
login_url: string;
|
||||||
|
notes?: string;
|
||||||
|
custom_fields?: CustomFields;
|
||||||
|
folders?: FoldersEntity[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomFields = {
|
||||||
|
[key: string]: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface FoldersEntity {
|
||||||
|
folder?: string | null;
|
||||||
|
shared_folder?: string | null;
|
||||||
|
can_edit?: boolean | null;
|
||||||
|
can_share?: boolean | null;
|
||||||
|
}
|
|
@ -49,7 +49,8 @@ import { Importer } from "../importers/importer";
|
||||||
import { KasperskyTxtImporter } from "../importers/kasperskyTxtImporter";
|
import { KasperskyTxtImporter } from "../importers/kasperskyTxtImporter";
|
||||||
import { KeePass2XmlImporter } from "../importers/keepass2XmlImporter";
|
import { KeePass2XmlImporter } from "../importers/keepass2XmlImporter";
|
||||||
import { KeePassXCsvImporter } from "../importers/keepassxCsvImporter";
|
import { KeePassXCsvImporter } from "../importers/keepassxCsvImporter";
|
||||||
import { KeeperCsvImporter } from "../importers/keeperCsvImporter";
|
import { KeeperCsvImporter } from "../importers/keeperImporters/keeperCsvImporter";
|
||||||
|
import { KeeperJsonImporter } from "../importers/keeperImporters/keeperJsonImporter";
|
||||||
import { LastPassCsvImporter } from "../importers/lastpassCsvImporter";
|
import { LastPassCsvImporter } from "../importers/lastpassCsvImporter";
|
||||||
import { LogMeOnceCsvImporter } from "../importers/logMeOnceCsvImporter";
|
import { LogMeOnceCsvImporter } from "../importers/logMeOnceCsvImporter";
|
||||||
import { MeldiumCsvImporter } from "../importers/meldiumCsvImporter";
|
import { MeldiumCsvImporter } from "../importers/meldiumCsvImporter";
|
||||||
|
@ -100,6 +101,7 @@ export class ImportService implements ImportServiceAbstraction {
|
||||||
{ id: "1passwordmaccsv", name: "1Password 6 and 7 Mac (csv)" },
|
{ id: "1passwordmaccsv", name: "1Password 6 and 7 Mac (csv)" },
|
||||||
{ id: "roboformcsv", name: "RoboForm (csv)" },
|
{ id: "roboformcsv", name: "RoboForm (csv)" },
|
||||||
{ id: "keepercsv", name: "Keeper (csv)" },
|
{ id: "keepercsv", name: "Keeper (csv)" },
|
||||||
|
{ id: "keeperjson", name: "Keeper (json)" },
|
||||||
{ id: "enpasscsv", name: "Enpass (csv)" },
|
{ id: "enpasscsv", name: "Enpass (csv)" },
|
||||||
{ id: "enpassjson", name: "Enpass (json)" },
|
{ id: "enpassjson", name: "Enpass (json)" },
|
||||||
{ id: "safeincloudxml", name: "SafeInCloud (xml)" },
|
{ id: "safeincloudxml", name: "SafeInCloud (xml)" },
|
||||||
|
@ -251,6 +253,8 @@ export class ImportService implements ImportServiceAbstraction {
|
||||||
return new OnePasswordMacCsvImporter();
|
return new OnePasswordMacCsvImporter();
|
||||||
case "keepercsv":
|
case "keepercsv":
|
||||||
return new KeeperCsvImporter();
|
return new KeeperCsvImporter();
|
||||||
|
case "keeperjson":
|
||||||
|
return new KeeperJsonImporter();
|
||||||
case "passworddragonxml":
|
case "passworddragonxml":
|
||||||
return new PasswordDragonXmlImporter();
|
return new PasswordDragonXmlImporter();
|
||||||
case "enpasscsv":
|
case "enpasscsv":
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
|
||||||
|
import { KeeperJsonImporter as Importer } from "jslib-common/importers/keeperImporters/keeperJsonImporter";
|
||||||
|
|
||||||
|
import { testData as TestData } from "./testData/keeperJson/testData";
|
||||||
|
|
||||||
|
describe("Keeper Json Importer", () => {
|
||||||
|
const testDataJson = JSON.stringify(TestData);
|
||||||
|
|
||||||
|
let importer: Importer;
|
||||||
|
beforeEach(() => {
|
||||||
|
importer = new Importer();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse login data", async () => {
|
||||||
|
const result = await importer.parse(testDataJson);
|
||||||
|
expect(result != null).toBe(true);
|
||||||
|
|
||||||
|
const cipher = result.ciphers.shift();
|
||||||
|
expect(cipher.name).toEqual("Bank Account 1");
|
||||||
|
expect(cipher.login.username).toEqual("customer1234");
|
||||||
|
expect(cipher.login.password).toEqual("4813fJDHF4239fdk");
|
||||||
|
expect(cipher.login.uris.length).toEqual(1);
|
||||||
|
const uriView = cipher.login.uris.shift();
|
||||||
|
expect(uriView.uri).toEqual("https://chase.com");
|
||||||
|
expect(cipher.notes).toEqual("These are some notes.");
|
||||||
|
|
||||||
|
const cipher2 = result.ciphers.shift();
|
||||||
|
expect(cipher2.name).toEqual("Bank Account 2");
|
||||||
|
expect(cipher2.login.username).toEqual("mybankusername");
|
||||||
|
expect(cipher2.login.password).toEqual("w4k4k193f$^&@#*%2");
|
||||||
|
expect(cipher2.login.uris.length).toEqual(1);
|
||||||
|
const uriView2 = cipher2.login.uris.shift();
|
||||||
|
expect(uriView2.uri).toEqual("https://amex.com");
|
||||||
|
expect(cipher2.notes).toEqual("Some great information here.");
|
||||||
|
|
||||||
|
const cipher3 = result.ciphers.shift();
|
||||||
|
expect(cipher3.name).toEqual("Some Account");
|
||||||
|
expect(cipher3.login.username).toEqual("someUserName");
|
||||||
|
expect(cipher3.login.password).toEqual("w4k4k1wergf$^&@#*%2");
|
||||||
|
expect(cipher3.notes).toBeNull();
|
||||||
|
expect(cipher3.fields).toBeNull();
|
||||||
|
expect(cipher3.login.uris.length).toEqual(1);
|
||||||
|
const uriView3 = cipher3.login.uris.shift();
|
||||||
|
expect(uriView3.uri).toEqual("https://example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should import TOTP when present", async () => {
|
||||||
|
const result = await importer.parse(testDataJson);
|
||||||
|
expect(result != null).toBe(true);
|
||||||
|
|
||||||
|
const cipher = result.ciphers.shift();
|
||||||
|
expect(cipher.login.totp).toBeNull();
|
||||||
|
|
||||||
|
// 2nd Cipher
|
||||||
|
const cipher2 = result.ciphers.shift();
|
||||||
|
expect(cipher2.login.totp).toEqual(
|
||||||
|
"otpauth://totp/Amazon:me@company.com?secret=JBSWY3DPEHPK3PXP&issuer=Amazon&algorithm=SHA1&digits=6&period=30"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse custom fields", async () => {
|
||||||
|
const result = await importer.parse(testDataJson);
|
||||||
|
expect(result != null).toBe(true);
|
||||||
|
|
||||||
|
const cipher = result.ciphers.shift();
|
||||||
|
expect(cipher.fields.length).toBe(1);
|
||||||
|
expect(cipher.fields[0].name).toEqual("Account Number");
|
||||||
|
expect(cipher.fields[0].value).toEqual("123-456-789");
|
||||||
|
|
||||||
|
// 2nd Cipher
|
||||||
|
const cipher2 = result.ciphers.shift();
|
||||||
|
expect(cipher2.fields.length).toBe(2);
|
||||||
|
expect(cipher2.fields[0].name).toEqual("Security Group");
|
||||||
|
expect(cipher2.fields[0].value).toEqual("Public");
|
||||||
|
|
||||||
|
expect(cipher2.fields[1].name).toEqual("IP Address");
|
||||||
|
expect(cipher2.fields[1].value).toEqual("12.45.67.8");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create folders and assigned ciphers to them", async () => {
|
||||||
|
const result = await importer.parse(testDataJson);
|
||||||
|
expect(result != null).toBe(true);
|
||||||
|
|
||||||
|
const folders = result.folders;
|
||||||
|
expect(folders.length).toBe(2);
|
||||||
|
expect(folders[0].name).toBe("Optional Private Folder 1");
|
||||||
|
expect(folders[1].name).toBe("My Customer 1");
|
||||||
|
|
||||||
|
expect(result.folderRelationships[0]).toEqual([0, 0]);
|
||||||
|
expect(result.folderRelationships[1]).toEqual([1, 0]);
|
||||||
|
expect(result.folderRelationships[2]).toEqual([1, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create collections if part of an organization", async () => {
|
||||||
|
importer.organizationId = Utils.newGuid();
|
||||||
|
const result = await importer.parse(testDataJson);
|
||||||
|
expect(result != null).toBe(true);
|
||||||
|
|
||||||
|
const collections = result.collections;
|
||||||
|
expect(collections.length).toBe(2);
|
||||||
|
expect(collections[0].name).toBe("Optional Private Folder 1");
|
||||||
|
expect(collections[1].name).toBe("My Customer 1");
|
||||||
|
|
||||||
|
expect(result.collectionRelationships[0]).toEqual([0, 0]);
|
||||||
|
expect(result.collectionRelationships[1]).toEqual([1, 0]);
|
||||||
|
expect(result.collectionRelationships[2]).toEqual([1, 1]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { KeeperJsonExport } from "jslib-common/importers/keeperImporters/types/keeperJsonTypes";
|
||||||
|
|
||||||
|
export const testData: KeeperJsonExport = {
|
||||||
|
shared_folders: [
|
||||||
|
{
|
||||||
|
path: "My Customer 1",
|
||||||
|
manage_users: true,
|
||||||
|
manage_records: true,
|
||||||
|
can_edit: true,
|
||||||
|
can_share: true,
|
||||||
|
permissions: [
|
||||||
|
{
|
||||||
|
uid: "kVM96KGEoGxhskZoSTd_jw",
|
||||||
|
manage_users: true,
|
||||||
|
manage_records: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user@mycompany.com",
|
||||||
|
manage_users: true,
|
||||||
|
manage_records: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "Testing\\My Customer 2",
|
||||||
|
manage_users: true,
|
||||||
|
manage_records: true,
|
||||||
|
can_edit: true,
|
||||||
|
can_share: true,
|
||||||
|
permissions: [
|
||||||
|
{
|
||||||
|
uid: "ih1CggiQ-3ENXcn4G0sl-g",
|
||||||
|
manage_users: true,
|
||||||
|
manage_records: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user@mycompany.com",
|
||||||
|
manage_users: true,
|
||||||
|
manage_records: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
title: "Bank Account 1",
|
||||||
|
login: "customer1234",
|
||||||
|
password: "4813fJDHF4239fdk",
|
||||||
|
login_url: "https://chase.com",
|
||||||
|
notes: "These are some notes.",
|
||||||
|
custom_fields: {
|
||||||
|
"Account Number": "123-456-789",
|
||||||
|
},
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
folder: "Optional Private Folder 1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Bank Account 2",
|
||||||
|
login: "mybankusername",
|
||||||
|
password: "w4k4k193f$^&@#*%2",
|
||||||
|
login_url: "https://amex.com",
|
||||||
|
notes: "Some great information here.",
|
||||||
|
custom_fields: {
|
||||||
|
"Security Group": "Public",
|
||||||
|
"IP Address": "12.45.67.8",
|
||||||
|
"TFC:Keeper":
|
||||||
|
"otpauth://totp/Amazon:me@company.com?secret=JBSWY3DPEHPK3PXP&issuer=Amazon&algorithm=SHA1&digits=6&period=30",
|
||||||
|
},
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
folder: "Optional Private Folder 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shared_folder: "My Customer 1",
|
||||||
|
can_edit: true,
|
||||||
|
can_share: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Some Account",
|
||||||
|
login: "someUserName",
|
||||||
|
password: "w4k4k1wergf$^&@#*%2",
|
||||||
|
login_url: "https://example.com",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
Loading…
Reference in New Issue