Securely (or as secure as it can be) store encryption keys

This commit is contained in:
2025-07-22 17:44:14 -07:00
parent c507a6fbe4
commit 49b5441136
8 changed files with 103 additions and 64 deletions

8
src-tauri/src/keys.rs Normal file
View File

@@ -0,0 +1,8 @@
pub struct Keys;
impl Keys {
pub const SERVER_RECEIVE_TRANSFER_KEY: &str = "";
pub const SERVER_SEND_TRANSFER_KEY: &str = "";
pub const CONFIG_ENCRYPTION_KEY: &str = "";
pub const VERSIONS_ENCRYPTION_KEY: &str = "";
}

View File

@@ -1,4 +1,7 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
mod keys;
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use std::{ use std::{
fs::{File, create_dir_all}, fs::{File, create_dir_all},
@@ -12,6 +15,7 @@ use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
use tauri_plugin_opener::OpenerExt; use tauri_plugin_opener::OpenerExt;
use tokio::{io::AsyncWriteExt, task::spawn_blocking, time::timeout}; use tokio::{io::AsyncWriteExt, task::spawn_blocking, time::timeout};
use zip::ZipArchive; use zip::ZipArchive;
use keys::Keys;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use std::{fs, os::unix::fs::PermissionsExt}; use std::{fs, os::unix::fs::PermissionsExt};
@@ -207,6 +211,17 @@ fn download_leaderboard(app: AppHandle, content: String) {
}) })
} }
#[tauri::command]
fn get_keys_config(key: i8) -> String {
match key {
0 => Keys::SERVER_RECEIVE_TRANSFER_KEY.to_string(),
1 => Keys::SERVER_SEND_TRANSFER_KEY.to_string(),
2 => Keys::CONFIG_ENCRYPTION_KEY.to_string(),
3 => Keys::VERSIONS_ENCRYPTION_KEY.to_string(),
_ => "".to_string(),
}
}
pub fn run() { pub fn run() {
#[allow(unused_variables)] #[allow(unused_variables)]
tauri::Builder::default() tauri::Builder::default()
@@ -225,7 +240,8 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
download, download,
launch_game, launch_game,
download_leaderboard download_leaderboard,
get_keys_config
]) ])
.setup(|app| { .setup(|app| {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]

View File

@@ -1,6 +0,0 @@
export enum Keys {
SERVER_RECEIVE_TRANSFER_KEY = '',
SERVER_SEND_TRANSFER_KEY = '',
CONFIG_ENCRYPTION_KEY = '',
VERSIONS_ENCRYPTION_KEY = ''
}

View File

@@ -14,21 +14,22 @@ export default function Leaderboards () {
async function refresh () { async function refresh () {
setLoading(true) setLoading(true)
setLeaderboardData([]) setLeaderboardData([])
const launcherVersion = await app.getVersion() try {
axios const launcherVersion = await app.getVersion()
.get('https://berrydash.lncvrt.xyz/database/getTopPlayers.php', { const response = await axios.get('https://berrydash.lncvrt.xyz/database/getTopPlayers.php', {
headers: { headers: {
Requester: 'BerryDashLauncher', Requester: 'BerryDashLauncher',
LauncherVersion: launcherVersion, LauncherVersion: launcherVersion,
ClientPlatform: platform() ClientPlatform: platform()
} }
}) })
.then(res => { const decrypted = await decrypt(response.data)
const decrypted = decrypt(res.data) setLeaderboardData(JSON.parse(decrypted))
setLeaderboardData(JSON.parse(decrypted)) } catch (e) {
}) console.error('Error fetching leaderboard data:', e)
.catch(e => console.error('Error fetching leaderboard data:', e)) } finally {
.finally(() => setLoading(false)) setLoading(false)
}
} }
function downloadLeaderboard () { function downloadLeaderboard () {

View File

@@ -8,6 +8,7 @@ export default function Settings () {
useState(false) useState(false)
const [useWineOnUnixWhenNeeded, setUseWineOnUnixWhenNeeded] = useState(false) const [useWineOnUnixWhenNeeded, setUseWineOnUnixWhenNeeded] = useState(false)
const [allowNotifications, setAllowNotifications] = useState(false) const [allowNotifications, setAllowNotifications] = useState(false)
const [loaded, setLoaded] = useState(false)
useEffect(() => { useEffect(() => {
;(async () => { ;(async () => {
@@ -15,45 +16,48 @@ export default function Settings () {
setCheckForNewVersionOnLoad(config.settings.checkForNewVersionOnLoad) setCheckForNewVersionOnLoad(config.settings.checkForNewVersionOnLoad)
setUseWineOnUnixWhenNeeded(config.settings.useWineOnUnixWhenNeeded) setUseWineOnUnixWhenNeeded(config.settings.useWineOnUnixWhenNeeded)
setAllowNotifications(config.settings.allowNotifications) setAllowNotifications(config.settings.allowNotifications)
setLoaded(true)
})() })()
}, []) }, [])
return ( return (
<> <>
<p className='text-3xl ml-4 mt-4'>Settings</p> <p className='text-3xl ml-4 mt-4'>Settings</p>
<div className='ml-4 mt-4 bg-[#161616] border border-[#242424] rounded-lg p-4 w-fit h-fit'> {loaded && (
<Setting <div className='ml-4 mt-4 bg-[#161616] border border-[#242424] rounded-lg p-4 w-fit h-fit'>
label='Check for new version on load' <Setting
value={checkForNewVersionOnLoad} label='Check for new version on load'
onChange={async () => { value={checkForNewVersionOnLoad}
setCheckForNewVersionOnLoad(!checkForNewVersionOnLoad) onChange={async () => {
const config = await readNormalConfig() setCheckForNewVersionOnLoad(!checkForNewVersionOnLoad)
config.settings.checkForNewVersionOnLoad = !checkForNewVersionOnLoad const config = await readNormalConfig()
await writeNormalConfig(config) config.settings.checkForNewVersionOnLoad = !checkForNewVersionOnLoad
}} await writeNormalConfig(config)
/> }}
<Setting />
label='Allow sending notifications' <Setting
value={allowNotifications} label='Allow sending notifications'
onChange={async () => { value={allowNotifications}
setAllowNotifications(!allowNotifications) onChange={async () => {
const config = await readNormalConfig() setAllowNotifications(!allowNotifications)
config.settings.allowNotifications = !allowNotifications const config = await readNormalConfig()
await writeNormalConfig(config) config.settings.allowNotifications = !allowNotifications
}} await writeNormalConfig(config)
/> }}
<Setting />
label='Use wine to launch Berry Dash when needed' <Setting
value={useWineOnUnixWhenNeeded} label='Use wine to launch Berry Dash when needed'
onChange={async () => { value={useWineOnUnixWhenNeeded}
setUseWineOnUnixWhenNeeded(!useWineOnUnixWhenNeeded) onChange={async () => {
const config = await readNormalConfig() setUseWineOnUnixWhenNeeded(!useWineOnUnixWhenNeeded)
config.settings.useWineOnUnixWhenNeeded = !useWineOnUnixWhenNeeded const config = await readNormalConfig()
await writeNormalConfig(config) config.settings.useWineOnUnixWhenNeeded = !useWineOnUnixWhenNeeded
}} await writeNormalConfig(config)
className={platform() == 'linux' || platform() == 'macos' ? '' : 'hidden'} }}
/> className={platform() == 'linux' || platform() == 'macos' ? '' : 'hidden'}
</div> />
</div>
)}
</> </>
) )
} }

View File

@@ -9,8 +9,8 @@ import {
writeFile writeFile
} from '@tauri-apps/plugin-fs' } from '@tauri-apps/plugin-fs'
import { decrypt, encrypt } from './Encryption' import { decrypt, encrypt } from './Encryption'
import { Keys } from '../enums/Keys'
import { VersionsConfig } from '../types/VersionsConfig' import { VersionsConfig } from '../types/VersionsConfig'
import { getKey } from './KeysHelper'
export async function readNormalConfig (): Promise<NormalConfig> { export async function readNormalConfig (): Promise<NormalConfig> {
const version = await app.getVersion() const version = await app.getVersion()
@@ -26,9 +26,9 @@ export async function readNormalConfig (): Promise<NormalConfig> {
const file = await create('config.dat', options) const file = await create('config.dat', options)
await file.write( await file.write(
new TextEncoder().encode( new TextEncoder().encode(
encrypt( await encrypt(
JSON.stringify(new NormalConfig(version)), JSON.stringify(new NormalConfig(version)),
Keys.CONFIG_ENCRYPTION_KEY await getKey(2)
) )
) )
) )
@@ -37,7 +37,7 @@ export async function readNormalConfig (): Promise<NormalConfig> {
} }
const config = await readTextFile('config.dat', options) const config = await readTextFile('config.dat', options)
return NormalConfig.import( return NormalConfig.import(
JSON.parse(decrypt(config, Keys.CONFIG_ENCRYPTION_KEY)) JSON.parse(await decrypt(config, await getKey(2)))
) )
} }
@@ -54,7 +54,7 @@ export async function writeNormalConfig (data: NormalConfig) {
const file = await create('config.dat', options) const file = await create('config.dat', options)
await file.write( await file.write(
new TextEncoder().encode( new TextEncoder().encode(
encrypt(JSON.stringify(data), Keys.CONFIG_ENCRYPTION_KEY) await encrypt(JSON.stringify(data), await getKey(2))
) )
) )
await file.close() await file.close()
@@ -62,7 +62,7 @@ export async function writeNormalConfig (data: NormalConfig) {
await writeFile( await writeFile(
'config.dat', 'config.dat',
new TextEncoder().encode( new TextEncoder().encode(
encrypt(JSON.stringify(data), Keys.CONFIG_ENCRYPTION_KEY) await encrypt(JSON.stringify(data), await getKey(2))
), ),
options options
) )
@@ -83,9 +83,9 @@ export async function readVersionsConfig (): Promise<VersionsConfig> {
const file = await create('versions.dat', options) const file = await create('versions.dat', options)
await file.write( await file.write(
new TextEncoder().encode( new TextEncoder().encode(
encrypt( await encrypt(
JSON.stringify(new VersionsConfig(version)), JSON.stringify(new VersionsConfig(version)),
Keys.VERSIONS_ENCRYPTION_KEY await getKey(3)
) )
) )
) )
@@ -94,7 +94,7 @@ export async function readVersionsConfig (): Promise<VersionsConfig> {
} }
const config = await readTextFile('versions.dat', options) const config = await readTextFile('versions.dat', options)
return VersionsConfig.import( return VersionsConfig.import(
JSON.parse(decrypt(config, Keys.VERSIONS_ENCRYPTION_KEY)) JSON.parse(await decrypt(config, await getKey(3)))
) )
} }
@@ -111,7 +111,7 @@ export async function writeVersionsConfig (data: VersionsConfig) {
const file = await create('versions.dat', options) const file = await create('versions.dat', options)
await file.write( await file.write(
new TextEncoder().encode( new TextEncoder().encode(
encrypt(JSON.stringify(data), Keys.VERSIONS_ENCRYPTION_KEY) await encrypt(JSON.stringify(data), await getKey(3))
) )
) )
await file.close() await file.close()
@@ -119,7 +119,7 @@ export async function writeVersionsConfig (data: VersionsConfig) {
await writeFile( await writeFile(
'versions.dat', 'versions.dat',
new TextEncoder().encode( new TextEncoder().encode(
encrypt(JSON.stringify(data), Keys.VERSIONS_ENCRYPTION_KEY) await encrypt(JSON.stringify(data), await getKey(3))
), ),
options options
) )

View File

@@ -1,7 +1,11 @@
import CryptoJS from 'crypto-js' import CryptoJS from 'crypto-js'
import { Keys } from '../enums/Keys' import { getKey } from './KeysHelper'
export function encrypt (plainText: string, key: string = Keys.SERVER_SEND_TRANSFER_KEY): string { export async function encrypt (
plainText: string,
key?: string
): Promise<string> {
if (!key) key = await getKey(1)
const iv = CryptoJS.lib.WordArray.random(16) const iv = CryptoJS.lib.WordArray.random(16)
const encrypted = CryptoJS.AES.encrypt( const encrypted = CryptoJS.AES.encrypt(
plainText, plainText,
@@ -16,7 +20,8 @@ export function encrypt (plainText: string, key: string = Keys.SERVER_SEND_TRANS
return CryptoJS.enc.Base64.stringify(combined) return CryptoJS.enc.Base64.stringify(combined)
} }
export function decrypt (dataB64: string, key: string = Keys.SERVER_RECEIVE_TRANSFER_KEY): string { export async function decrypt (dataB64: string, key?: string): Promise<string> {
if (!key) key = await getKey(0)
const data = CryptoJS.enc.Base64.parse(dataB64) const data = CryptoJS.enc.Base64.parse(dataB64)
const iv = CryptoJS.lib.WordArray.create(data.words.slice(0, 4), 16) const iv = CryptoJS.lib.WordArray.create(data.words.slice(0, 4), 16)
const ciphertext = CryptoJS.lib.WordArray.create( const ciphertext = CryptoJS.lib.WordArray.create(
@@ -35,6 +40,6 @@ export function decrypt (dataB64: string, key: string = Keys.SERVER_RECEIVE_TRAN
} }
) )
const result = decrypted.toString(CryptoJS.enc.Utf8) const result = decrypted.toString(CryptoJS.enc.Utf8)
if (!result) throw new Error(encrypt('-997')) if (!result) throw new Error(await encrypt('-997'))
return result return result
} }

11
src/util/KeysHelper.ts Normal file
View File

@@ -0,0 +1,11 @@
import { invoke } from "@tauri-apps/api/core";
export async function getKey(key: number): Promise<string> {
try {
const message = await invoke('get_keys_config', { key });
return message as string;
} catch (error) {
console.error('Failed to get key from Tauri backend', error);
return '';
}
}