mirror of
https://github.com/darkwire/darkwire.io.git
synced 2025-07-19 19:14:53 +00:00
Use asymmetric encryption to replace passphrase exchange
All parties now create public/private key pairs for encryption/decryption and signing/verification. Public keys are passed around and stored in server memory while the room is alive. Session keys, which are used to encrypt cleartext messages, are newly generated for each message and are encrypted using each participant’s public key. Messages are signed using the sender’s private signing key.
This commit is contained in:
parent
64b12776b8
commit
d46b8f24b5
536
src/js/main.js
536
src/js/main.js
@ -20,7 +20,6 @@ $(function() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let $window = $(window);
|
let $window = $(window);
|
||||||
let $usernameInput = $('.usernameInput'); // Input for username
|
|
||||||
let $messages = $('.messages'); // Messages area
|
let $messages = $('.messages'); // Messages area
|
||||||
let $inputMessage = $('.inputMessage'); // Input message input box
|
let $inputMessage = $('.inputMessage'); // Input message input box
|
||||||
let $key = $('.key');
|
let $key = $('.key');
|
||||||
@ -33,14 +32,15 @@ $(function() {
|
|||||||
|
|
||||||
// Prompt for setting a username
|
// Prompt for setting a username
|
||||||
let username;
|
let username;
|
||||||
|
let myUserId;
|
||||||
let connected = false;
|
let connected = false;
|
||||||
let typing = false;
|
let typing = false;
|
||||||
let lastTypingTime;
|
let lastTypingTime;
|
||||||
let $currentInput = $usernameInput.focus();
|
|
||||||
let encryptionKey;
|
|
||||||
|
|
||||||
let roomId = window.location.pathname.length ? window.location.pathname : null;
|
let roomId = window.location.pathname.length ? window.location.pathname : null;
|
||||||
|
|
||||||
|
let keys = {};
|
||||||
|
|
||||||
if (!roomId) return;
|
if (!roomId) return;
|
||||||
|
|
||||||
if (!window.crypto || (!window.crypto.subtle && !window.crypto.webkitSubtle)) {
|
if (!window.crypto || (!window.crypto.subtle && !window.crypto.webkitSubtle)) {
|
||||||
@ -53,11 +53,18 @@ $(function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$('textarea.share-text').val("Let's chat on darkwire.io at https://darkwire.io" + roomId);
|
||||||
|
|
||||||
|
$('textarea.share-text').click(function() {
|
||||||
|
$(this).focus();
|
||||||
|
$(this).select();
|
||||||
|
this.setSelectionRange(0, 9999);
|
||||||
|
});
|
||||||
|
|
||||||
var crypto = window.crypto;
|
var crypto = window.crypto;
|
||||||
var cryptoSubtle = window.crypto.subtle || window.crypto.webkitSubtle;
|
var cryptoSubtle = window.crypto.subtle || window.crypto.webkitSubtle;
|
||||||
|
|
||||||
let socket = io(roomId);
|
let socket = io(roomId);
|
||||||
$('#roomIdKey').text(roomId.replace('/', ''));
|
|
||||||
|
|
||||||
FastClick.attach(document.body);
|
FastClick.attach(document.body);
|
||||||
|
|
||||||
@ -69,7 +76,7 @@ $(function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sets the client's username
|
// Sets the client's username
|
||||||
function setUsername () {
|
function initChat () {
|
||||||
username = window.username;
|
username = window.username;
|
||||||
// warn not incognitor
|
// warn not incognitor
|
||||||
if (!fs) {
|
if (!fs) {
|
||||||
@ -83,19 +90,41 @@ $(function() {
|
|||||||
// If the username is valid
|
// If the username is valid
|
||||||
if (username) {
|
if (username) {
|
||||||
$chatPage.show();
|
$chatPage.show();
|
||||||
$currentInput = $inputMessage.focus();
|
$inputMessage.focus();
|
||||||
|
|
||||||
// Tell the server your username
|
Promise.all([
|
||||||
socket.emit('add user', username);
|
createPrimaryKeys(),
|
||||||
|
createSigningKeys()
|
||||||
|
])
|
||||||
|
.then(function(data) {
|
||||||
|
keys.primary = {
|
||||||
|
public: data[0].publicKey,
|
||||||
|
private: data[0].privateKey
|
||||||
|
};
|
||||||
|
keys.signing = {
|
||||||
|
public: data[1].publicKey,
|
||||||
|
private: data[1].privateKey
|
||||||
|
};
|
||||||
|
return Promise.all([
|
||||||
|
exportKey(data[0].publicKey),
|
||||||
|
exportKey(data[1].publicKey),
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
.then(function(exportedKeys) {
|
||||||
|
// Tell the server your username and send public keys
|
||||||
|
socket.emit('add user', {
|
||||||
|
username: username,
|
||||||
|
publicPrimaryKey: exportedKeys[0],
|
||||||
|
publicSigningKey: exportedKeys[1]
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sends a chat message
|
// Sends a chat message
|
||||||
function sendMessage () {
|
function sendMessage () {
|
||||||
// Don't allow sending if key is empty
|
// Don't send unless other users exist
|
||||||
if (!encryptionKey.trim().length) return;
|
if (users.length <= 1) return;
|
||||||
|
|
||||||
var vector = crypto.getRandomValues(new Uint8Array(16));
|
|
||||||
|
|
||||||
let message = $inputMessage.val();
|
let message = $inputMessage.val();
|
||||||
// Prevent markup from being injected into the message
|
// Prevent markup from being injected into the message
|
||||||
@ -108,16 +137,63 @@ $(function() {
|
|||||||
username: username,
|
username: username,
|
||||||
message: message
|
message: message
|
||||||
});
|
});
|
||||||
// tell server to execute 'new message' and send along one parameter
|
let vector = crypto.getRandomValues(new Uint8Array(16));
|
||||||
createKey(encryptionKey)
|
|
||||||
|
let secretKey;
|
||||||
|
let secretKeys;
|
||||||
|
let messageData;
|
||||||
|
let signature;
|
||||||
|
|
||||||
|
// Generate new secret key and vector for each message
|
||||||
|
createSecretKey()
|
||||||
.then(function(key) {
|
.then(function(key) {
|
||||||
return encryptData(message, key, vector);
|
secretKey = key;
|
||||||
|
// Generate secretKey and encrypt with each user's public key
|
||||||
|
let promises = [];
|
||||||
|
_.each(users, function(user) {
|
||||||
|
// It not me
|
||||||
|
if (user.username !== window.username) {
|
||||||
|
let promise = new Promise(function(resolve, reject) {
|
||||||
|
let thisUser = user;
|
||||||
|
|
||||||
|
let exportedSecretKey;
|
||||||
|
exportKey(key, "raw")
|
||||||
|
.then(function(data) {
|
||||||
|
exportedSecretKey = data;
|
||||||
|
return encryptSecretKey(data, thisUser.publicPrimaryKey);
|
||||||
|
})
|
||||||
|
.then(function(encryptedSecretKey) {
|
||||||
|
var encData = new Uint8Array(encryptedSecretKey);
|
||||||
|
var str = convertArrayBufferViewToString(encData);
|
||||||
|
resolve({
|
||||||
|
id: thisUser.id,
|
||||||
|
secretKey: str
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
promises.push(promise);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Promise.all(promises);
|
||||||
})
|
})
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
var encryptedData = new Uint8Array(data);
|
secretKeys = data;
|
||||||
|
messageData = convertStringToArrayBufferView(message);
|
||||||
|
return signKey(messageData, keys.signing.private)
|
||||||
|
})
|
||||||
|
.then(function(data) {
|
||||||
|
signature = data;
|
||||||
|
return encryptMessage(messageData, secretKey, vector)
|
||||||
|
})
|
||||||
|
.then(function(encryptedData) {
|
||||||
|
let msg = convertArrayBufferViewToString(new Uint8Array(encryptedData));
|
||||||
|
let vct = convertArrayBufferViewToString(new Uint8Array(vector));
|
||||||
|
let sig = convertArrayBufferViewToString(new Uint8Array(signature));
|
||||||
socket.emit('new message', {
|
socket.emit('new message', {
|
||||||
message: convertArrayBufferViewtoString(encryptedData),
|
message: msg,
|
||||||
vector: convertArrayBufferViewtoString(vector)
|
vector: vct,
|
||||||
|
secretKeys: secretKeys,
|
||||||
|
signature: sig
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -264,15 +340,6 @@ $(function() {
|
|||||||
typing = false;
|
typing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If enter is pressed on key input then close key modal
|
|
||||||
if (event.which === 13 && $('#join-modal input').is(':focus')) {
|
|
||||||
checkJoinKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If enter is pressed on edit key input
|
|
||||||
if (event.which === 13 && $('#settings-modal .edit-key input.key').is(':focus')) {
|
|
||||||
saveKey();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$inputMessage.on('input propertychange paste change', function() {
|
$inputMessage.on('input propertychange paste change', function() {
|
||||||
@ -285,11 +352,6 @@ $(function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$genKey.click(function () {
|
|
||||||
let key = generatePassword();
|
|
||||||
updateKeyVal(key);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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();
|
$inputMessage.focus();
|
||||||
@ -312,30 +374,57 @@ $(function() {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Socket events
|
|
||||||
|
|
||||||
// Whenever the server emits 'login', log the login message
|
// Whenever the server emits 'login', log the login message
|
||||||
socket.on('login', function (data) {
|
socket.on('user joined', function (data) {
|
||||||
connected = true;
|
connected = true;
|
||||||
addParticipantsMessage(data);
|
addParticipantsMessage(data);
|
||||||
|
|
||||||
users = data.users;
|
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([
|
||||||
|
importPrimaryKey(currentUser.publicPrimaryKey),
|
||||||
|
importSigningKey(currentUser.publicSigningKey)
|
||||||
|
])
|
||||||
|
.then(function(keys) {
|
||||||
|
users.push({
|
||||||
|
id: currentUser.id,
|
||||||
|
username: currentUser.username,
|
||||||
|
publicPrimaryKey: keys[0],
|
||||||
|
publicSigningKey: keys[1]
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
importKeysPromises.push(promise);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let key = generatePassword();
|
if (!myUserId) {
|
||||||
|
// Set my id if not already set
|
||||||
|
let me = _.findWhere(data.users, {username: username});
|
||||||
|
myUserId = me.id;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.numUsers > 1) {
|
Promise.all(importKeysPromises)
|
||||||
$('#join-modal').modal('show');
|
.then(function() {
|
||||||
$('#join-modal').on('shown.bs.modal', function (e) {
|
// All users' keys have been imported
|
||||||
$('#join-modal input').focus();
|
if (data.numUsers === 1) {
|
||||||
|
$('#first-modal').modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.modal').on('shown.bs.modal', function (e) {
|
||||||
|
autosize.update($('textarea.share-text'));
|
||||||
});
|
});
|
||||||
|
|
||||||
key = '';
|
log(data.username + ' joined');
|
||||||
}
|
|
||||||
updateKeyVal(key);
|
|
||||||
|
|
||||||
$('.modal').on('shown.bs.modal', function (e) {
|
renderParticipantsList();
|
||||||
autosize.update($('textarea.share-text'));
|
});
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -349,44 +438,64 @@ $(function() {
|
|||||||
beep.play();
|
beep.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var username = data.username;
|
|
||||||
|
|
||||||
createKey(encryptionKey)
|
let message = data.message;
|
||||||
.then(function(key) {
|
let messageData = convertStringToArrayBufferView(message);
|
||||||
var msg = convertStringToArrayBufferView(data.message);
|
let username = data.username;
|
||||||
var vector = convertStringToArrayBufferView(data.vector);
|
let senderId = data.id
|
||||||
return decryptData(msg, key, vector)
|
let vector = data.vector;
|
||||||
|
let vectorData = convertStringToArrayBufferView(vector);
|
||||||
|
let secretKeys = data.secretKeys;
|
||||||
|
let decryptedMessageData;
|
||||||
|
let decryptedMessage;
|
||||||
|
|
||||||
|
let mySecretKey = _.find(secretKeys, function(key) {
|
||||||
|
return key.id === myUserId;
|
||||||
|
});
|
||||||
|
let signature = data.signature;
|
||||||
|
let signatureData = convertStringToArrayBufferView(signature);
|
||||||
|
let secretKeyArrayBuffer = convertStringToArrayBufferView(mySecretKey.secretKey);
|
||||||
|
|
||||||
|
decryptSecretKey(secretKeyArrayBuffer, keys.primary.private)
|
||||||
|
.then(function(data) {
|
||||||
|
return new Uint8Array(data);
|
||||||
})
|
})
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
var decryptedData = new Uint8Array(data);
|
return importSecretKey(data, "raw");
|
||||||
var msg = convertArrayBufferViewtoString(decryptedData);
|
|
||||||
addChatMessage({
|
|
||||||
username: username,
|
|
||||||
message: msg
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(function() {
|
.then(function(data) {
|
||||||
|
let secretKey = data;
|
||||||
|
return decryptMessage(messageData, secretKey, vectorData);
|
||||||
|
})
|
||||||
|
.then(function(data) {
|
||||||
|
decryptedMessageData = data;
|
||||||
|
decryptedMessage = convertArrayBufferViewToString(new Uint8Array(data))
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
// Find who sent msg (senderId), get their public key and verifyKey() with it and signature
|
||||||
|
let sender = _.find(users, function(user) {
|
||||||
|
return user.id === senderId;
|
||||||
|
});
|
||||||
|
let senderPublicVerifyKey = sender.publicSigningKey;
|
||||||
|
return verifyKey(signatureData, decryptedMessageData, senderPublicVerifyKey)
|
||||||
|
})
|
||||||
|
.then(function(bool) {
|
||||||
|
if (bool) {
|
||||||
|
addChatMessage({
|
||||||
|
username: username,
|
||||||
|
message: decryptedMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Whenever the server emits 'user joined', log it in the chat body
|
|
||||||
socket.on('user joined', function (data) {
|
|
||||||
log(data.username + ' joined');
|
|
||||||
addParticipantsMessage(data);
|
|
||||||
|
|
||||||
users = data.users;
|
|
||||||
renderParticipantsList();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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');
|
log(data.username + ' left');
|
||||||
addParticipantsMessage(data);
|
addParticipantsMessage(data);
|
||||||
removeChatTyping(data);
|
removeChatTyping(data);
|
||||||
|
|
||||||
users = data.users;
|
users = _.without(users, _.findWhere(users, {id: data.id}));
|
||||||
|
|
||||||
renderParticipantsList();
|
renderParticipantsList();
|
||||||
});
|
});
|
||||||
@ -401,11 +510,7 @@ $(function() {
|
|||||||
removeChatTyping(data);
|
removeChatTyping(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('first', function() {
|
initChat();
|
||||||
$('#first-modal').modal('show');
|
|
||||||
});
|
|
||||||
|
|
||||||
setUsername();
|
|
||||||
|
|
||||||
window.onfocus = function () {
|
window.onfocus = function () {
|
||||||
isActive = true;
|
isActive = true;
|
||||||
@ -426,33 +531,8 @@ $(function() {
|
|||||||
$('#about-modal').modal('show');
|
$('#about-modal').modal('show');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.room-url').text('https://darkwire.io' + roomId);
|
|
||||||
$('.room-id').text(roomId.replace('/', ''));
|
|
||||||
|
|
||||||
$('[data-toggle="tooltip"]').tooltip();
|
$('[data-toggle="tooltip"]').tooltip();
|
||||||
|
|
||||||
function joinKeyInputChanged(val) {
|
|
||||||
if (!val.trim().length) {
|
|
||||||
$('#join-modal .modal-footer button').attr('disabled', 'disabled');
|
|
||||||
} else {
|
|
||||||
$('#join-modal .modal-footer button').removeAttr('disabled');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#join-modal .key').on('input propertychange paste change', function() {
|
|
||||||
let val = $(this).val().trim();
|
|
||||||
joinKeyInputChanged(val);
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#settings-modal input.key').on('input propertychange paste change', function() {
|
|
||||||
let val = $(this).val().trim();
|
|
||||||
if (val !== encryptionKey && val.length) {
|
|
||||||
$('#settings-modal #save-key-edit').removeAttr('disabled');
|
|
||||||
} else {
|
|
||||||
$('#settings-modal #save-key-edit').attr('disabled', 'disabled');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.navbar .participants').click(function() {
|
$('.navbar .participants').click(function() {
|
||||||
renderParticipantsList();
|
renderParticipantsList();
|
||||||
$('#participants-modal').modal('show');
|
$('#participants-modal').modal('show');
|
||||||
@ -460,101 +540,29 @@ $(function() {
|
|||||||
|
|
||||||
function renderParticipantsList() {
|
function renderParticipantsList() {
|
||||||
$('#participants-modal ul.users').empty();
|
$('#participants-modal ul.users').empty();
|
||||||
_.each(users, function(username) {
|
_.each(users, function(user) {
|
||||||
let li;
|
let li;
|
||||||
if (username === window.username) {
|
if (user.username === window.username) {
|
||||||
// User is me
|
// User is me
|
||||||
li = $("<li>" + username + " <span class='you'>(you)</span></li>").css('color', getUsernameColor(username));
|
li = $("<li>" + user.username + " <span class='you'>(you)</span></li>").css('color', getUsernameColor(user.username));
|
||||||
} else {
|
} else {
|
||||||
li = $("<li>" + username + "</li>").css('color', getUsernameColor(username));
|
li = $("<li>" + user.username + "</li>").css('color', getUsernameColor(user.username));
|
||||||
}
|
}
|
||||||
$('#participants-modal ul.users')
|
$('#participants-modal ul.users')
|
||||||
.append(li);
|
.append(li);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateKeyVal(val) {
|
|
||||||
$('.key').val(val);
|
|
||||||
$('.key').text(val);
|
|
||||||
|
|
||||||
encryptionKey = val;
|
|
||||||
$('textarea.share-text').val("Let's chat on darkwire.io at https://darkwire.io" + roomId + " using the passphrase " + encryptionKey);
|
|
||||||
autosize.update($('textarea.share-text'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent closing join-modal
|
|
||||||
$('#join-modal').modal({
|
|
||||||
backdrop: 'static',
|
|
||||||
show: false,
|
|
||||||
keyboard: false
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.read-key').click(function() {
|
|
||||||
$('.edit-key').show();
|
|
||||||
$('.edit-key input').focus();
|
|
||||||
$(this).hide();
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.edit-key #cancel-key-edit').click(function() {
|
|
||||||
cancelSaveKey();
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.edit-key #save-key-edit').click(function() {
|
|
||||||
saveKey();
|
|
||||||
});
|
|
||||||
|
|
||||||
function cancelSaveKey() {
|
|
||||||
$('.edit-key').hide();
|
|
||||||
$('.read-key').show();
|
|
||||||
updateKeyVal(encryptionKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveKey() {
|
|
||||||
let key = $('.edit-key input.key').val().trim();
|
|
||||||
if (!key.length) return;
|
|
||||||
$('.edit-key').hide();
|
|
||||||
$('.read-key').show();
|
|
||||||
updateKeyVal(key || encryptionKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#join-modal .modal-footer button').click(function() {
|
|
||||||
checkJoinKey();
|
|
||||||
});
|
|
||||||
|
|
||||||
function checkJoinKey() {
|
|
||||||
let key = $('#join-modal input').val().trim();
|
|
||||||
if (!key.length) return;
|
|
||||||
updateKeyVal(key);
|
|
||||||
$('#join-modal').modal('hide');
|
|
||||||
socket.emit('user joined');
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#settings-modal').on('hide.bs.modal', function (e) {
|
|
||||||
cancelSaveKey();
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#send-message-btn').click(function() {
|
$('#send-message-btn').click(function() {
|
||||||
sendMessage();
|
sendMessage();
|
||||||
socket.emit('stop typing');
|
socket.emit('stop typing');
|
||||||
typing = false;
|
typing = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
function generatePassword() {
|
|
||||||
return uuid.v4();
|
|
||||||
}
|
|
||||||
|
|
||||||
$('.navbar-collapse ul li a').click(function() {
|
$('.navbar-collapse ul li a').click(function() {
|
||||||
$('.navbar-toggle:visible').click();
|
$('.navbar-toggle:visible').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
autosize($('textarea.share-text'));
|
|
||||||
|
|
||||||
$('textarea.share-text').click(function() {
|
|
||||||
$(this).focus();
|
|
||||||
$(this).select();
|
|
||||||
this.setSelectionRange(0, 9999);
|
|
||||||
});
|
|
||||||
|
|
||||||
$('input.bs-switch').bootstrapSwitch();
|
$('input.bs-switch').bootstrapSwitch();
|
||||||
|
|
||||||
$('input.bs-switch').on('switchChange.bootstrapSwitch', function(event, state) {
|
$('input.bs-switch').on('switchChange.bootstrapSwitch', function(event, state) {
|
||||||
@ -570,7 +578,7 @@ $(function() {
|
|||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertArrayBufferViewtoString(buffer) {
|
function convertArrayBufferViewToString(buffer) {
|
||||||
var str = "";
|
var str = "";
|
||||||
for (var i = 0; i < buffer.byteLength; i++) {
|
for (var i = 0; i < buffer.byteLength; i++) {
|
||||||
str += String.fromCharCode(buffer[i]);
|
str += String.fromCharCode(buffer[i]);
|
||||||
@ -579,29 +587,161 @@ $(function() {
|
|||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createKey(password) {
|
function createSigningKeys() {
|
||||||
return cryptoSubtle.digest({
|
return crypto.subtle.generateKey(
|
||||||
name: "SHA-256"
|
{
|
||||||
}, convertStringToArrayBufferView(password))
|
name: "RSASSA-PKCS1-v1_5",
|
||||||
.then(function(result) {
|
modulusLength: 2048, //can be 1024, 2048, or 4096
|
||||||
return cryptoSubtle.importKey("raw", result, {
|
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
||||||
name: "AES-CBC"
|
hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
|
||||||
}, false, ["encrypt", "decrypt"]);
|
},
|
||||||
});
|
true, //whether the key is extractable (i.e. can be used in exportKey)
|
||||||
|
["sign", "verify"] //can be any combination of "sign" and "verify"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function encryptData(data, key, vector) {
|
function createPrimaryKeys() {
|
||||||
return cryptoSubtle.encrypt({
|
return crypto.subtle.generateKey(
|
||||||
name: "AES-CBC",
|
{
|
||||||
iv: vector
|
name: "RSA-OAEP",
|
||||||
}, key, convertStringToArrayBufferView(data));
|
modulusLength: 2048, //can be 1024, 2048, or 4096
|
||||||
|
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
||||||
|
hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
|
||||||
|
},
|
||||||
|
true, //whether the key is extractable (i.e. can be used in exportKey)
|
||||||
|
["encrypt", "decrypt"] //must be ["encrypt", "decrypt"] or ["wrapKey", "unwrapKey"]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function decryptData(data, key, vector) {
|
function createSecretKey() {
|
||||||
return cryptoSubtle.decrypt({
|
return crypto.subtle.generateKey(
|
||||||
name: "AES-CBC",
|
{
|
||||||
iv: vector
|
name: "AES-CBC",
|
||||||
}, key, data);
|
length: 256, //can be 128, 192, or 256
|
||||||
|
},
|
||||||
|
true, //whether the key is extractable (i.e. can be used in exportKey)
|
||||||
|
["encrypt", "decrypt", "wrapKey", "unwrapKey"] //can be "encrypt", "decrypt", "wrapKey", or "unwrapKey"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encryptSecretKey(data, secretKey) {
|
||||||
|
// Secret key will be recipient's public key
|
||||||
|
return crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: "RSA-OAEP"
|
||||||
|
},
|
||||||
|
secretKey,
|
||||||
|
data //ArrayBuffer of data you want to encrypt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decryptSecretKey(data, key) {
|
||||||
|
// key will be my private key
|
||||||
|
return crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: "RSA-OAEP",
|
||||||
|
//label: Uint8Array([...]) //optional
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
data //ArrayBuffer of the data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encryptMessage(data, secretKey, iv) {
|
||||||
|
return crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: "AES-CBC",
|
||||||
|
//Don't re-use initialization vectors!
|
||||||
|
//Always generate a new iv every time your encrypt!
|
||||||
|
iv: iv,
|
||||||
|
},
|
||||||
|
secretKey, //from generateKey or importKey above
|
||||||
|
data //ArrayBuffer of data you want to encrypt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decryptMessage(data, secretKey, iv) {
|
||||||
|
return crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: "AES-CBC",
|
||||||
|
iv: iv, //The initialization vector you used to encrypt
|
||||||
|
},
|
||||||
|
secretKey, //from generateKey or importKey above
|
||||||
|
data //ArrayBuffer of the data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importSecretKey(jwkData, format) {
|
||||||
|
return crypto.subtle.importKey(
|
||||||
|
format || "jwk", //can be "jwk" or "raw"
|
||||||
|
//this is an example jwk key, "raw" would be an ArrayBuffer
|
||||||
|
jwkData,
|
||||||
|
{ //this is the algorithm options
|
||||||
|
name: "AES-CBC",
|
||||||
|
},
|
||||||
|
true, //whether the key is extractable (i.e. can be used in exportKey)
|
||||||
|
["encrypt", "decrypt"] //can be "encrypt", "decrypt", "wrapKey", or "unwrapKey"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importPrimaryKey(jwkData) {
|
||||||
|
// Will be someone's public key
|
||||||
|
return crypto.subtle.importKey(
|
||||||
|
"jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only)
|
||||||
|
jwkData,
|
||||||
|
{ //these are the algorithm options
|
||||||
|
name: "RSA-OAEP",
|
||||||
|
hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
|
||||||
|
},
|
||||||
|
true, //whether the key is extractable (i.e. can be used in exportKey)
|
||||||
|
["encrypt"] //"encrypt" or "wrapKey" for public key import or
|
||||||
|
//"decrypt" or "unwrapKey" for private key imports
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportKey(key, format) {
|
||||||
|
// Will be public primary key or public signing key
|
||||||
|
return crypto.subtle.exportKey(
|
||||||
|
format || "jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only)
|
||||||
|
key //can be a publicKey or privateKey, as long as extractable was true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importSigningKey(jwkData) {
|
||||||
|
return crypto.subtle.importKey(
|
||||||
|
"jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only)
|
||||||
|
//this is an example jwk key, other key types are Uint8Array objects
|
||||||
|
jwkData,
|
||||||
|
{ //these are the algorithm options
|
||||||
|
name: "RSASSA-PKCS1-v1_5",
|
||||||
|
hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
|
||||||
|
},
|
||||||
|
true, //whether the key is extractable (i.e. can be used in exportKey)
|
||||||
|
["verify"] //"verify" for public key import, "sign" for private key imports
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function signKey(data, keyToSignWith) {
|
||||||
|
// Will use my private key
|
||||||
|
return crypto.subtle.sign(
|
||||||
|
{
|
||||||
|
name: "RSASSA-PKCS1-v1_5"
|
||||||
|
},
|
||||||
|
keyToSignWith, //from generateKey or importKey above
|
||||||
|
data //ArrayBuffer of data you want to sign
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyKey(signature, data, keyToVerifyWith) {
|
||||||
|
// Will verify with sender's public key
|
||||||
|
return crypto.subtle.verify(
|
||||||
|
{
|
||||||
|
name: "RSASSA-PKCS1-v1_5"
|
||||||
|
},
|
||||||
|
keyToVerifyWith, //from generateKey or importKey above
|
||||||
|
signature, //ArrayBuffer of the signature
|
||||||
|
data //ArrayBuffer of the data
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
40
src/room.js
40
src/room.js
@ -1,6 +1,7 @@
|
|||||||
import _ from 'underscore';
|
import _ from 'underscore';
|
||||||
import {EventEmitter} from 'events';
|
import {EventEmitter} from 'events';
|
||||||
import util from 'util';
|
import util from 'util';
|
||||||
|
import uuid from 'uuid';
|
||||||
|
|
||||||
class Room {
|
class Room {
|
||||||
constructor(io = {}, id = {}) {
|
constructor(io = {}, id = {}) {
|
||||||
@ -19,39 +20,34 @@ class Room {
|
|||||||
// we tell the client to execute 'new message'
|
// we tell the client to execute 'new message'
|
||||||
socket.broadcast.emit('new message', {
|
socket.broadcast.emit('new message', {
|
||||||
username: socket.username,
|
username: socket.username,
|
||||||
|
id: socket.user.id,
|
||||||
message: data.message,
|
message: data.message,
|
||||||
vector: data.vector
|
vector: data.vector,
|
||||||
|
secretKeys: data.secretKeys,
|
||||||
|
signature: data.signature
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('add user', (username) => {
|
socket.on('add user', (data) => {
|
||||||
if (addedUser) return;
|
if (addedUser) return;
|
||||||
|
|
||||||
if (this.numUsers === 0) {
|
data.id = uuid.v4();
|
||||||
socket.emit('first');
|
this.users.push(data);
|
||||||
}
|
|
||||||
|
|
||||||
this.users.push(username);
|
|
||||||
|
|
||||||
// we store the username in the socket session for this client
|
// we store the username in the socket session for this client
|
||||||
socket.username = username;
|
socket.username = data.username;
|
||||||
|
socket.user = data;
|
||||||
++this.numUsers;
|
++this.numUsers;
|
||||||
addedUser = true;
|
addedUser = true;
|
||||||
socket.emit('login', {
|
|
||||||
|
// Broadcast to ALL sockets, including this one
|
||||||
|
thisIO.emit('user joined', {
|
||||||
|
username: socket.username,
|
||||||
numUsers: this.numUsers,
|
numUsers: this.numUsers,
|
||||||
users: this.users
|
users: this.users
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('user joined', () => {
|
|
||||||
// echo globally (all clients) that a person has connected
|
|
||||||
socket.broadcast.emit('user joined', {
|
|
||||||
username: socket.username,
|
|
||||||
numUsers: this.numUsers,
|
|
||||||
users: this.users
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// when the client emits 'typing', we broadcast it to others
|
// when the client emits 'typing', we broadcast it to others
|
||||||
socket.on('typing', () => {
|
socket.on('typing', () => {
|
||||||
socket.broadcast.emit('typing', {
|
socket.broadcast.emit('typing', {
|
||||||
@ -70,22 +66,20 @@ class Room {
|
|||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
if (addedUser) {
|
if (addedUser) {
|
||||||
--this.numUsers;
|
--this.numUsers;
|
||||||
|
this.users = _.without(this.users, socket.user);
|
||||||
this.users = _.without(this.users, socket.username);
|
|
||||||
|
|
||||||
// echo globally that this client has left
|
// echo globally that this client has left
|
||||||
socket.broadcast.emit('user left', {
|
socket.broadcast.emit('user left', {
|
||||||
username: socket.username,
|
username: socket.username,
|
||||||
numUsers: this.numUsers,
|
numUsers: this.numUsers,
|
||||||
users: this.users
|
users: this.users,
|
||||||
|
id: socket.user.id
|
||||||
});
|
});
|
||||||
|
|
||||||
// remove room from rooms array
|
// remove room from rooms array
|
||||||
if (this.numUsers === 0) {
|
if (this.numUsers === 0) {
|
||||||
this.emit('empty');
|
this.emit('empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.users = _.without(this.users, socket.username);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>darkwire.io - anonymous, encrypted chat</title>
|
<title>darkwire.io - encrypted web chat</title>
|
||||||
<meta name="description" content="darkwire.io is the simplest way to chat online anonymously.">
|
<meta name="description" content="darkwire.io is the simplest way to chat with encryption online.">
|
||||||
<link rel="shortcut icon" type="image/png" href="favicon.ico">
|
<link rel="shortcut icon" type="image/png" href="favicon.ico">
|
||||||
<link rel="stylesheet" href="/vendor/bootstrap-switch.min.css">
|
<link rel="stylesheet" href="/vendor/bootstrap-switch.min.css">
|
||||||
<link rel="stylesheet" href="/vendor/bootstrap.min.css">
|
<link rel="stylesheet" href="/vendor/bootstrap.min.css">
|
||||||
@ -50,7 +50,7 @@
|
|||||||
<div class="chatArea">
|
<div class="chatArea">
|
||||||
<ul class="messages">
|
<ul class="messages">
|
||||||
<li class="log">
|
<li class="log">
|
||||||
<p>Welcome to darkwire.io - anonymous, encrypted chat</p>
|
<p>Welcome to darkwire.io - encrypted chat</p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -69,11 +69,9 @@
|
|||||||
<h4 class="modal-title">About</h4>
|
<h4 class="modal-title">About</h4>
|
||||||
</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.</p>
|
<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>
|
||||||
|
|
||||||
<p>darkwire.io is the simplest way to chat anonymously, and 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>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>Your encryption passphrase ensures that only people you trust can decipher your messages. If you change your passphrase, make sure to notify all other participants.</p>
|
|
||||||
|
|
||||||
<p>Questions/comments? Email us at hello[at]darkwire.io</p>
|
<p>Questions/comments? Email us at hello[at]darkwire.io</p>
|
||||||
</div>
|
</div>
|
||||||
@ -97,20 +95,6 @@
|
|||||||
<textarea class="form-control share-text" rows="3" readonly id="settings-share-text" onclick="this.select()"></textarea>
|
<textarea class="form-control share-text" rows="3" readonly id="settings-share-text" onclick="this.select()"></textarea>
|
||||||
</p>
|
</p>
|
||||||
<br>
|
<br>
|
||||||
<h6>Edit Your Passphrase</h6>
|
|
||||||
<div class="read-key">
|
|
||||||
<span class="key" id="read-key"></span> <span class="glyphicon glyphicon-pencil"></span>
|
|
||||||
</div>
|
|
||||||
<div class="edit-key">
|
|
||||||
<div class="input-group">
|
|
||||||
<input class="form-control key" placeholder="Enter passphrase here" type="text" id="settings-key"></input>
|
|
||||||
<div class="input-group-btn">
|
|
||||||
<button class="btn btn-default" type="button" id='cancel-key-edit'>Cancel</button>
|
|
||||||
<button class="btn btn-primary" type="button" id='save-key-edit' disabled="disabled">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<h6>Sound</h6>
|
<h6>Sound</h6>
|
||||||
<input type="checkbox" name="my-checkbox" class="form-control bs-switch" checked>
|
<input type="checkbox" name="my-checkbox" class="form-control bs-switch" checked>
|
||||||
</div>
|
</div>
|
||||||
@ -129,8 +113,8 @@
|
|||||||
<h4 class="modal-title">Welcome to darkwire.io</h4>
|
<h4 class="modal-title">Welcome to darkwire.io</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
<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>
|
<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>
|
||||||
<p>We've placed you in a new chat room.</p>
|
|
||||||
<br>
|
<br>
|
||||||
<h6>Invite People to This Room</h6>
|
<h6>Invite People to This Room</h6>
|
||||||
<p>
|
<p>
|
||||||
@ -144,25 +128,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal fade" id="join-modal" tabindex="-1" role="dialog">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h4 class="modal-title">Welcome to darkwire.io</h4>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<h6>Enter Your Passphrase Below</h6>
|
|
||||||
<input class="form-control key" placeholder="Enter passphrase here" type="text" id='join-key'></input>
|
|
||||||
<br>
|
|
||||||
<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.</p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-primary encryption-active" disabled="disabled" data-dismiss="modal">Done</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal fade" id="participants-modal" tabindex="-1" role="dialog">
|
<div class="modal fade" id="participants-modal" tabindex="-1" role="dialog">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user