Chapters

Hide chapters

Real-World Android by Tutorials

Second Edition · Android 12 · Kotlin 1.6+ · Android Studio Chipmunk

Section I: Developing Real World Apps

Section 1: 7 chapters
Show chapters Hide chapters

16. Securing Data at Rest
Written by Antonio Roa-Valverde

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

This chapter starts with a simple approach to protect your stored data and builds up to more fine-tuned and advanced implementations. You can stop at any time if you have what you need. If you only want to implement a simple login for your app, great, you’ll find that near the beginning. If your project requires customized protocols, carry on to the end of the chapter.

In this chapter, you’ll learn how to:

  • Store a password securely.
  • Protect saved data.
  • Use encryption.

If you missed the previous chapters, the sample app includes a list of pets, their medical data and a section that lets you report safety issues while remaining anonymous.

Launch the starter app for this chapter and you’ll see a simple sign-up screen. Once you enter an email and select Sign Up, the list of pets will populate. Tap the Report tab to report a concern:

Figure 16.1 — Report Section
Figure 16.1 — Report Section

This is quite easy but, is your app also secure? As first step you’ll now implement a login for the user.

Implementing the Login

The app saves data about you, such as your pet’s home address and medical history, your login passwords and the safety reports you’ve submitted. If someone were to take your device, they’d have access to all that personal information.

To ensure only you can access that app data, it’s standard to require a password. Many modern devices have biometric readers like face, retina and fingerprint scanners.

In this first section, you’ll implement a biometric prompt to log in so only you can access the app on your device. You’ll also implement a password fallback, giving the user an alternative log-in option.

First, you need to have the app check that the device is able to use biometrics. In MainActivity.kt, replace the contents of loginPressed() like in the following code:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
  // ...
  fun loginPressed(view: View) {
    val biometricManager = BiometricManager.from(this)
    when (biometricManager.canAuthenticate(BIOMETRIC_STRONG)) {
      BiometricManager.BIOMETRIC_SUCCESS ->
          displayLogin(view, false) // 1
      BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE ->
          displayLogin(view, true) // 2
      BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE ->
          toast("Biometric features are currently unavailable.")
      BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED ->
          toast("Please associate a biometric credential with your account.")
      else ->
          toast("An unknown error occurred. Please check your Biometric settings")
      }
  }
  // ...
}

In this code you see that:

  1. You call displayLogin() if the device can perform biometric authentication with BIOMETRIC_SUCCESS.
  2. Otherwise, the fallback flag is set to true, allowing for password or PIN authentication.

Note: Android 11 divides the biometric features in strong and week. Fingerprint is considered strong, while face recognition is considered weak.

Next, add the following variables to the same MainActivity class:

private lateinit var biometricPrompt: BiometricPrompt
private lateinit var promptInfo: BiometricPrompt.PromptInfo

BiometricPrompt is a class from AndroidX.

Next, replace the contents of displayLogin() with the following:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
  // ...
  private fun displayLogin(view: View, fallback: Boolean) {
    val executor = Executors.newSingleThreadExecutor()
    biometricPrompt = BiometricPrompt(this, executor, // 1
        object : BiometricPrompt.AuthenticationCallback() {
          override fun onAuthenticationError(errorCode: Int,
                                             errString: CharSequence) {
            super.onAuthenticationError(errorCode, errString)
            runOnUiThread {
              toast("Authentication error: $errString")
            }
          }

          override fun onAuthenticationFailed() {
            super.onAuthenticationFailed()
            runOnUiThread {
              toast("Authentication failed")
            }
          }

          override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {// 2
            super.onAuthenticationSucceeded(result)

            runOnUiThread {
              toast("Authentication succeeded!")
              if (!isSignedUp) {
                generateSecretKey() // 3
              }
              performLoginOperation(view)
            }
          }
        })

    if (fallback) {
      promptInfo = BiometricPrompt.PromptInfo.Builder()
          .setTitle("Biometric login for my app")
          .setSubtitle("Log in using your biometric credential")
          // Cannot call setNegativeButtonText() and
          // setDeviceCredentialAllowed() at the same time.
          // .setNegativeButtonText("Use account password")
          .setAllowedAuthenticators(DEVICE_CREDENTIAL) // 4
          .build()
    } else {
      promptInfo = BiometricPrompt.PromptInfo.Builder()
          .setTitle("Biometric login for my app")
          .setSubtitle("Log in using your biometric credential")
          .setNegativeButtonText("Use account password")
          .build()
    }
    biometricPrompt.authenticate(promptInfo)
  }

  // ... 
}

