Support for encrypted file transfer

Accept file, blob decoding

Remove double init of window handler

Added confirmation/acceptance message

Add lazy IDs to transferred files for file owner

Added chat class- initial support for slash commands

Abstraction of chat to its own class

Removed underscore from vendors, switching to import. Increased username color values

Not localizing username, organizing slash commands

Keeping context

Support for symbols/emojis. Fixes #9

Added back npm scripts, added method to check if log messages contain usernames

Checks and balances

Better parsing of commands and organization of valid commands

Fixed #10 - Added running version on modal and about section, Updated disclaimer/wording, displaying public IP if available through server

File transfer pre-confirmation

Encrypting stringified object versus string
This commit is contained in:
Dan Seripap 2016-02-22 09:16:19 -05:00
parent 078d10d177
commit 4cee744b07
13 changed files with 696 additions and 283 deletions

View File

@ -21,6 +21,17 @@ gulp.task('bundle', function() {
.pipe(gulp.dest('src/public')); .pipe(gulp.dest('src/public'));
}); });
gulp.task('dev', function() {
return browserify('src/js/main.js', {
debug: true
}).transform(babel.configure({
presets: ['es2015']
})).bundle()
.pipe(source('main.js'))
.pipe(buffer())
.pipe(gulp.dest('src/public'));
});
gulp.task('start', function() { gulp.task('start', function() {
nodemon({ nodemon({
script: 'index.js', script: 'index.js',
@ -29,7 +40,7 @@ gulp.task('start', function() {
env: { env: {
'NODE_ENV': 'development' 'NODE_ENV': 'development'
}, },
tasks: ['bundle'] tasks: ['dev']
}); });
}); });

View File

@ -11,9 +11,12 @@
"express": "^4.13.3", "express": "^4.13.3",
"gulp": "^3.9.0", "gulp": "^3.9.0",
"gulp-uglify": "^1.5.1", "gulp-uglify": "^1.5.1",
"moment": "^2.11.2",
"mustache-express": "^1.2.2", "mustache-express": "^1.2.2",
"sanitize-html": "^1.11.3",
"serve-favicon": "^2.3.0", "serve-favicon": "^2.3.0",
"shortid": "^2.2.4", "shortid": "^2.2.4",
"slug": "^0.9.1",
"socket.io": "^1.4.0", "socket.io": "^1.4.0",
"underscore": "^1.8.3", "underscore": "^1.8.3",
"uuid": "^2.0.1" "uuid": "^2.0.1"
@ -32,6 +35,11 @@
"vinyl-buffer": "^1.0.0", "vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0" "vinyl-source-stream": "^1.1.0"
}, },
"scripts": {
"start": "gulp start",
"bundle": "gulp bundle",
"test": "gulp test"
},
"author": "Daniel Seripap", "author": "Daniel Seripap",
"license": "MIT" "license": "MIT"
} }

View File

@ -3,20 +3,17 @@
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). 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 ### Installation
# For es6 compatability, be sure you have the latest stable version of Node JS installed # For es6 compatability, be sure you have the latest stable version of Node JS installed
npm install -g n npm install -g n
n stable n stable
npm install npm install
# Bundle JS files # Bundle JS files (for deployment)
npm run bundle npm bundle
# Start a local instance of darkwire # Start a local instance of darkwire
npm start npm start
Create a **.secret** file in **/src** folder with a your session secret. It doesn't matter what it is- just keep it private. Create a **.secret** file in the **/src** folder with a your session secret. It doesn't matter what it is- just keep it private.
Darkwire is now running on `http://localhost:3000` Darkwire is now running on `http://localhost:3000`
@ -48,11 +45,11 @@ Darkwire does not provide any guarantee that the person you're communicating wit
## File Transfer ## 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. Darkwire encodes documents (up to 1MB) 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. 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. 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). 3. Clients recieving the encrypted base64 string then decrypts and decodes the base64 string using [atob](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/atob).
## Sockets & Server ## Sockets & Server

View File

