diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..af0f0c3 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} \ No newline at end of file diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..f223c2b --- /dev/null +++ b/.jscsrc @@ -0,0 +1,8 @@ +{ + "preset": "google", + "esnext": true, + "requireCamelCaseOrUpperCaseIdentifiers": {"ignoreProperties": true}, + "maxErrors": null, + "maximumLineLength": null, + "excludeFiles": ["src/public"] +} \ No newline at end of file diff --git a/.jshintignore b/.jshintignore new file mode 100644 index 0000000..b921a69 --- /dev/null +++ b/.jshintignore @@ -0,0 +1,2 @@ +node_modules +src/public \ No newline at end of file diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..53b202c --- /dev/null +++ b/.jshintrc @@ -0,0 +1,3 @@ +{ + "esversion": 6 +} \ No newline at end of file diff --git a/gulpfile.babel.js b/gulpfile.babel.js new file mode 100644 index 0000000..19130e2 --- /dev/null +++ b/gulpfile.babel.js @@ -0,0 +1,42 @@ +import gulp from 'gulp'; +import uglify from 'gulp-uglify'; +import nodemon from 'gulp-nodemon'; +import browserify from 'browserify'; +import babel from 'babelify'; +import source from 'vinyl-source-stream'; +import buffer from 'vinyl-buffer'; +import childProcess from 'child_process'; + +let spawn = childProcess.spawn; + +gulp.task('bundle', function() { + return browserify('src/js/main.js', { + debug: true + }).transform(babel.configure({ + presets: ['es2015'] + })).bundle() + .pipe(source('main.js')) + .pipe(buffer()) + .pipe(uglify()) + .pipe(gulp.dest('src/public')); +}); + +gulp.task('start', function() { + nodemon({ + script: 'index.js', + ext: 'css js mustache', + ignore: ['src/public/main.js'], + env: { + 'NODE_ENV': 'development' + }, + tasks: ['bundle'] + }); +}); + +gulp.task('test', function() { + let test = spawn( + 'mocha', + ['test', '--compilers', 'js:babel-core/register'], + {stdio: 'inherit'} + ); +}); diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index 1c27cdb..0000000 --- a/gulpfile.js +++ /dev/null @@ -1,29 +0,0 @@ -var gulp = require('gulp'); -var uglify = require('gulp-uglify'); -var nodemon = require('gulp-nodemon'); -var browserify = require('browserify'); -var babel = require('babelify'); -var source = require('vinyl-source-stream'); -var buffer = require('vinyl-buffer'); - -gulp.task('bundle', function() { - return browserify('src/js/main.js', { debug: true }).transform(babel.configure({ - presets: ["es2015"] - })).bundle() - .pipe(source('main.js')) - .pipe(buffer()) - .pipe(uglify()) - .pipe(gulp.dest('src/public')) -}); - -gulp.task('start', function() { - nodemon({ - script: 'index.js', - ext: 'css js mustache', - ignore: ['src/public/main.js'], - env: { - 'NODE_ENV': 'development' - }, - tasks: ['bundle'] - }) -}); diff --git a/index.js b/index.js index 5b76ff8..fa780d2 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,2 @@ - -require('babel/register'); -require('./src/app.js'); +require('babel-register')(); +require('./src/app.js'); diff --git a/package.json b/package.json index 6a9acad..f9b9512 100644 --- a/package.json +++ b/package.json @@ -19,17 +19,19 @@ "uuid": "^2.0.1" }, "devDependencies": { + "babel-core": "^6.5.2", "babel-preset-es2015": "^6.3.13", + "babel-register": "^6.5.2", "compression": "^1.6.0", "gulp": "^3.9.1", "gulp-nodemon": "^2.0.6", + "jscs": "^2.10.1", + "jshint": "^2.9.1", + "mocha-jscs": "^4.2.0", + "mocha-jshint": "^2.3.1", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0" }, - "scripts": { - "start": "gulp start", - "bundle": "gulp bundle" - }, "author": "Daniel Seripap", "license": "MIT" } diff --git a/readme.md b/readme.md index 061f78f..953bccd 100644 --- a/readme.md +++ b/readme.md @@ -3,6 +3,10 @@ Simple encrypted web chat. Powered by [socket.io](http://socket.io) and the [web cryptography API](https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto). ### Installation + + # For es6 compatability, be sure you have the latest stable version of Node JS installed + npm install -g n + n stable npm install diff --git a/src/js/crypto.js b/src/js/crypto.js index 45d6c3a..7b1a79c 100644 --- a/src/js/crypto.js +++ b/src/js/crypto.js @@ -7,7 +7,8 @@ export default class CryptoUtil { backdrop: 'static', show: false, keyboard: false - }) + }); + $('#no-crypto').modal('show'); return; } @@ -28,7 +29,7 @@ export default class CryptoUtil { } convertArrayBufferViewToString(buffer) { - let str = ""; + let str = ''; for (let i = 0; i < buffer.byteLength; i++) { str += String.fromCharCode(buffer[i]); } @@ -36,39 +37,39 @@ export default class CryptoUtil { return str; } - createSigningKey() { + createSigningKey() { return this._crypto.subtle.generateKey( { - name: "HMAC", - hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512" + name: 'HMAC', + hash: {name: 'SHA-256'}, //can be 'SHA-1', 'SHA-256', 'SHA-384', or 'SHA-512' //length: 256, //optional, if you want your key length to differ from the hash function's block length }, true, //whether the key is extractable (i.e. can be used in exportKey) - ["sign", "verify"] //can be any combination of "sign" and "verify" + ['sign', 'verify'] //can be any combination of 'sign' and 'verify' ); } createPrimaryKeys() { return this._crypto.subtle.generateKey( { - name: "RSA-OAEP", + name: 'RSA-OAEP', modulusLength: 2048, //can be 1024, 2048, or 4096 publicExponent: new Uint8Array([0x01, 0x00, 0x01]), - hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512" + hash: {name: 'SHA-256'}, //can be 'SHA-1', 'SHA-256', 'SHA-384', or 'SHA-512' }, true, //whether the key is extractable (i.e. can be used in exportKey) - ["encrypt", "decrypt"] //must be ["encrypt", "decrypt"] or ["wrapKey", "unwrapKey"] + ['encrypt', 'decrypt'] //must be ['encrypt', 'decrypt'] or ['wrapKey', 'unwrapKey'] ); } createSecretKey() { return this._crypto.subtle.generateKey( { - name: "AES-CBC", + name: 'AES-CBC', length: 256, //can be 128, 192, or 256 }, true, //whether the key is extractable (i.e. can be used in exportKey) - ["encrypt", "decrypt"] //can be "encrypt", "decrypt", "wrapKey", or "unwrapKey" + ['encrypt', 'decrypt'] //can be 'encrypt', 'decrypt', 'wrapKey', or 'unwrapKey' ); } @@ -76,24 +77,24 @@ export default class CryptoUtil { // Secret key will be recipient's public key return this._crypto.subtle.encrypt( { - name: "RSA-OAEP", + name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), - hash: {name: "SHA-256"} + hash: {name: 'SHA-256'} }, secretKey, data //ArrayBuffer of data you want to encrypt ); - } + } decryptSecretKey(data, key) { // key will be my private key return this._crypto.subtle.decrypt( { - name: "RSA-OAEP", + name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), - hash: {name: "SHA-256"} + hash: {name: 'SHA-256'} //label: Uint8Array([...]) //optional }, key, @@ -105,24 +106,24 @@ export default class CryptoUtil { // Secret key will be recipient's public key return this._crypto.subtle.encrypt( { - name: "RSA-OAEP", + name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), - hash: {name: "SHA-256"} + hash: {name: 'SHA-256'} }, signingKey, data //ArrayBuffer of data you want to encrypt ); - } + } decryptSigningKey(data, key) { // key will be my private key return this._crypto.subtle.decrypt( { - name: "RSA-OAEP", + name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), - hash: {name: "SHA-256"} + hash: {name: 'SHA-256'} //label: Uint8Array([...]) //optional }, key, @@ -133,7 +134,7 @@ export default class CryptoUtil { encryptMessage(data, secretKey, iv) { return this._crypto.subtle.encrypt( { - name: "AES-CBC", + name: 'AES-CBC', //Don't re-use initialization vectors! //Always generate a new iv every time your encrypt! iv: iv, @@ -146,65 +147,65 @@ export default class CryptoUtil { decryptMessage(data, secretKey, iv) { return this._crypto.subtle.decrypt( { - name: "AES-CBC", + name: 'AES-CBC', iv: iv, //The initialization vector you used to encrypt }, secretKey, //from generateKey or importKey above data //ArrayBuffer of the data - ); + ); } importSecretKey(jwkData, format) { return this._crypto.subtle.importKey( - format || "jwk", //can be "jwk" or "raw" - //this is an example jwk key, "raw" would be an ArrayBuffer + format || 'jwk', //can be 'jwk' or 'raw' + //this is an example jwk key, 'raw' would be an ArrayBuffer jwkData, { //this is the algorithm options - name: "AES-CBC", + name: 'AES-CBC', }, true, //whether the key is extractable (i.e. can be used in exportKey) - ["encrypt", "decrypt"] //can be "encrypt", "decrypt", "wrapKey", or "unwrapKey" + ['encrypt', 'decrypt'] //can be 'encrypt', 'decrypt', 'wrapKey', or 'unwrapKey' ); } importPrimaryKey(jwkData, format) { // Will be someone's public key let hashObj = { - name: "RSA-OAEP" + name: 'RSA-OAEP' }; if (!this._crypto.webkitSubtle) { - hashObj.hash = {name: "SHA-256"}; + hashObj.hash = {name: 'SHA-256'}; } return this._crypto.subtle.importKey( - format || "jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only) + format || 'jwk', //can be 'jwk' (public or private), 'spki' (public only), or 'pkcs8' (private only) jwkData, hashObj, true, //whether the key is extractable (i.e. can be used in exportKey) - ["encrypt"] //"encrypt" or "wrapKey" for public key import or - //"decrypt" or "unwrapKey" for private key imports + ['encrypt'] //'encrypt' or 'wrapKey' for public key import or + //'decrypt' or 'unwrapKey' for private key imports ); } exportKey(key, format) { // Will be public primary key or public signing key return this._crypto.subtle.exportKey( - format || "jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only) + format || 'jwk', //can be 'jwk' (public or private), 'spki' (public only), or 'pkcs8' (private only) key //can be a publicKey or privateKey, as long as extractable was true - ); + ); } importSigningKey(jwkData) { return this._crypto.subtle.importKey( - "raw", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only) + 'raw', //can be 'jwk' (public or private), 'spki' (public only), or 'pkcs8' (private only) //this is an example jwk key, other key types are Uint8Array objects jwkData, { //these are the algorithm options - name: "HMAC", - hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512" + name: 'HMAC', + hash: {name: 'SHA-256'}, //can be 'SHA-1', 'SHA-256', 'SHA-384', or 'SHA-512' //length: 256, //optional, if you want your key length to differ from the hash function's block length }, true, //whether the key is extractable (i.e. can be used in exportKey) - ["verify"] //"verify" for public key import, "sign" for private key imports + ['verify'] //'verify' for public key import, 'sign' for private key imports ); } @@ -212,26 +213,25 @@ export default class CryptoUtil { // Will use my private key return this._crypto.subtle.sign( { - name: "HMAC", - hash: {name: "SHA-256"} + name: 'HMAC', + hash: {name: 'SHA-256'} }, keyToSignWith, //from generateKey or importKey above data //ArrayBuffer of data you want to sign - ); + ); } verifyKey(signature, data, keyToVerifyWith) { // Will verify with sender's public key return this._crypto.subtle.verify( { - name: "HMAC", - hash: {name: "SHA-256"} + name: 'HMAC', + hash: {name: 'SHA-256'} }, keyToVerifyWith, //from generateKey or importKey above signature, //ArrayBuffer of the signature data //ArrayBuffer of the data - ); + ); } - } diff --git a/src/js/main.js b/src/js/main.js index 100221e..d308ed9 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -17,7 +17,7 @@ $(function() { '#e21400', '#ffe400', '#ff8f00', '#58dc00', '#dd9cff', '#4ae8c4', '#3b88eb', '#f47777', '#d300e7', - ]; + ]; let $window = $(window); let $messages = $('.messages'); // Messages area @@ -41,9 +41,9 @@ $(function() { let keys = {}; - if (!roomId) return; + if (!roomId) { return; } - $('input.share-text').val(document.location.protocol + "//" + document.location.host + roomId); + $('input.share-text').val(document.location.protocol + '//' + document.location.host + roomId); $('input.share-text').click(function() { $(this).focus(); @@ -55,7 +55,7 @@ $(function() { FastClick.attach(document.body); - function addParticipantsMessage (data) { + function addParticipantsMessage(data) { let message = ''; let headerMsg = ''; @@ -63,7 +63,7 @@ $(function() { } // Sets the client's username - function initChat () { + function initChat() { username = window.username; // warn not incognitor if (!fs) { @@ -71,7 +71,7 @@ $(function() { } else { fs(window.TEMPORARY, 100, - log.bind(log, "WARNING: Your browser is not in incognito mode!")); + log.bind(log, 'WARNING: Your browser is not in incognito mode!')); } // If the username is valid @@ -88,7 +88,7 @@ $(function() { private: data[0].privateKey }; return Promise.all([ - cryptoUtil.exportKey(data[0].publicKey, "spki") + cryptoUtil.exportKey(data[0].publicKey, 'spki') ]); }) .then(function(exportedKeys) { @@ -102,7 +102,7 @@ $(function() { } // Log a message - function log (message, options) { + function log(message, options) { let html = options && options.html === true || false; let $el; if (html) { @@ -114,8 +114,10 @@ $(function() { } // Adds the visual chat message to the message list - function addChatMessage (data, options) { - if (!data.message.trim().length) return; + function addChatMessage(data, options) { + if (!data.message.trim().length) { + return; + } // Don't fade the message in if there is an 'X was typing' let $typingMessages = getTypingMessages(data); @@ -141,15 +143,15 @@ $(function() { } // Adds the visual chat typing message - function addChatTyping (data) { + function addChatTyping(data) { data.typing = true; data.message = 'is typing'; addChatMessage(data); } // Removes the visual chat typing message - function removeChatTyping (data) { - getTypingMessages(data).fadeOut(function () { + function removeChatTyping(data) { + getTypingMessages(data).fadeOut(function() { $(this).remove(); }); } @@ -159,7 +161,7 @@ $(function() { // options.fade - If the element should fade-in (default = true) // options.prepend - If the element should prepend // all other messages (default = false) - function addMessageElement (el, options) { + function addMessageElement(el, options) { let $el = $(el); // Setup default options @@ -187,14 +189,14 @@ $(function() { } // Prevents input from having injected markup - function cleanInput (input) { + function cleanInput(input) { let message = $('
').html(input).text(); message = Autolinker.link(message); return message; } // Updates the typing event - function updateTyping () { + function updateTyping() { if (connected) { if (!typing) { typing = true; @@ -202,7 +204,7 @@ $(function() { } lastTypingTime = (new Date()).getTime(); - setTimeout(function () { + setTimeout(function() { let typingTimer = (new Date()).getTime(); let timeDiff = typingTimer - lastTypingTime; if (timeDiff >= TYPING_TIMER_LENGTH && typing) { @@ -214,18 +216,18 @@ $(function() { } // Gets the 'X is typing' messages of a user - function getTypingMessages (data) { - return $('.typing.message').filter(function (i) { + function getTypingMessages(data) { + return $('.typing.message').filter(function(i) { return $(this).data('username') === data.username; }); } // Gets the color of a username through our hash function - function getUsernameColor (username) { + function getUsernameColor(username) { // Compute hash code let hash = 7; for (let i = 0; i < username.length; i++) { - hash = username.charCodeAt(i) + (hash << 5) - hash; + hash = username.charCodeAt(i) + (hash << 5) - hash; } // Calculate color let index = Math.abs(hash % COLORS.length); @@ -234,7 +236,7 @@ $(function() { // Keyboard events - $window.keydown(function (event) { + $window.keydown(function(event) { // When the client hits ENTER on their keyboard and chat message input is focused if (event.which === 13 && $('.inputMessage').is(':focus')) { sendMessage(); @@ -255,8 +257,8 @@ $(function() { }); // Select message input when closing modal - $('.modal').on('hidden.bs.modal', function (e) { - $inputMessage.focus(); + $('.modal').on('hidden.bs.modal', function(e) { + $inputMessage.focus(); }); // Select message input when clicking message body, unless selecting text @@ -267,29 +269,29 @@ $(function() { }); function getSelectedText() { - var text = ""; - if (typeof window.getSelection != "undefined") { + var text = ''; + if (typeof window.getSelection != 'undefined') { text = window.getSelection().toString(); - } else if (typeof document.selection != "undefined" && document.selection.type == "Text") { + } else if (typeof document.selection != 'undefined' && document.selection.type == 'Text') { text = document.selection.createRange().text; } return text; } // Whenever the server emits 'login', log the login message - socket.on('user joined', function (data) { + socket.on('user joined', function(data) { connected = true; addParticipantsMessage(data); let importKeysPromises = []; - + // Import all user keys if not already there _.each(data.users, function(user) { if (!_.findWhere(users, {id: user.id})) { let promise = new Promise(function(resolve, reject) { let currentUser = user; Promise.all([ - cryptoUtil.importPrimaryKey(currentUser.publicKey, "spki") + cryptoUtil.importPrimaryKey(currentUser.publicKey, 'spki') ]) .then(function(keys) { users.push({ @@ -320,14 +322,16 @@ $(function() { log(data.username + ' joined'); renderParticipantsList(); - }); + }); }); // Sends a chat message - function sendMessage () { + function sendMessage() { // Don't send unless other users exist - if (users.length <= 1) return; + if (users.length <= 1) { + return; + } let message = $inputMessage.val(); // Prevent markup from being injected into the message @@ -335,7 +339,7 @@ $(function() { // if there is a non-empty message and a socket connection if (message && connected) { $inputMessage.val(''); - $('#send-message-btn').removeClass('active'); + $('#send-message-btn').removeClass('active'); addChatMessage({ username: username, message: message @@ -368,7 +372,7 @@ $(function() { let secretKeyStr; // Export secret key - cryptoUtil.exportKey(secretKey, "raw") + cryptoUtil.exportKey(secretKey, 'raw') .then(function(data) { return cryptoUtil.encryptSecretKey(data, thisUser.publicKey); }) @@ -376,7 +380,7 @@ $(function() { let encData = new Uint8Array(encryptedSecretKey); secretKeyStr = cryptoUtil.convertArrayBufferViewToString(encData); // Export HMAC signing key - return cryptoUtil.exportKey(signingKey, "raw"); + return cryptoUtil.exportKey(signingKey, 'raw'); }) .then(function(data) { // Encrypt signing key with user's public key @@ -422,7 +426,7 @@ $(function() { } // Whenever the server emits 'new message', update the chat body - socket.on('new message', function (data) { + socket.on('new message', function(data) { // Don't show messages if no key if (!windowHandler.isActive) { windowHandler.notifyFavicon(); @@ -431,13 +435,13 @@ $(function() { let message = data.message; let messageData = cryptoUtil.convertStringToArrayBufferView(message); - let username = data.username; - let senderId = data.id + let username = data.username; + let senderId = data.id; let vector = data.vector; let vectorData = cryptoUtil.convertStringToArrayBufferView(vector); let secretKeys = data.secretKeys; let decryptedMessageData; - let decryptedMessage; + let decryptedMessage; let mySecretKey = _.find(secretKeys, function(key) { return key.id === myUserId; @@ -449,7 +453,7 @@ $(function() { cryptoUtil.decryptSecretKey(secretKeyArrayBuffer, keys.private) .then(function(data) { - return cryptoUtil.importSecretKey(new Uint8Array(data), "raw"); + return cryptoUtil.importSecretKey(new Uint8Array(data), 'raw'); }) .then(function(data) { let secretKey = data; @@ -457,11 +461,11 @@ $(function() { }) .then(function(data) { decryptedMessageData = data; - decryptedMessage = cryptoUtil.convertArrayBufferViewToString(new Uint8Array(data)) - return cryptoUtil.decryptSigningKey(signingKeyArrayBuffer, keys.private) + decryptedMessage = cryptoUtil.convertArrayBufferViewToString(new Uint8Array(data)); + return cryptoUtil.decryptSigningKey(signingKeyArrayBuffer, keys.private); }) .then(function(data) { - return cryptoUtil.importSigningKey(new Uint8Array(data), "raw"); + return cryptoUtil.importSigningKey(new Uint8Array(data), 'raw'); }) .then(function(data) { let signingKey = data; @@ -472,13 +476,13 @@ $(function() { addChatMessage({ username: username, message: decryptedMessage - }); + }); } }); }); // Whenever the server emits 'user left', log it in the chat body - socket.on('user left', function (data) { + socket.on('user left', function(data) { log(data.username + ' left'); addParticipantsMessage(data); removeChatTyping(data); @@ -489,12 +493,12 @@ $(function() { }); // Whenever the server emits 'typing', show the typing message - socket.on('typing', function (data) { + socket.on('typing', function(data) { addChatTyping(data); }); // Whenever the server emits 'stop typing', kill the typing message - socket.on('stop typing', function (data) { + socket.on('stop typing', function(data) { removeChatTyping(data); }); @@ -522,13 +526,13 @@ $(function() { let li; if (user.username === window.username) { // User is me - li = $("