diff --git a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/Argon2.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/Argon2Template.kt similarity index 99% rename from multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/Argon2.kt rename to multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/Argon2Template.kt index c4e1490..9b0c1e5 100644 --- a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/Argon2.kt +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/Argon2Template.kt @@ -35,7 +35,7 @@ import com.ionspin.kotlin.crypto.util.* */ @ExperimentalStdlibApi @ExperimentalUnsignedTypes -class Argon2 internal constructor( +class Argon2Template internal constructor( val password: Array, val salt: Array, val parallelism: UInt, diff --git a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/argon2/Argon2.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/argon2/Argon2.kt new file mode 100644 index 0000000..330fe3c --- /dev/null +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/argon2/Argon2.kt @@ -0,0 +1,326 @@ +/* + * Copyright 2019 Ugljesa Jovanovic + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS") + +package com.ionspin.kotlin.crypto.keyderivation.argon2 + +import com.ionspin.kotlin.bignum.integer.toBigInteger +import com.ionspin.kotlin.crypto.hash.blake2b.Blake2b +import com.ionspin.kotlin.crypto.keyderivation.argon2.Argon2Utils.argonBlake2bArbitraryLenghtHash +import com.ionspin.kotlin.crypto.keyderivation.argon2.Argon2Utils.compressionFunctionG +import com.ionspin.kotlin.crypto.util.fromLittleEndianArrayToUInt +import com.ionspin.kotlin.crypto.util.hexColumsPrint +import com.ionspin.kotlin.crypto.util.toLittleEndianUByteArray + +/** + * Created by Ugljesa Jovanovic + * ugljesa.jovanovic@ionspin.com + * on 16-May-2020 + */ + +enum class ArgonType(val typeId: Int) { + Argon2d(0), Argon2i(1), Argon2id(2) +} + +data class SegmentPosition( + val iteration: Int, + val lane: Int, + val slice: Int +) + +class Argon2( + val password: Array, + val salt: Array = emptyArray(), + val parallelism: Int = 1, + val tagLength: UInt = 64U, + requestedMemorySize: UInt = 0U, + val numberOfIterations: UInt = 1U, + val key: Array = emptyArray(), + val associatedData: Array = emptyArray(), + val argonType: ArgonType = ArgonType.Argon2id +) { + //We support only the latest version + val versionNumber: UInt = 0x13U + + //Use either requested memory size, or default, or throw exception if the requested amount is less than 8*parallelism + val memorySize = if (requestedMemorySize == 0U) { + ((8 * parallelism) * 2).toUInt() + } else { + if (requestedMemorySize < (8 * parallelism).toUInt()) { + throw RuntimeException("Requested memory size must be larger than 8 * parallelism. Requested size: $requestedMemorySize") + } + requestedMemorySize + } + val blockCount = (memorySize / (4U * parallelism.toUInt())) * (4U * parallelism.toUInt()) + val columnCount = (blockCount / parallelism.toUInt()).toInt() + val segmentLength = columnCount / 4 + + val useIndependentAddressing = argonType == ArgonType.Argon2id || argonType == ArgonType.Argon2i + + + // State + val matrix = Array(parallelism) { + Array(columnCount) { + Array(1024) { 0U } + } + } + + private fun clearMatrix() { + matrix.forEachIndexed { laneIndex, lane -> + lane.forEachIndexed { columnIndex, block -> + block.forEachIndexed { byteIndex, byte -> + matrix[laneIndex][columnIndex][byteIndex] = 0U + } + } + } + } + + private fun populateAddressBlock( + iteration: Int, + slice: Int, + lane: Int, + addressBlock: Array, + addressCounter: ULong + ): Array { + //Calculate first pass + val firstPass = compressionFunctionG( + Array(1024) { 0U }, + iteration.toULong().toLittleEndianUByteArray() + + lane.toULong().toLittleEndianUByteArray() + + slice.toULong().toLittleEndianUByteArray() + + blockCount.toULong().toLittleEndianUByteArray() + + numberOfIterations.toULong().toLittleEndianUByteArray() + + argonType.typeId.toULong().toLittleEndianUByteArray() + + addressCounter.toLittleEndianUByteArray() + + Array(968) { 0U }, + addressBlock, + false + ) + //Calculate second pass + val secondPass = compressionFunctionG( + firstPass, + addressBlock, + addressBlock, + false + ) + // Put into address block + return secondPass + } + + + private fun computeReferenceBlockIndexes(iteration: Int, slice: Int, lane: Int, column: Int, addressBlock: Array?): Pair { + val (j1, j2) = when (argonType) { + ArgonType.Argon2d -> { + val previousBlock = if (column == 0) { + matrix[lane][columnCount - 1] //Get last block in the SAME lane + } else { + matrix[lane][column - 1] + } + val first32Bit = previousBlock.sliceArray(0 until 4).fromLittleEndianArrayToUInt() + val second32Bit = previousBlock.sliceArray(4 until 8).fromLittleEndianArrayToUInt() + Pair(first32Bit, second32Bit) + } + ArgonType.Argon2i -> { + val selectedAddressBlock = addressBlock!!.sliceArray((column * 8) until (column * 8) + 8) + val first32Bit = selectedAddressBlock.sliceArray(0 until 4).fromLittleEndianArrayToUInt() + val second32Bit = selectedAddressBlock.sliceArray(4 until 8).fromLittleEndianArrayToUInt() + Pair(first32Bit, second32Bit) + } + ArgonType.Argon2id -> TODO() + } + + + //If this is first iteration and first slice, block is taken from the current lane + val l = if (iteration == 0 && slice == 0) { + lane + } else { + (j2.toBigInteger() % parallelism).intValue() + + } + + val segmentIndex = (column % (columnCount / 4)) + val referenceAreaSize = if (iteration == 0) { + if (slice == 0) { + //All indices except the previous + segmentIndex - 1 + } else { + if (lane == l) { + //Same lane + column - 1 + } else { + slice * (columnCount / 4) + if (segmentIndex == 0) { // Check if column is first block of the SEGMENT + -1 + } else { + 0 + } + } + } + } else { + if (lane == l) { + columnCount - (columnCount / 4) + (segmentIndex - 1) + } else { + columnCount - (columnCount / 4) + if (segmentIndex == 0) { + -1 + } else { + 0 + } + } + } + + val x = (j1.toULong() * j1) shr 32 + val y = (referenceAreaSize.toULong() * x) shr 32 + val z = referenceAreaSize.toULong() - 1U - y + + val startPosition = if (iteration == 0) { + 0 + } else { + if (slice == 3) { + 0 + } else { + (slice + 1) * segmentLength + } + } + val absolutePosition = (startPosition + z.toInt()) % columnCount + + return Pair(l, absolutePosition) + } + + fun derive(): Array { + val h0 = Blake2b.digest( + parallelism.toUInt() + .toLittleEndianUByteArray() + tagLength.toLittleEndianUByteArray() + memorySize.toLittleEndianUByteArray() + + numberOfIterations.toLittleEndianUByteArray() + versionNumber.toLittleEndianUByteArray() + argonType.typeId.toUInt() + .toLittleEndianUByteArray() + + password.size.toUInt().toLittleEndianUByteArray() + password + + salt.size.toUInt().toLittleEndianUByteArray() + salt + + key.size.toUInt().toLittleEndianUByteArray() + key + + associatedData.size.toUInt().toLittleEndianUByteArray() + associatedData + ) + + //Compute B[i][0] + for (i in 0 until parallelism.toInt()) { + matrix[i][0] = + argonBlake2bArbitraryLenghtHash( + h0 + 0.toUInt().toLittleEndianUByteArray() + i.toUInt().toLittleEndianUByteArray(), + 1024U + ) + } + + //Compute B[i][1] + for (i in 0 until parallelism.toInt()) { + matrix[i][1] = + argonBlake2bArbitraryLenghtHash( + h0 + 1.toUInt().toLittleEndianUByteArray() + i.toUInt().toLittleEndianUByteArray(), + 1024U + ) + } + executeArgonWithSingleThread() + + val result = matrix.foldIndexed(emptyArray()) { lane, acc, laneArray -> + if (acc.size == 0) { + acc + laneArray[columnCount - 1] // add last element in first lane to the accumulator + } else { + // For each element in our accumulator, xor it with an appropriate element from the last column in current lane (from 1 to `parallelism`) + acc.mapIndexed { index, it -> it xor laneArray[columnCount - 1][index] } + .toTypedArray() + } + } + //Hash the xored last blocks + println("Tag:") + val hash = argonBlake2bArbitraryLenghtHash(result, tagLength) + return hash + + + } + + fun executeArgonWithSingleThread() { + for (iteration in 0 until numberOfIterations.toInt()) { + for (slice in 0 until 4) { + for (lane in 0 until parallelism.toInt()) { + println("Processing segment I: $iteration, S: $slice, L: $lane") + val segmentPosition = SegmentPosition(iteration, lane, slice) + processSegment(segmentPosition) + } + } + //Debug prints + println("Done with $iteration") + matrix[0][0].slice(0..7).toTypedArray().hexColumsPrint(8) + matrix[parallelism.toInt() - 1][columnCount - 1].slice( + 1016..1023 + ).toTypedArray().hexColumsPrint(8) + + } + } + + fun processSegment(segmentPosition: SegmentPosition) { + val iteration = segmentPosition.iteration + val slice = segmentPosition.slice + val lane = segmentPosition.lane + + var addressBlock : Array? = null + var addressCounter = 1UL //Starts from 1 in each segment as defined by the spec + + //Generate initial segment address block + if (useIndependentAddressing) { + addressBlock = Array(1024) { + 0U + } + addressBlock = populateAddressBlock(iteration, slice, lane, addressBlock, addressCounter) + addressCounter++ + + addressBlock.hexColumsPrint(16) + } + + val startColumn = if (iteration == 0 && slice == 0) { + 2 + } else { + slice * segmentLength + } + + for (column in startColumn until (slice + 1) * segmentLength) { + //Each address block contains 128 addresses, and we use one per iteration, + //so once we do 128 iterations we need to calculate a new address block + if (useIndependentAddressing && column % 128 == 0) { + addressBlock = populateAddressBlock(iteration, slice, lane, addressBlock!!, addressCounter) + addressCounter++ + } + val previousColumn = if (column == 0) { + columnCount - 1 + } else { + column - 1 + } + val (l, z) = computeReferenceBlockIndexes( + iteration, + slice, + lane, + column, + addressBlock + ) + println("Calling compress for I: $iteration S: $slice Lane: $lane Column: $column with l: $l z: $z") + matrix[lane][column] = + compressionFunctionG( + matrix[lane][previousColumn], + matrix[l][z], + matrix[lane][column], + true + ) + } + + } + + +} diff --git a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/argon2/Argon2Utils.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/argon2/Argon2Utils.kt new file mode 100644 index 0000000..e24692d --- /dev/null +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/argon2/Argon2Utils.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2019 Ugljesa Jovanovic + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS") + +package com.ionspin.kotlin.crypto.keyderivation.argon2 + +import com.ionspin.kotlin.crypto.hash.blake2b.Blake2b +import com.ionspin.kotlin.crypto.keyderivation.Argon2Template +import com.ionspin.kotlin.crypto.util.* + +/** + * Created by Ugljesa Jovanovic + * ugljesa.jovanovic@ionspin.com + * on 16-May-2020 + */ +object Argon2Utils { + + const val R1 = 32 + const val R2 = 24 + const val R3 = 16 + const val R4 = 63 + + //based on Blake2b mixRound + internal inline fun mixRound(input: Array): Array { + var v = input.chunked(8).map { it.fromLittleEndianArrayToULong() }.toTypedArray() + v = mix(v, 0, 4, 8, 12) + v = mix(v, 1, 5, 9, 13) + v = mix(v, 2, 6, 10, 14) + v = mix(v, 3, 7, 11, 15) + v = mix(v, 0, 5, 10, 15) + v = mix(v, 1, 6, 11, 12) + v = mix(v, 2, 7, 8, 13) + v = mix(v, 3, 4, 9, 14) + return v + } + + //Based on Blake2b mix + private inline fun mix(v: Array, a: Int, b: Int, c: Int, d: Int): Array { + v[a] = (v[a] + v[b] + 2U * (v[a] and 0xFFFFFFFFUL) * (v[b] and 0xFFFFFFFFUL)) + v[d] = (v[d] xor v[a]) rotateRight R1 + v[c] = (v[c] + v[d] + 2U * (v[c] and 0xFFFFFFFFUL) * (v[d] and 0xFFFFFFFFUL)) + v[b] = (v[b] xor v[c]) rotateRight R2 + v[a] = (v[a] + v[b] + 2U * (v[a] and 0xFFFFFFFFUL) * (v[b] and 0xFFFFFFFFUL)) + v[d] = (v[d] xor v[a]) rotateRight R3 + v[c] = (v[c] + v[d] + 2U * (v[c] and 0xFFFFFFFFUL) * (v[d] and 0xFFFFFFFFUL)) + v[b] = (v[b] xor v[c]) rotateRight R4 + return v + } + + private fun extractColumnFromGBlock(gBlock: Array, columnPosition: Int): Array { + val result = Array(128) { 0U } + for (i in 0..7) { + gBlock.copyOfRange(i * 128 + (columnPosition * 16), i * 128 + (columnPosition * 16) + 16) + .copyInto(result, i * 16) + } + return result + } + + private fun copyIntoGBlockColumn(gBlock: Array, columnPosition: Int, columnData: Array) { + for (i in 0..7) { + val column = columnData.copyOfRange(i * 16, i * 16 + 16) + column.copyInto(gBlock, i * 128 + columnPosition * 16) + } + } + + fun compressionFunctionG( + previousBlock: Array, + referenceBlock: Array, + currentBlock: Array, + xorWithCurrentBlock: Boolean + ): Array { + val r = referenceBlock xor previousBlock + val q = Array(1024) { 0U } + val z = Array(1024) { 0U } + // Do the argon/blake2b mixing on rows + for (i in 0..7) { + val startOfRow = (i * 8 * 16) + val endOfRow = startOfRow + (8 * 16) + val rowToMix = r.copyOfRange(startOfRow, endOfRow) + mixRound(rowToMix) + .map { it.toLittleEndianUByteArray() } + .flatMap { it.asIterable() } + .toTypedArray() + .copyInto(q, startOfRow) + } + // Do the argon/blake2b mixing on columns + for (i in 0..7) { + copyIntoGBlockColumn( + z, + i, + mixRound(extractColumnFromGBlock(q, i)) + .map { it.toLittleEndianUByteArray() } + .flatMap { it.asIterable() } + .toTypedArray() + ) + } + val final = if (xorWithCurrentBlock) { + (z xor r) xor currentBlock + } else { + z xor r + } + return final + } + + fun argonBlake2bArbitraryLenghtHash(input: Array, length: UInt): Array { + if (length <= 64U) { + return Blake2b.digest(inputMessage = length + input, hashLength = length.toInt()) + } + //We can cast to int because UInt even if MAX_VALUE divided by 32 is guaranteed not to overflow + val numberOf64ByteBlocks = (1U + ((length - 1U) / 32U) - 2U).toInt() // equivalent to ceil(length/32) - 2 + val v = Array>(numberOf64ByteBlocks) { emptyArray() } + v[0] = Blake2b.digest(length + input) + for (i in 1 until numberOf64ByteBlocks) { + v[i] = Blake2b.digest(v[i - 1]) + } + val remainingPartOfInput = length.toInt() - numberOf64ByteBlocks * 32 + val vLast = Blake2b.digest(v[numberOf64ByteBlocks - 1], hashLength = remainingPartOfInput) + val concat = + (v.map { it.copyOfRange(0, 32) }) + .plus(listOf(vLast)) + .foldRight(emptyArray()) { arrayOfUBytes, acc -> arrayOfUBytes + acc } + + return concat + } +} \ No newline at end of file diff --git a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/hash/keyderivation/Argon2Test.kt b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/hash/keyderivation/Argon2Test.kt index 4a33cd9..8dcdafc 100644 --- a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/hash/keyderivation/Argon2Test.kt +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/hash/keyderivation/Argon2Test.kt @@ -18,9 +18,13 @@ package com.ionspin.kotlin.crypto.hash.keyderivation -import com.ionspin.kotlin.crypto.keyderivation.Argon2 +import com.ionspin.kotlin.crypto.keyderivation.Argon2Template +import com.ionspin.kotlin.crypto.keyderivation.argon2.Argon2 +import com.ionspin.kotlin.crypto.keyderivation.argon2.ArgonType import com.ionspin.kotlin.crypto.util.hexColumsPrint +import kotlin.math.exp import kotlin.test.Test +import kotlin.test.assertTrue /** * Created by Ugljesa Jovanovic @@ -46,7 +50,7 @@ class Argon2Test { val secret: Array = arrayOf(0x03U, 0x03U, 0x03U, 0x03U, 0x03U, 0x03U, 0x03U, 0x03U) val associatedData: Array = arrayOf(0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U) - val digest = Argon2( + val digest = Argon2Template( password, salt, parallelism, @@ -56,10 +60,92 @@ class Argon2Test { 0x13U, secret, associatedData, - type = Argon2.ArgonType.Argon2d + type = Argon2Template.ArgonType.Argon2d ) val result = digest.calculate() result.hexColumsPrint(8) } + + @Test + fun argon2dKATTest() { + val expected : Array = arrayOf( + 0x51U, 0x2BU, 0x39U, 0x1BU, 0x6FU, 0x11U, 0x62U, 0x97U, + 0x53U, 0x71U, 0xD3U, 0x09U, 0x19U, 0x73U, 0x42U, 0x94U, + 0xF8U, 0x68U, 0xE3U, 0xBEU, 0x39U, 0x84U, 0xF3U, 0xC1U, + 0xA1U, 0x3AU, 0x4DU, 0xB9U, 0xFAU, 0xBEU, 0x4AU, 0xCBU + ) + + + val memory = 32U //KiB + val iterations = 3U + val parallelism = 4U + val tagLength = 32U + val password: Array = arrayOf( + 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, + 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, + 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, + 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U + ) + val salt: Array = arrayOf(0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U) + val secret: Array = arrayOf(0x03U, 0x03U, 0x03U, 0x03U, 0x03U, 0x03U, 0x03U, 0x03U) + val associatedData: Array = arrayOf(0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U) + + val digest = Argon2( + password, + salt, + parallelism.toInt(), + tagLength, + memory, + iterations, + secret, + associatedData, + ArgonType.Argon2d + ) + val result = digest.derive() + result.hexColumsPrint(8) + assertTrue { expected.contentEquals(result) } + + } + + @Test + fun argon2iKATTest() { + val expected : Array = arrayOf( + 0xc8U, 0x14U, 0xd9U, 0xd1U, 0xdcU, 0x7fU, 0x37U, 0xaaU, + 0x13U, 0xf0U, 0xd7U, 0x7fU, 0x24U, 0x94U, 0xbdU, 0xa1U, + 0xc8U, 0xdeU, 0x6bU, 0x01U, 0x6dU, 0xd3U, 0x88U, 0xd2U, + 0x99U, 0x52U, 0xa4U, 0xc4U, 0x67U, 0x2bU, 0x6cU, 0xe8U + ) + + + val memory = 32U //KiB + val iterations = 3U + val parallelism = 4U + val tagLength = 32U + val password: Array = arrayOf( + 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, + 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, + 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, + 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U + ) + val salt: Array = arrayOf(0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U, 0x02U) + val secret: Array = arrayOf(0x03U, 0x03U, 0x03U, 0x03U, 0x03U, 0x03U, 0x03U, 0x03U) + val associatedData: Array = arrayOf(0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U, 0x04U) + + val digest = Argon2( + password, + salt, + parallelism.toInt(), + tagLength, + memory, + iterations, + secret, + associatedData, + ArgonType.Argon2i + ) + val result = digest.derive() + result.hexColumsPrint(8) + assertTrue { expected.contentEquals(result) } + + } } \ No newline at end of file