From 36dae1ece452aa6dec6c6ec8bf60914b4f922759 Mon Sep 17 00:00:00 2001 From: vanitasvitae Date: Tue, 28 Nov 2017 15:33:41 +0100 Subject: [PATCH] Move trust management from OmemoStore to TrustCallback Refactor various method names to more precisely reflect what happens Introduce CachingOmemoStore, SignalCachingOmemoStore, which can be either used standalone as ephemeral OmemoStore implementations, or as wrappers around other implementations to add a cache layer for reduced storage access. Get rid of "isFreshInstallation". Keys are now - given that they don't exist - generated on startup. Bump libsignal-protocol-java to 2.6.2 Prevent offline access to some functions which require authenticated connection Create more advanced unit tests and integration tests Add async initialization function for OmemoManager Remove session handling from smack-omemo. This is now handled - in case of smack-omemo-signal - solely by libsignal-protocol-java --- build.gradle | 2 + smack-integration-test/build.gradle | 1 + .../omemo/AbstractOmemoIntegrationTest.java | 39 +- .../AbstractTwoUsersOmemoIntegrationTest.java | 75 ++ .../MessageEncryptionIntegrationTest.java | 116 +++ .../smackx/omemo/OmemoInitializationTest.java | 81 -- .../omemo/OmemoIntegrationTestHelper.java | 156 --- .../smackx/omemo/OmemoKeyTransportTest.java | 109 -- .../smackx/omemo/OmemoManagerSetupHelper.java | 239 +++++ .../smackx/omemo/OmemoMessageSendingTest.java | 193 ---- .../omemo/OmemoSessionRenegotiationTest.java | 195 ---- .../smackx/omemo/OmemoStoreTest.java | 163 --- smack-omemo-signal/build.gradle | 3 +- .../omemo/signal/SignalCachingOmemoStore.java | 62 ++ .../signal/SignalFileBasedOmemoStore.java | 8 +- .../omemo/signal/SignalOmemoKeyUtil.java | 65 +- .../omemo/signal/SignalOmemoService.java | 48 +- .../omemo/signal/SignalOmemoSession.java | 142 --- .../signal/SignalOmemoSessionManager.java | 164 +++ .../smackx/omemo/signal/SignalOmemoStore.java | 6 +- .../signal/SignalOmemoStoreConnector.java | 111 +- .../omemo/LegacySignalOmemoKeyUtilTest.java | 122 +++ .../smack/omemo/OmemoManagerTest.java | 11 +- .../omemo/SignalFileBasedOmemoStoreTest.java | 218 ---- .../smack/omemo/SignalOmemoKeyUtilTest.java | 130 +-- .../omemo/SignalOmemoStoreConnectorTest.java | 2 +- .../smack/omemo/SignalOmemoStoreTest.java | 84 ++ .../smackx/omemo/CachingOmemoStore.java | 423 ++++++++ .../smackx/omemo/FileBasedOmemoStore.java | 945 ++++++++---------- .../smackx/omemo/OmemoFingerprint.java | 4 - .../smackx/omemo/OmemoManager.java | 688 +++++++++---- .../smackx/omemo/OmemoService.java | 881 ++++++++-------- .../smackx/omemo/OmemoSessionManager.java | 152 +++ .../jivesoftware/smackx/omemo/OmemoStore.java | 602 ++++------- .../omemo/exceptions/MultipleIOException.java | 40 + .../UntrustedOmemoIdentityException.java | 68 ++ .../omemo/internal/CachedDeviceList.java | 10 + .../internal/OmemoMessageInformation.java | 26 +- .../smackx/omemo/internal/OmemoSession.java | 266 ----- ...OmemoCarbonCopyStanzaReceivedListener.java | 29 + .../OmemoMessageStanzaReceivedListener.java | 25 + .../package-info.java} | 19 +- .../smackx/omemo/trust/TrustCallback.java | 28 + .../smackx/omemo/trust/TrustState.java | 21 + .../smackx/omemo/trust/package-info.java | 23 + .../smackx/omemo/util/OmemoKeyUtil.java | 36 +- .../omemo/util/OmemoMessageBuilder.java | 79 +- .../smack/omemo/DeviceListTest.java | 7 + .../smack/omemo/OmemoKeyUtilTest.java | 101 +- .../omemo/OmemoMessageInformationTest.java | 43 + .../smack/omemo/OmemoStoreTest.java | 346 +++++++ .../smack/omemo/WrapperObjectsTest.java | 14 +- .../omemo/util/EphemeralTrustCallback.java | 59 ++ 53 files changed, 4072 insertions(+), 3408 deletions(-) create mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java create mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java delete mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoInitializationTest.java delete mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoIntegrationTestHelper.java delete mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoKeyTransportTest.java create mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java delete mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessageSendingTest.java delete mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoSessionRenegotiationTest.java delete mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java create mode 100644 smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalCachingOmemoStore.java delete mode 100644 smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoSession.java create mode 100644 smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoSessionManager.java create mode 100644 smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/LegacySignalOmemoKeyUtilTest.java delete mode 100644 smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalFileBasedOmemoStoreTest.java create mode 100644 smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreTest.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/CachingOmemoStore.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoSessionManager.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/MultipleIOException.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UntrustedOmemoIdentityException.java delete mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoSession.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/listener/OmemoCarbonCopyStanzaReceivedListener.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/listener/OmemoMessageStanzaReceivedListener.java rename smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/{IdentityKeyWrapper.java => listener/package-info.java} (66%) create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/TrustCallback.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/TrustState.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/package-info.java create mode 100644 smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoMessageInformationTest.java create mode 100644 smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoStoreTest.java create mode 100644 smack-omemo/src/test/java/org/jivesoftware/smack/omemo/util/EphemeralTrustCallback.java diff --git a/build.gradle b/build.gradle index 7d628671a..48b5ad29b 100644 --- a/build.gradle +++ b/build.gradle @@ -548,10 +548,12 @@ task clirrRootReport(type: org.kordamp.gradle.clirr.ClirrReportTask) { } task integrationTest { + description 'Verify correct functionality of Smack by running some integration tests.' dependsOn project(':smack-integration-test').tasks.run } task omemoSignalIntTest { + description 'Run integration tests of the smack-omemo module in combination with smack-omemo-signal.' dependsOn project(':smack-omemo-signal-integration-test').tasks.run } diff --git a/smack-integration-test/build.gradle b/smack-integration-test/build.gradle index 0d4d197df..285bb74ea 100644 --- a/smack-integration-test/build.gradle +++ b/smack-integration-test/build.gradle @@ -13,6 +13,7 @@ dependencies { compile project(':smack-experimental') compile project(':smack-omemo') compile project(':smack-debug') + compile project(path: ":smack-omemo", configuration: "testRuntime") compile 'org.reflections:reflections:0.9.9-RC1' compile 'eu.geekplace.javapinning:java-pinning-java7:1.1.0-alpha1' // Note that the junit-vintage-engine runtime dependency is not diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoIntegrationTest.java index c137b0caa..0507fd278 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoIntegrationTest.java @@ -16,40 +16,21 @@ */ package org.jivesoftware.smackx.omemo; -import java.io.File; -import java.util.logging.Level; - import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPException; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; -import org.junit.AfterClass; -import org.junit.BeforeClass; /** * Super class for OMEMO integration tests. */ public abstract class AbstractOmemoIntegrationTest extends AbstractSmackIntegrationTest { - private static final File storePath; - - static { - String userHome = System.getProperty("user.home"); - if (userHome != null) { - File f = new File(userHome); - storePath = new File(f, ".config/smack-integration-test/store"); - } else { - storePath = new File("int_test_omemo_store"); - } - } - public AbstractOmemoIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { super(environment); - if (OmemoConfiguration.getFileBasedOmemoStoreDefaultPath() == null) { - OmemoConfiguration.setFileBasedOmemoStoreDefaultPath(storePath); - } + // Test for server support if (!OmemoManager.serverSupportsOmemo(connection, connection.getXMPPServiceDomain())) { throw new TestNotPossibleException("Server does not support OMEMO (PubSub)"); @@ -60,22 +41,4 @@ public abstract class AbstractOmemoIntegrationTest extends AbstractSmackIntegrat throw new TestNotPossibleException("No OmemoService registered."); } } - - @BeforeClass - public void beforeTest() { - LOGGER.log(Level.INFO, "START EXECUTION"); - OmemoIntegrationTestHelper.deletePath(storePath); - before(); - } - - @AfterClass - public void afterTest() { - after(); - OmemoIntegrationTestHelper.deletePath(storePath); - LOGGER.log(Level.INFO, "END EXECUTION"); - } - - public abstract void before(); - - public abstract void after(); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java new file mode 100644 index 000000000..a0c9c8bef --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java @@ -0,0 +1,75 @@ +/** + * + * 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.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.util.logging.Level; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; + +import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; +import org.igniterealtime.smack.inttest.TestNotPossibleException; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +/** + * Abstract OMEMO integration test framing, which creates and initializes two OmemoManagers (for conOne and conTwo). + * Both users subscribe to one another and trust their identities. + * After the test traces in PubSub and in the users Rosters are removed. + */ +public abstract class AbstractTwoUsersOmemoIntegrationTest extends AbstractOmemoIntegrationTest { + + protected OmemoManager alice, bob; + + public AbstractTwoUsersOmemoIntegrationTest(SmackIntegrationTestEnvironment environment) + throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException, TestNotPossibleException { + super(environment); + } + + @BeforeClass + public void setup() throws Exception { + alice = OmemoManagerSetupHelper.prepareOmemoManager(conOne); + bob = OmemoManagerSetupHelper.prepareOmemoManager(conTwo); + + LOGGER.log(Level.FINE, "Alice: " + alice.getOwnDevice() + " Bob: " + bob.getOwnDevice()); + assertFalse(alice.getDeviceId().equals(bob.getDeviceId())); + + // Subscribe presences + OmemoManagerSetupHelper.syncSubscribePresence(alice.getConnection(), bob.getConnection(), "bob", null); + OmemoManagerSetupHelper.syncSubscribePresence(bob.getConnection(), alice.getConnection(), "alice", null); + + OmemoManagerSetupHelper.trustAllIdentitiesWithTests(alice, bob); // Alice trusts Bob's devices + OmemoManagerSetupHelper.trustAllIdentitiesWithTests(bob, alice); // Bob trusts Alice' and Mallory's devices + + assertEquals(bob.getOwnFingerprint(), alice.getActiveFingerprints(bob.getOwnJid()).get(bob.getOwnDevice())); + assertEquals(alice.getOwnFingerprint(), bob.getActiveFingerprints(alice.getOwnJid()).get(alice.getOwnDevice())); + } + + @AfterClass + public void cleanUp() throws SmackException.NotLoggedInException { + alice.stopListeners(); + bob.stopListeners(); + OmemoManagerSetupHelper.cleanUpPubSub(alice); + OmemoManagerSetupHelper.cleanUpRoster(alice); + OmemoManagerSetupHelper.cleanUpPubSub(bob); + OmemoManagerSetupHelper.cleanUpRoster(bob); + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java new file mode 100644 index 000000000..f007aae63 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java @@ -0,0 +1,116 @@ +/** + * + * 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.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smackx.omemo.element.OmemoBundleElement; +import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag; +import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation; +import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener; + +import org.igniterealtime.smack.inttest.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; +import org.igniterealtime.smack.inttest.TestNotPossibleException; +import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; + +/** + * Simple OMEMO message encryption integration test. + * During this test Alice sends an encrypted message to Bob. Bob decrypts it and sends a response to Alice. + * It is checked whether the messages can be decrypted, and if used up pre-keys result in renewed bundles. + */ +public class MessageEncryptionIntegrationTest extends AbstractTwoUsersOmemoIntegrationTest { + + public MessageEncryptionIntegrationTest(SmackIntegrationTestEnvironment environment) + throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException, TestNotPossibleException { + super(environment); + } + + @SmackIntegrationTest + public void messageTest() throws Exception { + OmemoBundleElement aliceBundle1 = alice.getOmemoService().getOmemoStoreBackend().packOmemoBundle(alice.getOwnDevice()); + OmemoBundleElement bobsBundle1 = bob.getOmemoService().getOmemoStoreBackend().packOmemoBundle(bob.getOwnDevice()); + + final String message1 = "One is greater than zero (for small values of zero)."; + Message encrypted1 = alice.encrypt(bob.getOwnJid(), message1); + final SimpleResultSyncPoint bobReceivedMessage = new SimpleResultSyncPoint(); + + bob.addOmemoMessageListener(new OmemoMessageListener() { + @Override + public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) { + if (decryptedBody.equals(message1)) { + bobReceivedMessage.signal(); + } else { + bobReceivedMessage.signalFailure("Received decrypted message was not equal to sent message."); + } + } + + @Override + public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) { + // Not used + } + }); + + encrypted1.setTo(bob.getOwnJid()); + alice.getConnection().sendStanza(encrypted1); + bobReceivedMessage.waitForResult(10 * 1000); + + OmemoBundleElement aliceBundle2 = alice.getOmemoService().getOmemoStoreBackend().packOmemoBundle(alice.getOwnDevice()); + OmemoBundleElement bobsBundle2 = bob.getOmemoService().getOmemoStoreBackend().packOmemoBundle(bob.getOwnDevice()); + + // Alice bundle is still the same, but bobs bundle changed, because he used up a pre-key. + assertEquals(aliceBundle1, aliceBundle2); + assertFalse(bobsBundle1.equals(bobsBundle2)); + + final String message2 = "The german words for 'leek' and 'wimp' are the same."; + final Message encrypted2 = bob.encrypt(alice.getOwnJid(), message2); + final SimpleResultSyncPoint aliceReceivedMessage = new SimpleResultSyncPoint(); + + alice.addOmemoMessageListener(new OmemoMessageListener() { + @Override + public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) { + if (decryptedBody.equals(message2)) { + aliceReceivedMessage.signal(); + } else { + aliceReceivedMessage.signalFailure("Received decrypted message was not equal to sent message."); + } + } + + @Override + public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) { + // Not needed here either. + } + }); + + encrypted2.setTo(alice.getOwnJid()); + bob.getConnection().sendStanza(encrypted2); + aliceReceivedMessage.waitForResult(10 * 1000); + + OmemoBundleElement aliceBundle3 = alice.getOmemoService().getOmemoStoreBackend().packOmemoBundle(alice.getOwnDevice()); + OmemoBundleElement bobsBundle3 = bob.getOmemoService().getOmemoStoreBackend().packOmemoBundle(bob.getOwnDevice()); + + // Alice bundle did not change, because she already has a session with bob, which he initiated. + // Bobs bundle doesn't change this time. + assertEquals(aliceBundle2, aliceBundle3); + assertEquals(bobsBundle2, bobsBundle3); + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoInitializationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoInitializationTest.java deleted file mode 100644 index 8dc3b7915..000000000 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoInitializationTest.java +++ /dev/null @@ -1,81 +0,0 @@ -/** - * - * Copyright 2017 Florian Schmaus, 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 junit.framework.TestCase.assertNotNull; -import static junit.framework.TestCase.assertTrue; -import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.cleanServerSideTraces; -import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.setUpOmemoManager; - -import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.SmackException.NoResponseException; -import org.jivesoftware.smack.SmackException.NotConnectedException; -import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.XMPPException.XMPPErrorException; - -import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; -import org.jivesoftware.smackx.omemo.util.OmemoConstants; -import org.jivesoftware.smackx.pubsub.PubSubException; -import org.jivesoftware.smackx.pubsub.PubSubException.NotAPubSubNodeException; - -import org.igniterealtime.smack.inttest.SmackIntegrationTest; -import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; -import org.igniterealtime.smack.inttest.TestNotPossibleException; - -public class OmemoInitializationTest extends AbstractOmemoIntegrationTest { - - private OmemoManager alice; - private OmemoStore store; - - @Override - public void before() { - alice = OmemoManager.getInstanceFor(conOne, 666); - store = OmemoService.getInstance().getOmemoStoreBackend(); - } - - public OmemoInitializationTest(SmackIntegrationTestEnvironment environment) throws TestNotPossibleException, XMPPErrorException, NotConnectedException, NoResponseException, InterruptedException { - super(environment); - } - - /** - * Tests, if the initialization is done properly. - * @throws NotAPubSubNodeException - */ - @SmackIntegrationTest - public void initializationTest() throws XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, SmackException.NotLoggedInException, CorruptedOmemoKeyException, NotAPubSubNodeException { - // test keys. - setUpOmemoManager(alice); - assertNotNull("IdentityKey must not be null after initialization.", store.loadOmemoIdentityKeyPair(alice)); - assertTrue("We must have " + OmemoConstants.TARGET_PRE_KEY_COUNT + " preKeys.", - store.loadOmemoPreKeys(alice).size() == OmemoConstants.TARGET_PRE_KEY_COUNT); - assertNotNull("Our signedPreKey must not be null.", store.loadCurrentSignedPreKeyId(alice)); - - // Is deviceId published? - assertTrue("Published deviceList must contain our deviceId.", - OmemoService.fetchDeviceList(alice, alice.getOwnJid()) - .getDeviceIds().contains(alice.getDeviceId())); - - assertTrue("Our fingerprint must be of correct length.", - OmemoService.getInstance().getOmemoStoreBackend().getFingerprint(alice).length() == 64); - } - - @Override - public void after() { - alice.shutdown(); - cleanServerSideTraces(alice); - } -} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoIntegrationTestHelper.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoIntegrationTestHelper.java deleted file mode 100644 index 4cbc88e7a..000000000 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoIntegrationTestHelper.java +++ /dev/null @@ -1,156 +0,0 @@ -/** - * - * Copyright 2017 Florian Schmaus, 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 junit.framework.TestCase.assertEquals; -import static junit.framework.TestCase.assertNotNull; -import static junit.framework.TestCase.assertTrue; - -import java.io.File; -import java.util.logging.Level; -import java.util.logging.Logger; - -import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.roster.Roster; -import org.jivesoftware.smack.roster.RosterEntry; - -import org.jivesoftware.smackx.omemo.element.OmemoBundleElement; -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.util.OmemoConstants; -import org.jivesoftware.smackx.pubsub.PubSubException; -import org.jivesoftware.smackx.pubsub.PubSubException.NotAPubSubNodeException; -import org.jivesoftware.smackx.pubsub.PubSubManager; - -/** - * Class containing some helper methods for OmemoIntegrationTests. - */ -final class OmemoIntegrationTestHelper { - - private static final Logger LOGGER = Logger.getLogger(OmemoIntegrationTestHelper.class.getSimpleName()); - - static void cleanServerSideTraces(OmemoManager omemoManager) { - cleanUpPubSub(omemoManager); - cleanUpRoster(omemoManager); - } - - static void deletePath(File storePath) { - FileBasedOmemoStore.deleteDirectory(storePath); - } - - static void deletePath(OmemoManager omemoManager) { - OmemoService.getInstance().getOmemoStoreBackend().purgeOwnDeviceKeys(omemoManager); - } - - static void cleanUpPubSub(OmemoManager omemoManager) { - PubSubManager pm = PubSubManager.getInstance(omemoManager.getConnection(),omemoManager.getOwnJid()); - try { - omemoManager.requestDeviceListUpdateFor(omemoManager.getOwnJid()); - } catch (SmackException.NotConnectedException | InterruptedException | SmackException.NoResponseException e) { - // ignore - } - - CachedDeviceList deviceList = OmemoService.getInstance().getOmemoStoreBackend().loadCachedDeviceList(omemoManager, omemoManager.getOwnJid()); - for (int id : deviceList.getAllDevices()) { - try { - pm.getLeafNode(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(id)).deleteAllItems(); - } catch (InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException | PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException | NotAPubSubNodeException e) { - // Silent - } - - try { - pm.deleteNode(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(id)); - } catch (SmackException.NoResponseException | InterruptedException | SmackException.NotConnectedException | XMPPException.XMPPErrorException e) { - // Silent - } - } - - try { - pm.getLeafNode(OmemoConstants.PEP_NODE_DEVICE_LIST).deleteAllItems(); - } catch (InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException | PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException | NotAPubSubNodeException e) { - // Silent - } - - try { - pm.deleteNode(OmemoConstants.PEP_NODE_DEVICE_LIST); - } catch (SmackException.NoResponseException | InterruptedException | SmackException.NotConnectedException | XMPPException.XMPPErrorException e) { - // Silent - } - } - - static void cleanUpRoster(OmemoManager omemoManager) { - Roster roster = Roster.getInstanceFor(omemoManager.getConnection()); - for (RosterEntry r : roster.getEntries()) { - try { - roster.removeEntry(r); - } catch (InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException | XMPPException.XMPPErrorException | SmackException.NotLoggedInException e) { - // Silent - } - } - } - - /** - * Let Alice subscribe to Bob. - * @param alice - * @param bob - * @throws SmackException.NotLoggedInException - * @throws XMPPException.XMPPErrorException - * @throws SmackException.NotConnectedException - * @throws InterruptedException - * @throws SmackException.NoResponseException - */ - static void subscribe(OmemoManager alice, OmemoManager bob, String nick) - throws SmackException.NotLoggedInException, XMPPException.XMPPErrorException, - SmackException.NotConnectedException, InterruptedException, - SmackException.NoResponseException { - - Roster aliceRoster = Roster.getInstanceFor(alice.getConnection()); - Roster bobsRoster = Roster.getInstanceFor(bob.getConnection()); - bobsRoster.setSubscriptionMode(Roster.SubscriptionMode.accept_all); - aliceRoster.createEntry(bob.getOwnJid(), nick, null); - } - - - static void unidirectionalTrust(OmemoManager alice, OmemoManager bob) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, CannotEstablishOmemoSessionException { - // Fetch deviceList - alice.requestDeviceListUpdateFor(bob.getOwnJid()); - LOGGER.log(Level.INFO, "Current deviceList state: " + alice.getOwnDevice() + " knows " + bob.getOwnDevice() + ": " - + OmemoService.getInstance().getOmemoStoreBackend().loadCachedDeviceList(alice, bob.getOwnJid())); - assertTrue("Trusting party must know the others device at this point.", - alice.getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(alice, bob.getOwnJid()) - .getActiveDevices().contains(bob.getDeviceId())); - - // Create sessions - alice.buildSessionsWith(bob.getOwnJid()); - assertTrue("Trusting party must have a session with the other end at this point.", - !alice.getOmemoService().getOmemoStoreBackend().loadAllRawSessionsOf(alice, bob.getOwnJid()).isEmpty()); - - // Trust the other party - alice.getOmemoService().getOmemoStoreBackend().trustOmemoIdentity(alice, bob.getOwnDevice(), - alice.getOmemoService().getOmemoStoreBackend().getFingerprint(alice, bob.getOwnDevice())); - - } - - static void setUpOmemoManager(OmemoManager omemoManager) throws CorruptedOmemoKeyException, InterruptedException, SmackException.NoResponseException, SmackException.NotConnectedException, XMPPException.XMPPErrorException, SmackException.NotLoggedInException, PubSubException.NotALeafNodeException, NotAPubSubNodeException { - omemoManager.initialize(); - OmemoBundleElement bundle = OmemoService.fetchBundle(omemoManager, omemoManager.getOwnDevice()); - assertNotNull("Bundle must not be null.", bundle); - assertEquals("Published Bundle must equal our local bundle.", bundle, omemoManager.getOmemoService().getOmemoStoreBackend().packOmemoBundle(omemoManager)); - } -} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoKeyTransportTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoKeyTransportTest.java deleted file mode 100644 index dde41eb15..000000000 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoKeyTransportTest.java +++ /dev/null @@ -1,109 +0,0 @@ -/** - * - * Copyright 2017 Florian Schmaus, 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.OmemoIntegrationTestHelper.cleanServerSideTraces; -import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.setUpOmemoManager; -import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.subscribe; -import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.unidirectionalTrust; -import static org.junit.Assert.assertTrue; - -import java.util.Arrays; -import java.util.logging.Level; - -import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.chat2.ChatManager; -import org.jivesoftware.smack.packet.Message; - -import org.jivesoftware.smackx.omemo.element.OmemoElement; -import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag; -import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation; -import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener; -import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder; - -import org.igniterealtime.smack.inttest.SmackIntegrationTest; -import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; -import org.igniterealtime.smack.inttest.TestNotPossibleException; -import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; - -/** - * Test keyTransportMessages. - */ -public class OmemoKeyTransportTest extends AbstractOmemoIntegrationTest { - - private OmemoManager alice, bob; - - public OmemoKeyTransportTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { - super(environment); - } - - @Override - public void before() { - alice = OmemoManager.getInstanceFor(conOne, 11111); - bob = OmemoManager.getInstanceFor(conTwo, 222222); - } - - @SmackIntegrationTest - public void keyTransportTest() throws Exception { - final SimpleResultSyncPoint syncPoint = new SimpleResultSyncPoint(); - - subscribe(alice, bob, "Bob"); - subscribe(bob, alice, "Alice"); - - setUpOmemoManager(alice); - setUpOmemoManager(bob); - - unidirectionalTrust(alice, bob); - unidirectionalTrust(bob, alice); - - final byte[] key = OmemoMessageBuilder.generateKey(); - final byte[] iv = OmemoMessageBuilder.generateIv(); - - bob.addOmemoMessageListener(new OmemoMessageListener() { - @Override - public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - // Don't care - } - - @Override - public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - LOGGER.log(Level.INFO, "Received a keyTransportMessage."); - assertTrue("Key must match the one we sent.", Arrays.equals(key, cipherAndAuthTag.getKey())); - assertTrue("IV must match the one we sent.", Arrays.equals(iv, cipherAndAuthTag.getIv())); - syncPoint.signal(); - } - }); - - OmemoElement keyTransportElement = alice.createKeyTransportElement(key, iv, bob.getOwnDevice()); - Message message = new Message(bob.getOwnJid()); - message.addExtension(keyTransportElement); - ChatManager.getInstanceFor(alice.getConnection()).chatWith(bob.getOwnJid().asEntityBareJidIfPossible()) - .send(message); - - // TODO: Should use 'timeout' field instead of hardcoded '10 * 1000'. - syncPoint.waitForResult(10 * 1000); - } - - @Override - public void after() { - alice.shutdown(); - bob.shutdown(); - cleanServerSideTraces(alice); - cleanServerSideTraces(bob); - } -} 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 new file mode 100644 index 000000000..f5940c312 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java @@ -0,0 +1,239 @@ +/** + * + * 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.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.omemo.util.EphemeralTrustCallback; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.roster.PresenceEventListener; +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.OmemoDevice; +import org.jivesoftware.smackx.omemo.util.OmemoConstants; +import org.jivesoftware.smackx.pubsub.PubSubException; +import org.jivesoftware.smackx.pubsub.PubSubManager; + +import com.google.common.collect.Maps; +import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; +import org.jxmpp.jid.BareJid; +import org.jxmpp.jid.FullJid; +import org.jxmpp.jid.Jid; + +public class OmemoManagerSetupHelper { + + /** + * Synchronously subscribes presence. + * @param subscriber connection of user which subscribes. + * @param target connection of user which gets subscribed. + * @param targetNick nick of the subscribed user. + * @param targetGroups groups of the user. + * @throws Exception + */ + public static void syncSubscribePresence(final XMPPConnection subscriber, + final XMPPConnection target, + String targetNick, + String[] targetGroups) + throws Exception + { + final SimpleResultSyncPoint subscribed = new SimpleResultSyncPoint(); + + Roster subscriberRoster = Roster.getInstanceFor(subscriber); + Roster targetRoster = Roster.getInstanceFor(target); + + targetRoster.setSubscriptionMode(Roster.SubscriptionMode.accept_all); + subscriberRoster.addPresenceEventListener(new PresenceEventListener() { + @Override + public void presenceAvailable(FullJid address, Presence availablePresence) { + } + + @Override + public void presenceUnavailable(FullJid address, Presence presence) { + } + + @Override + public void presenceError(Jid address, Presence errorPresence) { + subscribed.signalFailure(); + } + + @Override + public void presenceSubscribed(BareJid address, Presence subscribedPresence) { + subscribed.signal(); + } + + @Override + public void presenceUnsubscribed(BareJid address, Presence unsubscribedPresence) { + } + }); + + subscriberRoster.createEntry(target.getUser().asBareJid(), targetNick, targetGroups); + + subscribed.waitForResult(10 * 1000); + } + + public static void trustAllIdentities(OmemoManager alice, OmemoManager bob) + throws InterruptedException, SmackException.NotConnectedException, SmackException.NotLoggedInException, + SmackException.NoResponseException, CannotEstablishOmemoSessionException, CorruptedOmemoKeyException + { + Roster roster = Roster.getInstanceFor(alice.getConnection()); + + if (alice.getOwnJid() != bob.getOwnJid() && + (!roster.iAmSubscribedTo(bob.getOwnJid()) || !roster.isSubscribedToMyPresence(bob.getOwnJid()))) { + throw new IllegalStateException("Before trusting identities of a user, we must be subscribed to one another."); + } + + alice.requestDeviceListUpdateFor(bob.getOwnJid()); + HashMap fingerprints = alice.getActiveFingerprints(bob.getOwnJid()); + + for (OmemoDevice device : fingerprints.keySet()) { + OmemoFingerprint fingerprint = fingerprints.get(device); + alice.trustOmemoIdentity(device, fingerprint); + } + } + + public static void trustAllIdentitiesWithTests(OmemoManager alice, OmemoManager bob) + throws InterruptedException, SmackException.NotConnectedException, SmackException.NotLoggedInException, + SmackException.NoResponseException, CannotEstablishOmemoSessionException, CorruptedOmemoKeyException + { + alice.requestDeviceListUpdateFor(bob.getOwnJid()); + HashMap fps1 = alice.getActiveFingerprints(bob.getOwnJid()); + + assertFalse(fps1.isEmpty()); + assertAllDevicesAreUndecided(alice, fps1); + assertAllDevicesAreUntrusted(alice, fps1); + + trustAllIdentities(alice, bob); + + HashMap fps2 = alice.getActiveFingerprints(bob.getOwnJid()); + assertEquals(fps1.size(), fps2.size()); + assertTrue(Maps.difference(fps1, fps2).areEqual()); + + assertAllDevicesAreDecided(alice, fps2); + assertAllDevicesAreTrusted(alice, fps2); + } + + public static OmemoManager prepareOmemoManager(XMPPConnection connection) throws Exception { + final OmemoManager manager = OmemoManager.getInstanceFor(connection, OmemoManager.randomDeviceId()); + manager.setTrustCallback(new EphemeralTrustCallback()); + + if (connection.isAuthenticated()) { + manager.initialize(); + } else { + throw new AssertionError("Connection must be authenticated."); + } + return manager; + } + + public static void assertAllDevicesAreUndecided(OmemoManager manager, HashMap devices) { + for (OmemoDevice device : devices.keySet()) { + // All fingerprints MUST be neither decided, nor trusted. + assertFalse(manager.isDecidedOmemoIdentity(device, devices.get(device))); + } + } + + public static void assertAllDevicesAreUntrusted(OmemoManager manager, HashMap devices) { + for (OmemoDevice device : devices.keySet()) { + // All fingerprints MUST be neither decided, nor trusted. + assertFalse(manager.isTrustedOmemoIdentity(device, devices.get(device))); + } + } + + public static void assertAllDevicesAreDecided(OmemoManager manager, HashMap devices) { + for (OmemoDevice device : devices.keySet()) { + // All fingerprints MUST be neither decided, nor trusted. + assertTrue(manager.isDecidedOmemoIdentity(device, devices.get(device))); + } + } + + public static void assertAllDevicesAreTrusted(OmemoManager manager, HashMap devices) { + for (OmemoDevice device : devices.keySet()) { + // All fingerprints MUST be neither decided, nor trusted. + assertTrue(manager.isTrustedOmemoIdentity(device, devices.get(device))); + } + } + + public static void cleanUpPubSub(OmemoManager omemoManager) throws SmackException.NotLoggedInException { + PubSubManager pm = PubSubManager.getInstance(omemoManager.getConnection(),omemoManager.getOwnJid()); + try { + omemoManager.requestDeviceListUpdateFor(omemoManager.getOwnJid()); + } catch (SmackException.NotConnectedException | InterruptedException | SmackException.NoResponseException e) { + // ignore + } + + CachedDeviceList deviceList = OmemoService.getInstance().getOmemoStoreBackend() + .loadCachedDeviceList(omemoManager.getOwnDevice(), omemoManager.getOwnJid()); + + for (int id : deviceList.getAllDevices()) { + try { + pm.getLeafNode(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(id)).deleteAllItems(); + } catch (InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException | + PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException | + PubSubException.NotAPubSubNodeException e) + { + // Silent + } + + try { + pm.deleteNode(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(id)); + } catch (SmackException.NoResponseException | InterruptedException | SmackException.NotConnectedException + | XMPPException.XMPPErrorException e) + { + // Silent + } + } + + try { + pm.getLeafNode(OmemoConstants.PEP_NODE_DEVICE_LIST).deleteAllItems(); + } catch (InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException | + PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException | + PubSubException.NotAPubSubNodeException e) + { + // Silent + } + + try { + pm.deleteNode(OmemoConstants.PEP_NODE_DEVICE_LIST); + } catch (SmackException.NoResponseException | InterruptedException | SmackException.NotConnectedException | + XMPPException.XMPPErrorException e) + { + // Silent + } + } + + public static void cleanUpRoster(OmemoManager omemoManager) { + Roster roster = Roster.getInstanceFor(omemoManager.getConnection()); + for (RosterEntry r : roster.getEntries()) { + try { + roster.removeEntry(r); + } catch (InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException | + XMPPException.XMPPErrorException | SmackException.NotLoggedInException e) + { + // Silent + } + } + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessageSendingTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessageSendingTest.java deleted file mode 100644 index e7b3011bf..000000000 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessageSendingTest.java +++ /dev/null @@ -1,193 +0,0 @@ -/** - * - * Copyright 2017 Florian Schmaus, 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 junit.framework.TestCase.assertEquals; -import static junit.framework.TestCase.assertNotSame; -import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.cleanServerSideTraces; -import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.setUpOmemoManager; -import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.subscribe; -import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.unidirectionalTrust; - -import java.security.NoSuchAlgorithmException; -import java.util.logging.Level; - -import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.chat2.ChatManager; -import org.jivesoftware.smack.packet.Message; - -import org.jivesoftware.smackx.omemo.element.OmemoBundleElement; -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.UndecidedOmemoIdentityException; -import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag; -import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation; -import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener; -import org.jivesoftware.smackx.pubsub.PubSubException; -import org.jivesoftware.smackx.pubsub.PubSubException.NotAPubSubNodeException; - -import junit.framework.TestCase; -import org.igniterealtime.smack.inttest.SmackIntegrationTest; -import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; -import org.igniterealtime.smack.inttest.TestNotPossibleException; -import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; - -/** - * Test message sending. - */ -public class OmemoMessageSendingTest extends AbstractOmemoIntegrationTest { - - private OmemoManager alice, bob; - private OmemoStore store; - - public OmemoMessageSendingTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, TestNotPossibleException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { - super(environment); - } - - @Override - public void before() { - alice = OmemoManager.getInstanceFor(conOne, 123); - bob = OmemoManager.getInstanceFor(conTwo, 345); - store = OmemoService.getInstance().getOmemoStoreBackend(); - } - - /** - * This Test tests sending and receiving messages. - * Alice and Bob create fresh devices, then they add another to their rosters. - * Next they build sessions with one another and Alice sends a message to Bob. - * After receiving and successfully decrypting the message, its tested, if Bob - * publishes a new Bundle. After that Bob replies to the message and its tested, - * whether Alice can decrypt the message and if she does NOT publish a new Bundle. - * - * @throws CorruptedOmemoKeyException - * @throws InterruptedException - * @throws SmackException.NoResponseException - * @throws SmackException.NotConnectedException - * @throws XMPPException.XMPPErrorException - * @throws SmackException.NotLoggedInException - * @throws PubSubException.NotALeafNodeException - * @throws CannotEstablishOmemoSessionException - * @throws UndecidedOmemoIdentityException - * @throws NoSuchAlgorithmException - * @throws CryptoFailedException - * @throws NotAPubSubNodeException - */ - @SmackIntegrationTest - public void messageSendingTest() - throws CorruptedOmemoKeyException, InterruptedException, SmackException.NoResponseException, - SmackException.NotConnectedException, XMPPException.XMPPErrorException, - SmackException.NotLoggedInException, PubSubException.NotALeafNodeException, - CannotEstablishOmemoSessionException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, - CryptoFailedException, PubSubException.NotAPubSubNodeException { - final String alicesSecret = "Hey Bob! I love you!"; - final String bobsSecret = "I love you too, Alice."; //aww <3 - - final SimpleResultSyncPoint messageOneSyncPoint = new SimpleResultSyncPoint(); - final SimpleResultSyncPoint messageTwoSyncPoint = new SimpleResultSyncPoint(); - - // Subscribe to one another - subscribe(alice, bob, "Bob"); - subscribe(bob, alice,"Alice"); - - // initialize OmemoManagers - setUpOmemoManager(alice); - setUpOmemoManager(bob); - - // Save initial bundles - OmemoBundleElement aliceBundle = store.packOmemoBundle(alice); - OmemoBundleElement bobsBundle = store.packOmemoBundle(bob); - - // Trust - unidirectionalTrust(alice, bob); - unidirectionalTrust(bob, alice); - - // Register messageListeners - bob.addOmemoMessageListener(new OmemoMessageListener() { - @Override - public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - LOGGER.log(Level.INFO,"Bob received message: " + decryptedBody); - if (decryptedBody.trim().equals(alicesSecret.trim())) { - messageOneSyncPoint.signal(); - } else { - messageOneSyncPoint.signal(new Exception("Received message must equal sent message.")); - } - } - - @Override - public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - } - }); - - alice.addOmemoMessageListener(new OmemoMessageListener() { - @Override - public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - LOGGER.log(Level.INFO, "Alice received message: " + decryptedBody); - if (decryptedBody.trim().equals(bobsSecret.trim())) { - messageTwoSyncPoint.signal(); - } else { - messageTwoSyncPoint.signal(new Exception("Received message must equal sent message.")); - } - } - - @Override - public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - - } - }); - - // Prepare Alice message for Bob - Message encryptedA = alice.encrypt(bob.getOwnJid(), alicesSecret); - ChatManager.getInstanceFor(alice.getConnection()).chatWith(bob.getOwnJid().asEntityBareJidIfPossible()) - .send(encryptedA); - - try { - messageOneSyncPoint.waitForResult(10 * 1000); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Exception while waiting for message: " + e, e); - TestCase.fail("Bob must have received Alice message."); - } - - // Check if Bob published a new Bundle - assertNotSame("Bob must have published another bundle at this point, since we used a PreKeyMessage.", - bobsBundle, OmemoService.fetchBundle(alice, bob.getOwnDevice())); - - // Prepare Bobs response - Message encryptedB = bob.encrypt(alice.getOwnJid(), bobsSecret); - ChatManager.getInstanceFor(bob.getConnection()).chatWith(alice.getOwnJid().asEntityBareJidIfPossible()) - .send(encryptedB); - - try { - messageTwoSyncPoint.waitForResult(10 * 1000); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Exception while waiting for response: " + e, e); - TestCase.fail("Alice must have received a response from Bob."); - } - - assertEquals("Alice must not have published a new bundle, since we built the session using Bobs bundle.", - aliceBundle, OmemoService.fetchBundle(bob, alice.getOwnDevice())); - } - - @Override - public void after() { - alice.shutdown(); - bob.shutdown(); - cleanServerSideTraces(alice); - cleanServerSideTraces(bob); - } -} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoSessionRenegotiationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoSessionRenegotiationTest.java deleted file mode 100644 index 1b4542252..000000000 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoSessionRenegotiationTest.java +++ /dev/null @@ -1,195 +0,0 @@ -/** - * - * Copyright 2017 Florian Schmaus, 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 junit.framework.TestCase.assertEquals; -import static junit.framework.TestCase.fail; -import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.cleanServerSideTraces; -import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.setUpOmemoManager; -import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.subscribe; -import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.unidirectionalTrust; - -import java.util.logging.Level; - -import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.chat2.ChatManager; -import org.jivesoftware.smack.packet.Message; - -import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag; -import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation; -import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener; - -import org.igniterealtime.smack.inttest.SmackIntegrationTest; -import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; -import org.igniterealtime.smack.inttest.TestNotPossibleException; -import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; - -/** - * Test session renegotiation. - */ -public class OmemoSessionRenegotiationTest extends AbstractOmemoIntegrationTest { - - private OmemoManager alice, bob; - private OmemoStore store; - - public OmemoSessionRenegotiationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, TestNotPossibleException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { - super(environment); - } - - @Override - public void before() { - alice = OmemoManager.getInstanceFor(conOne, 1337); - bob = OmemoManager.getInstanceFor(conTwo, 1009); - store = OmemoService.getInstance().getOmemoStoreBackend(); - } - - @SmackIntegrationTest - public void sessionRenegotiationTest() throws Exception { - - final boolean[] phaseTwo = new boolean[1]; - final SimpleResultSyncPoint sp1 = new SimpleResultSyncPoint(); - final SimpleResultSyncPoint sp2 = new SimpleResultSyncPoint(); - final SimpleResultSyncPoint sp3 = new SimpleResultSyncPoint(); - final SimpleResultSyncPoint sp4 = new SimpleResultSyncPoint(); - - final String m1 = "1: Alice says hello to bob."; - final String m2 = "2: Bob replies to Alice."; - final String m3 = "3. This message will arrive but Bob cannot decrypt it."; - final String m4 = "4. This message is readable by Bob again."; - - subscribe(alice, bob, "Bob"); - subscribe(bob, alice, "Alice"); - - setUpOmemoManager(alice); - setUpOmemoManager(bob); - - unidirectionalTrust(alice, bob); - unidirectionalTrust(bob, alice); - - OmemoMessageListener first = new OmemoMessageListener() { - @Override - public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - LOGGER.log(Level.INFO, "Bob received OMEMO message: " + decryptedBody); - assertEquals("Received message MUST match the one we sent.", decryptedBody, m1); - sp1.signal(); - } - - @Override - public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - - } - }; - bob.addOmemoMessageListener(first); - - ChatManager.getInstanceFor(alice.getConnection()).chatWith(bob.getOwnJid().asEntityBareJidIfPossible()) - .send(alice.encrypt(bob.getOwnJid(), m1)); - - sp1.waitForResult(10 * 1000); - - bob.removeOmemoMessageListener(first); - - OmemoMessageListener second = new OmemoMessageListener() { - @Override - public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - LOGGER.log(Level.INFO, "Alice received OMEMO message: " + decryptedBody); - assertEquals("Reply must match the message we sent.", decryptedBody, m2); - sp2.signal(); - } - - @Override - public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - - } - }; - alice.addOmemoMessageListener(second); - - ChatManager.getInstanceFor(bob.getConnection()).chatWith(alice.getOwnJid().asEntityBareJidIfPossible()) - .send(bob.encrypt(alice.getOwnJid(), m2)); - - sp2.waitForResult(10 * 1000); - - alice.removeOmemoMessageListener(second); - - store.forgetOmemoSessions(bob); - store.removeAllRawSessionsOf(bob, alice.getOwnJid()); - - OmemoMessageListener third = new OmemoMessageListener() { - @Override - public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - fail("Bob should not have received a decipherable message: " + decryptedBody); - } - - @Override - public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - - } - }; - bob.addOmemoMessageListener(third); - - OmemoMessageListener fourth = new OmemoMessageListener() { - @Override - public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - - } - - @Override - public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - LOGGER.log(Level.INFO, "Alice received preKeyMessage."); - sp3.signal(); - } - }; - alice.addOmemoMessageListener(fourth); - - ChatManager.getInstanceFor(alice.getConnection()).chatWith(bob.getOwnJid().asEntityBareJidIfPossible()) - .send(alice.encrypt(bob.getOwnJid(), m3)); - - sp3.waitForResult(10 * 1000); - - bob.removeOmemoMessageListener(third); - alice.removeOmemoMessageListener(fourth); - - OmemoMessageListener fifth = new OmemoMessageListener() { - @Override - public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - LOGGER.log(Level.INFO, "Bob received an OMEMO message: " + decryptedBody); - assertEquals("The received message must match the one we sent.", - decryptedBody, m4); - sp4.signal(); - } - - @Override - public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - - } - }; - bob.addOmemoMessageListener(fifth); - - ChatManager.getInstanceFor(alice.getConnection()).chatWith(bob.getOwnJid().asEntityBareJidIfPossible()) - .send(alice.encrypt(bob.getOwnJid(), m4)); - - sp4.waitForResult(10 * 1000); - } - - @Override - public void after() { - alice.shutdown(); - bob.shutdown(); - cleanServerSideTraces(alice); - cleanServerSideTraces(bob); - } -} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java deleted file mode 100644 index 6a8a37a20..000000000 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java +++ /dev/null @@ -1,163 +0,0 @@ -/** - * - * Copyright 2017 Florian Schmaus, 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 junit.framework.TestCase.assertEquals; -import static junit.framework.TestCase.assertFalse; -import static junit.framework.TestCase.assertNotNull; -import static junit.framework.TestCase.assertNotSame; -import static junit.framework.TestCase.assertNull; -import static junit.framework.TestCase.assertTrue; -import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.cleanServerSideTraces; -import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.deletePath; -import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.setUpOmemoManager; - -import java.util.Date; - -import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.XMPPException; - -import org.igniterealtime.smack.inttest.SmackIntegrationTest; -import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; -import org.igniterealtime.smack.inttest.TestNotPossibleException; - -/** - * Test the OmemoStore. - */ -public class OmemoStoreTest extends AbstractOmemoIntegrationTest { - - private OmemoManager alice; - private OmemoManager bob; - - public OmemoStoreTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { - super(environment); - } - - @Override - public void before() { - alice = OmemoManager.getInstanceFor(conOne); - bob = OmemoManager.getInstanceFor(conOne); - } - - @SmackIntegrationTest - public void storeTest() throws Exception { - - // ########### PRE-INITIALIZATION ############ - - assertEquals("Creating an OmemoManager without MUST have set the default deviceId.", alice.getDeviceId(), OmemoService.getInstance().getOmemoStoreBackend().getDefaultDeviceId(alice.getOwnJid())); - assertEquals("OmemoManager must be equal, since both got created without giving a deviceId.", alice, bob); - OmemoService.getInstance().getOmemoStoreBackend().setDefaultDeviceId(alice.getOwnJid(), -1); //Reset default deviceId - - alice.shutdown(); - - alice = OmemoManager.getInstanceFor(conOne); - assertNotSame("Instantiating OmemoManager without deviceId MUST assign random deviceId.", alice.getDeviceId(), bob.getDeviceId()); - - OmemoStore store = OmemoService.getInstance().getOmemoStoreBackend(); - OmemoFingerprint finger = new OmemoFingerprint("FINGER"); - // DefaultDeviceId - store.setDefaultDeviceId(alice.getOwnJid(), 777); - assertEquals("defaultDeviceId setting/getting must equal.", 777, store.getDefaultDeviceId(alice.getOwnJid())); - - // Trust/Distrust/Decide - bob.shutdown(); - bob = OmemoManager.getInstanceFor(conTwo, 998); - assertFalse("Bobs device MUST be undecided at this point", - store.isDecidedOmemoIdentity(alice, bob.getOwnDevice(), finger)); - assertFalse("Bobs device MUST not be trusted at this point", - store.isTrustedOmemoIdentity(alice, bob.getOwnDevice(), finger)); - store.trustOmemoIdentity(alice, bob.getOwnDevice(), finger); - assertTrue("Bobs device MUST be trusted at this point.", - store.isTrustedOmemoIdentity(alice, bob.getOwnDevice(), finger)); - assertTrue("Bobs device MUST be decided at this point.", - store.isDecidedOmemoIdentity(alice, bob.getOwnDevice(), finger)); - store.distrustOmemoIdentity(alice, bob.getOwnDevice(), finger); - assertFalse("Bobs device MUST be untrusted at this point.", - store.isTrustedOmemoIdentity(alice, bob.getOwnDevice(), finger)); - - // Dates - assertNull("Date of last received message must be null when no message was received ever.", - store.getDateOfLastReceivedMessage(alice, bob.getOwnDevice())); - Date now = new Date(); - store.setDateOfLastReceivedMessage(alice, bob.getOwnDevice(), now); - assertEquals("Date of last received message must match the one we set.", - now, store.getDateOfLastReceivedMessage(alice, bob.getOwnDevice())); - assertNull("Date of last signed preKey renewal must be null.", - store.getDateOfLastSignedPreKeyRenewal(alice)); - store.setDateOfLastSignedPreKeyRenewal(alice, now); - assertEquals("Date of last signed preKey renewal must match our date.", - now, store.getDateOfLastSignedPreKeyRenewal(alice)); - - // Keys - assertNull("IdentityKeyPair must be null at this point.", - store.loadOmemoIdentityKeyPair(alice)); - assertNull("IdentityKey of contact must be null at this point.", - store.loadOmemoIdentityKey(alice, bob.getOwnDevice())); - assertEquals("PreKeys list must be of length 0 at this point.", - 0, store.loadOmemoPreKeys(alice).size()); - assertEquals("SignedPreKeys list must be of length 0 at this point.", - 0, store.loadOmemoSignedPreKeys(alice).size()); - - assertNotNull("Generated IdentityKeyPair must not be null.", - store.generateOmemoIdentityKeyPair()); - assertEquals("Generated PreKey list must be of correct length.", - 100, store.generateOmemoPreKeys(1, 100).size()); - - - // LastPreKeyId - assertEquals("LastPreKeyId must be 0 at this point.", - 0, store.loadLastPreKeyId(alice)); - store.storeLastPreKeyId(alice, 1234); - Thread.sleep(100); - assertEquals("LastPreKeyId set/get must equal.", 1234, store.loadLastPreKeyId(alice)); - store.storeLastPreKeyId(alice, 0); - - // CurrentSignedPreKeyId - assertEquals("CurrentSignedPreKeyId must be 0 at this point.", - 0, store.loadCurrentSignedPreKeyId(alice)); - store.storeCurrentSignedPreKeyId(alice, 554); - Thread.sleep(100); - assertEquals("CurrentSignedPreKeyId must match the value we set.", - 554, store.loadCurrentSignedPreKeyId(alice)); - store.storeCurrentSignedPreKeyId(alice, 0); - - deletePath(alice); - - // ################# POST-INITIALIZATION ################# - setUpOmemoManager(alice); - - // Keys - assertNotNull("IdentityKeyPair must not be null after initialization", - store.loadOmemoIdentityKeyPair(alice)); - assertNotSame("LastPreKeyId must not be 0 after initialization.", - 0, store.loadLastPreKeyId(alice)); - assertNotSame("currentSignedPreKeyId must not be 0 after initialization.", - 0, store.loadCurrentSignedPreKeyId(alice)); - assertNotNull("The last PreKey must not be null.", - store.loadOmemoPreKey(alice, store.loadLastPreKeyId(alice) - 1)); - assertNotNull("The current signedPreKey must not be null.", - store.loadOmemoSignedPreKey(alice, store.loadCurrentSignedPreKeyId(alice))); - } - - @Override - public void after() { - cleanServerSideTraces(alice); - cleanServerSideTraces(bob); - alice.shutdown(); - bob.shutdown(); - } -} diff --git a/smack-omemo-signal/build.gradle b/smack-omemo-signal/build.gradle index 9ec09844d..66ed215bc 100644 --- a/smack-omemo-signal/build.gradle +++ b/smack-omemo-signal/build.gradle @@ -10,7 +10,8 @@ dependencies { compile project(":smack-im") compile project(":smack-extensions") compile project(":smack-omemo") - compile 'org.whispersystems:signal-protocol-java:2.4.0' + compile 'org.whispersystems:signal-protocol-java:2.6.2' testCompile project(path: ":smack-core", configuration: "testRuntime") + testCompile project(path: ":smack-omemo", configuration: "testRuntime") } diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalCachingOmemoStore.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalCachingOmemoStore.java new file mode 100644 index 000000000..cfadac46e --- /dev/null +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalCachingOmemoStore.java @@ -0,0 +1,62 @@ +/** + * + * 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.smackx.omemo.signal; + +import org.jivesoftware.smackx.omemo.CachingOmemoStore; +import org.jivesoftware.smackx.omemo.OmemoStore; + +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; + +/** + * Implementation of the CachingOmemoStore for smack-omemo-signal. + * This Store implementation can either be used as a proxy wrapping a persistent SignalOmemoStore in order to prevent + * excessive storage access, or it can be used standalone as an ephemeral store, which doesn't persist its contents. + */ +public class SignalCachingOmemoStore extends CachingOmemoStore +{ + + /** + * Create a new SignalCachingOmemoStore as a caching layer around a persisting OmemoStore + * (eg. a SignalFileBasedOmemoStore). + * @param wrappedStore + */ + public SignalCachingOmemoStore(OmemoStore wrappedStore) + { + super(wrappedStore); + } + + /** + * Create a new SignalCachingOmemoStore as an ephemeral standalone OmemoStore. + */ + public SignalCachingOmemoStore() { + super(new SignalOmemoKeyUtil()); + } +} diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalFileBasedOmemoStore.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalFileBasedOmemoStore.java index 89ea1f36b..014da93ae 100644 --- a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalFileBasedOmemoStore.java +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalFileBasedOmemoStore.java @@ -42,7 +42,9 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord; */ @SuppressWarnings("unused") public class SignalFileBasedOmemoStore - extends FileBasedOmemoStore { + extends FileBasedOmemoStore +{ public SignalFileBasedOmemoStore() { super(); @@ -53,7 +55,9 @@ public class SignalFileBasedOmemoStore } @Override - public OmemoKeyUtil keyUtil() { + public OmemoKeyUtil keyUtil() + { return new SignalOmemoKeyUtil(); } } 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 1306b790c..3d1935f5c 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 @@ -21,16 +21,13 @@ package org.jivesoftware.smackx.omemo.signal; import java.io.IOException; -import java.util.HashMap; import java.util.List; +import java.util.TreeMap; import org.jivesoftware.smackx.omemo.OmemoFingerprint; -import org.jivesoftware.smackx.omemo.OmemoManager; -import org.jivesoftware.smackx.omemo.OmemoStore; import org.jivesoftware.smackx.omemo.element.OmemoBundleVAxolotlElement; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; -import org.jivesoftware.smackx.omemo.internal.OmemoSession; import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil; import org.jxmpp.jid.impl.JidCreate; @@ -54,25 +51,27 @@ import org.whispersystems.libsignal.util.KeyHelper; * @author Paul Schaub */ public class SignalOmemoKeyUtil extends OmemoKeyUtil { - + SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> +{ @Override public IdentityKeyPair generateOmemoIdentityKeyPair() { return KeyHelper.generateIdentityKeyPair(); } @Override - public HashMap generateOmemoPreKeys(int currentPreKeyId, int count) { + public TreeMap generateOmemoPreKeys(int currentPreKeyId, int count) { List preKeyRecords = KeyHelper.generatePreKeys(currentPreKeyId, count); - HashMap hashMap = new HashMap<>(); + TreeMap map = new TreeMap<>(); for (PreKeyRecord p : preKeyRecords) { - hashMap.put(p.getId(), p); + map.put(p.getId(), p); } - return hashMap; + return map; } @Override - public SignedPreKeyRecord generateOmemoSignedPreKey(IdentityKeyPair identityKeyPair, int currentPreKeyId) throws CorruptedOmemoKeyException { + public SignedPreKeyRecord generateOmemoSignedPreKey(IdentityKeyPair identityKeyPair, int currentPreKeyId) + throws CorruptedOmemoKeyException + { try { return KeyHelper.generateSignedPreKey(identityKeyPair, currentPreKeyId); } catch (InvalidKeyException e) { @@ -82,6 +81,7 @@ public class SignalOmemoKeyUtil extends OmemoKeyUtil - createOmemoSession(OmemoManager omemoManager, OmemoStore omemoStore, - OmemoDevice contact, IdentityKey identityKey) { - return new SignalOmemoSession(omemoManager, omemoStore, contact, identityKey); - } - - @Override - public OmemoSession - createOmemoSession(OmemoManager omemoManager, OmemoStore omemoStore, OmemoDevice from) { - return new SignalOmemoSession(omemoManager, omemoStore, from); - } - @Override public SessionRecord rawSessionFromBytes(byte[] data) throws IOException { + if (data == null) return null; return new SessionRecord(data); } @@ -115,6 +103,7 @@ public class SignalOmemoKeyUtil extends OmemoKeyUtil { - +public final class SignalOmemoService + extends OmemoService +{ private static SignalOmemoService INSTANCE; private static boolean LICENSE_ACKNOWLEDGED = false; - public static void setup() throws InvalidKeyException, XMPPErrorException, NoSuchPaddingException, InvalidAlgorithmParameterException, UnsupportedEncodingException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, NoSuchProviderException, SmackException, InterruptedException, CorruptedOmemoKeyException { + @Override + protected SignalOmemoSessionManager createOmemoSessionManager( + OmemoManager.KnownBareJidGuard manager, + OmemoStore store) + { + return new SignalOmemoSessionManager(manager, getOmemoStoreBackend()); + } + + public static void setup() + throws InvalidKeyException, XMPPErrorException, NoSuchPaddingException, InvalidAlgorithmParameterException, + UnsupportedEncodingException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, + NoSuchProviderException, SmackException, InterruptedException, CorruptedOmemoKeyException + { if (!LICENSE_ACKNOWLEDGED) { - throw new IllegalStateException("smack-omemo-signal is licensed under the terms of the GPLv3. Please be aware that you " + - "can only use this library within the terms of the GPLv3. See for example " + - "https://www.gnu.org/licenses/quick-guide-gplv3 for more details. Please call " + + throw new IllegalStateException("smack-omemo-signal is licensed under the terms of the GPLv3. " + + "Please be aware that you can only use this library within the terms of the GPLv3. " + + "See for example https://www.gnu.org/licenses/quick-guide-gplv3 for more details. Please call " + "SignalOmemoService.acknowledgeLicense() prior to the setup() method in order to prevent " + "this exception."); } @@ -79,8 +92,10 @@ public final class SignalOmemoService extends OmemoService createDefaultOmemoStoreBackend() { - return new SignalFileBasedOmemoStore(); + public OmemoStore + createDefaultOmemoStoreBackend() { + return new SignalCachingOmemoStore(); } private SignalOmemoService() @@ -96,14 +111,17 @@ public final class SignalOmemoService extends OmemoService { - private static final Logger LOGGER = Logger.getLogger(SignalOmemoSession.class.getName()); - - /** - * Constructor used when the remote user initialized the session using a PreKeyOmemoMessage. - * - * @param omemoManager omemoManager - * @param omemoStore omemoStoreConnector that can be used to get information from - * @param remoteContact omemoDevice of the remote contact - * @param identityKey identityKey of the remote contact - */ - SignalOmemoSession(OmemoManager omemoManager, OmemoStore omemoStore, - OmemoDevice remoteContact, IdentityKey identityKey) { - super(omemoManager, omemoStore, remoteContact, identityKey); - } - - /** - * Constructor used when we initiate a new Session with the remote user. - * - * @param omemoManager omemoManager - * @param omemoStore omemoStore used to get information from - * @param remoteContact omemoDevice of the remote contact - */ - SignalOmemoSession(OmemoManager omemoManager, - OmemoStore omemoStore, - OmemoDevice remoteContact) { - super(omemoManager, omemoStore, remoteContact); - } - - @Override - public SessionCipher createCipher(OmemoDevice contact) { - SignalOmemoStoreConnector connector = new SignalOmemoStoreConnector(omemoManager, omemoStore); - return new SessionCipher(connector, connector, connector, connector, - omemoStore.keyUtil().omemoDeviceAsAddress(contact)); - } - - @Override - public CiphertextTuple encryptMessageKey(byte[] messageKey) { - CiphertextMessage ciphertextMessage; - ciphertextMessage = cipher.encrypt(messageKey); - int type = (ciphertextMessage.getType() == CiphertextMessage.PREKEY_TYPE ? - OmemoElement.TYPE_OMEMO_PREKEY_MESSAGE : OmemoElement.TYPE_OMEMO_MESSAGE); - return new CiphertextTuple(ciphertextMessage.serialize(), type); - } - - @Override - public byte[] decryptMessageKey(byte[] encryptedKey) throws NoRawSessionException { - byte[] decryptedKey = null; - try { - try { - PreKeySignalMessage message = new PreKeySignalMessage(encryptedKey); - if (!message.getPreKeyId().isPresent()) { - LOGGER.log(Level.WARNING, "PreKeySignalMessage did not contain a PreKeyId"); - return null; - } - LOGGER.log(Level.INFO, "PreKeySignalMessage received, new session ID: " + message.getSignedPreKeyId() + "/" + message.getPreKeyId().get()); - IdentityKey messageIdentityKey = message.getIdentityKey(); - if (this.identityKey != null && !this.identityKey.equals(messageIdentityKey)) { - LOGGER.log(Level.INFO, "Had session with fingerprint " + getFingerprint() + - ", received message with different fingerprint " + omemoStore.keyUtil().getFingerprint(messageIdentityKey) + - ". Silently drop the message."); - } else { - this.identityKey = messageIdentityKey; - decryptedKey = cipher.decrypt(message); - this.preKeyId = message.getPreKeyId().get(); - } - } catch (InvalidMessageException | InvalidVersionException e) { - SignalMessage message = new SignalMessage(encryptedKey); - decryptedKey = cipher.decrypt(message); - } catch (InvalidKeyIdException e) { - throw new NoRawSessionException(e); - } - catch (InvalidKeyException | UntrustedIdentityException e) { - LOGGER.log(Level.SEVERE, "Error decrypting message header, " + e.getClass().getName() + ": " + e.getMessage()); - } - } catch (InvalidMessageException | NoSessionException e) { - throw new NoRawSessionException(e); - } catch (LegacyMessageException | DuplicateMessageException e) { - LOGGER.log(Level.SEVERE, "Error decrypting message header, " + e.getClass().getName() + ": " + e.getMessage()); - } - return decryptedKey; - } -} diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoSessionManager.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoSessionManager.java new file mode 100644 index 000000000..c7abaa1fe --- /dev/null +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoSessionManager.java @@ -0,0 +1,164 @@ +/** + * + * 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.smackx.omemo.signal; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jivesoftware.smackx.omemo.OmemoManager; +import org.jivesoftware.smackx.omemo.OmemoSessionManager; +import org.jivesoftware.smackx.omemo.OmemoStore; +import org.jivesoftware.smackx.omemo.element.OmemoElement; +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.UntrustedOmemoIdentityException; +import org.jivesoftware.smackx.omemo.internal.CiphertextTuple; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; + +import org.whispersystems.libsignal.DuplicateMessageException; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.InvalidKeyIdException; +import org.whispersystems.libsignal.InvalidMessageException; +import org.whispersystems.libsignal.InvalidVersionException; +import org.whispersystems.libsignal.LegacyMessageException; +import org.whispersystems.libsignal.NoSessionException; +import org.whispersystems.libsignal.SessionCipher; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.UntrustedIdentityException; +import org.whispersystems.libsignal.ecc.ECPublicKey; +import org.whispersystems.libsignal.protocol.CiphertextMessage; +import org.whispersystems.libsignal.protocol.PreKeySignalMessage; +import org.whispersystems.libsignal.protocol.SignalMessage; +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 SignalOmemoSessionManager + extends OmemoSessionManager +{ + private static final Logger LOGGER = Logger.getLogger(OmemoSessionManager.class.getName()); + private final SignalOmemoStoreConnector storeConnector; + + public SignalOmemoSessionManager(OmemoManager.KnownBareJidGuard managerGuard, + OmemoStore store) + { + super(managerGuard, store); + this.storeConnector = new SignalOmemoStoreConnector(managerGuard, store); + } + + @Override + public byte[] doubleRatchetDecrypt(OmemoDevice sender, byte[] encryptedKey) + throws CorruptedOmemoKeyException, NoRawSessionException, CryptoFailedException, + UntrustedOmemoIdentityException + { + SessionCipher cipher = getCipher(sender); + byte[] decryptedKey; + + // Try to handle the message as a PreKeySignalMessage... + try { + PreKeySignalMessage preKeyMessage = new PreKeySignalMessage(encryptedKey); + + if (!preKeyMessage.getPreKeyId().isPresent()) { + throw new CryptoFailedException("PreKeyMessage did not contain a preKeyId."); + } + + IdentityKey messageIdentityKey = preKeyMessage.getIdentityKey(); + IdentityKey previousIdentityKey = store.loadOmemoIdentityKey(storeConnector.getOurDevice(), sender); + + if (previousIdentityKey != null && + !previousIdentityKey.getFingerprint().equals(messageIdentityKey.getFingerprint())) { + throw new UntrustedOmemoIdentityException(sender, + store.keyUtil().getFingerprintOfIdentityKey(previousIdentityKey), + store.keyUtil().getFingerprintOfIdentityKey(messageIdentityKey)); + } + + try { + decryptedKey = cipher.decrypt(preKeyMessage); + } + catch (UntrustedIdentityException e) { + throw new AssertionError("Signals trust management MUST be disabled."); + } + catch (LegacyMessageException | InvalidKeyException e) { + throw new CryptoFailedException(e); + } + catch (InvalidKeyIdException e) { + throw new NoRawSessionException(e); + } + catch (DuplicateMessageException e) { + LOGGER.log(Level.INFO, "Decryption of PreKeyMessage from " + sender + + " failed, since the message has been decrypted before."); + return null; + } + + } catch (InvalidVersionException | InvalidMessageException noPreKeyMessage) { + // ...if that fails, handle it as a SignalMessage + try { + SignalMessage message = new SignalMessage(encryptedKey); + decryptedKey = getCipher(sender).decrypt(message); + } + catch (UntrustedIdentityException e) { + throw new AssertionError("Signals trust management MUST be disabled."); + } + catch (InvalidMessageException | NoSessionException e) { + throw new NoRawSessionException(e); + } + catch (LegacyMessageException e) { + throw new CryptoFailedException(e); + } + catch (DuplicateMessageException e1) { + LOGGER.log(Level.INFO, "Decryption of SignalMessage from " + sender + + " failed, since the message has been decrypted before."); + return null; + } + } + + return decryptedKey; + } + + @Override + public CiphertextTuple doubleRatchetEncrypt(OmemoDevice recipient, byte[] messageKey) { + CiphertextMessage ciphertextMessage; + try { + ciphertextMessage = getCipher(recipient).encrypt(messageKey); + } catch (UntrustedIdentityException e) { + throw new AssertionError("Signals trust management MUST be disabled."); + } + + // TODO: Figure out, if this is enough... + int type = (ciphertextMessage.getType() == CiphertextMessage.PREKEY_TYPE ? + OmemoElement.TYPE_OMEMO_PREKEY_MESSAGE : OmemoElement.TYPE_OMEMO_MESSAGE); + + return new CiphertextTuple(ciphertextMessage.serialize(), type); + } + + private SessionCipher getCipher(OmemoDevice device) { + SignalOmemoKeyUtil keyUtil = (SignalOmemoKeyUtil) store.keyUtil(); + SignalProtocolAddress address = keyUtil.omemoDeviceAsAddress(device); + return new SessionCipher(storeConnector, storeConnector, storeConnector, storeConnector, address); + } +} diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStore.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStore.java index e5faa976f..38cd5a6d1 100644 --- a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStore.java +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStore.java @@ -40,12 +40,14 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord; */ @SuppressWarnings("unused") public abstract class SignalOmemoStore - extends OmemoStore { + extends OmemoStore { private final SignalOmemoKeyUtil signalKeyUtil = new SignalOmemoKeyUtil(); @Override - public OmemoKeyUtil keyUtil() { + public OmemoKeyUtil keyUtil() { return signalKeyUtil; } } diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStoreConnector.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStoreConnector.java index f82a0ddda..ba7c30faa 100644 --- a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStoreConnector.java +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStoreConnector.java @@ -21,15 +21,17 @@ package org.jivesoftware.smackx.omemo.signal; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; +import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smackx.omemo.OmemoManager; import org.jivesoftware.smackx.omemo.OmemoStore; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; +import org.jxmpp.jid.BareJid; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.stringprep.XmppStringprepException; import org.whispersystems.libsignal.IdentityKey; @@ -57,21 +59,28 @@ public class SignalOmemoStoreConnector private static final Logger LOGGER = Logger.getLogger(SignalOmemoStoreConnector.class.getName()); - private final OmemoManager omemoManager; - private final OmemoStore + private final OmemoManager.KnownBareJidGuard managerGuard; + private final OmemoStore omemoStore; - public SignalOmemoStoreConnector(OmemoManager omemoManager, OmemoStore store) { - this.omemoManager = omemoManager; + public SignalOmemoStoreConnector(OmemoManager.KnownBareJidGuard managerGuard, OmemoStore store) { + this.managerGuard = managerGuard; this.omemoStore = store; } + OmemoDevice getOurDevice() { + return managerGuard.get().getOwnDevice(); + } + @Override public IdentityKeyPair getIdentityKeyPair() { try { - return omemoStore.loadOmemoIdentityKeyPair(omemoManager); + return omemoStore.loadOmemoIdentityKeyPair(getOurDevice()); } catch (CorruptedOmemoKeyException e) { - LOGGER.log(Level.SEVERE, "getIdentityKeyPair has failed: " + e, e); + LOGGER.log(Level.SEVERE, "IdentityKeyPair seems to be invalid.", e); return null; } } @@ -86,33 +95,41 @@ public class SignalOmemoStoreConnector } @Override - public void saveIdentity(SignalProtocolAddress signalProtocolAddress, IdentityKey identityKey) { + public boolean saveIdentity(SignalProtocolAddress signalProtocolAddress, IdentityKey identityKey) { + OmemoDevice device; try { - omemoStore.storeOmemoIdentityKey(omemoManager, omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress), identityKey); + device = omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress); } catch (XmppStringprepException e) { throw new AssertionError(e); } + + omemoStore.storeOmemoIdentityKey(getOurDevice(), device, identityKey); + return true; } @Override - public boolean isTrustedIdentity(SignalProtocolAddress signalProtocolAddress, IdentityKey identityKey) { - // Disable internal trust management. Instead we use OmemoStore.isTrustedOmemoIdentity() before encrypting for a - // recipient. + public boolean isTrustedIdentity(SignalProtocolAddress signalProtocolAddress, + IdentityKey identityKey, + Direction direction) { + // Disable internal trust management. Instead we use OmemoStore.isTrustedOmemoIdentity() before encrypting + // for a recipient. return true; } @Override public PreKeyRecord loadPreKey(int i) throws InvalidKeyIdException { - PreKeyRecord pr = omemoStore.loadOmemoPreKey(omemoManager, i); - if (pr == null) { - throw new InvalidKeyIdException("No PreKey with Id " + i + " found!"); + PreKeyRecord preKey = omemoStore.loadOmemoPreKey(getOurDevice(), i); + + if (preKey == null) { + throw new InvalidKeyIdException("No PreKey with Id " + i + " found."); } - return pr; + + return preKey; } @Override public void storePreKey(int i, PreKeyRecord preKeyRecord) { - omemoStore.storeOmemoPreKey(omemoManager, i, preKeyRecord); + omemoStore.storeOmemoPreKey(getOurDevice(), i, preKeyRecord); } @Override @@ -120,96 +137,112 @@ public class SignalOmemoStoreConnector try { return (loadPreKey(i) != null); } catch (InvalidKeyIdException e) { - LOGGER.log(Level.WARNING, "containsPreKey has failed: " + e.getMessage()); return false; } } @Override public void removePreKey(int i) { - omemoStore.removeOmemoPreKey(omemoManager, i); + omemoStore.removeOmemoPreKey(getOurDevice(), i); } @Override public SessionRecord loadSession(SignalProtocolAddress signalProtocolAddress) { + OmemoDevice device; try { - SessionRecord s = omemoStore.loadRawSession(omemoManager, omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress)); - return (s != null ? s : new SessionRecord()); + device = omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress); } catch (XmppStringprepException e) { throw new AssertionError(e); } + + SessionRecord record = omemoStore.loadRawSession(getOurDevice(), device); + + if (record != null) { + return record; + } else { + return new SessionRecord(); + } } @Override public List getSubDeviceSessions(String s) { - HashMap contactsSessions; + BareJid jid; try { - contactsSessions = omemoStore.loadAllRawSessionsOf(omemoManager, JidCreate.bareFrom(s)); + jid = JidCreate.bareFrom(s); } catch (XmppStringprepException e) { throw new AssertionError(e); } - if (contactsSessions != null) { - return new ArrayList<>(contactsSessions.keySet()); - } - return new ArrayList<>(); + + return new ArrayList<>(omemoStore.loadAllRawSessionsOf(getOurDevice(), jid).keySet()); } @Override public void storeSession(SignalProtocolAddress signalProtocolAddress, SessionRecord sessionRecord) { + OmemoDevice device; try { - omemoStore.storeRawSession(omemoManager, omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress), sessionRecord); + device = omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress); } catch (XmppStringprepException e) { throw new AssertionError(e); } + + omemoStore.storeRawSession(getOurDevice(), device, sessionRecord); } @Override public boolean containsSession(SignalProtocolAddress signalProtocolAddress) { + OmemoDevice device; try { - return omemoStore.containsRawSession(omemoManager, omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress)); + device = omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress); } catch (XmppStringprepException e) { throw new AssertionError(e); } + + return omemoStore.containsRawSession(getOurDevice(), device); } @Override public void deleteSession(SignalProtocolAddress signalProtocolAddress) { + OmemoDevice device; try { - omemoStore.removeRawSession(omemoManager, omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress)); + device = omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress); } catch (XmppStringprepException e) { throw new AssertionError(e); } + + omemoStore.removeRawSession(getOurDevice(), device); } @Override public void deleteAllSessions(String s) { + BareJid jid; try { - omemoStore.removeAllRawSessionsOf(omemoManager, JidCreate.bareFrom(s)); + jid = JidCreate.bareFrom(s); } catch (XmppStringprepException e) { throw new AssertionError(e); } + + omemoStore.removeAllRawSessionsOf(getOurDevice(), jid); } @Override public SignedPreKeyRecord loadSignedPreKey(int i) throws InvalidKeyIdException { - SignedPreKeyRecord spkr = omemoStore.loadOmemoSignedPreKey(omemoManager, i); - if (spkr == null) { - throw new InvalidKeyIdException("No SignedPreKey with Id " + i + " found!"); - } - return spkr; + return omemoStore.loadOmemoSignedPreKey(getOurDevice(), i); } @Override public List loadSignedPreKeys() { - HashMap signedPreKeyRecordHashMap = omemoStore.loadOmemoSignedPreKeys(omemoManager); List signedPreKeyRecordList = new ArrayList<>(); + + TreeMap signedPreKeyRecordHashMap = + omemoStore.loadOmemoSignedPreKeys(getOurDevice()); signedPreKeyRecordList.addAll(signedPreKeyRecordHashMap.values()); + return signedPreKeyRecordList; } @Override public void storeSignedPreKey(int i, SignedPreKeyRecord signedPreKeyRecord) { - omemoStore.storeOmemoSignedPreKey(omemoManager, i, signedPreKeyRecord); + omemoStore.storeOmemoSignedPreKey(getOurDevice(), i, signedPreKeyRecord); } @Override @@ -224,6 +257,6 @@ public class SignalOmemoStoreConnector @Override public void removeSignedPreKey(int i) { - omemoStore.removeOmemoSignedPreKey(omemoManager, i); + omemoStore.removeOmemoSignedPreKey(getOurDevice(), i); } } diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/LegacySignalOmemoKeyUtilTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/LegacySignalOmemoKeyUtilTest.java new file mode 100644 index 000000000..67359cebb --- /dev/null +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/LegacySignalOmemoKeyUtilTest.java @@ -0,0 +1,122 @@ +/** + * + * 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 junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertNotSame; +import static junit.framework.TestCase.assertTrue; +import static junit.framework.TestCase.fail; + +import org.jivesoftware.smack.test.util.SmackTestSuite; +import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; +import org.jivesoftware.smackx.omemo.signal.SignalOmemoKeyUtil; + +import org.junit.Test; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.stringprep.XmppStringprepException; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.state.SignedPreKeyRecord; + +/** + * Test SignalOmemoKeyUtil methods. + * + * @author Paul Schaub + */ +public class LegacySignalOmemoKeyUtilTest extends SmackTestSuite { + + private final SignalOmemoKeyUtil keyUtil = new SignalOmemoKeyUtil(); + + @Test + public void omemoIdentityKeyPairSerializationTest() { + IdentityKeyPair ikp = keyUtil.generateOmemoIdentityKeyPair(); + byte[] bytes = keyUtil.identityKeyPairToBytes(ikp); + assertNotNull("serialized identityKeyPair must not be null.", + bytes); + assertNotSame("serialized identityKeyPair must not be of length 0.", + 0, bytes.length); + try { + IdentityKeyPair ikp2 = keyUtil.identityKeyPairFromBytes(bytes); + assertTrue("Deserialized IdentityKeyPairs PublicKey must equal the originals one.", + ikp.getPublicKey().equals(ikp2.getPublicKey())); + } catch (CorruptedOmemoKeyException e) { + fail("Caught exception while deserializing IdentityKeyPair."); + } + } + + @Test + public void omemoIdentityKeySerializationTest() { + IdentityKey k = keyUtil.generateOmemoIdentityKeyPair().getPublicKey(); + + try { + assertEquals("Deserialized IdentityKey must equal the original one.", + k, keyUtil.identityKeyFromBytes(keyUtil.identityKeyToBytes(k))); + } catch (CorruptedOmemoKeyException e) { + fail("Caught exception while serializing and deserializing identityKey (" + e + "): " + e.getMessage()); + } + } + + @Test + public void generateOmemoSignedPreKeyTest() { + IdentityKeyPair ikp = keyUtil.generateOmemoIdentityKeyPair(); + try { + SignedPreKeyRecord spk = keyUtil.generateOmemoSignedPreKey(ikp, 1); + assertNotNull("SignedPreKey must not be null.", spk); + assertEquals("SignedPreKeyId must match.", 1, spk.getId()); + assertEquals("singedPreKeyId must match here also.", 1, keyUtil.signedPreKeyIdFromKey(spk)); + } catch (CorruptedOmemoKeyException e) { + fail("Caught an exception while generating signedPreKey (" + e + "): " + e.getMessage()); + } + } + + @Test + public void getFingerprintTest() { + IdentityKeyPair ikp = keyUtil.generateOmemoIdentityKeyPair(); + IdentityKey ik = ikp.getPublicKey(); + assertTrue("Length of fingerprint must be 64.", + keyUtil.getFingerprintOfIdentityKey(ik).length() == 64); + } + + @Test + public void addressToDeviceTest() { + SignalProtocolAddress address = new SignalProtocolAddress("test@server.tld",1337); + try { + OmemoDevice device = keyUtil.addressAsOmemoDevice(address); + assertEquals(device, new OmemoDevice(JidCreate.bareFrom("test@server.tld"), 1337)); + } catch (XmppStringprepException e) { + fail("Could not convert address to device: " + e + " " + e.getMessage()); + } + } + + @Test + public void deviceToAddressTest() { + try { + OmemoDevice device = new OmemoDevice(JidCreate.bareFrom("test@server.tld"), 1337); + SignalProtocolAddress address = keyUtil.omemoDeviceAsAddress(device); + assertEquals(address, new SignalProtocolAddress("test@server.tld", 1337)); + } catch (XmppStringprepException e) { + fail("Could not convert device to address: " + e + " " + e.getMessage()); + } + } +} diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoManagerTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoManagerTest.java index f6f6429f8..1308782fe 100644 --- a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoManagerTest.java +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoManagerTest.java @@ -57,7 +57,12 @@ import org.junit.Test; public class OmemoManagerTest extends SmackTestSuite { @Test - public void instantiationTest() throws CorruptedOmemoKeyException, NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException, InterruptedException, XMPPException.XMPPErrorException, NoSuchPaddingException, BadPaddingException, InvalidAlgorithmParameterException, NoSuchProviderException, IllegalBlockSizeException, SmackException { + public void instantiationTest() + throws CorruptedOmemoKeyException, NoSuchAlgorithmException, UnsupportedEncodingException, + InvalidKeyException, InterruptedException, XMPPException.XMPPErrorException, NoSuchPaddingException, + BadPaddingException, InvalidAlgorithmParameterException, NoSuchProviderException, IllegalBlockSizeException, + SmackException + { SignalOmemoService.acknowledgeLicense(); SignalOmemoService.setup(); @@ -73,8 +78,8 @@ public class OmemoManagerTest extends SmackTestSuite { assertNotNull(c); assertNotNull(d); - assertEquals(123, a.getDeviceId()); - assertEquals(234, b.getDeviceId()); + assertEquals(Integer.valueOf(123), a.getDeviceId()); + assertEquals(Integer.valueOf(234), b.getDeviceId()); assertFalse(a == b); assertFalse(a == c); diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalFileBasedOmemoStoreTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalFileBasedOmemoStoreTest.java deleted file mode 100644 index 5a067c841..000000000 --- a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalFileBasedOmemoStoreTest.java +++ /dev/null @@ -1,218 +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 junit.framework.TestCase.assertEquals; -import static junit.framework.TestCase.assertFalse; -import static junit.framework.TestCase.assertNotNull; -import static junit.framework.TestCase.assertNull; -import static junit.framework.TestCase.assertTrue; -import static org.junit.Assert.assertArrayEquals; -import static org.powermock.api.mockito.PowerMockito.when; - -import java.io.File; -import java.util.Date; - -import org.jivesoftware.smackx.omemo.FileBasedOmemoStore; -import org.jivesoftware.smackx.omemo.OmemoConfiguration; -import org.jivesoftware.smackx.omemo.OmemoManager; -import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; -import org.jivesoftware.smackx.omemo.internal.CachedDeviceList; -import org.jivesoftware.smackx.omemo.internal.OmemoDevice; -import org.jivesoftware.smackx.omemo.signal.SignalFileBasedOmemoStore; - -import org.junit.After; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.jxmpp.jid.impl.JidCreate; -import org.jxmpp.stringprep.XmppStringprepException; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; - -/** - * Test the file-based signalOmemoStore. - */ -@RunWith(PowerMockRunner.class) -@PrepareForTest({OmemoManager.class}) -public class SignalFileBasedOmemoStoreTest { - - private static File storePath; - private static SignalFileBasedOmemoStore omemoStore; - private static OmemoManager omemoManager; - - - private void deleteStore() { - FileBasedOmemoStore.deleteDirectory(storePath); - } - - @BeforeClass - public static void setup() throws XmppStringprepException { - String userHome = System.getProperty("user.home"); - if (userHome != null) { - File f = new File(userHome); - storePath = new File(f, ".config/smack-integration-test/store"); - } else { - storePath = new File("int_test_omemo_store"); - } - - OmemoConfiguration.setFileBasedOmemoStoreDefaultPath(storePath); - omemoStore = new SignalFileBasedOmemoStore(); - - OmemoDevice device = new OmemoDevice(JidCreate.bareFrom("storeTest@server.tld"), 55155); - omemoManager = PowerMockito.mock(OmemoManager.class); - when(omemoManager.getDeviceId()).thenReturn(device.getDeviceId()); - when(omemoManager.getOwnJid()).thenReturn(device.getJid()); - when(omemoManager.getOwnDevice()).thenReturn(device); - } - - @Before - public void before() { - deleteStore(); - } - - @After - public void after() { - deleteStore(); - } - - @Test - public void isFreshInstallationTest() { - assertTrue(omemoStore.isFreshInstallation(omemoManager)); - omemoStore.storeOmemoIdentityKeyPair(omemoManager, omemoStore.generateOmemoIdentityKeyPair()); - assertFalse(omemoStore.isFreshInstallation(omemoManager)); - omemoStore.purgeOwnDeviceKeys(omemoManager); - assertTrue(omemoStore.isFreshInstallation(omemoManager)); - } - - @Test - public void defaultDeviceIdTest() throws XmppStringprepException { - assertEquals(-1, omemoStore.getDefaultDeviceId(omemoManager.getOwnJid())); - omemoStore.setDefaultDeviceId(omemoManager.getOwnJid(), 55); - assertEquals(55, omemoStore.getDefaultDeviceId(omemoManager.getOwnJid())); - assertEquals(-1, omemoStore.getDefaultDeviceId(JidCreate.bareFrom("randomGuy@server.tld"))); - } - - @Test - public void cachedDeviceListTest() throws XmppStringprepException { - OmemoDevice bob = new OmemoDevice(JidCreate.bareFrom("bob@builder.tv"), 666); - OmemoDevice craig = new OmemoDevice(JidCreate.bareFrom("craig@southpark.tv"), 3333333); - - CachedDeviceList bobsList = new CachedDeviceList(); - assertEquals(0, bobsList.getAllDevices().size()); - bobsList.getActiveDevices().add(bob.getDeviceId()); - bobsList.getActiveDevices().add(777); - bobsList.getInactiveDevices().add(888); - - CachedDeviceList craigsList = new CachedDeviceList(); - craigsList.addDevice(craig.getDeviceId()); - - assertEquals(3, bobsList.getAllDevices().size()); - assertEquals(2, bobsList.getActiveDevices().size()); - assertTrue(bobsList.getInactiveDevices().contains(888)); - assertTrue(bobsList.getActiveDevices().contains(777)); - assertTrue(bobsList.getAllDevices().contains(888)); - - assertEquals(0, craigsList.getInactiveDevices().size()); - assertEquals(1, craigsList.getActiveDevices().size()); - assertEquals(1, craigsList.getAllDevices().size()); - assertEquals(craig.getDeviceId(), craigsList.getActiveDevices().iterator().next().intValue()); - } - - @Test - public void omemoIdentityKeyPairTest() throws CorruptedOmemoKeyException { - assertNull(omemoStore.loadOmemoIdentityKeyPair(omemoManager)); - omemoStore.storeOmemoIdentityKeyPair(omemoManager, omemoStore.generateOmemoIdentityKeyPair()); - IdentityKeyPair ikp = omemoStore.loadOmemoIdentityKeyPair(omemoManager); - assertNotNull(ikp); - - assertTrue(omemoStore.keyUtil().getFingerprint(ikp.getPublicKey()).equals(omemoStore.getFingerprint(omemoManager))); - } - - @Test - public void signedPreKeyTest() throws CorruptedOmemoKeyException { - assertEquals(0, omemoStore.loadOmemoSignedPreKeys(omemoManager).size()); - IdentityKeyPair ikp = omemoStore.generateOmemoIdentityKeyPair(); - SignedPreKeyRecord spk = omemoStore.generateOmemoSignedPreKey(ikp, 14); - omemoStore.storeOmemoSignedPreKey(omemoManager, 14, spk); - assertEquals(1, omemoStore.loadOmemoSignedPreKeys(omemoManager).size()); - assertNotNull(omemoStore.loadOmemoSignedPreKey(omemoManager, 14)); - assertArrayEquals(spk.serialize(), omemoStore.loadOmemoSignedPreKey(omemoManager, 14).serialize()); - assertNull(omemoStore.loadOmemoSignedPreKey(omemoManager, 13)); - assertEquals(0, omemoStore.loadCurrentSignedPreKeyId(omemoManager)); - omemoStore.storeCurrentSignedPreKeyId(omemoManager, 15); - assertEquals(15, omemoStore.loadCurrentSignedPreKeyId(omemoManager)); - omemoStore.removeOmemoSignedPreKey(omemoManager, 14); - assertNull(omemoStore.loadOmemoSignedPreKey(omemoManager, 14)); - - assertNull(omemoStore.getDateOfLastSignedPreKeyRenewal(omemoManager)); - Date now = new Date(); - omemoStore.setDateOfLastSignedPreKeyRenewal(omemoManager, now); - assertEquals(now, omemoStore.getDateOfLastSignedPreKeyRenewal(omemoManager)); - } - - @Test - public void preKeyTest() { - assertEquals(0, omemoStore.loadOmemoPreKeys(omemoManager).size()); - assertNull(omemoStore.loadOmemoPreKey(omemoManager, 12)); - omemoStore.storeOmemoPreKeys(omemoManager, - omemoStore.generateOmemoPreKeys(1, 20)); - assertNotNull(omemoStore.loadOmemoPreKey(omemoManager, 12)); - assertEquals(20, omemoStore.loadOmemoPreKeys(omemoManager).size()); - omemoStore.removeOmemoPreKey(omemoManager, 12); - assertNull(omemoStore.loadOmemoPreKey(omemoManager, 12)); - assertEquals(19, omemoStore.loadOmemoPreKeys(omemoManager).size()); - - assertEquals(0, omemoStore.loadLastPreKeyId(omemoManager)); - omemoStore.storeLastPreKeyId(omemoManager, 35); - assertEquals(35, omemoStore.loadLastPreKeyId(omemoManager)); - } - - @Test - public void trustingTest() throws XmppStringprepException, CorruptedOmemoKeyException { - OmemoDevice bob = new OmemoDevice(JidCreate.bareFrom("bob@builder.tv"), 555); - IdentityKey bobsKey = omemoStore.generateOmemoIdentityKeyPair().getPublicKey(); - assertFalse(omemoStore.isDecidedOmemoIdentity(omemoManager, bob, bobsKey)); - assertFalse(omemoStore.isTrustedOmemoIdentity(omemoManager, bob, bobsKey)); - omemoStore.trustOmemoIdentity(omemoManager, bob, bobsKey); - assertTrue(omemoStore.isDecidedOmemoIdentity(omemoManager, bob, omemoStore.keyUtil().getFingerprint(bobsKey))); - assertTrue(omemoStore.isTrustedOmemoIdentity(omemoManager, bob, omemoStore.keyUtil().getFingerprint(bobsKey))); - assertNull(omemoStore.loadOmemoIdentityKey(omemoManager, bob)); - omemoStore.storeOmemoIdentityKey(omemoManager, bob, bobsKey); - assertNotNull(omemoStore.loadOmemoIdentityKey(omemoManager, bob)); - IdentityKey bobsOtherKey = omemoStore.generateOmemoIdentityKeyPair().getPublicKey(); - assertFalse(omemoStore.isTrustedOmemoIdentity(omemoManager, bob, bobsOtherKey)); - assertFalse(omemoStore.isDecidedOmemoIdentity(omemoManager, bob, bobsOtherKey)); - omemoStore.distrustOmemoIdentity(omemoManager, bob, omemoStore.keyUtil().getFingerprint(bobsKey)); - assertTrue(omemoStore.isDecidedOmemoIdentity(omemoManager, bob, bobsKey)); - assertFalse(omemoStore.isTrustedOmemoIdentity(omemoManager, bob, bobsKey)); - - assertNull(omemoStore.getDateOfLastReceivedMessage(omemoManager, bob)); - Date now = new Date(); - omemoStore.setDateOfLastReceivedMessage(omemoManager, bob, now); - assertEquals(now, omemoStore.getDateOfLastReceivedMessage(omemoManager, bob)); - } -} diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoKeyUtilTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoKeyUtilTest.java index ecd322e97..ddafffda8 100644 --- a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoKeyUtilTest.java +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoKeyUtilTest.java @@ -20,127 +20,43 @@ */ package org.jivesoftware.smack.omemo; -import static junit.framework.TestCase.assertEquals; -import static junit.framework.TestCase.assertNotNull; -import static junit.framework.TestCase.assertNotSame; -import static junit.framework.TestCase.assertTrue; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; - -import org.jivesoftware.smack.test.util.SmackTestSuite; -import org.jivesoftware.smack.test.util.TestUtils; - -import org.jivesoftware.smackx.omemo.element.OmemoBundleVAxolotlElement; -import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; -import org.jivesoftware.smackx.omemo.internal.OmemoDevice; -import org.jivesoftware.smackx.omemo.provider.OmemoBundleVAxolotlProvider; import org.jivesoftware.smackx.omemo.signal.SignalOmemoKeyUtil; +import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil; -import org.junit.Test; -import org.jxmpp.jid.impl.JidCreate; -import org.jxmpp.stringprep.XmppStringprepException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; 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 SignalOmemoKeyUtil methods. - * - * @author Paul Schaub + * smack-omemo-signal implementation of {@link OmemoKeyUtilTest}. + * This class executes tests of its super class with available implementations of {@link OmemoKeyUtil}. + * So far this includes {@link SignalOmemoKeyUtil}. */ -public class SignalOmemoKeyUtilTest extends SmackTestSuite { +@RunWith(value = Parameterized.class) +public class SignalOmemoKeyUtilTest + extends OmemoKeyUtilTest { - private final SignalOmemoKeyUtil keyUtil = new SignalOmemoKeyUtil(); - - @Test - public void generateOmemoIdentityKeyPairTest() { - IdentityKeyPair ikp = keyUtil.generateOmemoIdentityKeyPair(); - assertNotNull("IdentityKeyPair must not be null.", ikp); - assertNotNull("PrivateKey must not be null.", ikp.getPrivateKey()); - assertNotNull("PublicKey must not be null.", ikp.getPublicKey()); + public SignalOmemoKeyUtilTest(OmemoKeyUtil keyUtil) { + super(keyUtil); } - @Test - public void omemoIdentityKeyPairSerializationTest() throws CorruptedOmemoKeyException { - IdentityKeyPair ikp = keyUtil.generateOmemoIdentityKeyPair(); - byte[] bytes = keyUtil.identityKeyPairToBytes(ikp); - assertNotNull("serialized identityKeyPair must not be null.", - bytes); - assertNotSame("serialized identityKeyPair must not be of length 0.", - 0, bytes.length); - - IdentityKeyPair ikp2 = keyUtil.identityKeyPairFromBytes(bytes); - assertTrue("Deserialized IdentityKeyPairs PublicKey must equal the originals one.", - ikp.getPublicKey().equals(ikp2.getPublicKey())); - } - - @Test - public void omemoIdentityKeySerializationTest() throws CorruptedOmemoKeyException { - IdentityKey k = keyUtil.generateOmemoIdentityKeyPair().getPublicKey(); - assertEquals("Deserialized IdentityKey must equal the original one.", k, - keyUtil.identityKeyFromBytes(keyUtil.identityKeyToBytes(k))); - } - - @Test - public void generateOmemoPreKeysTest() { - HashMap pks = - keyUtil.generateOmemoPreKeys(1, 20); - assertTrue("There must be 20 preKeys.", pks.size() == 20); - assertTrue("PreKey ids must be within boundaries [1, 20]", pks.keySet().contains(1) && pks.keySet().contains(20)); - } - - @Test - public void generateOmemoSignedPreKeyTest() throws CorruptedOmemoKeyException { - IdentityKeyPair ikp = keyUtil.generateOmemoIdentityKeyPair(); - SignedPreKeyRecord spk = keyUtil.generateOmemoSignedPreKey(ikp, 1); - assertNotNull("SignedPreKey must not be null.", spk); - assertEquals("SignedPreKeyId must match.", 1, spk.getId()); - assertEquals("singedPreKeyId must match here also.", 1, keyUtil.signedPreKeyIdFromKey(spk)); - } - - @Test - public void getFingerprintTest() { - IdentityKeyPair ikp = keyUtil.generateOmemoIdentityKeyPair(); - IdentityKey ik = ikp.getPublicKey(); - assertTrue("Length of fingerprint must be 64.", - keyUtil.getFingerprint(ik).length() == 64); - } - - @Test - public void addressToDeviceTest() throws XmppStringprepException { - SignalProtocolAddress address = new SignalProtocolAddress("test@server.tld", 1337); - OmemoDevice device = keyUtil.addressAsOmemoDevice(address); - assertEquals(device, new OmemoDevice(JidCreate.bareFrom("test@server.tld"), 1337)); - } - - @Test - public void deviceToAddressTest() throws XmppStringprepException { - OmemoDevice device = new OmemoDevice(JidCreate.bareFrom("test@server.tld"), 1337); - SignalProtocolAddress address = keyUtil.omemoDeviceAsAddress(device); - assertEquals(address, new SignalProtocolAddress("test@server.tld", 1337)); - } - - @Test - public void bundlesFromOmemoBundleTest() throws Exception { - OmemoDevice device = new OmemoDevice(JidCreate.bareFrom("test@test.tld"), 1337); - String bundleXML = "BYKq4s6+plpjAAuCnGO+YFpLP71tMUPgj9ZZmkMSko4ETYMUtzWpc5USMCXStUrCbXFeHTOX3xkBTrU6/MuE/16s4ql1vRN0+JLtYPgZtTm3hb2dHwLA5BUzeTRGjSZwig==BY3AYRje4YBA6W4uuAXYNKzbII/UJbw7qE8kWHI15etiBbzKUJbnqYW19h2dWCyLMbYEpF8r477Ukv9wqMayERQEBeit9Pz31QxklV69BZ0qIxktnUO5TYAgHacFWDYsDnhdBSlbqC8nOpG4TMqvZmCPr6TCPNRcuuoO8Fp2rLGwLFYzBWYsJTsJLtmOgChiz4ilS/cgoEptnfv87tuvq5VpZFV+BY/xq67AkvgIaUO1NbROJeG+r6CcpzByoKvpIaPYyaw/BVRkNWaoocepKEqah95F1DG/uTE1iNEgIZ40wnGd39g/BWMI2ivYBIziOiJsnxJHmiUNN1GcPs3vP/E4vn7hu10BBd7QSMnxJULdKHohRhxUW/DVVRhdaY9SSX16j+CJF8YdBSgQ8NXIkq9fZrtYEdV6qkz5EK7YXVRAiIAFaaDuwUZHBf9Q2r9P4P15GvIiaHWTEU5gLyk/A8ys6Pzz01pLuu9ZBVU6/JKCXqaNa4ApbPFxYExxKuQKuRctk8a1brNcRbJUBfFGHormRpE7x92Eo3IcZcyhxa1//lKyLCNLdlL5Gg1PBd/Je4PdYYJy+6gXrcy7CRqDxBHVgPKN9AOiGxpRX7gkBVtdD2xyJnxPYNJPCT7sYdCXAoD7pMLgf27Dj0dU9vU3BX41BkuSp/qGYDlEzsuE5Tlia1IjzmYsiZRcjAp8D2tqBRY9W9zotVhB7DV2s/I7RYFzzg/Rok0AjU6ODs+iBUtFBb4DW8bURvMuh21PzHGqQlQm6eaI2S4pPLD482yV65IUBSFOrkueqrJDACBIUDpaYiOV51fUuFit4dGYYkvV3StyBT402/OG5FLw2jt+cpYepykpoRVPbI+bWcUx42CqSlwxBeMDEcZ23jnocObmU+esIhAGUvEVCyeiqq+n29Ex38FwBYUDDsKjORZTuZ1ImIIcwhL2peK1K+kTS+QhqCufoIRJBcC/x3Q3zZKv2DKaZlTWpM2Qzg8UogXJ2MmyKQzNI6RJBad8sDrpoVujQTlenKtSfc7JbWlXq5MGDb71q+5DCo88BYlAA5ZyhfiKLFE/U6lufiokNmQjGYP5eMCKhZsuv9BXBbK+LNKsLizmJtd6iEd+QUDdBEgmxIylkTyAS2gxghEHBZ+9oZGHWkRJXPnzT54+UPhQY0vpUdzGltMvneZHqfMLBRRXzcCruX3Gb+kbBodA9OaHcEx/XYT3dpwKK6hx8mYfBTgeei2VCoKk3dBG0FP45UjDoJBV9wQiDn2pW9xwTMkSBZHFWmtevdvuYAbMOpQ7nAAdv+oJxY+A7GFi2jU/PftPBRn4+vobphaBHjOl4gYrVIPHEGMvsn63pbAVgdx69XQRBaUv1tnXFTkJ2jiFT0vlUjH9upOASZHN4EmXGX9n9UAcBU+13hmRR2dkuIqBKxItFFaIdnaAti3beOnmezR+/VtWBbmmB27Q1B72qhxxW++CyrNHCy0UwiAOdkKOBUKCkyZ0BemHNdH5VhufFn9n4qu6e1pVyYjn47ivQy1xHmQL6eh1BSnbvvDgCRGpu/SkapLOe66hxxeJKw7U160d6vxUkYM6BVaUjCB5ZhooG2umXa4CVu6BjmNDkkUUM19pzangbfEUBZD+gzgJ4jXxjfJtMMuWvHJmr/f5vJ+u7vhH4y7KjYM3BW3zmMGSm5jhMTpSjT8u0dsDnK2pXMRVPTr08xmh7vhJBSE7XKChX5zcJrJtoBTAVtUL/gB9iFFb2rE0fKj2b2UQBXVao8jlCDAeOMr4thch7T8Gl+7h2OhcihFAOqkmzf9MBdPqg07COBd2OInhQqc1yCZbixd1CpEbpcG9NjbxGwRUBTzmunAmQX61OaIlTYdfWQU3VtkVXdiLCcegUIOzg/hwBfRxST8negQ2vxMQLufVXdOM/U5IPHCETGsV2uhkdz4HBTlbSzgCQwwkjD/EbEWfcontJobg9u5Odqn/x9QAmu1jBczhPhwuz7KQJW8KICaOgQ0J/+baVwptpqxOtwjFphQhBc0xu3QbrVWQDlIh2VdrfP/GowUF8CN5Q3iCpuabLhIKBaiPlpNAjMviSv3n3tJ+8vAQS7IORAuYJz8pZ/k7CdthBUDuxRt02ajimnvq8BeBQEies6TNDs/E0uvZ7aLHBJAIBQFVdgojx8r3LOjJbAk3CWhtCxU2DxFQHyoBewfJyTk9BdMJqMd7Rkiu9tcmcG1TDU0XKoEHJYPK3FBfRScvqlBrBSyfLhWFdFkUcyczOpIgwo5M5JrJEWWLBJLrOYZHlkYbBeM0hOY/zjvwbGgFHTLAKplV2A57bKzJOd0qhkc22zk1BRIohETGkJNWCDmZjnq7kgawbPWjjBaok4QMTSynT3QcBZykP11RVcyQQmYD+gxGYzL1aQlKce3+EzPZDunh0ftOBYwQvPfzvB97+QcBfCR2YW1EOIDw8KW5FrGmhw4/JxlPBYNXW8MBYvPvtsjo2LVUBy4JZdRfG1WKq2dNY8gt+OFeBSXp4XWf7UkZnM5IK0nQf2/PqHqkMXBq9s/z0YRWUt44BUzNkOEe1jnuoJ4sQpz9DeBojDr1qfpadPr6UbC9SSozBXONDctFe7rI0h5+erFwpp+LjU9MnVONIhpOsX+aiTQTBRElBbzl1sPRtu3r7kQfjqzXn1LxwnRU7gpWxjVMrplKBdGuy7iMtNqzmLOgG8QH63Jc22Mo7Tyquz4UkeT1F+8jBas8r4IYxDpWYCvwTE+esHgELip8d/C3BJP14W74RjJqBTJCGy3cDLmpDqHaXE9NPaEs1kKibx4fNx4SmEc74xMsBRLV4n9fTnOnt0omE4xfl9XsYlml78F7bs585qiWyAwNBUWflPRltdUAfkQbFWjEbTDc5FBImnSAxZk/GYqyGwB1BfKrwvFbKawM8Y18oPzXd8dNk821fZ3s2r+yXFrsLDlHBbqXgiP75kNoQPZ6MYNUdLvepRLQc1EBm5ZYV9VW56EfBazAZ71zu++p6o0LAJNBJIgKSacrque4veToF850TpQWBQptZxpQZugPAK9CMZnR3p+gF0rqYVihRnUIdWAmhMB9Ba70cNznf57ndU6NY62paZcDTTOZmPPS8/JZqLyP+ZVrBSDQwgSHsNjf3MOh4SRRd5jzq/kcjIlf6JEa1SoX06BnBQ1ATRmYMPCyNt8fu/GZ0UeAYWG+WtiDs0uDLsmklI4eBSAofueQkVpDo+I4SoFMdC8S35EOvOn7zmyOG4stSy4BBcpdJVI1JARw8QeKXhbsMIgFxQzTvMSuQeAyvdYfgFIXBURqmjb1lZU66KyPBlCWrjBbISJyqgMW8OaJOchk39YLBVcQm66sdtSBIYK9KymoaZnSvLQPNftBPi+BPfg20VwhBQDNPKib8FK5YquNUAzB7sirGjdj+El+HrOTlMr0w1omBQ66K4ENDGMAlZc7AqcE9dodeeAWfGzSyRYMto57iGAXBTnfRRbPKKBLyoV/BTeIZhkfs629J462AvxuE3pHgvcaBfyu+Cln9QhDLWz1AqOuYgqkh78LROOk4g326gj378gXBRZovbjk6iAtKaKGLvLWlGGml/SUhMtSJEgjrO4tWd9sBZ6OUOFAbuIPTaOwy0qyA6zZ9uYyxskF6i7EXWNQr1NrBWV8bGYfPvLq7Dla1gEqZv3eFej2UzcMWvFOiwurY7ASBSZQ8prazrspZeNKzJzZc0bp1PEs1odEHsI7PLYCUVQdBbAYn4nIg9EjRh92dTHKfgrTC/oAU/92U2WkDtCS+fs1BehKd0MHqJauFPVQsS37SIFwUXo0OOcMembkOhyMGPF8BS7CeBYN+H0s+GwxIrUc5SmdMZEXTprVZD6RYoM+YyxKBSBc48kcT2EN1Siv/hoX8ozuHSEfQXIS93SNY8+Jg7pzBdr7WFoKkG1m/CsTV7J2G9/yXV1pOupqPyU7Rs5FjVoJBc+i4mSLKDMm+ZxkcWMdVdM4p/MlBOFQLb+NF9j4QxlTBfQslqyOk1QwcdrJRJVUvlHUYGJc115O17sb5HIP7GE2BbHvfsMnJu2y60YmI509hoUkgGN1UqrOMLMwoC8TDqp6BVQDMiH5KfKHZLbhTwXxR4RdsADov1gD2elDd6SO+hIQBaNnLStoh3EygkLfA9tjULQYg6X7L/n1jNQeaFKaGjsaBffy5atUJ49XgzsxXMiAopLhTU0rJtGIId0g+kggLBYaBb5fC0qp2eJq8HvJVkf7MIJk+eBZ3TVasvwCn8t4MhEhBb8H48LSq/nxpOKovpLVYw8X3mIJM7JMk3yYgFUKdL0pBTfHeYAsa2hl/aoA3wslmL9RT+O26P6OWs0J2dif5o5pBf7u5QrY3Wrn0PYaRri5nDL6p6iNHFLSk6781wys0hkpBSBryRkeNrvLgGJgh95g9oWLmrptWVPIGPSzoXrVNlAd"; - OmemoBundleVAxolotlElement bundle = new OmemoBundleVAxolotlProvider().parse(TestUtils.getParser(bundleXML)); - HashMap bundles = keyUtil.BUNDLE.bundles(bundle, device); - - assertEquals("There must be 100 bundles in the HashMap.", 100, bundles.size()); - assertNotNull(keyUtil.BUNDLE.identityKey(bundle)); - - Iterator it = bundles.keySet().iterator(); - while (it.hasNext()) { - assertNotNull(keyUtil.BUNDLE.preKeyPublic(bundle, it.next())); - } - - assertEquals(1, keyUtil.BUNDLE.signedPreKeyId(bundle)); - assertNotNull(keyUtil.BUNDLE.signedPreKeyPublic(bundle)); - assertNotNull(keyUtil.BUNDLE.signedPreKeySignature(bundle)); + @Parameterized.Parameters + public static Collection getParameters() throws IOException { + return Arrays.asList(new Object[][] { + { new SignalOmemoKeyUtil()} + }); } } diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreConnectorTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreConnectorTest.java index f7a474cdb..496d4a488 100644 --- a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreConnectorTest.java +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreConnectorTest.java @@ -41,6 +41,6 @@ public class SignalOmemoStoreConnectorTest { @Test public void isTrustedIdentityTest() { SignalOmemoStoreConnector connector = new SignalOmemoStoreConnector(null, null); - assertTrue("All identities must be trusted by default.", connector.isTrustedIdentity(null, null)); + assertTrue("All identities must be trusted by default.", connector.isTrustedIdentity(null, null, null)); } } diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreTest.java new file mode 100644 index 000000000..6507d70d3 --- /dev/null +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreTest.java @@ -0,0 +1,84 @@ +/** + * + * 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.junit.Assert.assertTrue; + +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; + +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.jxmpp.stringprep.XmppStringprepException; +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; + +/** + * smack-omemo-signal implementation of {@link OmemoStoreTest}. + * This class executes tests of its super class with available implementations of {@link OmemoStore}. + * So far this includes {@link SignalFileBasedOmemoStore}, {@link SignalCachingOmemoStore}. + */ +@RunWith(value = Parameterized.class) +public class SignalOmemoStoreTest extends OmemoStoreTest { + + public SignalOmemoStoreTest(OmemoStore store) + throws XmppStringprepException { + super(store); + } + + /** + * We are running this Test with multiple available OmemoStore implementations. + * @return + * @throws IOException + */ + @Parameterized.Parameters + public static Collection getParameters() throws IOException { + TemporaryFolder temp = initStaticTemp(); + return Arrays.asList(new Object[][] { + // Simple file based store + { new SignalFileBasedOmemoStore(temp.newFolder("sigFileBased"))}, + // Ephemeral caching store + { new SignalCachingOmemoStore()}, + // Caching file based store + { new SignalCachingOmemoStore(new SignalFileBasedOmemoStore(temp.newFolder("cachingSigFileBased")))} + }); + } + + @Test + public void keyUtilTest() { + assertTrue(store.keyUtil() instanceof 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 new file mode 100644 index 000000000..5156dd66b --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/CachingOmemoStore.java @@ -0,0 +1,423 @@ +/** + * + * 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 java.util.Date; +import java.util.HashMap; +import java.util.SortedSet; +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.OmemoDevice; +import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil; + +import org.jxmpp.jid.BareJid; + +/** + * This class implements the Proxy Pattern in order to wrap an OmemoStore with a caching layer. + * This reduces access to the underlying storage layer (eg. database, filesystem) by only accessing it for + * missing/updated values. + * + * Alternatively this implementation can be used as an ephemeral keystore without a persisting backend. + * + * @param + * @param + * @param + * @param + * @param + * @param + * @param + * @param + * @param + */ +public class CachingOmemoStore + extends OmemoStore +{ + + private final HashMap> caches = new HashMap<>(); + private final OmemoStore persistent; + private final OmemoKeyUtil keyUtil; + + public CachingOmemoStore(OmemoKeyUtil keyUtil) { + if (keyUtil == null) { + throw new IllegalArgumentException("KeyUtil MUST NOT be null!"); + } + this.keyUtil = keyUtil; + persistent = null; + } + + public CachingOmemoStore(OmemoStore wrappedStore) { + this.keyUtil = null; + persistent = wrappedStore; + } + + @Override + public SortedSet localDeviceIdsOf(BareJid localUser) { + if (persistent != null) { + return persistent.localDeviceIdsOf(localUser); + } else { + return new TreeSet<>(); //TODO: ? + } + } + + @Override + public T_IdKeyPair loadOmemoIdentityKeyPair(OmemoDevice userDevice) + throws CorruptedOmemoKeyException + { + T_IdKeyPair pair = getCache(userDevice).identityKeyPair; + + if (pair == null && persistent != null) { + pair = persistent.loadOmemoIdentityKeyPair(userDevice); + if (pair != null) { + getCache(userDevice).identityKeyPair = pair; + } + } + + return pair; + } + + @Override + public void storeOmemoIdentityKeyPair(OmemoDevice userDevice, T_IdKeyPair identityKeyPair) { + getCache(userDevice).identityKeyPair = identityKeyPair; + if (persistent != null) { + persistent.storeOmemoIdentityKeyPair(userDevice, identityKeyPair); + } + } + + @Override + public void removeOmemoIdentityKeyPair(OmemoDevice userDevice) { + getCache(userDevice).identityKeyPair = null; + if (persistent != null) { + persistent.removeOmemoIdentityKeyPair(userDevice); + } + } + + @Override + public T_IdKey loadOmemoIdentityKey(OmemoDevice userDevice, OmemoDevice contactsDevice) + throws CorruptedOmemoKeyException + { + T_IdKey idKey = getCache(userDevice).identityKeys.get(contactsDevice); + + if (idKey == null && persistent != null) { + idKey = persistent.loadOmemoIdentityKey(userDevice, contactsDevice); + if (idKey != null) { + getCache(userDevice).identityKeys.put(contactsDevice, idKey); + } + } + + return idKey; + } + + @Override + public void storeOmemoIdentityKey(OmemoDevice userDevice, OmemoDevice device, T_IdKey t_idKey) { + getCache(userDevice).identityKeys.put(device, t_idKey); + if (persistent != null) { + persistent.storeOmemoIdentityKey(userDevice, device, t_idKey); + } + } + + @Override + public void removeOmemoIdentityKey(OmemoDevice userDevice, OmemoDevice contactsDevice) { + getCache(userDevice).identityKeys.remove(contactsDevice); + if (persistent != null) { + persistent.removeOmemoIdentityKey(userDevice, contactsDevice); + } + } + + @Override + public void setDateOfLastReceivedMessage(OmemoDevice userDevice, OmemoDevice from, Date date) { + getCache(userDevice).lastMessagesDates.put(from, date); + if (persistent != null) { + persistent.setDateOfLastReceivedMessage(userDevice, from, date); + } + } + + @Override + public Date getDateOfLastReceivedMessage(OmemoDevice userDevice, OmemoDevice from) { + Date last = getCache(userDevice).lastMessagesDates.get(from); + + if (last == null && persistent != null) { + last = persistent.getDateOfLastReceivedMessage(userDevice, from); + if (last != null) { + getCache(userDevice).lastMessagesDates.put(from, last); + } + } + + return last; + } + + @Override + public void setDateOfLastSignedPreKeyRenewal(OmemoDevice userDevice, Date date) { + getCache(userDevice).lastRenewalDate = date; + if (persistent != null) { + persistent.setDateOfLastSignedPreKeyRenewal(userDevice, date); + } + } + + @Override + public Date getDateOfLastSignedPreKeyRenewal(OmemoDevice userDevice) { + Date lastRenewal = getCache(userDevice).lastRenewalDate; + + if (lastRenewal == null && persistent != null) { + lastRenewal = persistent.getDateOfLastSignedPreKeyRenewal(userDevice); + if (lastRenewal != null) { + getCache(userDevice).lastRenewalDate = lastRenewal; + } + } + + return lastRenewal; + } + + @Override + public T_PreKey loadOmemoPreKey(OmemoDevice userDevice, int preKeyId) { + T_PreKey preKey = getCache(userDevice).preKeys.get(preKeyId); + + if (preKey == null && persistent != null) { + preKey = persistent.loadOmemoPreKey(userDevice, preKeyId); + if (preKey != null) { + getCache(userDevice).preKeys.put(preKeyId, preKey); + } + } + + return preKey; + } + + @Override + public void storeOmemoPreKey(OmemoDevice userDevice, int preKeyId, T_PreKey t_preKey) { + getCache(userDevice).preKeys.put(preKeyId, t_preKey); + if (persistent != null) { + persistent.storeOmemoPreKey(userDevice, preKeyId, t_preKey); + } + } + + @Override + public void removeOmemoPreKey(OmemoDevice userDevice, int preKeyId) { + getCache(userDevice).preKeys.remove(preKeyId); + if (persistent != null) { + persistent.removeOmemoPreKey(userDevice, preKeyId); + } + } + + @Override + public TreeMap loadOmemoPreKeys(OmemoDevice userDevice) { + TreeMap preKeys = getCache(userDevice).preKeys; + + if (preKeys.isEmpty() && persistent != null) { + preKeys.putAll(persistent.loadOmemoPreKeys(userDevice)); + } + + return new TreeMap<>(preKeys); + } + + @Override + public T_SigPreKey loadOmemoSignedPreKey(OmemoDevice userDevice, int signedPreKeyId) { + T_SigPreKey sigPreKey = getCache(userDevice).signedPreKeys.get(signedPreKeyId); + + if (sigPreKey == null && persistent != null) { + sigPreKey = persistent.loadOmemoSignedPreKey(userDevice, signedPreKeyId); + if (sigPreKey != null) { + getCache(userDevice).signedPreKeys.put(signedPreKeyId, sigPreKey); + } + } + + return sigPreKey; + } + + @Override + public TreeMap loadOmemoSignedPreKeys(OmemoDevice userDevice) { + TreeMap sigPreKeys = getCache(userDevice).signedPreKeys; + + if (sigPreKeys.isEmpty() && persistent != null) { + sigPreKeys.putAll(persistent.loadOmemoSignedPreKeys(userDevice)); + } + + return new TreeMap<>(sigPreKeys); + } + + @Override + public void storeOmemoSignedPreKey(OmemoDevice userDevice, + int signedPreKeyId, + T_SigPreKey signedPreKey) { + getCache(userDevice).signedPreKeys.put(signedPreKeyId, signedPreKey); + if (persistent != null) { + persistent.storeOmemoSignedPreKey(userDevice, signedPreKeyId, signedPreKey); + } + } + + @Override + public void removeOmemoSignedPreKey(OmemoDevice userDevice, int signedPreKeyId) { + getCache(userDevice).signedPreKeys.remove(signedPreKeyId); + if (persistent != null) { + persistent.removeOmemoSignedPreKey(userDevice, signedPreKeyId); + } + } + + @Override + public T_Sess loadRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice) { + HashMap contactSessions = getCache(userDevice).sessions.get(contactsDevice.getJid()); + if (contactSessions == null) { + contactSessions = new HashMap<>(); + getCache(userDevice).sessions.put(contactsDevice.getJid(), contactSessions); + } + + T_Sess session = contactSessions.get(contactsDevice.getDeviceId()); + if (session == null && persistent != null) { + session = persistent.loadRawSession(userDevice, contactsDevice); + if (session != null) { + contactSessions.put(contactsDevice.getDeviceId(), session); + } + } + + return session; + } + + @Override + public HashMap loadAllRawSessionsOf(OmemoDevice userDevice, BareJid contact) { + HashMap sessions = getCache(userDevice).sessions.get(contact); + if (sessions == null) { + sessions = new HashMap<>(); + getCache(userDevice).sessions.put(contact, sessions); + } + + if (sessions.isEmpty() && persistent != null) { + sessions.putAll(persistent.loadAllRawSessionsOf(userDevice, contact)); + } + + return new HashMap<>(sessions); + } + + @Override + public void storeRawSession(OmemoDevice userDevice, OmemoDevice contactsDevicece, T_Sess session) { + HashMap sessions = getCache(userDevice).sessions.get(contactsDevicece.getJid()); + if (sessions == null) { + sessions = new HashMap<>(); + getCache(userDevice).sessions.put(contactsDevicece.getJid(), sessions); + } + + sessions.put(contactsDevicece.getDeviceId(), session); + if (persistent != null) { + persistent.storeRawSession(userDevice, contactsDevicece, session); + } + } + + @Override + public void removeRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice) { + HashMap sessions = getCache(userDevice).sessions.get(contactsDevice.getJid()); + if (sessions != null) { + sessions.remove(contactsDevice.getDeviceId()); + } + + if (persistent != null) { + persistent.removeRawSession(userDevice, contactsDevice); + } + } + + @Override + public void removeAllRawSessionsOf(OmemoDevice userDevice, BareJid contact) { + getCache(userDevice).sessions.remove(contact); + if (persistent != null) { + persistent.removeAllRawSessionsOf(userDevice, contact); + } + } + + @Override + public boolean containsRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice) { + HashMap sessions = getCache(userDevice).sessions.get(contactsDevice.getJid()); + + return (sessions != null && sessions.get(contactsDevice.getDeviceId()) != null) || + (persistent != null && persistent.containsRawSession(userDevice, contactsDevice)); + } + + @Override + public CachedDeviceList loadCachedDeviceList(OmemoDevice userDevice, BareJid contact) { + CachedDeviceList list = getCache(userDevice).deviceLists.get(contact); + + if (list == null && persistent != null) { + list = persistent.loadCachedDeviceList(userDevice, contact); + if (list != null) { + getCache(userDevice).deviceLists.put(contact, list); + } + } + + return list == null ? new CachedDeviceList() : new CachedDeviceList(list); + } + + @Override + public void storeCachedDeviceList(OmemoDevice userDevice, + BareJid contact, + CachedDeviceList deviceList) { + getCache(userDevice).deviceLists.put(contact, new CachedDeviceList(deviceList)); + + if (persistent != null) { + persistent.storeCachedDeviceList(userDevice, contact, deviceList); + } + } + + @Override + public void purgeOwnDeviceKeys(OmemoDevice userDevice) { + caches.remove(userDevice); + + if (persistent != null) { + persistent.purgeOwnDeviceKeys(userDevice); + } + } + + @Override + public OmemoKeyUtil + keyUtil() { + if (persistent != null) { + return persistent.keyUtil(); + } else { + return keyUtil; + } + } + + /** + * Return the {@link KeyCache} object of an {@link OmemoManager}. + * @param device + * @return + */ + private KeyCache getCache(OmemoDevice device) { + KeyCache cache = caches.get(device); + if (cache == null) { + cache = new KeyCache<>(); + caches.put(device, cache); + } + return cache; + } + + /** + * Cache that stores values for an {@link OmemoManager}. + * @param + * @param + * @param + * @param + * @param + */ + private static class KeyCache { + private T_IdKeyPair identityKeyPair; + private final TreeMap preKeys = new TreeMap<>(); + private final TreeMap signedPreKeys = new TreeMap<>(); + private final HashMap> sessions = new HashMap<>(); + private final HashMap identityKeys = new HashMap<>(); + private final HashMap lastMessagesDates = 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 f744f0b80..28128f135 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 @@ -21,18 +21,20 @@ import java.io.DataOutputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Set; +import java.util.SortedSet; import java.util.Stack; +import java.util.TreeMap; +import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; -import org.jivesoftware.smack.util.StringUtils; - import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; import org.jivesoftware.smackx.omemo.internal.CachedDeviceList; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; @@ -47,8 +49,8 @@ import org.jxmpp.jid.BareJid; public abstract class FileBasedOmemoStore extends OmemoStore { - private static final Logger LOGGER = Logger.getLogger(FileBasedOmemoStore.class.getSimpleName()); private final FileHierarchy hierarchy; + private static final Logger LOGGER = Logger.getLogger(FileBasedOmemoStore.class.getName()); public FileBasedOmemoStore() { this(OmemoConfiguration.getFileBasedOmemoStoreDefaultPath()); @@ -63,335 +65,222 @@ public abstract class FileBasedOmemoStore localDeviceIdsOf(BareJid localUser) { + SortedSet deviceIds = new TreeSet<>(); + File userDir = hierarchy.getUserDirectory(localUser); + File[] list = userDir.listFiles(); + for (File d : (list != null ? list : new File[] {})) { + if (d.isDirectory()) { + try { + deviceIds.add(Integer.parseInt(d.getName())); + } catch (NumberFormatException e) { + // ignore + } + } + } + return deviceIds; + } + + @Override + public Date getDateOfLastReceivedMessage(OmemoDevice userDevice, OmemoDevice contactsDevice) { + File lastMessageReceived = hierarchy.getLastMessageReceivedDatePath(userDevice, contactsDevice); + Long date = readLong(lastMessageReceived); + return date != null ? new Date(date) : null; + } + + @Override + public void setDateOfLastSignedPreKeyRenewal(OmemoDevice userDevice, Date date) { + File lastSignedPreKeyRenewal = hierarchy.getLastSignedPreKeyRenewal(userDevice); + writeLong(lastSignedPreKeyRenewal, date.getTime()); + } + + @Override + public Date getDateOfLastSignedPreKeyRenewal(OmemoDevice userDevice) { + File lastSignedPreKeyRenewal = hierarchy.getLastSignedPreKeyRenewal(userDevice); + Long date = readLong(lastSignedPreKeyRenewal); + return date != null ? new Date(date) : null; + } + + @Override + public T_PreKey loadOmemoPreKey(OmemoDevice userDevice, int preKeyId) { + File preKeyPath = hierarchy.getPreKeyPath(userDevice, preKeyId); + byte[] bytes = readBytes(preKeyPath); + + if (bytes != null) { + try { + return keyUtil().preKeyFromBytes(bytes); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Could not deserialize preKey from bytes.", e); + } + } + + return null; + } + + @Override + public void storeOmemoPreKey(OmemoDevice userDevice, int preKeyId, T_PreKey t_preKey) { + File preKeyPath = hierarchy.getPreKeyPath(userDevice, preKeyId); + writeBytes(preKeyPath, keyUtil().preKeyToBytes(t_preKey)); + } + + @Override + public void removeOmemoPreKey(OmemoDevice userDevice, int preKeyId) { + File preKeyPath = hierarchy.getPreKeyPath(userDevice, preKeyId); + if (!preKeyPath.delete()) { + LOGGER.log(Level.WARNING, "Deleting OMEMO preKey " + preKeyPath.getAbsolutePath() + " failed."); } } @Override - public void storeLastPreKeyId(OmemoManager omemoManager, int currentPreKeyId) { - try { - writeInt(hierarchy.getLastPreKeyIdPath(omemoManager), currentPreKeyId); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Could not write lastPreKeyId: " + e, e); - } - } - - @Override - public T_IdKeyPair loadOmemoIdentityKeyPair(OmemoManager omemoManager) throws CorruptedOmemoKeyException { - File identityKeyPairPath = hierarchy.getIdentityKeyPairPath(omemoManager); - try { - byte[] bytes = readBytes(identityKeyPairPath); - return bytes != null ? keyUtil().identityKeyPairFromBytes(bytes) : null; - } catch (IOException e) { - return null; - } - } - - @Override - public void storeOmemoIdentityKeyPair(OmemoManager omemoManager, T_IdKeyPair identityKeyPair) { - File identityKeyPairPath = hierarchy.getIdentityKeyPairPath(omemoManager); - try { - writeBytes(identityKeyPairPath, keyUtil().identityKeyPairToBytes(identityKeyPair)); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Could not write omemoIdentityKeyPair: " + e, e); - } - } - - @Override - public T_IdKey loadOmemoIdentityKey(OmemoManager omemoManager, OmemoDevice device) throws CorruptedOmemoKeyException { - File identityKeyPath = hierarchy.getContactsIdentityKeyPath(omemoManager, device); - try { - byte[] bytes = readBytes(identityKeyPath); - return bytes != null ? keyUtil().identityKeyFromBytes(bytes) : null; - } catch (IOException e) { - return null; - } - } - - @Override - public void storeOmemoIdentityKey(OmemoManager omemoManager, OmemoDevice device, T_IdKey t_idKey) { - File identityKeyPath = hierarchy.getContactsIdentityKeyPath(omemoManager, device); - try { - writeBytes(identityKeyPath, keyUtil().identityKeyToBytes(t_idKey)); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Could not write omemoIdentityKey of " + device + ": " + e, e); - } - } - - @Override - public boolean isTrustedOmemoIdentity(OmemoManager omemoManager, OmemoDevice device, OmemoFingerprint fingerprint) { - File trustPath = hierarchy.getContactsTrustPath(omemoManager, device); - try { - String depositedFingerprint = new String(readBytes(trustPath), StringUtils.UTF8); - - return depositedFingerprint.length() > 2 - && depositedFingerprint.charAt(0) == '1' - && new OmemoFingerprint(depositedFingerprint.substring(2)).equals(fingerprint); - } catch (IOException e) { - return false; - } - } - - @Override - public boolean isDecidedOmemoIdentity(OmemoManager omemoManager, OmemoDevice device, OmemoFingerprint fingerprint) { - File trustPath = hierarchy.getContactsTrustPath(omemoManager, device); - try { - String depositedFingerprint = new String(readBytes(trustPath), StringUtils.UTF8); - - return depositedFingerprint.length() > 2 - && (depositedFingerprint.charAt(0) == '1' || depositedFingerprint.charAt(0) == '2') - && new OmemoFingerprint(depositedFingerprint.substring(2)).equals(fingerprint); - } catch (IOException e) { - return false; - } - } - - @Override - public void trustOmemoIdentity(OmemoManager omemoManager, OmemoDevice device, OmemoFingerprint fingerprint) { - File trustPath = hierarchy.getContactsTrustPath(omemoManager, device); - try { - writeBytes(trustPath, ("1 " + fingerprint.toString()).getBytes(StringUtils.UTF8)); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Could not trust " + device + ": " + e, e); - } - } - - @Override - public void distrustOmemoIdentity(OmemoManager omemoManager, OmemoDevice device, OmemoFingerprint fingerprint) { - File trustPath = hierarchy.getContactsTrustPath(omemoManager, device); - try { - writeBytes(trustPath, ("2 " + fingerprint.toString()).getBytes(StringUtils.UTF8)); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Could not distrust " + device + ": " + e, e); - } - } - - @Override - public void setDateOfLastReceivedMessage(OmemoManager omemoManager, OmemoDevice from, Date date) { - File lastMessageReceived = hierarchy.getLastMessageReceivedDatePath(omemoManager, from); - try { - writeLong(lastMessageReceived, date.getTime()); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Could not write date of last received message from " + from + ": " + e, e); - } - } - - @Override - public Date getDateOfLastReceivedMessage(OmemoManager omemoManager, OmemoDevice from) { - File lastMessageReceived = hierarchy.getLastMessageReceivedDatePath(omemoManager, from); - try { - long date = readLong(lastMessageReceived); - return date != -1 ? new Date(date) : null; - } catch (IOException e) { - return null; - } - } - - @Override - public void setDateOfLastSignedPreKeyRenewal(OmemoManager omemoManager, Date date) { - File lastSignedPreKeyRenewal = hierarchy.getLastSignedPreKeyRenewal(omemoManager); - try { - writeLong(lastSignedPreKeyRenewal, date.getTime()); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Could not write date of last singedPreKey renewal for " - + omemoManager.getOwnDevice() + ": " + e, e); - } - } - - @Override - public Date getDateOfLastSignedPreKeyRenewal(OmemoManager omemoManager) { - File lastSignedPreKeyRenewal = hierarchy.getLastSignedPreKeyRenewal(omemoManager); - - try { - long date = readLong(lastSignedPreKeyRenewal); - return date != -1 ? new Date(date) : null; - } catch (IOException e) { - return null; - } - } - - @Override - public T_PreKey loadOmemoPreKey(OmemoManager omemoManager, int preKeyId) { - File preKeyPath = hierarchy.getPreKeyPath(omemoManager, preKeyId); - try { - byte[] bytes = readBytes(preKeyPath); - return bytes != null ? keyUtil().preKeyFromBytes(bytes) : null; - } catch (IOException e) { - return null; - } - } - - @Override - public void storeOmemoPreKey(OmemoManager omemoManager, int preKeyId, T_PreKey t_preKey) { - File preKeyPath = hierarchy.getPreKeyPath(omemoManager, preKeyId); - try { - writeBytes(preKeyPath, keyUtil().preKeyToBytes(t_preKey)); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Could not write preKey with id " + preKeyId + ": " + e, e); - } - } - - @Override - public void removeOmemoPreKey(OmemoManager omemoManager, int preKeyId) { - File preKeyPath = hierarchy.getPreKeyPath(omemoManager, preKeyId); - preKeyPath.delete(); - } - - @Override - public int loadCurrentSignedPreKeyId(OmemoManager omemoManager) { - File currentSignedPreKeyIdPath = hierarchy.getCurrentSignedPreKeyIdPath(omemoManager); - try { - int i = readInt(currentSignedPreKeyIdPath); - return i == -1 ? 0 : i; - } catch (IOException e) { - return 0; - } - } - - @Override - public void storeCurrentSignedPreKeyId(OmemoManager omemoManager, int currentSignedPreKeyId) { - File currentSignedPreKeyIdPath = hierarchy.getCurrentSignedPreKeyIdPath(omemoManager); - try { - writeInt(currentSignedPreKeyIdPath, currentSignedPreKeyId); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Could not write currentSignedPreKeyId " - + currentSignedPreKeyId + " for " + omemoManager.getOwnDevice() + ": " - + e, e); - } - } - - @Override - public HashMap loadOmemoPreKeys(OmemoManager omemoManager) { - File preKeyDirectory = hierarchy.getPreKeysDirectory(omemoManager); - HashMap preKeys = new HashMap<>(); + public TreeMap loadOmemoPreKeys(OmemoDevice userDevice) { + File preKeyDirectory = hierarchy.getPreKeysDirectory(userDevice); + TreeMap preKeys = new TreeMap<>(); if (preKeyDirectory == null) { return preKeys; } File[] keys = preKeyDirectory.listFiles(); + for (File f : keys != null ? keys : new File[0]) { - - try { - byte[] bytes = readBytes(f); - if (bytes == null) { - continue; + byte[] bytes = readBytes(f); + if (bytes != null) { + try { + T_PreKey p = keyUtil().preKeyFromBytes(bytes); + preKeys.put(Integer.parseInt(f.getName()), p); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Could not deserialize preKey from bytes.", e); } - T_PreKey p = keyUtil().preKeyFromBytes(bytes); - preKeys.put(Integer.parseInt(f.getName()), p); - - } catch (IOException e) { - // Do nothing. } } + return preKeys; } @Override - public T_SigPreKey loadOmemoSignedPreKey(OmemoManager omemoManager, int signedPreKeyId) { - File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(omemoManager), Integer.toString(signedPreKeyId)); - try { - byte[] bytes = readBytes(signedPreKeyPath); - return bytes != null ? keyUtil().signedPreKeyFromBytes(bytes) : null; - } catch (IOException e) { - return null; + public T_SigPreKey loadOmemoSignedPreKey(OmemoDevice userDevice, int signedPreKeyId) { + File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(userDevice), Integer.toString(signedPreKeyId)); + byte[] bytes = readBytes(signedPreKeyPath); + if (bytes != null) { + try { + return keyUtil().signedPreKeyFromBytes(bytes); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Could not deserialize signed preKey from bytes.", e); + } } + return null; } @Override - public HashMap loadOmemoSignedPreKeys(OmemoManager omemoManager) { - File signedPreKeysDirectory = hierarchy.getSignedPreKeysDirectory(omemoManager); - HashMap signedPreKeys = new HashMap<>(); + public TreeMap loadOmemoSignedPreKeys(OmemoDevice userDevice) { + File signedPreKeysDirectory = hierarchy.getSignedPreKeysDirectory(userDevice); + TreeMap signedPreKeys = new TreeMap<>(); if (signedPreKeysDirectory == null) { return signedPreKeys; } File[] keys = signedPreKeysDirectory.listFiles(); + for (File f : keys != null ? keys : new File[0]) { - - try { - byte[] bytes = readBytes(f); - if (bytes == null) { - continue; + byte[] bytes = readBytes(f); + if (bytes != null) { + try { + T_SigPreKey p = keyUtil().signedPreKeyFromBytes(bytes); + signedPreKeys.put(Integer.parseInt(f.getName()), p); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Could not deserialize signed preKey.", e); } - T_SigPreKey p = keyUtil().signedPreKeyFromBytes(bytes); - signedPreKeys.put(Integer.parseInt(f.getName()), p); - - } catch (IOException e) { - // Do nothing. } } + return signedPreKeys; } @Override - public void storeOmemoSignedPreKey(OmemoManager omemoManager, int signedPreKeyId, T_SigPreKey signedPreKey) { - File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(omemoManager), Integer.toString(signedPreKeyId)); - try { - writeBytes(signedPreKeyPath, keyUtil().signedPreKeyToBytes(signedPreKey)); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Could not write signedPreKey " + signedPreKey - + " for " + omemoManager.getOwnDevice() + ": " + e, e); + public void storeOmemoSignedPreKey(OmemoDevice userDevice, + int signedPreKeyId, + T_SigPreKey signedPreKey) { + File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(userDevice), Integer.toString(signedPreKeyId)); + writeBytes(signedPreKeyPath, keyUtil().signedPreKeyToBytes(signedPreKey)); + } + + @Override + public void removeOmemoSignedPreKey(OmemoDevice userDevice, int signedPreKeyId) { + File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(userDevice), Integer.toString(signedPreKeyId)); + if (!signedPreKeyPath.delete()) { + LOGGER.log(Level.WARNING, "Deleting signed OMEMO preKey " + signedPreKeyPath.getAbsolutePath() + " failed."); } } @Override - public void removeOmemoSignedPreKey(OmemoManager omemoManager, int signedPreKeyId) { - File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(omemoManager), Integer.toString(signedPreKeyId)); - signedPreKeyPath.delete(); - } - - @Override - public T_Sess loadRawSession(OmemoManager omemoManager, OmemoDevice device) { - File sessionPath = hierarchy.getContactsSessionPath(omemoManager, device); - try { - byte[] bytes = readBytes(sessionPath); - return bytes != null ? keyUtil().rawSessionFromBytes(bytes) : null; - } catch (IOException e) { - return null; + public T_Sess loadRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice) { + File sessionPath = hierarchy.getContactsSessionPath(userDevice, contactsDevice); + byte[] bytes = readBytes(sessionPath); + if (bytes != null) { + try { + return keyUtil().rawSessionFromBytes(bytes); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Could not deserialize raw session.", e); + } } + return null; } @Override - public HashMap loadAllRawSessionsOf(OmemoManager omemoManager, BareJid contact) { - File contactsDirectory = hierarchy.getContactsDir(omemoManager, contact); + public HashMap loadAllRawSessionsOf(OmemoDevice userDevice, BareJid contact) { + File contactsDirectory = hierarchy.getContactsDir(userDevice, contact); HashMap sessions = new HashMap<>(); String[] devices = contactsDirectory.list(); @@ -403,238 +292,180 @@ public abstract class FileBasedOmemoStore active = readIntegers(activeDevicesPath); + if (active != null) { + cachedDeviceList.getActiveDevices().addAll(active); } // inactive - File inactiveDevicesPath = hierarchy.getContactsInactiveDevicesPath(omemoManager, contact); - try { - cachedDeviceList.getInactiveDevices().addAll(readIntegers(inactiveDevicesPath)); - } catch (IOException e) { - // It's ok :) + File inactiveDevicesPath = hierarchy.getContactsInactiveDevicesPath(userDevice, contact); + Set inactive = readIntegers(inactiveDevicesPath); + if (inactive != null) { + cachedDeviceList.getInactiveDevices().addAll(inactive); } return cachedDeviceList; } @Override - public void storeCachedDeviceList(OmemoManager omemoManager, BareJid contact, CachedDeviceList deviceList) { + public void storeCachedDeviceList(OmemoDevice userDevice, + BareJid contact, + CachedDeviceList contactsDeviceList) { if (contact == null) { return; } - File activeDevices = hierarchy.getContactsActiveDevicesPath(omemoManager, contact); - try { - writeIntegers(activeDevices, deviceList.getActiveDevices()); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Could not write active devices of deviceList of " - + contact + ": " + e.getMessage()); - } + File activeDevices = hierarchy.getContactsActiveDevicesPath(userDevice, contact); + writeIntegers(activeDevices, contactsDeviceList.getActiveDevices()); - File inactiveDevices = hierarchy.getContactsInactiveDevicesPath(omemoManager, contact); - try { - writeIntegers(inactiveDevices, deviceList.getInactiveDevices()); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Could not write inactive devices of deviceList of " - + contact + ": " + e.getMessage()); - } + File inactiveDevices = hierarchy.getContactsInactiveDevicesPath(userDevice, contact); + writeIntegers(inactiveDevices, contactsDeviceList.getInactiveDevices()); } @Override - public void purgeOwnDeviceKeys(OmemoManager omemoManager) { - File deviceDirectory = hierarchy.getUserDeviceDirectory(omemoManager); + public void purgeOwnDeviceKeys(OmemoDevice userDevice) { + File deviceDirectory = hierarchy.getUserDeviceDirectory(userDevice); deleteDirectory(deviceDirectory); } - private void writeInt(File target, int i) throws IOException { + private static void writeLong(File target, long i) { if (target == null) { - throw new IOException("Could not write integer to null-path."); + LOGGER.log(Level.WARNING, "Could not write long to null-path."); + return; } - FileHierarchy.createFile(target); - - IOException io = null; - DataOutputStream out = null; - try { - out = new DataOutputStream(new FileOutputStream(target)); - out.writeInt(i); - } catch (IOException e) { - io = e; - } finally { - if (out != null) { - out.close(); - } - } - - if (io != null) { - throw io; - } - } - - private int readInt(File target) throws IOException { - if (target == null) { - throw new IOException("Could not read integer from null-path."); - } - - IOException io = null; - int i = -1; - DataInputStream in = null; - try { - in = new DataInputStream(new FileInputStream(target)); - i = in.readInt(); - + FileHierarchy.createFile(target); } catch (IOException e) { - io = e; - - } finally { - if (in != null) { - in.close(); - } + LOGGER.log(Level.SEVERE, "Could not create file.", e); + return; } - if (io != null) { - throw io; - } - return i; - } - - private void writeLong(File target, long i) throws IOException { - if (target == null) { - throw new IOException("Could not write long to null-path."); - } - - FileHierarchy.createFile(target); - - IOException io = null; DataOutputStream out = null; try { out = new DataOutputStream(new FileOutputStream(target)); out.writeLong(i); } catch (IOException e) { - io = e; + LOGGER.log(Level.SEVERE, "Could not write longs to file.", e); } finally { if (out != null) { - out.close(); + try { + out.close(); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Could not close OutputStream.", e); + } } } - - if (io != null) { - throw io; - } } - private long readLong(File target) throws IOException { + private static Long readLong(File target) { if (target == null) { - throw new IOException("Could not read long from null-path."); + LOGGER.log(Level.WARNING, "Could not read long from null-path."); + return null; } - IOException io = null; - long l = -1; + Long l; DataInputStream in = null; try { in = new DataInputStream(new FileInputStream(target)); l = in.readLong(); - + } catch (FileNotFoundException e) { + l = null; } catch (IOException e) { - io = e; - + LOGGER.log(Level.SEVERE, "Could not read long from file.", e); + return null; } finally { if (in != null) { - in.close(); + try { + in.close(); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Could not close InputStream.", e); + } } } - if (io != null) { - throw io; - } - return l; } - private void writeBytes(File target, byte[] bytes) throws IOException { + private static void writeBytes(File target, byte[] bytes) { if (target == null) { - throw new IOException("Could not write bytes to null-path."); + LOGGER.log(Level.WARNING, "Could not write bytes to null-path."); + return; } // Create file - FileHierarchy.createFile(target); + try { + FileHierarchy.createFile(target); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Could not create file.", e); + return; + } - IOException io = null; DataOutputStream out = null; try { @@ -642,55 +473,56 @@ public abstract class FileBasedOmemoStore integers) throws IOException { + private static void writeIntegers(File target, Set integers) { if (target == null) { - throw new IOException("Could not write integers to null-path."); + LOGGER.log(Level.WARNING, "Could not write integers to null-path."); + return; } - IOException io = null; DataOutputStream out = null; try { @@ -700,26 +532,26 @@ public abstract class FileBasedOmemoStore readIntegers(File target) throws IOException { + private static Set readIntegers(File target) { if (target == null) { - throw new IOException("Could not write integers to null-path."); + LOGGER.log(Level.WARNING, "Could not read integers from null-path."); + return null; } HashSet integers = new HashSet<>(); - IOException io = null; DataInputStream in = null; try { @@ -733,22 +565,139 @@ public abstract class FileBasedOmemoStore integers) + throws IOException + { + if (target == null) { + throw new IOException("Could not write integers to null-path."); + } + + FileHierarchy.createFile(target); + + try (DataOutputStream out = new DataOutputStream(new FileOutputStream(target))) { + for (int i : integers) { + out.writeInt(i); + } + } + } + + private static Set readIntegers(File target) + throws IOException + { + if (target == null) { + throw new IOException("Could not write integers to null-path."); + } + + if (!target.exists() || !target.isFile()) { + return null; + } + + HashSet integers = new HashSet<>(); + + try (DataInputStream in = new DataInputStream(new FileInputStream(target))) { + while (true) { + try { + integers.add(in.readInt()); + } catch (EOFException e) { + break; + } + } + } + + return integers; + } + */ + + /** + * Delete a directory with all subdirectories. + * @param root + */ public static void deleteDirectory(File root) { File[] currList; Stack stack = new Stack<>(); @@ -770,18 +719,15 @@ public abstract class FileBasedOmemoStore> INSTANCES = new WeakHashMap<>(); + private static final WeakHashMap> INSTANCES = new WeakHashMap<>(); private final OmemoService service; + private final Object LOCK = new Object(); + private final HashSet omemoMessageListeners = new HashSet<>(); private final HashSet omemoMucMessageListeners = new HashSet<>(); - private OmemoService.OmemoStanzaListener omemoStanzaListener; - private OmemoService.OmemoCarbonCopyListener omemoCarbonCopyListener; + private TrustCallback trustCallback; - private int deviceId; + private BareJid ownJid; + private Integer deviceId; /** * Private constructor to prevent multiple instances on a single connection (which probably would be bad!). * * @param connection connection */ - private OmemoManager(XMPPConnection connection, int deviceId) { + private OmemoManager(XMPPConnection connection, Integer deviceId) { super(connection); + service = OmemoService.getInstance(); + + if (deviceId != null && deviceId <= 0) { + throw new IllegalArgumentException("DeviceId MUST be greater than 0."); + } + this.deviceId = deviceId; - connection.addConnectionListener(new AbstractConnectionListener() { - @Override - public void authenticated(XMPPConnection connection, boolean resumed) { - if (resumed) { - return; - } - Async.go(new Runnable() { - @Override - public void run() { - try { - initialize(); - } catch (InterruptedException | CorruptedOmemoKeyException | PubSubException.NotALeafNodeException | SmackException.NotLoggedInException | SmackException.NoResponseException | SmackException.NotConnectedException | XMPPException.XMPPErrorException e) { - LOGGER.log(Level.SEVERE, "connectionListener.authenticated() failed to initialize OmemoManager: " - + e.getMessage()); - } - } - }); - } - }); + // StanzaListeners + startStanzaListeners(); - PEPManager.getInstanceFor(connection).addPEPListener(deviceListUpdateListener); + // Announce OMEMO support ServiceDiscoveryManager.getInstanceFor(connection).addFeature(PEP_NODE_DEVICE_LIST_NOTIFY); - - service = OmemoService.getInstance(); } /** - * Get an instance of the OmemoManager for the given connection and deviceId. - * - * @param connection Connection - * @param deviceId deviceId of the Manager. If the deviceId is null, a random id will be generated. - * @return an OmemoManager + * Return an OmemoManager instance for the given connection and deviceId. + * If there was an OmemoManager for the connection and id before, return it. Otherwise create a new OmemoManager + * instance and return it. + * @param connection XmppConnection. + * @param deviceId can be null, to generate a random deviceId. DeviceIds MUST NOT be less than 1. + * @return */ - public static synchronized OmemoManager getInstanceFor(XMPPConnection connection, Integer deviceId) { - WeakHashMap managersOfConnection = INSTANCES.get(connection); + public synchronized static OmemoManager getInstanceFor(XMPPConnection connection, Integer deviceId) { + TreeMap managersOfConnection = INSTANCES.get(connection); if (managersOfConnection == null) { - managersOfConnection = new WeakHashMap<>(); + managersOfConnection = new TreeMap<>(); INSTANCES.put(connection, managersOfConnection); } @@ -161,42 +155,50 @@ public final class OmemoManager extends Manager { manager = new OmemoManager(connection, deviceId); managersOfConnection.put(deviceId, manager); } + return manager; } /** - * Get an instance of the OmemoManager for the given connection. - * This method creates the OmemoManager for the stored defaultDeviceId of the connections user. - * If there is no such id is stored, it uses a fresh deviceId and sets that as defaultDeviceId instead. - * - * @param connection connection - * @return OmemoManager + * Returns an OmemoManager instance for the given connection. If there was no OmemoManager for that connection + * before, create a new one. If there was one before, return it. If there were multiple managers before, return the + * one with the lowest deviceId. + * @param connection XmppConnection. + * @return manager */ - public static synchronized OmemoManager getInstanceFor(XMPPConnection connection) { - BareJid user; - if (connection.getUser() != null) { - user = connection.getUser().asBareJid(); + public synchronized static OmemoManager getInstanceFor(XMPPConnection connection) { + TreeMap managers = INSTANCES.get(connection); + if (managers == null) { + managers = new TreeMap<>(); + INSTANCES.put(connection, managers); + } + + OmemoManager manager; + if (managers.size() == 0) { + + manager = new OmemoManager(connection, null); + managers.put(null, manager); + } else { - // This might be dangerous - try { - user = JidCreate.bareFrom(((AbstractXMPPConnection) connection).getConfiguration().getUsername()); - } catch (XmppStringprepException e) { - throw new AssertionError("Username is not a valid Jid. " + - "Use OmemoManager.gerInstanceFor(Connection, deviceId) instead."); - } + manager = managers.get(managers.firstKey()); } - int defaultDeviceId = OmemoService.getInstance().getOmemoStoreBackend().getDefaultDeviceId(user); - if (defaultDeviceId < 1) { - defaultDeviceId = randomDeviceId(); - OmemoService.getInstance().getOmemoStoreBackend().setDefaultDeviceId(user, defaultDeviceId); - } - - return getInstanceFor(connection, defaultDeviceId); + return manager; } /** - * Initializes the OmemoManager. This method is called automatically once the client logs into the server successfully. + * Set a TrustCallback for this particular OmemoManager. + * @param callback trustCallback used to query and modify trust states. + */ + public void setTrustCallback(TrustCallback callback) { + if (trustCallback != null) { + throw new IllegalStateException("TrustCallback can only be set once."); + } + trustCallback = callback; + } + + /** + * Initializes the OmemoManager. This method must be called before the manager can be used. * * @throws CorruptedOmemoKeyException * @throws InterruptedException @@ -206,14 +208,48 @@ public final class OmemoManager extends Manager { * @throws SmackException.NotLoggedInException * @throws PubSubException.NotALeafNodeException */ - public void initialize() throws CorruptedOmemoKeyException, InterruptedException, SmackException.NoResponseException, - SmackException.NotConnectedException, XMPPException.XMPPErrorException, SmackException.NotLoggedInException, + public void initialize() + throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, InterruptedException, + SmackException.NoResponseException, SmackException.NotConnectedException, XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException { - getOmemoService().initialize(this); + synchronized (LOCK) { + if (!connection().isAuthenticated()) { + throw new SmackException.NotLoggedInException(); + } + + // Set jid and deviceId + this.ownJid = connection().getUser().asBareJid(); + if (deviceId == null) { + SortedSet localDeviceIds = getOmemoService().getOmemoStoreBackend().localDeviceIdsOf(ownJid); + setDeviceId(localDeviceIds.size() > 0 ? localDeviceIds.first() : randomDeviceId()); + } + + getOmemoService().initialize(new KnownBareJidGuard(this)); + } + } + + /** + * Initialize the manager without blocking. Once the manager is successfully initialized, the finishedCallback will + * be notified. It will also get notified, if an error occurs. + * @param finishedCallback + */ + public void initializeAsync(final FinishedCallback finishedCallback) { + Async.go(new Runnable() { + @Override + public void run() { + try { + initialize(); + finishedCallback.initializationFinished(OmemoManager.this); + } catch (Exception e) { + finishedCallback.initializationFailed(e); + } + } + }); } /** * 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 message text to encrypt @@ -222,15 +258,23 @@ public final class OmemoManager extends Manager { * @throws UndecidedOmemoIdentityException When there are undecided devices * @throws NoSuchAlgorithmException * @throws InterruptedException - * @throws CannotEstablishOmemoSessionException when we could not create session withs all of the recipients devices. + * @throws CannotEstablishOmemoSessionException when we could not create session withs all of the recipients + * devices. * @throws SmackException.NotConnectedException * @throws SmackException.NoResponseException */ - public Message encrypt(BareJid to, String message) throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, InterruptedException, CannotEstablishOmemoSessionException, SmackException.NotConnectedException, SmackException.NoResponseException { - Message m = new Message(); - m.setBody(message); - OmemoVAxolotlElement encrypted = getOmemoService().processSendingMessage(this, to, m); - return finishMessage(encrypted); + public Message encrypt(BareJid to, String message) + throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, + InterruptedException, CannotEstablishOmemoSessionException, SmackException.NotConnectedException, + SmackException.NoResponseException, SmackException.NotLoggedInException + { + synchronized (LOCK) { + KnownBareJidGuard guard = new KnownBareJidGuard(this); + Message plaintext = new Message(); + plaintext.setBody(message); + OmemoVAxolotlElement encrypted = getOmemoService().processSendingMessage(guard, to, plaintext); + return finishMessage(encrypted); + } } /** @@ -248,11 +292,18 @@ public final class OmemoManager extends Manager { * @throws SmackException.NotConnectedException * @throws SmackException.NoResponseException */ - public Message encrypt(ArrayList recipients, String message) throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, InterruptedException, CannotEstablishOmemoSessionException, SmackException.NotConnectedException, SmackException.NoResponseException { - Message m = new Message(); - m.setBody(message); - OmemoVAxolotlElement encrypted = getOmemoService().processSendingMessage(this, recipients, m); - return finishMessage(encrypted); + public Message 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); + OmemoVAxolotlElement encrypted = getOmemoService().processSendingMessage( + new KnownBareJidGuard(this), recipients, m); + return finishMessage(encrypted); + } } /** @@ -272,17 +323,26 @@ 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) throws UndecidedOmemoIdentityException, NoSuchAlgorithmException, CryptoFailedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, NoOmemoSupportException, CannotEstablishOmemoSessionException { - if (!multiUserChatSupportsOmemo(muc.getRoom())) { - throw new NoOmemoSupportException(); + public Message encrypt(MultiUserChat muc, String message) + throws UndecidedOmemoIdentityException, NoSuchAlgorithmException, CryptoFailedException, + XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException, NoOmemoSupportException, CannotEstablishOmemoSessionException, + SmackException.NotLoggedInException + { + synchronized (LOCK) { + if (!multiUserChatSupportsOmemo(muc.getRoom())) { + throw new NoOmemoSupportException(); + } + + Message m = new Message(); + m.setBody(message); + ArrayList recipients = new ArrayList<>(); + + for (EntityFullJid e : muc.getOccupants()) { + recipients.add(muc.getOccupant(e).getJid().asBareJid()); + } + return encrypt(recipients, message); } - Message m = new Message(); - m.setBody(message); - ArrayList recipients = new ArrayList<>(); - for (EntityFullJid e : muc.getOccupants()) { - recipients.add(muc.getOccupant(e).getJid().asBareJid()); - } - return encrypt(recipients, message); } /** @@ -296,11 +356,16 @@ public final class OmemoManager extends Manager { * @throws CryptoFailedException * @throws UndecidedOmemoIdentityException when there are undecided identities. */ - public Message encryptForExistingSessions(CannotEstablishOmemoSessionException exception, String message) throws CryptoFailedException, UndecidedOmemoIdentityException { - Message m = new Message(); - m.setBody(message); - OmemoVAxolotlElement encrypted = getOmemoService().encryptOmemoMessage(this, exception.getSuccesses(), m); - return finishMessage(encrypted); + public Message encryptForExistingSessions(CannotEstablishOmemoSessionException exception, String message) + throws CryptoFailedException, UndecidedOmemoIdentityException, SmackException.NotLoggedInException + { + synchronized (LOCK) { + Message m = new Message(); + m.setBody(message); + OmemoVAxolotlElement encrypted = getOmemoService() + .encryptOmemoMessage(new KnownBareJidGuard(this), exception.getSuccesses(), m); + return finishMessage(encrypted); + } } /** @@ -317,13 +382,19 @@ public final class OmemoManager extends Manager { * @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 { - return getOmemoService().processLocalMessage(this, sender, omemoMessage); + 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 KnownBareJidGuard(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. + * 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 @@ -332,10 +403,15 @@ public final class OmemoManager extends Manager { * @throws SmackException.NotConnectedException Exception * @throws SmackException.NoResponseException Exception */ - public List decryptMamQueryResult(MamManager.MamQueryResult mamQueryResult) throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException { - List l = new ArrayList<>(); - l.addAll(getOmemoService().decryptMamQueryResult(this, mamQueryResult)); - return l; + 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 KnownBareJidGuard(this), mamQueryResult)); + return l; + } } /** @@ -346,7 +422,11 @@ public final class OmemoManager extends Manager { * @param fingerprint fingerprint */ public void trustOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) { - getOmemoService().getOmemoStoreBackend().trustOmemoIdentity(this, device, fingerprint); + if (trustCallback == null) { + throw new IllegalStateException("No TrustCallback set."); + } + + trustCallback.setTrust(device, fingerprint, TrustState.trusted); } /** @@ -357,7 +437,11 @@ public final class OmemoManager extends Manager { * @param fingerprint fingerprint */ public void distrustOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) { - getOmemoService().getOmemoStoreBackend().distrustOmemoIdentity(this, device, fingerprint); + if (trustCallback == null) { + throw new IllegalStateException("No TrustCallback set."); + } + + trustCallback.setTrust(device, fingerprint, TrustState.untrusted); } /** @@ -369,7 +453,11 @@ public final class OmemoManager extends Manager { * @return */ public boolean isTrustedOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) { - return getOmemoService().getOmemoStoreBackend().isTrustedOmemoIdentity(this, device, fingerprint); + if (trustCallback == null) { + throw new IllegalStateException("No TrustCallback set."); + } + + return trustCallback.getTrust(device, fingerprint) == TrustState.trusted; } /** @@ -381,7 +469,11 @@ public final class OmemoManager extends Manager { * @return */ public boolean isDecidedOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) { - return getOmemoService().getOmemoStoreBackend().isDecidedOmemoIdentity(this, device, fingerprint); + if (trustCallback == null) { + throw new IllegalStateException("No TrustCallback set."); + } + + return trustCallback.getTrust(device, fingerprint) != TrustState.undecided; } /** @@ -392,9 +484,14 @@ public final class OmemoManager extends Manager { * @throws XMPPException.XMPPErrorException * @throws CorruptedOmemoKeyException */ - public void purgeDevices() throws SmackException, InterruptedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException { - getOmemoService().publishDeviceIdIfNeeded(this,true); - getOmemoService().publishBundle(this); + public void purgeDeviceList() + throws SmackException, InterruptedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException + { + synchronized (LOCK) { + KnownBareJidGuard managerGuard = new KnownBareJidGuard(this); + getOmemoService().publishDeviceIdIfNeeded(managerGuard, true); + getOmemoService().publishBundle(managerGuard); + } } /** @@ -404,11 +501,16 @@ public final class OmemoManager extends Manager { * @throws XMPPException.XMPPErrorException * @throws CorruptedOmemoKeyException */ - public void regenerate() throws SmackException, InterruptedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException { - // create a new identity and publish new keys to the server - getOmemoService().regenerate(this, null); - getOmemoService().publishDeviceIdIfNeeded(this,false); - getOmemoService().publishBundle(this); + public void regenerateIdentity() + throws SmackException, InterruptedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException + { + synchronized (LOCK) { + KnownBareJidGuard managerGuard = new KnownBareJidGuard(this); + // create a new identity and publish new keys to the server + getOmemoService().regenerate(managerGuard); + getOmemoService().publishDeviceIdIfNeeded(managerGuard, false); + getOmemoService().publishBundle(managerGuard); + } } /** @@ -423,8 +525,12 @@ public final class OmemoManager extends Manager { */ public void sendRatchetUpdateMessage(OmemoDevice recipient) throws CorruptedOmemoKeyException, UndecidedOmemoIdentityException, CryptoFailedException, - CannotEstablishOmemoSessionException { - getOmemoService().sendOmemoRatchetUpdateMessage(this, recipient, false); + CannotEstablishOmemoSessionException, SmackException.NotLoggedInException + { + synchronized (LOCK) { + getOmemoService().sendOmemoRatchetUpdateMessage( + new KnownBareJidGuard(this), recipient, false); + } } /** @@ -442,8 +548,12 @@ public final class OmemoManager extends Manager { */ public OmemoVAxolotlElement createKeyTransportElement(byte[] aesKey, byte[] iv, OmemoDevice ... to) throws UndecidedOmemoIdentityException, CorruptedOmemoKeyException, CryptoFailedException, - CannotEstablishOmemoSessionException { - return getOmemoService().prepareOmemoKeyTransportElement(this, aesKey, iv, to); + CannotEstablishOmemoSessionException, SmackException.NotLoggedInException + { + synchronized (LOCK) { + return getOmemoService().prepareOmemoKeyTransportElement( + new KnownBareJidGuard(this), aesKey, iv, to); + } } /** @@ -489,10 +599,17 @@ public final class OmemoManager extends Manager { * @throws InterruptedException * @throws SmackException.NoResponseException */ - public boolean contactSupportsOmemo(BareJid contact) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { - getOmemoService().refreshDeviceList(this, contact); - return !getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(this, contact) - .getActiveDevices().isEmpty(); + public boolean contactSupportsOmemo(BareJid contact) + throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, + SmackException.NotLoggedInException + { + synchronized (LOCK) { + KnownBareJidGuard managerGuard = new KnownBareJidGuard(this); + + getOmemoService().refreshDeviceList(managerGuard, contact); + return !getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(getOwnDevice(), contact) + .getActiveDevices().isEmpty(); + } } /** @@ -506,7 +623,10 @@ public final class OmemoManager extends Manager { * @throws InterruptedException goes * @throws SmackException.NoResponseException wrong */ - public boolean multiUserChatSupportsOmemo(EntityBareJid multiUserChat) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { + public boolean multiUserChatSupportsOmemo(EntityBareJid multiUserChat) + throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException + { RoomInfo roomInfo = MultiUserChatManager.getInstanceFor(connection()).getRoomInfo(multiUserChat); return roomInfo.isNonanonymous() && roomInfo.isMembersOnly(); } @@ -522,8 +642,12 @@ public final class OmemoManager extends Manager { * @throws InterruptedException * @throws SmackException.NoResponseException */ - public static boolean serverSupportsOmemo(XMPPConnection connection, DomainBareJid server) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { - return ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(server).containsFeature(PubSub.NAMESPACE); + public static boolean serverSupportsOmemo(XMPPConnection connection, DomainBareJid server) + throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException + { + return ServiceDiscoveryManager.getInstanceFor(connection) + .discoverInfo(server).containsFeature(PubSub.NAMESPACE); } /** @@ -531,16 +655,33 @@ public final class OmemoManager extends Manager { * * @return fingerprint */ - public OmemoFingerprint getOurFingerprint() { - return getOmemoService().getOmemoStoreBackend().getFingerprint(this); + public OmemoFingerprint getOwnFingerprint() + throws SmackException.NotLoggedInException, CorruptedOmemoKeyException + { + synchronized (LOCK) { + if (getOwnJid() == null) { + throw new SmackException.NotLoggedInException(); + } + + return getOmemoService().getOmemoStoreBackend().getFingerprint(getOwnDevice()); + } } - public OmemoFingerprint getFingerprint(OmemoDevice device) throws CannotEstablishOmemoSessionException { - if (device.equals(getOwnDevice())) { - return getOurFingerprint(); - } + public OmemoFingerprint getFingerprint(OmemoDevice device) + throws CannotEstablishOmemoSessionException, SmackException.NotLoggedInException, + CorruptedOmemoKeyException + { + synchronized (LOCK) { + if (getOwnJid() == null) { + throw new SmackException.NotLoggedInException(); + } - return getOmemoService().getOmemoStoreBackend().getFingerprint(this, device); + if (device.equals(getOwnDevice())) { + return getOwnFingerprint(); + } + + return getOmemoService().getOmemoStoreBackend().getFingerprint(new KnownBareJidGuard(this), device); + } } /** @@ -548,24 +689,30 @@ public final class OmemoManager extends Manager { * @param contact contact * @return HashMap of deviceIds and corresponding fingerprints. */ - public HashMap getActiveFingerprints(BareJid contact) { - HashMap fingerprints = new HashMap<>(); - CachedDeviceList deviceList = getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(this, contact); - for (int id : deviceList.getActiveDevices()) { - OmemoDevice device = new OmemoDevice(contact, id); - OmemoFingerprint fingerprint = null; - try { - fingerprint = getFingerprint(device); - } catch (CannotEstablishOmemoSessionException e) { - LOGGER.log(Level.WARNING, "Could not build session with device " + id - + " of user " + contact + ": " + e.getMessage()); + public HashMap getActiveFingerprints(BareJid contact) + throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, + CannotEstablishOmemoSessionException + { + synchronized (LOCK) { + if (getOwnJid() == null) { + throw new SmackException.NotLoggedInException(); } - if (fingerprint != null) { - fingerprints.put(device, fingerprint); + HashMap fingerprints = new HashMap<>(); + CachedDeviceList deviceList = getOmemoService().getOmemoStoreBackend() + .loadCachedDeviceList(getOwnDevice(), contact); + + for (int id : deviceList.getActiveDevices()) { + OmemoDevice device = new OmemoDevice(contact, id); + OmemoFingerprint fingerprint = getFingerprint(device); + + if (fingerprint != null) { + fingerprints.put(device, fingerprint); + } } + + return fingerprints; } - return fingerprints; } public void addOmemoMessageListener(OmemoMessageListener listener) { @@ -593,8 +740,13 @@ public final class OmemoManager extends Manager { * @throws SmackException.NotConnectedException * @throws SmackException.NoResponseException */ - public void buildSessionsWith(BareJid contact) throws InterruptedException, CannotEstablishOmemoSessionException, SmackException.NotConnectedException, SmackException.NoResponseException { - getOmemoService().buildOrCreateOmemoSessionsFromBundles(this, contact); + public void buildSessionsWith(BareJid contact) + throws InterruptedException, CannotEstablishOmemoSessionException, SmackException.NotConnectedException, + SmackException.NoResponseException, SmackException.NotLoggedInException + { + synchronized (LOCK) { + getOmemoService().buildMissingOmemoSessions(new KnownBareJidGuard(this), contact); + } } /** @@ -605,8 +757,13 @@ public final class OmemoManager extends Manager { * @throws InterruptedException * @throws SmackException.NoResponseException */ - public void requestDeviceListUpdateFor(BareJid contact) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { - getOmemoService().refreshDeviceList(this, contact); + public void requestDeviceListUpdateFor(BareJid contact) + throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, + SmackException.NotLoggedInException + { + synchronized (LOCK) { + getOmemoService().refreshDeviceList(new KnownBareJidGuard(this), contact); + } } /** @@ -621,12 +778,19 @@ public final class OmemoManager extends Manager { * @throws SmackException.NoResponseException XMPP error * @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 { - // generate key - getOmemoService().getOmemoStoreBackend().changeSignedPreKey(this); - // publish - getOmemoService().publishDeviceIdIfNeeded(this, false); - getOmemoService().publishBundle(this); + public void rotateSignedPreKey() + throws CorruptedOmemoKeyException, InterruptedException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, SmackException.NoResponseException, + PubSubException.NotALeafNodeException, SmackException.NotLoggedInException + { + synchronized (LOCK) { + KnownBareJidGuard managerGuard = new KnownBareJidGuard(this); + // generate key + getOmemoService().getOmemoStoreBackend().changeSignedPreKey(getOwnDevice()); + // publish + getOmemoService().publishDeviceIdIfNeeded(managerGuard, false); + getOmemoService().publishBundle(managerGuard); + } } /** @@ -647,14 +811,12 @@ public final class OmemoManager extends Manager { } } + /** + * Returns a pseudo random number from the interval [1, Integer.MAX_VALUE]. + * @return deviceId + */ public static int randomDeviceId() { - int i = new Random().nextInt(Integer.MAX_VALUE); - - if (i == 0) { - return randomDeviceId(); - } - - return Math.abs(i); + return new Random().nextInt(Integer.MAX_VALUE - 1) + 1; } /** @@ -663,9 +825,11 @@ public final class OmemoManager extends Manager { * @return bareJid */ public BareJid getOwnJid() { - EntityFullJid fullJid = connection().getUser(); - if (fullJid == null) return null; - return fullJid.asBareJid(); + if (ownJid == null && connection().isAuthenticated()) { + ownJid = connection().getUser().asBareJid(); + } + + return ownJid; } /** @@ -673,8 +837,10 @@ public final class OmemoManager extends Manager { * * @return deviceId */ - public int getDeviceId() { - return deviceId; + public Integer getDeviceId() { + synchronized (LOCK) { + return deviceId; + } } /** @@ -683,13 +849,23 @@ public final class OmemoManager extends Manager { * @return omemoDevice */ public OmemoDevice getOwnDevice() { - return new OmemoDevice(getOwnJid(), getDeviceId()); + synchronized (LOCK) { + BareJid jid = getOwnJid(); + if (jid == null) { + return null; + } + return new OmemoDevice(jid, getDeviceId()); + } } void setDeviceId(int nDeviceId) { - INSTANCES.get(connection()).remove(getDeviceId()); - INSTANCES.get(connection()).put(nDeviceId, this); - this.deviceId = nDeviceId; + synchronized (LOCK) { + // Move this instance inside the HashMaps + INSTANCES.get(connection()).remove(getDeviceId()); + INSTANCES.get(connection()).put(nDeviceId, this); + + this.deviceId = nDeviceId; + } } /** @@ -700,14 +876,21 @@ public final class OmemoManager extends Manager { * @param wrappingMessage message that wrapped the incoming message * @param messageInformation information about the messages encryption (used identityKey, carbon...) */ - void notifyOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation messageInformation) { + void notifyOmemoMessageReceived(String decryptedBody, + Message encryptedMessage, + Message wrappingMessage, + OmemoMessageInformation messageInformation) + { for (OmemoMessageListener l : omemoMessageListeners) { l.onOmemoMessageReceived(decryptedBody, encryptedMessage, wrappingMessage, messageInformation); } } - void notifyOmemoKeyTransportMessageReceived(CipherAndAuthTag cipherAndAuthTag, Message transportingMessage, - Message wrappingMessage, OmemoMessageInformation information) { + void notifyOmemoKeyTransportMessageReceived(CipherAndAuthTag cipherAndAuthTag, + Message transportingMessage, + Message wrappingMessage, + OmemoMessageInformation information) + { for (OmemoMessageListener l : omemoMessageListeners) { l.onOmemoKeyTransportReceived(cipherAndAuthTag, transportingMessage, wrappingMessage, information); } @@ -723,17 +906,26 @@ public final class OmemoManager extends Manager { * @param wrappingMessage wrapping message (in case of carbon copy) * @param omemoInformation information about the encryption of the message */ - void notifyOmemoMucMessageReceived(MultiUserChat muc, BareJid from, String decryptedBody, Message message, - Message wrappingMessage, OmemoMessageInformation omemoInformation) { + void notifyOmemoMucMessageReceived(MultiUserChat muc, + BareJid from, + String decryptedBody, + Message message, + Message wrappingMessage, + OmemoMessageInformation omemoInformation) + { for (OmemoMucMessageListener l : omemoMucMessageListeners) { l.onOmemoMucMessageReceived(muc, from, decryptedBody, message, wrappingMessage, omemoInformation); } } - void notifyOmemoMucKeyTransportMessageReceived(MultiUserChat muc, BareJid from, CipherAndAuthTag cipherAndAuthTag, - Message transportingMessage, Message wrappingMessage, - OmemoMessageInformation messageInformation) { + void notifyOmemoMucKeyTransportMessageReceived(MultiUserChat muc, + BareJid from, + CipherAndAuthTag cipherAndAuthTag, + Message transportingMessage, + Message wrappingMessage, + OmemoMessageInformation messageInformation) + { for (OmemoMucMessageListener l : omemoMucMessageListeners) { l.onOmemoKeyTransportReceived(muc, from, cipherAndAuthTag, transportingMessage, wrappingMessage, messageInformation); @@ -741,13 +933,28 @@ public final class OmemoManager extends Manager { } /** - * Remove all active stanza listeners of this manager from the connection. - * This is somewhat the counterpart of initialize(). + * Register stanza listeners needed for OMEMO. + * This method is called automatically in the constructor and should only be used to restore the previous state + * after {@link #stopListeners()} was called. */ - public void shutdown() { + public void startStanzaListeners() { + PEPManager pepManager = PEPManager.getInstanceFor(connection()); + pepManager.removePEPListener(deviceListUpdateListener); + pepManager.addPEPListener(deviceListUpdateListener); + connection().removeAsyncStanzaListener(internalOmemoMessageStanzaListener); + connection().addAsyncStanzaListener(internalOmemoMessageStanzaListener, omemoMessageStanzaFilter); + CarbonManager carbonManager = CarbonManager.getInstanceFor(connection()); + carbonManager.removeCarbonCopyReceivedListener(internalOmemoCarbonCopyListener); + carbonManager.addCarbonCopyReceivedListener(internalOmemoCarbonCopyListener); + } + + /** + * Remove active stanza listeners needed for OMEMO. + */ + public void stopListeners() { PEPManager.getInstanceFor(connection()).removePEPListener(deviceListUpdateListener); - connection().removeAsyncStanzaListener(omemoStanzaListener); - CarbonManager.getInstanceFor(connection()).removeCarbonCopyReceivedListener(omemoCarbonCopyListener); + connection().removeAsyncStanzaListener(internalOmemoMessageStanzaListener); + CarbonManager.getInstanceFor(connection()).removeCarbonCopyReceivedListener(internalOmemoCarbonCopyListener); } /** @@ -769,9 +976,21 @@ public final class OmemoManager extends Manager { return service; } - PEPListener deviceListUpdateListener = new PEPListener() { + private final PEPListener deviceListUpdateListener = new PEPListener() { + + KnownBareJidGuard managerGuard = null; + @Override public void eventReceived(EntityBareJid from, EventElement event, Message message) { + + if (managerGuard == null) { + try { + managerGuard = new KnownBareJidGuard(OmemoManager.this); + } catch (SmackException.NotLoggedInException e) { + throw new AssertionError(e); + } + } + for (ExtensionElement items : event.getExtensions()) { if (!(items instanceof ItemsExtension)) { continue; @@ -789,9 +1008,12 @@ public final class OmemoManager extends Manager { } // Device List - OmemoDeviceListVAxolotlElement omemoDeviceListElement = (OmemoDeviceListVAxolotlElement) payloadItem.getPayload(); + OmemoDeviceListVAxolotlElement omemoDeviceListElement = + (OmemoDeviceListVAxolotlElement) payloadItem.getPayload(); int ourDeviceId = getDeviceId(); - getOmemoService().getOmemoStoreBackend().mergeCachedDeviceList(OmemoManager.this, from, omemoDeviceListElement); + + getOmemoService().getOmemoStoreBackend() + .mergeCachedDeviceList(managerGuard.get().getOwnDevice(), from, omemoDeviceListElement); if (from == null) { // Unknown sender, no more work to do. @@ -812,22 +1034,24 @@ public final class OmemoManager extends Manager { // 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 OmemoDeviceListVAxolotlElement newOmemoDeviceListElement = new OmemoDeviceListVAxolotlElement(deviceListIds); + final OmemoDeviceListVAxolotlElement newOmemoDeviceListElement = + new OmemoDeviceListVAxolotlElement(deviceListIds); - // PEPListener is a synchronous listener. Avoid any deadlocks by using an async task to update the device list. + // 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(OmemoManager.this, newOmemoDeviceListElement); + 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()); + LOGGER.log(Level.SEVERE, "Could not publish our device list after an update " + + "without our id was received: " + e.getMessage()); } } }); @@ -836,19 +1060,75 @@ public final class OmemoManager extends Manager { } }; - - - OmemoService.OmemoStanzaListener getOmemoStanzaListener() { - if (omemoStanzaListener == null) { - omemoStanzaListener = getOmemoService().createStanzaListener(this); + private final StanzaListener internalOmemoMessageStanzaListener = new StanzaListener() { + @Override + public void processStanza(Stanza packet) throws SmackException.NotConnectedException, InterruptedException { + try { + getOmemoService().onOmemoMessageStanzaReceived(packet, + new KnownBareJidGuard(OmemoManager.this)); + } catch (SmackException.NotLoggedInException e) { + LOGGER.warning("Received OMEMO stanza while being offline: " + e); + } + } + }; + + private final CarbonCopyReceivedListener internalOmemoCarbonCopyListener = new CarbonCopyReceivedListener() { + @Override + public void onCarbonCopyReceived(CarbonExtension.Direction direction, + Message carbonCopy, + Message wrappingMessage) { + if (omemoMessageStanzaFilter.accept(carbonCopy)) { + try { + getOmemoService().onOmemoCarbonCopyReceived(direction, carbonCopy, wrappingMessage, + new KnownBareJidGuard(OmemoManager.this)); + } catch (SmackException.NotLoggedInException e) { + LOGGER.warning("Received OMEMO carbon copy while being offline: " + e); + } + } + } + }; + + /** + * StanzaFilter that filters messages containing a OMEMO element. + */ + private final StanzaFilter omemoMessageStanzaFilter = new StanzaFilter() { + @Override + public boolean accept(Stanza stanza) { + return stanza instanceof Message && OmemoManager.stanzaContainsOmemoElement(stanza); + } + }; + + public static class KnownBareJidGuard { + + private final OmemoManager manager; + + public KnownBareJidGuard(OmemoManager manager) + throws SmackException.NotLoggedInException { + + if (manager == null) { + throw new IllegalArgumentException("OmemoManager cannot be null."); + } + + if (manager.getOwnJid() == null) { + if (manager.getConnection().isAuthenticated()) { + manager.ownJid = manager.getConnection().getUser().asBareJid(); + } else { + throw new SmackException.NotLoggedInException(); + } + } + + this.manager = manager; + } + + public OmemoManager get() { + return manager; } - return omemoStanzaListener; } - OmemoService.OmemoCarbonCopyListener getOmemoCarbonCopyListener() { - if (omemoCarbonCopyListener == null) { - omemoCarbonCopyListener = getOmemoService().createOmemoCarbonCopyListener(this); - } - return omemoCarbonCopyListener; + public interface FinishedCallback { + + void initializationFinished(OmemoManager manager); + + void initializationFailed(Exception cause); } } 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 1ad4ae3ea..042ac2173 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 @@ -43,21 +43,19 @@ import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.StanzaListener; 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.CarbonManager; 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.OmemoBundleVAxolotlElement; import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement; import org.jivesoftware.smackx.omemo.element.OmemoDeviceListVAxolotlElement; @@ -71,10 +69,10 @@ 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.internal.IdentityKeyWrapper; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation; -import org.jivesoftware.smackx.omemo.internal.OmemoSession; +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; @@ -101,18 +99,27 @@ import org.jxmpp.jid.Jid; * @param Cipher class * @author Paul Schaub */ -public abstract class OmemoService { - +public abstract class OmemoService + implements OmemoCarbonCopyStanzaReceivedListener, OmemoMessageStanzaReceivedListener +{ static { Security.addProvider(new BouncyCastleProvider()); } protected static final Logger LOGGER = Logger.getLogger(OmemoService.class.getName()); + /** + * This is a singleton. + */ private static OmemoService INSTANCE; protected OmemoStore omemoStore; + protected final HashMap> sessionManagers = new HashMap<>(); + /** + * Return the singleton instance of this class. When no instance is set, throw an IllegalStateException instead. + * @return instance. + */ public static OmemoService getInstance() { if (INSTANCE == null) { throw new IllegalStateException("No OmemoService registered"); @@ -132,6 +139,11 @@ public abstract class OmemoService getOmemoStoreBackend() { if (omemoStore == null) { - setOmemoStoreBackend(createDefaultOmemoStoreBackend()); - return getOmemoStoreBackend(); + omemoStore = createDefaultOmemoStoreBackend(); } return omemoStore; } @@ -157,7 +168,8 @@ public abstract class OmemoService omemoStore) { + OmemoStore omemoStore) + { if (this.omemoStore != null) { throw new IllegalStateException("An OmemoStore backend has already been set."); } @@ -188,8 +200,8 @@ public abstract class OmemoService(null, null, ""); } /** * Generate a new unique deviceId and regenerate new keys. * - * @param omemoManager OmemoManager we want to regenerate. - * @param nDeviceId new DeviceId we want to use with the newly generated keys. + * @param managerGuard OmemoManager we want to regenerate. * @throws CorruptedOmemoKeyException when freshly generated identityKey is invalid * (should never ever happen *crosses fingers*) */ - void regenerate(OmemoManager omemoManager, Integer nDeviceId) throws CorruptedOmemoKeyException { + void regenerate(OmemoManager.KnownBareJidGuard managerGuard) + throws CorruptedOmemoKeyException + { + OmemoManager omemoManager = managerGuard.get(); + OmemoDevice userDevice = omemoManager.getOwnDevice(); + + int nDeviceId = OmemoManager.randomDeviceId(); + // Generate unique ID that is not already taken - while (nDeviceId == null || !getOmemoStoreBackend().isAvailableDeviceId(omemoManager, nDeviceId)) { + while (!getOmemoStoreBackend().isAvailableDeviceId(userDevice, nDeviceId)) { nDeviceId = OmemoManager.randomDeviceId(); } - getOmemoStoreBackend().forgetOmemoSessions(omemoManager); - getOmemoStoreBackend().purgeOwnDeviceKeys(omemoManager); + getOmemoStoreBackend().purgeOwnDeviceKeys(userDevice); omemoManager.setDeviceId(nDeviceId); - getOmemoStoreBackend().regenerate(omemoManager); } /** - * Publish a fresh bundle to the server. + * Publish a bundle to the server. * - * @param omemoManager OmemoManager + * @param managerGuard OmemoManager * @throws SmackException.NotConnectedException * @throws InterruptedException * @throws SmackException.NoResponseException * @throws CorruptedOmemoKeyException * @throws XMPPException.XMPPErrorException */ - void publishBundle(OmemoManager omemoManager) - throws SmackException.NotConnectedException, InterruptedException, - SmackException.NoResponseException, CorruptedOmemoKeyException, XMPPException.XMPPErrorException { - Date lastSignedPreKeyRenewal = getOmemoStoreBackend().getDateOfLastSignedPreKeyRenewal(omemoManager); + void publishBundle(OmemoManager.KnownBareJidGuard 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.INFO, "Renewing signedPreKey"); - getOmemoStoreBackend().changeSignedPreKey(omemoManager); + LOGGER.log(Level.FINE, "Renewing signedPreKey"); + getOmemoStoreBackend().changeSignedPreKey(userDevice); } } else { - getOmemoStoreBackend().setDateOfLastSignedPreKeyRenewal(omemoManager); + getOmemoStoreBackend().setDateOfLastSignedPreKeyRenewal(userDevice, new Date()); } // publish - PubSubManager.getInstance(omemoManager.getConnection(), omemoManager.getOwnJid()) - .tryToPublishAndPossibleAutoCreate(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(omemoManager.getDeviceId()), - new PayloadItem<>(getOmemoStoreBackend().packOmemoBundle(omemoManager))); + OmemoBundleElement bundleElement = getOmemoStoreBackend().packOmemoBundle(userDevice); + + PubSubManager.getInstance(omemoManager.getConnection(), userDevice.getJid()) + .tryToPublishAndPossibleAutoCreate( + OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(userDevice.getDeviceId()), + new PayloadItem<>(bundleElement)); } /** * Publish our deviceId in case it is not on the list already. * This method calls publishDeviceIdIfNeeded(omemoManager, deleteOtherDevices, false). - * @param omemoManager OmemoManager + * @param managerGuard OmemoManager * @param deleteOtherDevices Do we want to remove other devices from the list? * @throws InterruptedException * @throws PubSubException.NotALeafNodeException @@ -309,16 +325,17 @@ public abstract class OmemoService deviceListIds; if (deviceList == null) { @@ -351,10 +371,10 @@ public abstract class OmemoService deviceListIds) { + boolean removeStaleDevicesIfNeeded(OmemoManager.KnownBareJidGuard managerGuard, Set deviceListIds) { + OmemoManager omemoManager = managerGuard.get(); + OmemoDevice userDevice = omemoManager.getOwnDevice(); boolean publish = false; - int ownDeviceId = omemoManager.getDeviceId(); + // 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 == ownDeviceId) { + if (id == userDevice.getDeviceId()) { // Skip own id continue; } - OmemoDevice d = new OmemoDevice(omemoManager.getOwnJid(), id); - Date date = getOmemoStoreBackend().getDateOfLastReceivedMessage(omemoManager, d); + OmemoDevice contactsDevice = new OmemoDevice(userDevice.getJid(), id); + Date date = getOmemoStoreBackend().getDateOfLastReceivedMessage(userDevice, contactsDevice); if (date == null) { - getOmemoStoreBackend().setDateOfLastReceivedMessage(omemoManager, d); + + getOmemoStoreBackend().setDateOfLastReceivedMessage(userDevice, contactsDevice, new Date()); + } else { - if (System.currentTimeMillis() - date.getTime() > 1000L * 60 * 60 * OmemoConfiguration.getDeleteStaleDevicesAfterHours()) { + + 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(); @@ -397,8 +424,8 @@ public abstract class OmemoService(deviceList)); } @@ -416,7 +445,7 @@ public abstract class OmemoService items = node.getItems(); if (items.size() > 0) { - OmemoDeviceListVAxolotlElement listElement = (OmemoDeviceListVAxolotlElement) ((PayloadItem) items.get(items.size() - 1)).getPayload(); + + OmemoDeviceListVAxolotlElement listElement = + (OmemoDeviceListVAxolotlElement) ((PayloadItem) items.get(items.size() - 1)).getPayload(); + if (items.size() > 1) { node.deleteAllItems(); node.publish(new PayloadItem<>(listElement)); } + return listElement; } @@ -587,27 +639,35 @@ public abstract class OmemoService bundles = getOmemoStoreBackend().keyUtil().BUNDLE.bundles(bundle, device); + 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(omemoManager, randomPreKeyBundle, device); + processBundle(managerGuard, randomPreKeyBundle, contactsDevice); } /** * Process a received bundle. Typically that includes saving keys and building a session. * - * @param omemoManager omemoManager that will process the bundle + * @param managerGuard omemoManager that will process the bundle * @param bundle T_Bundle (depends on used Signal/Olm library) * @param device OmemoDevice * @throws CorruptedOmemoKeyException */ - protected abstract void processBundle(OmemoManager omemoManager, T_Bundle bundle, OmemoDevice device) throws CorruptedOmemoKeyException; + protected abstract void processBundle(OmemoManager.KnownBareJidGuard managerGuard, + T_Bundle bundle, + OmemoDevice device) + 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. * - * @param sender the BareJid of the sender of the message - * @param message the encrypted message - * @param information OmemoMessageInformation object which will contain meta data about the decrypted message + * @param contactsDevice OmemoDevice of the sender of the message + * @param message 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 @@ -696,9 +764,16 @@ public abstract class OmemoService messageRecipientKeys = message.getHeader().getKeys(); // Do we have a key with our ID in the message? @@ -708,14 +783,14 @@ public abstract class OmemoService recipients = new ArrayList<>(); recipients.add(recipient); - return processSendingMessage(omemoManager, recipients, message); + return processSendingMessage(managerGuard, recipients, message); } /** * Encrypt a clear text message for the given recipients. * The body of the message will be encrypted. * - * @param omemoManager omemoManager of the sending device. + * @param managerGuard omemoManager of the sending device. * @param recipients List of BareJids of all recipients * @param message message to encrypt. * @return OmemoMessageElement @@ -781,15 +866,22 @@ public abstract class OmemoService recipients, Message message) - throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, CannotEstablishOmemoSessionException { + OmemoVAxolotlElement processSendingMessage(OmemoManager.KnownBareJidGuard 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 { - buildOrCreateOmemoSessionsFromBundles(omemoManager, recipient); + buildMissingOmemoSessions(managerGuard, recipient); } catch (CannotEstablishOmemoSessionException e) { if (sessionException == null) { @@ -801,12 +893,12 @@ public abstract class OmemoService receivingDevices = new ArrayList<>(); for (int id : theirDevices.getActiveDevices()) { OmemoDevice recipientDevice = new OmemoDevice(recipient, id); - if (getOmemoStoreBackend().containsRawSession(omemoManager, recipientDevice)) { + if (getOmemoStoreBackend().containsRawSession(userDevice, recipientDevice)) { receivingDevices.add(recipientDevice); } @@ -821,33 +913,34 @@ public abstract class OmemoService ourReceivingDevices = new ArrayList<>(); for (int id : ourDevices.getActiveDevices()) { - OmemoDevice ourDevice = new OmemoDevice(omemoManager.getOwnJid(), id); + OmemoDevice remoteUserDevice = new OmemoDevice(omemoManager.getOwnJid(), id); if (id == omemoManager.getDeviceId()) { // Don't build session with our exact device. continue; } - Date lastReceived = getOmemoStoreBackend().getDateOfLastReceivedMessage(omemoManager, ourDevice); + Date lastReceived = getOmemoStoreBackend().getDateOfLastReceivedMessage(userDevice, remoteUserDevice); if (lastReceived == null) { - getOmemoStoreBackend().setDateOfLastReceivedMessage(omemoManager, ourDevice); + 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 " + ourDevice + + 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(omemoManager, ourDevice)) { - ourReceivingDevices.add(ourDevice); + + if (getOmemoStoreBackend().containsRawSession(userDevice, remoteUserDevice)) { + ourReceivingDevices.add(remoteUserDevice); } } } @@ -860,13 +953,13 @@ public abstract class OmemoService - session = getOmemoStoreBackend().getOmemoSessionOf(omemoManager, sender); - CipherAndAuthTag cipherAndAuthTag = session.decryptTransportedKey(omemoMessage, omemoManager.getDeviceId()); + CipherAndAuthTag cipherAndAuthTag = sessionManagers.get(managerGuard.get()).retrieveMessageKeyAndAuthTag(sender, omemoMessage); messageInfo.setSenderDevice(sender); - messageInfo.setSenderIdentityKey(new IdentityKeyWrapper(session.getIdentityKey())); + messageInfo.setSenderFingerprint(omemoStore.keyUtil().getFingerprintOfIdentityKey( + omemoStore.loadOmemoIdentityKey(userDevice, sender))); - if (preKeyCountBefore != getOmemoStoreBackend().loadOmemoPreKeys(omemoManager).size()) { - LOGGER.log(Level.INFO, "We used up a preKey. Publish new Bundle."); - publishBundle(omemoManager); + if (preKeyCountBefore != getOmemoStoreBackend().loadOmemoPreKeys(userDevice).size()) { + LOGGER.log(Level.FINE, "We used up a preKey. Publish new Bundle."); + publishBundle(managerGuard); } return cipherAndAuthTag; } @@ -929,7 +1028,7 @@ public abstract class OmemoService> recipients, Message message) - throws CryptoFailedException, UndecidedOmemoIdentityException { - + OmemoVAxolotlElement encryptOmemoMessage(OmemoManager.KnownBareJidGuard managerGuard, + HashMap> recipients, + Message message) + throws CryptoFailedException, UndecidedOmemoIdentityException + { OmemoMessageBuilder builder; try { - builder = new OmemoMessageBuilder<>(omemoManager, getOmemoStoreBackend(), message.getBody()); + builder = new OmemoMessageBuilder<>(managerGuard, getOmemoSessionManager(managerGuard), message.getBody()); } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { throw new CryptoFailedException(e); @@ -956,7 +1057,7 @@ public abstract class OmemoService builder; try { - builder = new OmemoMessageBuilder<>(omemoManager, getOmemoStoreBackend(), null); + builder = new OmemoMessageBuilder<>(managerGuard, getOmemoSessionManager(managerGuard), null); } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { @@ -1012,7 +1115,7 @@ public abstract class OmemoService builder; try { - builder = new OmemoMessageBuilder<>(omemoManager, getOmemoStoreBackend(), aesKey, iv); + builder = new OmemoMessageBuilder<>(managerGuard, getOmemoSessionManager(managerGuard), aesKey, iv); } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { @@ -1045,7 +1152,7 @@ public abstract class OmemoService decryptMamQueryResult(OmemoManager omemoManager, MamManager.MamQueryResult mamQueryResult) - throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException { + List decryptMamQueryResult(OmemoManager.KnownBareJidGuard 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(omemoManager, f.getForwardedStanza().getFrom().asBareJid(), (Message) f.getForwardedStanza())); + 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()); @@ -1143,7 +1240,8 @@ public abstract class OmemoService - service; + MultiUserChat muc = mucm.getMultiUserChat(stanza.getFrom().asEntityBareJidIfPossible()); + if (omemoMessage.isMessageElement()) { - OmemoStanzaListener(OmemoManager omemoManager, - OmemoService service) { - this.omemoManager = omemoManager; - this.service = service; - } + decrypted = processReceivingMessage(managerGuard, senderDevice, omemoMessage, messageInfo); + if (decrypted != null) { + omemoManager.notifyOmemoMucMessageReceived(muc, senderDevice.getJid(), decrypted.getBody(), + (Message) stanza, null, messageInfo); + } - @Override - public void processStanza(Stanza stanza) throws SmackException.NotConnectedException, InterruptedException { - Message decrypted; - OmemoElement omemoMessage = stanza.getExtension(OmemoElement.ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); - OmemoMessageInformation messageInfo = new OmemoMessageInformation(); - MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(omemoManager.getConnection()); - OmemoDevice senderDevice = getSender(omemoManager, stanza); + } 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 { - // Is it a MUC message... - if (isMucMessage(omemoManager, stanza)) { + LOGGER.log(Level.INFO, "Received message with invalid session from " + + senderDevice + ". Send RatchetUpdateMessage."); + sendOmemoRatchetUpdateMessage(managerGuard, senderDevice, true); - MultiUserChat muc = mucm.getMultiUserChat(stanza.getFrom().asEntityBareJidIfPossible()); - if (omemoMessage.isMessageElement()) { + } catch (UndecidedOmemoIdentityException | CorruptedOmemoKeyException | CannotEstablishOmemoSessionException + | CryptoFailedException e1) { - decrypted = processReceivingMessage(omemoManager, senderDevice, omemoMessage, messageInfo); - if (decrypted != null) { - omemoManager.notifyOmemoMucMessageReceived(muc, senderDevice.getJid(), decrypted.getBody(), - (Message) stanza, null, messageInfo); - } - - } else if (omemoMessage.isKeyTransportElement()) { - - CipherAndAuthTag cipherAndAuthTag = decryptTransportedOmemoKey(omemoManager, 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 = service.processReceivingMessage(omemoManager, senderDevice, omemoMessage, messageInfo); - if (decrypted != null) { - omemoManager.notifyOmemoMessageReceived(decrypted.getBody(), (Message) stanza, null, messageInfo); - } - - } else if (omemoMessage.isKeyTransportElement()) { - - CipherAndAuthTag cipherAndAuthTag = decryptTransportedOmemoKey(omemoManager, 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: " + LOGGER.log(Level.WARNING, "internal omemoMessageListener failed to establish a session for incoming OMEMO message: " + e.getMessage()); - - } catch (NoRawSessionException e) { - try { - LOGGER.log(Level.INFO, "Received message with invalid session from " + - senderDevice + ". Send RatchetUpdateMessage."); - service.sendOmemoRatchetUpdateMessage(omemoManager, 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()); - } } } } - OmemoCarbonCopyListener createOmemoCarbonCopyListener(OmemoManager omemoManager) { - return new OmemoCarbonCopyListener(omemoManager, this, omemoStanzaFilter); - } + @Override + public void onOmemoCarbonCopyReceived(CarbonExtension.Direction direction, + Message carbonCopy, + Message wrappingMessage, + final OmemoManager.KnownBareJidGuard managerGuard) + { + OmemoManager omemoManager = managerGuard.get(); - /** - * StanzaListener that listens for incoming OmemoElements that ARE sent in carbons. - */ - class OmemoCarbonCopyListener implements CarbonCopyReceivedListener { + final OmemoDevice senderDevice = getSender(managerGuard, carbonCopy); + Message decrypted; + MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(omemoManager.getConnection()); + OmemoElement omemoMessage = carbonCopy.getExtension(OmemoElement.ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); + OmemoMessageInformation messageInfo = new OmemoMessageInformation(); - private final OmemoManager omemoManager; - private final OmemoService service; - private final StanzaFilter filter; - - OmemoCarbonCopyListener(OmemoManager omemoManager, - OmemoService service, - StanzaFilter filter) { - this.omemoManager = omemoManager; - this.service = service; - this.filter = filter; + if (CarbonExtension.Direction.received.equals(direction)) { + messageInfo.setCarbon(OmemoMessageInformation.CARBON.RECV); + } else { + messageInfo.setCarbon(OmemoMessageInformation.CARBON.SENT); } - @Override - public void onCarbonCopyReceived(CarbonExtension.Direction direction, Message carbonCopy, Message wrappingMessage) { - if (filter.accept(carbonCopy)) { - final OmemoDevice senderDevice = getSender(omemoManager, carbonCopy); - Message decrypted; - MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(omemoManager.getConnection()); - OmemoElement omemoMessage = carbonCopy.getExtension(OmemoElement.ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); - OmemoMessageInformation messageInfo = new OmemoMessageInformation(); + try { + // Is it a MUC message... + if (isMucMessage(managerGuard, carbonCopy)) { - if (CarbonExtension.Direction.received.equals(direction)) { - messageInfo.setCarbon(OmemoMessageInformation.CARBON.RECV); - } else { - messageInfo.setCarbon(OmemoMessageInformation.CARBON.SENT); - } + MultiUserChat muc = mucm.getMultiUserChat(carbonCopy.getFrom().asEntityBareJidIfPossible()); + if (omemoMessage.isMessageElement()) { - try { - // Is it a MUC message... - if (isMucMessage(omemoManager, carbonCopy)) { - - MultiUserChat muc = mucm.getMultiUserChat(carbonCopy.getFrom().asEntityBareJidIfPossible()); - if (omemoMessage.isMessageElement()) { - - decrypted = processReceivingMessage(omemoManager, senderDevice, omemoMessage, messageInfo); - if (decrypted != null) { - omemoManager.notifyOmemoMucMessageReceived(muc, senderDevice.getJid(), decrypted.getBody(), - carbonCopy, wrappingMessage, messageInfo); - } - - } else if (omemoMessage.isKeyTransportElement()) { - - CipherAndAuthTag cipherAndAuthTag = decryptTransportedOmemoKey(omemoManager, 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 = service.processReceivingMessage(omemoManager, senderDevice, omemoMessage, messageInfo); - if (decrypted != null) { - omemoManager.notifyOmemoMessageReceived(decrypted.getBody(), carbonCopy, null, messageInfo); - } - - } else if (omemoMessage.isKeyTransportElement()) { - - CipherAndAuthTag cipherAndAuthTag = decryptTransportedOmemoKey(omemoManager, senderDevice, omemoMessage, messageInfo); - if (cipherAndAuthTag != null) { - omemoManager.notifyOmemoKeyTransportMessageReceived(cipherAndAuthTag, carbonCopy, null, messageInfo); - } - } + decrypted = processReceivingMessage(managerGuard, senderDevice, omemoMessage, messageInfo); + if (decrypted != null) { + omemoManager.notifyOmemoMucMessageReceived(muc, senderDevice.getJid(), decrypted.getBody(), + carbonCopy, wrappingMessage, 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."); - service.sendOmemoRatchetUpdateMessage(omemoManager, 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()); - } - } - }); + } 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 OmemoSessionManager + createOmemoSessionManager(OmemoManager.KnownBareJidGuard manager, + OmemoStore store); + + protected OmemoSessionManager + getOmemoSessionManager(OmemoManager.KnownBareJidGuard guard) { + OmemoSessionManager + sessionManager = sessionManagers.get(guard.get()); + if (sessionManager == null) { + sessionManager = createOmemoSessionManager(guard, omemoStore); + sessionManagers.put(guard.get(), sessionManager); + } + return sessionManager; + } } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoSessionManager.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoSessionManager.java new file mode 100644 index 000000000..9f83ee9bf --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoSessionManager.java @@ -0,0 +1,152 @@ +/** + * + * 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 java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; + +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.omemo.element.OmemoElement; +import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; +import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException; +import org.jivesoftware.smackx.omemo.exceptions.MultipleCryptoFailedException; +import org.jivesoftware.smackx.omemo.exceptions.NoRawSessionException; +import org.jivesoftware.smackx.omemo.exceptions.UntrustedOmemoIdentityException; +import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag; +import org.jivesoftware.smackx.omemo.internal.CiphertextTuple; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; + +public abstract class OmemoSessionManager { + private static final Logger LOGGER = Logger.getLogger(OmemoSessionManager.class.getName()); + + protected final OmemoManager.KnownBareJidGuard managerGuard; + protected final OmemoStore store; + + public OmemoSessionManager(OmemoManager.KnownBareJidGuard managerGuard, + OmemoStore store) { + this.managerGuard = managerGuard; + this.store = store; + } + + public abstract byte[] doubleRatchetDecrypt(OmemoDevice sender, byte[] encryptedKey) + throws CorruptedOmemoKeyException, NoRawSessionException, CryptoFailedException, + UntrustedOmemoIdentityException; + + public abstract CiphertextTuple doubleRatchetEncrypt(OmemoDevice recipient, byte[] messageKey); + + /** + * Try to decrypt the transported message key using the double ratchet session. + * + * @param element omemoElement + * @return tuple of cipher generated from the unpacked message key and the authtag + * @throws CryptoFailedException if decryption using the double ratchet fails + * @throws NoRawSessionException if we have no session, but the element was NOT a PreKeyMessage + */ + public CipherAndAuthTag retrieveMessageKeyAndAuthTag(OmemoDevice sender, OmemoElement element) throws CryptoFailedException, + NoRawSessionException { + int keyId = managerGuard.get().getDeviceId(); + byte[] unpackedKey = null; + List decryptExceptions = new ArrayList<>(); + List keys = element.getHeader().getKeys(); + // Find key with our ID. + for (OmemoElement.OmemoHeader.Key k : keys) { + if (k.getId() == keyId) { + try { + unpackedKey = doubleRatchetDecrypt(sender, k.getData()); + break; + } catch (CryptoFailedException e) { + // There might be multiple keys with our id, but we can only decrypt one. + // So we can't throw the exception, when decrypting the first duplicate which is not for us. + decryptExceptions.add(e); + } catch (CorruptedOmemoKeyException e) { + decryptExceptions.add(new CryptoFailedException(e)); + } catch (UntrustedOmemoIdentityException e) { + LOGGER.log(Level.WARNING, "Received message from " + sender + " contained unknown identityKey. Ignore message.", e); + } + } + } + + if (unpackedKey == null) { + if (!decryptExceptions.isEmpty()) { + throw MultipleCryptoFailedException.from(decryptExceptions); + } + + throw new CryptoFailedException("Transported key could not be decrypted, since no suitable message key " + + "was provided. Provides keys: " + keys); + } + + // Split in AES auth-tag and key + byte[] messageKey = new byte[16]; + byte[] authTag = null; + + if (unpackedKey.length == 32) { + authTag = new byte[16]; + // copy key part into messageKey + System.arraycopy(unpackedKey, 0, messageKey, 0, 16); + // copy tag part into authTag + System.arraycopy(unpackedKey, 16, authTag, 0,16); + } else if (element.isKeyTransportElement() && unpackedKey.length == 16) { + messageKey = unpackedKey; + } else { + throw new CryptoFailedException("MessageKey has wrong length: " + + unpackedKey.length + ". Probably legacy auth tag format."); + } + + return new CipherAndAuthTag(messageKey, element.getHeader().getIv(), authTag); + } + + /** + * Use the symmetric key in cipherAndAuthTag to decrypt the payload of the omemoMessage. + * The decrypted payload will be the body of the returned Message. + * + * @param element omemoElement containing a payload. + * @param cipherAndAuthTag cipher and authentication tag. + * @return Message containing the decrypted payload in its body. + * @throws CryptoFailedException + */ + public static Message decryptMessageElement(OmemoElement element, CipherAndAuthTag cipherAndAuthTag) throws CryptoFailedException { + if (!element.isMessageElement()) { + throw new IllegalArgumentException("decryptMessageElement cannot decrypt OmemoElement which is no MessageElement!"); + } + + if (cipherAndAuthTag.getAuthTag() == null || cipherAndAuthTag.getAuthTag().length != 16) { + throw new CryptoFailedException("AuthenticationTag is null or has wrong length: " + + (cipherAndAuthTag.getAuthTag() == null ? "null" : cipherAndAuthTag.getAuthTag().length)); + } + byte[] encryptedBody = new byte[element.getPayload().length + 16]; + byte[] payload = element.getPayload(); + System.arraycopy(payload, 0, encryptedBody, 0, payload.length); + System.arraycopy(cipherAndAuthTag.getAuthTag(), 0, encryptedBody, payload.length, 16); + + try { + String plaintext = new String(cipherAndAuthTag.getCipher().doFinal(encryptedBody), StringUtils.UTF8); + Message decrypted = new Message(); + decrypted.setBody(plaintext); + return decrypted; + + } catch (UnsupportedEncodingException | IllegalBlockSizeException | BadPaddingException e) { + throw new CryptoFailedException("decryptMessageElement could not decipher message body: " + + e.getMessage()); + } + } +} 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 1fe6fa279..ebcf6ac28 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 @@ -21,20 +21,17 @@ import static org.jivesoftware.smackx.omemo.util.OmemoConstants.TARGET_PRE_KEY_C import java.util.Date; import java.util.HashMap; import java.util.Map; -import java.util.WeakHashMap; +import java.util.SortedSet; +import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; -import org.jivesoftware.smack.roster.Roster; -import org.jivesoftware.smack.roster.RosterEntry; - import org.jivesoftware.smackx.omemo.element.OmemoBundleVAxolotlElement; 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.internal.OmemoDevice; -import org.jivesoftware.smackx.omemo.internal.OmemoSession; import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil; import org.jxmpp.jid.BareJid; @@ -56,9 +53,6 @@ import org.jxmpp.jid.BareJid; public abstract class OmemoStore { private static final Logger LOGGER = Logger.getLogger(OmemoStore.class.getName()); - private final WeakHashMap>> - omemoSessions = new WeakHashMap<>(); - /** * Create a new OmemoStore. */ @@ -67,26 +61,28 @@ public abstract class OmemoStore localDeviceIdsOf(BareJid localUser); /** * Check, if our freshly generated deviceId is available (unique) in our deviceList. * - * @param omemoManager omemoManager of our device. - * @param id our deviceId. + * @param userDevice our current device. + * @param id deviceId to check for. * @return true if list did not contain our id, else false */ - boolean isAvailableDeviceId(OmemoManager omemoManager, int id) { + boolean isAvailableDeviceId(OmemoDevice userDevice, int id) { LOGGER.log(Level.INFO, "Check if id " + id + " is available..."); // Lookup local cached device list - BareJid ownJid = omemoManager.getOwnJid(); - CachedDeviceList cachedDeviceList = loadCachedDeviceList(omemoManager, ownJid); + BareJid ownJid = userDevice.getJid(); + CachedDeviceList cachedDeviceList; + + cachedDeviceList = loadCachedDeviceList(userDevice, ownJid); if (cachedDeviceList == null) { cachedDeviceList = new CachedDeviceList(); @@ -95,32 +91,15 @@ public abstract class OmemoStore signedPreKeys = loadOmemoSignedPreKeys(userDevice); + if (signedPreKeys.size() == 0) { + T_SigPreKey newKey = generateOmemoSignedPreKey(idKeyPair, 1); + storeOmemoSignedPreKey(userDevice, 1, newKey); + } else { + int lastId = signedPreKeys.lastKey(); + T_SigPreKey newKey = generateOmemoSignedPreKey(idKeyPair, lastId + 1); + storeOmemoSignedPreKey(userDevice, lastId + 1, newKey); + } + + setDateOfLastSignedPreKeyRenewal(userDevice, new Date()); + removeOldSignedPreKeys(userDevice); } /** * Remove the oldest signedPreKey until there are only MAX_NUMBER_OF_STORED_SIGNED_PREKEYS left. * - * @param omemoManager omemoManager of our device. + * @param userDevice our OmemoDevice. */ - private void removeOldSignedPreKeys(OmemoManager omemoManager) { + private void removeOldSignedPreKeys(OmemoDevice userDevice) { if (OmemoConfiguration.getMaxNumberOfStoredSignedPreKeys() <= 0) { return; } - int currentId = loadCurrentSignedPreKeyId(omemoManager); - if (currentId == -1) currentId = 0; - HashMap signedPreKeys = loadOmemoSignedPreKeys(omemoManager); + TreeMap signedPreKeys = loadOmemoSignedPreKeys(userDevice); - for (int i : signedPreKeys.keySet()) { - if (i <= currentId - OmemoConfiguration.getMaxNumberOfStoredSignedPreKeys()) { - LOGGER.log(Level.INFO, "Remove signedPreKey " + i + "."); - removeOmemoSignedPreKey(omemoManager, i); - } + for (int i = 0; i < signedPreKeys.keySet().size() - OmemoConfiguration.getMaxNumberOfStoredSignedPreKeys(); i++) { + int keyId = signedPreKeys.firstKey(); + LOGGER.log(Level.INFO, "Remove signedPreKey " + keyId + "."); + removeOmemoSignedPreKey(userDevice, i); + signedPreKeys = loadOmemoSignedPreKeys(userDevice); } } @@ -182,26 +166,36 @@ public abstract class OmemoStore preKeys = loadOmemoPreKeys(omemoManager); + TreeMap signedPreKeys = loadOmemoSignedPreKeys(userDevice); + if (signedPreKeys.size() == 0) { + changeSignedPreKey(userDevice); + signedPreKeys = loadOmemoSignedPreKeys(userDevice); + } + + int currentSignedPreKeyId = signedPreKeys.lastKey(); + T_SigPreKey currentSignedPreKey = signedPreKeys.get(currentSignedPreKeyId); + + TreeMap preKeys = loadOmemoPreKeys(userDevice); int newKeysCount = TARGET_PRE_KEY_COUNT - preKeys.size(); + int startId = preKeys.size() == 0 ? 0 : preKeys.lastKey(); if (newKeysCount > 0) { - int lastPreKeyId = loadLastPreKeyId(omemoManager); - if (lastPreKeyId == -1) lastPreKeyId = 0; - HashMap newKeys = generateOmemoPreKeys(lastPreKeyId + 1, newKeysCount); - storeOmemoPreKeys(omemoManager, newKeys); + TreeMap newKeys = generateOmemoPreKeys(startId + 1, newKeysCount); + storeOmemoPreKeys(userDevice, newKeys); preKeys.putAll(newKeys); - storeLastPreKeyId(omemoManager, lastPreKeyId + newKeysCount); } return new OmemoBundleVAxolotlElement( @@ -213,159 +207,8 @@ public abstract class OmemoStore> - sessions = omemoSessions.get(omemoManager); - if (sessions == null) { - sessions = new HashMap<>(); - omemoSessions.put(omemoManager, sessions); - } - - // Sessions with our own devices - HashMap ourRawSessions = loadAllRawSessionsOf(omemoManager, omemoManager.getOwnJid()); - ourRawSessions.remove(omemoManager.getDeviceId()); //Just to make sure we have no session with ourselves... - sessions.putAll(createOmemoSessionsFromRawSessions(omemoManager, omemoManager.getOwnJid(), ourRawSessions)); - - // Sessions with contacts - for (RosterEntry rosterEntry : Roster.getInstanceFor(omemoManager.getConnection()).getEntries()) { - HashMap contactDevices = loadAllRawSessionsOf(omemoManager, rosterEntry.getJid().asBareJid()); - sessions.putAll(createOmemoSessionsFromRawSessions(omemoManager, rosterEntry.getJid().asBareJid(), contactDevices)); - } - } - - /** - * Forget all omemoSessions of the omemoManager from cache. - * This will not remove the sessions from persistent memory! - * - * @param omemoManager omemoManager we want to forget sessions from. - */ - void forgetOmemoSessions(OmemoManager omemoManager) { - omemoSessions.remove(omemoManager); - } - - /** - * Create a new concrete OmemoSession with a contact. - * - * @param omemoManager omemoManager of our device. - * @param device device to establish the session with - * @param identityKey identityKey of the device - * @return concrete OmemoSession - */ - private OmemoSession - createOmemoSession(OmemoManager omemoManager, OmemoDevice device, T_IdKey identityKey) { - return keyUtil().createOmemoSession(omemoManager, this, device, identityKey); - } - - /** - * Return the OmemoSession for the OmemoDevice. If there is no OmemoSession for the device yet, - * build one from local raw session material. - * - * @param omemoManager omemoManager of our device. - * @param device OmemoDevice - * @return OmemoSession - */ - public OmemoSession - getOmemoSessionOf(OmemoManager omemoManager, OmemoDevice device) { - - HashMap> - sessions = omemoSessions.get(omemoManager); - - if (sessions == null) { - sessions = new HashMap<>(); - omemoSessions.put(omemoManager, sessions); - } - - OmemoSession - session = sessions.get(device); - // No OmemoSession found - if (session == null) { - T_IdKey identityKey = null; - try { - identityKey = loadOmemoIdentityKey(omemoManager, device); - } catch (CorruptedOmemoKeyException e) { - LOGGER.log(Level.WARNING, "getOmemoSessionOf could not load identityKey of " + device + ": " + e.getMessage()); - } - - if (identityKey != null) { - session = createOmemoSession(omemoManager, device, identityKey); - - } else { - LOGGER.log(Level.INFO, "getOmemoSessionOf couldn't find an identityKey for " + device - + ". Initiate session without."); - session = createOmemoSession(omemoManager, device, null); - } - - sessions.put(device, session); - } - - if (session.getIdentityKey() == null) { - try { - session.setIdentityKey(loadOmemoIdentityKey(omemoManager, device)); - } catch (CorruptedOmemoKeyException e) { - LOGGER.log(Level.WARNING, "Can't update IdentityKey of " + device + ": " + e.getMessage()); - } - } - return session; - } - - /** - * Create OmemoSession objects for all T_Sess objects of the contact. - * The T_Sess objects will be wrapped inside a OmemoSession for every device of the contact. - * - * @param omemoManager omemoManager of our device. - * @param contact BareJid of the contact - * @param rawSessions HashMap of Integers (deviceIds) and T_Sess sessions. - * @return HashMap of OmemoContacts and OmemoSessions - */ - private HashMap> - createOmemoSessionsFromRawSessions(OmemoManager omemoManager, BareJid contact, HashMap rawSessions) { - - HashMap> - sessions = new HashMap<>(); - - for (Map.Entry sessionEntry : rawSessions.entrySet()) { - OmemoDevice omemoDevice = new OmemoDevice(contact, sessionEntry.getKey()); - try { - T_IdKey identityKey = loadOmemoIdentityKey(omemoManager, omemoDevice); - if (identityKey != null) { - sessions.put(omemoDevice, createOmemoSession(omemoManager, omemoDevice, identityKey)); - } else { - LOGGER.log(Level.WARNING, "IdentityKey of " + omemoDevice + " is null. Is this even possible at this point?"); - } - } catch (CorruptedOmemoKeyException e1) { - LOGGER.log(Level.WARNING, "buildOmemoSessionFor could not create a session for " + omemoDevice + - ": " + e1.getMessage()); - } - } - return sessions; - } - // *sigh* - /** - * Return the id of the last generated preKey. - * This is used to generate new preKeys without preKeyId collisions. - * - * @param omemoManager omemoManager of our device. - * @return id of the last preKey - */ - public abstract int loadLastPreKeyId(OmemoManager omemoManager); - - /** - * Store the id of the last preKey we generated. - * - * @param omemoManager omemoManager of our device. - * @param currentPreKeyId the id of the last generated PreKey - */ - public abstract void storeLastPreKeyId(OmemoManager omemoManager, int currentPreKeyId); - /** * Generate a new IdentityKeyPair. We should always have only one pair and usually keep this for a long time. * @@ -378,150 +221,89 @@ public abstract class OmemoStore generateOmemoPreKeys(int startId, int count) { + public TreeMap generateOmemoPreKeys(int startId, int count) { return keyUtil().generateOmemoPreKeys(startId, count); } /** * Load the preKey with id 'preKeyId' from storage. * - * @param omemoManager omemoManager of our device. + * @param userDevice our OmemoDevice. * @param preKeyId id of the key to be loaded * @return loaded preKey */ - public abstract T_PreKey loadOmemoPreKey(OmemoManager omemoManager, int preKeyId); + public abstract T_PreKey loadOmemoPreKey(OmemoDevice userDevice, int preKeyId); /** * Store a PreKey in storage. * - * @param omemoManager omemoManager of our device. + * @param userDevice our OmemoDevice. * @param preKeyId id of the key * @param preKey key */ - public abstract void storeOmemoPreKey(OmemoManager omemoManager, int preKeyId, T_PreKey preKey); + public abstract void storeOmemoPreKey(OmemoDevice userDevice, int preKeyId, T_PreKey preKey); /** * Store a whole bunch of preKeys. * - * @param omemoManager omemoManager of our device. + * @param userDevice our OmemoDevice. * @param preKeyHashMap HashMap of preKeys */ - public void storeOmemoPreKeys(OmemoManager omemoManager, HashMap preKeyHashMap) { - for (Map.Entry e : preKeyHashMap.entrySet()) { - storeOmemoPreKey(omemoManager, e.getKey(), e.getValue()); + public void storeOmemoPreKeys(OmemoDevice userDevice, TreeMap preKeyHashMap) { + for (Map.Entry entry : preKeyHashMap.entrySet()) { + storeOmemoPreKey(userDevice, entry.getKey(), entry.getValue()); } } @@ -569,52 +351,35 @@ public abstract class OmemoStore loadOmemoPreKeys(OmemoManager omemoManager); + public abstract TreeMap loadOmemoPreKeys(OmemoDevice userDevice); /** * Return the signedPreKey with the id 'singedPreKeyId'. * - * @param omemoManager omemoManager of our device. + * @param userDevice our OmemoDevice. * @param signedPreKeyId id of the key * @return key */ - public abstract T_SigPreKey loadOmemoSignedPreKey(OmemoManager omemoManager, int signedPreKeyId); + public abstract T_SigPreKey loadOmemoSignedPreKey(OmemoDevice userDevice, int signedPreKeyId); /** * Load all our signed PreKeys. * - * @param omemoManager omemoManager of our device. + * @param userDevice our OmemoDevice. * @return HashMap of our singedPreKeys */ - public abstract HashMap loadOmemoSignedPreKeys(OmemoManager omemoManager); + public abstract TreeMap loadOmemoSignedPreKeys(OmemoDevice userDevice); /** * Generate a new signed preKey. @@ -624,105 +389,109 @@ public abstract class OmemoStore loadAllRawSessionsOf(OmemoManager omemoManager, BareJid contact); + public abstract HashMap loadAllRawSessionsOf(OmemoDevice userDevice, BareJid contact); /** * Store a crypto-lib specific session to storage. * - * @param omemoManager omemoManager of our device. - * @param device OmemoDevice whose session we want to store + * @param userDevice our OmemoDevice. + * @param contactsDevice OmemoDevice whose session we want to store * @param session session */ - public abstract void storeRawSession(OmemoManager omemoManager, OmemoDevice device, T_Sess session); + public abstract void storeRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice, T_Sess session); /** * Remove a crypto-lib specific session from storage. * - * @param omemoManager omemoManager of our device. - * @param device device whose session we want to delete + * @param userDevice our OmemoDevice. + * @param contactsDevice device whose session we want to delete */ - public abstract void removeRawSession(OmemoManager omemoManager, OmemoDevice device); + public abstract void removeRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice); /** * Remove all crypto-lib specific session of a contact. * - * @param omemoManager omemoManager of our device. + * @param userDevice our OmemoDevice. * @param contact BareJid of the contact */ - public abstract void removeAllRawSessionsOf(OmemoManager omemoManager, BareJid contact); + public abstract void removeAllRawSessionsOf(OmemoDevice userDevice, BareJid contact); /** * Return true, if we have a session with the device, otherwise false. * Hint for Signal: Do not try 'return getSession() != null' since this will create a new session. * - * @param omemoManager omemoManager of our device. - * @param device device + * @param userDevice our OmemoDevice. + * @param contactsDevice device * @return true if we have session, otherwise false */ - public abstract boolean containsRawSession(OmemoManager omemoManager, OmemoDevice device); + public abstract boolean containsRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice); /** * Load a list of deviceIds from contact 'contact' from the local cache. * - * @param omemoManager omemoManager of our device. + * @param userDevice our OmemoDevice. * @param contact contact we want to get the deviceList of * @return CachedDeviceList of the contact */ - public abstract CachedDeviceList loadCachedDeviceList(OmemoManager omemoManager, BareJid contact); + public abstract CachedDeviceList loadCachedDeviceList(OmemoDevice userDevice, BareJid contact); /** * Store the DeviceList of the contact in local storage. * See this as a cache. * - * @param omemoManager omemoManager of our device. + * @param userDevice our OmemoDevice. * @param contact Contact - * @param deviceList list of the contacts devices' ids. + * @param contactsDeviceList list of the contacts devices' ids. */ - public abstract void storeCachedDeviceList(OmemoManager omemoManager, BareJid contact, CachedDeviceList deviceList); + public abstract void storeCachedDeviceList(OmemoDevice userDevice, + BareJid contact, + CachedDeviceList contactsDeviceList); /** * Delete this device's IdentityKey, PreKeys, SignedPreKeys and Sessions. * - * @param omemoManager omemoManager of our device. + * @param userDevice our OmemoDevice. */ - public abstract void purgeOwnDeviceKeys(OmemoManager omemoManager); + public abstract void purgeOwnDeviceKeys(OmemoDevice userDevice); /** * Return a concrete KeyUtil object that we can use as a utility to create keys etc. @@ -734,58 +503,43 @@ public abstract class OmemoStore exceptions = new ArrayList<>(); + + public MultipleIOException(IOException... exceptions) { + this.exceptions.addAll(Arrays.asList(exceptions)); + } + + public void addException(IOException e) { + exceptions.add(e); + } + + public ArrayList getExceptions() { + return exceptions; + } +} 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 new file mode 100644 index 000000000..17b171b95 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UntrustedOmemoIdentityException.java @@ -0,0 +1,68 @@ +/** + * + * 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.exceptions; + +import org.jivesoftware.smackx.omemo.OmemoFingerprint; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; + +/** + * Exception that gets thrown when we try to decrypt a message which contains an identityKey that differs from the one + * we previously trusted. + */ +public class UntrustedOmemoIdentityException extends Exception { + + private static final long serialVersionUID = 1L; + private final OmemoDevice device; + private final OmemoFingerprint trustedKey, untrustedKey; + + /** + * Constructor. + * @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. + */ + public UntrustedOmemoIdentityException(OmemoDevice device, OmemoFingerprint fpTrusted, OmemoFingerprint fpUntrusted) { + super(); + this.device = device; + this.trustedKey = fpTrusted; + this.untrustedKey = fpUntrusted; + } + + /** + * Return the device which sent the message. + * @return omemoDevice. + */ + public OmemoDevice getDevice() { + return device; + } + + /** + * Return the fingerprint of the key we expected. + * @return + */ + public OmemoFingerprint getTrustedFingerprint() { + return trustedKey; + } + + /** + * Return the fingerprint of the unexpected untrusted key. + * @return + */ + public OmemoFingerprint getUntrustedFingerprint() { + return untrustedKey; + } +} 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/CachedDeviceList.java index 41662e1ff..f518881cc 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/CachedDeviceList.java @@ -43,6 +43,16 @@ public class CachedDeviceList implements Serializable { this.inactiveDevices = new HashSet<>(); } + public CachedDeviceList(Set activeDevices, Set inactiveDevices) { + this(); + this.activeDevices.addAll(activeDevices); + this.inactiveDevices.addAll(inactiveDevices); + } + + public CachedDeviceList(CachedDeviceList original) { + this(original.getActiveDevices(), original.getInactiveDevices()); + } + /** * Returns all active devices. * Active devices are all devices that were in the latest DeviceList update. diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoMessageInformation.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoMessageInformation.java index ada9cc2b6..833ea39b8 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoMessageInformation.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoMessageInformation.java @@ -16,6 +16,8 @@ */ package org.jivesoftware.smackx.omemo.internal; +import org.jivesoftware.smackx.omemo.OmemoFingerprint; + /** * Class that contains information about a decrypted message (eg. which key was used, if it was a carbon...). * @@ -23,7 +25,7 @@ package org.jivesoftware.smackx.omemo.internal; */ public class OmemoMessageInformation { private boolean isOmemoMessage; - private IdentityKeyWrapper senderIdentityKey; + private OmemoFingerprint senderFingerprint; private OmemoDevice senderDevice; private CARBON carbon = CARBON.NONE; @@ -37,12 +39,12 @@ public class OmemoMessageInformation { /** * Creates a new OmemoMessageInformation object. Its assumed, that this is about an OMEMO message. * - * @param senderIdentityKey identityKey of the sender device + * @param senderFingerprint fingerprint of the identityKey of the sender device * @param senderDevice device that sent the message * @param carbon Carbon type */ - public OmemoMessageInformation(IdentityKeyWrapper senderIdentityKey, OmemoDevice senderDevice, CARBON carbon) { - this.senderIdentityKey = senderIdentityKey; + public OmemoMessageInformation(OmemoFingerprint senderFingerprint, OmemoDevice senderDevice, CARBON carbon) { + this.senderFingerprint = senderFingerprint; this.senderDevice = senderDevice; this.carbon = carbon; this.isOmemoMessage = true; @@ -51,13 +53,13 @@ public class OmemoMessageInformation { /** * Create a new OmemoMessageInformation. * - * @param senderIdentityKey identityKey of the sender device + * @param senderFingerprint fingerprint of the identityKey of the sender device * @param senderDevice device that sent the message * @param carbon Carbon type * @param omemo is this an omemo message? */ - public OmemoMessageInformation(IdentityKeyWrapper senderIdentityKey, OmemoDevice senderDevice, CARBON carbon, boolean omemo) { - this(senderIdentityKey, senderDevice, carbon); + public OmemoMessageInformation(OmemoFingerprint senderFingerprint, OmemoDevice senderDevice, CARBON carbon, boolean omemo) { + this(senderFingerprint, senderDevice, carbon); this.isOmemoMessage = omemo; } @@ -66,17 +68,17 @@ public class OmemoMessageInformation { * * @return identityKey */ - public IdentityKeyWrapper getSenderIdentityKey() { - return senderIdentityKey; + public OmemoFingerprint getSenderFingerprint() { + return senderFingerprint; } /** * Set the sender devices identityKey. * - * @param senderIdentityKey identityKey + * @param senderFingerprint fingerprint of the senders identityKey. */ - public void setSenderIdentityKey(IdentityKeyWrapper senderIdentityKey) { - this.senderIdentityKey = senderIdentityKey; + public void setSenderFingerprint(OmemoFingerprint senderFingerprint) { + this.senderFingerprint = senderFingerprint; } /** diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoSession.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoSession.java deleted file mode 100644 index 119137387..000000000 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoSession.java +++ /dev/null @@ -1,266 +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 java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.List; - -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; - -import org.jivesoftware.smack.packet.Message; -import org.jivesoftware.smack.util.StringUtils; - -import org.jivesoftware.smackx.omemo.OmemoFingerprint; -import org.jivesoftware.smackx.omemo.OmemoManager; -import org.jivesoftware.smackx.omemo.OmemoStore; -import org.jivesoftware.smackx.omemo.element.OmemoElement; -import org.jivesoftware.smackx.omemo.element.OmemoElement.OmemoHeader.Key; -import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException; -import org.jivesoftware.smackx.omemo.exceptions.MultipleCryptoFailedException; -import org.jivesoftware.smackx.omemo.exceptions.NoRawSessionException; - -/** - * This class represents a OMEMO session between us and another device. - * - * @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 OmemoSession { - - protected final T_Ciph cipher; - protected final OmemoStore omemoStore; - protected final OmemoDevice remoteDevice; - protected final OmemoManager omemoManager; - protected T_IdKey identityKey; - protected int preKeyId = -1; - - /** - * Constructor used when we establish the session. - * - * @param omemoManager OmemoManager of our device - * @param omemoStore OmemoStore where we want to store the session and get key information from - * @param remoteDevice the OmemoDevice we want to establish the session with - * @param identityKey identityKey of the recipient - */ - public OmemoSession(OmemoManager omemoManager, - OmemoStore omemoStore, - OmemoDevice remoteDevice, T_IdKey identityKey) { - this(omemoManager, omemoStore, remoteDevice); - this.identityKey = identityKey; - } - - /** - * Another constructor used when they establish the session with us. - * - * @param omemoManager OmemoManager of our device - * @param omemoStore OmemoStore we want to store the session and their key in - * @param remoteDevice identityKey of the partner - */ - public OmemoSession(OmemoManager omemoManager, OmemoStore omemoStore, - OmemoDevice remoteDevice) { - this.omemoManager = omemoManager; - this.omemoStore = omemoStore; - this.remoteDevice = remoteDevice; - this.cipher = createCipher(remoteDevice); - } - - /** - * Try to decrypt the transported message key using the double ratchet session. - * - * @param element omemoElement - * @param keyId our keyId - * @return tuple of cipher generated from the unpacked message key and the authtag - * @throws CryptoFailedException if decryption using the double ratchet fails - * @throws NoRawSessionException if we have no session, but the element was NOT a PreKeyMessage - */ - public CipherAndAuthTag decryptTransportedKey(OmemoElement element, int keyId) throws CryptoFailedException, - NoRawSessionException { - byte[] unpackedKey = null; - List decryptExceptions = new ArrayList<>(); - List keys = element.getHeader().getKeys(); - // Find key with our ID. - for (OmemoElement.OmemoHeader.Key k : keys) { - if (k.getId() == keyId) { - try { - unpackedKey = decryptMessageKey(k.getData()); - break; - } catch (CryptoFailedException e) { - // There might be multiple keys with our id, but we can only decrypt one. - // So we can't throw the exception, when decrypting the first duplicate which is not for us. - decryptExceptions.add(e); - } - } - } - - if (unpackedKey == null) { - if (!decryptExceptions.isEmpty()) { - throw MultipleCryptoFailedException.from(decryptExceptions); - } - - throw new CryptoFailedException("Transported key could not be decrypted, since no provided message key. Provides keys: " + keys); - } - - byte[] messageKey = new byte[16]; - byte[] authTag = null; - - if (unpackedKey.length == 32) { - authTag = new byte[16]; - // copy key part into messageKey - System.arraycopy(unpackedKey, 0, messageKey, 0, 16); - // copy tag part into authTag - System.arraycopy(unpackedKey, 16, authTag, 0,16); - } else if (element.isKeyTransportElement() && unpackedKey.length == 16) { - messageKey = unpackedKey; - } else { - throw new CryptoFailedException("MessageKey has wrong length: " - + unpackedKey.length + ". Probably legacy auth tag format."); - } - - return new CipherAndAuthTag(messageKey, element.getHeader().getIv(), authTag); - } - - /** - * Use the symmetric key in cipherAndAuthTag to decrypt the payload of the omemoMessage. - * The decrypted payload will be the body of the returned Message. - * - * @param element omemoElement containing a payload. - * @param cipherAndAuthTag cipher and authentication tag. - * @return Message containing the decrypted payload in its body. - * @throws CryptoFailedException - */ - public static Message decryptMessageElement(OmemoElement element, CipherAndAuthTag cipherAndAuthTag) throws CryptoFailedException { - if (!element.isMessageElement()) { - throw new IllegalArgumentException("decryptMessageElement cannot decrypt OmemoElement which is no MessageElement!"); - } - - if (cipherAndAuthTag.getAuthTag() == null || cipherAndAuthTag.getAuthTag().length != 16) { - throw new CryptoFailedException("AuthenticationTag is null or has wrong length: " - + (cipherAndAuthTag.getAuthTag() == null ? "null" : cipherAndAuthTag.getAuthTag().length)); - } - byte[] encryptedBody = new byte[element.getPayload().length + 16]; - byte[] payload = element.getPayload(); - System.arraycopy(payload, 0, encryptedBody, 0, payload.length); - System.arraycopy(cipherAndAuthTag.getAuthTag(), 0, encryptedBody, payload.length, 16); - - try { - String plaintext = new String(cipherAndAuthTag.getCipher().doFinal(encryptedBody), StringUtils.UTF8); - Message decrypted = new Message(); - decrypted.setBody(plaintext); - return decrypted; - - } catch (UnsupportedEncodingException | IllegalBlockSizeException | BadPaddingException e) { - throw new CryptoFailedException("decryptMessageElement could not decipher message body: " - + e.getMessage()); - } - } - - /** - * Try to decrypt the message. - * First decrypt the message key using our session with the sender. - * Second use the decrypted key to decrypt the message. - * The decrypted content of the 'encrypted'-element becomes the body of the clear text message. - * - * @param element OmemoElement - * @param keyId the key we want to decrypt (usually our own device id) - * @return message as plaintext - * @throws CryptoFailedException - * @throws NoRawSessionException - */ - // TODO find solution for what we actually want to decrypt (String, Message, List...) - public Message decryptMessageElement(OmemoElement element, int keyId) throws CryptoFailedException, NoRawSessionException { - if (!element.isMessageElement()) { - throw new IllegalArgumentException("OmemoElement is not a messageElement!"); - } - - CipherAndAuthTag cipherAndAuthTag = decryptTransportedKey(element, keyId); - return decryptMessageElement(element, cipherAndAuthTag); - } - - /** - * Create a new SessionCipher used to encrypt/decrypt keys. The cipher typically implements the ratchet and KDF-chains. - * - * @param contact OmemoDevice - * @return SessionCipher - */ - public abstract T_Ciph createCipher(OmemoDevice contact); - - /** - * Get the id of the preKey used to establish the session. - * - * @return id - */ - public int getPreKeyId() { - return this.preKeyId; - } - - /** - * Encrypt a message key for the recipient. This key can be deciphered by the recipient with its corresponding - * session cipher. The key is then used to decipher the message. - * - * @param messageKey serialized key to encrypt - * @return A CiphertextTuple containing the ciphertext and the messageType - * @throws CryptoFailedException - */ - public abstract CiphertextTuple encryptMessageKey(byte[] messageKey) throws CryptoFailedException; - - /** - * Decrypt a messageKey using our sessionCipher. We can use that key to decipher the actual message. - * Same as encryptMessageKey, just the other way round. - * - * @param encryptedKey encrypted key - * @return serialized decrypted key or null - * @throws CryptoFailedException when decryption fails. - * @throws NoRawSessionException when no session was found in the double ratchet library - */ - public abstract byte[] decryptMessageKey(byte[] encryptedKey) throws CryptoFailedException, NoRawSessionException; - - /** - * Return the identityKey of the session. - * - * @return identityKey - */ - public T_IdKey getIdentityKey() { - return identityKey; - } - - /** - * Set the identityKey of the remote device. - * @param identityKey identityKey - */ - public void setIdentityKey(T_IdKey identityKey) { - this.identityKey = identityKey; - } - - /** - * Return the fingerprint of the contacts identityKey. - * - * @return fingerprint or null - */ - public OmemoFingerprint getFingerprint() { - return (this.identityKey != null ? omemoStore.keyUtil().getFingerprint(this.identityKey) : null); - } -} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/listener/OmemoCarbonCopyStanzaReceivedListener.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/listener/OmemoCarbonCopyStanzaReceivedListener.java new file mode 100644 index 000000000..e5175165b --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/listener/OmemoCarbonCopyStanzaReceivedListener.java @@ -0,0 +1,29 @@ +/** + * + * 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.listener; + +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smackx.carbons.packet.CarbonExtension; +import org.jivesoftware.smackx.omemo.OmemoManager; + +/** + * Internal listener for OMEMO encrypted carbon copies. + */ +public interface OmemoCarbonCopyStanzaReceivedListener { + + void onOmemoCarbonCopyReceived(CarbonExtension.Direction direction, Message carbonCopy, Message wrappingMessage, OmemoManager.KnownBareJidGuard omemoManager); +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/listener/OmemoMessageStanzaReceivedListener.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/listener/OmemoMessageStanzaReceivedListener.java new file mode 100644 index 000000000..096e885ab --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/listener/OmemoMessageStanzaReceivedListener.java @@ -0,0 +1,25 @@ +/** + * + * 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.listener; + +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smackx.omemo.OmemoManager; + +public interface OmemoMessageStanzaReceivedListener { + + void onOmemoMessageStanzaReceived(Stanza stanza, OmemoManager.KnownBareJidGuard omemoManager); +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/IdentityKeyWrapper.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/listener/package-info.java similarity index 66% rename from smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/IdentityKeyWrapper.java rename to smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/listener/package-info.java index c89bd33f4..849ceb3f5 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/IdentityKeyWrapper.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/listener/package-info.java @@ -14,23 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jivesoftware.smackx.omemo.internal; - /** - * Wrapper for IdentityKey objects. + * StanzaListeners used for internal purposes. * * @author Paul Schaub + * @see XEP-0384: OMEMO */ -public class IdentityKeyWrapper { - private final Object identityKey; - - public IdentityKeyWrapper(Object wrapped) { - identityKey = wrapped; - } - - public Object getIdentityKey() { - return identityKey; - } - - -} +package org.jivesoftware.smackx.omemo.internal.listener; diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/TrustCallback.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/TrustCallback.java new file mode 100644 index 000000000..ff4e088e1 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/TrustCallback.java @@ -0,0 +1,28 @@ +/** + * + * 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.trust; + +import org.jivesoftware.smackx.omemo.OmemoFingerprint; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; + +public interface TrustCallback { + + TrustState getTrust(OmemoDevice device, OmemoFingerprint fingerprint); + + void setTrust(OmemoDevice device, OmemoFingerprint fingerprint, TrustState state); + +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/TrustState.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/TrustState.java new file mode 100644 index 000000000..def2216a1 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/TrustState.java @@ -0,0 +1,21 @@ +/** + * + * 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.trust; + +public enum TrustState { + undecided, untrusted, trusted +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/package-info.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/package-info.java new file mode 100644 index 000000000..1676ea869 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/package-info.java @@ -0,0 +1,23 @@ +/** + * + * 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. + */ +/** + * Callbacks used to pass trust decisions up to the client. + * + * @author Paul Schaub + * @see XEP-0384: OMEMO + */ +package org.jivesoftware.smackx.omemo.trust; 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 e7d091dab..6958458b6 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 @@ -19,16 +19,14 @@ package org.jivesoftware.smackx.omemo.util; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smackx.omemo.OmemoFingerprint; -import org.jivesoftware.smackx.omemo.OmemoManager; -import org.jivesoftware.smackx.omemo.OmemoStore; import org.jivesoftware.smackx.omemo.element.OmemoBundleVAxolotlElement; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; -import org.jivesoftware.smackx.omemo.internal.OmemoSession; import org.jxmpp.stringprep.XmppStringprepException; @@ -207,7 +205,7 @@ public abstract class OmemoKeyUtil generateOmemoPreKeys(int startId, int count); + public abstract TreeMap generateOmemoPreKeys(int startId, int count); /** * Generate a new signed preKey. @@ -330,7 +328,7 @@ public abstract class OmemoKeyUtil preKeyPublisKeysForBundle(HashMap preKeyHashMap) { + public HashMap preKeyPublisKeysForBundle(TreeMap preKeyHashMap) { HashMap out = new HashMap<>(); for (Map.Entry e : preKeyHashMap.entrySet()) { out.put(e.getKey(), preKeyForBundle(e.getValue())); @@ -352,32 +350,14 @@ public abstract class OmemoKeyUtil - createOmemoSession(OmemoManager omemoManager, OmemoStore omemoStore, - OmemoDevice from); - - /** - * Create a new concrete OmemoSession with a contact. - * - * @param omemoManager omemoManager of our device. - * @param omemoStore omemoStore - * @param device device to establish the session with - * @param identityKey identityKey of the device - * @return concrete OmemoSession - */ - public abstract OmemoSession - createOmemoSession(OmemoManager omemoManager, OmemoStore omemoStore, - OmemoDevice device, T_IdKey identityKey); + public abstract OmemoFingerprint getFingerprintOfIdentityKeyPair(T_IdKeyPair identityKeyPair); /** * Deserialize a raw OMEMO Session from bytes. 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 066a73c6a..572f530ab 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 @@ -28,7 +28,6 @@ import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SecureRandom; import java.util.ArrayList; - import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; @@ -38,17 +37,18 @@ 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.OmemoFingerprint; import org.jivesoftware.smackx.omemo.OmemoManager; -import org.jivesoftware.smackx.omemo.OmemoStore; +import org.jivesoftware.smackx.omemo.OmemoSessionManager; import org.jivesoftware.smackx.omemo.element.OmemoVAxolotlElement; +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.UndecidedOmemoIdentityException; import org.jivesoftware.smackx.omemo.internal.CiphertextTuple; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; -import org.jivesoftware.smackx.omemo.internal.OmemoSession; /** * Class used to build OMEMO messages. @@ -65,8 +65,8 @@ import org.jivesoftware.smackx.omemo.internal.OmemoSession; * @author Paul Schaub */ public class OmemoMessageBuilder { - private final OmemoStore omemoStore; - private final OmemoManager omemoManager; + private final OmemoSessionManager sessionManager; + private final OmemoManager.KnownBareJidGuard managerGuard; private byte[] messageKey = generateKey(); private byte[] initializationVector = generateIv(); @@ -77,8 +77,8 @@ public class OmemoMessageBuilder omemoStore, + public OmemoMessageBuilder(OmemoManager.KnownBareJidGuard managerGuard, + OmemoSessionManager sessionManager, byte[] aesKey, byte[] iv) throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, NoSuchProviderException, InvalidAlgorithmParameterException { - this.omemoStore = omemoStore; - this.omemoManager = omemoManager; + this.managerGuard = managerGuard; + this.sessionManager = sessionManager; this.messageKey = aesKey; this.initializationVector = iv; } @@ -104,9 +104,9 @@ public class OmemoMessageBuilder omemoStore, String message) + public OmemoMessageBuilder(OmemoManager.KnownBareJidGuard managerGuard, + OmemoSessionManager sessionManager, + String message) throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, NoSuchProviderException, InvalidAlgorithmParameterException { - this.omemoManager = omemoManager; - this.omemoStore = omemoStore; + this.managerGuard = managerGuard; + this.sessionManager = sessionManager; this.setMessage(message); } @@ -175,34 +176,40 @@ public class OmemoMessageBuilder session = - omemoStore.getOmemoSessionOf(omemoManager, device); + public void addRecipient(OmemoDevice contactsDevice, boolean ignoreTrust) throws + CryptoFailedException, UndecidedOmemoIdentityException, CorruptedOmemoKeyException, + CannotEstablishOmemoSessionException { - if (session != null) { - if (!ignoreTrust && !omemoStore.isDecidedOmemoIdentity(omemoManager, device, session.getIdentityKey())) { - // Warn user of undecided device - throw new UndecidedOmemoIdentityException(device); - } + OmemoFingerprint fingerprint; + try { + fingerprint = managerGuard.get().getFingerprint(contactsDevice); + } catch (SmackException.NotLoggedInException e) { + throw new AssertionError("This should never happen."); + } - if (!ignoreTrust && omemoStore.isTrustedOmemoIdentity(omemoManager, device, session.getIdentityKey())) { - // Encrypt key and save to header - CiphertextTuple encryptedKey = session.encryptMessageKey(messageKey); - keys.add(new OmemoVAxolotlElement.OmemoHeader.Key(encryptedKey.getCiphertext(), device.getDeviceId(), encryptedKey.isPreKeyMessage())); - } + if (!ignoreTrust && !managerGuard.get().isDecidedOmemoIdentity(contactsDevice, fingerprint)) { + // Warn user of undecided device + throw new UndecidedOmemoIdentityException(contactsDevice); + } + + if (ignoreTrust || managerGuard.get().isTrustedOmemoIdentity(contactsDevice, fingerprint)) { + // Encrypt key and save to header + CiphertextTuple encryptedKey = sessionManager.doubleRatchetEncrypt(contactsDevice, messageKey); + keys.add(new OmemoVAxolotlElement.OmemoHeader.Key(encryptedKey.getCiphertext(), contactsDevice.getDeviceId(), encryptedKey.isPreKeyMessage())); } } @@ -213,7 +220,7 @@ public class OmemoMessageBuilder +extends SmackTestSuite { + + protected OmemoKeyUtil keyUtil; + + public OmemoKeyUtilTest(OmemoKeyUtil keyUtil) { + this.keyUtil = keyUtil; + } + + public void test() { + + } + + @Test + public void identityKeyPairFromNullBytesReturnsNull() throws CorruptedOmemoKeyException { + assertNull(keyUtil.identityKeyPairFromBytes(null)); + } + + @Test + public void identityKeyFromNullBytesReturnsNull() throws CorruptedOmemoKeyException { + assertNull(keyUtil.identityKeyFromBytes(null)); + } + + @Test + public void preKeyFromNullBytesReturnsNull() throws IOException { + assertNull(keyUtil.preKeyFromBytes(null)); + } + + @Test + public void ellipticCurvePublicKeyFromNullBytesReturnsNull() throws CorruptedOmemoKeyException { + assertNull(keyUtil.ellipticCurvePublicKeyFromBytes(null)); + } + + @Test + public void signedPreKeyFromNullBytesReturnsNull() throws IOException { + assertNull(keyUtil.signedPreKeyFromBytes(null)); + } + + @Test + public void signedPreKeyPublicFromNullBytesReturnsNull() throws CorruptedOmemoKeyException { + assertNull(keyUtil.signedPreKeyPublicFromBytes(null)); + } + + @Test + public void rawSessionFromNullBytesReturnsNull() throws IOException { + assertNull(keyUtil.rawSessionFromBytes(null)); + } + + @Test + public void parsedBundlesDoNotContainNullValues() throws Exception { + OmemoDevice device = new OmemoDevice(JidCreate.bareFrom("test@test.tld"), 1337); + String bundleXML = "BYKq4s6+plpjAAuCnGO+YFpLP71tMUPgj9ZZmkMSko4ETYMUtzWpc5USMCXStUrCbXFeHTOX3xkBTrU6/MuE/16s4ql1vRN0+JLtYPgZtTm3hb2dHwLA5BUzeTRGjSZwig==BY3AYRje4YBA6W4uuAXYNKzbII/UJbw7qE8kWHI15etiBbzKUJbnqYW19h2dWCyLMbYEpF8r477Ukv9wqMayERQEBeit9Pz31QxklV69BZ0qIxktnUO5TYAgHacFWDYsDnhdBSlbqC8nOpG4TMqvZmCPr6TCPNRcuuoO8Fp2rLGwLFYzBWYsJTsJLtmOgChiz4ilS/cgoEptnfv87tuvq5VpZFV+BY/xq67AkvgIaUO1NbROJeG+r6CcpzByoKvpIaPYyaw/BVRkNWaoocepKEqah95F1DG/uTE1iNEgIZ40wnGd39g/BWMI2ivYBIziOiJsnxJHmiUNN1GcPs3vP/E4vn7hu10BBd7QSMnxJULdKHohRhxUW/DVVRhdaY9SSX16j+CJF8YdBSgQ8NXIkq9fZrtYEdV6qkz5EK7YXVRAiIAFaaDuwUZHBf9Q2r9P4P15GvIiaHWTEU5gLyk/A8ys6Pzz01pLuu9ZBVU6/JKCXqaNa4ApbPFxYExxKuQKuRctk8a1brNcRbJUBfFGHormRpE7x92Eo3IcZcyhxa1//lKyLCNLdlL5Gg1PBd/Je4PdYYJy+6gXrcy7CRqDxBHVgPKN9AOiGxpRX7gkBVtdD2xyJnxPYNJPCT7sYdCXAoD7pMLgf27Dj0dU9vU3BX41BkuSp/qGYDlEzsuE5Tlia1IjzmYsiZRcjAp8D2tqBRY9W9zotVhB7DV2s/I7RYFzzg/Rok0AjU6ODs+iBUtFBb4DW8bURvMuh21PzHGqQlQm6eaI2S4pPLD482yV65IUBSFOrkueqrJDACBIUDpaYiOV51fUuFit4dGYYkvV3StyBT402/OG5FLw2jt+cpYepykpoRVPbI+bWcUx42CqSlwxBeMDEcZ23jnocObmU+esIhAGUvEVCyeiqq+n29Ex38FwBYUDDsKjORZTuZ1ImIIcwhL2peK1K+kTS+QhqCufoIRJBcC/x3Q3zZKv2DKaZlTWpM2Qzg8UogXJ2MmyKQzNI6RJBad8sDrpoVujQTlenKtSfc7JbWlXq5MGDb71q+5DCo88BYlAA5ZyhfiKLFE/U6lufiokNmQjGYP5eMCKhZsuv9BXBbK+LNKsLizmJtd6iEd+QUDdBEgmxIylkTyAS2gxghEHBZ+9oZGHWkRJXPnzT54+UPhQY0vpUdzGltMvneZHqfMLBRRXzcCruX3Gb+kbBodA9OaHcEx/XYT3dpwKK6hx8mYfBTgeei2VCoKk3dBG0FP45UjDoJBV9wQiDn2pW9xwTMkSBZHFWmtevdvuYAbMOpQ7nAAdv+oJxY+A7GFi2jU/PftPBRn4+vobphaBHjOl4gYrVIPHEGMvsn63pbAVgdx69XQRBaUv1tnXFTkJ2jiFT0vlUjH9upOASZHN4EmXGX9n9UAcBU+13hmRR2dkuIqBKxItFFaIdnaAti3beOnmezR+/VtWBbmmB27Q1B72qhxxW++CyrNHCy0UwiAOdkKOBUKCkyZ0BemHNdH5VhufFn9n4qu6e1pVyYjn47ivQy1xHmQL6eh1BSnbvvDgCRGpu/SkapLOe66hxxeJKw7U160d6vxUkYM6BVaUjCB5ZhooG2umXa4CVu6BjmNDkkUUM19pzangbfEUBZD+gzgJ4jXxjfJtMMuWvHJmr/f5vJ+u7vhH4y7KjYM3BW3zmMGSm5jhMTpSjT8u0dsDnK2pXMRVPTr08xmh7vhJBSE7XKChX5zcJrJtoBTAVtUL/gB9iFFb2rE0fKj2b2UQBXVao8jlCDAeOMr4thch7T8Gl+7h2OhcihFAOqkmzf9MBdPqg07COBd2OInhQqc1yCZbixd1CpEbpcG9NjbxGwRUBTzmunAmQX61OaIlTYdfWQU3VtkVXdiLCcegUIOzg/hwBfRxST8negQ2vxMQLufVXdOM/U5IPHCETGsV2uhkdz4HBTlbSzgCQwwkjD/EbEWfcontJobg9u5Odqn/x9QAmu1jBczhPhwuz7KQJW8KICaOgQ0J/+baVwptpqxOtwjFphQhBc0xu3QbrVWQDlIh2VdrfP/GowUF8CN5Q3iCpuabLhIKBaiPlpNAjMviSv3n3tJ+8vAQS7IORAuYJz8pZ/k7CdthBUDuxRt02ajimnvq8BeBQEies6TNDs/E0uvZ7aLHBJAIBQFVdgojx8r3LOjJbAk3CWhtCxU2DxFQHyoBewfJyTk9BdMJqMd7Rkiu9tcmcG1TDU0XKoEHJYPK3FBfRScvqlBrBSyfLhWFdFkUcyczOpIgwo5M5JrJEWWLBJLrOYZHlkYbBeM0hOY/zjvwbGgFHTLAKplV2A57bKzJOd0qhkc22zk1BRIohETGkJNWCDmZjnq7kgawbPWjjBaok4QMTSynT3QcBZykP11RVcyQQmYD+gxGYzL1aQlKce3+EzPZDunh0ftOBYwQvPfzvB97+QcBfCR2YW1EOIDw8KW5FrGmhw4/JxlPBYNXW8MBYvPvtsjo2LVUBy4JZdRfG1WKq2dNY8gt+OFeBSXp4XWf7UkZnM5IK0nQf2/PqHqkMXBq9s/z0YRWUt44BUzNkOEe1jnuoJ4sQpz9DeBojDr1qfpadPr6UbC9SSozBXONDctFe7rI0h5+erFwpp+LjU9MnVONIhpOsX+aiTQTBRElBbzl1sPRtu3r7kQfjqzXn1LxwnRU7gpWxjVMrplKBdGuy7iMtNqzmLOgG8QH63Jc22Mo7Tyquz4UkeT1F+8jBas8r4IYxDpWYCvwTE+esHgELip8d/C3BJP14W74RjJqBTJCGy3cDLmpDqHaXE9NPaEs1kKibx4fNx4SmEc74xMsBRLV4n9fTnOnt0omE4xfl9XsYlml78F7bs585qiWyAwNBUWflPRltdUAfkQbFWjEbTDc5FBImnSAxZk/GYqyGwB1BfKrwvFbKawM8Y18oPzXd8dNk821fZ3s2r+yXFrsLDlHBbqXgiP75kNoQPZ6MYNUdLvepRLQc1EBm5ZYV9VW56EfBazAZ71zu++p6o0LAJNBJIgKSacrque4veToF850TpQWBQptZxpQZugPAK9CMZnR3p+gF0rqYVihRnUIdWAmhMB9Ba70cNznf57ndU6NY62paZcDTTOZmPPS8/JZqLyP+ZVrBSDQwgSHsNjf3MOh4SRRd5jzq/kcjIlf6JEa1SoX06BnBQ1ATRmYMPCyNt8fu/GZ0UeAYWG+WtiDs0uDLsmklI4eBSAofueQkVpDo+I4SoFMdC8S35EOvOn7zmyOG4stSy4BBcpdJVI1JARw8QeKXhbsMIgFxQzTvMSuQeAyvdYfgFIXBURqmjb1lZU66KyPBlCWrjBbISJyqgMW8OaJOchk39YLBVcQm66sdtSBIYK9KymoaZnSvLQPNftBPi+BPfg20VwhBQDNPKib8FK5YquNUAzB7sirGjdj+El+HrOTlMr0w1omBQ66K4ENDGMAlZc7AqcE9dodeeAWfGzSyRYMto57iGAXBTnfRRbPKKBLyoV/BTeIZhkfs629J462AvxuE3pHgvcaBfyu+Cln9QhDLWz1AqOuYgqkh78LROOk4g326gj378gXBRZovbjk6iAtKaKGLvLWlGGml/SUhMtSJEgjrO4tWd9sBZ6OUOFAbuIPTaOwy0qyA6zZ9uYyxskF6i7EXWNQr1NrBWV8bGYfPvLq7Dla1gEqZv3eFej2UzcMWvFOiwurY7ASBSZQ8prazrspZeNKzJzZc0bp1PEs1odEHsI7PLYCUVQdBbAYn4nIg9EjRh92dTHKfgrTC/oAU/92U2WkDtCS+fs1BehKd0MHqJauFPVQsS37SIFwUXo0OOcMembkOhyMGPF8BS7CeBYN+H0s+GwxIrUc5SmdMZEXTprVZD6RYoM+YyxKBSBc48kcT2EN1Siv/hoX8ozuHSEfQXIS93SNY8+Jg7pzBdr7WFoKkG1m/CsTV7J2G9/yXV1pOupqPyU7Rs5FjVoJBc+i4mSLKDMm+ZxkcWMdVdM4p/MlBOFQLb+NF9j4QxlTBfQslqyOk1QwcdrJRJVUvlHUYGJc115O17sb5HIP7GE2BbHvfsMnJu2y60YmI509hoUkgGN1UqrOMLMwoC8TDqp6BVQDMiH5KfKHZLbhTwXxR4RdsADov1gD2elDd6SO+hIQBaNnLStoh3EygkLfA9tjULQYg6X7L/n1jNQeaFKaGjsaBffy5atUJ49XgzsxXMiAopLhTU0rJtGIId0g+kggLBYaBb5fC0qp2eJq8HvJVkf7MIJk+eBZ3TVasvwCn8t4MhEhBb8H48LSq/nxpOKovpLVYw8X3mIJM7JMk3yYgFUKdL0pBTfHeYAsa2hl/aoA3wslmL9RT+O26P6OWs0J2dif5o5pBf7u5QrY3Wrn0PYaRri5nDL6p6iNHFLSk6781wys0hkpBSBryRkeNrvLgGJgh95g9oWLmrptWVPIGPSzoXrVNlAd"; + OmemoBundleVAxolotlElement bundle = new OmemoBundleVAxolotlProvider().parse(TestUtils.getParser(bundleXML)); + HashMap bundles = keyUtil.BUNDLE.bundles(bundle, device); + + assertEquals("There must be 100 bundles in the HashMap.", 100, bundles.size()); + assertNotNull(keyUtil.BUNDLE.identityKey(bundle)); + + Iterator it = bundles.keySet().iterator(); + while (it.hasNext()) { + assertNotNull(keyUtil.BUNDLE.preKeyPublic(bundle, it.next())); + } + + assertEquals(1, keyUtil.BUNDLE.signedPreKeyId(bundle)); + assertNotNull(keyUtil.BUNDLE.signedPreKeyPublic(bundle)); + assertNotNull(keyUtil.BUNDLE.signedPreKeySignature(bundle)); + } + + @Test + public void generateOmemoPreKeysIdsMatchAndNoNullValues() { + TreeMap pks = + keyUtil.generateOmemoPreKeys(1, 20); + + for (int i = 1; i <= 20; i++) { + assertEquals("PreKeyIds must correspond the requested ids.", Integer.valueOf(i), pks.firstKey()); + assertNotNull("All PreKeys must not be null.", pks.get(pks.firstKey())); + pks.remove(pks.firstKey()); + } + } @Test public void testAddInBounds() { diff --git a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoMessageInformationTest.java b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoMessageInformationTest.java new file mode 100644 index 000000000..ac7107825 --- /dev/null +++ b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoMessageInformationTest.java @@ -0,0 +1,43 @@ +/** + * + * 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.smack.omemo; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; +import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation; + +import org.junit.Test; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.stringprep.XmppStringprepException; + +public class OmemoMessageInformationTest { + + @Test + public void setterGetterTest() throws XmppStringprepException { + OmemoMessageInformation information = new OmemoMessageInformation(); + assertEquals(information.getCarbon(), OmemoMessageInformation.CARBON.NONE); + information.setCarbon(OmemoMessageInformation.CARBON.RECV); + assertEquals(OmemoMessageInformation.CARBON.RECV, information.getCarbon()); + + assertNull(information.getSenderDevice()); + OmemoDevice device = new OmemoDevice(JidCreate.bareFrom("test@a.bc"), 14); + information.setSenderDevice(device); + assertEquals(device, information.getSenderDevice()); + } +} diff --git a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoStoreTest.java b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoStoreTest.java new file mode 100644 index 000000000..97da2aea7 --- /dev/null +++ b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoStoreTest.java @@ -0,0 +1,346 @@ +/** + * + * Copyright 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.smack.omemo; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertNotSame; +import static junit.framework.TestCase.assertNull; +import static junit.framework.TestCase.assertSame; +import static junit.framework.TestCase.assertTrue; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.TreeMap; + +import org.jivesoftware.smackx.omemo.FileBasedOmemoStore; +import org.jivesoftware.smackx.omemo.OmemoFingerprint; +import org.jivesoftware.smackx.omemo.OmemoStore; +import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; +import org.jivesoftware.smackx.omemo.internal.CachedDeviceList; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; + +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.stringprep.XmppStringprepException; + +public abstract class OmemoStoreTest { + + protected final OmemoStore store; + private final OmemoDevice alice, bob; + + private static final TemporaryFolder tmp = initStaticTemp(); + + OmemoStoreTest(OmemoStore store) + throws XmppStringprepException { + this.store = store; + alice = new OmemoDevice(JidCreate.bareFrom("alice@wonderland.lit"), 123); + bob = new OmemoDevice(JidCreate.bareFrom("bob@builder.tv"), 987); + } + + // Tests + + @Test + public void keyUtilNotNull() { + assertNotNull(store.keyUtil()); + } + + @Test + public void generateOmemoIdentityKeyPairDoesNotReturnNull() { + assertNotNull(store.generateOmemoIdentityKeyPair()); + } + + @Test + public void identityKeyFromIdentityKeyPairIsNotNull() { + T_IdKeyPair pair = store.generateOmemoIdentityKeyPair(); + assertNotNull(store.keyUtil().identityKeyFromPair(pair)); + } + + @Test + public void storeLoadRemoveOmemoIdentityKeyPair() + throws IOException, CorruptedOmemoKeyException + { + T_IdKeyPair before = store.generateOmemoIdentityKeyPair(); + + assertNull(store.loadOmemoIdentityKeyPair(alice)); + store.storeOmemoIdentityKeyPair(alice, before); + + T_IdKeyPair after = store.loadOmemoIdentityKeyPair(alice); + assertNotNull(after); + + // Fingerprints equal + assertEquals(store.keyUtil().getFingerprintOfIdentityKeyPair(before), + store.keyUtil().getFingerprintOfIdentityKeyPair(after)); + + // Byte-representation equals + assertTrue(Arrays.equals( + store.keyUtil().identityKeyPairToBytes(before), + store.keyUtil().identityKeyPairToBytes(after))); + + // Non-existing keypair + assertNull("Must return null for non-existing key pairs.", store.loadOmemoIdentityKeyPair(bob)); + + // Deleting works + store.removeOmemoIdentityKeyPair(alice); + assertNull(store.loadOmemoIdentityKeyPair(alice)); + } + + @Test + public void storeLoadRemoveOmemoIdentityKey() + throws IOException, CorruptedOmemoKeyException + { + // Create IdentityKeys and get bytes + T_IdKey keyA1 = store.keyUtil().identityKeyFromPair(store.generateOmemoIdentityKeyPair()); + T_IdKey keyB1 = store.keyUtil().identityKeyFromPair(store.generateOmemoIdentityKeyPair()); + byte[] bytesA1 = store.keyUtil().identityKeyToBytes(keyA1); + byte[] bytesB = store.keyUtil().identityKeyToBytes(keyB1); + + // Not null and not of length 0 + assertNotNull("Serialized identityKey cannot be null.", bytesA1); + assertNotNull("Serialized identityKey cannot be null.", bytesB); + assertNotSame("Serialized identityKey must be of length > 0.", 0, bytesA1.length); + assertNotSame("Serialized identityKey must be of length > 0.", 0, bytesB.length); + + // Keys do not equal + assertFalse("Generated IdentityKeys must not be equal (ULTRA unlikely).", + Arrays.equals(bytesA1, bytesB)); + + // Loading must return null before and not null after saving + assertNull("Must return null, the store could not have this key by now.", + store.loadOmemoIdentityKey(alice, bob)); + store.storeOmemoIdentityKey(alice, bob, keyA1); + T_IdKey keyA2 = store.loadOmemoIdentityKey(alice, bob); + assertNotNull(keyA2); + + // Loaded key must equal stored one + byte[] bytesA2 = store.keyUtil().identityKeyToBytes(keyA2); + assertTrue("Serialized loaded key must equal serialized stored one.", + Arrays.equals(bytesA1, bytesA2)); + + // Non-existing keys must return null + assertNull("Non-existing keys must be returned as null.", store.loadOmemoIdentityKey(bob, alice)); + + // Key must vanish when deleted. + store.removeOmemoIdentityKey(alice, bob); + assertNull(store.loadOmemoIdentityKey(alice, bob)); + } + + @Test + public void generateOmemoPreKeys() { + TreeMap keys = store.generateOmemoPreKeys(31, 49); + assertNotNull("Generated data structure must not be null.", keys); + + byte[] lastKey = null; + + for (int i = 31; i <= 79; i++) { + assertEquals("Key ids must be ascending order, starting at 31.", Integer.valueOf(i), keys.firstKey()); + assertNotNull("Every id must match to a key.", keys.get(keys.firstKey())); + byte[] bytes = store.keyUtil().preKeyToBytes(keys.get(keys.firstKey())); + assertNotNull("Serialized preKey must not be null.", bytes); + assertNotSame("Serialized preKey must not be of length 0.", 0, bytes.length); + + if (lastKey != null) { + assertFalse("PreKeys MUST NOT be equal.", Arrays.equals(lastKey, bytes)); + } + lastKey = bytes; + + keys.remove(keys.firstKey()); + + } + + assertEquals("After deleting 49 keys, there must be no keys left.", 0, keys.size()); + } + + @Test + public void storeLoadRemoveOmemoPreKeys() + throws IOException, InterruptedException + { + TreeMap before = store.generateOmemoPreKeys(1, 10); + assertEquals("The store must have no prekeys before this test.", 0, store.loadOmemoPreKeys(alice).size()); + + store.storeOmemoPreKeys(alice, before); + TreeMap after = store.loadOmemoPreKeys(alice); + assertNotNull("Loaded preKeys must not be null.", after); + assertEquals("Loaded preKey count must equal stored count.", before.size(), after.size()); + + // Non-existing key must be returned as null + assertNull("Non-existing preKey must be returned as null.", store.loadOmemoPreKey(alice, 10000)); + + int last = after.size(); + for (int i = 1; i <= last; i++) { + T_PreKey bKey = before.get(i); + T_PreKey aKey = after.get(i); + + assertTrue("Loaded keys must equal stored ones.", Arrays.equals( + store.keyUtil().preKeyToBytes(bKey), + store.keyUtil().preKeyToBytes(aKey))); + + T_PreKey rKey = store.loadOmemoPreKey(alice, i); + assertNotNull("Randomly accessed preKeys must not be null.", rKey); + assertTrue("Randomly accessed preKeys must equal the stored ones.", Arrays.equals( + store.keyUtil().preKeyToBytes(aKey), + store.keyUtil().preKeyToBytes(rKey))); + + store.removeOmemoPreKey(alice, i); + assertNull("PreKey must be null after deletion.", store.loadOmemoPreKey(alice, i)); + } + + TreeMap postDeletion = store.loadOmemoPreKeys(alice); + assertSame("PreKey count must equal 0 after deletion of all keys.", 0, postDeletion.size()); + } + + @Test + public void storeLoadRemoveOmemoSignedPreKeys() + throws IOException, CorruptedOmemoKeyException + { + TreeMap before = store.loadOmemoSignedPreKeys(alice); + assertEquals("At this stage, there must be no signed prekeys in the store.", 0, before.size()); + + T_IdKeyPair idp = store.generateOmemoIdentityKeyPair(); + T_SigPreKey spk = store.generateOmemoSignedPreKey(idp, 125); + + assertNotNull("SignedPreKey must not be null.", spk); + assertEquals("ID of signedPreKey must match.", 125, store.keyUtil().signedPreKeyIdFromKey(spk)); + byte[] bytes = store.keyUtil().signedPreKeyToBytes(spk); + assertNotNull("Serialized signedPreKey must not be null", bytes); + assertNotSame("Serialized signedPreKey must not be of length 0.", 0, bytes.length); + + // Stored key must equal loaded key + store.storeOmemoSignedPreKey(alice, 125, spk); + TreeMap after = store.loadOmemoSignedPreKeys(alice); + assertEquals("We must have exactly 1 signedPreKey now.", 1, after.size()); + T_SigPreKey spk2 = after.get(after.firstKey()); + assertEquals("Id of the stored signedPreKey must match the one we stored.", + 125, store.keyUtil().signedPreKeyIdFromKey(spk2)); + assertTrue("Serialization of stored and loaded signed preKey must equal.", Arrays.equals( + store.keyUtil().signedPreKeyToBytes(spk), store.keyUtil().signedPreKeyToBytes(spk2))); + + // Random access + T_SigPreKey rspk = store.loadOmemoSignedPreKey(alice, 125); + assertTrue("Serialization of stored and randomly accessed signed preKey must equal.", Arrays.equals( + store.keyUtil().signedPreKeyToBytes(spk), store.keyUtil().signedPreKeyToBytes(rspk))); + assertNull("Non-existing signedPreKey must be returned as null.", + store.loadOmemoSignedPreKey(alice, 10000)); + + // Deleting + store.removeOmemoSignedPreKey(alice, 125); + assertNull("Deleted key must be returned as null.", store.loadOmemoSignedPreKey(alice, 125)); + assertEquals(0, store.loadOmemoSignedPreKeys(alice).size()); + } + + @Test + public void loadStoreDateOfLastSignedPreKeyRenewal() throws IOException { + assertNull("The date of last signed preKey renewal must be null at this stage.", + store.getDateOfLastSignedPreKeyRenewal(alice)); + Date before = new Date(); + store.setDateOfLastSignedPreKeyRenewal(alice, before); + Date after = store.getDateOfLastSignedPreKeyRenewal(alice); + assertEquals("Dates must equal.", after, before); + } + + @Test + public void loadStoreDateOfLastMessageReceived() throws IOException { + assertNull("The date of last message received must be null at this stage.", + store.getDateOfLastReceivedMessage(alice, bob)); + Date before = new Date(); + store.setDateOfLastReceivedMessage(alice, bob, before); + Date after = store.getDateOfLastReceivedMessage(alice, bob); + assertEquals("Dates must equal.", after, before); + } + + @Test + public void loadStoreCachedDeviceList() throws IOException { + Integer[] active = new Integer[] {1,5,999,10}; + Integer[] inactive = new Integer[] {6,7,8}; + CachedDeviceList before = new CachedDeviceList( + new HashSet<>(Arrays.asList(active)), + new HashSet<>(Arrays.asList(inactive))); + + assertNotNull("Loading a non-existent cached deviceList must return an empty list.", + store.loadCachedDeviceList(alice, bob.getJid())); + + store.storeCachedDeviceList(alice, bob.getJid(), before); + CachedDeviceList after = store.loadCachedDeviceList(alice, bob.getJid()); + assertTrue("Loaded deviceList must not be empty", after.getAllDevices().size() != 0); + + assertEquals("Number of entries in active devices must match.", active.length, after.getActiveDevices().size()); + assertEquals("Number of entries in inactive devices must match.", inactive.length, after.getInactiveDevices().size()); + assertEquals("Number of total entries must match.", active.length + inactive.length, after.getAllDevices().size()); + + for (Integer a : active) { + assertTrue(after.getActiveDevices().contains(a)); + assertTrue(after.getAllDevices().contains(a)); + } + + for (Integer i : inactive) { + assertTrue(after.getInactiveDevices().contains(i)); + assertTrue(after.getAllDevices().contains(i)); + } + + store.storeCachedDeviceList(alice, bob.getJid(), new CachedDeviceList()); + assertEquals("DeviceList must be empty after overwriting it with empty list.", 0, + store.loadCachedDeviceList(alice, bob.getJid()).getAllDevices().size()); + } + + @Test + public void loadAllRawSessionsReturnsEmptyMapTest() { + HashMap sessions = store.loadAllRawSessionsOf(alice, bob.getJid()); + assertNotNull(sessions); + assertEquals(0, sessions.size()); + } + + @Test + public void loadNonExistentRawSessionReturnsNullTest() { + T_Sess session = store.loadRawSession(alice, bob); + assertNull(session); + } + + @Test + public void getFingerprint() throws IOException, CorruptedOmemoKeyException { + assertNull("Method must return null for a non-existent fingerprint.", store.getFingerprint(alice)); + store.storeOmemoIdentityKeyPair(alice, store.generateOmemoIdentityKeyPair()); + OmemoFingerprint fingerprint = store.getFingerprint(alice); + assertNotNull("fingerprint must not be null", fingerprint); + assertEquals("Fingerprint must be of length 64", 64, fingerprint.length()); + + store.removeOmemoIdentityKeyPair(alice); //clean up + } + + // ############################################################## + // Workaround for https://github.com/junit-team/junit4/issues/671 + + static TemporaryFolder initStaticTemp() { + try { + return new TemporaryFolder() { { before(); } }; + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + @AfterClass + public static void cleanup() throws Exception { + FileBasedOmemoStore.deleteDirectory(tmp.getRoot()); + } + + // ############################################################## +} diff --git a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/WrapperObjectsTest.java b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/WrapperObjectsTest.java index a7c64926f..4c8e75f04 100644 --- a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/WrapperObjectsTest.java +++ b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/WrapperObjectsTest.java @@ -26,13 +26,11 @@ import java.security.NoSuchAlgorithmException; import java.security.Security; import org.jivesoftware.smack.packet.Message; - import org.jivesoftware.smackx.omemo.element.OmemoElement; import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException; import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag; import org.jivesoftware.smackx.omemo.internal.CiphertextTuple; import org.jivesoftware.smackx.omemo.internal.ClearTextMessage; -import org.jivesoftware.smackx.omemo.internal.IdentityKeyWrapper; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation; import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder; @@ -47,13 +45,6 @@ import org.jxmpp.jid.impl.JidCreate; */ public class WrapperObjectsTest { - @Test - public void identityKeyWrapperTest() { - Object pseudoKey = new Object(); - IdentityKeyWrapper wrapper = new IdentityKeyWrapper(pseudoKey); - assertEquals(pseudoKey, wrapper.getIdentityKey()); - } - @Test public void ciphertextTupleTest() { byte[] c = OmemoMessageBuilder.generateIv(); @@ -69,17 +60,14 @@ public class WrapperObjectsTest { @Test public void clearTextMessageTest() throws Exception { - Object pseudoKey = new Object(); - IdentityKeyWrapper wrapper = new IdentityKeyWrapper(pseudoKey); BareJid senderJid = JidCreate.bareFrom("bob@server.tld"); OmemoDevice sender = new OmemoDevice(senderJid, 1234); - OmemoMessageInformation information = new OmemoMessageInformation(wrapper, sender, OmemoMessageInformation.CARBON.NONE); + OmemoMessageInformation information = new OmemoMessageInformation(null, sender, OmemoMessageInformation.CARBON.NONE); assertTrue("OmemoInformation must state that the message is an OMEMO message.", information.isOmemoMessage()); assertEquals(OmemoMessageInformation.CARBON.NONE, information.getCarbon()); assertEquals(sender, information.getSenderDevice()); - assertEquals(wrapper, information.getSenderIdentityKey()); String body = "Decrypted Body"; Message message = new Message(senderJid, body); diff --git a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/util/EphemeralTrustCallback.java b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/util/EphemeralTrustCallback.java new file mode 100644 index 000000000..bad302523 --- /dev/null +++ b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/util/EphemeralTrustCallback.java @@ -0,0 +1,59 @@ +/** + * + * Copyright the original author or authors + * + * 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.smack.omemo.util; + +import java.util.HashMap; + +import org.jivesoftware.smackx.omemo.OmemoFingerprint; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; +import org.jivesoftware.smackx.omemo.trust.TrustCallback; +import org.jivesoftware.smackx.omemo.trust.TrustState; + +/** + * Ephemera Trust Callback used to make trust decisions in tests. + */ +public class EphemeralTrustCallback implements TrustCallback { + + private final HashMap> trustStates = new HashMap<>(); + + @Override + public TrustState getTrust(OmemoDevice device, OmemoFingerprint fingerprint) { + HashMap states = trustStates.get(device); + + if (states != null) { + TrustState state = states.get(fingerprint); + + if (state != null) { + return state; + } + } + + return TrustState.undecided; + } + + @Override + public void setTrust(OmemoDevice device, OmemoFingerprint fingerprint, TrustState state) { + HashMap states = trustStates.get(device); + + if (states == null) { + states = new HashMap<>(); + trustStates.put(device, states); + } + + states.put(fingerprint, state); + } +}