first commit
Build AsminVoicechat / prepare-release (push) Successful in 14s
Build AsminVoicechat / build-and-test (17) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (18) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (19) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (20) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (21) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (22) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (23) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (24) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (25) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (26) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (27-ea) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (16) (push) Canceled after 38s
Build AsminVoicechat / prepare-release (push) Successful in 14s
Build AsminVoicechat / build-and-test (17) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (18) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (19) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (20) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (21) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (22) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (23) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (24) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (25) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (26) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (27-ea) (push) Canceled after 0s
Build AsminVoicechat / build-and-test (16) (push) Canceled after 38s
This commit is contained in:
commit
bbccd4a815
454 files changed
+45050
No files matched your search
@@ -0,0 +1,137 @@
|
||||
package de.maxhenkel.voicechat.voice.common;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.Key;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class Secret {
|
||||
public static final int SECRET_SIZE_BYTES = 16;
|
||||
public static final int IV_SIZE_BYTES = 12;
|
||||
public static final int TAG_LEN_BITS = 128;
|
||||
public static final String CIPHER = "AES/GCM/NoPadding";
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
private final byte[] secret;
|
||||
private final SecretKeySpec keySpec;
|
||||
private volatile boolean legacyProtocol;
|
||||
|
||||
protected Secret(byte[] secret) {
|
||||
if (secret == null || secret.length != SECRET_SIZE_BYTES) {
|
||||
throw new IllegalArgumentException("Voice chat secret must contain exactly 16 bytes");
|
||||
}
|
||||
this.secret = secret.clone();
|
||||
this.keySpec = new SecretKeySpec(this.secret, "AES");
|
||||
}
|
||||
|
||||
public static Secret generateNewRandomSecret() {
|
||||
byte[] secret = new byte[SECRET_SIZE_BYTES];
|
||||
RANDOM.nextBytes(secret);
|
||||
return new Secret(secret);
|
||||
}
|
||||
|
||||
public static Secret fromBytes(byte[] secret) {
|
||||
return new Secret(secret);
|
||||
}
|
||||
|
||||
public static Secret fromBytes(ByteBuf buf) {
|
||||
byte[] secretBytes = new byte[SECRET_SIZE_BYTES];
|
||||
buf.readBytes(secretBytes);
|
||||
return Secret.fromBytes(secretBytes);
|
||||
}
|
||||
|
||||
public void toBytes(ByteBuf buf) {
|
||||
buf.writeBytes(this.secret);
|
||||
}
|
||||
|
||||
public byte[] getSecret() {
|
||||
return this.secret.clone();
|
||||
}
|
||||
|
||||
public SecretKeySpec getKeySpec() {
|
||||
return this.keySpec;
|
||||
}
|
||||
|
||||
public void setLegacyProtocol(boolean legacyProtocol) {
|
||||
this.legacyProtocol = legacyProtocol;
|
||||
}
|
||||
|
||||
public boolean isLegacyProtocol() {
|
||||
return this.legacyProtocol;
|
||||
}
|
||||
|
||||
public static byte[] generateIV() {
|
||||
byte[] iv = new byte[IV_SIZE_BYTES];
|
||||
RANDOM.nextBytes(iv);
|
||||
return iv;
|
||||
}
|
||||
|
||||
private static byte[] generateLegacyIV() {
|
||||
byte[] iv = new byte[16];
|
||||
RANDOM.nextBytes(iv);
|
||||
return iv;
|
||||
}
|
||||
|
||||
public byte[] encrypt(byte[] data) throws NoSuchPaddingException, NoSuchAlgorithmException,
|
||||
InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
|
||||
if (this.legacyProtocol) {
|
||||
byte[] iv = Secret.generateLegacyIV();
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, (Key) this.keySpec, new IvParameterSpec(iv));
|
||||
byte[] encrypted = cipher.doFinal(data);
|
||||
byte[] payload = new byte[iv.length + encrypted.length];
|
||||
System.arraycopy(iv, 0, payload, 0, iv.length);
|
||||
System.arraycopy(encrypted, 0, payload, iv.length, encrypted.length);
|
||||
return payload;
|
||||
}
|
||||
byte[] iv = Secret.generateIV();
|
||||
Cipher cipher = Cipher.getInstance(CIPHER);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, (Key) this.keySpec, new GCMParameterSpec(TAG_LEN_BITS, iv));
|
||||
byte[] encrypted = cipher.doFinal(data);
|
||||
byte[] payload = new byte[iv.length + encrypted.length];
|
||||
System.arraycopy(iv, 0, payload, 0, iv.length);
|
||||
System.arraycopy(encrypted, 0, payload, iv.length, encrypted.length);
|
||||
return payload;
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] payload) throws NoSuchPaddingException, NoSuchAlgorithmException,
|
||||
InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
|
||||
if (this.legacyProtocol) {
|
||||
if (payload.length < 32 || (payload.length - 16) % 16 != 0) {
|
||||
throw new IllegalBlockSizeException("Invalid legacy AES-CBC packet size");
|
||||
}
|
||||
byte[] iv = Arrays.copyOfRange(payload, 0, 16);
|
||||
byte[] encrypted = Arrays.copyOfRange(payload, 16, payload.length);
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, (Key) this.keySpec, new IvParameterSpec(iv));
|
||||
return cipher.doFinal(encrypted);
|
||||
}
|
||||
if (payload.length < IV_SIZE_BYTES + 16) {
|
||||
throw new IllegalBlockSizeException("Invalid AES-GCM packet size");
|
||||
}
|
||||
byte[] iv = Arrays.copyOfRange(payload, 0, IV_SIZE_BYTES);
|
||||
byte[] encrypted = Arrays.copyOfRange(payload, IV_SIZE_BYTES, payload.length);
|
||||
Cipher cipher = Cipher.getInstance(CIPHER);
|
||||
cipher.init(Cipher.DECRYPT_MODE, (Key) this.keySpec, new GCMParameterSpec(TAG_LEN_BITS, iv));
|
||||
return cipher.doFinal(encrypted);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof Secret && Arrays.equals(this.secret, ((Secret) o).secret);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(this.secret);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package de.maxhenkel.voicechat.voice.server;
|
||||
|
||||
import de.maxhenkel.voicechat.Voicechat;
|
||||
import de.maxhenkel.voicechat.compatibility.PlayerHideEvent;
|
||||
import de.maxhenkel.voicechat.compatibility.PlayerShowEvent;
|
||||
import de.maxhenkel.voicechat.net.NetManager;
|
||||
import de.maxhenkel.voicechat.net.PacketRateLimiter;
|
||||
import de.maxhenkel.voicechat.net.RequestSecretPacket;
|
||||
import de.maxhenkel.voicechat.net.SecretPacket;
|
||||
import de.maxhenkel.voicechat.plugins.PluginManager;
|
||||
import de.maxhenkel.voicechat.voice.common.Secret;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
|
||||
public class ServerVoiceEvents implements Listener {
|
||||
private static final int LEGACY_COMPATIBILITY_VERSION = 18;
|
||||
private static final int CURRENT_COMPATIBILITY_VERSION = 20;
|
||||
|
||||
private final Map<UUID, Integer> clientCompatibilities = new ConcurrentHashMap<>();
|
||||
private final PacketRateLimiter rateLimiter;
|
||||
private Server server;
|
||||
|
||||
public ServerVoiceEvents() {
|
||||
this.rateLimiter = new PacketRateLimiter(Voicechat.SERVER_CONFIG.tcpRateLimit.get());
|
||||
}
|
||||
|
||||
public void init() {
|
||||
if (this.server != null) {
|
||||
return;
|
||||
}
|
||||
if (!Bukkit.getOnlineMode()) {
|
||||
Voicechat.LOGGER.warn("Running in offline mode - Voice chat encryption is not secure!", new Object[0]);
|
||||
}
|
||||
Voicechat.compatibility.registerPlayerHideEvent(this::onPlayerHide);
|
||||
Voicechat.compatibility.registerPlayerShowEvent(this::onPlayerShow);
|
||||
this.server = new Server();
|
||||
this.server.start();
|
||||
PluginManager.instance().onServerStarted();
|
||||
}
|
||||
|
||||
public void onRequestSecretPacket(Player player, RequestSecretPacket packet) {
|
||||
UUID playerUUID;
|
||||
int clientVersion = packet.getCompatibilityVersion();
|
||||
Voicechat.LOGGER.info("Received secret request of {} ({})", player.getName(), clientVersion);
|
||||
try {
|
||||
playerUUID = player.getUniqueId();
|
||||
} catch (UnsupportedOperationException e) {
|
||||
player.kickPlayer("Tried to authenticate voice chat while still connecting");
|
||||
Voicechat.LOGGER.warn("{} tried to authenticate voice chat while still connecting", player.getName());
|
||||
return;
|
||||
}
|
||||
this.clientCompatibilities.put(playerUUID, clientVersion);
|
||||
if (!ServerVoiceEvents.isSupportedCompatibilityVersion(clientVersion)) {
|
||||
Voicechat.LOGGER.warn("Connected client {} has incompatible voice chat version (server={}, client={})",
|
||||
player.getName(), Voicechat.COMPATIBILITY_VERSION, clientVersion);
|
||||
ServerVoiceEvents.sendIncompatibleMessage(player, clientVersion);
|
||||
return;
|
||||
}
|
||||
this.initializePlayerConnection(player);
|
||||
}
|
||||
|
||||
private static boolean isSupportedCompatibilityVersion(int version) {
|
||||
return version == LEGACY_COMPATIBILITY_VERSION || version == CURRENT_COMPATIBILITY_VERSION;
|
||||
}
|
||||
|
||||
public boolean isCompatible(Player player) {
|
||||
return this.isCompatible(player.getUniqueId());
|
||||
}
|
||||
|
||||
public boolean isCompatible(UUID playerUuid) {
|
||||
return ServerVoiceEvents.isSupportedCompatibilityVersion(this.clientCompatibilities.getOrDefault(playerUuid, -1));
|
||||
}
|
||||
|
||||
public static void sendIncompatibleMessage(Player player, int clientCompatibilityVersion) {
|
||||
if (clientCompatibilityVersion <= 6) {
|
||||
player.sendMessage(String.format(Voicechat.TRANSLATIONS.voicechatNotCompatibleMessage.get(), "2.6.x", "Simple Voice Chat"));
|
||||
} else {
|
||||
Voicechat.compatibility.sendIncompatibleMessage(player, "2.6.x", "Simple Voice Chat");
|
||||
}
|
||||
}
|
||||
|
||||
public void initializePlayerConnection(Player player) {
|
||||
if (this.server == null) {
|
||||
return;
|
||||
}
|
||||
UUID playerUuid = player.getUniqueId();
|
||||
this.server.getPlayerStateManager().onPlayerCompatibilityCheckSucceeded(player);
|
||||
this.server.getCategoryManager().onPlayerCompatibilityCheckSucceeded(player);
|
||||
this.server.getGroupManager().onPlayerCompatibilityCheckSucceeded(player);
|
||||
Secret secret = this.server.generateNewSecret(playerUuid);
|
||||
if (secret == null) {
|
||||
Voicechat.LOGGER.warn("Player already requested secret - ignoring", new Object[0]);
|
||||
return;
|
||||
}
|
||||
secret.setLegacyProtocol(this.clientCompatibilities.getOrDefault(playerUuid, -1) == LEGACY_COMPATIBILITY_VERSION);
|
||||
String voiceHost = PluginManager.instance().getVoiceHost(player, Voicechat.SERVER_CONFIG.voiceHost.get());
|
||||
NetManager.sendToClient(player, new SecretPacket(player, secret, this.server.getPort(), Voicechat.SERVER_CONFIG, voiceHost));
|
||||
Voicechat.LOGGER.info("Sent secret to {}", player.getName());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
this.server.getPlayerStateManager().onPlayerJoin(event);
|
||||
if (!Voicechat.SERVER_CONFIG.forceVoiceChat.get().booleanValue()) {
|
||||
return;
|
||||
}
|
||||
Player player = event.getPlayer();
|
||||
Voicechat.compatibility.runTaskLater(() -> {
|
||||
if (!player.isOnline()) {
|
||||
return;
|
||||
}
|
||||
if (!this.isCompatible(player)) {
|
||||
player.kickPlayer(String.format(Voicechat.TRANSLATIONS.forceVoicechatKickMessage.get(), "Simple Voice Chat", Voicechat.INSTANCE.getDescription().getVersion()));
|
||||
}
|
||||
}, (long) Voicechat.SERVER_CONFIG.loginTimeout.get().intValue() / 50L);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||
this.server.getPlayerStateManager().onPlayerQuit(event);
|
||||
this.server.getGroupManager().onPlayerQuit(event);
|
||||
UUID playerUuid = event.getPlayer().getUniqueId();
|
||||
this.clientCompatibilities.remove(playerUuid);
|
||||
this.rateLimiter.onPlayerLoggedOut(event.getPlayer());
|
||||
if (this.server == null) {
|
||||
return;
|
||||
}
|
||||
this.server.disconnectClient(playerUuid);
|
||||
Voicechat.LOGGER.info("Disconnecting client {}", event.getPlayer().getName());
|
||||
}
|
||||
|
||||
public void onPlayerHide(PlayerHideEvent event) {
|
||||
this.server.getPlayerStateManager().onPlayerHide(event);
|
||||
}
|
||||
|
||||
public void onPlayerShow(PlayerShowEvent event) {
|
||||
this.server.getPlayerStateManager().onPlayerShow(event);
|
||||
}
|
||||
|
||||
public PacketRateLimiter getRateLimiter() {
|
||||
return this.rateLimiter;
|
||||
}
|
||||
|
||||
public Server getServer() {
|
||||
return this.server;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user