LarpixClient/main.js
2026-04-28 23:49:06 +02:00

275 lines
7.7 KiB
JavaScript

const collapseDmsBtn = document.getElementById("collapse-dms");
const sidebarHome = document.getElementById("sidebar-home");
const sidebarHomeButton = sidebarHome.children.item(1);
const sidebarHomeIndicator = sidebarHome.children.item(0);
const sidebarAdd = document.getElementById("sidebar-add");
const sidebarAddButton = sidebarAdd.children.item(1);
const sidebarAddIndicator = sidebarAdd.children.item(0);
function delay(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
async function packetEncPass(pass, key, username) {
let nonce;
let fetchRes = await fetchAsync(`${url}/nextnonce?u=${username}`);
try {
nonce = await decryptString(fetchRes, key);
}
catch(err) {
nonce = await decryptString(fetchRes, "");
}
return await encryptString(pass, nonce + key);
}
async function calcCommunicationKeyClient(p, g, pubServer) {
const secretClientBytes = window.crypto.getRandomValues(new Uint8Array(512));
const secretClient = BigInt('0x' + Array.from(secretClientBytes).map(b => b.toString(16).padStart(2, '0')).join(''));
const pubClient = power(BigInt(g), secretClient, BigInt(p));
const sharedSecret = power(BigInt(pubServer), secretClient, BigInt(p));
let sharedSecretStr = sharedSecret.toString(16);
if (sharedSecretStr.length % 2 !== 0) {
sharedSecretStr = '0' + sharedSecretStr;
}
const sharedSecretBytes = new Uint8Array(sharedSecretStr.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
const hashBuffer = await window.crypto.subtle.digest('SHA-256', sharedSecretBytes);
const aesKey = new Uint8Array(hashBuffer);
return [pubClient.toString(), aesKey];
}
function keyDataFromServerJson(jsonFromServer) {
const data = JSON.parse(jsonFromServer);
const p = BigInt(data.p);
const g = BigInt(data.g);
const pubServer = BigInt(data.pubServer);
return [p, g, pubServer, data.idKey]
}
const base64ToUint8 = (base64) => Uint8Array.from(atob(base64), c => c.charCodeAt(0));
const uint8ToBase64 = (uint8) => fixBase64Padding(btoa(String.fromCharCode(...uint8)));
async function encrypt(plainText, keyBytes) {
const iv = window.crypto.getRandomValues(new Uint8Array(16));
const encoder = new TextEncoder();
const data = encoder.encode(plainText);
const cryptoKey = await window.crypto.subtle.importKey(
"raw", keyBytes, "AES-CBC", false, ["encrypt"]
);
const encryptedContent = await window.crypto.subtle.encrypt(
{name: "AES-CBC", iv: iv},
cryptoKey,
data
);
const result = new Uint8Array(iv.length + encryptedContent.byteLength);
result.set(iv);
result.set(new Uint8Array(encryptedContent), iv.length);
return uint8ToBase64(result);
}
async function decrypt(cipherTextBase64, keyBytes) {
const fullData = base64ToUint8(cipherTextBase64);
const iv = fullData.slice(0, 16);
const cipherData = fullData.slice(16);
const cryptoKey = await window.crypto.subtle.importKey(
"raw", keyBytes, "AES-CBC", false, ["decrypt"]
);
const decrypted = await window.crypto.subtle.decrypt(
{name: "AES-CBC", iv: iv},
cryptoKey,
cipherData
);
return new TextDecoder().decode(decrypted);
}
async function getCryptoKey(passphrase) {
const encoder = new TextEncoder();
const data = encoder.encode(passphrase);
const hash = await crypto.subtle.digest('SHA-256', data);
return await crypto.subtle.importKey('raw', hash, {name: 'AES-CBC'}, false, ['encrypt', 'decrypt']);
}
async function encryptBytes(plainBytes, passphrase) {
const key = await getCryptoKey(passphrase);
const iv = crypto.getRandomValues(new Uint8Array(16));
const encryptedContent = await crypto.subtle.encrypt(
{name: 'AES-CBC', iv: iv},
key,
plainBytes
);
const result = new Uint8Array(iv.length + encryptedContent.byteLength);
result.set(iv);
result.set(new Uint8Array(encryptedContent), iv.length);
return result;
}
async function decryptBytes(combinedBytes, passphrase) {
const key = await getCryptoKey(passphrase);
const iv = combinedBytes.slice(0, 16);
const data = combinedBytes.slice(16);
const decryptedContent = await crypto.subtle.decrypt(
{name: 'AES-CBC', iv: iv},
key,
data
);
return new Uint8Array(decryptedContent);
}
async function encryptString(plainText, passphrase) {
const encoder = new TextEncoder();
const data = encoder.encode(plainText);
const pwHash = await crypto.subtle.digest('SHA-256', encoder.encode(passphrase));
const key = await crypto.subtle.importKey(
'raw', pwHash, {name: 'AES-CBC'}, false, ['encrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(16));
const encrypted = await crypto.subtle.encrypt(
{name: 'AES-CBC', iv: iv},
key,
data
);
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
return fixBase64Padding(btoa(String.fromCharCode(...combined)));
}
async function decryptString(base64Text, passphrase) {
const encoder = new TextEncoder();
const combined = new Uint8Array(atob(base64Text).split("").map(c => c.charCodeAt(0)));
const pwHash = await crypto.subtle.digest('SHA-256', encoder.encode(passphrase));
const key = await crypto.subtle.importKey(
'raw', pwHash, {name: 'AES-CBC'}, false, ['decrypt']
);
const iv = combined.slice(0, 16);
const data = combined.slice(16);
const decrypted = await crypto.subtle.decrypt(
{name: 'AES-CBC', iv: iv},
key,
data
);
return new TextDecoder().decode(decrypted);
}
async function fetchpost(url, value) {
let response = await fetch(url, {
method: "POST",
credentials: "omit",
body: value,
headers: {
"secret": passwordHash
}
});
let data = await response.text();
return data;
}
async function fetchAsync(url, sendSecret) {
let response;
if (sendSecret) {
response = await fetch(url, {
method: "GET", credentials: "omit", headers: {
"secret": passwordHash
}
});
}
else
{
response = await fetch(url, {method: "GET", credentials: "omit"});
}
let data = await response.text();
return data;
}
async function fetchEncrypted(request, body)
{
return await decryptString(
await fetchpost(`${url}/encryptedrequest?u=${username}`, await packetEncPass(
JSON.stringify({
string1: request,
string2: body
})
, passwordHash, username))
, passwordHash);
}
function power(base, exponent, mod) {
let res = 1n;
base = base % mod;
while (exponent > 0n) {
if (exponent % 2n === 1n) res = (res * base) % mod;
base = (base * base) % mod;
exponent = exponent / 2n;
}
return res;
}
function fixBase64Padding(base64String) {
let str = base64String.replace(/-/g, '+').replace(/_/g, '/');
const pad = str.length % 4;
if (pad) {
if (pad === 1) {
throw new Error("");
}
str += '='.repeat(4 - pad);
}
return str;
}
collapseDmsBtn.addEventListener("click", () => {
collapseDmsBtn.classList.toggle("collapsed");
});
sidebarHomeButton.addEventListener("mouseenter", () => {
sidebarHomeIndicator.classList.add("hover");
});
sidebarHomeButton.addEventListener("mouseleave", () => {
sidebarHomeIndicator.classList.remove("hover");
});
sidebarAddButton.addEventListener("mouseenter", () => {
sidebarAddIndicator.classList.add("hover");
});
sidebarAddButton.addEventListener("mouseleave", () => {
sidebarAddIndicator.classList.remove("hover");
});