Files
Barış Keser 83be058d23 feat: Athena v2 — Discord auth, AthenaCore mod, cross-platform launcher, CI
Backend (PHP+SQLite): Discord OAuth2 + JWT, coklu hesap (1 Discord = 3-5),
tek-kullanimlik join token (/api/join/prepare+verify), custom skin yukleme/sunma,
hesap/Discord/HWID ban, admin paneli kartlari; deterministik offline UUID (PHP<->C#
paritesi dogrulandi). 25 entegrasyon testi gecti.

AthenaCore (Forge 1.12.2): tek client+server mod — join token dogrulama + kick,
grace-gate, custom skin uygulama. Sunucu offline-mode kalir (UUID/dunya verisi korunur).

Masaustu: WPF -> Avalonia (Windows+Linux), Discord giris/hesap/skin/oyna; WMI/DPAPI
yerine cross-platform (machine-id HWID, AES-GCM secret store). Derlenir + calisir.

CI: athenacore-mod.yml (JDK8), desktop-release.yml (Win+Linux self-contained).
Deploy: deploy.sh (.env uretimi, php-gd, nginx Authorization gecisi) + HANDOFF.md.
Android: backend hazir; entegrasyon spec'i ANDROID_INTEGRATION.md.
2026-06-13 23:45:21 +03:00

96 lines
3.1 KiB
C#

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace OfflineMinecraftLauncher.Services;
/// <summary>
/// Encrypts the auth blob (JWT + per-account secrets) at rest with AES-GCM. The
/// 256-bit key is generated once and stored beside the data, restricted to the
/// current user (chmod 600 on Unix). Cross-platform, no extra dependencies.
/// </summary>
public static class SecretStore
{
private static string Dir =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppConfig.AppDataFolder);
private static string DataFile => Path.Combine(Dir, "auth.dat");
private static string KeyFile => Path.Combine(Dir, "auth.key");
private static byte[] GetKey()
{
Directory.CreateDirectory(Dir);
if (File.Exists(KeyFile))
{
var existing = File.ReadAllBytes(KeyFile);
if (existing.Length == 32) return existing;
}
var key = RandomNumberGenerator.GetBytes(32);
File.WriteAllBytes(KeyFile, key);
Restrict(KeyFile);
return key;
}
private static void Restrict(string path)
{
try
{
if (!OperatingSystem.IsWindows())
File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite);
}
catch { /* best effort */ }
}
public static void Save(string plaintext)
{
try
{
var key = GetKey();
var nonce = RandomNumberGenerator.GetBytes(12);
var pt = Encoding.UTF8.GetBytes(plaintext);
var ct = new byte[pt.Length];
var tag = new byte[16];
using (var aes = new AesGcm(key, 16))
aes.Encrypt(nonce, pt, ct, tag);
var blob = new byte[nonce.Length + tag.Length + ct.Length];
Buffer.BlockCopy(nonce, 0, blob, 0, nonce.Length);
Buffer.BlockCopy(tag, 0, blob, nonce.Length, tag.Length);
Buffer.BlockCopy(ct, 0, blob, nonce.Length + tag.Length, ct.Length);
Directory.CreateDirectory(Dir);
File.WriteAllBytes(DataFile, blob);
Restrict(DataFile);
}
catch { /* non-fatal */ }
}
public static string? Load()
{
try
{
if (!File.Exists(DataFile)) return null;
var key = GetKey();
var blob = File.ReadAllBytes(DataFile);
if (blob.Length < 28) return null;
var nonce = new byte[12];
var tag = new byte[16];
var ct = new byte[blob.Length - 28];
Buffer.BlockCopy(blob, 0, nonce, 0, 12);
Buffer.BlockCopy(blob, 12, tag, 0, 16);
Buffer.BlockCopy(blob, 28, ct, 0, ct.Length);
var pt = new byte[ct.Length];
using (var aes = new AesGcm(key, 16))
aes.Decrypt(nonce, ct, tag, pt);
return Encoding.UTF8.GetString(pt);
}
catch { return null; }
}
public static void Clear()
{
try { if (File.Exists(DataFile)) File.Delete(DataFile); } catch { }
}
}