diff --git a/build.gradle b/build.gradle index 1865f9e..19d6df8 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { ext.bouncycastleVersion = '1.69' ext.ed25519Version = '0.3.0' ext.curve25519Version = '0.5.0' - ext.keccakVersion = '1.1.3' + ext.bignumVersion = '0.3.8' ext.ktlintVersion = '0.45.1' repositories { google() diff --git a/library/build.gradle b/library/build.gradle index 47b6d9a..0636ee9 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -24,7 +24,7 @@ dependencies { api "com.squareup.retrofit2:retrofit:$retrofitVersion" implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:${coroutineAdapterVersion}" - implementation "com.github.komputing.khash:keccak-jvm:${keccakVersion}" + implementation "com.ionspin.kotlin:bignum:${bignumVersion}" implementation "com.github.mixinnetwork:tink-eddsa:0.0.13" implementation "com.github.mixinnetwork.jjwt:jjwt-api:2b1c61aa2f" runtimeOnly 'com.github.mixinnetwork.jjwt:jjwt-impl:2b1c61aa2f' diff --git a/library/src/main/kotlin/one/mixin/bot/util/CryptoUtil.kt b/library/src/main/kotlin/one/mixin/bot/util/CryptoUtil.kt index b01c3c4..d70cd41 100644 --- a/library/src/main/kotlin/one/mixin/bot/util/CryptoUtil.kt +++ b/library/src/main/kotlin/one/mixin/bot/util/CryptoUtil.kt @@ -6,12 +6,12 @@ import okio.ByteString.Companion.toByteString import one.mixin.bot.extension.base64Decode import one.mixin.bot.extension.base64Encode import one.mixin.bot.safe.EdKeyPair +import one.mixin.bot.util.keccak.KeccakParameter +import one.mixin.bot.util.keccak.extensions.digestKeccak import one.mixin.eddsa.Ed25519Sign import one.mixin.eddsa.Field25519 import one.mixin.eddsa.KeyPair.Companion.newKeyPair import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.komputing.khash.keccak.KeccakParameter -import org.komputing.khash.keccak.extensions.digestKeccak import org.whispersystems.curve25519.Curve25519 import java.security.KeyFactory import java.security.KeyPair diff --git a/library/src/main/kotlin/one/mixin/bot/util/keccak/Keccak.kt b/library/src/main/kotlin/one/mixin/bot/util/keccak/Keccak.kt new file mode 100644 index 0000000..4df6c7c --- /dev/null +++ b/library/src/main/kotlin/one/mixin/bot/util/keccak/Keccak.kt @@ -0,0 +1,187 @@ +package one.mixin.bot.util.keccak + +import com.ionspin.kotlin.bignum.integer.BigInteger +import one.mixin.bot.util.keccak.extensions.fillWith +import kotlin.math.min + +public object Keccak { + private val BIT_65 = BigInteger.ONE shl (64) + private val MAX_64_BITS = BIT_65 - BigInteger.ONE + + public fun digest( + value: ByteArray, + parameter: KeccakParameter, + ): ByteArray { + val uState = IntArray(200) + val uMessage = convertToUInt(value) + + var blockSize = 0 + var inputOffset = 0 + + // Absorbing phase + while (inputOffset < uMessage.size) { + blockSize = min(uMessage.size - inputOffset, parameter.rateInBytes) + for (i in 0 until blockSize) { + uState[i] = uState[i] xor uMessage[i + inputOffset] + } + + inputOffset += blockSize + + if (blockSize == parameter.rateInBytes) { + doF(uState) + blockSize = 0 + } + } + + // Padding phase + uState[blockSize] = uState[blockSize] xor parameter.d + if (parameter.d and 0x80 != 0 && blockSize == parameter.rateInBytes - 1) { + doF(uState) + } + + uState[parameter.rateInBytes - 1] = uState[parameter.rateInBytes - 1] xor 0x80 + doF(uState) + + // Squeezing phase + val byteResults = mutableListOf() + var tOutputLen = parameter.outputLengthInBytes + while (tOutputLen > 0) { + blockSize = min(tOutputLen, parameter.rateInBytes) + for (i in 0 until blockSize) { + byteResults.add(uState[i].toByte().toInt().toByte()) + } + + tOutputLen -= blockSize + if (tOutputLen > 0) { + doF(uState) + } + } + + return byteResults.toByteArray() + } + + private fun doF(uState: IntArray) { + val lState = Array(5) { Array(5) { BigInteger.ZERO } } + + for (i in 0..4) { + for (j in 0..4) { + val data = IntArray(8) + val index = 8 * (i + 5 * j) + uState.copyInto(data, 0, index, index + data.size) + lState[i][j] = convertFromLittleEndianTo64(data) + } + } + roundB(lState) + + uState.fillWith(0) + for (i in 0..4) { + for (j in 0..4) { + val data = convertFrom64ToLittleEndian(lState[i][j]) + data.copyInto(uState, 8 * (i + 5 * j)) + } + } + } + + /** + * Permutation on the given state. + */ + private fun roundB(state: Array>) { + var lfsrState = 1 + for (round in 0..23) { + val c = arrayOfNulls(5) + val d = arrayOfNulls(5) + + // θ step + for (i in 0..4) { + c[i] = state[i][0].xor(state[i][1]).xor(state[i][2]).xor(state[i][3]).xor(state[i][4]) + } + + for (i in 0..4) { + d[i] = c[(i + 4) % 5]!!.xor(c[(i + 1) % 5]!!.leftRotate64(1)) + } + + for (i in 0..4) { + for (j in 0..4) { + state[i][j] = state[i][j].xor(d[i]!!) + } + } + + // ρ and π steps + var x = 1 + var y = 0 + var current = state[x][y] + for (i in 0..23) { + val tX = x + x = y + y = (2 * tX + 3 * y) % 5 + + val shiftValue = current + current = state[x][y] + + state[x][y] = shiftValue.leftRotate64Safely((i + 1) * (i + 2) / 2) + } + + // χ step + for (j in 0..4) { + val t = arrayOfNulls(5) + for (i in 0..4) { + t[i] = state[i][j] + } + + for (i in 0..4) { + // ~t[(i + 1) % 5] + val invertVal = t[(i + 1) % 5]!!.xor(MAX_64_BITS) + // t[i] ^ ((~t[(i + 1) % 5]) & t[(i + 2) % 5]) + state[i][j] = t[i]!!.xor(invertVal.and(t[(i + 2) % 5]!!)) + } + } + + // ι step + for (i in 0..6) { + lfsrState = (lfsrState shl 1 xor (lfsrState shr 7) * 0x71) % 256 + // pow(2, i) - 1 + val bitPosition = (1 shl i) - 1 + if (lfsrState and 2 != 0) { + state[0][0] = state[0][0].xor(BigInteger.ONE shl bitPosition) + } + } + } + } + + /** + * Converts the given [data] array to an [IntArray] containing UInt values. + */ + private fun convertToUInt(data: ByteArray) = + IntArray(data.size) { + data[it].toInt() and 0xFF + } + + /** + * Converts the given [data] array containing the little endian representation of a number to a [BigInteger]. + */ + private fun convertFromLittleEndianTo64(data: IntArray): BigInteger { + val value = + data.map { it.toString(16) } + .map { if (it.length == 2) it else "0$it" } + .reversed() + .joinToString("") + return BigInteger.parseString(value, 16) + } + + /** + * Converts the given [BigInteger] to a little endian representation as an [IntArray]. + */ + private fun convertFrom64ToLittleEndian(uLong: BigInteger): IntArray { + val asHex = uLong.toString(16) + val asHexPadded = "0".repeat((8 * 2) - asHex.length) + asHex + return IntArray(8) { + ((7 - it) * 2).let { pos -> + asHexPadded.substring(pos, pos + 2).toInt(16) + } + } + } + + private fun BigInteger.leftRotate64Safely(rotate: Int) = leftRotate64(rotate % 64) + + private fun BigInteger.leftRotate64(rotate: Int) = (this shr (64 - rotate)).add(this shl rotate).mod(BIT_65) +} diff --git a/library/src/main/kotlin/one/mixin/bot/util/keccak/KeccakParameter.kt b/library/src/main/kotlin/one/mixin/bot/util/keccak/KeccakParameter.kt new file mode 100644 index 0000000..8beda68 --- /dev/null +++ b/library/src/main/kotlin/one/mixin/bot/util/keccak/KeccakParameter.kt @@ -0,0 +1,19 @@ +package one.mixin.bot.util.keccak + +/** + * Parameters defining the FIPS 202 standard. + */ +public enum class KeccakParameter constructor(public val rateInBytes: Int, public val outputLengthInBytes: Int, public val d: Int) { + KECCAK_224(144, 28, 0x01), + KECCAK_256(136, 32, 0x01), + KECCAK_384(104, 48, 0x01), + KECCAK_512(72, 64, 0x01), + + SHA3_224(144, 28, 0x06), + SHA3_256(136, 32, 0x06), + SHA3_384(104, 48, 0x06), + SHA3_512(72, 64, 0x06), + + SHAKE128(168, 32, 0x1F), + SHAKE256(136, 64, 0x1F), +} diff --git a/library/src/main/kotlin/one/mixin/bot/util/keccak/extensions/IntArrayExtensions.kt b/library/src/main/kotlin/one/mixin/bot/util/keccak/extensions/IntArrayExtensions.kt new file mode 100644 index 0000000..aa115a7 --- /dev/null +++ b/library/src/main/kotlin/one/mixin/bot/util/keccak/extensions/IntArrayExtensions.kt @@ -0,0 +1,46 @@ +package one.mixin.bot.util.keccak.extensions + +/** + * Assigns the specified int value to each element of the specified + * range of the specified array of ints. The range to be filled + * extends from index fromIndex, inclusive, to index + * toIndex, exclusive. (If fromIndex==toIndex, the + * range to be filled is empty.) + * + * @param fromIndex the index of the first element (inclusive) to be + * filled with the specified value + * @param toIndex the index of the last element (exclusive) to be + * filled with the specified value + * @param value the value to be stored in all elements of the array + * @throws IllegalArgumentException if fromIndex > toIndex + * @throws ArrayIndexOutOfBoundsException if fromIndex < 0 or + * toIndex > a.length + */ +internal fun IntArray.fillWith( + value: Int, + fromIndex: Int = 0, + toIndex: Int = this.size, +) { + if (fromIndex > toIndex) { + throw IllegalArgumentException( + "fromIndex($fromIndex) > toIndex($toIndex)", + ) + } + + if (fromIndex < 0) { + throw ArrayIndexOutOfBoundsException(fromIndex) + } + if (toIndex > this.size) { + throw ArrayIndexOutOfBoundsException(toIndex) + } + + for (i in fromIndex until toIndex) + this[i] = value +} + +/** + * Constructs a new [ArrayIndexOutOfBoundsException] + * class with an argument indicating the illegal index. + * @param index the illegal index. + */ +internal class ArrayIndexOutOfBoundsException(index: Int) : Throwable("Array index out of range: $index") diff --git a/library/src/main/kotlin/one/mixin/bot/util/keccak/extensions/PublicExtensions.kt b/library/src/main/kotlin/one/mixin/bot/util/keccak/extensions/PublicExtensions.kt new file mode 100644 index 0000000..2047e18 --- /dev/null +++ b/library/src/main/kotlin/one/mixin/bot/util/keccak/extensions/PublicExtensions.kt @@ -0,0 +1,18 @@ +package one.mixin.bot.util.keccak.extensions + +import one.mixin.bot.util.keccak.Keccak +import one.mixin.bot.util.keccak.KeccakParameter + +/** + * Computes the proper Keccak digest of [this] byte array based on the given [parameter] + */ +public fun ByteArray.digestKeccak(parameter: KeccakParameter): ByteArray { + return Keccak.digest(this, parameter) +} + +/** + * Computes the proper Keccak digest of [this] string based on the given [parameter] + */ +public fun String.digestKeccak(parameter: KeccakParameter): ByteArray { + return Keccak.digest(encodeToByteArray(), parameter) +}