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