Basic support for 1.19.2

This commit is contained in:
BruceChen 2022-08-27 02:10:44 +08:00
parent af1485c753
commit c34dd46067
14 changed files with 703 additions and 129 deletions

View file

@ -14,6 +14,7 @@ using MinecraftClient.Mapping;
using MinecraftClient.Inventory; using MinecraftClient.Inventory;
using MinecraftClient.Logger; using MinecraftClient.Logger;
using MinecraftClient.Protocol.Keys; using MinecraftClient.Protocol.Keys;
using MinecraftClient.Protocol.Message;
namespace MinecraftClient namespace MinecraftClient
{ {
@ -68,7 +69,8 @@ namespace MinecraftClient
private int port; private int port;
private int protocolversion; private int protocolversion;
private string username; private string username;
private string uuid; private Guid uuid;
private string uuidStr;
private string sessionid; private string sessionid;
private PlayerKeyPair? playerKeyPair; private PlayerKeyPair? playerKeyPair;
private DateTime lastKeepAlive; private DateTime lastKeepAlive;
@ -104,7 +106,8 @@ namespace MinecraftClient
public int GetServerPort() { return port; } public int GetServerPort() { return port; }
public string GetServerHost() { return host; } public string GetServerHost() { return host; }
public string GetUsername() { return username; } public string GetUsername() { return username; }
public string GetUserUUID() { return uuid; } public Guid GetUserUuid() { return uuid; }
public string GetUserUuidStr() { return uuidStr; }
public string GetSessionID() { return sessionid; } public string GetSessionID() { return sessionid; }
public Location GetCurrentLocation() { return location; } public Location GetCurrentLocation() { return location; }
public float GetYaw() { return playerYaw; } public float GetYaw() { return playerYaw; }
@ -180,7 +183,8 @@ namespace MinecraftClient
bool retry = false; bool retry = false;
this.sessionid = sessionID; this.sessionid = sessionID;
this.uuid = uuid; this.uuid = new Guid(uuid);
this.uuidStr = uuid;
this.username = user; this.username = user;
this.host = server_ip; this.host = server_ip;
this.port = port; this.port = port;
@ -2093,19 +2097,17 @@ namespace MinecraftClient
List<string> links = new(); List<string> links = new();
string messageText; string messageText;
if (message.isJson)
{
if (message.isSignedChat) if (message.isSignedChat)
{ {
if (!Settings.ShowIllegalSignedChat && !message.isSystemChat && !(bool)message.isSignatureLegal!) if (!Settings.ShowIllegalSignedChat && !message.isSystemChat && !(bool)message.isSignatureLegal!)
return; return;
messageText = ChatParser.ParseSignedChat(message, links); messageText = ChatParser.ParseSignedChat(message, links);
} }
else
messageText = ChatParser.ParseText(message.content, links);
}
else else
{ {
if (message.isJson)
messageText = ChatParser.ParseText(message.content, links);
else
messageText = message.content; messageText = message.content;
} }
@ -2243,15 +2245,16 @@ namespace MinecraftClient
if (player.Name == username) if (player.Name == username)
{ {
// 1.19+ offline server is possible to return different uuid // 1.19+ offline server is possible to return different uuid
this.uuid = player.UUID.ToString().Replace("-", string.Empty); this.uuid = player.Uuid;
this.uuidStr = player.Uuid.ToString().Replace("-", string.Empty);
} }
lock (onlinePlayers) lock (onlinePlayers)
{ {
onlinePlayers[player.UUID] = player; onlinePlayers[player.Uuid] = player;
} }
DispatchBotEvent(bot => bot.OnPlayerJoin(player.UUID, player.Name)); DispatchBotEvent(bot => bot.OnPlayerJoin(player.Uuid, player.Name));
} }
/// <summary> /// <summary>

View file

@ -1103,9 +1103,51 @@ namespace MinecraftClient.Protocol.Handlers
/// </summary> /// </summary>
/// <param name="bytes">Byte array</param> /// <param name="bytes">Byte array</param>
/// <returns>String representation</returns> /// <returns>String representation</returns>
public string ByteArrayToString(byte[] bytes) public string ByteArrayToString(byte[]? bytes)
{ {
if (bytes == null)
return "null";
else
return BitConverter.ToString(bytes).Replace("-", " "); return BitConverter.ToString(bytes).Replace("-", " ");
} }
/// <summary>
/// Write LastSeenMessageList
/// </summary>
/// <param name="msgList">Message.LastSeenMessageList</param>
/// <returns>Message.LastSeenMessageList Packet Data</returns>
public byte[] GetLastSeenMessageList(Message.LastSeenMessageList msgList)
{
List<byte> fields = new List<byte>();
fields.AddRange(GetVarInt(msgList.entries.Length));
foreach (Message.LastSeenMessageList.Entry entry in msgList.entries)
{
fields.AddRange(entry.profileId.ToBigEndianBytes());
fields.AddRange(GetVarInt(entry.lastSignature.Length));
fields.AddRange(entry.lastSignature);
}
return fields.ToArray();
}
/// <summary>
/// Write LastSeenMessageList.Acknowledgment
/// </summary>
/// <param name="ack">Acknowledgment</param>
/// <returns>Acknowledgment Packet Data</returns>
public byte[] GetAcknowledgment(Message.LastSeenMessageList.Acknowledgment ack)
{
List<byte> fields = new List<byte>();
fields.AddRange(GetLastSeenMessageList(ack.lastSeen));
if (ack.lastReceived == null)
fields.AddRange(GetBool(false));
else
{
fields.AddRange(GetBool(true));
fields.AddRange(ack.lastReceived.profileId.ToBigEndianBytes());
fields.AddRange(GetVarInt(ack.lastReceived.lastSignature.Length));
fields.AddRange(ack.lastReceived.lastSignature);
}
return fields.ToArray();
}
} }
} }

View file

@ -53,6 +53,7 @@ namespace MinecraftClient.Protocol.Handlers
PacketTypePalette p; PacketTypePalette p;
if (protocol > Protocol18Handler.MC_1_19_2_Version) if (protocol > Protocol18Handler.MC_1_19_2_Version)
throw new NotImplementedException(Translations.Get("exception.palette.packet")); throw new NotImplementedException(Translations.Get("exception.palette.packet"));
if (protocol <= Protocol18Handler.MC_1_8_Version) if (protocol <= Protocol18Handler.MC_1_8_Version)
p = new PacketPalette17(); p = new PacketPalette17();
else if (protocol <= Protocol18Handler.MC_1_11_2_Version) else if (protocol <= Protocol18Handler.MC_1_11_2_Version)

View file

