Skip to content

Commit

Permalink
Merge pull request #73 from muddykat-tech/master
Browse files Browse the repository at this point in the history
  • Loading branch information
perry-mitchell authored Apr 28, 2024
2 parents 2004c97 + 5ff6808 commit 5d1166e
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 0 deletions.
98 changes: 98 additions & 0 deletions source/importers/KeeperSecurityImporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
const fs = require("fs/promises");
const {
Vault,
Entry,
createEntryFacade,
consumeEntryFacade,
} = require("buttercup");

const DEFAULT_GROUP = "General";

/**
* Importer for Keeper Security vaults
* @memberof module:ButtercupImporter
*/
class KeeperSecurityImporter {
/**
* Create a new Keeper Security importer
* @param {String} data Raw JSON data of a Keeper Security vault export
*/
constructor(data) {
this._data = data;
}

/**
* Export to a Buttercup vault
* @returns {Promise.<Vault>}
* @memberof KeeperSecurityImporter
*/
export() {
const groups = {};
return Promise.resolve().then(() => {
const vault = new Vault();
const ksJson = JSON.parse(this._data); // Parse the new JSON data

// Create the root group
const rootGroup = vault.createGroup(DEFAULT_GROUP);
groups[null] = rootGroup;

ksJson.records.forEach((record) => {
if (record.folders) {
var folderPath = record.folders[0].folder;
var folders = folderPath
.split("\\")
.map((folderName) => folderName.trim());

var currentGroup = rootGroup;

for (
var folderIndex = 0;
folderIndex < folders.length;
folderIndex += 1
) {
if (groups[folders[folderIndex]] != undefined) {
currentGroup = groups[folders[folderIndex]];
} else {
currentGroup = currentGroup.createGroup(
folders[folderIndex]
);

groups[folders[folderIndex]] = currentGroup;
}
// Section untested due to an odd issue with createEntry not instancing correctly?
const entry = currentGroup.createEntry(record.title);
entry.setProperty(
"username",
record.login == null ? "" : record.login
);
entry.setProperty(
"password",
record.password == null ? "" : record.password
);
entry.setProperty(
"URL",
record.login_url == null ? "" : record.login_url
);
}
}
});

return vault;
});
}
}

/**
* Load an importer from a file
* @param {String} filename The file to load from
* @returns {Promise.<KeeperSecurityImporter>}
* @static
* @memberof KeeperSecurityImporter
*/
KeeperSecurityImporter.loadFromFile = function (filename) {
return fs
.readFile(filename, "utf8")
.then((data) => new KeeperSecurityImporter(data));
};

module.exports = KeeperSecurityImporter;
2 changes: 2 additions & 0 deletions source/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const CSVImporter = require("./importers/CSVImporter.js");
const KeePass2XMLImporter = require("./importers/KeePass2XMLImporter.js");
const LastPassImporter = require("./importers/LastPassImporter.js");
const OnePasswordImporter = require("./importers/OnePasswordImporter.js");
const KeeperSecurityImporter = require("./importers/KeeperSecurityImporter.js");

/**
* @module ButtercupImporter
Expand All @@ -18,4 +19,5 @@ module.exports = {
KeePass2XMLImporter,
LastPassImporter,
OnePasswordImporter,
KeeperSecurityImporter,
};
57 changes: 57 additions & 0 deletions test/resources/keeper.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"shared_folders": [],
"records": [
{
"title": "Username Test",
"$type": "login",
"schema": [
"$login::1",
"$password::1",
"$url::1",
"$fileRef::1",
"$oneTimeCode::1"
],
"login": "username2",
"password": "password2",
"login_url": "https://example.org/"
},
{
"title": "Folder Test",
"$type": "login",
"schema": [
"$login::1",
"$password::1",
"$url::1",
"$fileRef::1",
"$oneTimeCode::1"
],
"login": "username1",
"password": "password1",
"login_url": "https://example.org/",
"folders": [
{
"folder": "Foo \\Bar "
}
]
},
{
"title": "Sub Folder Entry Test",
"$type": "login",
"schema": [
"$login::1",
"$password::1",
"$url::1",
"$fileRef::1",
"$oneTimeCode::1"
],
"login": "username3",
"password": "password3",
"login_url": "https://example.org/",
"folders": [
{
"folder": "Foo \\Bar "
}
]
}
]
}
47 changes: 47 additions & 0 deletions test/specs/importers/KeeperSecurityImporter.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const path = require("path");
const KeeperSecurityImporter = require("../../../dist/importers/KeeperSecurityImporter.js");
const { Entry, Group, Vault } = require("buttercup");

const EXAMPLE_VAULT = path.resolve(__dirname, "../../resources/keeper.json");

describe("KeeperSecurityImporter", function () {
beforeEach(function () {
return KeeperSecurityImporter.loadFromFile(EXAMPLE_VAULT)
.then((importer) => importer.export())
.then((vault) => {
this.vault = vault;
});
});

it("creates a vault instance", function () {
expect(this.vault).to.be.an.instanceOf(Vault);
});

it("contains expected groups", function () {
// I'd prefer to use the function 'getGroups' however, this seems to be missing groups in the _groups structure?
const groups = this.vault._groups.map((g) => g.getTitle());
expect(groups).to.have.lengthOf(3);
expect(groups).to.contain("Foo");
expect(groups).to.contain("Bar");
});

it("imports login properties", function () {
const [entry] = this.vault.findEntriesByProperty(
"title",
"Item with login"
);
expect(entry).to.be.an.instanceOf(Entry);
expect(entry.getProperty("username")).to.equal("username1");
expect(entry.getProperty("password")).to.equal("password1");
});

it("imports URLs", function () {
const [entry] = this.vault.findEntriesByProperty(
"title",
"Item with login uri"
);
expect(entry).to.be.an.instanceOf(Entry);
expect(entry.getProperty("URL")).to.equal("https://example.org/");
// @todo should be a website
});
});

0 comments on commit 5d1166e

Please sign in to comment.