From 6e9ccc593f40b0908fa2293eee5caabfe89f2c06 Mon Sep 17 00:00:00 2001 From: Dominik Dzienia Date: Tue, 24 Mar 2020 22:22:23 +0100 Subject: [PATCH] Support for master password and storing password as hashes (HMAC) instead of plaintext, additional crypto utils with tests (partialy for in-progress pref export encryption), changed PasswordCheck to more common UI and to use lambdas (will need given password in lambda callback for prefs enc) --- .../activities/MyPreferenceFragment.kt | 45 +++++- .../nightscout/androidaps/utils/CryptoUtil.kt | 142 ++++++++++++++++++ .../utils/protection/PasswordCheck.kt | 60 +++++++- .../utils/protection/ProtectionCheck.kt | 12 +- app/src/main/res/drawable/ic_key.xml | 9 ++ app/src/main/res/drawable/ic_key_48dp.xml | 17 +++ app/src/main/res/layout/passwordprompt.xml | 10 +- app/src/main/res/values/protection.xml | 13 +- app/src/main/res/xml/pref_general.xml | 12 +- .../androidaps/utils/CryptoUtilTest.kt | 76 ++++++++++ 10 files changed, 369 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/info/nightscout/androidaps/utils/CryptoUtil.kt create mode 100644 app/src/main/res/drawable/ic_key.xml create mode 100644 app/src/main/res/drawable/ic_key_48dp.xml create mode 100644 app/src/test/java/info/nightscout/androidaps/utils/CryptoUtilTest.kt diff --git a/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt b/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt index 86bee1bca2..122ec8d5f0 100644 --- a/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt @@ -50,8 +50,10 @@ import info.nightscout.androidaps.plugins.source.EversensePlugin import info.nightscout.androidaps.plugins.source.GlimpPlugin import info.nightscout.androidaps.plugins.source.PoctechPlugin import info.nightscout.androidaps.plugins.source.TomatoPlugin +import info.nightscout.androidaps.utils.CryptoUtil import info.nightscout.androidaps.utils.OKDialog.show import info.nightscout.androidaps.utils.SafeParse +import info.nightscout.androidaps.utils.protection.PasswordCheck import info.nightscout.androidaps.utils.protection.ProtectionCheck import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.sharedPreferences.SP @@ -98,6 +100,8 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang @Inject lateinit var wearPlugin: WearPlugin @Inject lateinit var maintenancePlugin: MaintenancePlugin + @Inject lateinit var passwordCheck: PasswordCheck + @Inject lateinit var androidInjector: DispatchingAndroidInjector override fun androidInjector(): AndroidInjector = androidInjector @@ -254,19 +258,19 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang // Preferences if (pref.getKey() == resourceHelper.gs(R.string.key_settings_protection)) { val pass: Preference? = findPreference(resourceHelper.gs(R.string.key_settings_password)) - if (pass != null) pass.isEnabled = pref.value == ProtectionCheck.ProtectionType.PASSWORD.ordinal.toString() + if (pass != null) pass.isEnabled = pref.value == ProtectionCheck.ProtectionType.CUSTOM_PASSWORD.ordinal.toString() } // Application // Application if (pref.getKey() == resourceHelper.gs(R.string.key_application_protection)) { val pass: Preference? = findPreference(resourceHelper.gs(R.string.key_application_password)) - if (pass != null) pass.isEnabled = pref.value == ProtectionCheck.ProtectionType.PASSWORD.ordinal.toString() + if (pass != null) pass.isEnabled = pref.value == ProtectionCheck.ProtectionType.CUSTOM_PASSWORD.ordinal.toString() } // Bolus // Bolus if (pref.getKey() == resourceHelper.gs(R.string.key_bolus_protection)) { val pass: Preference? = findPreference(resourceHelper.gs(R.string.key_bolus_password)) - if (pass != null) pass.isEnabled = pref.value == ProtectionCheck.ProtectionType.PASSWORD.ordinal.toString() + if (pass != null) pass.isEnabled = pref.value == ProtectionCheck.ProtectionType.CUSTOM_PASSWORD.ordinal.toString() } } if (pref is EditTextPreference) { @@ -281,6 +285,16 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang } } } + + if (pref is Preference) { + if ((pref.getKey() != null) && (pref.getKey().contains("_password"))) { + if (sp.getString(pref.getKey(), "").startsWith("hmac:")) { + pref.setSummary("******") + } else { + pref.setSummary(resourceHelper.gs(R.string.password_not_set)) + } + } + } pref?.let { adjustUnitDependentPrefs(it) } } @@ -294,4 +308,29 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang updatePrefSummary(p) } } + + // We use Preference and custom editor instead of EditTextPreference + // to hash password while it is saved and never have to show it, even hashed + + override fun onPreferenceTreeClick(preference: Preference?): Boolean { + if (preference != null) { + if (preference.key == resourceHelper.gs(R.string.key_master_password)) { + passwordCheck.setPassword(this.context!!, R.string.master_password, R.string.key_master_password) + return true; + } + if (preference.key == resourceHelper.gs(R.string.key_settings_password)) { + passwordCheck.setPassword(this.context!!, R.string.settings_password, R.string.key_settings_password) + return true; + } + if (preference.key == resourceHelper.gs(R.string.key_bolus_password)) { + passwordCheck.setPassword(this.context!!, R.string.bolus_password, R.string.key_bolus_password) + return true; + } + if (preference.key == resourceHelper.gs(R.string.key_application_password)) { + passwordCheck.setPassword(this.context!!, R.string.application_password, R.string.key_application_password) + return true; + } + } + return super.onPreferenceTreeClick(preference) + } } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/CryptoUtil.kt b/app/src/main/java/info/nightscout/androidaps/utils/CryptoUtil.kt new file mode 100644 index 0000000000..745f4449ee --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/utils/CryptoUtil.kt @@ -0,0 +1,142 @@ +package info.nightscout.androidaps.utils + +import org.spongycastle.util.encoders.Base64 +import java.nio.ByteBuffer +import java.security.MessageDigest +import java.security.SecureRandom +import java.security.spec.KeySpec +import javax.crypto.Cipher +import javax.crypto.Mac +import javax.crypto.SecretKey +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.PBEKeySpec +import javax.crypto.spec.SecretKeySpec + +private val HEX_CHARS = "0123456789abcdef" +private val HEX_CHARS_ARRAY = "0123456789abcdef".toCharArray() + +fun String.hexStringToByteArray() : ByteArray { + + val upperCased = this.toLowerCase() + val result = ByteArray(length / 2) + for (i in 0 until length step 2) { + val firstIndex = HEX_CHARS.indexOf(upperCased[i]); + val secondIndex = HEX_CHARS.indexOf(upperCased[i + 1]); + + val octet = firstIndex.shl(4).or(secondIndex) + result.set(i.shr(1), octet.toByte()) + } + + return result +} + +fun ByteArray.toHex() : String{ + val result = StringBuffer() + + forEach { + val octet = it.toInt() + val firstIndex = (octet and 0xF0).ushr(4) + val secondIndex = octet and 0x0F + result.append(HEX_CHARS_ARRAY[firstIndex]) + result.append(HEX_CHARS_ARRAY[secondIndex]) + } + + return result.toString() +} + +object CryptoUtil { + + private const val IV_LENGTH_BYTE = 12 + private const val TAG_LENGTH_BIT = 128 + private const val AES_KEY_SIZE_BIT = 256 + private const val PBKDF2_ITERATIONS = 50000 // check delays it cause on real device + private const val SALT_SIZE_BYTE = 32 + + private val secureRandom: SecureRandom = SecureRandom() + + fun sha256(source: String): String { + val digest = MessageDigest.getInstance("SHA-256") + val hashRaw = digest.digest(source.toByteArray()) + return hashRaw.toHex() + } + + fun hmac256(str: String, secret: String): String? { + val sha256_HMAC = Mac.getInstance("HmacSHA256") + val secretKey = SecretKeySpec(secret.toByteArray(), "HmacSHA256") + sha256_HMAC.init(secretKey) + return sha256_HMAC.doFinal(str.toByteArray()).toHex() + } + + private fun prepCipherKey(passPhrase: String, salt:ByteArray, iterationCount:Int = PBKDF2_ITERATIONS, keyStrength:Int = AES_KEY_SIZE_BIT): SecretKeySpec { + val factory: SecretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA1") + val spec: KeySpec = PBEKeySpec(passPhrase.toCharArray(), salt, iterationCount, keyStrength) + val tmp: SecretKey = factory.generateSecret(spec) + return SecretKeySpec(tmp.getEncoded(), "AES") + } + + fun mineSalt(len :Int = SALT_SIZE_BYTE): ByteArray { + val salt = ByteArray(len) + secureRandom.nextBytes(salt) + return salt + } + + fun encrypt(passPhrase: String, salt:ByteArray, rawData: String ): String? { + val iv: ByteArray? + val encrypted: ByteArray? + return try { + iv = ByteArray(IV_LENGTH_BYTE) + secureRandom.nextBytes(iv) + val cipherEnc: Cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipherEnc.init(Cipher.ENCRYPT_MODE, prepCipherKey(passPhrase, salt), GCMParameterSpec(TAG_LENGTH_BIT, iv)) + encrypted = cipherEnc.doFinal(rawData.toByteArray()) + val byteBuffer: ByteBuffer = ByteBuffer.allocate(1 + iv.size + encrypted.size) + byteBuffer.put(iv.size.toByte()) + byteBuffer.put(iv) + byteBuffer.put(encrypted) + String(Base64.encode(byteBuffer.array())) + } catch (e: Exception) { + null + } + } + + fun decrypt(passPhrase: String, salt:ByteArray, encryptedData: String): String? { + val iv: ByteArray? + val encrypted: ByteArray? + return try { + val byteBuffer = ByteBuffer.wrap(Base64.decode(encryptedData)) + val ivLength = byteBuffer.get().toInt() + iv = ByteArray(ivLength) + byteBuffer[iv] + encrypted = ByteArray(byteBuffer.remaining()) + byteBuffer[encrypted] + val cipherDec: Cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipherDec.init(Cipher.DECRYPT_MODE, prepCipherKey(passPhrase, salt), GCMParameterSpec(TAG_LENGTH_BIT, iv)) + val dec = cipherDec.doFinal(encrypted) + String(dec) + } catch (e: Exception) { + null + } + } + + fun checkPassword(password: String, referenceHash: String): Boolean { + return if (referenceHash.startsWith("hmac:")) { + val hashSegments = referenceHash.split(":") + if (hashSegments.size != 3) + return false + return hmac256(password, hashSegments[1]) == hashSegments[2] + } else { + password == referenceHash + } + } + + fun hashPassword(password: String): String { + return if (!password.startsWith("hmac:")) { + val salt = mineSalt().toHex() + return "hmac:${salt}:${hmac256(password, salt)}" + } else { + password + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/protection/PasswordCheck.kt b/app/src/main/java/info/nightscout/androidaps/utils/protection/PasswordCheck.kt index 6a2e18c732..4d1adfb233 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/protection/PasswordCheck.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/protection/PasswordCheck.kt @@ -2,13 +2,17 @@ package info.nightscout.androidaps.utils.protection import android.annotation.SuppressLint import android.app.AlertDialog +import android.content.Context import android.view.LayoutInflater import android.view.View import android.widget.EditText +import android.widget.ImageView import android.widget.TextView import androidx.annotation.StringRes +import androidx.appcompat.view.ContextThemeWrapper import androidx.fragment.app.FragmentActivity import info.nightscout.androidaps.R +import info.nightscout.androidaps.utils.CryptoUtil import info.nightscout.androidaps.utils.ToastUtils import info.nightscout.androidaps.utils.sharedPreferences.SP import javax.inject.Inject @@ -18,34 +22,74 @@ import javax.inject.Singleton class PasswordCheck @Inject constructor(val sp: SP) { @SuppressLint("InflateParams") - fun queryPassword(activity: FragmentActivity, @StringRes labelId: Int, @StringRes preference: Int, ok: Runnable?, cancel: Runnable? = null, fail: Runnable? = null) { + fun queryPassword(activity: FragmentActivity, @StringRes labelId: Int, @StringRes preference: Int, ok: ( (String) -> Unit)?, cancel: (()->Unit)? = null, fail: (()->Unit)? = null) { val password = sp.getString(preference, "") if (password == "") { - ok?.run() + ok?.invoke("") return } + + val titleLayout = activity.layoutInflater.inflate(R.layout.dialog_alert_custom, null) + (titleLayout.findViewById(R.id.alertdialog_title) as TextView).text = activity.getString(labelId) + (titleLayout.findViewById(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_key_48dp) + val promptsView = LayoutInflater.from(activity).inflate(R.layout.passwordprompt, null) - val alertDialogBuilder = AlertDialog.Builder(activity) + val alertDialogBuilder = AlertDialog.Builder(ContextThemeWrapper(activity, R.style.AppTheme)) alertDialogBuilder.setView(promptsView) - val label = promptsView.findViewById(R.id.passwordprompt_text) as TextView - label.text = activity.getString(labelId) val userInput = promptsView.findViewById(R.id.passwordprompt_pass) as EditText alertDialogBuilder .setCancelable(false) + .setCustomTitle(titleLayout) .setPositiveButton(activity.getString(R.string.ok)) { _, _ -> val enteredPassword = userInput.text.toString() - if (password == enteredPassword) ok?.run() + if (CryptoUtil.checkPassword(enteredPassword, password)) ok?.invoke(enteredPassword) else { ToastUtils.showToastInUiThread(activity, activity.getString(R.string.wrongpassword)) - fail?.run() + fail?.invoke() } } .setNegativeButton(activity.getString(R.string.cancel) ) { dialog, _ -> - cancel?.run() + cancel?.invoke() + dialog.cancel() + } + + alertDialogBuilder.create().show() + } + + @SuppressLint("InflateParams") + fun setPassword(context: Context, @StringRes labelId: Int, @StringRes preference: Int) { + val promptsView = LayoutInflater.from(context).inflate(R.layout.passwordprompt, null) + + val titleLayout = LayoutInflater.from(context).inflate(R.layout.dialog_alert_custom, null) + (titleLayout.findViewById(R.id.alertdialog_title) as TextView).text = context.getText(labelId) + (titleLayout.findViewById(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_key_48dp) + + val alertDialogBuilder = AlertDialog.Builder(ContextThemeWrapper(context, R.style.AppTheme)) + alertDialogBuilder.setView(promptsView) + + val userInput = promptsView.findViewById(R.id.passwordprompt_pass) as EditText + + alertDialogBuilder + .setCancelable(false) + .setCustomTitle(titleLayout) + .setPositiveButton(context.getString(R.string.ok)) { _, _ -> + val enteredPassword = userInput.text.toString() + if (enteredPassword.isNotEmpty()) { + sp.putString(preference, CryptoUtil.hashPassword(enteredPassword)) + } else { + if (sp.contains(preference)) { + sp.remove(preference) + } + } + ToastUtils.showToastInUiThread(context, context.getString(R.string.password_set)) + } + .setNegativeButton(context.getString(R.string.cancel) + ) { dialog, _ -> + ToastUtils.showToastInUiThread(context, context.getString(R.string.password_not_changed)) dialog.cancel() } diff --git a/app/src/main/java/info/nightscout/androidaps/utils/protection/ProtectionCheck.kt b/app/src/main/java/info/nightscout/androidaps/utils/protection/ProtectionCheck.kt index c4960f4999..a1cca9116e 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/protection/ProtectionCheck.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/protection/ProtectionCheck.kt @@ -21,7 +21,8 @@ class ProtectionCheck @Inject constructor( enum class ProtectionType { NONE, BIOMETRIC, - PASSWORD + MASTER_PASSWORD, + CUSTOM_PASSWORD } private val passwordsResourceIDs = listOf( @@ -43,7 +44,8 @@ class ProtectionCheck @Inject constructor( return when (ProtectionType.values()[sp.getInt(protectionTypeResourceIDs[protection.ordinal], ProtectionType.NONE.ordinal)]) { ProtectionType.NONE -> false ProtectionType.BIOMETRIC -> true - ProtectionType.PASSWORD -> sp.getString(passwordsResourceIDs[protection.ordinal], "") != "" + ProtectionType.MASTER_PASSWORD -> sp.getString(R.string.key_master_password, "") != "" + ProtectionType.CUSTOM_PASSWORD -> sp.getString(passwordsResourceIDs[protection.ordinal], "") != "" } } @@ -55,8 +57,10 @@ class ProtectionCheck @Inject constructor( ok?.run() ProtectionType.BIOMETRIC -> BiometricCheck.biometricPrompt(activity, titleResourceIDs[protection.ordinal], ok, cancel, fail) - ProtectionType.PASSWORD -> - passwordCheck.queryPassword(activity, titleResourceIDs[protection.ordinal], passwordsResourceIDs[protection.ordinal], ok, cancel, fail) + ProtectionType.MASTER_PASSWORD -> + passwordCheck.queryPassword(activity, R.string.master_password, R.string.key_master_password, { ok?.run() }, { cancel?.run() }, { fail?.run() }) + ProtectionType.CUSTOM_PASSWORD -> + passwordCheck.queryPassword(activity, titleResourceIDs[protection.ordinal], passwordsResourceIDs[protection.ordinal], { ok?.run() }, { cancel?.run() }, { fail?.run() }) } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_key.xml b/app/src/main/res/drawable/ic_key.xml new file mode 100644 index 0000000000..4d4a6fa28d --- /dev/null +++ b/app/src/main/res/drawable/ic_key.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_key_48dp.xml b/app/src/main/res/drawable/ic_key_48dp.xml new file mode 100644 index 0000000000..813dc24d00 --- /dev/null +++ b/app/src/main/res/drawable/ic_key_48dp.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/passwordprompt.xml b/app/src/main/res/layout/passwordprompt.xml index 63ff30e531..ed2298e4d8 100644 --- a/app/src/main/res/layout/passwordprompt.xml +++ b/app/src/main/res/layout/passwordprompt.xml @@ -5,17 +5,13 @@ android:orientation="vertical" android:padding="10dp" > - - + android:hint="@string/password_hint" + android:inputType="textPassword" + > diff --git a/app/src/main/res/values/protection.xml b/app/src/main/res/values/protection.xml index 6bf675ab44..8b025f68cb 100644 --- a/app/src/main/res/values/protection.xml +++ b/app/src/main/res/values/protection.xml @@ -5,15 +5,22 @@ Settings protection Application protection Bolus protection + Master password Settings password Application password Bolus password Unlock settings Biometric - Password + Custom password No protection Protection + Password set! + Password not set + Password not changed + Enter password here + + master_password settings_password application_password translatable="false"bolus_password @@ -24,13 +31,15 @@ @string/noprotection @string/biometric - @string/password + @string/master_password + @string/custom_password 0 1 2 + 3 diff --git a/app/src/main/res/xml/pref_general.xml b/app/src/main/res/xml/pref_general.xml index 6daf95f3f8..b5f7074c7e 100644 --- a/app/src/main/res/xml/pref_general.xml +++ b/app/src/main/res/xml/pref_general.xml @@ -22,6 +22,12 @@ + + - @@ -41,7 +47,7 @@ android:key="@string/key_application_protection" android:title="@string/application_protection" /> - @@ -53,7 +59,7 @@ android:key="@string/key_bolus_protection" android:title="@string/bolus_protection" /> - diff --git a/app/src/test/java/info/nightscout/androidaps/utils/CryptoUtilTest.kt b/app/src/test/java/info/nightscout/androidaps/utils/CryptoUtilTest.kt new file mode 100644 index 0000000000..c147c0810c --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/utils/CryptoUtilTest.kt @@ -0,0 +1,76 @@ +package info.nightscout.androidaps.utils + +import info.nightscout.androidaps.TestBase +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import org.powermock.core.classloader.annotations.PowerMockIgnore +import org.powermock.modules.junit4.PowerMockRunner + +@PowerMockIgnore("javax.crypto.*") +@RunWith(PowerMockRunner::class) +class CryptoUtilTest: TestBase() { + + @Test + fun testFixedSaltCrypto() { + val salt = byteArrayOf( + -33, -29, 16, -19, 99, -111, -3, 2, 116, 106, 47, 38, -54, 11, -77, 28, + 111, -15, -65, -110, 4, -32, -29, -70, -95, -88, -53, 19, 87, -103, 123, -15) + + val password = "thisIsFixedPassword" + val payload = "FIXED-PAYLOAD" + val encrypted = CryptoUtil.encrypt(password, salt, payload) + + Assert.assertNotNull(encrypted) + val decrypted = CryptoUtil.decrypt(password, salt, encrypted!!) + Assert.assertEquals(decrypted, payload) + } + + @Test + fun testStandardCrypto() { + val salt = CryptoUtil.mineSalt() + + val password = "topSikret" + val payload = "{what:payloadYouWantToProtect}" + val encrypted = CryptoUtil.encrypt(password, salt, payload) + + Assert.assertNotNull(encrypted) + val decrypted = CryptoUtil.decrypt(password, salt, encrypted!!) + Assert.assertEquals(decrypted, payload) + } + + @Test + fun testHashVector() { + val payload = "{what:payloadYouWantToProtect}" + val hash = CryptoUtil.sha256(payload) + Assert.assertEquals(hash, "a1aafe3ed6cc127e6d102ddbc40a205147230e9cfd178daf108c83543bbdcd13") + } + + @Test + fun testHmac() { + val payload = "{what:payloadYouWantToProtect}" + val password = "topSikret" + val expectedHmac = "ea2213953d0f2e55047cae2d23fb4f0de1b805d55e6271efa70d6b85fb692bea" // generated using other HMAC tool + val hash = CryptoUtil.hmac256(payload, password) + Assert.assertEquals(hash, expectedHmac) + } + + @Test + fun testPlainPasswordCheck() { + Assert.assertTrue(CryptoUtil.checkPassword("same", "same")) + Assert.assertFalse(CryptoUtil.checkPassword("same", "other")) + } + + @Test + fun testHashedPasswordCheck() { + Assert.assertTrue(CryptoUtil.checkPassword("givenSecret", CryptoUtil.hashPassword("givenSecret"))) + Assert.assertFalse(CryptoUtil.checkPassword("givenSecret", CryptoUtil.hashPassword("otherSecret"))) + + Assert.assertTrue(CryptoUtil.checkPassword("givenHashToCheck", "hmac:7fe5f9c7b4b97c5d32d5cfad9d07473543a9938dc07af48a46dbbb49f4f68c12:a0c7cee14312bbe31b51359a67f0d2dfdf46813f319180269796f1f617a64be1")) + Assert.assertFalse(CryptoUtil.checkPassword("givenMashToCheck", "hmac:7fe5f9c7b4b97c5d32d5cfad9d07473543a9938dc07af48a46dbbb49f4f68c12:a0c7cee14312bbe31b51359a67f0d2dfdf46813f319180269796f1f617a64be1")) + Assert.assertFalse(CryptoUtil.checkPassword("givenHashToCheck", "hmac:0fe5f9c7b4b97c5d32d5cfad9d07473543a9938dc07af48a46dbbb49f4f68c12:a0c7cee14312bbe31b51359a67f0d2dfdf46813f319180269796f1f617a64be1")) + Assert.assertFalse(CryptoUtil.checkPassword("givenHashToCheck", "hmac:7fe5f9c7b4b97c5d32d5cfad9d07473543a9938dc07af48a46dbbb49f4f68c12:b0c7cee14312bbe31b51359a67f0d2dfdf46813f319180269796f1f617a64be1")) + } + +} +