Mirror von
https://github.com/ViaVersion/ViaVersion.git
synchronisiert 2024-12-27 00:22:51 +01:00
Fix: delay chat acknowledgements instead of spoofing (#3997)
An easy reproduction case is to mute a player using the Social Interactions screen, let them chat, and then send a message. As ViaVersion produces its own acknowledgements, this now does not match with what the client produced and signed its messages with - so the signature will fail verification. As an alternative, we *always* pass through the same acknowledgement bit set - and for 'spoofed' messages, we reuse the last one that we received. In the case of the chat ack packet with just an offset, we need to hold back the window by at least 20 messages to ensure we don't start making claims about whether the client saw a message that ViaVersion cannot yet make a judgement on.
Dieser Commit ist enthalten in:
Ursprung
e6da77cf47
Commit
17358120cd
@ -56,6 +56,7 @@ import com.viaversion.viaversion.rewriter.SoundRewriter;
|
||||
import com.viaversion.viaversion.rewriter.StatisticsRewriter;
|
||||
import com.viaversion.viaversion.rewriter.TagRewriter;
|
||||
import com.viaversion.viaversion.util.ProtocolLogger;
|
||||
import java.util.BitSet;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.viaversion.viaversion.util.ProtocolUtil.packetTypeMap;
|
||||
@ -124,26 +125,6 @@ public final class Protocol1_20_3To1_20_5 extends AbstractProtocol<ClientboundPa
|
||||
}
|
||||
});
|
||||
|
||||
// Big problem with this update: Without access to the client, this cannot 100% predict the
|
||||
// correct offset. This means we have to entirely discard client acknowledgements and fake them.
|
||||
registerClientbound(ClientboundPackets1_20_3.PLAYER_CHAT, wrapper -> {
|
||||
wrapper.passthrough(Types.UUID); // Sender
|
||||
wrapper.passthrough(Types.VAR_INT); // Index
|
||||
final byte[] signature = wrapper.passthrough(Types.OPTIONAL_SIGNATURE_BYTES);
|
||||
if (signature == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mimic client behavior for acknowledgements
|
||||
final AcknowledgedMessagesStorage storage = wrapper.user().get(AcknowledgedMessagesStorage.class);
|
||||
if (storage.add(signature) && storage.offset() > 64) {
|
||||
final PacketWrapper chatAck = wrapper.create(ServerboundPackets1_20_3.CHAT_ACK);
|
||||
chatAck.write(Types.VAR_INT, storage.offset());
|
||||
chatAck.sendToServer(Protocol1_20_3To1_20_5.class);
|
||||
|
||||
storage.clearOffset();
|
||||
}
|
||||
});
|
||||
registerServerbound(ServerboundPackets1_20_5.CHAT, wrapper -> {
|
||||
wrapper.passthrough(Types.STRING); // Message
|
||||
wrapper.passthrough(Types.LONG); // Timestamp
|
||||
@ -161,7 +142,7 @@ public final class Protocol1_20_3To1_20_5 extends AbstractProtocol<ClientboundPa
|
||||
wrapper.write(Types.OPTIONAL_SIGNATURE_BYTES, null);
|
||||
}
|
||||
|
||||
replaceChatAck(wrapper, storage);
|
||||
fixChatAck(wrapper, storage);
|
||||
});
|
||||
registerServerbound(ServerboundPackets1_20_5.CHAT_COMMAND_SIGNED, ServerboundPackets1_20_3.CHAT_COMMAND, wrapper -> {
|
||||
wrapper.passthrough(Types.STRING); // Command
|
||||
@ -188,7 +169,16 @@ public final class Protocol1_20_3To1_20_5 extends AbstractProtocol<ClientboundPa
|
||||
}
|
||||
}
|
||||
|
||||
replaceChatAck(wrapper, storage);
|
||||
fixChatAck(wrapper, storage);
|
||||
});
|
||||
registerServerbound(ServerboundPackets1_20_5.CHAT_ACK, wrapper -> {
|
||||
final int offset = wrapper.read(Types.VAR_INT);
|
||||
final int fixedOffset = wrapper.user().get(AcknowledgedMessagesStorage.class).accumulateAckCount(offset);
|
||||
if (fixedOffset > 0) {
|
||||
wrapper.write(Types.VAR_INT, fixedOffset);
|
||||
} else {
|
||||
wrapper.cancel();
|
||||
}
|
||||
});
|
||||
registerServerbound(ServerboundPackets1_20_5.CHAT_COMMAND, wrapper -> {
|
||||
wrapper.passthrough(Types.STRING); // Command
|
||||
@ -197,7 +187,7 @@ public final class Protocol1_20_3To1_20_5 extends AbstractProtocol<ClientboundPa
|
||||
wrapper.write(Types.LONG, 0L); // Salt
|
||||
wrapper.write(Types.VAR_INT, 0); // No signatures
|
||||
|
||||
writeChatAck(wrapper, wrapper.user().get(AcknowledgedMessagesStorage.class));
|
||||
writeSpoofedChatAck(wrapper, wrapper.user().get(AcknowledgedMessagesStorage.class));
|
||||
});
|
||||
registerServerbound(ServerboundPackets1_20_5.CHAT_SESSION_UPDATE, wrapper -> {
|
||||
// Delay this until we know whether the server enforces secure chat
|
||||
@ -214,7 +204,6 @@ public final class Protocol1_20_3To1_20_5 extends AbstractProtocol<ClientboundPa
|
||||
|
||||
wrapper.cancel();
|
||||
});
|
||||
cancelServerbound(ServerboundPackets1_20_5.CHAT_ACK);
|
||||
|
||||
registerClientbound(ClientboundPackets1_20_3.START_CONFIGURATION, wrapper -> wrapper.user().put(new AcknowledgedMessagesStorage()));
|
||||
|
||||
@ -241,16 +230,19 @@ public final class Protocol1_20_3To1_20_5 extends AbstractProtocol<ClientboundPa
|
||||
cancelServerbound(ServerboundPackets1_20_5.DEBUG_SAMPLE_SUBSCRIPTION);
|
||||
}
|
||||
|
||||
private void replaceChatAck(final PacketWrapper wrapper, final AcknowledgedMessagesStorage storage) {
|
||||
wrapper.read(Types.VAR_INT); // Offset
|
||||
wrapper.read(Types.ACKNOWLEDGED_BIT_SET); // Acknowledged
|
||||
writeChatAck(wrapper, storage);
|
||||
private void fixChatAck(final PacketWrapper wrapper, final AcknowledgedMessagesStorage storage) {
|
||||
final int offset = wrapper.read(Types.VAR_INT);
|
||||
final BitSet acknowledged = wrapper.read(Types.ACKNOWLEDGED_BIT_SET);
|
||||
final int fixedOffset = storage.updateFromMessage(offset, acknowledged);
|
||||
wrapper.write(Types.VAR_INT, fixedOffset);
|
||||
// Never change this, as this message (and future ones) are signed with it
|
||||
wrapper.write(Types.ACKNOWLEDGED_BIT_SET, acknowledged);
|
||||
}
|
||||
|
||||
private void writeChatAck(final PacketWrapper wrapper, final AcknowledgedMessagesStorage storage) {
|
||||
wrapper.write(Types.VAR_INT, storage.offset());
|
||||
wrapper.write(Types.ACKNOWLEDGED_BIT_SET, storage.toAck());
|
||||
storage.clearOffset();
|
||||
private void writeSpoofedChatAck(final PacketWrapper wrapper, final AcknowledgedMessagesStorage storage) {
|
||||
// As we don't have the new state from the client, replay what we last received
|
||||
wrapper.write(Types.VAR_INT, 0); // Offset
|
||||
wrapper.write(Types.ACKNOWLEDGED_BIT_SET, storage.createSpoofedAck()); // Acknowledged
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -23,46 +23,59 @@ import com.viaversion.viaversion.api.protocol.packet.PacketWrapper;
|
||||
import com.viaversion.viaversion.api.type.Types;
|
||||
import com.viaversion.viaversion.protocols.v1_20_2to1_20_3.packet.ServerboundPackets1_20_3;
|
||||
import com.viaversion.viaversion.protocols.v1_20_3to1_20_5.Protocol1_20_3To1_20_5;
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.UUID;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
/**
|
||||
* Tracks the last Secure Chat state that we received from the client. This is important to always have a valid 'last
|
||||
* seen' state that is consistent with future and past updates from the client (which may be signed). This state is
|
||||
* used to construct signed command packets to the outdated server from unsigned ones from the modern client.
|
||||
* <ul>
|
||||
* <li>If we last forwarded a chat or command packet from the client, we have a known 'last seen' that we can
|
||||
* reuse.</li>
|
||||
* <li>If we last forwarded a chat acknowledgement packet, the previous 'last seen' cannot be reused. We
|
||||
* cannot predict an up-to-date 'last seen', as we do not know which messages the client actually saw.</li>
|
||||
* <li>Therefore, we need to hold back any acknowledgement packets so that we can continue to reuse the last valid
|
||||
* 'last seen' state.</li>
|
||||
* <li>However, there is a limit to the number of messages that can remain unacknowledged on the server.</li>
|
||||
* <li>To address this, we know that if the client has moved its 'last seen' window far enough, we can fill in the
|
||||
* gap with dummy 'last seen', and it will never be checked.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class AcknowledgedMessagesStorage implements StorableObject {
|
||||
private static final int MAX_HISTORY = 20;
|
||||
private final boolean[] trackedMessages = new boolean[MAX_HISTORY];
|
||||
private static final int MINIMUM_DELAYED_ACK_COUNT = MAX_HISTORY;
|
||||
private static final BitSet DUMMY_LAST_SEEN_MESSAGES = new BitSet();
|
||||
|
||||
private Boolean secureChatEnforced;
|
||||
private ChatSession chatSession;
|
||||
private int offset;
|
||||
private int tail;
|
||||
private byte[] lastMessage;
|
||||
|
||||
public boolean add(final byte[] message) {
|
||||
if (Arrays.equals(message, lastMessage)) {
|
||||
return false;
|
||||
private BitSet lastSeenMessages = new BitSet();
|
||||
private int delayedAckCount;
|
||||
|
||||
public int updateFromMessage(int ackCount, BitSet lastSeenMessages) {
|
||||
// We held back some acknowledged messages, so flush that out now that we have a known 'last seen' state again
|
||||
int delayedAckCount = this.delayedAckCount;
|
||||
this.delayedAckCount = 0;
|
||||
this.lastSeenMessages = lastSeenMessages;
|
||||
return ackCount + delayedAckCount;
|
||||
}
|
||||
|
||||
public int accumulateAckCount(int ackCount) {
|
||||
delayedAckCount += ackCount;
|
||||
int ackCountToForward = delayedAckCount - MINIMUM_DELAYED_ACK_COUNT;
|
||||
if (ackCountToForward >= MAX_HISTORY) {
|
||||
// Because we only forward acknowledgements above the window size, we don't have to shift the previous 'last seen' state
|
||||
lastSeenMessages = DUMMY_LAST_SEEN_MESSAGES;
|
||||
delayedAckCount = MINIMUM_DELAYED_ACK_COUNT;
|
||||
return ackCountToForward;
|
||||
}
|
||||
this.lastMessage = message;
|
||||
this.offset++;
|
||||
this.trackedMessages[this.tail] = true;
|
||||
this.tail = (this.tail + 1) % MAX_HISTORY;
|
||||
return true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public BitSet toAck() {
|
||||
final BitSet acks = new BitSet(MAX_HISTORY);
|
||||
for (int i = 0; i < MAX_HISTORY; i++) {
|
||||
final int messageIndex = (this.tail + i) % MAX_HISTORY;
|
||||
acks.set(i, this.trackedMessages[messageIndex]);
|
||||
}
|
||||
return acks;
|
||||
}
|
||||
|
||||
public int offset() {
|
||||
return this.offset;
|
||||
}
|
||||
|
||||
public void clearOffset() {
|
||||
this.offset = 0;
|
||||
public BitSet createSpoofedAck() {
|
||||
return lastSeenMessages;
|
||||
}
|
||||
|
||||
public void setSecureChatEnforced(final boolean secureChatEnforced) {
|
||||
@ -98,10 +111,8 @@ public final class AcknowledgedMessagesStorage implements StorableObject {
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.offset = 0;
|
||||
this.tail = 0;
|
||||
this.lastMessage = null;
|
||||
Arrays.fill(this.trackedMessages, false);
|
||||
lastSeenMessages = new BitSet();
|
||||
delayedAckCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren