mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
13 Commits
v0.8.83
...
reactjs-su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de8c2c14bb | ||
|
|
baebfff321 | ||
|
|
24191404aa | ||
|
|
990842f5c2 | ||
|
|
2c2bd3f330 | ||
|
|
1ae14c4bfa | ||
|
|
61a1701dc9 | ||
|
|
c6ea11346e | ||
|
|
ad7c871e28 | ||
|
|
c77b3f8022 | ||
|
|
b263f4a1df | ||
|
|
1a11d41bba | ||
|
|
14d0a77644 |
4
.github/workflows/benchmarks.yml
vendored
4
.github/workflows/benchmarks.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Benchmarks
|
||||
on:
|
||||
push
|
||||
#on:
|
||||
# push
|
||||
#schedule:
|
||||
# - cron: '0 5 * * *'
|
||||
#push:
|
||||
|
||||
Submodule Lidgren.Network/Lidgren.Network updated: 1dd5c1f333...b723fc532e
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
122
Robust.Shared/Network/NetEncryption.cs
Normal file
122
Robust.Shared/Network/NetEncryption.cs
Normal 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!");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user