Compare commits

...

13 Commits

Author SHA1 Message Date
Pieter-Jan Briers
de8c2c14bb Replace Lidgren encryption stuff with Libsodium (#2646) 2022-04-02 16:24:04 +02:00
mirrorcult
baebfff321 Merge pull request #2673 from ElectroJr/fix-description 2022-04-01 17:02:43 -07:00
Paul
24191404aa version 0.8.86 2022-04-02 01:28:12 +02:00
Paul Ritter
990842f5c2 fixes pvs dirty crash (#2674) 2022-04-02 06:09:14 +11:00
ElectroJr
2c2bd3f330 Version: 0.8.85 2022-04-02 00:38:07 +13:00
Leon Friedrich
1ae14c4bfa Add error tolerance to physics map (#2672) 2022-04-01 22:07:27 +11:00
metalgearsloth
61a1701dc9 Fix PVS take 3 (#2671) 2022-04-01 22:04:30 +11:00
ElectroJr
c6ea11346e fix entity description proxy method. 2022-04-02 00:02:58 +13:00
mirrorcult
ad7c871e28 Make effects properly clean up after itself (#2670) 2022-04-01 13:00:54 +11:00
metalgearsloth
c77b3f8022 Version: 0.8.84 2022-04-01 10:10:20 +11:00
metalgearsloth
b263f4a1df Fix PVS crash 2 (#2668)
Co-authored-by: Paul <ritter.paul1@googlemail.com>
2022-04-01 10:09:31 +11:00
Paul
1a11d41bba disable benchmarks workflow 2022-03-31 17:06:48 +02:00
Leon Friedrich
14d0a77644 Make directional sprite matrices static (#2661) 2022-04-01 00:11:17 +11:00
23 changed files with 304 additions and 115 deletions

View File

@@ -1,6 +1,6 @@
name: Benchmarks
on:
push
#on:
# push
#schedule:
# - cron: '0 5 * * *'
#push:

View File

@@ -1,4 +1,4 @@
<Project>
<!-- This file automatically reset by Tools/version.py -->
<PropertyGroup><Version>0.8.83</Version></PropertyGroup>
<PropertyGroup><Version>0.8.86</Version></PropertyGroup>
</Project>

View File

@@ -2023,15 +2023,27 @@ namespace Robust.Client.GameObjects
/// </summary>
public void GetLayerDrawMatrix(RSIDirection dir, out Matrix3 layerDrawMatrix)
{
if (_parent.NoRotation)
if (_parent.NoRotation || dir == RSIDirection.South)
layerDrawMatrix = LocalMatrix;
else
{
var rsiDirectionMatrix = Matrix3.CreateTransform(Vector2.Zero, -dir.Convert().ToAngle());
Matrix3.Multiply(ref rsiDirectionMatrix, ref LocalMatrix, out layerDrawMatrix);
Matrix3.Multiply(ref _rsiDirectionMatrices[(int)dir], ref LocalMatrix, out layerDrawMatrix);
}
}
private static Matrix3[] _rsiDirectionMatrices = new Matrix3[]
{
// array order chosen such that this array can be indexed by casing an RSI direction to an int
Matrix3.Identity, // should probably just avoid matrix multiplication altogether if the direction is south.
Matrix3.CreateRotation(-Direction.North.ToAngle()),
Matrix3.CreateRotation(-Direction.East.ToAngle()),
Matrix3.CreateRotation(-Direction.West.ToAngle()),
Matrix3.CreateRotation(-Direction.SouthEast.ToAngle()),
Matrix3.CreateRotation(-Direction.SouthWest.ToAngle()),
Matrix3.CreateRotation(-Direction.NorthEast.ToAngle()),
Matrix3.CreateRotation(-Direction.NorthWest.ToAngle())
};
internal void Render(DrawingHandleWorld drawingHandle, ref Matrix3 spriteMatrix, Angle angle, Direction? overrideDirection)
{
if (!Visible)

View File

@@ -342,7 +342,6 @@ namespace Robust.Client.GameObjects
var map = _owner.eyeManager.CurrentMap;
var worldHandle = args.WorldHandle;
ShaderInstance? currentShader = null;
if (_playerManager.LocalPlayer?.ControlledEntity is not {} playerEnt)
return;
@@ -362,13 +361,8 @@ namespace Robust.Client.GameObjects
continue;
}
var newShader = effect.Shaded ? null : _unshadedShader;
if (newShader != currentShader)
{
worldHandle.UseShader(newShader);
currentShader = newShader;
}
if (!effect.Shaded)
worldHandle.UseShader(_unshadedShader);
// TODO: Should be doing matrix transformations
var effectSprite = effect.EffectSprite;
@@ -389,6 +383,9 @@ namespace Robust.Client.GameObjects
var rotatedBox = new Box2Rotated(effectArea, effect.Rotation + rotation, effectOrigin);
worldHandle.DrawTextureRect(effectSprite, rotatedBox, ToColor(effect.Color));
if (!effect.Shaded)
worldHandle.UseShader(null);
}
}
}

View File

@@ -152,9 +152,12 @@ namespace Robust.Client.Graphics
/// <summary>
/// Specifies a direction in an RSI state.
/// </summary>
/// <remarks>
/// Value of the enum here matches the index used to store it in the icons array. If this ever changes, then
/// <see cref="GameObjects.SpriteComponent.Layer._rsiDirectionMatrices"/> also needs to be updated.
/// </remarks>
public enum Direction : byte
{
// Value of the enum here matches the index used to store it in the icons array.
South = 0,
North = 1,
East = 2,

View File

@@ -8,6 +8,7 @@ using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
@@ -32,6 +33,13 @@ public interface IPVSCollection
/// </summary>
/// <param name="tick">The <see cref="GameTick"/> before which all deletions should be removed.</param>
public void CullDeletionHistoryUntil(GameTick tick);
public bool IsDirty(IChunkIndexLocation location);
public bool MarkDirty(IChunkIndexLocation location);
public void ClearDirty();
}
public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : IComparable<TIndex>, IEquatable<TIndex>
@@ -110,7 +118,6 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
public void Process()
{
_dirtyChunks.Clear();
_changedIndices.EnsureCapacity(_locationChangeBuffer.Count);
foreach (var (key, loc) in _locationChangeBuffer)
@@ -139,12 +146,16 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
switch (chunkLocation)
{
case GridChunkLocation gridChunkLocation:
if (_gridChunkContents[gridChunkLocation.GridId][gridChunkLocation.ChunkIndices].Count == 0)
_gridChunkContents[gridChunkLocation.GridId].Remove(gridChunkLocation.ChunkIndices);
if(!_gridChunkContents.TryGetValue(gridChunkLocation.GridId, out var gridChunks)) continue;
if(!gridChunks.TryGetValue(gridChunkLocation.ChunkIndices, out var chunk)) continue;
if(chunk.Count == 0)
gridChunks.Remove(gridChunkLocation.ChunkIndices);
break;
case MapChunkLocation mapChunkLocation:
if (_mapChunkContents[mapChunkLocation.MapId][mapChunkLocation.ChunkIndices].Count == 0)
_mapChunkContents[mapChunkLocation.MapId].Remove(mapChunkLocation.ChunkIndices);
if(!_mapChunkContents.TryGetValue(mapChunkLocation.MapId, out var mapChunks)) continue;
if(!mapChunks.TryGetValue(mapChunkLocation.ChunkIndices, out chunk)) continue;
if(chunk.Count == 0)
mapChunks.Remove(mapChunkLocation.ChunkIndices);
break;
}
}
@@ -165,6 +176,10 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
public bool IsDirty(IChunkIndexLocation location) => _dirtyChunks.Contains(location);
public bool MarkDirty(IChunkIndexLocation location) => _dirtyChunks.Add(location);
public void ClearDirty() => _dirtyChunks.Clear();
public bool TryGetChunk(MapId mapId, Vector2i chunkIndices, [NotNullWhen(true)] out HashSet<TIndex>? indices) =>
_mapChunkContents[mapId].TryGetValue(chunkIndices, out indices);
@@ -182,10 +197,9 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
break;
case GridChunkLocation gridChunkLocation:
// might be gone due to grid-deletions
if(!_gridChunkContents.ContainsKey(gridChunkLocation.GridId)) return;
if(!_gridChunkContents[gridChunkLocation.GridId].ContainsKey(gridChunkLocation.ChunkIndices))
_gridChunkContents[gridChunkLocation.GridId][gridChunkLocation.ChunkIndices] = new();
_gridChunkContents[gridChunkLocation.GridId][gridChunkLocation.ChunkIndices].Add(index);
if(!_gridChunkContents.TryGetValue(gridChunkLocation.GridId, out var gridChunk)) return;
var gridLoc = gridChunk.GetOrNew(gridChunkLocation.ChunkIndices);
gridLoc.Add(index);
dirtyChunks.Add(gridChunkLocation);
break;
case LocalOverride localOverride:
@@ -195,10 +209,9 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
break;
case MapChunkLocation mapChunkLocation:
// might be gone due to map-deletions
if(!_mapChunkContents.ContainsKey(mapChunkLocation.MapId)) return;
if(!_mapChunkContents[mapChunkLocation.MapId].ContainsKey(mapChunkLocation.ChunkIndices))
_mapChunkContents[mapChunkLocation.MapId][mapChunkLocation.ChunkIndices] = new();
_mapChunkContents[mapChunkLocation.MapId][mapChunkLocation.ChunkIndices].Add(index);
if(!_mapChunkContents.TryGetValue(mapChunkLocation.MapId, out var mapChunk)) return;
var mapLoc = mapChunk.GetOrNew(mapChunkLocation.ChunkIndices);
mapLoc.Add(index);
dirtyChunks.Add(mapChunkLocation);
break;
}
@@ -390,6 +403,20 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
UpdateIndex(index, mapCoordinates.MapId, mapIndices, true); //skip overridecheck bc we already did it (saves some dict lookups)
}
public IChunkIndexLocation GetChunkIndex(EntityCoordinates coordinates)
{
var gridId = coordinates.GetGridId(_entityManager);
if (gridId != GridId.Invalid)
{
var gridIndices = GetChunkIndices(coordinates.Position);
return new GridChunkLocation(gridId, gridIndices);
}
var mapCoordinates = coordinates.ToMap(_entityManager);
var mapIndices = GetChunkIndices(coordinates.Position);
return new MapChunkLocation(mapCoordinates.MapId, mapIndices);
}
/// <summary>
/// Updates an <see cref="TIndex"/> using the provided <see cref="gridId"/> and <see cref="chunkIndices"/>.
/// </summary>

View File

@@ -1,5 +1,4 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
@@ -114,6 +113,7 @@ internal sealed partial class PVSSystem : EntitySystem
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
SubscribeLocalEvent<MoveEvent>(OnEntityMove);
SubscribeLocalEvent<EntParentChangedMessage>(OnParentChange);
SubscribeLocalEvent<TransformComponent, ComponentStartup>(OnTransformStartup);
EntityManager.EntityDeleted += OnEntityDeleted;
@@ -123,6 +123,17 @@ internal sealed partial class PVSSystem : EntitySystem
InitializeDirty();
}
private void OnParentChange(ref EntParentChangedMessage ev)
{
if (_mapManager.IsGrid(ev.Entity) || _mapManager.IsMap(ev.Entity)) return;
// If parent changes then the RobustTree for that chunk will no longer be valid and we need to force it as dirty.
// Should still be at its old location as moveevent is called after.
var xform = Transform(ev.Entity);
var coordinates = _transform.GetMoverCoordinates(xform);
var index = _entityPvsCollection.GetChunkIndex(coordinates);
_entityPvsCollection.MarkDirty(index);
}
public override void Shutdown()
{
@@ -174,6 +185,11 @@ internal sealed partial class PVSSystem : EntitySystem
}
CleanupDirty(playerSessions);
foreach (var collection in _pvsCollections)
{
collection.ClearDirty();
}
}
public void CullDeletionHistory(GameTick oldestAck)

