diff --git a/android/.gitignore b/android/.gitignore index 34acf79f..be945cd7 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -94,9 +94,10 @@ lint/tmp/ #need this for actions # Copied web assets -app/src/main/assets/public +#app/src/main/assets/public +#need this for actions # Generated Config files -app/src/main/assets/capacitor.config.json -app/src/main/assets/capacitor.plugins.json -app/src/main/res/xml/config.xml +#app/src/main/assets/capacitor.config.json +#app/src/main/assets/capacitor.plugins.json +#app/src/main/res/xml/config.xml diff --git a/android/app/src/main/assets/capacitor.config.json b/android/app/src/main/assets/capacitor.config.json new file mode 100644 index 00000000..f55049e2 --- /dev/null +++ b/android/app/src/main/assets/capacitor.config.json @@ -0,0 +1,13 @@ +{ + "appId": "olcxja.miarven", + "appName": "Miarven", + "webDir": "webroot", + "plugins": { + "SystemBars": { + "insetsHandling": "css", + "style": "DARK", + "hidden": false, + "animation": "FADE" + } + } +} diff --git a/android/app/src/main/assets/capacitor.plugins.json b/android/app/src/main/assets/capacitor.plugins.json new file mode 100644 index 00000000..d717d5dc --- /dev/null +++ b/android/app/src/main/assets/capacitor.plugins.json @@ -0,0 +1,6 @@ +[ + { + "pkg": "@capacitor/status-bar", + "classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin" + } +] diff --git a/android/app/src/main/assets/public/Nunito-Regular.ttf b/android/app/src/main/assets/public/Nunito-Regular.ttf new file mode 100644 index 00000000..be80c3f0 Binary files /dev/null and b/android/app/src/main/assets/public/Nunito-Regular.ttf differ diff --git a/android/app/src/main/assets/public/cordova.js b/android/app/src/main/assets/public/cordova.js new file mode 100644 index 00000000..e69de29b diff --git a/android/app/src/main/assets/public/cordova_plugins.js b/android/app/src/main/assets/public/cordova_plugins.js new file mode 100644 index 00000000..e69de29b diff --git a/android/app/src/main/assets/public/favicon.svg b/android/app/src/main/assets/public/favicon.svg new file mode 100644 index 00000000..d70db7a4 --- /dev/null +++ b/android/app/src/main/assets/public/favicon.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + MN + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/assets/public/index.html b/android/app/src/main/assets/public/index.html new file mode 100644 index 00000000..125026b0 --- /dev/null +++ b/android/app/src/main/assets/public/index.html @@ -0,0 +1,139 @@ + + + + + + Larpix Client + + + + + + + + + + +
+ + + + + +
+
+ + + + Home + + + + + + + + + Splash + + +
+ + Welcome to Miarven +

First Larpix client. v1.0

+
+
+
+ + + + + + \ No newline at end of file diff --git a/android/app/src/main/assets/public/login/index.html b/android/app/src/main/assets/public/login/index.html new file mode 100644 index 00000000..9a34169e --- /dev/null +++ b/android/app/src/main/assets/public/login/index.html @@ -0,0 +1,498 @@ + + + + + + Larpix - Authentication + + + + + + +
+
+ +
+
+

Larpix

+

Sign in to your larpix instance.

+
+ +
+
+ + +

Connecting...

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

Join Larpix

+

Create new larpix account.

+
+ +
+
+ + +

Connecting...

+
+ +
+ + +
+ +
+ + +

Remember to use a strong password! It will be used to encrypt your keys

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

Verify

+

Prove you are a human.