@ -50,7 +50,13 @@ app.get('/:roomId', (req, res) => {
let roomExists = _.findWhere(rooms, {_id: roomId}) || false; let roomExists = _.findWhere(rooms, {_id: roomId}) || false;
if (roomExists) { if (roomExists) {
return res.render('index', {username: shortid.generate()}); return res.render('index', {
APP: {
version: process.env.npm_package_version,
ip: req.headers['x-forwarded-for']
},
username: shortid.generate()
});
} }
return res.redirect('/'); return res.redirect('/');

307
src/js/chat.js Normal file
View File

@ -0,0 +1,307 @@
import _ from 'underscore';
import sanitizeHtml from 'sanitize-html';
export default class Chat {
constructor(darkwire, socket) {
this.usernamesInMemory = [];
this.FADE_TIME = 150; // ms
this.TYPING_TIMER_LENGTH = 400; // ms
this.typing = false;
this.lastTypingTime = null;
this.darkwire = darkwire;
this.socket = socket;
this.messages = $('.messages'); // Messages area
this.inputMessage = $('.inputMessage'); // Input message input box
this.chatPage = $('.chat.page');
this.bindEvents();
}
// Log a message
log(message, options) {
let html = options && options.html === true || false;
let $el;
let matchedUsernames = this.checkIfUsername(message.split(' '));
if (matchedUsernames.length > 0) {
for (let i = 0; i < matchedUsernames.length; i++) {
let usernameContainer = $('<span/>')
.text(matchedUsernames[i])
.css('color', this.getUsernameColor(matchedUsernames[i]));
message = message.replace(matchedUsernames[i], usernameContainer.prop('outerHTML'));
}
}
if (options && options.error) {
$el = $('<li class="log-error">').addClass('log').html(message);
} else if (options && options.info) {
$el = $('<li class="log-info">').addClass('log').html(message);
} else {
$el = $('<li>').addClass('log').html(message);
}
this.addMessageElement($el, options);
}
checkIfUsername(words) {
let matchedUsernames = [];
this.darkwire.users.forEach((user) => {
let usernameMatch = new RegExp('^' + user.username + '$', 'g');
for (let i = 0; i < words.length; i++) {
let exactMatch = words[i].match(usernameMatch) || false;
let usernameInMemory = this.usernamesInMemory.indexOf(words[i]) > -1;
if (exactMatch && exactMatch.length > -1 || usernameInMemory) {
if (!usernameInMemory) {
this.usernamesInMemory.push(words[i]);
}
matchedUsernames.push(words[i]);
}
}
});
return matchedUsernames;
}
// Gets the color of a username through our hash function
getUsernameColor(username) {
const COLORS = [
'#e21400', '#ffe400', '#ff8f00',
'#58dc00', '#dd9cff', '#4ae8c4',
'#3b88eb', '#f47777', '#d300e7',
'#99FF33', '#99CC33', '#999933',
'#996633', '#993333', '#990033',
];
// Compute hash code
let hash = 7;
for (let i = 0; i < username.length; i++) {
hash = username.charCodeAt(i) + (hash << 5) - hash;
}
// Calculate color
let index = Math.abs(hash % COLORS.length);
return COLORS[index];
}
bindEvents() {
var _this = this;
// Select message input when clicking message body, unless selecting text
this.messages.on('click', () => {
if (!this.getSelectedText()) {
this.inputMessage.focus();
}
});
this.inputMessage.on('input propertychange paste change', function() {
_this.updateTyping();
let message = $(this).val().trim();
if (message.length) {
$('#send-message-btn').addClass('active');
} else {
$('#send-message-btn').removeClass('active');
}
});
}
// Updates the typing event
updateTyping() {
if (this.darkwire.connected) {
if (!this.typing) {
this.typing = true;
this.socket.emit('typing');
}
this.lastTypingTime = (new Date()).getTime();
setTimeout(() => {
let typingTimer = (new Date()).getTime();
let timeDiff = typingTimer - this.lastTypingTime;
if (timeDiff >= this.TYPING_TIMER_LENGTH && this.typing) {
this.socket.emit('stop typing');
this.typing = false;
}
}, this.TYPING_TIMER_LENGTH);
}
}
addChatTyping(data) {
data.typing = true;
data.message = 'is typing';
this.addChatMessage(data);
}
getSelectedText() {
let text = '';
if (typeof window.getSelection != 'undefined') {
text = window.getSelection().toString();
} else if (typeof document.selection != 'undefined' && document.selection.type == 'Text') {
text = document.selection.createRange().text;
}
return text;
}
getTypingMessages(data) {
return $('.typing.message').filter(function(i) {
return $(this).data('username') === data.username;
});
}
removeChatTyping(data) {
this.getTypingMessages(data).fadeOut(function() {
$(this).remove();
});
}
slashCommands(trigger) {
let validCommands = [];
let expectedParams = 0;
const triggerCommands = [{
command: 'nick',
description: 'Changes nickname.',
paramaters: ['{username}'],
usage: '/nick {username}',
action: () => {
let newUsername = trigger.params[0] || false;
if (newUsername > 16) {
return this.log('Username cannot be greater than 16 characters.', {error: true});
}
// Remove things that arent digits or chars
newUsername = newUsername.replace(/[^A-Za-z0-9]/g, '-');
if (!newUsername.match(/^[A-Z0-9]/i)) {
return this.log('Username must start with a letter or number.', {error: true});
}
this.darkwire.updateUsername(newUsername).then((socketData) => {
let modifiedSocketData = {
username: window.username,
newUsername: socketData.username,
publicKey: socketData.publicKey
};
this.socket.emit('update user', modifiedSocketData);
window.username = username = socketData.username;
});
}
}, {
command: 'help',
description: 'Shows a list of commands.',
paramaters: [],
usage: '/help',
action: () => {
validCommands = validCommands.map((command) => {
return '/' + command;
});
this.log('Valid commands: ' + validCommands.join(', '), {info: true});
}
}];
const color = () => {
const hexTex = new RegExp(/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i);
};
triggerCommands.forEach((command) => {
validCommands.push(command.command);
});
let commandToTrigger = _.findWhere(triggerCommands, {command: trigger.command});
if (commandToTrigger) {
expectedParams = commandToTrigger.paramaters.length;
if (trigger.params.length > expectedParams || trigger.params.length < expectedParams) {
return this.log('Missing or too many paramater. Usage: ' + commandToTrigger.usage, {error: true});
}
return commandToTrigger.action.call();
}
this.log(trigger.command + ' is not a valid command. Type /help for a list of valid commands.', {error: true});
return false;
}
executeCommand(trigger) {
trigger = trigger || false;
if (trigger) {
let command = trigger.command;
this.slashCommands(trigger);
}
}
parseCommand(cleanedMessage) {
let trigger = {
command: null,
params: []
};
if (cleanedMessage.indexOf('/') === 0) {
this.inputMessage.val('');
let parsedCommand = cleanedMessage.replace('/', '').split(' ');
trigger.command = sanitizeHtml(parsedCommand[0]) || null;
// Get params
if (parsedCommand.length >= 2) {
for (let i = 1; i < parsedCommand.length; i++) {
trigger.params.push(parsedCommand[i]);
}
}
return trigger;
}
return false;
}
addChatMessage(data, options) {
if (!data.message.trim().length) {
return;
}
let messageType = data.messageType || 'text';
// Don't fade the message in if there is an 'X was typing'
let $typingMessages = this.getTypingMessages(data);
options = options || {};
if ($typingMessages.length !== 0) {
options.fade = false;
$typingMessages.remove();
}
let $usernameDiv = $('<span class="username"/>')
.text(data.username)
.css('color', this.getUsernameColor(data.username));
let $messageBodyDiv = $('<span class="messageBody">');
if (messageType !== 'text') {
$messageBodyDiv.html(this.darkwire.addFileToQueue(data));
} else {
$messageBodyDiv.html(unescape(data.message));
}
let typingClass = data.typing ? 'typing' : '';
let $messageDiv = $('<li class="message"/>')
.data('username', data.username)
.addClass(typingClass)
.append($usernameDiv, $messageBodyDiv);
this.addMessageElement($messageDiv, options);
}
addMessageElement(el, options) {
let $el = $(el);
if (!options) {
options = {};
}
$el.hide().fadeIn(this.FADE_TIME);
this.messages.append($el);
this.messages[0].scrollTop = this.messages[0].scrollHeight; // minus 60 for key
}
replaceMessage(id, message) {
let container = $(id);
if (container) {
container.html(message);
}
}
}

View File

@ -9,7 +9,33 @@ export default class Darkwire {
this._myUserId = false; this._myUserId = false;
this._connected = false; this._connected = false;
this._users = []; this._users = [];
this._fileQueue = [];
this._keys = {}; this._keys = {};
this._autoEmbedImages = false;
}
getFile(id) {
let file = _.findWhere(this._fileQueue, {id: id}) || false;
if (file) {
// TODO: Destroy object from memory when retrieved
}
return file.data;
}
set fileQueue(fileQueue) {
this._fileQueue = fileQueue;
return this;
}
get autoEmbedImages() {
return this._autoEmbedImages;
}
set autoEmbedImages(state) {
this._autoEmbedImages = state;
return this._autoEmbedImages;
} }
get keys() { get keys() {
@ -76,9 +102,35 @@ export default class Darkwire {
return this._users; return this._users;
} }
encodeMessage(message, messageType) { updateUsername(username) {
return new Promise((resolve, reject) => {
if (username) {
Promise.all([
this._cryptoUtil.createPrimaryKeys()
])
.then((data) => {
this._keys = {
public: data[0].publicKey,
private: data[0].privateKey
};
return Promise.all([
this._cryptoUtil.exportKey(data[0].publicKey, 'spki')
]);
})
.then((exportedKeys) => {
resolve({
username: username,
publicKey: exportedKeys[0]
});
});
}
});
}
encodeMessage(message, messageType, additionalData) {
// Don't send unless other users exist // Don't send unless other users exist
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
additionalData = additionalData || {};
// if (this._users.length <= 1) { // if (this._users.length <= 1) {
// console.log('rejected:' + this._users); // console.log('rejected:' + this._users);
// reject(); // reject();
@ -95,7 +147,12 @@ export default class Darkwire {
let signature = null; let signature = null;
let signingKey = null; let signingKey = null;
let encryptedMessageData = null; let encryptedMessageData = null;
let messageToEncode = {
text: escape(message),
additionalData: additionalData
};
messageToEncode = JSON.stringify(messageToEncode);
// Generate new secret key and vector for each message // Generate new secret key and vector for each message
this._cryptoUtil.createSecretKey() this._cryptoUtil.createSecretKey()
.then((key) => { .then((key) => {
@ -146,7 +203,7 @@ export default class Darkwire {
}) })
.then((data) => { .then((data) => {
secretKeys = data; secretKeys = data;
messageData = this._cryptoUtil.convertStringToArrayBufferView(message); messageData = this._cryptoUtil.convertStringToArrayBufferView(messageToEncode);
return this._cryptoUtil.signKey(messageData, signingKey); return this._cryptoUtil.signKey(messageData, signingKey);
}) })
.then((data) => { .then((data) => {
@ -202,7 +259,7 @@ export default class Darkwire {
}) })
.then((data) => { .then((data) => {
decryptedMessageData = data; decryptedMessageData = data;
decryptedMessage = this._cryptoUtil.convertArrayBufferViewToString(new Uint8Array(data)); decryptedMessage = JSON.parse(this._cryptoUtil.convertArrayBufferViewToString(new Uint8Array(data)));
return this._cryptoUtil.decryptSigningKey(signingKeyArrayBuffer, this._keys.private); return this._cryptoUtil.decryptSigningKey(signingKeyArrayBuffer, this._keys.private);
}) })
.then((data) => { .then((data) => {
@ -217,10 +274,29 @@ export default class Darkwire {
resolve({ resolve({
username: username, username: username,
message: decryptedMessage, message: decryptedMessage,
messageType: data.messageType messageType: data.messageType,
timestamp: data.timestamp
}); });
} }
}); });
}); });
} }
generateMessage(fileId, fileName, messageType) {
let message = '<div id="file-transfer-request-' + fileId + '">is attempting to send you ' + fileName + ' (' + messageType + ')';
message += '<br><small class="file-disclaimer"><strong>WARNING: We cannot strictly verify the integrity of this file, its recipients or its owners. By accepting this file, you are liable for any risks that may arise from reciving this file.</strong></small>';
message += '<br><a onclick="triggerFileDownload(this);" data-file="' + fileId + '">Accept File</a></div>';
return message;
}
addFileToQueue(data) {
let fileData = {
id: data.additionalData.fileId,
data: data
};
this._fileQueue.push(fileData);
return this.generateMessage(data.additionalData.fileId, data.additionalData.fileName, data.messageType);
}
} }