View File

@@ -87,8 +87,8 @@ namespace Robust.Server.ServerStatus
var authInfo = new JsonObject
{
["mode"] = _netManager.Auth.ToString(),
["public_key"] = _netManager.RsaPublicKey != null
? Convert.ToBase64String(_netManager.RsaPublicKey)
["public_key"] = _netManager.CryptoPublicKey != null
? Convert.ToBase64String(_netManager.CryptoPublicKey)
: null
};

View File

@@ -355,6 +355,7 @@ namespace Robust.Shared.GameObjects
// offset position from world to parent
_parent = value.EntityId;
var oldMapId = MapID;
ChangeMapId(newParent.MapID, xformQuery);
// preserve world rotation
@@ -364,7 +365,7 @@ namespace Robust.Shared.GameObjects
// Cache new GridID before raising the event.
GridID = GetGridIndex(xformQuery);
var entParentChangedMessage = new EntParentChangedMessage(Owner, oldParent?.Owner);
var entParentChangedMessage = new EntParentChangedMessage(Owner, oldParent?.Owner, oldMapId);
_entMan.EventBus.RaiseLocalEvent(Owner, ref entParentChangedMessage);
}
@@ -381,7 +382,7 @@ namespace Robust.Shared.GameObjects
//TODO: This is a hack, look into WHY we can't call GridPosition before the comp is Running
if (Running)
{
if(!oldPosition.Position.Equals(Coordinates.Position))
if (!oldPosition.Position.Equals(Coordinates.Position))
{
var moveEvent = new MoveEvent(Owner, oldPosition, Coordinates, this);
_entMan.EventBus.RaiseLocalEvent(Owner, ref moveEvent);
@@ -733,8 +734,8 @@ namespace Robust.Shared.GameObjects
oldConcrete._children.Remove(uid);
_parent = EntityUid.Invalid;
var entParentChangedMessage = new EntParentChangedMessage(Owner, oldParent, MapID);
MapID = MapId.Nullspace;
var entParentChangedMessage = new EntParentChangedMessage(Owner, oldParent);
_entMan.EventBus.RaiseLocalEvent(Owner, ref entParentChangedMessage);
// Does it even make sense to call these since this is called purely from OnRemove right now?

View File

@@ -198,7 +198,7 @@ public partial class EntitySystem
if(!Resolve(uid, ref metaData, false))
throw CompNotFound<MetaDataComponent>(uid);
return metaData.EntityName;
return metaData.EntityDescription;
}
/// <summary>

View File

@@ -1,4 +1,6 @@
namespace Robust.Shared.GameObjects
using Robust.Shared.Map;
namespace Robust.Shared.GameObjects
{
/// <summary>
/// Raised when an entity parent is changed.
@@ -16,15 +18,26 @@
/// </summary>
public EntityUid? OldParent { get; }
/// <summary>
/// The map Id that the entity was on before its parent changed.
/// </summary>
/// <remarks>
/// If the old parent was detached to null without manually updating the map ID of its children, then this
/// is required as we cannot simply use the old parent's map ID. Also avoids having to fetch the old
/// parent's transform component.
/// </remarks>
public MapId OldMapId { get; }
/// <summary>
/// Creates a new instance of <see cref="EntParentChangedMessage"/>.
/// </summary>
/// <param name="entity"></param>
/// <param name="oldParent"></param>
public EntParentChangedMessage(EntityUid entity, EntityUid? oldParent)
public EntParentChangedMessage(EntityUid entity, EntityUid? oldParent, MapId oldMapId)
{
Entity = entity;
OldParent = oldParent;
OldMapId = oldMapId;
}
}
}

