Consistent Data Encryption in Android, iOS, and Flutter Apps with AES

Ensure Data Security Across Mobile App Platforms With Consistent Data Encryption Using Advanced Encryption Standards
Mar 28 2024 · 9 min read

Background

In today’s digital world, ensuring data security is paramount. We entrust our devices with sensitive information, and protecting that data is crucial for us.

Maintaining consistent security practices can be challenging when developing apps for multiple platforms (iOS, Android, Flutter).

Data is like your diary — keep it private with encryption!

In this blog post, we’ll explore how to achieve this consistency using AES encryption in iOS as well as in Android and Flutter.

We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!

Why Consistent Encryption Matters?

Building cross-platform apps is great, but ensuring data encrypted on one platform remains accessible on another is crucial.

Imagine a user switching between their iPhone and Android device — their encrypted information wouldn’t be readily available. This is why consistent encryption is crucial for a seamless user experience.

To address this, we turned to the Advanced Encryption Standard. It offers two key advantages:

  1. Efficiency: It’s a strong encryption method that doesn’t slow down devices.
  2. Platform Neutrality: AES works equally well on Android, iOS, and other platforms.

With that we can achieve consistent data protection across all devices, ensuring users can access their information no matter which device they use.

Introduction to AES

The Advanced Encryption Standard is a widely used symmetric encryption algorithm known for its security and efficiency, trusted by governments and security experts worldwide.

It operates on fixed-size data blocks and supports key lengths of 128, 192, or 256 bits using a symmetric key for both encryption and decryption. This means the same secret key is used for both encryption and decryption.

AES-128 is widely used and recommended for most applications due to its balance between security and performance.

However, for applications that require a higher level of security or have specific compliance requirements, AES-192 or AES-256 may be preferred.

The choice depends on the application's security requirements and compliance standards.

The Secret Ingredient: GCM Mode

In addition to AES encryption, we employ Galois/Counter Mode (GCM) for enhanced security. GCM offers two critical benefits:

  1. Confidentiality: Like AES, GCM ensures that only authorized users with the correct key can decrypt the data, maintaining confidentiality and protecting sensitive information from unauthorized access.
  2. Authentication: GCM adds an authentication tag to encrypted data, verifying whether it has been tampered with during transmission or storage. If the tag doesn't match, decryption fails, preventing unauthorized data modification.

Our implementation utilizes AES in Galois/Counter Mode (GCM), providing authenticated encryption with associated data.

Implementation in iOS

Let’s implement AES encryption and decryption in iOS using Swift and CryptoKit.

We’ll add the functionality within a class called AESEncryptionManager.

Use CryptoKit Framework

The CryptoKit framework provides high-level cryptographic primitives and algorithms, including support for AES/GCM.

import CryptoKit

AESEncryptionManager Class

This class encapsulates AES encryption and decryption methods. It provides a convenient interface for performing encryption and decryption operations.

class AESEncryptionManager {
    static func encrypt(plainText: String, key: String, keySize: Int = 32) -> String? {
        // Implementation of encryption method
    }

    static func decrypt(encryptedText: String, key: String, keySize: Int = 32) -> String? {
        // Implementation of decryption method
    }
}

Generate a Secure Key with Data Padding

Before encrypting our data, we need a strong and random encryption key, much like a sturdy lock for a safe. Weak or easily guessed keys pose risks, similar to using a simple password for securing valuables.

To make a strong key that works on all platforms, we can combine specific app data, such as unchangeable usernames, user IDs, some creation dates, etc.

For example, when encrypting a user's story, we merge their user ID with the story ID to ensure uniform encryption across platforms.

Generated key must be identical in all the platforms, in order to achieve consistant encryption.

It's crucial to ensure that the key is of the correct size for AES encryption (typically 16, 24, or 32 bytes).

To achieve this, we provide an extension method called padWithZeros. This method pads the key data with zeros if its size is less than the target size.

extension Data {
    func padWithZeros(targetSize: Int) -> Data {
        var paddedData = self

        // Get the current size (number of bytes) of the data
        let dataSize = self.count

        // Check if padding is needed
        if dataSize < targetSize {

            // Calculate the amount of padding required
            let paddingSize = targetSize - dataSize

            // Create padding data filled with zeros
            let padding = Data(repeating: 0, count: paddingSize)

            // Append the padding to the original data
            paddedData.append(padding)
        }
        return paddedData
    }
}
  • If the key data size (dataSize) is smaller than the target size, then the padding is needed.
  • A new Data object filled with zeros is created with a size equal to the difference between the target size and the current key data size.
  • Then the created zero padding is appended to the end of the existing key data using paddedData.append(padding).

By employing this method, we ensure the generation of secure and consistent encryption keys, safeguarding our data across all platforms.

Remember, just like a real lock, a lost or stolen key means anyone can access your stuff. Keeping your encryption key secure is vital for ultimate data protection!

Encryption Method

It converts the plaintext and key into data objects, creates a symmetric key using the key data, and then encrypts the data using AES.GCM.

