160 lines
5.9 KiB
JavaScript
160 lines
5.9 KiB
JavaScript
import { genKeys, sharedKey } from "./ecdh.js";
|
|
import { keccakAEAD } from "./aead.js";
|
|
import { keccakKDF } from "./kdf.js";
|
|
import { render_room, render_rooms_wrapper } from "./rooms.js";
|
|
|
|
const socket = io();
|
|
let secret = null;
|
|
let sharedsecret = {};
|
|
let dh_ratchets = {};
|
|
let sending_ratchets = {};
|
|
let receiving_ratchets = {};
|
|
|
|
render_rooms_wrapper();
|
|
|
|
function init_ratchets(order, user_pubkey) {
|
|
let dh_ratchet = new keccakKDF()
|
|
let key_1 = dh_ratchet.init(sharedsecret[user_pubkey], new Uint8Array(0));
|
|
let key_2 = dh_ratchet.next(new Uint8Array(0));
|
|
dh_ratchets[user_pubkey] = dh_ratchet;
|
|
let sending_ratchet = new keccakKDF();
|
|
let receiving_ratchet = new keccakKDF();
|
|
switch (order) {
|
|
case 0:
|
|
sending_ratchet.init(key_1, new Uint8Array(0));
|
|
receiving_ratchet.init(key_2, new Uint8Array(0));
|
|
break;
|
|
case 1:
|
|
sending_ratchet.init(key_2, new Uint8Array(0));
|
|
receiving_ratchet.init(key_1, new Uint8Array(0));
|
|
break;
|
|
}
|
|
sending_ratchets[user_pubkey] = sending_ratchet;
|
|
receiving_ratchets[user_pubkey] = receiving_ratchet;
|
|
|
|
}
|
|
|
|
socket.on('chat message', (msg, room, tag_received, iv, nonce, pubkey_received) => {
|
|
console.log(`received: ${msg}`);
|
|
let messages = document.getElementById(`messages-${room}`);
|
|
if (!messages) {
|
|
render_room(room, pubkey_received);
|
|
messages = document.getElementById(`messages-${room}`);
|
|
}
|
|
const associated_data = fromHexString(Array.from((document.getElementById('pubkey')).classList).find(className => className.startsWith('key-')).replace('key-', ''));
|
|
const pubkey = Array.from(messages.classList).find(className => className.startsWith('key-')).replace('key-', '');
|
|
let {plaintext, tag} = decrypt_message(msg, pubkey, fromHexString(iv), fromHexString(nonce), associated_data);
|
|
if (tag === tag_received && pubkey == pubkey_received) {
|
|
append_message(1, plaintext, messages);
|
|
} else {
|
|
append_message(2, "Corrupted message.", messages);
|
|
}
|
|
if (document.getElementById(`room-${room}`).style.display === 'block') {
|
|
window.scrollTo(0, document.body.scrollHeight);
|
|
}
|
|
});
|
|
|
|
socket.on('key exchange', (user_pubkey, pubkey, part) => {
|
|
let keys = null;
|
|
switch (part) {
|
|
case 0:
|
|
keys = genKeys();
|
|
secret = keys.privkey;
|
|
socket.emit('key exchange', user_pubkey, toHexString(keys.pubkey), 1);
|
|
break;
|
|
case 1:
|
|
keys = genKeys();
|
|
secret = keys.privkey
|
|
sharedsecret[user_pubkey] = sharedKey(secret, fromHexString(pubkey));
|
|
socket.emit('key exchange', user_pubkey, toHexString(keys.pubkey), 2);
|
|
console.log(`shared secret: ${toHexString(sharedsecret[user_pubkey])}`);
|
|
init_ratchets(0, user_pubkey);
|
|
break;
|
|
case 2:
|
|
sharedsecret[user_pubkey] = sharedKey(secret, fromHexString(pubkey));
|
|
console.log(`shared secret: ${toHexString(sharedsecret[user_pubkey])}`);
|
|
init_ratchets(1, user_pubkey);
|
|
break;
|
|
}
|
|
});
|
|
|
|
export function create_listener(form, input, messages) {
|
|
form.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
if (input.value) {
|
|
append_message(0, input.value, messages);
|
|
window.scrollTo(0, document.body.scrollHeight);
|
|
const pubkey = Array.from(form.classList).find(className => className.startsWith('key-')).replace('key-', '');
|
|
let {cipher, tag, iv, nonce} = encrypt_message(input.value, pubkey);
|
|
socket.emit('chat message', cipher, form.id, tag, iv, nonce);
|
|
input.value = '';
|
|
}
|
|
});
|
|
}
|
|
|
|
function encrypt_message(message, user_pubkey) {
|
|
let encryption_key = sending_ratchets[user_pubkey].next();
|
|
let encoded_msg = (new TextEncoder()).encode(message);
|
|
let iv = generateRandomUint8Array();
|
|
let nonce = generateRandomUint8Array();
|
|
let associated_data = fromHexString(user_pubkey);
|
|
let {cipher, tag} = keccakAEAD.encrypt(encryption_key, encoded_msg, iv, associated_data, nonce);
|
|
return {cipher: toHexString(cipher),
|
|
tag: toHexString(tag),
|
|
iv: toHexString(iv),
|
|
nonce: toHexString(nonce)
|
|
};
|
|
}
|
|
|
|
function decrypt_message(cipher, user_pubkey, iv, nonce, associated_data) {
|
|
let decryption_key = receiving_ratchets[user_pubkey].next();
|
|
let cipher_array = fromHexString(cipher);
|
|
let {plaintext, tag} = keccakAEAD.decrypt(decryption_key, cipher_array, iv, associated_data, nonce);
|
|
return {plaintext: (new TextDecoder('utf-8')).decode(plaintext),
|
|
tag: toHexString(tag)
|
|
};
|
|
}
|
|
|
|
function generateRandomUint8Array(length = 16) {
|
|
const randomArray = new Uint8Array(length);
|
|
window.crypto.getRandomValues(randomArray);
|
|
return randomArray;
|
|
}
|
|
|
|
function append_message(type, text, messageUl) {
|
|
const item = document.createElement('li');
|
|
const bubble = document.createElement('div');
|
|
bubble.textContent = text;
|
|
switch (type) {
|
|
case 0:
|
|
bubble.className = "sent-msg";
|
|
item.className = "sent";
|
|
break;
|
|
case 1:
|
|
bubble.className = "received-msg";
|
|
item.className = "received";
|
|
break;
|
|
case 2:
|
|
bubble.className = "corrupted-msg";
|
|
item.className = "corrupted";
|
|
break;
|
|
}
|
|
item.appendChild(bubble);
|
|
messageUl.appendChild(item);
|
|
}
|
|
|
|
const fromHexString = (hexString) =>
|
|
Uint8Array.from(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
|
|
|
|
const toHexString = (bytes) =>
|
|
bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
|
|
|
|
export async function reconnectSocket() {
|
|
socket.disconnect();
|
|
console.log("Socket disconnected.");
|
|
setTimeout(() => {
|
|
socket.connect();
|
|
console.log("Socket reconnected.");
|
|
}, 100);
|
|
}
|