View File

@@ -125,15 +125,12 @@ namespace Robust.Shared.GameObjects
_broadphase.UpdateBroadphase(body, xform: xform);
// Handle map change
var oldMapId = _transform.GetMapId(args.OldParent);
var mapId = _transform.GetMapId(args.Entity);
if (oldMapId != mapId)
{
HandleMapChange(body, xform, oldMapId, mapId);
}
if (args.OldMapId != mapId)
HandleMapChange(body, xform, args.OldMapId, mapId);
if (!_container.IsEntityInContainer(uid, meta))
if (mapId != MapId.Nullspace && !_container.IsEntityInContainer(uid, meta))
HandleParentChangeVelocity(uid, body, ref args, xform);
}
@@ -162,10 +159,10 @@ namespace Robust.Shared.GameObjects
map.AddBody(body);
}
if (_mapManager.IsGrid(body.Owner) ||
_mapManager.IsMap(body.Owner) ||
xform.ChildCount == 0 ||
(oldMap == null && map == null)) return;
if (xform.ChildCount == 0 ||
(oldMap == null && map == null) ||
_mapManager.IsGrid(body.Owner) ||
_mapManager.IsMap(body.Owner)) return;
var xformQuery = GetEntityQuery<TransformComponent>();
var bodyQuery = GetEntityQuery<PhysicsComponent>();