Here’s what’s happening:

  1. You create a BiometricPrompt object for authentication.
  2. You override onAuthenticationSucceeded to determine a successful authentication.
  3. You create a secret key that’s tied to the authentication for first-time users.
  4. You create a fallback to password authentication by calling .setAllowedAuthenticators(DEVICE_CREDENTIAL).

Be sure you have a face, fingerprint or similar biometric scanner on your device to test the biometric part. Build and run. You’ll now be able to log in with your credentials:

Figure 16.2 — Biometric Prompt
Figure 16.2 — Biometric Prompt

Once the authentication is successful, you’ll see the pet list:

Figure 16.3 — Animals Near You
Figure 16.3 — Animals Near You

With that, you’ve secured access to the app with biometric security! That was easy.

Deciding What Security Options To Use

Is biometrics always the safest type of security for your app? To answer that question, it helps to use a threat model, a risk-based approach to making decisions. In other words, you need to consider what the biggest risks your user will face are.

Exploring Hardware Security Modules

A Trusted Execution Environment (TEE) is software separate from the OS. It safely sandboxes security operations, and though it’s inside the main processor, it’s cordoned off from the main operating system. Security keys that are isolated this way are hardware-backed. You can find out if a key is hardware-backed by using KeyInfo.isInsideSecureHardware().

Hardening Data in the KeyStore

To protect your data, you’ll use MasterKey to generate a key in the KeyStore. This will encrypt your reports that you wish to send.

class Encryption {
  companion object {
    // ...
    fun encryptFile(context: Context, file: File): EncryptedFile {
      val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build() // 1
      return EncryptedFile.Builder(
          context,
          file,
          masterKey,
          EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB // 2
      ).build()
    }
  }  
  // ...
}
val file = File(theContext.filesDir?.absolutePath, "$reportID.txt") // 1
val encryptedFile = encryptFile(theContext, file) // 2
encryptedFile.openFileOutput().bufferedWriter().use {
    it.write(reportString) // 3
}

Securing Data with Biometrics

For additional security, you can auto-generate a key in KeyStore that’s also protected by your biometric credential. If the device becomes compromised, the key is still encrypted.

class Encryption {
  companion object {
    // ...
    @TargetApi(Build.VERSION_CODES.R)
    fun generateSecretKey() {
      val keyGenParameterSpec = KeyGenParameterSpec.Builder(
          KEYSTORE_ALIAS,
          KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
          .setBlockModes(KeyProperties.BLOCK_MODE_GCM) // 1
          .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
          .setUserAuthenticationRequired(true) // 2
          .setUserAuthenticationParameters(120, KeyProperties.AUTH_BIOMETRIC_STRONG) // 3
          .build()
      val keyGenerator = KeyGenerator.getInstance(
          KeyProperties.KEY_ALGORITHM_AES, PROVIDER) // 4
      keyGenerator.init(keyGenParameterSpec)
      keyGenerator.generateKey()
    }
    // ...
  }
}
class Encryption {
  companion object {
    // ...
    private fun getSecretKey(): SecretKey {
      val keyStore = KeyStore.getInstance(PROVIDER)

      // Before the keystore can be accessed, it must be loaded.
      keyStore.load(null)
      return keyStore.getKey(KEYSTORE_ALIAS, null) as SecretKey
    }

    private fun getCipher(): Cipher {
      return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
          + KeyProperties.BLOCK_MODE_GCM + "/"
          + KeyProperties.ENCRYPTION_PADDING_NONE)
    }
  }
}

Encrypting Data

At this point, you’ve stored the key in the KeyStore, protected by your credentials. But so far, you’ve stored the user’s generated password in the clear. For your next step, you’ll update the login method to encrypt it using the Cipher object, given the SecretKey.

