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