View File

@@ -10,7 +10,7 @@ namespace Robust.Shared.Network
{
public delegate Task<NetApproval> NetApprovalDelegate(NetApprovalEventArgs eventArgs);
byte[]? RsaPublicKey { get; }
byte[]? CryptoPublicKey { get; }
AuthMode Auth { get; }
Func<string, Task<NetUserId?>>? AssignUserIdCallback { get; set; }
NetApprovalDelegate? HandleApprovalCallback { get; set; }

View File

@@ -12,25 +12,20 @@ namespace Robust.Shared.Network.Messages.Handshake
public override MsgGroups MsgGroup => MsgGroups.Core;
public Guid UserId;
public byte[] SharedSecret;
public byte[] VerifyToken;
public byte[] SealedData;
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
UserId = buffer.ReadGuid();
var keyLength = buffer.ReadVariableInt32();
SharedSecret = buffer.ReadBytes(keyLength);
var tokenLength = buffer.ReadVariableInt32();
VerifyToken = buffer.ReadBytes(tokenLength);
SealedData = buffer.ReadBytes(keyLength);
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.Write(UserId);
buffer.WriteVariableInt32(SharedSecret.Length);
buffer.Write(SharedSecret);
buffer.WriteVariableInt32(VerifyToken.Length);
buffer.Write(VerifyToken);
buffer.WriteVariableInt32(SealedData.Length);
buffer.Write(SealedData);
}
}
}

