Skip to main content
  1. Posts/

Proprietary Cryptographic Protocol: From Idea to Implementation. Cloudy - Part 2.

·7 mins· loading · loading · ·
Technical Flutter Dart
Table of Contents

Dance Like No One’s Watching Encrypt Like Everyone Is

Without a doubt, my favorite part of the Cloudy story is the design and implementation of the ETP protocol. Although it’s hard to call it revolutionary or a “killer” of modern solutions, it’s important to me because I came to its creation on my own, having gone through criticism and revision of various options. Yes, it can be said that I invented a bicycle, and it may be inferior to modern solutions, but it was in this process that I found real value for myself and my application.

Since this is the technical part, for the sake of clarity, I’ll cover step-by-step how the internal logic of the app happens in conjunction with account registration, sending, receiving messages, and how the encryption logic works there.

Concept of component interaction.
#

How did I see it in the beginning?

Hmm I want to be able to write and call and everything, and even with encryption :D.

A user comes in and registers. At this stage, a unique entity is created, based on which unique keys are generated for text encryption. It is desirable that the private keys remain exclusively on the user’s devices, while the public keys are stored on the server. When you create a dialog something happens somewhere keys appear and with these keys you encrypt messages and send them to the server.

Realization.
#

Downloading the app and registering an account.
#

As with any interaction with the app, the first step is to identify who you are. The first page asks the user to fill in mandatory fields (name, keyword phrase) and optional fields (interests, avatar, gender). The mandatory fields play a key role in identifying the account, while the optional fields serve only for additional personalization. The Key Derivation Function is responsible for generating your private and public keys. First, it is worth explaining what a public and private key are. These keys are part of asymmetric encryption, where data cannot be encrypted and decrypted with the same key. The public and private key, in turn, is a set of bytes linked together mathematically to form a pair that allows data to be exchanged securely. Due to the complex mathematical algorithms used to create the key pair, it is impossible to calculate the private key knowing only the public key. Why this is necessary will become clear later.

So, let’s get back to creating a profile. When you enter your nickname and passphrase, a unique hash code is created and reserved for you. The algorithm of generation looks like this:

String createAID(String nickname, List<String> keyPhrase) {
  String input = nickname + keyPhrase.join();
  
  var bytes = utf8.encode(input);
  var digest = sha256.convert(bytes);
  
  return digest.toString();
}

The output is a unique Authentication IDentificator. If a nickname is matched when registering another user, the system will report an error with a suggestion to choose a different name, preventing the possibility of matching a passphrase to log in. This is important because the authorization method relies heavily on this process. (In other words, if the system alerted on a hash match, knowing this data, a person could authorize under someone else’s credentials. After all, in essence, the login is a nickname, and the passphrase is a password.)

Implementing an encryption algorithm into the application
#

As mentioned earlier, by creating a unique hash, you are also generating:

a) A unique seed associated with your nickname and keyword phrase. With it, you can always log into your account from any device without losing dialogs. The data remains the same and the seed will also be the same.

Uint8List _createSeed(String nickname, List<String> keyPhrase) {
  final input = nickname + keyPhrase.join();
  final bytes = utf8.encode(input);
  final digest = sha256.convert(bytes);
  return Uint8List.fromList(digest.bytes);
}

b) unique keys unique to you:

Future<AsymmetricKeyPair<PublicKey, PrivateKey>> createKeyPair(
    String nickname, List<String> keyPhrase) async {
  final seed = _createSeed(nickname, keyPhrase);
  final secureRandom = _secureRandomFromSeed(seed);

  final keyGen = RSAKeyGenerator()
    ..init(ParametersWithRandom(
      RSAKeyGeneratorParameters(BigInt.parse('65537'), 2048, 64),
      secureRandom,
    ));

  final keys = keyGen.generateKeyPair();

  return keys;
}

