Search code examples
pythonpython-3.xencryptionaes-gcmpushbullet

InvalidTag error decrypting pushbullet notification with python-cryptography


I am trying to implement End-to-end encryption support for pushbullet ephemeral messages in python3.

I'm using python-cryptography, but I get an InvalidTag-Exception while decrypting. I have double checked the key, iv and tag, but I can't figure out where it goes wrong.

The key is derived like this:

    salt = user_ident.encode()
    pw = password.encode()

    kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(), 
            length=32,
            salt=salt,
            iterations=30000,
            backend=backend)

    dkey = kdf.derive(pw)

It is then stored in a keyring as Base64 encoded string, but I double checked if I get the right byte string when encrypting (also by doing it manually in the REPL).

Decrypt:

ciphertxt = a2b_base64(msg['ciphertext'])
version = ciphertxt[0:1]
tag = ciphertxt[1:17]
iv = ciphertxt[17:29]
enc_msg = ciphertxt[29:]

# Construct an AES-GCM Cipher object
decryptor = Cipher(
    algorithms.AES(self.dkey_),
    modes.GCM(iv, tag),
    backend=backend
).decryptor()

cleartxt = decryptor.update(enc_msg) + decryptor.finalize()

All vars are byte strings, here the relevant docs of python-cryptography.

To clarify: I have tried my own methods to encrypt and successfully decrypt some text. But when I activate Pushbullet e2e encryption on my phone and my client and I receive a notification, I get the error above.

The encryption method assembles the encrypted message like this:

b'1' + encryptor.tag + iv + ciphertxt

And I can decipher it. Doesn't work with the tag from a received message.

Any ideas? :/


Solution

  • I recently added some interactive javascript stuff on the docs page: https://docs.pushbullet.com/#example-encrypt-a-message

    I have found that the best way to debug an issue like this is to test each part in isolation and make sure you get the correct output for a given input.

    In your case, I think you should create a key, IV and message that don't change, and make sure your library produces the same encrypted_message as the javascript code in that example does. Here is how this might look:

    // convert key from base64 to binary
    var key = atob("1sW28zp7CWv5TtGjlQpDHHG4Cbr9v36fG5o4f74LsKg=");
    var initialization_vector = atob("O2QAL8AYQB+qbre8"); // 96-bit
    var message = "meow!";
    
    var cipher = forge.cipher.createCipher('AES-GCM', key);
    cipher.start({"iv": initialization_vector});
    cipher.update(forge.util.createBuffer(forge.util.encodeUtf8(message)));
    cipher.finish();
    
    var tag = cipher.mode.tag.getBytes();
    console.log("tag", btoa(tag));
    var encrypted_message = cipher.output.getBytes();
    console.log("encrypted_message", btoa(encrypted_message));
    

    The output for that is:

    tag OBA7UU/Rd9j0Zn+9korAyQ== 
    encrypted_message 7YS1aTE= 
    

    Once your python encryption matches this, you should make sure the decryption part works.

    var key = atob("1sW28zp7CWv5TtGjlQpDHHG4Cbr9v36fG5o4f74LsKg=")
    var tag = atob("OBA7UU/Rd9j0Zn+9korAyQ==")
    var initialization_vector = atob("O2QAL8AYQB+qbre8"); // 96 bits
    var encrypted_message = atob("7YS1aTE=");
    
    var decipher = forge.cipher.createDecipher('AES-GCM', key);
    decipher.start({
        'iv': initialization_vector,
        'tag': tag
    });
    decipher.update(forge.util.createBuffer(encrypted_message));
    decipher.finish();
    
    var message = decipher.output.toString('utf8');
    console.log("message:", message);
    

    Which should print:

    message: meow! 
    

    I don't have any experience with that particular python library, but if you use this debugging technique you should be able to narrow down where the problem lies.