View File

@@ -0,0 +1,122 @@
using System;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Threading;
using Lidgren.Network;
using SpaceWizards.Sodium;
namespace Robust.Shared.Network;
internal sealed class NetEncryption
{
// Use a counter for nonces. The counter is 64-bit, I will be impressed if you ever manage to run it out.
// 64-bit counter (incl over the wire) is fine, don't need the whole 192-bit.
// Server starts at 0, client starts at 1, increment by two.
// This means server and client never use eachother's nonces (one side odd, one side even).
// Keep in mind, our keys are only valid for one session.
private ulong _nonce;
private readonly byte[] _key;
public NetEncryption(byte[] key, bool isServer)
{
if (key.Length != CryptoAeadXChaCha20Poly1305Ietf.KeyBytes)
throw new ArgumentException("Key is of wrong size!");
_nonce = isServer ? 0ul : 1ul;
_key = key;
}
public unsafe void Encrypt(NetOutgoingMessage message)
{
var nonce = Interlocked.Add(ref _nonce, 2);
var lengthBytes = message.LengthBytes;
var encryptedSize = CryptoAeadXChaCha20Poly1305Ietf.AddBytes + lengthBytes + sizeof(ulong);
var data = message.Data.AsSpan(0, lengthBytes);
Span<byte> plaintext;
Span<byte> ciphertext;
byte[]? returnPool = null;
if (message.Data.Length >= encryptedSize)
{
// Since we have enough space in the existing message data,
// we copy plaintext to an ArrayPool buffer and write ciphertext into existing message.
// This avoids an allocation at the cost of an extra copy operation.
returnPool = ArrayPool<byte>.Shared.Rent(lengthBytes);
plaintext = returnPool.AsSpan(0, lengthBytes);
data.CopyTo(plaintext);
ciphertext = message.Data.AsSpan(0, encryptedSize);
}
else
{
// Otherwise, an allocation is unavoidable,
// so we swap the data buffer in the message with a fresh allocation and don't do an extra copy of the data.
plaintext = data;
ciphertext = message.Data = new byte[encryptedSize];
}
// TODO: this is probably broken for big-endian machines.
Span<byte> nonceData = stackalloc byte[CryptoAeadXChaCha20Poly1305Ietf.NoncePublicBytes];
nonceData.Fill(0);
MemoryMarshal.Write(nonceData, ref nonce);
MemoryMarshal.Write(ciphertext, ref nonce);
CryptoAeadXChaCha20Poly1305Ietf.Encrypt(
// ciphertext
ciphertext[sizeof(ulong)..],
out _,
// plaintext
plaintext,
// additional data (unused)
ReadOnlySpan<byte>.Empty,
// nonce
nonceData,
// key
_key);
message.LengthBytes = encryptedSize;
if (returnPool != null)
ArrayPool<byte>.Shared.Return(returnPool);
}
public unsafe void Decrypt(NetIncomingMessage message)
{
var nonce = message.ReadUInt64();
var cipherText = message.Data.AsSpan(sizeof(ulong), message.LengthBytes - sizeof(ulong));
var buffer = ArrayPool<byte>.Shared.Rent(cipherText.Length);
cipherText.CopyTo(buffer);
// TODO: this is probably broken for big-endian machines.
Span<byte> nonceData = stackalloc byte[CryptoAeadXChaCha20Poly1305Ietf.NoncePublicBytes];
nonceData.Fill(0);
MemoryMarshal.Write(nonceData, ref nonce);
var result = CryptoAeadXChaCha20Poly1305Ietf.Decrypt(
// plaintext
message.Data,
out var messageLength,
// ciphertext
buffer.AsSpan(0, cipherText.Length),
// additional data (unused)
ReadOnlySpan<byte>.Empty,
// nonce
nonceData,
// key
_key);
message.Position = 0;
message.LengthBytes = messageLength;
ArrayPool<byte>.Shared.Return(buffer);
if (!result)
throw new SodiumException("Decryption operation failed!");
}
}

