Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth hash (engine-side) #2

Merged
merged 2 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Robust.Shared/Network/AuthManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal interface IAuthManager
string? ServerPublicKey { get; set; }
string? UserPublicKey { get; set; }
string? UserJWT { get; set; }
string? SharedSecretBase64 { get; set; }

void LoadFromEnv();
}
Expand All @@ -22,6 +23,7 @@ internal sealed class AuthManager : IAuthManager
public string? ServerPublicKey { get; set; }
public string? UserPublicKey { get; set; }
public string? UserJWT { get; set; }
public string? SharedSecretBase64 { get; set; }

public void LoadFromEnv()
{
Expand All @@ -40,6 +42,11 @@ public void LoadFromEnv()
UserJWT = userJWT;
}

if (TryGetVar("ROBUST_SHARED_SECRET", out var sharedSecretBase64))
{
SharedSecretBase64 = sharedSecretBase64;
}

static bool TryGetVar(string var, [NotNullWhen(true)] out string? val)
{
val = Environment.GetEnvironmentVariable(var);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ internal sealed class MsgEncryptionResponse : NetMessage
public byte[] SealedData;
public string UserJWT;
public string UserPublicKey;
public ulong StartingNonce;

public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
Expand All @@ -23,6 +24,7 @@ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer

UserJWT = buffer.ReadString();
UserPublicKey = buffer.ReadString();
StartingNonce = buffer.ReadUInt64();
}

public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
Expand All @@ -32,6 +34,7 @@ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer

buffer.Write(UserJWT);
buffer.Write(UserPublicKey);
buffer.Write(StartingNonce);
}
}
}
22 changes: 22 additions & 0 deletions Robust.Shared/Network/NetEncryption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ internal sealed class NetEncryption
private ulong _nonce;
private readonly byte[] _key;

/// <summary>
/// How much to offset noonce during reconnect. Noonce+key combination should not be re-used per
/// https://doc.libsodium.org/secret-key_cryptography/aead/chacha20-poly1305/ietf_chacha20-poly1305_construction
/// "The public nonce npub should never ever be reused with the same key. The recommended way to generate it is to
/// use randombytes_buf() for the first message, and increment it for each subsequent message using the same key."
/// Since key is no longer randomly generated per connection, the noonce must be incremented. Rather than just
/// set this to exactly where it left off, I am padding it a bit. This is just in case there's some messages from
/// server -> client that the client never received. This way, I push this much farther into the unused future for
/// safety.
/// </summary>
public const ulong RECONNECT_NOONCE_PADDING = 2000000;

public NetEncryption(byte[] key, bool isServer)
{
if (key.Length != CryptoAeadXChaCha20Poly1305Ietf.KeyBytes)
Expand Down Expand Up @@ -119,4 +131,14 @@ public unsafe void Decrypt(NetIncomingMessage message)
if (!result)
throw new SodiumException("Decryption operation failed!");
}

public void SetNonce(ulong newValue)
{
Interlocked.Exchange(ref _nonce, newValue);
}

public ulong GetNonce()
{
return Interlocked.Read(ref _nonce);
}
}
34 changes: 32 additions & 2 deletions Robust.Shared/Network/NetManager.ClientConnect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,39 @@ private async Task CCDoHandshake(NetPeerData peer, NetConnection connection, str
var encRequest = new MsgEncryptionRequest();
encRequest.ReadFromBuffer(response, _serializer);

var sharedSecret = new byte[SharedKeyLength];
RandomNumberGenerator.Fill(sharedSecret);
byte[] sharedSecret;

if (string.IsNullOrEmpty(_authManager.SharedSecretBase64))
{
// Generate new shared secret in robust client
sharedSecret = new byte[SharedKeyLength];
RandomNumberGenerator.Fill(sharedSecret); // In order for this to work, server must not be verifying JWT
}
else
{
// Use shared secret from launcher

// (Robust client does not have direct access to the user's private key for safety. The JWT needs to
// include the authhash to avoid a MITM. Thus, the launcher must generate the JWT and by extension
// the shared secret. Launcher will generate it and pass it to us in Base64 format.)
sharedSecret = System.Convert.FromBase64String(_authManager.SharedSecretBase64);

if (sharedSecret.Length != SharedKeyLength)
{
var msg = $"Invalid shared secret length from launcher. Expected {SharedKeyLength}, but was {sharedSecret.Length}.";
connection.Disconnect(msg);
throw new Exception(msg);
}
}

if (encrypt)
{
encryption = new NetEncryption(sharedSecret, isServer: false);

if (_clientEncryption != null)
encryption.SetNonce(_clientEncryption.GetNonce() + NetEncryption.RECONNECT_NOONCE_PADDING);
}

byte[] keyBytes;
if (hasServerPublicKey)
{
Expand Down Expand Up @@ -194,6 +221,9 @@ private async Task CCDoHandshake(NetPeerData peer, NetConnection connection, str
UserPublicKey = _authManager.UserPublicKey
};

if (encrypt && encryption != null)
encryptionResponse.StartingNonce = encryption.GetNonce() + 1;

var outEncRespMsg = peer.Peer.CreateMessage();
encryptionResponse.WriteToBuffer(outEncRespMsg, _serializer);
peer.Peer.SendMessage(outEncRespMsg, connection, NetDeliveryMethod.ReliableOrdered);
Expand Down
23 changes: 23 additions & 0 deletions Robust.Shared/Network/NetManager.ServerAuth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,13 @@ private async void HandleHandshake(NetPeerData peer, NetConnection connection)
}

if (msgLogin.Encrypt)
{
encryption = new NetEncryption(sharedSecret, isServer: true);
encryption.SetNonce(msgEncResponse.StartingNonce);
}

var authHashBytes = MakeAuthHash(sharedSecret, CryptoPublicKey!);
var authHash = Base64Helpers.ConvertToBase64Url(authHashBytes);

// Validate the JWT
var userPublicKeyString = msgEncResponse.UserPublicKey ?? "";
Expand Down Expand Up @@ -219,6 +225,23 @@ private async void HandleHandshake(NetPeerData peer, NetConnection connection)
connection.Disconnect("JWT Validation Error\nJWT appears to be for another server.\nTry returning to launcher and reconnect.");
return;
}

// Also verify authhash matches.
// (This step helps deter a MITM/proxy attack, since even if traffic was proxied, it should also
// be encrypted.)
string authHashClaim = "";
var authHashClaimNode = jsonNode["authhash"];
if (authHashClaimNode == null)
{
connection.Disconnect("JWT Validation Error - No auth hash in JWT\n(Ensure you are using latest launcher version).");
return;
}
authHashClaim = (string) authHashClaimNode.GetValue<string>();
if (authHashClaim != authHash)
{
connection.Disconnect("JWT Validation Error - Wrong auth hash in JWT\n(Check server address is correct).");
return;
}
}

_logger.Verbose(
Expand Down
Loading