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); + } +}