View File

@ -1,9 +1,14 @@
import _ from 'underscore';
import uuid from 'uuid';
export default class FileHandler { export default class FileHandler {
constructor(darkwire, socket) { constructor(darkwire, socket, chat) {
if (window.File && window.FileReader && window.FileList && window.Blob && window.btoa && window.atob) { this.localFileQueue = [];
if (window.File && window.FileReader && window.FileList && window.Blob && window.btoa && window.atob && window.Blob && window.URL) {
this._isSupported = true; this._isSupported = true;
this.darkwire = darkwire; this.darkwire = darkwire;
this.socket = socket; this.socket = socket;
this.chat = chat;
this.listen(); this.listen();
} else { } else {
this._isSupported = false; this._isSupported = false;
@ -14,16 +19,22 @@ export default class FileHandler {
return this._isSupported; return this._isSupported;
} }
encodeFile(event) { set isSupported(state) {
this._isSupported = state;
return this;
}
confirmTransfer(event) {
const validFileTypes = ['png','jpg','jpeg','gif','zip','rar','gzip','pdf','txt','json','doc','docx'];
const file = event.target.files && event.target.files[0]; const file = event.target.files && event.target.files[0];
if (file) { if (file) {
const fileExt = file.name.split('.').pop().toLowerCase();
// let encodedFile = { if (validFileTypes.indexOf(fileExt) <= -1) {
// fileName: file.name, alert('file type not supported');
// fileSize: file.fileSize, return false;
// base64: null }
// };
// Support for only 1MB // Support for only 1MB
if (file.size > 1000000) { if (file.size > 1000000) {
@ -31,32 +42,98 @@ export default class FileHandler {
alert('Max filesize is 1MB.'); alert('Max filesize is 1MB.');
return false; return false;
} }
let fileId = uuid.v4();
const reader = new FileReader(); let confirmMessage = '<span id="transfer-' + fileId + '">You are about to send ' + file.name + ' to all parties in this chat. <a onclick="triggerFileTransfer(this);" data-file="' + fileId + '">Confirm</a> | <a onclick="triggerFileDestroy(this)" data-file="' + fileId + '">Cancel</a></span>';
let fileData = {
reader.onload = (readerEvent) => { id: fileId,
const base64 = window.btoa(readerEvent.target.result); file: file
this.darkwire.encodeMessage(base64, 'file').then((socketData) => {
this.socket.emit('new message', socketData);
});
}; };
this.localFileQueue.push(fileData);
this.chat.addChatMessage({
username: username,
message: confirmMessage
});
this.filesSent++;
reader.readAsBinaryString(file);
} }
return false; return false;
} }
decodeFile(base64) { encodeFile(fileId) {
return window.atob(base64); const fileData = _.findWhere(this.localFileQueue, {id: fileId});
const file = fileData.file || false;
if (file) {
// TODO: Remove file from local queue
} else {
return false;
}
const reader = new FileReader();
const fileType = file.type || 'file';
reader.onload = (readerEvent) => {
const base64 = window.btoa(readerEvent.target.result);
const additionalData = {
fileId: fileId,
fileName: file.name
};
this.darkwire.encodeMessage(base64, fileType, additionalData).then((socketData) => {
this.chat.replaceMessage('#transfer-' + fileId, 'Sent ' + file.name);
this.socket.emit('new message', socketData);
});
this.resetInput();
};
reader.readAsBinaryString(file);
}
destroyFile(fileId) {
const file = _.findWhere(this.localFileQueue, {id: fileId});
this.localFileQueue = _.without(this.localFileQueue, file);
return this.chat.replaceMessage('#transfer-' + fileId, 'The file transfer for ' + file.file.name + ' has been canceled.');
}
createBlob(base64, fileType) {
base64 = unescape(base64);
return new Promise((resolve, reject) => {
const sliceSize = 1024;
let byteCharacters = window.atob(base64);
let byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
let slice = byteCharacters.slice(offset, offset + sliceSize);
let byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
let byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
resolve(new window.Blob(byteArrays, {type: fileType}));
});
}
createUrlFromBlob(blob) {
return window.URL.createObjectURL(blob);
} }
listen() { listen() {
// browser API // browser API
document.getElementById('fileInput').addEventListener('change', this.encodeFile.bind(this), false); document.getElementById('fileInput').addEventListener('change', this.confirmTransfer.bind(this), false);
// darkwire // darkwire
return this; return this;
} }
resetInput() {
document.getElementById('fileInput').value = '';
}
} }

View File

@ -1,6 +1,8 @@
import _ from 'underscore';
import Darkwire from './darkwire'; import Darkwire from './darkwire';
import WindowHandler from './window'; import WindowHandler from './window';
import CryptoUtil from './crypto'; import CryptoUtil from './crypto';
import Chat from './chat';
let fs = window.RequestFileSystem || window.webkitRequestFileSystem; let fs = window.RequestFileSystem || window.webkitRequestFileSystem;
@ -8,28 +10,9 @@ $(function() {
const darkwire = new Darkwire(); const darkwire = new Darkwire();
const cryptoUtil = new CryptoUtil(); const cryptoUtil = new CryptoUtil();
let FADE_TIME = 150; // ms
let TYPING_TIMER_LENGTH = 400; // ms
let COLORS = [
'#e21400', '#ffe400', '#ff8f00',
'#58dc00', '#dd9cff', '#4ae8c4',
'#3b88eb', '#f47777', '#d300e7',
];
let $window = $(window); let $window = $(window);
let $messages = $('.messages'); // Messages area
let $inputMessage = $('.inputMessage'); // Input message input box
let $key = $('.key');
let $genKey = $('.new_key');
let $participants = $('#participants'); let $participants = $('#participants');
let $chatPage = $('.chat.page'); // The chatroom page
let username;
let typing = false;
let lastTypingTime;
let roomId = window.location.pathname.length ? window.location.pathname : null; let roomId = window.location.pathname.length ? window.location.pathname : null;
if (!roomId) { return; } if (!roomId) { return; }
@ -43,7 +26,8 @@ $(function() {
}); });
let socket = io(roomId); let socket = io(roomId);
const windowHandler = new WindowHandler(darkwire, socket); const chat = new Chat(darkwire, socket);
const windowHandler = new WindowHandler(darkwire, socket, chat);
FastClick.attach(document.body); FastClick.attach(document.body);
@ -56,142 +40,22 @@ $(function() {
// Sets the client's username // Sets the client's username
function initChat() { function initChat() {
username = window.username;
// warn not incognitor // warn not incognitor
if (!fs) { if (fs) {
console.log('no fs');
} else {
fs(window.TEMPORARY, fs(window.TEMPORARY,
100, 100,
log.bind(log, 'WARNING: Your browser is not in incognito mode!')); () => {
} chat.log('WARNING: Your browser is not in incognito mode!', {error: true});
// If the username is valid
if (username) {
$chatPage.show();
$inputMessage.focus();
Promise.all([
cryptoUtil.createPrimaryKeys()
])
.then(function(data) {
darkwire.keys = {
public: data[0].publicKey,
private: data[0].privateKey
};
return Promise.all([
cryptoUtil.exportKey(data[0].publicKey, 'spki')
]);
})
.then(function(exportedKeys) {
// Tell the server your username and send public keys
socket.emit('add user', {
username: username,
publicKey: exportedKeys[0]
}); });
});
}
}
// Log a message
function log(message, options) {
let html = options && options.html === true || false;
let $el;
if (html) {
$el = $('<li>').addClass('log').html(message);
} else {
$el = $('<li>').addClass('log').text(message);
}
addMessageElement($el, options);
}
// Adds the visual chat message to the message list
function addChatMessage(data, options) {
if (!data.message.trim().length) {
return;
} }
let messageType = data.messageType || 'text'; darkwire.updateUsername(username).then((socketData) => {
chat.chatPage.show();
// Don't fade the message in if there is an 'X was typing' chat.inputMessage.focus();
let $typingMessages = getTypingMessages(data); socket.emit('add user', socketData);
options = options || {};
if ($typingMessages.length !== 0) {
options.fade = false;
$typingMessages.remove();
}
let $usernameDiv = $('<span class="username"/>')
.text(data.username)
.css('color', getUsernameColor(data.username));
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 (messageType === '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)
.append($usernameDiv, $messageBodyDiv);
addMessageElement($messageDiv, options);
}
// Adds the visual chat typing message
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() {
$(this).remove();
}); });
} }
// Adds a message element to the messages and scrolls to the bottom
// el - The element to add as a message
// 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) {
let $el = $(el);
// Setup default options
if (!options) {
options = {};
}
if (typeof options.fade === 'undefined') {
options.fade = true;
}
if (typeof options.prepend === 'undefined') {
options.prepend = false;
}
// Apply options
if (options.fade) {
$el.hide().fadeIn(FADE_TIME);
}
if (options.prepend) {
$messages.prepend($el);
} else {
$messages.append($el);
}
$messages[0].scrollTop = $messages[0].scrollHeight; // minus 60 for key
}
// Prevents input from having injected markup // Prevents input from having injected markup
function cleanInput(input) { function cleanInput(input) {
let message = $('<div/>').html(input).text(); let message = $('<div/>').html(input).text();
@ -199,45 +63,6 @@ $(function() {
return message; return message;
} }
// Updates the typing event
function updateTyping() {
if (darkwire.connected) {
if (!typing) {
typing = true;
socket.emit('typing');
}
lastTypingTime = (new Date()).getTime();
setTimeout(function() {
let typingTimer = (new Date()).getTime();
let timeDiff = typingTimer - lastTypingTime;
if (timeDiff >= TYPING_TIMER_LENGTH && typing) {
socket.emit('stop typing');
typing = false;
}
}, TYPING_TIMER_LENGTH);
}
}
// Gets the 'X is typing' messages of a user
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) {
// Compute hash code
let hash = 7;
for (let i = 0; i < username.length; i++) {
hash = username.charCodeAt(i) + (hash << 5) - hash;
}
// Calculate color
let index = Math.abs(hash % COLORS.length);
return COLORS[index];
}
// Keyboard events // Keyboard events
$window.keydown(function(event) { $window.keydown(function(event) {
@ -245,43 +70,16 @@ $(function() {
if (event.which === 13 && $('.inputMessage').is(':focus')) { if (event.which === 13 && $('.inputMessage').is(':focus')) {
handleMessageSending(); handleMessageSending();
socket.emit('stop typing'); socket.emit('stop typing');
typing = false; chat.typing = false;
} }
}); });
$inputMessage.on('input propertychange paste change', function() {
updateTyping();
let message = $(this).val().trim();
if (message.length) {
$('#send-message-btn').addClass('active');
} else {
$('#send-message-btn').removeClass('active');
}
});
// Select message input when closing modal // Select message input when closing modal
$('.modal').on('hidden.bs.modal', function(e) { $('.modal').on('hidden.bs.modal', function(e) {
$inputMessage.focus(); chat.inputMessage.focus();
}); });
// Select message input when clicking message body, unless selecting text
$('.messages').on('click', function() {
if (!getSelectedText()) {
$inputMessage.focus();
}
});
function getSelectedText() {
var text = '';
if (typeof window.getSelection != 'undefined') {
text = window.getSelection().toString();
} 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 // Whenever the server emits 'login', log the login message
socket.on('user joined', function(data) { socket.on('user joined', function(data) {
darkwire.connected = true; darkwire.connected = true;
@ -293,38 +91,40 @@ $(function() {
$('#first-modal').modal('show'); $('#first-modal').modal('show');
} }
log(data.username + ' joined'); chat.log(data.username + ' joined');
renderParticipantsList(); renderParticipantsList();
}); });
}); });
socket.on('user update', (data) => {
updateUser(data);
});
// Whenever the server emits 'new message', update the chat body // Whenever the server emits 'new message', update the chat body
socket.on('new message', function(data) { socket.on('new message', function(data) {
darkwire.decodeMessage(data).then((data) => { darkwire.decodeMessage(data).then((decodedMessage) => {
if (!windowHandler.isActive) { if (!windowHandler.isActive) {
windowHandler.notifyFavicon(); windowHandler.notifyFavicon();
darkwire.audio.play(); darkwire.audio.play();
} }
if (data.messageType === 'file') {
// let file = windowHandler.fileHandler.decodeFile(data.message); let data = {
// let chatMessage = { username: decodedMessage.username,
// username: data.username, message: decodedMessage.message.text,
// message: file messageType: decodedMessage.messageType,
// } additionalData: decodedMessage.message.additionalData
addChatMessage(data, {messageType: 'file'}); };
} else { chat.addChatMessage(data);
addChatMessage(data);
}
}); });
}); });
// Whenever the server emits 'user left', log it in the chat body // 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'); chat.log(data.username + ' left');
addParticipantsMessage(data); addParticipantsMessage(data);
removeChatTyping(data); chat.removeChatTyping(data);
darkwire.removeUser(data); darkwire.removeUser(data);
@ -333,12 +133,12 @@ $(function() {
// Whenever the server emits 'typing', show the typing message // Whenever the server emits 'typing', show the typing message
socket.on('typing', function(data) { socket.on('typing', function(data) {
addChatTyping(data); chat.addChatTyping(data);
}); });
// Whenever the server emits 'stop typing', kill the typing message // Whenever the server emits 'stop typing', kill the typing message
socket.on('stop typing', function(data) { socket.on('stop typing', function(data) {
removeChatTyping(data); chat.removeChatTyping(data);
}); });
initChat(); initChat();
@ -365,9 +165,9 @@ $(function() {
let li; let li;
if (user.username === window.username) { if (user.username === window.username) {
// User is me // User is me
li = $('<li>' + user.username + ' <span class="you">(you)</span></li>').css('color', getUsernameColor(user.username)); li = $('<li>' + user.username + ' <span class="you">(you)</span></li>').css('color', chat.getUsernameColor(user.username));
} else { } else {
li = $('<li>' + user.username + '</li>').css('color', getUsernameColor(user.username)); li = $('<li>' + user.username + '</li>').css('color', chat.getUsernameColor(user.username));
} }
$('#participants-modal ul.users') $('#participants-modal ul.users')
.append(li); .append(li);
@ -377,27 +177,38 @@ $(function() {
$('#send-message-btn').click(function() { $('#send-message-btn').click(function() {
handleMessageSending(); handleMessageSending();
socket.emit('stop typing'); socket.emit('stop typing');
typing = false; chat.typing = false;
}); });
$('.navbar-collapse ul li a').click(function() { $('.navbar-collapse ul li a').click(function() {
$('.navbar-toggle:visible').click(); $('.navbar-toggle:visible').click();
}); });
let audioSwitch = $('input.bs-switch').bootstrapSwitch(); let audioSwitch = $('input.sound-enabled').bootstrapSwitch();
let autoEmbedImages = $('input.auto-embed-files').bootstrapSwitch();
audioSwitch.on('switchChange.bootstrapSwitch', function(event, state) { audioSwitch.on('switchChange.bootstrapSwitch', function(event, state) {
darkwire.audio.soundEnabled = state; darkwire.audio.soundEnabled = state;
}); });
autoEmbedImages.on('switchChange.bootstrapSwitch', (event, state) => {
darkwire.autoEmbedImages = state;
});
function handleMessageSending() { function handleMessageSending() {
let message = $inputMessage; let message = chat.inputMessage;
let cleanedMessage = cleanInput(message.val()); let cleanedMessage = cleanInput(message.val());
let isCommand = chat.parseCommand(cleanedMessage);
if (isCommand) {
return chat.executeCommand(isCommand, this);
}
// Prevent markup from being injected into the message // Prevent markup from being injected into the message
darkwire.encodeMessage(cleanedMessage, 'text').then((socketData) => { darkwire.encodeMessage(cleanedMessage, 'text').then((socketData) => {
message.val(''); message.val('');
$('#send-message-btn').removeClass('active'); $('#send-message-btn').removeClass('active');
addChatMessage({ chat.addChatMessage({
username: username, username: username,
message: cleanedMessage message: cleanedMessage
}); });
@ -407,4 +218,65 @@ $(function() {
}); });
} }
function updateUser(data) {
let logMessage = data.username + ' changed name to ';
darkwire.removeUser(data);
data.username = data.newUsername;
logMessage += data.username;
let importKeysPromises = darkwire.addUser(data);
Promise.all(importKeysPromises).then(() => {
chat.log(logMessage);
renderParticipantsList();
});
}
window.triggerFileTransfer = function(context) {
const fileId = context.getAttribute('data-file');
if (fileId) {
return windowHandler.fileHandler.encodeFile(fileId);
}
return chat.log('Requested file transfer is no longer valid. Please try again.', {error: true});
};
window.triggerFileDestroy = function(context) {
const fileId = context.getAttribute('data-file');
if (fileId) {
return windowHandler.fileHandler.destroyFile(fileId);
}
return chat.log('Requested file transfer is no longer valid. Please try again.', {error: true});
};
window.triggerFileDownload = function(context) {
const fileId = context.getAttribute('data-file');
const file = darkwire.getFile(fileId);
windowHandler.fileHandler.createBlob(file.message, file.messageType).then((blob) => {
let url = windowHandler.fileHandler.createUrlFromBlob(blob);
if (file) {
if (file.messageType.match('image.*')) {
let image = new Image();
image.src = url;
chat.replaceMessage('#file-transfer-request-' + fileId, image);
} else {
let downloadLink = document.createElement('a');
downloadLink.href = url;
downloadLink.target = '_blank';
downloadLink.innerHTML = 'Download ' + file.additionalData.fileName;
chat.replaceMessage('#file-transfer-request-' + fileId, downloadLink);
}
}
darkwire.encodeMessage('Downloaded ' + file.additionalData.fileName, 'text').then((socketData) => {
socket.emit('new message', socketData);
}).catch((err) => {
console.log(err);
});
});
};
}); });

View File

@ -1,9 +1,9 @@
import FileHandler from './fileHandler'; import FileHandler from './fileHandler';
export default class WindowHandler { export default class WindowHandler {
constructor(darkwire, socket) { constructor(darkwire, socket, chat) {
this._isActive = false; this._isActive = false;
this.fileHandler = new FileHandler(darkwire, socket); this.fileHandler = new FileHandler(darkwire, socket, chat);
this.newMessages = 0; this.newMessages = 0;
this.favicon = new Favico({ this.favicon = new Favico({
@ -31,6 +31,7 @@ export default class WindowHandler {
enableFileTransfer() { enableFileTransfer() {
if (this.fileHandler.isSupported) { if (this.fileHandler.isSupported) {
console.log('enabled');
$('#send-file').click((e) => { $('#send-file').click((e) => {
e.preventDefault(); e.preventDefault();
$('#fileInput').trigger('click'); $('#fileInput').trigger('click');

View File

@ -296,3 +296,19 @@ html.no-touchevents .chat #input-icons {
#fileInput { #fileInput {
display: none; display: none;
} }
.file-disclaimer {
font-size: 10px;
color: red;
line-height: 10px;
}
.log-info {
color: #FFF;
font-weight: bold;
}
.log-error {
color: yellow;
font-weight: bold;
}

View File

@ -26,7 +26,8 @@ class Room {
data: data.data, data: data.data,
vector: data.vector, vector: data.vector,
secretKeys: data.secretKeys, secretKeys: data.secretKeys,
signature: data.signature signature: data.signature,
timestamp: new Date
}); });
}); });
@ -46,7 +47,8 @@ class Room {
thisIO.emit('user joined', { thisIO.emit('user joined', {
username: socket.username, username: socket.username,
numUsers: this.numUsers, numUsers: this.numUsers,
users: this.users users: this.users,
timestamp: new Date
}); });
}); });
@ -75,7 +77,8 @@ class Room {
username: socket.username, username: socket.username,
numUsers: this.numUsers, numUsers: this.numUsers,
users: this.users, users: this.users,
id: socket.user.id id: socket.user.id,
timestamp: new Date
}); });
// remove room from rooms array // remove room from rooms array
@ -84,6 +87,35 @@ class Room {
} }
} }
}); });
// Update user
socket.on('update user', (data) => {
if (data.newUsername.length > 16) {
return false;
}
this.users = _.without(this.users, socket.user);
let modifiedUser = {
id: socket.user.id,
username: data.newUsername,
publicKey: data.publicKey
};
this.users.push(modifiedUser);
socket.username = data.newUsername;
socket.user = modifiedUser;
thisIO.emit('user update', {
id: socket.user.id,
username: data.username,
newUsername: data.newUsername,
publicKey: data.publicKey,
users: this.users,
timestamp: new Date
});
});
}); });
} }

