package coffee.liz.rookery.bt import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertFailsWith @OptIn(ExperimentalStdlibApi::class) class GanCryptoTest { // Vectors computed with a Node.js re-implementation of gan-web-bluetooth's GanGen2CubeEncrypter // (aes-js CBC swapped for node:crypto aes-128-cbc, no padding). The MAC is made up; it is chosen so // that key[5] + salt[5] = 0x100, exercising the firmware's `% 0xFF` (255, not 256) salting. private val mac = "6F:F0:12:34:56:78" private val enc = GanEncrypter(JvmAes128, GanDefinitions.GAN_KEY, mac) @Test fun saltedKeyAndIv() { assertContentEquals("7958763a220116072005185442111253".hexToByteArray(), enc.key) assertContentEquals("8959663a127076272095781432120243".hexToByteArray(), enc.iv) } @Test fun encryptMatchesReference() { val m20 = "030a11181f262d343b424950575e656c737a8188".hexToByteArray() assertContentEquals("f8b5b4a9ac44104f5d5ce7635f869bf95bc95c02".hexToByteArray(), enc.encrypt(m20)) val m16 = "fffcf9f6f3f0edeae7e4e1dedbd8d5d2".hexToByteArray() assertContentEquals("c4f8c0629684911cb703cd9a76f44ffb".hexToByteArray(), enc.encrypt(m16)) } @Test fun roundTrips() { val moyu = GanEncrypter(JvmAes128, GanDefinitions.MOYU_AI_KEY, mac) for (size in listOf(16, 17, 19, 20, 31, 32)) { val msg = ByteArray(size) { (it * 37 + size).toByte() } assertContentEquals(msg, enc.decrypt(enc.encrypt(msg))) assertContentEquals(msg, moyu.decrypt(moyu.encrypt(msg))) } } @Test fun rejectsShortMessages() { assertFailsWith { enc.encrypt(ByteArray(15)) } assertFailsWith { enc.decrypt(ByteArray(15)) } } }