diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b68a56a --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +NEXT_PUBLIC_PUBLIC_SIGNING_KEY= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5ef6a52..7b8da95 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +!.env.example # vercel .vercel diff --git a/bun.lock b/bun.lock index 18c02ae..a578e20 100644 --- a/bun.lock +++ b/bun.lock @@ -13,11 +13,11 @@ "@tauri-apps/plugin-clipboard-manager": "2.3.2", "@tauri-apps/plugin-dialog": "2.6.0", "@tauri-apps/plugin-fs": "2.4.5", + "@tauri-apps/plugin-http": "2.5.7", "@tauri-apps/plugin-notification": "2.3.3", "@tauri-apps/plugin-opener": "2.5.3", "@tauri-apps/plugin-os": "2.3.2", "@tauri-apps/plugin-window-state": "2.4.1", - "axios": "1.13.6", "next": "16.1.6", "pretty-bytes": "7.1.0", "react": "19.2.4", @@ -272,6 +272,8 @@ "@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.5", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA=="], + "@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.7", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-+F2lEH/c9b0zSsOXKq+5hZNcd9F4IIKCK1T17RqMwpCmVnx2aoqY8yIBccCd25HTYUb3j6NPVbRax/m00hKG8A=="], + "@tauri-apps/plugin-notification": ["@tauri-apps/plugin-notification@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg=="], "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ=="], @@ -386,14 +388,10 @@ "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], - "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], "axe-core": ["axe-core@4.11.1", "", {}, "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A=="], - "axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], - "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], @@ -424,8 +422,6 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -452,8 +448,6 @@ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], @@ -540,12 +534,8 @@ "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], - "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], - "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], - "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], @@ -724,10 +714,6 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -792,8 +778,6 @@ "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], - "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], diff --git a/package.json b/package.json index 1fa3fff..e9403c9 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,11 @@ "@tauri-apps/plugin-clipboard-manager": "2.3.2", "@tauri-apps/plugin-dialog": "2.6.0", "@tauri-apps/plugin-fs": "2.4.5", + "@tauri-apps/plugin-http": "2.5.7", "@tauri-apps/plugin-notification": "2.3.3", "@tauri-apps/plugin-opener": "2.5.3", "@tauri-apps/plugin-os": "2.3.2", "@tauri-apps/plugin-window-state": "2.4.1", - "axios": "1.13.6", "next": "16.1.6", "pretty-bytes": "7.1.0", "react": "19.2.4", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c6ecc1c..a515c7d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -30,6 +30,9 @@ sysinfo = "0.38.2" sha2 = "0.10.9" tauri-plugin-clipboard-manager = "2.3.2" tauri-plugin-prevent-default = "4.0.4" +openssl = { version = "0.10.75", features = ["v110"] } +base64 = "0.22.1" +tauri-plugin-http = "2.5.7" [target.'cfg(target_os = "linux")'.dependencies] shlex = "1.3.0" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index a94f1a0..b5b00d2 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -38,6 +38,20 @@ "allow": [ { "path": "$APPLOCALDATA/game/**" } ] + }, + { + "identifier": "http:default", + "allow": [ + { "url": "https://games.lncvrt.xyz/api/launcher/latest" }, + { "url": "https://games.lncvrt.xyz/api/launcher/versions" }, + { "url": "https://games.lncvrt.xyz/api/launcher/download" }, + { "url": "https://games.lncvrt.xyz/api/berrydash/account" }, + { "url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/score" }, + { "url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/berry" }, + { "url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/coin" }, + { "url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/legacy" }, + { "url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/total" } + ] } ] } \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ee755c7..9a5653b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,7 @@ +use base64::{Engine as _, engine::general_purpose}; use futures_util::stream::StreamExt; +use openssl::pkey::PKey; +use openssl::sign::Verifier; use sha2::{Digest, Sha512}; use std::fs; use std::path::Path; @@ -415,10 +418,33 @@ fn launch_game( } } +#[tauri::command] +fn verify_signature(body: String, signature: String, public_key: String) -> bool { + let Ok(pem) = general_purpose::STANDARD.decode(public_key) else { + return false; + }; + let Ok(pubkey) = PKey::public_key_from_pem(&pem) else { + return false; + }; + let Ok(sig) = general_purpose::STANDARD.decode(signature) else { + return false; + }; + + let mut verifier = match Verifier::new_without_digest(&pubkey) { + Ok(v) => v, + Err(_) => return false, + }; + + verifier + .verify_oneshot(&sig, body.as_bytes()) + .unwrap_or(false) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { #[allow(unused_variables)] tauri::Builder::default() + .plugin(tauri_plugin_http::init()) .plugin( tauri_plugin_prevent_default::Builder::new() .with_flags( @@ -448,7 +474,12 @@ pub fn run() { .plugin(tauri_plugin_decorum::init()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_opener::init()) - .invoke_handler(tauri::generate_handler![download, launch_game, folder_size]) + .invoke_handler(tauri::generate_handler![ + download, + launch_game, + folder_size, + verify_signature + ]) .setup(|app| { #[cfg(target_os = "windows")] { diff --git a/src/app/game/berrydash/leaderboards/page.tsx b/src/app/game/berrydash/leaderboards/page.tsx index 6c36e7b..dfcb086 100644 --- a/src/app/game/berrydash/leaderboards/page.tsx +++ b/src/app/game/berrydash/leaderboards/page.tsx @@ -2,12 +2,13 @@ import { useCallback, useEffect, useState } from 'react' import { BirdColor } from '@/types/BerryDash/BirdColor' -import axios from 'axios' import { GetIconForUser } from '@/lib/BerryDash' import Image from 'next/image' import './styles.css' import { useRouter } from 'next/navigation' import { platform } from '@tauri-apps/plugin-os' +import { fetch } from '@tauri-apps/plugin-http' +import { verifySignature } from '@/lib/Util' interface BaseEntry { id: number @@ -90,11 +91,16 @@ export default function BerryDashLeaderboards () { const Refresh = useCallback(async () => { try { if (selected == 3 || selected == 4) { - const result = await axios.get( + const response = await fetch( 'https://games.lncvrt.xyz/api/berrydash/account?username=' ) - if (result.data.success) { - let accounts = result.data.data as Account[] + const signature = response.headers.get('x-signature') ?? '' + const data = await response.json() + if ( + (await verifySignature(JSON.stringify(data), signature)) && + data.success + ) { + let accounts = data.data as Account[] accounts = accounts.map(acc => { const xp = calculateXP( @@ -114,9 +120,9 @@ export default function BerryDashLeaderboards () { accounts.sort((a, b) => (b.xp > a.xp ? 1 : b.xp < a.xp ? -1 : 0)) setEntries(accounts) - } + } else console.log('Failed') } else { - const result = await axios.get( + const response = await fetch( 'https://games.lncvrt.xyz/api/berrydash/leaderboard/' + (selected == 0 ? 'score' @@ -128,9 +134,13 @@ export default function BerryDashLeaderboards () { ? 'legacy' : 'total') ) - if (result.data.success) { - setEntries(result.data.data as LeaderboardEntry[]) - } + const signature = response.headers.get('x-signature') ?? '' + const data = await response.json() + if ( + (await verifySignature(JSON.stringify(data), signature)) && + data.success + ) + setEntries(data.data as LeaderboardEntry[]) } } catch { setEntries([]) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 16a86ce..050465b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -15,7 +15,6 @@ import { import { VersionsConfig } from '@/types/VersionsConfig' import { NormalConfig } from '@/types/NormalConfig' import { app } from '@tauri-apps/api' -import axios from 'axios' import { openUrl } from '@tauri-apps/plugin-opener' import { GlobalProvider } from './GlobalProvider' import { Roboto } from 'next/font/google' @@ -36,6 +35,8 @@ import VersionsDownloadPopup from '@/componets/popups/VersionsDownload' import GamesDownloadPopup from '@/componets/popups/GamesDownload' import DownloadsPopup from '@/componets/popups/Downloads' import VersionVersionPopup from '@/componets/popups/VersionVersion' +import { fetch } from '@tauri-apps/plugin-http' +import { verifySignature } from '@/lib/Util' const roboto = Roboto({ subsets: ['latin'] @@ -252,11 +253,18 @@ export default function RootLayout ({ setVersion(client) if (process.env.NODE_ENV === 'production') { try { - const response = await axios.get( + const response = await fetch( 'https://games.lncvrt.xyz/api/launcher/latest' ) - if (response.data !== client) { - setOutdated(true) + const signature = response.headers.get('x-signature') ?? '' + const data = await response.text() + if (await verifySignature(data, signature)) { + if (data !== client) { + setOutdated(true) + return + } + } else { + setLoadingText('Failed to check latest version.') return } } catch { @@ -265,10 +273,17 @@ export default function RootLayout ({ } } try { - const res = await axios.get( + const response = await fetch( `https://games.lncvrt.xyz/api/launcher/versions?platform=${platform()}&arch=${arch()}` ) - setServerVersionList(res.data) + const signature = response.headers.get('x-signature') ?? '' + const data = await response.json() + if (await verifySignature(JSON.stringify(data), signature)) { + setServerVersionList(data) + } else { + setLoadingText('Failed to download versions list.') + return + } } catch { setLoadingText('Failed to download versions list.') return @@ -348,13 +363,18 @@ export default function RootLayout ({ prev.map(d => (d.version === versionId ? { ...d, queued: false } : d)) ) - const downloadInfoRequest = await axios.get( + const downloadInfoRequest = await fetch( 'https://games.lncvrt.xyz/api/launcher/download?gameId=' + info.id + '&downloadId=' + info.download ) - if (!downloadInfoRequest.data.success) { + const signature = downloadInfoRequest.headers.get('x-signature') ?? '' + const data = await downloadInfoRequest.json() + if ( + !(await verifySignature(JSON.stringify(data), signature)) || + !data.success + ) { setDownloadProgress(prev => prev.map(d => d.version === versionId @@ -371,10 +391,10 @@ export default function RootLayout ({ } const res = await invoke('download', { - url: downloadInfoRequest.data.data.url, + url: data.data.url, name: info.id, executable: info.executable, - hash: downloadInfoRequest.data.data.hash + hash: data.data.hash }) if (res === '1') { diff --git a/src/lib/Util.ts b/src/lib/Util.ts index e4454a0..32039b8 100644 --- a/src/lib/Util.ts +++ b/src/lib/Util.ts @@ -2,6 +2,7 @@ import { message } from '@tauri-apps/plugin-dialog' import { BaseDirectory, exists, stat } from '@tauri-apps/plugin-fs' import { openPath } from '@tauri-apps/plugin-opener' import { join, appLocalDataDir } from '@tauri-apps/api/path' +import { invoke } from '@tauri-apps/api/core' export const openFolder = async (name: string) => { const relativePath = await join('game', name) @@ -48,3 +49,16 @@ export const formatEtaSmart = (seconds: number) => { const h = Math.floor((seconds % 86400) / 3600) return `${d}d ${h}h` } + +export const verifySignature = async (body: string, signature: string) => { + try { + const result = await invoke('verify_signature', { + body: body ?? '', + signature: signature ?? '', + publicKey: process.env.NEXT_PUBLIC_PUBLIC_SIGNING_KEY ?? '' + }) + return Boolean(result) + } catch { + return false + } +}