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)
This commit is contained in:
Dominik Dzienia 2020-03-24 22:22:23 +01:00
parent 4716b04972
commit 6e9ccc593f
10 changed files with 369 additions and 27 deletions

View file

@ -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<Any>
override fun androidInjector(): AndroidInjector<Any> = 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)
}
}

View file

@ -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
}
}
}

View file

@ -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<View>(R.id.alertdialog_title) as TextView).text = activity.getString(labelId)
(titleLayout.findViewById<View>(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<View>(R.id.passwordprompt_text) as TextView
label.text = activity.getString(labelId)
val userInput = promptsView.findViewById<View>(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<View>(R.id.alertdialog_title) as TextView).text = context.getText(labelId)
(titleLayout.findViewById<View>(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<View>(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()
}

View file

@ -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() })
}
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportWidth="24"
android:viewportHeight="24"
android:width="24dp"
android:height="24dp">
<path
android:pathData="M22 18V22H18V19H15V16H12L9.74 13.74C9.19 13.91 8.61 14 8 14A6 6 0 0 1 2 8A6 6 0 0 1 8 2A6 6 0 0 1 14 8C14 8.61 13.91 9.19 13.74 9.74L22 18M7 5A2 2 0 0 0 5 7A2 2 0 0 0 7 9A2 2 0 0 0 9 7A2 2 0 0 0 7 5Z"
android:fillColor="#ffffff" />
</vector>

View file

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportWidth="24"
android:viewportHeight="24"
android:width="48dp"
android:height="48dp"
android:tint="#FFFFFF"
>
<group
android:scaleX="0.66"
android:scaleY="0.66"
android:pivotX="12"
android:pivotY="12">
<path
android:pathData="M22 18V22H18V19H15V16H12L9.74 13.74C9.19 13.91 8.61 14 8 14A6 6 0 0 1 2 8A6 6 0 0 1 8 2A6 6 0 0 1 14 8C14 8.61 13.91 9.19 13.74 9.74L22 18M7 5A2 2 0 0 0 5 7A2 2 0 0 0 7 9A2 2 0 0 0 9 7A2 2 0 0 0 7 5Z"
android:fillColor="#FF000000" />
</group>
</vector>

View file

@ -5,17 +5,13 @@
android:orientation="vertical"
android:padding="10dp" >
<TextView
android:id="@+id/passwordprompt_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge" />
<EditText
android:id="@+id/passwordprompt_pass"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword">
android:hint="@string/password_hint"
android:inputType="textPassword"
>
<requestFocus />

View file

@ -5,15 +5,22 @@
<string name="settings_protection">Settings protection</string>
<string name="application_protection">Application protection</string>
<string name="bolus_protection">Bolus protection</string>
<string name="master_password">Master password</string>
<string name="settings_password">Settings password</string>
<string name="application_password">Application password</string>
<string name="bolus_password">Bolus password</string>
<string name="unlock_settings">Unlock settings</string>
<string name="biometric">Biometric</string>
<string name="password">Password</string>
<string name="custom_password">Custom password</string>
<string name="noprotection">No protection</string>
<string name="protection">Protection</string>
<string name="password_set">Password set!</string>
<string name="password_not_set">Password not set</string>
<string name="password_not_changed">Password not changed</string>
<string name="password_hint">Enter password here</string>
<string name="key_master_password">master_password</string>
<string name="key_settings_password" translatable="false">settings_password</string>
<string name="key_application_password" translatable="false">application_password</string>
<string name="key_bolus_password">translatable="false"bolus_password</string>
@ -24,13 +31,15 @@
<string-array name="protectiontype">
<item>@string/noprotection</item>
<item>@string/biometric</item>
<item>@string/password</item>
<item>@string/master_password</item>
<item>@string/custom_password</item>
</string-array>
<string-array name="protectiontypeValues">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
</string-array>
</resources>

View file

@ -22,6 +22,12 @@
<PreferenceCategory android:title="@string/protection">
<Preference
android:inputType="textPassword"
android:key="@string/key_master_password"
android:title="@string/master_password"
/>
<ListPreference
android:defaultValue="1"
android:entries="@array/protectiontype"
@ -29,7 +35,7 @@
android:key="@string/key_settings_protection"
android:title="@string/settings_protection" />
<EditTextPreference
<Preference
android:inputType="textPassword"
android:key="@string/key_settings_password"
android:title="@string/settings_password" />
@ -41,7 +47,7 @@
android:key="@string/key_application_protection"
android:title="@string/application_protection" />
<EditTextPreference
<Preference
android:inputType="textPassword"
android:key="@string/key_application_password"
android:title="@string/application_password" />
@ -53,7 +59,7 @@
android:key="@string/key_bolus_protection"
android:title="@string/bolus_protection" />
<EditTextPreference
<Preference
android:inputType="textPassword"
android:key="@string/key_bolus_password"
android:title="@string/bolus_password" />

View file

@ -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"))
}
}