View File

@@ -14,6 +14,7 @@ using Lidgren.Network;
using Robust.Shared.Log;
using Robust.Shared.Network.Messages.Handshake;
using Robust.Shared.Utility;
using SpaceWizards.Sodium;
namespace Robust.Shared.Network
{
@@ -154,11 +155,11 @@ namespace Robust.Shared.Network
var encRequest = new MsgEncryptionRequest();
encRequest.ReadFromBuffer(response);
var sharedSecret = new byte[AesKeyLength];
var sharedSecret = new byte[SharedKeyLength];
RandomNumberGenerator.Fill(sharedSecret);
if (encrypt)
encryption = new NetAESEncryption(peer.Peer, sharedSecret, 0, sharedSecret.Length);
encryption = new NetEncryption(sharedSecret, isServer: false);
byte[] keyBytes;
if (hasPubKey)
@@ -172,11 +173,18 @@ namespace Robust.Shared.Network
keyBytes = encRequest.PublicKey;
}
var rsaKey = RSA.Create();
rsaKey.ImportRSAPublicKey(keyBytes, out _);
if (keyBytes.Length != CryptoBox.PublicKeyBytes)
{
connection.Disconnect("Invalid public key length");
return;
}
var encryptedSecret = rsaKey.Encrypt(sharedSecret, RSAEncryptionPadding.OaepSHA256);
var encryptedVerifyToken = rsaKey.Encrypt(encRequest.VerifyToken, RSAEncryptionPadding.OaepSHA256);
// Data is [shared]+[verify]
var data = new byte[sharedSecret.Length + encRequest.VerifyToken.Length];
sharedSecret.CopyTo(data.AsSpan());
encRequest.VerifyToken.CopyTo(data.AsSpan(sharedSecret.Length));
var sealedData = CryptoBox.Seal(data, keyBytes);
var authHashBytes = MakeAuthHash(sharedSecret, keyBytes);
var authHash = Convert.ToBase64String(authHashBytes);
@@ -190,8 +198,7 @@ namespace Robust.Shared.Network
var encryptionResponse = new MsgEncryptionResponse
{
SharedSecret = encryptedSecret,
VerifyToken = encryptedVerifyToken,
SealedData = sealedData,
UserId = userId!.Value.UserId
};

View File

