How to implement end-to-end encryption using PBKDF in Flutter

End-to-end encryption must be used in order to guarantee the security and privacy of user data. In this article, we’ll look at how to set up an end-to-end encryption system to protect user data.
- User Account Creation:
- Generating RSA Key Pair:
- Encrypting the Private Key:
- Storing Encrypted Keys:
- Encryption and Decryption Process:
- Password-based Key Retrieval:
- Password Reset:
When a user registers for an account on our system, the process begins. We use the Password-Based Key Derivation Function (PBKDF2) to create a customised encryption key for each user. PBKDF creates a secure and reliable encryption key by applying cryptographic hashing methods, like Bcrypt or Argon2, to the user's password. Using this key, the user's RSA Private key will be encrypted and decrypted.

Following the extraction of the encryption key, we create an RSA key pair for the user. A Private key and a Public key make up the RSA key pair. It is essential to protect the private key because if it is compromised, the system will be ineffective. The public key, on the other hand, is usable for data encryption and can be freely shared.
We use the symmetric key algorithm Advanced Encryption Standard (AES) to safeguard the private key. We encrypt the user’s private RSA key using AES using the PBKDF encryption key that was previously produced. Because of this encryption, even if the encrypted private key is discovered, it will still be impossible to decrypt it without the right decryption key.
Once the private key has been encrypted, it can stored in a database together with the public key. Without the encryption key produced from the user’s password, it is impossible to directly decrypt the encrypted private key.
Now that end-to-end encryption is in place, we can encrypt and decode user data using their public and private keys. Users encrypt sensitive data using the recipient’s public key when sending it to them. The original content cannot be decrypted or accessed by anyone other than the recipient who has the associated private key.
We follow the same process as when an account is first created when a user tries to log in using their password. We create the original encryption key by applying PBKDF to the supplied password. The user’s private key, which is kept encrypted in the database, is decrypted using this key, enabling them to access their encrypted data.

In the event that a user needs to change their password, we apply PBKDF to the existing password, get the encryption key, decode the private key, and then encrypt it once more using the newly obtained encryption key derived from the new password. This procedure makes sure that user data is kept secure even when passwords are changed.

End-to-end encryption systems are set up to ensure the security and confidentiality of user data, giving users and developers alike piece of mind. With the use of PBKDF, RSA key pairs, and AES encryption, we can safeguard sensitive data while still enabling features like password resets and data sharing.
NOTE: When a user registers an account on the system, the PBKDF technique is used to generate a key from their password. We do not, however, use this key to encrypt their data because it would prohibit them from changing their password in the future. Instead, we use asymmetric cryptography, often known as Public-key cryptography, in which a Public key and a Private key are used. This method allows developers to create sharing capabilities that would be impossible to achieve with symmetric key techniques. We create a RSA key pair consisting of a private key and a public key and encrypt the private key using AES encryption to store it securely in the DB. The public key stays accessible. By adopting this method, we maintain the ability to protect the user’s data and allow for password resets when necessary.
Now let’s implement this system in Flutter
crypton | Dart Packagepointycastle | Dart Package
Step 1: Create a class CryptoResult, which will be the response from our cryptographic function.
class CryptoResult {
& emsp;final bool status;
final String data;
CryptoResult({required this.data, required this.status});
}
Step 2: Create a class CryptoService, this class will be the engine of our application’s cryptographic requirements.
class CryptoService {
}
Step 3: Add a static method to CryptoService class to generate RSA key-pair.
import 'package:crypton/crypton.dart';
class CryptoService {
& emsp;// method to generate RSA key-pairs
static RSAKeypair getKeyPair() {
& emsp; RSAKeypair rsaKeypair = RSAKeypair.fromRandom();
return rsaKeypair;
}
}
Step 4: Let’s now add two new methods to CryptoService to perform Assymetric Cryptography (encryption and decryption), this methods will return a CryptoResult, we created the CryptoResult class in Step 1.
// method to encrypt a piece of text using the public key, returns a CryptoResult
static CryptoResult assymetricEncrypt(String plainText, RSAPublicKey pubKey) {
& emsp;try {
& emsp; // Encrypt the piece of text
String encrypted = pubKey.encrypt(plainText);
// Return a CryptoResult
return CryptoResult(data: encrypted, status: true);
} catch (err) {
& emsp; // Error handelling
return CryptoResult(data: err.toString(), status: false);
}
}
// method to decrypt a piece of text using the private key, returns a CryptoResult
static CryptoResult assymetricDecript(String encodedTxt, RSAPrivateKey pvKey) {
& emsp;try {
& emsp; // Decrypt the piece of text
String decoded = pvKey.decrypt(encodedTxt);
// Return a CryptoResult
return CryptoResult(data: decoded, status: true);
} catch (err) {
& emsp; // Error handelling
return CryptoResult(data: err.toString(), status: false);
}
}
Step 5: Add a method generateRandomSalt to CryptoService to generate Random salt, we will use the random key generated from this method while performing symmetric cryptography. Add an import to dart math library at the top of the file, this library is included with Dart SDK.
import 'dart:math';
class CryptoService {
& emsp;...
static Uint8List generateRandomSalt({int length = 16}) {
& emsp; final random = Random.secure();
final saltCodeUnits = List.generate(length, (_) => random.nextInt(256));
return Uint8List.fromList(saltCodeUnits);
}
}
Step 6: Add a method called generatePBKDFKey to CryptoService, this method will take password and a random salt string as input parameters and it will return the key generated using PBKDF (Password Based Key Derivation Function).
We need to add some imports at the top of the file at this point.
import 'dart:convert';
import 'dart:typed_data';
import 'package:pointycastle/macs/hmac.dart';
import 'package:pointycastle/digests/sha256.dart';
import 'package:pointycastle/key_derivators/api.dart';
import 'package:pointycastle/key_derivators/pbkdf2.dart';
// method to generate encryption key using user's password.
static Uint8List generatePBKDFKey(String password, String salt, {int iterations = 10000, int derivedKeyLength = 32}) {
& emsp;final passwordBytes = utf8.encode(password);
final saltBytes = utf8.encode(salt);
final params = Pbkdf2Parameters(Uint8List.fromList(saltBytes), iterations, derivedKeyLength);
final pbkdf2 = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64));
pbkdf2.init(params);
return pbkdf2.process(Uint8List.fromList(passwordBytes));
}
Step 7: We will now add two new methods to CryptoService to perform Symetric Cryptography (encryption and decryption), this methods will return a CryptoResult, we created the CryptoResult class in Step 1.
We need to add some more imports at the top.
import 'package:pointycastle/block/aes.dart';
import 'package:pointycastle/padded_block_cipher/padded_block_cipher_impl.dart';
import 'package:pointycastle/paddings/pkcs7.dart';
import 'package:pointycastle/api.dart';
// Encrypt a piece of text using symmetric algorithm, returns CryptoResult
static Uint8List symetricEncrypt(Uint8List key, Uint8List iv, Uint8List plaintext) {
& emsp;final cipher = PaddedBlockCipherImpl(PKCS7Padding(), AESEngine());
final params = PaddedBlockCipherParameters(
KeyParameter(key),
ParametersWithIV(KeyParameter(key), iv),
);
cipher.init(true, params);
return cipher.process(plaintext);
}
// Decrypt a piece of text using symmetric algorithm, returns CryptoResult
static Uint8List symetricDecrypt(Uint8List key, Uint8List iv, Uint8List ciphertext) {
& emsp;final cipher = PaddedBlockCipherImpl(PKCS7Padding(), AESEngine());
final params = PaddedBlockCipherParameters(
KeyParameter(key),
ParametersWithIV(KeyParameter(key), iv),
);
cipher.init(false, params);
return cipher.process(ciphertext);
}
That’s it our crypto service is now ready to handle all the flows i.e. SignUp, Login and Password reset.
Let’s now create a function to demonstrate the Signup process
// signup function will return a SignUpResult
class PrivateKeyEncryptionResult {
& emsp;final String publicKey;
final String encryptedPrivateKey;
final String randomSaltOne;
final String randomSaltTwo;
PrivateKeyEncryptionResult({
& emsp;required this.publicKey,
required this.encryptedPrivateKey,
required this.randomSaltOne,
required this.randomSaltTwo,
});
}
PrivateKeyEncryptionResult signUp(String password) {
& emsp;// Generate PBKDF key
final String randomSaltOne = CryptoService.generateRandomSalt();
final Uint8List pbkdfKey = CryptoService.generatePBKDFKey(password, randomSaltOne);
// Generate RSA Key Pair
final RSAKeypair keyPair = CryptoService.getKeyPair();
// Encrypt Private key
final privateKeySalt = CryptoService.generateRandomSalt();
final encryptedPrivateKey = CryptoService.symetricEncrypt(
pbkdfKey,
Uint8List.fromList(privateKeySalt.codeUnits),
Uint8List.fromList(keyPair.privateKey.toFormattedPEM().codeUnits),
);
return PrivateKeyEncryptionResult(
publicKey: keyPair.publicKey.toFormattedPEM(),
encryptedPrivateKey: String.fromCharCodes(encryptedPrivateKey),
randomSaltOne: randomSaltOne,
randomSaltTwo: privateKeySalt,
);
}

