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:
Daniel James Smith 2022-01-26 23:04:55 +01:00 committed by GitHub
parent 4722a287ec
commit 5353cf03b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 319 additions and 5 deletions

View File

@ -1,9 +1,9 @@
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
import { BaseImporter } from "../baseImporter";
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 {
parse(data: string): Promise<ImportResult> {

View File

@ -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;
}
});
}
}

View File

@ -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;
}

View File

@ -49,7 +49,8 @@ import { Importer } from "../importers/importer";
import { KasperskyTxtImporter } from "../importers/kasperskyTxtImporter";
import { KeePass2XmlImporter } from "../importers/keepass2XmlImporter";
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 { LogMeOnceCsvImporter } from "../importers/logMeOnceCsvImporter";
import { MeldiumCsvImporter } from "../importers/meldiumCsvImporter";
@ -100,6 +101,7 @@ export class ImportService implements ImportServiceAbstraction {
{ id: "1passwordmaccsv", name: "1Password 6 and 7 Mac (csv)" },
{ id: "roboformcsv", name: "RoboForm (csv)" },
{ id: "keepercsv", name: "Keeper (csv)" },
{ id: "keeperjson", name: "Keeper (json)" },
{ id: "enpasscsv", name: "Enpass (csv)" },
{ id: "enpassjson", name: "Enpass (json)" },
{ id: "safeincloudxml", name: "SafeInCloud (xml)" },
@ -251,6 +253,8 @@ export class ImportService implements ImportServiceAbstraction {
return new OnePasswordMacCsvImporter();
case "keepercsv":
return new KeeperCsvImporter();
case "keeperjson":
return new KeeperJsonImporter();
case "passworddragonxml":
return new PasswordDragonXmlImporter();
case "enpasscsv":

View File

@ -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]);
});
});

View File

@ -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",
},
],
};