Merge develop into cleanup

This commit is contained in:
Alan Friedman 2016-02-21 11:09:00 -05:00
commit e9b3498eaa
10 changed files with 393 additions and 62 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules
npm-debug.log
src/public/main.js
src/.secret

View File

@ -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).

View File

@ -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}.`);
});

226
src/js/darkwire.js Normal file
View File

@ -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
});
}
});
});
}
}

62
src/js/fileHandler.js Normal file
View File

@ -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;
}
}

View File

@ -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 = $('<span class="username"/>')
.text(data.username)
.css('color', getUsernameColor(data.username));
let $messageBodyDiv = $('<span class="messageBody">')
.html(data.message);
let $messageBodyDiv = $('<span class="messageBody">');
// 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 = $('<li class="message"/>')
.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);
});
}
});

View File

@ -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;

View File

@ -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;
}

View File

@ -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

View File

@ -55,6 +55,8 @@
</div>
<input class="inputMessage" placeholder="Type here..."/>
<div id="input-icons">
<span class="glyphicon glyphicon-file" id="send-file"></span>
<input type="file" id="fileInput">
<span class="glyphicon glyphicon-send" id="send-message-btn" aria-hidden="true"></span>
</div>
</li>