1 /* Ricochet - https://ricochet.im/
2  * Copyright (C) 2014, John Brooks <john.brooks@dereferenced.net>
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *    * Redistributions of source code must retain the above copyright
9  *      notice, this list of conditions and the following disclaimer.
10  *
11  *    * Redistributions in binary form must reproduce the above
12  *      copyright notice, this list of conditions and the following disclaimer
13  *      in the documentation and/or other materials provided with the
14  *      distribution.
15  *
16  *    * Neither the names of the copyright owners nor the names of its
17  *      contributors may be used to endorse or promote products derived from
18  *      this software without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32 
33 #include "ContactUser.h"
34 #include "UserIdentity.h"
35 #include "ContactsManager.h"
36 #include "utils/SecureRNG.h"
37 #include "utils/Useful.h"
38 #include "core/ContactIDValidator.h"
39 #include "core/OutgoingContactRequest.h"
40 #include "core/ConversationModel.h"
41 #include "tor/HiddenService.h"
42 #include "protocol/OutboundConnector.h"
43 #include <QtDebug>
44 #include <QDateTime>
45 #include <QTcpSocket>
46 #include <QtEndian>
47 
ContactUser(UserIdentity * ident,int id,QObject * parent)48 ContactUser::ContactUser(UserIdentity *ident, int id, QObject *parent)
49     : QObject(parent)
50     , identity(ident)
51     , uniqueID(id)
52     , m_connection(0)
53     , m_outgoingSocket(0)
54     , m_status(Offline)
55     , m_lastReceivedChatID(0)
56     , m_contactRequest(0)
57     , m_settings(0)
58     , m_conversation(0)
59 {
60     Q_ASSERT(uniqueID >= 0);
61 
62     m_settings = new SettingsObject(QStringLiteral("contacts.%1").arg(uniqueID));
63     connect(m_settings, &SettingsObject::modified, this, &ContactUser::onSettingsModified);
64 
65     m_conversation = new ConversationModel(this);
66     m_conversation->setContact(this);
67 
68     loadContactRequest();
69     updateStatus();
70     updateOutgoingSocket();
71 }
72 
~ContactUser()73 ContactUser::~ContactUser()
74 {
75     delete m_settings;
76 }
77 
loadContactRequest()78 void ContactUser::loadContactRequest()
79 {
80     if (m_contactRequest)
81         return;
82 
83     if (m_settings->read("request.status") != QJsonValue::Undefined) {
84         m_contactRequest = new OutgoingContactRequest(this);
85         connect(m_contactRequest, &OutgoingContactRequest::statusChanged, this, &ContactUser::updateStatus);
86         connect(m_contactRequest, &OutgoingContactRequest::removed, this, &ContactUser::requestRemoved);
87         connect(m_contactRequest, &OutgoingContactRequest::accepted, this, &ContactUser::requestAccepted);
88         updateStatus();
89     }
90 }
91 
addNewContact(UserIdentity * identity,int id)92 ContactUser *ContactUser::addNewContact(UserIdentity *identity, int id)
93 {
94     ContactUser *user = new ContactUser(identity, id);
95     user->settings()->write("whenCreated", QDateTime::currentDateTime());
96 
97     return user;
98 }
99 
updateStatus()100 void ContactUser::updateStatus()
101 {
102     Status newStatus;
103     if (m_contactRequest) {
104         if (m_contactRequest->status() == OutgoingContactRequest::Error ||
105             m_contactRequest->status() == OutgoingContactRequest::Rejected)
106         {
107             newStatus = RequestRejected;
108         } else {
109             newStatus = RequestPending;
110         }
111     } else if (m_connection && m_connection->isConnected()) {
112         newStatus = Online;
113     } else if (settings()->read("rejected").toBool()) {
114         newStatus = RequestRejected;
115     } else if (settings()->read("sentUpgradeNotification").toBool()) {
116         newStatus = Outdated;
117     } else {
118         newStatus = Offline;
119     }
120 
121     if (newStatus == m_status)
122         return;
123 
124     m_status = newStatus;
125     emit statusChanged();
126 
127     updateOutgoingSocket();
128 }
129 
onSettingsModified(const QString & key,const QJsonValue & value)130 void ContactUser::onSettingsModified(const QString &key, const QJsonValue &value)
131 {
132     Q_UNUSED(value);
133     if (key == QLatin1String("nickname"))
134         emit nicknameChanged();
135 }
136 
updateOutgoingSocket()137 void ContactUser::updateOutgoingSocket()
138 {
139     if (m_status != Offline && m_status != RequestPending) {
140         if (m_outgoingSocket) {
141             m_outgoingSocket->disconnect(this);
142             m_outgoingSocket->abort();
143             m_outgoingSocket->deleteLater();
144             m_outgoingSocket = 0;
145         }
146         return;
147     }
148 
149     // Refuse to make outgoing connections to the local hostname
150     if (hostname() == identity->hostname())
151         return;
152 
153     if (m_outgoingSocket && m_outgoingSocket->status() == Protocol::OutboundConnector::Ready) {
154         BUG() << "Called updateOutgoingSocket with an existing socket in Ready. This should've been deleted.";
155         m_outgoingSocket->disconnect(this);
156         m_outgoingSocket->deleteLater();
157         m_outgoingSocket = 0;
158     }
159 
160     if (!m_outgoingSocket) {
161         m_outgoingSocket = new Protocol::OutboundConnector(this);
162         m_outgoingSocket->setAuthPrivateKey(identity->hiddenService()->privateKey());
163         connect(m_outgoingSocket, &Protocol::OutboundConnector::ready, this,
164             [this]() {
165                 assignConnection(m_outgoingSocket->takeConnection());
166             }
167         );
168 
169         /* As an ugly hack, because Ricochet 1.0.x versions have no way to notify about
170          * protocol issues, and it's not feasible to support both protocols for this
171          * tiny upgrade period:
172          *
173          * The first time we make an outgoing connection to an existing contact, if they
174          * are using the old version, send a chat message that lets them know about the
175          * new version, then disconnect. This message is only sent once per contact.
176          *
177          * XXX: This logic should be removed an appropriate amount of time after the new
178          * protocol has been released.
179          */
180         connect(m_outgoingSocket, &Protocol::OutboundConnector::oldVersionNegotiated, this,
181             [this](QTcpSocket *socket) {
182                 if (m_settings->read("sentUpgradeNotification").toBool())
183                     return;
184                 QByteArray secret = m_settings->read<Base64Encode>("remoteSecret");
185                 if (secret.size() != 16)
186                     return;
187 
188                 static const char upgradeMessage[] =
189                     "[automatic message] I'm using a newer version of Ricochet that is not "
190                     "compatible with yours. This is a one-time change to help improve Ricochet. "
191                     "See https://ricochet.im/upgrade for instructions on getting the latest "
192                     "version. Once you have upgraded, I will be able to see your messages again.";
193                 uchar command[] = {
194                     0x00, 0x00, 0x10, 0x00, 0x00, 0x01, 0x00,
195                     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
196                 };
197 
198                 qToBigEndian(quint16(sizeof(upgradeMessage) + 7), command);
199                 qToBigEndian(quint16(sizeof(upgradeMessage) - 1), command + sizeof(command) - sizeof(quint16));
200 
201                 QByteArray data;
202                 data.append((char)0x00);
203                 data.append(secret);
204                 data.append(reinterpret_cast<const char*>(command), sizeof(command));
205                 data.append(upgradeMessage);
206                 socket->write(data);
207 
208                 m_settings->write("sentUpgradeNotification", true);
209                 updateStatus();
210             }
211         );
212     }
213 
214     m_outgoingSocket->connectToHost(hostname(), port());
215 }
216 
onConnected()217 void ContactUser::onConnected()
218 {
219     if (!m_connection || !m_connection->isConnected()) {
220         /* This case can happen if disconnected very quickly after connecting,
221          * before the (queued) slot has been called. Ignore the signal.
222          */
223         return;
224     }
225 
226     m_settings->write("lastConnected", QDateTime::currentDateTime());
227 
228     if (m_contactRequest && m_connection->purpose() == Protocol::Connection::Purpose::OutboundRequest) {
229         qDebug() << "Sending contact request for" << uniqueID << nickname();
230         m_contactRequest->sendRequest(m_connection);
231     }
232 
233     if (!m_settings->read("sentUpgradeNotification").isNull())
234         m_settings->unset("sentUpgradeNotification");
235 
236     /* The 'rejected' mark comes from failed authentication to someone who we thought was a known
237      * contact. Normally, it would mean that you were removed from that person's contacts. It's
238      * possible for this to be undone; for example, if that person sends you a new contact request,
239      * it will be automatically accepted. If this happens, unset the 'rejected' flag for correct UI.
240      */
241     if (m_settings->read("rejected").toBool()) {
242         qDebug() << "Contact had marked us as rejected, but now they've connected again. Re-enabling.";
243         m_settings->unset("rejected");
244     }
245 
246     updateStatus();
247     if (isConnected()) {
248         emit connected();
249         emit connectionChanged(m_connection);
250     }
251 
252     if (m_status != Online && m_status != RequestPending) {
253         BUG() << "Contact has a connection while in status" << m_status << "which is not expected.";
254         m_connection->close();
255     }
256 }
257 
onDisconnected()258 void ContactUser::onDisconnected()
259 {
260     qDebug() << "Contact" << uniqueID << "disconnected";
261     m_settings->write("lastConnected", QDateTime::currentDateTime());
262 
263     if (m_connection) {
264         if (m_connection->isConnected()) {
265             BUG() << "onDisconnected called, but connection is still connected";
266             return;
267         }
268 
269         m_connection.clear();
270     } else {
271         BUG() << "onDisconnected called without a connection";
272     }
273 
274     updateStatus();
275     emit disconnected();
276     emit connectionChanged(m_connection);
277 }
278 
settings()279 SettingsObject *ContactUser::settings()
280 {
281     return m_settings;
282 }
283 
nickname() const284 QString ContactUser::nickname() const
285 {
286     return m_settings->read("nickname").toString();
287 }
288 
setNickname(const QString & nickname)289 void ContactUser::setNickname(const QString &nickname)
290 {
291     m_settings->write("nickname", nickname);
292 }
293 
hostname() const294 QString ContactUser::hostname() const
295 {
296     return m_settings->read("hostname").toString();
297 }
298 
port() const299 quint16 ContactUser::port() const
300 {
301     return m_settings->read("port", 9878).toInt();
302 }
303 
contactID() const304 QString ContactUser::contactID() const
305 {
306     return ContactIDValidator::idFromHostname(hostname());
307 }
308 
setHostname(const QString & hostname)309 void ContactUser::setHostname(const QString &hostname)
310 {
311     QString fh = hostname;
312 
313     if (!hostname.endsWith(QLatin1String(".onion")))
314         fh.append(QLatin1String(".onion"));
315 
316     m_settings->write("hostname", fh);
317     updateOutgoingSocket();
318 }
319 
deleteContact()320 void ContactUser::deleteContact()
321 {
322     /* Anything that uses ContactUser is required to either respond to the contactDeleted signal
323      * synchronously, or make use of QWeakPointer. */
324 
325     qDebug() << "Deleting contact" << uniqueID;
326 
327     if (m_contactRequest) {
328         qDebug() << "Cancelling request associated with contact to be deleted";
329         m_contactRequest->cancel();
330         m_contactRequest->deleteLater();
331     }
332 
333     emit contactDeleted(this);
334 
335     m_settings->undefine();
336     deleteLater();
337 }
338 
requestAccepted()339 void ContactUser::requestAccepted()
340 {
341     if (!m_contactRequest) {
342         BUG() << "Request accepted but ContactUser doesn't know an active request";
343         return;
344     }
345 
346     if (m_connection) {
347         m_connection->setPurpose(Protocol::Connection::Purpose::KnownContact);
348         emit connected();
349     }
350 
351     requestRemoved();
352 }
353 
requestRemoved()354 void ContactUser::requestRemoved()
355 {
356     if (m_contactRequest) {
357         m_contactRequest->deleteLater();
358         m_contactRequest = 0;
359         updateStatus();
360     }
361 }
362 
assignConnection(const QSharedPointer<Protocol::Connection> & connection)363 void ContactUser::assignConnection(const QSharedPointer<Protocol::Connection> &connection)
364 {
365     if (connection == m_connection) {
366         BUG() << "Connection is already assigned to this ContactUser";
367         return;
368     }
369 
370     if (connection->purpose() == Protocol::Connection::Purpose::KnownContact) {
371         BUG() << "Connection is already assigned to a contact";
372         connection->close();
373         return;
374     }
375 
376     bool isOutbound = connection->direction() == Protocol::Connection::ClientSide;
377 
378     if (!connection->isConnected()) {
379         BUG() << "Connection assigned to contact but isn't connected; discarding";
380         connection->close();
381         return;
382     }
383 
384     if (!connection->hasAuthenticatedAs(Protocol::Connection::HiddenServiceAuth, hostname())) {
385         BUG() << "Connection assigned to contact without matching authentication";
386         connection->close();
387         return;
388     }
389 
390     /* KnownToPeer is set for an outbound connection when the remote end indicates
391      * that it knows us as a contact. If this is set, we can assume that the
392      * connection is fully built and will be kept open.
393      *
394      * If this isn't a request and KnownToPeer is not set, the connection has
395      * effectively failed: it will be timed out and closed without a purpose.
396      * This probably means that peer removed us a contact.
397      */
398     if (isOutbound) {
399         bool knownToPeer = connection->hasAuthenticated(Protocol::Connection::KnownToPeer);
400         if (m_contactRequest && knownToPeer) {
401             m_contactRequest->accept();
402             if (m_contactRequest)
403                 BUG() << "Outgoing contact request not unset after implicit accept during connection";
404         } else if (!m_contactRequest && !knownToPeer) {
405             qDebug() << "Contact says we're unknown; marking as rejected";
406             settings()->write("rejected", true);
407             connection->close();
408             updateStatus();
409             updateOutgoingSocket();
410             return;
411         }
412     }
413 
414     if (m_connection && !m_connection->isConnected()) {
415         qDebug() << "Replacing dead connection with new connection";
416         clearConnection();
417     }
418 
419     /* To resolve a race if two contacts try to connect at the same time:
420      *
421      * If the existing connection is in the same direction as the new one,
422      * always use the new one.
423      */
424     if (m_connection && connection->direction() == m_connection->direction()) {
425         qDebug() << "Replacing existing connection with contact because the new one goes the same direction";
426         clearConnection();
427     }
428 
429     /* If the existing connection is more than 30 seconds old, measured from
430      * when it was successfully established, it's replaced with the new one.
431      */
432     if (m_connection && m_connection->age() > 30) {
433         qDebug() << "Replacing existing connection with contact because it's more than 30 seconds old";
434         clearConnection();
435     }
436 
437     /* Otherwise, close the connection for which the server's onion-formatted
438      * hostname compares less with a strcmp function
439      */
440     bool preferOutbound = QString::compare(hostname(), identity->hostname()) < 0;
441     if (m_connection) {
442         if (isOutbound == preferOutbound) {
443             // New connection wins
444             clearConnection();
445         } else {
446             // Old connection wins
447             qDebug() << "Closing new connection with contact because the old connection won comparison";
448             connection->close();
449             return;
450         }
451     }
452 
453      /* If this connection is inbound and we have an outgoing connection attempt,
454       * use the inbound connection if we haven't sent authentication yet, or if
455       * we would lose the strcmp comparison above.
456       */
457     if (!isOutbound && m_outgoingSocket) {
458         if (m_outgoingSocket->status() != Protocol::OutboundConnector::Authenticating || !preferOutbound) {
459             // Inbound connection wins; outbound connection attempt will abort when status changes
460             qDebug() << "Aborting outbound connection attempt because we got an inbound connection instead";
461         } else {
462             // Outbound attempt wins
463             qDebug() << "Closing inbound connection with contact because the pending outbound connection won comparison";
464             connection->close();
465             return;
466         }
467     }
468 
469     if (m_connection) {
470         BUG() << "After resolving connection races, ContactUser still has two connections";
471         connection->close();
472         return;
473     }
474 
475     qDebug() << "Assigned" << (isOutbound ? "outbound" : "inbound") << "connection to contact" << uniqueID;
476 
477     if (m_contactRequest && isOutbound) {
478         if (!connection->setPurpose(Protocol::Connection::Purpose::OutboundRequest)) {
479             qWarning() << "BUG: Failed setting connection purpose for request";
480             connection->close();
481             return;
482         }
483     } else {
484         if (m_contactRequest && !isOutbound) {
485             qDebug() << "Implicitly accepting outgoing contact request for" << uniqueID << "due to incoming connection";
486             m_contactRequest->accept();
487         }
488 
489         if (!connection->setPurpose(Protocol::Connection::Purpose::KnownContact)) {
490             qWarning() << "BUG: Failed setting connection purpose";
491             connection->close();
492             return;
493         }
494     }
495 
496     m_connection = connection;
497 
498     /* Use a queued connection to onDisconnected, because it clears m_connection.
499      * If we cleared that immediately, it would be possible for the value to change
500      * effectively any time we call into protocol code, which would be dangerous.
501      */
502     connect(m_connection.data(), &Protocol::Connection::closed, this, &ContactUser::onDisconnected, Qt::QueuedConnection);
503 
504     /* Delay the call to onConnected to allow protocol code to finish before everything
505      * kicks in. In particular, this is important to allow AuthHiddenServiceChannel to
506      * respond before other channels are created. */
507     if (!metaObject()->invokeMethod(this, "onConnected", Qt::QueuedConnection))
508         BUG() << "Failed queuing invocation of onConnected method";
509 }
510 
clearConnection()511 void ContactUser::clearConnection()
512 {
513     if (!m_connection)
514         return;
515 
516     disconnect(m_connection.data(), 0, this, 0);
517     m_connection->close();
518     m_connection.clear();
519 }
520 
521