LarpixServer/LarpixServer/Account/Requests.cs
olcxja 79af6fcddf
All checks were successful
Server Build / publish (push) Successful in 29s
Voice Build / publish (push) Successful in 26s
Switch to X25519 + ML-KEM-768 encryption
2026-05-27 20:39:40 +02:00

657 lines
No EOL
24 KiB
C#

using System.Collections.Concurrent;
using System.Numerics;
using System.Text;
using System.Text.Json;
using LarpixServer.Filesystem;
using LarpixServer.Utils;
using LarpixServer.Utils.Jsons;
using static LarpixServer.Utils.Utils;
using Org.BouncyCastle.Crypto.Parameters;
namespace LarpixServer.Account;
public class Requests
{
public static ConcurrentDictionary<string, CreateHolder> createHolder = new();
public static ConcurrentDictionary<string, (string, DateTimeOffset)> nonceHolder = new();
public static SemaphoreSlim createLock = new SemaphoreSlim(1, 1);
public static async Task Delete(HttpContext context, Func<Task> next, IQueryCollection query,
StreamReader bodyReader)
{
if (!query.TryGetValue("id", out var idQuery))
{
return;
}
if (!context.Request.Headers.TryGetValue("secret", out var secret))
{
return;
}
string id = idQuery.ToString().Split(":")[0];
string password = await Utils.GetPassword(id);
secret = await Utils.NonceDecryptBody(id, password, secret);
string auth = await Utils.Auth(id, password, secret);
if (auth != Utils.LOGIN_SUCCESS)
{
await context.Response.WriteAsync(auth);
return;
}
SemaphoreSlim userLock = Utils.GetUserLock(id);
await userLock.WaitAsync();
try
{
await Fs.WriteFile($"{ACCOUNTS_DATA_DIR}/{id}/lastlogin",
Encoding.UTF8.GetBytes(DateTimeOffset.UtcNow.AddMonths(-11).ToUnixTimeSeconds().ToString()));
}
finally
{
userLock.Release();
}
await context.Response.WriteAsync(
"ok|Your account will be deleted in one month|Please make sure you are logged out on all devices. Logging back into your account during this period will stop the deletion process");
}
public static async Task Create(HttpContext context, Func<Task> next, IQueryCollection query,
StreamReader bodyReader)
{
if (!query.TryGetValue("step", out var step))
{
return;
}
await createLock.WaitAsync();
try
{
switch (step)
{
case "init":
{
foreach (var kvp in createHolder) // czyszczenie nieaktywnych od 2 minut requestow
{
if (kvp.Value.date < DateTimeOffset.UtcNow.AddMinutes(-2))
{
createHolder.TryRemove(kvp.Key, out _);
}
}
context.Response.ContentType = mimeTypes["json"];
var serverInfo = Encryption.Encryption.InitHybridKEM();
KeyExchangePayload payload = new KeyExchangePayload();
payload.pubX25519 = Convert.ToBase64String(serverInfo.pubX25519);
payload.pubMlKem = Convert.ToBase64String(serverInfo.pubMlKem);
payload.idKey = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + "\n";
while (createHolder.ContainsKey(payload.idKey))
{
payload.idKey += Random.Shared.Next(0, 10).ToString();
}
CreateHolder dataHolder = new();
dataHolder.date = DateTimeOffset.UtcNow;
dataHolder.privX25519Server = serverInfo.privX25519;
dataHolder.privMlKemServer = serverInfo.privMlKem;
createHolder.TryAdd(payload.idKey, dataHolder);
var serializedPayload = JsonSerializer.Serialize(
payload,
AppJsonSerializerContext.Default.KeyExchangePayload
);
await context.Response.WriteAsync(serializedPayload);
return;
}
case "register":
{
string body = await LoadBody(bodyReader);
KeyExchangePayloadClient serializedBody = JsonSerializer.Deserialize(
body,
AppJsonSerializerContext.Default.KeyExchangePayloadClient
);
if (!createHolder.TryGetValue(serializedBody.idKey, out CreateHolder entry))
{
await context.Response.WriteAsync("error:account.creation.request.expired");
return;
}
entry.date = DateTimeOffset.UtcNow;
byte[] pubX25519Client = Convert.FromBase64String(serializedBody.pubX25519);
byte[] ciphertextMlKem = Convert.FromBase64String(serializedBody.ciphertextMlKem);
byte[] sharedKey = Encryption.Encryption.CalcHybridSharedKey(
pubX25519Client, entry.privX25519Server, ciphertextMlKem, entry.privMlKemServer);
entry.name = Encryption.Encryption.Decrypt(serializedBody.username, sharedKey);
entry.pass = Encryption.Encryption.Decrypt(serializedBody.password, sharedKey);
if (!Utils.IsValidUsername(entry.name, out string message))
{
await context.Response.WriteAsync(message);
return;
}
if (!Utils.IsValidPassword(entry.pass, out message))
{
await context.Response.WriteAsync(message);
return;
}
(byte[] ImageBytes, string CaptchaText) captchaResult = Captcha.GenerateCaptcha();
entry.captcha = captchaResult.CaptchaText;
context.Response.ContentType = mimeTypes["webp"];
context.Response.ContentLength = captchaResult.ImageBytes.Length;
await context.Response.Body.WriteAsync(captchaResult.ImageBytes, 0,
captchaResult.ImageBytes.Length);
return;
}
case "finish":
{
string body = await LoadBody(bodyReader);
CaptchaPayloadClient serialized = JsonSerializer.Deserialize(
body,
AppJsonSerializerContext.Default.CaptchaPayloadClient
);
if (!createHolder.TryGetValue(serialized.idKey, out var entry))
{
await context.Response.WriteAsync("error:account.creation.request.expired");
return;
}
if (entry.captcha.ToLower() != serialized.captcha.ToLower())
{
createHolder.TryRemove(serialized.idKey, out _);
await context.Response.WriteAsync("error:incorrect.captcha");
return;
}
string lowerName = entry.name.ToLowerInvariant();
if (Fs.Exists($"{ACCOUNTS_NAME_DIR}/{lowerName}"))
{
await context.Response.WriteAsync("error:username.taken");
return;
}
string registrationString =
Encoding.UTF8.GetString(await Fs.ReadFile($"{ACCOUNTS_DIR}/registration"));
if (registrationString.StartsWith("0;"))
{
await context.Response.WriteAsync("error:registration.disabled");
return;
}
if (registrationString.StartsWith("code;"))
{
bool valid = false;
string[] regArray = registrationString.Split(';');
foreach (string regEntryString in regArray)
{
string[] regEntry = regEntryString.Split(":");
if (regEntry[0] == "|" + serialized.regKey)
{
if (regEntry[1] != "infinite")
{
int left = int.Parse(regEntry[1]) - 1;
if (left < 1)
{
await Fs.WriteFile($"{ACCOUNTS_DIR}/registration",
Encoding.UTF8.GetBytes(
registrationString.Replace(regEntryString + ";", "")));
}
}
valid = true;
break;
}
}
if (!valid)
{
await context.Response.WriteAsync(
"error:registration.disabled");
return;
}
}
else if (registrationString.StartsWith("first;"))
{
await Fs.WriteFile($"{ACCOUNTS_DIR}/registration", Encoding.UTF8.GetBytes("0;"));
}
ulong id = ulong.Parse(await Fs.ReadFile($"{ACCOUNTS_DIR}/last"));
id++;
var freeid = Path.GetFileName(Directory.EnumerateFiles(ACCOUNTS_FREEID_DIR).FirstOrDefault());
if (freeid != null)
{
id = ulong.Parse(freeid);
Fs.DeleteFile($"{ACCOUNTS_FREEID_DIR}/{freeid}");
}
else
{
if (id == ulong.MaxValue)
{
await context.Response.WriteAsync(
"error:accounts.slots.full");
return;
}
await Fs.WriteFile($"{ACCOUNTS_DIR}/last", Encoding.UTF8.GetBytes(id.ToString()));
}
SemaphoreSlim userLock = Utils.GetUserLock(id.ToString());
await userLock.WaitAsync();
try
{
await Fs.WriteFile($"{ACCOUNTS_NAME_DIR}/{lowerName}", Encoding.UTF8.GetBytes(id.ToString()));
await Fs.WriteFile($"{ACCOUNTS_DATA_DIR}/{id.ToString()}/username",
Encoding.UTF8.GetBytes(entry.name));
await Fs.WriteFile($"{ACCOUNTS_DATA_DIR}/{id.ToString()}/secret",
Encoding.UTF8.GetBytes(entry.pass));
createHolder.TryRemove(serialized.idKey, out _);
await Utils.UpdateLastLogin(id.ToString());
}
finally
{
userLock.Release();
}
await context.Response.WriteAsync("success:account.created");
return;
}
}
await next();
}
finally
{
createLock.Release();
}
}
public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollection query, StreamReader bodyReader)
{
if (!query.TryGetValue("id", out var idQuery))
{
return;
}
if (!context.Request.Headers.TryGetValue("secret", out var secret))
{
return;
}
string id = idQuery.ToString().Split(":")[0];
string password = await Utils.GetPassword(id);
secret = await Utils.NonceDecryptBody(id, password, secret);
string auth = await Utils.Auth(id, password, secret);
await context.Response.WriteAsync(auth);
return;
}
public static async Task CorrectedName(HttpContext context, Func<Task> next, IQueryCollection query)
{
if (!query.TryGetValue("u", out var username))
{
return;
}
if (!Utils.IsUserLocal(username, out string domain)) //federation :(
{
return;
}
username = username.ToString().Split(":")[0];
await context.Response.WriteAsync(await Utils.NameFromId(await Utils.IdFromName(username)));
return;
}
public static async Task NameToId(HttpContext context, Func<Task> next, IQueryCollection query)
{
if (!query.TryGetValue("u", out var username))
{
return;
}
if (!Utils.IsUserLocal(username, out string domain)) //federation
{
username = username.ToString().Split(":")[0];
string remoteId = await Federation.Sender.GetUserId(username, domain);
if (remoteId != "0" && !string.IsNullOrEmpty(remoteId))
{
await context.Response.WriteAsync(remoteId + $":{domain}");
}
return;
}
username = username.ToString().Split(":")[0];
await context.Response.WriteAsync(await Utils.IdFromName(username) + $":{DOMAIN}");
return;
}
public static async Task IdToName(HttpContext context, Func<Task> next, IQueryCollection query)
{
if (!query.TryGetValue("id", out var id))
{
return;
}
if (!Utils.IsUserLocal(id, out string domain)) //federation :(
{
return;
}
id = id.ToString().Split(":")[0];
await context.Response.WriteAsync(await Utils.NameFromId(id) + $":{DOMAIN}");
return;
}
public static async Task NextNonce(HttpContext context, Func<Task> next, IQueryCollection query)
{
if (!query.TryGetValue("id", out var idQuery))
{
return;
}
string id = idQuery.ToString().Split(":")[0];
string plainPass = await Utils.GetPassword(id);
foreach (var kvp in nonceHolder) //clearowanie nieuzytych nonce
{
if (kvp.Value.Item2 < DateTimeOffset.UtcNow.AddMinutes(-2))
{
nonceHolder.TryRemove(kvp.Key, out _);
}
else if (kvp.Key == id)
{
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(kvp.Value.Item1,
plainPass));
return;
}
}
string nonce = Encryption.Encryption.GetRandomString(64);
nonceHolder.TryAdd(id, (nonce, DateTimeOffset.UtcNow));
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(nonce,
plainPass));
return;
}
public static async Task ChangePassword(HttpContext context, Func<Task> next, IQueryCollection query,
StreamReader bodyReader)
{
if (!query.TryGetValue("id", out var idQuery))
{
return;
}
if (!context.Request.Headers.TryGetValue("secret", out var secret))
{
return;
}
string id = idQuery.ToString().Split(":")[0];
string body = await LoadBody(bodyReader);
string password = await Utils.GetPassword(id);
string newPass = await Utils.NonceDecryptBody(id, password, body, false);
secret = await Utils.NonceDecryptBody(id, password, secret);
string auth = await Utils.Auth(id, password, secret);
if (auth != Utils.LOGIN_SUCCESS)
{
await context.Response.WriteAsync(auth);
return;
}
SemaphoreSlim userLock = Utils.GetUserLock(id);
await userLock.WaitAsync();
try
{
if (!Utils.IsValidPassword(newPass, out string message))
{
await context.Response.WriteAsync(message);
return;
}
await Utils.SetPassword(id, newPass);
}
finally
{
userLock.Release();
}
await context.Response.WriteAsync("success:password.changed");
return;
}
public static async Task ChangeUsername(HttpContext context, Func<Task> next, IQueryCollection query,
StreamReader bodyReader)
{
if (!query.TryGetValue("id", out var idQuery))
{
return;
}
if (!context.Request.Headers.TryGetValue("secret", out var secret))
{
return;
}
string id = idQuery.ToString().Split(":")[0];
string body = await LoadBody(bodyReader);
string password = await Utils.GetPassword(id);
body = await Utils.NonceDecryptBody(id, password, body, false);
secret = await Utils.NonceDecryptBody(id, password, secret);
string auth = await Utils.Auth(id, secret, password);
if (auth != Utils.LOGIN_SUCCESS)
{
await context.Response.WriteAsync(auth);
return;
}
SemaphoreSlim userLock = Utils.GetUserLock(id.ToString());
await userLock.WaitAsync();
try
{
await context.Response.WriteAsync(await Utils.SetUsername(id, body));
}
finally
{
userLock.Release();
}
return;
}
public static async Task<string> UpdateUserKeys(string id, string body)
{
Universal2String serializedBody = JsonSerializer.Deserialize(
body,
AppJsonSerializerContext.Default.Universal2String
);
//string publicKey = serializedBody.string1;
//string privateEncryptedKey = serializedBody.string2;
await Utils.UpdateUserKeys(id, body);
return "success:keys.updated";
}
public static async Task GetUserPublicKey(HttpContext context, Func<Task> next, IQueryCollection query, StreamReader bodyReader)
{
if (!query.TryGetValue("id", out var idQuery))
{
return;
}
if (!Utils.IsUserLocal(idQuery, out string domain)) //federation :(
{
return;
}
string id = idQuery.ToString().Split(":")[0];
Universal2String keys = JsonSerializer.Deserialize(
await Utils.GetUserKeys(id),
AppJsonSerializerContext.Default.Universal2String
);
await context.Response.WriteAsync(keys.string2);
return;
}
public static async Task GetUserPublicStorageEntry(HttpContext context, Func<Task> next, IQueryCollection query, StreamReader bodyReader)
{
if (!query.TryGetValue("id", out var idQuery))
{
return;
}
if (!query.TryGetValue("e", out var entry))
{
return;
}
if (!Utils.IsUserLocal(idQuery, out string domain)) //federation :(
{
return;
}
string id = idQuery.ToString().Split(":")[0];
byte[] entryByte = await Utils.GetUserPublicStorageEntry(id, entry);
context.Response.ContentType = mimeTypes["unknown"];
context.Response.ContentLength = entryByte.Length;
await context.Response.Body.WriteAsync(entryByte, 0,
entryByte.Length);
return;
}
public static async Task EncryptedRequest(HttpContext context, Func<Task> next, IQueryCollection query, StreamReader bodyReader)
{
if (!query.TryGetValue("id", out var idQuery))
{
return;
}
if (!context.Request.Headers.TryGetValue("secret", out var secret))
{
return;
}
string id = idQuery.ToString().Split(":")[0];
string body = await LoadBody(bodyReader);
string password = await Utils.GetPassword(id);
body = await Utils.NonceDecryptBody(id, password, body, false);
secret = await Utils.NonceDecryptBody(id, password, secret);
Universal2String serializedBody = JsonSerializer.Deserialize(
body,
AppJsonSerializerContext.Default.Universal2String
);
string auth = await Utils.Auth(id, secret, password);
if (auth != Utils.LOGIN_SUCCESS)
{
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(auth, secret));
return;
}
SemaphoreSlim userLock = Utils.GetUserLock(id);
await userLock.WaitAsync();
try
{
switch (serializedBody.string1)
{
case "user/invites/sent":
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Utils.GetUserSentInvites(id)
, password));
break;
case "user/invites/received":
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Utils.GetUserReceivedInvites(id)
, password));
break;
case "user/key/update":
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await UpdateUserKeys(id, serializedBody.string2)
, password));
break;
case "user/key/get":
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Utils.GetUserKeys(id)
, password));
break;
case "user/dm/list":
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Utils.GetUserDms(id)
, password));
break;
case "user/dm/invite":
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Requests.DmInvite(id, serializedBody.string2)
, password));
break;
case "user/dm/invite/revoke":
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Requests.DmInviteRevoke(id, serializedBody.string2)
, password));
break;
case "user/dm/invite/decline":
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Requests.DmInviteDecline(id, serializedBody.string2)
, password));
break;
case "user/dm/create":
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Requests.DmCreate(id, serializedBody.string2)
, password));
break;
default:
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(
$"error:unknown.request:{serializedBody.string1}"
, password));
break;
}
}
finally
{
userLock.Release();
}
return;
}
}
public class CreateHolder
{
public DateTimeOffset date;
public string name;
public string pass;
public string captcha;
public X25519PrivateKeyParameters privX25519Server;
public MLKemPrivateKeyParameters privMlKemServer;
}