diff --git a/android/app/build.gradle b/android/app/build.gradle index 7204016e..efa09414 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "olcxja.miarven" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 6 - versionName "0.2.0" + versionCode 5 + versionName "0.1.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/android/app/src/main/assets/public/blah/en-cat.json b/android/app/src/main/assets/public/blah/en-cat.json index d3ffa2ed..01a784ae 100644 --- a/android/app/src/main/assets/public/blah/en-cat.json +++ b/android/app/src/main/assets/public/blah/en-cat.json @@ -233,7 +233,6 @@ "message.reaction.removed": "Purreaction remeowed", "no.invite": "No meowvite", "not.a.member": "Not a meowmber", - "removed.from.group": "You got hissed out of the clowder", "not.in.group": "Not in purroup", "reaction.not.found": "Purreaction not meownd", "rotation.not.required": "Purrotation not meowquired", diff --git a/android/app/src/main/assets/public/blah/en-us.json b/android/app/src/main/assets/public/blah/en-us.json index fc6eaaf1..fa67cf49 100644 --- a/android/app/src/main/assets/public/blah/en-us.json +++ b/android/app/src/main/assets/public/blah/en-us.json @@ -233,7 +233,6 @@ "message.reaction.removed": "Reaction removed", "no.invite": "No invite", "not.a.member": "Not a member", - "removed.from.group": "You were removed from the group", "not.in.group": "Not in group", "reaction.not.found": "Reaction not found", "rotation.not.required": "Rotation not required", diff --git a/android/app/src/main/assets/public/blah/pl-pl.json b/android/app/src/main/assets/public/blah/pl-pl.json index d8af39cc..68bed510 100644 --- a/android/app/src/main/assets/public/blah/pl-pl.json +++ b/android/app/src/main/assets/public/blah/pl-pl.json @@ -231,7 +231,6 @@ "message.reaction.removed": "Reakcja usunięta", "no.invite": "Brak zaproszenia", "not.a.member": "Nie jesteś członkiem", - "removed.from.group": "Zostałeś usunięty z grupy", "not.in.group": "Nie jesteś w grupie", "reaction.not.found": "Nie znaleziono reakcji", "rotation.not.required": "Rotacja niewymagana", diff --git a/android/app/src/main/assets/public/login/index.html b/android/app/src/main/assets/public/login/index.html index 9ef4bf68..111fc1b6 100644 --- a/android/app/src/main/assets/public/login/index.html +++ b/android/app/src/main/assets/public/login/index.html @@ -285,7 +285,7 @@ const registerHost = document.getElementById("reg-host"); const registerUsername = document.getElementById("reg-username"); const registerPassword = document.getElementById("reg-password"); - const registerPasswordConfirm = document.getElementById("reg-confirm"); + const registerPasswordConfirm = document.getElementById("reg-password"); const loginHost = document.getElementById("login-host"); const loginUsername = document.getElementById("login-username"); const loginPassword = document.getElementById("login-password"); @@ -382,13 +382,6 @@ var sharedkey = await calcHybridSharedKeyClient(dataarray[0], dataarray[1]); sharedpvkey = sharedkey[2]; createId = dataarray[2]; - //sól z init jest wymagana - hasło zawsze przez PBKDF2 - if (!dataarray[3]) { - showBlahNotification("error:something.wrong"); - authContainer.className = 'auth-container show-register'; - return; - } - let regCredHash = await deriveCredentialKdf(registerPassword.value, dataarray[3]); const captchaimage = await fetch(`${url}/createaccount?step=register`, { method: 'POST', body: JSON.stringify({ @@ -396,7 +389,7 @@ ciphertextMlKem: sharedkey[1], idKey: createId, username: await encrypt(registerUsername.value, sharedpvkey), - password: await encrypt(regCredHash, sharedpvkey) + password: await encrypt(await hashSHA3_512(registerPassword.value), sharedpvkey) }) }); const clone = captchaimage.clone(); diff --git a/android/app/src/main/assets/public/main.js b/android/app/src/main/assets/public/main.js index fa231a06..ff092762 100644 --- a/android/app/src/main/assets/public/main.js +++ b/android/app/src/main/assets/public/main.js @@ -83,9 +83,7 @@ async function packetEncPass(pass, key, accountId) { async function getNonce(accountId, key) { let nonce; - let fetchRes = await (await fetch(`${url}/nextnonce?id=${accountId}`, { - headers: {"X-Crypto": "g1"} - })).text(); + let fetchRes = await (await fetch(`${url}/nextnonce?id=${accountId}`)).text(); try { nonce = await decryptString(fetchRes, key); @@ -125,8 +123,7 @@ async function calcHybridSharedKeyClient(pubX25519ServerBase64, pubMlKemServerBa function keyDataFromServerJson(jsonFromServer) { const data = JSON.parse(jsonFromServer); - //0=pubX25519, 1=pubMlKem, 2=idKey, 3=kdfSalt (wymagany do PBKDF2 przy rejestracji) - return [data.pubX25519, data.pubMlKem, data.idKey, data.kdfSalt] + return [data.pubX25519, data.pubMlKem, data.idKey] } function base64ToUint8(base64) { @@ -708,20 +705,25 @@ async function decryptBytes(combinedBytes, passphrase) { 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-GCM'}, false, ['encrypt'] + 'raw', pwHash, {name: 'AES-CBC'}, false, ['encrypt'] ); - const iv = crypto.getRandomValues(new Uint8Array(12)); - const encrypted = new Uint8Array(await crypto.subtle.encrypt( - {name: 'AES-GCM', iv: iv}, + const iv = crypto.getRandomValues(new Uint8Array(16)); + const encrypted = await crypto.subtle.encrypt( + {name: 'AES-CBC', iv: iv}, key, - encoder.encode(plainText) - )); + data + ); - return "G1:" + uint8ToBase64(concatUint8(iv, encrypted)); + const combined = new Uint8Array(iv.length + encrypted.byteLength); + combined.set(iv); + combined.set(new Uint8Array(encrypted), iv.length); + + return uint8ToBase64(combined); } async function encryptBytesWithNonce(dataUint8, key, nonce) { @@ -730,44 +732,37 @@ async function encryptBytesWithNonce(dataUint8, key, nonce) { const pwHash = await crypto.subtle.digest('SHA-256', encoder.encode(passphrase)); const importedKey = await crypto.subtle.importKey( - 'raw', pwHash, {name: 'AES-GCM'}, false, ['encrypt'] + 'raw', pwHash, {name: 'AES-CBC'}, false, ['encrypt'] ); - const iv = crypto.getRandomValues(new Uint8Array(12)); - const encrypted = new Uint8Array(await crypto.subtle.encrypt( - {name: 'AES-GCM', iv: iv}, + const iv = crypto.getRandomValues(new Uint8Array(16)); + const encrypted = await crypto.subtle.encrypt( + {name: 'AES-CBC', iv: iv}, importedKey, dataUint8 - )); + ); - const prefix = encoder.encode("G1:"); - const combined = new Uint8Array(prefix.length + iv.length + encrypted.byteLength); - combined.set(prefix, 0); - combined.set(iv, prefix.length); - combined.set(encrypted, prefix.length + iv.length); + const combined = new Uint8Array(iv.length + encrypted.byteLength); + combined.set(iv); + combined.set(new Uint8Array(encrypted), iv.length); return combined; } async function decryptString(base64Text, passphrase) { - //tylko AES-GCM ("G1:") - pakiety bez prefiksu odrzucane - if (typeof base64Text !== "string" || !base64Text.startsWith("G1:")) { - throw new Error("error:invalid.cipher.format"); - } - const encoder = new TextEncoder(); - const combined = base64ToUint8(base64Text.substring(3)); + 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-GCM'}, false, ['decrypt'] + 'raw', pwHash, {name: 'AES-CBC'}, false, ['decrypt'] ); - const iv = combined.slice(0, 12); - const data = combined.slice(12); + const iv = combined.slice(0, 16); + const data = combined.slice(16); const decrypted = await crypto.subtle.decrypt( - {name: 'AES-GCM', iv: iv}, + {name: 'AES-CBC', iv: iv}, key, data ); @@ -789,7 +784,6 @@ async function fetchPost(url, value) { method: "POST", body: value, headers: { - "X-Crypto": "g1", "secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash)) } }); @@ -812,7 +806,6 @@ async function fetchPostEnc(url, value) { method: "POST", body: await encryptWithNonce(value, passwordHash, nonce), headers: { - "X-Crypto": "g1", "secret": await encryptWithNonce(passwordHash, passwordHash, nonce) } }); @@ -842,7 +835,6 @@ async function fetchAsyncWAuth(url) { let response = await fetch(url, { method: "GET", headers: { - "X-Crypto": "g1", "secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash)) } }); @@ -872,30 +864,17 @@ async function Auth(loginUsername, loginPassword) { } let actualId = resolvedId.split(":")[0]; - //sól KDF jest wymagana - konta bez soli nie mogą się zalogować - let saltRes = await fetchAsync(`${url}/kdfsalt?id=${actualId}`); - if (!saltRes || !saltRes.startsWith("P:")) { - return "error:invalid.username.or.password"; - } - let credHash = await deriveCredentialKdf(loginPassword, saltRes.substring(2).trim()); - + let passwordHash = await hashSHA3_512(loginPassword); let response = await fetch(`${url}/auth?id=${actualId}`, { method: "GET", headers: { - "X-Crypto": "g1", - "secret": await encryptWithNonce(credHash, credHash, await getNonce(actualId, credHash)) + "secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(actualId, passwordHash)) } }); let data = await response.text(); if (data.startsWith("success:")) { data += "|" + actualId; - //utrwal globale sesyjne - dalsze żądania w tej sesji mają od razu ważny hash - passwordHash = credHash; - if (!id) { - id = actualId; - try { localStorage.setItem("id", actualId); } catch (e) {} - } } return data; } finally { @@ -923,7 +902,6 @@ async function fetchEncrypted(request, body = "") { , passwordHash, nonce), headers: { - "X-Crypto": "g1", "secret": await encryptWithNonce(passwordHash, passwordHash, nonce) } }); @@ -951,7 +929,6 @@ async function fetchEncryptedUpload(target, fileBytesUint8) { method: "POST", body: await encryptBytesWithNonce(fileBytesUint8, passwordHash, nonce), headers: { - "X-Crypto": "g1", "secret": await encryptWithNonce(passwordHash, passwordHash, nonce), "target": await encryptWithNonce(target, passwordHash, nonce) } @@ -1069,35 +1046,13 @@ function clearAction(actionid) { } -//solony, wolny KDF - 600k iteracji PBKDF2-SHA256, wyjscie 512 bitow (128 hex) -async function deriveCredentialKdf(passwordPlain, saltB64) { +async function hashSHA3_512(input) { const encoder = new TextEncoder(); - const keyMaterial = await crypto.subtle.importKey( - "raw", - encoder.encode(passwordPlain), - {name: "PBKDF2"}, - false, - ["deriveBits"] - ); - const bits = await crypto.subtle.deriveBits( - {name: "PBKDF2", hash: "SHA-256", salt: base64ToUint8(saltB64), iterations: 600000}, - keyMaterial, - 512 - ); - return Array.from(new Uint8Array(bits)).map(b => b.toString(16).padStart(2, '0')).join(''); -} - -//zwraca null gdy brak konta/soli - celowo NIE rzuca, żeby nie wywalać bootu aplikacji -//(strona logowania i świeży użytkownik nie mają soli; start() sam zrobi Auth i przekieruje) -async function resolveCredentialHash(accountId) { - if (!accountId) return null; - try { - let res = await fetchAsync(`${url}/kdfsalt?id=${accountId}`); - if (res && res.startsWith("P:") && password) { - return await deriveCredentialKdf(password, res.substring(2).trim()); - } - } catch (e) {} - return null; + const data = encoder.encode(input); + const hashBuffer = await crypto.subtle.digest('SHA-512', data); //-3 kiedys xddddddddddddddddd + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + return hashHex; } //placeholder pfp @@ -1447,10 +1402,9 @@ async function mainJS() { } await initBlahs(); - //emotes.json leży w katalogu głównym - na stronie /login fetch by trafił w HTML - if (!window.location.pathname.includes("/login")) { - await loadEmotes(); - } + await loadEmotes(); + + passwordHash = await hashSHA3_512(password); if (host) { await updateProtocolAndUrl(host); @@ -1466,12 +1420,6 @@ async function mainJS() { } } - //hash tylko dla istniejącej sesji; brak soli/konta nie może blokować startu - - //start() i tak wykonuje własny Auth i przekieruje niezalogowanych na login - if (id && password) { - try { passwordHash = await resolveCredentialHash(id); } catch (e) { passwordHash = null; } - } - await start(); } @@ -1925,7 +1873,6 @@ async function openProfile(accountId) { try { if (!accountId) accountId = window.id; //var fallback - accountId = sanitizeId(accountId); if (accountId.endsWith(';' + host)) { accountId = accountId.substring(0, accountId.length - host.length - 1); @@ -1949,7 +1896,7 @@ async function openProfile(accountId) { let bio = await fetchAsync(`${url}/user/storage/public/getentry?id=${accountId}&e=larp.profile.bio`); if (bio.startsWith("error:")) bio = ""; - let bioHtml = bio && bio.trim() !== "" ? `