@@ -23,22 +23,24 @@ Note that the S->C packet AFTER `MsgLoginStart` is preceded with a bool (+pad) t
A more detailed overview is here:
First the client sends `MsgLoginStart`. This contains the client's username, whether it wants to authenticate, and whether it needs the server's public RSA key sent (when authenticating && it doesn't have it yet from the launcher).
First the client sends `MsgLoginStart`. This contains the client's username, whether it wants to authenticate, and whether it needs the server's public encryption key sent (when authenticating && it doesn't have it yet from the launcher).
The server can then choose to do block the client, let the client authenticate, or let the client in as guest. If it lets the client in as guest it skips straight to sending `MsgLoginSuccess` (see below). Otherwise it will send an `MsgEncryptionRequest` to the client to initiate authentication.
`MsgEncryptionRequest` contains a random verify token sent by the server, as well as the server's public RSA key (if requested).
`MsgEncryptionRequest` contains a random verify token sent by the server, as well as the server's public encryption key (if requested).
When the client receives `MsgEncryptionRequest`, it will generate a 32-byte random secret. It will then generate an SHA-256 hash of this secret and the server's public key. This hash is POSTed to `api/session/join` (along with login token in `Authorization` header) on the auth server. The shared secret and verify token are separately encrypted with the server's RSA key, then sent along with the client's account GUID to the server in `MsgEncryptionResponse`.
When the client receives `MsgEncryptionRequest`, it will generate a 32-byte random secret. It will then generate an SHA-256 hash of this secret and the server's public key. This hash is POSTed to `api/session/join` (along with login token in `Authorization` header) on the auth server. The shared secret and verify token are separately encrypted with the server's encryption key, then sent along with the client's account GUID to the server in `MsgEncryptionResponse`.
The server will then decrypt the verify token and shared secret with its private RSA key. If the verify token does not match then drop the client (to check if the client is using the correct key). Then the server will generate the same hash as mentioned earlier and GET it to `api/session/hasJoined?hash=<hash>&userId=<userId>` to check if the user did indeed authenticate correctly. And also gets the user's username and GUID again because why not.
The server will then decrypt the verify token and shared secret with its private encryption key. If the verify token does not match then drop the client (to check if the client is using the correct key). Then the server will generate the same hash as mentioned earlier and GET it to `api/session/hasJoined?hash=<hash>&userId=<userId>` to check if the user did indeed authenticate correctly. And also gets the user's username and GUID again because why not.
From this point on, if authenticating, all messages sent between client and server will be AES encrypted with the shared secret generated earlier.
From this point on, if authenticating, all messages sent between client and server will be encrypted with the shared secret generated earlier.
Then the server shall reply with `MsgLoginSuccess` with the assigned username/userID if login is successful.
I think that was everything.
Oh yeah, the server generates a new 2048-bit RSA key every startup and exposes it via its status API on `/info`.
Oh yeah, the server generates a new encryption key every startup and exposes it via its status API on `/info`.
We use libsodium for most of the crypto stuff. The public/private key stuff is with [Sealed box](https://doc.libsodium.org/public-key_cryptography/sealed_boxes). Packet encryption for game packets is done with [AEAD XChaCha20-Poly1305](https://doc.libsodium.org/secret-key_cryptography/aead/chacha20-poly1305/xchacha20-poly1305_construction).
This is a rough outline. If you want complete gritty details just check the damn code.

View File

@@ -10,31 +10,25 @@ using Robust.Shared.AuthLib;
using Robust.Shared.Log;
using Robust.Shared.Network.Messages.Handshake;
using Robust.Shared.Utility;
using SpaceWizards.Sodium;
namespace Robust.Shared.Network
{
partial class NetManager
{
private const int RsaKeySize = 2048;
private readonly byte[] _cryptoPrivateKey = new byte[CryptoBox.SecretKeyBytes];
private RSA? _authRsaPrivateKey;
public byte[]? RsaPublicKey { get; private set; }
public byte[] CryptoPublicKey { get; } = new byte[CryptoBox.PublicKeyBytes];
public AuthMode Auth { get; private set; }
public Func<string, Task<NetUserId?>>? AssignUserIdCallback { get; set; }
public IServerNetManager.NetApprovalDelegate? HandleApprovalCallback { get; set; }
private void SAGenerateRsaKeys()
private void SAGenerateKeys()
{
_authRsaPrivateKey = RSA.Create(RsaKeySize);
RsaPublicKey = _authRsaPrivateKey.ExportRSAPublicKey();
CryptoBox.KeyPair(CryptoPublicKey, _cryptoPrivateKey);
/*
Logger.DebugS("auth", "Private RSA key is {0}",
Convert.ToBase64String(_authRsaPrivateKey.ExportRSAPrivateKey()));
*/
Logger.DebugS("auth", "Public RSA key is {0}", Convert.ToBase64String(RsaPublicKey));
Logger.DebugS("auth", "Public key is {0}", Convert.ToBase64String(CryptoPublicKey));
}
private async void HandleHandshake(NetPeerData peer, NetConnection connection)
@@ -72,7 +66,7 @@ namespace Robust.Shared.Network
RandomNumberGenerator.Fill(verifyToken);
var msgEncReq = new MsgEncryptionRequest
{
PublicKey = needPk ? RsaPublicKey : Array.Empty<byte>(),
PublicKey = needPk ? CryptoPublicKey : Array.Empty<byte>(),
VerifyToken = verifyToken
};
@@ -87,18 +81,14 @@ namespace Robust.Shared.Network
var msgEncResponse = new MsgEncryptionResponse();
msgEncResponse.ReadFromBuffer(incPacket);
byte[] verifyTokenCheck;
byte[] sharedSecret;
try
{
verifyTokenCheck = _authRsaPrivateKey!.Decrypt(
msgEncResponse.VerifyToken,
RSAEncryptionPadding.OaepSHA256);
sharedSecret = _authRsaPrivateKey!.Decrypt(
msgEncResponse.SharedSecret,
RSAEncryptionPadding.OaepSHA256);
}
catch (CryptographicException)
var encResp = new byte[verifyToken.Length + SharedKeyLength];
var ret = CryptoBox.SealOpen(
encResp,
msgEncResponse.SealedData,
CryptoPublicKey,
_cryptoPrivateKey);
if (!ret)
{
// Launcher gives the client the public RSA key of the server BUT
// that doesn't persist if the server restarts.
@@ -108,16 +98,20 @@ namespace Robust.Shared.Network
return;
}
if (!verifyToken.SequenceEqual(verifyTokenCheck))
// Data is [shared]+[verify]
var verifyTokenCheck = encResp[SharedKeyLength..];
var sharedSecret = encResp[..SharedKeyLength];
if (!verifyToken.AsSpan().SequenceEqual(verifyTokenCheck))
{
connection.Disconnect("Verify token is invalid");
return;
}
if (msgLogin.Encrypt)
encryption = new NetAESEncryption(peer.Peer, sharedSecret, 0, sharedSecret.Length);
encryption = new NetEncryption(sharedSecret, isServer: true);
var authHashBytes = MakeAuthHash(sharedSecret, RsaPublicKey!);
var authHashBytes = MakeAuthHash(sharedSecret, CryptoPublicKey!);
var authHash = Base64Helpers.ConvertToBase64Url(authHashBytes);
var client = new HttpClient();

View File

@@ -18,6 +18,7 @@ using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using SpaceWizards.Sodium;
namespace Robust.Shared.Network
{
@@ -38,7 +39,7 @@ namespace Robust.Shared.Network
/// </summary>
public sealed partial class NetManager : IClientNetManager, IServerNetManager
{
internal const int AesKeyLength = 32;
internal const int SharedKeyLength = CryptoAeadXChaCha20Poly1305Ietf.KeyBytes; // 32 bytes
[Dependency] private readonly IRobustSerializer _serializer = default!;
@@ -266,7 +267,7 @@ namespace Robust.Shared.Network
if (IsServer)
{
SAGenerateRsaKeys();
SAGenerateKeys();
}
}
@@ -830,10 +831,7 @@ namespace Robust.Shared.Network
var encryption = IsServer ? channel.Encryption : _clientEncryption;
if (encryption != null)
{
msg.Decrypt(encryption);
}
encryption?.Decrypt(msg);
var id = msg.ReadByte();
@@ -1062,10 +1060,8 @@ namespace Robust.Shared.Network
var peer = channel.Connection.Peer;
var packet = BuildMessage(message, peer);
if (channel.Encryption != null)
{
packet.Encrypt(channel.Encryption);
}
channel.Encryption?.Encrypt(packet);
var method = message.DeliveryMethod;
peer.SendMessage(packet, channel.Connection, method);
@@ -1105,10 +1101,8 @@ namespace Robust.Shared.Network
var peer = _netPeers[0];
var packet = BuildMessage(message, peer.Peer);
var method = message.DeliveryMethod;
if (_clientEncryption != null)
{
packet.Encrypt(_clientEncryption);
}
_clientEncryption?.Encrypt(packet);
peer.Peer.SendMessage(packet, peer.ConnectionsWithChannels[0], method);
LogSend(message, method, packet);

View File

@@ -302,6 +302,14 @@ namespace Robust.Shared.Physics.Dynamics
{
// TODO: When this gets ECSd add a helper and remove
if (seed.Deleted)
{
// This should never happen. Yet it does.
Logger.Error($"Deleted physics component in awake bodies set. Owner Uid: {seed.Owner}. Physics map: {_entityManager.ToPrettyString(Owner)}");
RemoveBody(seed);
continue;
}
// I tried not running prediction for non-contacted entities but unfortunately it looked like shit
// when contact broke so if you want to try that then GOOD LUCK.
if (seed.Island ||

View File

@@ -20,6 +20,7 @@
<PackageReference Include="YamlDotNet" Version="9.1.4" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="Linguini.Bundle" Version="0.1.3" />
<PackageReference Include="SpaceWizards.Sodium" Version="0.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lidgren.Network\Lidgren.Network.csproj" />

View File

@@ -302,7 +302,7 @@ namespace Robust.UnitTesting
}
}
public byte[]? RsaPublicKey => null;
public byte[]? CryptoPublicKey => null;
public AuthMode Auth => AuthMode.Disabled;
public Func<string, Task<NetUserId?>>? AssignUserIdCallback { get; set; }
public IServerNetManager.NetApprovalDelegate? HandleApprovalCallback { get; set; }