diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/ReadOnlyDeviceIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/ReadOnlyDeviceIntegrationTest.java index 3164d6668..1eddc92b0 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/ReadOnlyDeviceIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/ReadOnlyDeviceIntegrationTest.java @@ -20,9 +20,14 @@ import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertTrue; +import java.security.NoSuchAlgorithmException; + import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPException; +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.ReadOnlyDeviceException; import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException; @@ -37,36 +42,42 @@ public class ReadOnlyDeviceIntegrationTest extends AbstractTwoUsersOmemoIntegrat } @SmackIntegrationTest - public void test() throws InterruptedException, SmackException.NoResponseException, SmackException.NotLoggedInException, SmackException.NotConnectedException, CryptoFailedException, UndecidedOmemoIdentityException { + public void test() throws InterruptedException, SmackException.NoResponseException, SmackException.NotLoggedInException, SmackException.NotConnectedException, CryptoFailedException, UndecidedOmemoIdentityException, CorruptedOmemoKeyException, NoSuchAlgorithmException, CannotEstablishOmemoSessionException, NoRawSessionException { + int allowedChainLength = 5; boolean prevIgnoreReadOnlyConf = OmemoConfiguration.getIgnoreReadOnlyDevices(); int prevMaxMessageCounter = OmemoConfiguration.getMaxReadOnlyMessageCount(); OmemoConfiguration.setIgnoreReadOnlyDevices(true); // Set the maxReadOnlyMessageCount to ridiculously low threshold of 5. // This means that Alice will be able to encrypt 5 messages for Bob, while the 6th will not be encrypted for Bob. - OmemoConfiguration.setMaxReadOnlyMessageCount(5); + OmemoConfiguration.setMaxReadOnlyMessageCount(allowedChainLength); // Reset counter to begin test - alice.getOmemoService().getOmemoStoreBackend().storeOmemoMessageCounter(alice.getOwnDevice(), bob.getOwnDevice(), 0); + alice.getOmemoService().buildFreshSessionWithDevice(alice.getConnection(), alice.getOwnDevice(), bob.getOwnDevice()); + assertChainLengthEquals(alice, bob, 0); - // Since the max threshold is 5, we must be able to encrypt 5 messages for Bob. - for (int i = 0; i < 5; i++) { - assertEquals(i, alice.getOmemoService().getOmemoStoreBackend().loadOmemoMessageCounter(alice.getOwnDevice(), bob.getOwnDevice())); + // Since the max threshold is allowedChainLength, we must be able to encrypt as many messages for Bob. + for (int i = 0; i < allowedChainLength; i++) { OmemoMessage.Sent message = alice.encrypt(bob.getOwnJid(), "Hello World!"); assertFalse(message.getSkippedDevices().containsKey(bob.getOwnDevice())); + assertChainLengthEquals(alice, bob, i + 1); } // Now the message counter must be too high and Bobs device must be skipped. OmemoMessage.Sent message = alice.encrypt(bob.getOwnJid(), "Hello World!"); + assertChainLengthEquals(alice, bob, allowedChainLength); + assertTrue(message.getSkippedDevices().containsKey(bob.getOwnDevice())); Throwable exception = message.getSkippedDevices().get(bob.getOwnDevice()); assertTrue(exception instanceof ReadOnlyDeviceException); assertEquals(bob.getOwnDevice(), ((ReadOnlyDeviceException) exception).getDevice()); - // Reset the message counter - alice.getOmemoService().getOmemoStoreBackend().storeOmemoMessageCounter(alice.getOwnDevice(), bob.getOwnDevice(), 0); - // Reset the configuration to previous values OmemoConfiguration.setMaxReadOnlyMessageCount(prevMaxMessageCounter); OmemoConfiguration.setIgnoreReadOnlyDevices(prevIgnoreReadOnlyConf); } + + private void assertChainLengthEquals(OmemoManager sender, OmemoManager receiver, int length) throws NoRawSessionException { + assertEquals(length, sender.getOmemoService().getOmemoStoreBackend() + .getLengthOfSessionSendingChain(sender.getOwnDevice(), receiver.getOwnDevice())); + } } diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalChainKeyIndexStalenessStrategy.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalChainKeyIndexStalenessStrategy.java new file mode 100644 index 000000000..b3cc736e2 --- /dev/null +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalChainKeyIndexStalenessStrategy.java @@ -0,0 +1,51 @@ +/** + * + * Copyright 2019 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.smackx.omemo.signal; + +import org.jivesoftware.smackx.omemo.OmemoStore; +import org.jivesoftware.smackx.omemo.util.ChainKeyIndexStalenessStrategy; + +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; + +public class SignalChainKeyIndexStalenessStrategy extends ChainKeyIndexStalenessStrategy { + + /** + * Create a new {@link SignalChainKeyIndexStalenessStrategy}. + * + * @param store {@link OmemoStore} from which we can read the devices {@link SessionRecord}. + * @param maxChainKeyIndex the maximum value of messages we are allowed to send to the device unanswered before + */ + public SignalChainKeyIndexStalenessStrategy( + OmemoStore store, + int maxChainKeyIndex) { + super(store, maxChainKeyIndex); + } +} diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoKeyUtil.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoKeyUtil.java index 010eb65ed..53baec083 100644 --- a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoKeyUtil.java +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoKeyUtil.java @@ -96,6 +96,11 @@ public class SignalOmemoKeyUtil extends OmemoKeyUtil integers = readIntegers(messageCounterFile); - - if (integers == null || integers.isEmpty()) { - return 0; - } - - return integers.iterator().next(); - } - @Override public OmemoCachedDeviceList loadCachedDeviceList(OmemoDevice userDevice, BareJid contact) { OmemoCachedDeviceList cachedDeviceList = new OmemoCachedDeviceList(); diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoConfiguration.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoConfiguration.java index c53f97dac..ebf69614b 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoConfiguration.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoConfiguration.java @@ -24,7 +24,7 @@ package org.jivesoftware.smackx.omemo; public final class OmemoConfiguration { private static boolean IGNORE_READ_ONLY_DEVICES = true; - private static int MAX_READ_ONLY_MESSAGE_COUNT = 400; + private static int MAX_READ_ONLY_MESSAGE_COUNT = 750; /** * Set to true, in order to ignore read-only devices. 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 27f775f3e..a263be8c0 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 @@ -75,6 +75,7 @@ import org.jivesoftware.smackx.omemo.trust.TrustState; import org.jivesoftware.smackx.omemo.util.MessageOrOmemoMessage; import org.jivesoftware.smackx.omemo.util.OmemoConstants; import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder; +import org.jivesoftware.smackx.omemo.util.StalenessStrategy; import org.jivesoftware.smackx.pubsub.LeafNode; import org.jivesoftware.smackx.pubsub.PayloadItem; import org.jivesoftware.smackx.pubsub.PubSubException; @@ -124,6 +125,15 @@ public abstract class OmemoService= OmemoConfiguration.getMaxReadOnlyMessageCount(); + boolean readOnly = getStalenessStrategy().isStale(userDevice, contactsDevice); if (readOnly) { - LOGGER.log(Level.FINE, "Device " + contactsDevice + " seems to be read-only (We sent " - + messageCounter + " messages without getting a reply back (max allowed is " + - OmemoConfiguration.getMaxReadOnlyMessageCount() + "). Ignoring the device."); + LOGGER.log(Level.FINE, "Device " + contactsDevice + " seems to be read-only " + + "(We sent more than " + OmemoConfiguration.getMaxReadOnlyMessageCount() + + "without getting a reply back."); skippedRecipients.put(contactsDevice, new ReadOnlyDeviceException(contactsDevice)); - // Skip this device and handle next device continue; } @@ -421,10 +428,6 @@ public abstract class OmemoService devices) { - if (devices.contains(userDevice)) { - devices.remove(userDevice); - } + devices.remove(userDevice); } /** - * Determine, whether another one of *our* devices is stale or not. + * Determine, whether another device is stale or not. * * @param userDevice our omemoDevice - * @param subject another one of our devices - * @param lastReceipt date of last received message from that device - * @param maxAgeHours threshold + * @param subject another device * * @return staleness */ - static boolean isStale(OmemoDevice userDevice, OmemoDevice subject, Date lastReceipt, int maxAgeHours) { - if (userDevice.equals(subject)) { - return false; - } - - if (lastReceipt == null) { - return false; - } - - long maxAgeMillis = MILLIS_PER_HOUR * maxAgeHours; - Date now = new Date(); - - return now.getTime() - lastReceipt.getTime() > maxAgeMillis; + private boolean isStale(OmemoDevice userDevice, OmemoDevice subject) { + return getStalenessStrategy().isStale(userDevice, subject); } /** 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 4626f6e00..46084b54c 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 @@ -32,6 +32,7 @@ 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.exceptions.NoIdentityKeyException; +import org.jivesoftware.smackx.omemo.exceptions.NoRawSessionException; import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; @@ -290,27 +291,6 @@ public abstract class OmemoStore implements StalenessStrategy { + + private static final Logger LOGGER = Logger.getLogger(ChainKeyIndexStalenessStrategy.class.getName()); + + private final OmemoStore store; + private final int maxChainKeyIndex; + + /** + * Create a new {@link ChainKeyIndexStalenessStrategy}. + * + * @param store {@link OmemoStore} from which we can read the devices session. + * @param maxChainKeyIndex the maximum value of messages we are allowed to send to the device unanswered before + * we consider it stale. + */ + public ChainKeyIndexStalenessStrategy(OmemoStore store, int maxChainKeyIndex) { + this.store = store; + this.maxChainKeyIndex = maxChainKeyIndex; + } + + @Override + public boolean isStale(OmemoDevice userDevice, OmemoDevice contactsDevice) { + T_Sess sessionRecord = store.loadRawSession(userDevice, contactsDevice); + if (sessionRecord == null) { + return false; + } + + int chainLength = store.keyUtil().lengthOfSessionSendingChain(sessionRecord); + LOGGER.log(Level.FINE, "Staleness factor of OMEMO session between " + + userDevice + " and " + contactsDevice + ": " + chainLength + "/" + maxChainKeyIndex); + return chainLength >= maxChainKeyIndex; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/NeverStaleStalenessStrategy.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/NeverStaleStalenessStrategy.java new file mode 100644 index 000000000..a23c40eda --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/NeverStaleStalenessStrategy.java @@ -0,0 +1,32 @@ +/** + * + * Copyright 2019 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.util; + +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; + +/** + * Example {@link StalenessStrategy} which never rates OMEMO sessions as stale. + * + * Note: This should only be used for testing purposes. + */ +public class NeverStaleStalenessStrategy implements StalenessStrategy { + @Override + public boolean isStale(OmemoDevice userDevice, OmemoDevice contactsDevice) { + // Accept all sessions as non-stale + return false; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoKeyUtil.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoKeyUtil.java index 8fa56f071..444498342 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoKeyUtil.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoKeyUtil.java @@ -388,4 +388,13 @@ public abstract class OmemoKeyUtil