diff --git a/crypto/src/main/java/dev/openrs2/crypto/Xtea.kt b/crypto/src/main/java/dev/openrs2/crypto/Xtea.kt new file mode 100644 index 0000000000..77012b55a1 --- /dev/null +++ b/crypto/src/main/java/dev/openrs2/crypto/Xtea.kt @@ -0,0 +1,45 @@ +package dev.openrs2.crypto + +import io.netty.buffer.ByteBuf + +private const val GOLDEN_RATIO = 0x9e3779b9.toInt() +private const val ROUNDS = 32 + +fun ByteBuf.xteaEncrypt(index: Int, length: Int, key: IntArray) { + require(key.size == 4) + + for (i in index until index + length step 8) { + var sum = 0 + var v0 = getInt(i) + var v1 = getInt(i + 4) + + for (j in 0 until ROUNDS) { + v0 += (((v1 shl 4) xor (v1 ushr 5)) + v1) xor (sum + key[sum and 3]) + sum += GOLDEN_RATIO + v1 += (((v0 shl 4) xor (v0 ushr 5)) + v0) xor (sum + key[(sum ushr 11) and 3]) + } + + setInt(i, v0) + setInt(i + 4, v1) + } +} + +fun ByteBuf.xteaDecrypt(index: Int, length: Int, key: IntArray) { + require(key.size == 4) + + for (i in index until index + length step 8) { + @Suppress("INTEGER_OVERFLOW") + var sum = GOLDEN_RATIO * ROUNDS + var v0 = getInt(i) + var v1 = getInt(i + 4) + + for (j in 0 until ROUNDS) { + v1 -= (((v0 shl 4) xor (v0 ushr 5)) + v0) xor (sum + key[(sum ushr 11) and 3]) + sum -= GOLDEN_RATIO + v0 -= (((v1 shl 4) xor (v1 ushr 5)) + v1) xor (sum + key[sum and 3]) + } + + setInt(i, v0) + setInt(i + 4, v1) + } +} diff --git a/crypto/src/test/java/dev/openrs2/crypto/XteaTest.kt b/crypto/src/test/java/dev/openrs2/crypto/XteaTest.kt new file mode 100644 index 0000000000..ad0ff3653d --- /dev/null +++ b/crypto/src/test/java/dev/openrs2/crypto/XteaTest.kt @@ -0,0 +1,63 @@ +package dev.openrs2.crypto + +import io.netty.buffer.ByteBufUtil +import io.netty.buffer.Unpooled +import kotlin.test.Test +import kotlin.test.assertEquals + +object XteaTest { + private class TestVector(key: String, plaintext: String, ciphertext: String) { + val key = IntArray(4) { + Integer.parseUnsignedInt(key, it * 8, it * 8 + 8, 16) + } + val plaintext: ByteArray = ByteBufUtil.decodeHexDump(plaintext) + val ciphertext: ByteArray = ByteBufUtil.decodeHexDump(ciphertext) + } + + private val TEST_VECTORS = listOf( + TestVector("000102030405060708090a0b0c0d0e0f", "4142434445464748", "497df3d072612cb5"), + TestVector("000102030405060708090a0b0c0d0e0f", "4141414141414141", "e78f2d13744341d8"), + TestVector("000102030405060708090a0b0c0d0e0f", "5a5b6e278948d77f", "4141414141414141"), + TestVector("00000000000000000000000000000000", "4142434445464748", "a0390589f8b8efa5"), + TestVector("00000000000000000000000000000000", "4141414141414141", "ed23375a821a8c2d"), + TestVector("00000000000000000000000000000000", "70e1225d6e4e7655", "4141414141414141") + ) + + @Test + fun testEncrypt() { + for (vector in TEST_VECTORS) { + val buffer = Unpooled.copiedBuffer(vector.plaintext) + try { + buffer.xteaEncrypt(0, buffer.readableBytes(), vector.key) + + val expected = Unpooled.wrappedBuffer(vector.ciphertext) + try { + assertEquals(expected, buffer) + } finally { + expected.release() + } + } finally { + buffer.release() + } + } + } + + @Test + fun testDecrypt() { + for (vector in TEST_VECTORS) { + val buffer = Unpooled.copiedBuffer(vector.ciphertext) + try { + buffer.xteaDecrypt(0, buffer.readableBytes(), vector.key) + + val expected = Unpooled.wrappedBuffer(vector.plaintext) + try { + assertEquals(expected, buffer) + } finally { + expected.release() + } + } finally { + buffer.release() + } + } + } +}