aboutsummaryrefslogtreecommitdiff
path: root/plugins/omemo/src/logic/decrypt.vala
blob: cfbb9c58bf5f3eae6d7ccda202cae09b844c515e (plain) (blame)
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
using Dino.Entities;
using Qlite;
using Gee;
using Signal;
using Xmpp;

namespace Dino.Plugins.Omemo {

    public class OmemoDecryptor : Xep.Omemo.OmemoDecryptor {

        private Account account;
        private Store store;
        private Database db;
        private StreamInteractor stream_interactor;
        private TrustManager trust_manager;

        public override uint32 own_device_id { get { return store.local_registration_id; }}

        public OmemoDecryptor(Account account, StreamInteractor stream_interactor, TrustManager trust_manager, Database db, Store store) {
            this.account = account;
            this.stream_interactor = stream_interactor;
            this.trust_manager = trust_manager;
            this.db = db;
            this.store = store;
        }

        public bool decrypt_message(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
            StanzaNode? encrypted_node = stanza.stanza.get_subnode("encrypted", NS_URI);
            if (encrypted_node == null || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false;

            if (message.body == null && Xep.ExplicitEncryption.get_encryption_tag(stanza) == NS_URI) {
                message.body = "[This message is OMEMO encrypted]"; // TODO temporary
            }
            if (!Plugin.ensure_context()) return false;
            int identity_id = db.identity.get_id(conversation.account.id);

            MessageFlag flag = new MessageFlag();
            stanza.add_flag(flag);

            Xep.Omemo.ParsedData? data = parse_node(encrypted_node);
            if (data == null || data.ciphertext == null) return false;


            foreach (Bytes encr_key in data.our_potential_encrypted_keys.keys) {
                data.is_prekey = data.our_potential_encrypted_keys[encr_key];
                data.encrypted_key = encr_key.get_data();
                Gee.List<Jid> possible_jids = get_potential_message_jids(message, data, identity_id);
                if (possible_jids.size == 0) {
                    debug("Received message from unknown entity with device id %d", data.sid);
                }

                foreach (Jid possible_jid in possible_jids) {
                    try {
                        uint8[] key = decrypt_key(data, possible_jid);
                        string cleartext = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, data.iv, data.ciphertext));

                        // If we figured out which real jid a message comes from due to decryption working, save it
                        if (conversation.type_ == Conversation.Type.GROUPCHAT && message.real_jid == null) {
                            message.real_jid = possible_jid;
                        }

                        message.body = cleartext;
                        message.encryption = Encryption.OMEMO;

                        trust_manager.message_device_id_map[message] = data.sid;
                        return true;
                    } catch (Error e) {
                        debug("Decrypting message from %s/%d failed: %s", possible_jid.to_string(), data.sid, e.message);
                    }
                }
            }

            if (
                encrypted_node.get_deep_string_content("payload") != null && // Ratchet forwarding doesn't contain payload and might not include us, which is ok
                data.our_potential_encrypted_keys.size == 0 && // The message was not encrypted to us
                stream_interactor.module_manager.get_module(message.account, StreamModule.IDENTITY).store.local_registration_id != data.sid // Message from this device. Never encrypted to itself.
            ) {
                db.identity_meta.update_last_message_undecryptable(identity_id, data.sid, message.time);
                trust_manager.bad_message_state_updated(conversation.account, message.from, data.sid);
            }

            debug("Received OMEMO encryped message that could not be decrypted.");
            return false;
        }

        public Gee.List<Jid> get_potential_message_jids(Entities.Message message, Xmpp.Xep.Omemo.ParsedData data, int identity_id) {
            Gee.List<Jid> possible_jids = new ArrayList<Jid>();
            if (message.type_ == Message.Type.CHAT) {
                possible_jids.add(message.from.bare_jid);
            } else {
                if (message.real_jid != null) {
                    possible_jids.add(message.real_jid.bare_jid);
                } else if (data.is_prekey) {
                    // pre key messages do store the identity key, so we can use that to find the real jid
                    PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(data.encrypted_key);
                    string identity_key = Base64.encode(msg.identity_key.serialize());
                    foreach (Row row in db.identity_meta.get_with_device_id(identity_id, data.sid).with(db.identity_meta.identity_key_public_base64, "=", identity_key)) {
                        try {
                            possible_jids.add(new Jid(row[db.identity_meta.address_name]));
                        } catch (InvalidJidError e) {
                            warning("Ignoring invalid jid from database: %s", e.message);
                        }
                    }
                } else {
                    // If we don't know the device name (MUC history w/o MAM), test decryption with all keys with fitting device id
                    foreach (Row row in db.identity_meta.get_with_device_id(identity_id, data.sid)) {
                        try {
                            possible_jids.add(new Jid(row[db.identity_meta.address_name]));
                        } catch (InvalidJidError e) {
                            warning("Ignoring invalid jid from database: %s", e.message);
                        }
                    }
                }
            }
            return possible_jids;
        }

