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