Telegram: goto fail (twice)

Probably everyone knows that a couple of days ago Facebook acquired WhatsApp for the astonishing price of 19 billion dollars. Anyway that's not the point of this post. The problem here is the inherent privacy problem that this acquisition involves. Online there are tons of people that are concerned for their privacy and they are looking for possible instant messagging alternatives which are encrypted and privacy aware. Probably the one which is receiving more attention right now is Telegram.

I gave a quick read about protocol specification and tried to understand how OTR messaging is implemented to offer end to end encryption and Perfect Forward Secrecy. I am not an expert in cryptography but here's what I understand from a quick read of the protocol:

  • It is not a zero knowledge architecture. In telegram you have to trust someone, and that someone is the Telegram server
  • Chats and group chats are not encrypted end-to-end. What I mean here is that, although the message exchange between you and server is adequately encrypted, the server has still to decrypt (and possibly read/store/analyze your incoming messages) before relaying to the final recipient(s).
  • Secret chats implement OTR through Diffie Hellman algorithm key exchange, but Diffie Hellman does not provide authentication by default so it is vulnerable to MITM attacks. It is theoretically possible that the Telegram server acts as Mallory here and intercepting every single message being exchanged between you (Alice) and your bestest friend Bob.
  • Perfect Forward Secrecy is said to be implemented but it is not. If you want PFS you simply have to create another Secret Chat. Cool.

What a bummer! EDIT: Apparently I am not the only one who thinks that the entire protocol and trust the server approach is flawed. Take a look at this blog post that explains each point in detail.

I looked around to check whether it is possible to create a zero-knowledge architecture supporting multi party OTR messaging. The first place I looked for was Wikipedia, that explicitly says:

Due to limitations of the protocol, OTR does not support multi-user group chat as of 2009 but may be implemented in the future.

So I started searching for existing literature in this field using Google Scholar. I found several papers trying to solve the problem (see Goldberg et al. and Liu et al.). But since I am not an expert in cryptography I was looking for something lighter to read. I discovered that the problem was actually solved (I guess, but please correct me if I am wrong) by the guys behind Syme, a zero-knowledge key architecture and encrypted messaging platform. You can find the white paper of their architecture here. Starting from their premises I decided to implement a demo app in Python implementing multiparty OTR.

I am gonna be using pyelliptic, pbkdf2 and srp. The first package is a really cool binding to OpenSSL, but be sure to have an up to date version otherwise you will encounter in some trouble. If you are a Mac OS user I suggest you to install an updated version of openssl using brew install openssl.

Then you can safely use DYLD_LIBRARY_PATH=/usr/local/Cellar/openssl/1.0.1f/lib prior to python invokation, as I explain in this Github issue. The second package (pbkdf2) provides the password-based key derivation function, PBKDF2. The latter implements the Secure Remote Password protocol (SRP), a cryptographically strong authentication protocol for password-based over an insecure network connection. You can easilly install the three dependecies with the usual pip install <packagename>.

You can find the code of this demo on GitHub. I am not going to do a step by step explanation of the code but rather explain what it happens from an high level perspective. First we import all the libraries:

1
2
3
4
import json
import pbkdf2
from srp import _pysrp as srp
import pyelliptic

We then define an helper class for dealing with AES-CFB (256 bit key) encryption/decription of texts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class AESCipher(object):
    def __init__(self, key):
        self.key = key

    def pad(self, s):
        BS = 16
        return s + (BS - len(s) % BS) * chr(BS - len(s) % BS) 

    def unpad(self, s):
        return s[0:-ord(s[-1])]

    def encrypt(self, raw):
        raw = self.pad(raw)
        iv = pyelliptic.Cipher.gen_IV('aes-256-cfb')
        ctx = pyelliptic.Cipher(self.key, iv, 1, ciphername='aes-256-cfb')
        ciphertext = ctx.update(raw)
        ciphertext += ctx.final()
        return (iv + ciphertext).encode("hex")

    def decrypt(self, enc):
        enc = enc.decode("hex")
        (iv, enc) = enc[:16], enc[16:]
        ctx = pyelliptic.Cipher(self.key, iv, 0, ciphername='aes-256-cfb')
        return self.unpad(ctx.ciphering(enc))

We then define a class for our zero-knowledge server, as in Syme whitepaper. The server will just store a salt and a verification key for each user (SRP protocol). The server can additionally store the keychain of each user (encrypted with the other initialization_key of the client), if the user is willing to do so.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Server(object):
    def __init__(self):
        self.users = {}
        self.verifiers = {}
        self.storage = {}

    def auth_request(self, username, A):
        salt, vkey = self.users[username]

        verifier = srp.Verifier(username, salt, vkey, A)
        s, B = verifier.get_challenge()

        self.verifiers[username] = verifier

        return s, B

    def verify_session(self, username, M):
        HAMK = self.verifiers[username].verify_session(M)

        if HAMK is None:
            raise srp.AuthenticationFailed()

        return HAMK

    def store(self, username, **kwargs):
        self.storage[username] = kwargs
        print "Stored %s for %s" % (', '.join(kwargs.keys()), username)

