diff --git a/app/src/main/java/com/lambdapioneer/argon2kt/app/MainActivity.kt b/app/src/main/java/com/lambdapioneer/argon2kt/app/MainActivity.kt index 416db9f..314bf7e 100644 --- a/app/src/main/java/com/lambdapioneer/argon2kt/app/MainActivity.kt +++ b/app/src/main/java/com/lambdapioneer/argon2kt/app/MainActivity.kt @@ -18,7 +18,7 @@ import androidx.core.content.ContextCompat import com.lambdapioneer.argon2kt.Argon2Kt import com.lambdapioneer.argon2kt.Argon2Mode import com.lambdapioneer.argon2kt.Argon2Version -import com.lambdapioneer.argon2kt.decodeAsHex +import com.lambdapioneer.argon2kt.Argon2KtUtils import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { @@ -63,7 +63,7 @@ class MainActivity : AppCompatActivity() { val result = Argon2Kt().hash( mode = params.mode, password = params.passwordInUnicode.toByteArray(), - salt = params.saltInHex.decodeAsHex(), + salt = Argon2KtUtils.decodeAsHex(params.saltInHex), tCostInIterations = params.iterations, mCostInKibibyte = params.memory, version = params.version diff --git a/lib/src/androidTest/java/com/lambdapioneer/argon2kt/Argon2KtUtilsTest.kt b/lib/src/androidTest/java/com/lambdapioneer/argon2kt/Argon2KtBenchmarkTest.kt similarity index 89% rename from lib/src/androidTest/java/com/lambdapioneer/argon2kt/Argon2KtUtilsTest.kt rename to lib/src/androidTest/java/com/lambdapioneer/argon2kt/Argon2KtBenchmarkTest.kt index 5fbcd46..714e897 100644 --- a/lib/src/androidTest/java/com/lambdapioneer/argon2kt/Argon2KtUtilsTest.kt +++ b/lib/src/androidTest/java/com/lambdapioneer/argon2kt/Argon2KtBenchmarkTest.kt @@ -8,13 +8,13 @@ package com.lambdapioneer.argon2kt import org.assertj.core.api.Assertions.assertThat import org.junit.Test -class Argon2KtUtilsInstrumentedTest { +class Argon2KtBenchmarkUtilsInstrumentedTest { @Test fun searchIterationCountForArgon2_whenGivenSensibleConfiguration_thenResultSensible() { // As we cannot make assumptions about the tested device, we will just make sure that the returned iteration // count is in a sensible range. See the "Argon2KtUtilsUnitTest" for white-box tests of the underlying logic. - val iterationsCount = searchIterationCountForArgon2( + val iterationsCount = Argon2KtBenchmark.searchIterationCount( argon2Kt = Argon2Kt(), argon2Mode = Argon2Mode.ARGON2_ID, targetTimeMs = 1000, diff --git a/lib/src/main/java/com/lambdapioneer/argon2kt/Argon2KtBenchmark.kt b/lib/src/main/java/com/lambdapioneer/argon2kt/Argon2KtBenchmark.kt new file mode 100644 index 0000000..223d54d --- /dev/null +++ b/lib/src/main/java/com/lambdapioneer/argon2kt/Argon2KtBenchmark.kt @@ -0,0 +1,85 @@ +// Copyright (c) Daniel Hugenroth +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +package com.lambdapioneer.argon2kt + +import kotlin.math.ceil + +/** + * Utils class to help determining Argon2 parameters. + */ +class Argon2KtBenchmark private constructor() { + + companion object { + + /** + * Returns an iteration count for the given configuration that makes Argon2 take just above the given "targetTimeMs". + * Note that there can be vast differences between devices and debug/release builds. + * + * Do not rely on this method to make claims on "how long it would take to crack a password". However, it is helpful + * to choose an iteration count that provides sensible/convenient speed for a given configuration. + */ + fun searchIterationCount( + argon2Kt: Argon2Kt, + argon2Mode: Argon2Mode, + targetTimeMs: Long, + mCostInKibibyte: Int = ARGON2KT_DEFAULT_M_COST, + parallelism: Int = ARGON2KT_DEFAULT_PARALLELISM, + hashLengthInBytes: Int = ARGON2KT_DEFAULT_HASH_LENGTH, + version: Argon2Version = ARGON2KT_DEFAULT_VERSION + ): Int = + searchIterationCountForMethod(targetTimeMs) { tCostInIterations -> + argon2Kt.hash( + mode = argon2Mode, + password = "dummypassword".toByteArray(), + salt = "dummysalt".toByteArray(), + tCostInIterations = tCostInIterations, + mCostInKibibyte = mCostInKibibyte, + parallelism = parallelism, + hashLengthInBytes = hashLengthInBytes, + version = version + ) + } + } +} + +/** See [searchIterationCountForMetric] */ +internal fun searchIterationCountForMethod(targetTimeMs: Long, methodToMeasure: (Int) -> Unit) = + searchIterationCountForMetric( + targetTimeMs + ) { + val start = System.nanoTime() + methodToMeasure(it) + (System.nanoTime() - start) / 1000000 + } + +/** + * Returns an iteration count that results in the "measureTimeMs" to take more than "targetTimeMs". It assumes that the + * measured time increases (roughly) proportionally with the number of iterations. + */ +internal fun searchIterationCountForMetric( + targetTimeMs: Long, + iterationToMetric: (Int) -> Long +): Int { + var iterations = 1 + var iterationsTime = iterationToMetric(iterations) + + while (iterationsTime <= targetTimeMs) { + checkArgument(iterationsTime > 0, "The to-be measured method must always take >0ms\"") + val timePerIteration = iterationsTime.toFloat() / iterations.toFloat() + + // approximate assuming proportional relationship: iterations ~ time + val newIterations = ceil(targetTimeMs.toFloat() / timePerIteration).toInt() + + iterations = if (newIterations <= iterations) + newIterations + 1 // avoid infinite loop by growing strictly monotonically + else + newIterations + + iterationsTime = iterationToMetric(iterations) + } + + return iterations +} diff --git a/lib/src/main/java/com/lambdapioneer/argon2kt/Argon2KtUtils.kt b/lib/src/main/java/com/lambdapioneer/argon2kt/Argon2KtUtils.kt index 626627f..bca48af 100644 --- a/lib/src/main/java/com/lambdapioneer/argon2kt/Argon2KtUtils.kt +++ b/lib/src/main/java/com/lambdapioneer/argon2kt/Argon2KtUtils.kt @@ -5,69 +5,110 @@ package com.lambdapioneer.argon2kt -import kotlin.math.ceil +import java.nio.ByteBuffer +import java.util.* /** - * Returns an iteration count for the given configuration that makes Argon2 take just above the given "targetTimeMs". - * Note that there can be vast differences between devices and debug/release builds. + * Decodes a [String] holding a hexadecimal encoding as a [ByteArray]. * - * Do not rely on this method to make claims on "how long it would take to crack a password". However, it is helpful - * to choose an iteration count that provides sensible/convenient speed for a given configuration. + * @return [ByteArray] which length is half the string's length. + * + * @throw [IllegalAccessException] Will throw if it encounters illegal characters (i.e. not 0-9a-fA-F) or if the String + * has an odd length. */ -fun searchIterationCountForArgon2( - argon2Kt: Argon2Kt, - argon2Mode: Argon2Mode, - targetTimeMs: Long, - mCostInKibibyte: Int = ARGON2KT_DEFAULT_M_COST, - parallelism: Int = ARGON2KT_DEFAULT_PARALLELISM, - hashLengthInBytes: Int = ARGON2KT_DEFAULT_HASH_LENGTH, - version: Argon2Version = ARGON2KT_DEFAULT_VERSION -): Int = - searchIterationCountForMethod(targetTimeMs) { tCostInIterations -> - argon2Kt.hash( - mode = argon2Mode, - password = "dummypassword".toByteArray(), - salt = "dummysalt".toByteArray(), - tCostInIterations = tCostInIterations, - mCostInKibibyte = mCostInKibibyte, - parallelism = parallelism, - hashLengthInBytes = hashLengthInBytes, - version = version - ) +internal fun String.decodeAsHex(): ByteArray { + checkArgument(this.length % 2 == 0, "A valid hex string must have an even number of characters") + + return ByteArray(this.length / 2) { + this.substring(2 * it, 2 * it + 2).toInt(radix = 16).toByte() } +} -/** See "searchIterationCountForMetricMethod" */ -internal fun searchIterationCountForMethod(targetTimeMs: Long, methodToMeasure: (Int) -> Unit) = - searchIterationCountForMetric( - targetTimeMs - ) { - val start = System.nanoTime() - methodToMeasure(it) - (System.nanoTime() - start) / 1000000 +/** + * Encodes a byte array into a hexadecimal encoded String. + * + * @param uppercase If true uppercase letters are used (A..F), otherwise lowercase letters are used (a..f). + * + * @return [String] which length the twice the [ByteArray]'s length. + */ +internal fun ByteArray.encodeAsHex(uppercase: Boolean = true): String { + val sb = java.lang.StringBuilder(size * 2) + val formatString = if (uppercase) "%02X" else "%02x" + + for (b in this) { + sb.append(String.format(formatString, b)) } + return sb.toString() +} + +/** + * Overwrites the bytes of a byte buffer with random bytes. The method asserts that the buffer is a direct buffer as a + * precondition. + * + * @param random The random generator to use for overwriting. Default's to Java's standard [Random] implementation. + * However, you might want to use a [java.security.SecureRandom] source for more adverse threat models. + * + * @throws [IllegalStateException] if the buffer [ByteBuffer.isDirect] is false. + */ +internal fun ByteBuffer.wipeDirectBuffer(random: Random = Random()) { + if (!this.isDirect) throw IllegalStateException("Only direct-allocated byte buffers can be meaningfully wiped") + + val arr = ByteArray(this.capacity()) + this.rewind() + + // overwrite bytes (actually overwrites the memory since it is a direct buffer) + random.nextBytes(arr) + this.put(arr) +} + +/** If the assertion holds nothing happens. Otherwise, an IllegalArgumentException is thrown with the given message. */ +internal fun checkArgument(assertion: Boolean, message: String) { + if (!assertion) throw IllegalArgumentException(message) +} + /** - * Returns an iteration count that results in the "measureTimeMs" to take more than "targetTimeMs". It assumes that the - * measured time increases (roughly) proportionally with the number of iterations. + * Util class with helper methods for dealing with HEX encodings and [ByteBuffer] objects. */ -internal fun searchIterationCountForMetric(targetTimeMs: Long, iterationToMetric: (Int) -> Long): Int { - var iterations = 1 - var iterationsTime = iterationToMetric(iterations) +class Argon2KtUtils private constructor() { - while (iterationsTime <= targetTimeMs) { - checkArgument(iterationsTime > 0, "The to-be measured method must always take >0ms\"") - val timePerIteration = iterationsTime.toFloat() / iterations.toFloat() + companion object { - // approximate assuming proportional relationship: iterations ~ time - val newIterations = ceil(targetTimeMs.toFloat() / timePerIteration).toInt() + /** + * Decodes a [String] holding a hexadecimal encoding as a [ByteArray]. + * + * @param string The string holding the hexadecimal encoding. + * + * @return [ByteArray] which length is half the string's length. + * + * @throw [IllegalAccessException] Will throw if it encounters illegal characters (i.e. not 0-9a-fA-F) or if the String + * has an odd length. + */ + fun decodeAsHex(string: String): ByteArray = string.decodeAsHex() - iterations = if (newIterations <= iterations) - newIterations + 1 // avoid infinite loop by growing strictly monotonically - else - newIterations + /** + * Encodes a byte array into a hexadecimal encoded String. + * + * @param byteArray The [ByteArray] to convert. + * @param uppercase If true uppercase letters are used (A..F), otherwise lowercase letters are used (a..f). + * + * @return [String] which length the twice the [ByteArray]'s length. + */ + fun ByteArray.encodeAsHex(byteArray: ByteArray, uppercase: Boolean = true) = + byteArray.encodeAsHex(uppercase) - iterationsTime = iterationToMetric(iterations) - } - return iterations + /** + * Overwrites the bytes of a byte buffer with random bytes. The method asserts that the buffer is a direct buffer as a + * precondition. + * + * @param byteBuffer THe [ByteBuffer] to overwrite. Must be directly allocated. + * @param random The random generator to use for overwriting. Default's to Java's standard [Random] implementation. + * However, you might want to use a [java.security.SecureRandom] source for more adverse threat models. + * + * @throws [IllegalStateException] if the buffer [ByteBuffer.isDirect] is false. + */ + fun ByteBuffer.wipeDirectBuffer(byteBuffer: ByteBuffer, random: Random = Random()) = + byteBuffer.wipeDirectBuffer(random) + } } diff --git a/lib/src/main/java/com/lambdapioneer/argon2kt/Utils.kt b/lib/src/main/java/com/lambdapioneer/argon2kt/Utils.kt deleted file mode 100644 index f1859ed..0000000 --- a/lib/src/main/java/com/lambdapioneer/argon2kt/Utils.kt +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Daniel Hugenroth -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. - -package com.lambdapioneer.argon2kt - -import java.nio.ByteBuffer -import java.util.* - -/** - * Decodes a hexadecimal String. Will throw if it encounters illegal characters (i.e. not 0-9a-fA-F) or if the String - * has an odd length. - */ -fun String.decodeAsHex(): ByteArray { - checkArgument(this.length % 2 == 0, "A valid hex string must have an even number of characters") - - return ByteArray(this.length / 2) { - this.substring(2 * it, 2 * it + 2).toInt(radix = 16).toByte() - } -} - -/** Encodes a byte array into a hexadecimal String. */ -fun ByteArray.encodeAsHex(uppercase: Boolean = true): String { - val sb = java.lang.StringBuilder(size * 2) - val formatString = if (uppercase) "%02X" else "%02x" - - for (b in this) { - sb.append(String.format(formatString, b)) - } - - return sb.toString() -} - -/** - * Overwrites the bytes of a byte buffer with random bytes. The method asserts that the buffer is a direct buffer as a - * precondition. - */ -fun ByteBuffer.wipeDirectBuffer(random: Random = Random()) { - if (!this.isDirect) throw IllegalStateException("Only direct-allocated byte buffers can be meaningfully wiped") - - val arr = ByteArray(this.capacity()) - this.rewind() - - // overwrite bytes (actually overwrites the memory since it is a direct buffer) - random.nextBytes(arr) - this.put(arr) -} - -/** If the assertion holds nothing happens. Otherwise, an IllegalArgumenException is thrown with the given message. */ -internal fun checkArgument(assertion: Boolean, message: String) { - if (!assertion) throw IllegalArgumentException(message) -} diff --git a/lib/src/test/java/com/lambdapioneer/argon2kt/Argon2KtUtilsUnitTest.kt b/lib/src/test/java/com/lambdapioneer/argon2kt/Argon2KtBenchmarkUnitTest.kt similarity index 97% rename from lib/src/test/java/com/lambdapioneer/argon2kt/Argon2KtUtilsUnitTest.kt rename to lib/src/test/java/com/lambdapioneer/argon2kt/Argon2KtBenchmarkUnitTest.kt index b1e4929..eaf64a3 100644 --- a/lib/src/test/java/com/lambdapioneer/argon2kt/Argon2KtUtilsUnitTest.kt +++ b/lib/src/test/java/com/lambdapioneer/argon2kt/Argon2KtBenchmarkUnitTest.kt @@ -8,7 +8,7 @@ package com.lambdapioneer.argon2kt import org.assertj.core.api.Assertions.assertThat import org.junit.Test -class Argon2KtUtilsUnitTest { +class Argon2KtBenchmarkUnitTest { @Test(expected = IllegalArgumentException::class) fun searchIterationCountForMetric_whenMetricReturns0_thenThrows() { diff --git a/lib/src/test/java/com/lambdapioneer/argon2kt/UtilsTest.kt b/lib/src/test/java/com/lambdapioneer/argon2kt/Argon2KtUtilsTest.kt similarity index 98% rename from lib/src/test/java/com/lambdapioneer/argon2kt/UtilsTest.kt rename to lib/src/test/java/com/lambdapioneer/argon2kt/Argon2KtUtilsTest.kt index 21c4214..6681a7b 100644 --- a/lib/src/test/java/com/lambdapioneer/argon2kt/UtilsTest.kt +++ b/lib/src/test/java/com/lambdapioneer/argon2kt/Argon2KtUtilsTest.kt @@ -8,7 +8,7 @@ package com.lambdapioneer.argon2kt import org.assertj.core.api.Assertions.assertThat import org.junit.Test -class UtilsTest { +class Argon2KtUtilsTest { @Test fun decode_whenEmptyString_thenEmptyArray() {