Let’s now add a login function to demonstrate the process.
// login function will return a LoginResult
class LoginResult {
& emsp;final String publicKey;
final String privateKey;
final String randomSaltOne;
final String randomSaltTwo;
LoginResult({
& emsp;required this.publicKey,
required this.privateKey,
required this.randomSaltOne,
required this.randomSaltTwo,
});
}
LoginResult login(SignUpResult signUpResult, String password) {
& emsp;// Generate pbkdfKey using the random salt stored in DB and user's password
final Uint8List pbkdfKey = CryptoService.generatePBKDFKey(
password,
signUpResult.randomSaltOne,
);
// decrypt private key using the pbkdfKey generated above
// and the second random slat stored in DB
Uint8List decryptedPrivateKey = CryptoService.symetricDecrypt(
pbkdfKey,
Uint8List.fromList(signUpResult.randomSaltTwo.codeUnits),
Uint8List.fromList(signUpResult.encryptedPrivateKey.codeUnits),
);
return LoginResult(
publicKey: signUpResult.publicKey,
privateKey: String.fromCharCodes(decryptedPrivateKey),
randomSaltOne: signUpResult.randomSaltOne,
randomSaltTwo: signUpResult.randomSaltTwo,
);
}

Login process is now complete, let’s work on the Password Reset process and create a function for that.
PrivateKeyEncryptionResult resetPassword(SignUpResult dataInDB, String currentPass, String newPass) {
& emsp;// Generate pbkdfKey using the random salt stored in DB and user's password
final Uint8List pbkdfKey = CryptoService.generatePBKDFKey(
currentPass,
dataInDB.randomSaltOne,
);
// decrypt private key
Uint8List decryptedPrivateKey = CryptoService.symetricDecrypt(
pbkdfKey,
Uint8List.fromList(dataInDB.randomSaltTwo.codeUnits),
Uint8List.fromList(dataInDB.encryptedPrivateKey.codeUnits),
);
// generate pbkdf key using new password
final Uint8List newPbkdfKey = CryptoService.generatePBKDFKey(
newPass,
dataInDB.randomSaltOne,
);
// encrypt private key with new pbkdf key
final encryptedPrivateKey = CryptoService.symetricEncrypt(
newPbkdfKey,
Uint8List.fromList(dataInDB.randomSaltTwo.codeUnits),
decryptedPrivateKey,
);
return PrivateKeyEncryptionResult(
publicKey: dataInDB.publicKey,
encryptedPrivateKey: String.fromCharCodes(encryptedPrivateKey),
randomSaltOne: dataInDB.randomSaltOne,
randomSaltTwo: dataInDB.randomSaltTwo,
);
}

At this point we have completed setup for an end-to-end encryption system. Let’s now add some code to give this system a trial run.
Congratulations, you have completed this looooong article, you now have created an end-to-end encryption system in Flutter. Great Job 👏.
Check out this entire code setup in a Github Gist
Anddddd, that’s it 🥳