mirror of
https://github.com/darkwire/darkwire.io.git
synced 2025-07-18 10:49:02 +00:00
Merge develop
into cleanup
This commit is contained in:
commit
e9b3498eaa
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
src/public/main.js
|
||||
src/.secret
|
||||
|
10
readme.md
10
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).
|
||||
|
||||
|
@ -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
226
src/js/darkwire.js
Normal 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
62
src/js/fileHandler.js
Normal 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;
|
||||
}
|
||||
}
|
120
src/js/main.js
120
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 = $('<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);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user