Switch to X25519 + ML-KEM-768 encryption
All checks were successful
Android Build / publish (push) Successful in 28s
Linux Build / publish (push) Successful in 52s

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

File diff suppressed because one or more lines are too long

View file

@ -6,6 +6,7 @@
<title>Miarven</title> <title>Miarven</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<link rel="icon" type="image/svg+xml" href="favicon.svg"> <link rel="icon" type="image/svg+xml" href="favicon.svg">
<script src="crypto-pq.js"></script>
</head> </head>
<body> <body>
<loading> <loading>
@ -94,7 +95,9 @@
{ {
await updateProtocolAndUrl(window.location.hostname); await updateProtocolAndUrl(window.location.hostname);
} }
sidebarPfp.src = await getAvatarUrl(`${username}:${host}`); let localIdRes = await fetchAsync(`${url}/nametoid?u=${username}`);
let localId = localIdRes.trim();
sidebarPfp.src = await getAvatarUrl(localId, `${username}:${host}`);
showAction("action.auth", "startauth"); showAction("action.auth", "startauth");
let res = await Auth(username, password); let res = await Auth(username, password);
@ -134,7 +137,15 @@
async function addDm() { async function addDm() {
try { try {
showAction("action.dm.adding", "dmadd"); showAction("action.dm.adding", "dmadd");
let res = await fetchEncrypted("user/dm/invite", document.getElementById("addchat-username").value); let username = document.getElementById("addchat-username").value;
let idRes = await fetchAsync(`${url}/nametoid?u=${username}`);
if (idRes.startsWith("error") || idRes.trim() === "0" || idRes.trim() === "") {
clearAction("dmadd");
showBlahNotification("error:user.not.found");
return;
}
let targetId = idRes.trim();
let res = await fetchEncrypted("user/dm/invite", targetId);
clearAction("dmadd"); clearAction("dmadd");
showBlahNotification(res); showBlahNotification(res);

View file

@ -6,6 +6,7 @@
<title>Miarven - Login</title> <title>Miarven - Login</title>
<link rel="stylesheet" href="../style.css"> <link rel="stylesheet" href="../style.css">
<link rel="icon" type="image/svg+xml" href="../favicon.svg"> <link rel="icon" type="image/svg+xml" href="../favicon.svg">
<script src="../crypto-pq.js"></script>
<style> <style>
html { html {
font-size: max(17px, calc(100vw / 100)); font-size: max(17px, calc(100vw / 100));
@ -321,16 +322,20 @@
formLogin.addEventListener('submit', async (e) => { formLogin.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
await updateProtocolAndUrl(loginHost.value);
let res = await Auth(loginUsername.value, loginPassword.value); let res = await Auth(loginUsername.value, loginPassword.value);
if (res.startsWith("success:")) if (res.startsWith("success:"))
{ {
showBlahNotification(res); let parts = res.split("|");
showBlahNotification(parts[0]);
await delay(800); await delay(800);
localStorage.setItem("username", loginUsername.value); localStorage.setItem("username", loginUsername.value);
localStorage.setItem("password", loginPassword.value); localStorage.setItem("password", loginPassword.value);
localStorage.setItem("host", loginHost.value); localStorage.setItem("host", loginHost.value);
if (parts.length > 1) {
localStorage.setItem("id", parts[1]);
}
await loadingFadeIn(); await loadingFadeIn();
@ -371,14 +376,18 @@
container.className = 'auth-container show-register'; container.className = 'auth-container show-register';
return; return;
} }
await updateProtocolAndUrl(registerHost.value);
let dataarray = keyDataFromServerJson(await fetchAsync(`${url}/createaccount?step=init`)); let dataarray = keyDataFromServerJson(await fetchAsync(`${url}/createaccount?step=init`));
var sharedkey = await calcCommunicationKeyClient(dataarray[0], dataarray[1], dataarray[2]); var sharedkey = await calcHybridSharedKeyClient(dataarray[0], dataarray[1]);
sharedpvkey = sharedkey[1]; sharedpvkey = sharedkey[2];
createId = dataarray[3]; createId = dataarray[2];
const captchaimage = await fetch(`${url}/createaccount?step=register`, { const captchaimage = await fetch(`${url}/createaccount?step=register`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
pubClient: sharedkey[0], pubX25519: sharedkey[0],
ciphertextMlKem: sharedkey[1],
idKey: createId, idKey: createId,
username: await encrypt(registerUsername.value, sharedpvkey), username: await encrypt(registerUsername.value, sharedpvkey),
password: await encrypt(await hashSHA3_512(registerPassword.value), sharedpvkey) password: await encrypt(await hashSHA3_512(registerPassword.value), sharedpvkey)
@ -428,6 +437,12 @@
if (res.startsWith("success")) { if (res.startsWith("success")) {
showBlahNotification(res); showBlahNotification(res);
await delay(1000); await delay(1000);
let resolvedId = await fetchAsync(`${url}/nametoid?u=${registerUsername.value}`);
if (resolvedId && !resolvedId.startsWith("error:")) {
localStorage.setItem("id", resolvedId.split(":")[0]);
}
localStorage.setItem("username", registerUsername.value); localStorage.setItem("username", registerUsername.value);
localStorage.setItem("password", registerPassword.value); localStorage.setItem("password", registerPassword.value);
localStorage.setItem("host", registerHost.value); localStorage.setItem("host", registerHost.value);

View file

@ -78,14 +78,14 @@ function delay(time) {
return new Promise(resolve => setTimeout(resolve, time)); return new Promise(resolve => setTimeout(resolve, time));
} }
async function packetEncPass(pass, key, username) { async function packetEncPass(pass, key, accountId) {
return await encryptWithNonce(pass, key, getNonce(username, key)); return await encryptWithNonce(pass, key, getNonce(accountId, key));
} }
async function getNonce(username, key) { async function getNonce(accountId, key) {
let nonce; let nonce;
let fetchRes = await (await fetch(`${url}/nextnonce?u=${username}`)).text(); let fetchRes = await (await fetch(`${url}/nextnonce?id=${accountId}`)).text();
try { try {
nonce = await decryptString(fetchRes, key); nonce = await decryptString(fetchRes, key);
@ -99,34 +99,33 @@ async function encryptWithNonce(value, key, nonce) {
return await encryptString(value, nonce + key); return await encryptString(value, nonce + key);
} }
async function calcCommunicationKeyClient(p, g, pubServer) { async function calcHybridSharedKeyClient(pubX25519ServerBase64, pubMlKemServerBase64) {
const secretClientBytes = window.crypto.getRandomValues(new Uint8Array(512)); const pubX25519Server = base64ToUint8(pubX25519ServerBase64);
const secretClient = BigInt('0x' + Array.from(secretClientBytes).map(b => b.toString(16).padStart(2, '0')).join('')); const pubMlKemServer = base64ToUint8(pubMlKemServerBase64);
const pubClient = power(BigInt(g), secretClient, BigInt(p));
const sharedSecret = power(BigInt(pubServer), secretClient, BigInt(p));
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) { // ML-KEM-768
sharedSecretStr = '0' + sharedSecretStr; 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); const aesKey = new Uint8Array(hashBuffer);
return [pubClient.toString(), aesKey]; return [uint8ToBase64(pubX25519Client), uint8ToBase64(ciphertextMlKem), aesKey];
} }
function keyDataFromServerJson(jsonFromServer) { function keyDataFromServerJson(jsonFromServer) {
const data = JSON.parse(jsonFromServer); const data = JSON.parse(jsonFromServer);
return [data.pubX25519, data.pubMlKem, data.idKey]
const p = BigInt(data.p);
const g = BigInt(data.g);
const pubServer = BigInt(data.pubServer);
return [p, g, pubServer, data.idKey]
} }
function base64ToUint8(base64) { function base64ToUint8(base64) {
@ -264,7 +263,7 @@ async function fetchPost(url, value) {
method: "POST", method: "POST",
body: value, body: value,
headers: { headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(username, passwordHash)) "secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash))
} }
}); });
let data = await response.text(); let data = await response.text();
@ -272,7 +271,7 @@ async function fetchPost(url, value) {
} }
async function fetchPostEnc(url, value) { async function fetchPostEnc(url, value) {
let nonce = await getNonce(username, passwordHash); let nonce = await getNonce(id, passwordHash);
let response = await fetch(url, { let response = await fetch(url, {
method: "POST", method: "POST",
body: await encryptWithNonce(value, passwordHash, nonce), body: await encryptWithNonce(value, passwordHash, nonce),
@ -297,7 +296,7 @@ async function fetchAsyncWAuth(url) {
let response = await fetch(url, { let response = await fetch(url, {
method: "GET", method: "GET",
headers: { 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`)); return JSON.parse(await fetchAsync(`${prot}//${host}/_larpix/serverinfo`));
} }
async function Auth(username, password) { async function Auth(loginUsername, loginPassword) {
let passwordHash = await hashSHA3_512(password); let resolvedId = await fetchAsync(`${url}/nametoid?u=${loginUsername}`);
let response = await fetch(`${url}/auth?u=${username}`, { 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", method: "GET",
headers: { headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(username, passwordHash)) "secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(actualId, passwordHash))
} }
}); });
let data = await response.text(); let data = await response.text();
if (data.startsWith("success:")) {
data += "|" + actualId;
}
return data; return data;
} }
@ -327,9 +335,9 @@ async function Auth(username, password) {
async function fetchEncrypted(request, body = "") { 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", method: "POST",
body: await encryptWithNonce( body: await encryptWithNonce(
JSON.stringify({ JSON.stringify({
@ -514,10 +522,10 @@ function createAvatarSvg(name, size = 512) {
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`; return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
} }
async function getAvatarUrl(username) async function getAvatarUrl(id, username)
{ {
try { 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)) == "") if ((await fetchAsync(pfpUrl)) == "")
{ {
throw Error(); throw Error();
@ -656,6 +664,7 @@ function getLang() {
return (navigator.language || navigator.languages[0]); return (navigator.language || navigator.languages[0]);
} }
var id = "";
var password = ""; var password = "";
var username = ""; var username = "";
var passwordHash = ""; var passwordHash = "";
@ -670,8 +679,23 @@ async function mainJS() {
lang = localStorage.getItem('lang'); 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(); await start();
} }
id = localStorage.getItem('id');
username = localStorage.getItem('username'); username = localStorage.getItem('username');
password = localStorage.getItem('password'); password = localStorage.getItem('password');
host = localStorage.getItem('host'); host = localStorage.getItem('host');
@ -1024,7 +1048,7 @@ async function renderInvites(res, type) {
if (!(username && username.trim() !== "")) return; if (!(username && username.trim() !== "")) return;
count++; count++;
let pfp = await getAvatarUrl(username); let pfp = await getAvatarUrl(id, username);
let actions = ""; let actions = "";
let desc = ""; let desc = "";
@ -1032,17 +1056,17 @@ async function renderInvites(res, type) {
if (type === "received") { if (type === "received") {
desc = `:desc.invite.dm.received:${username};${id}`; desc = `:desc.invite.dm.received:${username};${id}`;
actions = ` 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> <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>
<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> <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> </button>
`; `;
} else { } else {
desc = `:desc.invite.dm.sent:${username};${id}`; desc = `:desc.invite.dm.sent:${username};${id}`;
actions = ` 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> <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> </button>
`; `;
@ -1096,10 +1120,15 @@ async function switchInvitesTab(tab) {
await loadInvites(tab); await loadInvites(tab);
} }
async function acceptInvite(username) { //TODO async function acceptInvite(targetId) { //TODO: Implement key generation
try { try {
showAction("action.invite.accepting", "invite.action"); 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"); clearAction("invite.action");
showBlahNotification(res || "success:invite.accepted"); showBlahNotification(res || "success:invite.accepted");
await loadInvites('received'); await loadInvites('received');
@ -1109,10 +1138,10 @@ async function acceptInvite(username) { //TODO
} }
} }
async function declineInvite(username) { //TODO async function declineInvite(username) {
try { try {
showAction("action.invite.declining", "invite.action"); 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"); clearAction("invite.action");
showBlahNotification(res || "success:invite.declined"); showBlahNotification(res || "success:invite.declined");
await loadInvites('received'); await loadInvites('received');
@ -1122,10 +1151,10 @@ async function declineInvite(username) { //TODO
} }
} }
async function revokeInvite(username) { //TODO async function revokeInvite(username) {
try { try {
showAction("action.invite.revoking", "invite.action"); 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"); clearAction("invite.action");
showBlahNotification(res || "success:invite.revoked"); showBlahNotification(res || "success:invite.revoked");
await loadInvites('sent'); await loadInvites('sent');

View file

@ -15,8 +15,8 @@
--border-width: 0.09rem; --border-width: 0.09rem;
--big-default: rgb(207, 207, 207); --big-default: rgb(207, 207, 207);
--big-red: rgb(195, 75, 75); --big-red: hsl(0, 60%, 55%);
--big-green: rgb(75, 165, 95); --big-green: hsl(120, 60%, 55%);
} }
*:focus { *:focus {
outline: none; outline: none;
@ -544,6 +544,7 @@ space{
width: 2.8rem; width: 2.8rem;
height: 2.8rem; height: 2.8rem;
border-radius: 0.8rem; border-radius: 0.8rem;
border: var(--border-width) solid rgba(255, 255, 255, 0.2);
object-fit: cover; object-fit: cover;
} }
.invite-details { .invite-details {
@ -565,6 +566,9 @@ space{
} }
.invite-actions button { .invite-actions button {
margin: 0; margin: 0;
border-color: rgba(255, 255, 255, 0.2);
width: 3.1rem;
height: 3.1rem;
} }
@media (hover: hover) { @media (hover: hover) {
@ -591,11 +595,17 @@ space{
.context-menu button:hover { .context-menu button:hover {
background-color: rgba(255, 255, 255, 0.08); background-color: rgba(255, 255, 255, 0.08);
} }
.invite-actions button:hover {
filter: brightness(0.93);
}
} }
button:active, button.is-pressed, input:active, input.is-pressed { button:active, button.is-pressed, input:active, input.is-pressed {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
transform: var(--press-scale); transform: var(--press-scale);
} }
.invite-actions button:active {
filter: brightness(0.85);
}
button.active, input.active { button.active, input.active {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }

14
webroot/crypto-pq.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -6,6 +6,7 @@
<title>Miarven</title> <title>Miarven</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<link rel="icon" type="image/svg+xml" href="favicon.svg"> <link rel="icon" type="image/svg+xml" href="favicon.svg">
<script src="crypto-pq.js"></script>
</head> </head>
<body> <body>
<loading> <loading>

View file

@ -6,6 +6,7 @@
<title>Miarven - Login</title> <title>Miarven - Login</title>
<link rel="stylesheet" href="../style.css"> <link rel="stylesheet" href="../style.css">
<link rel="icon" type="image/svg+xml" href="../favicon.svg"> <link rel="icon" type="image/svg+xml" href="../favicon.svg">
<script src="../crypto-pq.js"></script>
<style> <style>
html { html {
font-size: max(17px, calc(100vw / 100)); font-size: max(17px, calc(100vw / 100));
@ -321,7 +322,7 @@
formLogin.addEventListener('submit', async (e) => { formLogin.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
await updateProtocolAndUrl(loginHost.value);
let res = await Auth(loginUsername.value, loginPassword.value); let res = await Auth(loginUsername.value, loginPassword.value);
if (res.startsWith("success:")) if (res.startsWith("success:"))
@ -375,14 +376,18 @@
container.className = 'auth-container show-register'; container.className = 'auth-container show-register';
return; return;
} }
await updateProtocolAndUrl(registerHost.value);
let dataarray = keyDataFromServerJson(await fetchAsync(`${url}/createaccount?step=init`)); let dataarray = keyDataFromServerJson(await fetchAsync(`${url}/createaccount?step=init`));
var sharedkey = await calcCommunicationKeyClient(dataarray[0], dataarray[1], dataarray[2]); var sharedkey = await calcHybridSharedKeyClient(dataarray[0], dataarray[1]);
sharedpvkey = sharedkey[1]; sharedpvkey = sharedkey[2];
createId = dataarray[3]; createId = dataarray[2];
const captchaimage = await fetch(`${url}/createaccount?step=register`, { const captchaimage = await fetch(`${url}/createaccount?step=register`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
pubClient: sharedkey[0], pubX25519: sharedkey[0],
ciphertextMlKem: sharedkey[1],
idKey: createId, idKey: createId,
username: await encrypt(registerUsername.value, sharedpvkey), username: await encrypt(registerUsername.value, sharedpvkey),
password: await encrypt(await hashSHA3_512(registerPassword.value), sharedpvkey) password: await encrypt(await hashSHA3_512(registerPassword.value), sharedpvkey)

View file

@ -99,34 +99,33 @@ async function encryptWithNonce(value, key, nonce) {
return await encryptString(value, nonce + key); return await encryptString(value, nonce + key);
} }
async function calcCommunicationKeyClient(p, g, pubServer) { async function calcHybridSharedKeyClient(pubX25519ServerBase64, pubMlKemServerBase64) {
const secretClientBytes = window.crypto.getRandomValues(new Uint8Array(512)); const pubX25519Server = base64ToUint8(pubX25519ServerBase64);
const secretClient = BigInt('0x' + Array.from(secretClientBytes).map(b => b.toString(16).padStart(2, '0')).join('')); const pubMlKemServer = base64ToUint8(pubMlKemServerBase64);
const pubClient = power(BigInt(g), secretClient, BigInt(p));
const sharedSecret = power(BigInt(pubServer), secretClient, BigInt(p));
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) { // ML-KEM-768
sharedSecretStr = '0' + sharedSecretStr; 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); const aesKey = new Uint8Array(hashBuffer);
return [pubClient.toString(), aesKey]; return [uint8ToBase64(pubX25519Client), uint8ToBase64(ciphertextMlKem), aesKey];
} }
function keyDataFromServerJson(jsonFromServer) { function keyDataFromServerJson(jsonFromServer) {
const data = JSON.parse(jsonFromServer); const data = JSON.parse(jsonFromServer);
return [data.pubX25519, data.pubMlKem, data.idKey]
const p = BigInt(data.p);
const g = BigInt(data.g);
const pubServer = BigInt(data.pubServer);
return [p, g, pubServer, data.idKey]
} }
function base64ToUint8(base64) { function base64ToUint8(base64) {
@ -680,6 +679,12 @@ async function mainJS() {
lang = localStorage.getItem('lang'); lang = localStorage.getItem('lang');
} }
if (host) {
await updateProtocolAndUrl(host);
} else {
await updateProtocolAndUrl(window.location.hostname);
}
if (!id && username) { if (!id && username) {
let resolvedId = await fetchAsync(`${url}/nametoid?u=${username}`); let resolvedId = await fetchAsync(`${url}/nametoid?u=${username}`);
if (resolvedId && !resolvedId.startsWith("error:")) { if (resolvedId && !resolvedId.startsWith("error:")) {