massive security update
This commit is contained in:
parent
1915198d82
commit
6dc145a2c8
11 changed files with 739 additions and 225 deletions
|
|
@ -1,6 +1,7 @@
|
|||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace LarpixServer.Account;
|
||||
|
||||
|
|
@ -20,7 +21,8 @@ public class Captcha
|
|||
|
||||
for (int i = 0; i < charCount; i++)
|
||||
{
|
||||
captchaSpan[i] = Chars[Random.Shared.Next(Chars.Length)];
|
||||
//tekst captchy MUSI pochodzić z CSPRNG - Random.Shared (MT19937) jest przewidywalny
|
||||
captchaSpan[i] = Chars[RandomNumberGenerator.GetInt32(Chars.Length)];
|
||||
charColors[i] = new SKColor(
|
||||
(byte)Random.Shared.Next(100, 256),
|
||||
(byte)Random.Shared.Next(100, 256),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Numerics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using LarpixServer.Filesystem;
|
||||
|
|
@ -36,6 +37,18 @@ public class Requests
|
|||
return false;
|
||||
}
|
||||
|
||||
//wywoływane przez sweeper w Program.cs - usuwa wygasłe okna
|
||||
public static void PruneActionRateLimits(long nowSec)
|
||||
{
|
||||
foreach (var entry in actionRateLimits)
|
||||
{
|
||||
if (entry.Value.resetTime <= DateTimeOffset.FromUnixTimeSeconds(nowSec))
|
||||
{
|
||||
actionRateLimits.TryRemove(entry.Key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static SemaphoreSlim createLock = new SemaphoreSlim(1, 1);
|
||||
|
||||
|
||||
|
|
@ -90,6 +103,14 @@ public class Requests
|
|||
{
|
||||
case "init":
|
||||
{
|
||||
//KEM keygen jest kosztowny - limit per IP
|
||||
string initIp = context.Connection.RemoteIpAddress?.ToString() ?? "";
|
||||
if (RateLimited(AccountInitRateLimits, initIp, ACCOUNT_INIT_RATE_LIMIT))
|
||||
{
|
||||
await context.Response.WriteAsync("error:rate.limit");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var kvp in createHolder) // czyszczenie nieaktywnych od 2 minut requestow
|
||||
{
|
||||
if (kvp.Value.date < DateTimeOffset.UtcNow.AddMinutes(-2))
|
||||
|
|
@ -106,15 +127,17 @@ public class Requests
|
|||
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;
|
||||
dataHolder.KdfSalt = Convert.ToBase64String(RandomNumberGenerator.GetBytes(16));
|
||||
payload.kdfSalt = dataHolder.KdfSalt;
|
||||
|
||||
while (createHolder.ContainsKey(payload.idKey))
|
||||
{
|
||||
payload.idKey += Random.Shared.Next(0, 10).ToString();
|
||||
}
|
||||
|
||||
createHolder.TryAdd(payload.idKey, dataHolder);
|
||||
|
||||
|
|
@ -271,6 +294,11 @@ public class Requests
|
|||
Encoding.UTF8.GetBytes(entry.name));
|
||||
await Fs.WriteFile($"{ACCOUNTS_DATA_DIR}/{id.ToString()}/secret",
|
||||
Encoding.UTF8.GetBytes(entry.pass));
|
||||
if (!string.IsNullOrEmpty(entry.KdfSalt))
|
||||
{
|
||||
await Fs.WriteFile($"{ACCOUNTS_DATA_DIR}/{id.ToString()}/kdfsalt",
|
||||
Encoding.UTF8.GetBytes(entry.KdfSalt));
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
|
@ -314,6 +342,15 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
}
|
||||
|
||||
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
||||
|
||||
//rate limit prob logowania (konto+IP)
|
||||
string clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "";
|
||||
if (RateLimited(AuthRateLimits, $"{id}:{clientIp}", AUTH_RATE_LIMIT))
|
||||
{
|
||||
await context.Response.WriteAsync("error:rate.limit");
|
||||
return;
|
||||
}
|
||||
|
||||
string password = await Utils.GetPassword(id);
|
||||
secret = await Utils.NonceDecryptBody(id, password, secret);
|
||||
string auth = await Utils.Auth(id, password, secret);
|
||||
|
|
@ -347,14 +384,9 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
return;
|
||||
}
|
||||
|
||||
if (!Utils.IsUserLocal(username, out string domain)) //federation
|
||||
if (!Utils.IsUserLocal(username, out string domain)) //federation wylaczona (brak uwierzytelnienia)
|
||||
{
|
||||
username = Utils.GetIdFromUsernameWD(username.ToString());
|
||||
string remoteId = await Federation.Sender.GetUserId(username, domain);
|
||||
if (remoteId != "0" && !string.IsNullOrEmpty(remoteId))
|
||||
{
|
||||
await context.Response.WriteAsync(remoteId + $":{domain}");
|
||||
}
|
||||
await context.Response.WriteAsync("error:federation.disabled");
|
||||
return;
|
||||
}
|
||||
username = Utils.GetIdFromUsernameWD(username.ToString());
|
||||
|
|
@ -381,6 +413,34 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
|
||||
}
|
||||
|
||||
// Zwraca sól KDF konta: "P:<base64>" = PBKDF2, "S:" = brak soli (konto nielogowalne nowym klientem).
|
||||
public static async Task GetKdfSalt(HttpContext context, IQueryCollection query)
|
||||
{
|
||||
if (!query.TryGetValue("id", out var idQuery))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//rate limit per IP
|
||||
string clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "";
|
||||
if (RateLimited(KdfSaltRateLimits, clientIp, KDFSALT_RATE_LIMIT))
|
||||
{
|
||||
context.Response.StatusCode = 429;
|
||||
return;
|
||||
}
|
||||
|
||||
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
||||
string path = $"{ACCOUNTS_DATA_DIR}/{id}/kdfsalt";
|
||||
if (!Fs.Exists(path))
|
||||
{
|
||||
await context.Response.WriteAsync("S:");
|
||||
return;
|
||||
}
|
||||
|
||||
string salt = Encoding.UTF8.GetString(await Fs.ReadFile(path));
|
||||
await context.Response.WriteAsync("P:" + salt);
|
||||
}
|
||||
|
||||
public static async Task NextNonce(HttpContext context, Func<Task> next, IQueryCollection query)
|
||||
{
|
||||
if (!query.TryGetValue("id", out var idQuery))
|
||||
|
|
@ -389,6 +449,15 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
}
|
||||
|
||||
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
||||
|
||||
//rate limit pobran nonce (konto)
|
||||
if (RateLimited(NonceRateLimits, id, NONCE_RATE_LIMIT))
|
||||
{
|
||||
string pass = await Utils.GetPassword(id);
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient("error:rate.limit", pass));
|
||||
return;
|
||||
}
|
||||
|
||||
string plainPass = await Utils.GetPassword(id);
|
||||
foreach (var kvp in nonceHolder) //clearowanie nieuzytych nonce
|
||||
{
|
||||
|
|
@ -398,7 +467,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
}
|
||||
else if (kvp.Key == id)
|
||||
{
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(kvp.Value.Item1,
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(kvp.Value.Item1,
|
||||
plainPass));
|
||||
return;
|
||||
}
|
||||
|
|
@ -406,7 +475,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
|
||||
string nonce = Encryption.Encryption.GetRandomString(64);
|
||||
nonceHolder.TryAdd(id, (nonce, DateTimeOffset.UtcNow));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(nonce,
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(nonce,
|
||||
plainPass));
|
||||
return;
|
||||
}
|
||||
|
|
@ -524,16 +593,8 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
return;
|
||||
}
|
||||
|
||||
SemaphoreSlim userLock = Utils.GetUserLock(id.ToString());
|
||||
await userLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
//SetUsername sam zarządza lockami (createLock -> userLock) - patrz Account.Utils.SetUsername
|
||||
await context.Response.WriteAsync(await Utils.SetUsername(id, body));
|
||||
}
|
||||
finally
|
||||
{
|
||||
userLock.Release();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -657,29 +718,16 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
|
||||
if (auth != Utils.LOGIN_SUCCESS)
|
||||
{
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(auth, secret));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(auth, secret));
|
||||
return;
|
||||
}
|
||||
|
||||
string[] actionRequests = {
|
||||
"user/dm/invite", "user/dm/invite/revoke", "user/dm/invite/decline", "user/dm/create",
|
||||
"dm/message/send", "dm/message/delete", "dm/message/react", "dm/key/update", "dm/message/pin/toggle",
|
||||
"dm/message/react/add", "dm/message/react/remove",
|
||||
"user/storage/public/update", "user/storage/private/update", "dm/read",
|
||||
"group/create", "user/group/list", "group/info", "group/settings/update",
|
||||
"user/invite/accept", "user/invite/decline", "user/invite/revoke", "user/dm/list", "user/group/list",
|
||||
"user/storage/public/update", "user/storage/private/update", "group/storage/public/update", "group/storage/private/update", "group/storage/private/getentry", "dm/read", "group/read",
|
||||
"dm/message/send", "dm/message/delete", "dm/message/react/add", "dm/message/react/remove", "dm/message/pin/toggle",
|
||||
"group/messages/get", "group/messages/pinned"
|
||||
};
|
||||
if (System.Array.IndexOf(actionRequests, serializedBody.string1) >= 0)
|
||||
{
|
||||
//limit wszystkich żądań encryptedrequest per konto
|
||||
if (IsActionRateLimited(id))
|
||||
{
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString("error:rate.limit", password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient("error:rate.limit", password));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SemaphoreSlim userLock = Utils.GetUserLock(id);
|
||||
await userLock.WaitAsync();
|
||||
|
|
@ -689,25 +737,25 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
{
|
||||
case "user/invites/sent":
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Utils.GetUserSentInvites(id)
|
||||
, password));
|
||||
break;
|
||||
case "user/invites/received":
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Utils.GetUserReceivedInvites(id)
|
||||
, password));
|
||||
break;
|
||||
case "user/key/update":
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await UpdateUserKeys(id, serializedBody.string2)
|
||||
, password));
|
||||
break;
|
||||
case "user/key/get":
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Utils.GetUserKeys(id)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -718,7 +766,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal2String
|
||||
);
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Utils.UpdateUserPublicStorageEntry(id, storageUpdate.string1, storageUpdate.string2)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -730,7 +778,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal2String
|
||||
);
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Utils.UpdateUserPrivateStorageEntry(id, storageUpdate.string1, storageUpdate.string2)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -742,7 +790,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal3String
|
||||
);
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Groups.UpdateGroupPublicStorageEntry(id, storageUpdate.string1, storageUpdate.string2, storageUpdate.string3)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -754,7 +802,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal3String
|
||||
);
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Groups.UpdateGroupPrivateStorageEntry(id, storageUpdate.string1, storageUpdate.string2, storageUpdate.string3)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -766,43 +814,43 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal2String
|
||||
);
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Groups.GetGroupPrivateStorageEntry(id, storageUpdate.string1, storageUpdate.string2)
|
||||
, password));
|
||||
break;
|
||||
}
|
||||
case "user/dm/list":
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Utils.GetUserDms(id)
|
||||
, password));
|
||||
break;
|
||||
case "user/group/list":
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Groups.GetUserGroups(id)
|
||||
, password));
|
||||
break;
|
||||
case "user/dm/invite":
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Requests.DmInvite(id, serializedBody.string2)
|
||||
, password));
|
||||
break;
|
||||
case "user/dm/invite/revoke":
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Requests.DmInviteRevoke(id, serializedBody.string2)
|
||||
, password));
|
||||
break;
|
||||
case "user/dm/invite/decline":
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
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));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(await Room.Requests.DmCreate(id, serializedBody.string2), password));
|
||||
break;
|
||||
case "group/create":
|
||||
{
|
||||
|
|
@ -810,11 +858,11 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
serializedBody.string2,
|
||||
AppJsonSerializerContext.Default.Universal3String
|
||||
);
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupCreate(id, groupCreate.string1, groupCreate.string2, groupCreate.string3), password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(await Room.Groups.GroupCreate(id, groupCreate.string1, groupCreate.string2, groupCreate.string3), password));
|
||||
break;
|
||||
}
|
||||
case "group/info":
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupInfo(serializedBody.string2), password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(await Room.Groups.GroupInfo(serializedBody.string2), password));
|
||||
break;
|
||||
case "group/settings/update":
|
||||
{
|
||||
|
|
@ -822,7 +870,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
serializedBody.string2,
|
||||
AppJsonSerializerContext.Default.Universal3String
|
||||
);
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupSettingsUpdate(id, req.string1, req.string2, req.string3), password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(await Room.Groups.GroupSettingsUpdate(id, req.string1, req.string2, req.string3), password));
|
||||
break;
|
||||
}
|
||||
case "group/invite":
|
||||
|
|
@ -831,7 +879,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
serializedBody.string2,
|
||||
AppJsonSerializerContext.Default.Universal3String
|
||||
);
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupInvite(id, payload.string1, payload.string2, payload.string3), password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(await Room.Groups.GroupInvite(id, payload.string1, payload.string2, payload.string3), password));
|
||||
break;
|
||||
}
|
||||
case "group/invite/revoke":
|
||||
|
|
@ -840,7 +888,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
serializedBody.string2,
|
||||
AppJsonSerializerContext.Default.Universal2String
|
||||
);
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupInviteRevoke(id, payload.string1, payload.string2), password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(await Room.Groups.GroupInviteRevoke(id, payload.string1, payload.string2), password));
|
||||
break;
|
||||
}
|
||||
case "group/invite/decline":
|
||||
|
|
@ -849,7 +897,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
serializedBody.string2,
|
||||
AppJsonSerializerContext.Default.Universal2String
|
||||
);
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupInviteDecline(id, payload.string1, payload.string2), password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(await Room.Groups.GroupInviteDecline(id, payload.string1, payload.string2), password));
|
||||
break;
|
||||
}
|
||||
case "group/join":
|
||||
|
|
@ -858,7 +906,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
serializedBody.string2,
|
||||
AppJsonSerializerContext.Default.Universal2String
|
||||
);
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupJoin(id, payload.string1, payload.string2), password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(await Room.Groups.GroupJoin(id, payload.string1, payload.string2), password));
|
||||
break;
|
||||
}
|
||||
case "group/leave":
|
||||
|
|
@ -867,7 +915,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
serializedBody.string2,
|
||||
AppJsonSerializerContext.Default.Universal2String
|
||||
);
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupLeave(id, payload.string1), password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(await Room.Groups.GroupLeave(id, payload.string1), password));
|
||||
break;
|
||||
}
|
||||
case "group/kick":
|
||||
|
|
@ -876,7 +924,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
serializedBody.string2,
|
||||
AppJsonSerializerContext.Default.Universal2String
|
||||
);
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupKickMember(id, payload.string1, payload.string2), password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(await Room.Groups.GroupKickMember(id, payload.string1, payload.string2), password));
|
||||
break;
|
||||
}
|
||||
case "group/transferowner":
|
||||
|
|
@ -885,7 +933,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
serializedBody.string2,
|
||||
AppJsonSerializerContext.Default.Universal2String
|
||||
);
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupTransferOwner(id, payload.string1, payload.string2), password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(await Room.Groups.GroupTransferOwner(id, payload.string1, payload.string2), password));
|
||||
break;
|
||||
}
|
||||
case "room/role":
|
||||
|
|
@ -894,14 +942,14 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
serializedBody.string2,
|
||||
AppJsonSerializerContext.Default.Universal3String
|
||||
);
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Shared.GetUserRole(id, payload.string1, payload.string2, payload.string3), password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(await Room.Shared.GetUserRole(id, payload.string1, payload.string2, payload.string3), password));
|
||||
break;
|
||||
}
|
||||
case "group/members":
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupMembers(id, serializedBody.string2), password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(await Room.Groups.GroupMembers(id, serializedBody.string2), password));
|
||||
break;
|
||||
case "group/key/rotate":
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupKeyRotate(id, serializedBody.string2), password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(await Room.Groups.GroupKeyRotate(id, serializedBody.string2), password));
|
||||
break;
|
||||
case "group/key/get":
|
||||
{
|
||||
|
|
@ -909,7 +957,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
serializedBody.string2,
|
||||
AppJsonSerializerContext.Default.Universal2String
|
||||
);
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupKeyGet(id, payload.string1, payload.string2), password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(await Room.Groups.GroupKeyGet(id, payload.string1, payload.string2), password));
|
||||
break;
|
||||
}
|
||||
case "dm/message/send":
|
||||
|
|
@ -919,7 +967,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal4String
|
||||
);
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Shared.MessageSend(id, "dms", msgSend.string1, msgSend.string2, msgSend.string3, msgSend.string4)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -931,7 +979,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal2String
|
||||
);
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Shared.MessageDelete(id, "dms", msgDel.string1, msgDel.string2)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -943,7 +991,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal2String
|
||||
);
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Shared.MessagePinToggle(id, "dms", msgPin.string1, msgPin.string2)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -955,7 +1003,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal3String
|
||||
);
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Shared.MessageReactAdd(id, "dms", msgReact.string1, msgReact.string2, msgReact.string3)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -967,7 +1015,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal3String
|
||||
);
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Shared.MessageReactRemove(id, "dms", msgReactRm.string1, msgReactRm.string2, msgReactRm.string3)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -978,12 +1026,12 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
serializedBody.string2,
|
||||
AppJsonSerializerContext.Default.Universal3String
|
||||
);
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Shared.GetMessages(id, "dms", msgGet.string1, msgGet.string3, msgGet.string2), password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(await Room.Shared.GetMessages(id, "dms", msgGet.string1, msgGet.string3, msgGet.string2), password));
|
||||
break;
|
||||
}
|
||||
case "dm/messages/pinned":
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Shared.MessageGetPinned(id, "dms", serializedBody.string2)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -993,12 +1041,12 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
serializedBody.string2,
|
||||
AppJsonSerializerContext.Default.Universal3String
|
||||
);
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Shared.GetMessages(id, "groups", msgGet.string1, msgGet.string3, msgGet.string2), password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(await Room.Shared.GetMessages(id, "groups", msgGet.string1, msgGet.string3, msgGet.string2), password));
|
||||
break;
|
||||
}
|
||||
case "group/messages/pinned":
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Shared.MessageGetPinned(id, "groups", serializedBody.string2)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -1009,7 +1057,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal5String
|
||||
);
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Shared.MessageSend(id, "groups", msgSend.string1, msgSend.string2, msgSend.string3, msgSend.string4, msgSend.string5)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -1021,7 +1069,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal2String
|
||||
);
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Shared.MessageDelete(id, "groups", msgDel.string1, msgDel.string2)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -1033,26 +1081,26 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal3String
|
||||
);
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Shared.MessageReactAdd(id, "groups", msgReact.string1, msgReact.string2, msgReact.string3)
|
||||
, password));
|
||||
break;
|
||||
}
|
||||
case "room/voice/join":
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Shared.VoiceJoin(id, serializedBody.string2)
|
||||
, password));
|
||||
break;
|
||||
case "room/voice/leave":
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Shared.VoiceLeave(id, serializedBody.string2)
|
||||
, password));
|
||||
break;
|
||||
case "room/signal":
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Shared.RoomSignal(id, serializedBody.string2)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -1063,7 +1111,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal3String
|
||||
);
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Shared.MessageReactRemove(id, "groups", msgReact.string1, msgReact.string2, msgReact.string3)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -1075,7 +1123,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal2String
|
||||
);
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Shared.MessagePinToggle(id, "groups", msgPin.string1, msgPin.string2)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -1087,7 +1135,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal2String
|
||||
);
|
||||
await Account.Utils.UpdateUserDm(id, readGet.string1, "true", "");
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString("success:dm.read", password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient("success:dm.read", password));
|
||||
break;
|
||||
}
|
||||
case "group/read":
|
||||
|
|
@ -1097,12 +1145,12 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal2String
|
||||
);
|
||||
await Room.Groups.UpdateUserGroup(id, readGet.string1, "true", "");
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString("success:group.read", password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient("success:group.read", password));
|
||||
break;
|
||||
}
|
||||
case "dm/key/get":
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Requests.GetDmKey(id, serializedBody.string2)
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -1113,13 +1161,13 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
AppJsonSerializerContext.Default.Universal2String
|
||||
);
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Room.Requests.UpdateDmKey(id, dmKeyUpdate.string1, dmKeyUpdate.string2)
|
||||
, password));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(
|
||||
$"error:unknown.request:{serializedBody.string1}"
|
||||
, password));
|
||||
break;
|
||||
|
|
@ -1152,13 +1200,13 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
|
||||
if (auth != Utils.LOGIN_SUCCESS)
|
||||
{
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(auth, secretStr));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(auth, secretStr));
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsActionRateLimited(id))
|
||||
{
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString("error:rate.limit", password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient("error:rate.limit", password));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1171,7 +1219,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
{
|
||||
if (ex.Message.StartsWith("error:"))
|
||||
{
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(ex.Message, password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(ex.Message, password));
|
||||
return;
|
||||
}
|
||||
throw;
|
||||
|
|
@ -1180,7 +1228,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
|
||||
if (fileBytes == null)
|
||||
{
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString("error:invalid.nonce", password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient("error:invalid.nonce", password));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1189,7 +1237,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
try
|
||||
{
|
||||
await context.Response.WriteAsync(
|
||||
Encryption.Encryption.EncryptString(
|
||||
Encryption.Encryption.EncryptForClient(
|
||||
await Utils.UpdateUserPublicStorageEntryRaw(id, target, fileBytes)
|
||||
, password));
|
||||
}
|
||||
|
|
@ -1210,13 +1258,13 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
|
||||
if (auth != Utils.LOGIN_SUCCESS)
|
||||
{
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(auth, secretStr));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(auth, secretStr));
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsActionRateLimited(id))
|
||||
{
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString("error:rate.limit", password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient("error:rate.limit", password));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1229,14 +1277,14 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
{
|
||||
if (ex.Message.StartsWith("error:"))
|
||||
{
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(ex.Message, password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient(ex.Message, password));
|
||||
return;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
if (fileBytes == null || fileBytes.Length == 0)
|
||||
{
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString("error:empty.body", password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient("error:empty.body", password));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1256,7 +1304,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
}
|
||||
});
|
||||
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptString($"success:{stagingId}", password));
|
||||
await context.Response.WriteAsync(Encryption.Encryption.EncryptForClient($"success:{stagingId}", password));
|
||||
}
|
||||
|
||||
public static async Task GetDmAttachment(HttpContext context, Func<Task> next, IQueryCollection query)
|
||||
|
|
@ -1281,6 +1329,13 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
// Check if user is in room
|
||||
if (!await Room.Shared.IsMemberOfRoom(id, "dms", dmId)) return;
|
||||
|
||||
//walidacja komponentow sciezki - blokuje traversal do dowolnych plikow
|
||||
if (!Utils.IsSafePathComponent(msgId) || !Utils.IsSafePathComponent(attId))
|
||||
{
|
||||
context.Response.StatusCode = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
string filePath = $"{Room.Shared.GetRoomPath("dms", dmId)}/attachments/{msgId}/{attId}";
|
||||
if (!Fs.Exists(filePath))
|
||||
{
|
||||
|
|
@ -1314,6 +1369,13 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
|
|||
|
||||
if (!await Room.Shared.IsMemberOfRoom(id, "groups", dmId)) return;
|
||||
|
||||
//walidacja komponentow sciezki - blokuje traversal do dowolnych plikow
|
||||
if (!Utils.IsSafePathComponent(msgId) || !Utils.IsSafePathComponent(attId))
|
||||
{
|
||||
context.Response.StatusCode = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
string filePath = $"{Room.Shared.GetRoomPath("groups", dmId)}/attachments/{msgId}/{attId}";
|
||||
if (!Fs.Exists(filePath))
|
||||
{
|
||||
|
|
@ -1333,6 +1395,7 @@ public class CreateHolder
|
|||
public string name;
|
||||
public string pass;
|
||||
public string captcha;
|
||||
public string KdfSalt;
|
||||
public X25519PrivateKeyParameters privX25519Server;
|
||||
public MLKemPrivateKeyParameters privMlKemServer;
|
||||
}
|
||||
|
|
@ -45,6 +45,21 @@ public class Utils
|
|||
return ulong.TryParse(idPart, out _) ? idPart : "0";
|
||||
}
|
||||
|
||||
//Pojedynczy komponent sciezki (nazwa pliku/katalogu, id pokoju, klucz itp.)
|
||||
//Blokuje traversal ("..", "/", "\\") oraz puste/nadmiernie dlugie wartosci.
|
||||
public static bool IsSafePathComponent(string value, int maxLength = 128)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value) || value.Length > maxLength)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (value.Contains("..") || value.Contains('/') || value.Contains('\\'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string GetDmId(string id1, string domain1, string id2, string domain2)
|
||||
{
|
||||
string u1 = $"{id1};{domain1}";
|
||||
|
|
@ -191,7 +206,7 @@ public class Utils
|
|||
}
|
||||
}
|
||||
|
||||
return Encryption.Encryption.DecryptByte(body, nonce.Item1 + password);
|
||||
return Encryption.Encryption.DecryptBytesAuto(body, nonce.Item1 + password);
|
||||
}
|
||||
|
||||
public static async Task UpdateLastLogin(string id)
|
||||
|
|
@ -498,6 +513,24 @@ public class Utils
|
|||
await Fs.WriteFile(dmPath, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(fileDm, AppJsonSerializerContext.Default.Universal3String)));
|
||||
}
|
||||
|
||||
//Usuwa nieaktualny wpis DM z listy użytkownika - wywoływane gdy serwer wykryje
|
||||
//brak permisji przy próbie dostępu (użytkownik nie jest uczestnikiem rozmowy).
|
||||
public static void RemoveStaleUserDmEntry(string userId, string dmId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(dmId) || dmId.Contains("..") || dmId.Contains('/') || dmId.Contains('\\')) return;
|
||||
string entryPath = $"{ACCOUNTS_DATA_DIR}/{userId}/dms/{dmId}";
|
||||
if (Fs.Exists(entryPath))
|
||||
{
|
||||
Fs.DeleteFile(entryPath);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<string> GetUserDms(string id)
|
||||
{
|
||||
string path = $"{ACCOUNTS_DATA_DIR}/{id}/dms";
|
||||
|
|
@ -554,6 +587,11 @@ public class Utils
|
|||
string lowerNewName = newName.ToLowerInvariant();
|
||||
string lowerOldName = oldName.ToLowerInvariant();
|
||||
|
||||
//createLock -> userLock (ta sama kolejność co Create/finish i BackgroundDelete).
|
||||
//Zamyka TOCTOU z rejestracją: nazwa rezerwowana pod wspólnym lockiem.
|
||||
await Requests.createLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (lowerNewName != lowerOldName)
|
||||
{
|
||||
if (Fs.Exists($"{ACCOUNTS_NAME_DIR}/{lowerNewName}"))
|
||||
|
|
@ -564,6 +602,10 @@ public class Utils
|
|||
Fs.MoveFile($"{ACCOUNTS_NAME_DIR}/{lowerOldName}", $"{ACCOUNTS_NAME_DIR}/{lowerNewName}");
|
||||
}
|
||||
|
||||
SemaphoreSlim userLock = GetUserLock(id);
|
||||
await userLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
await Fs.WriteFile($"{ACCOUNTS_DATA_DIR}/{id}/username", Encoding.UTF8.GetBytes(newName));
|
||||
|
|
@ -576,6 +618,16 @@ public class Utils
|
|||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
userLock.Release();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Requests.createLock.Release();
|
||||
}
|
||||
|
||||
return "success:username.changed";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,117 @@ namespace LarpixServer.Encryption;
|
|||
|
||||
public class Encryption
|
||||
{
|
||||
public const string GcmPrefix = "G1:";
|
||||
|
||||
// Odpowiedź do klienta - zawsze AES-GCM (format "G1:" + b64(iv||ct||tag)).
|
||||
public static string EncryptForClient(string plainText, string passphrase)
|
||||
{
|
||||
return EncryptStringGcm(plainText, passphrase);
|
||||
}
|
||||
|
||||
private static byte[] PassphraseToKey(string passphrase)
|
||||
{
|
||||
return SHA256.HashData(Encoding.UTF8.GetBytes(passphrase));
|
||||
}
|
||||
|
||||
public static bool IsGcmPacket(string text)
|
||||
{
|
||||
return text != null && text.StartsWith(GcmPrefix, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public static string EncryptStringGcm(string plainText, string passphrase)
|
||||
{
|
||||
byte[] key = PassphraseToKey(passphrase);
|
||||
byte[] iv = RandomNumberGenerator.GetBytes(12);
|
||||
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
|
||||
byte[] cipherBytes = new byte[plainBytes.Length];
|
||||
byte[] tag = new byte[16];
|
||||
|
||||
using (AesGcm gcm = new AesGcm(key, 16))
|
||||
{
|
||||
gcm.Encrypt(iv, plainBytes, cipherBytes, tag);
|
||||
}
|
||||
|
||||
byte[] combined = new byte[iv.Length + cipherBytes.Length + tag.Length];
|
||||
Buffer.BlockCopy(iv, 0, combined, 0, iv.Length);
|
||||
Buffer.BlockCopy(cipherBytes, 0, combined, iv.Length, cipherBytes.Length);
|
||||
Buffer.BlockCopy(tag, 0, combined, iv.Length + cipherBytes.Length, tag.Length);
|
||||
|
||||
return GcmPrefix + Convert.ToBase64String(combined);
|
||||
}
|
||||
|
||||
public static string DecryptStringGcm(string gcmCipherText, string passphrase)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] combined = Convert.FromBase64String(gcmCipherText.Substring(GcmPrefix.Length));
|
||||
byte[] key = PassphraseToKey(passphrase);
|
||||
|
||||
byte[] iv = new byte[12];
|
||||
byte[] tag = new byte[16];
|
||||
byte[] cipherBytes = new byte[combined.Length - iv.Length - tag.Length];
|
||||
Buffer.BlockCopy(combined, 0, iv, 0, iv.Length);
|
||||
Buffer.BlockCopy(combined, iv.Length, cipherBytes, 0, cipherBytes.Length);
|
||||
Buffer.BlockCopy(combined, iv.Length + cipherBytes.Length, tag, 0, tag.Length);
|
||||
|
||||
byte[] plainBytes = new byte[cipherBytes.Length];
|
||||
using (AesGcm gcm = new AesGcm(key, 16))
|
||||
{
|
||||
gcm.Decrypt(iv, cipherBytes, tag, plainBytes);
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString(plainBytes);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Binaria przychodzące: wyłącznie "G1:" + iv||ct||tag. Bez prefiksu = odrzucamy.
|
||||
public static byte[] DecryptBytesAuto(byte[] combinedBytes, string passphrase)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (combinedBytes == null || combinedBytes.Length < 3)
|
||||
{
|
||||
return new byte[] { };
|
||||
}
|
||||
|
||||
if (combinedBytes[0] != 'G' || combinedBytes[1] != '1' || combinedBytes[2] != ':')
|
||||
{
|
||||
return new byte[] { };
|
||||
}
|
||||
|
||||
byte[] key = PassphraseToKey(passphrase);
|
||||
int bodyOffset = Encoding.ASCII.GetByteCount(GcmPrefix);
|
||||
byte[] iv = new byte[12];
|
||||
byte[] tag = new byte[16];
|
||||
byte[] cipherBytes = new byte[combinedBytes.Length - bodyOffset - iv.Length - tag.Length];
|
||||
|
||||
if (cipherBytes.Length < 0)
|
||||
{
|
||||
return new byte[] { };
|
||||
}
|
||||
|
||||
Buffer.BlockCopy(combinedBytes, bodyOffset, iv, 0, iv.Length);
|
||||
Buffer.BlockCopy(combinedBytes, bodyOffset + iv.Length, cipherBytes, 0, cipherBytes.Length);
|
||||
Buffer.BlockCopy(combinedBytes, bodyOffset + iv.Length + cipherBytes.Length, tag, 0, tag.Length);
|
||||
|
||||
byte[] plainBytes = new byte[cipherBytes.Length];
|
||||
using (AesGcm gcm = new AesGcm(key, 16))
|
||||
{
|
||||
gcm.Decrypt(iv, cipherBytes, tag, plainBytes);
|
||||
}
|
||||
|
||||
return plainBytes;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return new byte[] { };
|
||||
}
|
||||
}
|
||||
|
||||
public static (byte[] pubX25519, X25519PrivateKeyParameters privX25519, byte[] pubMlKem, MLKemPrivateKeyParameters privMlKem) InitHybridKEM()
|
||||
{
|
||||
var random = new SecureRandom();
|
||||
|
|
@ -188,14 +299,15 @@ public class Encryption
|
|||
|
||||
public static string PacketDecPass(string encResult, string key, string nonce)
|
||||
{
|
||||
return DecryptString(encResult, nonce + key);
|
||||
//tylko GCM - pakiety bez prefiksu "G1:" odrzucane
|
||||
return IsGcmPacket(encResult) ? DecryptStringGcm(encResult, nonce + key) : "";
|
||||
}
|
||||
|
||||
public static string GetRandomString(int length)
|
||||
{
|
||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz!@_";
|
||||
return new string(Enumerable.Repeat(chars, length)
|
||||
.Select(s => s[Random.Shared.Next(s.Length)]).ToArray());
|
||||
.Select(s => s[RandomNumberGenerator.GetInt32(s.Length)]).ToArray());
|
||||
}
|
||||
|
||||
public static string ComputeSHA3_512(string input)
|
||||
|
|
|
|||
|
|
@ -143,18 +143,26 @@ public class Fs
|
|||
}
|
||||
|
||||
var fileInfo = new FileInfo(path);
|
||||
if (fileInfo.Length < CACHE_ENTRY_SIZE)
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
//samonaprawa cache: wpis existCache może być nieaktualny
|
||||
existCache[path] = false;
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
byte[] content = await File.ReadAllBytesAsync(path);
|
||||
fileCache[path] = new FileData(content, false, fileInfo.Length);
|
||||
if (content.Length < CACHE_ENTRY_SIZE)
|
||||
{
|
||||
fileCache[path] = new FileData(content, false, content.Length);
|
||||
}
|
||||
existCache[path] = true;
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
byte[] bytes = await File.ReadAllBytesAsync(path);
|
||||
existCache[path] = true;
|
||||
return bytes;
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
existCache[path] = false;
|
||||
fileCache.TryRemove(path, out _);
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using LarpixServer.Account;
|
||||
|
|
@ -33,6 +34,12 @@ public class Program
|
|||
{
|
||||
if (entry.Value.resetAt <= nowSec) VoiceAuthLimits.TryRemove(entry.Key, out _);
|
||||
}
|
||||
//prune pozostałych rate limitów (okna 60s)
|
||||
PruneRateLimits(AuthRateLimits, nowSec);
|
||||
PruneRateLimits(NonceRateLimits, nowSec);
|
||||
PruneRateLimits(KdfSaltRateLimits, nowSec);
|
||||
PruneRateLimits(AccountInitRateLimits, nowSec);
|
||||
LarpixServer.Account.Requests.PruneActionRateLimits(nowSec);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -44,7 +51,7 @@ public class Program
|
|||
{
|
||||
serverOptions.AddServerHeader = false;
|
||||
serverOptions.ListenAnyIP(PORT);
|
||||
serverOptions.Limits.MaxRequestBodySize = null;
|
||||
serverOptions.Limits.MaxRequestBodySize = MAX_BODY_SIZE;
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
|
@ -98,6 +105,7 @@ public class Program
|
|||
context.Response.Headers["Access-Control-Allow-Origin"] = "*";
|
||||
context.Response.Headers["Access-Control-Allow-Headers"] = "*";
|
||||
context.Response.Headers["Access-Control-Allow-Methods"] = "*";
|
||||
|
||||
if (context.Request.Method.ToUpper() == "OPTIONS")
|
||||
{
|
||||
context.Response.StatusCode = 200;
|
||||
|
|
@ -252,9 +260,9 @@ public class Program
|
|||
case "/_larpix/group/attachment/get":
|
||||
await Account.Requests.GetGroupAttachment(context, next, query);
|
||||
return;
|
||||
case "/_larpix/federation":
|
||||
await Federation.Receiver.FederationRequest(context, next, query, reader);
|
||||
return;
|
||||
//case "/_larpix/federation": //federation wylaczona (brak uwierzytelnienia miedzyserwerowego)
|
||||
// await Federation.Receiver.FederationRequest(context, next, query, reader);
|
||||
// return;
|
||||
case "/_larpix/user/key/getpublic":
|
||||
await Account.Requests.GetUserPublicKey(context, next, query, reader);
|
||||
return;
|
||||
|
|
@ -264,6 +272,9 @@ public class Program
|
|||
case "/_larpix/group/storage/public/getentry":
|
||||
await Account.Requests.GetGroupPublicStorageEntry(context, next, query, reader);
|
||||
return;
|
||||
case "/_larpix/kdfsalt":
|
||||
await Account.Requests.GetKdfSalt(context, query);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -280,4 +291,12 @@ public class Program
|
|||
Console.WriteLine("Starting server at port: " + PORT);
|
||||
app.Run();
|
||||
}
|
||||
|
||||
private static void PruneRateLimits(ConcurrentDictionary<string, (int count, long resetAt)> store, long nowSec)
|
||||
{
|
||||
foreach (var entry in store)
|
||||
{
|
||||
if (entry.Value.resetAt <= nowSec) store.TryRemove(entry.Key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,19 @@ namespace LarpixServer.Room;
|
|||
|
||||
public class Groups
|
||||
{
|
||||
//groupId składa się z części ścieżek (id;domain) - blokuj traversal
|
||||
public static bool IsSafeGroupId(string groupId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(groupId) || groupId.Length > 128) return false;
|
||||
string[] parts = groupId.Split(';');
|
||||
foreach (string part in parts)
|
||||
{
|
||||
if (string.IsNullOrEmpty(part)) return false;
|
||||
if (part.Contains("..") || part.Contains('/') || part.Contains('\\')) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async Task UpdateUserGroup(string id, string groupId, string isRead = "false", string timestamp = "")
|
||||
{
|
||||
if (await Account.Utils.NameFromId(id) == "") //if user account just got deleted
|
||||
|
|
@ -53,10 +66,34 @@ public class Groups
|
|||
await Fs.WriteFile(groupPath, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(fileGroup, AppJsonSerializerContext.Default.Universal3String)));
|
||||
}
|
||||
|
||||
//Usuwa nieaktualny wpis grupy z listy użytkownika - wywoływane gdy serwer wykryje
|
||||
//brak permisji przy próbie dostępu (np. po kicku, gdy wpis został na liście).
|
||||
public static void RemoveStaleUserGroupEntry(string userId, string groupId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsSafeGroupId(groupId)) return;
|
||||
if (!groupId.Contains(';'))
|
||||
{
|
||||
groupId = $"{groupId};{Utils.Utils.DOMAIN}";
|
||||
}
|
||||
string entryPath = $"{Utils.Utils.ACCOUNTS_DATA_DIR}/{userId}/groups/{groupId}";
|
||||
if (Fs.Exists(entryPath))
|
||||
{
|
||||
Fs.DeleteFile(entryPath);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
//Globalny lock alokacji groupId - zapobiega duplikacji ID przy równoległych tworzeniach.
|
||||
private static readonly SemaphoreSlim GroupIdLock = new SemaphoreSlim(1, 1);
|
||||
|
||||
public static async Task<string> GroupCreate(string id, string name, string body, string historyVisible = "false")
|
||||
{
|
||||
SemaphoreSlim roomLock = Room.Requests.GetRoomLock(id);
|
||||
await roomLock.WaitAsync();
|
||||
await GroupIdLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
string groupsDir = $"{ROOMS_DIR}/groups";
|
||||
|
|
@ -114,7 +151,7 @@ public class Groups
|
|||
}
|
||||
finally
|
||||
{
|
||||
roomLock.Release();
|
||||
GroupIdLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -154,6 +191,7 @@ public class Groups
|
|||
|
||||
public static async Task<string> GroupSettingsUpdate(string userId, string groupId, string name, string historyVisible)
|
||||
{
|
||||
if (!IsSafeGroupId(groupId)) return "error:group.not.found";
|
||||
string groupPath = GetGroupPath(groupId);
|
||||
if (!Fs.Exists(groupPath)) return "error:group.not.found";
|
||||
|
||||
|
|
@ -219,6 +257,7 @@ public class Groups
|
|||
|
||||
public static async Task<string> GroupInfo(string groupId)
|
||||
{
|
||||
if (!IsSafeGroupId(groupId)) return "error:group.not.found";
|
||||
string groupPath = GetGroupPath(groupId);
|
||||
if (!Fs.Exists(groupPath))
|
||||
{
|
||||
|
|
@ -277,6 +316,7 @@ public class Groups
|
|||
|
||||
public static async Task<string> GroupInvite(string id, string groupId, string targetId, string keysPayload = "")
|
||||
{
|
||||
if (!IsSafeGroupId(groupId)) return "error:group.not.found";
|
||||
bool isLocal = Account.Utils.IsUserLocal(targetId, out string domain);
|
||||
string tId = Account.Utils.GetValidIdOrZero(targetId);
|
||||
|
||||
|
|
@ -312,6 +352,15 @@ public class Groups
|
|||
|
||||
if (keysDict == null) return "error:missing.keys";
|
||||
|
||||
//indeksy kluczy trafiają do ścieżek plików przy GroupJoin - tylko cyfry
|
||||
foreach (var key in keysDict.Keys)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || !System.Text.RegularExpressions.Regex.IsMatch(key, "^[0-9]{1,10}$"))
|
||||
{
|
||||
return "error:invalid.keys";
|
||||
}
|
||||
}
|
||||
|
||||
string lastKeyFile = $"{groupPath}/keys/last";
|
||||
if (Fs.Exists(lastKeyFile))
|
||||
{
|
||||
|
|
@ -379,6 +428,7 @@ public class Groups
|
|||
|
||||
public static async Task<string> GroupInviteRevoke(string id, string groupId, string targetId)
|
||||
{
|
||||
if (!IsSafeGroupId(groupId)) return "error:group.not.found";
|
||||
string tId = Account.Utils.GetValidIdOrZero(targetId);
|
||||
if (tId == "0") return "error:invalid.user";
|
||||
|
||||
|
|
@ -406,6 +456,7 @@ public class Groups
|
|||
|
||||
public static async Task<string> GroupInviteDecline(string id, string groupId, string inviterId)
|
||||
{
|
||||
if (!IsSafeGroupId(groupId)) return "error:group.not.found";
|
||||
string iId = Account.Utils.GetValidIdOrZero(inviterId);
|
||||
if (iId == "0") return "error:invalid.user";
|
||||
|
||||
|
|
@ -433,6 +484,7 @@ public class Groups
|
|||
|
||||
public static async Task<string> GroupJoin(string id, string groupId, string inviterId)
|
||||
{
|
||||
if (!IsSafeGroupId(groupId)) return "error:group.not.found";
|
||||
string iId = Account.Utils.GetValidIdOrZero(inviterId);
|
||||
if (iId == "0") return "error:invalid.user";
|
||||
|
||||
|
|
@ -491,10 +543,19 @@ public class Groups
|
|||
|
||||
if (keysDict != null)
|
||||
{
|
||||
//obrona w głębi: GroupInvite już waliduje, ale tu dane trafiają do ścieżek
|
||||
foreach (var key in keysDict.Keys)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || !System.Text.RegularExpressions.Regex.IsMatch(key, "^[0-9]{1,10}$"))
|
||||
{
|
||||
return "error:invalid.keys";
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var kvp in keysDict)
|
||||
{
|
||||
string keyIndex = kvp.Key;
|
||||
string keyData = kvp.Value;
|
||||
string keyData = kvp.Value ?? "";
|
||||
string newKeyDir = $"{groupPath}/keys/{keyIndex}";
|
||||
Fs.CreateDirectory(newKeyDir);
|
||||
await Fs.WriteFile($"{newKeyDir}/{id};{DOMAIN}", Encoding.UTF8.GetBytes(keyData));
|
||||
|
|
@ -555,6 +616,7 @@ public class Groups
|
|||
|
||||
public static async Task<string> GroupMembers(string id, string groupId)
|
||||
{
|
||||
if (!IsSafeGroupId(groupId)) return "error:group.not.found";
|
||||
string groupPath = GetGroupPath(groupId);
|
||||
if (!Fs.Exists(groupPath)) return "error:group.not.found";
|
||||
|
||||
|
|
@ -580,6 +642,16 @@ public class Groups
|
|||
return "error:invalid.payload";
|
||||
|
||||
string groupId = payload.groupId;
|
||||
if (!IsSafeGroupId(groupId)) return "error:group.not.found";
|
||||
//klucze rotacji trafiają do ścieżek - członkowie (nazwy plików) i wartości muszą być bezpieczne
|
||||
foreach (var kvp in payload.keys)
|
||||
{
|
||||
if (!Account.Utils.IsSafePathComponent(kvp.Key) || kvp.Value == null ||
|
||||
kvp.Value.Length > LarpixServer.Room.Requests.MAX_DM_KEY_SIZE)
|
||||
{
|
||||
return "error:invalid.payload";
|
||||
}
|
||||
}
|
||||
string groupPath = GetGroupPath(groupId);
|
||||
|
||||
if (!Fs.Exists(groupPath)) return "error:group.not.found";
|
||||
|
|
@ -648,6 +720,10 @@ public class Groups
|
|||
|
||||
public static async Task<string> GroupKeyGet(string id, string groupId, string keyIndex)
|
||||
{
|
||||
if (!IsSafeGroupId(groupId)) return "error:group.not.found";
|
||||
//keyIndex trafia do sciezki - tylko cyfry
|
||||
if (string.IsNullOrEmpty(keyIndex) || !System.Text.RegularExpressions.Regex.IsMatch(keyIndex, "^[0-9]{1,10}$")) return "";
|
||||
|
||||
string groupPath = GetGroupPath(groupId);
|
||||
if (!Fs.Exists(groupPath)) return "error:group.not.found";
|
||||
|
||||
|
|
@ -663,6 +739,7 @@ public class Groups
|
|||
|
||||
public static async Task<byte[]> GetGroupPublicStorageEntry(string groupId, string entry)
|
||||
{
|
||||
if (!IsSafeGroupId(groupId)) return new byte[] {};
|
||||
if (string.IsNullOrEmpty(entry) || entry.Contains("..") || entry.Contains("/") || entry.Contains("\\")) return new byte[] {};
|
||||
string[] parts = groupId.Split(';');
|
||||
string id = parts[0];
|
||||
|
|
@ -673,6 +750,7 @@ public class Groups
|
|||
}
|
||||
public static async Task<string> UpdateGroupPublicStorageEntry(string userId, string groupId, string entry, string contentBase64)
|
||||
{
|
||||
if (!IsSafeGroupId(groupId)) return "error:group.not.found";
|
||||
if (string.IsNullOrEmpty(entry) || entry.Contains("..") || entry.Contains("/") || entry.Contains("\\")) return "error:invalid.entry.name";
|
||||
string[] parts = groupId.Split(';');
|
||||
string id = parts[0];
|
||||
|
|
@ -707,6 +785,7 @@ public class Groups
|
|||
}
|
||||
public static async Task<string> GetGroupPrivateStorageEntry(string userId, string groupId, string entry)
|
||||
{
|
||||
if (!IsSafeGroupId(groupId)) return "";
|
||||
if (string.IsNullOrEmpty(entry) || entry.Contains("..") || entry.Contains("/") || entry.Contains("\\")) return "";
|
||||
string[] parts = groupId.Split(';');
|
||||
string id = parts[0];
|
||||
|
|
@ -719,6 +798,7 @@ public class Groups
|
|||
}
|
||||
public static async Task<string> UpdateGroupPrivateStorageEntry(string userId, string groupId, string entry, string contentBase64)
|
||||
{
|
||||
if (!IsSafeGroupId(groupId)) return "error:group.not.found";
|
||||
if (string.IsNullOrEmpty(entry) || entry.Contains("..") || entry.Contains("/") || entry.Contains("\\")) return "error:invalid.entry.name";
|
||||
string[] parts = groupId.Split(';');
|
||||
string id = parts[0];
|
||||
|
|
@ -749,6 +829,7 @@ public class Groups
|
|||
|
||||
public static async Task<string> GroupDelete(string groupId)
|
||||
{
|
||||
if (!IsSafeGroupId(groupId)) return "error:group.not.found";
|
||||
string[] parts = groupId.Split(';');
|
||||
string id = parts[0];
|
||||
string domain = parts.Length > 1 ? parts[1] : Utils.Utils.DOMAIN;
|
||||
|
|
@ -786,6 +867,7 @@ public class Groups
|
|||
|
||||
public static async Task<string> GroupLeave(string userId, string groupId)
|
||||
{
|
||||
if (!IsSafeGroupId(groupId)) return "error:group.not.found";
|
||||
string[] parts = groupId.Split(';');
|
||||
string id = parts[0];
|
||||
string domain = parts.Length > 1 ? parts[1] : Utils.Utils.DOMAIN;
|
||||
|
|
@ -892,6 +974,7 @@ public class Groups
|
|||
|
||||
public static async Task<string> GroupKickMember(string id, string groupId, string targetId)
|
||||
{
|
||||
if (!IsSafeGroupId(groupId)) return "error:group.not.found";
|
||||
string groupPath = GetGroupPath(groupId);
|
||||
if (!Fs.Exists(groupPath)) return "error:group.not.found";
|
||||
|
||||
|
|
@ -916,7 +999,10 @@ public class Groups
|
|||
|
||||
Fs.DeleteFile(memberPath);
|
||||
|
||||
string userGroupPath = $"{Utils.Utils.ACCOUNTS_DATA_DIR}/{tId}/groups/{groupId}";
|
||||
//UpdateUserGroup normalizuje wpis do "{id};{DOMAIN}" - tu musi być ta sama forma,
|
||||
//inaczej przy kicku zostanie osierocony wpis na liście grup użytkownika
|
||||
string normalizedGroupId = groupId.Contains(';') ? groupId : $"{groupId};{Utils.Utils.DOMAIN}";
|
||||
string userGroupPath = $"{Utils.Utils.ACCOUNTS_DATA_DIR}/{tId}/groups/{normalizedGroupId}";
|
||||
if (Fs.Exists(userGroupPath))
|
||||
{
|
||||
Fs.DeleteFile(userGroupPath);
|
||||
|
|
@ -978,6 +1064,7 @@ public class Groups
|
|||
|
||||
public static async Task<string> GroupTransferOwner(string id, string groupId, string targetId)
|
||||
{
|
||||
if (!IsSafeGroupId(groupId)) return "error:group.not.found";
|
||||
string groupPath = GetGroupPath(groupId);
|
||||
if (!Fs.Exists(groupPath)) return "error:group.not.found";
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,12 @@ public class Requests
|
|||
}
|
||||
public static async Task<string> GetDmMessages(string id, string dmId, string startOffset, string countStr)
|
||||
{
|
||||
if (!await IsMemberOfDm(id, dmId)) return "error:forbidden";
|
||||
if (!await IsMemberOfDm(id, dmId))
|
||||
{
|
||||
//brak permisji - usuń nieaktualny wpis DM z listy użytkownika (jeśli istnieje)
|
||||
Account.Utils.RemoveStaleUserDmEntry(id, dmId);
|
||||
return "error:forbidden";
|
||||
}
|
||||
|
||||
string msgDir = $"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/messages";
|
||||
if (!Fs.Exists(msgDir)) return "{}";
|
||||
|
|
@ -176,18 +181,17 @@ public class Requests
|
|||
bool isLocal = Account.Utils.IsUserLocal(targetId, out string domain);
|
||||
string tId = Account.Utils.GetValidIdOrZero(targetId);
|
||||
|
||||
if (!isLocal) //federation wylaczona (brak uwierzytelnienia miedzyserwerowego)
|
||||
{
|
||||
return "error:federation.disabled";
|
||||
}
|
||||
|
||||
string checkDmId = Account.Utils.GetDmId(id, DOMAIN, tId, domain);
|
||||
if (Fs.Exists($"{ROOMS_DIR}/dms/{DOMAIN}/{checkDmId}"))
|
||||
{
|
||||
return "error:dm.already.exists";
|
||||
}
|
||||
|
||||
if (!isLocal) //federation
|
||||
{
|
||||
await Fs.WriteFile(ACCOUNTS_DATA_DIR + $"/{id}/dminvites/sent/{tId};{domain}", Encoding.UTF8.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString()));
|
||||
return await Federation.Sender.DmInvite(id, domain, tId);
|
||||
}
|
||||
|
||||
string id2 = tId;
|
||||
if (id2 == "0" || string.IsNullOrEmpty(id2))
|
||||
{
|
||||
|
|
@ -229,10 +233,9 @@ public class Requests
|
|||
bool isLocal = Account.Utils.IsUserLocal(targetId, out string domain);
|
||||
string tId = Account.Utils.GetValidIdOrZero(targetId);
|
||||
|
||||
if (!isLocal) //federation
|
||||
if (!isLocal) //federation wylaczona
|
||||
{
|
||||
Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id}/dminvites/sent/{tId};{domain}");
|
||||
return await Federation.Sender.DmInviteRevoke(id, domain, tId);
|
||||
return "error:federation.disabled";
|
||||
}
|
||||
|
||||
string id2 = tId;
|
||||
|
|
@ -262,10 +265,9 @@ public class Requests
|
|||
bool isLocal = Account.Utils.IsUserLocal(targetId, out string domain);
|
||||
string tId = Account.Utils.GetValidIdOrZero(targetId);
|
||||
|
||||
if (!isLocal) //federation
|
||||
if (!isLocal) //federation wylaczona
|
||||
{
|
||||
Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id}/dminvites/recv/{tId};{domain}");
|
||||
return await Federation.Sender.DmInviteDecline(id, domain, tId);
|
||||
return "error:federation.disabled";
|
||||
}
|
||||
|
||||
string id2 = tId;
|
||||
|
|
@ -291,9 +293,6 @@ public class Requests
|
|||
}
|
||||
|
||||
public static async Task<string> DmCreate(string id, string body)
|
||||
{
|
||||
await createLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
Universal3String serializedBody = JsonSerializer.Deserialize( //1 is targetId, 2 is creators dm key, 3 is key for ID2
|
||||
body,
|
||||
|
|
@ -304,11 +303,23 @@ public class Requests
|
|||
bool isUserLocal = Account.Utils.IsUserLocal(targetId, out string domain);
|
||||
string id2 = Account.Utils.GetValidIdOrZero(targetId);
|
||||
|
||||
if (!isUserLocal) //federation wylaczona
|
||||
{
|
||||
return "error:federation.disabled";
|
||||
}
|
||||
|
||||
if (id2 == "0" || string.IsNullOrEmpty(id2))
|
||||
{
|
||||
return "error:user.not.found";
|
||||
}
|
||||
|
||||
//klucze DM trafiaja na dysk - twardy limit rozmiaru
|
||||
if (serializedBody.string2 == null || serializedBody.string2.Length > MAX_DM_KEY_SIZE ||
|
||||
serializedBody.string3 == null || serializedBody.string3.Length > MAX_DM_KEY_SIZE)
|
||||
{
|
||||
return "error:dm.key.too.large";
|
||||
}
|
||||
|
||||
string checkDmId = Account.Utils.GetDmId(id, DOMAIN, id2, domain);
|
||||
if (Fs.Exists($"{ROOMS_DIR}/dms/{DOMAIN}/{checkDmId}"))
|
||||
{
|
||||
|
|
@ -330,17 +341,28 @@ public class Requests
|
|||
}
|
||||
Fs.DeleteFile(inviteSentFile);
|
||||
}
|
||||
else //check sent on federated server
|
||||
{
|
||||
if (!await Federation.Sender.HasSentDmInvite(id2, domain, id))
|
||||
{
|
||||
return "error:cant.create.dm.without.invitation";
|
||||
}
|
||||
}
|
||||
//else //check sent on federated server (federation wylaczona)
|
||||
//{
|
||||
// if (!await Federation.Sender.HasSentDmInvite(id2, domain, id))
|
||||
// {
|
||||
// return "error:cant.create.dm.without.invitation";
|
||||
// }
|
||||
//}
|
||||
|
||||
string inviteFile = $"{ACCOUNTS_DATA_DIR}/{id}/dminvites/recv/{id2}" + (isUserLocal ? "" : $";{domain}");
|
||||
if (Fs.Exists(inviteFile))
|
||||
{
|
||||
//lock na docelowy dmId - serializuje tworzenie tego samego pokoju
|
||||
//bez globalnego blokowania wszystkich DM-ów
|
||||
SemaphoreSlim roomLock = GetRoomLock(checkDmId);
|
||||
await roomLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
//re-check pod lockiem - drugi równoległy DmCreate może już utworzyć pokój
|
||||
if (Fs.Exists($"{ROOMS_DIR}/dms/{DOMAIN}/{checkDmId}"))
|
||||
{
|
||||
return "error:dm.already.exists";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -400,10 +422,10 @@ public class Requests
|
|||
}
|
||||
});
|
||||
}
|
||||
else //federacja
|
||||
{
|
||||
await Federation.Sender.AddToDm(id, domain, id2);
|
||||
}
|
||||
//else //federacja (wylaczona)
|
||||
//{
|
||||
// await Federation.Sender.AddToDm(id, domain, id2);
|
||||
//}
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
@ -413,13 +435,11 @@ public class Requests
|
|||
}
|
||||
|
||||
return "success:dm.accepted";
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
createLock.Release();
|
||||
roomLock.Release();
|
||||
}
|
||||
}
|
||||
return "error:cant.create.dm.without.invitation";
|
||||
}
|
||||
|
|
@ -443,11 +463,19 @@ public class Requests
|
|||
return $"error:message.too.long:{sizeStr}";
|
||||
}
|
||||
|
||||
//pola załączników trafiają do ścieżek plików - odrzuć niebezpieczne wartości
|
||||
if (!string.IsNullOrEmpty(attachment) && !Shared.ValidateAttachmentsJson(attachment))
|
||||
{
|
||||
return "error:invalid.attachment";
|
||||
}
|
||||
|
||||
string msgDir = $"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/messages";
|
||||
|
||||
long nextId = 0;
|
||||
Message msg = new Message();
|
||||
await createLock.WaitAsync();
|
||||
//lock per pokój (spójnie z Shared.MessageSend) zamiast globalnego createLock
|
||||
SemaphoreSlim roomSendLock = GetRoomLock(dmId);
|
||||
await roomSendLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
long last = 0;
|
||||
|
|
@ -507,7 +535,7 @@ public class Requests
|
|||
}
|
||||
finally
|
||||
{
|
||||
createLock.Release();
|
||||
roomSendLock.Release();
|
||||
}
|
||||
|
||||
string[] members = Fs.ReadDirectory($"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/members");
|
||||
|
|
@ -632,7 +660,8 @@ public class Requests
|
|||
string reactDir = $"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/reactions/{msgId}";
|
||||
if (!Fs.Exists(reactDir)) Fs.CreateDirectory(reactDir);
|
||||
|
||||
await createLock.WaitAsync();
|
||||
SemaphoreSlim reactRoomLock = GetRoomLock(dmId);
|
||||
await reactRoomLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
long last = 0;
|
||||
|
|
@ -647,7 +676,7 @@ public class Requests
|
|||
}
|
||||
finally
|
||||
{
|
||||
createLock.Release();
|
||||
reactRoomLock.Release();
|
||||
}
|
||||
|
||||
string[] members = Fs.ReadDirectory($"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/members");
|
||||
|
|
@ -727,7 +756,8 @@ public class Requests
|
|||
string pinnedFilePath = $"{pinnedDir}/{msgId}";
|
||||
bool isNowPinned = false;
|
||||
|
||||
await createLock.WaitAsync();
|
||||
SemaphoreSlim pinRoomLock = GetRoomLock(dmId);
|
||||
await pinRoomLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (Fs.Exists(pinnedFilePath))
|
||||
|
|
@ -767,7 +797,7 @@ public class Requests
|
|||
}
|
||||
finally
|
||||
{
|
||||
createLock.Release();
|
||||
pinRoomLock.Release();
|
||||
}
|
||||
|
||||
string[] members = Fs.ReadDirectory($"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/members");
|
||||
|
|
|
|||
|
|
@ -32,6 +32,41 @@ public class Shared
|
|||
return true;
|
||||
}
|
||||
|
||||
//Pola JSON-a załącznika (staging_id / id) trafiają do ścieżek plików - muszą być bezpieczne.
|
||||
public static bool IsSafeAttachmentField(string value)
|
||||
{
|
||||
return Account.Utils.IsSafePathComponent(value, 128);
|
||||
}
|
||||
|
||||
//Waliduje tablicę załączników: true = OK, false = porzuć wiadomość.
|
||||
public static bool ValidateAttachmentsJson(string attachmentJson)
|
||||
{
|
||||
if (string.IsNullOrEmpty(attachmentJson) || attachmentJson == "[]") return true;
|
||||
try
|
||||
{
|
||||
var node = System.Text.Json.Nodes.JsonNode.Parse(attachmentJson);
|
||||
if (node is not System.Text.Json.Nodes.JsonArray arr) return true;
|
||||
|
||||
foreach (var item in arr)
|
||||
{
|
||||
if (item is not System.Text.Json.Nodes.JsonObject obj) continue;
|
||||
foreach (var fieldName in new[] { "staging_id", "id" })
|
||||
{
|
||||
if (obj.ContainsKey(fieldName) &&
|
||||
!IsSafeAttachmentField(obj[fieldName]?.ToString()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true; //parsowanie nie powiedzie się później i tak przy zapisie treści
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> IsMemberOfRoom(string id, string roomType, string roomId)
|
||||
{
|
||||
if (!IsSafeId(roomId)) return false;
|
||||
|
|
@ -83,7 +118,13 @@ public class Shared
|
|||
|
||||
public static async Task<string> GetMessages(string id, string roomType, string roomId, string startOffset, string countStr)
|
||||
{
|
||||
if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden";
|
||||
if (!await IsMemberOfRoom(id, roomType, roomId))
|
||||
{
|
||||
//brak permisji - jeśli użytkownik ma ten pokój na liście (stan po kicku/usunięciu),
|
||||
//usuń nieaktualny wpis, żeby lista sama się oczyściła
|
||||
if (roomType == "groups") Groups.RemoveStaleUserGroupEntry(id, roomId);
|
||||
return "error:forbidden";
|
||||
}
|
||||
|
||||
string msgDir = $"{GetRoomPath(roomType, roomId)}/messages";
|
||||
if (!Fs.Exists(msgDir)) return "{}";
|
||||
|
|
@ -204,6 +245,12 @@ public class Shared
|
|||
return $"error:message.too.long:{sizeStr}";
|
||||
}
|
||||
|
||||
//pola załączników trafiają do ścieżek plików - odrzuć niebezpieczne wartości
|
||||
if (!string.IsNullOrEmpty(attachment) && !ValidateAttachmentsJson(attachment))
|
||||
{
|
||||
return "error:invalid.attachment";
|
||||
}
|
||||
|
||||
long last = 0;
|
||||
if (Fs.Exists($"{msgDir}/last"))
|
||||
{
|
||||
|
|
@ -601,6 +648,13 @@ public class Shared
|
|||
return "room";
|
||||
}
|
||||
|
||||
//Współdzielony klient HTTP do odpytywania serwerów głosowych o load
|
||||
//(new HttpClient() per żądanie wyczerpałoby gniazda)
|
||||
private static readonly System.Net.Http.HttpClient VoiceLoadHttp = new System.Net.Http.HttpClient()
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(2)
|
||||
};
|
||||
|
||||
public static async Task<string> VoiceJoin(string id, string body)
|
||||
{
|
||||
Universal2String serializedBody = JsonSerializer.Deserialize(
|
||||
|
|
@ -631,10 +685,8 @@ public class Shared
|
|||
{
|
||||
try
|
||||
{
|
||||
using var client = new System.Net.Http.HttpClient();
|
||||
client.Timeout = TimeSpan.FromSeconds(2);
|
||||
string url = ip.StartsWith("http") ? ip : $"http://{ip}";
|
||||
string loadStr = await client.GetStringAsync($"{url}/load");
|
||||
string loadStr = await VoiceLoadHttp.GetStringAsync($"{url}/load");
|
||||
if (long.TryParse(loadStr, out long load))
|
||||
{
|
||||
if (load < bestLoad)
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ public class KeyExchangePayload
|
|||
public string pubX25519 { get; set; }
|
||||
public string pubMlKem { get; set; }
|
||||
public string idKey { get; set; }
|
||||
public string kdfSalt { get; set; }
|
||||
}
|
||||
|
||||
public class KeyExchangePayloadClient
|
||||
|
|
|
|||
|
|
@ -30,6 +30,28 @@ public class Utils
|
|||
// Fixed 60s window: (request count, window reset unix time).
|
||||
public static ConcurrentDictionary<string, (int count, long resetAt)> VoiceAuthLimits = new();
|
||||
|
||||
// Rate limity per-minute (okno 60s). Wszystkie konfigurowalne przez .env.
|
||||
public static ConcurrentDictionary<string, (int count, long resetAt)> AuthRateLimits = new();
|
||||
public static ConcurrentDictionary<string, (int count, long resetAt)> NonceRateLimits = new();
|
||||
public static ConcurrentDictionary<string, (int count, long resetAt)> KdfSaltRateLimits = new();
|
||||
public static ConcurrentDictionary<string, (int count, long resetAt)> AccountInitRateLimits = new();
|
||||
|
||||
public static int AUTH_RATE_LIMIT = 10; // próby logowania /auth + /ws na konto+IP
|
||||
public static int NONCE_RATE_LIMIT = 240; // pobrania nonce per konto
|
||||
public static int KDFSALT_RATE_LIMIT = 60; // pobrania soli per IP
|
||||
public static int ACCOUNT_INIT_RATE_LIMIT = 10; // inicjalizacje rejestracji (KEM keygen) per IP
|
||||
|
||||
// Zwraca true jeśli przekroczono limit. Okno stałe 60s.
|
||||
public static bool RateLimited(ConcurrentDictionary<string, (int count, long resetAt)> store, string key, int limit)
|
||||
{
|
||||
if (limit <= 0) return false; // <=0 wyłącza limit
|
||||
long nowSec = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var window = store.AddOrUpdate(key,
|
||||
(1, nowSec + 60),
|
||||
(_, old) => old.resetAt <= nowSec ? (1, nowSec + 60) : (old.count + 1, old.resetAt));
|
||||
return window.count > limit;
|
||||
}
|
||||
|
||||
public static string DATA_DIR = "./data";
|
||||
public static string ROOMS_DIR = "./data/rooms";
|
||||
public static string ACCOUNTS_DIR = "./data/accounts";
|
||||
|
|
@ -126,14 +148,28 @@ public class Utils
|
|||
public static async Task WsHandler(HttpContext context, System.Net.WebSockets.WebSocket webSocket, IQueryCollection query)
|
||||
{
|
||||
var buffer = new byte[8192];
|
||||
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
|
||||
//odbierz PIERWSZĄ pełną ramkę (moze byc pofragmentowana)
|
||||
int totalRead = 0;
|
||||
System.Net.WebSockets.WebSocketReceiveResult result;
|
||||
while (true)
|
||||
{
|
||||
if (totalRead >= buffer.Length)
|
||||
{
|
||||
await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.MessageTooBig, "Auth too large", CancellationToken.None);
|
||||
return;
|
||||
}
|
||||
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer, totalRead, buffer.Length - totalRead), CancellationToken.None);
|
||||
totalRead += result.Count;
|
||||
if (result.MessageType == System.Net.WebSockets.WebSocketMessageType.Close)
|
||||
{
|
||||
await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
|
||||
return;
|
||||
}
|
||||
if (result.EndOfMessage) break;
|
||||
}
|
||||
|
||||
string authMessage = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||
string authMessage = Encoding.UTF8.GetString(buffer, 0, totalRead);
|
||||
Universal2String authParams;
|
||||
try {
|
||||
authParams = JsonSerializer.Deserialize<Universal2String>(authMessage, AppJsonSerializerContext.Default.Universal2String);
|
||||
|
|
@ -143,6 +179,15 @@ public class Utils
|
|||
}
|
||||
|
||||
string id = Account.Utils.GetValidIdOrZero(authParams.string1);
|
||||
|
||||
//rate limit prób autoryzacji WS (konto+IP) - wspólny zasobnik z /auth
|
||||
string clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "";
|
||||
if (RateLimited(AuthRateLimits, $"{id}:{clientIp}", AUTH_RATE_LIMIT))
|
||||
{
|
||||
await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.PolicyViolation, "Rate limited", CancellationToken.None);
|
||||
return;
|
||||
}
|
||||
|
||||
string password = await Account.Utils.GetPassword(id);
|
||||
string secret = await Account.Utils.NonceDecryptBody(id, password, authParams.string2);
|
||||
|
||||
|
|
@ -181,6 +226,7 @@ public class Utils
|
|||
list.Remove(webSocket);
|
||||
}
|
||||
}
|
||||
WsSendLocks.TryRemove(webSocket, out _);
|
||||
if (webSocket.State == System.Net.WebSockets.WebSocketState.Open)
|
||||
{
|
||||
await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
|
||||
|
|
@ -221,29 +267,66 @@ public class Utils
|
|||
}
|
||||
}
|
||||
|
||||
//Jeden outstanding SendAsync na WebSocket - semafory per-socket serializują wysyłkę
|
||||
//(równoległe SendAsync na tym samym sockecie rzucają InvalidOperationException).
|
||||
public static readonly ConcurrentDictionary<System.Net.WebSockets.WebSocket, SemaphoreSlim> WsSendLocks = new();
|
||||
|
||||
private static SemaphoreSlim GetWsSendLock(System.Net.WebSockets.WebSocket ws)
|
||||
{
|
||||
return WsSendLocks.GetOrAdd(ws, _ => new SemaphoreSlim(1, 1));
|
||||
}
|
||||
|
||||
public static async Task SendWsMessage(string id, string message)
|
||||
{
|
||||
if (ActiveWebSockets.TryGetValue(id, out var list))
|
||||
if (!ActiveWebSockets.TryGetValue(id, out var list))
|
||||
{
|
||||
List<System.Net.WebSockets.WebSocket> toRemove = null;
|
||||
var bytes = Encoding.UTF8.GetBytes(message);
|
||||
lock(list) {
|
||||
foreach(var ws in list)
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] bytes;
|
||||
System.Net.WebSockets.WebSocket[] snapshot;
|
||||
lock (list)
|
||||
{
|
||||
if (list.Count == 0) return;
|
||||
bytes = Encoding.UTF8.GetBytes(message);
|
||||
snapshot = list.ToArray();
|
||||
}
|
||||
|
||||
foreach (var ws in snapshot)
|
||||
{
|
||||
bool remove = false;
|
||||
var sendLock = GetWsSendLock(ws);
|
||||
await sendLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (ws.State == System.Net.WebSockets.WebSocketState.Open)
|
||||
{
|
||||
_ = ws.SendAsync(new ArraySegment<byte>(bytes), System.Net.WebSockets.WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
try
|
||||
{
|
||||
await ws.SendAsync(new ArraySegment<byte>(bytes), System.Net.WebSockets.WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
}
|
||||
catch
|
||||
{
|
||||
remove = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (toRemove == null) toRemove = new();
|
||||
toRemove.Add(ws);
|
||||
remove = true;
|
||||
}
|
||||
}
|
||||
if (toRemove != null)
|
||||
finally
|
||||
{
|
||||
foreach(var ws in toRemove) list.Remove(ws);
|
||||
sendLock.Release();
|
||||
}
|
||||
|
||||
if (remove)
|
||||
{
|
||||
lock (list)
|
||||
{
|
||||
list.Remove(ws);
|
||||
}
|
||||
WsSendLocks.TryRemove(ws, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -346,6 +429,11 @@ public class Utils
|
|||
VOICE_AUTH_RATE_LIMIT = parsedVoiceAuthLimit;
|
||||
}
|
||||
|
||||
AUTH_RATE_LIMIT = int.TryParse(Environment.GetEnvironmentVariable("AUTH_RATE_LIMIT"), out int parsedAuthLimit) ? parsedAuthLimit : AUTH_RATE_LIMIT;
|
||||
NONCE_RATE_LIMIT = int.TryParse(Environment.GetEnvironmentVariable("NONCE_RATE_LIMIT"), out int parsedNonceLimit) ? parsedNonceLimit : NONCE_RATE_LIMIT;
|
||||
KDFSALT_RATE_LIMIT = int.TryParse(Environment.GetEnvironmentVariable("KDFSALT_RATE_LIMIT"), out int parsedKdfSaltLimit) ? parsedKdfSaltLimit : KDFSALT_RATE_LIMIT;
|
||||
ACCOUNT_INIT_RATE_LIMIT = int.TryParse(Environment.GetEnvironmentVariable("ACCOUNT_INIT_RATE_LIMIT"), out int parsedAccountInitLimit) ? parsedAccountInitLimit : ACCOUNT_INIT_RATE_LIMIT;
|
||||
|
||||
VOICE_IPS.Clear();
|
||||
|
||||
port = Environment.GetEnvironmentVariable("VOICE_IPS");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue