diff --git a/source/importers/KeeperSecurityImporter.js b/source/importers/KeeperSecurityImporter.js new file mode 100644 index 0000000..70ddda2 --- /dev/null +++ b/source/importers/KeeperSecurityImporter.js @@ -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.} + * @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.} + * @static + * @memberof KeeperSecurityImporter + */ +KeeperSecurityImporter.loadFromFile = function (filename) { + return fs + .readFile(filename, "utf8") + .then((data) => new KeeperSecurityImporter(data)); +}; + +module.exports = KeeperSecurityImporter; diff --git a/source/index.js b/source/index.js index a26e2f2..17ffd7b 100644 --- a/source/index.js +++ b/source/index.js @@ -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 @@ -18,4 +19,5 @@ module.exports = { KeePass2XMLImporter, LastPassImporter, OnePasswordImporter, + KeeperSecurityImporter, }; diff --git a/test/resources/keeper.json b/test/resources/keeper.json new file mode 100644 index 0000000..21c3157 --- /dev/null +++ b/test/resources/keeper.json @@ -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 " + } + ] + } + ] +} diff --git a/test/specs/importers/KeeperSecurityImporter.spec.js b/test/specs/importers/KeeperSecurityImporter.spec.js new file mode 100644 index 0000000..dc54843 --- /dev/null +++ b/test/specs/importers/KeeperSecurityImporter.spec.js @@ -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 + }); +});