@ -10,6 +10,7 @@ using System.Security.Cryptography;
using MinecraftClient.Mapping; using MinecraftClient.Mapping;
using MinecraftClient.Inventory; using MinecraftClient.Inventory;
using MinecraftClient.Protocol.Keys; using MinecraftClient.Protocol.Keys;
using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Protocol.Handlers namespace MinecraftClient.Protocol.Handlers
{ {
@ -559,7 +560,7 @@ namespace MinecraftClient.Protocol.Handlers
public bool Login(PlayerKeyPair playerKeyPair) public bool Login(PlayerKeyPair playerKeyPair)
{ {
if (Handshake(handler.GetUserUUID(), handler.GetUsername(), handler.GetSessionID(), handler.GetServerHost(), handler.GetServerPort())) if (Handshake(handler.GetUserUuidStr(), handler.GetUsername(), handler.GetSessionID(), handler.GetServerHost(), handler.GetServerPort()))
{ {
Send(new byte[] { 0xCD, 0 }); Send(new byte[] { 0xCD, 0 });
try try

View file

@ -19,6 +19,7 @@ using MinecraftClient.Logger;
using System.Threading.Tasks; using System.Threading.Tasks;
using MinecraftClient.Protocol.Keys; using MinecraftClient.Protocol.Keys;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Protocol.Handlers namespace MinecraftClient.Protocol.Handlers
{ {
@ -66,6 +67,10 @@ namespace MinecraftClient.Protocol.Handlers
private int protocolversion; private int protocolversion;
private int currentDimension; private int currentDimension;
private int pendingAcknowledgments = 0;
private LastSeenMessagesCollector lastSeenMessagesCollector = new(5);
private LastSeenMessageList.Entry? lastReceivedMessage = null;
Protocol18Forge pForge; Protocol18Forge pForge;
Protocol18Terrain pTerrain; Protocol18Terrain pTerrain;
IMinecraftComHandler handler; IMinecraftComHandler handler;
@ -163,6 +168,36 @@ namespace MinecraftClient.Protocol.Handlers
else itemPalette = new ItemPalette1161(); else itemPalette = new ItemPalette1161();
} }
else itemPalette = new ItemPalette115(); else itemPalette = new ItemPalette115();
// MessageType
// You can find it in https://wiki.vg/Protocol#Player_Chat_Message or /net/minecraft/network/message/MessageType.java
if (protocolversion >= MC_1_19_2_Version)
{
ChatParser.ChatId2Type = new()
{
{ 0, ChatParser.MessageType.CHAT },
{ 1, ChatParser.MessageType.SAY_COMMAND },
{ 2, ChatParser.MessageType.MSG_COMMAND_INCOMING },
{ 3, ChatParser.MessageType.MSG_COMMAND_OUTGOING },
{ 4, ChatParser.MessageType.TEAM_MSG_COMMAND_INCOMING },
{ 5, ChatParser.MessageType.TEAM_MSG_COMMAND_OUTGOING },
{ 6, ChatParser.MessageType.EMOTE_COMMAND },
};
}
else if (protocolversion >= MC_1_19_Version)
{
ChatParser.ChatId2Type = new()
{
{ 0, ChatParser.MessageType.CHAT },
{ 1, ChatParser.MessageType.RAW_MSG },
{ 2, ChatParser.MessageType.RAW_MSG },
{ 3, ChatParser.MessageType.SAY_COMMAND },
{ 4, ChatParser.MessageType.MSG_COMMAND_INCOMING },
{ 5, ChatParser.MessageType.TEAM_MSG_COMMAND_INCOMING },
{ 6, ChatParser.MessageType.EMOTE_COMMAND },
{ 7, ChatParser.MessageType.RAW_MSG },
};
}
} }
/// <summary> /// <summary>
@ -439,14 +474,104 @@ namespace MinecraftClient.Protocol.Handlers
byte[] messageSignature = dataTypes.ReadNextByteArray(packetData); byte[] messageSignature = dataTypes.ReadNextByteArray(packetData);
bool verifyResult;
if (senderUUID.ToString().Replace("-", String.Empty) == handler.GetUserUuidStr())
verifyResult = true;
else
{
PlayerInfo? player = handler.GetPlayerInfo(senderUUID); PlayerInfo? player = handler.GetPlayerInfo(senderUUID);
bool verifyResult = player == null ? false : player.VerifyMessage(signedChat, senderUUID, timestamp, salt, ref messageSignature); verifyResult = player == null ? false : player.VerifyMessage(signedChat, timestamp, salt, ref messageSignature);
}
handler.OnTextReceived(new(signedChat, true, messageType, senderUUID, unsignedChatContent, senderDisplayName, senderTeamName, timestamp, verifyResult)); handler.OnTextReceived(new(signedChat, true, messageType, senderUUID, unsignedChatContent, senderDisplayName, senderTeamName, timestamp, messageSignature, verifyResult));
} }
else // 1.19.1 + else // 1.19.1 +
{ {
// Todo byte[]? precedingSignature = dataTypes.ReadNextBool(packetData) ? dataTypes.ReadNextByteArray(packetData) : null;
Guid senderUUID = dataTypes.ReadNextUUID(packetData);
byte[] headerSignature = dataTypes.ReadNextByteArray(packetData);
string signedChat = dataTypes.ReadNextString(packetData);
string? decorated = dataTypes.ReadNextBool(packetData) ? dataTypes.ReadNextString(packetData) : null;
long timestamp = dataTypes.ReadNextLong(packetData);
long salt = dataTypes.ReadNextLong(packetData);
int lastSeenMessageListLen = dataTypes.ReadNextVarInt(packetData);
LastSeenMessageList.Entry[] lastSeenMessageList = new LastSeenMessageList.Entry[lastSeenMessageListLen];
for (int i = 0; i < lastSeenMessageListLen; ++i)
{
Guid user = dataTypes.ReadNextUUID(packetData);
byte[] lastSignature = dataTypes.ReadNextByteArray(packetData);
lastSeenMessageList[i] = new(user, lastSignature);
}
LastSeenMessageList lastSeenMessages = new(lastSeenMessageList);
string? unsignedChatContent = dataTypes.ReadNextBool(packetData) ? dataTypes.ReadNextString(packetData) : null;
int filterEnum = dataTypes.ReadNextVarInt(packetData);
if (filterEnum == 2) // PARTIALLY_FILTERED
dataTypes.ReadNextULongArray(packetData);
int chatTypeId = dataTypes.ReadNextVarInt(packetData);
string chatName = dataTypes.ReadNextString(packetData);
string? targetName = dataTypes.ReadNextBool(packetData) ? dataTypes.ReadNextString(packetData) : null;
bool verifyResult;
if (senderUUID.ToString().Replace("-", String.Empty) == handler.GetUserUuidStr())
verifyResult = true;
else
{
PlayerInfo? player = handler.GetPlayerInfo(senderUUID);
if (player == null)
verifyResult = false;
else
{
bool lastVerifyResult = player.IsMessageChainLegal();
verifyResult = player.VerifyMessage(signedChat, timestamp, salt, ref headerSignature, ref precedingSignature, lastSeenMessages);
if (lastVerifyResult && !verifyResult)
log.Warn("Player " + player.DisplayName + " message chains broken!");
}
}
Dictionary<string, Json.JSONData> chatInfo = Json.ParseJson(chatName).Properties;
string senderDisplayName = (chatInfo.ContainsKey("insertion") ? chatInfo["insertion"] : chatInfo["text"]).StringValue;
string? senderTeamName = null;
ChatParser.MessageType messageTypeEnum = ChatParser.ChatId2Type![chatTypeId];
if (targetName != null &&
(messageTypeEnum == ChatParser.MessageType.TEAM_MSG_COMMAND_INCOMING || messageTypeEnum == ChatParser.MessageType.TEAM_MSG_COMMAND_OUTGOING))
senderTeamName = Json.ParseJson(targetName).Properties["with"].DataArray[0].Properties["text"].StringValue;
ChatMessage chat = new(signedChat, false, chatTypeId, senderUUID, unsignedChatContent, senderDisplayName, senderTeamName, timestamp, headerSignature, verifyResult);
if (!chat.lacksSender())
this.acknowledge(chat);
handler.OnTextReceived(chat);
}
break;
case PacketTypesIn.MessageHeader:
if (protocolversion >= MC_1_19_2_Version)
{
byte[]? precedingSignature = dataTypes.ReadNextBool(packetData) ? dataTypes.ReadNextByteArray(packetData) : null;
Guid senderUUID = dataTypes.ReadNextUUID(packetData);
byte[] headerSignature = dataTypes.ReadNextByteArray(packetData);
byte[] bodyDigest = dataTypes.ReadNextByteArray(packetData);
bool verifyResult;
if (senderUUID.ToString().Replace("-", String.Empty) == handler.GetUserUuidStr())
verifyResult = true;
else
{
PlayerInfo? player = handler.GetPlayerInfo(senderUUID);
if (player == null)
verifyResult = false;
else
{
bool lastVerifyResult = player.IsMessageChainLegal();
verifyResult = player.VerifyMessageHead(ref precedingSignature, ref headerSignature, ref bodyDigest);
if (lastVerifyResult && !verifyResult)
log.Warn("Player " + player.DisplayName + " message chains broken!");
}
}
} }
break; break;
case PacketTypesIn.Respawn: case PacketTypesIn.Respawn:
@ -826,6 +951,8 @@ namespace MinecraftClient.Protocol.Handlers
//handler.OnTextReceived(message, true); //handler.OnTextReceived(message, true);
} }
break;
case PacketTypesIn.ChatSuggestions:
break; break;
case PacketTypesIn.MapChunkBulk: case PacketTypesIn.MapChunkBulk:
if (protocolversion < MC_1_9_Version && handler.GetTerrainEnabled()) if (protocolversion < MC_1_9_Version && handler.GetTerrainEnabled())
@ -1546,7 +1673,7 @@ namespace MinecraftClient.Protocol.Handlers
} }
if (protocolversion >= MC_1_19_2_Version) if (protocolversion >= MC_1_19_2_Version)
{ {
string uuid = handler.GetUserUUID(); string uuid = handler.GetUserUuidStr();
if (uuid == "0") if (uuid == "0")
fullLoginPacket.AddRange(dataTypes.GetBool(false)); // Has UUID fullLoginPacket.AddRange(dataTypes.GetBool(false)); // Has UUID
else else
@ -1572,7 +1699,7 @@ namespace MinecraftClient.Protocol.Handlers
string serverID = dataTypes.ReadNextString(packetData); string serverID = dataTypes.ReadNextString(packetData);
byte[] serverPublicKey = dataTypes.ReadNextByteArray(packetData); byte[] serverPublicKey = dataTypes.ReadNextByteArray(packetData);
byte[] token = dataTypes.ReadNextByteArray(packetData); byte[] token = dataTypes.ReadNextByteArray(packetData);
return StartEncryption(handler.GetUserUUID(), handler.GetSessionID(), token, serverID, serverPublicKey, playerKeyPair); return StartEncryption(handler.GetUserUuidStr(), handler.GetSessionID(), token, serverID, serverPublicKey, playerKeyPair);
} }
else if (packetID == 0x02) //Login successful else if (packetID == 0x02) //Login successful
{ {
@ -1854,15 +1981,57 @@ namespace MinecraftClient.Protocol.Handlers
return protocolversion; return protocolversion;
} }
/// <summary>
/// Send MessageAcknowledgment packet
/// </summary>
/// <param name="acknowledgment">Message acknowledgment</param>
/// <returns>True if properly sent</returns>
public bool SendMessageAcknowledgment(LastSeenMessageList.Acknowledgment acknowledgment)
{
try
{
byte[] fields = dataTypes.GetAcknowledgment(acknowledgment);
SendPacket(PacketTypesOut.MessageAcknowledgment, fields);
return true;
}
catch (SocketException) { return false; }
catch (System.IO.IOException) { return false; }
catch (ObjectDisposedException) { return false; }
}
public LastSeenMessageList.Acknowledgment consumeAcknowledgment()
{
this.pendingAcknowledgments = 0;
return new LastSeenMessageList.Acknowledgment(this.lastSeenMessagesCollector.GetLastSeenMessages(), this.lastReceivedMessage);
}
public void acknowledge(ChatMessage message)
{
LastSeenMessageList.Entry? entry = message.toLastSeenMessageEntry();
if (entry != null)
{
lastSeenMessagesCollector.Add(entry);
lastReceivedMessage = null;
if (pendingAcknowledgments++ > 64)
SendMessageAcknowledgment(this.consumeAcknowledgment());
}
}
/// <summary> /// <summary>
/// The signable argument names and their values from command /// The signable argument names and their values from command
/// Signature will used in Vanilla's say, me, msg, teammsg, ban, banip, and kick commands. /// Signature will used in Vanilla's say, me, msg, teammsg, ban, banip, and kick commands.
/// https://gist.github.com/kennytv/ed783dd244ca0321bbd882c347892874#signed-command-arguments /// https://gist.github.com/kennytv/ed783dd244ca0321bbd882c347892874#signed-command-arguments
/// You can find all the commands that need to be signed by searching for "MessageArgumentType.getSignedMessage" in the source code.
/// Don't forget to handle the redirected commands, e.g. /tm, /w
///
/// </summary> /// </summary>
/// <param name="command">Command</param> /// <param name="command">Command</param>
/// <returns> List< Argument Name, Argument Value > </returns> /// <returns> List< Argument Name, Argument Value > </returns>
private List<Tuple<string, string>> collectCommandArguments(string command) private static List<Tuple<string, string>> CollectCommandArguments(string command)
{ {
List<Tuple<string, string>> needSigned = new(); List<Tuple<string, string>> needSigned = new();
@ -1874,21 +2043,25 @@ namespace MinecraftClient.Protocol.Handlers
{ {
/* /me <action> /* /me <action>
/say <message> /say <message>
/teammsg <message> */ /teammsg <message>
/tm <message> */
if (argStage1[0] == "me") if (argStage1[0] == "me")
needSigned.Add(new("action", argStage1[1])); needSigned.Add(new("action", argStage1[1]));
else if (argStage1[0] == "say" || argStage1[0] == "teammsg") else if (argStage1[0] == "say" || argStage1[0] == "teammsg" || argStage1[0] == "tm")
needSigned.Add(new("message", argStage1[1])); needSigned.Add(new("message", argStage1[1]));
else if (argStage1[0] == "msg" || argStage1[0] == "ban" || argStage1[0] == "ban-ip" || argStage1[0] == "kick") else if (argStage1[0] == "msg" || argStage1[0] == "tell" || argStage1[0] == "w" ||
argStage1[0] == "ban" || argStage1[0] == "ban-ip" || argStage1[0] == "kick")
{ {
/* /msg <targets> <message> /* /msg <targets> <message>
/tell <targets> <message>
/w <targets> <message>
/ban <target> [<reason>] /ban <target> [<reason>]
/ban-ip <target> [<reason>] /ban-ip <target> [<reason>]
/kick <target> [<reason>] */ /kick <target> [<reason>] */
string[] argStage2 = argStage1[1].Split(' ', 2, StringSplitOptions.None); string[] argStage2 = argStage1[1].Split(' ', 2, StringSplitOptions.None);
if (argStage2.Length == 2) if (argStage2.Length == 2)
{ {
if (argStage1[0] == "msg") if (argStage1[0] == "msg" || argStage1[0] == "tell" || argStage1[0] == "w")
needSigned.Add(new("message", argStage2[1])); needSigned.Add(new("message", argStage2[1]));
else if (argStage1[0] == "ban" || argStage1[0] == "ban-ip" || argStage1[0] == "kick") else if (argStage1[0] == "ban" || argStage1[0] == "ban-ip" || argStage1[0] == "kick")
needSigned.Add(new("reason", argStage2[1])); needSigned.Add(new("reason", argStage2[1]));
@ -1917,6 +2090,8 @@ namespace MinecraftClient.Protocol.Handlers
try try
{ {
LastSeenMessageList.Acknowledgment? acknowledgment = (protocolversion >= MC_1_19_2_Version) ? this.consumeAcknowledgment() : null;
List<byte> fields = new(); List<byte> fields = new();
// Command: String // Command: String
@ -1926,9 +2101,7 @@ namespace MinecraftClient.Protocol.Handlers
DateTimeOffset timeNow = DateTimeOffset.UtcNow; DateTimeOffset timeNow = DateTimeOffset.UtcNow;
fields.AddRange(dataTypes.GetLong(timeNow.ToUnixTimeMilliseconds())); fields.AddRange(dataTypes.GetLong(timeNow.ToUnixTimeMilliseconds()));
List<Tuple<string, string>> needSigned = collectCommandArguments(command); // List< Argument Name, Argument Value > List<Tuple<string, string>> needSigned = CollectCommandArguments(command); // List< Argument Name, Argument Value >
// foreach (var msg in needSigned)
// log.Info("<" + msg.Item1 + ">: " + msg.Item2);
if (needSigned.Count == 0 || playerKeyPair == null || !Settings.SignMessageInCommand) if (needSigned.Count == 0 || playerKeyPair == null || !Settings.SignMessageInCommand)
{ {
fields.AddRange(dataTypes.GetLong(0)); // Salt: Long fields.AddRange(dataTypes.GetLong(0)); // Salt: Long
@ -1936,14 +2109,16 @@ namespace MinecraftClient.Protocol.Handlers
} }
else else
{ {
string uuid = handler.GetUserUUID()!; Guid uuid = handler.GetUserUuid();
byte[] salt = GenerateSalt(); byte[] salt = GenerateSalt();
fields.AddRange(salt); // Salt: Long fields.AddRange(salt); // Salt: Long
fields.AddRange(dataTypes.GetVarInt(needSigned.Count)); // Signature Length: VarInt fields.AddRange(dataTypes.GetVarInt(needSigned.Count)); // Signature Length: VarInt
foreach (var argument in needSigned) foreach (var argument in needSigned)
{ {
fields.AddRange(dataTypes.GetString(argument.Item1)); // Argument name: String fields.AddRange(dataTypes.GetString(argument.Item1)); // Argument name: String
byte[] sign = playerKeyPair.PrivateKey.SignMessage(argument.Item2, uuid, timeNow, ref salt); byte[] sign = (protocolversion >= MC_1_19_2_Version) ?
playerKeyPair.PrivateKey.SignMessage(argument.Item2, uuid, timeNow, ref salt, acknowledgment!.lastSeen) :
playerKeyPair.PrivateKey.SignMessage(argument.Item2, uuid, timeNow, ref salt);
fields.AddRange(dataTypes.GetVarInt(sign.Length)); // Signature length: VarInt fields.AddRange(dataTypes.GetVarInt(sign.Length)); // Signature length: VarInt
fields.AddRange(sign); // Signature: Byte Array fields.AddRange(sign); // Signature: Byte Array
} }
@ -1952,6 +2127,12 @@ namespace MinecraftClient.Protocol.Handlers
// Signed Preview: Boolean // Signed Preview: Boolean
fields.AddRange(dataTypes.GetBool(false)); fields.AddRange(dataTypes.GetBool(false));
if (protocolversion >= MC_1_19_2_Version)
{
// Message Acknowledgment
fields.AddRange(dataTypes.GetAcknowledgment(acknowledgment!));
}
SendPacket(PacketTypesOut.ChatCommand, fields); SendPacket(PacketTypesOut.ChatCommand, fields);
return true; return true;
} }
@ -1971,19 +2152,14 @@ namespace MinecraftClient.Protocol.Handlers
if (String.IsNullOrEmpty(message)) if (String.IsNullOrEmpty(message))
return true; return true;
if (protocolversion >= MC_1_19_2_Version)
{
// Todo
log.Warn("Not implement");
return false;
}
// Process Chat Command - 1.19 and above // Process Chat Command - 1.19 and above
if (protocolversion >= MC_1_19_Version && message.StartsWith('/')) if (protocolversion >= MC_1_19_Version && message.StartsWith('/'))
return SendChatCommand(message[1..], playerKeyPair); return SendChatCommand(message[1..], playerKeyPair);
try try
{ {
LastSeenMessageList.Acknowledgment? acknowledgment = (protocolversion >= MC_1_19_2_Version) ? this.consumeAcknowledgment() : null;
List<byte> fields = new(); List<byte> fields = new();
// Message: String (up to 256 chars) // Message: String (up to 256 chars)
@ -2007,8 +2183,10 @@ namespace MinecraftClient.Protocol.Handlers
fields.AddRange(salt); fields.AddRange(salt);
// Signature Length & Signature: (VarInt) and Byte Array // Signature Length & Signature: (VarInt) and Byte Array
string uuid = handler.GetUserUUID()!; Guid uuid = handler.GetUserUuid();
byte[] sign = playerKeyPair.PrivateKey.SignMessage(message, uuid, timeNow, ref salt); byte[] sign = (protocolversion >= MC_1_19_2_Version) ?
playerKeyPair.PrivateKey.SignMessage(message, uuid, timeNow, ref salt, acknowledgment!.lastSeen) :
playerKeyPair.PrivateKey.SignMessage(message, uuid, timeNow, ref salt);
fields.AddRange(dataTypes.GetVarInt(sign.Length)); fields.AddRange(dataTypes.GetVarInt(sign.Length));
fields.AddRange(sign); fields.AddRange(sign);
} }
@ -2016,8 +2194,11 @@ namespace MinecraftClient.Protocol.Handlers
// Signed Preview: Boolean // Signed Preview: Boolean
fields.AddRange(dataTypes.GetBool(false)); fields.AddRange(dataTypes.GetBool(false));
fields.AddRange(dataTypes.GetVarInt(0)); if (protocolversion >= MC_1_19_2_Version)
fields.AddRange(dataTypes.GetBool(false)); {
// Message Acknowledgment
fields.AddRange(dataTypes.GetAcknowledgment(acknowledgment!));
}
} }
SendPacket(PacketTypesOut.ChatMessage, fields); SendPacket(PacketTypesOut.ChatMessage, fields);
return true; return true;
@ -2098,9 +2279,12 @@ namespace MinecraftClient.Protocol.Handlers
List<byte> fields = new List<byte>(); List<byte> fields = new List<byte>();
fields.AddRange(dataTypes.GetString(language)); fields.AddRange(dataTypes.GetString(language));
fields.Add(viewDistance); fields.Add(viewDistance);
fields.AddRange(protocolversion >= MC_1_9_Version
? dataTypes.GetVarInt(chatMode) if (protocolversion >= MC_1_9_Version)
: new byte[] { chatMode }); fields.AddRange(dataTypes.GetVarInt(chatMode));
else
fields.AddRange(new byte[] { chatMode });
fields.Add(chatColors ? (byte)1 : (byte)0); fields.Add(chatColors ? (byte)1 : (byte)0);
if (protocolversion < MC_1_8_Version) if (protocolversion < MC_1_8_Version)
{ {

View file

@ -5,6 +5,7 @@ using System.Text;
using MinecraftClient.Mapping; using MinecraftClient.Mapping;
using MinecraftClient.Inventory; using MinecraftClient.Inventory;
using MinecraftClient.Logger; using MinecraftClient.Logger;
using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Protocol namespace MinecraftClient.Protocol
{ {
@ -22,7 +23,8 @@ namespace MinecraftClient.Protocol
int GetServerPort(); int GetServerPort();
string GetServerHost(); string GetServerHost();
string GetUsername(); string GetUsername();
string GetUserUUID(); Guid GetUserUuid();
string GetUserUuidStr();
string GetSessionID(); string GetSessionID();
string[] GetOnlinePlayers(); string[] GetOnlinePlayers();
Dictionary<string, string> GetOnlinePlayersWithUUID(); Dictionary<string, string> GetOnlinePlayersWithUUID();

View file

@ -4,7 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace MinecraftClient.Protocol namespace MinecraftClient.Protocol.Message
{ {
public class ChatMessage public class ChatMessage
{ {
@ -17,7 +17,7 @@ namespace MinecraftClient.Protocol
// 0: chat (chat box), 1: system message (chat box), 2: game info (above hotbar), 3: say command, // 0: chat (chat box), 1: system message (chat box), 2: game info (above hotbar), 3: say command,
// 4: msg command, 5: team msg command, 6: emote command, 7: tellraw command // 4: msg command, 5: team msg command, 6: emote command, 7: tellraw command
public readonly int chatType; public readonly int chatTypeId;
public readonly Guid senderUUID; public readonly Guid senderUUID;
@ -31,31 +31,44 @@ namespace MinecraftClient.Protocol
public readonly DateTime? timestamp; public readonly DateTime? timestamp;
public readonly byte[]? signature;
public readonly bool? isSignatureLegal; public readonly bool? isSignatureLegal;
public ChatMessage(string content, bool isJson, int chatType, Guid senderUUID, string? unsignedContent, string displayName, string? teamName, long timestamp, bool isSignatureLegal) public ChatMessage(string content, bool isJson, int chatType, Guid senderUUID, string? unsignedContent, string displayName, string? teamName, long timestamp, byte[] signature, bool isSignatureLegal)
{ {
this.isSignedChat = true; isSignedChat = true;
this.isSystemChat = false; isSystemChat = false;
this.content = content; this.content = content;
this.isJson = isJson; this.isJson = isJson;
this.chatType = chatType; this.chatTypeId = chatType;
this.senderUUID = senderUUID; this.senderUUID = senderUUID;
this.unsignedContent = unsignedContent; this.unsignedContent = unsignedContent;
this.displayName = displayName; this.displayName = displayName;
this.teamName = teamName; this.teamName = teamName;
this.timestamp = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).DateTime; this.timestamp = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).DateTime;
this.signature = signature;
this.isSignatureLegal = isSignatureLegal; this.isSignatureLegal = isSignatureLegal;
} }
public ChatMessage(string content, bool isJson, int chatType, Guid senderUUID, bool isSystemChat = false) public ChatMessage(string content, bool isJson, int chatType, Guid senderUUID, bool isSystemChat = false)
{ {
this.isSignedChat = isSystemChat; isSignedChat = isSystemChat;
this.isSystemChat = isSystemChat; this.isSystemChat = isSystemChat;
this.content = content; this.content = content;
this.isJson = isJson; this.isJson = isJson;
this.chatType = chatType; this.chatTypeId = chatType;
this.senderUUID = senderUUID; this.senderUUID = senderUUID;
} }
public LastSeenMessageList.Entry? toLastSeenMessageEntry()
{
return signature != null ? new LastSeenMessageList.Entry(senderUUID, signature) : null;
}
public bool lacksSender()
{
return this.senderUUID == Guid.Empty;
}
} }
} }

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Protocol namespace MinecraftClient.Protocol
{ {
@ -12,6 +13,20 @@ namespace MinecraftClient.Protocol
static class ChatParser static class ChatParser
{ {
public enum MessageType
{
CHAT,
SAY_COMMAND,
MSG_COMMAND_INCOMING,
MSG_COMMAND_OUTGOING,
TEAM_MSG_COMMAND_INCOMING,
TEAM_MSG_COMMAND_OUTGOING,
EMOTE_COMMAND,
RAW_MSG
};
public static Dictionary<int, MessageType>? ChatId2Type;
/// <summary> /// <summary>
/// The main function to convert text from MC 1.6+ JSON to MC 1.5.2 formatted text /// The main function to convert text from MC 1.6+ JSON to MC 1.5.2 formatted text
/// </summary> /// </summary>
@ -31,56 +46,64 @@ namespace MinecraftClient.Protocol
/// <returns>Returns the translated text</returns> /// <returns>Returns the translated text</returns>
public static string ParseSignedChat(ChatMessage message, List<string>? links = null) public static string ParseSignedChat(ChatMessage message, List<string>? links = null)
{ {
string content; string chatContent = Settings.ShowModifiedChat && message.unsignedContent != null ? message.unsignedContent : message.content;
if (Settings.ShowModifiedChat && message.unsignedContent != null) string content = message.isJson ? ParseText(chatContent, links) : chatContent;
content = ChatParser.ParseText(message.unsignedContent, links);
else
content = ChatParser.ParseText(message.content, links);
string sender = message.displayName!; string sender = message.displayName!;
string text; string text;
List<string> usingData = new(); List<string> usingData = new();
switch (message.chatType)
MessageType chatType;
if (message.isSystemChat)
chatType = MessageType.RAW_MSG;
else if (!ChatId2Type!.TryGetValue(message.chatTypeId, out chatType))
chatType = MessageType.CHAT;
switch (chatType)
{ {
case 0: // chat (chat box) case MessageType.CHAT:
usingData.Add(sender); usingData.Add(sender);
usingData.Add(content); usingData.Add(content);
text = TranslateString("chat.type.text", usingData); text = TranslateString("chat.type.text", usingData);
break; break;
case 1: // system message (chat box) case MessageType.SAY_COMMAND:
text = content;
break;
case 2: // game info (above hotbar)
text = content;
break;
case 3: // say command
usingData.Add(sender); usingData.Add(sender);
usingData.Add(content); usingData.Add(content);
text = TranslateString("chat.type.announcement", usingData); text = TranslateString("chat.type.announcement", usingData);
break; break;
case 4: // msg command case MessageType.MSG_COMMAND_INCOMING:
usingData.Add(sender); usingData.Add(sender);
usingData.Add(content); usingData.Add(content);
text = TranslateString("commands.message.display.incoming", usingData); text = TranslateString("commands.message.display.incoming", usingData);
break; break;
case 5: // team msg command (/teammsg) case MessageType.MSG_COMMAND_OUTGOING:
usingData.Add(sender);
usingData.Add(content);
text = TranslateString("commands.message.display.outgoing", usingData);
break;
case MessageType.TEAM_MSG_COMMAND_INCOMING:
usingData.Add(message.teamName!); usingData.Add(message.teamName!);
usingData.Add(sender); usingData.Add(sender);
usingData.Add(content); usingData.Add(content);
text = TranslateString("chat.type.team.text", usingData); text = TranslateString("chat.type.team.text", usingData);
break; break;
case 6: // emote command (/me) case MessageType.TEAM_MSG_COMMAND_OUTGOING:
usingData.Add(message.teamName!);
usingData.Add(sender);
usingData.Add(content);
text = TranslateString("chat.type.team.sent", usingData);
break;
case MessageType.EMOTE_COMMAND:
usingData.Add(sender); usingData.Add(sender);
usingData.Add(content); usingData.Add(content);
text = TranslateString("chat.type.emote", usingData); text = TranslateString("chat.type.emote", usingData);
break; break;
case 7: // tellraw command case MessageType.RAW_MSG:
text = content; text = content;
break; break;
default: default:
text = $"{sender}: {content}"; goto case MessageType.CHAT;
break;
} }
string color = String.Empty; string color = string.Empty;
if (message.isSystemChat) if (message.isSystemChat)
{ {
if (Settings.MarkSystemMessage) if (Settings.MarkSystemMessage)
@ -172,13 +195,13 @@ namespace MinecraftClient.Protocol
TranslationRules["commands.message.display.outgoing"] = "§7You whisper to %s: %s"; TranslationRules["commands.message.display.outgoing"] = "§7You whisper to %s: %s";
//Language file in a subfolder, depending on the language setting //Language file in a subfolder, depending on the language setting
if (!System.IO.Directory.Exists("lang")) if (!Directory.Exists("lang"))
System.IO.Directory.CreateDirectory("lang"); Directory.CreateDirectory("lang");
string Language_File = "lang" + Path.DirectorySeparatorChar + Settings.Language + ".lang"; string Language_File = "lang" + Path.DirectorySeparatorChar + Settings.Language + ".lang";
//File not found? Try downloading language file from Mojang's servers? //File not found? Try downloading language file from Mojang's servers?
if (!System.IO.File.Exists(Language_File)) if (!File.Exists(Language_File))
{ {
ConsoleIO.WriteLineFormatted(Translations.Get("chat.download", Settings.Language)); ConsoleIO.WriteLineFormatted(Translations.Get("chat.download", Settings.Language));
try try
@ -197,7 +220,7 @@ namespace MinecraftClient.Protocol
stringBuilder.Append(entry.Key + "=" + entry.Value.StringValue + Environment.NewLine); stringBuilder.Append(entry.Key + "=" + entry.Value.StringValue + Environment.NewLine);
} }
System.IO.File.WriteAllText(Language_File, stringBuilder.ToString()); File.WriteAllText(Language_File, stringBuilder.ToString());
ConsoleIO.WriteLineFormatted(Translations.Get("chat.done", Language_File)); ConsoleIO.WriteLineFormatted(Translations.Get("chat.done", Language_File));
} }
catch catch
@ -207,17 +230,17 @@ namespace MinecraftClient.Protocol
} }
//Download Failed? Defaulting to en_GB.lang if the game is installed //Download Failed? Defaulting to en_GB.lang if the game is installed
if (!System.IO.File.Exists(Language_File) //Try en_GB.lang if (!File.Exists(Language_File) //Try en_GB.lang
&& System.IO.File.Exists(Settings.TranslationsFile_FromMCDir)) && File.Exists(Settings.TranslationsFile_FromMCDir))
{ {
Language_File = Settings.TranslationsFile_FromMCDir; Language_File = Settings.TranslationsFile_FromMCDir;
Translations.WriteLineFormatted("chat.from_dir"); Translations.WriteLineFormatted("chat.from_dir");
} }
//Load the external dictionnary of translation rules or display an error message //Load the external dictionnary of translation rules or display an error message
if (System.IO.File.Exists(Language_File)) if (File.Exists(Language_File))
{ {
string[] translations = System.IO.File.ReadAllLines(Language_File); string[] translations = File.ReadAllLines(Language_File);
foreach (string line in translations) foreach (string line in translations)
{ {
if (line.Length > 0) if (line.Length > 0)
@ -289,7 +312,7 @@ namespace MinecraftClient.Protocol
} }
return result.ToString(); return result.ToString();
} }
else return "[" + rulename + "] " + String.Join(" ", using_data); else return "[" + rulename + "] " + string.Join(" ", using_data);
} }
/// <summary> /// <summary>
@ -315,7 +338,7 @@ namespace MinecraftClient.Protocol
if (clickEvent.Properties.ContainsKey("action") if (clickEvent.Properties.ContainsKey("action")
&& clickEvent.Properties.ContainsKey("value") && clickEvent.Properties.ContainsKey("value")
&& clickEvent.Properties["action"].StringValue == "open_url" && clickEvent.Properties["action"].StringValue == "open_url"
&& !String.IsNullOrEmpty(clickEvent.Properties["value"].StringValue)) && !string.IsNullOrEmpty(clickEvent.Properties["value"].StringValue))
{ {
links.Add(clickEvent.Properties["value"].StringValue); links.Add(clickEvent.Properties["value"].StringValue);
} }
@ -372,7 +395,7 @@ namespace MinecraftClient.Protocol
System.Net.HttpWebRequest myRequest = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(url); System.Net.HttpWebRequest myRequest = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(url);
myRequest.Method = "GET"; myRequest.Method = "GET";
System.Net.WebResponse myResponse = myRequest.GetResponse(); System.Net.WebResponse myResponse = myRequest.GetResponse();
System.IO.StreamReader sr = new System.IO.StreamReader(myResponse.GetResponseStream(), System.Text.Encoding.UTF8); StreamReader sr = new StreamReader(myResponse.GetResponseStream(), Encoding.UTF8);
string result = sr.ReadToEnd(); string result = sr.ReadToEnd();
sr.Close(); sr.Close();
myResponse.Close(); myResponse.Close();

View file

@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MinecraftClient.Protocol.Message
{
/// <summary>
/// A list of messages a client has seen.
/// </summary>
public class LastSeenMessageList
{
public static readonly LastSeenMessageList EMPTY = new(new Entry[0]);
public static readonly int MAX_ENTRIES = 5;
public Entry[] entries;
public LastSeenMessageList(Entry[] list)
{
entries = list;
}
public void WriteForSign(List<byte> data)
{
foreach (Entry entry in entries)
{
data.Add(70);
data.AddRange(entry.profileId.ToBigEndianBytes());
data.AddRange(entry.lastSignature);
}
}
/// <summary>
/// A pair of a player's UUID and the signature of the last message they saw, used as an entry of LastSeenMessageList.
/// </summary>
public class Entry
{
public Guid profileId;
public byte[] lastSignature;
public Entry(Guid profileId, byte[] lastSignature)
{
this.profileId = profileId;
this.lastSignature = lastSignature;
}
}
/// <summary>
/// A record of messages acknowledged by a client.
/// This holds the messages the client has recently seen, as well as the last message they received, if any.
/// </summary>
public class Acknowledgment
{
public LastSeenMessageList lastSeen;
public Entry? lastReceived;
public Acknowledgment(LastSeenMessageList lastSeenMessageList, Entry? lastReceivedMessage)
{
lastSeen = lastSeenMessageList;
lastReceived = lastReceivedMessage;
}
}
}
/// <summary>
/// Collects the message that are last seen by a client.
/// The message, along with the "last received" message, forms an "acknowledgment" of received messages.
/// They are sent to the server when the client has enough messages received or when they send a message.
/// The maximum amount of message entries are specified in the constructor.The vanilla clients collect 5 entries.
/// Calling add adds the message to the beginning of the entries list, and evicts the oldest message.
/// If there are entries with the same sender profile ID, the older entry will be replaced with null instead of filling the hole.
/// </summary>
public class LastSeenMessagesCollector
{
private readonly LastSeenMessageList.Entry[] entries;
private int size = 0;
private LastSeenMessageList lastSeenMessages;
public LastSeenMessagesCollector(int size)
{
lastSeenMessages = LastSeenMessageList.EMPTY;
entries = new LastSeenMessageList.Entry[size];
}
public void Add(LastSeenMessageList.Entry entry)
{
LastSeenMessageList.Entry? lastEntry = entry;
for (int i = 0; i < size; ++i)
{
LastSeenMessageList.Entry curEntry = entries[i];
entries[i] = lastEntry;
lastEntry = curEntry;
if (curEntry.profileId == entry.profileId)
{
lastEntry = null;
break;
}
}
if (lastEntry != null && size < entries.Length)
entries[size++] = lastEntry;
LastSeenMessageList.Entry[] msgList = new LastSeenMessageList.Entry[size];
for (int i = 0; i < size; ++i)
msgList[i] = entries[i];
lastSeenMessages = new LastSeenMessageList(msgList);
}
public LastSeenMessageList GetLastSeenMessages()
{
return lastSeenMessages;
}
}
}

View file

@ -4,12 +4,13 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using MinecraftClient.Protocol.Keys; using MinecraftClient.Protocol.Keys;
using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Protocol namespace MinecraftClient.Protocol
{ {
public class PlayerInfo public class PlayerInfo
{ {
public readonly Guid UUID; public readonly Guid Uuid;
public readonly string Name; public readonly string Name;
@ -26,9 +27,13 @@ namespace MinecraftClient.Protocol
private readonly DateTime? KeyExpiresAt; private readonly DateTime? KeyExpiresAt;
private bool lastMessageVerified;
private byte[]? precedingSignature;
public PlayerInfo(Guid uuid, string name, Tuple<string, string, string>[]? property, int gamemode, int ping, string? displayName, long? timeStamp, byte[]? publicKey, byte[]? signature) public PlayerInfo(Guid uuid, string name, Tuple<string, string, string>[]? property, int gamemode, int ping, string? displayName, long? timeStamp, byte[]? publicKey, byte[]? signature)
{ {
UUID = uuid; Uuid = uuid;
Name = name; Name = name;
if (property != null) if (property != null)
Property = property; Property = property;
@ -48,36 +53,107 @@ namespace MinecraftClient.Protocol
PublicKey = null; PublicKey = null;
} }
} }
lastMessageVerified = true;
precedingSignature = null;
} }
public PlayerInfo(string name, Guid uuid) public PlayerInfo(string name, Guid uuid)
{ {
Name = name; Name = name;
UUID = uuid; Uuid = uuid;
Gamemode = -1; Gamemode = -1;
Ping = 0; Ping = 0;
lastMessageVerified = true;
precedingSignature = null;
} }
public bool IsKeyVaild() public bool IsMessageChainLegal()
{ {
return PublicKey != null && DateTime.Now.ToUniversalTime() > this.KeyExpiresAt; return this.lastMessageVerified;
} }
public bool VerifyMessage(string message, Guid uuid, long timestamp, long salt, ref byte[] signature) public bool IsKeyExpired()
{ {
if (PublicKey == null) return DateTime.Now.ToUniversalTime() > this.KeyExpiresAt;
}
/// <summary>
/// Verify message - 1.19
/// </summary>
/// <param name="message">Message content</param>
/// <param name="timestamp">Timestamp</param>
/// <param name="salt">Salt</param>
/// <param name="signature">Message signature</param>
/// <returns>Is this message vaild</returns>
public bool VerifyMessage(string message, long timestamp, long salt, ref byte[] signature)
{
if (PublicKey == null || IsKeyExpired())
return false; return false;
else else
{ {
string uuidString = uuid.ToString().Replace("-", string.Empty); DateTimeOffset timeOffset = DateTimeOffset.FromUnixTimeMilliseconds(timestamp);
byte[] saltByte = BitConverter.GetBytes(salt);
Array.Reverse(saltByte);
return PublicKey.VerifyMessage(message, Uuid, timeOffset, ref saltByte, ref signature);
}
}
/// <summary>
/// Verify message - 1.19.1 and above
/// </summary>
/// <param name="message">Message content</param>
/// <param name="timestamp">Timestamp</param>
/// <param name="salt">Salt</param>
/// <param name="signature">Message signature</param>
/// <param name="precedingSignature">Preceding message signature</param>
/// <param name="lastSeenMessages">LastSeenMessages</param>
/// <returns>Is this message chain vaild</returns>
public bool VerifyMessage(string message, long timestamp, long salt, ref byte[] signature, ref byte[]? precedingSignature, LastSeenMessageList lastSeenMessages)
{
if (this.lastMessageVerified == false)
return false;
if (PublicKey == null || IsKeyExpired() || (this.precedingSignature != null && precedingSignature == null))
return false;
if (this.precedingSignature != null && !this.precedingSignature.SequenceEqual(precedingSignature!))
return false;
DateTimeOffset timeOffset = DateTimeOffset.FromUnixTimeMilliseconds(timestamp); DateTimeOffset timeOffset = DateTimeOffset.FromUnixTimeMilliseconds(timestamp);
byte[] saltByte = BitConverter.GetBytes(salt); byte[] saltByte = BitConverter.GetBytes(salt);
Array.Reverse(saltByte); Array.Reverse(saltByte);
return PublicKey.VerifyMessage(message, uuidString, timeOffset, ref saltByte, ref signature); bool res = PublicKey.VerifyMessage(message, Uuid, timeOffset, ref saltByte, ref signature, ref precedingSignature, lastSeenMessages);
}
this.lastMessageVerified = res;
this.precedingSignature = signature;
return res;
}
/// <summary>
/// Verify message head - 1.19.1 and above
/// </summary>
/// <param name="precedingSignature">Preceding message signature</param>
/// <param name="headerSignature">Message signature</param>
/// <param name="bodyDigest">Message body hash</param>
/// <returns>Is this message chain vaild</returns>
public bool VerifyMessageHead(ref byte[]? precedingSignature, ref byte[] headerSignature, ref byte[] bodyDigest)
{
if (this.lastMessageVerified == false)
return false;
if (PublicKey == null || IsKeyExpired() || (this.precedingSignature != null && precedingSignature == null))
return false;
if (this.precedingSignature != null && !this.precedingSignature.SequenceEqual(precedingSignature!))
return false;
bool res = PublicKey.VerifyHeader(ref bodyDigest, ref headerSignature);
this.lastMessageVerified = res;
this.precedingSignature = headerSignature;
return res;
} }
} }
} }

View file

@ -1,14 +1,19 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using MinecraftClient.Protocol.Handlers;
using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Protocol.Keys namespace MinecraftClient.Protocol.Keys
{ {
static class KeyUtils static class KeyUtils
{ {
private static string certificates = "https://api.minecraftservices.com/player/certificates"; private static readonly SHA256 sha256Hash = SHA256.Create();
private static readonly string certificates = "https://api.minecraftservices.com/player/certificates";
public static PlayerKeyPair? GetKeys(string accessToken) public static PlayerKeyPair? GetKeys(string accessToken)
{ {
@ -67,19 +72,18 @@ namespace MinecraftClient.Protocol.Keys
return Convert.FromBase64String(key); return Convert.FromBase64String(key);
} }
public static byte[] GetSignatureData(string message, string uuid, DateTimeOffset timestamp, ref byte[] salt) public static byte[] ComputeHash(byte[] data)
{
return sha256Hash.ComputeHash(data);
}
public static byte[] GetSignatureData(string message, Guid uuid, DateTimeOffset timestamp, ref byte[] salt)
{ {
List<byte> data = new(); List<byte> data = new();
data.AddRange(salt); data.AddRange(salt);
byte[] UUIDLeastSignificantBits = BitConverter.GetBytes(Convert.ToInt64(uuid[..16], 16)); data.AddRange(uuid.ToBigEndianBytes());
Array.Reverse(UUIDLeastSignificantBits);
data.AddRange(UUIDLeastSignificantBits);
byte[] UUIDMostSignificantBits = BitConverter.GetBytes(Convert.ToInt64(uuid.Substring(16, 16), 16));
Array.Reverse(UUIDMostSignificantBits);
data.AddRange(UUIDMostSignificantBits);
byte[] timestampByte = BitConverter.GetBytes(timestamp.ToUnixTimeSeconds()); byte[] timestampByte = BitConverter.GetBytes(timestamp.ToUnixTimeSeconds());
Array.Reverse(timestampByte); Array.Reverse(timestampByte);
@ -90,6 +94,39 @@ namespace MinecraftClient.Protocol.Keys
return data.ToArray(); return data.ToArray();
} }
public static byte[] GetSignatureData(string message, DateTimeOffset timestamp, ref byte[] salt, LastSeenMessageList lastSeenMessages)
{
List<byte> data = new();
data.AddRange(salt);
byte[] timestampByte = BitConverter.GetBytes(timestamp.ToUnixTimeSeconds());
Array.Reverse(timestampByte);
data.AddRange(timestampByte);
data.AddRange(Encoding.UTF8.GetBytes(message));
data.Add(70);
lastSeenMessages.WriteForSign(data);
return data.ToArray();
}
public static byte[] GetSignatureData(byte[]? precedingSignature, Guid sender, byte[] bodySign)
{
List<byte> data = new();
if (precedingSignature != null)
data.AddRange(precedingSignature);
data.AddRange(sender.ToBigEndianBytes());
data.AddRange(bodySign);
return data.ToArray();
}
// https://github.com/mono/mono/blob/master/mcs/class/System.Json/System.Json/JsonValue.cs // https://github.com/mono/mono/blob/master/mcs/class/System.Json/System.Json/JsonValue.cs
public static string EscapeString(string src) public static string EscapeString(string src)
{ {

View file

@ -4,6 +4,7 @@ using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Protocol.Keys namespace MinecraftClient.Protocol.Keys
{ {
@ -13,6 +14,8 @@ namespace MinecraftClient.Protocol.Keys
private readonly RSA rsa; private readonly RSA rsa;
private byte[]? precedingSignature = null;
public PrivateKey(string pemKey) public PrivateKey(string pemKey)
{ {
this.Key = KeyUtils.DecodePemKey(pemKey, "-----BEGIN RSA PRIVATE KEY-----", "-----END RSA PRIVATE KEY-----"); this.Key = KeyUtils.DecodePemKey(pemKey, "-----BEGIN RSA PRIVATE KEY-----", "-----END RSA PRIVATE KEY-----");
@ -26,7 +29,15 @@ namespace MinecraftClient.Protocol.Keys
return rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); return rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
} }
public byte[] SignMessage(string message, string uuid, DateTimeOffset timestamp, ref byte[] salt) /// <summary>
/// Sign message - 1.19
/// </summary>
/// <param name="message">Message content</param>
/// <param name="uuid">Sender uuid</param>
/// <param name="timestamp">Timestamp</param>
/// <param name="salt">Salt</param>
/// <returns>Signature data</returns>
public byte[] SignMessage(string message, Guid uuid, DateTimeOffset timestamp, ref byte[] salt)
{ {
string messageJson = "{\"text\":\"" + KeyUtils.EscapeString(message) + "\"}"; string messageJson = "{\"text\":\"" + KeyUtils.EscapeString(message) + "\"}";
@ -35,5 +46,27 @@ namespace MinecraftClient.Protocol.Keys
return SignData(data); return SignData(data);
} }
/// <summary>
/// Sign message - 1.19.1 and above
/// </summary>
/// <param name="message">Message content</param>
/// <param name="uuid">Sender uuid</param>
/// <param name="timestamp">Timestamp</param>
/// <param name="salt">Salt</param>
/// <param name="lastSeenMessages">LastSeenMessageList</param>
/// <returns>Signature data</returns>
public byte[] SignMessage(string message, Guid uuid, DateTimeOffset timestamp, ref byte[] salt, LastSeenMessageList lastSeenMessages)
{
byte[] bodySignData = KeyUtils.GetSignatureData(message, timestamp, ref salt, lastSeenMessages);
byte[] bodyDigest = KeyUtils.ComputeHash(bodySignData);
byte[] msgSignData = KeyUtils.GetSignatureData(precedingSignature, uuid, bodyDigest);
byte[] msgSign = SignData(msgSignData);
this.precedingSignature = msgSign;
return msgSign;
}
} }
} }

View file

@ -4,6 +4,7 @@ using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Protocol.Keys namespace MinecraftClient.Protocol.Keys
{ {
@ -44,12 +45,53 @@ namespace MinecraftClient.Protocol.Keys
return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
} }
public bool VerifyMessage(string message, string uuid, DateTimeOffset timestamp, ref byte[] salt, ref byte[] signature) /// <summary>
/// Verify message - 1.19
/// </summary>
/// <param name="message">Message content</param>
/// <param name="uuid">Sender uuid</param>
/// <param name="timestamp">Timestamp</param>
/// <param name="salt">Salt</param>
/// <param name="signature">Message signature</param>
/// <returns>Is this message vaild</returns>
public bool VerifyMessage(string message, Guid uuid, DateTimeOffset timestamp, ref byte[] salt, ref byte[] signature)
{ {
byte[] data = KeyUtils.GetSignatureData(message, uuid, timestamp, ref salt); byte[] data = KeyUtils.GetSignatureData(message, uuid, timestamp, ref salt);
return VerifyData(data, signature); return VerifyData(data, signature);
} }
/// <summary>
/// Verify message - 1.19.1 and above
/// </summary>
/// <param name="message">Message content</param>
/// <param name="uuid">Sender uuid</param>
/// <param name="timestamp">Timestamp</param>
/// <param name="salt">Salt</param>
/// <param name="signature">Message signature</param>
/// <param name="precedingSignature">Preceding message signature</param>
/// <param name="lastSeenMessages">LastSeenMessages</param>
/// <returns>Is this message vaild</returns>
public bool VerifyMessage(string message, Guid uuid, DateTimeOffset timestamp, ref byte[] salt, ref byte[] signature, ref byte[]? precedingSignature, LastSeenMessageList lastSeenMessages)
{
byte[] bodySignData = KeyUtils.GetSignatureData(message, timestamp, ref salt, lastSeenMessages);
byte[] bodyDigest = KeyUtils.ComputeHash(bodySignData);
byte[] msgSignData = KeyUtils.GetSignatureData(precedingSignature, uuid, bodyDigest);
return VerifyData(msgSignData, signature);
}
/// <summary>
/// Verify message head - 1.19.1 and above
/// </summary>
/// <param name="bodyDigest">Message body hash</param>
/// <param name="signature">Message signature</param>
/// <returns>Is this message header vaild</returns>
public bool VerifyHeader(ref byte[] bodyDigest, ref byte[] signature)
{
return VerifyData(bodyDigest, signature);
}
} }
} }

View file

@ -1110,7 +1110,7 @@ namespace MinecraftClient
/// <returns>UserUUID of the current account</returns> /// <returns>UserUUID of the current account</returns>
protected string GetUserUUID() protected string GetUserUUID()
{ {
return Handler.GetUserUUID(); return Handler.GetUserUuidStr();
} }
/// <summary> /// <summary>