From 5a5fb6baafae3498f30d3f4f734fa22e58dd9c01 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Fri, 25 Sep 2026 08:42:48 +0000 Subject: [PATCH] Encrypt each packet with its own EAX cipher TS3 derives a packet's key and nonce from its id, so resending a packet encrypts it under the same pair again. BouncyCastle has since learned to refuse that on a cipher that has already used the pair ("cannot reuse nonce for EAX encryption"), which broke resends on the current version the Android build uses. A fresh cipher per packet keeps them working. Moves to bcprov-jdk18on 1.86, the maintained line, so every platform runs the same BouncyCastle. Co-Authored-By: Claude Opus 5.5 --- pom.xml | 4 ++-- .../transformation/PacketTransformation.java | 24 +++++++++---------- .../manevolent/ts3j/EncryptionTest.java | 4 ++++ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/pom.xml b/pom.xml index a6d53c3..e268f0d 100644 --- a/pom.xml +++ b/pom.xml @@ -36,8 +36,8 @@ org.bouncycastle - bcprov-jdk15on - 1.67 + bcprov-jdk18on + 1.86 diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/transformation/PacketTransformation.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/transformation/PacketTransformation.java index 0a6a3bb..c2a8a3f 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/transformation/PacketTransformation.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/transformation/PacketTransformation.java @@ -24,7 +24,7 @@ public class PacketTransformation { public PacketTransformation(byte[] ivStruct, byte[] fakeMac) { this.ivStruct = ivStruct; - this.cipher = new EAXBlockCipher(new AESEngine()); + this.cipher = new EAXBlockCipher(AESEngine.newInstance()); this.fakeMac = fakeMac; } @@ -86,20 +86,18 @@ public class PacketTransformation { int dataLen = packet.getBody().getSize(); ByteBuffer packetBuffer = packet.writeBody(ByteBuffer.allocate(dataLen).order(ByteOrder.BIG_ENDIAN)); - byte[] result; - int len; + // TS3 derives key and nonce from the packet id, so a resend encrypts under the same pair + // again. BouncyCastle refuses that on a cipher that has seen it, hence one per packet. + EAXBlockCipher cipher = new EAXBlockCipher(AESEngine.newInstance()); + cipher.init(true, cipherParameters); - synchronized (cipher) { - cipher.init(true, cipherParameters); + byte[] result = new byte[cipher.getOutputSize(dataLen)]; + int len = cipher.processBytes(packetBuffer.array(), 0, dataLen, result, 0); - result = new byte[cipher.getOutputSize(dataLen)]; - len = cipher.processBytes(packetBuffer.array(), 0, dataLen, result, 0); - - try { - len += cipher.doFinal(result, len); - } catch (InvalidCipherTextException e) { - throw new RuntimeException(e); - } + try { + len += cipher.doFinal(result, len); + } catch (InvalidCipherTextException e) { + throw new RuntimeException(e); } ByteBuffer outputBuffer = ByteBuffer.allocate(MAC_LEN + headerWithoutMac.length + (len - MAC_LEN)); diff --git a/src/test/java/com/github/manevolent/ts3j/EncryptionTest.java b/src/test/java/com/github/manevolent/ts3j/EncryptionTest.java index 03461b2..e521337 100644 --- a/src/test/java/com/github/manevolent/ts3j/EncryptionTest.java +++ b/src/test/java/com/github/manevolent/ts3j/EncryptionTest.java @@ -53,6 +53,10 @@ public class EncryptionTest extends TestCase { byte[] packetBytes = transformation.encrypt(packet).array(); transformation.decrypt(header, ByteBuffer.wrap(packetBytes), packet.getBody().getSize()); + + // A resend encrypts the same packet again, under the same key and nonce. + byte[] resent = transformation.encrypt(packet).array(); + assertTrue(java.util.Arrays.equals(packetBytes, resent)); } }