forked from olcxjas-softworks/LarpixClient
Switch to X25519 + ML-KEM-768 encryption
This commit is contained in:
parent
9e6d128839
commit
a660ba32bd
9 changed files with 180 additions and 76 deletions
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue