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!
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:
With that we can achieve consistent data protection across all devices, ensuring users can access their information no matter which device they use.
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.
In addition to AES encryption, we employ Galois/Counter Mode (GCM) for enhanced security. GCM offers two critical benefits:
Our implementation utilizes AES in Galois/Counter Mode (GCM), providing authenticated encryption with associated data.
Let’s implement AES encryption and decryption in iOS using Swift and CryptoKit.
We’ll add the functionality within a class called AESEncryptionManager
.
The CryptoKit framework provides high-level cryptographic primitives and algorithms, including support for AES/GCM.
import CryptoKit
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
}
}
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
}
}
dataSize
) is smaller than the target size, then the padding is needed.Data
object filled with zeros is created with a size equal to the difference between the target size and the current key data size.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!
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:
plainText
and key
can be converted into data using UTF-8 encoding.keySize
bytes.SymmetricKey
object is created using the padded key data, which ensures that the key has the correct size for the encryption algorithm.do-try-catch
block.AES.GCM.seal
to encrypt the plain text using the AES-GCM algorithm with the generated SymmetricKey
and a random nonce
..combined
property combines the encrypted data with additional authentication information for verification during decryption.SealedBox
is converted into a base64 encoded string to make it more compact and easier to store or transmit.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…
encryptedText
can be converted back into data using base64 decoding.keySize
bytes from the key data.SymmetricKey
object is created using the padded key data.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.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.
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!
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!
Whether you need...