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.
Why Consistent Encryption Matters?
Building cross-platform apps is great, but ensuring data encrypted on one platform remains accessible on another is crucial.
Without consistent encryption, transitioning between devices like iPhone and Android can lead to inaccessible information.
To tackle this challenge, we rely on the Advanced Encryption Standard (AES), which provides two key advantages.
- Efficiency — It’s a strong encryption method that doesn’t slow down devices.
- 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:
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.
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.
Step 1: Use CryptoKit Framework
The CryptoKit framework provides high-level cryptographic primitives and algorithms, including support for AES/GCM.
import CryptoKit
Step 2: 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
}
}
Step 3: Generating a Secure Encryption 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!
Step 4: 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.
Step 5: 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…
First, check if the
encryptedText
can be converted back into data using base64 decoding.Similar to encryption, it extracts the first
keySize
bytes from the key data.The
SymmetricKey
object is created using the padded key data.The decryption happens within a
do-try-catch
block:
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.
And we are done.
This post only has iOS implementation, to read the complete guide including Android and Flutter encryption implementation please visit our full blog on Canopas.
I encourage you to share your thoughts in the comments section below.
Your input not only enriches our content but also fuels our motivation to create more valuable and informative articles for you.