diff --git a/readme.md b/readme.md index 061f78f..66fa446 100644 --- a/readme.md +++ b/readme.md @@ -42,7 +42,15 @@ Group chats work the same way because in step 5 we encrypt keys with everyone's Darkwire does not provide any guarantee that the person you're communicating with is who you think they are. Authentication functionality may be incorporated in future versions. -### Sockets & Server +## File Transfer + +Files are not transferred over the wire-only the file name and extension. Darkwire encodes documents into base64 using [btoa](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/btoa) and is encrypted the same way chat messages are. + +1. When a file is "uploaded", the document is encoded on the client and the server recieves the encrypted base64 string. +2. The server sends the encrypted base64 string to clients in the same chat room. +3. Clients recieving the encrypted base64 string then decrypts the string, then decodes the base64 string using [atob](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/atob). + +## Sockets & Server Darkwire uses [socket.io](http://socket.io) to transmit encrypted information using secure [WebSockets](https://en.wikipedia.org/wiki/WebSocket) (WSS). diff --git a/src/app.js b/src/app.js index 41bc917..898de97 100644 --- a/src/app.js +++ b/src/app.js @@ -10,7 +10,9 @@ import fs from 'fs'; import Room from './room'; -const $PORT = 3000; +const $CONFIG = { + port: 3000 +} const app = express(); const server = http.createServer(app); @@ -54,6 +56,6 @@ app.get('/:roomId', (req, res) => { return res.redirect('/'); }); -server.listen($PORT, () => { - console.log(`darkwire is online on port ${$PORT}.`); +server.listen($CONFIG.port, () => { + console.log(`darkwire is online on port ${$CONFIG.port}.`); }); diff --git a/src/js/darkwire.js b/src/js/darkwire.js new file mode 100644 index 0000000..e0c188c --- /dev/null +++ b/src/js/darkwire.js @@ -0,0 +1,200 @@ +import _ from 'underscore'; +import AudioHandler from './audio'; +import CryptoUtil from './crypto'; + +export default class Darkwire { + constructor() { + this._audio = new AudioHandler(); + this._cryptoUtil = new CryptoUtil(); + this._myUserId = false; + this._connected = false; + this._users = []; + } + + get users() { + return this._users; + } + + get audio() { + return this._audio; + } + + addUser(data) { + let importKeysPromises = []; + // Import all user keys if not already there + _.each(data.users, (user) => { + if (!_.findWhere(this._users, {id: user.id})) { + let promise = new Promise((resolve, reject) => { + let currentUser = user; + Promise.all([ + this._cryptoUtil.importPrimaryKey(currentUser.publicKey, "spki") + ]) + .then((keys) => { + this._users.push({ + id: currentUser.id, + username: currentUser.username, + publicKey: keys[0] + }); + resolve(); + }); + }); + + importKeysPromises.push(promise); + } + }); + + if (!this._myUserId) { + // Set my id if not already set + let me = _.findWhere(data.users, {username: username}); + this._myUserId = me.id; + } + + return importKeysPromises; + } + + sendMessage(message, messageType) { + // Don't send unless other users exist + console.log(this._users); + if (this._users.length <= 1) return; + + // if there is a non-empty message and a socket connection + if (message && this._connected) { + $inputMessage.val(''); + $('#send-message-btn').removeClass('active'); + addChatMessage({ + username: username, + message: message + }); + let vector = this._cryptoUtil.crypto.getRandomValues(new Uint8Array(16)); + + let secretKey; + let secretKeys; + let messageData; + let signature; + let signingKey; + let encryptedMessageData; + + // Generate new secret key and vector for each message + this._cryptoUtil.createSecretKey() + .then(function(key) { + secretKey = key; + return this._cryptoUtil.createSigningKey(); + }) + .then(function(key) { + signingKey = key; + // Generate secretKey and encrypt with each user's public key + let promises = []; + _.each(this._users, function(user) { + // If not me + if (user.username !== window.username) { + let promise = new Promise(function(resolve, reject) { + let thisUser = user; + + let secretKeyStr; + + // Export secret key + this._cryptoUtil.exportKey(secretKey, "raw") + .then(function(data) { + return this._cryptoUtil.encryptSecretKey(data, thisUser.publicKey); + }) + .then(function(encryptedSecretKey) { + let encData = new Uint8Array(encryptedSecretKey); + secretKeyStr = this._cryptoUtil.convertArrayBufferViewToString(encData); + // Export HMAC signing key + return this._cryptoUtil.exportKey(signingKey, "raw"); + }) + .then(function(data) { + // Encrypt signing key with user's public key + return this._cryptoUtil.encryptSigningKey(data, thisUser.publicKey); + }) + .then(function(encryptedSigningKey) { + let encData = new Uint8Array(encryptedSigningKey); + var str = this._cryptoUtil.convertArrayBufferViewToString(encData); + resolve({ + id: thisUser.id, + secretKey: secretKeyStr, + encryptedSigningKey: str + }); + }); + }); + promises.push(promise); + } + }); + return Promise.all(promises); + }) + .then(function(data) { + secretKeys = data; + messageData = this._cryptoUtil.convertStringToArrayBufferView(message); + return this._cryptoUtil.signKey(messageData, signingKey); + }) + .then(function(data) { + signature = data; + return this._cryptoUtil.encryptMessage(messageData, secretKey, vector); + }) + .then(function(data) { + encryptedMessageData = data; + let msg = this._cryptoUtil.convertArrayBufferViewToString(new Uint8Array(encryptedMessageData)); + let vct = this._cryptoUtil.convertArrayBufferViewToString(new Uint8Array(vector)); + let sig = this._cryptoUtil.convertArrayBufferViewToString(new Uint8Array(signature)); + socket.emit('new message', { + message: msg, + vector: vct, + messageType: type, + secretKeys: secretKeys, + signature: sig + }); + }); + } + } + + decodeMessage(data) { + return new Promise( (resolve, reject) => { + let message = data.message; + let messageData = this._cryptoUtil.convertStringToArrayBufferView(message); + let username = data.username; + let senderId = data.id + let vector = data.vector; + let vectorData = this._cryptoUtil.convertStringToArrayBufferView(vector); + let secretKeys = data.secretKeys; + let decryptedMessageData; + let decryptedMessage; + + let mySecretKey = _.find(secretKeys, (key) => { + return key.id === this._myUserId; + }); + let signature = data.signature; + let signatureData = this._cryptoUtil.convertStringToArrayBufferView(signature); + let secretKeyArrayBuffer = this._cryptoUtil.convertStringToArrayBufferView(mySecretKey.secretKey); + let signingKeyArrayBuffer = this._cryptoUtil.convertStringToArrayBufferView(mySecretKey.encryptedSigningKey); + + this._cryptoUtil.decryptSecretKey(secretKeyArrayBuffer, keys.private) + .then((data) => { + return this._cryptoUtil.importSecretKey(new Uint8Array(data), "raw"); + }) + .then((data) => { + let secretKey = data; + return this._cryptoUtil.decryptMessage(messageData, secretKey, vectorData); + }) + .then((data) => { + decryptedMessageData = data; + decryptedMessage = this._cryptoUtil.convertArrayBufferViewToString(new Uint8Array(data)) + return this._cryptoUtil.decryptSigningKey(signingKeyArrayBuffer, keys.private) + }) + .then((data) => { + return this._cryptoUtil.importSigningKey(new Uint8Array(data), "raw"); + }) + .then((data) => { + let signingKey = data; + return this._cryptoUtil.verifyKey(signatureData, decryptedMessageData, signingKey); + }) + .then((bool) => { + if (bool) { + resolve({ + username: username, + message: decryptedMessage + }); + } + }); + }); + } +} diff --git a/src/js/fileHandler.js b/src/js/fileHandler.js new file mode 100644 index 0000000..adb1744 --- /dev/null +++ b/src/js/fileHandler.js @@ -0,0 +1,56 @@ +export default class FileHandler { + constructor() { + if (window.File && window.FileReader && window.FileList && window.Blob && window.btoa) { + this._isSupported = true; + this.listen(); + } else { + this._isSupported = false; + } + } + + get isSupported() { + return this._isSupported; + } + + encodeFile(event) { + const file = event.target.files && event.target.files[0]; + + if (file) { + + let encodedFile = { + fileName: file.name, + fileSize: file.fileSize, + base64: null + }; + + // Support for only 1MB + if (file.size > 1000000) { + console.log(file); + alert("Max filesize is 1MB."); + return false; + } + + const reader = new FileReader(); + + reader.onload = (readerEvent) => { + const base64 = window.btoa(readerEvent.target.result); + encodedFile.base64 = base64; + } + + reader.readAsBinaryString(file); + + return base64; + } + + return false; + } + + listen() { + // browser API + document.getElementById('fileInput').addEventListener('change', this.encodeFile, false); + + // darkwire + + return this; + } +} diff --git a/src/js/main.js b/src/js/main.js index 100221e..042279c 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -1,15 +1,14 @@ -import AudioHandler from './audio'; -import CryptoUtil from './crypto'; +import Darkwire from './darkwire'; import WindowHandler from './window'; +import CryptoUtil from './crypto'; let fs = window.RequestFileSystem || window.webkitRequestFileSystem; $(function() { - const audio = new AudioHandler(); + const darkwire = new Darkwire(); const cryptoUtil = new CryptoUtil(); const windowHandler = new WindowHandler(); - let newMessages = 0; let FADE_TIME = 150; // ms let TYPING_TIMER_LENGTH = 400; // ms @@ -28,12 +27,7 @@ $(function() { let $chatPage = $('.chat.page'); // The chatroom page - let users = []; - - // Prompt for setting a username let username; - let myUserId; - let connected = false; let typing = false; let lastTypingTime; @@ -195,7 +189,7 @@ $(function() { // Updates the typing event function updateTyping () { - if (connected) { + if (darkwire.connected) { if (!typing) { typing = true; socket.emit('typing'); @@ -237,7 +231,8 @@ $(function() { $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(); + let message = cleanInput($inputMessage.val()); + darkwire.sendMessage(message, 'chat'); socket.emit('stop typing'); typing = false; } @@ -278,203 +273,34 @@ $(function() { // Whenever the server emits 'login', log the login message socket.on('user joined', function (data) { - connected = true; + darkwire.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") - ]) - .then(function(keys) { - users.push({ - id: currentUser.id, - username: currentUser.username, - publicKey: keys[0] - }); - resolve(); - }); - }); - importKeysPromises.push(promise); - } - }); - - if (!myUserId) { - // Set my id if not already set - let me = _.findWhere(data.users, {username: username}); - myUserId = me.id; - } - - Promise.all(importKeysPromises) - .then(function() { + let importKeysPromises = darkwire.addUser(data); + Promise.all(importKeysPromises).then(() => { // All users' keys have been imported if (data.numUsers === 1) { $('#first-modal').modal('show'); } log(data.username + ' joined'); - renderParticipantsList(); - }); + + }); }); - // Sends a chat message - function sendMessage () { - // Don't send unless other users exist - if (users.length <= 1) return; - - let message = $inputMessage.val(); - // Prevent markup from being injected into the message - message = cleanInput(message); - // if there is a non-empty message and a socket connection - if (message && connected) { - $inputMessage.val(''); - $('#send-message-btn').removeClass('active'); - addChatMessage({ - username: username, - message: message - }); - let vector = cryptoUtil.crypto.getRandomValues(new Uint8Array(16)); - - let secretKey; - let secretKeys; - let messageData; - let signature; - let signingKey; - let encryptedMessageData; - - // Generate new secret key and vector for each message - cryptoUtil.createSecretKey() - .then(function(key) { - secretKey = key; - return cryptoUtil.createSigningKey(); - }) - .then(function(key) { - signingKey = key; - // Generate secretKey and encrypt with each user's public key - let promises = []; - _.each(users, function(user) { - // If not me - if (user.username !== window.username) { - let promise = new Promise(function(resolve, reject) { - let thisUser = user; - - let secretKeyStr; - - // Export secret key - cryptoUtil.exportKey(secretKey, "raw") - .then(function(data) { - return cryptoUtil.encryptSecretKey(data, thisUser.publicKey); - }) - .then(function(encryptedSecretKey) { - let encData = new Uint8Array(encryptedSecretKey); - secretKeyStr = cryptoUtil.convertArrayBufferViewToString(encData); - // Export HMAC signing key - return cryptoUtil.exportKey(signingKey, "raw"); - }) - .then(function(data) { - // Encrypt signing key with user's public key - return cryptoUtil.encryptSigningKey(data, thisUser.publicKey); - }) - .then(function(encryptedSigningKey) { - let encData = new Uint8Array(encryptedSigningKey); - var str = cryptoUtil.convertArrayBufferViewToString(encData); - resolve({ - id: thisUser.id, - secretKey: secretKeyStr, - encryptedSigningKey: str - }); - }); - }); - promises.push(promise); - } - }); - return Promise.all(promises); - }) - .then(function(data) { - secretKeys = data; - messageData = cryptoUtil.convertStringToArrayBufferView(message); - return cryptoUtil.signKey(messageData, signingKey); - }) - .then(function(data) { - signature = data; - return cryptoUtil.encryptMessage(messageData, secretKey, vector); - }) - .then(function(data) { - encryptedMessageData = data; - let msg = cryptoUtil.convertArrayBufferViewToString(new Uint8Array(encryptedMessageData)); - let vct = cryptoUtil.convertArrayBufferViewToString(new Uint8Array(vector)); - let sig = cryptoUtil.convertArrayBufferViewToString(new Uint8Array(signature)); - socket.emit('new message', { - message: msg, - vector: vct, - secretKeys: secretKeys, - signature: sig - }); - }); - } - } - // Whenever the server emits 'new message', update the chat body socket.on('new message', function (data) { // Don't show messages if no key if (!windowHandler.isActive) { windowHandler.notifyFavicon(); - audio.play(); + darkwire.audio.play(); } - let message = data.message; - let messageData = cryptoUtil.convertStringToArrayBufferView(message); - 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 mySecretKey = _.find(secretKeys, function(key) { - return key.id === myUserId; - }); - let signature = data.signature; - let signatureData = cryptoUtil.convertStringToArrayBufferView(signature); - let secretKeyArrayBuffer = cryptoUtil.convertStringToArrayBufferView(mySecretKey.secretKey); - let signingKeyArrayBuffer = cryptoUtil.convertStringToArrayBufferView(mySecretKey.encryptedSigningKey); - - cryptoUtil.decryptSecretKey(secretKeyArrayBuffer, keys.private) - .then(function(data) { - return cryptoUtil.importSecretKey(new Uint8Array(data), "raw"); + let decoded = darkwire.decode(data); + decoded.then( (data) => { + console.log(data); }) - .then(function(data) { - let secretKey = data; - return cryptoUtil.decryptMessage(messageData, secretKey, vectorData); - }) - .then(function(data) { - decryptedMessageData = data; - decryptedMessage = cryptoUtil.convertArrayBufferViewToString(new Uint8Array(data)) - return cryptoUtil.decryptSigningKey(signingKeyArrayBuffer, keys.private) - }) - .then(function(data) { - return cryptoUtil.importSigningKey(new Uint8Array(data), "raw"); - }) - .then(function(data) { - let signingKey = data; - return cryptoUtil.verifyKey(signatureData, decryptedMessageData, signingKey); - }) - .then(function(bool) { - if (bool) { - addChatMessage({ - username: username, - message: decryptedMessage - }); - } - }); }); // Whenever the server emits 'user left', log it in the chat body @@ -518,7 +344,7 @@ $(function() { function renderParticipantsList() { $('#participants-modal ul.users').empty(); - _.each(users, function(user) { + _.each(darkwire.users, function(user) { let li; if (user.username === window.username) { // User is me @@ -532,7 +358,10 @@ $(function() { } $('#send-message-btn').click(function() { - sendMessage(); + let message = $inputMessage.val(); + // Prevent markup from being injected into the message + message = cleanInput(message); + darkwire.sendMessage(message, 'chat'); socket.emit('stop typing'); typing = false; }); @@ -544,7 +373,7 @@ $(function() { let audioSwitch = $('input.bs-switch').bootstrapSwitch(); audioSwitch.on('switchChange.bootstrapSwitch', function(event, state) { - audio.soundEnabled = state; + darkwire.audio.soundEnabled = state; }); }); diff --git a/src/js/window.js b/src/js/window.js index 88e217f..8aff85b 100644 --- a/src/js/window.js +++ b/src/js/window.js @@ -1,6 +1,9 @@ +import FileHandler from './fileHandler'; + export default class WindowHandler { constructor() { this._isActive = false; + this.fileHandler = new FileHandler(); this.newMessages = 0; this.favicon = new Favico({ @@ -8,6 +11,7 @@ export default class WindowHandler { type : 'rectangle' }); + this.enableFileTransfer(); this.bindEvents(); } @@ -25,6 +29,15 @@ export default class WindowHandler { this.favicon.badge(this.newMessages); } + enableFileTransfer() { + if (this.fileHandler.isSupported) { + $('#send-file').click((e) => { + e.preventDefault(); + $('#fileInput').trigger('click'); + }); + } + } + bindEvents() { window.onfocus = () => { this._isActive = true; diff --git a/src/public/style.css b/src/public/style.css index eb9beff..c8ea770 100644 --- a/src/public/style.css +++ b/src/public/style.css @@ -222,11 +222,12 @@ input { cursor: pointer; } +/* html.no-touchevents .chat #input-icons { display: none; -} +}*/ -.chat #input-icons #send-message-btn { +.chat #input-icons #send-message-btn, .chat #input-icons #send-file { font-size: 25px; opacity: 0.3; color: white; @@ -291,3 +292,7 @@ html.no-touchevents .chat #input-icons { background: #2a9fd6 !important; border-color: #2a9fd6 !important; } + +#fileSendForm { + display: none; +} diff --git a/src/room.js b/src/room.js index 221634a..1d17bcc 100644 --- a/src/room.js +++ b/src/room.js @@ -22,6 +22,7 @@ class Room { username: socket.username, id: socket.user.id, message: data.message, + messageType: data.messageType, vector: data.vector, secretKeys: data.secretKeys, signature: data.signature diff --git a/src/views/index.mustache b/src/views/index.mustache index 1d8c8ae..4d9f076 100644 --- a/src/views/index.mustache +++ b/src/views/index.mustache @@ -55,6 +55,8 @@
+ +