001/** 002 * 003 * Copyright 2017 Paul Schaub, 2020 Florian Schmaus 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jivesoftware.smackx.omemo; 018 019import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL; 020 021import java.io.IOException; 022import java.security.NoSuchAlgorithmException; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.HashMap; 026import java.util.HashSet; 027import java.util.List; 028import java.util.Map; 029import java.util.Random; 030import java.util.Set; 031import java.util.SortedSet; 032import java.util.TreeMap; 033import java.util.WeakHashMap; 034import java.util.logging.Level; 035import java.util.logging.Logger; 036 037import org.jivesoftware.smack.ConnectionListener; 038import org.jivesoftware.smack.Manager; 039import org.jivesoftware.smack.SmackException; 040import org.jivesoftware.smack.SmackException.NotConnectedException; 041import org.jivesoftware.smack.XMPPConnection; 042import org.jivesoftware.smack.XMPPException; 043import org.jivesoftware.smack.packet.Message; 044import org.jivesoftware.smack.packet.MessageBuilder; 045import org.jivesoftware.smack.packet.Stanza; 046import org.jivesoftware.smack.util.Async; 047 048import org.jivesoftware.smackx.carbons.CarbonManager; 049import org.jivesoftware.smackx.carbons.packet.CarbonExtension; 050import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 051import org.jivesoftware.smackx.hints.element.StoreHint; 052import org.jivesoftware.smackx.mam.MamManager; 053import org.jivesoftware.smackx.muc.MultiUserChat; 054import org.jivesoftware.smackx.muc.MultiUserChatManager; 055import org.jivesoftware.smackx.muc.RoomInfo; 056import org.jivesoftware.smackx.omemo.element.OmemoBundleElement; 057import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement; 058import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement_VAxolotl; 059import org.jivesoftware.smackx.omemo.element.OmemoElement; 060import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException; 061import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; 062import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException; 063import org.jivesoftware.smackx.omemo.exceptions.NoOmemoSupportException; 064import org.jivesoftware.smackx.omemo.exceptions.NoRawSessionException; 065import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException; 066import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList; 067import org.jivesoftware.smackx.omemo.internal.OmemoDevice; 068import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener; 069import org.jivesoftware.smackx.omemo.listener.OmemoMucMessageListener; 070import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; 071import org.jivesoftware.smackx.omemo.trust.OmemoTrustCallback; 072import org.jivesoftware.smackx.omemo.trust.TrustState; 073import org.jivesoftware.smackx.omemo.util.MessageOrOmemoMessage; 074import org.jivesoftware.smackx.omemo.util.OmemoConstants; 075import org.jivesoftware.smackx.pep.PepEventListener; 076import org.jivesoftware.smackx.pep.PepManager; 077import org.jivesoftware.smackx.pubsub.PubSubException; 078import org.jivesoftware.smackx.pubsub.PubSubManager; 079import org.jivesoftware.smackx.pubsub.packet.PubSub; 080 081import org.jxmpp.jid.BareJid; 082import org.jxmpp.jid.DomainBareJid; 083import org.jxmpp.jid.EntityBareJid; 084import org.jxmpp.jid.EntityFullJid; 085 086/** 087 * Manager that allows sending messages encrypted with OMEMO. 088 * This class also provides some methods useful for a client that implements OMEMO. 089 * 090 * @author Paul Schaub 091 */ 092 093public final class OmemoManager extends Manager { 094 private static final Logger LOGGER = Logger.getLogger(OmemoManager.class.getName()); 095 096 private static final Integer UNKNOWN_DEVICE_ID = -1; 097 098 private static final WeakHashMap<XMPPConnection, TreeMap<Integer, OmemoManager>> INSTANCES = new WeakHashMap<>(); 099 private final OmemoService<?, ?, ?, ?, ?, ?, ?, ?, ?> service; 100 101 private final HashSet<OmemoMessageListener> omemoMessageListeners = new HashSet<>(); 102 private final HashSet<OmemoMucMessageListener> omemoMucMessageListeners = new HashSet<>(); 103 104 private final PepManager pepManager; 105 106 private OmemoTrustCallback trustCallback; 107 108 private BareJid ownJid; 109 private Integer deviceId; 110 111 /** 112 * Private constructor. 113 * 114 * @param connection connection 115 * @param deviceId deviceId 116 */ 117 private OmemoManager(XMPPConnection connection, Integer deviceId) { 118 super(connection); 119 120 service = OmemoService.getInstance(); 121 pepManager = PepManager.getInstanceFor(connection); 122 123 this.deviceId = deviceId; 124 125 if (connection.isAuthenticated()) { 126 initBareJidAndDeviceId(this); 127 } else { 128 connection.addConnectionListener(new ConnectionListener() { 129 @Override 130 public void authenticated(XMPPConnection connection, boolean resumed) { 131 initBareJidAndDeviceId(OmemoManager.this); 132 } 133 }); 134 } 135 136 service.registerRatchetForManager(this); 137 138 // StanzaListeners 139 resumeStanzaAndPEPListeners(); 140 } 141 142 /** 143 * Return an OmemoManager instance for the given connection and deviceId. 144 * If there was an OmemoManager for the connection and id before, return it. Otherwise create a new OmemoManager 145 * instance and return it. 146 * 147 * @param connection XmppConnection. 148 * @param deviceId MUST NOT be null and MUST be greater than 0. 149 * 150 * @return OmemoManager instance for the given connection and deviceId. 151 */ 152 public static synchronized OmemoManager getInstanceFor(XMPPConnection connection, Integer deviceId) { 153 if (deviceId == null || deviceId < 1) { 154 throw new IllegalArgumentException("DeviceId MUST NOT be null and MUST be greater than 0."); 155 } 156 157 TreeMap<Integer, OmemoManager> managersOfConnection = INSTANCES.get(connection); 158 if (managersOfConnection == null) { 159 managersOfConnection = new TreeMap<>(); 160 INSTANCES.put(connection, managersOfConnection); 161 } 162 163 OmemoManager manager = managersOfConnection.get(deviceId); 164 if (manager == null) { 165 manager = new OmemoManager(connection, deviceId); 166 managersOfConnection.put(deviceId, manager); 167 } 168 169 return manager; 170 } 171 172 /** 173 * Returns an OmemoManager instance for the given connection. If there was one manager for the connection before, 174 * return it. If there were multiple managers before, return the one with the lowest deviceId. 175 * If there was no manager before, return a new one. As soon as the connection gets authenticated, the manager 176 * will look for local deviceIDs and select the lowest one as its id. If there are not local deviceIds, the manager 177 * will assign itself a random id. 178 * 179 * @param connection XmppConnection. 180 * 181 * @return OmemoManager instance for the given connection and a determined deviceId. 182 */ 183 public static synchronized OmemoManager getInstanceFor(XMPPConnection connection) { 184 TreeMap<Integer, OmemoManager> managers = INSTANCES.get(connection); 185 if (managers == null) { 186 managers = new TreeMap<>(); 187 INSTANCES.put(connection, managers); 188 } 189 190 OmemoManager manager; 191 if (managers.size() == 0) { 192 193 manager = new OmemoManager(connection, UNKNOWN_DEVICE_ID); 194 managers.put(UNKNOWN_DEVICE_ID, manager); 195 196 } else { 197 manager = managers.get(managers.firstKey()); 198 } 199 200 return manager; 201 } 202 203 /** 204 * Set a TrustCallback for this particular OmemoManager. 205 * TrustCallbacks are used to query and modify trust decisions. 206 * 207 * @param callback trustCallback. 208 */ 209 public void setTrustCallback(OmemoTrustCallback callback) { 210 if (trustCallback != null) { 211 throw new IllegalStateException("TrustCallback can only be set once."); 212 } 213 trustCallback = callback; 214 } 215 216 /** 217 * Return the TrustCallback of this manager. 218 * 219 * @return callback that is used for trust decisions. 220 */ 221 OmemoTrustCallback getTrustCallback() { 222 return trustCallback; 223 } 224 225 /** 226 * Initializes the OmemoManager. This method must be called before the manager can be used. 227 * 228 * @throws CorruptedOmemoKeyException if the OMEMO key is corrupted. 229 * @throws InterruptedException if the calling thread was interrupted. 230 * @throws SmackException.NoResponseException if there was no response from the remote entity. 231 * @throws SmackException.NotConnectedException if the XMPP connection is not connected. 232 * @throws XMPPException.XMPPErrorException if there was an XMPP error returned. 233 * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. 234 * @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node. 235 * @throws IOException if an I/O error occurred. 236 */ 237 public synchronized void initialize() 238 throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, InterruptedException, 239 SmackException.NoResponseException, SmackException.NotConnectedException, XMPPException.XMPPErrorException, 240 PubSubException.NotALeafNodeException, IOException { 241 if (!connection().isAuthenticated()) { 242 throw new SmackException.NotLoggedInException(); 243 } 244 245 if (getTrustCallback() == null) { 246 throw new IllegalStateException("No TrustCallback set."); 247 } 248 249 getOmemoService().init(new LoggedInOmemoManager(this)); 250 } 251 252 /** 253 * Initialize the manager without blocking. Once the manager is successfully initialized, the finishedCallback will 254 * be notified. It will also get notified, if an error occurs. 255 * 256 * @param finishedCallback callback that gets called once the manager is initialized. 257 */ 258 public void initializeAsync(final InitializationFinishedCallback finishedCallback) { 259 Async.go(new Runnable() { 260 @Override 261 public void run() { 262 try { 263 initialize(); 264 finishedCallback.initializationFinished(OmemoManager.this); 265 } catch (Exception e) { 266 finishedCallback.initializationFailed(e); 267 } 268 } 269 }); 270 } 271 272 /** 273 * Return a set of all OMEMO capable devices of a contact. 274 * Note, that this method does not explicitly refresh the device list of the contact, so it might be outdated. 275 * 276 * @see #requestDeviceListUpdateFor(BareJid) 277 * 278 * @param contact contact we want to get a set of device of. 279 * @return set of known devices of that contact. 280 * 281 * @throws IOException if an I/O error occurred. 282 */ 283 public Set<OmemoDevice> getDevicesOf(BareJid contact) throws IOException { 284 OmemoCachedDeviceList list = getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(getOwnDevice(), contact); 285 HashSet<OmemoDevice> devices = new HashSet<>(); 286 287 for (int deviceId : list.getActiveDevices()) { 288 devices.add(new OmemoDevice(contact, deviceId)); 289 } 290 291 return devices; 292 } 293 294 /** 295 * OMEMO encrypt a cleartext message for a single recipient. 296 * Note that this method does NOT set the 'to' attribute of the message. 297 * 298 * @param recipient recipients bareJid 299 * @param message text to encrypt 300 * @return encrypted message 301 * 302 * @throws CryptoFailedException when something crypto related fails 303 * @throws UndecidedOmemoIdentityException When there are undecided devices 304 * @throws InterruptedException if the calling thread was interrupted. 305 * @throws SmackException.NotConnectedException if the XMPP connection is not connected. 306 * @throws SmackException.NoResponseException if there was no response from the remote entity. 307 * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. 308 * @throws IOException if an I/O error occurred. 309 */ 310 public OmemoMessage.Sent encrypt(BareJid recipient, String message) 311 throws CryptoFailedException, UndecidedOmemoIdentityException, 312 InterruptedException, SmackException.NotConnectedException, 313 SmackException.NoResponseException, SmackException.NotLoggedInException, IOException { 314 Set<BareJid> recipients = new HashSet<>(); 315 recipients.add(recipient); 316 return encrypt(recipients, message); 317 } 318 319 /** 320 * OMEMO encrypt a cleartext message for multiple recipients. 321 * 322 * @param recipients recipients barejids 323 * @param message text to encrypt 324 * @return encrypted message. 325 * 326 * @throws CryptoFailedException When something crypto related fails 327 * @throws UndecidedOmemoIdentityException When there are undecided devices. 328 * @throws InterruptedException if the calling thread was interrupted. 329 * @throws SmackException.NotConnectedException if the XMPP connection is not connected. 330 * @throws SmackException.NoResponseException if there was no response from the remote entity. 331 * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. 332 * @throws IOException if an I/O error occurred. 333 */ 334 public synchronized OmemoMessage.Sent encrypt(Set<BareJid> recipients, String message) 335 throws CryptoFailedException, UndecidedOmemoIdentityException, 336 InterruptedException, SmackException.NotConnectedException, 337 SmackException.NoResponseException, SmackException.NotLoggedInException, IOException { 338 LoggedInOmemoManager guard = new LoggedInOmemoManager(this); 339 Set<OmemoDevice> devices = getDevicesOf(getOwnJid()); 340 for (BareJid recipient : recipients) { 341 devices.addAll(getDevicesOf(recipient)); 342 } 343 return service.createOmemoMessage(guard, devices, message); 344 } 345 346 /** 347 * Encrypt a message for all recipients in the MultiUserChat. 348 * 349 * @param muc multiUserChat 350 * @param message message to send 351 * @return encrypted message 352 * 353 * @throws UndecidedOmemoIdentityException when there are undecided devices. 354 * @throws CryptoFailedException if the OMEMO cryptography failed. 355 * @throws XMPPException.XMPPErrorException if there was an XMPP error returned. 356 * @throws SmackException.NotConnectedException if the XMPP connection is not connected. 357 * @throws InterruptedException if the calling thread was interrupted. 358 * @throws SmackException.NoResponseException if there was no response from the remote entity. 359 * @throws NoOmemoSupportException When the muc doesn't support OMEMO. 360 * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. 361 * @throws IOException if an I/O error occurred. 362 */ 363 public synchronized OmemoMessage.Sent encrypt(MultiUserChat muc, String message) 364 throws UndecidedOmemoIdentityException, CryptoFailedException, 365 XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, 366 SmackException.NoResponseException, NoOmemoSupportException, 367 SmackException.NotLoggedInException, IOException { 368 if (!multiUserChatSupportsOmemo(muc)) { 369 throw new NoOmemoSupportException(); 370 } 371 372 Set<BareJid> recipients = new HashSet<>(); 373 374 for (EntityFullJid e : muc.getOccupants()) { 375 recipients.add(muc.getOccupant(e).getJid().asBareJid()); 376 } 377 return encrypt(recipients, message); 378 } 379 380 /** 381 * Manually decrypt an OmemoElement. 382 * This method should only be used for use-cases, where the internal listeners don't pick up on an incoming message. 383 * (for example MAM query results). 384 * 385 * @param sender bareJid of the message sender (must be the jid of the contact who sent the message) 386 * @param omemoElement omemoElement 387 * @return decrypted OmemoMessage 388 * 389 * @throws SmackException.NotLoggedInException if the Manager is not authenticated 390 * @throws CorruptedOmemoKeyException if our or their key is corrupted 391 * @throws NoRawSessionException if the message was not a preKeyMessage, but we had no session with the contact 392 * @throws CryptoFailedException if decryption fails 393 * @throws IOException if an I/O error occurred. 394 */ 395 public OmemoMessage.Received decrypt(BareJid sender, OmemoElement omemoElement) 396 throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, NoRawSessionException, 397 CryptoFailedException, IOException { 398 LoggedInOmemoManager managerGuard = new LoggedInOmemoManager(this); 399 return getOmemoService().decryptMessage(managerGuard, sender, omemoElement); 400 } 401 402 /** 403 * Decrypt messages from a MAM query. 404 * 405 * @param mamQuery The MAM query 406 * @return list of decrypted OmemoMessages 407 * 408 * @throws SmackException.NotLoggedInException if the Manager is not authenticated. 409 * @throws IOException if an I/O error occurred. 410 */ 411 public List<MessageOrOmemoMessage> decryptMamQueryResult(MamManager.MamQuery mamQuery) 412 throws SmackException.NotLoggedInException, IOException { 413 return new ArrayList<>(getOmemoService().decryptMamQueryResult(new LoggedInOmemoManager(this), mamQuery)); 414 } 415 416 /** 417 * Trust that a fingerprint belongs to an OmemoDevice. 418 * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must 419 * be of length 64. 420 * 421 * @param device device 422 * @param fingerprint fingerprint 423 */ 424 public void trustOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) { 425 if (trustCallback == null) { 426 throw new IllegalStateException("No TrustCallback set."); 427 } 428 429 trustCallback.setTrust(device, fingerprint, TrustState.trusted); 430 } 431 432 /** 433 * Distrust the fingerprint/OmemoDevice tuple. 434 * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must 435 * be of length 64. 436 * 437 * @param device device 438 * @param fingerprint fingerprint 439 */ 440 public void distrustOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) { 441 if (trustCallback == null) { 442 throw new IllegalStateException("No TrustCallback set."); 443 } 444 445 trustCallback.setTrust(device, fingerprint, TrustState.untrusted); 446 } 447 448 /** 449 * Returns true, if the fingerprint/OmemoDevice tuple is trusted, otherwise false. 450 * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must 451 * be of length 64. 452 * 453 * @param device device 454 * @param fingerprint fingerprint 455 * @return <code>true</code> if this is a trusted OMEMO identity. 456 */ 457 public boolean isTrustedOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) { 458 if (trustCallback == null) { 459 throw new IllegalStateException("No TrustCallback set."); 460 } 461 462 return trustCallback.getTrust(device, fingerprint) == TrustState.trusted; 463 } 464 465 /** 466 * Returns true, if the fingerprint/OmemoDevice tuple is decided by the user. 467 * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must 468 * be of length 64. 469 * 470 * @param device device 471 * @param fingerprint fingerprint 472 * @return <code>true</code> if the trust is decided for the identity. 473 */ 474 public boolean isDecidedOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) { 475 if (trustCallback == null) { 476 throw new IllegalStateException("No TrustCallback set."); 477 } 478 479 return trustCallback.getTrust(device, fingerprint) != TrustState.undecided; 480 } 481 482 /** 483 * Send a ratchet update message. This can be used to advance the ratchet of a session in order to maintain forward 484 * secrecy. 485 * 486 * @param recipient recipient 487 * 488 * @throws CorruptedOmemoKeyException When the used identityKeys are corrupted 489 * @throws CryptoFailedException When something fails with the crypto 490 * @throws CannotEstablishOmemoSessionException When we can't establish a session with the recipient 491 * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. 492 * @throws InterruptedException if the calling thread was interrupted. 493 * @throws SmackException.NoResponseException if there was no response from the remote entity. 494 * @throws NoSuchAlgorithmException if no such algorithm is available. 495 * @throws SmackException.NotConnectedException if the XMPP connection is not connected. 496 * @throws IOException if an I/O error occurred. 497 */ 498 public synchronized void sendRatchetUpdateMessage(OmemoDevice recipient) 499 throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, InterruptedException, 500 SmackException.NoResponseException, NoSuchAlgorithmException, SmackException.NotConnectedException, 501 CryptoFailedException, CannotEstablishOmemoSessionException, IOException { 502 XMPPConnection connection = connection(); 503 MessageBuilder message = connection.getStanzaFactory() 504 .buildMessageStanza() 505 .to(recipient.getJid()); 506 507 OmemoElement element = getOmemoService().createRatchetUpdateElement(new LoggedInOmemoManager(this), recipient); 508 message.addExtension(element); 509 510 // Set MAM Storage hint 511 StoreHint.set(message); 512 connection.sendStanza(message.build()); 513 } 514 515 /** 516 * Returns true, if the contact has any active devices published in a deviceList. 517 * 518 * @param contact contact 519 * @return true if contact has at least one OMEMO capable device. 520 * 521 * @throws SmackException.NotConnectedException if the XMPP connection is not connected. 522 * @throws InterruptedException if the calling thread was interrupted. 523 * @throws SmackException.NoResponseException if there was no response from the remote entity. 524 * @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node. 525 * @throws XMPPException.XMPPErrorException if there was an XMPP error returned. 526 * @throws IOException if an I/O error occurred. 527 */ 528 public synchronized boolean contactSupportsOmemo(BareJid contact) 529 throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, 530 SmackException.NotConnectedException, SmackException.NoResponseException, IOException { 531 OmemoCachedDeviceList deviceList = getOmemoService().refreshDeviceList(connection(), getOwnDevice(), contact); 532 return !deviceList.getActiveDevices().isEmpty(); 533 } 534 535 /** 536 * Returns true, if the MUC with the EntityBareJid multiUserChat is non-anonymous and members only (prerequisite 537 * for OMEMO encryption in MUC). 538 * 539 * @param multiUserChat MUC 540 * @return true if chat supports OMEMO 541 * 542 * @throws XMPPException.XMPPErrorException if there was an XMPP protocol level error 543 * @throws SmackException.NotConnectedException if the connection is not connected 544 * @throws InterruptedException if the thread is interrupted 545 * @throws SmackException.NoResponseException if the server does not respond 546 */ 547 public boolean multiUserChatSupportsOmemo(MultiUserChat multiUserChat) 548 throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, 549 SmackException.NoResponseException { 550 EntityBareJid jid = multiUserChat.getRoom(); 551 RoomInfo roomInfo = MultiUserChatManager.getInstanceFor(connection()).getRoomInfo(jid); 552 return roomInfo.isNonanonymous() && roomInfo.isMembersOnly(); 553 } 554 555 /** 556 * Returns true, if the Server supports PEP. 557 * 558 * @param connection XMPPConnection 559 * @param server domainBareJid of the server to test 560 * @return true if server supports pep 561 * 562 * @throws XMPPException.XMPPErrorException if there was an XMPP error returned. 563 * @throws SmackException.NotConnectedException if the XMPP connection is not connected. 564 * @throws InterruptedException if the calling thread was interrupted. 565 * @throws SmackException.NoResponseException if there was no response from the remote entity. 566 */ 567 public static boolean serverSupportsOmemo(XMPPConnection connection, DomainBareJid server) 568 throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, 569 SmackException.NoResponseException { 570 return ServiceDiscoveryManager.getInstanceFor(connection) 571 .discoverInfo(server).containsFeature(PubSub.NAMESPACE); 572 } 573 574 /** 575 * Return the fingerprint of our identity key. 576 * 577 * @return our own OMEMO fingerprint 578 * 579 * @throws SmackException.NotLoggedInException if we don't know our bareJid yet. 580 * @throws CorruptedOmemoKeyException if our identityKey is corrupted. 581 * @throws IOException if an I/O error occurred. 582 */ 583 public synchronized OmemoFingerprint getOwnFingerprint() 584 throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, IOException { 585 if (getOwnJid() == null) { 586 throw new SmackException.NotLoggedInException(); 587 } 588 589 return getOmemoService().getOmemoStoreBackend().getFingerprint(getOwnDevice()); 590 } 591 592 /** 593 * Get the fingerprint of a contacts device. 594 * 595 * @param device contacts OmemoDevice 596 * @return fingerprint of the given OMEMO device. 597 * 598 * @throws CannotEstablishOmemoSessionException if we have no session yet, and are unable to create one. 599 * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. 600 * @throws CorruptedOmemoKeyException if the copy of the fingerprint we have is corrupted. 601 * @throws SmackException.NotConnectedException if the XMPP connection is not connected. 602 * @throws InterruptedException if the calling thread was interrupted. 603 * @throws SmackException.NoResponseException if there was no response from the remote entity. 604 * @throws IOException if an I/O error occurred. 605 */ 606 public synchronized OmemoFingerprint getFingerprint(OmemoDevice device) 607 throws CannotEstablishOmemoSessionException, SmackException.NotLoggedInException, 608 CorruptedOmemoKeyException, SmackException.NotConnectedException, InterruptedException, 609 SmackException.NoResponseException, IOException { 610 if (getOwnJid() == null) { 611 throw new SmackException.NotLoggedInException(); 612 } 613 614 if (device.equals(getOwnDevice())) { 615 return getOwnFingerprint(); 616 } 617 618 return getOmemoService().getOmemoStoreBackend() 619 .getFingerprintAndMaybeBuildSession(new LoggedInOmemoManager(this), device); 620 } 621 622 /** 623 * Return all OmemoFingerprints of active devices of a contact. 624 * TODO: Make more fail-safe 625 * 626 * @param contact contact 627 * @return Map of all active devices of the contact and their fingerprints. 628 * 629 * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. 630 * @throws CorruptedOmemoKeyException if the OMEMO key is corrupted. 631 * @throws CannotEstablishOmemoSessionException if no OMEMO session could be established. 632 * @throws SmackException.NotConnectedException if the XMPP connection is not connected. 633 * @throws InterruptedException if the calling thread was interrupted. 634 * @throws SmackException.NoResponseException if there was no response from the remote entity. 635 * @throws IOException if an I/O error occurred. 636 */ 637 public synchronized Map<OmemoDevice, OmemoFingerprint> getActiveFingerprints(BareJid contact) 638 throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, 639 CannotEstablishOmemoSessionException, SmackException.NotConnectedException, InterruptedException, 640 SmackException.NoResponseException, IOException { 641 if (getOwnJid() == null) { 642 throw new SmackException.NotLoggedInException(); 643 } 644 645 Map<OmemoDevice, OmemoFingerprint> fingerprints = new HashMap<>(); 646 OmemoCachedDeviceList deviceList = getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(getOwnDevice(), 647 contact); 648 649 for (int id : deviceList.getActiveDevices()) { 650 OmemoDevice device = new OmemoDevice(contact, id); 651 OmemoFingerprint fingerprint = getFingerprint(device); 652 653 if (fingerprint != null) { 654 fingerprints.put(device, fingerprint); 655 } 656 } 657 658 return fingerprints; 659 } 660 661 /** 662 * Add an OmemoMessageListener. This listener will be informed about incoming OMEMO messages 663 * (as well as KeyTransportMessages) and OMEMO encrypted message carbons. 664 * 665 * @param listener OmemoMessageListener 666 */ 667 public void addOmemoMessageListener(OmemoMessageListener listener) { 668 omemoMessageListeners.add(listener); 669 } 670 671 /** 672 * Remove an OmemoMessageListener. 673 * 674 * @param listener OmemoMessageListener 675 */ 676 public void removeOmemoMessageListener(OmemoMessageListener listener) { 677 omemoMessageListeners.remove(listener); 678 } 679 680 /** 681 * Add an OmemoMucMessageListener. This listener will be informed about incoming OMEMO encrypted MUC messages. 682 * 683 * @param listener OmemoMessageListener. 684 */ 685 public void addOmemoMucMessageListener(OmemoMucMessageListener listener) { 686 omemoMucMessageListeners.add(listener); 687 } 688 689 /** 690 * Remove an OmemoMucMessageListener. 691 * 692 * @param listener OmemoMucMessageListener 693 */ 694 public void removeOmemoMucMessageListener(OmemoMucMessageListener listener) { 695 omemoMucMessageListeners.remove(listener); 696 } 697 698 /** 699 * Request a deviceList update from contact contact. 700 * 701 * @param contact contact we want to obtain the deviceList from. 702 * 703 * @throws InterruptedException if the calling thread was interrupted. 704 * @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node. 705 * @throws XMPPException.XMPPErrorException if there was an XMPP error returned. 706 * @throws SmackException.NotConnectedException if the XMPP connection is not connected. 707 * @throws SmackException.NoResponseException if there was no response from the remote entity. 708 * @throws IOException if an I/O error occurred. 709 */ 710 public synchronized void requestDeviceListUpdateFor(BareJid contact) 711 throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, 712 SmackException.NotConnectedException, SmackException.NoResponseException, IOException { 713 getOmemoService().refreshDeviceList(connection(), getOwnDevice(), contact); 714 } 715 716 /** 717 * Publish a new device list with just our own deviceId in it. 718 * 719 * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. 720 * @throws InterruptedException if the calling thread was interrupted. 721 * @throws XMPPException.XMPPErrorException if there was an XMPP error returned. 722 * @throws SmackException.NotConnectedException if the XMPP connection is not connected. 723 * @throws SmackException.NoResponseException if there was no response from the remote entity. 724 * @throws IOException if an I/O error occurred. 725 * @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node. 726 */ 727 public void purgeDeviceList() 728 throws SmackException.NotLoggedInException, InterruptedException, XMPPException.XMPPErrorException, 729 SmackException.NotConnectedException, SmackException.NoResponseException, IOException, PubSubException.NotALeafNodeException { 730 getOmemoService().purgeDeviceList(new LoggedInOmemoManager(this)); 731 } 732 733 public List<Exception> purgeEverything() throws NotConnectedException, InterruptedException, IOException { 734 List<Exception> exceptions = new ArrayList<>(5); 735 PubSubManager pm = PubSubManager.getInstanceFor(getConnection(), getOwnJid()); 736 try { 737 requestDeviceListUpdateFor(getOwnJid()); 738 } catch (SmackException.NoResponseException | PubSubException.NotALeafNodeException 739 | XMPPException.XMPPErrorException e) { 740 exceptions.add(e); 741 } 742 743 OmemoCachedDeviceList deviceList = OmemoService.getInstance().getOmemoStoreBackend() 744 .loadCachedDeviceList(getOwnDevice(), getOwnJid()); 745 746 for (int id : deviceList.getAllDevices()) { 747 try { 748 pm.getLeafNode(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(id)).deleteAllItems(); 749 } catch (SmackException.NoResponseException | PubSubException.NotALeafNodeException 750 | XMPPException.XMPPErrorException | PubSubException.NotAPubSubNodeException e) { 751 exceptions.add(e); 752 } 753 754 try { 755 pm.deleteNode(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(id)); 756 } catch (SmackException.NoResponseException | XMPPException.XMPPErrorException e) { 757 exceptions.add(e); 758 } 759 } 760 761 try { 762 pm.getLeafNode(OmemoConstants.PEP_NODE_DEVICE_LIST).deleteAllItems(); 763 } catch (SmackException.NoResponseException | PubSubException.NotALeafNodeException 764 | XMPPException.XMPPErrorException | PubSubException.NotAPubSubNodeException e) { 765 exceptions.add(e); 766 } 767 768 try { 769 pm.deleteNode(OmemoConstants.PEP_NODE_DEVICE_LIST); 770 } catch (SmackException.NoResponseException | XMPPException.XMPPErrorException e) { 771 exceptions.add(e); 772 } 773 774 return exceptions; 775 } 776 777 /** 778 * Rotate the signedPreKey published in our OmemoBundle and republish it. This should be done every now and 779 * then (7-14 days). The old signedPreKey should be kept for some more time (a month or so) to enable decryption 780 * of messages that have been sent since the key was changed. 781 * 782 * @throws CorruptedOmemoKeyException When the IdentityKeyPair is damaged. 783 * @throws InterruptedException XMPP error 784 * @throws XMPPException.XMPPErrorException XMPP error 785 * @throws SmackException.NotConnectedException XMPP error 786 * @throws SmackException.NoResponseException XMPP error 787 * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. 788 * @throws IOException if an I/O error occurred. 789 * @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node. 790 */ 791 public synchronized void rotateSignedPreKey() 792 throws CorruptedOmemoKeyException, SmackException.NotLoggedInException, XMPPException.XMPPErrorException, 793 SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, 794 IOException, PubSubException.NotALeafNodeException { 795 if (!connection().isAuthenticated()) { 796 throw new SmackException.NotLoggedInException(); 797 } 798 799 // generate key 800 getOmemoService().getOmemoStoreBackend().changeSignedPreKey(getOwnDevice()); 801 802 // publish 803 OmemoBundleElement bundle = getOmemoService().getOmemoStoreBackend().packOmemoBundle(getOwnDevice()); 804 OmemoService.publishBundle(connection(), getOwnDevice(), bundle); 805 } 806 807 /** 808 * Return true, if the given Stanza contains an OMEMO element 'encrypted'. 809 * 810 * @param stanza stanza 811 * @return true if stanza has extension 'encrypted' 812 */ 813 static boolean stanzaContainsOmemoElement(Stanza stanza) { 814 return stanza.hasExtension(OmemoElement.NAME_ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); 815 } 816 817 /** 818 * Throw an IllegalStateException if no OmemoService is set. 819 */ 820 private void throwIfNoServiceSet() { 821 if (service == null) { 822 throw new IllegalStateException("No OmemoService set in OmemoManager."); 823 } 824 } 825 826 /** 827 * Returns a pseudo random number from the interval [1, Integer.MAX_VALUE]. 828 * 829 * @return a random deviceId. 830 */ 831 public static int randomDeviceId() { 832 return new Random().nextInt(Integer.MAX_VALUE - 1) + 1; 833 } 834 835 /** 836 * Return the BareJid of the user. 837 * 838 * @return our own bare JID. 839 */ 840 public BareJid getOwnJid() { 841 if (ownJid == null && connection().isAuthenticated()) { 842 ownJid = connection().getUser().asBareJid(); 843 } 844 845 return ownJid; 846 } 847 848 /** 849 * Return the deviceId of this OmemoManager. 850 * 851 * @return this OmemoManagers deviceId. 852 */ 853 public synchronized Integer getDeviceId() { 854 return deviceId; 855 } 856 857 /** 858 * Return the OmemoDevice of the user. 859 * 860 * @return our own OmemoDevice 861 */ 862 public synchronized OmemoDevice getOwnDevice() { 863 BareJid jid = getOwnJid(); 864 if (jid == null) { 865 return null; 866 } 867 return new OmemoDevice(jid, getDeviceId()); 868 } 869 870 /** 871 * Set the deviceId of the manager to nDeviceId. 872 * 873 * @param nDeviceId new deviceId 874 */ 875 synchronized void setDeviceId(int nDeviceId) { 876 // Move this instance inside the HashMaps 877 INSTANCES.get(connection()).remove(getDeviceId()); 878 INSTANCES.get(connection()).put(nDeviceId, this); 879 880 this.deviceId = nDeviceId; 881 } 882 883 /** 884 * Notify all registered OmemoMessageListeners about a received OmemoMessage. 885 * 886 * @param stanza original stanza 887 * @param decryptedMessage decrypted OmemoMessage. 888 */ 889 void notifyOmemoMessageReceived(Stanza stanza, OmemoMessage.Received decryptedMessage) { 890 for (OmemoMessageListener l : omemoMessageListeners) { 891 l.onOmemoMessageReceived(stanza, decryptedMessage); 892 } 893 } 894 895 /** 896 * Notify all registered OmemoMucMessageListeners of an incoming OmemoMessageElement in a MUC. 897 * 898 * @param muc MultiUserChat the message was received in. 899 * @param stanza Original Stanza. 900 * @param decryptedMessage Decrypted OmemoMessage. 901 */ 902 void notifyOmemoMucMessageReceived(MultiUserChat muc, 903 Stanza stanza, 904 OmemoMessage.Received decryptedMessage) { 905 for (OmemoMucMessageListener l : omemoMucMessageListeners) { 906 l.onOmemoMucMessageReceived(muc, stanza, decryptedMessage); 907 } 908 } 909 910 /** 911 * Notify all registered OmemoMessageListeners of an incoming OMEMO encrypted Carbon Copy. 912 * Remember: If you want to receive OMEMO encrypted carbon copies, you have to enable carbons using 913 * {@link CarbonManager#enableCarbons()}. 914 * 915 * @param direction direction of the carbon copy 916 * @param carbonCopy carbon copy itself 917 * @param wrappingMessage wrapping message 918 * @param decryptedCarbonCopy decrypted carbon copy OMEMO element 919 */ 920 void notifyOmemoCarbonCopyReceived(CarbonExtension.Direction direction, 921 Message carbonCopy, 922 Message wrappingMessage, 923 OmemoMessage.Received decryptedCarbonCopy) { 924 for (OmemoMessageListener l : omemoMessageListeners) { 925 l.onOmemoCarbonCopyReceived(direction, carbonCopy, wrappingMessage, decryptedCarbonCopy); 926 } 927 } 928 929 /** 930 * Register stanza listeners needed for OMEMO. 931 * This method is called automatically in the constructor and should only be used to restore the previous state 932 * after {@link #stopStanzaAndPEPListeners()} was called. 933 */ 934 public void resumeStanzaAndPEPListeners() { 935 CarbonManager carbonManager = CarbonManager.getInstanceFor(connection()); 936 937 // Remove listeners to avoid them getting added twice 938 connection().removeAsyncStanzaListener(this::internalOmemoMessageStanzaListener); 939 carbonManager.removeCarbonCopyReceivedListener(this::internalOmemoCarbonCopyListener); 940 941 // Add listeners 942 pepManager.addPepEventListener(OmemoConstants.PEP_NODE_DEVICE_LIST, OmemoDeviceListElement.class, pepOmemoDeviceListEventListener); 943 connection().addAsyncStanzaListener(this::internalOmemoMessageStanzaListener, OmemoManager::isOmemoMessage); 944 carbonManager.addCarbonCopyReceivedListener(this::internalOmemoCarbonCopyListener); 945 } 946 947 /** 948 * Remove active stanza listeners needed for OMEMO. 949 */ 950 public void stopStanzaAndPEPListeners() { 951 pepManager.removePepEventListener(pepOmemoDeviceListEventListener); 952 connection().removeAsyncStanzaListener(this::internalOmemoMessageStanzaListener); 953 CarbonManager.getInstanceFor(connection()).removeCarbonCopyReceivedListener(this::internalOmemoCarbonCopyListener); 954 } 955 956 /** 957 * Build a fresh session with a contacts device. 958 * This might come in handy if a session is broken. 959 * 960 * @param contactsDevice OmemoDevice of a contact. 961 * 962 * @throws InterruptedException if the calling thread was interrupted. 963 * @throws SmackException.NoResponseException if there was no response from the remote entity. 964 * @throws CorruptedOmemoKeyException if our or their identityKey is corrupted. 965 * @throws SmackException.NotConnectedException if the XMPP connection is not connected. 966 * @throws CannotEstablishOmemoSessionException if no new session can be established. 967 * @throws SmackException.NotLoggedInException if the connection is not authenticated. 968 */ 969 public void rebuildSessionWith(OmemoDevice contactsDevice) 970 throws InterruptedException, SmackException.NoResponseException, CorruptedOmemoKeyException, 971 SmackException.NotConnectedException, CannotEstablishOmemoSessionException, 972 SmackException.NotLoggedInException { 973 if (!connection().isAuthenticated()) { 974 throw new SmackException.NotLoggedInException(); 975 } 976 getOmemoService().buildFreshSessionWithDevice(connection(), getOwnDevice(), contactsDevice); 977 } 978 979 /** 980 * Get our connection. 981 * 982 * @return the connection of this manager 983 */ 984 XMPPConnection getConnection() { 985 return connection(); 986 } 987 988 /** 989 * Return the OMEMO service object. 990 * 991 * @return the OmemoService object related to this OmemoManager. 992 */ 993 OmemoService<?, ?, ?, ?, ?, ?, ?, ?, ?> getOmemoService() { 994 throwIfNoServiceSet(); 995 return service; 996 } 997 998 /** 999 * StanzaListener that listens for incoming Stanzas which contain OMEMO elements. 1000 */ 1001 private void internalOmemoMessageStanzaListener(final Stanza packet) { 1002 Async.go(new Runnable() { 1003 @Override 1004 public void run() { 1005 try { 1006 getOmemoService().onOmemoMessageStanzaReceived(packet, 1007 new LoggedInOmemoManager(OmemoManager.this)); 1008 } catch (SmackException.NotLoggedInException | IOException e) { 1009 LOGGER.log(Level.SEVERE, "Exception while processing OMEMO stanza", e); 1010 } 1011 } 1012 }); 1013 } 1014 1015 /** 1016 * CarbonCopyListener that listens for incoming carbon copies which contain OMEMO elements. 1017 */ 1018 private void internalOmemoCarbonCopyListener(final CarbonExtension.Direction direction, 1019 final Message carbonCopy, 1020 final Message wrappingMessage) { 1021 Async.go(new Runnable() { 1022 @Override 1023 public void run() { 1024 if (isOmemoMessage(carbonCopy)) { 1025 try { 1026 getOmemoService().onOmemoCarbonCopyReceived(direction, carbonCopy, wrappingMessage, 1027 new LoggedInOmemoManager(OmemoManager.this)); 1028 } catch (SmackException.NotLoggedInException | IOException e) { 1029 LOGGER.log(Level.SEVERE, "Exception while processing OMEMO stanza", e); 1030 } 1031 } 1032 } 1033 }); 1034 } 1035 1036 @SuppressWarnings("UnnecessaryLambda") 1037 private final PepEventListener<OmemoDeviceListElement> pepOmemoDeviceListEventListener = 1038 (from, receivedDeviceList, id, message) -> { 1039 // Device List <list> 1040 OmemoCachedDeviceList deviceList; 1041 try { 1042 getOmemoService().getOmemoStoreBackend().mergeCachedDeviceList(getOwnDevice(), from, 1043 receivedDeviceList); 1044 1045 if (!from.asBareJid().equals(getOwnJid())) { 1046 return; 1047 } 1048 1049 deviceList = getOmemoService().cleanUpDeviceList(getOwnDevice()); 1050 } catch (IOException e) { 1051 LOGGER.log(Level.SEVERE, 1052 "IOException while processing OMEMO PEP device updates. Message: " + message, 1053 e); 1054 return; 1055 } 1056 final OmemoDeviceListElement_VAxolotl newDeviceList = new OmemoDeviceListElement_VAxolotl(deviceList); 1057 1058 if (!newDeviceList.copyDeviceIds().equals(receivedDeviceList.copyDeviceIds())) { 1059 LOGGER.log(Level.FINE, "Republish deviceList due to changes:" + 1060 " Received: " + Arrays.toString(receivedDeviceList.copyDeviceIds().toArray()) + 1061 " Published: " + Arrays.toString(newDeviceList.copyDeviceIds().toArray())); 1062 Async.go(new Runnable() { 1063 @Override 1064 public void run() { 1065 try { 1066 OmemoService.publishDeviceList(connection(), newDeviceList); 1067 } catch (InterruptedException | XMPPException.XMPPErrorException | 1068 SmackException.NotConnectedException | SmackException.NoResponseException | PubSubException.NotALeafNodeException e) { 1069 LOGGER.log(Level.WARNING, "Could not publish our deviceList upon an received update.", e); 1070 } 1071 } 1072 }); 1073 } 1074 }; 1075 1076 /** 1077 * StanzaFilter that filters messages containing a OMEMO element. 1078 */ 1079 private static boolean isOmemoMessage(Stanza stanza) { 1080 return stanza instanceof Message && OmemoManager.stanzaContainsOmemoElement(stanza); 1081 } 1082 1083 /** 1084 * Guard class which ensures that the wrapped OmemoManager knows its BareJid. 1085 */ 1086 public static class LoggedInOmemoManager { 1087 1088 private final OmemoManager manager; 1089 1090 public LoggedInOmemoManager(OmemoManager manager) 1091 throws SmackException.NotLoggedInException { 1092 1093 if (manager == null) { 1094 throw new IllegalArgumentException("OmemoManager cannot be null."); 1095 } 1096 1097 if (manager.getOwnJid() == null) { 1098 if (manager.getConnection().isAuthenticated()) { 1099 manager.ownJid = manager.getConnection().getUser().asBareJid(); 1100 } else { 1101 throw new SmackException.NotLoggedInException(); 1102 } 1103 } 1104 1105 this.manager = manager; 1106 } 1107 1108 public OmemoManager get() { 1109 return manager; 1110 } 1111 } 1112 1113 /** 1114 * Callback which can be used to get notified, when the OmemoManager finished initializing. 1115 */ 1116 public interface InitializationFinishedCallback { 1117 1118 void initializationFinished(OmemoManager manager); 1119 1120 void initializationFailed(Exception cause); 1121 } 1122 1123 /** 1124 * Get the bareJid of the user from the authenticated XMPP connection. 1125 * If our deviceId is unknown, use the bareJid to look up deviceIds available in the omemoStore. 1126 * If there are ids available, choose the smallest one. Otherwise generate a random deviceId. 1127 * 1128 * @param manager OmemoManager 1129 */ 1130 private static void initBareJidAndDeviceId(OmemoManager manager) { 1131 if (!manager.getConnection().isAuthenticated()) { 1132 throw new IllegalStateException("Connection MUST be authenticated."); 1133 } 1134 1135 if (manager.ownJid == null) { 1136 manager.ownJid = manager.getConnection().getUser().asBareJid(); 1137 } 1138 1139 if (UNKNOWN_DEVICE_ID.equals(manager.deviceId)) { 1140 SortedSet<Integer> storedDeviceIds = manager.getOmemoService().getOmemoStoreBackend().localDeviceIdsOf(manager.ownJid); 1141 if (storedDeviceIds.size() > 0) { 1142 manager.setDeviceId(storedDeviceIds.first()); 1143 } else { 1144 manager.setDeviceId(randomDeviceId()); 1145 } 1146 } 1147 } 1148}