class Encryption {
  companion object {
    // ...
    fun createLoginPassword(context: Context): ByteArray {
      val cipher = getCipher()
      val secretKey = getSecretKey()
      val random = SecureRandom()
      val passwordBytes = ByteArray(256)
      random.nextBytes(passwordBytes) // 1
      cipher.init(Cipher.ENCRYPT_MODE, secretKey)
      val ivParameters = cipher.parameters.getParameterSpec(GCMParameterSpec::class.java)
      val iv = ivParameters.iv
      PreferencesHelper.saveIV(context, iv) // 2
      return cipher.doFinal(passwordBytes) // 3
    }
    // ...
  }
}

Decrypting Data

You’ve encrypted the password, so now you need to decrypt it when the user authenticates.

class Encryption {
  companion object {
    // ...
    fun decryptPassword(context: Context, password: ByteArray): ByteArray {
      val cipher = getCipher()
      val secretKey = getSecretKey()
      val iv = PreferencesHelper.iv(context) // 1
      val ivParameters = GCMParameterSpec(128, iv)
      cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameters) // 2
      return cipher.doFinal(password) // 3
    }
    // ...
  }
}
val encryptedInfo = createLoginPassword(this)
UserRepository.createDataSource(applicationContext, it, encryptedInfo)
val password = decryptPassword(this,
    Base64.decode(firstUser.password, Base64.NO_WRAP))
if (password.isNotEmpty()) {
  //Send password to authenticate with server etc
  success = true
}
kotlin.TypeCastException: null cannot be cast to non-null type javax.crypto.SecretKey
Figure 16.4 — Animals Near You
Cosadi 75.7 — Eyikedf Woeq Vio

Customizing Encryption

In this part, you’ll focus on the recommended standard for encryption, Advanced Encryption Standard (AES). AES uses a substitution–permutation network to encrypt your data with a key. Using this approach, it replaces bytes from one table with the bytes from another, and so creates permutations of data. Just like before, AES requires an encryption key. You’ll customize how that key is created.

Creating a Key

As mentioned above, AES uses a key for encryption. You also use that same key to decrypt the data. This property is called symmetric encryption.

class Encryption {
  companion object {
    // ...
    fun encrypt(dataToEncrypt: ByteArray,
                password: CharArray): HashMap<String, ByteArray> {
      val map = HashMap<String, ByteArray>()
      val random = SecureRandom() // HERE
      val salt = ByteArray(256)
      random.nextBytes(salt) 

      return map
    }
    // ...
  }
}
class Encryption {
  companion object {
    // ...
    fun encrypt(dataToEncrypt: ByteArray,
                password: CharArray): HashMap<String, ByteArray> {
      val map = HashMap<String, ByteArray>()
      val random = SecureRandom()
      val salt = ByteArray(256) 
      random.nextBytes(salt) 

      val pbKeySpec = PBEKeySpec(password, salt, 1324, 256) // 1
      val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") // 2
      val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded // 3
      val keySpec = SecretKeySpec(keyBytes, "AES") // 4
      return map
    }
    // ...
  }
}

Choosing an Encryption Mode

The mode defines how the data is processed. One example is Electronic Code Book (ECB). It’s simplistic in that it splits up the data and repeats the encryption process for every chunk with the same key. Because each block uses the same key, this mode is highly insecure. Don’t use this mode.

Adding an Initialization Vector

As mentioned above, you’re going to use the standard mode, cipher block chaining (CBC), to encrypt your data one chunk at a time. You’ll XOR each block of data in the pipeline with the previous block that it encrypted. That dependency on previous blocks makes the encryption strong.

class Encryption {
  companion object {
    // ...
    fun encrypt(dataToEncrypt: ByteArray,
                password: CharArray): HashMap<String, ByteArray> {
      val map = HashMap<String, ByteArray>()
      val random = SecureRandom()
      val salt = ByteArray(256) 
      random.nextBytes(salt) 
      val pbKeySpec = PBEKeySpec(password, salt, 1324, 256)
      val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
      val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded
      val keySpec = SecretKeySpec(keyBytes, "AES")

      val ivRandom = SecureRandom() //not caching previous seeded instance of SecureRandom
      val iv = ByteArray(16)
      ivRandom.nextBytes(iv) // 1
      val ivSpec = IvParameterSpec(iv) // 2      
      return map
    }
    // ...
  }
}

