diff --git a/.gitignore b/.gitignore
index f918f96..1c3273d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
node_modules
+npm-debug.log
src/public/main.js
src/.secret
diff --git a/readme.md b/readme.md
index 953bccd..253f612 100644
--- a/readme.md
+++ b/readme.md
@@ -46,7 +46,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..343660a 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: process.env.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..1b13949
--- /dev/null
+++ b/src/js/darkwire.js
@@ -0,0 +1,226 @@
+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 = [];
+ this._keys = {};
+ }
+
+ get keys() {
+ return this._keys;
+ }
+
+ set keys(keys) {
+ this._keys = keys;
+ return this._keys;
+ }
+
+ get connected() {
+ return this._connected;
+ }
+
+ set connected(state) {
+ this._connected = state;
+ return this._connected;
+ }
+
+ 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;
+ }
+
+ removeUser(data) {
+ this._users = _.without(this._users, _.findWhere(this._users, {id: data.id}));
+ return this._users;
+ }
+
+ encodeMessage(message, messageType) {
+ // Don't send unless other users exist
+ return new Promise((resolve, reject) => {
+ // if (this._users.length <= 1) {
+ // console.log('rejected:' + this._users);
+ // reject();
+ // return;
+ // };
+
+ // if there is a non-empty message and a socket connection
+ if (message && this._connected) {
+ let vector = this._cryptoUtil.crypto.getRandomValues(new Uint8Array(16));
+
+ let secretKey = null;
+ let secretKeys = null;
+ let messageData = null;
+ let signature = null;
+ let signingKey = null;
+ let encryptedMessageData = null;
+
+ // Generate new secret key and vector for each message
+ this._cryptoUtil.createSecretKey()
+ .then((key) => {
+ secretKey = key;
+ return this._cryptoUtil.createSigningKey();
+ })
+ .then((key) => {
+ signingKey = key;
+ // Generate secretKey and encrypt with each user's public key
+ let promises = [];
+ _.each(this._users, (user) => {
+ // If not me
+ if (user.username !== window.username) {
+ let promise = new Promise((res, rej) => {
+ let thisUser = user;
+
+ let secretKeyStr;
+
+ // Export secret key
+ this._cryptoUtil.exportKey(secretKey, 'raw')
+ .then((data) => {
+ return this._cryptoUtil.encryptSecretKey(data, thisUser.publicKey);
+ })
+ .then((encryptedSecretKey) => {
+ let encData = new Uint8Array(encryptedSecretKey);
+ secretKeyStr = this._cryptoUtil.convertArrayBufferViewToString(encData);
+ // Export HMAC signing key
+ return this._cryptoUtil.exportKey(signingKey, 'raw');
+ })
+ .then((data) => {
+ // Encrypt signing key with user's public key
+ return this._cryptoUtil.encryptSigningKey(data, thisUser.publicKey);
+ })
+ .then((encryptedSigningKey) => {
+ let encData = new Uint8Array(encryptedSigningKey);
+ var str = this._cryptoUtil.convertArrayBufferViewToString(encData);
+ res({
+ id: thisUser.id,
+ secretKey: secretKeyStr,
+ encryptedSigningKey: str
+ });
+ });
+ });
+ promises.push(promise);
+ }
+ });
+ return Promise.all(promises);
+ })
+ .then((data) => {
+ secretKeys = data;
+ messageData = this._cryptoUtil.convertStringToArrayBufferView(message);
+ return this._cryptoUtil.signKey(messageData, signingKey);
+ })
+ .then((data) => {
+ signature = data;
+ return this._cryptoUtil.encryptMessage(messageData, secretKey, vector);
+ })
+ .then((data) => {
+ encryptedMessageData = data;
+ let vct = this._cryptoUtil.convertArrayBufferViewToString(new Uint8Array(vector));
+ let sig = this._cryptoUtil.convertArrayBufferViewToString(new Uint8Array(signature));
+ let msg = this._cryptoUtil.convertArrayBufferViewToString(new Uint8Array(encryptedMessageData));
+
+ resolve({
+ message: msg,
+ vector: vct,
+ messageType: messageType,
+ 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, this._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, this._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,
+ messageType: data.messageType
+ });
+ }
+ });
+ });
+ }
+}
diff --git a/src/js/fileHandler.js b/src/js/fileHandler.js
new file mode 100644
index 0000000..509d955
--- /dev/null
+++ b/src/js/fileHandler.js
@@ -0,0 +1,62 @@
+export default class FileHandler {
+ constructor(darkwire, socket) {
+ if (window.File && window.FileReader && window.FileList && window.Blob && window.btoa && window.atob) {
+ this._isSupported = true;
+ this.darkwire = darkwire;
+ this.socket = socket;
+ 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);
+ this.darkwire.encodeMessage(base64, 'file').then((socketData) => {
+ this.socket.emit('new message', socketData);
+ });
+ };
+
+ reader.readAsBinaryString(file);
+ }
+
+ return false;
+ }
+
+ decodeFile(base64) {
+ return window.atob(base64);
+ }
+
+ listen() {
+ // browser API
+ document.getElementById('fileInput').addEventListener('change', this.encodeFile.bind(this), false);
+
+ // darkwire
+
+ return this;
+ }
+}
diff --git a/src/js/main.js b/src/js/main.js
index d308ed9..a890142 100644
--- a/src/js/main.js
+++ b/src/js/main.js
@@ -1,15 +1,13 @@
-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,19 +26,12 @@ $(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;
let roomId = window.location.pathname.length ? window.location.pathname : null;
- let keys = {};
-
if (!roomId) { return; }
$('input.share-text').val(document.location.protocol + '//' + document.location.host + roomId);
@@ -52,6 +43,7 @@ $(function() {
});
let socket = io(roomId);
+ const windowHandler = new WindowHandler(darkwire, socket);
FastClick.attach(document.body);
@@ -83,7 +75,7 @@ $(function() {
cryptoUtil.createPrimaryKeys()
])
.then(function(data) {
- keys = {
+ darkwire.keys = {
public: data[0].publicKey,
private: data[0].privateKey
};
@@ -130,10 +122,20 @@ $(function() {
let $usernameDiv = $('')
.text(data.username)
.css('color', getUsernameColor(data.username));
- let $messageBodyDiv = $('')
- .html(data.message);
+ let $messageBodyDiv = $('');
+ // TODO: Ask client if accept/reject attachment
+ // If reject, destroy object in memory
+ // If accept, render image or content dispose
+ if (dataType.file) {
+ let image = new Image();
+ image.src = `data:image/png;base64,${data.message}`;
+ $messageBodyDiv.html(image);
+ } else {
+ $messageBodyDiv.html(data.message);
+ }
let typingClass = data.typing ? 'typing' : '';
+
let $messageDiv = $('')
.data('username', data.username)
.addClass(typingClass)
@@ -197,7 +199,7 @@ $(function() {
// Updates the typing event
function updateTyping() {
- if (connected) {
+ if (darkwire.connected) {
if (!typing) {
typing = true;
socket.emit('typing');
@@ -239,7 +241,7 @@ $(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();
+ handleMessageSending();
socket.emit('stop typing');
typing = false;
}
@@ -280,47 +282,16 @@ $(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();
});
@@ -479,6 +450,28 @@ $(function() {
});
}
});
+
+ });
+
+ // Whenever the server emits 'new message', update the chat body
+ socket.on('new message', function(data) {
+ darkwire.decodeMessage(data).then((data) => {
+ if (!windowHandler.isActive) {
+ windowHandler.notifyFavicon();
+ darkwire.audio.play();
+ }
+ if (data.messageType === 'file') {
+ // let file = windowHandler.fileHandler.decodeFile(data.message);
+ // let chatMessage = {
+ // username: data.username,
+ // message: file
+ // }
+ addChatMessage(data, false, {file: true});
+ } else {
+ addChatMessage(data);
+ }
+ });
+
});
// Whenever the server emits 'user left', log it in the chat body
@@ -487,7 +480,7 @@ $(function() {
addParticipantsMessage(data);
removeChatTyping(data);
- users = _.without(users, _.findWhere(users, {id: data.id}));
+ darkwire.removeUser(data);
renderParticipantsList();
});
@@ -522,7 +515,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
@@ -536,7 +529,7 @@ $(function() {
}
$('#send-message-btn').click(function() {
- sendMessage();
+ handleMessageSending();
socket.emit('stop typing');
typing = false;
});
@@ -548,7 +541,24 @@ $(function() {
let audioSwitch = $('input.bs-switch').bootstrapSwitch();
audioSwitch.on('switchChange.bootstrapSwitch', function(event, state) {
- audio.soundEnabled = state;
+ darkwire.audio.soundEnabled = state;
});
+ function handleMessageSending() {
+ let message = $inputMessage;
+ let cleanedMessage = cleanInput(message.val());
+ // Prevent markup from being injected into the message
+ darkwire.encodeMessage(cleanedMessage, 'chat').then((socketData) => {
+ message.val('');
+ $('#send-message-btn').removeClass('active');
+ addChatMessage({
+ username: username,
+ message: cleanedMessage
+ });
+ socket.emit('new message', socketData);
+ }).catch((err) => {
+ console.log(err);
+ });
+ }
+
});
diff --git a/src/js/window.js b/src/js/window.js
index 784e4d4..db196c9 100644
--- a/src/js/window.js
+++ b/src/js/window.js
@@ -1,6 +1,9 @@
+import FileHandler from './fileHandler';
+
export default class WindowHandler {
- constructor() {
+ constructor(darkwire, socket) {
this._isActive = false;
+ this.fileHandler = new FileHandler(darkwire, socket);
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..873608f 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;
}
+
+#fileInput {
+ display: none;
+}
diff --git a/src/room.js b/src/room.js
index e10ac59..d8ea3bc 100644
--- a/src/room.js
+++ b/src/room.js
@@ -22,6 +22,8 @@ class Room {
username: socket.username,
id: socket.user.id,
message: data.message,
+ messageType: data.messageType,
+ data: data.data,
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 @@
+
+