Skip to content

Commit

Permalink
No longer export extension functions for utils; Renamed Argon2KtUtils…
Browse files Browse the repository at this point in the history
… -> Argon2KtBenchmark; Creaded static Argon2KtUtils with previous extension functions
  • Loading branch information
lambdapioneer committed Apr 12, 2020
1 parent 3964fb2 commit ede60ad
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
85 changes: 85 additions & 0 deletions lib/src/main/java/com/lambdapioneer/argon2kt/Argon2KtBenchmark.kt
Original file line number Diff line number Diff line change
@@ -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
}
141 changes: 91 additions & 50 deletions lib/src/main/java/com/lambdapioneer/argon2kt/Argon2KtUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
53 changes: 0 additions & 53 deletions lib/src/main/java/com/lambdapioneer/argon2kt/Utils.kt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down

0 comments on commit ede60ad

Please sign in to comment.