Finalizing the Encryption

Now that you have all the necessary pieces, you can finally get to the encryption! Add the following code to encrypt() in the Encryption.kt file to perform the customized encryption:

class Encryption {
  companion object {
    // ...
    fun encrypt(dataToEncrypt: ByteArray,
                password: CharArray): HashMap<String, ByteArray> {
      val map = HashMap<String, ByteArray>()
      val random = SecureRandom()
      val salt = ByteArray(256) 
      random.nextBytes(salt) 
      val pbKeySpec = PBEKeySpec(password, salt, 1324, 256)
      val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
      val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded
      val keySpec = SecretKeySpec(keyBytes, "AES")
      val ivRandom = SecureRandom() //not caching previous seeded instance of SecureRandom
      val iv = ByteArray(16)
      ivRandom.nextBytes(iv)
      val ivSpec = IvParameterSpec(iv)

      val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding") // 1
      cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
      val encrypted = cipher.doFinal(dataToEncrypt) // 2      
      return map
    }
    // ...
  }
}
class Encryption {
  companion object {
    // ...
    fun encrypt(dataToEncrypt: ByteArray,
                password: CharArray): HashMap<String, ByteArray> {
      val map = HashMap<String, ByteArray>()
      val random = SecureRandom()
      val salt = ByteArray(256) 
      random.nextBytes(salt) 
      val pbKeySpec = PBEKeySpec(password, salt, 1324, 256)
      val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
      val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded
      val keySpec = SecretKeySpec(keyBytes, "AES")
      val ivRandom = SecureRandom() //not caching previous seeded instance of SecureRandom
      val iv = ByteArray(16)
      ivRandom.nextBytes(iv)
      val ivSpec = IvParameterSpec(iv)
      val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
      cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
      val encrypted = cipher.doFinal(dataToEncrypt)


      map["salt"] = salt // HERE
      map["iv"] = iv // HERE
      map["encrypted"] = encrypted // HERE
      return map
    }
    // ...
  }
}

Decrypting with Salts and IVs

You have some encrypted data. To decrypt it, you’ll have to change the mode of Cipher in the init method from ENCRYPT_MODE to DECRYPT_MODE.

class Encryption {
  companion object {
    // ...
    fun decrypt(map: HashMap<String, ByteArray>, password: CharArray): ByteArray? {
      var decrypted: ByteArray? = null
      try {
        // 1
        val salt = map["salt"]
        val iv = map["iv"]
        val encrypted = map["encrypted"]

        // 2
        //regenerate key from password
        val pbKeySpec = PBEKeySpec(password, salt, 1324, 256)
        val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
        val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded
        val keySpec = SecretKeySpec(keyBytes, "AES")

        // 3
        //Decrypt
        val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
        val ivSpec = IvParameterSpec(iv)
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
        decrypted = cipher.doFinal(encrypted)
      } catch (e: Exception) {
        Log.e("MYAPP", "decryption exception", e)
      }
      return decrypted
    }
    // ...
  }
}

Updating the Saving Method

Now that the encryption process is complete, you need to test it. The app is already writing data to storage.

@AndroidEntryPoint
class ReportDetailFragment : Fragment() {
  // ...
  private fun testCustomEncryption(reportString: String) {
    val password = REPORT_SESSION_KEY.toCharArray()
    val bytes = reportString.toByteArray(Charsets.UTF_8)
    val map = Encryption.encrypt(bytes, password) // 1
    val reportID = UUID.randomUUID().toString()
    val outFile = File(activity?.filesDir?.absolutePath, "$reportID.txt")
    ObjectOutputStream(FileOutputStream(outFile)).use { // 2
      it.writeObject(map)
    }

    //TEST decrypt
    val decryptedBytes = Encryption.decrypt(map, password) // 3
    decryptedBytes?.let {
      val decryptedString = String(it, Charsets.UTF_8)
      Log.e("Encryption Test", "The decrypted string is: $decryptedString") // 4
    }
  }
  // ...
}
Figure 16.5 — Encryption Test
Fepuco 56.5 — Avmsxpqaof Cuxl

Key Points

In this chapter, you learned the following:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now