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
14
android/app/src/main/assets/public/crypto-pq.js
Normal file
14
android/app/src/main/assets/public/crypto-pq.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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
14
webroot/crypto-pq.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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:")) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue