From 2db7717abc44a50fe2489735a39bcd6ba51a7590 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 31 Dec 2017 16:30:27 +0100 Subject: [PATCH] Temp commit --- .../smackx/omemo/OmemoManagerSetupHelper.java | 4 +- .../smack/omemo/OmemoMessageBuilderTest.java | 91 - .../omemo/LegacySignalOmemoKeyUtilTest.java | 2 +- .../omemo/SignalOmemoKeyUtilTest.java | 2 +- .../omemo/SignalOmemoManagerTest.java} | 7 +- .../omemo/SignalOmemoStoreConnectorTest.java | 2 +- .../omemo/SignalOmemoStoreTest.java | 3 +- .../smackx/omemo/CachingOmemoStore.java | 14 +- .../smackx/omemo/FileBasedOmemoStore.java | 8 +- .../smackx/omemo/OmemoConfiguration.java | 2 + .../smackx/omemo/OmemoManager.java | 371 +--- .../smackx/omemo/OmemoMessage.java | 128 ++ .../smackx/omemo/OmemoRatchet.java | 3 - .../smackx/omemo/OmemoService.java | 1766 ++++++----------- .../smackx/omemo/OmemoServiceOld.java | 1323 ++++++++++++ .../jivesoftware/smackx/omemo/OmemoStore.java | 50 +- .../omemo/element/OmemoDeviceListElement.java | 5 + .../OmemoDeviceListElement_VAxolotl.java | 6 + .../omemo/element/OmemoHeaderElement.java | 7 +- .../element/OmemoHeaderElement_VAxolotl.java | 4 +- .../smackx/omemo/element/OmemoKeyElement.java | 2 +- .../CannotEstablishOmemoSessionException.java | 4 - .../exceptions/CryptoFailedException.java | 11 + ...ption.java => NoIdentityKeyException.java} | 21 +- .../UndecidedOmemoIdentityException.java | 6 + .../UntrustedOmemoIdentityException.java | 19 +- .../omemo/internal/ClearTextMessage.java | 63 - ...ceList.java => OmemoCachedDeviceList.java} | 13 +- .../smackx/omemo/internal/OmemoDevice.java | 10 + .../omemo/util/OmemoMessageBuilder.java | 181 +- .../omemo/DeviceListTest.java | 6 +- .../omemo/OmemoBundleVAxolotlElementTest.java | 2 +- .../omemo/OmemoConfigurationTest.java | 2 +- .../OmemoDeviceListVAxolotlElementTest.java | 2 +- .../omemo/OmemoDeviceTest.java | 2 +- .../omemo/OmemoExceptionsTest.java | 2 +- .../omemo/OmemoFingerprintTest.java | 2 +- .../omemo/OmemoKeyUtilTest.java | 2 +- .../omemo/OmemoMessageInformationTest.java | 2 +- .../smackx/omemo/OmemoServiceTest.java | 61 + .../omemo/OmemoStoreTest.java | 12 +- .../omemo/OmemoVAxolotlElementTest.java | 2 +- .../omemo/WrapperObjectsTest.java | 2 +- .../omemo/util/EphemeralTrustCallback.java | 2 +- .../omemo/util/OmemoMessageBuilderTest.java | 79 + 45 files changed, 2587 insertions(+), 1721 deletions(-) delete mode 100644 smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoMessageBuilderTest.java rename smack-omemo-signal/src/test/java/org/jivesoftware/{smack => smackx}/omemo/LegacySignalOmemoKeyUtilTest.java (99%) rename smack-omemo-signal/src/test/java/org/jivesoftware/{smack => smackx}/omemo/SignalOmemoKeyUtilTest.java (98%) rename smack-omemo-signal/src/test/java/org/jivesoftware/{smack/omemo/OmemoManagerTest.java => smackx/omemo/SignalOmemoManagerTest.java} (96%) rename smack-omemo-signal/src/test/java/org/jivesoftware/{smack => smackx}/omemo/SignalOmemoStoreConnectorTest.java (97%) rename smack-omemo-signal/src/test/java/org/jivesoftware/{smack => smackx}/omemo/SignalOmemoStoreTest.java (97%) create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessage.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoServiceOld.java rename smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/{MultipleIOException.java => NoIdentityKeyException.java} (59%) delete mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/ClearTextMessage.java rename smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/{CachedDeviceList.java => OmemoCachedDeviceList.java} (91%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/DeviceListTest.java (92%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/OmemoBundleVAxolotlElementTest.java (99%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/OmemoConfigurationTest.java (99%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/OmemoDeviceListVAxolotlElementTest.java (98%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/OmemoDeviceTest.java (98%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/OmemoExceptionsTest.java (99%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/OmemoFingerprintTest.java (96%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/OmemoKeyUtilTest.java (99%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/OmemoMessageInformationTest.java (97%) create mode 100644 smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoServiceTest.java rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/OmemoStoreTest.java (97%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/OmemoVAxolotlElementTest.java (98%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/WrapperObjectsTest.java (98%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/util/EphemeralTrustCallback.java (97%) create mode 100644 smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilderTest.java diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java index 96a629be2..f9c6feb33 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java @@ -32,7 +32,7 @@ import org.jivesoftware.smack.roster.Roster; import org.jivesoftware.smack.roster.RosterEntry; import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; -import org.jivesoftware.smackx.omemo.internal.CachedDeviceList; +import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; import org.jivesoftware.smackx.omemo.util.OmemoConstants; @@ -185,7 +185,7 @@ public class OmemoManagerSetupHelper { // ignore } - CachedDeviceList deviceList = OmemoService.getInstance().getOmemoStoreBackend() + OmemoCachedDeviceList deviceList = OmemoService.getInstance().getOmemoStoreBackend() .loadCachedDeviceList(omemoManager.getOwnDevice(), omemoManager.getOwnJid()); for (int id : deviceList.getAllDevices()) { diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoMessageBuilderTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoMessageBuilderTest.java deleted file mode 100644 index a8d3b3e19..000000000 --- a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoMessageBuilderTest.java +++ /dev/null @@ -1,91 +0,0 @@ -/** - * - * Copyright 2017 Paul Schaub - * - * This file is part of smack-omemo-signal. - * - * smack-omemo-signal is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ -package org.jivesoftware.smack.omemo; - -import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.CIPHERMODE; -import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE; -import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.PROVIDER; -import static org.junit.Assert.assertArrayEquals; - -import java.io.UnsupportedEncodingException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.Security; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -import org.jivesoftware.smack.test.util.SmackTestSuite; -import org.jivesoftware.smack.util.StringUtils; - -import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.junit.Test; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.SessionCipher; -import org.whispersystems.libsignal.SignalProtocolAddress; -import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.state.PreKeyBundle; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SessionRecord; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; - -/** - * Test the OmemoMessageBuilder. - */ -public class OmemoMessageBuilderTest extends SmackTestSuite { - - @Test - public void setTextTest() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, UnsupportedEncodingException, IllegalBlockSizeException, BadPaddingException, NoSuchProviderException, InvalidKeyException { - Security.addProvider(new BouncyCastleProvider()); - String message = "Hello World!"; - byte[] key = OmemoMessageBuilder.generateKey(); - byte[] iv = OmemoMessageBuilder.generateIv(); - - SecretKey secretKey = new SecretKeySpec(key, KEYTYPE); - IvParameterSpec ivSpec = new IvParameterSpec(iv); - Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER); - cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); - - OmemoMessageBuilder - mb = new OmemoMessageBuilder<>(null, null, key, iv); - mb.setMessage(message); - - byte[] expected = cipher.doFinal(message.getBytes(StringUtils.UTF8)); - byte[] messageKey = new byte[16]; - System.arraycopy(mb.getMessageKey(),0, messageKey, 0, 16); - byte[] messagePlusTag = new byte[mb.getCiphertextMessage().length + 16]; - System.arraycopy(mb.getCiphertextMessage(),0,messagePlusTag,0,mb.getCiphertextMessage().length); - System.arraycopy(mb.getMessageKey(), 16, messagePlusTag, mb.getCiphertextMessage().length, 16); - - assertArrayEquals(key, messageKey); - assertArrayEquals(expected, messagePlusTag); - } -} diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/LegacySignalOmemoKeyUtilTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/LegacySignalOmemoKeyUtilTest.java similarity index 99% rename from smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/LegacySignalOmemoKeyUtilTest.java rename to smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/LegacySignalOmemoKeyUtilTest.java index e27592626..ce3839231 100644 --- a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/LegacySignalOmemoKeyUtilTest.java +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/LegacySignalOmemoKeyUtilTest.java @@ -18,7 +18,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jivesoftware.smack.omemo; +package org.jivesoftware.smackx.omemo; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertNotNull; diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoKeyUtilTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoKeyUtilTest.java similarity index 98% rename from smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoKeyUtilTest.java rename to smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoKeyUtilTest.java index a296b5681..c9766ccdc 100644 --- a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoKeyUtilTest.java +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoKeyUtilTest.java @@ -18,7 +18,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jivesoftware.smack.omemo; +package org.jivesoftware.smackx.omemo; import java.io.IOException; import java.util.Arrays; diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoManagerTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoManagerTest.java similarity index 96% rename from smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoManagerTest.java rename to smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoManagerTest.java index 1308782fe..64fd6ae0d 100644 --- a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoManagerTest.java +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoManagerTest.java @@ -18,7 +18,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jivesoftware.smack.omemo; +package org.jivesoftware.smackx.omemo; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertFalse; @@ -31,7 +31,6 @@ import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; - import javax.crypto.BadPaddingException; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; @@ -42,8 +41,6 @@ import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.test.util.SmackTestSuite; import org.jivesoftware.smack.test.util.TestUtils; - -import org.jivesoftware.smackx.omemo.OmemoManager; import org.jivesoftware.smackx.omemo.element.OmemoElement; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; import org.jivesoftware.smackx.omemo.provider.OmemoVAxolotlProvider; @@ -54,7 +51,7 @@ import org.junit.Test; /** * Test OmemoManager functionality. */ -public class OmemoManagerTest extends SmackTestSuite { +public class SignalOmemoManagerTest extends SmackTestSuite { @Test public void instantiationTest() diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreConnectorTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreConnectorTest.java similarity index 97% rename from smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreConnectorTest.java rename to smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreConnectorTest.java index 496d4a488..cffa0b621 100644 --- a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreConnectorTest.java +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreConnectorTest.java @@ -18,7 +18,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jivesoftware.smack.omemo; +package org.jivesoftware.smackx.omemo; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertTrue; diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreTest.java similarity index 97% rename from smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreTest.java rename to smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreTest.java index 6507d70d3..c39f72fa7 100644 --- a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreTest.java +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreTest.java @@ -18,7 +18,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jivesoftware.smack.omemo; +package org.jivesoftware.smackx.omemo; import static org.junit.Assert.assertTrue; @@ -26,7 +26,6 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collection; -import org.jivesoftware.smackx.omemo.OmemoStore; import org.jivesoftware.smackx.omemo.signal.SignalCachingOmemoStore; import org.jivesoftware.smackx.omemo.signal.SignalFileBasedOmemoStore; import org.jivesoftware.smackx.omemo.signal.SignalOmemoKeyUtil; diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/CachingOmemoStore.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/CachingOmemoStore.java index 92b67111a..944db6eb4 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/CachingOmemoStore.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/CachingOmemoStore.java @@ -23,7 +23,7 @@ import java.util.TreeMap; import java.util.TreeSet; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; -import org.jivesoftware.smackx.omemo.internal.CachedDeviceList; +import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil; @@ -345,8 +345,8 @@ public class CachingOmemoStore> sessions = new HashMap<>(); private final HashMap identityKeys = new HashMap<>(); private final HashMap lastMessagesDates = new HashMap<>(); - private final HashMap deviceLists = new HashMap<>(); + private final HashMap deviceLists = new HashMap<>(); private Date lastRenewalDate = null; } } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/FileBasedOmemoStore.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/FileBasedOmemoStore.java index 28128f135..430701d77 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/FileBasedOmemoStore.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/FileBasedOmemoStore.java @@ -36,7 +36,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; -import org.jivesoftware.smackx.omemo.internal.CachedDeviceList; +import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jxmpp.jid.BareJid; @@ -345,8 +345,8 @@ public abstract class FileBasedOmemoStore getDevicesOf(BareJid recipient) { + OmemoCachedDeviceList list = getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(getOwnDevice(), recipient); + ArrayList devices = new ArrayList<>(); + + for (int deviceId : list.getActiveDevices()) { + devices.add(new OmemoDevice(recipient, deviceId)); + } + + return devices; + } + /** * OMEMO encrypt a cleartext message for a single recipient. * Note that this method does NOT set the 'to' attribute of the message. * - * @param to recipients bareJid + * @param recipient recipients bareJid * @param message text to encrypt * @return encrypted message * @throws CryptoFailedException when something crypto related fails @@ -277,17 +286,15 @@ public final class OmemoManager extends Manager { * @throws SmackException.NotConnectedException * @throws SmackException.NoResponseException */ - public Message encrypt(BareJid to, String message) + public OmemoMessage.Sent encrypt(BareJid recipient, String message) throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, InterruptedException, CannotEstablishOmemoSessionException, SmackException.NotConnectedException, SmackException.NoResponseException, SmackException.NotLoggedInException { synchronized (LOCK) { - LoggedInOmemoManager guard = new LoggedInOmemoManager(this); - Message plaintext = new Message(); - plaintext.setBody(message); - OmemoElement encrypted = getOmemoService().processSendingMessage(guard, to, plaintext); - return finishMessage(encrypted); + ArrayList recipients = new ArrayList<>(); + recipients.add(recipient); + return encrypt(recipients, message); } } @@ -306,17 +313,18 @@ public final class OmemoManager extends Manager { * @throws SmackException.NotConnectedException * @throws SmackException.NoResponseException */ - public Message encrypt(ArrayList recipients, String message) + public OmemoMessage.Sent encrypt(ArrayList recipients, String message) throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, InterruptedException, CannotEstablishOmemoSessionException, SmackException.NotConnectedException, SmackException.NoResponseException, SmackException.NotLoggedInException { synchronized (LOCK) { - Message m = new Message(); - m.setBody(message); - OmemoElement encrypted = getOmemoService().processSendingMessage( - new LoggedInOmemoManager(this), recipients, m); - return finishMessage(encrypted); + LoggedInOmemoManager guard = new LoggedInOmemoManager(this); + List devices = getDevicesOf(getOwnJid()); + for (BareJid recipient : recipients) { + devices.addAll(getDevicesOf(recipient)); + } + return service.createOmemoMessage(guard, devices, message); } } @@ -337,7 +345,7 @@ public final class OmemoManager extends Manager { * @throws CannotEstablishOmemoSessionException when there is a user for whom we could not create a session * with any of their devices. */ - public Message encrypt(MultiUserChat muc, String message) + public OmemoMessage.Sent encrypt(MultiUserChat muc, String message) throws UndecidedOmemoIdentityException, NoSuchAlgorithmException, CryptoFailedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, NoOmemoSupportException, CannotEstablishOmemoSessionException, @@ -348,8 +356,6 @@ public final class OmemoManager extends Manager { throw new NoOmemoSupportException(); } - Message m = new Message(); - m.setBody(message); ArrayList recipients = new ArrayList<>(); for (EntityFullJid e : muc.getOccupants()) { @@ -359,75 +365,6 @@ public final class OmemoManager extends Manager { } } - /** - * Encrypt a message for all users we could build a session with successfully in a previous attempt. - * This method can come in handy as a fallback when encrypting a message fails due to devices we cannot - * build a session with. - * - * @param exception CannotEstablishSessionException from a previous encrypt(user(s), message) call. - * @param message message we want to send. - * @return encrypted message - * @throws CryptoFailedException - * @throws UndecidedOmemoIdentityException when there are undecided identities. - */ - public Message encryptForExistingSessions(CannotEstablishOmemoSessionException exception, String message) - throws CryptoFailedException, UndecidedOmemoIdentityException, SmackException.NotLoggedInException - { - synchronized (LOCK) { - Message m = new Message(); - m.setBody(message); - OmemoElement encrypted = getOmemoService() - .encryptOmemoMessage(new LoggedInOmemoManager(this), exception.getSuccesses(), m); - return finishMessage(encrypted); - } - } - - /** - * Decrypt an OMEMO message. This method comes handy when dealing with messages that were not automatically - * decrypted by smack-omemo, eg. MAM query messages. - * @param sender sender of the message - * @param omemoMessage message - * @return decrypted message - * @throws InterruptedException Exception - * @throws SmackException.NoResponseException Exception - * @throws SmackException.NotConnectedException Exception - * @throws CryptoFailedException When decryption fails - * @throws XMPPException.XMPPErrorException Exception - * @throws CorruptedOmemoKeyException When the used keys are invalid - * @throws NoRawSessionException When there is no double ratchet session found for this message - */ - public ClearTextMessage decrypt(BareJid sender, Message omemoMessage) - throws InterruptedException, SmackException.NoResponseException, SmackException.NotConnectedException, - CryptoFailedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException, NoRawSessionException, - SmackException.NotLoggedInException - { - synchronized (LOCK) { - return getOmemoService().processLocalMessage(new LoggedInOmemoManager(this), sender, omemoMessage); - } - } - - /** - * Return a list of all OMEMO messages that were found in the MAM query result, that could be successfully - * decrypted. Normal cleartext messages are also added to this list. - * - * @param mamQueryResult mamQueryResult - * @return list of decrypted OmemoMessages - * @throws InterruptedException Exception - * @throws XMPPException.XMPPErrorException Exception - * @throws SmackException.NotConnectedException Exception - * @throws SmackException.NoResponseException Exception - */ - public List decryptMamQueryResult(MamManager.MamQueryResult mamQueryResult) - throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, - SmackException.NoResponseException, SmackException.NotLoggedInException - { - synchronized (LOCK) { - List l = new ArrayList<>(); - l.addAll(getOmemoService().decryptMamQueryResult(new LoggedInOmemoManager(this), mamQueryResult)); - return l; - } - } - /** * Trust that a fingerprint belongs to an OmemoDevice. * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must @@ -501,72 +438,30 @@ public final class OmemoManager extends Manager { * @throws CannotEstablishOmemoSessionException When we can't establish a session with the recipient */ public void sendRatchetUpdateMessage(OmemoDevice recipient) - throws CorruptedOmemoKeyException, UndecidedOmemoIdentityException, CryptoFailedException, - CannotEstablishOmemoSessionException, SmackException.NotLoggedInException + throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, InterruptedException, + SmackException.NoResponseException, NoSuchAlgorithmException, SmackException.NotConnectedException, + CryptoFailedException, CannotEstablishOmemoSessionException { synchronized (LOCK) { - getOmemoService().sendOmemoRatchetUpdateMessage( - new LoggedInOmemoManager(this), recipient, false); + 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); + + if (OmemoConfiguration.getAddEmeEncryptionHint()) { + message.addExtension(new ExplicitMessageEncryptionElement( + ExplicitMessageEncryptionElement.ExplicitMessageEncryptionProtocol.omemoVAxolotl)); + } + connection().sendStanza(message); } } - /** - * Create a new KeyTransportElement. This message will contain the AES-Key and IV that can be used eg. for encrypted - * Jingle file transfer. - * - * @param aesKey AES key to transport - * @param iv Initialization vector - * @param to list of recipient devices - * @return KeyTransportMessage - * @throws UndecidedOmemoIdentityException When the trust of session with the recipient is not decided yet - * @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 - */ - public OmemoElement createKeyTransportElement(byte[] aesKey, byte[] iv, OmemoDevice ... to) - throws UndecidedOmemoIdentityException, CorruptedOmemoKeyException, CryptoFailedException, - CannotEstablishOmemoSessionException, SmackException.NotLoggedInException - { - synchronized (LOCK) { - return getOmemoService().prepareOmemoKeyTransportElement( - new LoggedInOmemoManager(this), aesKey, iv, to); - } - } - - /** - * Create a new Message from a encrypted OmemoMessageElement. - * Add ourselves as the sender and the encrypted element. - * Also tell the server to store the message despite a possible missing body. - * The body will be set to a hint message that we are using OMEMO. - * - * @param encrypted OmemoMessageElement - * @return Message containing the OMEMO element and some additional information - */ - Message finishMessage(OmemoElement encrypted) { - if (encrypted == null) { - return null; - } - - Message chatMessage = new Message(); - chatMessage.setFrom(connection().getUser().asBareJid()); - chatMessage.addExtension(encrypted); - - if (OmemoConfiguration.getAddOmemoHintBody()) { - chatMessage.setBody(BODY_OMEMO_HINT); - } - - if (OmemoConfiguration.getAddMAMStorageProcessingHint()) { - StoreHint.set(chatMessage); - } - - if (OmemoConfiguration.getAddEmeEncryptionHint()) { - chatMessage.addExtension(new ExplicitMessageEncryptionElement( - ExplicitMessageEncryptionElement.ExplicitMessageEncryptionProtocol.omemoVAxolotl)); - } - - return chatMessage; - } - /** * Returns true, if the contact has any active devices published in a deviceList. * @@ -577,15 +472,12 @@ public final class OmemoManager extends Manager { * @throws SmackException.NoResponseException */ public boolean contactSupportsOmemo(BareJid contact) - throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, - SmackException.NotLoggedInException + throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, SmackException.NoResponseException { synchronized (LOCK) { - LoggedInOmemoManager managerGuard = new LoggedInOmemoManager(this); - - getOmemoService().refreshDeviceList(managerGuard, contact); - return !getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(getOwnDevice(), contact) - .getActiveDevices().isEmpty(); + OmemoCachedDeviceList deviceList = getOmemoService().refreshDeviceList(connection(), getOwnDevice(), contact); + return !deviceList.getActiveDevices().isEmpty(); } } @@ -657,7 +549,7 @@ public final class OmemoManager extends Manager { return getOwnFingerprint(); } - return getOmemoService().getOmemoStoreBackend().getFingerprint(new LoggedInOmemoManager(this), device); + return getOmemoService().getOmemoStoreBackend().getFingerprintAndMaybeBuildSession(new LoggedInOmemoManager(this), device); } } @@ -676,7 +568,7 @@ public final class OmemoManager extends Manager { } HashMap fingerprints = new HashMap<>(); - CachedDeviceList deviceList = getOmemoService().getOmemoStoreBackend() + OmemoCachedDeviceList deviceList = getOmemoService().getOmemoStoreBackend() .loadCachedDeviceList(getOwnDevice(), contact); for (int id : deviceList.getActiveDevices()) { @@ -708,45 +600,28 @@ public final class OmemoManager extends Manager { omemoMucMessageListeners.remove(listener); } - /** - * Build OMEMO sessions with devices of contact. - * - * @param contact contact we want to build session with. - * @throws InterruptedException - * @throws CannotEstablishOmemoSessionException - * @throws SmackException.NotConnectedException - * @throws SmackException.NoResponseException - */ - public void buildSessionsWith(BareJid contact) - throws InterruptedException, CannotEstablishOmemoSessionException, SmackException.NotConnectedException, - SmackException.NoResponseException, SmackException.NotLoggedInException - { - synchronized (LOCK) { - getOmemoService().buildMissingOmemoSessions(new LoggedInOmemoManager(this), contact); - } - } - /** * Request a deviceList update from contact contact. * * @param contact contact we want to obtain the deviceList from. - * @throws SmackException.NotConnectedException * @throws InterruptedException + * @throws PubSubException.NotALeafNodeException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException * @throws SmackException.NoResponseException */ public void requestDeviceListUpdateFor(BareJid contact) - throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, - SmackException.NotLoggedInException - { + throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, SmackException.NoResponseException { synchronized (LOCK) { - getOmemoService().refreshDeviceList(new LoggedInOmemoManager(this), contact); + getOmemoService().refreshDeviceList(connection(), getOwnDevice(), contact); } } /** - * Rotate the signedPreKey published in our OmemoBundle. 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. + * 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. * * @throws CorruptedOmemoKeyException When the IdentityKeyPair is damaged. * @throws InterruptedException XMPP error @@ -756,16 +631,20 @@ public final class OmemoManager extends Manager { * @throws PubSubException.NotALeafNodeException if the bundle node on the server is a CollectionNode */ public void rotateSignedPreKey() - throws CorruptedOmemoKeyException, InterruptedException, XMPPException.XMPPErrorException, - SmackException.NotConnectedException, SmackException.NoResponseException, - PubSubException.NotALeafNodeException, SmackException.NotLoggedInException + throws CorruptedOmemoKeyException, SmackException.NotLoggedInException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { synchronized (LOCK) { - LoggedInOmemoManager managerGuard = new LoggedInOmemoManager(this); + if (!connection().isAuthenticated()) { + throw new SmackException.NotLoggedInException(); + } + // generate key getOmemoService().getOmemoStoreBackend().changeSignedPreKey(getOwnDevice()); + // publish - getOmemoService().publish(managerGuard); + OmemoBundleElement bundle = getOmemoService().getOmemoStoreBackend().packOmemoBundle(getOwnDevice()); + OmemoService.publishBundle(connection(), getOwnDevice(), bundle); } } @@ -920,10 +799,10 @@ public final class OmemoManager extends Manager { // Remove listeners to avoid them getting added twice connection().removeAsyncStanzaListener(internalOmemoMessageStanzaListener); carbonManager.removeCarbonCopyReceivedListener(internalOmemoCarbonCopyListener); - pepManager.removePEPListener(deviceListUpdateListener); + //pepManager.removePEPListener(deviceListUpdateListener); // Add listeners - pepManager.addPEPListener(deviceListUpdateListener); + //pepManager.addPEPListener(deviceListUpdateListener); connection().addAsyncStanzaListener(internalOmemoMessageStanzaListener, omemoMessageStanzaFilter); carbonManager.addCarbonCopyReceivedListener(internalOmemoCarbonCopyListener); } @@ -932,7 +811,7 @@ public final class OmemoManager extends Manager { * Remove active stanza listeners needed for OMEMO. */ public void stopListeners() { - PEPManager.getInstanceFor(connection()).removePEPListener(deviceListUpdateListener); + //PEPManager.getInstanceFor(connection()).removePEPListener(deviceListUpdateListener); connection().removeAsyncStanzaListener(internalOmemoMessageStanzaListener); CarbonManager.getInstanceFor(connection()).removeCarbonCopyReceivedListener(internalOmemoCarbonCopyListener); } @@ -956,90 +835,6 @@ public final class OmemoManager extends Manager { return service; } - private final PEPListener deviceListUpdateListener = new PEPListener() { - - LoggedInOmemoManager managerGuard = null; - - @Override - public void eventReceived(EntityBareJid from, EventElement event, Message message) { - - if (managerGuard == null) { - try { - managerGuard = new LoggedInOmemoManager(OmemoManager.this); - } catch (SmackException.NotLoggedInException e) { - throw new AssertionError(e); - } - } - - for (ExtensionElement items : event.getExtensions()) { - if (!(items instanceof ItemsExtension)) { - continue; - } - - for (NamedElement item : ((ItemsExtension) items).getItems()) { - if (!(item instanceof PayloadItem)) { - continue; - } - - PayloadItem payloadItem = (PayloadItem) item; - - if (!(payloadItem.getPayload() instanceof OmemoDeviceListElement_VAxolotl)) { - continue; - } - - // Device List - OmemoDeviceListElement_VAxolotl omemoDeviceListElement = - (OmemoDeviceListElement_VAxolotl) payloadItem.getPayload(); - Integer ourDeviceId = getDeviceId(); - - getOmemoService().getOmemoStoreBackend() - .mergeCachedDeviceList(managerGuard.get().getOwnDevice(), from, omemoDeviceListElement); - - if (from == null) { - // Unknown sender, no more work to do. - // TODO: This DOES happen for some reason. Figure out when... - continue; - } - - if (!from.equals(getOwnJid())) { - // Not our deviceList, so nothing more to do - continue; - } - - if (omemoDeviceListElement.getDeviceIds().contains(ourDeviceId)) { - // We are on the list. Nothing more to do - continue; - } - - // Our deviceList and we are not on it! We don't want to miss all the action!!! - LOGGER.log(Level.INFO, "Our deviceId was not on the list!"); - Set deviceListIds = omemoDeviceListElement.copyDeviceIds(); - - // enroll at the deviceList - deviceListIds.add(ourDeviceId); - final OmemoDeviceListElement_VAxolotl newOmemoDeviceListElement = - new OmemoDeviceListElement_VAxolotl(deviceListIds); - - // PEPListener is a synchronous listener. - // Avoid any deadlocks by using an async task to update the device list. - Async.go(new Runnable() { - @Override - public void run() { - try { - OmemoService.publishDeviceIds(managerGuard, newOmemoDeviceListElement); - } - catch (SmackException | InterruptedException | XMPPException.XMPPErrorException e) { - // TODO: It might be dangerous NOT to retry publishing our deviceId - LOGGER.log(Level.SEVERE, "Could not publish our device list after an update " + - "without our id was received: " + e.getMessage()); - } - } - }); - } - } - } - }; - private final StanzaListener internalOmemoMessageStanzaListener = new StanzaListener() { @Override public void processStanza(Stanza packet) throws SmackException.NotConnectedException, InterruptedException { diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessage.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessage.java new file mode 100644 index 000000000..5ba1887b1 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessage.java @@ -0,0 +1,128 @@ +/** + * + * 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; + +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.BODY_OMEMO_HINT; +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO; +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smackx.eme.element.ExplicitMessageEncryptionElement; +import org.jivesoftware.smackx.hints.element.StoreHint; +import org.jivesoftware.smackx.omemo.element.OmemoElement; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; +import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; + +public class OmemoMessage { + + private final OmemoElement element; + + OmemoMessage(OmemoElement element) { + this.element = element; + } + + public OmemoElement getElement() { + return element; + } + + public static class Sent extends OmemoMessage { + private final ArrayList intendedDevices = new ArrayList<>(); + private final HashMap skippedDevices = new HashMap<>(); + + Sent(OmemoElement element, List intendedDevices, HashMap skippedDevices) { + super(element); + this.intendedDevices.addAll(intendedDevices); + this.skippedDevices.putAll(skippedDevices); + } + + public ArrayList getIntendedDevices() { + return intendedDevices; + } + + public HashMap getSkippedDevices() { + return skippedDevices; + } + + public boolean isMissingRecipients() { + return !getSkippedDevices().isEmpty(); + } + + public Message asMessage() { + + Message messageStanza = new Message(); + messageStanza.addExtension(getElement()); + + if (OmemoConfiguration.getAddOmemoHintBody()) { + messageStanza.setBody(BODY_OMEMO_HINT); + } + + StoreHint.set(messageStanza); + messageStanza.addExtension(new ExplicitMessageEncryptionElement(OMEMO_NAMESPACE_V_AXOLOTL, OMEMO)); + + return messageStanza; + } + } + + public static class Received extends OmemoMessage { + private final String message; + private final OmemoFingerprint sendersFingerprint; + private final OmemoDevice senderDevice; + private final CARBON carbon; + + Received(OmemoElement element, String message, OmemoFingerprint sendersFingerprint, OmemoDevice senderDevice, CARBON carbon) { + super(element); + this.message = message; + this.sendersFingerprint = sendersFingerprint; + this.senderDevice = senderDevice; + this.carbon = carbon; + } + + public String getMessage() { + return message; + } + + public OmemoFingerprint getSendersFingerprint() { + return sendersFingerprint; + } + + public OmemoDevice getSenderDevice() { + return senderDevice; + } + + /** + * Return the carbon type. + * + * @return carbon type + */ + public CARBON getCarbon() { + return carbon; + } + } + + /** + * Types of Carbon Messages. + */ + public enum CARBON { + NONE, //No carbon + SENT, //Sent carbon + RECV //Received Carbon + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoRatchet.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoRatchet.java index db2f852c0..f75473771 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoRatchet.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoRatchet.java @@ -68,10 +68,7 @@ public abstract class OmemoRatchet decryptExceptions = new ArrayList<>(); - // CHECKSTYLE: OFF - @SuppressWarnings("unchecked") List keys = element.getHeader().getKeys(); - // CHECKSTYLE: ON // Find key with our ID. for (OmemoKeyElement k : keys) { diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java index e1b3b5ad4..a2a3aa64c 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java @@ -16,9 +16,8 @@ */ package org.jivesoftware.smackx.omemo; -import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL; -import static org.jivesoftware.smackx.omemo.util.OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID; -import static org.jivesoftware.smackx.omemo.util.OmemoConstants.PEP_NODE_DEVICE_LIST; +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYLENGTH; +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE; import java.io.UnsupportedEncodingException; import java.security.InvalidAlgorithmParameterException; @@ -27,14 +26,12 @@ import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.Security; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; -import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Random; -import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.crypto.BadPaddingException; @@ -42,46 +39,35 @@ 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.filter.StanzaFilter; -import org.jivesoftware.smack.packet.Message; -import org.jivesoftware.smack.packet.Stanza; -import org.jivesoftware.smack.packet.StanzaError; -import org.jivesoftware.smack.util.Async; -import org.jivesoftware.smackx.carbons.CarbonCopyReceivedListener; -import org.jivesoftware.smackx.carbons.packet.CarbonExtension; -import org.jivesoftware.smackx.forward.packet.Forwarded; -import org.jivesoftware.smackx.mam.MamManager; -import org.jivesoftware.smackx.muc.MultiUserChat; -import org.jivesoftware.smackx.muc.MultiUserChatManager; + 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.OmemoKeyElement; 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.NoRawSessionException; +import org.jivesoftware.smackx.omemo.exceptions.NoIdentityKeyException; import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException; -import org.jivesoftware.smackx.omemo.internal.CachedDeviceList; -import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag; -import org.jivesoftware.smackx.omemo.internal.ClearTextMessage; +import org.jivesoftware.smackx.omemo.exceptions.UntrustedOmemoIdentityException; +import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; -import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation; 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.TrustCallback; +import org.jivesoftware.smackx.omemo.trust.TrustState; import org.jivesoftware.smackx.omemo.util.OmemoConstants; import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder; import org.jivesoftware.smackx.pubsub.LeafNode; import org.jivesoftware.smackx.pubsub.PayloadItem; import org.jivesoftware.smackx.pubsub.PubSubException; -import org.jivesoftware.smackx.pubsub.PubSubException.NotAPubSubNodeException; import org.jivesoftware.smackx.pubsub.PubSubManager; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.jxmpp.jid.BareJid; -import org.jxmpp.jid.Jid; /** * This class contains OMEMO related logic and registers listeners etc. @@ -106,6 +92,8 @@ public abstract class OmemoService omemoStore; protected final HashMap> omemoRatchets = new HashMap<>(); + /** + * Create a new OmemoService object. This should only happen once. + * When the service gets created, it tries a placeholder crypto function in order to test, if all necessary + * algorithms are available on the system. + * + * @throws NoSuchPaddingException When no Cipher could be instantiated. + * @throws NoSuchAlgorithmException when no Cipher could be instantiated. + * @throws NoSuchProviderException when BouncyCastle could not be found. + * @throws InvalidAlgorithmParameterException when the Cipher could not be initialized + * @throws InvalidKeyException when the generated key is invalid + * @throws UnsupportedEncodingException when UTF8 is unavailable + * @throws BadPaddingException when cipher.doFinal gets wrong padding + * @throws IllegalBlockSizeException when cipher.doFinal gets wrong Block size. + */ + protected OmemoService() + throws NoSuchPaddingException, InvalidKeyException, UnsupportedEncodingException, IllegalBlockSizeException, + BadPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException + { + // TODO + } + /** * Return the singleton instance of this class. When no instance is set, throw an IllegalStateException instead. * @return instance. @@ -178,36 +187,45 @@ public abstract class OmemoService + protected abstract OmemoStore createDefaultOmemoStoreBackend(); /** - * Create a new OmemoService object. This should only happen once. - * When the service gets created, it tries a placeholder crypto function in order to test, if all necessary - * algorithms are available on the system. + * Return a new instance of the OMEMO ratchet. + * The ratchet is internally used to encrypt/decrypt message keys. * - * @throws NoSuchPaddingException When no Cipher could be instantiated. - * @throws NoSuchAlgorithmException when no Cipher could be instantiated. - * @throws NoSuchProviderException when BouncyCastle could not be found. - * @throws InvalidAlgorithmParameterException when the Cipher could not be initialized - * @throws InvalidKeyException when the generated key is invalid - * @throws UnsupportedEncodingException when UTF8 is unavailable - * @throws BadPaddingException when cipher.doFinal gets wrong padding - * @throws IllegalBlockSizeException when cipher.doFinal gets wrong Block size. + * @param manager OmemoManager + * @param store OmemoStore + * @return instance of the OmemoRatchet */ - public OmemoService() - throws NoSuchPaddingException, InvalidKeyException, UnsupportedEncodingException, IllegalBlockSizeException, - BadPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException - { - // Check availability of algorithms and encodings needed for crypto - checkAvailableAlgorithms(); + protected abstract OmemoRatchet + instantiateOmemoRatchet(OmemoManager manager, + OmemoStore 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 + getOmemoRatchet(OmemoManager manager) { + OmemoRatchet + omemoRatchet = omemoRatchets.get(manager); + if (omemoRatchet == null) { + omemoRatchet = instantiateOmemoRatchet(manager, omemoStore); + omemoRatchets.put(manager, omemoRatchet); + } + return omemoRatchet; } /** - * Makes the OmemoManager known to the OmemoService. + * Instantiate and deposit a Ratchet for the given OmemoManager. + * * @param manager manager. */ - void registerManager(OmemoManager manager) { + void registerRatchetForManager(OmemoManager manager) { omemoRatchets.put(manager, instantiateOmemoRatchet(manager, getOmemoStoreBackend())); } @@ -222,7 +240,7 @@ public abstract class OmemoService(null, null, ""); + 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 builder; + try { + builder = new OmemoMessageBuilder<>(userDevice, gullibleTrustCallback, getOmemoRatchet(manager), + messageKey, iv, null); + } catch (InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException | BadPaddingException | UnsupportedEncodingException | NoSuchProviderException | 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); + } + + return builder.finish(); } /** - * Publish a bundle to the server. + * Encrypt a message with a messageKey and an IV and create an OmemoMessage from it. + * + * @param managerGuard authenticated OmemoManager + * @param contactsDevices list 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. * - * @param managerGuard OmemoManager * @throws SmackException.NotConnectedException * @throws InterruptedException * @throws SmackException.NoResponseException - * @throws CorruptedOmemoKeyException - * @throws XMPPException.XMPPErrorException + * @throws UndecidedOmemoIdentityException if the list of recipient devices contains undecided devices + * @throws CryptoFailedException if we are lacking some crypto primitives */ - void publishBundle(OmemoManager.LoggedInOmemoManager managerGuard) + OmemoMessage.Sent encrypt(OmemoManager.LoggedInOmemoManager managerGuard, + List contactsDevices, + byte[] messageKey, + byte[] iv, + String message) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, - CorruptedOmemoKeyException, XMPPException.XMPPErrorException + UndecidedOmemoIdentityException, CryptoFailedException { - OmemoManager omemoManager = managerGuard.get(); - OmemoDevice userDevice = omemoManager.getOwnDevice(); + OmemoManager manager = managerGuard.get(); + OmemoDevice userDevice = manager.getOwnDevice(); - Date lastSignedPreKeyRenewal = getOmemoStoreBackend().getDateOfLastSignedPreKeyRenewal(userDevice); - if (OmemoConfiguration.getRenewOldSignedPreKeys() && lastSignedPreKeyRenewal != null) { - if (System.currentTimeMillis() - lastSignedPreKeyRenewal.getTime() - > 1000L * 60 * 60 * OmemoConfiguration.getRenewOldSignedPreKeysAfterHours()) { - LOGGER.log(Level.FINE, "Renewing signedPreKey"); - getOmemoStoreBackend().changeSignedPreKey(userDevice); - } - } else { - getOmemoStoreBackend().setDateOfLastSignedPreKeyRenewal(userDevice, new Date()); + // Do not encrypt for our own device. + removeOurDevice(userDevice, contactsDevices); + + ArrayList undecidedDevices = getUndecidedDevices(userDevice, manager.getTrustCallback(), contactsDevices); + if (!undecidedDevices.isEmpty()) { + throw new UndecidedOmemoIdentityException(undecidedDevices); } - // publish - OmemoBundleElement bundleElement = getOmemoStoreBackend().packOmemoBundle(userDevice); + // Keep track of skipped devices + HashMap skippedRecipients = new HashMap<>(); - PubSubManager.getInstance(omemoManager.getConnection(), userDevice.getJid()) - .tryToPublishAndPossibleAutoCreate( - OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(userDevice.getDeviceId()), - new PayloadItem<>(bundleElement)); - } + OmemoMessageBuilder builder; + try { + builder = new OmemoMessageBuilder<>( + userDevice, manager.getTrustCallback(), getOmemoRatchet(managerGuard.get()), messageKey, iv, message); + } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | + NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { + throw new CryptoFailedException(e); + } - /** - * Remove stale devices from our device list. - * This does only delete devices, if that's configured in OmemoConfiguration. - * - * @param managerGuard OmemoManager - * @param deviceListIds deviceIds we plan to publish. Stale devices are deleted from that list. - * @return - */ - boolean removeStaleDevicesIfNeeded(OmemoManager.LoggedInOmemoManager managerGuard, Set deviceListIds) { - OmemoManager omemoManager = managerGuard.get(); - OmemoDevice userDevice = omemoManager.getOwnDevice(); - boolean publish = false; - - // Clear devices that we didn't receive a message from for a while - Iterator it = deviceListIds.iterator(); - while (OmemoConfiguration.getDeleteStaleDevices() && it.hasNext()) { - - int id = it.next(); - if (id == userDevice.getDeviceId()) { - // Skip own id - continue; - } - - OmemoDevice contactsDevice = new OmemoDevice(userDevice.getJid(), id); - Date date = getOmemoStoreBackend().getDateOfLastReceivedMessage(userDevice, contactsDevice); - - if (date == null) { - - getOmemoStoreBackend().setDateOfLastReceivedMessage(userDevice, contactsDevice, new Date()); - - } else { - - if (System.currentTimeMillis() - date.getTime() > 1000L * 60 * 60 * - OmemoConfiguration.getDeleteStaleDevicesAfterHours()) { - LOGGER.log(Level.INFO, "Remove device " + id + " because of more than " + - OmemoConfiguration.getDeleteStaleDevicesAfterHours() + " hours of inactivity."); - it.remove(); - publish = true; + 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; } } - } - return publish; - } - /** - * Publish the given deviceList to the server. - *omemoManager.getOwnJid() - * @param managerGuard OmemoManager - * @param deviceList list of deviceIDs - * @throws InterruptedException Exception - * @throws XMPPException.XMPPErrorException Exception - * @throws SmackException.NotConnectedException Exception - * @throws SmackException.NoResponseException Exception - * @throws PubSubException.NotALeafNodeException Exception - */ - static void publishDeviceIds(OmemoManager.LoggedInOmemoManager managerGuard, OmemoDeviceListElement deviceList) - throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, - SmackException.NoResponseException, PubSubException.NotALeafNodeException - { - OmemoManager omemoManager = managerGuard.get(); - PubSubManager.getInstance(omemoManager.getConnection(), omemoManager.getOwnJid()) - .tryToPublishAndPossibleAutoCreate(OmemoConstants.PEP_NODE_DEVICE_LIST, new PayloadItem<>(deviceList)); - } - - /** - * Fetch the deviceList node of a contact. - * - * @param managerGuard omemoManager - * @param contact contact - * @return LeafNode - * @throws InterruptedException - * @throws PubSubException.NotALeafNodeException - * @throws XMPPException.XMPPErrorException - * @throws SmackException.NotConnectedException - * @throws SmackException.NoResponseException - * @throws NotAPubSubNodeException - */ - static LeafNode fetchDeviceListNode(OmemoManager.LoggedInOmemoManager managerGuard, BareJid contact) - throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, - SmackException.NotConnectedException, SmackException.NoResponseException, NotAPubSubNodeException - { - OmemoManager omemoManager = managerGuard.get(); - return PubSubManager.getInstance(omemoManager.getConnection(), contact).getLeafNode(PEP_NODE_DEVICE_LIST); - } - - /** - * Directly fetch the device list of a contact. - * - * @param managerGuard OmemoManager - * @param contact BareJid of the contact - * @return The OmemoDeviceListElement of the contact - * @throws XMPPException.XMPPErrorException When - * @throws SmackException.NotConnectedException something - * @throws InterruptedException goes - * @throws SmackException.NoResponseException wrong - * @throws PubSubException.NotALeafNodeException when the device lists node is not a LeafNode - * @throws NotAPubSubNodeException - */ - static OmemoDeviceListElement fetchDeviceList(OmemoManager.LoggedInOmemoManager managerGuard, BareJid contact) - throws InterruptedException, SmackException.NotConnectedException, SmackException.NoResponseException, - XMPPException.XMPPErrorException { - try { - return extractDeviceListFrom(fetchDeviceListNode(managerGuard, contact)); - } - - catch (XMPPException.XMPPErrorException e) { - - if (e.getXMPPError().getCondition() == StanzaError.Condition.item_not_found) { - LOGGER.log(Level.WARNING, "Could not refresh own deviceList, because the node did not exist: " - + e.getMessage()); + // 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("At this point, devices cannot be undecided.", e); + } + catch (UntrustedOmemoIdentityException e) { + LOGGER.log(Level.WARNING, "Device " + contactsDevice + " is untrusted. Message is not encrypted for it."); + skippedRecipients.put(contactsDevice, e); } - - throw e; - - } catch (PubSubException.NotALeafNodeException e) { - LOGGER.log(Level.WARNING, "Could not refresh own deviceList, because the Node is not a LeafNode: " + - e.getMessage()); } - catch (PubSubException.NotAPubSubNodeException e) { - LOGGER.log(Level.WARNING, "Caught a NotAPubSubNodeException when fetching a deviceList node. " + - "This probably means that we're dealing with an ejabberd server and the LeafNode does not exist."); - } - return null; + OmemoElement element = builder.finish(); + + return new OmemoMessage.Sent(element, contactsDevices, skippedRecipients); } /** - * Refresh our deviceList from the server. * - * @param managerGuard omemoManager - * @return true, if we should publish our device list again (because its broken or not existent or - * doesn't contain our id...) - * - * @throws SmackException.NotConnectedException + * @param managerGuard + * @param contactsDevices + * @param key + * @param iv + * @return * @throws InterruptedException + * @throws UndecidedOmemoIdentityException if the list of recipients contains an undecided device + * @throws CryptoFailedException if we are lacking some cryptographic algorithms + * @throws SmackException.NotConnectedException * @throws SmackException.NoResponseException */ - private CachedDeviceList refreshOwnDeviceList(OmemoManager.LoggedInOmemoManager managerGuard) - throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, - XMPPException.XMPPErrorException + OmemoMessage.Sent createKeyTransportMessage(OmemoManager.LoggedInOmemoManager managerGuard, + List contactsDevices, + byte[] key, + byte[] iv) + throws InterruptedException, UndecidedOmemoIdentityException, CryptoFailedException, + SmackException.NotConnectedException, SmackException.NoResponseException { - OmemoManager omemoManager = managerGuard.get(); - OmemoDevice userDevice = omemoManager.getOwnDevice(); - - OmemoDeviceListElement list = fetchDeviceList(managerGuard, omemoManager.getOwnJid()); - return getOmemoStoreBackend().mergeCachedDeviceList(userDevice, userDevice.getJid(), list); + return encrypt(managerGuard, contactsDevices, key, iv, null); } /** - * Refresh the deviceList of contact and merge it with the one stored locally. - * @param managerGuard omemoManager - * @param contact contact - * @throws SmackException.NotConnectedException + * + * @param managerGuard + * @param contactsDevices + * @param message + * @return * @throws InterruptedException + * @throws UndecidedOmemoIdentityException if the list of recipient devices contains an undecided device. + * @throws CryptoFailedException if we are lacking some cryptographic algorithms + * @throws SmackException.NotConnectedException * @throws SmackException.NoResponseException */ - CachedDeviceList refreshDeviceList(OmemoManager.LoggedInOmemoManager managerGuard, BareJid contact) - throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException + OmemoMessage.Sent createOmemoMessage(OmemoManager.LoggedInOmemoManager managerGuard, + List contactsDevices, + String message) + throws InterruptedException, UndecidedOmemoIdentityException, CryptoFailedException, + SmackException.NotConnectedException, SmackException.NoResponseException { - OmemoDeviceListElement omemoDeviceListElement = null; + byte[] key, iv; + iv = OmemoMessageBuilder.generateIv(); try { - omemoDeviceListElement = fetchDeviceList(managerGuard, contact); - } catch (XMPPException.XMPPErrorException e) { - LOGGER.log(Level.WARNING, "Could not fetch device list of " + contact + ": " + e); + key = OmemoMessageBuilder.generateKey(KEYTYPE, KEYLENGTH); + } catch (NoSuchAlgorithmException e) { + throw new CryptoFailedException(e); } - return getOmemoStoreBackend().mergeCachedDeviceList(managerGuard.get().getOwnDevice(), contact, omemoDeviceListElement); + return encrypt(managerGuard, contactsDevices, key, iv, message); } /** - * Fetch the OmemoBundleElement of the contact. + * Retrieve a users OMEMO bundle. * - * @param managerGuard OmemoManager - * @param contact the contacts BareJid - * @return the OmemoBundleElement of the contact - * @throws XMPPException.XMPPErrorException When - * @throws SmackException.NotConnectedException something - * @throws InterruptedException goes - * @throws SmackException.NoResponseException wrong - * @throws PubSubException.NotALeafNodeException when the bundles node is not a LeafNode - * @throws NotAPubSubNodeException + * @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 + * @throws InterruptedException + * @throws SmackException.NoResponseException + * @throws XMPPException.XMPPErrorException + * @throws PubSubException.NotALeafNodeException + * @throws PubSubException.NotAPubSubNodeException */ - static OmemoBundleElement fetchBundle(OmemoManager.LoggedInOmemoManager managerGuard, OmemoDevice contact) - throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, - SmackException.NoResponseException, PubSubException.NotALeafNodeException, NotAPubSubNodeException + private static OmemoBundleElement fetchBundle(XMPPConnection connection, + OmemoDevice contactsDevice) + throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, + XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException, + PubSubException.NotAPubSubNodeException { - OmemoManager omemoManager = managerGuard.get(); - LeafNode node = PubSubManager.getInstance(omemoManager.getConnection(), contact.getJid()).getLeafNode( - PEP_NODE_BUNDLE_FROM_DEVICE_ID(contact.getDeviceId())); - return extractBundleFrom(node); - } + PubSubManager pm = PubSubManager.getInstance(connection, contactsDevice.getJid()); + LeafNode node = pm.getLeafNode(contactsDevice.getBundleNodeName()); - /** - * Extract the OmemoBundleElement of a contact from a LeafNode. - * - * @param node typically a LeafNode containing the OmemoBundles of a contact - * @return the OmemoBundleElement - * @throws XMPPException.XMPPErrorException When - * @throws SmackException.NotConnectedException something - * @throws InterruptedException goes - * @throws SmackException.NoResponseException wrong - */ - private static OmemoBundleElement extractBundleFrom(LeafNode node) - throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, - SmackException.NoResponseException - { if (node == null) { return null; } @@ -516,906 +488,490 @@ public abstract class OmemoService(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 + * @throws InterruptedException + * @throws PubSubException.NotALeafNodeException + * @throws SmackException.NoResponseException + * @throws SmackException.NotConnectedException + * @throws XMPPException.XMPPErrorException + * @throws PubSubException.NotAPubSubNodeException + */ + private static OmemoDeviceListElement fetchDeviceList(XMPPConnection connection, BareJid contact) + throws InterruptedException, PubSubException.NotALeafNodeException, SmackException.NoResponseException, + SmackException.NotConnectedException, XMPPException.XMPPErrorException, + PubSubException.NotAPubSubNodeException { + PubSubManager pm = PubSubManager.getInstance(connection, contact); + String nodeName = OmemoConstants.PEP_NODE_DEVICE_LIST; + LeafNode node = pm.getLeafNode(nodeName); + if (node == null) { - LOGGER.log(Level.WARNING, "DeviceListNode is null."); return null; } List> items = node.getItems(); - if (items.size() > 0) { - - OmemoDeviceListElement listElement = items.get(items.size() - 1).getPayload(); - - if (items.size() > 1) { - node.deleteAllItems(); - node.publish(new PayloadItem<>(listElement)); - } - - return listElement; + if (items.isEmpty()) { + return null; } - Set emptySet = Collections.emptySet(); - return new OmemoDeviceListElement_VAxolotl(emptySet); + return items.get(items.size() - 1).getPayload(); } /** - * Build sessions for all devices of the contacts in the list, of which we have no session yet. - * When there are devices, this method throws a {@link CannotEstablishOmemoSessionException} which contains all - * those devices, while still establishing sessions with all other devices. + * Publish the given device list to the server. + * @param connection authenticated XMPP connection. + * @param deviceList users deviceList. + * @throws InterruptedException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + * @throws PubSubException.NotALeafNodeException + */ + private static void publishDeviceList(XMPPConnection connection, OmemoDeviceListElement deviceList) + throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, + SmackException.NoResponseException, PubSubException.NotALeafNodeException + { + PubSubManager.getInstance(connection, connection.getUser().asBareJid()) + .tryToPublishAndPossibleAutoCreate(OmemoConstants.PEP_NODE_DEVICE_LIST, new PayloadItem<>(deviceList)); + } + + private void refreshAndRepublishDeviceList(XMPPConnection connection, OmemoDevice userDevice) + throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, SmackException.NoResponseException + { + // refreshOmemoDeviceList; + OmemoDeviceListElement publishedList; + try { + publishedList = fetchDeviceList(connection, userDevice.getJid()); + } catch (PubSubException.NotAPubSubNodeException e) { + publishedList = null; + } + if (publishedList == null) { + publishedList = new OmemoDeviceListElement_VAxolotl(Collections.emptySet()); + } + OmemoCachedDeviceList cachedList = getOmemoStoreBackend().mergeCachedDeviceList( + userDevice, userDevice.getJid(), publishedList); + + + // Delete stale devices if allowed and necessary + if (OmemoConfiguration.getDeleteStaleDevices()) { + cachedList = deleteStaleDevices(userDevice); + } + + // Add back our device if necessary + if (!cachedList.getActiveDevices().contains(userDevice.getDeviceId())) { + cachedList.addDevice(userDevice.getDeviceId()); + } + + getOmemoStoreBackend().storeCachedDeviceList(userDevice, userDevice.getJid(), cachedList); + + // Republish our deviceId if it is missing from the published list. + if (!publishedList.getDeviceIds().equals(cachedList.getActiveDevices())) { + publishDeviceList(connection, new OmemoDeviceListElement_VAxolotl(cachedList)); + } + } + + /** + * Refresh and merge device list of contact. * - * @param managerGuard manager - * @param jids list of BareJids + * @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 + * @throws PubSubException.NotALeafNodeException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + */ + OmemoCachedDeviceList refreshDeviceList(XMPPConnection connection, OmemoDevice userDevice, BareJid contact) + throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, SmackException.NoResponseException { + // refreshOmemoDeviceList; + OmemoDeviceListElement publishedList; + try { + publishedList = fetchDeviceList(connection, userDevice.getJid()); + } catch (PubSubException.NotAPubSubNodeException e) { + publishedList = null; + } + if (publishedList == null) { + publishedList = new OmemoDeviceListElement_VAxolotl(Collections.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 * @throws InterruptedException * @throws SmackException.NoResponseException - * @throws CannotEstablishOmemoSessionException if we cannot create sessions with some devices. + * @throws CorruptedOmemoKeyException if our IdentityKeyPair is corrupted. */ - void buildMissingOmemoSessions(OmemoManager.LoggedInOmemoManager managerGuard, List jids) - throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, - CannotEstablishOmemoSessionException { + private void buildFreshSessionWithDevice(XMPPConnection connection, OmemoDevice userDevice, OmemoDevice contactsDevice) + throws CannotEstablishOmemoSessionException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException, CorruptedOmemoKeyException { - CannotEstablishOmemoSessionException ex = null; - - for (BareJid jid : jids) { - try { - buildMissingOmemoSessions(managerGuard, jid); - } catch (CannotEstablishOmemoSessionException e) { - if (ex == null) { - ex = e; - } else { - ex.addFailures(e); - } - } - } - - if (ex != null) { - throw ex; - } - } - - /** - * Build sessions for all devices of the contact that we do not have a session with yet. - * - * @param managerGuard omemoManager - * @param jid the BareJid of the contact - */ - void buildMissingOmemoSessions(OmemoManager.LoggedInOmemoManager managerGuard, BareJid jid) - throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, - CannotEstablishOmemoSessionException - { - OmemoManager omemoManager = managerGuard.get(); - OmemoDevice userDevice = omemoManager.getOwnDevice(); - - CachedDeviceList devices = getOmemoStoreBackend().loadCachedDeviceList(userDevice, jid); - CannotEstablishOmemoSessionException sessionException = null; - - if (devices == null || devices.getAllDevices().isEmpty()) { - refreshDeviceList(managerGuard, jid); - devices = getOmemoStoreBackend().loadCachedDeviceList(userDevice, jid); - } - - for (int id : devices.getActiveDevices()) { - - OmemoDevice device = new OmemoDevice(jid, id); - if (getOmemoStoreBackend().containsRawSession(userDevice, device)) { - // We have a session already. - continue; - } - - // Build missing session - try { - buildSessionWithDevice(managerGuard, device, false); - } catch (CannotEstablishOmemoSessionException e) { - - if (sessionException == null) { - sessionException = e; - } else { - sessionException.addFailures(e); - } - - } catch (CorruptedOmemoKeyException e) { - CannotEstablishOmemoSessionException fail = - new CannotEstablishOmemoSessionException(device, e); - - if (sessionException == null) { - sessionException = fail; - } else { - sessionException.addFailures(fail); - } - } - } - - if (sessionException != null) { - throw sessionException; - } - } - - /** - * Build an OmemoSession for the given OmemoDevice. - * - * @param managerGuard omemoManager - * @param contactsDevice OmemoDevice - * @param fresh Do we want to build a session even if we already have one? - * @throws CannotEstablishOmemoSessionException when no session could be established - * @throws CorruptedOmemoKeyException when the bundle contained an invalid OMEMO identityKey - */ - public void buildSessionWithDevice(OmemoManager.LoggedInOmemoManager managerGuard, - OmemoDevice contactsDevice, - boolean fresh) - throws CannotEstablishOmemoSessionException, CorruptedOmemoKeyException - { - OmemoManager omemoManager = managerGuard.get(); - OmemoDevice userDevice = omemoManager.getOwnDevice(); - - if (contactsDevice.equals(omemoManager.getOwnDevice())) { + if (contactsDevice.equals(userDevice)) { + // Do not build a session with yourself. return; } - // Do not build sessions with devices we already know... - if (!fresh && getOmemoStoreBackend().containsRawSession(userDevice, contactsDevice)) { - return; - } - - OmemoBundleElement bundle; + OmemoBundleElement bundleElement; try { - bundle = fetchBundle(managerGuard, contactsDevice); - } catch (SmackException | XMPPException.XMPPErrorException | InterruptedException e) { + bundleElement = fetchBundle(connection, contactsDevice); + } catch (XMPPException.XMPPErrorException | PubSubException.NotALeafNodeException | + PubSubException.NotAPubSubNodeException e) { throw new CannotEstablishOmemoSessionException(contactsDevice, e); } - HashMap bundles = getOmemoStoreBackend().keyUtil().BUNDLE.bundles(bundle, contactsDevice); - // Select random Bundle - int randomIndex = new Random().nextInt(bundles.size()); - T_Bundle randomPreKeyBundle = new ArrayList<>(bundles.values()).get(randomIndex); - // Build raw session - processBundle(managerGuard, randomPreKeyBundle, contactsDevice); + HashMap 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 + processBundle(connection, userDevice, randomPreKeyBundle, contactsDevice); + } + + /** + * Build OMEMO sessions with all devices of the contact, we haven't had sessions with before. + * This method returns a list of OmemoDevices. This list contains all devices, with which we either had sessions + * before, plus those devices with which we just built sessions. + * + * @param connection authenticated XMPP connection. + * @param userDevice our OmemoDevice + * @param contact the BareJid of the contact with whom we want to build sessions with. + * @return list of devices with a session. + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + */ + private ArrayList buildMissingSessionsWithContact(XMPPConnection connection, + OmemoDevice userDevice, + BareJid contact) + throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException + { + OmemoCachedDeviceList contactsDeviceIds = getOmemoStoreBackend().loadCachedDeviceList(userDevice, contact); + ArrayList contactsDevices = new ArrayList<>(); + for (int deviceId : contactsDeviceIds.getActiveDevices()) { + contactsDevices.add(new OmemoDevice(contact, deviceId)); + } + + return buildMissingSessionsWithDevices(connection, userDevice, contactsDevices); + } + + /** + * Build sessions with all devices from the list, we don't have a session with yet. + * Return the list of all devices we have a session with afterwards. + * @param connection authenticated XMPP connection + * @param userDevice our OmemoDevice + * @param devices list of devices we may want to build a session with if necessary + * @return list of all devices with sessions + * + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + */ + private ArrayList buildMissingSessionsWithDevices(XMPPConnection connection, + OmemoDevice userDevice, + List devices) + throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { + ArrayList devicesWithSession = new ArrayList<>(); + 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; + } + + /** + * Build OMEMO sessions with all devices of the contacts, we haven't had sessions with before. + * This method returns a list of OmemoDevices. This list contains all devices, with which we either had sessions + * before, plus those devices with which we just built sessions. + * + * @param connection authenticated XMPP connection. + * @param userDevice our OmemoDevice + * @param contacts list of BareJids of contacts, we want to build sessions with. + * @return list of devices, we have sessions with. + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + */ + private ArrayList buildMissingSessionsWithContacts(XMPPConnection connection, + OmemoDevice userDevice, + List contacts) + throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException + { + ArrayList devicesWithSessions = new ArrayList<>(); + + for (BareJid contact : contacts) { + ArrayList devices = buildMissingSessionsWithContact(connection, userDevice, contact); + devicesWithSessions.addAll(devices); + } + + return devicesWithSessions; + } + + private ArrayList getUndecidedDevices(OmemoDevice userDevice, TrustCallback callback, List devices) { + ArrayList undecidedDevices = new ArrayList<>(); + + for (OmemoDevice device : devices) { + + OmemoFingerprint fingerprint; + try { + fingerprint = getOmemoStoreBackend().getFingerprint(userDevice, device); + } catch (CorruptedOmemoKeyException | NoIdentityKeyException e) { + // TODO: Best solution? + undecidedDevices.add(device); + continue; + } + + if (callback.getTrust(device, fingerprint) == TrustState.undecided) { + undecidedDevices.add(device); + } + } + + return undecidedDevices; + } + + private ArrayList getUntrustedDeviced(OmemoDevice userDevice, TrustCallback trustCallback, List devices) { + ArrayList untrustedDevices = new ArrayList<>(); + + for (OmemoDevice device : devices) { + + OmemoFingerprint fingerprint; + try { + fingerprint = getOmemoStoreBackend().getFingerprint(userDevice, device); + } catch (CorruptedOmemoKeyException | NoIdentityKeyException e) { + // TODO: Best solution? + untrustedDevices.add(device); + continue; + } + + if (trustCallback.getTrust(device, fingerprint) == TrustState.untrusted) { + untrustedDevices.add(device); + } + } + + return untrustedDevices; + } + + /** + * 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. + */ + private boolean hasSession(OmemoDevice userDevice, OmemoDevice contactsDevice) { + return getOmemoStoreBackend().loadRawSession(userDevice, contactsDevice) != null; } /** * Process a received bundle. Typically that includes saving keys and building a session. * - * @param managerGuard omemoManager that will process the bundle - * @param bundle T_Bundle (depends on used Signal/Olm library) - * @param device OmemoDevice + * @param connection authenticated XMPP connection + * @param userDevice our OmemoDevice + * @param contactsBundle bundle of the contact + * @param contactsDevice OmemoDevice of the contact * @throws CorruptedOmemoKeyException */ - protected abstract void processBundle(OmemoManager.LoggedInOmemoManager managerGuard, - T_Bundle bundle, - OmemoDevice device) + protected abstract void processBundle(XMPPConnection connection, + OmemoDevice userDevice, + T_Bundle contactsBundle, + OmemoDevice contactsDevice) throws CorruptedOmemoKeyException; /** - * Process a received message. Try to decrypt it in case we are a recipient device. If we are not a recipient - * device, return null. + * Returns true, if a rotation of the signed preKey is necessary. * - * @param contactsDevice OmemoDevice of the sender of the message - * @param omemoElement the encrypted message - * @param information OmemoMessageInformation object which will contain meta data about the decrypted message - * @return decrypted message or null - * @throws NoRawSessionException - * @throws InterruptedException - * @throws SmackException.NoResponseException - * @throws SmackException.NotConnectedException - * @throws CryptoFailedException - * @throws XMPPException.XMPPErrorException - * @throws CorruptedOmemoKeyException + * @param userDevice our OmemoDevice + * @return true if rotation is necessary */ - private Message processReceivingMessage(OmemoManager.LoggedInOmemoManager managerGuard, - OmemoDevice contactsDevice, - OmemoElement omemoElement, - final OmemoMessageInformation information) - throws NoRawSessionException, InterruptedException, SmackException.NoResponseException, - SmackException.NotConnectedException, CryptoFailedException, XMPPException.XMPPErrorException, - CorruptedOmemoKeyException - { - OmemoManager omemoManager = managerGuard.get(); - OmemoDevice userDevice = omemoManager.getOwnDevice(); - - // CHECKSTYLE: OFF - @SuppressWarnings("unchecked") - ArrayList messageRecipientKeys = omemoElement.getHeader().getKeys(); - // CHECKSTYLE: ON - - // Do we have a key with our ID in the message? - for (OmemoKeyElement k : messageRecipientKeys) { - // Only decrypt with our deviceID - if (k.getId() != omemoManager.getDeviceId()) { - continue; - } - - Message decrypted = decryptOmemoMessageElement(managerGuard, contactsDevice, omemoElement, information); - if (contactsDevice.equals(omemoManager.getOwnJid()) && decrypted != null) { - getOmemoStoreBackend().setDateOfLastReceivedMessage(userDevice, contactsDevice, new Date()); - } - return decrypted; + private boolean shouldRotateSignedPreKey(OmemoDevice userDevice) { + if (!OmemoConfiguration.getRenewOldSignedPreKeys()) { + return false; } - LOGGER.log(Level.INFO, "There is no key with our deviceId " + omemoManager.getDeviceId() + ". Silently discard the message."); - return null; + 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; } /** - * Decrypt a given OMEMO encrypted message. Return null, if there is no OMEMO element in the message, - * otherwise try to decrypt the message and return a ClearTextMessage object. + * 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()}! * - * @param managerGuard omemoManager of the receiving device - * @param sender barejid of the sender - * @param message encrypted message - * @return decrypted message or null - * @throws InterruptedException Exception - * @throws SmackException.NoResponseException Exception - * @throws SmackException.NotConnectedException Exception - * @throws CryptoFailedException When the message could not be decrypted. - * @throws XMPPException.XMPPErrorException Exception - * @throws CorruptedOmemoKeyException When the used OMEMO keys are invalid. - * @throws NoRawSessionException When there is no session to decrypt the message with in the double - * ratchet library + * 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#DELETE_STALE_DEVICE_AFTER_HOURS} hours. + * + * @param userDevice our OmemoDevice + * @return our altered deviceList with stale devices marked as inactive. */ - ClearTextMessage processLocalMessage(OmemoManager.LoggedInOmemoManager managerGuard, - BareJid sender, - Message message) - throws InterruptedException, SmackException.NoResponseException, SmackException.NotConnectedException, - CryptoFailedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException, NoRawSessionException - { - if (OmemoManager.stanzaContainsOmemoElement(message)) { - OmemoElement omemoMessageElement = message.getExtension(OmemoElement.NAME_ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); - OmemoMessageInformation info = new OmemoMessageInformation(); - Message decrypted = processReceivingMessage(managerGuard, - new OmemoDevice(sender, omemoMessageElement.getHeader().getSid()), - omemoMessageElement, info); - return new ClearTextMessage(decrypted != null ? decrypted.getBody() : null, message, info); - } else { - LOGGER.log(Level.WARNING, "Stanza does not contain an OMEMO message."); - return null; + private OmemoCachedDeviceList deleteStaleDevices(OmemoDevice userDevice) { + OmemoCachedDeviceList deviceList = getOmemoStoreBackend().loadCachedDeviceList(userDevice); + int maxAgeHours = OmemoConfiguration.getDeleteStaleDevicesAfterHours(); + return removeStaleDevicesFromDeviceList(userDevice, userDevice.getJid(), deviceList, maxAgeHours); + } + + /** + * Return a copy of the contactsDeviceList, but with stale devices marked as inactive. + * Never mark our own device as stale. + * This method ignores {@link OmemoConfiguration#getIgnoreStaleDevices()}! + * + * 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#IGNORE_STALE_DEVICE_AFTER_HOURS} hours. + * + * @param userDevice our OmemoDevice + * @param contact subjects BareJid + * @param contactsDeviceList subjects deviceList + * @return + */ + private OmemoCachedDeviceList ignoreStaleDevices(OmemoDevice userDevice, BareJid contact, OmemoCachedDeviceList contactsDeviceList) { + int maxAgeHours = OmemoConfiguration.getIgnoreStaleDevicesAfterHours(); + return removeStaleDevicesFromDeviceList(userDevice, contact, contactsDeviceList, 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. + */ + private OmemoCachedDeviceList removeStaleDevicesFromDeviceList(OmemoDevice userDevice, + BareJid contact, + OmemoCachedDeviceList contactsDeviceList, + int maxAgeHours) { + OmemoCachedDeviceList deviceList = new OmemoCachedDeviceList(contactsDeviceList); // Don't work on original list. + + for (int deviceId : deviceList.getActiveDevices()) { + OmemoDevice device = new OmemoDevice(contact, deviceId); + + Date lastMessageReceived = getOmemoStoreBackend().getDateOfLastReceivedMessage(userDevice, device); + if (lastMessageReceived == null) { + lastMessageReceived = new Date(); + getOmemoStoreBackend().setDateOfLastReceivedMessage(userDevice, device, lastMessageReceived); + } + + if (isStale(userDevice, device, lastMessageReceived, maxAgeHours)) { + 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 devices) { + if (devices.contains(userDevice)) { + devices.remove(userDevice); } } /** - * Encrypt a clear text message for the given recipient. - * The body of the message will be encrypted. * - * @param managerGuard omemoManager of the sending device - * @param recipient BareJid of the recipient - * @param message message to encrypt. - * @return OmemoMessageElement - * @throws CryptoFailedException - * @throws UndecidedOmemoIdentityException - * @throws NoSuchAlgorithmException + * @param userDevice + * @param subject + * @param lastReceipt + * @param maxAgeHours + * @return */ - OmemoElement processSendingMessage(OmemoManager.LoggedInOmemoManager managerGuard, - BareJid recipient, - Message message) - throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, - SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, - CannotEstablishOmemoSessionException - { - ArrayList recipients = new ArrayList<>(); - recipients.add(recipient); - return processSendingMessage(managerGuard, recipients, message); + static boolean isStale(OmemoDevice userDevice, OmemoDevice subject, Date lastReceipt, int maxAgeHours) { + if (userDevice.equals(subject)) { + return false; + } + + long maxAgeMillis = MILLIS_PER_HOUR * maxAgeHours; + Date now = new Date(); + + return now.getTime() - lastReceipt.getTime() > maxAgeMillis; } /** - * Encrypt a clear text message for the given recipients. - * The body of the message will be encrypted. - * - * @param managerGuard omemoManager of the sending device. - * @param recipients List of BareJids of all recipients - * @param message message to encrypt. - * @return OmemoMessageElement - * @throws CryptoFailedException - * @throws UndecidedOmemoIdentityException - * @throws NoSuchAlgorithmException + * 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! */ - OmemoElement processSendingMessage(OmemoManager.LoggedInOmemoManager managerGuard, - ArrayList recipients, - Message message) - throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, - SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, - CannotEstablishOmemoSessionException - { - OmemoManager omemoManager = managerGuard.get(); - OmemoDevice userDevice = omemoManager.getOwnDevice(); - - CannotEstablishOmemoSessionException sessionException = null; - // Them - The contact wants to read the message on all their devices. - HashMap> receivers = new HashMap<>(); - for (BareJid recipient : recipients) { - try { - buildMissingOmemoSessions(managerGuard, recipient); - } catch (CannotEstablishOmemoSessionException e) { - - if (sessionException == null) { - sessionException = e; - } else { - sessionException.addFailures(e); - } - } + private static final TrustCallback gullibleTrustCallback = new TrustCallback() { + @Override + public TrustState getTrust(OmemoDevice device, OmemoFingerprint fingerprint) { + return TrustState.trusted; } - for (BareJid recipient : recipients) { - CachedDeviceList theirDevices = getOmemoStoreBackend().loadCachedDeviceList(userDevice, recipient); - ArrayList receivingDevices = new ArrayList<>(); - for (int id : theirDevices.getActiveDevices()) { - OmemoDevice recipientDevice = new OmemoDevice(recipient, id); - - if (getOmemoStoreBackend().containsRawSession(userDevice, recipientDevice)) { - receivingDevices.add(recipientDevice); - } - - if (sessionException != null) { - sessionException.addSuccess(recipientDevice); - } - } - - if (!receivingDevices.isEmpty()) { - receivers.put(recipient, receivingDevices); - } + @Override + public void setTrust(OmemoDevice device, OmemoFingerprint fingerprint, TrustState state) { + // Not needed } + }; - // Us - We want to read the message on all of our devices - CachedDeviceList ourDevices = getOmemoStoreBackend().loadCachedDeviceList(userDevice, omemoManager.getOwnJid()); - if (ourDevices == null) { - ourDevices = new CachedDeviceList(); - } - - ArrayList ourReceivingDevices = new ArrayList<>(); - for (int id : ourDevices.getActiveDevices()) { - OmemoDevice remoteUserDevice = new OmemoDevice(omemoManager.getOwnJid(), id); - if (id == omemoManager.getDeviceId()) { - // Don't build session with our exact device. - continue; - } - - Date lastReceived = getOmemoStoreBackend().getDateOfLastReceivedMessage(userDevice, remoteUserDevice); - if (lastReceived == null) { - getOmemoStoreBackend().setDateOfLastReceivedMessage(userDevice, remoteUserDevice, new Date()); - lastReceived = new Date(); - } - - if (OmemoConfiguration.getIgnoreStaleDevices() && System.currentTimeMillis() - lastReceived.getTime() - > 1000L * 60 * 60 * OmemoConfiguration.getIgnoreStaleDevicesAfterHours()) { - LOGGER.log(Level.WARNING, "Refusing to encrypt message for stale device " + remoteUserDevice + - " which was inactive for at least " + OmemoConfiguration.getIgnoreStaleDevicesAfterHours() + - " hours."); - } else { - - if (getOmemoStoreBackend().containsRawSession(userDevice, remoteUserDevice)) { - ourReceivingDevices.add(remoteUserDevice); - } - } - } - - if (!ourReceivingDevices.isEmpty()) { - receivers.put(omemoManager.getOwnJid(), ourReceivingDevices); - } - - if (sessionException != null && sessionException.requiresThrowing()) { - throw sessionException; - } - - return encryptOmemoMessage(managerGuard, receivers, message); - } - - /** - * Decrypt a incoming OmemoMessageElement that was sent by the OmemoDevice 'from'. - * - * @param managerGuard omemoManager of the decrypting device. - * @param from OmemoDevice that sent the message - * @param message Encrypted OmemoMessageElement - * @param information OmemoMessageInformation object which will contain metadata about the encryption - * @return Decrypted message - * @throws CryptoFailedException when decrypting message fails for some reason - * @throws InterruptedException - * @throws CorruptedOmemoKeyException - * @throws XMPPException.XMPPErrorException - * @throws SmackException.NotConnectedException - * @throws SmackException.NoResponseException - * @throws NoRawSessionException - */ - private Message decryptOmemoMessageElement(OmemoManager.LoggedInOmemoManager managerGuard, - OmemoDevice from, - OmemoElement message, - final OmemoMessageInformation information) - throws CryptoFailedException, InterruptedException, CorruptedOmemoKeyException, - XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException, - NoRawSessionException - { - CipherAndAuthTag transportedKey = decryptTransportedOmemoKey(managerGuard, from, message, information); - return OmemoRatchet.decryptMessageElement(message, transportedKey); - } - - /** - * Decrypt a messageKey that was transported in an OmemoElement. - * - * @param managerGuard omemoManager of the receiving device. - * @param sender omemoDevice of the sender. - * @param omemoMessage omemoElement containing the key. - * @param messageInfo omemoMessageInformation that will contain metadata about the encryption. - * @return a CipherAndAuthTag pair - * @throws CryptoFailedException - * @throws NoRawSessionException - * @throws InterruptedException - * @throws CorruptedOmemoKeyException - * @throws XMPPException.XMPPErrorException - * @throws SmackException.NotConnectedException - * @throws SmackException.NoResponseException - */ - private CipherAndAuthTag decryptTransportedOmemoKey(OmemoManager.LoggedInOmemoManager managerGuard, - OmemoDevice sender, - OmemoElement omemoMessage, - OmemoMessageInformation messageInfo) - throws CryptoFailedException, NoRawSessionException, InterruptedException, CorruptedOmemoKeyException, - XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException - { - OmemoManager omemoManager = managerGuard.get(); - OmemoDevice userDevice = omemoManager.getOwnDevice(); - - int preKeyCountBefore = getOmemoStoreBackend().loadOmemoPreKeys(userDevice).size(); - - CipherAndAuthTag cipherAndAuthTag = omemoRatchets.get(managerGuard.get()).retrieveMessageKeyAndAuthTag(sender, omemoMessage); - - messageInfo.setSenderDevice(sender); - messageInfo.setSenderFingerprint(omemoStore.keyUtil().getFingerprintOfIdentityKey( - omemoStore.loadOmemoIdentityKey(userDevice, sender))); - - if (preKeyCountBefore != getOmemoStoreBackend().loadOmemoPreKeys(userDevice).size()) { - LOGGER.log(Level.FINE, "We used up a preKey. Publish new Bundle."); - publishBundle(managerGuard); - } - return cipherAndAuthTag; - } - - /** - * Encrypt the message and return it as an OmemoMessageElement. - * - * @param managerGuard omemoManager of the encrypting device. - * @param recipients List of devices that will be able to decipher the message. - * @param message Clear text message - * - * @throws CryptoFailedException when some cryptographic function fails - * @throws UndecidedOmemoIdentityException when the identity of one or more contacts is undecided - * - * @return OmemoMessageElement - */ - OmemoElement encryptOmemoMessage(OmemoManager.LoggedInOmemoManager managerGuard, - HashMap> recipients, - Message message) - throws CryptoFailedException, UndecidedOmemoIdentityException - { - OmemoMessageBuilder - builder; - try { - builder = new OmemoMessageBuilder<>(managerGuard, getOmemoRatchet(managerGuard.get()), message.getBody()); - } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | - NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { - throw new CryptoFailedException(e); - } - - UndecidedOmemoIdentityException undecided = null; - - for (Map.Entry> entry : recipients.entrySet()) { - for (OmemoDevice c : entry.getValue()) { - try { - builder.addRecipient(c); - } catch (CorruptedOmemoKeyException | CannotEstablishOmemoSessionException e) { - // TODO: How to react? - LOGGER.log(Level.SEVERE, "encryptOmemoMessage failed to establish a session with device " - + c + ": " + e.getMessage()); - } catch (UndecidedOmemoIdentityException e) { - // Collect all undecided devices - if (undecided == null) { - undecided = e; - } else { - undecided.join(e); - } - } - } - } - - if (undecided != null) { - throw undecided; - } - - return builder.finish(); - } - - /** - * Prepares a keyTransportElement with a random aes key and iv. - * - * @param managerGuard omemoManager of the sending device. - * @param recipients recipients of the omemoKeyTransportElement - * @return KeyTransportElement - * @throws CryptoFailedException - * @throws UndecidedOmemoIdentityException - * @throws CorruptedOmemoKeyException - * @throws CannotEstablishOmemoSessionException - */ - OmemoElement prepareOmemoKeyTransportElement(OmemoManager.LoggedInOmemoManager managerGuard, - OmemoDevice... recipients) - throws CryptoFailedException, UndecidedOmemoIdentityException, CorruptedOmemoKeyException, - CannotEstablishOmemoSessionException - { - OmemoMessageBuilder - builder; - try { - builder = new OmemoMessageBuilder<>(managerGuard, getOmemoRatchet(managerGuard.get()), null); - - } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | - NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { - throw new CryptoFailedException(e); - } - - for (OmemoDevice r : recipients) { - builder.addRecipient(r); - } - - return builder.finish(); - } - - /** - * Prepare a KeyTransportElement with aesKey and iv. - * - * @param managerGuard OmemoManager of the sending device. - * @param aesKey AES key - * @param iv initialization vector - * @param recipients recipients - * @return KeyTransportElement - * @throws CryptoFailedException - * @throws UndecidedOmemoIdentityException - * @throws CorruptedOmemoKeyException - * @throws CannotEstablishOmemoSessionException - */ - OmemoElement prepareOmemoKeyTransportElement(OmemoManager.LoggedInOmemoManager managerGuard, - byte[] aesKey, - byte[] iv, - OmemoDevice... recipients) - throws CryptoFailedException, UndecidedOmemoIdentityException, CorruptedOmemoKeyException, - CannotEstablishOmemoSessionException - { - OmemoMessageBuilder - builder; - try { - builder = new OmemoMessageBuilder<>(managerGuard, getOmemoRatchet(managerGuard.get()), aesKey, iv); - - } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | - NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { - throw new CryptoFailedException(e); - } - - for (OmemoDevice r : recipients) { - builder.addRecipient(r); - } - - return builder.finish(); - } - - /** - * Return a new RatchetUpdateMessage. - * - * @param managerGuard omemoManager of the sending device. - * @param recipient recipient - * @param preKeyMessage if true, a new session will be built for this message (useful to repair broken sessions) - * otherwise the message will be encrypted using the existing session. - * @return OmemoRatchetUpdateMessage - * @throws CannotEstablishOmemoSessionException - * @throws CorruptedOmemoKeyException - * @throws CryptoFailedException - * @throws UndecidedOmemoIdentityException - */ - protected Message getOmemoRatchetUpdateMessage(OmemoManager.LoggedInOmemoManager managerGuard, - OmemoDevice recipient, - boolean preKeyMessage) - throws CannotEstablishOmemoSessionException, CorruptedOmemoKeyException, CryptoFailedException, - UndecidedOmemoIdentityException - { - if (preKeyMessage) { - buildSessionWithDevice(managerGuard, recipient, true); - } - - OmemoElement keyTransportElement = prepareOmemoKeyTransportElement(managerGuard, recipient); - Message ratchetUpdateMessage = managerGuard.get().finishMessage(keyTransportElement); - ratchetUpdateMessage.setTo(recipient.getJid()); - - return ratchetUpdateMessage; - } - - /** - * Send an OmemoRatchetUpdateMessage to recipient. If preKeyMessage is true, the message will be encrypted using a - * freshly built session. This can be used to repair broken sessions. - * - * @param managerGuard omemoManager of the sending device. - * @param recipient recipient - * @param preKeyMessage shall this be a preKeyMessage? - * @throws UndecidedOmemoIdentityException - * @throws CorruptedOmemoKeyException - * @throws CryptoFailedException - * @throws CannotEstablishOmemoSessionException - */ - protected void sendOmemoRatchetUpdateMessage(OmemoManager.LoggedInOmemoManager managerGuard, - OmemoDevice recipient, - boolean preKeyMessage) - throws UndecidedOmemoIdentityException, CorruptedOmemoKeyException, CryptoFailedException, - CannotEstablishOmemoSessionException - { - Message ratchetUpdateMessage = getOmemoRatchetUpdateMessage(managerGuard, recipient, preKeyMessage); - - try { - managerGuard.get().getConnection().sendStanza(ratchetUpdateMessage); - - } catch (SmackException.NotConnectedException | InterruptedException e) { - LOGGER.log(Level.WARNING, "sendOmemoRatchetUpdateMessage failed: " + e.getMessage()); - } - } - - /** - * Try to decrypt a mamQueryResult. Note that OMEMO messages can only be decrypted once on a device, so if you - * try to decrypt a message that has been decrypted earlier in time, the decryption will fail. You should handle - * message history locally when using OMEMO, since you cannot rely on MAM. - * - * @param managerGuard omemoManager of the decrypting device. - * @param mamQueryResult mamQueryResult that shall be decrypted. - * @return list of decrypted messages. - * @throws InterruptedException - * @throws XMPPException.XMPPErrorException - * @throws SmackException.NotConnectedException - * @throws SmackException.NoResponseException - */ - List decryptMamQueryResult(OmemoManager.LoggedInOmemoManager managerGuard, - MamManager.MamQueryResult mamQueryResult) - throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, - SmackException.NoResponseException - { - List result = new ArrayList<>(); - for (Forwarded f : mamQueryResult.forwardedMessages) { - if (OmemoManager.stanzaContainsOmemoElement(f.getForwardedStanza())) { - // Decrypt OMEMO messages - try { - result.add(processLocalMessage(managerGuard, f.getForwardedStanza().getFrom().asBareJid(), - (Message) f.getForwardedStanza())); - } catch (NoRawSessionException | CorruptedOmemoKeyException | CryptoFailedException e) { - LOGGER.log(Level.WARNING, "decryptMamQueryResult failed to decrypt message from " - + f.getForwardedStanza().getFrom() + " due to corrupted session/key: " + e.getMessage()); - } - } else { - // Wrap cleartext messages - Message m = (Message) f.getForwardedStanza(); - result.add(new ClearTextMessage(m.getBody(), m, - new OmemoMessageInformation(null, null, - OmemoMessageInformation.CARBON.NONE, false))); - } - } - return result; - } - - /** - * Return the barejid of the user that sent the message inside the MUC. If the message wasn't sent in a MUC, - * return null; - * - * @param managerGuard omemoManager - * @param stanza message - * @return BareJid of the sender. - */ - private static OmemoDevice getSender(OmemoManager.LoggedInOmemoManager managerGuard, - Stanza stanza) { - OmemoElement omemoElement = stanza.getExtension(OmemoElement.NAME_ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); - Jid sender = stanza.getFrom(); - if (isMucMessage(managerGuard, stanza)) { - MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(managerGuard.get().getConnection()); - MultiUserChat muc = mucm.getMultiUserChat(sender.asEntityBareJidIfPossible()); - sender = muc.getOccupant(sender.asEntityFullJidIfPossible()).getJid().asBareJid(); - } - if (sender == null) { - throw new AssertionError("Sender is null."); - } - return new OmemoDevice(sender.asBareJid(), omemoElement.getHeader().getSid()); - } - - /** - * Return true, if the user knows a multiUserChat with a jid matching the sender of the stanza. - * @param managerGuard omemoManager of the user - * @param stanza stanza in question - * @return true if MUC message, otherwise false. - */ - private static boolean isMucMessage(OmemoManager.LoggedInOmemoManager managerGuard, Stanza stanza) { - BareJid sender = stanza.getFrom().asBareJid(); - MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(managerGuard.get().getConnection()); - - return mucm.getJoinedRooms().contains(sender.asEntityBareJidIfPossible()); - } - - @Override - public void onOmemoMessageStanzaReceived(Stanza stanza, OmemoManager.LoggedInOmemoManager managerGuard) { - OmemoManager omemoManager = managerGuard.get(); - Message decrypted; - OmemoElement omemoMessage = stanza.getExtension(OmemoElement.NAME_ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); - OmemoMessageInformation messageInfo = new OmemoMessageInformation(); - MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(omemoManager.getConnection()); - OmemoDevice senderDevice = getSender(managerGuard, stanza); - try { - // Is it a MUC message... - if (isMucMessage(managerGuard, stanza)) { - - MultiUserChat muc = mucm.getMultiUserChat(stanza.getFrom().asEntityBareJidIfPossible()); - if (omemoMessage.isMessageElement()) { - - decrypted = processReceivingMessage(managerGuard, senderDevice, omemoMessage, messageInfo); - if (decrypted != null) { - omemoManager.notifyOmemoMucMessageReceived(muc, senderDevice.getJid(), decrypted.getBody(), - (Message) stanza, null, messageInfo); - } - - } else if (omemoMessage.isKeyTransportElement()) { - - CipherAndAuthTag cipherAndAuthTag = decryptTransportedOmemoKey(managerGuard, senderDevice, - omemoMessage, messageInfo); - if (cipherAndAuthTag != null) { - omemoManager.notifyOmemoMucKeyTransportMessageReceived(muc, senderDevice.getJid(), cipherAndAuthTag, - (Message) stanza, null, messageInfo); - } - } - } - // ... or a normal chat message... - else { - if (omemoMessage.isMessageElement()) { - - decrypted = processReceivingMessage(managerGuard, senderDevice, omemoMessage, messageInfo); - if (decrypted != null) { - omemoManager.notifyOmemoMessageReceived(decrypted.getBody(), (Message) stanza, null, - messageInfo); - } - - } else if (omemoMessage.isKeyTransportElement()) { - - CipherAndAuthTag cipherAndAuthTag = decryptTransportedOmemoKey(managerGuard, senderDevice, omemoMessage, - messageInfo); - if (cipherAndAuthTag != null) { - omemoManager.notifyOmemoKeyTransportMessageReceived(cipherAndAuthTag, (Message) stanza, - null, messageInfo); - } - } - } - - } catch (CryptoFailedException | CorruptedOmemoKeyException | InterruptedException | - SmackException.NotConnectedException | XMPPException.XMPPErrorException | SmackException.NoResponseException e) { - - LOGGER.log(Level.WARNING, "internal omemoMessageListener failed to decrypt incoming OMEMO message: " - + e.getMessage()); - - } catch (NoRawSessionException e) { - try { - LOGGER.log(Level.INFO, "Received message with invalid session from " + - senderDevice + ". Send RatchetUpdateMessage."); - sendOmemoRatchetUpdateMessage(managerGuard, senderDevice, true); - - } catch (UndecidedOmemoIdentityException | CorruptedOmemoKeyException | CannotEstablishOmemoSessionException - | CryptoFailedException e1) { - - LOGGER.log(Level.WARNING, "internal omemoMessageListener failed to establish a session for incoming OMEMO message: " - + e.getMessage()); - } - } - } - - @Override - public void onOmemoCarbonCopyReceived(CarbonExtension.Direction direction, - Message carbonCopy, - Message wrappingMessage, - final OmemoManager.LoggedInOmemoManager managerGuard) - { - OmemoManager omemoManager = managerGuard.get(); - - final OmemoDevice senderDevice = getSender(managerGuard, carbonCopy); - Message decrypted; - MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(omemoManager.getConnection()); - OmemoElement omemoMessage = carbonCopy.getExtension(OmemoElement.NAME_ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); - OmemoMessageInformation messageInfo = new OmemoMessageInformation(); - - if (CarbonExtension.Direction.received.equals(direction)) { - messageInfo.setCarbon(OmemoMessageInformation.CARBON.RECV); - } else { - messageInfo.setCarbon(OmemoMessageInformation.CARBON.SENT); - } - - try { - // Is it a MUC message... - if (isMucMessage(managerGuard, carbonCopy)) { - - MultiUserChat muc = mucm.getMultiUserChat(carbonCopy.getFrom().asEntityBareJidIfPossible()); - if (omemoMessage.isMessageElement()) { - - decrypted = processReceivingMessage(managerGuard, senderDevice, omemoMessage, messageInfo); - if (decrypted != null) { - omemoManager.notifyOmemoMucMessageReceived(muc, senderDevice.getJid(), decrypted.getBody(), - carbonCopy, wrappingMessage, messageInfo); - } - - } else if (omemoMessage.isKeyTransportElement()) { - - CipherAndAuthTag cipherAndAuthTag = decryptTransportedOmemoKey(managerGuard, senderDevice, - omemoMessage, messageInfo); - if (cipherAndAuthTag != null) { - omemoManager.notifyOmemoMucKeyTransportMessageReceived(muc, senderDevice.getJid(), cipherAndAuthTag, - carbonCopy, wrappingMessage, messageInfo); - } - } - } - // ... or a normal chat message... - else { - if (omemoMessage.isMessageElement()) { - - decrypted = processReceivingMessage(managerGuard, senderDevice, omemoMessage, messageInfo); - if (decrypted != null) { - omemoManager.notifyOmemoMessageReceived(decrypted.getBody(), carbonCopy, wrappingMessage, messageInfo); - } - - } else if (omemoMessage.isKeyTransportElement()) { - - CipherAndAuthTag cipherAndAuthTag = decryptTransportedOmemoKey(managerGuard, senderDevice, - omemoMessage, messageInfo); - if (cipherAndAuthTag != null) { - omemoManager.notifyOmemoKeyTransportMessageReceived(cipherAndAuthTag, carbonCopy, - null, messageInfo); - } - } - } - - } catch (CryptoFailedException | CorruptedOmemoKeyException | InterruptedException | - SmackException.NotConnectedException | XMPPException.XMPPErrorException | SmackException.NoResponseException e) { - LOGGER.log(Level.WARNING, "internal omemoMessageListener failed to decrypt incoming OMEMO carbon copy: " - + e.getMessage()); - - } catch (final NoRawSessionException e) { - Async.go(new Runnable() { - @Override - public void run() { - try { - LOGGER.log(Level.INFO, "Received OMEMO carbon copy message with invalid session from " + - senderDevice + ". Send RatchetUpdateMessage."); - sendOmemoRatchetUpdateMessage(managerGuard, senderDevice, true); - - } catch (UndecidedOmemoIdentityException | CorruptedOmemoKeyException | - CannotEstablishOmemoSessionException | CryptoFailedException e1) { - LOGGER.log(Level.WARNING, "internal omemoMessageListener failed to establish a session for incoming OMEMO carbon message: " - + e.getMessage()); - } - } - }); - - } - } - - protected abstract OmemoRatchet - instantiateOmemoRatchet(OmemoManager manager, - OmemoStore store); - - protected OmemoRatchet - getOmemoRatchet(OmemoManager manager) { - OmemoRatchet - omemoRatchet = omemoRatchets.get(manager); - if (omemoRatchet == null) { - omemoRatchet = instantiateOmemoRatchet(manager, omemoStore); - omemoRatchets.put(manager, omemoRatchet); - } - return omemoRatchet; - } } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoServiceOld.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoServiceOld.java new file mode 100644 index 000000000..db46b7e94 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoServiceOld.java @@ -0,0 +1,1323 @@ +package org.jivesoftware.smackx.omemo; + +/** + * + * 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. + */ + +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL; +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID; +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.PEP_NODE_DEVICE_LIST; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Security; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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.XMPPException; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.util.Async; +import org.jivesoftware.smackx.carbons.packet.CarbonExtension; +import org.jivesoftware.smackx.forward.packet.Forwarded; +import org.jivesoftware.smackx.mam.MamManager; +import org.jivesoftware.smackx.muc.MultiUserChat; +import org.jivesoftware.smackx.muc.MultiUserChatManager; +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.OmemoKeyElement; +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.NoRawSessionException; +import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException; +import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList; +import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag; +import org.jivesoftware.smackx.omemo.internal.ClearTextMessage; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; +import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation; +import org.jivesoftware.smackx.omemo.internal.listener.OmemoCarbonCopyStanzaReceivedListener; +import org.jivesoftware.smackx.omemo.internal.listener.OmemoMessageStanzaReceivedListener; +import org.jivesoftware.smackx.omemo.util.OmemoConstants; +import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder; +import org.jivesoftware.smackx.pubsub.LeafNode; +import org.jivesoftware.smackx.pubsub.PayloadItem; +import org.jivesoftware.smackx.pubsub.PubSubException; +import org.jivesoftware.smackx.pubsub.PubSubException.NotAPubSubNodeException; +import org.jivesoftware.smackx.pubsub.PubSubManager; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.jxmpp.jid.BareJid; +import org.jxmpp.jid.Jid; + +/** + * This class contains OMEMO related logic and registers listeners etc. + * + * @param IdentityKeyPair class + * @param IdentityKey class + * @param PreKey class + * @param SignedPreKey class + * @param Session class + * @param Address class + * @param Elliptic Curve PublicKey class + * @param Bundle class + * @param Cipher class + * @author Paul Schaub + */ +public abstract class OmemoServiceOld + implements OmemoCarbonCopyStanzaReceivedListener, OmemoMessageStanzaReceivedListener +{ + static { + Security.addProvider(new BouncyCastleProvider()); + } + + protected static final Logger LOGGER = Logger.getLogger(OmemoService.class.getName()); + + /** + * This is a singleton. + */ + private static OmemoServiceOld INSTANCE; + + protected OmemoStore omemoStore; + protected final HashMap> omemoRatchets = new HashMap<>(); + + /** + * Return the singleton instance of this class. When no instance is set, throw an IllegalStateException instead. + * @return instance. + */ + public static OmemoServiceOld 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(OmemoServiceOld 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 + 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. + */ + public void setOmemoStoreBackend( + OmemoStore 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. + */ + public abstract OmemoStore + createDefaultOmemoStoreBackend(); + + /** + * Create a new OmemoService object. This should only happen once. + * When the service gets created, it tries a placeholder crypto function in order to test, if all necessary + * algorithms are available on the system. + * + * @throws NoSuchPaddingException When no Cipher could be instantiated. + * @throws NoSuchAlgorithmException when no Cipher could be instantiated. + * @throws NoSuchProviderException when BouncyCastle could not be found. + * @throws InvalidAlgorithmParameterException when the Cipher could not be initialized + * @throws InvalidKeyException when the generated key is invalid + * @throws UnsupportedEncodingException when UTF8 is unavailable + * @throws BadPaddingException when cipher.doFinal gets wrong padding + * @throws IllegalBlockSizeException when cipher.doFinal gets wrong Block size. + */ + public OmemoServiceOld() + throws NoSuchPaddingException, InvalidKeyException, UnsupportedEncodingException, IllegalBlockSizeException, + BadPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException + { + // Check availability of algorithms and encodings needed for crypto + checkAvailableAlgorithms(); + } + + /** + * Makes the OmemoManager known to the OmemoService. + * @param manager manager. + */ + void registerManager(OmemoManager manager) { + omemoRatchets.put(manager, instantiateOmemoRatchet(manager, getOmemoStoreBackend())); + } + + /** + * Initialize OMEMO functionality for OmemoManager omemoManager. + * + * @param managerGuard OmemoManager we'd like to initialize. + * @throws InterruptedException + * @throws CorruptedOmemoKeyException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + * @throws PubSubException.NotALeafNodeException + */ + void publish(OmemoManager.LoggedInOmemoManager managerGuard) + throws InterruptedException, CorruptedOmemoKeyException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, SmackException.NoResponseException, + PubSubException.NotALeafNodeException + { + OmemoManager manager = managerGuard.get(); + OmemoDevice userDevice = manager.getOwnDevice(); + + // Create new keys if necessary and publish to the server. + publishBundle(managerGuard); + + // Get fresh device list from server + OmemoCachedDeviceList list = refreshOwnDeviceList(managerGuard); + if (!list.isActive(userDevice.getDeviceId())) { + list.addDevice(userDevice.getDeviceId()); + } + + publishDeviceIds(managerGuard, new OmemoDeviceListElement_VAxolotl(list.getActiveDevices())); + } + + /** + * Test availability of required algorithms. We do this in advance, so we can simplify exception handling later. + * + * @throws NoSuchPaddingException + * @throws UnsupportedEncodingException + * @throws InvalidAlgorithmParameterException + * @throws NoSuchAlgorithmException + * @throws IllegalBlockSizeException + * @throws BadPaddingException + * @throws NoSuchProviderException + * @throws InvalidKeyException + */ + protected static void checkAvailableAlgorithms() + throws NoSuchPaddingException, UnsupportedEncodingException, InvalidAlgorithmParameterException, + NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, NoSuchProviderException, + InvalidKeyException + { + // Test crypto functions. The method below throws an exception, when crypto algorithms are missing. + new OmemoMessageBuilder<>(null, null, ""); + } + + /** + * Publish a bundle to the server. + * + * @param managerGuard OmemoManager + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + * @throws CorruptedOmemoKeyException + * @throws XMPPException.XMPPErrorException + */ + void publishBundle(OmemoManager.LoggedInOmemoManager managerGuard) + throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, + CorruptedOmemoKeyException, XMPPException.XMPPErrorException + { + OmemoManager omemoManager = managerGuard.get(); + OmemoDevice userDevice = omemoManager.getOwnDevice(); + + Date lastSignedPreKeyRenewal = getOmemoStoreBackend().getDateOfLastSignedPreKeyRenewal(userDevice); + if (OmemoConfiguration.getRenewOldSignedPreKeys() && lastSignedPreKeyRenewal != null) { + if (System.currentTimeMillis() - lastSignedPreKeyRenewal.getTime() + > 1000L * 60 * 60 * OmemoConfiguration.getRenewOldSignedPreKeysAfterHours()) { + LOGGER.log(Level.FINE, "Renewing signedPreKey"); + getOmemoStoreBackend().changeSignedPreKey(userDevice); + } + } else { + getOmemoStoreBackend().setDateOfLastSignedPreKeyRenewal(userDevice, new Date()); + } + + // publish + OmemoBundleElement bundleElement = getOmemoStoreBackend().packOmemoBundle(userDevice); + + PubSubManager.getInstance(omemoManager.getConnection(), omemoManager.getOwnJid()) + .tryToPublishAndPossibleAutoCreate( + OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(userDevice.getDeviceId()), + new PayloadItem<>(bundleElement)); + } + + /** + * Publish the given deviceList to the server. + * + * @param managerGuard OmemoManager + * @param deviceList list of deviceIDs + * @throws InterruptedException Exception + * @throws XMPPException.XMPPErrorException Exception + * @throws SmackException.NotConnectedException Exception + * @throws SmackException.NoResponseException Exception + * @throws PubSubException.NotALeafNodeException Exception + */ + static void publishDeviceIds(OmemoManager.LoggedInOmemoManager managerGuard, OmemoDeviceListElement deviceList) + throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, + SmackException.NoResponseException, PubSubException.NotALeafNodeException + { + OmemoManager omemoManager = managerGuard.get(); + PubSubManager.getInstance(omemoManager.getConnection(), omemoManager.getOwnJid()) + .tryToPublishAndPossibleAutoCreate(OmemoConstants.PEP_NODE_DEVICE_LIST, new PayloadItem<>(deviceList)); + } + + /** + * Directly fetch the device list of a contact. + * + * @param managerGuard OmemoManager + * @param contact BareJid of the contact + * @return The OmemoDeviceListElement of the contact + * @throws XMPPException.XMPPErrorException When + * @throws SmackException.NotConnectedException something + * @throws InterruptedException goes + * @throws SmackException.NoResponseException wrong + * @throws PubSubException.NotALeafNodeException when the device lists node is not a LeafNode + * @throws NotAPubSubNodeException + */ + static OmemoDeviceListElement fetchDeviceList(OmemoManager.LoggedInOmemoManager managerGuard, BareJid contact) + throws InterruptedException, SmackException.NotConnectedException, SmackException.NoResponseException, + XMPPException.XMPPErrorException { + OmemoManager omemoManager = managerGuard.get(); + try { + LeafNode node = PubSubManager.getInstance(omemoManager.getConnection(), contact).getLeafNode(PEP_NODE_DEVICE_LIST); + return extractDeviceListFrom(node); + } + + catch (XMPPException.XMPPErrorException e) { + + if (e.getXMPPError().getCondition() == XMPPError.Condition.item_not_found) { + LOGGER.log(Level.WARNING, "Could not refresh own deviceList, because the node did not exist: " + + e.getMessage()); + } + + throw e; + + } catch (PubSubException.NotALeafNodeException e) { + LOGGER.log(Level.WARNING, "Could not refresh own deviceList, because the Node is not a LeafNode: " + + e.getMessage()); + } + + catch (PubSubException.NotAPubSubNodeException e) { + LOGGER.log(Level.WARNING, "Caught a NotAPubSubNodeException when fetching a deviceList node. " + + "This probably means that we're dealing with an ejabberd server and the LeafNode does not exist."); + } + return null; + } + + /** + * Refresh our deviceList from the server. + * + * @param managerGuard omemoManager + * @return true, if we should publish our device list again (because its broken or not existent or + * doesn't contain our id...) + * + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + */ + private OmemoCachedDeviceList refreshOwnDeviceList(OmemoManager.LoggedInOmemoManager managerGuard) + throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, + XMPPException.XMPPErrorException + { + return refreshDeviceList(managerGuard, managerGuard.get().getOwnJid()); + } + + /** + * Refresh the deviceList of contact and merge it with the one stored locally. + * @param managerGuard omemoManager + * @param contact contact + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + */ + OmemoCachedDeviceList refreshDeviceList(OmemoManager.LoggedInOmemoManager managerGuard, BareJid contact) + throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException + { + OmemoManager omemoManager = managerGuard.get(); + OmemoDevice userDevice = omemoManager.getOwnDevice(); + + OmemoDeviceListElement list = null; + try { + list = fetchDeviceList(managerGuard, contact); + } catch (XMPPException.XMPPErrorException e) { + LOGGER.log(Level.WARNING, "Could not refresh DeviceList of " + contact + ".", e); + } + return getOmemoStoreBackend().mergeCachedDeviceList(userDevice, contact, list); + } + + /** + * Fetch the OmemoBundleElement of the contact. + * + * @param managerGuard OmemoManager + * @param contact the contacts BareJid + * @return the OmemoBundleElement of the contact + * @throws XMPPException.XMPPErrorException When + * @throws SmackException.NotConnectedException something + * @throws InterruptedException goes + * @throws SmackException.NoResponseException wrong + * @throws PubSubException.NotALeafNodeException when the bundles node is not a LeafNode + * @throws NotAPubSubNodeException + */ + static OmemoBundleElement fetchBundle(OmemoManager.LoggedInOmemoManager managerGuard, OmemoDevice contact) + throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException, PubSubException.NotALeafNodeException, NotAPubSubNodeException + { + OmemoManager omemoManager = managerGuard.get(); + LeafNode node = PubSubManager.getInstance(omemoManager.getConnection(), contact.getJid()).getLeafNode( + PEP_NODE_BUNDLE_FROM_DEVICE_ID(contact.getDeviceId())); + + if (node == null) { + return null; + } + + List> bundleItems = node.getItems(); + if (bundleItems.isEmpty()) { + return null; + } + + return bundleItems.get(bundleItems.size() - 1).getPayload(); + } + + /** + * Extract the OmemoDeviceListElement of a contact from a node containing his OmemoDeviceListElement. + * This method also ensures, that the node only contains one item. If there are more than one item in the node, + * all items are deleted and the list gets published again. + * + * @param node typically a LeafNode containing the OmemoDeviceListElement of a contact + * @return the extracted OmemoDeviceListElement. + * @throws XMPPException.XMPPErrorException When + * @throws SmackException.NotConnectedException something + * @throws InterruptedException goes + * @throws SmackException.NoResponseException wrong + */ + private static OmemoDeviceListElement extractDeviceListFrom(LeafNode node) + throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException + { + if (node == null) { + LOGGER.log(Level.WARNING, "DeviceListNode is null."); + return null; + } + + List> items = node.getItems(); + if (items.size() > 0) { + + OmemoDeviceListElement listElement = items.get(items.size() - 1).getPayload(); + + if (items.size() > 1) { + node.deleteAllItems(); + node.publish(new PayloadItem<>(listElement)); + } + + return listElement; + } + + Set emptySet = Collections.emptySet(); + return new OmemoDeviceListElement_VAxolotl(emptySet); + } + + /** + * Build sessions for all devices of the contacts in the list, of which we have no session yet. + * When there are devices, this method throws a {@link CannotEstablishOmemoSessionException} which contains all + * those devices, while still establishing sessions with all other devices. + * + * @param managerGuard manager + * @param jids list of BareJids + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + * @throws CannotEstablishOmemoSessionException if we cannot create sessions with some devices. + */ + void buildMissingOmemoSessions(OmemoManager.LoggedInOmemoManager managerGuard, List jids) + throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, + CannotEstablishOmemoSessionException { + + CannotEstablishOmemoSessionException ex = null; + + for (BareJid jid : jids) { + try { + buildMissingOmemoSessions(managerGuard, jid); + } catch (CannotEstablishOmemoSessionException e) { + if (ex == null) { + ex = e; + } else { + ex.addFailures(e); + } + } + } + + if (ex != null) { + throw ex; + } + } + + /** + * Build sessions for all devices of the contact that we do not have a session with yet. + * + * @param managerGuard omemoManager + * @param jid the BareJid of the contact + */ + void buildMissingOmemoSessions(OmemoManager.LoggedInOmemoManager managerGuard, BareJid jid) + throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, + CannotEstablishOmemoSessionException { + OmemoManager omemoManager = managerGuard.get(); + OmemoDevice userDevice = omemoManager.getOwnDevice(); + + OmemoCachedDeviceList devices = getOmemoStoreBackend().loadCachedDeviceList(userDevice, jid); + CannotEstablishOmemoSessionException sessionException = null; + + if (devices == null || devices.getAllDevices().isEmpty()) { + refreshDeviceList(managerGuard, jid); + devices = getOmemoStoreBackend().loadCachedDeviceList(userDevice, jid); + } + + for (int id : devices.getActiveDevices()) { + + OmemoDevice device = new OmemoDevice(jid, id); + if (getOmemoStoreBackend().containsRawSession(userDevice, device)) { + // We have a session already. + continue; + } + + // Build missing session + try { + buildSessionWithDevice(managerGuard, device, false); + } catch (CannotEstablishOmemoSessionException e) { + + if (sessionException == null) { + sessionException = e; + } else { + sessionException.addFailures(e); + } + + } catch (CorruptedOmemoKeyException e) { + CannotEstablishOmemoSessionException fail = + new CannotEstablishOmemoSessionException(device, e); + + if (sessionException == null) { + sessionException = fail; + } else { + sessionException.addFailures(fail); + } + } + } + + if (sessionException != null) { + throw sessionException; + } + } + + /** + * Build an OmemoSession for the given OmemoDevice. + * + * @param managerGuard omemoManager + * @param contactsDevice OmemoDevice + * @param fresh Do we want to build a session even if we already have one? + * @throws CannotEstablishOmemoSessionException when no session could be established + * @throws CorruptedOmemoKeyException when the bundle contained an invalid OMEMO identityKey + */ + public void buildSessionWithDevice(OmemoManager.LoggedInOmemoManager managerGuard, + OmemoDevice contactsDevice, + boolean fresh) + throws CannotEstablishOmemoSessionException, CorruptedOmemoKeyException + { + OmemoManager omemoManager = managerGuard.get(); + OmemoDevice userDevice = omemoManager.getOwnDevice(); + + if (contactsDevice.equals(omemoManager.getOwnDevice())) { + return; + } + + // Do not build sessions with devices we already know... + if (!fresh && getOmemoStoreBackend().containsRawSession(userDevice, contactsDevice)) { + return; + } + + OmemoBundleElement bundle; + try { + bundle = fetchBundle(managerGuard, contactsDevice); + } catch (SmackException | XMPPException.XMPPErrorException | InterruptedException e) { + throw new CannotEstablishOmemoSessionException(contactsDevice, e); + } + + HashMap bundles = getOmemoStoreBackend().keyUtil().BUNDLE.bundles(bundle, contactsDevice); + + // Select random Bundle + int randomIndex = new Random().nextInt(bundles.size()); + T_Bundle randomPreKeyBundle = new ArrayList<>(bundles.values()).get(randomIndex); + // Build raw session + processBundle(managerGuard, randomPreKeyBundle, contactsDevice); + } + + + + /** + * Process a received message. Try to decrypt it in case we are a recipient device. If we are not a recipient + * device, return null. + * + * @param contactsDevice OmemoDevice of the sender of the message + * @param omemoElement the encrypted message + * @param information OmemoMessageInformation object which will contain meta data about the decrypted message + * @return decrypted message or null + * @throws NoRawSessionException + * @throws InterruptedException + * @throws SmackException.NoResponseException + * @throws SmackException.NotConnectedException + * @throws CryptoFailedException + * @throws XMPPException.XMPPErrorException + * @throws CorruptedOmemoKeyException + */ + private Message processReceivingMessage(OmemoManager.LoggedInOmemoManager managerGuard, + OmemoDevice contactsDevice, + OmemoElement omemoElement, + final OmemoMessageInformation information) + throws NoRawSessionException, InterruptedException, SmackException.NoResponseException, + SmackException.NotConnectedException, CryptoFailedException, XMPPException.XMPPErrorException, + CorruptedOmemoKeyException + { + OmemoManager omemoManager = managerGuard.get(); + OmemoDevice userDevice = omemoManager.getOwnDevice(); + + ArrayList messageRecipientKeys = omemoElement.getHeader().getKeys(); + + // Do we have a key with our ID in the message? + for (OmemoKeyElement k : messageRecipientKeys) { + // Only decrypt with our deviceID + if (k.getId() != omemoManager.getDeviceId()) { + continue; + } + + Message decrypted = decryptOmemoMessageElement(managerGuard, contactsDevice, omemoElement, information); + if (contactsDevice.equals(omemoManager.getOwnJid()) && decrypted != null) { + getOmemoStoreBackend().setDateOfLastReceivedMessage(userDevice, contactsDevice, new Date()); + } + return decrypted; + } + + LOGGER.log(Level.INFO, "There is no key with our deviceId " + omemoManager.getDeviceId() + ". Silently discard the message."); + return null; + } + + /** + * Decrypt a given OMEMO encrypted message. Return null, if there is no OMEMO element in the message, + * otherwise try to decrypt the message and return a ClearTextMessage object. + * + * @param managerGuard omemoManager of the receiving device + * @param sender barejid of the sender + * @param message encrypted message + * @return decrypted message or null + * @throws InterruptedException Exception + * @throws SmackException.NoResponseException Exception + * @throws SmackException.NotConnectedException Exception + * @throws CryptoFailedException When the message could not be decrypted. + * @throws XMPPException.XMPPErrorException Exception + * @throws CorruptedOmemoKeyException When the used OMEMO keys are invalid. + * @throws NoRawSessionException When there is no session to decrypt the message with in the double + * ratchet library + */ + ClearTextMessage processLocalMessage(OmemoManager.LoggedInOmemoManager managerGuard, + BareJid sender, + Message message) + throws InterruptedException, SmackException.NoResponseException, SmackException.NotConnectedException, + CryptoFailedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException, NoRawSessionException + { + if (OmemoManager.stanzaContainsOmemoElement(message)) { + OmemoElement omemoMessageElement = message.getExtension(OmemoElement.NAME_ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); + OmemoMessageInformation info = new OmemoMessageInformation(); + Message decrypted = processReceivingMessage(managerGuard, + new OmemoDevice(sender, omemoMessageElement.getHeader().getSid()), + omemoMessageElement, info); + return new ClearTextMessage(decrypted != null ? decrypted.getBody() : null, message, info); + } else { + LOGGER.log(Level.WARNING, "Stanza does not contain an OMEMO message."); + return null; + } + } + + /** + * Encrypt a clear text message for the given recipient. + * The body of the message will be encrypted. + * + * @param managerGuard omemoManager of the sending device + * @param recipient BareJid of the recipient + * @param message message to encrypt. + * @return OmemoMessageElement + * @throws CryptoFailedException + * @throws UndecidedOmemoIdentityException + * @throws NoSuchAlgorithmException + */ + OmemoElement processSendingMessage(OmemoManager.LoggedInOmemoManager managerGuard, + BareJid recipient, + Message message) + throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, + SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, + CannotEstablishOmemoSessionException + { + ArrayList recipients = new ArrayList<>(); + recipients.add(recipient); + return processSendingMessage(managerGuard, recipients, message); + } + + /** + * Encrypt a clear text message for the given recipients. + * The body of the message will be encrypted. + * + * @param managerGuard omemoManager of the sending device. + * @param recipients List of BareJids of all recipients + * @param message message to encrypt. + * @return OmemoMessageElement + * @throws CryptoFailedException + * @throws UndecidedOmemoIdentityException + * @throws NoSuchAlgorithmException + */ + OmemoElement processSendingMessage(OmemoManager.LoggedInOmemoManager managerGuard, + ArrayList recipients, + Message message) + throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, + SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, + CannotEstablishOmemoSessionException + { + OmemoManager omemoManager = managerGuard.get(); + OmemoDevice userDevice = omemoManager.getOwnDevice(); + + CannotEstablishOmemoSessionException sessionException = null; + // Them - The contact wants to read the message on all their devices. + HashMap> receivers = new HashMap<>(); + for (BareJid recipient : recipients) { + try { + buildMissingOmemoSessions(managerGuard, recipient); + } catch (CannotEstablishOmemoSessionException e) { + + if (sessionException == null) { + sessionException = e; + } else { + sessionException.addFailures(e); + } + } + } + + for (BareJid recipient : recipients) { + OmemoCachedDeviceList theirDevices = getOmemoStoreBackend().loadCachedDeviceList(userDevice, recipient); + ArrayList receivingDevices = new ArrayList<>(); + for (int id : theirDevices.getActiveDevices()) { + OmemoDevice recipientDevice = new OmemoDevice(recipient, id); + + if (getOmemoStoreBackend().containsRawSession(userDevice, recipientDevice)) { + receivingDevices.add(recipientDevice); + } + + if (sessionException != null) { + sessionException.addSuccess(recipientDevice); + } + } + + if (!receivingDevices.isEmpty()) { + receivers.put(recipient, receivingDevices); + } + } + + // Us - We want to read the message on all of our devices + OmemoCachedDeviceList ourDevices = getOmemoStoreBackend().loadCachedDeviceList(userDevice, omemoManager.getOwnJid()); + if (ourDevices == null) { + ourDevices = new OmemoCachedDeviceList(); + } + + ArrayList ourReceivingDevices = new ArrayList<>(); + for (int id : ourDevices.getActiveDevices()) { + OmemoDevice remoteUserDevice = new OmemoDevice(omemoManager.getOwnJid(), id); + if (id == omemoManager.getDeviceId()) { + // Don't build session with our exact device. + continue; + } + + Date lastReceived = getOmemoStoreBackend().getDateOfLastReceivedMessage(userDevice, remoteUserDevice); + if (lastReceived == null) { + getOmemoStoreBackend().setDateOfLastReceivedMessage(userDevice, remoteUserDevice, new Date()); + lastReceived = new Date(); + } + + if (OmemoConfiguration.getIgnoreStaleDevices() && System.currentTimeMillis() - lastReceived.getTime() + > 1000L * 60 * 60 * OmemoConfiguration.getIgnoreStaleDevicesAfterHours()) { + LOGGER.log(Level.WARNING, "Refusing to encrypt message for stale device " + remoteUserDevice + + " which was inactive for at least " + OmemoConfiguration.getIgnoreStaleDevicesAfterHours() + + " hours."); + } else { + + if (getOmemoStoreBackend().containsRawSession(userDevice, remoteUserDevice)) { + ourReceivingDevices.add(remoteUserDevice); + } + } + } + + if (!ourReceivingDevices.isEmpty()) { + receivers.put(omemoManager.getOwnJid(), ourReceivingDevices); + } + + if (sessionException != null && sessionException.requiresThrowing()) { + throw sessionException; + } + + return encryptOmemoMessage(managerGuard, receivers, message); + } + + /** + * Decrypt a incoming OmemoMessageElement that was sent by the OmemoDevice 'from'. + * + * @param managerGuard omemoManager of the decrypting device. + * @param from OmemoDevice that sent the message + * @param message Encrypted OmemoMessageElement + * @param information OmemoMessageInformation object which will contain metadata about the encryption + * @return Decrypted message + * @throws CryptoFailedException when decrypting message fails for some reason + * @throws InterruptedException + * @throws CorruptedOmemoKeyException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + * @throws NoRawSessionException + */ + private Message decryptOmemoMessageElement(OmemoManager.LoggedInOmemoManager managerGuard, + OmemoDevice from, + OmemoElement message, + final OmemoMessageInformation information) + throws CryptoFailedException, InterruptedException, CorruptedOmemoKeyException, + XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException, + NoRawSessionException + { + CipherAndAuthTag transportedKey = decryptTransportedOmemoKey(managerGuard, from, message, information); + return OmemoRatchet.decryptMessageElement(message, transportedKey); + } + + /** + * Decrypt a messageKey that was transported in an OmemoElement. + * + * @param managerGuard omemoManager of the receiving device. + * @param sender omemoDevice of the sender. + * @param omemoMessage omemoElement containing the key. + * @param messageInfo omemoMessageInformation that will contain metadata about the encryption. + * @return a CipherAndAuthTag pair + * @throws CryptoFailedException + * @throws NoRawSessionException + * @throws InterruptedException + * @throws CorruptedOmemoKeyException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + */ + private CipherAndAuthTag decryptTransportedOmemoKey(OmemoManager.LoggedInOmemoManager managerGuard, + OmemoDevice sender, + OmemoElement omemoMessage, + OmemoMessageInformation messageInfo) + throws CryptoFailedException, NoRawSessionException, InterruptedException, CorruptedOmemoKeyException, + XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException + { + OmemoManager omemoManager = managerGuard.get(); + OmemoDevice userDevice = omemoManager.getOwnDevice(); + + int preKeyCountBefore = getOmemoStoreBackend().loadOmemoPreKeys(userDevice).size(); + + CipherAndAuthTag cipherAndAuthTag = omemoRatchets.get(managerGuard.get()).retrieveMessageKeyAndAuthTag(sender, omemoMessage); + + messageInfo.setSenderDevice(sender); + messageInfo.setSenderFingerprint(omemoStore.keyUtil().getFingerprintOfIdentityKey( + omemoStore.loadOmemoIdentityKey(userDevice, sender))); + + if (preKeyCountBefore != getOmemoStoreBackend().loadOmemoPreKeys(userDevice).size()) { + LOGGER.log(Level.FINE, "We used up a preKey. Publish new Bundle."); + publishBundle(managerGuard); + } + return cipherAndAuthTag; + } + + /** + * Encrypt the message and return it as an OmemoMessageElement. + * + * @param managerGuard omemoManager of the encrypting device. + * @param recipients List of devices that will be able to decipher the message. + * @param message Clear text message + * + * @throws CryptoFailedException when some cryptographic function fails + * @throws UndecidedOmemoIdentityException when the identity of one or more contacts is undecided + * + * @return OmemoMessageElement + */ + OmemoElement encryptOmemoMessage(OmemoManager.LoggedInOmemoManager managerGuard, + HashMap> recipients, + Message message) + throws CryptoFailedException, UndecidedOmemoIdentityException + { + OmemoMessageBuilder + builder; + try { + builder = new OmemoMessageBuilder<>(managerGuard, getOmemoRatchet(managerGuard.get()), message.getBody()); + } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | + NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { + throw new CryptoFailedException(e); + } + + UndecidedOmemoIdentityException undecided = null; + + for (Map.Entry> entry : recipients.entrySet()) { + for (OmemoDevice c : entry.getValue()) { + try { + builder.addRecipient(c); + } catch (CorruptedOmemoKeyException | CannotEstablishOmemoSessionException e) { + // TODO: How to react? + LOGGER.log(Level.SEVERE, "encryptOmemoMessage failed to establish a session with device " + + c + ": " + e.getMessage()); + } catch (UndecidedOmemoIdentityException e) { + // Collect all undecided devices + if (undecided == null) { + undecided = e; + } else { + undecided.join(e); + } + } + } + } + + if (undecided != null) { + throw undecided; + } + + return builder.finish(); + } + + /** + * Prepares a keyTransportElement with a random aes key and iv. + * + * @param managerGuard omemoManager of the sending device. + * @param recipients recipients of the omemoKeyTransportElement + * @return KeyTransportElement + * @throws CryptoFailedException + * @throws UndecidedOmemoIdentityException + * @throws CorruptedOmemoKeyException + * @throws CannotEstablishOmemoSessionException + */ + OmemoElement prepareOmemoKeyTransportElement(OmemoManager.LoggedInOmemoManager managerGuard, + OmemoDevice... recipients) + throws CryptoFailedException, UndecidedOmemoIdentityException, CorruptedOmemoKeyException, + CannotEstablishOmemoSessionException + { + OmemoMessageBuilder + builder; + try { + builder = new OmemoMessageBuilder<>(managerGuard, getOmemoRatchet(managerGuard.get()), null); + + } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | + NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { + throw new CryptoFailedException(e); + } + + for (OmemoDevice r : recipients) { + builder.addRecipient(r); + } + + return builder.finish(); + } + + /** + * Prepare a KeyTransportElement with aesKey and iv. + * + * @param managerGuard OmemoManager of the sending device. + * @param aesKey AES key + * @param iv initialization vector + * @param recipients recipients + * @return KeyTransportElement + * @throws CryptoFailedException + * @throws UndecidedOmemoIdentityException + * @throws CorruptedOmemoKeyException + * @throws CannotEstablishOmemoSessionException + */ + OmemoElement prepareOmemoKeyTransportElement(OmemoManager.LoggedInOmemoManager managerGuard, + byte[] aesKey, + byte[] iv, + OmemoDevice... recipients) + throws CryptoFailedException, UndecidedOmemoIdentityException, CorruptedOmemoKeyException, + CannotEstablishOmemoSessionException + { + OmemoMessageBuilder + builder; + try { + builder = new OmemoMessageBuilder<>(managerGuard, getOmemoRatchet(managerGuard.get()), aesKey, iv); + + } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | + NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { + throw new CryptoFailedException(e); + } + + for (OmemoDevice r : recipients) { + builder.addRecipient(r); + } + + return builder.finish(); + } + + /** + * Return a new RatchetUpdateMessage. + * + * @param managerGuard omemoManager of the sending device. + * @param recipient recipient + * @param preKeyMessage if true, a new session will be built for this message (useful to repair broken sessions) + * otherwise the message will be encrypted using the existing session. + * @return OmemoRatchetUpdateMessage + * @throws CannotEstablishOmemoSessionException + * @throws CorruptedOmemoKeyException + * @throws CryptoFailedException + * @throws UndecidedOmemoIdentityException + */ + protected Message getOmemoRatchetUpdateMessage(OmemoManager.LoggedInOmemoManager managerGuard, + OmemoDevice recipient, + boolean preKeyMessage) + throws CannotEstablishOmemoSessionException, CorruptedOmemoKeyException, CryptoFailedException, + UndecidedOmemoIdentityException + { + if (preKeyMessage) { + buildSessionWithDevice(managerGuard, recipient, true); + } + + OmemoElement keyTransportElement = prepareOmemoKeyTransportElement(managerGuard, recipient); + Message ratchetUpdateMessage = managerGuard.get().finishMessage(keyTransportElement); + ratchetUpdateMessage.setTo(recipient.getJid()); + + return ratchetUpdateMessage; + } + + /** + * Send an OmemoRatchetUpdateMessage to recipient. If preKeyMessage is true, the message will be encrypted using a + * freshly built session. This can be used to repair broken sessions. + * + * @param managerGuard omemoManager of the sending device. + * @param recipient recipient + * @param preKeyMessage shall this be a preKeyMessage? + * @throws UndecidedOmemoIdentityException + * @throws CorruptedOmemoKeyException + * @throws CryptoFailedException + * @throws CannotEstablishOmemoSessionException + */ + protected void sendOmemoRatchetUpdateMessage(OmemoManager.LoggedInOmemoManager managerGuard, + OmemoDevice recipient, + boolean preKeyMessage) + throws UndecidedOmemoIdentityException, CorruptedOmemoKeyException, CryptoFailedException, + CannotEstablishOmemoSessionException + { + Message ratchetUpdateMessage = getOmemoRatchetUpdateMessage(managerGuard, recipient, preKeyMessage); + + try { + managerGuard.get().getConnection().sendStanza(ratchetUpdateMessage); + + } catch (SmackException.NotConnectedException | InterruptedException e) { + LOGGER.log(Level.WARNING, "sendOmemoRatchetUpdateMessage failed: " + e.getMessage()); + } + } + + /** + * Try to decrypt a mamQueryResult. Note that OMEMO messages can only be decrypted once on a device, so if you + * try to decrypt a message that has been decrypted earlier in time, the decryption will fail. You should handle + * message history locally when using OMEMO, since you cannot rely on MAM. + * + * @param managerGuard omemoManager of the decrypting device. + * @param mamQueryResult mamQueryResult that shall be decrypted. + * @return list of decrypted messages. + * @throws InterruptedException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + */ + List decryptMamQueryResult(OmemoManager.LoggedInOmemoManager managerGuard, + MamManager.MamQueryResult mamQueryResult) + throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, + SmackException.NoResponseException + { + List result = new ArrayList<>(); + for (Forwarded f : mamQueryResult.forwardedMessages) { + if (OmemoManager.stanzaContainsOmemoElement(f.getForwardedStanza())) { + // Decrypt OMEMO messages + try { + result.add(processLocalMessage(managerGuard, f.getForwardedStanza().getFrom().asBareJid(), + (Message) f.getForwardedStanza())); + } catch (NoRawSessionException | CorruptedOmemoKeyException | CryptoFailedException e) { + LOGGER.log(Level.WARNING, "decryptMamQueryResult failed to decrypt message from " + + f.getForwardedStanza().getFrom() + " due to corrupted session/key: " + e.getMessage()); + } + } else { + // Wrap cleartext messages + Message m = (Message) f.getForwardedStanza(); + result.add(new ClearTextMessage(m.getBody(), m, + new OmemoMessageInformation(null, null, + OmemoMessageInformation.CARBON.NONE, false))); + } + } + return result; + } + + /** + * Return the barejid of the user that sent the message inside the MUC. If the message wasn't sent in a MUC, + * return null; + * + * @param managerGuard omemoManager + * @param stanza message + * @return BareJid of the sender. + */ + private static OmemoDevice getSender(OmemoManager.LoggedInOmemoManager managerGuard, + Stanza stanza) { + OmemoElement omemoElement = stanza.getExtension(OmemoElement.NAME_ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); + Jid sender = stanza.getFrom(); + if (isMucMessage(managerGuard, stanza)) { + MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(managerGuard.get().getConnection()); + MultiUserChat muc = mucm.getMultiUserChat(sender.asEntityBareJidIfPossible()); + sender = muc.getOccupant(sender.asEntityFullJidIfPossible()).getJid().asBareJid(); + } + if (sender == null) { + throw new AssertionError("Sender is null."); + } + return new OmemoDevice(sender.asBareJid(), omemoElement.getHeader().getSid()); + } + + /** + * Return true, if the user knows a multiUserChat with a jid matching the sender of the stanza. + * @param managerGuard omemoManager of the user + * @param stanza stanza in question + * @return true if MUC message, otherwise false. + */ + private static boolean isMucMessage(OmemoManager.LoggedInOmemoManager managerGuard, Stanza stanza) { + BareJid sender = stanza.getFrom().asBareJid(); + MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(managerGuard.get().getConnection()); + + return mucm.getJoinedRooms().contains(sender.asEntityBareJidIfPossible()); + } + + @Override + public void onOmemoMessageStanzaReceived(Stanza stanza, OmemoManager.LoggedInOmemoManager managerGuard) { + OmemoManager omemoManager = managerGuard.get(); + Message decrypted; + OmemoElement omemoMessage = stanza.getExtension(OmemoElement.NAME_ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); + OmemoMessageInformation messageInfo = new OmemoMessageInformation(); + MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(omemoManager.getConnection()); + OmemoDevice senderDevice = getSender(managerGuard, stanza); + try { + // Is it a MUC message... + if (isMucMessage(managerGuard, stanza)) { + + MultiUserChat muc = mucm.getMultiUserChat(stanza.getFrom().asEntityBareJidIfPossible()); + if (omemoMessage.isMessageElement()) { + + decrypted = processReceivingMessage(managerGuard, senderDevice, omemoMessage, messageInfo); + if (decrypted != null) { + omemoManager.notifyOmemoMucMessageReceived(muc, senderDevice.getJid(), decrypted.getBody(), + (Message) stanza, null, messageInfo); + } + + } else if (omemoMessage.isKeyTransportElement()) { + + CipherAndAuthTag cipherAndAuthTag = decryptTransportedOmemoKey(managerGuard, senderDevice, + omemoMessage, messageInfo); + if (cipherAndAuthTag != null) { + omemoManager.notifyOmemoMucKeyTransportMessageReceived(muc, senderDevice.getJid(), cipherAndAuthTag, + (Message) stanza, null, messageInfo); + } + } + } + // ... or a normal chat message... + else { + if (omemoMessage.isMessageElement()) { + + decrypted = processReceivingMessage(managerGuard, senderDevice, omemoMessage, messageInfo); + if (decrypted != null) { + omemoManager.notifyOmemoMessageReceived(decrypted.getBody(), (Message) stanza, null, + messageInfo); + } + + } else if (omemoMessage.isKeyTransportElement()) { + + CipherAndAuthTag cipherAndAuthTag = decryptTransportedOmemoKey(managerGuard, senderDevice, omemoMessage, + messageInfo); + if (cipherAndAuthTag != null) { + omemoManager.notifyOmemoKeyTransportMessageReceived(cipherAndAuthTag, (Message) stanza, + null, messageInfo); + } + } + } + + } catch (CryptoFailedException | CorruptedOmemoKeyException | InterruptedException | + SmackException.NotConnectedException | XMPPException.XMPPErrorException | SmackException.NoResponseException e) { + + LOGGER.log(Level.WARNING, "internal omemoMessageListener failed to decrypt incoming OMEMO message: " + + e.getMessage()); + + } catch (NoRawSessionException e) { + try { + LOGGER.log(Level.INFO, "Received message with invalid session from " + + senderDevice + ". Send RatchetUpdateMessage."); + sendOmemoRatchetUpdateMessage(managerGuard, senderDevice, true); + + } catch (UndecidedOmemoIdentityException | CorruptedOmemoKeyException | CannotEstablishOmemoSessionException + | CryptoFailedException e1) { + + LOGGER.log(Level.WARNING, "internal omemoMessageListener failed to establish a session for incoming OMEMO message: " + + e.getMessage()); + } + } + } + + @Override + public void onOmemoCarbonCopyReceived(CarbonExtension.Direction direction, + Message carbonCopy, + Message wrappingMessage, + final OmemoManager.LoggedInOmemoManager managerGuard) + { + OmemoManager omemoManager = managerGuard.get(); + + final OmemoDevice senderDevice = getSender(managerGuard, carbonCopy); + Message decrypted; + MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(omemoManager.getConnection()); + OmemoElement omemoMessage = carbonCopy.getExtension(OmemoElement.NAME_ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); + OmemoMessageInformation messageInfo = new OmemoMessageInformation(); + + if (CarbonExtension.Direction.received.equals(direction)) { + messageInfo.setCarbon(OmemoMessageInformation.CARBON.RECV); + } else { + messageInfo.setCarbon(OmemoMessageInformation.CARBON.SENT); + } + + try { + // Is it a MUC message... + if (isMucMessage(managerGuard, carbonCopy)) { + + MultiUserChat muc = mucm.getMultiUserChat(carbonCopy.getFrom().asEntityBareJidIfPossible()); + if (omemoMessage.isMessageElement()) { + + decrypted = processReceivingMessage(managerGuard, senderDevice, omemoMessage, messageInfo); + if (decrypted != null) { + omemoManager.notifyOmemoMucMessageReceived(muc, senderDevice.getJid(), decrypted.getBody(), + carbonCopy, wrappingMessage, messageInfo); + } + + } else if (omemoMessage.isKeyTransportElement()) { + + CipherAndAuthTag cipherAndAuthTag = decryptTransportedOmemoKey(managerGuard, senderDevice, + omemoMessage, messageInfo); + if (cipherAndAuthTag != null) { + omemoManager.notifyOmemoMucKeyTransportMessageReceived(muc, senderDevice.getJid(), cipherAndAuthTag, + carbonCopy, wrappingMessage, messageInfo); + } + } + } + // ... or a normal chat message... + else { + if (omemoMessage.isMessageElement()) { + + decrypted = processReceivingMessage(managerGuard, senderDevice, omemoMessage, messageInfo); + if (decrypted != null) { + omemoManager.notifyOmemoMessageReceived(decrypted.getBody(), carbonCopy, wrappingMessage, messageInfo); + } + + } else if (omemoMessage.isKeyTransportElement()) { + + CipherAndAuthTag cipherAndAuthTag = decryptTransportedOmemoKey(managerGuard, senderDevice, + omemoMessage, messageInfo); + if (cipherAndAuthTag != null) { + omemoManager.notifyOmemoKeyTransportMessageReceived(cipherAndAuthTag, carbonCopy, + null, messageInfo); + } + } + } + + } catch (CryptoFailedException | CorruptedOmemoKeyException | InterruptedException | + SmackException.NotConnectedException | XMPPException.XMPPErrorException | SmackException.NoResponseException e) { + LOGGER.log(Level.WARNING, "internal omemoMessageListener failed to decrypt incoming OMEMO carbon copy: " + + e.getMessage()); + + } catch (final NoRawSessionException e) { + Async.go(new Runnable() { + @Override + public void run() { + try { + LOGGER.log(Level.INFO, "Received OMEMO carbon copy message with invalid session from " + + senderDevice + ". Send RatchetUpdateMessage."); + sendOmemoRatchetUpdateMessage(managerGuard, senderDevice, true); + + } catch (UndecidedOmemoIdentityException | CorruptedOmemoKeyException | + CannotEstablishOmemoSessionException | CryptoFailedException e1) { + LOGGER.log(Level.WARNING, "internal omemoMessageListener failed to establish a session for incoming OMEMO carbon message: " + + e.getMessage()); + } + } + }); + + } + } + + protected abstract OmemoRatchet + instantiateOmemoRatchet(OmemoManager manager, + OmemoStore store); + + protected OmemoRatchet + getOmemoRatchet(OmemoManager manager) { + OmemoRatchet + omemoRatchet = omemoRatchets.get(manager); + if (omemoRatchet == null) { + omemoRatchet = instantiateOmemoRatchet(manager, omemoStore); + omemoRatchets.put(manager, omemoRatchet); + } + return omemoRatchet; + } +} + diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoStore.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoStore.java index 9112f6e29..118a9ec8e 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoStore.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoStore.java @@ -30,7 +30,8 @@ import org.jivesoftware.smackx.omemo.element.OmemoBundleElement_VAxolotl; import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement; import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; -import org.jivesoftware.smackx.omemo.internal.CachedDeviceList; +import org.jivesoftware.smackx.omemo.exceptions.NoIdentityKeyException; +import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil; @@ -81,12 +82,12 @@ public abstract class OmemoStore getDeviceIds() { return deviceIds; } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListElement_VAxolotl.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListElement_VAxolotl.java index daa21d217..a1c525f57 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListElement_VAxolotl.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListElement_VAxolotl.java @@ -20,6 +20,8 @@ import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_ import java.util.Set; +import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList; + /** * The OMEMO device list element with the legacy Axolotl namespace. * @@ -31,6 +33,10 @@ public class OmemoDeviceListElement_VAxolotl extends OmemoDeviceListElement { super(deviceIds); } + public OmemoDeviceListElement_VAxolotl(OmemoCachedDeviceList cachedList) { + super(cachedList); + } + @Override public String getNamespace() { return OMEMO_NAMESPACE_V_AXOLOTL; diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement.java index 37cedf64e..37e4b3d19 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement.java @@ -17,6 +17,7 @@ package org.jivesoftware.smackx.omemo.element; import java.util.ArrayList; +import java.util.List; import org.jivesoftware.smack.packet.NamedElement; import org.jivesoftware.smack.util.XmlStringBuilder; @@ -33,10 +34,10 @@ public abstract class OmemoHeaderElement implements NamedElement { public static final String ATTR_IV = "iv"; private final int sid; - private final ArrayList keys; + private final List keys; private final byte[] iv; - public OmemoHeaderElement(int sid, ArrayList keys, byte[] iv) { + public OmemoHeaderElement(int sid, List keys, byte[] iv) { this.sid = sid; this.keys = keys; this.iv = iv; @@ -79,4 +80,4 @@ public abstract class OmemoHeaderElement implements NamedElement { } -} \ No newline at end of file +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement_VAxolotl.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement_VAxolotl.java index 018a9acaf..7ee3a60bb 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement_VAxolotl.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement_VAxolotl.java @@ -16,11 +16,11 @@ */ package org.jivesoftware.smackx.omemo.element; -import java.util.ArrayList; +import java.util.List; public class OmemoHeaderElement_VAxolotl extends OmemoHeaderElement { - public OmemoHeaderElement_VAxolotl(int sid, ArrayList keys, byte[] iv) { + public OmemoHeaderElement_VAxolotl(int sid, List keys, byte[] iv) { super(sid, keys, iv); } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoKeyElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoKeyElement.java index 610c941b5..cb7a4a461 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoKeyElement.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoKeyElement.java @@ -65,4 +65,4 @@ public class OmemoKeyElement implements NamedElement { sb.closeElement(this); return sb; } -} \ No newline at end of file +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CannotEstablishOmemoSessionException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CannotEstablishOmemoSessionException.java index df98a7248..d8b25bbab 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CannotEstablishOmemoSessionException.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CannotEstablishOmemoSessionException.java @@ -35,10 +35,6 @@ public class CannotEstablishOmemoSessionException extends Exception { private final HashMap> failures = new HashMap<>(); private final HashMap> successes = new HashMap<>(); - public CannotEstablishOmemoSessionException() { - super(); - } - public CannotEstablishOmemoSessionException(OmemoDevice failed, Throwable reason) { super(); getFailsOfContact(failed.getJid()).put(failed, reason); diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CryptoFailedException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CryptoFailedException.java index 7212bf883..4e452af44 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CryptoFailedException.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CryptoFailedException.java @@ -16,6 +16,10 @@ */ package org.jivesoftware.smackx.omemo.exceptions; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * Exception gets thrown when some cryptographic function failed. * @@ -25,11 +29,18 @@ public class CryptoFailedException extends Exception { private static final long serialVersionUID = 3466888654338119924L; + private final ArrayList exceptions = new ArrayList<>(); + public CryptoFailedException(String message) { super(message); } public CryptoFailedException(Exception e) { super(e); + exceptions.add(e); + } + + public List getExceptions() { + return Collections.unmodifiableList(exceptions); } } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/MultipleIOException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoIdentityKeyException.java similarity index 59% rename from smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/MultipleIOException.java rename to smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoIdentityKeyException.java index 728f68dfb..2f2f9dee3 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/MultipleIOException.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoIdentityKeyException.java @@ -16,25 +16,18 @@ */ package org.jivesoftware.smackx.omemo.exceptions; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; - -public class MultipleIOException extends IOException { +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; +public class NoIdentityKeyException extends Exception { private static final long serialVersionUID = 1L; - private final ArrayList exceptions = new ArrayList<>(); + private final OmemoDevice device; - public MultipleIOException(IOException... exceptions) { - this.exceptions.addAll(Arrays.asList(exceptions)); + public NoIdentityKeyException(OmemoDevice device) { + this.device = device; } - public void addException(IOException e) { - exceptions.add(e); - } - - public ArrayList getExceptions() { - return exceptions; + public OmemoDevice getDevice() { + return device; } } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UndecidedOmemoIdentityException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UndecidedOmemoIdentityException.java index b477f539a..2105d588a 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UndecidedOmemoIdentityException.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UndecidedOmemoIdentityException.java @@ -16,6 +16,7 @@ */ package org.jivesoftware.smackx.omemo.exceptions; +import java.util.Collection; import java.util.HashSet; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; @@ -34,6 +35,11 @@ public class UndecidedOmemoIdentityException extends Exception { this.devices.add(contact); } + public UndecidedOmemoIdentityException(Collection devices) { + super(); + this.devices.addAll(devices); + } + /** * Return the HashSet of undecided devices. * diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UntrustedOmemoIdentityException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UntrustedOmemoIdentityException.java index 91d4fe3c3..65c128225 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UntrustedOmemoIdentityException.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UntrustedOmemoIdentityException.java @@ -20,8 +20,9 @@ import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; /** - * Exception that gets thrown when we try to decrypt a message which contains an identityKey that differs from the one - * we previously trusted. + * Exception that gets thrown when we try to en-/decrypt a message for an untrusted contact. + * This might either be because the user actively untrusted a device, or we receive a message from a contact + * which contains an identityKey that differs from the one the user trusted. */ public class UntrustedOmemoIdentityException extends Exception { @@ -30,7 +31,8 @@ public class UntrustedOmemoIdentityException extends Exception { private final OmemoFingerprint trustedKey, untrustedKey; /** - * Constructor. + * Constructor for when we receive a message with an identityKey different from the one we trusted. + * * @param device device which sent the message. * @param fpTrusted fingerprint of the identityKey we previously had and trusted. * @param fpUntrusted fingerprint of the new key which is untrusted. @@ -42,6 +44,16 @@ public class UntrustedOmemoIdentityException extends Exception { this.untrustedKey = fpUntrusted; } + /** + * Constructor for when encryption fails because the user untrusted a recipients device. + * + * @param device device the user wants to encrypt for, but which has been marked as untrusted. + * @param untrustedKey fingerprint of that device. + */ + public UntrustedOmemoIdentityException(OmemoDevice device, OmemoFingerprint untrustedKey) { + this(device, null, untrustedKey); + } + /** * Return the device which sent the message. * @return omemoDevice. @@ -52,6 +64,7 @@ public class UntrustedOmemoIdentityException extends Exception { /** * Return the fingerprint of the key we expected. + * This might return null in case this exception got thrown during encryption process. * @return */ public OmemoFingerprint getTrustedFingerprint() { diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/ClearTextMessage.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/ClearTextMessage.java deleted file mode 100644 index 928e4fb5f..000000000 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/ClearTextMessage.java +++ /dev/null @@ -1,63 +0,0 @@ -/** - * - * 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.internal; - -import org.jivesoftware.smack.packet.Message; - -/** - * Class that bundles a decrypted message together with the original message and some information about the encryption. - * - * @author Paul Schaub - */ -public class ClearTextMessage { - private final String body; - private final Message encryptedMessage; - private final OmemoMessageInformation messageInformation; - - public ClearTextMessage(String message, Message original, OmemoMessageInformation messageInfo) { - this.body = message; - this.encryptedMessage = original; - this.messageInformation = messageInfo; - } - - /** - * Return the body of the decrypted message. - * - * @return plaintext body - */ - public String getBody() { - return body; - } - - /** - * Return the original Message object. - * - * @return original message - */ - public Message getOriginalMessage() { - return encryptedMessage; - } - - /** - * Return the OmemoMessageInformation. - * - * @return omemoMessageInformation - */ - public OmemoMessageInformation getMessageInformation() { - return messageInformation; - } -} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CachedDeviceList.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoCachedDeviceList.java similarity index 91% rename from smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CachedDeviceList.java rename to smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoCachedDeviceList.java index 7efca2ce5..88bb2e34c 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CachedDeviceList.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoCachedDeviceList.java @@ -32,24 +32,24 @@ import java.util.Set; * * @author Paul Schaub */ -public class CachedDeviceList implements Serializable { +public class OmemoCachedDeviceList implements Serializable { private static final long serialVersionUID = 3153579238321261203L; private final Set activeDevices; private final Set inactiveDevices; - public CachedDeviceList() { + public OmemoCachedDeviceList() { this.activeDevices = new HashSet<>(); this.inactiveDevices = new HashSet<>(); } - public CachedDeviceList(Set activeDevices, Set inactiveDevices) { + public OmemoCachedDeviceList(Set activeDevices, Set inactiveDevices) { this(); this.activeDevices.addAll(activeDevices); this.inactiveDevices.addAll(inactiveDevices); } - public CachedDeviceList(CachedDeviceList original) { + public OmemoCachedDeviceList(OmemoCachedDeviceList original) { this(original.getActiveDevices(), original.getInactiveDevices()); } @@ -109,6 +109,11 @@ public class CachedDeviceList implements Serializable { inactiveDevices.remove(deviceId); } + public void addInactiveDevice(int deviceId) { + activeDevices.remove(deviceId); + inactiveDevices.add(deviceId); + } + /** * Returns true if deviceId is either in the list of active or inactive devices. * diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoDevice.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoDevice.java index 81102ce25..05a07594a 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoDevice.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoDevice.java @@ -16,6 +16,8 @@ */ package org.jivesoftware.smackx.omemo.internal; +import org.jivesoftware.smackx.omemo.util.OmemoConstants; + import org.jxmpp.jid.BareJid; /** @@ -74,4 +76,12 @@ public class OmemoDevice { i = jid.hashCode() + deviceId; return i.hashCode(); } + + /** + * Return the name of the PubSub {@link org.jivesoftware.smackx.pubsub.LeafNode} of this device. + * @return node name. + */ + public String getBundleNodeName() { + return OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(getDeviceId()); + } } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilder.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilder.java index f79fa5b6a..7b66dad5b 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilder.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilder.java @@ -37,21 +37,21 @@ import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; -import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.util.StringUtils; -import org.jivesoftware.smackx.omemo.OmemoManager; import org.jivesoftware.smackx.omemo.OmemoRatchet; +import org.jivesoftware.smackx.omemo.OmemoService; import org.jivesoftware.smackx.omemo.element.OmemoElement; import org.jivesoftware.smackx.omemo.element.OmemoElement_VAxolotl; import org.jivesoftware.smackx.omemo.element.OmemoHeaderElement_VAxolotl; import org.jivesoftware.smackx.omemo.element.OmemoKeyElement; -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.UndecidedOmemoIdentityException; +import org.jivesoftware.smackx.omemo.exceptions.UntrustedOmemoIdentityException; import org.jivesoftware.smackx.omemo.internal.CiphertextTuple; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; +import org.jivesoftware.smackx.omemo.trust.TrustCallback; /** @@ -69,22 +69,27 @@ import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; * @author Paul Schaub */ public class OmemoMessageBuilder { - private final OmemoRatchet ratchet; - private final OmemoManager.LoggedInOmemoManager managerGuard; - private byte[] messageKey = generateKey(); - private byte[] initializationVector = generateIv(); + private final OmemoDevice userDevice; + private final OmemoRatchet ratchet; + private final TrustCallback trustCallback; + + private byte[] messageKey; + private final byte[] initializationVector; private byte[] ciphertextMessage; private final ArrayList keys = new ArrayList<>(); /** - * Create a OmemoMessageBuilder. + * Create an OmemoMessageBuilder. + * + * @param userDevice our OmemoDevice + * @param callback trustCallback for querying trust decisions + * @param ratchet our OmemoRatchet + * @param aesKey aes message key used for message encryption + * @param iv initialization vector used for message encryption + * @param message message we want to send * - * @param managerGuard OmemoManager of our device. - * @param ratchet OmemoRatchet. - * @param aesKey AES key that will be transported to the recipient. This is used eg. to encrypt the body. - * @param iv IV * @throws NoSuchPaddingException * @throws BadPaddingException * @throws InvalidKeyException @@ -94,24 +99,31 @@ public class OmemoMessageBuilder ratchet, - byte[] aesKey, byte[] iv) + byte[] aesKey, + byte[] iv, + String message) throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, NoSuchProviderException, InvalidAlgorithmParameterException { - this.managerGuard = managerGuard; + this.userDevice = userDevice; + this.trustCallback = callback; this.ratchet = ratchet; this.messageKey = aesKey; this.initializationVector = iv; + setMessage(message); } /** - * Create a new OmemoMessageBuilder with random IV and AES key. + * Create an OmemoMessageBuilder. + * + * @param userDevice our OmemoDevice + * @param callback trustCallback for querying trust decisions + * @param ratchet our OmemoRatchet + * @param message message we want to send * - * @param managerGuard omemoManager of our device. - * @param ratchet omemoSessionManager. - * @param message Messages body. * @throws NoSuchPaddingException * @throws BadPaddingException * @throws InvalidKeyException @@ -121,31 +133,32 @@ public class OmemoMessageBuilder ratchet, String message) throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, NoSuchProviderException, InvalidAlgorithmParameterException { - this.managerGuard = managerGuard; - this.ratchet = ratchet; - this.setMessage(message); + this(userDevice, callback, ratchet, generateKey(KEYTYPE, KEYLENGTH), generateIv(), message); } /** - * Create an AES messageKey and use it to encrypt the message. - * Optionally append the Auth Tag of the encrypted message to the messageKey afterwards. + * Encrypt the message with the aes key. + * Move the AuthTag from the end of the cipherText to the end of the messageKey afterwards. + * This prevents an attacker which compromised one recipient device to switch out the cipherText for other recipients. + * @see OMEMO security audit. * - * @param message content of the message - * @throws NoSuchPaddingException When no Cipher could be instantiated. - * @throws NoSuchAlgorithmException when no Cipher could be instantiated. - * @throws NoSuchProviderException when BouncyCastle could not be found. - * @throws InvalidAlgorithmParameterException when the Cipher could not be initialized - * @throws InvalidKeyException when the generated key is invalid - * @throws UnsupportedEncodingException when UTF8 is unavailable - * @throws BadPaddingException when cipher.doFinal gets wrong padding - * @throws IllegalBlockSizeException when cipher.doFinal gets wrong Block size. + * @param message plaintext message + * @throws NoSuchPaddingException + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException + * @throws InvalidAlgorithmParameterException + * @throws InvalidKeyException + * @throws UnsupportedEncodingException + * @throws BadPaddingException + * @throws IllegalBlockSizeException */ - public void setMessage(String message) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, InvalidKeyException, UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException { + private void setMessage(String message) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, InvalidKeyException, UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException { if (message == null) { return; } @@ -165,56 +178,70 @@ public class OmemoMessageBuilder(Arrays.asList(active)), new HashSet<>(Arrays.asList(inactive))); @@ -281,7 +279,7 @@ public abstract class OmemoStoreTest