View File

@ -68,13 +68,13 @@
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">About</h4> <h4 class="modal-title">About</h4>
<p><strong>Darkwire Version:</strong> v{{APP.version}} (<a href="https://github.com/seripap/darkwire.io/releases/tag/v{{APP.version}}" target="_blank">View Release Notes</a>)<br></p>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p class="bold">WARNING: This product is in beta and its source code has not been peer-reviewed or undergone a security audit. This is a demo only and not intended for security-critical use. <a href="https://github.com/seripap/darkwire.io" target="_blank">View source code</a>.</p> {{>partials/disclaimer}}
<p>darkwire.io is the simplest way to chat with encryption online. Chat history is never stored on a server or database, and plain text messages are never transferred over the wire.</p> <p>Questions/comments? Email us at hello[at]darkwire.io<br>
Found a bug or want a new feature? <a href="https://github.com/seripap/darkwire.io/issues" target="_blank">Open a ticket on Github</a>.</p>
<p>Questions/comments? Email us at hello[at]darkwire.io</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
@ -97,7 +97,10 @@
</p> </p>
<br> <br>
<h6>Sound</h6> <h6>Sound</h6>
<input type="checkbox" name="my-checkbox" class="form-control bs-switch" checked> <input type="checkbox" name="sound-enabled" class="form-control bs-switch sound-enabled" checked>
<br>
<h6>Auto-embed images?</h6>
<input type="checkbox" name="embed-images" class="form-control bs-switch auto-embed-files">
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default encryption-active" data-dismiss="modal">Close</button> <button type="button" class="btn btn-default encryption-active" data-dismiss="modal">Close</button>
@ -111,11 +114,11 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Welcome to darkwire.io</h4> <h4 class="modal-title">Welcome to darkwire.io v{{APP.version}}</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<h6>We've placed you in a new chat room</h6> <h6>We've placed you in a new chat room</h6>
<p class="bold">WARNING: This product is in beta and its source code has not been peer-reviewed or undergone a security audit. This is a demo only and not intended for security-critical use. <a href="https://github.com/seripap/darkwire.io" target="_blank">View source code</a>.</p> {{>partials/disclaimer}}
<br> <br>
<h6>Invite People to This Room</h6> <h6>Invite People to This Room</h6>
<p> <p>
@ -166,7 +169,6 @@
<script src="/favicon.js"></script> <script src="/favicon.js"></script>
<script src="/vendor/bootstrap-3.3.6.min.js"></script> <script src="/vendor/bootstrap-3.3.6.min.js"></script>
<script src="/vendor/autolinker.min.js"></script> <script src="/vendor/autolinker.min.js"></script>
<script src="/vendor/underscore.min.js"></script>
<script src="/vendor/modernizr-custom.min.js"></script> <script src="/vendor/modernizr-custom.min.js"></script>
<script src="/vendor/bootstrap-switch.min.js"></script> <script src="/vendor/bootstrap-switch.min.js"></script>
<script src="/vendor/web-crypto-shim.js"></script> <script src="/vendor/web-crypto-shim.js"></script>

View File

@ -0,0 +1,8 @@
<p class="bold">DISCLAIMER: This software uses the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Crypto" target="_blank">Crypto Web API</a> to encrypt data which is transferred using <a href="https://en.wikipedia.org/wiki/WebSocket" target="_blank">secure WebSockets</a>. Darkwire does not mask IP addresses nor can verify the integrity parties recieving messages. Proceed with caution and always confirm recipients before continuing.</p>
<p><a href="https://github.com/seripap/darkwire.io" target="_blank">Darkwire.io believes in privacy and full transparency. View the source code and documentation on Github.</a></p>
{{#APP.ip}}
<p class="bold">Recognized IP: <a href="http://ipinfo.io/{{APP.ip}}/json" target="_blank">{{APP.ip}}</a>*</p>
<p>*This IP is your public IP recognized by all web servers. If this IP and/or its location data seems familiar to you, we recommend using <a href="https://www.torproject.org/" target="_blank">TOR</a> or a proxy before starting a new chat session.</p>
{{/APP.ip}}