static func encrypt(plainText: String, key: String, keySize: Int = 32) -> String? {
    guard let data = plainText.data(using: .utf8), let keyData = key.data(using: .utf8)?.prefix(keySize) else {
        return nil
    }
    let symmetricKey = SymmetricKey(data: keyData.padWithZeros(targetSize: keySize))
    do {
        let sealedBox = try AES.GCM.seal(data, using: symmetricKey, nonce: AES.GCM.Nonce()).combined
        return sealedBox?.base64EncodedString() ?? nil
    } catch {
        print("AESEncryption: Encryption failed with error \(error)")
        return nil
    }
}

This function takes three arguments:

  • plainText: — The secret message or data you want to encrypt.
  • key: —  The secret key used for encryption, is crucially important to keep confidential.
  • keySize: — The size of the key in bytes. The default value is 32, which corresponds to a 256-bit key (considered the most secure option).

Let’s break it down:

  • It first checks if the plainText and key can be converted into data using UTF-8 encoding.
     —  UTF-8 is a common character encoding that represents text as bytes.
  • After the successful conversion, it ensures the key has the correct size by extracting the first keySize bytes.
     —  Remember, using a key with the wrong size can compromise encryption security.
  • A SymmetricKey object is created using the padded key data, which ensures that the key has the correct size for the encryption algorithm.
  • The actual encryption happens within a do-try-catch block.
     —  It uses AES.GCM.seal to encrypt the plain text using the AES-GCM algorithm with the generated SymmetricKey and a random nonce.
     —  The .combined property combines the encrypted data with additional authentication information for verification during decryption.
  • If encryption is successful, the combined data from the SealedBox is converted into a base64 encoded string to make it more compact and easier to store or transmit.

Decryption Method

It reverses the encryption process by decoding the base64 encoded data, creating a sealed box from the combined data, and then decrypting it using the symmetric key.

static func decrypt(encryptedText: String, key: String, keySize: Int = 32) -> String? {
    guard let combinedData = Data(base64Encoded: encryptedText), let keyData = key.data(using: .utf8)?.prefix(keySize) else {
        return nil
    }
    let symmetricKey = SymmetricKey(data: keyData.padWithZeros(targetSize: keySize))
    do {
        let sealedBox = try AES.GCM.SealedBox(combined: combinedData)
        let decryptedData = try AES.GCM.open(sealedBox, using: symmetricKey)
        return String(data: decryptedData, encoding: .utf8)
    } catch let error {
        print("AESEncryption: Decryption failed with error \(error)")
        return nil
    }
}

Here is the breakdown, of how it works…

  1. First, check if the encryptedText can be converted back into data using base64 decoding.
  2. Similar to encryption, it extracts the first keySize bytes from the key data.
  3. The SymmetricKey object is created using the padded key data.
  4. The decryption happens within a do-try-catch block:
    —  try AES.GCM.SealedBox(combined: combinedData): It creates a SealedBox object from the provided base64 encoded data.
    —  try AES.GCM.open(sealedBox, using: symmetricKey): It decrypts the data within the SealedBox using the key. If the authentication information matches, the decrypted data is obtained.
    —  String(data: decryptedData, encoding: .utf8): The decrypted data is converted back into a String using UTF-8 encoding.

AES Encryption in Android

Now, let’s explore how to implement AES encryption and decryption in Android using Kotlin.

First, we need to import relevant libraries. Then we encapsulate our methods within an object named AES256Encryption.

Here’s how we encrypt plaintext using AES encryption.

// import the necessary libraries for cryptographic operations,
import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
import java.nio.charset.Charset
import java.security.SecureRandom
import android.util.Base64

object AES256Encryption {

    // Define character set for text encoding
    private val charset = Charset.forName("UTF-8")

    // Define encryption parameters
    private const val transformation = "AES/GCM/NoPadding" // Encryption algorithm and mode
    private const val secretKeySpecAlgorithm = "AES" // Secret key algorithm
    private const val ivSize = 12 // Size of the initialization vector (IV)
    private const val gcmKeySize = 128 // Size of the GCM key in bits

    private fun encrypt(plainText: String, key: String): String? {
        try {
            // Generate a random IV
            val iv = ByteArray(ivSize)
            SecureRandom().nextBytes(iv)

            // Create a secret key using the provided encryption key
            val secretKeySpec = SecretKeySpec(key.toByteArray(charset).copyOf(32), secretKeySpecAlgorithm)

            // Initialize the Cipher for encryption with the secret key and IV
            val cipher = Cipher.getInstance(transformation)
            val gcmSpec = GCMParameterSpec(gcmKeySize, iv)
            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmSpec)

            // Encrypt the plaintext
            val encrypted = cipher.doFinal(plainText.toByteArray(charset))

            // Combine the IV and encrypted bytes into a single byte array
            val combined = iv + encrypted

            // Encode the combined byte array into a Base64 string and return
            return Base64.encodeToString(combined, Base64.DEFAULT)
        } catch (e: Exception) {
            e.printStackTrace() // Handle any exceptions and print the stack trace
        }
        return null
    }
}

