diff --git a/.travis.yml b/.travis.yml index a4288b2..eaab6c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ matrix: - 'if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then bash ./linuxBuild.sh; fi' - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash ./linuxBuildAndPublish.sh; fi' - os: osx - osx_image: xcode11.2 + osx_image: xcode11.4 language: java jdk: openjdk12 # before_script: diff --git a/README.md b/README.md index 37e22d1..e2a562a 100644 --- a/README.md +++ b/README.md @@ -40,11 +40,17 @@ It's not peer reviewed, not guaranteed to be bug free, and not guaranteed to be * SHA512 * SHA256 -## Symmetric cipher (Currently only available only in 0.0.3-SNAPSHOT) +## Symmetric cipher * AES * Modes: CBC, CTR + +## Key Derivation -More to come. +* Argon2 + +## AEAD + +TODO() ## Integration @@ -181,6 +187,35 @@ plainText == decrypted.toHexString() ``` +### Key derivation + +#### Argon2 + +NOTE: This implementation is tested against KAT generated by reference Argon2 implementation, which does not follow +specification completely. See this issue https://github.com/P-H-C/phc-winner-argon2/issues/183 + +```kotlin +val argon2Instance = Argon2( + password = "Password", + salt = "RandomSalt", + parallelism = 8, + tagLength = 64U, + requestedMemorySize = 256U, //4GB + numberOfIterations = 4U, + key = "", + associatedData = "", + argonType = ArgonType.Argon2id + ) +val tag = argon2Instance.derive() +val tagString = tag.map { it.toString(16).padStart(2, '0') }.joinToString(separator = "") +val expectedTagString = "c255e3e94305817d5e09a7c771e574e3a81cc78fef5da4a9644b6df0" + + "0ba1c9b424e3dd0ce7e600b1269b14c84430708186a8a60403e1bfbda935991592b9ff37" +println("Tag: ${tagString}") +assertEquals(tagString, expectedTagString) +``` + + + diff --git a/buildSrc/src/main/kotlin/Deps.kt b/buildSrc/src/main/kotlin/Deps.kt index 432d57a..70adb05 100644 --- a/buildSrc/src/main/kotlin/Deps.kt +++ b/buildSrc/src/main/kotlin/Deps.kt @@ -15,13 +15,13 @@ */ object Versions { - val kotlinCoroutines = "1.3.3" - val kotlin = "1.3.61" - val kotlinSerialization = "0.11.1" + val kotlinCoroutines = "1.3.6" + val kotlin = "1.3.72" + val kotlinSerialization = "0.20.0" val nodePlugin = "1.3.0" val dokkaPlugin = "0.9.18" - val kotlinBigNumVersion = "0.1.5-SNAPSHOT" + val kotlinBigNumVersion = "0.1.6-SNAPSHOT" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3c28bf7..65fa2e2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -16,6 +16,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/multiplatform-crypto/build.gradle.kts b/multiplatform-crypto/build.gradle.kts index ea519f3..b4e42dd 100644 --- a/multiplatform-crypto/build.gradle.kts +++ b/multiplatform-crypto/build.gradle.kts @@ -19,6 +19,8 @@ import com.moowork.gradle.node.task.NodeTask import org.gradle.api.tasks.testing.logging.TestLogging +import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest +import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile plugins { @@ -47,7 +49,25 @@ repositories { group = "com.ionspin.kotlin" version = "0.0.3-SNAPSHOT" +val ideaActive = System.getProperty("idea.active") == "true" + +fun getHostOsName(): String { + val target = System.getProperty("os.name") + if (target == "Linux") return "linux" + if (target.startsWith("Windows")) return "windows" + if (target.startsWith("Mac")) return "macos" + return "unknown" +} + kotlin { + val hostOsName = getHostOsName() + if (ideaActive) { + when(hostOsName) { + "linux" -> linuxX64("native") + "macos" -> macosX64("native") + "windows" -> mingwX64("native") + } + } jvm() js { compilations { @@ -63,13 +83,23 @@ kotlin { println("Destination dir ${it.compileKotlinTask.destinationDir}") } } - nodejs() { + //Until I figure out how to run headless chrome on travis +// browser { +// +// testTask { +// useKarma { +// useChrome() +// } +// } +// } + nodejs { testTask { useMocha() { timeout = "10s" } } } + } linuxX64("linux") { binaries { @@ -115,14 +145,14 @@ kotlin { } } } - - mingwX86() { - binaries { - staticLib { - - } - } - } +// No coroutines support for mingwX86 +// mingwX86() { +// binaries { +// staticLib { +// +// } +// } +// } linuxArm32Hfp() { binaries { @@ -153,7 +183,6 @@ kotlin { dependencies { implementation(kotlin(Deps.Common.test)) implementation(kotlin(Deps.Common.testAnnotation)) - implementation(Deps.Common.coroutines) } } val jvmMain by getting { @@ -184,15 +213,35 @@ kotlin { implementation(kotlin("test-js")) } } - val nativeMain by creating { - dependsOn(commonMain) - } - val nativeTest by creating { - dependsOn(commonTest) - dependencies { - implementation(Deps.Native.coroutines) + val nativeMain = if (ideaActive) { + val nativeMain by getting { + dependsOn(commonMain) } + nativeMain + } else { + val nativeMain by creating { + dependsOn(commonMain) + } + nativeMain } + val nativeTest = if (ideaActive) { + val nativeTest by getting { + dependsOn(commonTest) + dependencies { + implementation(Deps.Native.coroutines) + } + } + nativeTest + } else { + val nativeTest by creating { + dependsOn(commonTest) + dependencies { + implementation(Deps.Native.coroutines) + } + } + nativeTest + } + val iosMain by getting { dependsOn(nativeMain) @@ -227,21 +276,27 @@ kotlin { val linuxTest by getting { dependsOn(nativeTest) } +// Coroutines don't support mingwx86 yet +// val mingwX86Main by getting { +// dependsOn(commonMain) +// dependencies { +// implementation(Deps.Native.coroutines) +// } +// } - val mingwX86Main by getting { - dependsOn(nativeMain) - } - - val mingwX86Test by getting { - dependsOn(nativeTest) - } - +// val mingwX86Test by getting { +// dependsOn(commonTest) +// } +// val mingwX64Main by getting { - dependsOn(nativeMain) + dependsOn(commonMain) + dependencies { + implementation(Deps.Native.coroutines) + } } val mingwX64Test by getting { - dependsOn(nativeTest) + dependsOn(commonTest) } val linuxArm32HfpMain by getting { @@ -259,6 +314,9 @@ kotlin { val linuxArm64Test by getting { dependsOn(nativeTest) } + all { + languageSettings.enableLanguageFeature("InlineClasses") + } } @@ -304,13 +362,42 @@ tasks { } } + val linuxTest by getting(KotlinNativeTest::class) { + + testLogging { + events("PASSED", "FAILED", "SKIPPED") + // showStandardStreams = true + } + } + + val mingwX64Test by getting(KotlinNativeTest::class) { + + testLogging { + events("PASSED", "FAILED", "SKIPPED") + showStandardStreams = true + } + } + + val jsNodeTest by getting(KotlinJsTest::class) { + + testLogging { + events("PASSED", "FAILED", "SKIPPED") + showStandardStreams = true + } + } + +// val jsBrowserTest by getting(KotlinJsTest::class) { +// +// testLogging { +// events("PASSED", "FAILED", "SKIPPED") +// showStandardStreams = true +// } +// } + } - - - signing { isRequired = false sign(publishing.publications) diff --git a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/Util.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/Util.kt deleted file mode 100644 index 1b222ce..0000000 --- a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/Util.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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. - */ - -package com.ionspin.kotlin.crypto - -/** - * Created by Ugljesa Jovanovic - * ugljesa.jovanovic@ionspin.com - * on 15-Jul-2019 - */ -fun Array.hexColumsPrint() { - val printout = this.map { it.toString(16) }.chunked(16) - printout.forEach { println(it.joinToString(separator = " ") { it.toUpperCase() }) } -} - -fun Array.hexColumsPrint() { - val printout = this.map { it.toString(16) }.chunked(16) - printout.forEach { println(it.joinToString(separator = " ") { it.toUpperCase() }) } -} - -fun Array.hexColumsPrint() { - val printout = this.map { it.toString(16) }.chunked(3) - printout.forEach { println(it.joinToString(separator = " ") { it.toUpperCase() }) } -} - -inline fun Array.chunked(sliceSize: Int): Array> { - val last = this.size % sliceSize - val hasLast = last != 0 - val numberOfSlices = this.size / sliceSize - - - val result : MutableList> = MutableList>(0) { emptyList() } - - for (i in 0 until numberOfSlices) { - result.add(this.slice(i * sliceSize until (i + 1) * sliceSize)) - } - if (hasLast) { - result.add(this.slice(numberOfSlices * sliceSize until this.size)) - } - - return result.map { it.toTypedArray() }.toTypedArray() - -} - -@ExperimentalUnsignedTypes -infix fun UInt.rotateRight(places: Int): UInt { - return (this shr places) xor (this shl (32 - places)) -} - -@ExperimentalUnsignedTypes -infix fun ULong.rotateRight(places: Int): ULong { - return (this shr places) xor (this shl (64 - places)) -} - -@ExperimentalUnsignedTypes -infix fun Array.xor(other : Array) : Array { - if (this.size != other.size) { - throw RuntimeException("Operands of different sizes are not supported yet") - } - return this.copyOf().mapIndexed { index, it -> it xor other[index] }.toTypedArray() -} - -@ExperimentalUnsignedTypes -fun String.hexStringToUByteArray() : Array { - return this.chunked(2).map { it.toUByte(16) }.toTypedArray() -} - -@ExperimentalUnsignedTypes -fun Array.toHexString() : String { - return this.joinToString(separator = "") { - if (it <= 0x0FU) { - "0${it.toString(16)}" - } else { - it.toString(16) - } - } -} \ No newline at end of file diff --git a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/hash/blake2b/Blake2b.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/hash/blake2b/Blake2b.kt index b462faa..afcc519 100644 --- a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/hash/blake2b/Blake2b.kt +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/hash/blake2b/Blake2b.kt @@ -21,6 +21,8 @@ import com.ionspin.kotlin.bignum.integer.toBigInteger import com.ionspin.kotlin.crypto.* import com.ionspin.kotlin.crypto.hash.StatelessHash import com.ionspin.kotlin.crypto.hash.UpdatableHash +import com.ionspin.kotlin.crypto.util.chunked +import com.ionspin.kotlin.crypto.util.rotateRight /** * Created by Ugljesa Jovanovic @@ -148,7 +150,7 @@ class Blake2b(val key: Array? = null, val hashLength: Int = 64) : Updatab val keyBytes = key?.run { encodeToByteArray().map { it.toUByte() }.toTypedArray() } ?: emptyArray() - return digest(inputMessage = array, key = keyBytes) + return digest(inputMessage = array, key = keyBytes, hashLength = hashLength) } @@ -157,6 +159,9 @@ class Blake2b(val key: Array? = null, val hashLength: Int = 64) : Updatab key: Array, hashLength: Int ): Array { + if (hashLength > MAX_HASH_BYTES) { + throw RuntimeException("Invalid hash length. Requested length more than maximum length. Requested length $hashLength") + } val chunkedMessage = inputMessage.chunked(BLOCK_BYTES) val h = iv.copyOf() @@ -200,7 +205,7 @@ class Blake2b(val key: Array? = null, val hashLength: Int = 64) : Updatab compress(h, lastBlockPadded, lastSize.toBigInteger(), true).copyInto(h) - return formatResult(h) + return formatResult(h).copyOfRange(0, hashLength) } private fun formatResult(h: Array): Array { diff --git a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/hash/sha/Sha256.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/hash/sha/Sha256.kt index 34995a7..1a222ff 100644 --- a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/hash/sha/Sha256.kt +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/hash/sha/Sha256.kt @@ -16,10 +16,10 @@ package com.ionspin.kotlin.crypto.hash.sha -import com.ionspin.kotlin.crypto.chunked +import com.ionspin.kotlin.crypto.util.chunked import com.ionspin.kotlin.crypto.hash.StatelessHash import com.ionspin.kotlin.crypto.hash.UpdatableHash -import com.ionspin.kotlin.crypto.rotateRight +import com.ionspin.kotlin.crypto.util.rotateRight /** diff --git a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/hash/sha/Sha512.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/hash/sha/Sha512.kt index 217175e..69618cf 100644 --- a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/hash/sha/Sha512.kt +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/hash/sha/Sha512.kt @@ -16,10 +16,10 @@ package com.ionspin.kotlin.crypto.hash.sha -import com.ionspin.kotlin.crypto.chunked +import com.ionspin.kotlin.crypto.util.chunked import com.ionspin.kotlin.crypto.hash.StatelessHash import com.ionspin.kotlin.crypto.hash.UpdatableHash -import com.ionspin.kotlin.crypto.rotateRight +import com.ionspin.kotlin.crypto.util.rotateRight /** * Created by Ugljesa Jovanovic diff --git a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/KeyDerivationFunction.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/KeyDerivationFunction.kt new file mode 100644 index 0000000..bf8b6ed --- /dev/null +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/KeyDerivationFunction.kt @@ -0,0 +1,26 @@ +/* + * 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. + */ + +package com.ionspin.kotlin.crypto.keyderivation + +/** + * Created by Ugljesa Jovanovic + * ugljesa.jovanovic@ionspin.com + * on 16-May-2020 + */ +interface KeyDerivationFunction { + fun derive() : Array +} \ No newline at end of file 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..b294f51 --- /dev/null +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/argon2/Argon2.kt @@ -0,0 +1,377 @@ +/* + * 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.KeyDerivationFunction +import com.ionspin.kotlin.crypto.keyderivation.argon2.Argon2Utils.argonBlake2bArbitraryLenghtHash +import com.ionspin.kotlin.crypto.keyderivation.argon2.Argon2Utils.compressionFunctionG +import com.ionspin.kotlin.crypto.keyderivation.argon2.Argon2Utils.validateArgonParameters +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 +) + +@ExperimentalStdlibApi +class Argon2( + private val password: Array, + private val salt: Array = emptyArray(), + private val parallelism: Int = 1, + private val tagLength: UInt = 64U, + requestedMemorySize: UInt = 0U, + private val numberOfIterations: UInt = 1U, + private val key: Array = emptyArray(), + private val associatedData: Array = emptyArray(), + private val argonType: ArgonType = ArgonType.Argon2id +) : KeyDerivationFunction { + + constructor( + password: String, + salt: String = "", + parallelism: Int = 1, + tagLength: UInt = 64U, + requestedMemorySize: UInt = 0U, + numberOfIterations: UInt = 10U, + key: String = "", + associatedData: String = "", + argonType: ArgonType = ArgonType.Argon2id + ) : this( + password.encodeToByteArray().map { it.toUByte() }.toList().toTypedArray(), + salt.encodeToByteArray().map { it.toUByte() }.toList().toTypedArray(), + parallelism, + tagLength, + requestedMemorySize, + numberOfIterations, + key.encodeToByteArray().map { it.toUByte() }.toList().toTypedArray(), + associatedData.encodeToByteArray().map { it.toUByte() }.toList().toTypedArray(), + argonType + ) + + init { + validateArgonParameters( + password, + salt, + parallelism, + tagLength, + requestedMemorySize, + numberOfIterations, + key, + associatedData, + argonType + ) + } + + //We support only the latest version + private val versionNumber: UInt = 0x13U + + //Use either requested memory size, or default, or throw exception if the requested amount is less than 8*parallelism + private val memorySize = if (requestedMemorySize == 0U) { + ((8 * parallelism) * 2).toUInt() + } else { + requestedMemorySize + } + private val blockCount = (memorySize / (4U * parallelism.toUInt())) * (4U * parallelism.toUInt()) + private val columnCount = (blockCount / parallelism.toUInt()).toInt() + private val segmentLength = columnCount / 4 + + private val useIndependentAddressing = argonType == ArgonType.Argon2id || argonType == ArgonType.Argon2i + + + // State + private 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 + ) + val secondPass = compressionFunctionG( + Array(1024) { 0U }, + firstPass, + firstPass, + false + ) + return secondPass + } + + + private fun computeReferenceBlockIndexes( + iteration: Int, + slice: Int, + lane: Int, + column: Int, + addressBlock: Array? + ): Pair { + val segmentIndex = (column % segmentLength) + val independentIndex = segmentIndex % 128 // 128 is the number of addresses in address block + 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((independentIndex * 8) until (independentIndex * 8) + 8) + val first32Bit = selectedAddressBlock.sliceArray(0 until 4).fromLittleEndianArrayToUInt() + val second32Bit = selectedAddressBlock.sliceArray(4 until 8).fromLittleEndianArrayToUInt() + Pair(first32Bit, second32Bit) + } + ArgonType.Argon2id -> { + if (iteration == 0 && (slice == 0 || slice == 1)) { + val selectedAddressBlock = + addressBlock!!.sliceArray((independentIndex * 8) until (independentIndex * 8) + 8) + val first32Bit = selectedAddressBlock.sliceArray(0 until 4).fromLittleEndianArrayToUInt() + val second32Bit = selectedAddressBlock.sliceArray(4 until 8).fromLittleEndianArrayToUInt() + Pair(first32Bit, second32Bit) + } else { + 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) + } + + } + } + + //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 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) + } + + override 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 + val hash = argonBlake2bArbitraryLenghtHash(result, tagLength) + clearMatrix() + return hash + + + } + + private fun executeArgonWithSingleThread() { + for (iteration in 0 until numberOfIterations.toInt()) { + for (slice in 0 until 4) { + for (lane in 0 until parallelism) { + val segmentPosition = SegmentPosition(iteration, lane, slice) + processSegment(segmentPosition) + } + } + } + } + + private 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++ + } + val startColumn = if (iteration == 0 && slice == 0) { + 2 + } else { + slice * segmentLength + } + + + for (column in startColumn until (slice + 1) * segmentLength) { + val segmentIndex = column - (slice * 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 && segmentIndex != 0 && segmentIndex % 128 == 0) { + addressBlock = populateAddressBlock(iteration, slice, lane, addressBlock!!, addressCounter) + addressCounter++ + addressBlock.hexColumsPrint(16) + } + val previousColumn = if (column == 0) { + columnCount - 1 + } else { + column - 1 + } + val (l, z) = computeReferenceBlockIndexes( + iteration, + slice, + lane, + column, + addressBlock + ) + 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/Argon2Exceptions.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/argon2/Argon2Exceptions.kt new file mode 100644 index 0000000..b6080a1 --- /dev/null +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/argon2/Argon2Exceptions.kt @@ -0,0 +1,31 @@ +/* + * 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. + */ + +package com.ionspin.kotlin.crypto.keyderivation.argon2 + +/** + * Created by Ugljesa Jovanovic + * ugljesa.jovanovic@ionspin.com + * on 16-May-2020 + */ +class Argon2TagTooShort(tagLength: UInt) : RuntimeException("Too short tag (output) requested. Requested: $tagLength") +class Argon2TagTooLong(tagLength: UInt) : RuntimeException("Too long tag (output) requested. Requested: $tagLength") +class Argon2TimeTooShort(iterations: UInt) : RuntimeException("Too short time parameter (Too few iterations). Requested iterations: $iterations") +class Argon2TimeTooLong(iterations: UInt) : RuntimeException("Too long time parameter (Too many iterations). Requested iterations: $iterations") +class Argon2MemoryTooLitlle(requestedMemorySize: UInt) : RuntimeException("Requested memory size must be larger than 8 * parallelism. Requested size: $requestedMemorySize") +class Argon2MemoryTooMuch(requestedMemorySize: UInt) : RuntimeException("Requested memory size too large. Requested size: $requestedMemorySize") +class Argon2LanesTooFew(parallelism: Int) : RuntimeException("Too few, or invalid number of threads requested $parallelism") +class Argon2LanesTooMany(parallelism: Int) : RuntimeException("Too many threads requested (parallelism). Requested: $parallelism") \ No newline at end of file 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..b27b0d0 --- /dev/null +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/keyderivation/argon2/Argon2Utils.kt @@ -0,0 +1,178 @@ +/* + * 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.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 + private 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 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) + } + } + + internal 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 + } + + internal 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 + } + + /** + * Validates the argon 2 parameters. + * Since Kotlin arrays that we are currently using cannot have more than 2^31 bytes, we don't need to check + * sizes for password, salt, key and associated data. Also since UInt is 32bit we cant set more than 2^32-1 of + * tagLength, requested memory size and number of iterations, so no need to check for upper bound, just lower. + */ + internal fun validateArgonParameters( + password: Array, + salt: Array, + parallelism: Int , + tagLength: UInt, + requestedMemorySize: UInt , + numberOfIterations: UInt , + key: Array, + associatedData: Array, + argonType: ArgonType + ) { + + //Parallelism + if (parallelism > 0xFFFFFF) { + throw Argon2LanesTooMany(parallelism) + } + if (parallelism <= 0) { + throw Argon2LanesTooFew(parallelism) + } + //Tag length + if (tagLength <= 0U) { + throw Argon2TagTooShort(tagLength) + } + //Requested memory + if (requestedMemorySize < 8U || requestedMemorySize < (8 * parallelism).toUInt()) { + throw Argon2MemoryTooLitlle(requestedMemorySize) + } + //Number of iterations + if (numberOfIterations <= 0U) { + throw Argon2TimeTooShort(numberOfIterations) + } + + } +} \ No newline at end of file diff --git a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbc.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbc.kt index 4d7125e..3d6d7e0 100644 --- a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbc.kt +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbc.kt @@ -17,8 +17,8 @@ package com.ionspin.kotlin.crypto.symmetric import com.ionspin.kotlin.crypto.SRNG -import com.ionspin.kotlin.crypto.chunked -import com.ionspin.kotlin.crypto.xor +import com.ionspin.kotlin.crypto.util.chunked +import com.ionspin.kotlin.crypto.util.xor /** * Advanced encryption standard with cipher block chaining and PKCS #5 diff --git a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtr.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtr.kt index 4d5373a..66b7423 100644 --- a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtr.kt +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtr.kt @@ -20,9 +20,9 @@ import com.ionspin.kotlin.bignum.Endianness import com.ionspin.kotlin.bignum.integer.BigInteger import com.ionspin.kotlin.bignum.modular.ModularBigInteger import com.ionspin.kotlin.crypto.SRNG -import com.ionspin.kotlin.crypto.chunked +import com.ionspin.kotlin.crypto.util.chunked import com.ionspin.kotlin.crypto.symmetric.AesCtr.Companion.encrypt -import com.ionspin.kotlin.crypto.xor +import com.ionspin.kotlin.crypto.util.xor /** * diff --git a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/util/Util.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/util/Util.kt new file mode 100644 index 0000000..129eef0 --- /dev/null +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/util/Util.kt @@ -0,0 +1,165 @@ +/* + * 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. + */ + +package com.ionspin.kotlin.crypto.util + +/** + * Created by Ugljesa Jovanovic + * ugljesa.jovanovic@ionspin.com + * on 15-Jul-2019 + */ +fun Array.hexColumsPrint() { + val printout = this.map { it.toString(16) }.chunked(16) + printout.forEach { println(it.joinToString(separator = " ") { it.toUpperCase() }) } +} + +fun Array.hexColumsPrint(chunk : Int = 16) { + val printout = this.map { it.toString(16).padStart(2, '0') }.chunked(chunk) + printout.forEach { println(it.joinToString(separator = " ") { it.toUpperCase() }) } +} + +fun Array.hexColumsPrint(chunk: Int = 3) { + val printout = this.map { it.toString(16) }.chunked(chunk) + printout.forEach { println(it.joinToString(separator = " ") { it.toUpperCase() }) } +} + +inline fun Array.chunked(sliceSize: Int): Array> { + val last = this.size % sliceSize + val hasLast = last != 0 + val numberOfSlices = this.size / sliceSize + + + val result : MutableList> = MutableList>(0) { emptyList() } + + for (i in 0 until numberOfSlices) { + result.add(this.slice(i * sliceSize until (i + 1) * sliceSize)) + } + if (hasLast) { + result.add(this.slice(numberOfSlices * sliceSize until this.size)) + } + + return result.map { it.toTypedArray() }.toTypedArray() + +} + +@ExperimentalUnsignedTypes +infix fun UInt.rotateRight(places: Int): UInt { + return (this shr places) xor (this shl (32 - places)) +} + +@ExperimentalUnsignedTypes +infix fun ULong.rotateRight(places: Int): ULong { + return (this shr places) xor (this shl (64 - places)) +} + +@ExperimentalUnsignedTypes +infix fun Array.xor(other : Array) : Array { + if (this.size != other.size) { + throw RuntimeException("Operands of different sizes are not supported yet") + } + return Array(this.size) { this[it] xor other[it] } +} + +@ExperimentalUnsignedTypes +fun String.hexStringToUByteArray() : Array { + return this.chunked(2).map { it.toUByte(16) }.toTypedArray() +} + +@ExperimentalUnsignedTypes +fun Array.toHexString() : String { + return this.joinToString(separator = "") { + if (it <= 0x0FU) { + "0${it.toString(16)}" + } else { + it.toString(16) + } + } +} + +// UInt / Array utils +@ExperimentalUnsignedTypes +fun UInt.toBigEndianUByteArray() : Array { + return Array (4) { + ((this shr (24 - (it * 8))) and 0xFFU).toUByte() + } +} +@ExperimentalUnsignedTypes +fun UInt.toLittleEndianUByteArray() : Array { + return Array (4) { + ((this shr (it * 8)) and 0xFFU).toUByte() + } +} + +// UInt / Array utils +@ExperimentalUnsignedTypes +fun ULong.toBigEndianUByteArray() : Array { + return Array (8) { + ((this shr (56 - (it * 8))) and 0xFFU).toUByte() + } +} +@ExperimentalUnsignedTypes +fun ULong.toLittleEndianUByteArray() : Array { + return Array (8) { + ((this shr (it * 8)) and 0xFFU).toUByte() + } +} +@ExperimentalUnsignedTypes +fun Array.fromLittleEndianArrayToULong() : ULong { + if (this.size > 8) { + throw RuntimeException("ore than 8 bytes in input, potential overflow") + } + var ulong = this.foldIndexed(0UL) { index, acc, uByte -> acc or (uByte.toULong() shl (index * 8))} + return ulong +} + + +@ExperimentalUnsignedTypes +fun Array.fromBigEndianArrayToULong() : ULong { + if (this.size > 8) { + throw RuntimeException("ore than 8 bytes in input, potential overflow") + } + var ulong = this.foldIndexed(0UL) { + index, acc, uByte -> + val res = acc or (uByte.toULong() shl (56 - (index * 8))) + res + + } + return ulong +} + +@ExperimentalUnsignedTypes +fun Array.fromLittleEndianArrayToUInt() : UInt { + if (this.size > 4) { + throw RuntimeException("ore than 8 bytes in input, potential overflow") + } + var uint = this.foldIndexed(0U) { index, acc, uByte -> acc or (uByte.toUInt() shl (index * 8))} + return uint +} + + +@ExperimentalUnsignedTypes +fun Array.fromBigEndianArrayToUInt() : UInt { + if (this.size > 4) { + throw RuntimeException("ore than 8 bytes in input, potential overflow") + } + var uint = this.foldIndexed(0U) { index, acc, uByte -> acc or (uByte.toUInt() shl (24 - (index * 8))) } + return uint +} + +@ExperimentalUnsignedTypes +operator fun UInt.plus(other : Array) : Array { + return this.toLittleEndianUByteArray() + other +} \ No newline at end of file diff --git a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/ReadmeTest.kt b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/ReadmeTest.kt index ee449cb..4cea3e0 100644 --- a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/ReadmeTest.kt +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/ReadmeTest.kt @@ -19,8 +19,10 @@ package com.ionspin.kotlin.crypto import com.ionspin.kotlin.crypto.hash.blake2b.Blake2b import com.ionspin.kotlin.crypto.hash.sha.Sha256 import com.ionspin.kotlin.crypto.hash.sha.Sha512 +import com.ionspin.kotlin.crypto.keyderivation.argon2.Argon2 +import com.ionspin.kotlin.crypto.keyderivation.argon2.ArgonType import kotlin.test.Test - +import kotlin.test.assertEquals import kotlin.test.assertTrue /** @@ -73,7 +75,7 @@ class ReadmeTest { @ExperimentalStdlibApi @Test fun sha256Example() { - val input ="abc" + val input = "abc" val result = Sha256.digest(inputString = input) val expectedResult = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" assertTrue { @@ -86,9 +88,9 @@ class ReadmeTest { @ExperimentalStdlibApi @Test fun sha512Example() { - val input ="abc" + val input = "abc" val result = Sha512.digest(inputMessage = input.encodeToByteArray().map { it.toUByte() }.toTypedArray()) - println(result.map {it.toString(16)}) + println(result.map { it.toString(16) }) val expectedResult = "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a" + "2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f" assertTrue { @@ -124,4 +126,26 @@ class ReadmeTest { } + + @Test + fun argon2StringExample() { + val argon2Instance = Argon2( + password = "Password", + salt = "RandomSalt", + parallelism = 4, + tagLength = 64U, + requestedMemorySize = 32U, //Travis build on mac fails with higher values + numberOfIterations = 4U, + key = "", + associatedData = "", + argonType = ArgonType.Argon2id + ) + val tag = argon2Instance.derive() + val tagString = tag.map { it.toString(16).padStart(2, '0') }.joinToString(separator = "") + val expectedTagString = "ca134003c9f9f76ca8869359c1d9065603ec54ac30f5158f06af647cacaef2c1c3e" + + "c71e81960278c0596febc64125acbbe5959146db1c128199a1b7cb38982a9" + println("Tag: ${tagString}") + assertEquals(tagString, expectedTagString) + + } } \ No newline at end of file diff --git a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/SRNGTest.kt b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/SRNGTest.kt index 532da42..dba842a 100644 --- a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/SRNGTest.kt +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/SRNGTest.kt @@ -27,9 +27,11 @@ import kotlin.test.assertTrue class SRNGTest { @Test fun testSrng() { + //Just a sanity test, need to add better srng tests. val randomBytes1 = SRNG.getRandomBytes(10) val randomBytes2 = SRNG.getRandomBytes(10) -// assertTrue { !randomBytes1.contentEquals(randomBytes2) } - //TODO implement SRNG for minGW + randomBytes1.forEach { println("RB1: $it")} + randomBytes2.forEach { println("RB2: $it")} + assertTrue { !randomBytes1.contentEquals(randomBytes2) } } } \ No newline at end of file diff --git a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/hash/blake2b/Blake2BTest.kt b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/hash/blake2b/Blake2BTest.kt index d4bdd51..852e7dc 100644 --- a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/hash/blake2b/Blake2BTest.kt +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/hash/blake2b/Blake2BTest.kt @@ -17,6 +17,7 @@ package com.ionspin.kotlin.crypto.hash.blake2b import kotlin.test.Test +import kotlin.test.assertFailsWith import kotlin.test.assertTrue /** @@ -280,4 +281,13 @@ class Blake2BTest { } + + @Test + fun testInvalidHashLength() { + val test = "1234567890" + assertFailsWith(RuntimeException::class) { + val result = Blake2b.digest(inputString = test, hashLength = 65) + } + } + } \ 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 new file mode 100644 index 0000000..3cd6e49 --- /dev/null +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/hash/keyderivation/Argon2Test.kt @@ -0,0 +1,157 @@ +/* + * 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_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE") + +package com.ionspin.kotlin.crypto.hash.keyderivation + +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.test.Test +import kotlin.test.assertTrue + +/** + * Created by Ugljesa Jovanovic + * ugljesa.jovanovic@ionspin.com + * on 10-May-2020 + */ +@ExperimentalStdlibApi +class Argon2Test { + + @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) } + + } + + @Test + fun argon2idKATTest() { + val expected : Array = arrayOf( + 0x0dU, 0x64U, 0x0dU, 0xf5U, 0x8dU, 0x78U, 0x76U, 0x6cU, + 0x08U, 0xc0U, 0x37U, 0xa3U, 0x4aU, 0x8bU, 0x53U, 0xc9U, + 0xd0U, 0x1eU, 0xf0U, 0x45U, 0x2dU, 0x75U, 0xb6U, 0x5eU, + 0xb5U, 0x25U, 0x20U, 0xe9U, 0x6bU, 0x01U, 0xe6U, 0x59U + ) + + + 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.Argon2id + ) + val result = digest.derive() + result.hexColumsPrint(8) + assertTrue { expected.contentEquals(result) } + + } +} \ No newline at end of file diff --git a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbcTest.kt b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbcTest.kt index b55a715..f0bada0 100644 --- a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbcTest.kt +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbcTest.kt @@ -16,8 +16,8 @@ package com.ionspin.kotlin.crypto.symmetric -import com.ionspin.kotlin.crypto.hexStringToUByteArray -import com.ionspin.kotlin.crypto.toHexString +import com.ionspin.kotlin.crypto.util.hexStringToUByteArray +import com.ionspin.kotlin.crypto.util.toHexString import kotlin.test.Test import kotlin.test.assertTrue diff --git a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtrTest.kt b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtrTest.kt index e80a995..4b140b5 100644 --- a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtrTest.kt +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtrTest.kt @@ -16,8 +16,8 @@ package com.ionspin.kotlin.crypto.symmetric -import com.ionspin.kotlin.crypto.hexStringToUByteArray -import com.ionspin.kotlin.crypto.toHexString +import com.ionspin.kotlin.crypto.util.hexStringToUByteArray +import com.ionspin.kotlin.crypto.util.toHexString import kotlin.test.Test import kotlin.test.assertTrue diff --git a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/util/UtilTest.kt b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/util/UtilTest.kt new file mode 100644 index 0000000..407368a --- /dev/null +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/util/UtilTest.kt @@ -0,0 +1,109 @@ +/* + * 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. + */ + +package com.ionspin.kotlin.crypto.util + +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Created by Ugljesa Jovanovic + * ugljesa.jovanovic@ionspin.com + * on 17-Jul-2019 + */ +@ExperimentalUnsignedTypes +class UtilTest { + + @Test + fun testSlicer() { + val array = arrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17) + val chunked = array.chunked(2) + assertTrue { + chunked.size == 9 && chunked[8][0] == 17 + } + } + + @Test + fun testUIntToBigEndianArray() { + assertTrue { + val original = 1U + val converted = original.toBigEndianUByteArray() + converted[0] = 1U + true + } + assertTrue { + val original = 0xAABBCCDDU + val converted = original.toBigEndianUByteArray() + converted[0] == 0xAAU.toUByte() && + converted[1] == 0xBBU.toUByte() && + converted[2] == 0xCCU.toUByte() && + converted[3] == 0xDDU.toUByte() + + } + } + + @Test + fun testUIntToLittleEndianArray() { + assertTrue { + val original = 1U + val converted = original.toLittleEndianUByteArray() + converted[3] = 1U + true + } + assertTrue { + val original = 0xAABBCCDDU + val converted = original.toLittleEndianUByteArray() + converted[0] == 0xDDU.toUByte() && + converted[1] == 0xCCU.toUByte() && + converted[2] == 0xBBU.toUByte() && + converted[3] == 0xAAU.toUByte() + + } + assertTrue { + val original = 123456U + val converted = original.toLittleEndianUByteArray() + converted[0] == 0x40U.toUByte() && + converted[1] == 0xE2U.toUByte() && + converted[2] == 0x01U.toUByte() && + converted[3] == 0x00U.toUByte() + + } + } + + @Test + fun testFromBigEndianByteArrayToLong() { + + assertTrue { + val ubyteArray = ubyteArrayOf(0xA1U, 0xA2U, 0xB1U, 0xB2U, 0xC1U, 0xC2U, 0xD1U, 0xD2U).toTypedArray() + val expected = 0xA1A2B1B2C1C2D1D2U + val reconstructed = ubyteArray.fromBigEndianArrayToULong(); + reconstructed == expected + } + + } + + @Test + fun testFromLittleEndianByteArrayToLong() { + + assertTrue { + val ubyteArray = ubyteArrayOf(0xA1U, 0xA2U, 0xB1U, 0xB2U, 0xC1U, 0xC2U, 0xD1U, 0xD2U).toTypedArray() + val expected = 0xD2D1C2C1B2B1A2A1UL + val reconstructed = ubyteArray.fromLittleEndianArrayToULong(); + reconstructed == expected + } + + } +} \ No newline at end of file diff --git a/multiplatform-crypto/src/jsMain/kotlin/com/ionspin/kotlin/bignum/integer/Placeholder b/multiplatform-crypto/src/jsMain/kotlin/com/ionspin/kotlin/bignum/integer/Placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/multiplatform-crypto/src/jsMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt b/multiplatform-crypto/src/jsMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt index 0a7a85c..e7d7e7d 100644 --- a/multiplatform-crypto/src/jsMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt +++ b/multiplatform-crypto/src/jsMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt @@ -25,12 +25,27 @@ actual object SRNG { var counter = 0 @ExperimentalUnsignedTypes actual fun getRandomBytes(amount: Int): Array { -// val runningOnNode = js("(typeof window === 'undefined')").unsafeCast() -// if (runningOnNode) { -// js("var crypto = require('crypto')").asDynamic().randomBytes(amount) -// } else { -// throw RuntimeException("Secure random not supported yet for non-nodejs environment") -// } - return Array(amount) { (counter++).toUByte() } // TODO Wow. Such random. Very entropy. + val runningOnNode = js( + "if (typeof window === 'undefined') {\n" + + " true;\n" + + " } else {\n" + + " false;\n" + + " }" + ) + val randomBytes = if (runningOnNode) { + js("require('crypto')").randomBytes(amount).toJSON().data + } else { + js( + """ + var randomArray = new Uint8Array(amount); + var crypto = (self.crypto || self.msCrypto); + crypto.getRandomValues(randomArray); + """ + ) + var randomArrayResult = js("Array.prototype.slice.call(randomArray);") + randomArrayResult + } + + return randomBytes as Array } } \ No newline at end of file diff --git a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/UtilTest.kt b/multiplatform-crypto/src/jsTest/kotlin/com/ionspin/kotlin/crypto/SRNGJsTest.kt similarity index 73% rename from multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/UtilTest.kt rename to multiplatform-crypto/src/jsTest/kotlin/com/ionspin/kotlin/crypto/SRNGJsTest.kt index 79a6bbd..55981de 100644 --- a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/UtilTest.kt +++ b/multiplatform-crypto/src/jsTest/kotlin/com/ionspin/kotlin/crypto/SRNGJsTest.kt @@ -16,23 +16,26 @@ package com.ionspin.kotlin.crypto -import com.ionspin.kotlin.crypto.chunked import kotlin.test.Test import kotlin.test.assertTrue /** * Created by Ugljesa Jovanovic * ugljesa.jovanovic@ionspin.com - * on 17-Jul-2019 + * on 05-Jan-2020 */ -class UtilTest { +@ExperimentalUnsignedTypes +class SRNGJsTest { @Test - fun testSlicer() { - val array = arrayOf(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17) - val chunked = array.chunked(2) + fun testJsSrng() { + val bytes1 = SRNG.getRandomBytes(10) + val bytes2 = SRNG.getRandomBytes(10) assertTrue { - chunked.size == 9 && chunked[8][0] == 17 + !bytes1.contentEquals(bytes2) && + bytes1.size == 10 && + bytes2.size == 10 } + } } \ No newline at end of file diff --git a/multiplatform-crypto/src/mingwX64Main/kotlin/com/ionspin/kotlin/crypto/SRNG.kt b/multiplatform-crypto/src/mingwX64Main/kotlin/com/ionspin/kotlin/crypto/SRNG.kt new file mode 100644 index 0000000..f082a4b --- /dev/null +++ b/multiplatform-crypto/src/mingwX64Main/kotlin/com/ionspin/kotlin/crypto/SRNG.kt @@ -0,0 +1,43 @@ +/* + * 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. + */ + +package com.ionspin.kotlin.crypto + +import kotlinx.cinterop.* +import platform.windows.* + +/** + * Created by Ugljesa Jovanovic + * ugljesa.jovanovic@ionspin.com + * on 21-Sep-2019 + */ +actual object SRNG { + private val advapi by lazy { LoadLibraryA("ADVAPI32.DLL")} + + private val advapiRandom by lazy { + GetProcAddress(advapi, "SystemFunction036")?.reinterpret, ULong, Int>>>() ?: error("Failed getting advapi random") + } + + @Suppress("EXPERIMENTAL_UNSIGNED_LITERALS") + actual fun getRandomBytes(amount: Int): Array { + memScoped { + val randArray = allocArray(amount) + val pointer = randArray.getPointer(this) + val status = advapiRandom(pointer.reinterpret(), amount.convert()) + return Array(amount) { pointer[it].toUByte() } + } + } +} \ No newline at end of file diff --git a/multiplatform-crypto/src/mingwX64Test/kotlin/com/ionspin/kotlin/bignum/crypto/util/testBlocking.kt b/multiplatform-crypto/src/mingwX64Test/kotlin/com/ionspin/kotlin/bignum/crypto/util/testBlocking.kt new file mode 100644 index 0000000..328fcc4 --- /dev/null +++ b/multiplatform-crypto/src/mingwX64Test/kotlin/com/ionspin/kotlin/bignum/crypto/util/testBlocking.kt @@ -0,0 +1,27 @@ +/* + * 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. + */ + +package com.ionspin.kotlin.crypto.util + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking + +/** + * Created by Ugljesa Jovanovic + * ugljesa.jovanovic@ionspin.com + * on 20-Jul-2019 + */ +actual fun testBlocking(block: suspend (scope: CoroutineScope) -> Unit) = runBlocking { block(this) } \ No newline at end of file