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