OmemoService.java
/**
*
* Copyright 2017 Paul Schaub, 2019 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYLENGTH;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.StanzaError;
import org.jivesoftware.smackx.carbons.packet.CarbonExtension;
import org.jivesoftware.smackx.mam.MamManager;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.jivesoftware.smackx.muc.MultiUserChatManager;
import org.jivesoftware.smackx.muc.Occupant;
import org.jivesoftware.smackx.omemo.element.OmemoBundleElement;
import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement;
import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement_VAxolotl;
import org.jivesoftware.smackx.omemo.element.OmemoElement;
import org.jivesoftware.smackx.omemo.element.OmemoElement_VAxolotl;
import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
import org.jivesoftware.smackx.omemo.exceptions.NoIdentityKeyException;
import org.jivesoftware.smackx.omemo.exceptions.NoRawSessionException;
import org.jivesoftware.smackx.omemo.exceptions.ReadOnlyDeviceException;
import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException;
import org.jivesoftware.smackx.omemo.exceptions.UntrustedOmemoIdentityException;
import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag;
import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.internal.listener.OmemoCarbonCopyStanzaReceivedListener;
import org.jivesoftware.smackx.omemo.internal.listener.OmemoMessageStanzaReceivedListener;
import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
import org.jivesoftware.smackx.omemo.trust.OmemoTrustCallback;
import org.jivesoftware.smackx.omemo.trust.TrustState;
import org.jivesoftware.smackx.omemo.util.MessageOrOmemoMessage;
import org.jivesoftware.smackx.omemo.util.OmemoConstants;
import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder;
import org.jivesoftware.smackx.pep.PepManager;
import org.jivesoftware.smackx.pubsub.LeafNode;
import org.jivesoftware.smackx.pubsub.PayloadItem;
import org.jivesoftware.smackx.pubsub.PubSubException;
import org.jivesoftware.smackx.pubsub.PubSubException.NotALeafNodeException;
import org.jivesoftware.smackx.pubsub.PubSubManager;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.Jid;
/**
* This class contains OMEMO related logic and registers listeners etc.
*
* @param <T_IdKeyPair> IdentityKeyPair class
* @param <T_IdKey> IdentityKey class
* @param <T_PreKey> PreKey class
* @param <T_SigPreKey> SignedPreKey class
* @param <T_Sess> Session class
* @param <T_Addr> Address class
* @param <T_ECPub> Elliptic Curve PublicKey class
* @param <T_Bundle> Bundle class
* @param <T_Ciph> Cipher class
*
* @author Paul Schaub
*/
public abstract class OmemoService<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>
implements OmemoCarbonCopyStanzaReceivedListener, OmemoMessageStanzaReceivedListener {
protected static final Logger LOGGER = Logger.getLogger(OmemoService.class.getName());
private static final long MILLIS_PER_HOUR = 1000L * 60 * 60;
private static OmemoService<?, ?, ?, ?, ?, ?, ?, ?, ?> INSTANCE;
private OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore;
private final HashMap<OmemoManager, OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>> omemoRatchets = new HashMap<>();
protected OmemoService() {
}
/**
* Return the singleton instance of this class. When no instance is set, throw an IllegalStateException instead.
*
* @return instance.
*/
public static OmemoService<?, ?, ?, ?, ?, ?, ?, ?, ?> getInstance() {
if (INSTANCE == null) {
throw new IllegalStateException("No OmemoService registered");
}
return INSTANCE;
}
/**
* Set singleton instance. Throws an IllegalStateException, if there is already a service set as instance.
*
* @param omemoService instance
*/
protected static void setInstance(OmemoService<?, ?, ?, ?, ?, ?, ?, ?, ?> omemoService) {
if (INSTANCE != null) {
throw new IllegalStateException("An OmemoService is already registered");
}
INSTANCE = omemoService;
}
/**
* Returns true, if an instance of the service singleton is set. Otherwise return false.
*
* @return true, if instance is not null.
*/
public static boolean isServiceRegistered() {
return INSTANCE != null;
}
/**
* Return the used omemoStore backend.
* If there is no store backend set yet, set the default one (typically a file-based one).
*
* @return omemoStore backend
*/
public OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>
getOmemoStoreBackend() {
if (omemoStore == null) {
omemoStore = createDefaultOmemoStoreBackend();
}
return omemoStore;
}
/**
* Set an omemoStore as backend. Throws an IllegalStateException, if there is already a backend set.
*
* @param omemoStore store.
*/
@SuppressWarnings("unused")
public void setOmemoStoreBackend(
OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore) {
if (this.omemoStore != null) {
throw new IllegalStateException("An OmemoStore backend has already been set.");
}
this.omemoStore = omemoStore;
}
/**
* Create a default OmemoStore object.
*
* @return default omemoStore.
*/
protected abstract OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>
createDefaultOmemoStoreBackend();
/**
* Return a new instance of the OMEMO ratchet.
* The ratchet is internally used to encrypt/decrypt message keys.
*
* @param manager OmemoManager
* @param store OmemoStore
* @return instance of the OmemoRatchet
*/
protected abstract OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>
instantiateOmemoRatchet(OmemoManager manager,
OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> store);
/**
* Return the deposited instance of the OmemoRatchet for the given manager.
* If there is none yet, create a new one, deposit it and return it.
*
* @param manager OmemoManager we want to have the ratchet for.
* @return OmemoRatchet instance
*/
protected OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>
getOmemoRatchet(OmemoManager manager) {
OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>
omemoRatchet = omemoRatchets.get(manager);
if (omemoRatchet == null) {
omemoRatchet = instantiateOmemoRatchet(manager, omemoStore);
omemoRatchets.put(manager, omemoRatchet);
}
return omemoRatchet;
}
/**
* Instantiate and deposit a Ratchet for the given OmemoManager.
*
* @param manager manager.
*/
void registerRatchetForManager(OmemoManager manager) {
omemoRatchets.put(manager, instantiateOmemoRatchet(manager, getOmemoStoreBackend()));
}
/**
* Initialize OMEMO functionality for the given {@link OmemoManager}.
*
* @param managerGuard OmemoManager we'd like to initialize.
*
* @throws InterruptedException if the calling thread was interrupted.
* @throws CorruptedOmemoKeyException if the OMEMO key is corrupted.
* @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
* @throws SmackException.NotConnectedException if the XMPP connection is not connected.
* @throws SmackException.NoResponseException if there was no response from the remote entity.
* @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node.
* @throws IOException if an I/O error occurred.
*/
void init(OmemoManager.LoggedInOmemoManager managerGuard)
throws InterruptedException, CorruptedOmemoKeyException, XMPPException.XMPPErrorException,
SmackException.NotConnectedException, SmackException.NoResponseException,
PubSubException.NotALeafNodeException, IOException {
OmemoManager manager = managerGuard.get();
OmemoDevice userDevice = manager.getOwnDevice();
// Create new keys if necessary and publish to the server.
getOmemoStoreBackend().replenishKeys(userDevice);
// Rotate signed preKey if necessary.
if (shouldRotateSignedPreKey(userDevice)) {
getOmemoStoreBackend().changeSignedPreKey(userDevice);
}
// Pack and publish bundle
OmemoBundleElement bundle = getOmemoStoreBackend().packOmemoBundle(userDevice);
publishBundle(manager.getConnection(), userDevice, bundle);
// Fetch device list and republish deviceId if necessary
refreshAndRepublishDeviceList(manager.getConnection(), userDevice);
}
/**
* Create an empty OMEMO message, which is used to forward the ratchet of the recipient.
* This message type is typically used to create stable sessions.
* Note that trust decisions are ignored for the creation of this message.
*
* @param managerGuard Logged in OmemoManager
* @param contactsDevice OmemoDevice of the contact
* @return ratchet update message
*
* @throws NoSuchAlgorithmException if AES algorithms are not supported on this system.
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException.NoResponseException if there was no response from the remote entity.
* @throws CorruptedOmemoKeyException if our IdentityKeyPair is corrupted.
* @throws SmackException.NotConnectedException if the XMPP connection is not connected.
* @throws CannotEstablishOmemoSessionException if session negotiation fails.
* @throws IOException if an I/O error occurred.
*/
OmemoElement createRatchetUpdateElement(OmemoManager.LoggedInOmemoManager managerGuard,
OmemoDevice contactsDevice)
throws InterruptedException, SmackException.NoResponseException, CorruptedOmemoKeyException,
SmackException.NotConnectedException, CannotEstablishOmemoSessionException, NoSuchAlgorithmException,
CryptoFailedException, IOException {
OmemoManager manager = managerGuard.get();
OmemoDevice userDevice = manager.getOwnDevice();
if (contactsDevice.equals(userDevice)) {
throw new IllegalArgumentException("\"Thou shall not update thy own ratchet!\" - William Shakespeare");
}
// Establish session if necessary
if (!hasSession(userDevice, contactsDevice)) {
buildFreshSessionWithDevice(manager.getConnection(), userDevice, contactsDevice);
}
// Generate fresh AES key and IV
byte[] messageKey = OmemoMessageBuilder.generateKey(KEYTYPE, KEYLENGTH);
byte[] iv = OmemoMessageBuilder.generateIv();
// Create message builder
OmemoMessageBuilder<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> builder;
try {
builder = new OmemoMessageBuilder<>(userDevice, gullibleTrustCallback, getOmemoRatchet(manager),
messageKey, iv, null);
} catch (InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException e) {
throw new CryptoFailedException(e);
}
// Add recipient
try {
builder.addRecipient(contactsDevice);
} catch (UndecidedOmemoIdentityException | UntrustedOmemoIdentityException e) {
throw new AssertionError("Gullible Trust Callback reported undecided or untrusted device, " +
"even though it MUST NOT do that.");
} catch (NoIdentityKeyException e) {
throw new AssertionError("We MUST have an identityKey for " + contactsDevice + " since we built a session." + e);
}
// Note: We don't need to update our message counter for a ratchet update message.
return builder.finish();
}
/**
* Encrypt a message with a messageKey and an IV and create an OmemoMessage from it.
*
* @param managerGuard authenticated OmemoManager
* @param contactsDevices set of recipient OmemoDevices
* @param messageKey AES key to encrypt the message
* @param iv iv to be used with the messageKey
* @return OmemoMessage object which contains the OmemoElement and some information.
*
* @throws SmackException.NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException.NoResponseException if there was no response from the remote entity.
* @throws UndecidedOmemoIdentityException if the list of recipient devices contains undecided devices
* @throws CryptoFailedException if we are lacking some crypto primitives
* @throws IOException if an I/O error occurred.
*/
private OmemoMessage.Sent encrypt(OmemoManager.LoggedInOmemoManager managerGuard,
Set<OmemoDevice> contactsDevices,
byte[] messageKey,
byte[] iv,
String message)
throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException,
UndecidedOmemoIdentityException, CryptoFailedException, IOException {
OmemoManager manager = managerGuard.get();
OmemoDevice userDevice = manager.getOwnDevice();
// Do not encrypt for our own device.
removeOurDevice(userDevice, contactsDevices);
buildMissingSessionsWithDevices(manager.getConnection(), userDevice, contactsDevices);
Set<OmemoDevice> undecidedDevices = getUndecidedDevices(userDevice, manager.getTrustCallback(), contactsDevices);
if (!undecidedDevices.isEmpty()) {
throw new UndecidedOmemoIdentityException(undecidedDevices);
}
// Keep track of skipped devices
HashMap<OmemoDevice, Throwable> skippedRecipients = new HashMap<>();
OmemoMessageBuilder<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> builder;
try {
builder = new OmemoMessageBuilder<>(
userDevice, manager.getTrustCallback(), getOmemoRatchet(managerGuard.get()), messageKey, iv, message);
} catch (BadPaddingException | IllegalBlockSizeException |
NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) {
throw new CryptoFailedException(e);
}
for (OmemoDevice contactsDevice : contactsDevices) {
// Build missing sessions
if (!hasSession(userDevice, contactsDevice)) {
try {
buildFreshSessionWithDevice(manager.getConnection(), userDevice, contactsDevice);
} catch (CorruptedOmemoKeyException | CannotEstablishOmemoSessionException e) {
LOGGER.log(Level.WARNING, "Could not build session with " + contactsDevice + ".", e);
skippedRecipients.put(contactsDevice, e);
continue;
}
}
int messageCounter = omemoStore.loadOmemoMessageCounter(userDevice, contactsDevice);
// Ignore read-only devices
if (OmemoConfiguration.getIgnoreReadOnlyDevices()) {
boolean readOnly = messageCounter >= OmemoConfiguration.getMaxReadOnlyMessageCount();
if (readOnly) {
LOGGER.log(Level.FINE, "Device " + contactsDevice + " seems to be read-only (We sent "
+ messageCounter + " messages without getting a reply back (max allowed is " +
OmemoConfiguration.getMaxReadOnlyMessageCount() + "). Ignoring the device.");
skippedRecipients.put(contactsDevice, new ReadOnlyDeviceException(contactsDevice));
// Skip this device and handle next device
continue;
}
}
// Add recipients
try {
builder.addRecipient(contactsDevice);
}
catch (NoIdentityKeyException | CorruptedOmemoKeyException e) {
LOGGER.log(Level.WARNING, "Encryption failed for device " + contactsDevice + ".", e);
skippedRecipients.put(contactsDevice, e);
}
catch (UndecidedOmemoIdentityException e) {
throw new AssertionError("Recipients device seems to be undecided, even though we should have thrown" +
" an exception earlier in that case. " + e);
}
catch (UntrustedOmemoIdentityException e) {
LOGGER.log(Level.WARNING, "Device " + contactsDevice + " is untrusted. Message is not encrypted for it.");
skippedRecipients.put(contactsDevice, e);
}
// Increment the message counter of the device
omemoStore.storeOmemoMessageCounter(userDevice, contactsDevice,
messageCounter + 1);
}
OmemoElement element = builder.finish();
return new OmemoMessage.Sent(element, messageKey, iv, contactsDevices, skippedRecipients);
}
/**
* Decrypt an OMEMO message.
*
* @param managerGuard authenticated OmemoManager.
* @param senderJid BareJid of the sender.
* @param omemoElement omemoElement.
* @return decrypted OmemoMessage object.
*
* @throws CorruptedOmemoKeyException if the identityKey of the sender is damaged.
* @throws CryptoFailedException if decryption fails.
* @throws NoRawSessionException if we have no session with the device and it sent a normal (non-preKey) message.
* @throws IOException if an I/O error occurred.
*/
OmemoMessage.Received decryptMessage(OmemoManager.LoggedInOmemoManager managerGuard,
BareJid senderJid,
OmemoElement omemoElement)
throws CorruptedOmemoKeyException, CryptoFailedException, NoRawSessionException, IOException {
OmemoManager manager = managerGuard.get();
int senderId = omemoElement.getHeader().getSid();
OmemoDevice senderDevice = new OmemoDevice(senderJid, senderId);
CipherAndAuthTag cipherAndAuthTag = getOmemoRatchet(manager)
.retrieveMessageKeyAndAuthTag(senderDevice, omemoElement);
// Retrieve senders fingerprint.
OmemoFingerprint senderFingerprint;
try {
senderFingerprint = getOmemoStoreBackend().getFingerprint(manager.getOwnDevice(), senderDevice);
} catch (NoIdentityKeyException e) {
throw new AssertionError("Cannot retrieve OmemoFingerprint of sender although decryption was successful: " + e);
}
// Reset the message counter.
omemoStore.storeOmemoMessageCounter(manager.getOwnDevice(), senderDevice, 0);
if (omemoElement.isMessageElement()) {
// Use symmetric message key to decrypt message payload.
String plaintext = OmemoRatchet.decryptMessageElement(omemoElement, cipherAndAuthTag);
return new OmemoMessage.Received(omemoElement, cipherAndAuthTag.getKey(), cipherAndAuthTag.getIv(),
plaintext, senderFingerprint, senderDevice, cipherAndAuthTag.wasPreKeyEncrypted());
} else {
// KeyTransportMessages don't require decryption of the payload.
return new OmemoMessage.Received(omemoElement, cipherAndAuthTag.getKey(), cipherAndAuthTag.getIv(),
null, senderFingerprint, senderDevice, cipherAndAuthTag.wasPreKeyEncrypted());
}
}
/**
* Create an OMEMO KeyTransportElement.
*
* @see <a href="https://xmpp.org/extensions/xep-0384.html#usecases-keysend">XEP-0384: Sending a key</a>.
*
* @param managerGuard Initialized OmemoManager.
* @param contactsDevices set of recipient devices.
* @param key AES-Key to be transported.
* @param iv initialization vector to be used with the key.
*
* @return a new key transport element
*
* @throws InterruptedException if the calling thread was interrupted.
* @throws UndecidedOmemoIdentityException if the list of recipients contains an undecided device
* @throws CryptoFailedException if we are lacking some cryptographic algorithms
* @throws SmackException.NotConnectedException if the XMPP connection is not connected.
* @throws SmackException.NoResponseException if there was no response from the remote entity.
* @throws IOException if an I/O error occurred.
*/
OmemoMessage.Sent createKeyTransportElement(OmemoManager.LoggedInOmemoManager managerGuard,
Set<OmemoDevice> contactsDevices,
byte[] key,
byte[] iv)
throws InterruptedException, UndecidedOmemoIdentityException, CryptoFailedException,
SmackException.NotConnectedException, SmackException.NoResponseException, IOException {
return encrypt(managerGuard, contactsDevices, key, iv, null);
}
/**
* Create an OmemoMessage.
*
* @param managerGuard initialized OmemoManager
* @param contactsDevices set of recipient devices
* @param message message we want to send
* @return encrypted OmemoMessage
*
* @throws InterruptedException if the calling thread was interrupted.
* @throws UndecidedOmemoIdentityException if the list of recipient devices contains an undecided device.
* @throws CryptoFailedException if we are lacking some cryptographic algorithms
* @throws SmackException.NotConnectedException if the XMPP connection is not connected.
* @throws SmackException.NoResponseException if there was no response from the remote entity.
* @throws IOException if an I/O error occurred.
*/
OmemoMessage.Sent createOmemoMessage(OmemoManager.LoggedInOmemoManager managerGuard,
Set<OmemoDevice> contactsDevices,
String message)
throws InterruptedException, UndecidedOmemoIdentityException, CryptoFailedException,
SmackException.NotConnectedException, SmackException.NoResponseException, IOException {
byte[] key, iv;
iv = OmemoMessageBuilder.generateIv();
try {
key = OmemoMessageBuilder.generateKey(KEYTYPE, KEYLENGTH);
} catch (NoSuchAlgorithmException e) {
throw new CryptoFailedException(e);
}
return encrypt(managerGuard, contactsDevices, key, iv, message);
}
/**
* Retrieve a users OMEMO bundle.
*
* @param connection authenticated XMPP connection.
* @param contactsDevice device of which we want to retrieve the bundle.
* @return OmemoBundle of the device or null, if it doesn't exist.
*
* @throws SmackException.NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException.NoResponseException if there was no response from the remote entity.
* @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
* @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node.
* @throws PubSubException.NotAPubSubNodeException if a involved node is not a PubSub node.
*/
private static OmemoBundleElement fetchBundle(XMPPConnection connection,
OmemoDevice contactsDevice)
throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException,
XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException,
PubSubException.NotAPubSubNodeException {
PubSubManager pm = PubSubManager.getInstanceFor(connection, contactsDevice.getJid());
LeafNode node = pm.getLeafNode(contactsDevice.getBundleNodeName());
if (node == null) {
return null;
}
List<PayloadItem<OmemoBundleElement>> bundleItems = node.getItems();
if (bundleItems.isEmpty()) {
return null;
}
return bundleItems.get(bundleItems.size() - 1).getPayload();
}
/**
* Publish the given OMEMO bundle to the server using PubSub.
*
* @param connection our connection.
* @param userDevice our device
* @param bundle the bundle we want to publish
*
* @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
* @throws SmackException.NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException.NoResponseException if there was no response from the remote entity.
* @throws NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node.
*/
static void publishBundle(XMPPConnection connection, OmemoDevice userDevice, OmemoBundleElement bundle)
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException, NotALeafNodeException {
PepManager pm = PepManager.getInstanceFor(connection);
pm.publish(userDevice.getBundleNodeName(), new PayloadItem<>(bundle));
}
/**
* Retrieve the OMEMO device list of a contact.
*
* @param connection authenticated XMPP connection.
* @param contact BareJid of the contact of which we want to retrieve the device list from.
* @return device list
*
* @throws InterruptedException if the calling thread was interrupted.
* @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node.
* @throws SmackException.NoResponseException if there was no response from the remote entity.
* @throws SmackException.NotConnectedException if the XMPP connection is not connected.
* @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
* @throws PubSubException.NotAPubSubNodeException if a involved node is not a PubSub node.
*/
private static OmemoDeviceListElement fetchDeviceList(XMPPConnection connection, BareJid contact)
throws InterruptedException, PubSubException.NotALeafNodeException, SmackException.NoResponseException,
SmackException.NotConnectedException, XMPPException.XMPPErrorException,
PubSubException.NotAPubSubNodeException {
PubSubManager pm = PubSubManager.getInstanceFor(connection, contact);
String nodeName = OmemoConstants.PEP_NODE_DEVICE_LIST;
LeafNode node = pm.getLeafNode(nodeName);
if (node == null) {
return null;
}
List<PayloadItem<OmemoDeviceListElement>> items = node.getItems();
if (items.isEmpty()) {
return null;
}
return items.get(items.size() - 1).getPayload();
}
/**
* Publish the given device list to the server.
*
* @param connection authenticated XMPP connection.
* @param deviceList users deviceList.
*
* @throws InterruptedException if the calling thread was interrupted.
* @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
* @throws SmackException.NotConnectedException if the XMPP connection is not connected.
* @throws SmackException.NoResponseException if there was no response from the remote entity.
* @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node.
*/
static void publishDeviceList(XMPPConnection connection, OmemoDeviceListElement deviceList)
throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException,
SmackException.NoResponseException, NotALeafNodeException {
PepManager pm = PepManager.getInstanceFor(connection);
pm.publish(OmemoConstants.PEP_NODE_DEVICE_LIST, new PayloadItem<>(deviceList));
}
/**
* Refresh our own device list and publish it to the server.
*
* @param connection XMPPConnection
* @param userDevice our OMEMO device
*
* @throws InterruptedException if the calling thread was interrupted.
* @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node.
* @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
* @throws SmackException.NotConnectedException if the XMPP connection is not connected.
* @throws SmackException.NoResponseException if there was no response from the remote entity.
* @throws IOException if an I/O error occurred.
*/
private void refreshAndRepublishDeviceList(XMPPConnection connection, OmemoDevice userDevice)
throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException,
SmackException.NotConnectedException, SmackException.NoResponseException, IOException {
// refreshOmemoDeviceList;
OmemoDeviceListElement publishedList;
try {
publishedList = fetchDeviceList(connection, userDevice.getJid());
} catch (PubSubException.NotAPubSubNodeException e) {
// Node is not a PubSub node. This might happen on some ejabberd servers.
publishedList = null;
} catch (XMPPException.XMPPErrorException e) {
if (e.getStanzaError().getCondition() == StanzaError.Condition.item_not_found) {
// Items not found -> items do not exist
publishedList = null;
} else {
// Some other error -> throw
throw e;
}
}
if (publishedList == null) {
publishedList = new OmemoDeviceListElement_VAxolotl(Collections.<Integer>emptySet());
}
getOmemoStoreBackend().mergeCachedDeviceList(userDevice, userDevice.getJid(), publishedList);
OmemoCachedDeviceList cachedList = cleanUpDeviceList(userDevice);
// Republish our deviceId if it is missing from the published list.
if (!publishedList.getDeviceIds().equals(cachedList.getActiveDevices())) {
publishDeviceList(connection, new OmemoDeviceListElement_VAxolotl(cachedList));
}
}
/**
* Add our load the deviceList of the user from cache, delete stale devices if needed, add the users device
* back if necessary, store the refurbished list in cache and return it.
*
* @param userDevice our own OMEMO device
* @return cleaned device list
*
* @throws IOException if an I/O error occurred.
*/
OmemoCachedDeviceList cleanUpDeviceList(OmemoDevice userDevice) throws IOException {
OmemoCachedDeviceList cachedDeviceList;
// Delete stale devices if allowed and necessary
if (OmemoConfiguration.getDeleteStaleDevices()) {
cachedDeviceList = deleteStaleDevices(userDevice);
} else {
cachedDeviceList = getOmemoStoreBackend().loadCachedDeviceList(userDevice);
}
// Add back our device if necessary
if (!cachedDeviceList.getActiveDevices().contains(userDevice.getDeviceId())) {
cachedDeviceList.addDevice(userDevice.getDeviceId());
}
getOmemoStoreBackend().storeCachedDeviceList(userDevice, userDevice.getJid(), cachedDeviceList);
return cachedDeviceList;
}
/**
* Refresh and merge device list of contact.
*
* @param connection authenticated XMPP connection
* @param userDevice our OmemoDevice
* @param contact contact we want to fetch the deviceList from
* @return cached device list after refresh.
*
* @throws InterruptedException if the calling thread was interrupted.
* @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node.
* @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
* @throws SmackException.NotConnectedException if the XMPP connection is not connected.
* @throws SmackException.NoResponseException if there was no response from the remote entity.
* @throws IOException if an I/O error occurred.
*/
OmemoCachedDeviceList refreshDeviceList(XMPPConnection connection, OmemoDevice userDevice, BareJid contact)
throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException,
SmackException.NotConnectedException, SmackException.NoResponseException, IOException {
// refreshOmemoDeviceList;
OmemoDeviceListElement publishedList;
try {
publishedList = fetchDeviceList(connection, contact);
} catch (PubSubException.NotAPubSubNodeException e) {
LOGGER.log(Level.WARNING, "Error refreshing deviceList: ", e);
publishedList = null;
}
if (publishedList == null) {
publishedList = new OmemoDeviceListElement_VAxolotl(Collections.<Integer>emptySet());
}
return getOmemoStoreBackend().mergeCachedDeviceList(
userDevice, contact, publishedList);
}
/**
* Fetch the bundle of a contact and build a fresh OMEMO session with the contacts device.
* Note that this builds a fresh session, regardless if we have had a session before or not.
*
* @param connection authenticated XMPP connection
* @param userDevice our OmemoDevice
* @param contactsDevice OmemoDevice of a contact.
*
* @throws CannotEstablishOmemoSessionException if we cannot establish a session (because of missing bundle etc.)
* @throws SmackException.NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException.NoResponseException if there was no response from the remote entity.
* @throws CorruptedOmemoKeyException if our IdentityKeyPair is corrupted.
*/
void buildFreshSessionWithDevice(XMPPConnection connection, OmemoDevice userDevice, OmemoDevice contactsDevice)
throws CannotEstablishOmemoSessionException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException, CorruptedOmemoKeyException {
if (contactsDevice.equals(userDevice)) {
// Do not build a session with yourself.
return;
}
OmemoBundleElement bundleElement;
try {
bundleElement = fetchBundle(connection, contactsDevice);
} catch (XMPPException.XMPPErrorException | PubSubException.NotALeafNodeException |
PubSubException.NotAPubSubNodeException e) {
throw new CannotEstablishOmemoSessionException(contactsDevice, e);
}
// Select random Bundle
HashMap<Integer, T_Bundle> bundlesList = getOmemoStoreBackend().keyUtil().BUNDLE.bundles(bundleElement, contactsDevice);
int randomIndex = new Random().nextInt(bundlesList.size());
T_Bundle randomPreKeyBundle = new ArrayList<>(bundlesList.values()).get(randomIndex);
// build the session
OmemoManager omemoManager = OmemoManager.getInstanceFor(connection, userDevice.getDeviceId());
processBundle(omemoManager, randomPreKeyBundle, contactsDevice);
}
/**
* Build sessions with all devices from the set, we don't have a session with yet.
* Return the set of all devices we have a session with afterwards.
*
* @param connection authenticated XMPP connection
* @param userDevice our OmemoDevice
* @param devices set of devices we may want to build a session with if necessary
* @return set of all devices with sessions
*
* @throws SmackException.NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException.NoResponseException if there was no response from the remote entity.
* @throws IOException if an I/O error occurred.
*/
private Set<OmemoDevice> buildMissingSessionsWithDevices(XMPPConnection connection,
OmemoDevice userDevice,
Set<OmemoDevice> devices)
throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, IOException {
Set<OmemoDevice> devicesWithSession = new HashSet<>();
for (OmemoDevice device : devices) {
if (hasSession(userDevice, device)) {
devicesWithSession.add(device);
continue;
}
try {
buildFreshSessionWithDevice(connection, userDevice, device);
devicesWithSession.add(device);
} catch (CannotEstablishOmemoSessionException e) {
LOGGER.log(Level.WARNING, userDevice + " cannot establish session with " + device +
" because their bundle could not be fetched.", e);
} catch (CorruptedOmemoKeyException e) {
LOGGER.log(Level.WARNING, userDevice + " could not establish session with " + device +
"because their bundle seems to be corrupt.", e);
}
}
return devicesWithSession;
}
/**
* Return a set of all devices from the provided set, which trust level is undecided.
* A device is also considered undecided, if its fingerprint cannot be loaded.
*
* @param userDevice our OmemoDevice
* @param callback OmemoTrustCallback to query the trust decisions from
* @param devices set of OmemoDevices
* @return set of OmemoDevices which contains all devices from the set devices, which are undecided
*
* @throws IOException if an I/O error occurred.
*/
private Set<OmemoDevice> getUndecidedDevices(OmemoDevice userDevice, OmemoTrustCallback callback, Set<OmemoDevice> devices) throws IOException {
Set<OmemoDevice> undecidedDevices = new HashSet<>();
for (OmemoDevice device : devices) {
OmemoFingerprint fingerprint;
try {
fingerprint = getOmemoStoreBackend().getFingerprint(userDevice, device);
} catch (CorruptedOmemoKeyException | NoIdentityKeyException e) {
LOGGER.log(Level.WARNING, "Could not load fingerprint of " + device, e);
undecidedDevices.add(device);
continue;
}
if (callback.getTrust(device, fingerprint) == TrustState.undecided) {
undecidedDevices.add(device);
}
}
return undecidedDevices;
}
/**
* Return true, if the OmemoManager of userDevice has a session with the contactsDevice.
*
* @param userDevice our OmemoDevice.
* @param contactsDevice OmemoDevice of the contact.
* @return true if userDevice has session with contactsDevice.
*
* @throws IOException if an I/O error occurred.
*/
private boolean hasSession(OmemoDevice userDevice, OmemoDevice contactsDevice) throws IOException {
return getOmemoStoreBackend().loadRawSession(userDevice, contactsDevice) != null;
}
/**
* Process a received bundle. Typically that includes saving keys and building a session.
*
* @param omemoManager our OmemoManager
* @param contactsBundle bundle of the contact
* @param contactsDevice OmemoDevice of the contact
*
* @throws CorruptedOmemoKeyException if the OMEMO key is corrupted.
*/
protected abstract void processBundle(OmemoManager omemoManager,
T_Bundle contactsBundle,
OmemoDevice contactsDevice)
throws CorruptedOmemoKeyException;
/**
* Returns true, if a rotation of the signed preKey is necessary.
*
* @param userDevice our OmemoDevice
* @return true if rotation is necessary
*
* @throws IOException if an I/O error occurred.
*/
private boolean shouldRotateSignedPreKey(OmemoDevice userDevice) throws IOException {
if (!OmemoConfiguration.getRenewOldSignedPreKeys()) {
return false;
}
Date now = new Date();
Date lastRenewal = getOmemoStoreBackend().getDateOfLastSignedPreKeyRenewal(userDevice);
if (lastRenewal == null) {
lastRenewal = new Date();
getOmemoStoreBackend().setDateOfLastSignedPreKeyRenewal(userDevice, lastRenewal);
}
long allowedAgeMillis = MILLIS_PER_HOUR * OmemoConfiguration.getRenewOldSignedPreKeysAfterHours();
return now.getTime() - lastRenewal.getTime() > allowedAgeMillis;
}
/**
* Return a copy of our deviceList, but with stale devices marked as inactive.
* Never mark our own device as stale.
* This method ignores {@link OmemoConfiguration#getDeleteStaleDevices()}!
*
* In this case, a stale device is one of our devices, from which we haven't received an OMEMO message from
* for more than {@link OmemoConfiguration#getDeleteStaleDevicesAfterHours()} hours.
*
* @param userDevice our OmemoDevice
* @return our altered deviceList with stale devices marked as inactive.
*
* @throws IOException if an I/O error occurred.
*/
private OmemoCachedDeviceList deleteStaleDevices(OmemoDevice userDevice) throws IOException {
OmemoCachedDeviceList deviceList = getOmemoStoreBackend().loadCachedDeviceList(userDevice);
int maxAgeHours = OmemoConfiguration.getDeleteStaleDevicesAfterHours();
return removeStaleDevicesFromDeviceList(userDevice, userDevice.getJid(), deviceList, maxAgeHours);
}
/**
* Return a copy of the given deviceList of user contact, but with stale devices marked as inactive.
* Never mark our own device as stale. If we haven't yet received a message from a device, store the current date
* as last date of message receipt to allow future decisions.
*
* A stale device is a device, from which we haven't received an OMEMO message from for more than
* "maxAgeMillis" milliseconds.
*
* @param userDevice our OmemoDevice.
* @param contact subjects BareJid.
* @param contactsDeviceList subjects deviceList.
* @return copy of subjects deviceList with stale devices marked as inactive.
*
* @throws IOException if an I/O error occurred.
*/
private OmemoCachedDeviceList removeStaleDevicesFromDeviceList(OmemoDevice userDevice,
BareJid contact,
OmemoCachedDeviceList contactsDeviceList,
int maxAgeHours) throws IOException {
OmemoCachedDeviceList deviceList = new OmemoCachedDeviceList(contactsDeviceList); // Don't work on original list.
// Iterate through original list, but modify copy instead
for (int deviceId : contactsDeviceList.getActiveDevices()) {
OmemoDevice device = new OmemoDevice(contact, deviceId);
Date lastDeviceIdPublication = getOmemoStoreBackend().getDateOfLastDeviceIdPublication(userDevice, device);
if (lastDeviceIdPublication == null) {
lastDeviceIdPublication = new Date();
getOmemoStoreBackend().setDateOfLastDeviceIdPublication(userDevice, device, lastDeviceIdPublication);
}
Date lastMessageReceived = getOmemoStoreBackend().getDateOfLastReceivedMessage(userDevice, device);
if (lastMessageReceived == null) {
lastMessageReceived = new Date();
getOmemoStoreBackend().setDateOfLastReceivedMessage(userDevice, device, lastMessageReceived);
}
boolean stale = isStale(userDevice, device, lastDeviceIdPublication, maxAgeHours);
stale &= isStale(userDevice, device, lastMessageReceived, maxAgeHours);
if (stale) {
deviceList.addInactiveDevice(deviceId);
}
}
return deviceList;
}
/**
* Remove our device from the collection of devices.
*
* @param userDevice our OmemoDevice
* @param devices collection of OmemoDevices
*/
static void removeOurDevice(OmemoDevice userDevice, Collection<OmemoDevice> devices) {
if (devices.contains(userDevice)) {
devices.remove(userDevice);
}
}
/**
* Determine, whether another one of *our* devices is stale or not.
*
* @param userDevice our omemoDevice
* @param subject another one of our devices
* @param lastReceipt date of last received message from that device
* @param maxAgeHours threshold
*
* @return true if the subject device is considered stale
*/
static boolean isStale(OmemoDevice userDevice, OmemoDevice subject, Date lastReceipt, int maxAgeHours) {
if (userDevice.equals(subject)) {
return false;
}
if (lastReceipt == null) {
return false;
}
long maxAgeMillis = MILLIS_PER_HOUR * maxAgeHours;
Date now = new Date();
return now.getTime() - lastReceipt.getTime() > maxAgeMillis;
}
/**
* Gullible TrustCallback, which returns all queried identities as trusted.
* This is only used for insensitive OMEMO messages like RatchetUpdateMessages.
* DO NOT USE THIS FOR ANYTHING ELSE!
*/
private static final OmemoTrustCallback gullibleTrustCallback = new OmemoTrustCallback() {
@Override
public TrustState getTrust(OmemoDevice device, OmemoFingerprint fingerprint) {
return TrustState.trusted;
}
@Override
public void setTrust(OmemoDevice device, OmemoFingerprint fingerprint, TrustState state) {
// Not needed
}
};
/**
* Decrypt a possible OMEMO encrypted messages in a {@link MamManager.MamQuery}.
* The returned list contains wrappers that either hold an {@link OmemoMessage} in case the message was decrypted
* properly, otherwise it contains the message itself.
*
* @param managerGuard authenticated OmemoManager.
* @param mamQuery Mam archive query
* @return list of {@link MessageOrOmemoMessage MessageOrOmemoMessages}.
*
* @throws IOException if an I/O error occurred.
*/
List<MessageOrOmemoMessage> decryptMamQueryResult(OmemoManager.LoggedInOmemoManager managerGuard,
MamManager.MamQuery mamQuery) throws IOException {
List<MessageOrOmemoMessage> result = new ArrayList<>();
for (Message message : mamQuery.getMessages()) {
if (OmemoManager.stanzaContainsOmemoElement(message)) {
OmemoElement element =
(OmemoElement) message.getExtensionElement(OmemoElement.NAME_ENCRYPTED, OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL);
// Decrypt OMEMO messages
try {
OmemoMessage.Received omemoMessage = decryptMessage(managerGuard, message.getFrom().asBareJid(), element);
result.add(new MessageOrOmemoMessage(omemoMessage));
} catch (NoRawSessionException | CorruptedOmemoKeyException | CryptoFailedException e) {
LOGGER.log(Level.WARNING, "decryptMamQueryResult failed to decrypt message from "
+ message.getFrom() + " due to corrupted session/key: " + e.getMessage());
result.add(new MessageOrOmemoMessage(message));
}
} else {
// Wrap cleartext messages
result.add(new MessageOrOmemoMessage(message));
}
}
return result;
}
@Override
public void onOmemoCarbonCopyReceived(CarbonExtension.Direction direction,
Message carbonCopy,
Message wrappingMessage,
OmemoManager.LoggedInOmemoManager managerGuard) throws IOException {
OmemoManager manager = managerGuard.get();
// Avoid the ratchet being manipulated and the bundle being published multiple times simultaneously
synchronized (manager) {
OmemoDevice userDevice = manager.getOwnDevice();
OmemoElement element = (OmemoElement) carbonCopy.getExtensionElement(OmemoElement.NAME_ENCRYPTED, OmemoElement_VAxolotl.NAMESPACE);
if (element == null) {
return;
}
OmemoMessage.Received decrypted;
BareJid sender = carbonCopy.getFrom().asBareJid();
try {
decrypted = decryptMessage(managerGuard, sender, element);
manager.notifyOmemoCarbonCopyReceived(direction, carbonCopy, wrappingMessage, decrypted);
if (decrypted.isPreKeyMessage() && OmemoConfiguration.getCompleteSessionWithEmptyMessage()) {
LOGGER.log(Level.FINE, "Received a preKeyMessage in a carbon copy from " + decrypted.getSenderDevice() + ".\n" +
"Complete the session by sending an empty response message.");
try {
sendRatchetUpdate(managerGuard, decrypted.getSenderDevice());
} catch (CannotEstablishOmemoSessionException e) {
throw new AssertionError("Since we successfully received a message, we MUST be able to " +
"establish a session. " + e);
} catch (NoSuchAlgorithmException | InterruptedException | SmackException.NotConnectedException | SmackException.NoResponseException e) {
LOGGER.log(Level.WARNING, "Cannot send a ratchet update message.", e);
}
}
} catch (NoRawSessionException e) {
OmemoDevice device = e.getDeviceWithoutSession();
LOGGER.log(Level.WARNING, "No raw session found for contact " + device + ". ", e);
if (OmemoConfiguration.getRepairBrokenSessionsWithPreKeyMessages()) {
repairBrokenSessionWithPreKeyMessage(managerGuard, device);
}
} catch (CorruptedOmemoKeyException | CryptoFailedException e) {
LOGGER.log(Level.WARNING, "Could not decrypt incoming carbon copy: ", e);
}
// Upload fresh bundle.
if (getOmemoStoreBackend().loadOmemoPreKeys(userDevice).size() < OmemoConstants.PRE_KEY_COUNT_PER_BUNDLE) {
LOGGER.log(Level.FINE, "We used up a preKey. Upload a fresh bundle.");
try {
getOmemoStoreBackend().replenishKeys(userDevice);
OmemoBundleElement bundleElement = getOmemoStoreBackend().packOmemoBundle(userDevice);
publishBundle(manager.getConnection(), userDevice, bundleElement);
} catch (CorruptedOmemoKeyException | InterruptedException | SmackException.NoResponseException
| SmackException.NotConnectedException | XMPPException.XMPPErrorException
| NotALeafNodeException e) {
LOGGER.log(Level.WARNING, "Could not republish replenished bundle.", e);
}
}
}
}
@Override
public void onOmemoMessageStanzaReceived(Stanza stanza, OmemoManager.LoggedInOmemoManager managerGuard) throws IOException {
OmemoManager manager = managerGuard.get();
// Avoid the ratchet being manipulated and the bundle being published multiple times simultaneously
synchronized (manager) {
OmemoDevice userDevice = manager.getOwnDevice();
OmemoElement element = (OmemoElement) stanza.getExtensionElement(OmemoElement.NAME_ENCRYPTED, OmemoElement_VAxolotl.NAMESPACE);
if (element == null) {
return;
}
OmemoMessage.Received decrypted;
BareJid sender;
try {
MultiUserChat muc = getMuc(manager.getConnection(), stanza.getFrom());
if (muc != null) {
Occupant occupant = muc.getOccupant(stanza.getFrom().asEntityFullJidIfPossible());
if (occupant == null) {
LOGGER.log(Level.WARNING, "Cannot decrypt OMEMO MUC message; MUC Occupant is null.");
return;
}
Jid occupantJid = occupant.getJid();
if (occupantJid == null) {
LOGGER.log(Level.WARNING, "Cannot decrypt OMEMO MUC message; Senders Jid is null. " +
stanza.getFrom());
return;
}
sender = occupantJid.asBareJid();
// try is for this
decrypted = decryptMessage(managerGuard, sender, element);
manager.notifyOmemoMucMessageReceived(muc, stanza, decrypted);
} else {
sender = stanza.getFrom().asBareJid();
// and this
decrypted = decryptMessage(managerGuard, sender, element);
manager.notifyOmemoMessageReceived(stanza, decrypted);
}
if (decrypted.isPreKeyMessage() && OmemoConfiguration.getCompleteSessionWithEmptyMessage()) {
LOGGER.log(Level.FINE, "Received a preKeyMessage from " + decrypted.getSenderDevice() + ".\n" +
"Complete the session by sending an empty response message.");
try {
sendRatchetUpdate(managerGuard, decrypted.getSenderDevice());
} catch (CannotEstablishOmemoSessionException e) {
throw new AssertionError("Since we successfully received a message, we MUST be able to " +
"establish a session. " + e);
} catch (NoSuchAlgorithmException | InterruptedException | SmackException.NotConnectedException | SmackException.NoResponseException e) {
LOGGER.log(Level.WARNING, "Cannot send a ratchet update message.", e);
}
}
} catch (NoRawSessionException e) {
OmemoDevice device = e.getDeviceWithoutSession();
LOGGER.log(Level.WARNING, "No raw session found for contact " + device + ". ", e);
if (OmemoConfiguration.getRepairBrokenSessionsWithPreKeyMessages()) {
repairBrokenSessionWithPreKeyMessage(managerGuard, device);
}
} catch (CorruptedOmemoKeyException | CryptoFailedException e) {
LOGGER.log(Level.WARNING, "Could not decrypt incoming message: ", e);
}
// Upload fresh bundle.
if (getOmemoStoreBackend().loadOmemoPreKeys(userDevice).size() < OmemoConstants.PRE_KEY_COUNT_PER_BUNDLE) {
LOGGER.log(Level.FINE, "We used up a preKey. Upload a fresh bundle.");
try {
getOmemoStoreBackend().replenishKeys(userDevice);
OmemoBundleElement bundleElement = getOmemoStoreBackend().packOmemoBundle(userDevice);
publishBundle(manager.getConnection(), userDevice, bundleElement);
} catch (CorruptedOmemoKeyException | InterruptedException | SmackException.NoResponseException
| SmackException.NotConnectedException | XMPPException.XMPPErrorException
| NotALeafNodeException e) {
LOGGER.log(Level.WARNING, "Could not republish replenished bundle.", e);
}
}
}
}
/**
* Decrypt the OmemoElement inside the given Stanza and return it.
* Return null if something goes wrong.
*
* @param stanza stanza
* @param managerGuard authenticated OmemoManager
* @return decrypted OmemoMessage or null
*
* @throws IOException if an I/O error occurred.
*/
OmemoMessage.Received decryptStanza(Stanza stanza, OmemoManager.LoggedInOmemoManager managerGuard) throws IOException {
OmemoManager manager = managerGuard.get();
// Avoid the ratchet being manipulated and the bundle being published multiple times simultaneously
synchronized (manager) {
OmemoDevice userDevice = manager.getOwnDevice();
OmemoElement element = (OmemoElement) stanza.getExtensionElement(OmemoElement.NAME_ENCRYPTED, OmemoElement_VAxolotl.NAMESPACE);
if (element == null) {
return null;
}
OmemoMessage.Received decrypted = null;
BareJid sender;
try {
MultiUserChat muc = getMuc(manager.getConnection(), stanza.getFrom());
if (muc != null) {
Occupant occupant = muc.getOccupant(stanza.getFrom().asEntityFullJidIfPossible());
Jid occupantJid = occupant.getJid();
if (occupantJid == null) {
LOGGER.log(Level.WARNING, "MUC message received, but there is no way to retrieve the senders Jid. " +
stanza.getFrom());
return null;
}
sender = occupantJid.asBareJid();
// try is for this
decrypted = decryptMessage(managerGuard, sender, element);
} else {
sender = stanza.getFrom().asBareJid();
// and this
decrypted = decryptMessage(managerGuard, sender, element);
}
if (decrypted.isPreKeyMessage() && OmemoConfiguration.getCompleteSessionWithEmptyMessage()) {
LOGGER.log(Level.FINE, "Received a preKeyMessage from " + decrypted.getSenderDevice() + ".\n" +
"Complete the session by sending an empty response message.");
try {
sendRatchetUpdate(managerGuard, decrypted.getSenderDevice());
} catch (CannotEstablishOmemoSessionException e) {
throw new AssertionError("Since we successfully received a message, we MUST be able to " +
"establish a session. " + e);
} catch (NoSuchAlgorithmException | InterruptedException | SmackException.NotConnectedException | SmackException.NoResponseException e) {
LOGGER.log(Level.WARNING, "Cannot send a ratchet update message.", e);
}
}
} catch (NoRawSessionException e) {
OmemoDevice device = e.getDeviceWithoutSession();
LOGGER.log(Level.WARNING, "No raw session found for contact " + device + ". ", e);
} catch (CorruptedOmemoKeyException | CryptoFailedException e) {
LOGGER.log(Level.WARNING, "Could not decrypt incoming message: ", e);
}
// Upload fresh bundle.
if (getOmemoStoreBackend().loadOmemoPreKeys(userDevice).size() < OmemoConstants.PRE_KEY_COUNT_PER_BUNDLE) {
LOGGER.log(Level.FINE, "We used up a preKey. Upload a fresh bundle.");
try {
getOmemoStoreBackend().replenishKeys(userDevice);
OmemoBundleElement bundleElement = getOmemoStoreBackend().packOmemoBundle(userDevice);
publishBundle(manager.getConnection(), userDevice, bundleElement);
} catch (CorruptedOmemoKeyException | InterruptedException | SmackException.NoResponseException
| SmackException.NotConnectedException | XMPPException.XMPPErrorException
| NotALeafNodeException e) {
LOGGER.log(Level.WARNING, "Could not republish replenished bundle.", e);
}
}
return decrypted;
}
}
/**
* Fetch and process a fresh bundle and send an empty preKeyMessage in order to establish a fresh session.
*
* @param managerGuard authenticated OmemoManager.
* @param brokenDevice device which session broke.
*
* @throws IOException if an I/O error occurred.
*/
private void repairBrokenSessionWithPreKeyMessage(OmemoManager.LoggedInOmemoManager managerGuard,
OmemoDevice brokenDevice) throws IOException {
LOGGER.log(Level.WARNING, "Attempt to repair the session by sending a fresh preKey message to "
+ brokenDevice);
OmemoManager manager = managerGuard.get();
try {
// Create fresh session and send new preKeyMessage.
buildFreshSessionWithDevice(manager.getConnection(), manager.getOwnDevice(), brokenDevice);
sendRatchetUpdate(managerGuard, brokenDevice);
} catch (CannotEstablishOmemoSessionException | CorruptedOmemoKeyException e) {
LOGGER.log(Level.WARNING, "Unable to repair session with " + brokenDevice, e);
} catch (SmackException.NotConnectedException | InterruptedException | SmackException.NoResponseException e) {
LOGGER.log(Level.WARNING, "Could not fetch fresh bundle for " + brokenDevice, e);
} catch (CryptoFailedException | NoSuchAlgorithmException e) {
LOGGER.log(Level.WARNING, "Could not create PreKeyMessage", e);
}
}
/**
* Send an empty OMEMO message to contactsDevice in order to forward the ratchet.
*
* @param managerGuard OMEMO manager
* @param contactsDevice contacts OMEMO device
*
* @throws CorruptedOmemoKeyException if our or their OMEMO key is corrupted.
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException.NoResponseException if there was no response from the remote entity.
* @throws NoSuchAlgorithmException if AES encryption fails
* @throws SmackException.NotConnectedException if the XMPP connection is not connected.
* @throws CryptoFailedException if encryption fails (should not happen though, but who knows...)
* @throws CannotEstablishOmemoSessionException if we cannot establish a session with contactsDevice.
* @throws IOException if an I/O error occurred.
*/
private void sendRatchetUpdate(OmemoManager.LoggedInOmemoManager managerGuard, OmemoDevice contactsDevice)
throws CorruptedOmemoKeyException, InterruptedException, SmackException.NoResponseException,
NoSuchAlgorithmException, SmackException.NotConnectedException, CryptoFailedException,
CannotEstablishOmemoSessionException, IOException {
OmemoManager manager = managerGuard.get();
OmemoElement ratchetUpdate = createRatchetUpdateElement(managerGuard, contactsDevice);
XMPPConnection connection = manager.getConnection();
Message message = connection.getStanzaFactory().buildMessageStanza()
.to(contactsDevice.getJid())
.addExtension(ratchetUpdate)
.build();
connection.sendStanza(message);
}
/**
* Return the joined MUC with EntityBareJid jid, or null if its not a room and/or not joined.
*
* @param connection xmpp connection
* @param jid jid (presumably) of the MUC
* @return MultiUserChat or null if not a MUC.
*/
private static MultiUserChat getMuc(XMPPConnection connection, Jid jid) {
EntityBareJid ebj = jid.asEntityBareJidIfPossible();
if (ebj == null) {
return null;
}
MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(connection);
Set<EntityBareJid> joinedRooms = mucm.getJoinedRooms();
if (joinedRooms.contains(ebj)) {
return mucm.getMultiUserChat(ebj);
}
return null;
}
/**
* Publish a new DeviceList with just our device in it.
*
* @param managerGuard authenticated OmemoManager.
*
* @throws InterruptedException if the calling thread was interrupted.
* @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
* @throws SmackException.NotConnectedException if the XMPP connection is not connected.
* @throws SmackException.NoResponseException if there was no response from the remote entity.
* @throws IOException if an I/O error occurred.
* @throws NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node.
*/
public void purgeDeviceList(OmemoManager.LoggedInOmemoManager managerGuard)
throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException,
SmackException.NoResponseException, IOException, NotALeafNodeException {
OmemoManager omemoManager = managerGuard.get();
OmemoDevice userDevice = omemoManager.getOwnDevice();
OmemoDeviceListElement_VAxolotl newList =
new OmemoDeviceListElement_VAxolotl(Collections.singleton(userDevice.getDeviceId()));
// Merge list
getOmemoStoreBackend().mergeCachedDeviceList(userDevice, userDevice.getJid(), newList);
OmemoService.publishDeviceList(omemoManager.getConnection(), newList);
}
}