commit ed3ed8dab772d766a2e1c73792f56174c3d3b90a Author: olcxja Date: Tue Apr 28 23:49:06 2026 +0200 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..62c89355 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 00000000..4e506b0c --- /dev/null +++ b/index.html @@ -0,0 +1,61 @@ + + + + + + Larpix Client + + + + + + + + + + +
+ + + + + +
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/login/index.html b/login/index.html new file mode 100644 index 00000000..e44650a6 --- /dev/null +++ b/login/index.html @@ -0,0 +1,245 @@ + + + + + + Larpix - Authentication + + + + + + +
+
+ +
+
+

Larpix

+

Sign in to your secure instance.

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+ +
+
+

Join Larpix

+

Start your encrypted journey today.

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+ +
+
+ + + + + \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 00000000..06515227 --- /dev/null +++ b/main.js @@ -0,0 +1,275 @@ +const collapseDmsBtn = document.getElementById("collapse-dms"); + +const sidebarHome = document.getElementById("sidebar-home"); +const sidebarHomeButton = sidebarHome.children.item(1); +const sidebarHomeIndicator = sidebarHome.children.item(0); + +const sidebarAdd = document.getElementById("sidebar-add"); +const sidebarAddButton = sidebarAdd.children.item(1); +const sidebarAddIndicator = sidebarAdd.children.item(0); + + + +function delay(time) { + return new Promise(resolve => setTimeout(resolve, time)); +} +async function packetEncPass(pass, key, username) { + let nonce; + let fetchRes = await fetchAsync(`${url}/nextnonce?u=${username}`); + try { + nonce = await decryptString(fetchRes, key); + } + catch(err) { + nonce = await decryptString(fetchRes, ""); + } + + return await encryptString(pass, 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)); + + let sharedSecretStr = sharedSecret.toString(16); + + if (sharedSecretStr.length % 2 !== 0) { + sharedSecretStr = '0' + sharedSecretStr; + } + + const sharedSecretBytes = new Uint8Array(sharedSecretStr.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); + + const hashBuffer = await window.crypto.subtle.digest('SHA-256', sharedSecretBytes); + const aesKey = new Uint8Array(hashBuffer); + + return [pubClient.toString(), 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] +} + +const base64ToUint8 = (base64) => Uint8Array.from(atob(base64), c => c.charCodeAt(0)); +const uint8ToBase64 = (uint8) => fixBase64Padding(btoa(String.fromCharCode(...uint8))); + +async function encrypt(plainText, keyBytes) { + const iv = window.crypto.getRandomValues(new Uint8Array(16)); + const encoder = new TextEncoder(); + const data = encoder.encode(plainText); + + const cryptoKey = await window.crypto.subtle.importKey( + "raw", keyBytes, "AES-CBC", false, ["encrypt"] + ); + + const encryptedContent = await window.crypto.subtle.encrypt( + {name: "AES-CBC", iv: iv}, + cryptoKey, + data + ); + const result = new Uint8Array(iv.length + encryptedContent.byteLength); + result.set(iv); + result.set(new Uint8Array(encryptedContent), iv.length); + + return uint8ToBase64(result); +} + +async function decrypt(cipherTextBase64, keyBytes) { + const fullData = base64ToUint8(cipherTextBase64); + const iv = fullData.slice(0, 16); + const cipherData = fullData.slice(16); + + const cryptoKey = await window.crypto.subtle.importKey( + "raw", keyBytes, "AES-CBC", false, ["decrypt"] + ); + + const decrypted = await window.crypto.subtle.decrypt( + {name: "AES-CBC", iv: iv}, + cryptoKey, + cipherData + ); + + return new TextDecoder().decode(decrypted); +} + + +async function getCryptoKey(passphrase) { + const encoder = new TextEncoder(); + const data = encoder.encode(passphrase); + const hash = await crypto.subtle.digest('SHA-256', data); + return await crypto.subtle.importKey('raw', hash, {name: 'AES-CBC'}, false, ['encrypt', 'decrypt']); +} + +async function encryptBytes(plainBytes, passphrase) { + const key = await getCryptoKey(passphrase); + const iv = crypto.getRandomValues(new Uint8Array(16)); + + const encryptedContent = await crypto.subtle.encrypt( + {name: 'AES-CBC', iv: iv}, + key, + plainBytes + ); + + const result = new Uint8Array(iv.length + encryptedContent.byteLength); + result.set(iv); + result.set(new Uint8Array(encryptedContent), iv.length); + return result; +} + +async function decryptBytes(combinedBytes, passphrase) { + const key = await getCryptoKey(passphrase); + const iv = combinedBytes.slice(0, 16); + const data = combinedBytes.slice(16); + + const decryptedContent = await crypto.subtle.decrypt( + {name: 'AES-CBC', iv: iv}, + key, + data + ); + + return new Uint8Array(decryptedContent); +} + +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-CBC'}, false, ['encrypt'] + ); + + const iv = crypto.getRandomValues(new Uint8Array(16)); + const encrypted = await crypto.subtle.encrypt( + {name: 'AES-CBC', iv: iv}, + key, + data + ); + + const combined = new Uint8Array(iv.length + encrypted.byteLength); + combined.set(iv); + combined.set(new Uint8Array(encrypted), iv.length); + + return fixBase64Padding(btoa(String.fromCharCode(...combined))); +} + +async function decryptString(base64Text, passphrase) { + const encoder = new TextEncoder(); + 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-CBC'}, false, ['decrypt'] + ); + + const iv = combined.slice(0, 16); + const data = combined.slice(16); + + const decrypted = await crypto.subtle.decrypt( + {name: 'AES-CBC', iv: iv}, + key, + data + ); + + return new TextDecoder().decode(decrypted); +} + + +async function fetchpost(url, value) { + let response = await fetch(url, { + method: "POST", + credentials: "omit", + body: value, + headers: { + "secret": passwordHash + } + }); + let data = await response.text(); + return data; +} + +async function fetchAsync(url, sendSecret) { + let response; + if (sendSecret) { + response = await fetch(url, { + method: "GET", credentials: "omit", headers: { + "secret": passwordHash + } + }); + } + else + { + response = await fetch(url, {method: "GET", credentials: "omit"}); + } + let data = await response.text(); + return data; +} + +async function fetchEncrypted(request, body) +{ + return await decryptString( + await fetchpost(`${url}/encryptedrequest?u=${username}`, await packetEncPass( + + JSON.stringify({ + string1: request, + string2: body + }) + + , passwordHash, username)) + + , passwordHash); +} + +function power(base, exponent, mod) { + let res = 1n; + base = base % mod; + while (exponent > 0n) { + if (exponent % 2n === 1n) res = (res * base) % mod; + base = (base * base) % mod; + exponent = exponent / 2n; + } + return res; +} + +function fixBase64Padding(base64String) { + let str = base64String.replace(/-/g, '+').replace(/_/g, '/'); + const pad = str.length % 4; + + if (pad) { + if (pad === 1) { + throw new Error(""); + } + str += '='.repeat(4 - pad); + } + + return str; +} + + + + + +collapseDmsBtn.addEventListener("click", () => { + collapseDmsBtn.classList.toggle("collapsed"); +}); + +sidebarHomeButton.addEventListener("mouseenter", () => { + sidebarHomeIndicator.classList.add("hover"); +}); +sidebarHomeButton.addEventListener("mouseleave", () => { + sidebarHomeIndicator.classList.remove("hover"); +}); + +sidebarAddButton.addEventListener("mouseenter", () => { + sidebarAddIndicator.classList.add("hover"); +}); +sidebarAddButton.addEventListener("mouseleave", () => { + sidebarAddIndicator.classList.remove("hover"); +}); + diff --git a/style.css b/style.css new file mode 100644 index 00000000..43dd672c --- /dev/null +++ b/style.css @@ -0,0 +1,151 @@ +@font-face { + font-family: 'Nunito'; + src: URL('Nunito-Regular.ttf') format('truetype'); +} +:root { + --main-bg-color: rgb(20, 20, 20); + --text-color: rgb(240, 240, 245); + --icon-button-size: 2rem; + --press-scale: scale(0.92); + --light-border-color: rgba(255, 255, 255, 0.08); + --button-margin: 0.4rem; + --icon-button-height: 3.4rem; + --button-height: 2.4rem; + --border-width: 0.09rem +} +html { + font-size: max(16px, calc(100vw / 120)); +} +body { + background-color: var(--main-bg-color); + display: flex; + height: 100dvh; + overflow: hidden; +} +* { + font-size: 1rem; + scrollbar-width: none; + box-sizing: border-box; + margin: 0; + padding: 0; + transition: all 0.2s ease; + color: var(--text-color); + font-family: "Nunito", sans-serif; + -webkit-tap-highlight-color: transparent; +} +button, input { + border-radius: 0.8rem; + background-color: rgba(255, 255, 255, 0.05); + margin: var(--button-margin); + padding: var(--button-margin); + height: var(--button-height); + display: flex; + cursor: pointer; + align-items: center; + justify-content: flex-start; + border: var(--border-width) solid var(--light-border-color); +} + + + +indicator { + content: ""; + position: absolute; + width: 0; + height: 0; + left: -0.08rem; + border: 0.15rem solid transparent; + border-radius: 0.8rem; + transform: translateY(calc( ( var(--icon-button-height) + (var(--button-margin) * 2) ) / 2 - 0.25rem)); +} +indicator.notification { + content: ""; + position: absolute; + width: 0; + height: 0.5rem; + left: -0.08rem; + border: 0.15rem solid var(--text-color); + border-radius: 0.8rem; + transform: translateY(calc( ( var(--icon-button-height) + (var(--button-margin) * 2) ) / 2 - 0.25rem)); +} +indicator.hover { + content: ""; + position: absolute; + width: 0; + height: 1.2rem; + left: -0.08rem; + border: 0.15rem solid var(--text-color); + border-radius: 0.8rem; + transform: translateY(calc( ( var(--icon-button-height) + (var(--button-margin) * 2) ) / 2 - 0.6rem)); +} +indicator.active { + content: ""; + position: absolute; + width: 0; + height: 2rem; + left: -0.08rem; + border: 0.15rem solid var(--text-color); + border-radius: 0.8rem; + transform: translateY(calc( ( var(--icon-button-height) + (var(--button-margin) * 2) ) / 2 - 1rem)); +} + + +.icon-button { + height: var(--icon-button-height); + width: var(--icon-button-height); + justify-content: center; +} +.icon-button svg { + width: var(--icon-button-size); + height: var(--icon-button-size); +} +button:hover, input:hover { + background-color: rgba(255, 255, 255, 0.1); +} +button:active, input:active { + transform: var(--press-scale); +} +roomcontent { + display: flex; + flex-direction: column; + + height: 100dvh; + width: calc(100vw - (var(--icon-button-height) + (var(--button-margin) * 2) + var(--border-width)) - 22rem); + border-right: var(--border-width) solid var(--light-border-color); +} +sidebar { + display: flex; + flex-direction: column; + + height: 100dvh; + width: calc(var(--icon-button-height) + (var(--button-margin) * 2) + var(--border-width)); + border-right: var(--border-width) solid var(--light-border-color); +} +sidebar.second { + width: 22rem; +} +.collapse-text-button { + padding-left: calc(var(--button-margin) + 0.2rem); + flex-shrink: 0; + border: none; + background-color: transparent; + height: 2rem; + font-weight: 600; +} + +.collapse-text-button .chevron { + width: 1.25rem; + height: 1.25rem; +} + +.collapse-text-button.collapsed .chevron { + transform: rotate(-90deg); +} +hr { + border: none; + height: 0; + background-color: transparent; + border-bottom: var(--border-width) solid var(--light-border-color); + width: 60%; + margin-left: 20%; +} \ No newline at end of file