Switch to X25519 + ML-KEM-768 encryption

This commit is contained in:
olcxja 2026-05-27 20:40:24 +02:00
commit a660ba32bd
9 changed files with 180 additions and 76 deletions

View file

@ -78,14 +78,14 @@ function delay(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
async function packetEncPass(pass, key, username) {
return await encryptWithNonce(pass, key, getNonce(username, key));
async function packetEncPass(pass, key, accountId) {
return await encryptWithNonce(pass, key, getNonce(accountId, key));
}
async function getNonce(username, key) {
async function getNonce(accountId, key) {
let nonce;
let fetchRes = await (await fetch(`${url}/nextnonce?u=${username}`)).text();
let fetchRes = await (await fetch(`${url}/nextnonce?id=${accountId}`)).text();
try {
nonce = await decryptString(fetchRes, key);
@ -99,34 +99,33 @@ async function encryptWithNonce(value, key, nonce) {
return await encryptString(value, 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));
async function calcHybridSharedKeyClient(pubX25519ServerBase64, pubMlKemServerBase64) {
const pubX25519Server = base64ToUint8(pubX25519ServerBase64);
const pubMlKemServer = base64ToUint8(pubMlKemServerBase64);
let sharedSecretStr = sharedSecret.toString(16);
// X25519
const privX25519Client = window.x25519.utils.randomSecretKey();
const pubX25519Client = window.x25519.getPublicKey(privX25519Client);
const secretX25519 = window.x25519.getSharedSecret(privX25519Client, pubX25519Server);
if (sharedSecretStr.length % 2 !== 0) {
sharedSecretStr = '0' + sharedSecretStr;
}
// ML-KEM-768
const mlkem = new window.MlKem768();
const [ciphertextMlKem, secretMlKem] = await mlkem.encap(pubMlKemServer);
const sharedSecretBytes = new Uint8Array(sharedSecretStr.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
// Combine and Hash: SHA256(X25519_Secret || MLKEM_Secret)
const combined = new Uint8Array(secretX25519.length + secretMlKem.length);
combined.set(secretX25519);
combined.set(secretMlKem, secretX25519.length);
const hashBuffer = await window.crypto.subtle.digest('SHA-256', sharedSecretBytes);
const hashBuffer = await window.crypto.subtle.digest('SHA-256', combined);
const aesKey = new Uint8Array(hashBuffer);
return [pubClient.toString(), aesKey];
return [uint8ToBase64(pubX25519Client), uint8ToBase64(ciphertextMlKem), 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]
return [data.pubX25519, data.pubMlKem, data.idKey]
}
function base64ToUint8(base64) {
@ -264,7 +263,7 @@ async function fetchPost(url, value) {
method: "POST",
body: value,
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(username, passwordHash))
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash))
}
});
let data = await response.text();
@ -272,7 +271,7 @@ async function fetchPost(url, value) {
}
async function fetchPostEnc(url, value) {
let nonce = await getNonce(username, passwordHash);
let nonce = await getNonce(id, passwordHash);
let response = await fetch(url, {
method: "POST",
body: await encryptWithNonce(value, passwordHash, nonce),
@ -297,7 +296,7 @@ async function fetchAsyncWAuth(url) {
let response = await fetch(url, {
method: "GET",
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(username, passwordHash))
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash))
}
});
@ -310,16 +309,25 @@ async function getServerInfo(host) {
return JSON.parse(await fetchAsync(`${prot}//${host}/_larpix/serverinfo`));
}
async function Auth(username, password) {
let passwordHash = await hashSHA3_512(password);
let response = await fetch(`${url}/auth?u=${username}`, {
async function Auth(loginUsername, loginPassword) {
let resolvedId = await fetchAsync(`${url}/nametoid?u=${loginUsername}`);
if (!resolvedId || resolvedId.trim() === "" || resolvedId.startsWith("error:")) {
return "error:invalid.username.or.password";
}
let actualId = resolvedId.split(":")[0];
let passwordHash = await hashSHA3_512(loginPassword);
let response = await fetch(`${url}/auth?id=${actualId}`, {
method: "GET",
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(username, passwordHash))
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(actualId, passwordHash))
}
});
let data = await response.text();
if (data.startsWith("success:")) {
data += "|" + actualId;
}
return data;
}
@ -327,9 +335,9 @@ async function Auth(username, password) {
async function fetchEncrypted(request, body = "") {
let nonce = await getNonce(username, passwordHash);
let nonce = await getNonce(id, passwordHash);
let response = await fetch(`${url}/encryptedrequest?u=${username}`, {
let response = await fetch(`${url}/encryptedrequest?id=${id}`, {
method: "POST",
body: await encryptWithNonce(
JSON.stringify({
@ -514,10 +522,10 @@ function createAvatarSvg(name, size = 512) {
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
}
async function getAvatarUrl(username)
async function getAvatarUrl(id, username)
{
try {
let pfpUrl = `${url}/user/storage/public/getentry?u=${username}&e=larp.profile.pfp`;
let pfpUrl = `${url}/user/storage/public/getentry?id=${id}&e=larp.profile.pfp`;
if ((await fetchAsync(pfpUrl)) == "")
{
throw Error();
@ -656,6 +664,7 @@ function getLang() {
return (navigator.language || navigator.languages[0]);
}
var id = "";
var password = "";
var username = "";
var passwordHash = "";
@ -670,8 +679,23 @@ async function mainJS() {
lang = localStorage.getItem('lang');
}
if (host) {
await updateProtocolAndUrl(host);
} else {
await updateProtocolAndUrl(window.location.hostname);
}
if (!id && username) {
let resolvedId = await fetchAsync(`${url}/nametoid?u=${username}`);
if (resolvedId && !resolvedId.startsWith("error:")) {
id = resolvedId.split(":")[0];
localStorage.setItem("id", id);
}
}
await start();
}
id = localStorage.getItem('id');
username = localStorage.getItem('username');
password = localStorage.getItem('password');
host = localStorage.getItem('host');
@ -1024,7 +1048,7 @@ async function renderInvites(res, type) {
if (!(username && username.trim() !== "")) return;
count++;
let pfp = await getAvatarUrl(username);
let pfp = await getAvatarUrl(id, username);
let actions = "";
let desc = "";
@ -1032,17 +1056,17 @@ async function renderInvites(res, type) {
if (type === "received") {
desc = `:desc.invite.dm.received:${username};${id}`;
actions = `
<button class="icon-button" style="background-color: var(--big-green); border-color: transparent;" onclick="acceptInvite('${id}')">
<button class="icon-button" style="background-color: var(--big-green);" onclick="acceptInvite('${id}')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#fff"><path d="M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z"/></svg>
</button>
<button class="icon-button" style="background-color: var(--big-red); border-color: transparent;" onclick="declineInvite('${id}')">
<button class="icon-button" style="background-color: var(--big-red);" onclick="declineInvite('${id}')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#fff"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>
</button>
`;
} else {
desc = `:desc.invite.dm.sent:${username};${id}`;
actions = `
<button class="icon-button" style="background-color: var(--big-red); border-color: transparent;" onclick="revokeInvite('${id}')">
<button class="icon-button" style="background-color: var(--big-red);" onclick="revokeInvite('${id}')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#fff"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>
</button>
`;
@ -1096,10 +1120,15 @@ async function switchInvitesTab(tab) {
await loadInvites(tab);
}
async function acceptInvite(username) { //TODO
async function acceptInvite(targetId) { //TODO: Implement key generation
try {
showAction("action.invite.accepting", "invite.action");
let res = await fetchEncrypted("user/dm/create", username);
let payload = JSON.stringify({
string1: targetId,
string2: "", // TODO: Generate symmetric keys
string3: "" // TODO: Encrypt key for targetId
});
let res = await fetchEncrypted("user/dm/create", payload);
clearAction("invite.action");
showBlahNotification(res || "success:invite.accepted");
await loadInvites('received');
@ -1109,10 +1138,10 @@ async function acceptInvite(username) { //TODO
}
}
async function declineInvite(username) { //TODO
async function declineInvite(username) {
try {
showAction("action.invite.declining", "invite.action");
let res = await fetchEncrypted("user/dm/decline", username);
let res = await fetchEncrypted("user/dm/invite/decline", username);
clearAction("invite.action");
showBlahNotification(res || "success:invite.declined");
await loadInvites('received');
@ -1122,10 +1151,10 @@ async function declineInvite(username) { //TODO
}
}
async function revokeInvite(username) { //TODO
async function revokeInvite(username) {
try {
showAction("action.invite.revoking", "invite.action");
let res = await fetchEncrypted("user/dm/revoke", username);
let res = await fetchEncrypted("user/dm/invite/revoke", username);
clearAction("invite.action");
showBlahNotification(res || "success:invite.revoked");
await loadInvites('sent');