We then define User object representing the user that tries to register to the Server and communicates with other peers through the use of send_message primitive. Here we assume the server to correctly deliver ECC public keys. MITM attack is possible if the server is acting as a rouge server. In order to solve this problem the exchange of keys can be executed offline. In this case Alice and Bob will set up an appointment in the public park to exchange their public keys. Another approach is using the Socialist Millionaire problem (funny name) as in Section 2.3.5 of A User Study of Off-the-Record Messaging .

Creating a group chat (2 or more peers) involves the creation of new ECC key pairs for each peer. In this way it is possible to easilly implement forward secrecy. For example, every day the peers can decide to generate new key pairs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def create_verification_key(user, salt):
    hash_class = user.hash_class
    return srp.long_to_bytes(pow(user.g, srp.gen_x(hash_class, salt, user.I, user.p), user.N))

class User(object):
    def __init__(self, username, password):
        self.username = username
        self.salt = srp.long_to_bytes(srp.get_random(16))

        key = pbkdf2.PBKDF2(password, self.salt).read(64)
        self.authentication_key, self.initialization_key = (key[:32], key[32:])

        self.cipher = AESCipher(self.initialization_key)
        self.ecc_key = pyelliptic.ECC()

        self.keychain = {
            self.username: self.ecc_key.get_pubkey(),
        }

        self.ecc_group_key = {}
        self.group_keys = {}

    def get_srp_user(self):
        username = self.username
        password = self.initialization_key
        return srp.User(username, password)

    def register(self, server):
        assert self.username not in server.users, "%s already registered" % self.username

        salt = self.salt
        user = self.get_srp_user()

        server.users[self.username] = (salt, create_verification_key(user, salt))

    def login(self, server):
        user = self.get_srp_user()
        username, A = user.start_authentication()

        # We send username and A to the server and obtain a challenge
        s, B = server.auth_request(username, A)
        M = user.process_challenge(s, B)

        if M is None:
            raise srp.AuthenticationFailed()

        # Send M to the verifier
        HAMK = server.verify_session(username, M)
        user.verify_session(HAMK)

        if user.authenticated():
            print "Successfully logged in"

            encrypted = self.cipher.encrypt(self.ecc_key.get_pubkey())

            server.store(username, mykey=encrypted)

    def create_group(self, name, users):
        # Now every other user will generate a pub/key pair
        for user in users:
            user.ecc_group_key[name] = pyelliptic.ECC()
            user.group_keys[name] = {
                user.username: user.ecc_group_key[name].get_pubkey()
            }

        for source in users:
            for dest in users:
                if source != dest:
                    source.group_keys[name][dest.username] = dest.ecc_group_key[name].get_pubkey()


    def send_message(self, name, message):
        session_key = pyelliptic.OpenSSL.rand(32)
        ekeys = []

        for username, pubkey in self.group_keys[name].items():
            ecc_key = self.ecc_group_key[name]
            ekeys.append(ecc_key.encrypt(session_key, pubkey).encode('hex'))

        c = AESCipher(session_key)
        emessage = c.encrypt(message)

        encoded = json.dumps({
            'group': name,
            'message': emessage,
            'keys': ekeys,
        })

        return Message(self.username, self.ecc_key.sign(encoded), encoded)

The last piece missing is the Message class representing the message being exchanged between peers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Message(object):
    def __init__(self, source, signature, encoded):
        self.source = source
        self.signature = signature
        self.encoded = encoded

    def verify(self, user):
        pubkey_source = user.keychain[self.source]
        return pyelliptic.ECC(pubkey=pubkey_source).verify(self.signature, self.encoded)

    def read(self, user):
        decoded = json.loads(self.encoded)
        ecc_key = user.ecc_group_key[decoded['group']]

        for key in decoded['keys']:
            try:
                session_key = ecc_key.decrypt(key.decode('hex'))
                c = AESCipher(session_key)
                print "%s received: %s" % (user.username, c.decrypt(decoded['message']))
                return
            except:
                print "Trying next key"

Now you can try to play the secure chat game easilly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server = Server()

alice = User("Alice", "hello")
alice.register(server)
alice.login(server)

bob = User("Bob", "hello")
bob.register(server)
bob.login(server)

alice.create_group('#lmv', (alice, bob))
message = alice.send_message('#lmv', 'Hello there')

message.read(bob)
message.read(alice)
1
2
3
4
5
6
7
Successfully logged in
Stored mykey for Alice
Successfully logged in
Stored mykey for Bob
Bob received: Hello there
Trying next key
Alice received: Hello there

DISCLAIMER: This entire protocol might be wrong and flawed. I am not a cryptographer so use the information of this blog post at your own discretion and risk.

PS: for those of you who understand the reference yes the title of this post referes to the Apple bug in SSL stack. If you are intereseted in reading more about it, check out this link.