+
+
+ Captcha +
+
+
+ + + +
+
+ + +
+ +
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/android/app/src/main/assets/public/main.js b/android/app/src/main/assets/public/main.js new file mode 100644 index 00000000..efc5d22a --- /dev/null +++ b/android/app/src/main/assets/public/main.js @@ -0,0 +1,460 @@ +console.log(window.location.protocol); + +var url = `${window.location.protocol}//${window.location.hostname}/_larpix`; +var params = new URLSearchParams(window.location.search); + +const collapseDmsBtn = document.getElementById("collapse-dms"); +const collapseGroupsBtn = document.getElementById("collapse-groups"); + +const addDmBtn = document.getElementById("add-dm-btn"); +const addGroupBtn = document.getElementById("add-group-btn"); + +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); + +const roomDetailsBar = document.getElementById("roomdetailsbar"); +const roomContent = document.getElementsByTagName("roomcontent")[0]; +const roomsBar = document.getElementById("roomsbar"); +const sideBar = document.getElementsByTagName("sidebar")[0]; + +const roomContentMain = document.getElementsByTagName("roomcontent2")[0]; + +const roomContentBar = roomContent.children[0]; + + + +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 getNonce(username, key) { + + let nonce; + let fetchRes = await (await fetch(`${url}/nextnonce?u=${username}`)).text(); + + try { + nonce = await decryptString(fetchRes, key); + } + catch(err) { + nonce = await decryptString(fetchRes, ""); + } + return nonce; +} + +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)); + + 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] +} + +function base64ToUint8(base64) { + return Uint8Array.from(atob(base64), c => c.charCodeAt(0)); +} +function uint8ToBase64(uint8) { + return 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": await encryptWithNonce(passwordHash, passwordHash, await getNonce(username, passwordHash)) + } + }); + let data = await response.text(); + return data; +} + +async function fetchPostEnc(url, value) { + let nonce = await getNonce(username, passwordHash); + let response = await fetch(url, { + method: "POST", + credentials: "omit", + body: await encryptWithNonce(value, passwordHash, nonce), + headers: { + "secret": await encryptWithNonce(passwordHash, passwordHash, nonce) + } + }); + let data = await response.text(); + return data; +} + +async function fetchAsync(url) { + let response = await fetch(url, { + method: "GET", + credentials: "omit" + }); + + let data = await response.text(); + return data; +} + +async function fetchAsyncWAuth(url) { + let response = await fetch(url, { + method: "GET", + credentials: "omit", + headers: { + "secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(username, passwordHash)) + } + }); + + let data = await response.text(); + return data; +} + +async function getServerInfo(host){ + console.log(`${window.location.protocol}//${host}/_larpix/serverinfo`) + return JSON.parse(await fetchAsync(`${window.location.protocol}//${host}/_larpix/serverinfo`)); +} + +async function Auth(username, password) { + + + let passwordHash = await hashSHA3_512(password); + let response = await fetch(`${url}/auth?u=${username}`, { + method: "GET", + credentials: "omit", + headers: { + "secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(username, passwordHash)) + } + }); + + let data = await response.text(); + return data; + +} + + +async function fetchEncrypted(request, body) +{ + + let nonce = await getNonce(username, passwordHash); + + let response = await fetch(`${url}/encryptedrequest?u=${username}`, { + method: "POST", + credentials: "omit", + body: await encryptWithNonce( + + JSON.stringify({ + string1: request, + string2: body + }) + + , passwordHash, nonce), + headers: { + "secret": await encryptWithNonce(passwordHash, passwordHash, nonce) + } + }); + let data = await response.text(); + return decryptString(data, 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; +} + +function showNotification(message, type = 'info', duration = 3500) { + let container = document.getElementById('notification-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'notification-container'; + document.body.appendChild(container); + } + + const notif = document.createElement('div'); + notif.className = `notification ${type}`; + notif.textContent = message; + + container.appendChild(notif); + + requestAnimationFrame(() => { + requestAnimationFrame(() => { + notif.classList.add('show'); + }); + }); + + setTimeout(() => { + notif.classList.remove('show'); + + notif.addEventListener('transitionend', () => { + notif.remove(); + }); + }, duration); +} + + +function showAction(message, actionid) { + let container = document.getElementById('notification-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'notification-container'; + document.body.appendChild(container); + } + + const notif = document.createElement('div'); + notif.className = `notification action`; + notif.textContent = message; + notif.id = `notification-${actionid}`; + + container.appendChild(notif); + + requestAnimationFrame(() => { + requestAnimationFrame(() => { + notif.classList.add('show'); + }); + }); +} +function clearAction(actionid) { + + let notif = document.getElementById(`notification-${actionid}`); + notif.classList.remove('show'); + + notif.addEventListener('transitionend', () => { + notif.remove(); + }); +} + + +async function hashSHA3_512(input) { + const encoder = new TextEncoder(); + 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; +} + + + +var password = ""; +var username = ""; +var passwordHash = ""; +var host = ""; +async function mainJS() +{ + username = localStorage.getItem('username'); + password = localStorage.getItem('password'); + host = localStorage.getItem('host'); + passwordHash = await hashSHA3_512(password); + url = `${window.location.protocol}//${host}/_larpix`; +} +mainJS(); + +collapseDmsBtn.addEventListener("click", () => { + collapseDmsBtn.classList.toggle("collapsed"); +}); +collapseGroupsBtn.addEventListener("click", () => { + collapseGroupsBtn.classList.toggle("collapsed"); +}); + +addDmBtn.addEventListener("click", () => { + roomContentMain.innerHTML = + ` +
+ + Add Chat +

Add a private, encrypted chat by entering a username

+
+ ` +}); +addGroupBtn.addEventListener("click", () => { + +}); + + + +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/android/app/src/main/assets/public/style.css b/android/app/src/main/assets/public/style.css new file mode 100644 index 00000000..15f1613f --- /dev/null +++ b/android/app/src/main/assets/public/style.css @@ -0,0 +1,300 @@ +@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; + + --big-default: rgb(207 207 207); + --big-red: rgb(202 0 0); + --big-green: rgb(25 189 0); +} +.red { + color: var(--big-red); +} +.green { + color: var(--big-green); +} +.mininote { + font-size: 80%; + padding: 0 0.5rem; +} +.aqua { + color: rgb(0 139 200); +} +html { + font-size: max(16px, calc(100vw / 120)); +} +body { + padding-top: env(safe-area-inset-top); + 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); +} +input { + padding-left: calc(var(--button-margin) * 2); +} + + +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)) - 17rem); + border-right: var(--border-width) solid var(--light-border-color); +} +roomcontent2 { + display: flex; + flex-direction: column; + + height: calc(100dvh - (var(--button-height) * 1.5)); + width: 100%; +} + + +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: 17rem; + min-width: 17rem; + max-width: 17rem; +} +.sidebar-section-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.collapse-text-button { + padding-left: calc(var(--button-margin) + 0.2rem); + flex-shrink: 0; + flex-grow: 1; + border: none; + background-color: transparent; + height: 2rem; + font-weight: 600; + gap: 0.3rem; +} + +.collapse-text-button .chevron { + width: 1.25rem; + height: 1.25rem; +} + +.collapse-text-button.collapsed .chevron { + transform: rotate(-90deg); +} + +.add-action-button { + border: none; + background-color: transparent; + height: 2rem; + width: 2rem; + padding: 0; + flex-shrink: 0; + justify-content: center; + color: rgba(255, 255, 255, 0.5); +} + +.add-action-button:hover { + background-color: rgba(255, 255, 255, 0.1); + color: var(--text-color); +} + +.add-action-button svg { + width: 1.25rem; + height: 1.25rem; +} +hr { + border: none; + height: 0; + background-color: transparent; + border-bottom: var(--border-width) solid var(--light-border-color); + width: 60%; + margin-left: 20%; +} + + +#notification-container { + padding-top: env(safe-area-inset-top); + position: fixed; + top: 1.5rem; + left: 50%; + transform: translateX(-50%); + z-index: 1500; + display: flex; + flex-direction: column; + gap: 0.8rem; + pointer-events: none; + align-items: center; +} + +.notification { + background: rgba(255, 255, 255, 0.015); + backdrop-filter: blur(1.2rem); + -webkit-backdrop-filter: blur(1.2rem); + + border: var(--border-width) solid var(--light-border-color); + border-radius: 0.9rem; + + padding: 0.8rem 1.3rem; + color: var(--text-color); + font-size: 1rem; + font-weight: 500; + text-align: center; + + box-shadow: 0 0.5rem 2rem rgba(0, 0, 0, 0.4); + + opacity: 0; + transform: translateY(-1rem); + transition: + opacity 0.3s ease, + transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), + background-color 0.2s ease; +} + +.notification.show { + opacity: 1; + transform: translateY(0); +} + +.notification.info { + border-color: var(--big-default); +} + +.notification.error { + border-color: var(--big-red); +} + +.notification.success { + border-color: var(--big-green); +} + +roomtopbar { + height: calc(var(--button-height) * 1.5); + + display: flex; + flex-direction: row; + + width: 100%; + border-bottom: var(--border-width) solid var(--light-border-color); + + line-height: calc(var(--button-height) * 1.5); + font-weight: bold; + padding-left: calc(var(--button-margin) * 3); + font-size: 1.5rem; +} +sidebar.second#roomdetailsbar { + height: calc(var(--button-height) * 1.5); + + display: flex; + flex-direction: row; + + border-bottom: var(--border-width) solid var(--light-border-color); +} +fullcontainer { + width: 100%; + height: 100%; +} +herotitle { + font-weight: bold; + font-size: 1.6rem; +} \ No newline at end of file diff --git a/android/app/src/main/res/xml/config.xml b/android/app/src/main/res/xml/config.xml new file mode 100644 index 00000000..1b1b0e0d --- /dev/null +++ b/android/app/src/main/res/xml/config.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file