From 4262486e1e52e1d7464b4705013c07b3f87e44e5 Mon Sep 17 00:00:00 2001 From: Wladimir Palant Date: Mon, 25 Apr 2016 21:26:53 +0200 Subject: [PATCH] Added initial Chrome packaging support - not really working, but at least the extension installs --- .gitignore | 5 +- README.md | 2 +- chrome/data/options.html | 24 ++++ chrome/lib/main.js | 13 ++ chrome/lib/sdk/event/core.js | 12 ++ chrome/lib/sdk/event/target.js | 47 +++++++ chrome/lib/sdk/page-worker.js | 15 ++ chrome/lib/sdk/self.js | 16 +++ chrome/lib/sdk/simple-prefs.js | 18 +++ chrome/lib/sdk/simple-storage.js | 27 ++++ chrome/lib/sdk/timers.js | 11 ++ gulpfile.js | 227 ++++++++++++++++++++++--------- locale/en-US.properties | 3 + manifest.json | 35 +++++ package.json | 8 +- 15 files changed, 397 insertions(+), 66 deletions(-) create mode 100644 chrome/data/options.html create mode 100644 chrome/lib/main.js create mode 100644 chrome/lib/sdk/event/core.js create mode 100644 chrome/lib/sdk/event/target.js create mode 100644 chrome/lib/sdk/page-worker.js create mode 100644 chrome/lib/sdk/self.js create mode 100644 chrome/lib/sdk/simple-prefs.js create mode 100644 chrome/lib/sdk/simple-storage.js create mode 100644 chrome/lib/sdk/timers.js create mode 100644 manifest.json diff --git a/.gitignore b/.gitignore index 5ec45836..5f27d0d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ -/build/ +/build-jpm/ +/build-chrome/ /node_modules/ *.xpi +*.crx *.zip *.pyc *.sh +*.pem diff --git a/README.md b/README.md index 2706e876..697db2fd 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ If all the dependencies are installed building EasyPasswords is simply a matter gulp xpi -This will create a package inside the `build` directory with the file name like `easypasswords@palant.de-n.n.n.xpi` that you can install in Firefox. +This will create a package inside the `build-jpm` directory with the file name like `easypasswords@palant.de-n.n.n.xpi` that you can install in Firefox. How to test ----------- diff --git a/chrome/data/options.html b/chrome/data/options.html new file mode 100644 index 00000000..615a4c73 --- /dev/null +++ b/chrome/data/options.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + +
+ + + +
+ + + diff --git a/chrome/lib/main.js b/chrome/lib/main.js new file mode 100644 index 00000000..7e5aeb4f --- /dev/null +++ b/chrome/lib/main.js @@ -0,0 +1,13 @@ +/* + * This Source Code is subject to the terms of the Mozilla Public License + * version 2.0 (the "License"). You can obtain a copy of the License at + * http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +require("sdk/simple-storage").init.then(() => +{ + require("../../lib/masterPassword"); + require("../../lib/passwords"); +}); diff --git a/chrome/lib/sdk/event/core.js b/chrome/lib/sdk/event/core.js new file mode 100644 index 00000000..4687ca6b --- /dev/null +++ b/chrome/lib/sdk/event/core.js @@ -0,0 +1,12 @@ +/* + * This Source Code is subject to the terms of the Mozilla Public License + * version 2.0 (the "License"). You can obtain a copy of the License at + * http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +exports.emit = function(obj) +{ + obj.emit.apply(obj, [].slice.call(arguments, 1)); +} diff --git a/chrome/lib/sdk/event/target.js b/chrome/lib/sdk/event/target.js new file mode 100644 index 00000000..a2df902d --- /dev/null +++ b/chrome/lib/sdk/event/target.js @@ -0,0 +1,47 @@ +/* + * This Source Code is subject to the terms of the Mozilla Public License + * version 2.0 (the "License"). You can obtain a copy of the License at + * http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +let proto = { + on: function(eventName, listener) + { + if (!(eventName in this._listeners)) + this._listeners[eventName] = []; + this._listeners[eventName].push(listener); + }, + + off: function(eventName, listener) + { + let index = (eventName in this._listeners ? this._listeners[eventName].indexOf(listener) : -1); + if (index >= 0) + this._listeners[eventName].splice(index, 1); + }, + + once: function(eventName, listener) + { + let wrapper = () => + { + this.off(eventName, wrapper); + listener.apply(this, arguments); + }; + this.on(eventName, wrapper); + }, + + emit: function(eventName) + { + let args = [].slice.call(arguments, 1); + for (let listener of this._listeners[eventName] || []) + listener.apply(null, args); + } +}; + +exports.EventTarget = function() +{ + let result = Object.create(proto); + result._listeners = []; + return result; +}; diff --git a/chrome/lib/sdk/page-worker.js b/chrome/lib/sdk/page-worker.js new file mode 100644 index 00000000..84e3e033 --- /dev/null +++ b/chrome/lib/sdk/page-worker.js @@ -0,0 +1,15 @@ +/* + * This Source Code is subject to the terms of the Mozilla Public License + * version 2.0 (the "License"). You can obtain a copy of the License at + * http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +exports.Page = function(options) +{ + this.frame = document.createElement("iframe"); + document.body.appendChild(this.frame); + + this.frame.src = options.contentURL; +}; diff --git a/chrome/lib/sdk/self.js b/chrome/lib/sdk/self.js new file mode 100644 index 00000000..30847c04 --- /dev/null +++ b/chrome/lib/sdk/self.js @@ -0,0 +1,16 @@ +/* + * This Source Code is subject to the terms of the Mozilla Public License + * version 2.0 (the "License"). You can obtain a copy of the License at + * http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +/* global chrome */ + +exports.data = { + url: function(path) + { + return chrome.runtime.getURL("data/" + path); + } +}; diff --git a/chrome/lib/sdk/simple-prefs.js b/chrome/lib/sdk/simple-prefs.js new file mode 100644 index 00000000..e9543fd7 --- /dev/null +++ b/chrome/lib/sdk/simple-prefs.js @@ -0,0 +1,18 @@ +/* + * This Source Code is subject to the terms of the Mozilla Public License + * version 2.0 (the "License"). You can obtain a copy of the License at + * http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +exports.prefs = window.simpleStorage; + +exports.on = function(key, handler) +{ + window.addEventListener("storage", function(event) + { + if (event.key == key) + handler(); + }); +}; diff --git a/chrome/lib/sdk/simple-storage.js b/chrome/lib/sdk/simple-storage.js new file mode 100644 index 00000000..84552130 --- /dev/null +++ b/chrome/lib/sdk/simple-storage.js @@ -0,0 +1,27 @@ +/* + * This Source Code is subject to the terms of the Mozilla Public License + * version 2.0 (the "License"). You can obtain a copy of the License at + * http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +/* global chrome */ + +exports.init = new Promise((resolve, reject) => +{ + chrome.storage.local.get("passwords", function(items) + { + if (chrome.runtime.lastError) + reject(chrome.runtime.lastError); + else + { + exports.storage = items.passwords; + setTimeout(function() + { + chrome.storage.local.set({passwords: exports.storage}); + }, 30000); + resolve(); + } + }); +}); diff --git a/chrome/lib/sdk/timers.js b/chrome/lib/sdk/timers.js new file mode 100644 index 00000000..69df8cf6 --- /dev/null +++ b/chrome/lib/sdk/timers.js @@ -0,0 +1,11 @@ +/* + * This Source Code is subject to the terms of the Mozilla Public License + * version 2.0 (the "License"). You can obtain a copy of the License at + * http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +exports.setTimeout = window.setTimeout; +exports.setInterval = window.setInterval; +exports.clearTimeout = window.clearTimeout; diff --git a/gulpfile.js b/gulpfile.js index 05e83d45..0c34608d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -6,15 +6,23 @@ "use strict"; +let fs = require("fs"); let path = require("path"); let spawn = require("child_process").spawn; +let Transform = require("stream").Transform; let gulp = require("gulp"); +let source = require("vinyl-source-stream"); let less = require("gulp-less"); let rename = require("gulp-rename"); +let merge = require("merge-stream"); let del = require("del"); let eslint = require("gulp-eslint"); let htmlhint = require("gulp-htmlhint"); +let RSA = require("node-rsa"); +let zip = require("gulp-zip"); +let browserify = require("browserify"); +let jsonModify = require("gulp-json-modify"); function readArg(prefix, defaultValue) { @@ -28,7 +36,7 @@ function jpm(args) { return new Promise((resolve, reject) => { - let builddir = path.resolve(process.cwd(), "build"); + let builddir = path.resolve(process.cwd(), "build-jpm"); let jpm = path.resolve(process.cwd(), "node_modules/.bin/jpm"); let ps = spawn(jpm, args, {cwd: builddir}); ps.stdout.pipe(process.stdout); @@ -37,99 +45,181 @@ function jpm(args) }); } -gulp.task("default", ["xpi"], function() -{ -}); - -gulp.task("package.json", function() -{ - return gulp.src("package.json") - .pipe(gulp.dest("build")); -}); - -gulp.task("LICENSE.txt", function() -{ - return gulp.src("LICENSE.txt") - .pipe(gulp.dest("build")); -}); - -gulp.task("icon.png", function() -{ - return gulp.src(["data/images/icon48.png"]) - .pipe(rename("icon.png")) - .pipe(gulp.dest("build")); -}); - -gulp.task("icon64.png", function() -{ - return gulp.src(["data/images/icon64.png"]) - .pipe(gulp.dest("build")); -}); - -gulp.task("data", function() -{ - return gulp.src(["data/**/*.js", "data/**/*.html", "data/**/*.png", "data/**/*.svg", "!data/images/icon48.png"]) - .pipe(gulp.dest("build/data")); -}); - -gulp.task("less", function() +function signCRX(keyFile) { - return gulp.src("data/**/*.less") - .pipe(less()) - .pipe(gulp.dest("build/data")); -}); - -gulp.task("lib", function() -{ - return gulp.src("lib/**/*.js") - .pipe(gulp.dest("build/lib")); -}); + let stream = new Transform({objectMode: true}); + stream._transform = function(file, encoding, callback) + { + if (!file.isBuffer()) + throw new Error("Unexpected file type"); + + new Promise((resolve, reject) => + { + fs.readFile(keyFile, function(error, data) + { + if (error) + reject(error); + else + resolve(data); + }); + }).then(keyData => + { + let privateKey = RSA(keyData, {signingScheme: "pkcs1-sha1"}); + let publicKey = privateKey.exportKey("pkcs8-public-der"); + let signature = privateKey.sign(file.contents, "buffer"); + + let header = new Buffer(16); + header.write("Cr24", 0); + header.writeInt32LE(2, 4); + header.writeInt32LE(publicKey.length, 8); + header.writeInt32LE(signature.length, 12); + return Buffer.concat([header, publicKey, signature, file.contents]); + }).then(contents => + { + file.path = file.path.replace(/\.zip$/, ".crx"); + file.contents = contents; + callback(null, file); + }).catch(function(error) + { + console.error(error); + callback(error); + }); + }; + return stream; +} -gulp.task("locale", function() +function toChromeLocale() { - return gulp.src("locale/**/*.properties") - .pipe(gulp.dest("build/locale")); -}); + let stream = new Transform({objectMode: true}); + stream._transform = function(file, encoding, callback) + { + if (!file.isBuffer()) + throw new Error("Unexpected file type"); + + let locale = path.basename(file.path).replace(/\.properties$/, ""); + let lines = file.contents.toString("utf-8").split(/[\r\n]+/); + let data = {}; + for (let line of lines) + { + if (/^\s*#/.test(line)) + continue; + + let parts = line.split(/\s*=\s*/, 2); + if (parts.length < 2) + continue; + + data[parts[0].replace(/-/g, "_")] = {"message": parts[1]}; + } + + let manifest = require("./package.json"); + data.name = {"message": manifest.title}; + data.description = {"message": manifest.description}; + if ("locales" in manifest && locale in manifest.locales) + { + let localized = manifest.locales[locale]; + if ("title" in localized) + data.name.message = localized.title; + if ("description" in localized) + data.description.message = localized.description; + } + + file.path = path.join(path.dirname(file.path), locale.replace(/-/g, "_"), "messages.json"); + file.contents = new Buffer(JSON.stringify(data), "utf-8"); + callback(null, file); + }; + return stream; +} -gulp.task("builddir", ["package.json", "LICENSE.txt", "icon.png", "icon64.png", "data", "less", "lib", "locale"], function() +gulp.task("default", ["xpi"], function() { }); -gulp.task("eslint-data", ["package.json", "data"], function() -{ - return gulp.src("build/data/**/*.js") +gulp.task("build-jpm", ["validate"], function() +{ + return merge( + gulp.src(["package.json", "LICENSE.TXT", "data/images/icon64.png"]) + .pipe(gulp.dest("build-jpm")), + gulp.src("data/images/icon48.png") + .pipe(rename("icon.png")) + .pipe(gulp.dest("build-jpm")), + gulp.src(["data/**/*.js", "data/**/*.html", "data/**/*.png", "data/**/*.svg", "!data/images/icon48.png"]) + .pipe(gulp.dest("build-jpm/data")), + gulp.src("data/**/*.less") + .pipe(less()) + .pipe(gulp.dest("build-jpm/data")), + gulp.src("lib/**/*.js") + .pipe(gulp.dest("build-jpm/lib")), + gulp.src("locale/**/*.properties") + .pipe(gulp.dest("build-jpm/locale")) + ); +}); + +gulp.task("build-chrome", ["validate"], function() +{ + return merge( + gulp.src("LICENSE.TXT") + .pipe(gulp.dest("build-chrome")), + gulp.src("manifest.json") + .pipe(jsonModify({key: "version", value: require("./package.json").version})) + .pipe(gulp.dest("build-chrome")), + gulp.src(["data/**/*.js", "data/**/*.html", "data/**/*.png", "data/**/*.svg", "chrome/data/**/*.html"]) + .pipe(gulp.dest("build-chrome/data")), + gulp.src("data/**/*.less") + .pipe(less()) + .pipe(gulp.dest("build-chrome/data")), + browserify("chrome/lib/main.js", {"paths": "chrome/lib"}) + .bundle() + .pipe(source("background.js")) + .pipe(gulp.dest("build-chrome")), + gulp.src("locale/**/*.properties") + .pipe(toChromeLocale()) + .pipe(gulp.dest("build-chrome/_locales")) + ); +}); + +gulp.task("eslint-data", function() +{ + return gulp.src(["data/**/*.js", "chrome/data/**/*.js"]) .pipe(eslint({envs: ["browser", "es6"]})) .pipe(eslint.format()) .pipe(eslint.failAfterError()); }); -gulp.task("eslint-lib", ["package.json", "lib"], function() +gulp.task("eslint-lib", function() { - return gulp.src("build/lib/**/*.js") + return gulp.src(["lib/**/*.js"]) .pipe(eslint({envs: ["commonjs", "es6"]})) .pipe(eslint.format()) .pipe(eslint.failAfterError()); }); -gulp.task("eslint", ["eslint-data", "eslint-lib"], function() +gulp.task("eslint-chromelib", function() { + return gulp.src(["chrome/lib/**/*.js"]) + .pipe(eslint({envs: ["commonjs", "browser", "es6"]})) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()); }); -gulp.task("htmlhint", ["data"], function() +gulp.task("htmlhint", function() { - return gulp.src("build/**/*.html") + return gulp.src(["data/**/*.html", "chrome/data/**/*.html"]) .pipe(htmlhint({ "title-require": false })) .pipe(htmlhint.failReporter()); }); -gulp.task("xpi", ["eslint", "htmlhint"], function() +gulp.task("validate", ["eslint-data", "eslint-lib", "eslint-chromelib", "htmlhint"], function() +{ +}); + +gulp.task("xpi", ["build-jpm"], function() { return jpm(["xpi"]); }); -gulp.task("post", ["eslint", "htmlhint"], function() +gulp.task("post", ["build-jpm"], function() { let postUrl = readArg("--post-url=", "http://localhost:8888/"); if (/^\d+$/.test(postUrl)) @@ -145,7 +235,18 @@ gulp.task("watch", ["post"], function() gulp.watch(["data/**/*", "lib/**/*", "locale/**/*"], ["post"]); }); +gulp.task("crx", ["build-chrome"], function() +{ + let manifest = require("./package.json"); + let result = gulp.src(["build-chrome/**", "!build-chrome/**/.*", "!build-chrome/**/*.zip", "!build-chrome/**/*.crx"]) + .pipe(zip("easypasswords-" + manifest.version + ".zip")); + let keyFile = readArg("--private-key="); + if (keyFile) + result = result.pipe(signCRX(keyFile)); + return result.pipe(gulp.dest("build-chrome")); +}); + gulp.task("clean", function() { - return del("build"); + return del(["build-jpm", "build-chrome"]); }); diff --git a/locale/en-US.properties b/locale/en-US.properties index f8f66689..3953f841 100644 --- a/locale/en-US.properties +++ b/locale/en-US.properties @@ -72,6 +72,9 @@ unknown-data-format = Unknown data format! allpasswords-import-confirm = Importing passwords will only produce meaningful results if the master password didn't change. Your existing passwords might get overwritten. Are you sure you want to proceed? allpasswords-import-success = Passwords data has been imported. +# options +options-title = Easy Passwords options + # simple-prefs autolock_title = Enable auto-lock autolock_description = Lock passwords automatically when the panel is closed diff --git a/manifest.json b/manifest.json new file mode 100644 index 00000000..8088b118 --- /dev/null +++ b/manifest.json @@ -0,0 +1,35 @@ +{ + "manifest_version": 2, + "minimum_chrome_version": "37.0", + "default_locale": "en_US", + "name": "__MSG_name__", + "short_name": "__MSG_name__", + "description": "__MSG_description__", + "permissions": [ + "tabs", + "http://*/*", + "https://*/*", + "storage", + "unlimitedStorage" + ], + "background": { + "scripts": [ + "background.js" + ] + }, + "options_page": "data/options.html", + "icons": { + "16": "data/images/icon16.png", + "32": "data/images/icon32.png", + "48": "data/images/icon48.png", + "64": "data/images/icon64.png" + }, + "browser_action": { + "default_icon": { + "16": "data/images/icon16.png", + "32": "data/images/icon32.png" + }, + "default_popup": "data/panel/panel.html", + "default_title": "__MSG_name__" + } +} diff --git a/package.json b/package.json index 63461bfe..16d43e8e 100644 --- a/package.json +++ b/package.json @@ -47,12 +47,18 @@ "url": "https://github.com/palant/easypasswords.git" }, "devDependencies": { + "browserify": "^13.0.0", "del": "^2.2.0", "gulp": "^3.9.1", "gulp-eslint": "^2.0.0", "gulp-htmlhint": "^0.3.1", + "gulp-json-modify": "^1.0.0", "gulp-less": "^3.0.5", "gulp-rename": "^1.2.2", - "jpm": "^1.0.6" + "gulp-zip": "^3.2.0", + "jpm": "^1.0.6", + "merge-stream": "^1.0.0", + "node-rsa": "^0.3.2", + "vinyl-source-stream": "^1.1.0" } }