By the way, the createKeyPair method uses the RSA algorithm to generate a key pair (public and private). RSA is an asymmetric encryption algorithm that provides data security based on the difficulty of factorizing large numbers. The security of the algorithm is based on the difficulty of factorizing the product of two large prime numbers. Since nicknames and passphrases are unique, the keys generated are also unique for each user. The BigInt.parse(‘65537’) parameter used is an open exponent, and the key size of 2048 bits gives a tangible cryptographic strength.

It turns out we already have both the keys and the AID. Fortunately, this is all we need to create a secure dialog. Let’s simulate the situation: I’m user A, you’re user B, and I want to write to you with your public hash(AID), and with the fact that you have access enabled to add you as a friend.

What’s going on under the hood?
#

Generate an AES key.
#

AES Advanced Encryption Standard is a symmetric block encryption algorithm that can be used for both encryption and decryption with the same key. I decided not to go into complicated details and to use an off-the-shelf solution for several reasons, the main one being the lack of knowledge and time to create my own algorithm. People spend years and sometimes generations to develop at least one effective algorithm, so I chose a more realistic approach.

I mention AES for a reason: when I initiate a dialog with you, the system generates a completely random key with which to encrypt all correspondence. The process looks something like this:

String generateRandomAESKey() {
  final keyBytes = Uint8List(32);
  final random = Random.secure();
  
  for (int i = 0; i < keyBytes.length; i++) {
    keyBytes[i] = random.nextInt(256);
  }
  
  return base64Encode(keyBytes);
}

Encrypt the symmetric key with public keys.
#

Now the most interesting thing: when the key is created, a request is made to the database to get your public key at the same second. Once it is received, the newly created symmetric key is encrypted with my public key and stored in a variable. The algorithm is as follows:

String encryptSymmetricKey(String symmetricKey, PublicKey publicKey) {
  final symmetricKeyBytes = base64Decode(symmetricKey);

  final encryptor = RSAEngine()
    ..init(true, PublicKeyParameter<RSAPublicKey>(publicKey as RSAPublicKey));

  final encrypted = _processInBlocks(encryptor, symmetricKeyBytes);

  return base64Encode(encrypted);
}

The same key is then encrypted with your public key and also stored in a variable. This variable is then sent to a database that will store the encrypted keys for the dialog. The data structure will look something like this:

{
  "<first my 4 digits AID>": "my encrypted key",
  "<your first 4 digits AID>": "your encrypted key".
}

Initiate correspondence.
#

Once we have a symmetric key encrypted with a public key (tautology, I know :D ), we can decrypt it as mentioned earlier. The decryption algorithm is as follows:

Uint8List decryptSymmetricKey(
    String encryptedSymmetricKey, PrivateKey privateKey) {
  final symmetricKeyBytes = base64Decode(encryptedSymmetricKey);

  final decryptor = RSAEngine()
    ..init(false, PrivateKeyParameter<RSAPrivateKey>(privateKey as RSAPrivateKey));

  final decryptedSymmetricKey = _processInBlocks(decryptor, symmetricKeyBytes);

  return decryptedSymmetricKey;
}

Now, using this decrypted symmetric key (as bytes), we encrypt each of our messages before sending them to the server, where they will be stored in the appropriate dialog folder.

Referring back to one post above:

They had to be stored encrypted, shown decrypted, and sent encrypted.

I believe that I have successfully accomplished this task. My algorithm not only does not expose the key with which the dialog is encrypted, but also does not store vulnerable data that can be decrypted. Thus, the security of the correspondence is ensured at every step.

What is the bottom line?
#

Yes, I may not have covered all the intricacies of the application itself, but here I tried to focus on the ETP (Edge Transport Protocol) encryption algorithm. This project brought a lot of new and interesting things to my history as a developer. Perhaps in the future I can refine it and turn it into something more complete and presentable. For now, I will leave it here as one of my favorite developments, version 1.0.0.

Bonus: the thumbnail is an illustration of ETP v 0.0.0.

ETP Protocol Image
Illustration of the first drafts of the ETP

upd 2.0: If you haven’t used or seen Cloudy in action - I’ll leave the video on github README

kmdshi/cloudy

Dart
3
0
Author
Bogdan Lipatov
Middle Flutter Developer & Knowledge enjoyer