Now, let's decrypt the encrypted text back to plaintext.

private fun decrypt(encryptedText: String, key: String): String? {
    try {
        // Decode the Base64-encoded encrypted text into a byte array
        val encryptedData = Base64.decode(encryptedText, Base64.DEFAULT)

        // Extract the initialization vector (IV) from the encrypted data
        val iv = encryptedData.copyOfRange(0, ivSize)

        // Extract the encrypted bytes (excluding the IV) from the encrypted data
        val encrypted = encryptedData.copyOfRange(ivSize, encryptedData.size)

        // Create a secret key using the provided encryption key
        val secretKeySpec = SecretKeySpec(key.toByteArray(charset).copyOf(32), secretKeySpecAlgorithm)

        // Initialize the Cipher for decryption with the secret key and IV
        val cipher = Cipher.getInstance(transformation)
        val gcmSpec = GCMParameterSpec(gcmKeySize, iv)
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmSpec)

        // Decrypt the encrypted bytes
        val decrypted = cipher.doFinal(encrypted)

        // Convert the decrypted bytes into a String using the specified character set
        return String(decrypted, charset)
    } catch (e: Exception) {
        e.printStackTrace() // Handle any exceptions and print the stack trace
    }
    return null
}

This explanation provides a simple overview of how to use AES encryption in Android, ensuring data security for your applications.

AES Encryption in Flutter

Now, let’s explore how to implement AES encryption and decryption in Flutter.

First, we need to import the necessary libraries. Then we encapsulate our methods within the class named AES256Encryption.

Here’s how we encrypt plaintext using AES encryption:

import 'dart:math';
import 'dart:convert';
import 'dart:typed_data';
import 'package:encrypt/encrypt.dart' as enc;

class AES256Encryption {

  static const int ivSize = 12;

  static String? encrypt(String plainText, String key) {
    try {

      // Generate a random IV (Initialization Vector)
      final iv = enc.IV.fromSecureRandom(ivSize);

      // Convert the key into bytes and pad it to 32 bytes
      final keyBytes = Uint8List.fromList(
        List.filled(32, 0)..setRange(0, key.length, utf8.encode(key)),
      );

      // Create an AES encrypter with the key and IV
      final cipher = enc.Encrypter(enc.AES(enc.Key(keyBytes), mode: enc.AESMode.gcm));

      // Encrypt the plaintext using AES-GCM
      final encrypted = cipher.encrypt(plainText, iv: iv);

      // Combine the IV and encrypted bytes into a single byte array
      final combined = iv.bytes + encrypted.bytes;

      // Encode the combined byte array into a Base64 string and return
      return base64Encode(combined);
    } catch (e) {
      print('AES256Encryption - Error while encrypt: $e');
      return null;
    }
  }
}

Now, let's decrypt the encrypted text back to plaintext.

static String? decrypt(String encryptedText, String key) {
    try {
      // Decode the Base64-encoded encrypted text into a byte array
      final encryptedData = base64Decode(encryptedText.replaceAll(RegExp(r'\s'), ''));

      // Extract the IV (Initialization Vector) from the encrypted data
      final iv = enc.IV(encryptedData.sublist(0, ivSize));

      // Extract the encrypted bytes (excluding the IV) from the encrypted data
      final encrypted = encryptedData.sublist(ivSize);

      // Convert the key into bytes and pad it to 32 bytes
      final myKey = utf8.encode(key);
      final keyBytes = Uint8List.fromList(
        List.filled(32, 0)..setRange(0, min(32, myKey.length), myKey),
      );

      // Create an AES decrypter with the key and IV
      final cipher = enc.Encrypter(enc.AES(enc.Key(keyBytes), mode: enc.AESMode.gcm));

      // Decrypt the encrypted bytes using AES-GCM
      return cipher.decrypt(enc.Encrypted(encrypted), iv: iv);
    } catch (e) {
      print('AES256Encryption - Error while decrypt: $e');
      return null;
    }
}

This explanation provides a simple overview of how to use AES encryption in Flutter, ensuring data security for your applications.

With this, we are done now!

Conclusion

In this blog post, we’ve delved into the implementation of AES encryption and decryption for mobile platforms including Flutter.

By implementing consistent AES encryption practices, you can ensure your users’ data remains secure and accessible across the various platforms they use. This commitment to data security not only builds trust but also enhances the overall user experience.

This approach enables secure data exchange between iOS, Android, and Flutter applications, enhancing overall data security and integrity.

As you embark on your coding journey, remember the importance of prioritizing data security and implementing robust encryption mechanisms. Together, we can create safer digital environments for users worldwide.

Happy coding!


amisha-i image
Amisha Italiya
iOS developer @canopas | Sharing knowledge of iOS development


amisha-i image
Amisha Italiya
iOS developer @canopas | Sharing knowledge of iOS development

Whether you need...

  • *
    High-performing mobile apps
  • *
    Bulletproof cloud solutions
  • *
    Custom solutions for your business.
Bring us your toughest challenge and we'll show you the path to a sleek solution.
Talk To Our Experts
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.