        public override uint8[] decrypt_key(Xmpp.Xep.Omemo.ParsedData data, Jid from_jid) throws GLib.Error {
            int sid = data.sid;
            uint8[] ciphertext = data.ciphertext;
            uint8[] encrypted_key = data.encrypted_key;

            Address address = new Address(from_jid.to_string(), sid);
            uint8[] key;

            if (data.is_prekey) {
                int identity_id = db.identity.get_id(account.id);
                PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(encrypted_key);
                string identity_key = Base64.encode(msg.identity_key.serialize());

                bool ok = update_db_for_prekey(identity_id, identity_key, from_jid, sid);
                if (!ok) return null;

                debug("Starting new session for decryption with device from %s/%d", from_jid.to_string(), sid);
                SessionCipher cipher = store.create_session_cipher(address);
                key = cipher.decrypt_pre_key_signal_message(msg);
                // TODO: Finish session
                } else {
                debug("Continuing session for decryption with device from %s/%d", from_jid.to_string(), sid);
                SignalMessage msg = Plugin.get_context().deserialize_signal_message(encrypted_key);
                SessionCipher cipher = store.create_session_cipher(address);
                key = cipher.decrypt_signal_message(msg);
            }

            if (key.length >= 32) {
                int authtaglength = key.length - 16;
                uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength];
                uint8[] new_key = new uint8[16];
                Memory.copy(new_ciphertext, ciphertext, ciphertext.length);
                Memory.copy((uint8*)new_ciphertext + ciphertext.length, (uint8*)key + 16, authtaglength);
                Memory.copy(new_key, key, 16);
                data.ciphertext = new_ciphertext;
                key = new_key;
            }

            return key;
        }

        public override string decrypt(uint8[] ciphertext, uint8[] key, uint8[] iv) throws GLib.Error {
            return arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, ciphertext));
        }

        private bool update_db_for_prekey(int identity_id, string identity_key, Jid from_jid, int sid) {
            Row? device = db.identity_meta.get_device(identity_id, from_jid.to_string(), sid);
            if (device != null && device[db.identity_meta.identity_key_public_base64] != null) {
                if (device[db.identity_meta.identity_key_public_base64] != identity_key) {
                    critical("Tried to use a different identity key for a known device id.");
                    return false;
                }
            } else {
                debug("Learn new device from incoming message from %s/%d", from_jid.to_string(), sid);
                bool blind_trust = db.trust.get_blind_trust(identity_id, from_jid.to_string(), true);
                if (db.identity_meta.insert_device_session(identity_id, from_jid.to_string(), sid, identity_key, blind_trust ? TrustLevel.TRUSTED : TrustLevel.UNKNOWN) < 0) {
                    critical("Failed learning a device.");
                    return false;
                }

                XmppStream? stream = stream_interactor.get_stream(account);
                if (device == null && stream != null) {
                    stream.get_module(StreamModule.IDENTITY).request_user_devicelist.begin(stream, from_jid);
                }
            }
            return true;
        }

        private string arr_to_str(uint8[] arr) {
            // null-terminate the array
            uint8[] rarr = new uint8[arr.length+1];
            Memory.copy(rarr, arr, arr.length);
            return (string)rarr;
        }
    }

    public class DecryptMessageListener : MessageListener {
        public string[] after_actions_const = new string[]{ };
        public override string action_group { get { return "DECRYPT"; } }
        public override string[] after_actions { get { return after_actions_const; } }

        private HashMap<Account, OmemoDecryptor> decryptors;

        public DecryptMessageListener(HashMap<Account, OmemoDecryptor> decryptors) {
            this.decryptors = decryptors;
        }

        public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
            decryptors[message.account].decrypt_message(message, stanza, conversation);
            return false;
        }
    }
}