2017-06-02 12:26:37 +02:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* Copyright 2017 Paul Schaub
|
|
|
|
*
|
|
|
|
* 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;
|
|
|
|
|
2017-06-14 17:12:43 +02:00
|
|
|
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL;
|
2017-09-29 15:36:28 +02:00
|
|
|
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.PEP_NODE_DEVICE_LIST_NOTIFY;
|
2017-06-14 17:12:43 +02:00
|
|
|
|
|
|
|
import java.security.NoSuchAlgorithmException;
|
|
|
|
import java.util.ArrayList;
|
2018-06-13 12:29:16 +02:00
|
|
|
import java.util.Arrays;
|
2017-06-14 17:12:43 +02:00
|
|
|
import java.util.HashMap;
|
|
|
|
import java.util.HashSet;
|
|
|
|
import java.util.List;
|
|
|
|
import java.util.Random;
|
|
|
|
import java.util.Set;
|
2018-06-13 12:29:16 +02:00
|
|
|
import java.util.SortedSet;
|
|
|
|
import java.util.TreeMap;
|
2017-06-14 17:12:43 +02:00
|
|
|
import java.util.WeakHashMap;
|
|
|
|
import java.util.logging.Level;
|
|
|
|
import java.util.logging.Logger;
|
|
|
|
|
2017-07-19 14:02:20 +02:00
|
|
|
import org.jivesoftware.smack.AbstractConnectionListener;
|
2017-06-02 12:26:37 +02:00
|
|
|
import org.jivesoftware.smack.Manager;
|
|
|
|
import org.jivesoftware.smack.SmackException;
|
2018-06-13 12:29:16 +02:00
|
|
|
import org.jivesoftware.smack.StanzaListener;
|
2017-06-02 12:26:37 +02:00
|
|
|
import org.jivesoftware.smack.XMPPConnection;
|
|
|
|
import org.jivesoftware.smack.XMPPException;
|
2018-06-13 12:29:16 +02:00
|
|
|
import org.jivesoftware.smack.filter.StanzaFilter;
|
2017-06-02 12:26:37 +02:00
|
|
|
import org.jivesoftware.smack.packet.ExtensionElement;
|
|
|
|
import org.jivesoftware.smack.packet.Message;
|
|
|
|
import org.jivesoftware.smack.packet.Stanza;
|
2017-07-19 14:00:15 +02:00
|
|
|
import org.jivesoftware.smack.util.Async;
|
2018-06-13 12:29:16 +02:00
|
|
|
import org.jivesoftware.smackx.carbons.CarbonCopyReceivedListener;
|
2017-06-02 12:26:37 +02:00
|
|
|
import org.jivesoftware.smackx.carbons.CarbonManager;
|
2018-06-13 12:29:16 +02:00
|
|
|
import org.jivesoftware.smackx.carbons.packet.CarbonExtension;
|
2017-06-02 12:26:37 +02:00
|
|
|
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
|
|
|
|
import org.jivesoftware.smackx.hints.element.StoreHint;
|
|
|
|
import org.jivesoftware.smackx.mam.MamManager;
|
|
|
|
import org.jivesoftware.smackx.muc.MultiUserChat;
|
|
|
|
import org.jivesoftware.smackx.muc.MultiUserChatManager;
|
|
|
|
import org.jivesoftware.smackx.muc.RoomInfo;
|
2018-06-13 12:29:16 +02:00
|
|
|
import org.jivesoftware.smackx.omemo.element.OmemoBundleElement;
|
|
|
|
import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement;
|
|
|
|
import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement_VAxolotl;
|
2017-06-02 12:26:37 +02:00
|
|
|
import org.jivesoftware.smackx.omemo.element.OmemoElement;
|
|
|
|
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.NoOmemoSupportException;
|
|
|
|
import org.jivesoftware.smackx.omemo.exceptions.NoRawSessionException;
|
|
|
|
import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException;
|
2018-06-13 12:29:16 +02:00
|
|
|
import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList;
|
2017-06-02 12:26:37 +02:00
|
|
|
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
|
|
|
|
import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener;
|
|
|
|
import org.jivesoftware.smackx.omemo.listener.OmemoMucMessageListener;
|
2018-06-13 12:29:16 +02:00
|
|
|
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;
|
2017-06-02 12:26:37 +02:00
|
|
|
import org.jivesoftware.smackx.pep.PEPListener;
|
|
|
|
import org.jivesoftware.smackx.pep.PEPManager;
|
|
|
|
import org.jivesoftware.smackx.pubsub.EventElement;
|
|
|
|
import org.jivesoftware.smackx.pubsub.ItemsExtension;
|
|
|
|
import org.jivesoftware.smackx.pubsub.PayloadItem;
|
|
|
|
import org.jivesoftware.smackx.pubsub.PubSubException;
|
|
|
|
import org.jivesoftware.smackx.pubsub.packet.PubSub;
|
2017-06-14 17:12:43 +02:00
|
|
|
|
2017-06-02 12:26:37 +02:00
|
|
|
import org.jxmpp.jid.BareJid;
|
|
|
|
import org.jxmpp.jid.DomainBareJid;
|
|
|
|
import org.jxmpp.jid.EntityBareJid;
|
|
|
|
import org.jxmpp.jid.EntityFullJid;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Manager that allows sending messages encrypted with OMEMO.
|
|
|
|
* This class also provides some methods useful for a client that implements OMEMO.
|
|
|
|
*
|
|
|
|
* @author Paul Schaub
|
|
|
|
*/
|
|
|
|
|
|
|
|
public final class OmemoManager extends Manager {
|
|
|
|
private static final Logger LOGGER = Logger.getLogger(OmemoManager.class.getName());
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
private static final Integer UNKNOWN_DEVICE_ID = -1;
|
|
|
|
final Object LOCK = new Object();
|
|
|
|
|
|
|
|
private static final WeakHashMap<XMPPConnection, TreeMap<Integer,OmemoManager>> INSTANCES = new WeakHashMap<>();
|
2017-06-02 12:26:37 +02:00
|
|
|
private final OmemoService<?, ?, ?, ?, ?, ?, ?, ?, ?> service;
|
|
|
|
|
|
|
|
private final HashSet<OmemoMessageListener> omemoMessageListeners = new HashSet<>();
|
|
|
|
private final HashSet<OmemoMucMessageListener> omemoMucMessageListeners = new HashSet<>();
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
private OmemoTrustCallback trustCallback;
|
2017-06-02 12:26:37 +02:00
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
private BareJid ownJid;
|
|
|
|
private Integer deviceId;
|
2017-06-02 12:26:37 +02:00
|
|
|
|
|
|
|
/**
|
2018-06-13 12:29:16 +02:00
|
|
|
* Private constructor.
|
2017-06-02 12:26:37 +02:00
|
|
|
*
|
|
|
|
* @param connection connection
|
2018-06-13 12:29:16 +02:00
|
|
|
* @param deviceId deviceId
|
2017-06-02 12:26:37 +02:00
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
private OmemoManager(XMPPConnection connection, Integer deviceId) {
|
2017-06-02 12:26:37 +02:00
|
|
|
super(connection);
|
2017-07-19 14:02:20 +02:00
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
service = OmemoService.getInstance();
|
|
|
|
|
2017-06-02 12:26:37 +02:00
|
|
|
this.deviceId = deviceId;
|
2017-07-19 14:02:20 +02:00
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
if (connection.isAuthenticated()) {
|
|
|
|
initBareJidAndDeviceId(this);
|
|
|
|
} else {
|
|
|
|
connection.addConnectionListener(new AbstractConnectionListener() {
|
|
|
|
@Override
|
|
|
|
public void authenticated(XMPPConnection connection, boolean resumed) {
|
|
|
|
initBareJidAndDeviceId(OmemoManager.this);
|
2017-07-19 14:02:20 +02:00
|
|
|
}
|
2018-06-13 12:29:16 +02:00
|
|
|
});
|
|
|
|
}
|
2017-07-19 14:02:20 +02:00
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
service.registerRatchetForManager(this);
|
2017-09-29 15:36:28 +02:00
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
// StanzaListeners
|
|
|
|
resumeStanzaAndPEPListeners();
|
|
|
|
|
|
|
|
// Announce OMEMO support
|
|
|
|
ServiceDiscoveryManager.getInstanceFor(connection).addFeature(PEP_NODE_DEVICE_LIST_NOTIFY);
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-06-13 12:29:16 +02:00
|
|
|
* Return an OmemoManager instance for the given connection and deviceId.
|
|
|
|
* If there was an OmemoManager for the connection and id before, return it. Otherwise create a new OmemoManager
|
|
|
|
* instance and return it.
|
2017-06-02 12:26:37 +02:00
|
|
|
*
|
2018-06-13 12:29:16 +02:00
|
|
|
* @param connection XmppConnection.
|
|
|
|
* @param deviceId MUST NOT be null and MUST be greater than 0.
|
|
|
|
*
|
|
|
|
* @return manager
|
2017-06-02 12:26:37 +02:00
|
|
|
*/
|
2018-03-29 12:35:11 +02:00
|
|
|
public static synchronized OmemoManager getInstanceFor(XMPPConnection connection, Integer deviceId) {
|
2018-06-13 12:29:16 +02:00
|
|
|
if (deviceId == null || deviceId < 1) {
|
|
|
|
throw new IllegalArgumentException("DeviceId MUST NOT be null and MUST be greater than 0.");
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
TreeMap<Integer,OmemoManager> managersOfConnection = INSTANCES.get(connection);
|
|
|
|
if (managersOfConnection == null) {
|
|
|
|
managersOfConnection = new TreeMap<>();
|
|
|
|
INSTANCES.put(connection, managersOfConnection);
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
OmemoManager manager = managersOfConnection.get(deviceId);
|
|
|
|
if (manager == null) {
|
|
|
|
manager = new OmemoManager(connection, deviceId);
|
|
|
|
managersOfConnection.put(deviceId, manager);
|
|
|
|
}
|
2018-06-13 12:29:16 +02:00
|
|
|
|
2017-06-02 12:26:37 +02:00
|
|
|
return manager;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-06-13 12:29:16 +02:00
|
|
|
* Returns an OmemoManager instance for the given connection. If there was one manager for the connection before,
|
|
|
|
* return it. If there were multiple managers before, return the one with the lowest deviceId.
|
|
|
|
* If there was no manager before, return a new one. As soon as the connection gets authenticated, the manager
|
|
|
|
* will look for local deviceIDs and select the lowest one as its id. If there are not local deviceIds, the manager
|
|
|
|
* will assign itself a random id.
|
2017-06-02 12:26:37 +02:00
|
|
|
*
|
2018-06-13 12:29:16 +02:00
|
|
|
* @param connection XmppConnection.
|
|
|
|
*
|
|
|
|
* @return manager
|
2017-06-02 12:26:37 +02:00
|
|
|
*/
|
2018-03-29 12:35:11 +02:00
|
|
|
public static synchronized OmemoManager getInstanceFor(XMPPConnection connection) {
|
2018-06-13 12:29:16 +02:00
|
|
|
TreeMap<Integer, OmemoManager> managers = INSTANCES.get(connection);
|
|
|
|
if (managers == null) {
|
|
|
|
managers = new TreeMap<>();
|
|
|
|
INSTANCES.put(connection, managers);
|
|
|
|
}
|
|
|
|
|
|
|
|
OmemoManager manager;
|
|
|
|
if (managers.size() == 0) {
|
|
|
|
|
|
|
|
manager = new OmemoManager(connection, UNKNOWN_DEVICE_ID);
|
|
|
|
managers.put(UNKNOWN_DEVICE_ID, manager);
|
|
|
|
|
2017-06-02 12:26:37 +02:00
|
|
|
} else {
|
2018-06-13 12:29:16 +02:00
|
|
|
manager = managers.get(managers.firstKey());
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
return manager;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set a TrustCallback for this particular OmemoManager.
|
|
|
|
* TrustCallbacks are used to query and modify trust decisions.
|
|
|
|
*
|
|
|
|
* @param callback trustCallback.
|
|
|
|
*/
|
|
|
|
public void setTrustCallback(OmemoTrustCallback callback) {
|
|
|
|
if (trustCallback != null) {
|
|
|
|
throw new IllegalStateException("TrustCallback can only be set once.");
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
2018-06-13 12:29:16 +02:00
|
|
|
trustCallback = callback;
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
/**
|
|
|
|
* Return the TrustCallback of this manager.
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
OmemoTrustCallback getTrustCallback() {
|
|
|
|
return trustCallback;
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-06-13 12:29:16 +02:00
|
|
|
* Initializes the OmemoManager. This method must be called before the manager can be used.
|
2017-06-02 12:26:37 +02:00
|
|
|
*
|
|
|
|
* @throws CorruptedOmemoKeyException
|
|
|
|
* @throws InterruptedException
|
|
|
|
* @throws SmackException.NoResponseException
|
|
|
|
* @throws SmackException.NotConnectedException
|
|
|
|
* @throws XMPPException.XMPPErrorException
|
|
|
|
* @throws SmackException.NotLoggedInException
|
|
|
|
* @throws PubSubException.NotALeafNodeException
|
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
public void initialize()
|
|
|
|
throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, InterruptedException,
|
|
|
|
SmackException.NoResponseException, SmackException.NotConnectedException, XMPPException.XMPPErrorException,
|
2017-06-02 12:26:37 +02:00
|
|
|
PubSubException.NotALeafNodeException {
|
2018-06-13 12:29:16 +02:00
|
|
|
synchronized (LOCK) {
|
|
|
|
if (!connection().isAuthenticated()) {
|
|
|
|
throw new SmackException.NotLoggedInException();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (getTrustCallback() == null) {
|
|
|
|
throw new IllegalStateException("No TrustCallback set.");
|
|
|
|
}
|
|
|
|
|
|
|
|
getOmemoService().init(new LoggedInOmemoManager(this));
|
|
|
|
ServiceDiscoveryManager.getInstanceFor(connection()).addFeature(PEP_NODE_DEVICE_LIST_NOTIFY);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialize the manager without blocking. Once the manager is successfully initialized, the finishedCallback will
|
|
|
|
* be notified. It will also get notified, if an error occurs.
|
|
|
|
*
|
|
|
|
* @param finishedCallback callback that gets called once the manager is initialized.
|
|
|
|
*/
|
|
|
|
public void initializeAsync(final InitializationFinishedCallback finishedCallback) {
|
|
|
|
Async.go(new Runnable() {
|
|
|
|
@Override
|
|
|
|
public void run() {
|
|
|
|
try {
|
|
|
|
initialize();
|
|
|
|
finishedCallback.initializationFinished(OmemoManager.this);
|
|
|
|
} catch (Exception e) {
|
|
|
|
finishedCallback.initializationFailed(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a set of all OMEMO capable devices of a contact.
|
|
|
|
* Note, that this method does not explicitly refresh the device list of the contact, so it might be outdated.
|
|
|
|
* @see #requestDeviceListUpdateFor(BareJid)
|
|
|
|
* @param contact contact we want to get a set of device of.
|
|
|
|
* @return set of known devices of that contact.
|
|
|
|
*/
|
|
|
|
public Set<OmemoDevice> getDevicesOf(BareJid contact) {
|
|
|
|
OmemoCachedDeviceList list = getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(getOwnDevice(), contact);
|
|
|
|
HashSet<OmemoDevice> devices = new HashSet<>();
|
|
|
|
|
|
|
|
for (int deviceId : list.getActiveDevices()) {
|
|
|
|
devices.add(new OmemoDevice(contact, deviceId));
|
|
|
|
}
|
|
|
|
|
|
|
|
return devices;
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* OMEMO encrypt a cleartext message for a single recipient.
|
2018-06-13 12:29:16 +02:00
|
|
|
* Note that this method does NOT set the 'to' attribute of the message.
|
2017-06-02 12:26:37 +02:00
|
|
|
*
|
2018-06-13 12:29:16 +02:00
|
|
|
* @param recipient recipients bareJid
|
2017-06-02 12:26:37 +02:00
|
|
|
* @param message text to encrypt
|
|
|
|
* @return encrypted message
|
|
|
|
* @throws CryptoFailedException when something crypto related fails
|
|
|
|
* @throws UndecidedOmemoIdentityException When there are undecided devices
|
|
|
|
* @throws InterruptedException
|
|
|
|
* @throws SmackException.NotConnectedException
|
|
|
|
* @throws SmackException.NoResponseException
|
2018-06-13 12:29:16 +02:00
|
|
|
* @throws SmackException.NotLoggedInException
|
2017-06-02 12:26:37 +02:00
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
public OmemoMessage.Sent encrypt(BareJid recipient, String message)
|
|
|
|
throws CryptoFailedException, UndecidedOmemoIdentityException,
|
|
|
|
InterruptedException, SmackException.NotConnectedException,
|
|
|
|
SmackException.NoResponseException, SmackException.NotLoggedInException {
|
|
|
|
synchronized (LOCK) {
|
|
|
|
Set<BareJid> recipients = new HashSet<>();
|
|
|
|
recipients.add(recipient);
|
|
|
|
return encrypt(recipients, message);
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* OMEMO encrypt a cleartext message for multiple recipients.
|
|
|
|
*
|
|
|
|
* @param recipients recipients barejids
|
|
|
|
* @param message text to encrypt
|
|
|
|
* @return encrypted message.
|
|
|
|
* @throws CryptoFailedException When something crypto related fails
|
|
|
|
* @throws UndecidedOmemoIdentityException When there are undecided devices.
|
|
|
|
* @throws InterruptedException
|
|
|
|
* @throws SmackException.NotConnectedException
|
|
|
|
* @throws SmackException.NoResponseException
|
2018-06-13 12:29:16 +02:00
|
|
|
* @throws SmackException.NotLoggedInException
|
2017-06-02 12:26:37 +02:00
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
public OmemoMessage.Sent encrypt(Set<BareJid> recipients, String message)
|
|
|
|
throws CryptoFailedException, UndecidedOmemoIdentityException,
|
|
|
|
InterruptedException, SmackException.NotConnectedException,
|
|
|
|
SmackException.NoResponseException, SmackException.NotLoggedInException {
|
|
|
|
synchronized (LOCK) {
|
|
|
|
LoggedInOmemoManager guard = new LoggedInOmemoManager(this);
|
|
|
|
Set<OmemoDevice> devices = getDevicesOf(getOwnJid());
|
|
|
|
for (BareJid recipient : recipients) {
|
|
|
|
devices.addAll(getDevicesOf(recipient));
|
|
|
|
}
|
|
|
|
return service.createOmemoMessage(guard, devices, message);
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Encrypt a message for all recipients in the MultiUserChat.
|
|
|
|
*
|
|
|
|
* @param muc multiUserChat
|
|
|
|
* @param message message to send
|
|
|
|
* @return encrypted message
|
|
|
|
* @throws UndecidedOmemoIdentityException when there are undecided devices.
|
|
|
|
* @throws CryptoFailedException
|
|
|
|
* @throws XMPPException.XMPPErrorException
|
|
|
|
* @throws SmackException.NotConnectedException
|
|
|
|
* @throws InterruptedException
|
|
|
|
* @throws SmackException.NoResponseException
|
|
|
|
* @throws NoOmemoSupportException When the muc doesn't support OMEMO.
|
2018-06-13 12:29:16 +02:00
|
|
|
* @throws SmackException.NotLoggedInException
|
2017-06-02 12:26:37 +02:00
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
public OmemoMessage.Sent encrypt(MultiUserChat muc, String message)
|
|
|
|
throws UndecidedOmemoIdentityException, CryptoFailedException,
|
|
|
|
XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
|
|
|
|
SmackException.NoResponseException, NoOmemoSupportException,
|
|
|
|
SmackException.NotLoggedInException {
|
|
|
|
synchronized (LOCK) {
|
|
|
|
if (!multiUserChatSupportsOmemo(muc)) {
|
|
|
|
throw new NoOmemoSupportException();
|
|
|
|
}
|
|
|
|
|
|
|
|
Set<BareJid> recipients = new HashSet<>();
|
|
|
|
|
|
|
|
for (EntityFullJid e : muc.getOccupants()) {
|
|
|
|
recipients.add(muc.getOccupant(e).getJid().asBareJid());
|
|
|
|
}
|
|
|
|
return encrypt(recipients, message);
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-06-13 12:29:16 +02:00
|
|
|
* Manually decrypt an OmemoElement.
|
|
|
|
* This method should only be used for use-cases, where the internal listeners don't pick up on an incoming message.
|
|
|
|
* (for example MAM query results).
|
2017-06-02 12:26:37 +02:00
|
|
|
*
|
2018-06-13 12:29:16 +02:00
|
|
|
* @param sender bareJid of the message sender (must be the jid of the contact who sent the message)
|
|
|
|
* @param omemoElement omemoElement
|
|
|
|
* @return decrypted OmemoMessage
|
|
|
|
*
|
|
|
|
* @throws SmackException.NotLoggedInException if the Manager is not authenticated
|
|
|
|
* @throws CorruptedOmemoKeyException if our or their key is corrupted
|
|
|
|
* @throws NoRawSessionException if the message was not a preKeyMessage, but we had no session with the contact
|
|
|
|
* @throws CryptoFailedException if decryption fails
|
2017-06-02 12:26:37 +02:00
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
public OmemoMessage.Received decrypt(BareJid sender, OmemoElement omemoElement)
|
|
|
|
throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, NoRawSessionException,
|
|
|
|
CryptoFailedException {
|
|
|
|
LoggedInOmemoManager managerGuard = new LoggedInOmemoManager(this);
|
|
|
|
return getOmemoService().decryptMessage(managerGuard, sender, omemoElement);
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-06-13 12:29:16 +02:00
|
|
|
* Decrypt messages from a MAM query.
|
2017-06-02 12:26:37 +02:00
|
|
|
*
|
2018-06-09 15:03:14 +02:00
|
|
|
* @param mamQuery The MAM query
|
2017-06-02 12:26:37 +02:00
|
|
|
* @return list of decrypted OmemoMessages
|
2018-06-13 12:29:16 +02:00
|
|
|
* @throws SmackException.NotLoggedInException if the Manager is not authenticated.
|
2017-06-02 12:26:37 +02:00
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
public List<MessageOrOmemoMessage> decryptMamQueryResult(MamManager.MamQuery mamQuery)
|
|
|
|
throws SmackException.NotLoggedInException {
|
|
|
|
return new ArrayList<>(getOmemoService().decryptMamQueryResult(new LoggedInOmemoManager(this), mamQuery));
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Trust that a fingerprint belongs to an OmemoDevice.
|
|
|
|
* The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must
|
|
|
|
* be of length 64.
|
2018-06-13 12:29:16 +02:00
|
|
|
*
|
2017-06-02 12:26:37 +02:00
|
|
|
* @param device device
|
|
|
|
* @param fingerprint fingerprint
|
|
|
|
*/
|
|
|
|
public void trustOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) {
|
2018-06-13 12:29:16 +02:00
|
|
|
if (trustCallback == null) {
|
|
|
|
throw new IllegalStateException("No TrustCallback set.");
|
|
|
|
}
|
|
|
|
|
|
|
|
trustCallback.setTrust(device, fingerprint, TrustState.trusted);
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Distrust the fingerprint/OmemoDevice tuple.
|
|
|
|
* The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must
|
|
|
|
* be of length 64.
|
|
|
|
* @param device device
|
|
|
|
* @param fingerprint fingerprint
|
|
|
|
*/
|
|
|
|
public void distrustOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) {
|
2018-06-13 12:29:16 +02:00
|
|
|
if (trustCallback == null) {
|
|
|
|
throw new IllegalStateException("No TrustCallback set.");
|
|
|
|
}
|
|
|
|
|
|
|
|
trustCallback.setTrust(device, fingerprint, TrustState.untrusted);
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true, if the fingerprint/OmemoDevice tuple is trusted, otherwise false.
|
|
|
|
* The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must
|
|
|
|
* be of length 64.
|
|
|
|
* @param device device
|
|
|
|
* @param fingerprint fingerprint
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
public boolean isTrustedOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) {
|
2018-06-13 12:29:16 +02:00
|
|
|
if (trustCallback == null) {
|
|
|
|
throw new IllegalStateException("No TrustCallback set.");
|
|
|
|
}
|
|
|
|
|
|
|
|
return trustCallback.getTrust(device, fingerprint) == TrustState.trusted;
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true, if the fingerprint/OmemoDevice tuple is decided by the user.
|
|
|
|
* The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must
|
|
|
|
* be of length 64.
|
|
|
|
* @param device device
|
|
|
|
* @param fingerprint fingerprint
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
public boolean isDecidedOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) {
|
2018-06-13 12:29:16 +02:00
|
|
|
if (trustCallback == null) {
|
|
|
|
throw new IllegalStateException("No TrustCallback set.");
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
return trustCallback.getTrust(device, fingerprint) != TrustState.undecided;
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Send a ratchet update message. This can be used to advance the ratchet of a session in order to maintain forward
|
|
|
|
* secrecy.
|
|
|
|
*
|
|
|
|
* @param recipient recipient
|
|
|
|
* @throws CorruptedOmemoKeyException When the used identityKeys are corrupted
|
|
|
|
* @throws CryptoFailedException When something fails with the crypto
|
|
|
|
* @throws CannotEstablishOmemoSessionException When we can't establish a session with the recipient
|
2018-06-13 12:29:16 +02:00
|
|
|
* @throws SmackException.NotLoggedInException
|
|
|
|
* @throws InterruptedException
|
|
|
|
* @throws SmackException.NoResponseException
|
|
|
|
* @throws NoSuchAlgorithmException
|
|
|
|
* @throws SmackException.NotConnectedException
|
2017-06-02 12:26:37 +02:00
|
|
|
*/
|
|
|
|
public void sendRatchetUpdateMessage(OmemoDevice recipient)
|
2018-06-13 12:29:16 +02:00
|
|
|
throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, InterruptedException,
|
|
|
|
SmackException.NoResponseException, NoSuchAlgorithmException, SmackException.NotConnectedException,
|
|
|
|
CryptoFailedException, CannotEstablishOmemoSessionException {
|
|
|
|
synchronized (LOCK) {
|
|
|
|
Message message = new Message();
|
|
|
|
message.setFrom(getOwnJid());
|
|
|
|
message.setTo(recipient.getJid());
|
|
|
|
|
|
|
|
OmemoElement element = getOmemoService()
|
|
|
|
.createRatchetUpdateElement(new LoggedInOmemoManager(this), recipient);
|
|
|
|
message.addExtension(element);
|
|
|
|
|
|
|
|
// Set MAM Storage hint
|
|
|
|
StoreHint.set(message);
|
|
|
|
connection().sendStanza(message);
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true, if the contact has any active devices published in a deviceList.
|
|
|
|
*
|
|
|
|
* @param contact contact
|
|
|
|
* @return true if contact has at least one OMEMO capable device.
|
|
|
|
* @throws SmackException.NotConnectedException
|
|
|
|
* @throws InterruptedException
|
|
|
|
* @throws SmackException.NoResponseException
|
2018-06-13 12:29:16 +02:00
|
|
|
* @throws PubSubException.NotALeafNodeException
|
|
|
|
* @throws XMPPException.XMPPErrorException
|
2017-06-02 12:26:37 +02:00
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
public boolean contactSupportsOmemo(BareJid contact)
|
|
|
|
throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException,
|
|
|
|
SmackException.NotConnectedException, SmackException.NoResponseException {
|
|
|
|
synchronized (LOCK) {
|
|
|
|
OmemoCachedDeviceList deviceList = getOmemoService().refreshDeviceList(connection(), getOwnDevice(), contact);
|
|
|
|
return !deviceList.getActiveDevices().isEmpty();
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true, if the MUC with the EntityBareJid multiUserChat is non-anonymous and members only (prerequisite
|
|
|
|
* for OMEMO encryption in MUC).
|
|
|
|
*
|
2018-06-13 12:29:16 +02:00
|
|
|
* @param multiUserChat MUC
|
2017-06-02 12:26:37 +02:00
|
|
|
* @return true if chat supports OMEMO
|
|
|
|
* @throws XMPPException.XMPPErrorException if
|
|
|
|
* @throws SmackException.NotConnectedException something
|
|
|
|
* @throws InterruptedException goes
|
|
|
|
* @throws SmackException.NoResponseException wrong
|
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
public boolean multiUserChatSupportsOmemo(MultiUserChat multiUserChat)
|
|
|
|
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
|
|
|
|
SmackException.NoResponseException {
|
|
|
|
EntityBareJid jid = multiUserChat.getRoom();
|
|
|
|
RoomInfo roomInfo = MultiUserChatManager.getInstanceFor(connection()).getRoomInfo(jid);
|
2017-06-02 12:26:37 +02:00
|
|
|
return roomInfo.isNonanonymous() && roomInfo.isMembersOnly();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true, if the Server supports PEP.
|
|
|
|
*
|
|
|
|
* @param connection XMPPConnection
|
|
|
|
* @param server domainBareJid of the server to test
|
|
|
|
* @return true if server supports pep
|
|
|
|
* @throws XMPPException.XMPPErrorException
|
|
|
|
* @throws SmackException.NotConnectedException
|
|
|
|
* @throws InterruptedException
|
|
|
|
* @throws SmackException.NoResponseException
|
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
public static boolean serverSupportsOmemo(XMPPConnection connection, DomainBareJid server)
|
|
|
|
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
|
|
|
|
SmackException.NoResponseException {
|
|
|
|
return ServiceDiscoveryManager.getInstanceFor(connection)
|
|
|
|
.discoverInfo(server).containsFeature(PubSub.NAMESPACE);
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the fingerprint of our identity key.
|
|
|
|
*
|
|
|
|
* @return fingerprint
|
2018-06-13 12:29:16 +02:00
|
|
|
* @throws SmackException.NotLoggedInException if we don't know our bareJid yet.
|
|
|
|
* @throws CorruptedOmemoKeyException if our identityKey is corrupted.
|
2017-06-02 12:26:37 +02:00
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
public OmemoFingerprint getOwnFingerprint()
|
|
|
|
throws SmackException.NotLoggedInException, CorruptedOmemoKeyException {
|
|
|
|
synchronized (LOCK) {
|
|
|
|
if (getOwnJid() == null) {
|
|
|
|
throw new SmackException.NotLoggedInException();
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
return getOmemoService().getOmemoStoreBackend().getFingerprint(getOwnDevice());
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
2018-06-13 12:29:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the fingerprint of a contacts device.
|
|
|
|
* @param device contacts OmemoDevice
|
|
|
|
* @return fingerprint
|
|
|
|
* @throws CannotEstablishOmemoSessionException if we have no session yet, and are unable to create one.
|
|
|
|
* @throws SmackException.NotLoggedInException
|
|
|
|
* @throws CorruptedOmemoKeyException if the copy of the fingerprint we have is corrupted.
|
|
|
|
* @throws SmackException.NotConnectedException
|
|
|
|
* @throws InterruptedException
|
|
|
|
* @throws SmackException.NoResponseException
|
|
|
|
*/
|
|
|
|
public OmemoFingerprint getFingerprint(OmemoDevice device)
|
|
|
|
throws CannotEstablishOmemoSessionException, SmackException.NotLoggedInException,
|
|
|
|
CorruptedOmemoKeyException, SmackException.NotConnectedException, InterruptedException,
|
|
|
|
SmackException.NoResponseException {
|
|
|
|
synchronized (LOCK) {
|
|
|
|
if (getOwnJid() == null) {
|
|
|
|
throw new SmackException.NotLoggedInException();
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
if (device.equals(getOwnDevice())) {
|
|
|
|
return getOwnFingerprint();
|
|
|
|
}
|
|
|
|
|
|
|
|
return getOmemoService().getOmemoStoreBackend().getFingerprintAndMaybeBuildSession(new LoggedInOmemoManager(this), device);
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-06-13 12:29:16 +02:00
|
|
|
* Return all OmemoFingerprints of active devices of a contact.
|
|
|
|
* TODO: Make more fail-safe
|
2017-06-02 12:26:37 +02:00
|
|
|
* @param contact contact
|
2018-06-13 12:29:16 +02:00
|
|
|
* @return Map of all active devices of the contact and their fingerprints.
|
|
|
|
*
|
|
|
|
* @throws SmackException.NotLoggedInException
|
|
|
|
* @throws CorruptedOmemoKeyException
|
|
|
|
* @throws CannotEstablishOmemoSessionException
|
|
|
|
* @throws SmackException.NotConnectedException
|
|
|
|
* @throws InterruptedException
|
|
|
|
* @throws SmackException.NoResponseException
|
|
|
|
*/
|
|
|
|
public HashMap<OmemoDevice, OmemoFingerprint> getActiveFingerprints(BareJid contact)
|
|
|
|
throws SmackException.NotLoggedInException, CorruptedOmemoKeyException,
|
|
|
|
CannotEstablishOmemoSessionException, SmackException.NotConnectedException, InterruptedException,
|
|
|
|
SmackException.NoResponseException {
|
|
|
|
synchronized (LOCK) {
|
|
|
|
if (getOwnJid() == null) {
|
|
|
|
throw new SmackException.NotLoggedInException();
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
HashMap<OmemoDevice, OmemoFingerprint> fingerprints = new HashMap<>();
|
|
|
|
OmemoCachedDeviceList deviceList = getOmemoService().getOmemoStoreBackend()
|
|
|
|
.loadCachedDeviceList(getOwnDevice(), contact);
|
|
|
|
|
|
|
|
for (int id : deviceList.getActiveDevices()) {
|
|
|
|
OmemoDevice device = new OmemoDevice(contact, id);
|
|
|
|
OmemoFingerprint fingerprint = getFingerprint(device);
|
|
|
|
|
|
|
|
if (fingerprint != null) {
|
|
|
|
fingerprints.put(device, fingerprint);
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
2018-06-13 12:29:16 +02:00
|
|
|
|
|
|
|
return fingerprints;
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
/**
|
|
|
|
* Add an OmemoMessageListener. This listener will be informed about incoming OMEMO messages
|
|
|
|
* (as well as KeyTransportMessages) and OMEMO encrypted message carbons.
|
|
|
|
*
|
|
|
|
* @param listener OmemoMessageListener
|
|
|
|
*/
|
2017-06-02 12:26:37 +02:00
|
|
|
public void addOmemoMessageListener(OmemoMessageListener listener) {
|
|
|
|
omemoMessageListeners.add(listener);
|
|
|
|
}
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
/**
|
|
|
|
* Remove an OmemoMessageListener.
|
|
|
|
* @param listener OmemoMessageListener
|
|
|
|
*/
|
2017-06-02 12:26:37 +02:00
|
|
|
public void removeOmemoMessageListener(OmemoMessageListener listener) {
|
|
|
|
omemoMessageListeners.remove(listener);
|
|
|
|
}
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
/**
|
|
|
|
* Add an OmemoMucMessageListener. This listener will be informed about incoming OMEMO encrypted MUC messages.
|
|
|
|
*
|
|
|
|
* @param listener OmemoMessageListener.
|
|
|
|
*/
|
2017-06-02 12:26:37 +02:00
|
|
|
public void addOmemoMucMessageListener(OmemoMucMessageListener listener) {
|
|
|
|
omemoMucMessageListeners.add(listener);
|
|
|
|
}
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
/**
|
|
|
|
* Remove an OmemoMucMessageListener.
|
|
|
|
* @param listener OmemoMucMessageListener
|
|
|
|
*/
|
2017-06-02 12:26:37 +02:00
|
|
|
public void removeOmemoMucMessageListener(OmemoMucMessageListener listener) {
|
|
|
|
omemoMucMessageListeners.remove(listener);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-06-13 12:29:16 +02:00
|
|
|
* Request a deviceList update from contact contact.
|
2017-06-02 12:26:37 +02:00
|
|
|
*
|
2018-06-13 12:29:16 +02:00
|
|
|
* @param contact contact we want to obtain the deviceList from.
|
2017-06-02 12:26:37 +02:00
|
|
|
* @throws InterruptedException
|
2018-06-13 12:29:16 +02:00
|
|
|
* @throws PubSubException.NotALeafNodeException
|
|
|
|
* @throws XMPPException.XMPPErrorException
|
2017-06-02 12:26:37 +02:00
|
|
|
* @throws SmackException.NotConnectedException
|
|
|
|
* @throws SmackException.NoResponseException
|
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
public void requestDeviceListUpdateFor(BareJid contact)
|
|
|
|
throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException,
|
|
|
|
SmackException.NotConnectedException, SmackException.NoResponseException {
|
|
|
|
synchronized (LOCK) {
|
|
|
|
getOmemoService().refreshDeviceList(connection(), getOwnDevice(), contact);
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-06-13 12:29:16 +02:00
|
|
|
* Publish a new device list with just our own deviceId in it.
|
2017-06-02 12:26:37 +02:00
|
|
|
*
|
2018-06-13 12:29:16 +02:00
|
|
|
* @throws SmackException.NotLoggedInException
|
2017-06-02 12:26:37 +02:00
|
|
|
* @throws InterruptedException
|
2018-06-13 12:29:16 +02:00
|
|
|
* @throws XMPPException.XMPPErrorException
|
|
|
|
* @throws SmackException.NotConnectedException
|
2017-06-02 12:26:37 +02:00
|
|
|
* @throws SmackException.NoResponseException
|
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
public void purgeDeviceList()
|
|
|
|
throws SmackException.NotLoggedInException, InterruptedException, XMPPException.XMPPErrorException,
|
|
|
|
SmackException.NotConnectedException, SmackException.NoResponseException {
|
|
|
|
synchronized (LOCK) {
|
|
|
|
getOmemoService().purgeDeviceList(new LoggedInOmemoManager(this));
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-06-13 12:29:16 +02:00
|
|
|
* Rotate the signedPreKey published in our OmemoBundle and republish it. This should be done every now and
|
|
|
|
* then (7-14 days). The old signedPreKey should be kept for some more time (a month or so) to enable decryption
|
|
|
|
* of messages that have been sent since the key was changed.
|
2017-06-02 12:26:37 +02:00
|
|
|
*
|
|
|
|
* @throws CorruptedOmemoKeyException When the IdentityKeyPair is damaged.
|
|
|
|
* @throws InterruptedException XMPP error
|
|
|
|
* @throws XMPPException.XMPPErrorException XMPP error
|
|
|
|
* @throws SmackException.NotConnectedException XMPP error
|
|
|
|
* @throws SmackException.NoResponseException XMPP error
|
2018-06-13 12:29:16 +02:00
|
|
|
* @throws SmackException.NotLoggedInException
|
2017-06-02 12:26:37 +02:00
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
public void rotateSignedPreKey()
|
|
|
|
throws CorruptedOmemoKeyException, SmackException.NotLoggedInException, XMPPException.XMPPErrorException,
|
|
|
|
SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
|
|
|
|
synchronized (LOCK) {
|
|
|
|
if (!connection().isAuthenticated()) {
|
|
|
|
throw new SmackException.NotLoggedInException();
|
|
|
|
}
|
|
|
|
|
|
|
|
// generate key
|
|
|
|
getOmemoService().getOmemoStoreBackend().changeSignedPreKey(getOwnDevice());
|
|
|
|
|
|
|
|
// publish
|
|
|
|
OmemoBundleElement bundle = getOmemoService().getOmemoStoreBackend().packOmemoBundle(getOwnDevice());
|
|
|
|
OmemoService.publishBundle(connection(), getOwnDevice(), bundle);
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return true, if the given Stanza contains an OMEMO element 'encrypted'.
|
|
|
|
* @param stanza stanza
|
|
|
|
* @return true if stanza has extension 'encrypted'
|
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
static boolean stanzaContainsOmemoElement(Stanza stanza) {
|
|
|
|
return stanza.hasExtension(OmemoElement.NAME_ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL);
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Throw an IllegalStateException if no OmemoService is set.
|
|
|
|
*/
|
|
|
|
private void throwIfNoServiceSet() {
|
|
|
|
if (service == null) {
|
|
|
|
throw new IllegalStateException("No OmemoService set in OmemoManager.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
/**
|
|
|
|
* Returns a pseudo random number from the interval [1, Integer.MAX_VALUE].
|
|
|
|
* @return deviceId
|
|
|
|
*/
|
2017-06-02 12:26:37 +02:00
|
|
|
public static int randomDeviceId() {
|
2018-06-13 12:29:16 +02:00
|
|
|
return new Random().nextInt(Integer.MAX_VALUE - 1) + 1;
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the BareJid of the user.
|
|
|
|
*
|
|
|
|
* @return bareJid
|
|
|
|
*/
|
|
|
|
public BareJid getOwnJid() {
|
2018-06-13 12:29:16 +02:00
|
|
|
if (ownJid == null && connection().isAuthenticated()) {
|
|
|
|
ownJid = connection().getUser().asBareJid();
|
|
|
|
}
|
|
|
|
|
|
|
|
return ownJid;
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the deviceId of this OmemoManager.
|
|
|
|
*
|
|
|
|
* @return deviceId
|
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
public Integer getDeviceId() {
|
|
|
|
synchronized (LOCK) {
|
|
|
|
return deviceId;
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the OmemoDevice of the user.
|
|
|
|
*
|
|
|
|
* @return omemoDevice
|
|
|
|
*/
|
|
|
|
public OmemoDevice getOwnDevice() {
|
2018-06-13 12:29:16 +02:00
|
|
|
synchronized (LOCK) {
|
|
|
|
BareJid jid = getOwnJid();
|
|
|
|
if (jid == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return new OmemoDevice(jid, getDeviceId());
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
/**
|
|
|
|
* Set the deviceId of the manager to nDeviceId.
|
|
|
|
* @param nDeviceId new deviceId
|
|
|
|
*/
|
2017-06-02 12:26:37 +02:00
|
|
|
void setDeviceId(int nDeviceId) {
|
2018-06-13 12:29:16 +02:00
|
|
|
synchronized (LOCK) {
|
|
|
|
// Move this instance inside the HashMaps
|
|
|
|
INSTANCES.get(connection()).remove(getDeviceId());
|
|
|
|
INSTANCES.get(connection()).put(nDeviceId, this);
|
|
|
|
|
|
|
|
this.deviceId = nDeviceId;
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Notify all registered OmemoMessageListeners about a received OmemoMessage.
|
|
|
|
*
|
2018-06-13 12:29:16 +02:00
|
|
|
* @param stanza original stanza
|
|
|
|
* @param decryptedMessage decrypted OmemoMessage.
|
2017-06-02 12:26:37 +02:00
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
void notifyOmemoMessageReceived(Stanza stanza, OmemoMessage.Received decryptedMessage) {
|
2017-06-02 12:26:37 +02:00
|
|
|
for (OmemoMessageListener l : omemoMessageListeners) {
|
2018-06-13 12:29:16 +02:00
|
|
|
l.onOmemoMessageReceived(stanza, decryptedMessage);
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Notify all registered OmemoMucMessageListeners of an incoming OmemoMessageElement in a MUC.
|
|
|
|
*
|
2018-06-13 12:29:16 +02:00
|
|
|
* @param muc MultiUserChat the message was received in.
|
|
|
|
* @param stanza Original Stanza.
|
|
|
|
* @param decryptedMessage Decrypted OmemoMessage.
|
2017-06-02 12:26:37 +02:00
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
void notifyOmemoMucMessageReceived(MultiUserChat muc,
|
|
|
|
Stanza stanza,
|
|
|
|
OmemoMessage.Received decryptedMessage) {
|
2017-06-02 12:26:37 +02:00
|
|
|
for (OmemoMucMessageListener l : omemoMucMessageListeners) {
|
2018-06-13 12:29:16 +02:00
|
|
|
l.onOmemoMucMessageReceived(muc, stanza, decryptedMessage);
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
/**
|
|
|
|
* Notify all registered OmemoMessageListeners of an incoming OMEMO encrypted Carbon Copy.
|
|
|
|
* Remember: If you want to receive OMEMO encrypted carbon copies, you have to enable carbons using
|
|
|
|
* {@link CarbonManager#enableCarbons()}.
|
|
|
|
*
|
|
|
|
* @param direction direction of the carbon copy
|
|
|
|
* @param carbonCopy carbon copy itself
|
|
|
|
* @param wrappingMessage wrapping message
|
|
|
|
* @param decryptedCarbonCopy decrypted carbon copy OMEMO element
|
|
|
|
*/
|
|
|
|
void notifyOmemoCarbonCopyReceived(CarbonExtension.Direction direction,
|
|
|
|
Message carbonCopy,
|
|
|
|
Message wrappingMessage,
|
|
|
|
OmemoMessage.Received decryptedCarbonCopy) {
|
|
|
|
for (OmemoMessageListener l : omemoMessageListeners) {
|
|
|
|
l.onOmemoCarbonCopyReceived(direction, carbonCopy, wrappingMessage, decryptedCarbonCopy);
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-06-13 12:29:16 +02:00
|
|
|
* Register stanza listeners needed for OMEMO.
|
|
|
|
* This method is called automatically in the constructor and should only be used to restore the previous state
|
|
|
|
* after {@link #stopStanzaAndPEPListeners()} was called.
|
2017-06-02 12:26:37 +02:00
|
|
|
*/
|
2018-06-13 12:29:16 +02:00
|
|
|
public void resumeStanzaAndPEPListeners() {
|
|
|
|
PEPManager pepManager = PEPManager.getInstanceFor(connection());
|
|
|
|
CarbonManager carbonManager = CarbonManager.getInstanceFor(connection());
|
|
|
|
|
|
|
|
// Remove listeners to avoid them getting added twice
|
|
|
|
connection().removeAsyncStanzaListener(internalOmemoMessageStanzaListener);
|
|
|
|
carbonManager.removeCarbonCopyReceivedListener(internalOmemoCarbonCopyListener);
|
|
|
|
pepManager.removePEPListener(deviceListUpdateListener);
|
|
|
|
|
|
|
|
// Add listeners
|
|
|
|
pepManager.addPEPListener(deviceListUpdateListener);
|
|
|
|
connection().addAsyncStanzaListener(internalOmemoMessageStanzaListener, omemoMessageStanzaFilter);
|
|
|
|
carbonManager.addCarbonCopyReceivedListener(internalOmemoCarbonCopyListener);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove active stanza listeners needed for OMEMO.
|
|
|
|
*/
|
|
|
|
public void stopStanzaAndPEPListeners() {
|
2017-06-02 12:26:37 +02:00
|
|
|
PEPManager.getInstanceFor(connection()).removePEPListener(deviceListUpdateListener);
|
2018-06-13 12:29:16 +02:00
|
|
|
connection().removeAsyncStanzaListener(internalOmemoMessageStanzaListener);
|
|
|
|
CarbonManager.getInstanceFor(connection()).removeCarbonCopyReceivedListener(internalOmemoCarbonCopyListener);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Build a fresh session with a contacts device.
|
|
|
|
* This might come in handy if a session is broken.
|
|
|
|
*
|
|
|
|
* @param contactsDevice OmemoDevice of a contact.
|
|
|
|
*
|
|
|
|
* @throws InterruptedException
|
|
|
|
* @throws SmackException.NoResponseException
|
|
|
|
* @throws CorruptedOmemoKeyException if our or their identityKey is corrupted.
|
|
|
|
* @throws SmackException.NotConnectedException
|
|
|
|
* @throws CannotEstablishOmemoSessionException if no new session can be established.
|
|
|
|
* @throws SmackException.NotLoggedInException if the connection is not authenticated.
|
|
|
|
*/
|
|
|
|
public void rebuildSessionWith(OmemoDevice contactsDevice)
|
|
|
|
throws InterruptedException, SmackException.NoResponseException, CorruptedOmemoKeyException,
|
|
|
|
SmackException.NotConnectedException, CannotEstablishOmemoSessionException,
|
|
|
|
SmackException.NotLoggedInException {
|
|
|
|
if (!connection().isAuthenticated()) {
|
|
|
|
throw new SmackException.NotLoggedInException();
|
|
|
|
}
|
|
|
|
getOmemoService().buildFreshSessionWithDevice(connection(), getOwnDevice(), contactsDevice);
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get our connection.
|
|
|
|
*
|
|
|
|
* @return the connection of this manager
|
|
|
|
*/
|
|
|
|
XMPPConnection getConnection() {
|
|
|
|
return connection();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the OMEMO service object.
|
|
|
|
*
|
|
|
|
* @return omemoService
|
|
|
|
*/
|
|
|
|
OmemoService<?,?,?,?,?,?,?,?,?> getOmemoService() {
|
|
|
|
throwIfNoServiceSet();
|
|
|
|
return service;
|
|
|
|
}
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
/**
|
|
|
|
* StanzaListener that listens for incoming Stanzas which contain OMEMO elements.
|
|
|
|
*/
|
|
|
|
private final StanzaListener internalOmemoMessageStanzaListener = new StanzaListener() {
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void processStanza(final Stanza packet) {
|
|
|
|
Async.go(new Runnable() {
|
|
|
|
@Override
|
|
|
|
public void run() {
|
|
|
|
try {
|
|
|
|
getOmemoService().onOmemoMessageStanzaReceived(packet,
|
|
|
|
new LoggedInOmemoManager(OmemoManager.this));
|
|
|
|
} catch (SmackException.NotLoggedInException e) {
|
|
|
|
LOGGER.warning("Received OMEMO stanza while being offline: " + e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* CarbonCopyListener that listens for incoming carbon copies which contain OMEMO elements.
|
|
|
|
*/
|
|
|
|
private final CarbonCopyReceivedListener internalOmemoCarbonCopyListener = new CarbonCopyReceivedListener() {
|
|
|
|
@Override
|
|
|
|
public void onCarbonCopyReceived(final CarbonExtension.Direction direction,
|
|
|
|
final Message carbonCopy,
|
|
|
|
final Message wrappingMessage) {
|
|
|
|
Async.go(new Runnable() {
|
|
|
|
@Override
|
|
|
|
public void run() {
|
|
|
|
if (omemoMessageStanzaFilter.accept(carbonCopy)) {
|
|
|
|
try {
|
|
|
|
getOmemoService().onOmemoCarbonCopyReceived(direction, carbonCopy, wrappingMessage,
|
|
|
|
new LoggedInOmemoManager(OmemoManager.this));
|
|
|
|
} catch (SmackException.NotLoggedInException e) {
|
|
|
|
LOGGER.warning("Received OMEMO carbon copy while being offline: " + e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* PEPListener that listens for OMEMO deviceList updates.
|
|
|
|
*/
|
|
|
|
private final PEPListener deviceListUpdateListener = new PEPListener() {
|
2017-06-02 12:26:37 +02:00
|
|
|
@Override
|
|
|
|
public void eventReceived(EntityBareJid from, EventElement event, Message message) {
|
2018-06-13 12:29:16 +02:00
|
|
|
|
|
|
|
// Unknown sender, no more work to do.
|
|
|
|
if (from == null) {
|
|
|
|
// TODO: This DOES happen for some reason. Figure out when...
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-06-02 12:26:37 +02:00
|
|
|
for (ExtensionElement items : event.getExtensions()) {
|
|
|
|
if (!(items instanceof ItemsExtension)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
for (ExtensionElement item : ((ItemsExtension) items).getExtensions()) {
|
2017-06-02 12:26:37 +02:00
|
|
|
if (!(item instanceof PayloadItem<?>)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
PayloadItem<?> payloadItem = (PayloadItem<?>) item;
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
if (!(payloadItem.getPayload() instanceof OmemoDeviceListElement)) {
|
2017-06-02 12:26:37 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2017-11-20 08:53:19 +01:00
|
|
|
// Device List <list>
|
2018-06-13 12:29:16 +02:00
|
|
|
OmemoDeviceListElement receivedDeviceList = (OmemoDeviceListElement) payloadItem.getPayload();
|
|
|
|
getOmemoService().getOmemoStoreBackend().mergeCachedDeviceList(getOwnDevice(), from, receivedDeviceList);
|
2017-06-02 12:26:37 +02:00
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
if (!from.asBareJid().equals(getOwnJid())) {
|
2017-06-02 12:26:37 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
OmemoCachedDeviceList deviceList = getOmemoService().cleanUpDeviceList(getOwnDevice());
|
|
|
|
final OmemoDeviceListElement_VAxolotl newDeviceList = new OmemoDeviceListElement_VAxolotl(deviceList);
|
|
|
|
|
|
|
|
if (!newDeviceList.copyDeviceIds().equals(receivedDeviceList.copyDeviceIds())) {
|
|
|
|
LOGGER.log(Level.FINE, "Republish deviceList due to changes:" +
|
|
|
|
" Received: " + Arrays.toString(receivedDeviceList.copyDeviceIds().toArray()) +
|
|
|
|
" Published: " + Arrays.toString(newDeviceList.copyDeviceIds().toArray()));
|
|
|
|
Async.go(new Runnable() {
|
|
|
|
@Override
|
|
|
|
public void run() {
|
|
|
|
try {
|
|
|
|
OmemoService.publishDeviceList(connection(), newDeviceList);
|
|
|
|
} catch (InterruptedException | XMPPException.XMPPErrorException |
|
|
|
|
SmackException.NotConnectedException | SmackException.NoResponseException e) {
|
|
|
|
LOGGER.log(Level.WARNING, "Could not publish our deviceList upon an received update.", e);
|
|
|
|
}
|
2017-09-29 15:43:29 +02:00
|
|
|
}
|
2018-06-13 12:29:16 +02:00
|
|
|
});
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
/**
|
|
|
|
* StanzaFilter that filters messages containing a OMEMO element.
|
|
|
|
*/
|
|
|
|
private final StanzaFilter omemoMessageStanzaFilter = new StanzaFilter() {
|
|
|
|
@Override
|
|
|
|
public boolean accept(Stanza stanza) {
|
|
|
|
return stanza instanceof Message && OmemoManager.stanzaContainsOmemoElement(stanza);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Guard class which ensures that the wrapped OmemoManager knows its BareJid.
|
|
|
|
*/
|
|
|
|
public static class LoggedInOmemoManager {
|
|
|
|
|
|
|
|
private final OmemoManager manager;
|
|
|
|
|
|
|
|
public LoggedInOmemoManager(OmemoManager manager)
|
|
|
|
throws SmackException.NotLoggedInException {
|
|
|
|
|
|
|
|
if (manager == null) {
|
|
|
|
throw new IllegalArgumentException("OmemoManager cannot be null.");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (manager.getOwnJid() == null) {
|
|
|
|
if (manager.getConnection().isAuthenticated()) {
|
|
|
|
manager.ownJid = manager.getConnection().getUser().asBareJid();
|
|
|
|
} else {
|
|
|
|
throw new SmackException.NotLoggedInException();
|
|
|
|
}
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
this.manager = manager;
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
public OmemoManager get() {
|
|
|
|
return manager;
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-13 12:29:16 +02:00
|
|
|
/**
|
|
|
|
* Callback which can be used to get notified, when the OmemoManager finished initializing.
|
|
|
|
*/
|
|
|
|
public interface InitializationFinishedCallback {
|
|
|
|
|
|
|
|
void initializationFinished(OmemoManager manager);
|
|
|
|
|
|
|
|
void initializationFailed(Exception cause);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the bareJid of the user from the authenticated XMPP connection.
|
|
|
|
* If our deviceId is unknown, use the bareJid to look up deviceIds available in the omemoStore.
|
|
|
|
* If there are ids available, choose the smallest one. Otherwise generate a random deviceId.
|
|
|
|
*
|
|
|
|
* @param manager OmemoManager
|
|
|
|
*/
|
|
|
|
private static void initBareJidAndDeviceId(OmemoManager manager) {
|
|
|
|
if (!manager.getConnection().isAuthenticated()) {
|
|
|
|
throw new IllegalStateException("Connection MUST be authenticated.");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (manager.ownJid == null) {
|
|
|
|
manager.ownJid = manager.getConnection().getUser().asBareJid();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (UNKNOWN_DEVICE_ID.equals(manager.deviceId)) {
|
|
|
|
SortedSet<Integer> storedDeviceIds = manager.getOmemoService().getOmemoStoreBackend().localDeviceIdsOf(manager.ownJid);
|
|
|
|
if (storedDeviceIds.size() > 0) {
|
|
|
|
manager.setDeviceId(storedDeviceIds.first());
|
|
|
|
} else {
|
|
|
|
manager.setDeviceId(randomDeviceId());
|
|
|
|
}
|
2017-06-02 12:26:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|