From c77b44e93c32c1ae1b3d34f1863942fd7a18c02b Mon Sep 17 00:00:00 2001 From: Guillaume Vincent Date: Sat, 13 May 2017 18:46:40 +0200 Subject: [PATCH] Use webcrypto only for browser * prepare injection of encryption functions (https://github.com/lesspass/lesspass/issues/223) * reduce bundle size by 80% --- package.json | 13 +++++++----- src/hmac.browser.js | 31 +++++++++++++++++++++++++++++ src/hmac.js | 9 +++++++++ src/lesspass.js | 8 ++------ src/pbkdf2.browser.js | 42 +++++++++++++++++++++++++++++++++++++++ src/pbkdf2.js | 55 ++++----------------------------------------------- src/v1.js | 8 ++------ test/hmac.tests.js | 21 ++++++++++++++++++++ 8 files changed, 119 insertions(+), 68 deletions(-) create mode 100644 src/hmac.browser.js create mode 100644 src/hmac.js create mode 100644 src/pbkdf2.browser.js create mode 100644 test/hmac.tests.js diff --git a/package.json b/package.json index e89d2cc..b675cd1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lesspass", - "version": "6.1.0", + "version": "7.0.0", "description": "LessPass node module used to generate LessPass passwords", "keywords": [ "crypto", @@ -13,8 +13,13 @@ "dist", "src" ], - "main": "dist/lesspass.min.js", + "main": "src/lesspass.js", + "browser": { + "./src/pbkdf2.js": "./src/pbkdf2.browser.js", + "./src/hmac.js": "./src/hmac.browser.js" + }, "module": "src/lesspass.js", + "jsnext:main": "src/lesspass.js", "repository": "lesspass/core", "scripts": { "precommit": "npm test && lint-staged", @@ -27,11 +32,9 @@ }, "dependencies": { "big-integer": "^1.6.22", - "buffer": "^5.0.6", - "create-hmac": "^1.1.4", "es6-promise": "^4.1.0", "lodash.assign": "^4.2.0", - "pbkdf2": "^3.0.9" + "unibabel": "^2.1.4" }, "devDependencies": { "browserify": "^14.3.0", diff --git a/src/hmac.browser.js b/src/hmac.browser.js new file mode 100644 index 0000000..382f12c --- /dev/null +++ b/src/hmac.browser.js @@ -0,0 +1,31 @@ +require("unibabel"); +require("unibabel/unibabel.hex"); + +module.exports = function(digest, string, salt) { + var algorithms = { + sha1: "SHA-1", + "sha-1": "SHA-1", + sha256: "SHA-256", + "sha-256": "SHA-256", + sha512: "SHA-512", + "sha-512": "SHA-512" + }; + return window.crypto.subtle + .importKey( + "raw", + Unibabel.utf8ToBuffer(string), + { + name: "HMAC", + hash: { name: algorithms[digest.toLowerCase()] } + }, + true, + ["sign", "verify"] + ) + .then(function(key) { + return window.crypto.subtle + .sign({ name: "HMAC" }, key, Unibabel.utf8ToBuffer(salt || "")) + .then(function(signature) { + return Unibabel.bufferToHex(new Uint8Array(signature)); + }); + }); +}; diff --git a/src/hmac.js b/src/hmac.js new file mode 100644 index 0000000..1a3f25e --- /dev/null +++ b/src/hmac.js @@ -0,0 +1,9 @@ +var crypto = require("crypto"); +var Promise = require("es6-promise").Promise; + +module.exports = function(digest, string, salt) { + return new Promise(function(resolve) { + var hmac = crypto.createHmac(digest, string); + resolve(hmac.update(salt || "").digest("hex")); + }); +}; diff --git a/src/lesspass.js b/src/lesspass.js index 6aea642..1b0adb6 100644 --- a/src/lesspass.js +++ b/src/lesspass.js @@ -1,8 +1,6 @@ var v1 = require("./v1"); var v2 = require("./v2"); -var createHMAC = require("create-hmac"); -var Buffer = require("buffer/").Buffer; -var Promise = require("es6-promise").Promise; +var hmac = require("./hmac"); module.exports = { generatePassword: function(site, login, masterPassword, options) { @@ -12,8 +10,6 @@ module.exports = { return v2.generatePassword(site, login, masterPassword, options); }, createFingerprint: function(str) { - return new Promise(function(resolve) { - resolve(createHMAC("sha256", new Buffer(str)).digest("hex")); - }); + return hmac("sha256", str); } }; diff --git a/src/pbkdf2.browser.js b/src/pbkdf2.browser.js new file mode 100644 index 0000000..c8f2181 --- /dev/null +++ b/src/pbkdf2.browser.js @@ -0,0 +1,42 @@ +require("unibabel"); +require("unibabel/unibabel.hex"); + +module.exports = function(password, salt, iterations, keylen, digest) { + var algorithms = { + sha1: "SHA-1", + "sha-1": "SHA-1", + sha256: "SHA-256", + "sha-256": "SHA-256", + sha512: "SHA-512", + "sha-512": "SHA-512" + }; + return window.crypto.subtle + .importKey("raw", Unibabel.utf8ToBuffer(password), "PBKDF2", false, [ + "deriveKey" + ]) + .then(function(key) { + var algo = { + name: "PBKDF2", + salt: Unibabel.utf8ToBuffer(salt), + iterations: iterations, + hash: algorithms[digest.toLowerCase()] + }; + return window.crypto.subtle.deriveKey( + algo, + key, + { + name: "AES-CTR", + length: keylen * 8 + }, + true, + ["encrypt", "decrypt"] + ); + }) + .then(function(derivedKey) { + return window.crypto.subtle + .exportKey("raw", derivedKey) + .then(function(keyArray) { + return Unibabel.bufferToHex(new Uint8Array(keyArray)); + }); + }); +}; diff --git a/src/pbkdf2.js b/src/pbkdf2.js index cba6f7f..4e2b1cc 100644 --- a/src/pbkdf2.js +++ b/src/pbkdf2.js @@ -1,54 +1,9 @@ -var pbkdf2 = require("pbkdf2"); -var Buffer = require("buffer/").Buffer; +const crypto = require("crypto"); var Promise = require("es6-promise").Promise; -function shouldUseNative() { - return !!(typeof window !== "undefined" && - window.crypto && - window.crypto.subtle); -} - -function pbkdf2WebCrypto(password, salt, iterations, keylen, digest) { - var algorithms = { - sha1: "SHA-1", - "sha-1": "SHA-1", - sha256: "SHA-256", - "sha-256": "SHA-256", - sha512: "SHA-512", - "sha-512": "SHA-512" - }; - return window.crypto.subtle - .importKey("raw", new Buffer(password), "PBKDF2", false, ["deriveKey"]) - .then(function(key) { - var algo = { - name: "PBKDF2", - salt: new Buffer(salt), - iterations: iterations, - hash: algorithms[digest.toLowerCase()] - }; - return window.crypto.subtle.deriveKey( - algo, - key, - { - name: "AES-CTR", - length: keylen * 8 - }, - true, - ["encrypt", "decrypt"] - ); - }) - .then(function(derivedKey) { - return window.crypto.subtle - .exportKey("raw", derivedKey) - .then(function(keyArray) { - return new Buffer(keyArray).toString("hex"); - }); - }); -} - -function pbkdf2Browserified(password, salt, iterations, keylen, digest) { +module.exports = function(password, salt, iterations, keylen, digest) { return new Promise(function(resolve, reject) { - pbkdf2.pbkdf2(password, salt, iterations, keylen, digest, function( + crypto.pbkdf2(password, salt, iterations, keylen, digest, function( error, key ) { @@ -59,6 +14,4 @@ function pbkdf2Browserified(password, salt, iterations, keylen, digest) { } }); }); -} - -module.exports = shouldUseNative() ? pbkdf2WebCrypto : pbkdf2Browserified; +}; diff --git a/src/v1.js b/src/v1.js index 93fe1bb..443ef3b 100644 --- a/src/v1.js +++ b/src/v1.js @@ -1,6 +1,6 @@ var pbkdf2 = require("./pbkdf2"); var assign = require("lodash.assign"); -var createHMAC = require("create-hmac"); +var hmac = require("./hmac"); module.exports = { generatePassword: generatePassword, @@ -58,11 +58,7 @@ function renderPassword(encryptedLogin, site, passwordOptions) { function createHmac(encryptedLogin, salt) { return new Promise(function(resolve) { - resolve( - createHMAC("sha256", new Buffer(encryptedLogin)) - .update(salt) - .digest("hex") - ); + resolve(hmac("sha256", encryptedLogin, salt)); }); } diff --git a/test/hmac.tests.js b/test/hmac.tests.js new file mode 100644 index 0000000..fc7cd18 --- /dev/null +++ b/test/hmac.tests.js @@ -0,0 +1,21 @@ +var assert = require("assert"); +var createHmac = require("../src/hmac"); + +describe("hmac", function() { + it("createHmac", function() { + return createHmac("sha256", "password").then(function(fingerprint) { + assert.equal( + "e56a207acd1e6714735487c199c6f095844b7cc8e5971d86c003a7b6f36ef51e", + fingerprint + ); + }); + }); + it("createHmac and update", function() { + return createHmac("sha256", "password", "salt").then(function(fingerprint) { + assert.equal( + "fc328232993ff34ca56631e4a101d60393cad12171997ee0b562bf7852b2fed0", + fingerprint + ); + }); + }); +});