From 1f731f6318785a84b9741280d586a61dc37ecb2e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 13 Jun 2018 12:29:16 +0200 Subject: [PATCH] Rework support for XEP-0384: OMEMO Encryption Changes: Rework integration tests New structure of base integration test classes bump dependency on signal-protocol-java from 2.4.0 to 2.6.2 Introduced CachingOmemoStore implementations Use CachingOmemoStore classes in integration tests Removed OmemoSession classes (replaced with more logical OmemoRatchet classes) Consequently also removed load/storeOmemoSession methods from OmemoStore Removed some clutter from KeyUtil classes Moved trust decision related code from OmemoStore to TrustCallback Require authenticated connection for many functions Add async initialization function in OmemoStore Refactor omemo test package (/java/org/jivesoftware/smack/omemo -> /java/org/jivesoftware/smackx) Remove OmemoStore method isFreshInstallation() as well as defaultDeviceId related stuff FileBasedOmemoStore: Add cleaner methods to store/load base data types (Using tryWithResource, only for future releases, once Android API gets bumped) Attempt to make OmemoManager thread safe new logic for getInstanceFor() deviceId determination OmemoManagers encrypt methods now don't throw exceptions when encryption for some devices fails. Instead message gets encrypted when possible and more information about failures gets returned alongside the message itself Added OmemoMessage class for that purpose Reworked entire OmemoService class Use safer logic for creating trust-ignoring messages (like ratchet-update messages) Restructure elements/provider in order to prepare for OMEMO namespace bumps Remove OmemoManager.regenerate() methods in favor of getInstanceFor(connection, randomDeviceId) Removed some unnecessary configuration options Prepare for support of more AES message key types Simplify session creation Where possible, avoid side effects in methods Add UntrustedOmemoIdentityException Add TrustState enum More improved tests --- build.gradle | 2 + documentation/extensions/omemo.md | 338 ++- .../extensions/omemo_migration_4.2.0_head.md | 43 + smack-integration-test/build.gradle | 1 + .../omemo/AbstractOmemoIntegrationTest.java | 42 +- .../omemo/AbstractOmemoMessageListener.java | 149 ++ .../AbstractTwoUsersOmemoIntegrationTest.java | 75 + .../MessageEncryptionIntegrationTest.java | 102 + .../smackx/omemo/OmemoInitializationTest.java | 81 - .../omemo/OmemoIntegrationTestHelper.java | 156 -- .../smackx/omemo/OmemoKeyTransportTest.java | 109 - .../smackx/omemo/OmemoMamDecryptionTest.java | 74 + .../smackx/omemo/OmemoManagerSetupHelper.java | 235 ++ .../smackx/omemo/OmemoMessageSendingTest.java | 193 -- .../omemo/OmemoSessionRenegotiationTest.java | 195 -- .../smackx/omemo/OmemoStoreTest.java | 163 -- .../SessionRenegotiationIntegrationTest.java | 79 + smack-omemo-signal/build.gradle | 3 +- .../omemo/signal/SignalCachingOmemoStore.java | 60 + .../signal/SignalFileBasedOmemoStore.java | 9 +- .../omemo/signal/SignalOmemoKeyUtil.java | 69 +- .../omemo/signal/SignalOmemoRatchet.java | 162 ++ .../omemo/signal/SignalOmemoService.java | 59 +- .../omemo/signal/SignalOmemoSession.java | 142 -- .../smackx/omemo/signal/SignalOmemoStore.java | 5 +- .../signal/SignalOmemoStoreConnector.java | 122 +- .../smack/omemo/OmemoMessageBuilderTest.java | 91 - .../omemo/SignalFileBasedOmemoStoreTest.java | 218 -- .../omemo/LegacySignalOmemoKeyUtilTest.java | 93 + .../smackx/omemo/SignalOmemoKeyUtilTest.java | 58 + .../omemo/SignalOmemoManagerTest.java} | 25 +- .../omemo/SignalOmemoStoreConnectorTest.java | 4 +- .../smackx/omemo/SignalOmemoStoreTest.java | 83 + smack-omemo/build.gradle | 2 + .../smackx/omemo/CachingOmemoStore.java | 446 ++++ .../smackx/omemo/FileBasedOmemoStore.java | 965 ++++--- .../smackx/omemo/OmemoConfiguration.java | 149 +- .../smackx/omemo/OmemoManager.java | 1058 +++++--- .../smackx/omemo/OmemoMessage.java | 213 ++ .../smackx/omemo/OmemoRatchet.java | 197 ++ .../smackx/omemo/OmemoService.java | 2238 +++++++++-------- .../jivesoftware/smackx/omemo/OmemoStore.java | 691 ++--- .../omemo/element/OmemoBundleElement.java | 172 +- .../element/OmemoBundleElement_VAxolotl.java | 43 + .../element/OmemoBundleVAxolotlElement.java | 207 -- .../omemo/element/OmemoDeviceListElement.java | 5 + ...a => OmemoDeviceListElement_VAxolotl.java} | 10 +- .../smackx/omemo/element/OmemoElement.java | 141 +- .../omemo/element/OmemoElement_VAxolotl.java | 44 + .../omemo/element/OmemoHeaderElement.java | 83 + .../element/OmemoHeaderElement_VAxolotl.java | 27 + .../smackx/omemo/element/OmemoKeyElement.java | 84 + .../omemo/element/OmemoVAxolotlElement.java | 86 - .../exceptions/CryptoFailedException.java | 11 + .../exceptions/NoIdentityKeyException.java | 33 + .../exceptions/NoRawSessionException.java | 11 +- .../exceptions/StaleDeviceException.java | 66 + .../UndecidedOmemoIdentityException.java | 6 + .../UntrustedOmemoIdentityException.java | 93 + .../omemo/internal/CipherAndAuthTag.java | 9 +- .../omemo/internal/ClearTextMessage.java | 63 - ...ceList.java => OmemoCachedDeviceList.java} | 26 +- .../smackx/omemo/internal/OmemoDevice.java | 10 + .../internal/OmemoMessageInformation.java | 141 -- .../smackx/omemo/internal/OmemoSession.java | 266 -- ...OmemoCarbonCopyStanzaReceivedListener.java | 29 + .../OmemoMessageStanzaReceivedListener.java | 25 + .../package-info.java} | 19 +- .../omemo/listener/OmemoMessageListener.java | 27 +- .../listener/OmemoMucMessageListener.java | 30 +- .../provider/OmemoBundleVAxolotlProvider.java | 8 +- .../OmemoDeviceListVAxolotlProvider.java | 8 +- .../omemo/provider/OmemoVAxolotlProvider.java | 44 +- .../omemo/{ => trust}/OmemoFingerprint.java | 20 +- .../omemo/trust/OmemoTrustCallback.java | 27 + .../smackx/omemo/trust/TrustState.java | 23 + .../smackx/omemo/trust/package-info.java | 23 + .../omemo/util/MessageOrOmemoMessage.java | 49 + .../smackx/omemo/util/OmemoConstants.java | 4 +- .../smackx/omemo/util/OmemoKeyUtil.java | 99 +- .../omemo/util/OmemoMessageBuilder.java | 210 +- .../smack/omemo/OmemoKeyUtilTest.java | 47 - .../omemo/DeviceListTest.java | 13 +- .../omemo/OmemoBundleVAxolotlElementTest.java | 8 +- .../omemo/OmemoConfigurationTest.java | 40 +- .../OmemoDeviceListVAxolotlElementTest.java | 8 +- .../omemo/OmemoDeviceTest.java | 2 +- .../omemo/OmemoExceptionsTest.java | 2 +- .../omemo/OmemoFingerprintTest.java | 4 +- .../smackx/omemo/OmemoKeyUtilTest.java | 153 +- .../smackx/omemo/OmemoServiceTest.java | 91 + .../smackx/omemo/OmemoStoreTest.java | 345 +++ .../omemo/OmemoVAxolotlElementTest.java | 24 +- .../omemo/WrapperObjectsTest.java | 47 +- .../omemo/util/EphemeralTrustCallback.java | 59 + .../omemo/util/OmemoMessageBuilderTest.java | 79 + 96 files changed, 6915 insertions(+), 5488 deletions(-) create mode 100644 documentation/extensions/omemo_migration_4.2.0_head.md create mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoMessageListener.java create mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java create mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java delete mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoInitializationTest.java delete mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoIntegrationTestHelper.java delete mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoKeyTransportTest.java create mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMamDecryptionTest.java create mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java delete mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessageSendingTest.java delete mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoSessionRenegotiationTest.java delete mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java create mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/SessionRenegotiationIntegrationTest.java create mode 100644 smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalCachingOmemoStore.java create mode 100644 smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoRatchet.java delete mode 100644 smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoSession.java delete mode 100644 smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoMessageBuilderTest.java delete mode 100644 smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalFileBasedOmemoStoreTest.java create mode 100644 smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/LegacySignalOmemoKeyUtilTest.java create mode 100644 smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoKeyUtilTest.java rename smack-omemo-signal/src/test/java/org/jivesoftware/{smack/omemo/OmemoManagerTest.java => smackx/omemo/SignalOmemoManagerTest.java} (74%) rename smack-omemo-signal/src/test/java/org/jivesoftware/{smack => smackx}/omemo/SignalOmemoStoreConnectorTest.java (94%) create mode 100644 smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreTest.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/CachingOmemoStore.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessage.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoRatchet.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement_VAxolotl.java delete mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleVAxolotlElement.java rename smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/{OmemoDeviceListVAxolotlElement.java => OmemoDeviceListElement_VAxolotl.java} (75%) create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoElement_VAxolotl.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement_VAxolotl.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoKeyElement.java delete mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoVAxolotlElement.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoIdentityKeyException.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/StaleDeviceException.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UntrustedOmemoIdentityException.java delete mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/ClearTextMessage.java rename smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/{CachedDeviceList.java => OmemoCachedDeviceList.java} (82%) delete mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoMessageInformation.java delete mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoSession.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/listener/OmemoCarbonCopyStanzaReceivedListener.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/listener/OmemoMessageStanzaReceivedListener.java rename smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/{IdentityKeyWrapper.java => listener/package-info.java} (66%) rename smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/{ => trust}/OmemoFingerprint.java (76%) create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/OmemoTrustCallback.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/TrustState.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/package-info.java create mode 100644 smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/MessageOrOmemoMessage.java delete mode 100644 smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoKeyUtilTest.java rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/DeviceListTest.java (84%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/OmemoBundleVAxolotlElementTest.java (93%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/OmemoConfigurationTest.java (75%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/OmemoDeviceListVAxolotlElementTest.java (85%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/OmemoDeviceTest.java (98%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/OmemoExceptionsTest.java (99%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/OmemoFingerprintTest.java (91%) rename smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoKeyUtilTest.java => smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoKeyUtilTest.java (68%) create mode 100644 smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoServiceTest.java create mode 100644 smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/OmemoVAxolotlElementTest.java (74%) rename smack-omemo/src/test/java/org/jivesoftware/{smack => smackx}/omemo/WrapperObjectsTest.java (58%) create mode 100644 smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/util/EphemeralTrustCallback.java create mode 100644 smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilderTest.java diff --git a/build.gradle b/build.gradle index 7d628671a..48b5ad29b 100644 --- a/build.gradle +++ b/build.gradle @@ -548,10 +548,12 @@ task clirrRootReport(type: org.kordamp.gradle.clirr.ClirrReportTask) { } task integrationTest { + description 'Verify correct functionality of Smack by running some integration tests.' dependsOn project(':smack-integration-test').tasks.run } task omemoSignalIntTest { + description 'Run integration tests of the smack-omemo module in combination with smack-omemo-signal.' dependsOn project(':smack-omemo-signal-integration-test').tasks.run } diff --git a/documentation/extensions/omemo.md b/documentation/extensions/omemo.md index 898ba9c4c..815b6c14d 100644 --- a/documentation/extensions/omemo.md +++ b/documentation/extensions/omemo.md @@ -3,6 +3,9 @@ Encrypting messages with OMEMO [Back](index.md) +About OMEMO +----------- + OMEMO ([XEP-0384](https://xmpp.org/extensions/xep-0384.html)) is an adaption of the Signal protocol for XMPP. It provides an important set of cryptographic properties including but not restricted to @@ -22,18 +25,44 @@ It does NOT provide a server side message archive, so that a new device could fetch old chat history. Most implementations of OMEMO use the signal-protocol libraries provided by -OpenWhisperSystems. Unlike Smack, those libraries are licensed under the GPL, +OpenWhisperSystems. Unlike Smack, those libraries are licensed under the GPLv3, which prevents a Apache licensed OMEMO implementation using those libraries (see [licensing situation](https://github.com/igniterealtime/Smack/wiki/OMEMO-libsignal-Licensing-Situation)). The module smack-omemo therefore contains no code related to signal-protocol. However, almost all functionality is encapsulated in that module. If you want -to use OMEMO in a GPL client, you can use the smack-omemo-signal +to use OMEMO in a GPLv3 licensed client, you can use the smack-omemo-signal Smack module, which binds the signal-protocol library to smack-omemo. It is also possible, to port smack-omemo to other libraries implementing the double ratchet algorithm. -Requirements ------------- +Understanding the Double Ratchet Algorithm +------------------------------------------ + +In the context of OMEMO encryption, a *recipient* is a not a user, but a users *device* (a user might have +multiple devices of course). +Unlike in PGP, each device capable of OMEMO has its own identity key and publishes its own key bundle. +It is not advised to migrate OMEMO identities from one device to another, as it might damage the ratchet +if not done properly (more on that later). Sharing one identity key between multiple devices is not the purpose of +OMEMO. If a contact has three OMEMO capable devices, you will see three different OMEMO identities and their +fingerprints. + +OMEMO utilizes multiple layers of encryption when encrypting a message. +The body of the message is encrypted with a symmetric message key (AES-128-GCM) producing a *payload*. +The message key is encrypted for each recipient using the double ratchet algorithm. +For that purpose, the sending device creates a session with the recipient device (if there was no session already). +Upon receiving a message, the recipient selects the encrypted key addressed to them and decrypts it with their +counterpart of the OMEMO session. The decrypted key gets then used to decrypt the message. + +One important consequence of forward secrecy is, that whenever an OMEMO message gets decrypted, +the state of the ratchet changes and the key used to decrypt the message gets deleted. +There is no way to recover this key a second time. The result is, that every message can be decrypted +exactly once. + +In order to provide the best user experience, it is therefore advised to implement a client side message archive, +since solutions like MAM cannot be used to fetch old, already once decrypted OMEMO messages. + +Server-side Requirements +------------------------ In order to use OMEMO encryption, your server and the servers of your chat partners must support PEP ([XEP-0163](http://xmpp.org/extensions/xep-0163.html)) @@ -42,170 +71,221 @@ Optionally your server should support Message Carbons ([XEP-0280](http://xmpp.or and Message Archive Management ([XEP-0313](http://xmpp.org/extensions/xep-0313.html)) to achieve message synchronization across all (on- and offline) devices. -Setup ------ +Client-side Requirements +------------------------ -First you need to setup a OmemoService, for example the libsignal one: +If you are want to run smack-omemo related code on the Windows platform, you might have to install the +[Java Cryptography Extension](http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html). +This is needed to generate cryptographically strong keys. + +Storing Keys +------------ + +smack-omemo needs to create, store and delete some information like keys and session states during operation. +For that purpose the `OmemoStore` class is used. There are multiple implementations with different properties. + +* The `(Signal)FileBasedOmemoStore` stores all information in individual files organized in a directory tree. +While this is the most basic and easy to use implementation, it is not the best solution in terms of performance. + +* The `(Signal)CachingOmemoStore` is a multi-purpose store implementation. It can be used to wrap another +`(Signal)OmemoStore` implementation to provide a caching layer (for example in order to reduce access to a database backend or +a the file system of the `FileBasedOmemoStore`. It is therefore advised to wrap persistent `(Signal)OmemoStore` +implementations with a `(Signal)CachingOmemoStore`. +On the other hand it can also be used standalone as an ephemeral `OmemoStore`, which "forgets" all stored information +once the program terminates. This comes in handy for testing purposes. + +If you are unhappy with the `(Signal)FileBasedOmemoStore`, you can implement your own store (for example with a +SQL database) by extending the `(Signal)OmemoStore` class. + +It most certainly makes sense to store the data of the used `OmemoStore` in a secure way (for example an +encrypted database). + +Handling Trust Decisions +------------------------ + +In order for a cryptographic system to make sense, decisions must be made whether to *trust* an identity or not. +For technical reasons those decisions cannot be stored within the `OmemoStore`. Instead a client must implement +the `OmemoTrustCallback`. This interface provides methods to mark `OmemoFingerprints` as trusted or untrusted and to +query trust decisions. + +In order to provide security, a client should communicate to the user, that it is important for them to compare +fingerprints through an external channel (reading it out on the phone, scanning QR codes...) before starting to chat. + +While not implemented in smack-omemo, it is certainly for the client to implement different trust models like +[Blind Trust Before Verification](https://gultsch.de/trust.html). + +Basic Setup +----------- + +Before you can start to send and receive messages, some preconditions have to be met. These steps should be executed +in the order as presented below. In this example we will use components from the *smack-omemo-signal* module. + +1. Register an OmemoService + + The `OmemoService` class is responsible for handling incoming messages and manages access to the Double Ratchet. + + ``` + SignalOmemoService.setup(); + ``` + + The `setup()` method registers the service as a singleton. You can later access the instance + by calling `SignalOmemoService.getInstace()`. The service can only be registered once. + Subsequent calls will throw an `IllegalStateException`. + +2. Set an OmemoStore + + Now you have to decide, what `OmemoStore` implementation you want to use to store and access + keys and session states. In this example we'll use the `SignalFileBasedOmemoStore` wrapped in a + `SignalCachingOmemoStore` for better performance. + + ``` + SignalOmemoService service = SignalOmemoService.getInstace(); + service.setOmemoStoreBackend(new SignalCachingOmemoStore(new SignalFileBasedOmemoStore(new File("/path/to/store")))); + ``` + + Just like the `OmemoService` instance, the `OmemoStore` instance can only be set once. + +3. Get an instance of the OmemoManager for your connection + + For the greater part of OMEMO related actions, you'll use the `OmemoManager`. The `OmemoManager` represents + your OMEMO device. While it is possible to have multiple `OmemoManager`s per `XMPPConnection`, you really + only need one. + + ``` + OmemoManager manager = OmemoManager.getInstanceFor(connection); + ``` + + If for whatever reason you decide to use multiple `OmemoManager`s at once, + it is highly advised to get them like this: + + ``` + OmemoManager first = OmemoManager.getInstanceFor(connection, firstId); + OmemoManager second = OmemoManager.getInstanceFor(connection, secondId); + ``` + +4. Set an OmemoTrustCallback + + As stated above, the `OmemoTrustCallback` is used to query trust decisions. Set the callback like this: + + ``` + manager.setTrustCallback(trustCallback); + ``` + + If you use multiple `OmemoManager`s each `OmemoManager` MUST have its own callback. + +5. Set listeners for OMEMO messages. + + To get notified of incoming OMEMO encrypted messages, you need to register corresponding listeners. + There are two types of listeners. + + * `OmemoMessageListener` is used to listen for incoming encrypted OMEMO single chat messages and + KeyTransportMessages. + * `OmemoMucMessageListener` is used to listen for encrypted OMEMO messages sent in a MultiUserChat. + + Note that an incoming message might not have a body. That might be the case for + [KeyTransportMessages](https://xmpp.org/extensions/xep-0384.html#usecases-keysend) + or messages sent to update the ratchet. You can check, whether a received message is such a message by calling + `OmemoMessage.Received.isKeyTransportMessage()`, which will return true if the message has no body. + + The received message will include the senders device and fingerprint, which you can use in + `OmemoManager.isTrustedOmemoIdentity(device, fingerprint)` to determine, if the message was sent by a trusted device. + +6. Initialize the manager(s) + + Ideally all above steps should be executed *before* `connection.login()` gets called. That way you won't miss + any offline messages. If the connection is not yet logged in, now is the time to do so. + + ``` + connection.login(); + manager.initialize(); + ``` + + Since a lot of keys are generated in this step, this might take a little longer on older devices. + You might want to use the asynchronous call `OmemoManager.initializeAsync(initializationFinishedCallback)` + instead to prevent the thread from blocking. + +Send Messages +------------- + +Encrypting a message for a contact really means to encrypt the message for all trusted devices of the contact, as well +as all trusted devices of the user itself (except the sending device). The encryption process will fail if there are +devices for which the user has not yet made a trust decision. + +### Make Trust Decisions + +To get a list of all devices of a contact, you can do the following: ``` -SignalOmemoService.setup(); +List devices = manager.getDevicesOf(contactsBareJid); ``` -As a first step you have to prepare the OmemoStore. -You can either use your own implementation, or use the builtin FileBasedOmemoStore (default). -If you do not want to use your own store, the implementation uses a file based store, so you HAVE to set the default path. +To get the OmemoFingerprint of a device, you can call ``` -//set path in case we want to use a file-based store (default) -OmemoConfiguration.setFileBasedOmemoStoreDefaultPath(new File("path/to/your/store")); +OmemoFingerprint fingerprint = manager.getFingerprint(device); ``` -For each device you need an OmemoManager. -In this example, we use the smack-omemo-signal -implementation, so we use the SignalOmemoService as -OmemoService. The OmemoManager must be initialized with either a deviceId (of an existing -device), or null in case you want to generate a fresh device. -The OmemoManager can be used to execute OMEMO related actions like sending a -message etc. If you don't pass a deviceId, the value of defaultDeviceId will be used if present. +This fingerprint can now be displayed to the user who can decide whether to trust the device, or not. ``` -OmemoManager omemoManager = OmemoManager.getInstanceFor(connection); +// Trust +manager.trustOmemoIdentity(device, fingerprint); + +// Distrust +manager.distrustOmemoIdentity(device, fingerprint); ``` -As soon as the connection is authenticated, the module generates some keys and -announces OMEMO support. -To get updated with new OMEMO messages, you should register message listeners. +### Encrypt a Message +Currently only Message bodies can be encrypted. ``` -omemoManager.addOmemoMessageListener(new OmemoMessageListener() { - @Overwrite - public void omOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - System.out.println(decryptedBody); - } -}); - -omemoManager.addOmemoMucMessageListener(new OmemoMucMessageListener() { - @Overwrite - public void onOmemoMucMessageReceived(MultiUserChat muc, BareJid from, String decryptedBody, Message message, - Message wrappingMessage, OmemoMessageInformation omemoInformation) { - System.out.println(decryptedBody); - } -}); +String secret = "Mallory is a twerp!"; +OmemoMessage.Sent encrypted = manager.encrypt(contactsBareJid, secret); ``` -Usage ------ +The encrypted message will contain some information about the message. It might for example happen, that the encryption +failed for some recipient devices. For that reason the encrypted message will contain a map of skipped devices and +the reasons. -Before you can encrypt a message for a device, you have to trust its identity. -smack-omemo will throw an UndecidedOmemoIdentityException whenever you try -to send a message to a device, which the user has not yet decided to trust or distrust. +### Encrypt a Message for a MultiUserChat + +A MultiUserChat must fulfill some criteria in order to be OMEMO capable. +The MUC must be non-anonymous. Furthermore all members of the MUC must have subscribed to one another. +You can check for the non-anonymity like follows: ``` -omemoManager.trustOmemoIdentity(trustedDevice, trustedFingerprint); -omemoManager.distrustOmemoIdentity(untrustedDevice, untrustedFingerprint); +manager.multiUserChatSupportsOmemo(muc); ``` -The trust decision should be made by the user based on comparing fingerprints. -You can get fingerprints of your own and contacts devices: +Encryption is then done analog to single message encryption: ``` -OmemoFingerprint myFingerprint = omemoManager.getFingerprint(); -OmemoFingerprint otherFingerprint = omemoStore.getFingerprint(omemoManager, otherDevice); +OmemoMessage.Sent encrypted = manager.encrypt(multiUserChat, secret); ``` -To encrypt a message for a single contact or a MUC, you do as follows: +### Sending an encrypted Message + +To send the message, it has to be wrapped in a `Message` object. That can conveniently be done like follows. ``` -Message encryptedSingleMessage = omemoManager.encrypt(bobsBareJid, "Hi Bob!"); - -Message encryptedMucMessage = omemoManager.encrypt(multiUserChat, "Hi everybody!"); +Message message = encrypted.asMessage(contactsJid); +connection.sendStanza(message): ``` -Note: It may happen, that smack-omemo is unable to create a session with a device. -In case we could not create a single valid session for a recipient, a -CannotCreateOmemoSessionException will be thrown. This exception contains information -about which sessions could (not) be created and why. If you want to ignore those devices, -you can encrypt the message for all remaining devices like this: - -``` -Message encryptedMessage = omemoManager.encryptForExistingSession(cannotEstablishSessionException, "Hi there!"); -``` - -The resulting message can then be sent via the ChatManager/MultiUserChatManager. - -You may want to generate a new identity sometime in the future. That's pretty straight -forward. No need to manually publish bundles etc. - -``` -omemoManager.regenerate(); -``` - -In case your device list gets filled with old unused identities, you can clean it up. -This will remove all active devices from the device list and only publish the device -you are using right now. - -``` -omemoManager.purgeDevices(); -``` - -If you want to find out, whether a server, MUC or contacts resource supports OMEMO, -you can use the following methods: - -``` -boolean serverCan = omemoManager.serverSupportsOmemo(serverJid); -boolean mucCan = omemoManager.multiUserChatSupportsOmemo(mucJid); -boolean resourceCan = omemoManager.resourceSupportsOmemo(contactsResourceJid); -``` - -It might happen, that the server you or your contact are using is not delivering devicelist updates correctly. -In such a case smack-omemo cannot fetch bundles or send messages to devices it hasn\'t seen before. To mitigate this, it -might help to explicitly request the latest device list from the server. -``` -omemoManager.requestDeviceListUpdateFor(contactJid); -``` - -If you want to decrypt a MamQueryResult, you can do so using the following method: -```` -List decryptedMamQuery = omemoManager.decryptMamQueryResult(mamQueryResult); -```` -Note, that you cannot decrypt an OMEMO encrypted message twice for reasons of forward secrecy. -A ClearTextMessage contains the decrypted body of the message, as well as additional information like if/how the message was encrypted in the first place. -Unfortunately due to the fact that you cannot decrypt messages twice, you have to keep track of the message history locally on the device and ideally also keep track of the last received message, so you can query the server only for messages newer than that. - +This will add a [Message Processing Hint](https://xmpp.org/extensions/xep-0334.html) for MAM, +an [Explicit Message Encryption](https://xmpp.org/extensions/xep-0380.html) hint for OMEMO, +as well as an optional cleartext hint about OMEMO to the message. Configuration ------------- smack-omemo has some configuration options that can be changed on runtime via the `OmemoConfiguration` class: -* setFileBasedOmemoStoreDefaultPath sets the default directory for the FileBasedOmemoStore implementations. * setIgnoreStaleDevices when set to true, smack-omemo will stop encrypting messages for **own** devices that have not send a message for some period of time (configurable in setIgnoreStaleDevicesAfterHours) * setDeleteStaleDevices when set to true, smack-omemo will remove own devices from the device list, if no messages were received from them for a period of time (configurable in setDeleteStaleDevicesAfterHours) * setRenewOldSignedPreKeys when set to true, smack-omemo will periodically generate and publish new signed prekeys. Via setRenewOldSignedPreKeysAfterHours you can configure, after what period of time new keys are generated and setMaxNumberOfStoredSignedPreKeys allows configuration of how many signed PreKeys are kept in storage for decryption of delayed messages. * setAddOmemoBodyHint when set to true, a plaintext body with a hint about OMEMO encryption will be added to the message. This hint will be displayed by clients that do not support OMEMO. Note that this might not be desirable when communicating with clients that do not support EME. -* setAddEmeEncryptionHint when set to true, an Explicit Message Encryption element will be added to the message. This element tells clients, that the message is encrypted with OMEMO. -* setAddMAMStorageProcessingHint when set to true, a storage hint for Message Archive Management will be added to the message. This enabled servers to store messages that contain no body. - -Customization -------------- -You can integrate smack-omemo with your existing infrastructure. -It is possible to create your own OmemoStore implementations eg. using an SQL database as backend. -For this purpose, just inherit OmemoStore/SignalOmemoStore and implement the missing methods. -You can register that Store with your OmemoService by calling -``` -SignalOmemoService.getInstance().setOmemoStoreBackend(myStore); -``` - -Features --------- -* decryption and encryption of OMEMO messages (single and multi user chat) -* provides information about trust status of incoming messages -* automatic publishing of bundle -* automatic merging of incoming deviceList updates -* ignores stale devices after period of inactivity -* removes stale devices from device list after period of inactivity -* automatic repair of broken sessions through ratchet update messages -* automatic renewal of signed preKeys -* multiple devices per connection possible +* setRepairBrokenSessionsWithPreKeyMessages when set to true, whenever a message arrives, which cannot be decrypted, smack-omemo will respond with a preKeyMessage which discards the old session and builds a fresh one. +* setCompleteSessionWithEmptyMessage when set to true, whenever a preKeyMessage arrives, smack-omemo will respond with an empty message to complete the session. Integration Tests ----------------- diff --git a/documentation/extensions/omemo_migration_4.2.0_head.md b/documentation/extensions/omemo_migration_4.2.0_head.md new file mode 100644 index 000000000..8d73190cb --- /dev/null +++ b/documentation/extensions/omemo_migration_4.2.0_head.md @@ -0,0 +1,43 @@ +Migrating smack-omemo from 4.2.1 to 4.x.x +========================================= + +The implementation of smack-omemo and smack-omemo-signal was originally started as an +academic project under pressure of time. +For that reason, the API was not perfect when OMEMO support was first introduced in +Smack in version 4.2.1. +Many issues of smack-omemo have been resolved over the course of the last year in +a major effort, which is why smack-omemo and smack-omemo-signalwere excluded from +the 4.2.2 release. + +During this time major parts of the implementation were redone and the API changed +as a consequence of that. This guide will go through all notable changes in order +to make the process of upgrading as easy and straight forward as possible. + +## Trust +One major change is, that the OmemoStore implementations no longer store trust decisions. +Methods related to trust have been removed from OmemoStore implementations. +Instead the client is now responsible to store those. +Upon startup, the client now must pass an `OmemoTrustCallback` to the `OmemoManager` +which is used to access and change trust decisions. + +It is recommended for the client to store trust decisions as tuples of (omemo device, +fingerprint of identityKey, trust state). +When querying a trust decision (aka. "Is this fingerprint trusted for that device?), +the local fingerprint should be compared to the provided fingerprint. + +The method signatures for setting and querying trust from inside the OmemoManager are +still the same. Internally they access the `OmemoTrustCallback` set by the client. + +## Encryption +Message encryption in smack-omemo 4.2.1 was ugly. Encryption for multiple devices +could fail because session negotiation could go wrong, which resulted in an +exception, which contained all devices with working sessions. +That exception could then be used in +`OmemoManager.encryptForExistingSessions(CannotEstablishOmemoSessionException exception, String message)`, +to encrypt the message for all devices with a session. + +The new API is + + + + 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..225ffddb5 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)"); @@ -59,23 +40,8 @@ public abstract class AbstractOmemoIntegrationTest extends AbstractSmackIntegrat if (!OmemoService.isServiceRegistered()) { throw new TestNotPossibleException("No OmemoService registered."); } + + OmemoConfiguration.setCompleteSessionWithEmptyMessage(true); + OmemoConfiguration.setRepairBrokenSessionsWithPrekeyMessages(true); } - - @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/AbstractOmemoMessageListener.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoMessageListener.java new file mode 100644 index 000000000..1ad4fa0bf --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoMessageListener.java @@ -0,0 +1,149 @@ +/** + * + * Copyright 2018 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 org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smackx.carbons.packet.CarbonExtension; +import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener; + +import org.igniterealtime.smack.inttest.util.ResultSyncPoint; +import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; + +/** + * Convenience class. This listener is used so that implementers of OmemoMessageListener don't have to implement + * both messages. Instead they can just overwrite the message they want to implement. + */ +public class AbstractOmemoMessageListener implements OmemoMessageListener { + + @Override + public void onOmemoMessageReceived(Stanza stanza, OmemoMessage.Received decryptedMessage) { + // Override me + } + + @Override + public void onOmemoCarbonCopyReceived(CarbonExtension.Direction direction, Message carbonCopy, Message wrappingMessage, OmemoMessage.Received decryptedCarbonCopy) { + // Override me + } + + private static class SyncPointListener extends AbstractOmemoMessageListener { + protected final ResultSyncPoint syncPoint; + + SyncPointListener(ResultSyncPoint syncPoint) { + this.syncPoint = syncPoint; + } + + public ResultSyncPoint getSyncPoint() { + return syncPoint; + } + } + + static class MessageListener extends SyncPointListener { + + protected final String expectedMessage; + + MessageListener(String expectedMessage, SimpleResultSyncPoint syncPoint) { + super(syncPoint); + this.expectedMessage = expectedMessage; + } + + MessageListener(String expectedMessage) { + this(expectedMessage, new SimpleResultSyncPoint()); + } + + @Override + public void onOmemoMessageReceived(Stanza stanza, OmemoMessage.Received received) { + SimpleResultSyncPoint srp = (SimpleResultSyncPoint) syncPoint; + if (received.isKeyTransportMessage()) { + return; + } + + if (received.getBody().equals(expectedMessage)) { + srp.signal(); + } else { + srp.signalFailure("Received decrypted message was not equal to sent message."); + } + } + } + + static class PreKeyMessageListener extends MessageListener { + PreKeyMessageListener(String expectedMessage, SimpleResultSyncPoint syncPoint) { + super(expectedMessage, syncPoint); + } + + PreKeyMessageListener(String expectedMessage) { + this(expectedMessage, new SimpleResultSyncPoint()); + } + + @Override + public void onOmemoMessageReceived(Stanza stanza, OmemoMessage.Received received) { + SimpleResultSyncPoint srp = (SimpleResultSyncPoint) syncPoint; + if (received.isKeyTransportMessage()) { + return; + } + + if (received.isPreKeyMessage()) { + if (received.getBody().equals(expectedMessage)) { + srp.signal(); + } else { + srp.signalFailure("Received decrypted message was not equal to sent message."); + } + } else { + srp.signalFailure("Received message was not a PreKeyMessage."); + } + } + } + + static class KeyTransportListener extends SyncPointListener { + + KeyTransportListener(SimpleResultSyncPoint resultSyncPoint) { + super(resultSyncPoint); + } + + KeyTransportListener() { + this(new SimpleResultSyncPoint()); + } + + @Override + public void onOmemoMessageReceived(Stanza stanza, OmemoMessage.Received received) { + SimpleResultSyncPoint s = (SimpleResultSyncPoint) syncPoint; + if (received.isKeyTransportMessage()) { + s.signal(); + } + } + } + + static class PreKeyKeyTransportListener extends KeyTransportListener { + PreKeyKeyTransportListener(SimpleResultSyncPoint resultSyncPoint) { + super(resultSyncPoint); + } + + PreKeyKeyTransportListener() { + this(new SimpleResultSyncPoint()); + } + + @Override + public void onOmemoMessageReceived(Stanza stanza, OmemoMessage.Received received) { + SimpleResultSyncPoint s = (SimpleResultSyncPoint) syncPoint; + if (received.isPreKeyMessage()) { + if (received.isKeyTransportMessage()) { + s.signal(); + } + } + } + } +} 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..b53b831bb --- /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() { + alice.stopStanzaAndPEPListeners(); + bob.stopStanzaAndPEPListeners(); + 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..63cccbf1b --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java @@ -0,0 +1,102 @@ +/** + * + * 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.assertNotEquals; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.omemo.element.OmemoBundleElement; + +import org.igniterealtime.smack.inttest.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; +import org.igniterealtime.smack.inttest.TestNotPossibleException; + +/** + * 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); + } + + /** + * This test checks whether the following actions are performed. + * + * Alice publishes bundle A1 + * Bob publishes bundle B1 + * + * Alice sends message to Bob (preKeyMessage) + * Bob publishes bundle B2 + * Alice still has A1 + * + * Bob responds to Alice (normal message) + * Alice still has A1 + * Bob still has B2 + * @throws Exception + */ + @SmackIntegrationTest + public void messageTest() throws Exception { + OmemoBundleElement a1 = alice.getOmemoService().getOmemoStoreBackend().packOmemoBundle(alice.getOwnDevice()); + OmemoBundleElement b1 = bob.getOmemoService().getOmemoStoreBackend().packOmemoBundle(bob.getOwnDevice()); + + // Alice sends message(s) to bob + // PreKeyMessage A -> B + final String body1 = "One is greater than zero (for small values of zero)."; + AbstractOmemoMessageListener.PreKeyMessageListener listener1 = + new AbstractOmemoMessageListener.PreKeyMessageListener(body1); + bob.addOmemoMessageListener(listener1); + OmemoMessage.Sent e1 = alice.encrypt(bob.getOwnJid(), body1); + alice.getConnection().sendStanza(e1.asMessage(bob.getOwnJid())); + listener1.getSyncPoint().waitForResult(10 * 1000); + bob.removeOmemoMessageListener(listener1); + + OmemoBundleElement a1_ = alice.getOmemoService().getOmemoStoreBackend().packOmemoBundle(alice.getOwnDevice()); + OmemoBundleElement b2; + + synchronized (bob.LOCK) { // Circumvent race condition where bundle gets replenished after getting stored in b2 + b2 = bob.getOmemoService().getOmemoStoreBackend().packOmemoBundle(bob.getOwnDevice()); + } + + assertEquals("Alice sent bob a preKeyMessage, so her bundle MUST still be the same.", a1, a1_); + assertNotEquals("Bob just received a preKeyMessage from alice, so his bundle must have changed.", b1, b2); + + // Message B -> A + final String body3 = "The german words for 'leek' and 'wimp' are the same."; + AbstractOmemoMessageListener.MessageListener listener3 = + new AbstractOmemoMessageListener.MessageListener(body3); + alice.addOmemoMessageListener(listener3); + OmemoMessage.Sent e3 = bob.encrypt(alice.getOwnJid(), body3); + bob.getConnection().sendStanza(e3.asMessage(alice.getOwnJid())); + listener3.getSyncPoint().waitForResult(10 * 1000); + alice.removeOmemoMessageListener(listener3); + + OmemoBundleElement a1__ = alice.getOmemoService().getOmemoStoreBackend().packOmemoBundle(alice.getOwnDevice()); + OmemoBundleElement b2_ = bob.getOmemoService().getOmemoStoreBackend().packOmemoBundle(bob.getOwnDevice()); + + assertEquals("Since alice initiated the session with bob, at no time he sent a preKeyMessage, " + + "so her bundle MUST still be the same.", a1_, a1__); + assertEquals("Bob changed his bundle earlier, but at this point his bundle must be equal to " + + "after the first change.", b2, b2_); + } +} 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/OmemoMamDecryptionTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMamDecryptionTest.java new file mode 100644 index 000000000..1ff1e6d33 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMamDecryptionTest.java @@ -0,0 +1,74 @@ +/** + * + * Copyright 2018 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 java.util.List; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.mam.MamManager; +import org.jivesoftware.smackx.mam.element.MamPrefsIQ; +import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException; +import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException; +import org.jivesoftware.smackx.omemo.util.MessageOrOmemoMessage; + +import org.igniterealtime.smack.inttest.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; +import org.igniterealtime.smack.inttest.TestNotPossibleException; + +/** + * This test sends a message from Alice to Bob, while Bob has automatic decryption disabled. + * Then Bob fetches his Mam archive and decrypts the result. + */ +public class OmemoMamDecryptionTest extends AbstractTwoUsersOmemoIntegrationTest { + public OmemoMamDecryptionTest(SmackIntegrationTestEnvironment environment) + throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException, TestNotPossibleException { + super(environment); + MamManager bobsMamManager = MamManager.getInstanceFor(conTwo); + if (!bobsMamManager.isSupported()) { + throw new TestNotPossibleException("Test is not possible, because MAM is not supported on the server."); + } + } + + @SmackIntegrationTest + public void mamDecryptionTest() throws XMPPException.XMPPErrorException, SmackException.NotLoggedInException, + SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, + CryptoFailedException, UndecidedOmemoIdentityException { + // Make sure, Bobs server stores messages in the archive + MamManager bobsMamManager = MamManager.getInstanceFor(bob.getConnection()); + bobsMamManager.enableMamForAllMessages(); + bobsMamManager.setDefaultBehavior(MamPrefsIQ.DefaultBehavior.always); + + // Prevent bob from automatically decrypting MAM messages. + bob.stopStanzaAndPEPListeners(); + + String body = "This message will be stored in MAM!"; + OmemoMessage.Sent encrypted = alice.encrypt(bob.getOwnJid(), body); + alice.getConnection().sendStanza(encrypted.asMessage(bob.getOwnJid())); + + MamManager.MamQuery query = bobsMamManager.queryArchive(MamManager.MamQueryArgs.builder().limitResultsToJid(alice.getOwnJid()).build()); + assertEquals(1, query.getMessageCount()); + + List decryptedMamQuery = bob.decryptMamQueryResult(query); + + assertEquals(1, decryptedMamQuery.size()); + assertEquals(body, decryptedMamQuery.get(decryptedMamQuery.size() - 1).getOmemoMessage().getBody()); + } +} 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..8f5556ba6 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java @@ -0,0 +1,235 @@ +/** + * + * 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.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.OmemoCachedDeviceList; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; +import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; +import org.jivesoftware.smackx.omemo.util.EphemeralTrustCallback; +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, + XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException { + 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, + XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException { + 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) { + PubSubManager pm = PubSubManager.getInstance(omemoManager.getConnection(),omemoManager.getOwnJid()); + try { + omemoManager.requestDeviceListUpdateFor(omemoManager.getOwnJid()); + } catch (SmackException.NotConnectedException | InterruptedException | SmackException.NoResponseException | PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException e) { + // ignore + } + + OmemoCachedDeviceList 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-integration-test/src/main/java/org/jivesoftware/smackx/omemo/SessionRenegotiationIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/SessionRenegotiationIntegrationTest.java new file mode 100644 index 000000000..90670b6be --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/SessionRenegotiationIntegrationTest.java @@ -0,0 +1,79 @@ +/** + * + * 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 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; + +public class SessionRenegotiationIntegrationTest extends AbstractTwoUsersOmemoIntegrationTest { + + public SessionRenegotiationIntegrationTest(SmackIntegrationTestEnvironment environment) + throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException, TestNotPossibleException { + super(environment); + } + + @SmackIntegrationTest + public void sessionRenegotiationTest() throws Exception { + + boolean prevRepairProperty = OmemoConfiguration.getRepairBrokenSessionsWithPreKeyMessages(); + OmemoConfiguration.setRepairBrokenSessionsWithPrekeyMessages(true); + boolean prevCompleteSessionProperty = OmemoConfiguration.getCompleteSessionWithEmptyMessage(); + OmemoConfiguration.setCompleteSessionWithEmptyMessage(false); + + // send PreKeyMessage -> Success + final String body1 = "P = NP is true for all N,P from the set of complex numbers, where P is equal to 0"; + AbstractOmemoMessageListener.PreKeyMessageListener listener1 = + new AbstractOmemoMessageListener.PreKeyMessageListener(body1); + OmemoMessage.Sent e1 = alice.encrypt(bob.getOwnJid(), body1); + bob.addOmemoMessageListener(listener1); + alice.getConnection().sendStanza(e1.asMessage(bob.getOwnJid())); + listener1.getSyncPoint().waitForResult(10 * 1000); + bob.removeOmemoMessageListener(listener1); + + // Remove the session on Bobs side. + synchronized (bob.LOCK) { + bob.getOmemoService().getOmemoStoreBackend().removeRawSession(bob.getOwnDevice(), alice.getOwnDevice()); + } + + // Send normal message -> fail, bob repairs session with preKeyMessage + final String body2 = "P = NP is also true for all N,P from the set of complex numbers, where N is equal to 1."; + AbstractOmemoMessageListener.PreKeyKeyTransportListener listener2 = + new AbstractOmemoMessageListener.PreKeyKeyTransportListener(); + OmemoMessage.Sent e2 = alice.encrypt(bob.getOwnJid(), body2); + alice.addOmemoMessageListener(listener2); + alice.getConnection().sendStanza(e2.asMessage(bob.getOwnJid())); + listener2.getSyncPoint().waitForResult(10 * 1000); + alice.removeOmemoMessageListener(listener2); + + // Send normal message -> success + final String body3 = "P = NP would be a disaster for the world of cryptography."; + AbstractOmemoMessageListener.MessageListener listener3 = new AbstractOmemoMessageListener.MessageListener(body3); + OmemoMessage.Sent e3 = alice.encrypt(bob.getOwnJid(), body3); + bob.addOmemoMessageListener(listener3); + alice.getConnection().sendStanza(e3.asMessage(bob.getOwnJid())); + listener3.getSyncPoint().waitForResult(10 * 1000); + bob.removeOmemoMessageListener(listener3); + + OmemoConfiguration.setRepairBrokenSessionsWithPrekeyMessages(prevRepairProperty); + OmemoConfiguration.setCompleteSessionWithEmptyMessage(prevCompleteSessionProperty); + } +} 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..084d95f2b --- /dev/null +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalCachingOmemoStore.java @@ -0,0 +1,60 @@ +/** + * + * 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 other store implementation that gets wrapped + */ + 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..f474b9b19 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,18 +42,15 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord; */ @SuppressWarnings("unused") public class SignalFileBasedOmemoStore - extends FileBasedOmemoStore { - - public SignalFileBasedOmemoStore() { - super(); - } + extends FileBasedOmemoStore { public SignalFileBasedOmemoStore(File base) { super(base); } @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..34568b47a 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,25 +21,18 @@ 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.element.OmemoBundleElement; 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.trust.OmemoFingerprint; import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil; -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.InvalidKeyException; -import org.whispersystems.libsignal.SessionCipher; -import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.state.PreKeyBundle; @@ -54,7 +47,7 @@ import org.whispersystems.libsignal.util.KeyHelper; * @author Paul Schaub */ public class SignalOmemoKeyUtil extends OmemoKeyUtil { + SessionRecord, ECPublicKey, PreKeyBundle> { @Override public IdentityKeyPair generateOmemoIdentityKeyPair() { @@ -62,17 +55,18 @@ public class SignalOmemoKeyUtil extends OmemoKeyUtil 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 +76,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 +98,7 @@ public class SignalOmemoKeyUtil extends OmemoKeyUtil { + + private static final Logger LOGGER = Logger.getLogger(OmemoRatchet.class.getName()); + private final SignalOmemoStoreConnector storeConnector; + + SignalOmemoRatchet(OmemoManager omemoManager, + OmemoStore store) { + super(omemoManager, store); + this.storeConnector = new SignalOmemoStoreConnector(omemoManager, 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(sender, 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(sender, 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) { + return new SessionCipher(storeConnector, storeConnector, storeConnector, storeConnector, + SignalOmemoStoreConnector.asAddress(device)); + } +} diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoService.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoService.java index 977fccbff..68f8f2d3a 100644 --- a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoService.java +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoService.java @@ -20,21 +20,8 @@ */ package org.jivesoftware.smackx.omemo.signal; -import java.io.UnsupportedEncodingException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; import java.util.logging.Level; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; - -import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.XMPPException.XMPPErrorException; - import org.jivesoftware.smackx.omemo.OmemoManager; import org.jivesoftware.smackx.omemo.OmemoService; import org.jivesoftware.smackx.omemo.OmemoStore; @@ -59,16 +46,27 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord; * @author Paul Schaub */ @SuppressWarnings("unused") -public final class SignalOmemoService extends OmemoService { +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 SignalOmemoRatchet instantiateOmemoRatchet( + OmemoManager manager, + OmemoStore store) { + + return new SignalOmemoRatchet(manager, getOmemoStoreBackend()); + } + + public static void setup() { 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,15 +77,13 @@ public final class SignalOmemoService extends OmemoService createDefaultOmemoStoreBackend() { - return new SignalFileBasedOmemoStore(); + public OmemoStore + createDefaultOmemoStoreBackend() { + return new SignalCachingOmemoStore(); } - private SignalOmemoService() - throws SmackException, InterruptedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException, - NoSuchPaddingException, InvalidAlgorithmParameterException, UnsupportedEncodingException, - IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, NoSuchProviderException, - java.security.InvalidKeyException { + private SignalOmemoService() { super(); } @@ -96,14 +92,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/SignalOmemoStore.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStore.java index e5faa976f..bde4f802a 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,13 @@ 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..bfb1a20d9 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 OmemoStore omemoStore; + private final OmemoManager omemoManager; - public SignalOmemoStoreConnector(OmemoManager omemoManager, OmemoStore store) { + public SignalOmemoStoreConnector(OmemoManager omemoManager, OmemoStore store) { this.omemoManager = omemoManager; this.omemoStore = store; } + OmemoDevice getOurDevice() { + return omemoManager.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 = asOmemoDevice(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,113 @@ 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 = asOmemoDevice(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 = asOmemoDevice(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 = asOmemoDevice(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 = asOmemoDevice(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!"); + SignedPreKeyRecord signedPreKeyRecord = omemoStore.loadOmemoSignedPreKey(getOurDevice(), i); + if (signedPreKeyRecord == null) { + throw new InvalidKeyIdException("No signed preKey with id " + i + " found."); } - return spkr; + return signedPreKeyRecord; } @Override public List loadSignedPreKeys() { - HashMap signedPreKeyRecordHashMap = omemoStore.loadOmemoSignedPreKeys(omemoManager); - List signedPreKeyRecordList = new ArrayList<>(); - signedPreKeyRecordList.addAll(signedPreKeyRecordHashMap.values()); - return signedPreKeyRecordList; + + TreeMap signedPreKeyRecordHashMap = + omemoStore.loadOmemoSignedPreKeys(getOurDevice()); + return new ArrayList<>(signedPreKeyRecordHashMap.values()); } @Override public void storeSignedPreKey(int i, SignedPreKeyRecord signedPreKeyRecord) { - omemoStore.storeOmemoSignedPreKey(omemoManager, i, signedPreKeyRecord); + omemoStore.storeOmemoSignedPreKey(getOurDevice(), i, signedPreKeyRecord); } @Override @@ -224,6 +258,14 @@ public class SignalOmemoStoreConnector @Override public void removeSignedPreKey(int i) { - omemoStore.removeOmemoSignedPreKey(omemoManager, i); + omemoStore.removeOmemoSignedPreKey(getOurDevice(), i); + } + + private static OmemoDevice asOmemoDevice(SignalProtocolAddress address) throws XmppStringprepException { + return new OmemoDevice(JidCreate.bareFrom(address.getName()), address.getDeviceId()); + } + + public static SignalProtocolAddress asAddress(OmemoDevice device) { + return new SignalProtocolAddress(device.getJid().toString(), device.getDeviceId()); } } diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoMessageBuilderTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoMessageBuilderTest.java deleted file mode 100644 index a8d3b3e19..000000000 --- a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoMessageBuilderTest.java +++ /dev/null @@ -1,91 +0,0 @@ -/** - * - * Copyright 2017 Paul Schaub - * - * This file is part of smack-omemo-signal. - * - * smack-omemo-signal is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ -package org.jivesoftware.smack.omemo; - -import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.CIPHERMODE; -import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE; -import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.PROVIDER; -import static org.junit.Assert.assertArrayEquals; - -import java.io.UnsupportedEncodingException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.Security; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -import org.jivesoftware.smack.test.util.SmackTestSuite; -import org.jivesoftware.smack.util.StringUtils; - -import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.junit.Test; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.SessionCipher; -import org.whispersystems.libsignal.SignalProtocolAddress; -import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.state.PreKeyBundle; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SessionRecord; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; - -/** - * Test the OmemoMessageBuilder. - */ -public class OmemoMessageBuilderTest extends SmackTestSuite { - - @Test - public void setTextTest() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, UnsupportedEncodingException, IllegalBlockSizeException, BadPaddingException, NoSuchProviderException, InvalidKeyException { - Security.addProvider(new BouncyCastleProvider()); - String message = "Hello World!"; - byte[] key = OmemoMessageBuilder.generateKey(); - byte[] iv = OmemoMessageBuilder.generateIv(); - - SecretKey secretKey = new SecretKeySpec(key, KEYTYPE); - IvParameterSpec ivSpec = new IvParameterSpec(iv); - Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER); - cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); - - OmemoMessageBuilder - mb = new OmemoMessageBuilder<>(null, null, key, iv); - mb.setMessage(message); - - byte[] expected = cipher.doFinal(message.getBytes(StringUtils.UTF8)); - byte[] messageKey = new byte[16]; - System.arraycopy(mb.getMessageKey(),0, messageKey, 0, 16); - byte[] messagePlusTag = new byte[mb.getCiphertextMessage().length + 16]; - System.arraycopy(mb.getCiphertextMessage(),0,messagePlusTag,0,mb.getCiphertextMessage().length); - System.arraycopy(mb.getMessageKey(), 16, messagePlusTag, mb.getCiphertextMessage().length, 16); - - assertArrayEquals(key, messageKey); - assertArrayEquals(expected, messagePlusTag); - } -} diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/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/smackx/omemo/LegacySignalOmemoKeyUtilTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/LegacySignalOmemoKeyUtilTest.java new file mode 100644 index 000000000..737d3b24c --- /dev/null +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/LegacySignalOmemoKeyUtilTest.java @@ -0,0 +1,93 @@ +/** + * + * 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; + +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.signal.SignalOmemoKeyUtil; + +import org.junit.Test; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.IdentityKeyPair; +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() 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() { + 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); + } +} diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoKeyUtilTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoKeyUtilTest.java new file mode 100644 index 000000000..d14e08ef5 --- /dev/null +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoKeyUtilTest.java @@ -0,0 +1,58 @@ +/** + * + * 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; + +import java.util.Arrays; +import java.util.Collection; + +import org.jivesoftware.smackx.omemo.signal.SignalOmemoKeyUtil; +import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.IdentityKeyPair; +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 OmemoKeyUtilTest}. + * This class executes tests of its super class with available implementations of {@link OmemoKeyUtil}. + * So far this includes {@link SignalOmemoKeyUtil}. + */ +@RunWith(value = Parameterized.class) +public class SignalOmemoKeyUtilTest + extends OmemoKeyUtilTest { + + public SignalOmemoKeyUtilTest(OmemoKeyUtil keyUtil) { + super(keyUtil); + } + + @Parameterized.Parameters + public static Collection getParameters() { + return Arrays.asList(new Object[][] { + { new SignalOmemoKeyUtil()} + }); + } +} diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoManagerTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoManagerTest.java similarity index 74% rename from smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoManagerTest.java rename to smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoManagerTest.java index f6f6429f8..64d07a9ff 100644 --- a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoManagerTest.java +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoManagerTest.java @@ -18,7 +18,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jivesoftware.smack.omemo; +package org.jivesoftware.smackx.omemo; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertFalse; @@ -26,26 +26,11 @@ import static junit.framework.TestCase.assertNotNull; import static junit.framework.TestCase.assertNotSame; import static junit.framework.TestCase.assertTrue; -import java.io.UnsupportedEncodingException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; - -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; - import org.jivesoftware.smack.DummyConnection; -import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.test.util.SmackTestSuite; import org.jivesoftware.smack.test.util.TestUtils; - -import org.jivesoftware.smackx.omemo.OmemoManager; import org.jivesoftware.smackx.omemo.element.OmemoElement; -import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; import org.jivesoftware.smackx.omemo.provider.OmemoVAxolotlProvider; import org.jivesoftware.smackx.omemo.signal.SignalOmemoService; @@ -54,10 +39,10 @@ import org.junit.Test; /** * Test OmemoManager functionality. */ -public class OmemoManagerTest extends SmackTestSuite { +public class SignalOmemoManagerTest extends SmackTestSuite { @Test - public void instantiationTest() throws CorruptedOmemoKeyException, NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException, InterruptedException, XMPPException.XMPPErrorException, NoSuchPaddingException, BadPaddingException, InvalidAlgorithmParameterException, NoSuchProviderException, IllegalBlockSizeException, SmackException { + public void instantiationTest() { SignalOmemoService.acknowledgeLicense(); SignalOmemoService.setup(); @@ -73,8 +58,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/SignalOmemoStoreConnectorTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreConnectorTest.java similarity index 94% rename from smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreConnectorTest.java rename to smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreConnectorTest.java index f7a474cdb..cffa0b621 100644 --- a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreConnectorTest.java +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreConnectorTest.java @@ -18,7 +18,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jivesoftware.smack.omemo; +package org.jivesoftware.smackx.omemo; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertTrue; @@ -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/smackx/omemo/SignalOmemoStoreTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreTest.java new file mode 100644 index 000000000..c39f72fa7 --- /dev/null +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreTest.java @@ -0,0 +1,83 @@ +/** + * + * 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; + +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; + +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/build.gradle b/smack-omemo/build.gradle index 9c16a3739..1715a1db4 100644 --- a/smack-omemo/build.gradle +++ b/smack-omemo/build.gradle @@ -9,5 +9,7 @@ dependencies { compile project(":smack-extensions") compile project(":smack-experimental") + compile "org.bouncycastle:bcprov-jdk15on:1.59" + testCompile project(path: ":smack-core", configuration: "testRuntime") } 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..ec4b23a46 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/CachingOmemoStore.java @@ -0,0 +1,446 @@ +/** + * + * 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.OmemoCachedDeviceList; +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) { + if (wrappedStore == null) { + throw new NullPointerException("Wrapped OmemoStore MUST NOT be null!"); + } + 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 setDateOfLastDeviceIdPublication(OmemoDevice userDevice, OmemoDevice contactsDevice, Date date) { + getCache(userDevice).lastDeviceIdPublicationDates.put(contactsDevice, date); + if (persistent != null) { + persistent.setDateOfLastReceivedMessage(userDevice, contactsDevice, date); + } + } + + @Override + public Date getDateOfLastDeviceIdPublication(OmemoDevice userDevice, OmemoDevice contactsDevice) { + Date last = getCache(userDevice).lastDeviceIdPublicationDates.get(contactsDevice); + + if (last == null && persistent != null) { + last = persistent.getDateOfLastDeviceIdPublication(userDevice, contactsDevice); + if (last != null) { + getCache(userDevice).lastDeviceIdPublicationDates.put(contactsDevice, 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 OmemoCachedDeviceList loadCachedDeviceList(OmemoDevice userDevice, BareJid contact) { + OmemoCachedDeviceList 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 OmemoCachedDeviceList() : new OmemoCachedDeviceList(list); + } + + @Override + public void storeCachedDeviceList(OmemoDevice userDevice, + BareJid contact, + OmemoCachedDeviceList deviceList) { + getCache(userDevice).deviceLists.put(contact, new OmemoCachedDeviceList(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 lastDeviceIdPublicationDates = 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..b7c5b00c9 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,20 +21,22 @@ 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.OmemoCachedDeviceList; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jxmpp.jid.BareJid; @@ -47,12 +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; - - public FileBasedOmemoStore() { - this(OmemoConfiguration.getFileBasedOmemoStoreDefaultPath()); - } + private static final Logger LOGGER = Logger.getLogger(FileBasedOmemoStore.class.getName()); public FileBasedOmemoStore(File basePath) { super(); @@ -63,335 +61,233 @@ 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 void setDateOfLastReceivedMessage(OmemoDevice userDevice, OmemoDevice contactsDevice, Date date) { + File lastMessageReceived = hierarchy.getLastMessageReceivedDatePath(userDevice, contactsDevice); + writeLong(lastMessageReceived, date.getTime()); + } + + @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 setDateOfLastDeviceIdPublication(OmemoDevice userDevice, OmemoDevice contactsDevice, Date date) { + File lastDeviceIdPublished = hierarchy.getLastDeviceIdPublicationDatePath(userDevice, contactsDevice); + writeLong(lastDeviceIdPublished, date.getTime()); + } + + @Override + public Date getDateOfLastDeviceIdPublication(OmemoDevice userDevice, OmemoDevice contactsDevice) { + File lastDeviceIdPublished = hierarchy.getLastDeviceIdPublicationDatePath(userDevice, contactsDevice); + Long date = readLong(lastDeviceIdPublished); + 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 +299,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, + OmemoCachedDeviceList 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 +480,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 +539,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 +572,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 +726,15 @@ public abstract class FileBasedOmemoStore> INSTANCES = new WeakHashMap<>(); + private static final Integer UNKNOWN_DEVICE_ID = -1; + final Object LOCK = new Object(); + + private static final WeakHashMap> INSTANCES = new WeakHashMap<>(); private final OmemoService service; private final HashSet omemoMessageListeners = new HashSet<>(); private final HashSet omemoMucMessageListeners = new HashSet<>(); - private OmemoService.OmemoStanzaListener omemoStanzaListener; - private OmemoService.OmemoCarbonCopyListener omemoCarbonCopyListener; + private OmemoTrustCallback 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!). + * Private constructor. * * @param connection connection + * @param deviceId deviceId */ - private OmemoManager(XMPPConnection connection, int deviceId) { + private OmemoManager(XMPPConnection connection, Integer deviceId) { super(connection); + service = OmemoService.getInstance(); + this.deviceId = deviceId; - connection.addConnectionListener(new AbstractConnectionListener() { - @Override - public void authenticated(XMPPConnection connection, boolean resumed) { - if (resumed) { - return; + if (connection.isAuthenticated()) { + initBareJidAndDeviceId(this); + } else { + connection.addConnectionListener(new AbstractConnectionListener() { + @Override + public void authenticated(XMPPConnection connection, boolean resumed) { + initBareJidAndDeviceId(OmemoManager.this); } - 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()); - } - } - }); - } - }); + }); + } - PEPManager.getInstanceFor(connection).addPEPListener(deviceListUpdateListener); + service.registerRatchetForManager(this); + + // StanzaListeners + resumeStanzaAndPEPListeners(); + + // 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. + * 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 Connection - * @param deviceId deviceId of the Manager. If the deviceId is null, a random id will be generated. - * @return an OmemoManager + * @param connection XmppConnection. + * @param deviceId MUST NOT be null and MUST be greater than 0. + * + * @return manager */ public static synchronized OmemoManager getInstanceFor(XMPPConnection connection, Integer deviceId) { - WeakHashMap managersOfConnection = INSTANCES.get(connection); - if (managersOfConnection == null) { - managersOfConnection = new WeakHashMap<>(); - INSTANCES.put(connection, managersOfConnection); + if (deviceId == null || deviceId < 1) { + throw new IllegalArgumentException("DeviceId MUST NOT be null and MUST be greater than 0."); } - if (deviceId == null || deviceId < 1) { - deviceId = randomDeviceId(); + TreeMap managersOfConnection = INSTANCES.get(connection); + if (managersOfConnection == null) { + managersOfConnection = new TreeMap<>(); + INSTANCES.put(connection, managersOfConnection); } OmemoManager manager = managersOfConnection.get(deviceId); @@ -161,42 +167,64 @@ 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. + * Returns an OmemoManager instance for the given connection. If there was one manager for the connection before, + * return it. If there were multiple managers before, return the one with the lowest deviceId. + * If there was no manager before, return a new one. As soon as the connection gets authenticated, the manager + * will look for local deviceIDs and select the lowest one as its id. If there are not local deviceIds, the manager + * will assign itself a random id. * - * @param connection connection - * @return OmemoManager + * @param connection XmppConnection. + * + * @return manager */ public static synchronized OmemoManager getInstanceFor(XMPPConnection connection) { - BareJid user; - if (connection.getUser() != null) { - user = connection.getUser().asBareJid(); + 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, UNKNOWN_DEVICE_ID); + managers.put(UNKNOWN_DEVICE_ID, 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. + * TrustCallbacks are used to query and modify trust decisions. + * + * @param callback trustCallback. + */ + public void setTrustCallback(OmemoTrustCallback callback) { + if (trustCallback != null) { + throw new IllegalStateException("TrustCallback can only be set once."); + } + trustCallback = callback; + } + + /** + * Return the TrustCallback of this manager. + * @return + */ + OmemoTrustCallback getTrustCallback() { + return trustCallback; + } + + /** + * Initializes the OmemoManager. This method must be called before the manager can be used. * * @throws CorruptedOmemoKeyException * @throws InterruptedException @@ -206,31 +234,85 @@ 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(); + } + + if (getTrustCallback() == null) { + throw new IllegalStateException("No TrustCallback set."); + } + + getOmemoService().init(new LoggedInOmemoManager(this)); + ServiceDiscoveryManager.getInstanceFor(connection()).addFeature(PEP_NODE_DEVICE_LIST_NOTIFY); + } + } + + /** + * Initialize the manager without blocking. Once the manager is successfully initialized, the finishedCallback will + * be notified. It will also get notified, if an error occurs. + * + * @param finishedCallback callback that gets called once the manager is initialized. + */ + public void initializeAsync(final InitializationFinishedCallback finishedCallback) { + Async.go(new Runnable() { + @Override + public void run() { + try { + initialize(); + finishedCallback.initializationFinished(OmemoManager.this); + } catch (Exception e) { + finishedCallback.initializationFailed(e); + } + } + }); + } + + /** + * Return a set of all OMEMO capable devices of a contact. + * Note, that this method does not explicitly refresh the device list of the contact, so it might be outdated. + * @see #requestDeviceListUpdateFor(BareJid) + * @param contact contact we want to get a set of device of. + * @return set of known devices of that contact. + */ + public Set getDevicesOf(BareJid contact) { + OmemoCachedDeviceList list = getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(getOwnDevice(), contact); + HashSet devices = new HashSet<>(); + + for (int deviceId : list.getActiveDevices()) { + devices.add(new OmemoDevice(contact, deviceId)); + } + + return devices; } /** * OMEMO encrypt a cleartext message for a single recipient. + * Note that this method does NOT set the 'to' attribute of the message. * - * @param to recipients barejid + * @param recipient recipients bareJid * @param message text to encrypt * @return encrypted message * @throws CryptoFailedException when something crypto related fails * @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 SmackException.NotConnectedException * @throws SmackException.NoResponseException + * @throws SmackException.NotLoggedInException */ - 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 OmemoMessage.Sent encrypt(BareJid recipient, String message) + throws CryptoFailedException, UndecidedOmemoIdentityException, + InterruptedException, SmackException.NotConnectedException, + SmackException.NoResponseException, SmackException.NotLoggedInException { + synchronized (LOCK) { + Set recipients = new HashSet<>(); + recipients.add(recipient); + return encrypt(recipients, message); + } } /** @@ -241,18 +323,23 @@ public final class OmemoManager extends Manager { * @return encrypted message. * @throws CryptoFailedException When something crypto related fails * @throws UndecidedOmemoIdentityException When there are undecided devices. - * @throws NoSuchAlgorithmException * @throws InterruptedException - * @throws CannotEstablishOmemoSessionException When there is one recipient, for whom we failed to create a session - * with every one of their devices. * @throws SmackException.NotConnectedException * @throws SmackException.NoResponseException + * @throws SmackException.NotLoggedInException */ - 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 OmemoMessage.Sent encrypt(Set recipients, String message) + throws CryptoFailedException, UndecidedOmemoIdentityException, + InterruptedException, SmackException.NotConnectedException, + SmackException.NoResponseException, SmackException.NotLoggedInException { + synchronized (LOCK) { + LoggedInOmemoManager guard = new LoggedInOmemoManager(this); + Set devices = getDevicesOf(getOwnJid()); + for (BareJid recipient : recipients) { + devices.addAll(getDevicesOf(recipient)); + } + return service.createOmemoMessage(guard, devices, message); + } } /** @@ -262,91 +349,80 @@ public final class OmemoManager extends Manager { * @param message message to send * @return encrypted message * @throws UndecidedOmemoIdentityException when there are undecided devices. - * @throws NoSuchAlgorithmException * @throws CryptoFailedException * @throws XMPPException.XMPPErrorException * @throws SmackException.NotConnectedException * @throws InterruptedException * @throws SmackException.NoResponseException * @throws NoOmemoSupportException When the muc doesn't support OMEMO. - * @throws CannotEstablishOmemoSessionException when there is a user for whom we could not create a session - * with any of their devices. + * @throws SmackException.NotLoggedInException */ - 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 OmemoMessage.Sent encrypt(MultiUserChat muc, String message) + throws UndecidedOmemoIdentityException, CryptoFailedException, + XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException, NoOmemoSupportException, + SmackException.NotLoggedInException { + synchronized (LOCK) { + if (!multiUserChatSupportsOmemo(muc)) { + throw new NoOmemoSupportException(); + } + + Set recipients = new HashSet<>(); + + 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); } /** - * Encrypt a message for all users we could build a session with successfully in a previous attempt. - * This method can come in handy as a fallback when encrypting a message fails due to devices we cannot - * build a session with. + * Manually decrypt an OmemoElement. + * This method should only be used for use-cases, where the internal listeners don't pick up on an incoming message. + * (for example MAM query results). * - * @param exception CannotEstablishSessionException from a previous encrypt(user(s), message) call. - * @param message message we want to send. - * @return encrypted message - * @throws CryptoFailedException - * @throws UndecidedOmemoIdentityException when there are undecided identities. + * @param sender bareJid of the message sender (must be the jid of the contact who sent the message) + * @param omemoElement omemoElement + * @return decrypted OmemoMessage + * + * @throws SmackException.NotLoggedInException if the Manager is not authenticated + * @throws CorruptedOmemoKeyException if our or their key is corrupted + * @throws NoRawSessionException if the message was not a preKeyMessage, but we had no session with the contact + * @throws CryptoFailedException if decryption fails */ - 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 OmemoMessage.Received decrypt(BareJid sender, OmemoElement omemoElement) + throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, NoRawSessionException, + CryptoFailedException { + LoggedInOmemoManager managerGuard = new LoggedInOmemoManager(this); + return getOmemoService().decryptMessage(managerGuard, sender, omemoElement); } /** - * Decrypt an OMEMO message. This method comes handy when dealing with messages that were not automatically - * decrypted by smack-omemo, eg. MAM query messages. - * @param sender sender of the message - * @param omemoMessage message - * @return decrypted message - * @throws InterruptedException Exception - * @throws SmackException.NoResponseException Exception - * @throws SmackException.NotConnectedException Exception - * @throws CryptoFailedException When decryption fails - * @throws XMPPException.XMPPErrorException Exception - * @throws CorruptedOmemoKeyException When the used keys are invalid - * @throws NoRawSessionException When there is no double ratchet session found for this message - */ - public ClearTextMessage decrypt(BareJid sender, Message omemoMessage) throws InterruptedException, SmackException.NoResponseException, SmackException.NotConnectedException, CryptoFailedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException, NoRawSessionException { - return getOmemoService().processLocalMessage(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. + * Decrypt messages from a MAM query. * * @param mamQuery The MAM query * @return list of decrypted OmemoMessages - * @throws InterruptedException Exception - * @throws XMPPException.XMPPErrorException Exception - * @throws SmackException.NotConnectedException Exception - * @throws SmackException.NoResponseException Exception + * @throws SmackException.NotLoggedInException if the Manager is not authenticated. */ - public List decryptMamQueryResult(MamManager.MamQuery mamQuery) throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException { - List l = new ArrayList<>(); - l.addAll(getOmemoService().decryptMamQueryResult(this, mamQuery)); - return l; + public List decryptMamQueryResult(MamManager.MamQuery mamQuery) + throws SmackException.NotLoggedInException { + return new ArrayList<>(getOmemoService().decryptMamQueryResult(new LoggedInOmemoManager(this), mamQuery)); } /** * Trust that a fingerprint belongs to an OmemoDevice. * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must * be of length 64. + * * @param device device * @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 +433,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 +449,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,34 +465,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."); + } - /** - * Clear all other devices except this one from our device list and republish the list. - * - * @throws InterruptedException - * @throws SmackException - * @throws XMPPException.XMPPErrorException - * @throws CorruptedOmemoKeyException - */ - public void purgeDevices() throws SmackException, InterruptedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException { - getOmemoService().publishDeviceIdIfNeeded(this,true); - getOmemoService().publishBundle(this); - } - - /** - * Generate fresh identity keys and bundle and publish it to the server. - * @throws SmackException - * @throws InterruptedException - * @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); + return trustCallback.getTrust(device, fingerprint) != TrustState.undecided; } /** @@ -416,68 +477,32 @@ public final class OmemoManager extends Manager { * secrecy. * * @param recipient recipient - * @throws UndecidedOmemoIdentityException When the trust of session with the recipient is not decided yet * @throws CorruptedOmemoKeyException When the used identityKeys are corrupted * @throws CryptoFailedException When something fails with the crypto * @throws CannotEstablishOmemoSessionException When we can't establish a session with the recipient + * @throws SmackException.NotLoggedInException + * @throws InterruptedException + * @throws SmackException.NoResponseException + * @throws NoSuchAlgorithmException + * @throws SmackException.NotConnectedException */ public void sendRatchetUpdateMessage(OmemoDevice recipient) - throws CorruptedOmemoKeyException, UndecidedOmemoIdentityException, CryptoFailedException, - CannotEstablishOmemoSessionException { - getOmemoService().sendOmemoRatchetUpdateMessage(this, recipient, false); - } + throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, InterruptedException, + SmackException.NoResponseException, NoSuchAlgorithmException, SmackException.NotConnectedException, + CryptoFailedException, CannotEstablishOmemoSessionException { + synchronized (LOCK) { + Message message = new Message(); + message.setFrom(getOwnJid()); + message.setTo(recipient.getJid()); - /** - * Create a new KeyTransportElement. This message will contain the AES-Key and IV that can be used eg. for encrypted - * Jingle file transfer. - * - * @param aesKey AES key to transport - * @param iv Initialization vector - * @param to list of recipient devices - * @return KeyTransportMessage - * @throws UndecidedOmemoIdentityException When the trust of session with the recipient is not decided yet - * @throws CorruptedOmemoKeyException When the used identityKeys are corrupted - * @throws CryptoFailedException When something fails with the crypto - * @throws CannotEstablishOmemoSessionException When we can't establish a session with the recipient - */ - public OmemoVAxolotlElement createKeyTransportElement(byte[] aesKey, byte[] iv, OmemoDevice ... to) - throws UndecidedOmemoIdentityException, CorruptedOmemoKeyException, CryptoFailedException, - CannotEstablishOmemoSessionException { - return getOmemoService().prepareOmemoKeyTransportElement(this, aesKey, iv, to); - } + OmemoElement element = getOmemoService() + .createRatchetUpdateElement(new LoggedInOmemoManager(this), recipient); + message.addExtension(element); - /** - * Create a new Message from a encrypted OmemoMessageElement. - * Add ourselves as the sender and the encrypted element. - * Also tell the server to store the message despite a possible missing body. - * The body will be set to a hint message that we are using OMEMO. - * - * @param encrypted OmemoMessageElement - * @return Message containing the OMEMO element and some additional information - */ - Message finishMessage(OmemoVAxolotlElement encrypted) { - if (encrypted == null) { - return null; + // Set MAM Storage hint + StoreHint.set(message); + connection().sendStanza(message); } - - Message chatMessage = new Message(); - chatMessage.setFrom(connection().getUser().asBareJid()); - chatMessage.addExtension(encrypted); - - if (OmemoConfiguration.getAddOmemoHintBody()) { - chatMessage.setBody(BODY_OMEMO_HINT); - } - - if (OmemoConfiguration.getAddMAMStorageProcessingHint()) { - StoreHint.set(chatMessage); - } - - if (OmemoConfiguration.getAddEmeEncryptionHint()) { - chatMessage.addExtension(new ExplicitMessageEncryptionElement( - ExplicitMessageEncryptionElement.ExplicitMessageEncryptionProtocol.omemoVAxolotl)); - } - - return chatMessage; } /** @@ -488,26 +513,34 @@ public final class OmemoManager extends Manager { * @throws SmackException.NotConnectedException * @throws InterruptedException * @throws SmackException.NoResponseException + * @throws PubSubException.NotALeafNodeException + * @throws XMPPException.XMPPErrorException */ - 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 InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, SmackException.NoResponseException { + synchronized (LOCK) { + OmemoCachedDeviceList deviceList = getOmemoService().refreshDeviceList(connection(), getOwnDevice(), contact); + return !deviceList.getActiveDevices().isEmpty(); + } } /** * Returns true, if the MUC with the EntityBareJid multiUserChat is non-anonymous and members only (prerequisite * for OMEMO encryption in MUC). * - * @param multiUserChat EntityBareJid of the MUC + * @param multiUserChat MUC * @return true if chat supports OMEMO * @throws XMPPException.XMPPErrorException if * @throws SmackException.NotConnectedException something * @throws InterruptedException goes * @throws SmackException.NoResponseException wrong */ - public boolean multiUserChatSupportsOmemo(EntityBareJid multiUserChat) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { - RoomInfo roomInfo = MultiUserChatManager.getInstanceFor(connection()).getRoomInfo(multiUserChat); + public boolean multiUserChatSupportsOmemo(MultiUserChat multiUserChat) + throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException { + EntityBareJid jid = multiUserChat.getRoom(); + RoomInfo roomInfo = MultiUserChatManager.getInstanceFor(connection()).getRoomInfo(jid); return roomInfo.isNonanonymous() && roomInfo.isMembersOnly(); } @@ -522,111 +555,195 @@ 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); } /** * Return the fingerprint of our identity key. * * @return fingerprint + * @throws SmackException.NotLoggedInException if we don't know our bareJid yet. + * @throws CorruptedOmemoKeyException if our identityKey is corrupted. */ - public OmemoFingerprint getOurFingerprint() { - return getOmemoService().getOmemoStoreBackend().getFingerprint(this); - } + public OmemoFingerprint getOwnFingerprint() + throws SmackException.NotLoggedInException, CorruptedOmemoKeyException { + synchronized (LOCK) { + if (getOwnJid() == null) { + throw new SmackException.NotLoggedInException(); + } - public OmemoFingerprint getFingerprint(OmemoDevice device) throws CannotEstablishOmemoSessionException { - if (device.equals(getOwnDevice())) { - return getOurFingerprint(); + return getOmemoService().getOmemoStoreBackend().getFingerprint(getOwnDevice()); } - - return getOmemoService().getOmemoStoreBackend().getFingerprint(this, device); } /** - * Return all fingerprints of active devices of a contact. - * @param contact contact - * @return HashMap of deviceIds and corresponding fingerprints. + * Get the fingerprint of a contacts device. + * @param device contacts OmemoDevice + * @return fingerprint + * @throws CannotEstablishOmemoSessionException if we have no session yet, and are unable to create one. + * @throws SmackException.NotLoggedInException + * @throws CorruptedOmemoKeyException if the copy of the fingerprint we have is corrupted. + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException */ - public 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 OmemoFingerprint getFingerprint(OmemoDevice device) + throws CannotEstablishOmemoSessionException, SmackException.NotLoggedInException, + CorruptedOmemoKeyException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException { + synchronized (LOCK) { + if (getOwnJid() == null) { + throw new SmackException.NotLoggedInException(); } - if (fingerprint != null) { - fingerprints.put(device, fingerprint); + if (device.equals(getOwnDevice())) { + return getOwnFingerprint(); } + + return getOmemoService().getOmemoStoreBackend().getFingerprintAndMaybeBuildSession(new LoggedInOmemoManager(this), device); } - return fingerprints; } + /** + * Return all OmemoFingerprints of active devices of a contact. + * TODO: Make more fail-safe + * @param contact contact + * @return Map of all active devices of the contact and their fingerprints. + * + * @throws SmackException.NotLoggedInException + * @throws CorruptedOmemoKeyException + * @throws CannotEstablishOmemoSessionException + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + */ + public HashMap getActiveFingerprints(BareJid contact) + throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, + CannotEstablishOmemoSessionException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException { + synchronized (LOCK) { + if (getOwnJid() == null) { + throw new SmackException.NotLoggedInException(); + } + + HashMap fingerprints = new HashMap<>(); + OmemoCachedDeviceList deviceList = getOmemoService().getOmemoStoreBackend() + .loadCachedDeviceList(getOwnDevice(), contact); + + for (int id : deviceList.getActiveDevices()) { + OmemoDevice device = new OmemoDevice(contact, id); + OmemoFingerprint fingerprint = getFingerprint(device); + + if (fingerprint != null) { + fingerprints.put(device, fingerprint); + } + } + + return fingerprints; + } + } + + /** + * Add an OmemoMessageListener. This listener will be informed about incoming OMEMO messages + * (as well as KeyTransportMessages) and OMEMO encrypted message carbons. + * + * @param listener OmemoMessageListener + */ public void addOmemoMessageListener(OmemoMessageListener listener) { omemoMessageListeners.add(listener); } + /** + * Remove an OmemoMessageListener. + * @param listener OmemoMessageListener + */ public void removeOmemoMessageListener(OmemoMessageListener listener) { omemoMessageListeners.remove(listener); } + /** + * Add an OmemoMucMessageListener. This listener will be informed about incoming OMEMO encrypted MUC messages. + * + * @param listener OmemoMessageListener. + */ public void addOmemoMucMessageListener(OmemoMucMessageListener listener) { omemoMucMessageListeners.add(listener); } + /** + * Remove an OmemoMucMessageListener. + * @param listener OmemoMucMessageListener + */ public void removeOmemoMucMessageListener(OmemoMucMessageListener listener) { omemoMucMessageListeners.remove(listener); } - /** - * Build OMEMO sessions with devices of contact. - * - * @param contact contact we want to build session with. - * @throws InterruptedException - * @throws CannotEstablishOmemoSessionException - * @throws SmackException.NotConnectedException - * @throws SmackException.NoResponseException - */ - public void buildSessionsWith(BareJid contact) throws InterruptedException, CannotEstablishOmemoSessionException, SmackException.NotConnectedException, SmackException.NoResponseException { - getOmemoService().buildOrCreateOmemoSessionsFromBundles(this, contact); - } - /** * Request a deviceList update from contact contact. * * @param contact contact we want to obtain the deviceList from. - * @throws SmackException.NotConnectedException * @throws InterruptedException + * @throws PubSubException.NotALeafNodeException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException * @throws SmackException.NoResponseException */ - public void requestDeviceListUpdateFor(BareJid contact) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { - getOmemoService().refreshDeviceList(this, contact); + public void requestDeviceListUpdateFor(BareJid contact) + throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, SmackException.NoResponseException { + synchronized (LOCK) { + getOmemoService().refreshDeviceList(connection(), getOwnDevice(), contact); + } } /** - * Rotate the signedPreKey published in our OmemoBundle. This should be done every now and then (7-14 days). - * The old signedPreKey should be kept for some more time (a month or so) to enable decryption of messages - * that have been sent since the key was changed. + * Publish a new device list with just our own deviceId in it. + * + * @throws SmackException.NotLoggedInException + * @throws InterruptedException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + */ + public void purgeDeviceList() + throws SmackException.NotLoggedInException, InterruptedException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, SmackException.NoResponseException { + synchronized (LOCK) { + getOmemoService().purgeDeviceList(new LoggedInOmemoManager(this)); + } + } + + /** + * Rotate the signedPreKey published in our OmemoBundle and republish it. This should be done every now and + * then (7-14 days). The old signedPreKey should be kept for some more time (a month or so) to enable decryption + * of messages that have been sent since the key was changed. * * @throws CorruptedOmemoKeyException When the IdentityKeyPair is damaged. * @throws InterruptedException XMPP error * @throws XMPPException.XMPPErrorException XMPP error * @throws SmackException.NotConnectedException XMPP error * @throws SmackException.NoResponseException XMPP error - * @throws PubSubException.NotALeafNodeException if the bundle node on the server is a CollectionNode + * @throws SmackException.NotLoggedInException */ - 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, SmackException.NotLoggedInException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { + synchronized (LOCK) { + if (!connection().isAuthenticated()) { + throw new SmackException.NotLoggedInException(); + } + + // generate key + getOmemoService().getOmemoStoreBackend().changeSignedPreKey(getOwnDevice()); + + // publish + OmemoBundleElement bundle = getOmemoService().getOmemoStoreBackend().packOmemoBundle(getOwnDevice()); + OmemoService.publishBundle(connection(), getOwnDevice(), bundle); + } } /** @@ -634,8 +751,8 @@ public final class OmemoManager extends Manager { * @param stanza stanza * @return true if stanza has extension 'encrypted' */ - public static boolean stanzaContainsOmemoElement(Stanza stanza) { - return stanza.hasExtension(OmemoElement.ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); + static boolean stanzaContainsOmemoElement(Stanza stanza) { + return stanza.hasExtension(OmemoElement.NAME_ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); } /** @@ -647,14 +764,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 +778,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 +790,10 @@ public final class OmemoManager extends Manager { * * @return deviceId */ - public int getDeviceId() { - return deviceId; + public Integer getDeviceId() { + synchronized (LOCK) { + return deviceId; + } } /** @@ -683,71 +802,125 @@ 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()); + } } + /** + * Set the deviceId of the manager to nDeviceId. + * @param nDeviceId new deviceId + */ 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; + } } /** * Notify all registered OmemoMessageListeners about a received OmemoMessage. * - * @param decryptedBody decrypted Body element of the message - * @param encryptedMessage unmodified message as it was received - * @param wrappingMessage message that wrapped the incoming message - * @param messageInformation information about the messages encryption (used identityKey, carbon...) + * @param stanza original stanza + * @param decryptedMessage decrypted OmemoMessage. */ - void notifyOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation messageInformation) { + void notifyOmemoMessageReceived(Stanza stanza, OmemoMessage.Received decryptedMessage) { for (OmemoMessageListener l : omemoMessageListeners) { - l.onOmemoMessageReceived(decryptedBody, encryptedMessage, wrappingMessage, messageInformation); - } - } - - void notifyOmemoKeyTransportMessageReceived(CipherAndAuthTag cipherAndAuthTag, Message transportingMessage, - Message wrappingMessage, OmemoMessageInformation information) { - for (OmemoMessageListener l : omemoMessageListeners) { - l.onOmemoKeyTransportReceived(cipherAndAuthTag, transportingMessage, wrappingMessage, information); + l.onOmemoMessageReceived(stanza, decryptedMessage); } } /** * Notify all registered OmemoMucMessageListeners of an incoming OmemoMessageElement in a MUC. * - * @param muc MultiUserChat the message was received in - * @param from BareJid of the user that sent the message - * @param decryptedBody decrypted body - * @param message original message with encrypted content - * @param wrappingMessage wrapping message (in case of carbon copy) - * @param omemoInformation information about the encryption of the message + * @param muc MultiUserChat the message was received in. + * @param stanza Original Stanza. + * @param decryptedMessage Decrypted OmemoMessage. */ - void notifyOmemoMucMessageReceived(MultiUserChat muc, BareJid from, String decryptedBody, Message message, - Message wrappingMessage, OmemoMessageInformation omemoInformation) { + void notifyOmemoMucMessageReceived(MultiUserChat muc, + Stanza stanza, + OmemoMessage.Received decryptedMessage) { 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) { - for (OmemoMucMessageListener l : omemoMucMessageListeners) { - l.onOmemoKeyTransportReceived(muc, from, cipherAndAuthTag, - transportingMessage, wrappingMessage, messageInformation); + l.onOmemoMucMessageReceived(muc, stanza, decryptedMessage); } } /** - * Remove all active stanza listeners of this manager from the connection. - * This is somewhat the counterpart of initialize(). + * Notify all registered OmemoMessageListeners of an incoming OMEMO encrypted Carbon Copy. + * Remember: If you want to receive OMEMO encrypted carbon copies, you have to enable carbons using + * {@link CarbonManager#enableCarbons()}. + * + * @param direction direction of the carbon copy + * @param carbonCopy carbon copy itself + * @param wrappingMessage wrapping message + * @param decryptedCarbonCopy decrypted carbon copy OMEMO element */ - public void shutdown() { + void notifyOmemoCarbonCopyReceived(CarbonExtension.Direction direction, + Message carbonCopy, + Message wrappingMessage, + OmemoMessage.Received decryptedCarbonCopy) { + for (OmemoMessageListener l : omemoMessageListeners) { + l.onOmemoCarbonCopyReceived(direction, carbonCopy, wrappingMessage, decryptedCarbonCopy); + } + } + + /** + * Register stanza listeners needed for OMEMO. + * This method is called automatically in the constructor and should only be used to restore the previous state + * after {@link #stopStanzaAndPEPListeners()} was called. + */ + public void resumeStanzaAndPEPListeners() { + PEPManager pepManager = PEPManager.getInstanceFor(connection()); + CarbonManager carbonManager = CarbonManager.getInstanceFor(connection()); + + // Remove listeners to avoid them getting added twice + connection().removeAsyncStanzaListener(internalOmemoMessageStanzaListener); + carbonManager.removeCarbonCopyReceivedListener(internalOmemoCarbonCopyListener); + pepManager.removePEPListener(deviceListUpdateListener); + + // Add listeners + pepManager.addPEPListener(deviceListUpdateListener); + connection().addAsyncStanzaListener(internalOmemoMessageStanzaListener, omemoMessageStanzaFilter); + carbonManager.addCarbonCopyReceivedListener(internalOmemoCarbonCopyListener); + } + + /** + * Remove active stanza listeners needed for OMEMO. + */ + public void stopStanzaAndPEPListeners() { PEPManager.getInstanceFor(connection()).removePEPListener(deviceListUpdateListener); - connection().removeAsyncStanzaListener(omemoStanzaListener); - CarbonManager.getInstanceFor(connection()).removeCarbonCopyReceivedListener(omemoCarbonCopyListener); + connection().removeAsyncStanzaListener(internalOmemoMessageStanzaListener); + CarbonManager.getInstanceFor(connection()).removeCarbonCopyReceivedListener(internalOmemoCarbonCopyListener); + } + + /** + * Build a fresh session with a contacts device. + * This might come in handy if a session is broken. + * + * @param contactsDevice OmemoDevice of a contact. + * + * @throws InterruptedException + * @throws SmackException.NoResponseException + * @throws CorruptedOmemoKeyException if our or their identityKey is corrupted. + * @throws SmackException.NotConnectedException + * @throws CannotEstablishOmemoSessionException if no new session can be established. + * @throws SmackException.NotLoggedInException if the connection is not authenticated. + */ + public void rebuildSessionWith(OmemoDevice contactsDevice) + throws InterruptedException, SmackException.NoResponseException, CorruptedOmemoKeyException, + SmackException.NotConnectedException, CannotEstablishOmemoSessionException, + SmackException.NotLoggedInException { + if (!connection().isAuthenticated()) { + throw new SmackException.NotLoggedInException(); + } + getOmemoService().buildFreshSessionWithDevice(connection(), getOwnDevice(), contactsDevice); } /** @@ -769,86 +942,185 @@ public final class OmemoManager extends Manager { return service; } - PEPListener deviceListUpdateListener = new PEPListener() { + /** + * StanzaListener that listens for incoming Stanzas which contain OMEMO elements. + */ + private final StanzaListener internalOmemoMessageStanzaListener = new StanzaListener() { + + @Override + public void processStanza(final Stanza packet) { + Async.go(new Runnable() { + @Override + public void run() { + try { + getOmemoService().onOmemoMessageStanzaReceived(packet, + new LoggedInOmemoManager(OmemoManager.this)); + } catch (SmackException.NotLoggedInException e) { + LOGGER.warning("Received OMEMO stanza while being offline: " + e); + } + } + }); + } + }; + + /** + * CarbonCopyListener that listens for incoming carbon copies which contain OMEMO elements. + */ + private final CarbonCopyReceivedListener internalOmemoCarbonCopyListener = new CarbonCopyReceivedListener() { + @Override + public void onCarbonCopyReceived(final CarbonExtension.Direction direction, + final Message carbonCopy, + final Message wrappingMessage) { + Async.go(new Runnable() { + @Override + public void run() { + if (omemoMessageStanzaFilter.accept(carbonCopy)) { + try { + getOmemoService().onOmemoCarbonCopyReceived(direction, carbonCopy, wrappingMessage, + new LoggedInOmemoManager(OmemoManager.this)); + } catch (SmackException.NotLoggedInException e) { + LOGGER.warning("Received OMEMO carbon copy while being offline: " + e); + } + } + } + }); + } + }; + + /** + * PEPListener that listens for OMEMO deviceList updates. + */ + private final PEPListener deviceListUpdateListener = new PEPListener() { @Override public void eventReceived(EntityBareJid from, EventElement event, Message message) { + + // Unknown sender, no more work to do. + if (from == null) { + // TODO: This DOES happen for some reason. Figure out when... + return; + } + for (ExtensionElement items : event.getExtensions()) { if (!(items instanceof ItemsExtension)) { continue; } - for (NamedElement item : ((ItemsExtension) items).getItems()) { + for (ExtensionElement item : ((ItemsExtension) items).getExtensions()) { if (!(item instanceof PayloadItem)) { continue; } PayloadItem payloadItem = (PayloadItem) item; - if (!(payloadItem.getPayload() instanceof OmemoDeviceListVAxolotlElement)) { + if (!(payloadItem.getPayload() instanceof OmemoDeviceListElement)) { continue; } // Device List - OmemoDeviceListVAxolotlElement omemoDeviceListElement = (OmemoDeviceListVAxolotlElement) payloadItem.getPayload(); - int ourDeviceId = getDeviceId(); - getOmemoService().getOmemoStoreBackend().mergeCachedDeviceList(OmemoManager.this, from, omemoDeviceListElement); + OmemoDeviceListElement receivedDeviceList = (OmemoDeviceListElement) payloadItem.getPayload(); + getOmemoService().getOmemoStoreBackend().mergeCachedDeviceList(getOwnDevice(), from, receivedDeviceList); - if (from == null) { - // Unknown sender, no more work to do. - // TODO: This DOES happen for some reason. Figure out when... + if (!from.asBareJid().equals(getOwnJid())) { continue; } - if (!from.equals(getOwnJid())) { - // Not our deviceList, so nothing more to do - continue; - } + OmemoCachedDeviceList deviceList = getOmemoService().cleanUpDeviceList(getOwnDevice()); + final OmemoDeviceListElement_VAxolotl newDeviceList = new OmemoDeviceListElement_VAxolotl(deviceList); - if (omemoDeviceListElement.getDeviceIds().contains(ourDeviceId)) { - // We are on the list. Nothing more to do - continue; - } - - // Our deviceList and we are not on it! We don't want to miss all the action!!! - LOGGER.log(Level.INFO, "Our deviceId was not on the list!"); - Set deviceListIds = omemoDeviceListElement.copyDeviceIds(); - // enroll at the deviceList - deviceListIds.add(ourDeviceId); - final OmemoDeviceListVAxolotlElement newOmemoDeviceListElement = new OmemoDeviceListVAxolotlElement(deviceListIds); - - // PEPListener is a synchronous listener. Avoid any deadlocks by using an async task to update the device list. - Async.go(new Runnable() { - @Override - public void run() { - try { - OmemoService.publishDeviceIds(OmemoManager.this, newOmemoDeviceListElement); + if (!newDeviceList.copyDeviceIds().equals(receivedDeviceList.copyDeviceIds())) { + LOGGER.log(Level.FINE, "Republish deviceList due to changes:" + + " Received: " + Arrays.toString(receivedDeviceList.copyDeviceIds().toArray()) + + " Published: " + Arrays.toString(newDeviceList.copyDeviceIds().toArray())); + Async.go(new Runnable() { + @Override + public void run() { + try { + OmemoService.publishDeviceList(connection(), newDeviceList); + } catch (InterruptedException | XMPPException.XMPPErrorException | + SmackException.NotConnectedException | SmackException.NoResponseException e) { + LOGGER.log(Level.WARNING, "Could not publish our deviceList upon an received update.", e); + } } - 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()); - } - } - }); + }); + } } } } }; - - - OmemoService.OmemoStanzaListener getOmemoStanzaListener() { - if (omemoStanzaListener == null) { - omemoStanzaListener = getOmemoService().createStanzaListener(this); + /** + * StanzaFilter that filters messages containing a OMEMO element. + */ + private final StanzaFilter omemoMessageStanzaFilter = new StanzaFilter() { + @Override + public boolean accept(Stanza stanza) { + return stanza instanceof Message && OmemoManager.stanzaContainsOmemoElement(stanza); + } + }; + + /** + * Guard class which ensures that the wrapped OmemoManager knows its BareJid. + */ + public static class LoggedInOmemoManager { + + private final OmemoManager manager; + + public LoggedInOmemoManager(OmemoManager manager) + throws SmackException.NotLoggedInException { + + if (manager == null) { + throw new IllegalArgumentException("OmemoManager cannot be null."); + } + + if (manager.getOwnJid() == null) { + if (manager.getConnection().isAuthenticated()) { + manager.ownJid = manager.getConnection().getUser().asBareJid(); + } else { + throw new SmackException.NotLoggedInException(); + } + } + + this.manager = manager; + } + + public OmemoManager get() { + return manager; } - return omemoStanzaListener; } - OmemoService.OmemoCarbonCopyListener getOmemoCarbonCopyListener() { - if (omemoCarbonCopyListener == null) { - omemoCarbonCopyListener = getOmemoService().createOmemoCarbonCopyListener(this); + /** + * Callback which can be used to get notified, when the OmemoManager finished initializing. + */ + public interface InitializationFinishedCallback { + + void initializationFinished(OmemoManager manager); + + void initializationFailed(Exception cause); + } + + /** + * Get the bareJid of the user from the authenticated XMPP connection. + * If our deviceId is unknown, use the bareJid to look up deviceIds available in the omemoStore. + * If there are ids available, choose the smallest one. Otherwise generate a random deviceId. + * + * @param manager OmemoManager + */ + private static void initBareJidAndDeviceId(OmemoManager manager) { + if (!manager.getConnection().isAuthenticated()) { + throw new IllegalStateException("Connection MUST be authenticated."); + } + + if (manager.ownJid == null) { + manager.ownJid = manager.getConnection().getUser().asBareJid(); + } + + if (UNKNOWN_DEVICE_ID.equals(manager.deviceId)) { + SortedSet storedDeviceIds = manager.getOmemoService().getOmemoStoreBackend().localDeviceIdsOf(manager.ownJid); + if (storedDeviceIds.size() > 0) { + manager.setDeviceId(storedDeviceIds.first()); + } else { + manager.setDeviceId(randomDeviceId()); + } } - return omemoCarbonCopyListener; } } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessage.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessage.java new file mode 100644 index 000000000..e4d2f1bc3 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessage.java @@ -0,0 +1,213 @@ +/** + * + * Copyright 2017 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.omemo; + +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.BODY_OMEMO_HINT; +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO; +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smackx.eme.element.ExplicitMessageEncryptionElement; +import org.jivesoftware.smackx.hints.element.StoreHint; +import org.jivesoftware.smackx.omemo.element.OmemoElement; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; +import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; + +import org.jxmpp.jid.Jid; + +public class OmemoMessage { + + private final OmemoElement element; + private final byte[] messageKey, iv; + + OmemoMessage(OmemoElement element, byte[] key, byte[] iv) { + this.element = element; + this.messageKey = key; + this.iv = iv; + } + + /** + * Return the original OmemoElement (<encrypted/>). + * + * @return omemoElement + */ + public OmemoElement getElement() { + return element; + } + + /** + * Return the messageKey (or transported key in case of a KeyTransportMessage). + * + * @return key + */ + public byte[] getKey() { + return messageKey.clone(); + } + + /** + * Return the initialization vector belonging to the key. + * @return initialization vector + */ + public byte[] getIv() { + return iv.clone(); + } + + /** + * Outgoing OMEMO message. + */ + public static class Sent extends OmemoMessage { + private final Set intendedDevices = new HashSet<>(); + private final HashMap skippedDevices = new HashMap<>(); + + /** + * Create a new outgoing OMEMO message. + * @param element OmemoElement + * @param key messageKey (or transported key) + * @param iv initialization vector belonging to key + * @param intendedDevices devices the client intended to encrypt the message for + * @param skippedDevices devices which were skipped during encryption process because encryption + * failed for some reason + */ + Sent(OmemoElement element, byte[] key, byte[] iv, Set intendedDevices, HashMap skippedDevices) { + super(element, key, iv); + this.intendedDevices.addAll(intendedDevices); + this.skippedDevices.putAll(skippedDevices); + } + + /** + * Return a list of all devices the sender originally intended to encrypt the message for. + * @return list of intended recipients. + */ + public Set getIntendedDevices() { + return intendedDevices; + } + + /** + * Return a map of all skipped recipients and the reasons for skipping. + * @return map of skipped recipients and reasons for that. + */ + public HashMap getSkippedDevices() { + return skippedDevices; + } + + /** + * Determine, if some recipients were skipped during encryption. + * @return true if recipients were skipped. + */ + public boolean isMissingRecipients() { + return !getSkippedDevices().isEmpty(); + } + + /** + * Return the OmemoElement wrapped in a Message ready to be sent. + * The message is addressed to recipient, contains the OmemoElement + * as well as an optional clear text hint as body, a MAM storage hint + * and an EME hint about OMEMO encryption. + * + * @param recipient recipient for the to-field of the message. + * @return Message + */ + public Message asMessage(Jid recipient) { + + Message messageStanza = new Message(); + messageStanza.setTo(recipient); + messageStanza.addExtension(getElement()); + + if (OmemoConfiguration.getAddOmemoHintBody()) { + messageStanza.setBody(BODY_OMEMO_HINT); + } + + StoreHint.set(messageStanza); + messageStanza.addExtension(new ExplicitMessageEncryptionElement(OMEMO_NAMESPACE_V_AXOLOTL, OMEMO)); + + return messageStanza; + } + } + + /** + * Incoming OMEMO message. + */ + public static class Received extends OmemoMessage { + private final String message; + private final OmemoFingerprint sendersFingerprint; + private final OmemoDevice senderDevice; + private final boolean preKeyMessage; + + /** + * Create a new incoming OMEMO message. + * @param element original OmemoElement + * @param key message key (or transported key) + * @param iv respective initialization vector + * @param body decrypted body + * @param sendersFingerprint OmemoFingerprint of the senders identityKey + * @param senderDevice OmemoDevice of the sender + * @param preKeyMessage if this was a preKeyMessage or not + */ + Received(OmemoElement element, byte[] key, byte[] iv, String body, OmemoFingerprint sendersFingerprint, OmemoDevice senderDevice, boolean preKeyMessage) { + super(element, key, iv); + this.message = body; + this.sendersFingerprint = sendersFingerprint; + this.senderDevice = senderDevice; + this.preKeyMessage = preKeyMessage; + } + + /** + * Return the decrypted body of the message. + * @return decrypted body + */ + public String getBody() { + return message; + } + + /** + * Return the fingerprint of the messages sender device. + * @return fingerprint of sender + */ + public OmemoFingerprint getSendersFingerprint() { + return sendersFingerprint; + } + + /** + * Return the OmemoDevice which sent the message. + * @return senderDevice + */ + public OmemoDevice getSenderDevice() { + return senderDevice; + } + + /** + * Return true, if this message was sent as a preKeyMessage. + * @return preKeyMessage or not + */ + boolean isPreKeyMessage() { + return preKeyMessage; + } + + /** + * Return true, if the message was a KeyTransportMessage. + * A KeyTransportMessage is a OmemoMessage without a payload. + * @return keyTransportMessage? + */ + public boolean isKeyTransportMessage() { + return message == null; + } + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoRatchet.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoRatchet.java new file mode 100644 index 000000000..88d40d033 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoRatchet.java @@ -0,0 +1,197 @@ +/** + * + * 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.util.StringUtils; +import org.jivesoftware.smackx.omemo.element.OmemoElement; +import org.jivesoftware.smackx.omemo.element.OmemoKeyElement; +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 OmemoRatchet { + private static final Logger LOGGER = Logger.getLogger(OmemoRatchet.class.getName()); + + protected final OmemoManager omemoManager; + protected final OmemoStore store; + + /** + * Constructor. + * + * @param omemoManager omemoManager + * @param store omemoStore + */ + public OmemoRatchet(OmemoManager omemoManager, + OmemoStore store) { + this.omemoManager = omemoManager; + this.store = store; + } + + /** + * Decrypt a double-ratchet-encrypted message key. + * + * @param sender sender of the message. + * @param encryptedKey key encrypted with the ratchet of the sender. + * @return decrypted message key. + * + * @throws CorruptedOmemoKeyException + * @throws NoRawSessionException when no double ratchet session was found. + * @throws CryptoFailedException + * @throws UntrustedOmemoIdentityException + */ + public abstract byte[] doubleRatchetDecrypt(OmemoDevice sender, byte[] encryptedKey) + throws CorruptedOmemoKeyException, NoRawSessionException, CryptoFailedException, + UntrustedOmemoIdentityException; + + /** + * Encrypt a messageKey with the double ratchet session of the recipient. + * + * @param recipient recipient of the message. + * @param messageKey key we want to encrypt. + * @return encrypted message key. + */ + 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 auth-tag + * @throws CryptoFailedException if decryption using the double ratchet fails + * @throws NoRawSessionException if we have no session, but the element was NOT a PreKeyMessage + */ + CipherAndAuthTag retrieveMessageKeyAndAuthTag(OmemoDevice sender, OmemoElement element) throws CryptoFailedException, + NoRawSessionException { + int keyId = omemoManager.getDeviceId(); + byte[] unpackedKey = null; + List decryptExceptions = new ArrayList<>(); + List keys = element.getHeader().getKeys(); + + boolean preKey = false; + + // Find key with our ID. + for (OmemoKeyElement k : keys) { + if (k.getId() == keyId) { + try { + unpackedKey = doubleRatchetDecrypt(sender, k.getData()); + preKey = k.isPreKey(); + 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, preKey); + } + + /** + * 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 decrypted plain text. + * @throws CryptoFailedException if decryption using AES key fails. + */ + static String 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 = payloadAndAuthTag(element, cipherAndAuthTag.getAuthTag()); + + try { + String plaintext = new String(cipherAndAuthTag.getCipher().doFinal(encryptedBody), StringUtils.UTF8); + return plaintext; + + } catch (UnsupportedEncodingException | IllegalBlockSizeException | BadPaddingException e) { + throw new CryptoFailedException("decryptMessageElement could not decipher message body: " + + e.getMessage()); + } + } + + /** + * Return the concatenation of the payload of the OmemoElement and the given auth tag. + * + * @param element omemoElement (message element) + * @param authTag authTag + * @return payload + authTag + */ + static byte[] payloadAndAuthTag(OmemoElement element, byte[] authTag) { + if (!element.isMessageElement()) { + throw new IllegalArgumentException("OmemoElement has no payload."); + } + + byte[] payload = new byte[element.getPayload().length + authTag.length]; + System.arraycopy(element.getPayload(), 0, payload, 0, element.getPayload().length); + System.arraycopy(authTag, 0, payload, element.getPayload().length, authTag.length); + return payload; + } + +} 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 d6781fff2..3fde065bc 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java @@ -16,9 +16,8 @@ */ package org.jivesoftware.smackx.omemo; -import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL; -import static org.jivesoftware.smackx.omemo.util.OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID; -import static org.jivesoftware.smackx.omemo.util.OmemoConstants.PEP_NODE_DEVICE_LIST; +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYLENGTH; +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE; import java.io.UnsupportedEncodingException; import java.security.InvalidAlgorithmParameterException; @@ -27,13 +26,12 @@ import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.Security; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Random; import java.util.Set; import java.util.logging.Level; @@ -43,47 +41,48 @@ import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.StanzaListener; +import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.filter.StanzaFilter; + import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Stanza; -import org.jivesoftware.smack.packet.StanzaError; -import org.jivesoftware.smack.util.Async; - -import org.jivesoftware.smackx.carbons.CarbonCopyReceivedListener; -import org.jivesoftware.smackx.carbons.CarbonManager; import org.jivesoftware.smackx.carbons.packet.CarbonExtension; import org.jivesoftware.smackx.mam.MamManager; import org.jivesoftware.smackx.muc.MultiUserChat; import org.jivesoftware.smackx.muc.MultiUserChatManager; -import org.jivesoftware.smackx.omemo.element.OmemoBundleVAxolotlElement; +import org.jivesoftware.smackx.muc.Occupant; +import org.jivesoftware.smackx.omemo.element.OmemoBundleElement; import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement; -import org.jivesoftware.smackx.omemo.element.OmemoDeviceListVAxolotlElement; +import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement_VAxolotl; import org.jivesoftware.smackx.omemo.element.OmemoElement; -import org.jivesoftware.smackx.omemo.element.OmemoVAxolotlElement; +import org.jivesoftware.smackx.omemo.element.OmemoElement_VAxolotl; import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException; +import org.jivesoftware.smackx.omemo.exceptions.NoIdentityKeyException; import org.jivesoftware.smackx.omemo.exceptions.NoRawSessionException; +import org.jivesoftware.smackx.omemo.exceptions.StaleDeviceException; import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException; -import org.jivesoftware.smackx.omemo.internal.CachedDeviceList; +import org.jivesoftware.smackx.omemo.exceptions.UntrustedOmemoIdentityException; 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.OmemoCachedDeviceList; 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.trust.OmemoFingerprint; +import org.jivesoftware.smackx.omemo.trust.OmemoTrustCallback; +import org.jivesoftware.smackx.omemo.trust.TrustState; +import org.jivesoftware.smackx.omemo.util.MessageOrOmemoMessage; import org.jivesoftware.smackx.omemo.util.OmemoConstants; import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder; import org.jivesoftware.smackx.pubsub.LeafNode; import org.jivesoftware.smackx.pubsub.PayloadItem; import org.jivesoftware.smackx.pubsub.PubSubException; -import org.jivesoftware.smackx.pubsub.PubSubException.NotAPubSubNodeException; import org.jivesoftware.smackx.pubsub.PubSubManager; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.jxmpp.jid.BareJid; +import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.Jid; /** @@ -100,7 +99,8 @@ 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()); @@ -108,10 +108,26 @@ public abstract class OmemoService INSTANCE; - protected OmemoStore omemoStore; + private OmemoStore omemoStore; + private final HashMap> omemoRatchets = new HashMap<>(); + /** + * Create a new OmemoService object. This should only happen once. + */ + protected OmemoService() { + } + + /** + * 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"); @@ -131,6 +147,11 @@ public abstract class OmemoService getOmemoStoreBackend() { if (omemoStore == null) { - setOmemoStoreBackend(createDefaultOmemoStoreBackend()); - return getOmemoStoreBackend(); + omemoStore = createDefaultOmemoStoreBackend(); } return omemoStore; } @@ -155,6 +175,7 @@ public abstract class OmemoService omemoStore) { if (this.omemoStore != null) { @@ -168,1202 +189,1279 @@ public abstract class OmemoService + protected abstract OmemoStore createDefaultOmemoStoreBackend(); /** - * Create a new OmemoService object. This should only happen once. - * When the service gets created, it tries a placeholder crypto function in order to test, if all necessary - * algorithms are available on the system. + * Return a new instance of the OMEMO ratchet. + * The ratchet is internally used to encrypt/decrypt message keys. * - * @throws NoSuchPaddingException When no Cipher could be instantiated. - * @throws NoSuchAlgorithmException when no Cipher could be instantiated. - * @throws NoSuchProviderException when BouncyCastle could not be found. - * @throws InvalidAlgorithmParameterException when the Cipher could not be initialized - * @throws InvalidKeyException when the generated key is invalid - * @throws UnsupportedEncodingException when UTF8 is unavailable - * @throws BadPaddingException when cipher.doFinal gets wrong padding - * @throws IllegalBlockSizeException when cipher.doFinal gets wrong Block size. + * @param manager OmemoManager + * @param store OmemoStore + * @return instance of the OmemoRatchet */ - public OmemoService() - throws NoSuchPaddingException, InvalidKeyException, UnsupportedEncodingException, IllegalBlockSizeException, - BadPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException { + protected abstract OmemoRatchet + instantiateOmemoRatchet(OmemoManager manager, + OmemoStore store); - // Check availability of algorithms and encodings needed for crypto - checkAvailableAlgorithms(); + /** + * Return the deposited instance of the OmemoRatchet for the given manager. + * If there is none yet, create a new one, deposit it and return it. + * + * @param manager OmemoManager we want to have the ratchet for. + * @return OmemoRatchet instance + */ + protected OmemoRatchet + getOmemoRatchet(OmemoManager manager) { + OmemoRatchet + omemoRatchet = omemoRatchets.get(manager); + if (omemoRatchet == null) { + omemoRatchet = instantiateOmemoRatchet(manager, omemoStore); + omemoRatchets.put(manager, omemoRatchet); + } + return omemoRatchet; + } + + /** + * Instantiate and deposit a Ratchet for the given OmemoManager. + * + * @param manager manager. + */ + void registerRatchetForManager(OmemoManager manager) { + omemoRatchets.put(manager, instantiateOmemoRatchet(manager, getOmemoStoreBackend())); } /** * Initialize OMEMO functionality for OmemoManager omemoManager. * - * @param omemoManager OmemoManager we'd like to initialize. + * @param managerGuard OmemoManager we'd like to initialize. * @throws InterruptedException * @throws CorruptedOmemoKeyException * @throws XMPPException.XMPPErrorException * @throws SmackException.NotConnectedException * @throws SmackException.NoResponseException - * @throws SmackException.NotLoggedInException * @throws PubSubException.NotALeafNodeException */ - void initialize(OmemoManager omemoManager) throws InterruptedException, CorruptedOmemoKeyException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException, SmackException.NotLoggedInException, PubSubException.NotALeafNodeException { - if (!omemoManager.getConnection().isAuthenticated()) { - throw new SmackException.NotLoggedInException(); + void init(OmemoManager.LoggedInOmemoManager managerGuard) + throws InterruptedException, CorruptedOmemoKeyException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, SmackException.NoResponseException, + PubSubException.NotALeafNodeException { + + OmemoManager manager = managerGuard.get(); + OmemoDevice userDevice = manager.getOwnDevice(); + + // Create new keys if necessary and publish to the server. + getOmemoStoreBackend().replenishKeys(userDevice); + + // Rotate signed preKey if necessary. + if (shouldRotateSignedPreKey(userDevice)) { + getOmemoStoreBackend().changeSignedPreKey(userDevice); } - boolean mustPublishId = false; - if (getOmemoStoreBackend().isFreshInstallation(omemoManager)) { - LOGGER.log(Level.INFO, "No key material found. Looks like we have a fresh installation."); - // Create new key material and publish it to the server - regenerate(omemoManager, omemoManager.getDeviceId()); - mustPublishId = true; - } + // Pack and publish bundle + OmemoBundleElement bundle = getOmemoStoreBackend().packOmemoBundle(userDevice); + publishBundle(manager.getConnection(), userDevice, bundle); - // Get fresh device list from server - mustPublishId |= refreshOwnDeviceList(omemoManager); - - publishDeviceIdIfNeeded(omemoManager, false, mustPublishId); - publishBundle(omemoManager); - - registerOmemoMessageStanzaListeners(omemoManager); //Wait for new OMEMO messages - getOmemoStoreBackend().initializeOmemoSessions(omemoManager); //Preload existing OMEMO sessions + // Fetch device list and republish deviceId if necessary + refreshAndRepublishDeviceList(manager.getConnection(), userDevice); } /** - * Test availability of required algorithms. We do this in advance, so we can simplify exception handling later. + * Create an empty OMEMO message, which is used to forward the ratchet of the recipient. + * This message type is typically used to create stable sessions. + * Note that trust decisions are ignored for the creation of this message. * - * @throws NoSuchPaddingException - * @throws UnsupportedEncodingException - * @throws InvalidAlgorithmParameterException - * @throws NoSuchAlgorithmException - * @throws IllegalBlockSizeException - * @throws BadPaddingException - * @throws NoSuchProviderException - * @throws InvalidKeyException + * @param managerGuard Logged in OmemoManager + * @param contactsDevice OmemoDevice of the contact + * @return ratchet update message + * @throws NoSuchAlgorithmException if AES algorithms are not supported on this system. + * @throws InterruptedException + * @throws SmackException.NoResponseException + * @throws CorruptedOmemoKeyException if our IdentityKeyPair is corrupted. + * @throws SmackException.NotConnectedException + * @throws CannotEstablishOmemoSessionException if session negotiation fails. */ - protected static void checkAvailableAlgorithms() throws NoSuchPaddingException, UnsupportedEncodingException, - InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, - NoSuchProviderException, InvalidKeyException { - // Test crypto functions - new OmemoMessageBuilder<>(null, null, ""); - } + OmemoElement createRatchetUpdateElement(OmemoManager.LoggedInOmemoManager managerGuard, + OmemoDevice contactsDevice) + throws InterruptedException, SmackException.NoResponseException, CorruptedOmemoKeyException, + SmackException.NotConnectedException, CannotEstablishOmemoSessionException, NoSuchAlgorithmException, + CryptoFailedException { - /** - * 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. - * @throws CorruptedOmemoKeyException when freshly generated identityKey is invalid - * (should never ever happen *crosses fingers*) - */ - void regenerate(OmemoManager omemoManager, Integer nDeviceId) throws CorruptedOmemoKeyException { - // Generate unique ID that is not already taken - while (nDeviceId == null || !getOmemoStoreBackend().isAvailableDeviceId(omemoManager, nDeviceId)) { - nDeviceId = OmemoManager.randomDeviceId(); + OmemoManager manager = managerGuard.get(); + OmemoDevice userDevice = manager.getOwnDevice(); + + if (contactsDevice.equals(userDevice)) { + throw new IllegalArgumentException("\"Thou shall not update thy own ratchet!\" - William Shakespeare"); } - getOmemoStoreBackend().forgetOmemoSessions(omemoManager); - getOmemoStoreBackend().purgeOwnDeviceKeys(omemoManager); - omemoManager.setDeviceId(nDeviceId); - getOmemoStoreBackend().regenerate(omemoManager); + // Establish session if necessary + if (!hasSession(userDevice, contactsDevice)) { + buildFreshSessionWithDevice(manager.getConnection(), userDevice, contactsDevice); + } + + // Generate fresh AES key and IV + byte[] messageKey = OmemoMessageBuilder.generateKey(KEYTYPE, KEYLENGTH); + byte[] iv = OmemoMessageBuilder.generateIv(); + + // Create message builder + OmemoMessageBuilder builder; + try { + builder = new OmemoMessageBuilder<>(userDevice, gullibleTrustCallback, getOmemoRatchet(manager), + messageKey, iv, null); + } catch (InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException | BadPaddingException | UnsupportedEncodingException | NoSuchProviderException | IllegalBlockSizeException e) { + throw new CryptoFailedException(e); + } + + // Add recipient + try { + builder.addRecipient(contactsDevice); + } catch (UndecidedOmemoIdentityException | UntrustedOmemoIdentityException e) { + throw new AssertionError("Gullible Trust Callback reported undecided or untrusted device, " + + "even though it MUST NOT do that."); + } catch (NoIdentityKeyException e) { + throw new AssertionError("We MUST have an identityKey for " + contactsDevice + " since we built a session." + e); + } + + return builder.finish(); } /** - * Publish a fresh bundle to the server. + * Encrypt a message with a messageKey and an IV and create an OmemoMessage from it. + * + * @param managerGuard authenticated OmemoManager + * @param contactsDevices set of recipient OmemoDevices + * @param messageKey AES key to encrypt the message + * @param iv iv to be used with the messageKey + * @return OmemoMessage object which contains the OmemoElement and some information. * - * @param omemoManager OmemoManager * @throws SmackException.NotConnectedException * @throws InterruptedException * @throws SmackException.NoResponseException - * @throws CorruptedOmemoKeyException - * @throws XMPPException.XMPPErrorException + * @throws UndecidedOmemoIdentityException if the list of recipient devices contains undecided devices + * @throws CryptoFailedException if we are lacking some crypto primitives */ - void publishBundle(OmemoManager omemoManager) - throws SmackException.NotConnectedException, InterruptedException, - SmackException.NoResponseException, CorruptedOmemoKeyException, XMPPException.XMPPErrorException { - Date lastSignedPreKeyRenewal = getOmemoStoreBackend().getDateOfLastSignedPreKeyRenewal(omemoManager); - if (OmemoConfiguration.getRenewOldSignedPreKeys() && lastSignedPreKeyRenewal != null) { - if (System.currentTimeMillis() - lastSignedPreKeyRenewal.getTime() - > 1000L * 60 * 60 * OmemoConfiguration.getRenewOldSignedPreKeysAfterHours()) { - LOGGER.log(Level.INFO, "Renewing signedPreKey"); - getOmemoStoreBackend().changeSignedPreKey(omemoManager); - } - } else { - getOmemoStoreBackend().setDateOfLastSignedPreKeyRenewal(omemoManager); - } - - // publish - PubSubManager.getInstance(omemoManager.getConnection(), omemoManager.getOwnJid()) - .tryToPublishAndPossibleAutoCreate(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(omemoManager.getDeviceId()), - new PayloadItem<>(getOmemoStoreBackend().packOmemoBundle(omemoManager))); - } - - /** - * Publish our deviceId in case it is not on the list already. - * This method calls publishDeviceIdIfNeeded(omemoManager, deleteOtherDevices, false). - * @param omemoManager OmemoManager - * @param deleteOtherDevices Do we want to remove other devices from the list? - * @throws InterruptedException - * @throws PubSubException.NotALeafNodeException - * @throws XMPPException.XMPPErrorException - * @throws SmackException.NotConnectedException - * @throws SmackException.NoResponseException - */ - void publishDeviceIdIfNeeded(OmemoManager omemoManager, boolean deleteOtherDevices) throws InterruptedException, - PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, - SmackException.NotConnectedException, SmackException.NoResponseException { - publishDeviceIdIfNeeded(omemoManager, deleteOtherDevices, false); - } - - /** - * Publish our deviceId in case it is not on the list already. - * - * @param omemoManager OmemoManager - * @param deleteOtherDevices Do we want to remove other devices from the list? - * If we do, publish the list with only our id, regardless if we were on the list - * already. - * @param publish Do we want to force publishing our id? - * @throws SmackException.NotConnectedException - * @throws InterruptedException - * @throws SmackException.NoResponseException - * @throws XMPPException.XMPPErrorException - * @throws PubSubException.NotALeafNodeException - */ - void publishDeviceIdIfNeeded(OmemoManager omemoManager, boolean deleteOtherDevices, boolean publish) + private OmemoMessage.Sent encrypt(OmemoManager.LoggedInOmemoManager managerGuard, + Set contactsDevices, + byte[] messageKey, + byte[] iv, + String message) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, - XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException { + UndecidedOmemoIdentityException, CryptoFailedException { - CachedDeviceList deviceList = getOmemoStoreBackend().loadCachedDeviceList(omemoManager, omemoManager.getOwnJid()); + OmemoManager manager = managerGuard.get(); + OmemoDevice userDevice = manager.getOwnDevice(); - Set deviceListIds; - if (deviceList == null) { - deviceListIds = new HashSet<>(); - } else { - deviceListIds = new HashSet<>(deviceList.getActiveDevices()); + // Do not encrypt for our own device. + removeOurDevice(userDevice, contactsDevices); + + buildMissingSessionsWithDevices(manager.getConnection(), userDevice, contactsDevices); + + Set undecidedDevices = getUndecidedDevices(userDevice, manager.getTrustCallback(), contactsDevices); + if (!undecidedDevices.isEmpty()) { + throw new UndecidedOmemoIdentityException(undecidedDevices); } - if (deleteOtherDevices) { - deviceListIds.clear(); + // Keep track of skipped devices + HashMap skippedRecipients = new HashMap<>(); + + OmemoMessageBuilder builder; + try { + builder = new OmemoMessageBuilder<>( + userDevice, manager.getTrustCallback(), getOmemoRatchet(managerGuard.get()), messageKey, iv, message); + } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | + NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { + throw new CryptoFailedException(e); } - int ourDeviceId = omemoManager.getDeviceId(); - if (deviceListIds.add(ourDeviceId)) { - publish = true; - } - - publish |= removeStaleDevicesIfNeeded(omemoManager, deviceListIds); - - if (publish) { - publishDeviceIds(omemoManager, new OmemoDeviceListVAxolotlElement(deviceListIds)); - } - } - - /** - * Remove stale devices from our device list. - * This does only delete devices, if that's configured in OmemoConfiguration. - * - * @param omemoManager OmemoManager - * @param deviceListIds deviceIds we plan to publish. Stale devices are deleted from that list. - * @return - */ - boolean removeStaleDevicesIfNeeded(OmemoManager omemoManager, Set deviceListIds) { - 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) { - // Skip own id - continue; - } - - OmemoDevice d = new OmemoDevice(omemoManager.getOwnJid(), id); - Date date = getOmemoStoreBackend().getDateOfLastReceivedMessage(omemoManager, d); - - if (date == null) { - getOmemoStoreBackend().setDateOfLastReceivedMessage(omemoManager, d); - } else { - if (System.currentTimeMillis() - date.getTime() > 1000L * 60 * 60 * OmemoConfiguration.getDeleteStaleDevicesAfterHours()) { - LOGGER.log(Level.INFO, "Remove device " + id + " because of more than " + - OmemoConfiguration.getDeleteStaleDevicesAfterHours() + " hours of inactivity."); - it.remove(); - publish = true; + for (OmemoDevice contactsDevice : contactsDevices) { + // Build missing sessions + if (!hasSession(userDevice, contactsDevice)) { + try { + buildFreshSessionWithDevice(manager.getConnection(), userDevice, contactsDevice); + } catch (CorruptedOmemoKeyException | CannotEstablishOmemoSessionException e) { + LOGGER.log(Level.WARNING, "Could not build session with " + contactsDevice + ".", e); + skippedRecipients.put(contactsDevice, e); + continue; } } + + // Ignore stale devices + if (OmemoConfiguration.getIgnoreStaleDevices()) { + + Date lastMessageDate = getOmemoStoreBackend().getDateOfLastReceivedMessage(userDevice, contactsDevice); + if (lastMessageDate == null) { + lastMessageDate = new Date(); + getOmemoStoreBackend().setDateOfLastReceivedMessage(userDevice, contactsDevice, lastMessageDate); + } + + Date lastPublicationDate = getOmemoStoreBackend().getDateOfLastDeviceIdPublication(userDevice, contactsDevice); + if (lastPublicationDate == null) { + lastPublicationDate = new Date(); + getOmemoStoreBackend().setDateOfLastDeviceIdPublication(userDevice, contactsDevice, lastPublicationDate); + } + + boolean stale = isStale(userDevice, contactsDevice, lastPublicationDate, OmemoConfiguration.getIgnoreStaleDevicesAfterHours()); + stale &= isStale(userDevice, contactsDevice, lastMessageDate, OmemoConfiguration.getIgnoreStaleDevicesAfterHours()); + + if (stale) { + LOGGER.log(Level.FINE, "Device " + contactsDevice + " seems to be stale (last message received " + + lastMessageDate + ", last publication of deviceId: " + lastPublicationDate + "). Ignore it."); + skippedRecipients.put(contactsDevice, new StaleDeviceException(contactsDevice, lastMessageDate, lastPublicationDate)); + continue; + } + } + + // Add recipients + try { + builder.addRecipient(contactsDevice); + } + catch (NoIdentityKeyException | CorruptedOmemoKeyException e) { + LOGGER.log(Level.WARNING, "Encryption failed for device " + contactsDevice + ".", e); + skippedRecipients.put(contactsDevice, e); + } + catch (UndecidedOmemoIdentityException e) { + throw new AssertionError("Recipients device seems to be undecided, even though we should have thrown" + + " an exception earlier in that case. " + e); + } + catch (UntrustedOmemoIdentityException e) { + LOGGER.log(Level.WARNING, "Device " + contactsDevice + " is untrusted. Message is not encrypted for it."); + skippedRecipients.put(contactsDevice, e); + } } - return publish; + + OmemoElement element = builder.finish(); + + return new OmemoMessage.Sent(element, messageKey, iv, contactsDevices, skippedRecipients); } /** - * Publish the given deviceList to the server. + * Decrypt an OMEMO message. + * @param managerGuard authenticated OmemoManager. + * @param senderJid BareJid of the sender. + * @param omemoElement omemoElement. + * @return decrypted OmemoMessage object. * - * @param omemoManager OmemoManager - * @param deviceList list of deviceIDs - * @throws InterruptedException Exception - * @throws XMPPException.XMPPErrorException Exception - * @throws SmackException.NotConnectedException Exception - * @throws SmackException.NoResponseException Exception - * @throws PubSubException.NotALeafNodeException Exception + * @throws CorruptedOmemoKeyException if the identityKey of the sender is damaged. + * @throws CryptoFailedException if decryption fails. + * @throws NoRawSessionException if we have no session with the device and it sent a normal (non-preKey) message. */ - static void publishDeviceIds(OmemoManager omemoManager, OmemoDeviceListElement deviceList) - throws InterruptedException, XMPPException.XMPPErrorException, - SmackException.NotConnectedException, SmackException.NoResponseException, PubSubException.NotALeafNodeException { - PubSubManager.getInstance(omemoManager.getConnection(), omemoManager.getOwnJid()) + OmemoMessage.Received decryptMessage(OmemoManager.LoggedInOmemoManager managerGuard, + BareJid senderJid, + OmemoElement omemoElement) + throws CorruptedOmemoKeyException, CryptoFailedException, NoRawSessionException { + + OmemoManager manager = managerGuard.get(); + int senderId = omemoElement.getHeader().getSid(); + OmemoDevice senderDevice = new OmemoDevice(senderJid, senderId); + + CipherAndAuthTag cipherAndAuthTag = getOmemoRatchet(manager) + .retrieveMessageKeyAndAuthTag(senderDevice, omemoElement); + + // Retrieve senders fingerprint. TODO: Find a way to do this without the store. + OmemoFingerprint senderFingerprint; + try { + senderFingerprint = getOmemoStoreBackend().getFingerprint(manager.getOwnDevice(), senderDevice); + } catch (NoIdentityKeyException e) { + throw new AssertionError("Cannot retrieve OmemoFingerprint of sender although decryption was successful: " + e); + } + + if (omemoElement.isMessageElement()) { + // Use symmetric message key to decrypt message payload. + String plaintext = OmemoRatchet.decryptMessageElement(omemoElement, cipherAndAuthTag); + + return new OmemoMessage.Received(omemoElement, cipherAndAuthTag.getKey(), cipherAndAuthTag.getIv(), + plaintext, senderFingerprint, senderDevice, cipherAndAuthTag.wasPreKeyEncrypted()); + + } else { + // KeyTransportMessages don't require decryption of the payload. + return new OmemoMessage.Received(omemoElement, cipherAndAuthTag.getKey(), cipherAndAuthTag.getIv(), + null, senderFingerprint, senderDevice, cipherAndAuthTag.wasPreKeyEncrypted()); + } + } + + /** + * Create an OMEMO KeyTransportElement. + * @see XEP-0384: Sending a key. + * + * @param managerGuard Initialized OmemoManager. + * @param contactsDevices set of recipient devices. + * @param key AES-Key to be transported. + * @param iv initialization vector to be used with the key. + * @return KeyTransportElement + * + * @throws InterruptedException + * @throws UndecidedOmemoIdentityException if the list of recipients contains an undecided device + * @throws CryptoFailedException if we are lacking some cryptographic algorithms + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + */ + OmemoMessage.Sent createKeyTransportElement(OmemoManager.LoggedInOmemoManager managerGuard, + Set contactsDevices, + byte[] key, + byte[] iv) + throws InterruptedException, UndecidedOmemoIdentityException, CryptoFailedException, + SmackException.NotConnectedException, SmackException.NoResponseException { + return encrypt(managerGuard, contactsDevices, key, iv, null); + } + + /** + * Create an OmemoMessage. + * + * @param managerGuard initialized OmemoManager + * @param contactsDevices set of recipient devices + * @param message message we want to send + * @return encrypted OmemoMessage + * + * @throws InterruptedException + * @throws UndecidedOmemoIdentityException if the list of recipient devices contains an undecided device. + * @throws CryptoFailedException if we are lacking some cryptographic algorithms + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + */ + OmemoMessage.Sent createOmemoMessage(OmemoManager.LoggedInOmemoManager managerGuard, + Set contactsDevices, + String message) + throws InterruptedException, UndecidedOmemoIdentityException, CryptoFailedException, + SmackException.NotConnectedException, SmackException.NoResponseException { + + byte[] key, iv; + iv = OmemoMessageBuilder.generateIv(); + + try { + key = OmemoMessageBuilder.generateKey(KEYTYPE, KEYLENGTH); + } catch (NoSuchAlgorithmException e) { + throw new CryptoFailedException(e); + } + + return encrypt(managerGuard, contactsDevices, key, iv, message); + } + + /** + * Retrieve a users OMEMO bundle. + * + * @param connection authenticated XMPP connection. + * @param contactsDevice device of which we want to retrieve the bundle. + * @return OmemoBundle of the device or null, if it doesn't exist. + * + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + * @throws XMPPException.XMPPErrorException + * @throws PubSubException.NotALeafNodeException + * @throws PubSubException.NotAPubSubNodeException + */ + private static OmemoBundleElement fetchBundle(XMPPConnection connection, + OmemoDevice contactsDevice) + throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, + XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException, + PubSubException.NotAPubSubNodeException { + + PubSubManager pm = PubSubManager.getInstance(connection, contactsDevice.getJid()); + LeafNode node = pm.getLeafNode(contactsDevice.getBundleNodeName()); + + if (node == null) { + return null; + } + + List> bundleItems = node.getItems(); + if (bundleItems.isEmpty()) { + return null; + } + + return bundleItems.get(bundleItems.size() - 1).getPayload(); + } + + /** + * Publish the given OMEMO bundle to the server using PubSub. + * @param connection our connection. + * @param userDevice our device + * @param bundle the bundle we want to publish + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + */ + static void publishBundle(XMPPConnection connection, OmemoDevice userDevice, OmemoBundleElement bundle) + throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException { + PubSubManager pm = PubSubManager.getInstance(connection, connection.getUser().asBareJid()); + pm.tryToPublishAndPossibleAutoCreate(userDevice.getBundleNodeName(), new PayloadItem<>(bundle)); + } + + /** + * Retrieve the OMEMO device list of a contact. + * + * @param connection authenticated XMPP connection. + * @param contact BareJid of the contact of which we want to retrieve the device list from. + * @return + * @throws InterruptedException + * @throws PubSubException.NotALeafNodeException + * @throws SmackException.NoResponseException + * @throws SmackException.NotConnectedException + * @throws XMPPException.XMPPErrorException + * @throws PubSubException.NotAPubSubNodeException + */ + private static OmemoDeviceListElement fetchDeviceList(XMPPConnection connection, BareJid contact) + throws InterruptedException, PubSubException.NotALeafNodeException, SmackException.NoResponseException, + SmackException.NotConnectedException, XMPPException.XMPPErrorException, + PubSubException.NotAPubSubNodeException { + + PubSubManager pm = PubSubManager.getInstance(connection, contact); + String nodeName = OmemoConstants.PEP_NODE_DEVICE_LIST; + LeafNode node = pm.getLeafNode(nodeName); + + if (node == null) { + return null; + } + + List> items = node.getItems(); + if (items.isEmpty()) { + return null; + } + + return items.get(items.size() - 1).getPayload(); + } + + /** + * Publish the given device list to the server. + * @param connection authenticated XMPP connection. + * @param deviceList users deviceList. + * @throws InterruptedException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + * @throws PubSubException.NotALeafNodeException + */ + static void publishDeviceList(XMPPConnection connection, OmemoDeviceListElement deviceList) + throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, + SmackException.NoResponseException { + + PubSubManager.getInstance(connection, connection.getUser().asBareJid()) .tryToPublishAndPossibleAutoCreate(OmemoConstants.PEP_NODE_DEVICE_LIST, new PayloadItem<>(deviceList)); } /** - * Fetch the deviceList node of a contact. * - * @param omemoManager omemoManager - * @param contact contact - * @return LeafNode + * @param connection + * @param userDevice * @throws InterruptedException * @throws PubSubException.NotALeafNodeException * @throws XMPPException.XMPPErrorException * @throws SmackException.NotConnectedException * @throws SmackException.NoResponseException - * @throws NotAPubSubNodeException */ - static LeafNode fetchDeviceListNode(OmemoManager omemoManager, BareJid contact) + private void refreshAndRepublishDeviceList(XMPPConnection connection, OmemoDevice userDevice) throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, - SmackException.NotConnectedException, SmackException.NoResponseException, NotAPubSubNodeException { - return PubSubManager.getInstance(omemoManager.getConnection(), contact).getLeafNode(PEP_NODE_DEVICE_LIST); + SmackException.NotConnectedException, SmackException.NoResponseException { + + // refreshOmemoDeviceList; + OmemoDeviceListElement publishedList; + try { + publishedList = fetchDeviceList(connection, userDevice.getJid()); + } catch (PubSubException.NotAPubSubNodeException e) { + publishedList = null; + } + if (publishedList == null) { + publishedList = new OmemoDeviceListElement_VAxolotl(Collections.emptySet()); + } + + getOmemoStoreBackend().mergeCachedDeviceList(userDevice, userDevice.getJid(), publishedList); + + OmemoCachedDeviceList cachedList = cleanUpDeviceList(userDevice); + + // Republish our deviceId if it is missing from the published list. + if (!publishedList.getDeviceIds().equals(cachedList.getActiveDevices())) { + publishDeviceList(connection, new OmemoDeviceListElement_VAxolotl(cachedList)); + } } /** - * Directly fetch the device list of a contact. + * Add our load the deviceList of the user from cache, delete stale devices if needed, add the users device + * back if necessary, store the refurbished list in cache and return it. * - * @param omemoManager OmemoManager - * @param contact BareJid of the contact - * @return The OmemoDeviceListElement of the contact - * @throws XMPPException.XMPPErrorException When - * @throws SmackException.NotConnectedException something - * @throws InterruptedException goes - * @throws SmackException.NoResponseException wrong - * @throws PubSubException.NotALeafNodeException when the device lists node is not a LeafNode - * @throws NotAPubSubNodeException + * @param userDevice + * @return */ - static OmemoDeviceListElement fetchDeviceList(OmemoManager omemoManager, BareJid contact) - throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, - SmackException.NoResponseException, PubSubException.NotALeafNodeException, NotAPubSubNodeException { - return extractDeviceListFrom(fetchDeviceListNode(omemoManager, contact)); + OmemoCachedDeviceList cleanUpDeviceList(OmemoDevice userDevice) { + OmemoCachedDeviceList cachedDeviceList; + + // Delete stale devices if allowed and necessary + if (OmemoConfiguration.getDeleteStaleDevices()) { + cachedDeviceList = deleteStaleDevices(userDevice); + } else { + cachedDeviceList = getOmemoStoreBackend().loadCachedDeviceList(userDevice); + } + + + // Add back our device if necessary + if (!cachedDeviceList.getActiveDevices().contains(userDevice.getDeviceId())) { + cachedDeviceList.addDevice(userDevice.getDeviceId()); + } + + getOmemoStoreBackend().storeCachedDeviceList(userDevice, userDevice.getJid(), cachedDeviceList); + return cachedDeviceList; } /** - * Refresh our deviceList from the server. + * Refresh and merge device list of contact. * - * @param omemoManager omemoManager - * @return true, if we should publish our device list again (because its broken or not existent...) + * @param connection authenticated XMPP connection + * @param userDevice our OmemoDevice + * @param contact contact we want to fetch the deviceList from + * @return cached device list after refresh. + * + * @throws InterruptedException + * @throws PubSubException.NotALeafNodeException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + */ + OmemoCachedDeviceList refreshDeviceList(XMPPConnection connection, OmemoDevice userDevice, BareJid contact) + throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, SmackException.NoResponseException { + // refreshOmemoDeviceList; + OmemoDeviceListElement publishedList; + try { + publishedList = fetchDeviceList(connection, contact); + } catch (PubSubException.NotAPubSubNodeException e) { + LOGGER.log(Level.WARNING, "Error refreshing deviceList: ", e); + publishedList = null; + } + if (publishedList == null) { + publishedList = new OmemoDeviceListElement_VAxolotl(Collections.emptySet()); + } + + return getOmemoStoreBackend().mergeCachedDeviceList( + userDevice, contact, publishedList); + } + + /** + * Fetch the bundle of a contact and build a fresh OMEMO session with the contacts device. + * Note that this builds a fresh session, regardless if we have had a session before or not. + * + * @param connection authenticated XMPP connection + * @param userDevice our OmemoDevice + * @param contactsDevice OmemoDevice of a contact. + * @throws CannotEstablishOmemoSessionException if we cannot establish a session (because of missing bundle etc.) + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + * @throws CorruptedOmemoKeyException if our IdentityKeyPair is corrupted. + */ + void buildFreshSessionWithDevice(XMPPConnection connection, OmemoDevice userDevice, OmemoDevice contactsDevice) + throws CannotEstablishOmemoSessionException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException, CorruptedOmemoKeyException { + + if (contactsDevice.equals(userDevice)) { + // Do not build a session with yourself. + return; + } + + OmemoBundleElement bundleElement; + try { + bundleElement = fetchBundle(connection, contactsDevice); + } catch (XMPPException.XMPPErrorException | PubSubException.NotALeafNodeException | + PubSubException.NotAPubSubNodeException e) { + throw new CannotEstablishOmemoSessionException(contactsDevice, e); + } + + // Select random Bundle + HashMap bundlesList = getOmemoStoreBackend().keyUtil().BUNDLE.bundles(bundleElement, contactsDevice); + int randomIndex = new Random().nextInt(bundlesList.size()); + T_Bundle randomPreKeyBundle = new ArrayList<>(bundlesList.values()).get(randomIndex); + + // build the session + OmemoManager omemoManager = OmemoManager.getInstanceFor(connection, userDevice.getDeviceId()); + processBundle(omemoManager, randomPreKeyBundle, contactsDevice); + } + + /** + * Build OMEMO sessions with all devices of the contact, we haven't had sessions with before. + * This method returns a set of OmemoDevices. This set contains all devices, with which we either had sessions + * before, plus those devices with which we just built sessions. + * + * @param connection authenticated XMPP connection. + * @param userDevice our OmemoDevice + * @param contact the BareJid of the contact with whom we want to build sessions with. + * @return set of devices with a session. + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + */ + private Set buildMissingSessionsWithContact(XMPPConnection connection, + OmemoDevice userDevice, + BareJid contact) + throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { + + OmemoCachedDeviceList contactsDeviceIds = getOmemoStoreBackend().loadCachedDeviceList(userDevice, contact); + Set contactsDevices = new HashSet<>(); + for (int deviceId : contactsDeviceIds.getActiveDevices()) { + contactsDevices.add(new OmemoDevice(contact, deviceId)); + } + + return buildMissingSessionsWithDevices(connection, userDevice, contactsDevices); + } + + /** + * Build sessions with all devices from the set, we don't have a session with yet. + * Return the set of all devices we have a session with afterwards. + * @param connection authenticated XMPP connection + * @param userDevice our OmemoDevice + * @param devices set of devices we may want to build a session with if necessary + * @return set of all devices with sessions * * @throws SmackException.NotConnectedException * @throws InterruptedException * @throws SmackException.NoResponseException */ - private boolean refreshOwnDeviceList(OmemoManager omemoManager) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, XMPPException.XMPPErrorException { - try { - getOmemoStoreBackend().mergeCachedDeviceList(omemoManager, omemoManager.getOwnJid(), - fetchDeviceList(omemoManager, omemoManager.getOwnJid())); + private Set buildMissingSessionsWithDevices(XMPPConnection connection, + OmemoDevice userDevice, + Set devices) + throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { - } catch (XMPPException.XMPPErrorException e) { + Set devicesWithSession = new HashSet<>(); + for (OmemoDevice device : devices) { - if (e.getXMPPError().getCondition() == StanzaError.Condition.item_not_found) { - LOGGER.log(Level.WARNING, "Could not refresh own deviceList, because the node did not exist: " - + e.getMessage()); - return true; - } - - throw e; - - } catch (PubSubException.NotALeafNodeException e) { - LOGGER.log(Level.WARNING, "Could not refresh own deviceList, because the Node is not a LeafNode: " + - e.getMessage()); - } - - catch (PubSubException.NotAPubSubNodeException e) { - LOGGER.log(Level.WARNING, "Caught a PubSubAssertionError when fetching a deviceList node. " + - "This probably means that we're dealing with an ejabberd server and the LeafNode does not exist.", e); - return true; - } - return false; - } - - /** - * Refresh the deviceList of contact and merge it with the one stored locally. - * @param omemoManager omemoManager - * @param contact contact - * @throws SmackException.NotConnectedException - * @throws InterruptedException - * @throws SmackException.NoResponseException - */ - void refreshDeviceList(OmemoManager omemoManager, BareJid contact) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { - OmemoDeviceListElement omemoDeviceListElement; - try { - omemoDeviceListElement = fetchDeviceList(omemoManager, contact); - } catch (PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException e) { - LOGGER.log(Level.WARNING, "Could not fetch device list of " + contact + ": " + e, e); - return; - } - catch (NotAPubSubNodeException e) { - LOGGER.log(Level.WARNING, "Could not fetch device list of " + contact ,e); - return; - } - - getOmemoStoreBackend().mergeCachedDeviceList(omemoManager, contact, omemoDeviceListElement); - } - - /** - * Fetch the OmemoBundleElement of the contact. - * - * @param omemoManager OmemoManager - * @param contact the contacts BareJid - * @return the OmemoBundleElement of the contact - * @throws XMPPException.XMPPErrorException When - * @throws SmackException.NotConnectedException something - * @throws InterruptedException goes - * @throws SmackException.NoResponseException wrong - * @throws PubSubException.NotALeafNodeException when the bundles node is not a LeafNode - * @throws NotAPubSubNodeException - */ - static OmemoBundleVAxolotlElement fetchBundle(OmemoManager omemoManager, OmemoDevice contact) - throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, - SmackException.NoResponseException, PubSubException.NotALeafNodeException, NotAPubSubNodeException { - LeafNode node = PubSubManager.getInstance(omemoManager.getConnection(), contact.getJid()).getLeafNode( - PEP_NODE_BUNDLE_FROM_DEVICE_ID(contact.getDeviceId())); - return extractBundleFrom(node); - } - - /** - * Extract the OmemoBundleElement of a contact from a LeafNode. - * - * @param node typically a LeafNode containing the OmemoBundles of a contact - * @return the OmemoBundleElement - * @throws XMPPException.XMPPErrorException When - * @throws SmackException.NotConnectedException something - * @throws InterruptedException goes - * @throws SmackException.NoResponseException wrong - */ - private static OmemoBundleVAxolotlElement extractBundleFrom(LeafNode node) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { - if (node == null) { - return null; - } - try { - return (OmemoBundleVAxolotlElement) ((PayloadItem) node.getItems().get(0)).getPayload(); - } catch (IndexOutOfBoundsException e) { - return null; - } - } - - /** - * Extract the OmemoDeviceListElement of a contact from a node containing his OmemoDeviceListElement. - * - * @param node typically a LeafNode containing the OmemoDeviceListElement of a contact - * @return the extracted OmemoDeviceListElement. - * @throws XMPPException.XMPPErrorException When - * @throws SmackException.NotConnectedException something - * @throws InterruptedException goes - * @throws SmackException.NoResponseException wrong - */ - private static OmemoDeviceListElement extractDeviceListFrom(LeafNode node) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { - if (node == null) { - LOGGER.log(Level.WARNING, "DeviceListNode is null."); - return null; - } - List items = node.getItems(); - if (items.size() > 0) { - OmemoDeviceListVAxolotlElement listElement = (OmemoDeviceListVAxolotlElement) ((PayloadItem) items.get(items.size() - 1)).getPayload(); - if (items.size() > 1) { - node.deleteAllItems(); - node.publish(new PayloadItem<>(listElement)); - } - return listElement; - } - - Set emptySet = Collections.emptySet(); - return new OmemoDeviceListVAxolotlElement(emptySet); - } - - /** - * Build sessions for all devices of the contact that we do not have a session with yet. - * - * @param omemoManager omemoManager - * @param jid the BareJid of the contact - */ - void buildOrCreateOmemoSessionsFromBundles(OmemoManager omemoManager, BareJid jid) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, CannotEstablishOmemoSessionException { - CachedDeviceList devices = getOmemoStoreBackend().loadCachedDeviceList(omemoManager, jid); - CannotEstablishOmemoSessionException sessionException = null; - if (devices == null || devices.getAllDevices().isEmpty()) { - refreshDeviceList(omemoManager, jid); - devices = getOmemoStoreBackend().loadCachedDeviceList(omemoManager, jid); - } - - for (int id : devices.getActiveDevices()) { - OmemoDevice device = new OmemoDevice(jid, id); - if (getOmemoStoreBackend().containsRawSession(omemoManager, device)) { - // We have a session already. + if (hasSession(userDevice, device)) { + devicesWithSession.add(device); continue; } - // Build missing session try { - buildSessionFromOmemoBundle(omemoManager, device, false); + buildFreshSessionWithDevice(connection, userDevice, device); + devicesWithSession.add(device); } catch (CannotEstablishOmemoSessionException e) { - - if (sessionException == null) { - sessionException = e; - } else { - sessionException.addFailures(e); - } - + LOGGER.log(Level.WARNING, userDevice + " cannot establish session with " + device + + " because their bundle could not be fetched.", e); } catch (CorruptedOmemoKeyException e) { - CannotEstablishOmemoSessionException fail = - new CannotEstablishOmemoSessionException(device, e); - - if (sessionException == null) { - sessionException = fail; - } else { - sessionException.addFailures(fail); - } + LOGGER.log(Level.WARNING, userDevice + " could not establish session with " + device + + "because their bundle seems to be corrupt.", e); } + } - if (sessionException != null) { - throw sessionException; - } + return devicesWithSession; } /** - * Build an OmemoSession for the given OmemoDevice. + * Build OMEMO sessions with all devices of the contacts, we haven't had sessions with before. + * This method returns a set of OmemoDevices. This set contains all devices, with which we either had sessions + * before, plus those devices with which we just built sessions. * - * @param omemoManager omemoManager - * @param device OmemoDevice - * @param fresh Do we want to build a session even if we already have one? - * @throws CannotEstablishOmemoSessionException when no session could be established - * @throws CorruptedOmemoKeyException when the bundle contained an invalid OMEMO identityKey + * @param connection authenticated XMPP connection. + * @param userDevice our OmemoDevice + * @param contacts set of BareJids of contacts, we want to build sessions with. + * @return set of devices, we have sessions with. + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException */ - public void buildSessionFromOmemoBundle(OmemoManager omemoManager, OmemoDevice device, boolean fresh) throws CannotEstablishOmemoSessionException, CorruptedOmemoKeyException { + private Set buildMissingSessionsWithContacts(XMPPConnection connection, + OmemoDevice userDevice, + Set contacts) + throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { - if (device.equals(omemoManager.getOwnDevice())) { - return; + Set devicesWithSessions = new HashSet<>(); + + for (BareJid contact : contacts) { + Set devices = buildMissingSessionsWithContact(connection, userDevice, contact); + devicesWithSessions.addAll(devices); } - // Do not build sessions with devices we already know... - if (!fresh && getOmemoStoreBackend().containsRawSession(omemoManager, device)) { - getOmemoStoreBackend().getOmemoSessionOf(omemoManager, device); //Make sure its loaded though - return; + return devicesWithSessions; + } + + /** + * Return a set of all devices from the provided set, which trust level is undecided. + * A device is also considered undecided, if its fingerprint cannot be loaded. + * + * @param userDevice our OmemoDevice + * @param callback OmemoTrustCallback to query the trust decisions from + * @param devices set of OmemoDevices + * @return set of OmemoDevices which contains all devices from the set devices, which are undecided + */ + private Set getUndecidedDevices(OmemoDevice userDevice, OmemoTrustCallback callback, Set devices) { + Set undecidedDevices = new HashSet<>(); + + for (OmemoDevice device : devices) { + + OmemoFingerprint fingerprint; + try { + fingerprint = getOmemoStoreBackend().getFingerprint(userDevice, device); + } catch (CorruptedOmemoKeyException | NoIdentityKeyException e) { + // TODO: Best solution? + LOGGER.log(Level.WARNING, "Could not load fingerprint of " + device, e); + undecidedDevices.add(device); + continue; + } + + if (callback.getTrust(device, fingerprint) == TrustState.undecided) { + undecidedDevices.add(device); + } } - OmemoBundleVAxolotlElement bundle; - try { - bundle = fetchBundle(omemoManager, device); - } catch (SmackException | XMPPException.XMPPErrorException | InterruptedException e) { - throw new CannotEstablishOmemoSessionException(device, e); + return undecidedDevices; + } + + /** + * Return a set of all devices from the provided set, which are untrusted. + * A device is also considered untrusted, if its fingerprint cannot be loaded. + * + * @param userDevice our own OmemoDevice + * @param trustCallback OmemoTrustCallback to query trust decisions from + * @param devices set of OmemoDevices + * @return set of OmemoDevices from devices, which contains all devices which are untrusted + */ + private Set getUntrustedDeviced(OmemoDevice userDevice, OmemoTrustCallback trustCallback, Set devices) { + Set untrustedDevices = new HashSet<>(); + + for (OmemoDevice device : devices) { + + OmemoFingerprint fingerprint; + try { + fingerprint = getOmemoStoreBackend().getFingerprint(userDevice, device); + } catch (CorruptedOmemoKeyException | NoIdentityKeyException e) { + // TODO: Best solution? + untrustedDevices.add(device); + continue; + } + + if (trustCallback.getTrust(device, fingerprint) == TrustState.untrusted) { + untrustedDevices.add(device); + } } - HashMap bundles = getOmemoStoreBackend().keyUtil().BUNDLE.bundles(bundle, device); + return untrustedDevices; + } - // 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); + /** + * Return true, if the OmemoManager of userDevice has a session with the contactsDevice. + * + * @param userDevice our OmemoDevice. + * @param contactsDevice OmemoDevice of the contact. + * @return true if userDevice has session with contactsDevice. + */ + private boolean hasSession(OmemoDevice userDevice, OmemoDevice contactsDevice) { + return getOmemoStoreBackend().loadRawSession(userDevice, contactsDevice) != null; } /** * Process a received bundle. Typically that includes saving keys and building a session. * - * @param omemoManager omemoManager that will process the bundle - * @param bundle T_Bundle (depends on used Signal/Olm library) - * @param device OmemoDevice + * @param omemoManager our OmemoManager + * @param contactsBundle bundle of the contact + * @param contactsDevice OmemoDevice of the contact * @throws CorruptedOmemoKeyException */ - protected abstract void processBundle(OmemoManager omemoManager, T_Bundle bundle, OmemoDevice device) throws CorruptedOmemoKeyException; + protected abstract void processBundle(OmemoManager omemoManager, + T_Bundle contactsBundle, + OmemoDevice contactsDevice) + throws CorruptedOmemoKeyException; /** - * Process a received message. Try to decrypt it in case we are a recipient device. If we are not a recipient - * device, return null. + * Returns true, if a rotation of the signed preKey is necessary. * - * @param 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 - * @return decrypted message or null - * @throws NoRawSessionException - * @throws InterruptedException - * @throws SmackException.NoResponseException - * @throws SmackException.NotConnectedException - * @throws CryptoFailedException - * @throws XMPPException.XMPPErrorException - * @throws CorruptedOmemoKeyException + * @param userDevice our OmemoDevice + * @return true if rotation is necessary */ - private Message processReceivingMessage(OmemoManager omemoManager, OmemoDevice sender, OmemoElement message, final OmemoMessageInformation information) - throws NoRawSessionException, InterruptedException, SmackException.NoResponseException, SmackException.NotConnectedException, - CryptoFailedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException { + private boolean shouldRotateSignedPreKey(OmemoDevice userDevice) { + if (!OmemoConfiguration.getRenewOldSignedPreKeys()) { + return false; + } - ArrayList messageRecipientKeys = message.getHeader().getKeys(); - // Do we have a key with our ID in the message? - for (OmemoVAxolotlElement.OmemoHeader.Key k : messageRecipientKeys) { - // Only decrypt with our deviceID - if (k.getId() != omemoManager.getDeviceId()) { - continue; + Date now = new Date(); + Date lastRenewal = getOmemoStoreBackend().getDateOfLastSignedPreKeyRenewal(userDevice); + + if (lastRenewal == null) { + lastRenewal = new Date(); + getOmemoStoreBackend().setDateOfLastSignedPreKeyRenewal(userDevice, lastRenewal); + } + + long allowedAgeMillis = MILLIS_PER_HOUR * OmemoConfiguration.getRenewOldSignedPreKeysAfterHours(); + return now.getTime() - lastRenewal.getTime() > allowedAgeMillis; + } + + /** + * Return a copy of our deviceList, but with stale devices marked as inactive. + * Never mark our own device as stale. + * This method ignores {@link OmemoConfiguration#getDeleteStaleDevices()}! + * + * In this case, a stale device is one of our devices, from which we haven't received an OMEMO message from + * for more than {@link OmemoConfiguration#DELETE_STALE_DEVICE_AFTER_HOURS} hours. + * + * @param userDevice our OmemoDevice + * @return our altered deviceList with stale devices marked as inactive. + */ + private OmemoCachedDeviceList deleteStaleDevices(OmemoDevice userDevice) { + OmemoCachedDeviceList deviceList = getOmemoStoreBackend().loadCachedDeviceList(userDevice); + int maxAgeHours = OmemoConfiguration.getDeleteStaleDevicesAfterHours(); + return removeStaleDevicesFromDeviceList(userDevice, userDevice.getJid(), deviceList, maxAgeHours); + } + + /** + * Return a copy of the contactsDeviceList, but with stale devices marked as inactive. + * Never mark our own device as stale. + * This method ignores {@link OmemoConfiguration#getIgnoreStaleDevices()}! + * + * In this case, a stale device is one of our devices, from which we haven't received an OMEMO message from + * for more than {@link OmemoConfiguration#IGNORE_STALE_DEVICE_AFTER_HOURS} hours. + * + * @param userDevice our OmemoDevice + * @param contact subjects BareJid + * @param contactsDeviceList subjects deviceList + * @return + */ + private OmemoCachedDeviceList ignoreStaleDevices(OmemoDevice userDevice, BareJid contact, OmemoCachedDeviceList contactsDeviceList) { + int maxAgeHours = OmemoConfiguration.getIgnoreStaleDevicesAfterHours(); + return removeStaleDevicesFromDeviceList(userDevice, contact, contactsDeviceList, maxAgeHours); + } + + /** + * Return a copy of the given deviceList of user contact, but with stale devices marked as inactive. + * Never mark our own device as stale. If we haven't yet received a message from a device, store the current date + * as last date of message receipt to allow future decisions. + * + * A stale device is a device, from which we haven't received an OMEMO message from for more than + * "maxAgeMillis" milliseconds. + * + * @param userDevice our OmemoDevice. + * @param contact subjects BareJid. + * @param contactsDeviceList subjects deviceList. + * @return copy of subjects deviceList with stale devices marked as inactive. + */ + private OmemoCachedDeviceList removeStaleDevicesFromDeviceList(OmemoDevice userDevice, + BareJid contact, + OmemoCachedDeviceList contactsDeviceList, + int maxAgeHours) { + OmemoCachedDeviceList deviceList = new OmemoCachedDeviceList(contactsDeviceList); // Don't work on original list. + + // Iterate through original list, but modify copy instead + for (int deviceId : contactsDeviceList.getActiveDevices()) { + OmemoDevice device = new OmemoDevice(contact, deviceId); + + Date lastDeviceIdPublication = getOmemoStoreBackend().getDateOfLastDeviceIdPublication(userDevice, device); + if (lastDeviceIdPublication == null) { + lastDeviceIdPublication = new Date(); + getOmemoStoreBackend().setDateOfLastDeviceIdPublication(userDevice, device, lastDeviceIdPublication); } - Message decrypted = decryptOmemoMessageElement(omemoManager, sender, message, information); - if (sender.equals(omemoManager.getOwnJid()) && decrypted != null) { - getOmemoStoreBackend().setDateOfLastReceivedMessage(omemoManager, sender); - } - return decrypted; - } - - LOGGER.log(Level.INFO, "There is no key with our deviceId. Silently discard the message."); - return null; - } - - /** - * Decrypt a given OMEMO encrypted message. Return null, if there is no OMEMO element in the message, - * otherwise try to decrypt the message and return a ClearTextMessage object. - * - * @param omemoManager omemoManager of the receiving device - * @param sender barejid of the sender - * @param message encrypted message - * @return decrypted message or null - * @throws InterruptedException Exception - * @throws SmackException.NoResponseException Exception - * @throws SmackException.NotConnectedException Exception - * @throws CryptoFailedException When the message could not be decrypted. - * @throws XMPPException.XMPPErrorException Exception - * @throws CorruptedOmemoKeyException When the used OMEMO keys are invalid. - * @throws NoRawSessionException When there is no session to decrypt the message with in the double - * ratchet library - */ - ClearTextMessage processLocalMessage(OmemoManager omemoManager, BareJid sender, Message message) throws InterruptedException, SmackException.NoResponseException, SmackException.NotConnectedException, CryptoFailedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException, NoRawSessionException { - if (OmemoManager.stanzaContainsOmemoElement(message)) { - OmemoElement omemoMessageElement = message.getExtension(OmemoElement.ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); - OmemoMessageInformation info = new OmemoMessageInformation(); - Message decrypted = processReceivingMessage(omemoManager, - new OmemoDevice(sender, omemoMessageElement.getHeader().getSid()), - omemoMessageElement, info); - return new ClearTextMessage(decrypted != null ? decrypted.getBody() : null, message, info); - } else { - LOGGER.log(Level.WARNING, "Stanza does not contain an OMEMO message."); - return null; - } - } - - /** - * Encrypt a clear text message for the given recipient. - * The body of the message will be encrypted. - * - * @param omemoManager omemoManager of the sending device - * @param recipient BareJid of the recipient - * @param message message to encrypt. - * @return OmemoMessageElement - * @throws CryptoFailedException - * @throws UndecidedOmemoIdentityException - * @throws NoSuchAlgorithmException - */ - OmemoVAxolotlElement processSendingMessage(OmemoManager omemoManager, BareJid recipient, Message message) - throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, CannotEstablishOmemoSessionException { - ArrayList recipients = new ArrayList<>(); - recipients.add(recipient); - return processSendingMessage(omemoManager, 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 recipients List of BareJids of all recipients - * @param message message to encrypt. - * @return OmemoMessageElement - * @throws CryptoFailedException - * @throws UndecidedOmemoIdentityException - * @throws NoSuchAlgorithmException - */ - OmemoVAxolotlElement processSendingMessage(OmemoManager omemoManager, ArrayList recipients, Message message) - throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, CannotEstablishOmemoSessionException { - - 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); - } catch (CannotEstablishOmemoSessionException e) { - - if (sessionException == null) { - sessionException = e; - } else { - sessionException.addFailures(e); - } - } - } - - for (BareJid recipient : recipients) { - CachedDeviceList theirDevices = getOmemoStoreBackend().loadCachedDeviceList(omemoManager, recipient); - ArrayList receivingDevices = new ArrayList<>(); - for (int id : theirDevices.getActiveDevices()) { - OmemoDevice recipientDevice = new OmemoDevice(recipient, id); - - if (getOmemoStoreBackend().containsRawSession(omemoManager, recipientDevice)) { - receivingDevices.add(recipientDevice); - } - - if (sessionException != null) { - sessionException.addSuccess(recipientDevice); - } + Date lastMessageReceived = getOmemoStoreBackend().getDateOfLastReceivedMessage(userDevice, device); + if (lastMessageReceived == null) { + lastMessageReceived = new Date(); + getOmemoStoreBackend().setDateOfLastReceivedMessage(userDevice, device, lastMessageReceived); } - if (!receivingDevices.isEmpty()) { - receivers.put(recipient, receivingDevices); + boolean stale = isStale(userDevice, device, lastDeviceIdPublication, maxAgeHours); + stale &= isStale(userDevice, device, lastMessageReceived, maxAgeHours); + + if (stale) { + deviceList.addInactiveDevice(deviceId); } } - - // Us - We want to read the message on all of our devices - CachedDeviceList ourDevices = getOmemoStoreBackend().loadCachedDeviceList(omemoManager, omemoManager.getOwnJid()); - if (ourDevices == null) { - ourDevices = new CachedDeviceList(); - } - - ArrayList ourReceivingDevices = new ArrayList<>(); - for (int id : ourDevices.getActiveDevices()) { - OmemoDevice ourDevice = new OmemoDevice(omemoManager.getOwnJid(), id); - if (id == omemoManager.getDeviceId()) { - // Don't build session with our exact device. - continue; - } - - Date lastReceived = getOmemoStoreBackend().getDateOfLastReceivedMessage(omemoManager, ourDevice); - if (lastReceived == null) { - getOmemoStoreBackend().setDateOfLastReceivedMessage(omemoManager, ourDevice); - 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 + - " which was inactive for at least " + OmemoConfiguration.getIgnoreStaleDevicesAfterHours() + - " hours."); - } else { - if (getOmemoStoreBackend().containsRawSession(omemoManager, ourDevice)) { - ourReceivingDevices.add(ourDevice); - } - } - } - - if (!ourReceivingDevices.isEmpty()) { - receivers.put(omemoManager.getOwnJid(), ourReceivingDevices); - } - - if (sessionException != null && sessionException.requiresThrowing()) { - throw sessionException; - } - - return encryptOmemoMessage(omemoManager, receivers, message); + return deviceList; } - /** - * Decrypt a incoming OmemoMessageElement that was sent by the OmemoDevice 'from'. - * - * @param omemoManager omemoManager of the decrypting device. - * @param from OmemoDevice that sent the message - * @param message Encrypted OmemoMessageElement - * @param information OmemoMessageInformation object which will contain metadata about the encryption - * @return Decrypted message - * @throws CryptoFailedException when decrypting message fails for some reason - * @throws InterruptedException - * @throws CorruptedOmemoKeyException - * @throws XMPPException.XMPPErrorException - * @throws SmackException.NotConnectedException - * @throws SmackException.NoResponseException - * @throws NoRawSessionException - */ - private Message decryptOmemoMessageElement(OmemoManager omemoManager, OmemoDevice from, OmemoElement message, - final OmemoMessageInformation information) - throws CryptoFailedException, InterruptedException, CorruptedOmemoKeyException, XMPPException.XMPPErrorException, - SmackException.NotConnectedException, SmackException.NoResponseException, NoRawSessionException { - - CipherAndAuthTag transportedKey = decryptTransportedOmemoKey(omemoManager, from, message, information); - return OmemoSession.decryptMessageElement(message, transportedKey); - } /** - * Decrypt a messageKey that was transported in an OmemoElement. + * Remove our device from the collection of devices. * - * @param omemoManager omemoManager of the receiving device. - * @param sender omemoDevice of the sender. - * @param omemoMessage omemoElement containing the key. - * @param messageInfo omemoMessageInformation that will contain metadata about the encryption. - * @return a CipherAndAuthTag pair - * @throws CryptoFailedException - * @throws NoRawSessionException - * @throws InterruptedException - * @throws CorruptedOmemoKeyException - * @throws XMPPException.XMPPErrorException - * @throws SmackException.NotConnectedException - * @throws SmackException.NoResponseException + * @param userDevice our OmemoDevice + * @param devices collection of OmemoDevices */ - private CipherAndAuthTag decryptTransportedOmemoKey(OmemoManager omemoManager, OmemoDevice sender, - OmemoElement omemoMessage, - OmemoMessageInformation messageInfo) - throws CryptoFailedException, NoRawSessionException, InterruptedException, CorruptedOmemoKeyException, - XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException { - - int preKeyCountBefore = getOmemoStoreBackend().loadOmemoPreKeys(omemoManager).size(); - - OmemoSession - session = getOmemoStoreBackend().getOmemoSessionOf(omemoManager, sender); - CipherAndAuthTag cipherAndAuthTag = session.decryptTransportedKey(omemoMessage, omemoManager.getDeviceId()); - - messageInfo.setSenderDevice(sender); - messageInfo.setSenderIdentityKey(new IdentityKeyWrapper(session.getIdentityKey())); - - if (preKeyCountBefore != getOmemoStoreBackend().loadOmemoPreKeys(omemoManager).size()) { - LOGGER.log(Level.INFO, "We used up a preKey. Publish new Bundle."); - publishBundle(omemoManager); - } - return cipherAndAuthTag; - } - - /** - * Encrypt the message and return it as an OmemoMessageElement. - * - * @param omemoManager omemoManager of the encrypting device. - * @param recipients List of devices that will be able to decipher the message. - * @param message Clear text message - * - * @throws CryptoFailedException when some cryptographic function fails - * @throws UndecidedOmemoIdentityException when the identity of one or more contacts is undecided - * - * @return OmemoMessageElement - */ - OmemoVAxolotlElement encryptOmemoMessage(OmemoManager omemoManager, HashMap> recipients, Message message) - throws CryptoFailedException, UndecidedOmemoIdentityException { - - OmemoMessageBuilder - builder; - try { - builder = new OmemoMessageBuilder<>(omemoManager, getOmemoStoreBackend(), message.getBody()); - } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | - NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { - throw new CryptoFailedException(e); - } - - UndecidedOmemoIdentityException undecided = null; - - for (Map.Entry> entry : recipients.entrySet()) { - for (OmemoDevice c : entry.getValue()) { - try { - builder.addRecipient(c); - } catch (CorruptedOmemoKeyException e) { - // TODO: How to react? - LOGGER.log(Level.SEVERE, "encryptOmemoMessage failed to establish a session with device " - + c + ": " + e.getMessage()); - } catch (UndecidedOmemoIdentityException e) { - // Collect all undecided devices - if (undecided == null) { - undecided = e; - } else { - undecided.join(e); - } - } - } - } - - if (undecided != null) { - throw undecided; - } - - return builder.finish(); - } - - /** - * Prepares a keyTransportElement with a random aes key and iv. - * - * @param omemoManager omemoManager of the sending device. - * @param recipients recipients of the omemoKeyTransportElement - * @return KeyTransportElement - * @throws CryptoFailedException - * @throws UndecidedOmemoIdentityException - * @throws CorruptedOmemoKeyException - * @throws CannotEstablishOmemoSessionException - */ - OmemoVAxolotlElement prepareOmemoKeyTransportElement(OmemoManager omemoManager, OmemoDevice... recipients) throws CryptoFailedException, - UndecidedOmemoIdentityException, CorruptedOmemoKeyException, CannotEstablishOmemoSessionException { - - OmemoMessageBuilder - builder; - try { - builder = new OmemoMessageBuilder<>(omemoManager, getOmemoStoreBackend(), null); - - } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | - NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { - throw new CryptoFailedException(e); - } - - for (OmemoDevice r : recipients) { - builder.addRecipient(r); - } - - return builder.finish(); - } - - /** - * Prepare a KeyTransportElement with aesKey and iv. - * - * @param omemoManager OmemoManager of the sending device. - * @param aesKey AES key - * @param iv initialization vector - * @param recipients recipients - * @return KeyTransportElement - * @throws CryptoFailedException - * @throws UndecidedOmemoIdentityException - * @throws CorruptedOmemoKeyException - * @throws CannotEstablishOmemoSessionException - */ - OmemoVAxolotlElement prepareOmemoKeyTransportElement(OmemoManager omemoManager, byte[] aesKey, byte[] iv, OmemoDevice... recipients) throws CryptoFailedException, - UndecidedOmemoIdentityException, CorruptedOmemoKeyException, CannotEstablishOmemoSessionException { - - OmemoMessageBuilder - builder; - try { - builder = new OmemoMessageBuilder<>(omemoManager, getOmemoStoreBackend(), aesKey, iv); - - } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | - NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { - throw new CryptoFailedException(e); - } - - for (OmemoDevice r : recipients) { - builder.addRecipient(r); - } - - return builder.finish(); - } - - /** - * Return a new RatchetUpdateMessage. - * - * @param omemoManager omemoManager of the sending device. - * @param recipient recipient - * @param preKeyMessage if true, a new session will be built for this message (useful to repair broken sessions) - * otherwise the message will be encrypted using the existing session. - * @return OmemoRatchetUpdateMessage - * @throws CannotEstablishOmemoSessionException - * @throws CorruptedOmemoKeyException - * @throws CryptoFailedException - * @throws UndecidedOmemoIdentityException - */ - protected Message getOmemoRatchetUpdateMessage(OmemoManager omemoManager, OmemoDevice recipient, boolean preKeyMessage) throws CannotEstablishOmemoSessionException, CorruptedOmemoKeyException, CryptoFailedException, UndecidedOmemoIdentityException { - if (preKeyMessage) { - buildSessionFromOmemoBundle(omemoManager, recipient, true); - } - - OmemoVAxolotlElement keyTransportElement = prepareOmemoKeyTransportElement(omemoManager, recipient); - Message ratchetUpdateMessage = omemoManager.finishMessage(keyTransportElement); - ratchetUpdateMessage.setTo(recipient.getJid()); - - return ratchetUpdateMessage; - } - - /** - * Send an OmemoRatchetUpdateMessage to recipient. If preKeyMessage is true, the message will be encrypted using a - * freshly built session. This can be used to repair broken sessions. - * - * @param omemoManager omemoManager of the sending device. - * @param recipient recipient - * @param preKeyMessage shall this be a preKeyMessage? - * @throws UndecidedOmemoIdentityException - * @throws CorruptedOmemoKeyException - * @throws CryptoFailedException - * @throws CannotEstablishOmemoSessionException - */ - protected void sendOmemoRatchetUpdateMessage(OmemoManager omemoManager, OmemoDevice recipient, boolean preKeyMessage) throws UndecidedOmemoIdentityException, CorruptedOmemoKeyException, CryptoFailedException, CannotEstablishOmemoSessionException { - Message ratchetUpdateMessage = getOmemoRatchetUpdateMessage(omemoManager, recipient, preKeyMessage); - - try { - omemoManager.getConnection().sendStanza(ratchetUpdateMessage); - - } catch (SmackException.NotConnectedException | InterruptedException e) { - LOGGER.log(Level.WARNING, "sendOmemoRatchetUpdateMessage failed: " + e.getMessage()); + static void removeOurDevice(OmemoDevice userDevice, Collection devices) { + if (devices.contains(userDevice)) { + devices.remove(userDevice); } } /** - * Listen for incoming messages and carbons, decrypt them and pass the cleartext messages to the registered - * OmemoMessageListeners. * - * @param omemoManager omemoManager we want to register with + * @param userDevice + * @param subject + * @param lastReceipt + * @param maxAgeHours + * @return */ - private void registerOmemoMessageStanzaListeners(OmemoManager omemoManager) { - omemoManager.getConnection().removeAsyncStanzaListener(omemoManager.getOmemoStanzaListener()); - omemoManager.getConnection().addAsyncStanzaListener(omemoManager.getOmemoStanzaListener(), omemoStanzaFilter); + static boolean isStale(OmemoDevice userDevice, OmemoDevice subject, Date lastReceipt, int maxAgeHours) { + if (userDevice.equals(subject)) { + return false; + } - CarbonManager.getInstanceFor(omemoManager.getConnection()).removeCarbonCopyReceivedListener(omemoManager.getOmemoCarbonCopyListener()); - CarbonManager.getInstanceFor(omemoManager.getConnection()).addCarbonCopyReceivedListener(omemoManager.getOmemoCarbonCopyListener()); + if (lastReceipt == null) { + return false; + } + + long maxAgeMillis = MILLIS_PER_HOUR * maxAgeHours; + Date now = new Date(); + + return now.getTime() - lastReceipt.getTime() > maxAgeMillis; } /** - * StanzaFilter that filters messages containing a OMEMO element. + * Gullible TrustCallback, which returns all queried identities as trusted. + * This is only used for insensitive OMEMO messages like RatchetUpdateMessages. + * DO NOT USE THIS FOR ANYTHING ELSE! */ - private final StanzaFilter omemoStanzaFilter = new StanzaFilter() { + private static final OmemoTrustCallback gullibleTrustCallback = new OmemoTrustCallback() { @Override - public boolean accept(Stanza stanza) { - return stanza instanceof Message && OmemoManager.stanzaContainsOmemoElement(stanza); + public TrustState getTrust(OmemoDevice device, OmemoFingerprint fingerprint) { + return TrustState.trusted; + } + + @Override + public void setTrust(OmemoDevice device, OmemoFingerprint fingerprint, TrustState state) { + // Not needed } }; /** - * Try to decrypt a mamQueryResult. Note that OMEMO messages can only be decrypted once on a device, so if you - * try to decrypt a message that has been decrypted earlier in time, the decryption will fail. You should handle - * message history locally when using OMEMO, since you cannot rely on MAM. + * Decrypt a possible OMEMO encrypted messages in a {@link MamManager.MamQuery}. + * The returned list contains wrappers that either hold an {@link OmemoMessage} in case the message was decrypted + * properly, otherwise it contains the message itself. * - * @param omemoManager omemoManager of the decrypting device. - * @param mamQueryResult mamQueryResult that shall be decrypted. - * @return list of decrypted messages. + * @param managerGuard authenticated OmemoManager. + * @param mamQuery Mam archive query + * @return list of {@link MessageOrOmemoMessage}s. + */ + List decryptMamQueryResult(OmemoManager.LoggedInOmemoManager managerGuard, + MamManager.MamQuery mamQuery) { + List result = new ArrayList<>(); + for (Message message : mamQuery.getMessages()) { + if (OmemoManager.stanzaContainsOmemoElement(message)) { + OmemoElement element = + message.getExtension(OmemoElement.NAME_ENCRYPTED, OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL); + // Decrypt OMEMO messages + try { + OmemoMessage.Received omemoMessage = decryptMessage(managerGuard, message.getFrom().asBareJid(), element); + result.add(new MessageOrOmemoMessage(omemoMessage)); + } catch (NoRawSessionException | CorruptedOmemoKeyException | CryptoFailedException e) { + LOGGER.log(Level.WARNING, "decryptMamQueryResult failed to decrypt message from " + + message.getFrom() + " due to corrupted session/key: " + e.getMessage()); + result.add(new MessageOrOmemoMessage(message)); + } + } else { + // Wrap cleartext messages + result.add(new MessageOrOmemoMessage(message)); + } + } + + return result; + } + + + @Override + public void onOmemoCarbonCopyReceived(CarbonExtension.Direction direction, + Message carbonCopy, + Message wrappingMessage, + OmemoManager.LoggedInOmemoManager managerGuard) { + OmemoManager manager = managerGuard.get(); + // Avoid the ratchet being manipulated and the bundle being published multiple times simultaneously + synchronized (manager.LOCK) { + OmemoDevice userDevice = manager.getOwnDevice(); + OmemoElement element = carbonCopy.getExtension(OmemoElement.NAME_ENCRYPTED, OmemoElement_VAxolotl.NAMESPACE); + if (element == null) { + return; + } + + OmemoMessage.Received decrypted; + BareJid sender = carbonCopy.getFrom().asBareJid(); + + try { + decrypted = decryptMessage(managerGuard, sender, element); + manager.notifyOmemoCarbonCopyReceived(direction, carbonCopy, wrappingMessage, decrypted); + + if (decrypted.isPreKeyMessage() && OmemoConfiguration.getCompleteSessionWithEmptyMessage()) { + LOGGER.log(Level.FINE, "Received a preKeyMessage in a carbon copy from " + decrypted.getSenderDevice() + ".\n" + + "Complete the session by sending an empty response message."); + try { + sendRatchetUpdate(managerGuard, decrypted.getSenderDevice()); + } catch (CannotEstablishOmemoSessionException e) { + throw new AssertionError("Since we successfully received a message, we MUST be able to " + + "establish a session. " + e); + } catch (NoSuchAlgorithmException | InterruptedException | SmackException.NotConnectedException | SmackException.NoResponseException e) { + LOGGER.log(Level.WARNING, "Cannot send a ratchet update message.", e); + } + } + } catch (NoRawSessionException e) { + OmemoDevice device = e.getDeviceWithoutSession(); + LOGGER.log(Level.WARNING, "No raw session found for contact " + device + ". ", e); + + if (OmemoConfiguration.getRepairBrokenSessionsWithPreKeyMessages()) { + repairBrokenSessionWithPreKeyMessage(managerGuard, device); + } + } catch (CorruptedOmemoKeyException | CryptoFailedException e) { + LOGGER.log(Level.WARNING, "Could not decrypt incoming carbon copy: ", e); + } + + // Upload fresh bundle. + if (getOmemoStoreBackend().loadOmemoPreKeys(userDevice).size() < OmemoConstants.PRE_KEY_COUNT_PER_BUNDLE) { + LOGGER.log(Level.FINE, "We used up a preKey. Upload a fresh bundle."); + try { + getOmemoStoreBackend().replenishKeys(userDevice); + OmemoBundleElement bundleElement = getOmemoStoreBackend().packOmemoBundle(userDevice); + publishBundle(manager.getConnection(), userDevice, bundleElement); + } catch (CorruptedOmemoKeyException | InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException | XMPPException.XMPPErrorException e) { + LOGGER.log(Level.WARNING, "Could not republish replenished bundle.", e); + } + } + } + } + + @Override + public void onOmemoMessageStanzaReceived(Stanza stanza, OmemoManager.LoggedInOmemoManager managerGuard) { + OmemoManager manager = managerGuard.get(); + // Avoid the ratchet being manipulated and the bundle being published multiple times simultaneously + synchronized (manager.LOCK) { + OmemoDevice userDevice = manager.getOwnDevice(); + OmemoElement element = stanza.getExtension(OmemoElement.NAME_ENCRYPTED, OmemoElement_VAxolotl.NAMESPACE); + if (element == null) { + return; + } + + OmemoMessage.Received decrypted; + BareJid sender; + + try { + MultiUserChat muc = getMuc(manager.getConnection(), stanza.getFrom()); + if (muc != null) { + Occupant occupant = muc.getOccupant(stanza.getFrom().asEntityFullJidIfPossible()); + if (occupant == null) { + LOGGER.log(Level.WARNING, "Cannot decrypt OMEMO MUC message; MUC Occupant is null."); + return; + } + Jid occupantJid = occupant.getJid(); + + if (occupantJid == null) { + LOGGER.log(Level.WARNING, "Cannot decrypt OMEMO MUC message; Senders Jid is null. " + + stanza.getFrom()); + return; + } + + sender = occupantJid.asBareJid(); + + // try is for this + decrypted = decryptMessage(managerGuard, sender, element); + manager.notifyOmemoMucMessageReceived(muc, stanza, decrypted); + + } else { + sender = stanza.getFrom().asBareJid(); + + // and this + decrypted = decryptMessage(managerGuard, sender, element); + manager.notifyOmemoMessageReceived(stanza, decrypted); + } + + if (decrypted.isPreKeyMessage() && OmemoConfiguration.getCompleteSessionWithEmptyMessage()) { + LOGGER.log(Level.FINE, "Received a preKeyMessage from " + decrypted.getSenderDevice() + ".\n" + + "Complete the session by sending an empty response message."); + try { + sendRatchetUpdate(managerGuard, decrypted.getSenderDevice()); + } catch (CannotEstablishOmemoSessionException e) { + throw new AssertionError("Since we successfully received a message, we MUST be able to " + + "establish a session. " + e); + } catch (NoSuchAlgorithmException | InterruptedException | SmackException.NotConnectedException | SmackException.NoResponseException e) { + LOGGER.log(Level.WARNING, "Cannot send a ratchet update message.", e); + } + } + } catch (NoRawSessionException e) { + OmemoDevice device = e.getDeviceWithoutSession(); + LOGGER.log(Level.WARNING, "No raw session found for contact " + device + ". ", e); + + if (OmemoConfiguration.getRepairBrokenSessionsWithPreKeyMessages()) { + repairBrokenSessionWithPreKeyMessage(managerGuard, device); + } + } catch (CorruptedOmemoKeyException | CryptoFailedException e) { + LOGGER.log(Level.WARNING, "Could not decrypt incoming message: ", e); + } + + // Upload fresh bundle. + if (getOmemoStoreBackend().loadOmemoPreKeys(userDevice).size() < OmemoConstants.PRE_KEY_COUNT_PER_BUNDLE) { + LOGGER.log(Level.FINE, "We used up a preKey. Upload a fresh bundle."); + try { + getOmemoStoreBackend().replenishKeys(userDevice); + OmemoBundleElement bundleElement = getOmemoStoreBackend().packOmemoBundle(userDevice); + publishBundle(manager.getConnection(), userDevice, bundleElement); + } catch (CorruptedOmemoKeyException | InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException | XMPPException.XMPPErrorException e) { + LOGGER.log(Level.WARNING, "Could not republish replenished bundle.", e); + } + } + } + } + + /** + * Decrypt the OmemoElement inside the given Stanza and return it. + * Return null if something goes wrong. + * + * @param stanza stanza + * @param managerGuard authenticated OmemoManager + * @return decrypted OmemoMessage or null + */ + OmemoMessage.Received decryptStanza(Stanza stanza, OmemoManager.LoggedInOmemoManager managerGuard) { + OmemoManager manager = managerGuard.get(); + // Avoid the ratchet being manipulated and the bundle being published multiple times simultaneously + synchronized (manager.LOCK) { + OmemoDevice userDevice = manager.getOwnDevice(); + OmemoElement element = stanza.getExtension(OmemoElement.NAME_ENCRYPTED, OmemoElement_VAxolotl.NAMESPACE); + if (element == null) { + return null; + } + + OmemoMessage.Received decrypted = null; + BareJid sender; + + try { + MultiUserChat muc = getMuc(manager.getConnection(), stanza.getFrom()); + if (muc != null) { + Occupant occupant = muc.getOccupant(stanza.getFrom().asEntityFullJidIfPossible()); + Jid occupantJid = occupant.getJid(); + + if (occupantJid == null) { + LOGGER.log(Level.WARNING, "MUC message received, but there is no way to retrieve the senders Jid. " + + stanza.getFrom()); + return null; + } + + sender = occupantJid.asBareJid(); + + // try is for this + decrypted = decryptMessage(managerGuard, sender, element); + + } else { + sender = stanza.getFrom().asBareJid(); + + // and this + decrypted = decryptMessage(managerGuard, sender, element); + } + + if (decrypted.isPreKeyMessage() && OmemoConfiguration.getCompleteSessionWithEmptyMessage()) { + LOGGER.log(Level.FINE, "Received a preKeyMessage from " + decrypted.getSenderDevice() + ".\n" + + "Complete the session by sending an empty response message."); + try { + sendRatchetUpdate(managerGuard, decrypted.getSenderDevice()); + } catch (CannotEstablishOmemoSessionException e) { + throw new AssertionError("Since we successfully received a message, we MUST be able to " + + "establish a session. " + e); + } catch (NoSuchAlgorithmException | InterruptedException | SmackException.NotConnectedException | SmackException.NoResponseException e) { + LOGGER.log(Level.WARNING, "Cannot send a ratchet update message.", e); + } + } + } catch (NoRawSessionException e) { + OmemoDevice device = e.getDeviceWithoutSession(); + LOGGER.log(Level.WARNING, "No raw session found for contact " + device + ". ", e); + + } catch (CorruptedOmemoKeyException | CryptoFailedException e) { + LOGGER.log(Level.WARNING, "Could not decrypt incoming message: ", e); + } + + // Upload fresh bundle. + if (getOmemoStoreBackend().loadOmemoPreKeys(userDevice).size() < OmemoConstants.PRE_KEY_COUNT_PER_BUNDLE) { + LOGGER.log(Level.FINE, "We used up a preKey. Upload a fresh bundle."); + try { + getOmemoStoreBackend().replenishKeys(userDevice); + OmemoBundleElement bundleElement = getOmemoStoreBackend().packOmemoBundle(userDevice); + publishBundle(manager.getConnection(), userDevice, bundleElement); + } catch (CorruptedOmemoKeyException | InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException | XMPPException.XMPPErrorException e) { + LOGGER.log(Level.WARNING, "Could not republish replenished bundle.", e); + } + } + return decrypted; + } + } + + /** + * Fetch and process a fresh bundle and send an empty preKeyMessage in order to establish a fresh session. + * + * @param managerGuard authenticated OmemoManager. + * @param brokenDevice device which session broke. + */ + private void repairBrokenSessionWithPreKeyMessage(OmemoManager.LoggedInOmemoManager managerGuard, + OmemoDevice brokenDevice) { + + LOGGER.log(Level.WARNING, "Attempt to repair the session by sending a fresh preKey message to " + + brokenDevice); + OmemoManager manager = managerGuard.get(); + try { + // Create fresh session and send new preKeyMessage. + buildFreshSessionWithDevice(manager.getConnection(), manager.getOwnDevice(), brokenDevice); + sendRatchetUpdate(managerGuard, brokenDevice); + + } catch (CannotEstablishOmemoSessionException | CorruptedOmemoKeyException e) { + LOGGER.log(Level.WARNING, "Unable to repair session with " + brokenDevice, e); + } catch (SmackException.NotConnectedException | InterruptedException | SmackException.NoResponseException e) { + LOGGER.log(Level.WARNING, "Could not fetch fresh bundle for " + brokenDevice, e); + } catch (CryptoFailedException | NoSuchAlgorithmException e) { + LOGGER.log(Level.WARNING, "Could not create PreKeyMessage", e); + } + } + + /** + * Send an empty OMEMO message to contactsDevice in order to forward the ratchet. + * @param managerGuard + * @param contactsDevice + * @throws CorruptedOmemoKeyException if our or their OMEMO key is corrupted. + * @throws InterruptedException + * @throws SmackException.NoResponseException + * @throws NoSuchAlgorithmException if AES encryption fails + * @throws SmackException.NotConnectedException + * @throws CryptoFailedException if encryption fails (should not happen though, but who knows...) + * @throws CannotEstablishOmemoSessionException if we cannot establish a session with contactsDevice. + */ + private void sendRatchetUpdate(OmemoManager.LoggedInOmemoManager managerGuard, OmemoDevice contactsDevice) + throws CorruptedOmemoKeyException, InterruptedException, SmackException.NoResponseException, + NoSuchAlgorithmException, SmackException.NotConnectedException, CryptoFailedException, + CannotEstablishOmemoSessionException { + + OmemoManager manager = managerGuard.get(); + OmemoElement ratchetUpdate = createRatchetUpdateElement(managerGuard, contactsDevice); + Message m = new Message(); + m.setTo(contactsDevice.getJid()); + m.addExtension(ratchetUpdate); + manager.getConnection().sendStanza(m); + } + + /** + * Return the joined MUC with EntityBareJid jid, or null if its not a room and/or not joined. + * @param connection xmpp connection + * @param jid jid (presumably) of the MUC + * @return MultiUserChat or null if not a MUC. + */ + private static MultiUserChat getMuc(XMPPConnection connection, Jid jid) { + EntityBareJid ebj = jid.asEntityBareJidIfPossible(); + if (ebj == null) { + return null; + } + + MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(connection); + Set joinedRooms = mucm.getJoinedRooms(); + if (joinedRooms.contains(ebj)) { + return mucm.getMultiUserChat(ebj); + } + + return null; + } + + /** + * Publish a new DeviceList with just our device in it. + * + * @param managerGuard authenticated OmemoManager. * @throws InterruptedException * @throws XMPPException.XMPPErrorException * @throws SmackException.NotConnectedException * @throws SmackException.NoResponseException */ - List decryptMamQueryResult(OmemoManager omemoManager, MamManager.MamQuery mamQuery) - throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException { - List result = new ArrayList<>(); - for (Message message : mamQuery.getMessages()) { - if (OmemoManager.stanzaContainsOmemoElement(message)) { - // Decrypt OMEMO messages - try { - result.add(processLocalMessage(omemoManager, message.getFrom().asBareJid(), message)); - } catch (NoRawSessionException | CorruptedOmemoKeyException | CryptoFailedException e) { - LOGGER.log(Level.WARNING, "decryptMamQueryResult failed to decrypt message from " - + message.getFrom() + " due to corrupted session/key: " + e.getMessage()); - } - } else { - // Wrap cleartext messages - Message m = message; - result.add(new ClearTextMessage(m.getBody(), m, - new OmemoMessageInformation(null, null, OmemoMessageInformation.CARBON.NONE, false))); - } - } - return result; - } + public void purgeDeviceList(OmemoManager.LoggedInOmemoManager managerGuard) + throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, + SmackException.NoResponseException { - /** - * Return the barejid of the user that sent the message inside the MUC. If the message wasn't sent in a MUC, - * return null; - * - * @param omemoManager omemoManager - * @param stanza message - * @return BareJid of the sender. - */ - private static OmemoDevice getSender(OmemoManager omemoManager, Stanza stanza) { - OmemoElement omemoElement = stanza.getExtension(OmemoElement.ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); - Jid sender = stanza.getFrom(); - if (isMucMessage(omemoManager, stanza)) { - MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(omemoManager.getConnection()); - MultiUserChat muc = mucm.getMultiUserChat(sender.asEntityBareJidIfPossible()); - sender = muc.getOccupant(sender.asEntityFullJidIfPossible()).getJid().asBareJid(); - } - if (sender == null) { - throw new AssertionError("Sender is null."); - } - return new OmemoDevice(sender.asBareJid(), omemoElement.getHeader().getSid()); - } + OmemoManager omemoManager = managerGuard.get(); + OmemoDevice userDevice = omemoManager.getOwnDevice(); - /** - * Return true, if the user knows a multiUserChat with a jid matching the sender of the stanza. - * @param omemoManager omemoManager of the user - * @param stanza stanza in question - * @return true if MUC message, otherwise false. - */ - private static boolean isMucMessage(OmemoManager omemoManager, Stanza stanza) { - BareJid sender = stanza.getFrom().asBareJid(); - MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(omemoManager.getConnection()); + OmemoDeviceListElement_VAxolotl newList = + new OmemoDeviceListElement_VAxolotl(Collections.singleton(userDevice.getDeviceId())); - return mucm.getJoinedRooms().contains(sender.asEntityBareJidIfPossible()); - } + // Merge list + getOmemoStoreBackend().mergeCachedDeviceList(userDevice, userDevice.getJid(), newList); - OmemoStanzaListener createStanzaListener(OmemoManager omemoManager) { - return new OmemoStanzaListener(omemoManager, this); - } - - /** - * StanzaListener that listens for incoming omemoElements that are NOT send via carbons. - */ - class OmemoStanzaListener implements StanzaListener { - private final OmemoManager omemoManager; - private final OmemoService - service; - - OmemoStanzaListener(OmemoManager omemoManager, - OmemoService service) { - this.omemoManager = omemoManager; - this.service = service; - } - - @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); - try { - // Is it a MUC message... - if (isMucMessage(omemoManager, stanza)) { - - MultiUserChat muc = mucm.getMultiUserChat(stanza.getFrom().asEntityBareJidIfPossible()); - if (omemoMessage.isMessageElement()) { - - 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: " - + 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); - } - - /** - * StanzaListener that listens for incoming OmemoElements that ARE sent in carbons. - */ - class OmemoCarbonCopyListener implements CarbonCopyReceivedListener { - - 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; - } - - @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(); - - if (CarbonExtension.Direction.received.equals(direction)) { - messageInfo.setCarbon(OmemoMessageInformation.CARBON.RECV); - } else { - messageInfo.setCarbon(OmemoMessageInformation.CARBON.SENT); - } - - try { - // Is it a MUC message... - if (isMucMessage(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); - } - } - } - - } 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()); - } - } - }); - - } - } - } + OmemoService.publishDeviceList(omemoManager.getConnection(), newList); } } 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..f97bbbd4e 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 @@ -16,25 +16,25 @@ */ package org.jivesoftware.smackx.omemo; -import static org.jivesoftware.smackx.omemo.util.OmemoConstants.TARGET_PRE_KEY_COUNT; +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.PRE_KEY_COUNT_PER_BUNDLE; 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.smack.SmackException; +import org.jivesoftware.smackx.omemo.element.OmemoBundleElement_VAxolotl; import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement; import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; -import org.jivesoftware.smackx.omemo.internal.CachedDeviceList; +import org.jivesoftware.smackx.omemo.exceptions.NoIdentityKeyException; +import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; -import org.jivesoftware.smackx.omemo.internal.OmemoSession; +import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil; import org.jxmpp.jid.BareJid; @@ -56,9 +56,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,305 +64,169 @@ 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(); + OmemoCachedDeviceList cachedDeviceList; + + cachedDeviceList = loadCachedDeviceList(userDevice, ownJid); if (cachedDeviceList == null) { - cachedDeviceList = new CachedDeviceList(); + cachedDeviceList = new OmemoCachedDeviceList(); } // Does the list already contain that id? return !cachedDeviceList.contains(id); } - /** - * Generate a new Identity (deviceId, identityKeys, preKeys...). - * - * @param omemoManager omemoManager of our device we want to regenerate. - * @throws CorruptedOmemoKeyException in case something goes wrong - */ - void regenerate(OmemoManager omemoManager) throws CorruptedOmemoKeyException { - LOGGER.log(Level.INFO, "Regenerating with deviceId " + omemoManager.getDeviceId() + "..."); - int nextPreKeyId = 1; - storeOmemoIdentityKeyPair(omemoManager, generateOmemoIdentityKeyPair()); - storeOmemoPreKeys(omemoManager, generateOmemoPreKeys(nextPreKeyId, TARGET_PRE_KEY_COUNT)); - storeLastPreKeyId(omemoManager, OmemoKeyUtil.addInBounds(nextPreKeyId, TARGET_PRE_KEY_COUNT)); - storeCurrentSignedPreKeyId(omemoManager, -1); //Set back to no-value default - changeSignedPreKey(omemoManager); - initializeOmemoSessions(omemoManager); - } - /** * Merge the received OmemoDeviceListElement with the one we already have. If we had none, the received one is saved. * - * @param omemoManager omemoManager of our device. + * @param userDevice our OmemoDevice. * @param contact Contact we received the list from. * @param list List we received. */ - void mergeCachedDeviceList(OmemoManager omemoManager, BareJid contact, OmemoDeviceListElement list) { - CachedDeviceList cached = loadCachedDeviceList(omemoManager, contact); + OmemoCachedDeviceList mergeCachedDeviceList(OmemoDevice userDevice, BareJid contact, OmemoDeviceListElement list) { + OmemoCachedDeviceList cached = loadCachedDeviceList(userDevice, contact); if (cached == null) { - cached = new CachedDeviceList(); + cached = new OmemoCachedDeviceList(); } - if (list != null) { - cached.merge(list.getDeviceIds()); + if (list == null) { + return cached; } - storeCachedDeviceList(omemoManager, contact, cached); + + for (int devId : list.getDeviceIds()) { + if (!cached.contains(devId)) { + setDateOfLastDeviceIdPublication(userDevice, new OmemoDevice(contact, devId), new Date()); + } + } + + cached.merge(list.getDeviceIds()); + storeCachedDeviceList(userDevice, contact, cached); + + return cached; } /** * Renew our singed preKey. This should be done once every 7-14 days. * The old signed PreKey should be kept for around a month or so (look it up in the XEP). * - * @param omemoManager omemoManager of our device. - * @throws CorruptedOmemoKeyException when our identityKey is invalid + * @param userDevice our OmemoDevice. + * @throws CorruptedOmemoKeyException when our identityKey is invalid. + * @throws IllegalStateException when our IdentityKeyPair is null. */ - void changeSignedPreKey(OmemoManager omemoManager) throws CorruptedOmemoKeyException { - int lastSignedPreKeyId = loadCurrentSignedPreKeyId(omemoManager); - if (lastSignedPreKeyId == -1) lastSignedPreKeyId = 0; - try { - T_SigPreKey newSignedPreKey = generateOmemoSignedPreKey(loadOmemoIdentityKeyPair(omemoManager), lastSignedPreKeyId + 1); - storeOmemoSignedPreKey(omemoManager, lastSignedPreKeyId + 1, newSignedPreKey); - storeCurrentSignedPreKeyId(omemoManager, lastSignedPreKeyId + 1); - setDateOfLastSignedPreKeyRenewal(omemoManager); - removeOldSignedPreKeys(omemoManager); + void changeSignedPreKey(OmemoDevice userDevice) + throws CorruptedOmemoKeyException { - } catch (CorruptedOmemoKeyException e) { - LOGGER.log(Level.INFO, "Couldn't change SignedPreKey: " + e.getMessage()); - throw e; + T_IdKeyPair idKeyPair = loadOmemoIdentityKeyPair(userDevice); + if (idKeyPair == null) { + throw new IllegalStateException("Our IdentityKeyPair is null."); } + + TreeMap 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); } } /** * Pack a OmemoBundleElement containing our key material. - * If we used up n preKeys since we last published our bundle, generate n new preKeys and add them to the bundle. - * We should always publish TARGET_PRE_KEY_COUNT keys. * - * @param omemoManager omemoManager of our device. + * @param userDevice our OmemoDevice. * @return OmemoBundleElement * @throws CorruptedOmemoKeyException when a key could not be loaded */ - OmemoBundleVAxolotlElement packOmemoBundle(OmemoManager omemoManager) throws CorruptedOmemoKeyException { - int currentSignedPreKeyId = loadCurrentSignedPreKeyId(omemoManager); - if (currentSignedPreKeyId == -1) currentSignedPreKeyId = 0; - T_SigPreKey currentSignedPreKey = loadOmemoSignedPreKey(omemoManager, currentSignedPreKeyId); - T_IdKeyPair identityKeyPair = loadOmemoIdentityKeyPair(omemoManager); + OmemoBundleElement_VAxolotl packOmemoBundle(OmemoDevice userDevice) + throws CorruptedOmemoKeyException { - HashMap preKeys = loadOmemoPreKeys(omemoManager); - int newKeysCount = TARGET_PRE_KEY_COUNT - preKeys.size(); + int currentSignedPreKeyId = loadCurrentOmemoSignedPreKeyId(userDevice); + T_SigPreKey currentSignedPreKey = loadOmemoSignedPreKeys(userDevice).get(currentSignedPreKeyId); - if (newKeysCount > 0) { - int lastPreKeyId = loadLastPreKeyId(omemoManager); - if (lastPreKeyId == -1) lastPreKeyId = 0; - HashMap newKeys = generateOmemoPreKeys(lastPreKeyId + 1, newKeysCount); - storeOmemoPreKeys(omemoManager, newKeys); - preKeys.putAll(newKeys); - storeLastPreKeyId(omemoManager, lastPreKeyId + newKeysCount); - } - - return new OmemoBundleVAxolotlElement( + return new OmemoBundleElement_VAxolotl( currentSignedPreKeyId, keyUtil().signedPreKeyPublicForBundle(currentSignedPreKey), keyUtil().signedPreKeySignatureFromKey(currentSignedPreKey), - keyUtil().identityKeyForBundle(keyUtil().identityKeyFromPair(identityKeyPair)), - keyUtil().preKeyPublisKeysForBundle(preKeys) + keyUtil().identityKeyForBundle(keyUtil().identityKeyFromPair(loadOmemoIdentityKeyPair(userDevice))), + keyUtil().preKeyPublicKeysForBundle(loadOmemoPreKeys(userDevice)) ); } /** - * Preload all OMEMO sessions for our devices and our contacts from existing raw sessions. - * - * @param omemoManager omemoManager of our device. + * Replenish our supply of keys. If we are missing any type of keys, generate them fresh. + * @param userDevice + * @throws CorruptedOmemoKeyException */ - void initializeOmemoSessions(OmemoManager omemoManager) { + public void replenishKeys(OmemoDevice userDevice) + throws CorruptedOmemoKeyException { - // Get HashMap of our omemoSessions - HashMap> - sessions = omemoSessions.get(omemoManager); - if (sessions == null) { - sessions = new HashMap<>(); - omemoSessions.put(omemoManager, sessions); + T_IdKeyPair identityKeyPair = loadOmemoIdentityKeyPair(userDevice); + if (identityKeyPair == null) { + identityKeyPair = generateOmemoIdentityKeyPair(); + storeOmemoIdentityKeyPair(userDevice, identityKeyPair); } - // 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); + TreeMap signedPreKeys = loadOmemoSignedPreKeys(userDevice); + if (signedPreKeys.size() == 0) { + changeSignedPreKey(userDevice); } - 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()); - } + TreeMap preKeys = loadOmemoPreKeys(userDevice); + int newKeysCount = PRE_KEY_COUNT_PER_BUNDLE - preKeys.size(); + int startId = preKeys.size() == 0 ? 0 : preKeys.lastKey(); - 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 (newKeysCount > 0) { + TreeMap newKeys = generateOmemoPreKeys(startId + 1, newKeysCount); + storeOmemoPreKeys(userDevice, newKeys); } - - 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. * @@ -377,151 +238,111 @@ 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()); } } /** - * remove a preKey from storage. This is called, when a contact used one of our preKeys to establish a session + * Remove a preKey from storage. This is called, when a contact used one of our preKeys to establish a session * with us. * - * @param omemoManager omemoManager of our device. + * @param userDevice our OmemoDevice. * @param preKeyId id of the used key that will be deleted */ - public abstract void removeOmemoPreKey(OmemoManager omemoManager, int preKeyId); - - /** - * Return the id of the currently used signed preKey. - * This is used to avoid collisions when generating a new signedPreKey. - * - * @param omemoManager omemoManager of our device. - * @return id - */ - public abstract int loadCurrentSignedPreKeyId(OmemoManager omemoManager); - - /** - * Store the id of the currently used signedPreKey. - * - * @param omemoManager omemoManager of our device. - * @param currentSignedPreKeyId if of the signedPreKey that is currently in use - */ - public abstract void storeCurrentSignedPreKeyId(OmemoManager omemoManager, int currentSignedPreKeyId); + public abstract void removeOmemoPreKey(OmemoDevice userDevice, int preKeyId); /** * Return all our current OmemoPreKeys. * - * @param omemoManager omemoManager of our device. + * @param userDevice our OmemoDevice. * @return Map containing our preKeys */ - public abstract HashMap 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); + + public int loadCurrentOmemoSignedPreKeyId(OmemoDevice userDevice) { + return loadOmemoSignedPreKeys(userDevice).lastKey(); + } /** * 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,168 +432,197 @@ 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 OmemoCachedDeviceList loadCachedDeviceList(OmemoDevice userDevice, BareJid contact); + + /** + * Load a list of deviceIds from our own devices. + * @param userDevice + * @return + */ + public OmemoCachedDeviceList loadCachedDeviceList(OmemoDevice userDevice) { + return loadCachedDeviceList(userDevice, userDevice.getJid()); + } /** * 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, + OmemoCachedDeviceList 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. * * @return KeyUtil object */ - public abstract OmemoKeyUtil keyUtil(); + public abstract OmemoKeyUtil keyUtil(); /** * Return our identityKeys fingerprint. * - * @param omemoManager omemoManager of our device. + * @param userDevice our OmemoDevice. * @return fingerprint of our identityKeyPair + * + * @throws CorruptedOmemoKeyException if the identityKey of userDevice is corrupted. */ - public OmemoFingerprint getFingerprint(OmemoManager omemoManager) { - try { - return keyUtil().getFingerprint(keyUtil().identityKeyFromPair(loadOmemoIdentityKeyPair(omemoManager))); + public OmemoFingerprint getFingerprint(OmemoDevice userDevice) + throws CorruptedOmemoKeyException { - } catch (CorruptedOmemoKeyException e) { - LOGGER.log(Level.WARNING, "getFingerprint failed due to corrupted identityKeyPair: " + e.getMessage()); + T_IdKeyPair keyPair = loadOmemoIdentityKeyPair(userDevice); + if (keyPair == null) { return null; } + + return keyUtil().getFingerprintOfIdentityKey(keyUtil().identityKeyFromPair(keyPair)); } /** - * Return the default deviceId for a user. - * The defaultDeviceId will be used when the OmemoManager gets instantiated without passing a specific deviceId. - * If no default id is set, return -1; + * Return the fingerprint of the identityKey belonging to contactsDevice. * - * @param user user - * @return defaultDeviceId or -1 + * @param userDevice our OmemoDevice. + * @param contactsDevice OmemoDevice we want to have the fingerprint for. + * @return fingerprint of the userDevices IdentityKey. + * @throws CorruptedOmemoKeyException if the IdentityKey is corrupted. + * @throws NoIdentityKeyException if no IdentityKey for contactsDevice has been found locally. */ - public abstract int getDefaultDeviceId(BareJid user); + public OmemoFingerprint getFingerprint(OmemoDevice userDevice, OmemoDevice contactsDevice) + throws CorruptedOmemoKeyException, NoIdentityKeyException { - /** - * Set the default deviceId of a user. - * - * @param user user - * @param defaultDeviceId defaultDeviceId - */ - public abstract void setDefaultDeviceId(BareJid user, int defaultDeviceId); + T_IdKey identityKey = loadOmemoIdentityKey(userDevice, contactsDevice); + if (identityKey == null) { + throw new NoIdentityKeyException(contactsDevice); + } + return keyUtil().getFingerprintOfIdentityKey(identityKey); + } /** * Return the fingerprint of the given devices announced identityKey. + * If we have no local copy of the identityKey of the contact, build a fresh session in order to get the key. * - * @param omemoManager omemoManager of our device. - * @param device device - * @throws CannotEstablishOmemoSessionException if we cannot establish a session - * @return fingerprint of the identityKey + * @param managerGuard authenticated OmemoManager + * @param contactsDevice OmemoDevice we want to get the fingerprint from + * @return fingerprint + * + * @throws CannotEstablishOmemoSessionException If we have no local copy of the identityKey of the contact + * and are unable to build a fresh session + * @throws CorruptedOmemoKeyException If the identityKey we have of the contact is corrupted + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException */ - public OmemoFingerprint getFingerprint(OmemoManager omemoManager, OmemoDevice device) throws CannotEstablishOmemoSessionException { - T_IdKey idKey; + public OmemoFingerprint getFingerprintAndMaybeBuildSession(OmemoManager.LoggedInOmemoManager managerGuard, OmemoDevice contactsDevice) + throws CannotEstablishOmemoSessionException, CorruptedOmemoKeyException, + SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { + OmemoManager omemoManager = managerGuard.get(); - try { - idKey = loadOmemoIdentityKey(omemoManager, device); - if (idKey == null) { - OmemoService.getInstance().buildSessionFromOmemoBundle(omemoManager, device, true); - } - idKey = loadOmemoIdentityKey(omemoManager, device); - } catch (CorruptedOmemoKeyException e) { - LOGGER.log(Level.WARNING, "getFingerprint failed due to corrupted identityKey: " + e.getMessage()); + // Load identityKey + T_IdKey identityKey = loadOmemoIdentityKey(omemoManager.getOwnDevice(), contactsDevice); + if (identityKey == null) { + // Key cannot be loaded. Maybe it doesn't exist. Fetch a bundle to get it... + OmemoService.getInstance().buildFreshSessionWithDevice(omemoManager.getConnection(), + omemoManager.getOwnDevice(), contactsDevice); + } + + // Load identityKey again + identityKey = loadOmemoIdentityKey(omemoManager.getOwnDevice(), contactsDevice); + if (identityKey == null) { return null; } - return keyUtil().getFingerprint(idKey); + + return keyUtil().getFingerprintOfIdentityKey(identityKey); } } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement.java index c0edbe3de..cab41b636 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement.java @@ -16,12 +16,15 @@ */ package org.jivesoftware.smackx.omemo.element; +import java.util.HashMap; +import java.util.Map; + import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.util.XmlStringBuilder; +import org.jivesoftware.smack.util.stringencoder.Base64; /** * Class that represents an OMEMO Bundle element. - * TODO: Move functionality to here. * * @author Paul Schaub */ @@ -36,8 +39,173 @@ public abstract class OmemoBundleElement implements ExtensionElement { public static final String PRE_KEY_PUB = "preKeyPublic"; public static final String PRE_KEY_ID = "preKeyId"; + private final int signedPreKeyId; + private final String signedPreKeyB64; + private byte[] signedPreKey; + private final String signedPreKeySignatureB64; + private byte[] signedPreKeySignature; + private final String identityKeyB64; + private byte[] identityKey; + private final HashMap preKeysB64; + private HashMap preKeys; + + /** + * Constructor to create a Bundle Element from base64 Strings. + * + * @param signedPreKeyId id + * @param signedPreKeyB64 base64 encoded signedPreKey + * @param signedPreKeySigB64 base64 encoded signedPreKeySignature + * @param identityKeyB64 base64 encoded identityKey + * @param preKeysB64 HashMap of base64 encoded preKeys + */ + public OmemoBundleElement(int signedPreKeyId, String signedPreKeyB64, String signedPreKeySigB64, String identityKeyB64, HashMap preKeysB64) { + this.signedPreKeyId = signedPreKeyId; + this.signedPreKeyB64 = signedPreKeyB64; + this.signedPreKeySignatureB64 = signedPreKeySigB64; + this.identityKeyB64 = identityKeyB64; + this.preKeysB64 = preKeysB64; + } + + /** + * Constructor to create a Bundle Element from decoded byte arrays. + * + * @param signedPreKeyId id + * @param signedPreKey signedPreKey + * @param signedPreKeySig signedPreKeySignature + * @param identityKey identityKey + * @param preKeys HashMap of preKeys + */ + public OmemoBundleElement(int signedPreKeyId, byte[] signedPreKey, byte[] signedPreKeySig, byte[] identityKey, HashMap preKeys) { + this.signedPreKeyId = signedPreKeyId; + + this.signedPreKey = signedPreKey; + this.signedPreKeyB64 = Base64.encodeToString(signedPreKey); + + this.signedPreKeySignature = signedPreKeySig; + this.signedPreKeySignatureB64 = Base64.encodeToString(signedPreKeySignature); + + this.identityKey = identityKey; + this.identityKeyB64 = Base64.encodeToString(identityKey); + + this.preKeys = preKeys; + this.preKeysB64 = new HashMap<>(); + for (int id : preKeys.keySet()) { + preKeysB64.put(id, Base64.encodeToString(preKeys.get(id))); + } + } + + /** + * Return the signedPreKey of the OmemoBundleElement. + * + * @return signedPreKey as byte array + */ + public byte[] getSignedPreKey() { + if (signedPreKey == null) { + signedPreKey = Base64.decode(signedPreKeyB64); + } + return this.signedPreKey.clone(); + } + + /** + * Return the id of the signedPreKey in the bundle. + * + * @return id of signedPreKey + */ + public int getSignedPreKeyId() { + return this.signedPreKeyId; + } + + /** + * Get the signature of the signedPreKey. + * + * @return signature as byte array + */ + public byte[] getSignedPreKeySignature() { + if (signedPreKeySignature == null) { + signedPreKeySignature = Base64.decode(signedPreKeySignatureB64); + } + return signedPreKeySignature.clone(); + } + + /** + * Return the public identityKey of the bundles owner. + * This can be used to check the signedPreKeys signature. + * The fingerprint of this key is, what the user has to verify. + * + * @return public identityKey as byte array + */ + public byte[] getIdentityKey() { + if (identityKey == null) { + identityKey = Base64.decode(identityKeyB64); + } + return this.identityKey.clone(); + } + + /** + * Return the HashMap of preKeys in the bundle. + * The map uses the preKeys ids as key and the preKeys as value. + * + * @return preKeys + */ + public HashMap getPreKeys() { + if (preKeys == null) { + preKeys = new HashMap<>(); + for (int id : preKeysB64.keySet()) { + preKeys.put(id, Base64.decode(preKeysB64.get(id))); + } + } + return this.preKeys; + } + + /** + * Return a single preKey from the map. + * + * @param id id of the preKey + * @return the preKey + */ + public byte[] getPreKey(int id) { + return getPreKeys().get(id); + } + @Override - public abstract XmlStringBuilder toXML(String enclosingNamespace); + public String getElementName() { + return BUNDLE; + } + + @Override + public XmlStringBuilder toXML(String enclosingNamespace) { + XmlStringBuilder sb = new XmlStringBuilder(this, enclosingNamespace).rightAngleBracket(); + + sb.halfOpenElement(SIGNED_PRE_KEY_PUB).attribute(SIGNED_PRE_KEY_ID, signedPreKeyId).rightAngleBracket() + .append(signedPreKeyB64).closeElement(SIGNED_PRE_KEY_PUB); + + sb.openElement(SIGNED_PRE_KEY_SIG).append(signedPreKeySignatureB64).closeElement(SIGNED_PRE_KEY_SIG); + + sb.openElement(IDENTITY_KEY).append(identityKeyB64).closeElement(IDENTITY_KEY); + + sb.openElement(PRE_KEYS); + for (Map.Entry p : this.preKeysB64.entrySet()) { + sb.halfOpenElement(PRE_KEY_PUB).attribute(PRE_KEY_ID, p.getKey()).rightAngleBracket() + .append(p.getValue()).closeElement(PRE_KEY_PUB); + } + sb.closeElement(PRE_KEYS); + + sb.closeElement(this); + return sb; + } + + @Override + public String toString() { + String out = "OmemoBundleElement[\n"; + out += SIGNED_PRE_KEY_PUB + " " + SIGNED_PRE_KEY_ID + "=" + signedPreKeyId + ": " + signedPreKeyB64 + "\n"; + out += SIGNED_PRE_KEY_SIG + ": " + signedPreKeySignatureB64 + "\n"; + out += IDENTITY_KEY + ": " + identityKeyB64 + "\n"; + out += PRE_KEYS + " (" + preKeysB64.size() + ")\n"; + for (Map.Entry e : preKeysB64.entrySet()) { + out += PRE_KEY_PUB + " " + PRE_KEY_ID + "=" + e.getKey() + ": " + e.getValue() + "\n"; + } + return out; + } @Override public boolean equals(Object other) { diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement_VAxolotl.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement_VAxolotl.java new file mode 100644 index 000000000..945a6611f --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement_VAxolotl.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.smackx.omemo.element; + +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL; + +import java.util.HashMap; + +/** + * OMEMO device bundle as described here: + * https://xmpp.org/extensions/xep-0384.html#usecases-announcing (Example 3). + * + * @author Paul Schaub + */ +public class OmemoBundleElement_VAxolotl extends OmemoBundleElement { + + public OmemoBundleElement_VAxolotl(int signedPreKeyId, String signedPreKeyB64, String signedPreKeySigB64, String identityKeyB64, HashMap preKeysB64) { + super(signedPreKeyId, signedPreKeyB64, signedPreKeySigB64, identityKeyB64, preKeysB64); + } + + public OmemoBundleElement_VAxolotl(int signedPreKeyId, byte[] signedPreKey, byte[] signedPreKeySig, byte[] identityKey, HashMap preKeys) { + super(signedPreKeyId, signedPreKey, signedPreKeySig, identityKey, preKeys); + } + + @Override + public String getNamespace() { + return OMEMO_NAMESPACE_V_AXOLOTL; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleVAxolotlElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleVAxolotlElement.java deleted file mode 100644 index d27df5d87..000000000 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleVAxolotlElement.java +++ /dev/null @@ -1,207 +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.element; - -import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL; - -import java.util.HashMap; -import java.util.Map; - -import org.jivesoftware.smack.util.XmlStringBuilder; -import org.jivesoftware.smack.util.stringencoder.Base64; - -/** - * OMEMO device bundle as described here: - * https://xmpp.org/extensions/xep-0384.html#usecases-announcing (Example 3). - * - * @author Paul Schaub - */ -public class OmemoBundleVAxolotlElement extends OmemoBundleElement { - - private final int signedPreKeyId; - private final String signedPreKeyB64; - private byte[] signedPreKey; - private final String signedPreKeySignatureB64; - private byte[] signedPreKeySignature; - private final String identityKeyB64; - private byte[] identityKey; - private final HashMap preKeysB64; - private HashMap preKeys; - - /** - * Constructor to create a Bundle Element from base64 Strings. - * - * @param signedPreKeyId id - * @param signedPreKeyB64 base64 encoded signedPreKey - * @param signedPreKeySigB64 base64 encoded signedPreKeySignature - * @param identityKeyB64 base64 encoded identityKey - * @param preKeysB64 HashMap of base64 encoded preKeys - */ - public OmemoBundleVAxolotlElement(int signedPreKeyId, String signedPreKeyB64, String signedPreKeySigB64, String identityKeyB64, HashMap preKeysB64) { - this.signedPreKeyId = signedPreKeyId; - this.signedPreKeyB64 = signedPreKeyB64; - this.signedPreKeySignatureB64 = signedPreKeySigB64; - this.identityKeyB64 = identityKeyB64; - this.preKeysB64 = preKeysB64; - } - - /** - * Constructor to create a Bundle Element from decoded byte arrays. - * - * @param signedPreKeyId id - * @param signedPreKey signedPreKey - * @param signedPreKeySig signedPreKeySignature - * @param identityKey identityKey - * @param preKeys HashMap of preKeys - */ - public OmemoBundleVAxolotlElement(int signedPreKeyId, byte[] signedPreKey, byte[] signedPreKeySig, byte[] identityKey, HashMap preKeys) { - this.signedPreKeyId = signedPreKeyId; - - this.signedPreKey = signedPreKey; - this.signedPreKeyB64 = Base64.encodeToString(signedPreKey); - - this.signedPreKeySignature = signedPreKeySig; - this.signedPreKeySignatureB64 = Base64.encodeToString(signedPreKeySignature); - - this.identityKey = identityKey; - this.identityKeyB64 = Base64.encodeToString(identityKey); - - this.preKeys = preKeys; - this.preKeysB64 = new HashMap<>(); - for (int id : preKeys.keySet()) { - preKeysB64.put(id, Base64.encodeToString(preKeys.get(id))); - } - } - - /** - * Return the signedPreKey of the OmemoBundleElement. - * - * @return signedPreKey as byte array - */ - public byte[] getSignedPreKey() { - if (signedPreKey == null) { - signedPreKey = Base64.decode(signedPreKeyB64); - } - return this.signedPreKey.clone(); - } - - /** - * Return the id of the signedPreKey in the bundle. - * - * @return id of signedPreKey - */ - public int getSignedPreKeyId() { - return this.signedPreKeyId; - } - - /** - * Get the signature of the signedPreKey. - * - * @return signature as byte array - */ - public byte[] getSignedPreKeySignature() { - if (signedPreKeySignature == null) { - signedPreKeySignature = Base64.decode(signedPreKeySignatureB64); - } - return signedPreKeySignature.clone(); - } - - /** - * Return the public identityKey of the bundles owner. - * This can be used to check the signedPreKeys signature. - * The fingerprint of this key is, what the user has to verify. - * - * @return public identityKey as byte array - */ - public byte[] getIdentityKey() { - if (identityKey == null) { - identityKey = Base64.decode(identityKeyB64); - } - return this.identityKey.clone(); - } - - /** - * Return the HashMap of preKeys in the bundle. - * The map uses the preKeys ids as key and the preKeys as value. - * - * @return preKeys - */ - public HashMap getPreKeys() { - if (preKeys == null) { - preKeys = new HashMap<>(); - for (int id : preKeysB64.keySet()) { - preKeys.put(id, Base64.decode(preKeysB64.get(id))); - } - } - return this.preKeys; - } - - /** - * Return a single preKey from the map. - * - * @param id id of the preKey - * @return the preKey - */ - public byte[] getPreKey(int id) { - return getPreKeys().get(id); - } - - @Override - public String getElementName() { - return BUNDLE; - } - - @Override - public XmlStringBuilder toXML(String enclosingNamespace) { - XmlStringBuilder sb = new XmlStringBuilder(this).rightAngleBracket(); - - sb.halfOpenElement(SIGNED_PRE_KEY_PUB).attribute(SIGNED_PRE_KEY_ID, signedPreKeyId).rightAngleBracket() - .append(signedPreKeyB64).closeElement(SIGNED_PRE_KEY_PUB); - - sb.openElement(SIGNED_PRE_KEY_SIG).append(signedPreKeySignatureB64).closeElement(SIGNED_PRE_KEY_SIG); - - sb.openElement(IDENTITY_KEY).append(identityKeyB64).closeElement(IDENTITY_KEY); - - sb.openElement(PRE_KEYS); - for (Map.Entry p : this.preKeysB64.entrySet()) { - sb.halfOpenElement(PRE_KEY_PUB).attribute(PRE_KEY_ID, p.getKey()).rightAngleBracket() - .append(p.getValue()).closeElement(PRE_KEY_PUB); - } - sb.closeElement(PRE_KEYS); - - sb.closeElement(this); - return sb; - } - - @Override - public String getNamespace() { - return OMEMO_NAMESPACE_V_AXOLOTL; - } - - @Override - public String toString() { - String out = "OmemoBundleElement[\n"; - out += SIGNED_PRE_KEY_PUB + " " + SIGNED_PRE_KEY_ID + "=" + signedPreKeyId + ": " + signedPreKeyB64 + "\n"; - out += SIGNED_PRE_KEY_SIG + ": " + signedPreKeySignatureB64 + "\n"; - out += IDENTITY_KEY + ": " + identityKeyB64 + "\n"; - out += PRE_KEYS + " (" + preKeysB64.size() + ")\n"; - for (Map.Entry e : preKeysB64.entrySet()) { - out += PRE_KEY_PUB + " " + PRE_KEY_ID + "=" + e.getKey() + ": " + e.getValue() + "\n"; - } - return out; - } -} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListElement.java index 9c4daac21..707d1cff6 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListElement.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListElement.java @@ -23,6 +23,7 @@ import java.util.Set; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.XmlStringBuilder; +import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList; /** * A OMEMO device list update containing the IDs of all active devices of a contact. @@ -45,6 +46,10 @@ public abstract class OmemoDeviceListElement implements ExtensionElement { this.deviceIds = Collections.unmodifiableSet(deviceIds); } + public OmemoDeviceListElement(OmemoCachedDeviceList cachedList) { + this.deviceIds = Collections.unmodifiableSet(cachedList.getActiveDevices()); + } + public Set getDeviceIds() { return deviceIds; } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListVAxolotlElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListElement_VAxolotl.java similarity index 75% rename from smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListVAxolotlElement.java rename to smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListElement_VAxolotl.java index 197566c5d..a1c525f57 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListVAxolotlElement.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListElement_VAxolotl.java @@ -20,17 +20,23 @@ import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_ import java.util.Set; +import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList; + /** * The OMEMO device list element with the legacy Axolotl namespace. * * @author Paul Schaub */ -public class OmemoDeviceListVAxolotlElement extends OmemoDeviceListElement { +public class OmemoDeviceListElement_VAxolotl extends OmemoDeviceListElement { - public OmemoDeviceListVAxolotlElement(Set deviceIds) { + public OmemoDeviceListElement_VAxolotl(Set deviceIds) { super(deviceIds); } + public OmemoDeviceListElement_VAxolotl(OmemoCachedDeviceList cachedList) { + super(cachedList); + } + @Override public String getNamespace() { return OMEMO_NAMESPACE_V_AXOLOTL; diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoElement.java index a68c5913d..27db720b9 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoElement.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoElement.java @@ -16,17 +16,13 @@ */ package org.jivesoftware.smackx.omemo.element; -import java.util.ArrayList; - import org.jivesoftware.smack.packet.ExtensionElement; -import org.jivesoftware.smack.packet.NamedElement; import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smack.util.stringencoder.Base64; /** - * Class that represents a OmemoElement. - * TODO: Move functionality here. + * Class that represents an OmemoElement. * * @author Paul Schaub */ @@ -35,17 +31,11 @@ public abstract class OmemoElement implements ExtensionElement { public static final int TYPE_OMEMO_PREKEY_MESSAGE = 1; public static final int TYPE_OMEMO_MESSAGE = 0; - public static final String ENCRYPTED = "encrypted"; - public static final String HEADER = "header"; - public static final String SID = "sid"; - public static final String KEY = "key"; - public static final String RID = "rid"; - public static final String PREKEY = "prekey"; - public static final String IV = "iv"; - public static final String PAYLOAD = "payload"; + public static final String NAME_ENCRYPTED = "encrypted"; + public static final String ATTR_PAYLOAD = "payload"; - protected final OmemoElement.OmemoHeader header; - protected final byte[] payload; + private final OmemoHeaderElement header; + private final byte[] payload; /** * Create a new OmemoMessageElement from a header and a payload. @@ -53,12 +43,12 @@ public abstract class OmemoElement implements ExtensionElement { * @param header header of the message * @param payload payload */ - public OmemoElement(OmemoElement.OmemoHeader header, byte[] payload) { + public OmemoElement(OmemoHeaderElement header, byte[] payload) { this.header = Objects.requireNonNull(header); this.payload = payload; } - public OmemoElement.OmemoHeader getHeader() { + public OmemoHeaderElement getHeader() { return header; } @@ -82,113 +72,22 @@ public abstract class OmemoElement implements ExtensionElement { return payload != null; } - /** - * Header element of the message. The header contains information about the sender and the encrypted keys for - * the recipients, as well as the iv element for AES. - */ - public static class OmemoHeader implements NamedElement { - private final int sid; - private final ArrayList keys; - private final byte[] iv; + @Override + public XmlStringBuilder toXML(String enclosingNamespace) { + XmlStringBuilder sb = new XmlStringBuilder(this, enclosingNamespace).rightAngleBracket(); - public OmemoHeader(int sid, ArrayList keys, byte[] iv) { - this.sid = sid; - this.keys = keys; - this.iv = iv; + sb.element(header); + + if (payload != null) { + sb.openElement(ATTR_PAYLOAD).append(Base64.encodeToString(payload)).closeElement(ATTR_PAYLOAD); } - /** - * Return the deviceId of the sender of the message. - * - * @return senders id - */ - public int getSid() { - return sid; - } + sb.closeElement(this); + return sb; + } - public ArrayList getKeys() { - return new ArrayList<>(keys); - } - - public byte[] getIv() { - return iv != null ? iv.clone() : null; - } - - @Override - public String getElementName() { - return HEADER; - } - - @Override - public CharSequence toXML(String enclosingNamespace) { - XmlStringBuilder sb = new XmlStringBuilder(this); - sb.attribute(SID, getSid()).rightAngleBracket(); - - for (OmemoHeader.Key k : getKeys()) { - sb.element(k); - } - - sb.openElement(IV).append(Base64.encodeToString(getIv())).closeElement(IV); - - return sb.closeElement(this); - } - - /** - * Small class to collect key (byte[]), its id and whether its a prekey or not. - */ - public static class Key implements NamedElement { - final byte[] data; - final int id; - final boolean preKey; - - public Key(byte[] data, int id) { - this.data = data; - this.id = id; - this.preKey = false; - } - - public Key(byte[] data, int id, boolean preKey) { - this.data = data; - this.id = id; - this.preKey = preKey; - } - - public int getId() { - return this.id; - } - - public byte[] getData() { - return this.data; - } - - public boolean isPreKey() { - return this.preKey; - } - - @Override - public String toString() { - return Integer.toString(id); - } - - @Override - public String getElementName() { - return KEY; - } - - @Override - public CharSequence toXML(String enclosingNamespace) { - XmlStringBuilder sb = new XmlStringBuilder(this); - - if (isPreKey()) { - sb.attribute(PREKEY, true); - } - - sb.attribute(RID, getId()); - sb.rightAngleBracket(); - sb.append(Base64.encodeToString(getData())); - sb.closeElement(this); - return sb; - } - } + @Override + public String getElementName() { + return NAME_ENCRYPTED; } } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoElement_VAxolotl.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoElement_VAxolotl.java new file mode 100644 index 000000000..858ac6151 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoElement_VAxolotl.java @@ -0,0 +1,44 @@ +/** + * + * 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.element; + +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL; + +/** + * An OMEMO (PreKey)WhisperMessage element. + * + * @author Paul Schaub + */ +public class OmemoElement_VAxolotl extends OmemoElement { + + public static final String NAMESPACE = OMEMO_NAMESPACE_V_AXOLOTL; + + /** + * Create a new OmemoMessageElement from a header and a payload. + * + * @param header header of the message + * @param payload payload + */ + public OmemoElement_VAxolotl(OmemoHeaderElement_VAxolotl header, byte[] payload) { + super(header, payload); + } + + @Override + public String getNamespace() { + return NAMESPACE; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement.java new file mode 100644 index 000000000..bafe4d2e4 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement.java @@ -0,0 +1,83 @@ +/** + * + * 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.element; + +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.packet.NamedElement; +import org.jivesoftware.smack.util.XmlStringBuilder; +import org.jivesoftware.smack.util.stringencoder.Base64; + +/** + * Header element of the message. The header contains information about the sender and the encrypted keys for + * the recipients, as well as the iv element for AES. + */ +public abstract class OmemoHeaderElement implements NamedElement { + + public static final String NAME_HEADER = "header"; + public static final String ATTR_SID = "sid"; + public static final String ATTR_IV = "iv"; + + private final int sid; + private final List keys; + private final byte[] iv; + + public OmemoHeaderElement(int sid, List keys, byte[] iv) { + this.sid = sid; + this.keys = keys; + this.iv = iv; + } + + /** + * Return the deviceId of the sender of the message. + * + * @return senders id + */ + public int getSid() { + return sid; + } + + public ArrayList getKeys() { + return new ArrayList<>(keys); + } + + public byte[] getIv() { + return iv != null ? iv.clone() : null; + } + + @Override + public String getElementName() { + return NAME_HEADER; + } + + @Override + public CharSequence toXML(String enclosingNamespace) { + XmlStringBuilder sb = new XmlStringBuilder(this); + sb.attribute(ATTR_SID, getSid()).rightAngleBracket(); + + for (OmemoKeyElement k : getKeys()) { + sb.element(k); + } + + sb.openElement(ATTR_IV).append(Base64.encodeToString(getIv())).closeElement(ATTR_IV); + + return sb.closeElement(this); + } + + +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement_VAxolotl.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement_VAxolotl.java new file mode 100644 index 000000000..7ee3a60bb --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement_VAxolotl.java @@ -0,0 +1,27 @@ +/** + * + * 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.element; + +import java.util.List; + +public class OmemoHeaderElement_VAxolotl extends OmemoHeaderElement { + + public OmemoHeaderElement_VAxolotl(int sid, List keys, byte[] iv) { + super(sid, keys, iv); + } + +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoKeyElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoKeyElement.java new file mode 100644 index 000000000..6c2652495 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoKeyElement.java @@ -0,0 +1,84 @@ +/** + * + * 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.element; + +import org.jivesoftware.smack.packet.NamedElement; +import org.jivesoftware.smack.util.XmlStringBuilder; +import org.jivesoftware.smack.util.stringencoder.Base64; + +/** + * Small class to collect key (byte[]), its id and whether its a preKey or not. + */ +public class OmemoKeyElement implements NamedElement { + + public static final String NAME_KEY = "key"; + public static final String ATTR_RID = "rid"; + public static final String ATTR_PREKEY = "prekey"; + + private final byte[] data; + private final int id; + private final boolean preKey; + + public OmemoKeyElement(byte[] data, int id) { + this.data = data; + this.id = id; + this.preKey = false; + } + + public OmemoKeyElement(byte[] data, int id, boolean preKey) { + this.data = data; + this.id = id; + this.preKey = preKey; + } + + public int getId() { + return this.id; + } + + public byte[] getData() { + return this.data; + } + + public boolean isPreKey() { + return this.preKey; + } + + @Override + public String toString() { + return Integer.toString(id); + } + + @Override + public String getElementName() { + return NAME_KEY; + } + + @Override + public CharSequence toXML(String enclosingNamespace) { + XmlStringBuilder sb = new XmlStringBuilder(this); + + if (isPreKey()) { + sb.attribute(ATTR_PREKEY, true); + } + + sb.attribute(ATTR_RID, getId()); + sb.rightAngleBracket(); + sb.append(Base64.encodeToString(getData())); + sb.closeElement(this); + return sb; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoVAxolotlElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoVAxolotlElement.java deleted file mode 100644 index 3f189d594..000000000 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoVAxolotlElement.java +++ /dev/null @@ -1,86 +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.element; - -import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL; - -import java.io.UnsupportedEncodingException; - -import org.jivesoftware.smack.util.StringUtils; -import org.jivesoftware.smack.util.XmlStringBuilder; -import org.jivesoftware.smack.util.stringencoder.Base64; - -/** - * An OMEMO (PreKey)WhisperMessage element. - * - * @author Paul Schaub - */ -public class OmemoVAxolotlElement extends OmemoElement { - - /** - * Create a new OmemoMessageElement from a header and a payload. - * - * @param header header of the message - * @param payload payload - */ - public OmemoVAxolotlElement(OmemoHeader header, byte[] payload) { - super(header, payload); - } - - @Override - public String getElementName() { - return ENCRYPTED; - } - - @Override - public XmlStringBuilder toXML(String enclosingNamespace) { - XmlStringBuilder sb = new XmlStringBuilder(this).rightAngleBracket(); - - sb.element(header); - - if (payload != null) { - sb.openElement(PAYLOAD).append(Base64.encodeToString(payload)).closeElement(PAYLOAD); - } - - sb.closeElement(this); - return sb; - } - - @Override - public String getNamespace() { - return OMEMO_NAMESPACE_V_AXOLOTL; - } - - @Override - public String toString() { - try { - StringBuilder s = new StringBuilder("Encrypted:\n") - .append(" header: sid: ").append(getHeader().getSid()).append('\n'); - for (OmemoHeader.Key k : getHeader().getKeys()) { - s.append(" key: prekey: ").append(k.isPreKey()).append(" rid: ") - .append(k.getId()).append(' ') - .append(new String(k.getData(), StringUtils.UTF8)).append('\n'); - } - s.append(" iv: ").append(new String(getHeader().getIv(), StringUtils.UTF8)).append('\n'); - s.append(" payload: ").append(new String(getPayload(), StringUtils.UTF8)); - return s.toString(); - } catch (UnsupportedEncodingException e) { - // UTF-8 must be supported on all platforms claiming to be java compatible. - throw new AssertionError(e); - } - } -} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CryptoFailedException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CryptoFailedException.java index 7212bf883..4e452af44 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CryptoFailedException.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CryptoFailedException.java @@ -16,6 +16,10 @@ */ package org.jivesoftware.smackx.omemo.exceptions; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * Exception gets thrown when some cryptographic function failed. * @@ -25,11 +29,18 @@ public class CryptoFailedException extends Exception { private static final long serialVersionUID = 3466888654338119924L; + private final ArrayList exceptions = new ArrayList<>(); + public CryptoFailedException(String message) { super(message); } public CryptoFailedException(Exception e) { super(e); + exceptions.add(e); + } + + public List getExceptions() { + return Collections.unmodifiableList(exceptions); } } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoIdentityKeyException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoIdentityKeyException.java new file mode 100644 index 000000000..2f2f9dee3 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoIdentityKeyException.java @@ -0,0 +1,33 @@ +/** + * + * 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.internal.OmemoDevice; + +public class NoIdentityKeyException extends Exception { + private static final long serialVersionUID = 1L; + + private final OmemoDevice device; + + public NoIdentityKeyException(OmemoDevice device) { + this.device = device; + } + + public OmemoDevice getDevice() { + return device; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoRawSessionException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoRawSessionException.java index 7cc211ab1..6233fc539 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoRawSessionException.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoRawSessionException.java @@ -16,6 +16,8 @@ */ package org.jivesoftware.smackx.omemo.exceptions; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; + /** * Exception that gets thrown whenever a OmemoMessage arrives that no OmemoSession was found for to decrypt it. * @@ -25,7 +27,14 @@ public class NoRawSessionException extends Exception { private static final long serialVersionUID = 3466888654338119954L; - public NoRawSessionException(Exception e) { + private final OmemoDevice device; + + public NoRawSessionException(OmemoDevice device, Exception e) { super(e); + this.device = device; + } + + public OmemoDevice getDeviceWithoutSession() { + return device; } } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/StaleDeviceException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/StaleDeviceException.java new file mode 100644 index 000000000..be2c31e89 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/StaleDeviceException.java @@ -0,0 +1,66 @@ +/** + * + * Copyright 2018 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 java.util.Date; + +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; + +public class StaleDeviceException extends Exception { + private static final long serialVersionUID = 1L; + + private final OmemoDevice device; + private final Date lastMessageDate; + private final Date lastDeviceIdPublication; + + /** + * This exception gets thrown if a message cannot be encrypted for a device due to the device being inactive for too long (stale). + * + * @param device OmemoDevice. + * @param lastMessageDate + * @param lastDeviceIdPublicationDate + */ + public StaleDeviceException(OmemoDevice device, Date lastMessageDate, Date lastDeviceIdPublicationDate) { + this.device = device; + this.lastMessageDate = lastMessageDate; + this.lastDeviceIdPublication = lastDeviceIdPublicationDate; + } + + /** + * Return the date on which the last OMEMO message sent from the device was received. + * @return last messages date + */ + public Date getLastMessageDate() { + return lastMessageDate; + } + + /** + * Return the date of the last time the deviceId was republished after being inactive/non-existent before. + * @return date of last deviceId (re)publication. + */ + public Date getLastDeviceIdPublicationDate() { + return lastDeviceIdPublication; + } + + /** + * Return the stale OMEMO device. + * @return stale device + */ + public OmemoDevice getDevice() { + return device; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UndecidedOmemoIdentityException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UndecidedOmemoIdentityException.java index b477f539a..2105d588a 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UndecidedOmemoIdentityException.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UndecidedOmemoIdentityException.java @@ -16,6 +16,7 @@ */ package org.jivesoftware.smackx.omemo.exceptions; +import java.util.Collection; import java.util.HashSet; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; @@ -34,6 +35,11 @@ public class UndecidedOmemoIdentityException extends Exception { this.devices.add(contact); } + public UndecidedOmemoIdentityException(Collection devices) { + super(); + this.devices.addAll(devices); + } + /** * Return the HashSet of undecided devices. * diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UntrustedOmemoIdentityException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UntrustedOmemoIdentityException.java new file mode 100644 index 000000000..6f3ca4c2b --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UntrustedOmemoIdentityException.java @@ -0,0 +1,93 @@ +/** + * + * 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.internal.OmemoDevice; +import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; + +/** + * Exception that gets thrown when we try to en-/decrypt a message for an untrusted contact. + * This might either be because the user actively untrusted a device, or we receive a message from a contact + * which contains an identityKey that differs from the one the user trusted. + */ +public class UntrustedOmemoIdentityException extends Exception { + + private static final long serialVersionUID = 1L; + private final OmemoDevice device; + private final OmemoFingerprint trustedKey, untrustedKey; + + /** + * Constructor for when we receive a message with an identityKey different from the one we trusted. + * + * @param device device which sent the message. + * @param fpTrusted fingerprint of the identityKey we previously had and trusted. + * @param fpUntrusted fingerprint of the new key which is untrusted. + */ + public UntrustedOmemoIdentityException(OmemoDevice device, OmemoFingerprint fpTrusted, OmemoFingerprint fpUntrusted) { + super(); + this.device = device; + this.trustedKey = fpTrusted; + this.untrustedKey = fpUntrusted; + } + + /** + * Constructor for when encryption fails because the user untrusted a recipients device. + * + * @param device device the user wants to encrypt for, but which has been marked as untrusted. + * @param untrustedKey fingerprint of that device. + */ + public UntrustedOmemoIdentityException(OmemoDevice device, OmemoFingerprint untrustedKey) { + this(device, null, untrustedKey); + } + + /** + * Return the device which sent the message. + * @return omemoDevice. + */ + public OmemoDevice getDevice() { + return device; + } + + /** + * Return the fingerprint of the key we expected. + * This might return null in case this exception got thrown during encryption process. + * @return + */ + public OmemoFingerprint getTrustedFingerprint() { + return trustedKey; + } + + /** + * Return the fingerprint of the unexpected untrusted key. + * @return + */ + public OmemoFingerprint getUntrustedFingerprint() { + return untrustedKey; + } + + @Override + public String toString() { + if (trustedKey != null) { + return "Untrusted OMEMO Identity encountered:\n" + + "Fingerprint of trusted key:\n" + trustedKey.blocksOf8Chars() + "\n" + + "Fingerprint of untrusted key:\n" + untrustedKey.blocksOf8Chars(); + } else { + return "Untrusted OMEMO Identity encountered:\n" + + "Fingerprint of untrusted key:\n" + untrustedKey.blocksOf8Chars(); + } + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CipherAndAuthTag.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CipherAndAuthTag.java index 70df61b8c..964a33910 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CipherAndAuthTag.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CipherAndAuthTag.java @@ -23,7 +23,6 @@ import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.PROVIDER; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; - import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; @@ -38,11 +37,13 @@ import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException; */ public class CipherAndAuthTag { private final byte[] key, iv, authTag; + private final boolean wasPreKey; - public CipherAndAuthTag(byte[] key, byte[] iv, byte[] authTag) throws CryptoFailedException { + public CipherAndAuthTag(byte[] key, byte[] iv, byte[] authTag, boolean wasPreKey) { this.authTag = authTag; this.key = key; this.iv = iv; + this.wasPreKey = wasPreKey; } public Cipher getCipher() throws CryptoFailedException { @@ -82,4 +83,8 @@ public class CipherAndAuthTag { } return null; } + + public boolean wasPreKeyEncrypted() { + return wasPreKey; + } } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/ClearTextMessage.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/ClearTextMessage.java deleted file mode 100644 index 928e4fb5f..000000000 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/ClearTextMessage.java +++ /dev/null @@ -1,63 +0,0 @@ -/** - * - * Copyright 2017 Paul Schaub - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jivesoftware.smackx.omemo.internal; - -import org.jivesoftware.smack.packet.Message; - -/** - * Class that bundles a decrypted message together with the original message and some information about the encryption. - * - * @author Paul Schaub - */ -public class ClearTextMessage { - private final String body; - private final Message encryptedMessage; - private final OmemoMessageInformation messageInformation; - - public ClearTextMessage(String message, Message original, OmemoMessageInformation messageInfo) { - this.body = message; - this.encryptedMessage = original; - this.messageInformation = messageInfo; - } - - /** - * Return the body of the decrypted message. - * - * @return plaintext body - */ - public String getBody() { - return body; - } - - /** - * Return the original Message object. - * - * @return original message - */ - public Message getOriginalMessage() { - return encryptedMessage; - } - - /** - * Return the OmemoMessageInformation. - * - * @return omemoMessageInformation - */ - public OmemoMessageInformation getMessageInformation() { - return messageInformation; - } -} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CachedDeviceList.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoCachedDeviceList.java similarity index 82% rename from smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CachedDeviceList.java rename to smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoCachedDeviceList.java index 41662e1ff..88bb2e34c 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CachedDeviceList.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoCachedDeviceList.java @@ -32,17 +32,27 @@ import java.util.Set; * * @author Paul Schaub */ -public class CachedDeviceList implements Serializable { +public class OmemoCachedDeviceList implements Serializable { private static final long serialVersionUID = 3153579238321261203L; private final Set activeDevices; private final Set inactiveDevices; - public CachedDeviceList() { + public OmemoCachedDeviceList() { this.activeDevices = new HashSet<>(); this.inactiveDevices = new HashSet<>(); } + public OmemoCachedDeviceList(Set activeDevices, Set inactiveDevices) { + this(); + this.activeDevices.addAll(activeDevices); + this.inactiveDevices.addAll(inactiveDevices); + } + + public OmemoCachedDeviceList(OmemoCachedDeviceList original) { + this(original.getActiveDevices(), original.getInactiveDevices()); + } + /** * Returns all active devices. * Active devices are all devices that were in the latest DeviceList update. @@ -90,12 +100,18 @@ public class CachedDeviceList implements Serializable { } /** - * Add a device to the list of active devices. + * Add a device to the list of active devices and remove it from inactive. * * @param deviceId deviceId that will be added */ public void addDevice(int deviceId) { activeDevices.add(deviceId); + inactiveDevices.remove(deviceId); + } + + public void addInactiveDevice(int deviceId) { + activeDevices.remove(deviceId); + inactiveDevices.add(deviceId); } /** @@ -108,6 +124,10 @@ public class CachedDeviceList implements Serializable { return activeDevices.contains(deviceId) || inactiveDevices.contains(deviceId); } + public boolean isActive(int deviceId) { + return getActiveDevices().contains(deviceId); + } + @Override public String toString() { String out = "active: ["; diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoDevice.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoDevice.java index 81102ce25..05a07594a 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoDevice.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoDevice.java @@ -16,6 +16,8 @@ */ package org.jivesoftware.smackx.omemo.internal; +import org.jivesoftware.smackx.omemo.util.OmemoConstants; + import org.jxmpp.jid.BareJid; /** @@ -74,4 +76,12 @@ public class OmemoDevice { i = jid.hashCode() + deviceId; return i.hashCode(); } + + /** + * Return the name of the PubSub {@link org.jivesoftware.smackx.pubsub.LeafNode} of this device. + * @return node name. + */ + public String getBundleNodeName() { + return OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(getDeviceId()); + } } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoMessageInformation.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoMessageInformation.java deleted file mode 100644 index ada9cc2b6..000000000 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoMessageInformation.java +++ /dev/null @@ -1,141 +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; - -/** - * Class that contains information about a decrypted message (eg. which key was used, if it was a carbon...). - * - * @author Paul Schaub - */ -public class OmemoMessageInformation { - private boolean isOmemoMessage; - private IdentityKeyWrapper senderIdentityKey; - private OmemoDevice senderDevice; - private CARBON carbon = CARBON.NONE; - - /** - * Empty constructor. - */ - // TOOD Move this class into smackx.omemo and make this constructor package protected. -Flow - public OmemoMessageInformation() { - } - - /** - * Creates a new OmemoMessageInformation object. Its assumed, that this is about an OMEMO message. - * - * @param senderIdentityKey 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; - this.senderDevice = senderDevice; - this.carbon = carbon; - this.isOmemoMessage = true; - } - - /** - * Create a new OmemoMessageInformation. - * - * @param senderIdentityKey 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); - this.isOmemoMessage = omemo; - } - - /** - * Return the sender devices identityKey. - * - * @return identityKey - */ - public IdentityKeyWrapper getSenderIdentityKey() { - return senderIdentityKey; - } - - /** - * Set the sender devices identityKey. - * - * @param senderIdentityKey identityKey - */ - public void setSenderIdentityKey(IdentityKeyWrapper senderIdentityKey) { - this.senderIdentityKey = senderIdentityKey; - } - - /** - * Return the sender device. - * - * @return sender device - */ - public OmemoDevice getSenderDevice() { - return senderDevice; - } - - /** - * Return true, if this is (was) an OMEMO message. - * @return true if omemo - */ - public boolean isOmemoMessage() { - return this.isOmemoMessage; - } - - /** - * Set the sender device. - * - * @param senderDevice sender device - */ - public void setSenderDevice(OmemoDevice senderDevice) { - this.senderDevice = senderDevice; - } - - /** - * Return the carbon type. - * - * @return carbon type - */ - public CARBON getCarbon() { - return carbon; - } - - /** - * Set the carbon type. - * - * @param carbon carbon type - */ - public void setCarbon(CARBON carbon) { - this.carbon = carbon; - } - - /** - * Types of Carbon Messages. - */ - public enum CARBON { - NONE, //No carbon - SENT, //Sent carbon - RECV //Received Carbon - } - - @Override - public String toString() { - return (senderDevice != null ? senderDevice.toString() : "") + " " + carbon; - } -} - - 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..e9239ea24 --- /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.LoggedInOmemoManager 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..0c6e95a58 --- /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.LoggedInOmemoManager 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/listener/OmemoMessageListener.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/OmemoMessageListener.java index 9ca870aaf..8e4d0ddd0 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/OmemoMessageListener.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/OmemoMessageListener.java @@ -17,9 +17,9 @@ package org.jivesoftware.smackx.omemo.listener; import org.jivesoftware.smack.packet.Message; - -import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag; -import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smackx.carbons.packet.CarbonExtension; +import org.jivesoftware.smackx.omemo.OmemoMessage; /** * Listener interface that allows implementations to receive decrypted OMEMO messages. @@ -30,20 +30,13 @@ public interface OmemoMessageListener { /** * Gets called, whenever an OmemoMessage has been received and was successfully decrypted. * - * @param decryptedBody Decrypted body - * @param encryptedMessage Encrypted Message - * @param wrappingMessage Wrapping carbon message, in case the message was a carbon copy, else null. - * @param omemoInformation Information about the messages encryption etc. + * @param stanza Received (encrypted) stanza. + * @param decryptedMessage decrypted OmemoMessage. */ - void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation); + void onOmemoMessageReceived(Stanza stanza, OmemoMessage.Received decryptedMessage); - /** - * Gets called, whenever an OmemoElement without a body (an OmemoKeyTransportElement) is received. - * - * @param cipherAndAuthTag transported Cipher along with an optional AuthTag - * @param message Message that contained the KeyTransport - * @param wrappingMessage Wrapping message (eg. carbon), or null - * @param omemoInformation Information about the messages encryption etc. - */ - void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation); + void onOmemoCarbonCopyReceived(CarbonExtension.Direction direction, + Message carbonCopy, + Message wrappingMessage, + OmemoMessage.Received decryptedCarbonCopy); } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/OmemoMucMessageListener.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/OmemoMucMessageListener.java index 7d0011a8a..15d9685be 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/OmemoMucMessageListener.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/OmemoMucMessageListener.java @@ -16,13 +16,9 @@ */ package org.jivesoftware.smackx.omemo.listener; -import org.jivesoftware.smack.packet.Message; - +import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smackx.muc.MultiUserChat; -import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag; -import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation; - -import org.jxmpp.jid.BareJid; +import org.jivesoftware.smackx.omemo.OmemoMessage; /** * Listener interface that allows implementations to receive decrypted OMEMO MUC messages. @@ -33,24 +29,8 @@ public interface OmemoMucMessageListener { /** * Gets called whenever an OMEMO message has been received in a MultiUserChat and successfully decrypted. * @param muc MultiUserChat the message was sent in - * @param from the bareJid of the sender - * @param decryptedBody the decrypted Body of the message - * @param message the original message with encrypted element - * @param wrappingMessage in case of a carbon copy, this is the wrapping message - * @param omemoInformation information about the encryption of the message + * @param stanza Original Stanza + * @param decryptedOmemoMessage decrypted Omemo message */ - void onOmemoMucMessageReceived(MultiUserChat muc, BareJid from, String decryptedBody, Message message, - Message wrappingMessage, OmemoMessageInformation omemoInformation); - - /** - * Gets called, whenever an OmemoElement without a body (an OmemoKeyTransportElement) is received. - * - * @param muc MultiUserChat the message was sent in - * @param from bareJid of the sender - * @param cipherAndAuthTag transportedKey along with an optional authTag - * @param message Message that contained the KeyTransport - * @param wrappingMessage Wrapping message (eg. carbon), or null - * @param omemoInformation Information about the messages encryption etc. - */ - void onOmemoKeyTransportReceived(MultiUserChat muc, BareJid from, CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation); + void onOmemoMucMessageReceived(MultiUserChat muc, Stanza stanza, OmemoMessage.Received decryptedOmemoMessage); } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoBundleVAxolotlProvider.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoBundleVAxolotlProvider.java index 2fcd3ad04..d79aea302 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoBundleVAxolotlProvider.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoBundleVAxolotlProvider.java @@ -31,7 +31,7 @@ import java.util.HashMap; import org.jivesoftware.smack.provider.ExtensionElementProvider; -import org.jivesoftware.smackx.omemo.element.OmemoBundleVAxolotlElement; +import org.jivesoftware.smackx.omemo.element.OmemoBundleElement_VAxolotl; import org.xmlpull.v1.XmlPullParser; @@ -40,9 +40,9 @@ import org.xmlpull.v1.XmlPullParser; * * @author Paul Schaub */ -public class OmemoBundleVAxolotlProvider extends ExtensionElementProvider { +public class OmemoBundleVAxolotlProvider extends ExtensionElementProvider { @Override - public OmemoBundleVAxolotlElement parse(XmlPullParser parser, int initialDepth) throws Exception { + public OmemoBundleElement_VAxolotl parse(XmlPullParser parser, int initialDepth) throws Exception { boolean stop = false; boolean inPreKeys = false; @@ -96,6 +96,6 @@ public class OmemoBundleVAxolotlProvider extends ExtensionElementProvider { +public class OmemoDeviceListVAxolotlProvider extends ExtensionElementProvider { @Override - public OmemoDeviceListVAxolotlElement parse(XmlPullParser parser, int initialDepth) throws Exception { + public OmemoDeviceListElement_VAxolotl parse(XmlPullParser parser, int initialDepth) throws Exception { Set deviceListIds = new HashSet<>(); boolean stop = false; while (!stop) { @@ -63,6 +63,6 @@ public class OmemoDeviceListVAxolotlProvider extends ExtensionElementProvider { +public class OmemoVAxolotlProvider extends ExtensionElementProvider { @Override - public OmemoVAxolotlElement parse(XmlPullParser parser, int initialDepth) throws Exception { + public OmemoElement_VAxolotl parse(XmlPullParser parser, int initialDepth) throws Exception { boolean inEncrypted = true; int sid = -1; - ArrayList keys = new ArrayList<>(); + ArrayList keys = new ArrayList<>(); byte[] iv = null; byte[] payload = null; @@ -57,41 +53,41 @@ public class OmemoVAxolotlProvider extends ExtensionElementProviderXEP-0384: OMEMO + */ +package org.jivesoftware.smackx.omemo.trust; diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/MessageOrOmemoMessage.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/MessageOrOmemoMessage.java new file mode 100644 index 000000000..81683e358 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/MessageOrOmemoMessage.java @@ -0,0 +1,49 @@ +/** + * + * Copyright 2018 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.omemo.util; + +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.util.Objects; +import org.jivesoftware.smackx.omemo.OmemoMessage; + +public class MessageOrOmemoMessage { + + private final Message message; + private final OmemoMessage.Received omemoMessage; + + public MessageOrOmemoMessage(Message message) { + this.message = Objects.requireNonNull(message); + this.omemoMessage = null; + } + + public MessageOrOmemoMessage(OmemoMessage.Received omemoMessage) { + this.omemoMessage = Objects.requireNonNull(omemoMessage); + this.message = null; + } + + public boolean isOmemoMessage() { + return omemoMessage != null; + } + + public Message getMessage() { + return message; + } + + public OmemoMessage.Received getOmemoMessage() { + return omemoMessage; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoConstants.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoConstants.java index 7ed0a1994..c362145c6 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoConstants.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoConstants.java @@ -37,7 +37,7 @@ public final class OmemoConstants { /** * How many preKeys do we want to publish? */ - public static final int TARGET_PRE_KEY_COUNT = 100; + public static final int PRE_KEY_COUNT_PER_BUNDLE = 100; /** * Return the node name of the PEP node containing the device bundle of the device with device id deviceId. @@ -49,7 +49,7 @@ public final class OmemoConstants { return PEP_NODE_BUNDLES + ":" + deviceId; } - public static final String BODY_OMEMO_HINT = "I sent you an OMEMO encrypted message but your client doesn’t seem to support that. Find more information on https://conversations.im/omemo"; + public static final String BODY_OMEMO_HINT = "I sent you an OMEMO encrypted message but your client doesn't seem to support that. Find more information on https://conversations.im/omemo"; /** * Information about the keys used for message encryption. 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..ba045cc2e 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,18 +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.element.OmemoBundleElement; 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; +import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; /** * Class that is used to convert bytes to keys and vice versa. @@ -40,13 +36,11 @@ import org.jxmpp.stringprep.XmppStringprepException; * @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 OmemoKeyUtil { +public abstract class OmemoKeyUtil { private static final Logger LOGGER = Logger.getLogger(OmemoKeyUtil.class.getName()); public final Bundle BUNDLE = new Bundle(); @@ -63,7 +57,7 @@ public abstract class OmemoKeyUtil bundles(OmemoBundleVAxolotlElement bundle, OmemoDevice contact) throws CorruptedOmemoKeyException { + public HashMap bundles(OmemoBundleElement bundle, OmemoDevice contact) throws CorruptedOmemoKeyException { HashMap bundles = new HashMap<>(); for (int deviceId : bundle.getPreKeys().keySet()) { try { @@ -207,7 +201,7 @@ public abstract class OmemoKeyUtil generateOmemoPreKeys(int startId, int count); + public abstract TreeMap generateOmemoPreKeys(int startId, int count); /** * Generate a new signed preKey. @@ -259,7 +253,7 @@ public abstract class OmemoKeyUtil preKeyPublisKeysForBundle(HashMap preKeyHashMap) { + public HashMap preKeyPublicKeysForBundle(TreeMap preKeyHashMap) { HashMap out = new HashMap<>(); for (Map.Entry e : preKeyHashMap.entrySet()) { out.put(e.getKey(), preKeyForBundle(e.getValue())); @@ -341,7 +335,7 @@ 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. @@ -396,43 +372,6 @@ public abstract class OmemoKeyUtil { - private final OmemoStore omemoStore; - private final OmemoManager omemoManager; - private byte[] messageKey = generateKey(); - private byte[] initializationVector = generateIv(); + private final OmemoDevice userDevice; + private final OmemoRatchet ratchet; + private final OmemoTrustCallback trustCallback; + + private byte[] messageKey; + private final byte[] initializationVector; private byte[] ciphertextMessage; - private final ArrayList keys = new ArrayList<>(); + private final ArrayList keys = new ArrayList<>(); /** - * Create a OmemoMessageBuilder. + * Create an OmemoMessageBuilder. + * + * @param userDevice our OmemoDevice + * @param callback trustCallback for querying trust decisions + * @param ratchet our OmemoRatchet + * @param aesKey aes message key used for message encryption + * @param iv initialization vector used for message encryption + * @param message message we want to send * - * @param omemoManager OmemoManager of our device. - * @param omemoStore OmemoStore. - * @param aesKey AES key that will be transported to the recipient. This is used eg. to encrypt the body. - * @param iv IV * @throws NoSuchPaddingException * @throws BadPaddingException * @throws InvalidKeyException @@ -90,23 +99,31 @@ public class OmemoMessageBuilder omemoStore, - byte[] aesKey, byte[] iv) - throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, + public OmemoMessageBuilder(OmemoDevice userDevice, + OmemoTrustCallback callback, + OmemoRatchet ratchet, + byte[] aesKey, + byte[] iv, + String message) + throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, + IllegalBlockSizeException, UnsupportedEncodingException, NoSuchProviderException, InvalidAlgorithmParameterException { - this.omemoStore = omemoStore; - this.omemoManager = omemoManager; + this.userDevice = userDevice; + this.trustCallback = callback; + this.ratchet = ratchet; this.messageKey = aesKey; this.initializationVector = iv; + setMessage(message); } /** - * Create a new OmemoMessageBuilder with random IV and AES key. + * Create an OmemoMessageBuilder. + * + * @param userDevice our OmemoDevice + * @param callback trustCallback for querying trust decisions + * @param ratchet our OmemoRatchet + * @param message message we want to send * - * @param omemoManager omemoManager of our device. - * @param omemoStore omemoStore. - * @param message Messages body. * @throws NoSuchPaddingException * @throws BadPaddingException * @throws InvalidKeyException @@ -116,30 +133,32 @@ public class OmemoMessageBuilder omemoStore, String message) + public OmemoMessageBuilder(OmemoDevice userDevice, + OmemoTrustCallback callback, + OmemoRatchet ratchet, + String message) throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, NoSuchProviderException, InvalidAlgorithmParameterException { - this.omemoManager = omemoManager; - this.omemoStore = omemoStore; - this.setMessage(message); + this(userDevice, callback, ratchet, generateKey(KEYTYPE, KEYLENGTH), generateIv(), message); } /** - * Create an AES messageKey and use it to encrypt the message. - * Optionally append the Auth Tag of the encrypted message to the messageKey afterwards. + * Encrypt the message with the aes key. + * Move the AuthTag from the end of the cipherText to the end of the messageKey afterwards. + * This prevents an attacker which compromised one recipient device to switch out the cipherText for other recipients. + * @see OMEMO security audit. * - * @param message content of the message - * @throws NoSuchPaddingException When no Cipher could be instantiated. - * @throws NoSuchAlgorithmException when no Cipher could be instantiated. - * @throws NoSuchProviderException when BouncyCastle could not be found. - * @throws InvalidAlgorithmParameterException when the Cipher could not be initialized - * @throws InvalidKeyException when the generated key is invalid - * @throws UnsupportedEncodingException when UTF8 is unavailable - * @throws BadPaddingException when cipher.doFinal gets wrong padding - * @throws IllegalBlockSizeException when cipher.doFinal gets wrong Block size. + * @param message plaintext message + * @throws NoSuchPaddingException + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException + * @throws InvalidAlgorithmParameterException + * @throws InvalidKeyException + * @throws UnsupportedEncodingException + * @throws BadPaddingException + * @throws IllegalBlockSizeException */ - public void setMessage(String message) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, InvalidKeyException, UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException { + private void setMessage(String message) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, InvalidKeyException, UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException { if (message == null) { return; } @@ -159,50 +178,71 @@ public class OmemoMessageBuilder session = - omemoStore.getOmemoSessionOf(omemoManager, device); + public void addRecipient(OmemoDevice contactsDevice) + throws NoIdentityKeyException, CorruptedOmemoKeyException, UndecidedOmemoIdentityException, + UntrustedOmemoIdentityException { - if (session != null) { - if (!ignoreTrust && !omemoStore.isDecidedOmemoIdentity(omemoManager, device, session.getIdentityKey())) { - // Warn user of undecided device - throw new UndecidedOmemoIdentityException(device); - } + OmemoFingerprint fingerprint; + fingerprint = OmemoService.getInstance().getOmemoStoreBackend().getFingerprint(userDevice, contactsDevice); + + switch (trustCallback.getTrust(contactsDevice, fingerprint)) { + + case undecided: + throw new UndecidedOmemoIdentityException(contactsDevice); + + case trusted: + CiphertextTuple encryptedKey = ratchet.doubleRatchetEncrypt(contactsDevice, messageKey); + keys.add(new OmemoKeyElement(encryptedKey.getCiphertext(), contactsDevice.getDeviceId(), encryptedKey.isPreKeyMessage())); + break; + + case untrusted: + throw new UntrustedOmemoIdentityException(contactsDevice, fingerprint); - 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())); - } } } @@ -211,24 +251,26 @@ public class OmemoMessageBuilder +extends SmackTestSuite { - private final SignalOmemoKeyUtil keyUtil = new SignalOmemoKeyUtil(); + protected OmemoKeyUtil keyUtil; + + public OmemoKeyUtilTest(OmemoKeyUtil keyUtil) { + this.keyUtil = keyUtil; + } + + public void test() { - @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()); } @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())); + public void identityKeyPairFromNullBytesReturnsNull() throws CorruptedOmemoKeyException { + assertNull(keyUtil.identityKeyPairFromBytes(null)); } @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))); + public void identityKeyFromNullBytesReturnsNull() throws CorruptedOmemoKeyException { + assertNull(keyUtil.identityKeyFromBytes(null)); } @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)); + public void preKeyFromNullBytesReturnsNull() throws IOException { + assertNull(keyUtil.preKeyFromBytes(null)); } @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)); + public void ellipticCurvePublicKeyFromNullBytesReturnsNull() throws CorruptedOmemoKeyException { + assertNull(keyUtil.ellipticCurvePublicKeyFromBytes(null)); } @Test - public void getFingerprintTest() { - IdentityKeyPair ikp = keyUtil.generateOmemoIdentityKeyPair(); - IdentityKey ik = ikp.getPublicKey(); - assertTrue("Length of fingerprint must be 64.", - keyUtil.getFingerprint(ik).length() == 64); + public void signedPreKeyFromNullBytesReturnsNull() throws IOException { + assertNull(keyUtil.signedPreKeyFromBytes(null)); } @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)); + public void signedPreKeyPublicFromNullBytesReturnsNull() throws CorruptedOmemoKeyException { + assertNull(keyUtil.signedPreKeyPublicFromBytes(null)); } @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)); + public void rawSessionFromNullBytesReturnsNull() throws IOException { + assertNull(keyUtil.rawSessionFromBytes(null)); } @Test - public void bundlesFromOmemoBundleTest() throws Exception { + 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); + OmemoBundleElement_VAxolotl 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)); @@ -143,4 +104,32 @@ public class SignalOmemoKeyUtilTest extends SmackTestSuite { 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() { + int high = Integer.MAX_VALUE - 2; + int max = Integer.MAX_VALUE; + assertEquals(OmemoKeyUtil.addInBounds(high, 3), 1); + assertEquals(OmemoKeyUtil.addInBounds(1,2), 3); + assertEquals(OmemoKeyUtil.addInBounds(max, 5), 5); + } + + @Test + public void testPrettyFingerprint() { + OmemoFingerprint fingerprint = new OmemoFingerprint("FFFFFFFFEEEEEEEEDDDDDDDDCCCCCCCCBBBBBBBBAAAAAAAA9999999988888888"); + String pretty = fingerprint.blocksOf8Chars(); + assertEquals(pretty, "FFFFFFFF EEEEEEEE DDDDDDDD CCCCCCCC BBBBBBBB AAAAAAAA 99999999 88888888"); + } } diff --git a/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoServiceTest.java b/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoServiceTest.java new file mode 100644 index 000000000..c4aefb815 --- /dev/null +++ b/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoServiceTest.java @@ -0,0 +1,91 @@ +/** + * + * 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 junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; + +import java.util.Date; +import java.util.HashSet; + +import org.jivesoftware.smack.test.util.SmackTestSuite; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; + +import org.junit.Test; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.stringprep.XmppStringprepException; + +public class OmemoServiceTest extends SmackTestSuite { + + private static final long ONE_HOUR = 1000L * 60 * 60; + private static final int IGNORE_STALE = OmemoConfiguration.getIgnoreStaleDevicesAfterHours(); + private static final int DELETE_STALE = OmemoConfiguration.getDeleteStaleDevicesAfterHours(); + + @Test(expected = IllegalStateException.class) + public void getInstanceFailsWhenNullTest() { + OmemoService.getInstance(); + } + + @Test + public void isServiceRegisteredTest() { + assertFalse(OmemoService.isServiceRegistered()); + } + + /** + * Test correct functionality of isStale method. + * @throws XmppStringprepException + */ + @Test + public void isStaleDeviceTest() throws XmppStringprepException { + OmemoDevice user = new OmemoDevice(JidCreate.bareFrom("alice@wonderland.lit"), 123); + OmemoDevice other = new OmemoDevice(JidCreate.bareFrom("bob@builder.tv"), 444); + + Date now = new Date(); + Date ignoreMe = new Date(now.getTime() - ((IGNORE_STALE + 1) * ONE_HOUR)); + Date deleteMe = new Date(now.getTime() - ((DELETE_STALE + 1) * ONE_HOUR)); + Date imFine = new Date(now.getTime() - ONE_HOUR); + + // One hour "old" devices are (probably) not not stale + assertFalse(OmemoService.isStale(user, other, imFine, IGNORE_STALE)); + + // Devices one hour "older" than max ages are stale + assertTrue(OmemoService.isStale(user, other, ignoreMe, IGNORE_STALE)); + assertTrue(OmemoService.isStale(user, other, deleteMe, DELETE_STALE)); + + // Own device is never stale, no matter how old + assertFalse(OmemoService.isStale(user, user, deleteMe, DELETE_STALE)); + + // Always return false if date is null. + assertFalse(OmemoService.isStale(user, other, null, DELETE_STALE)); + } + + @Test + public void removeOurDeviceTest() throws XmppStringprepException { + OmemoDevice a = new OmemoDevice(JidCreate.bareFrom("a@b.c"), 123); + OmemoDevice b = new OmemoDevice(JidCreate.bareFrom("a@b.c"), 124); + + HashSet devices = new HashSet<>(); + devices.add(a); devices.add(b); + + assertTrue(devices.contains(a)); + assertTrue(devices.contains(b)); + OmemoService.removeOurDevice(a, devices); + + assertFalse(devices.contains(a)); + assertTrue(devices.contains(b)); + } +} diff --git a/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java b/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java new file mode 100644 index 000000000..ef5946f82 --- /dev/null +++ b/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java @@ -0,0 +1,345 @@ +/** + * + * 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.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.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.exceptions.CorruptedOmemoKeyException; +import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; +import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; + +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}; + OmemoCachedDeviceList before = new OmemoCachedDeviceList( + 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); + OmemoCachedDeviceList 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 OmemoCachedDeviceList()); + 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/OmemoVAxolotlElementTest.java b/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoVAxolotlElementTest.java similarity index 74% rename from smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoVAxolotlElementTest.java rename to smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoVAxolotlElementTest.java index 12030e5c9..94d4aa1fa 100644 --- a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoVAxolotlElementTest.java +++ b/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoVAxolotlElementTest.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jivesoftware.smack.omemo; +package org.jivesoftware.smackx.omemo; import static org.junit.Assert.assertEquals; @@ -24,9 +24,9 @@ import org.jivesoftware.smack.test.util.SmackTestSuite; import org.jivesoftware.smack.test.util.TestUtils; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.stringencoder.Base64; - -import org.jivesoftware.smackx.omemo.element.OmemoElement; -import org.jivesoftware.smackx.omemo.element.OmemoVAxolotlElement; +import org.jivesoftware.smackx.omemo.element.OmemoElement_VAxolotl; +import org.jivesoftware.smackx.omemo.element.OmemoHeaderElement_VAxolotl; +import org.jivesoftware.smackx.omemo.element.OmemoKeyElement; import org.jivesoftware.smackx.omemo.provider.OmemoVAxolotlProvider; import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder; @@ -47,12 +47,12 @@ public class OmemoVAxolotlElementTest extends SmackTestSuite { int sid = 12131415; byte[] iv = OmemoMessageBuilder.generateIv(); - ArrayList keys = new ArrayList<>(); - keys.add(new OmemoElement.OmemoHeader.Key(keyData1, keyId1)); - keys.add(new OmemoElement.OmemoHeader.Key(keyData2, keyId2, true)); + ArrayList keys = new ArrayList<>(); + keys.add(new OmemoKeyElement(keyData1, keyId1)); + keys.add(new OmemoKeyElement(keyData2, keyId2, true)); - OmemoVAxolotlElement.OmemoHeader header = new OmemoElement.OmemoHeader(sid, keys, iv); - OmemoVAxolotlElement element = new OmemoVAxolotlElement(header, payload); + OmemoHeaderElement_VAxolotl header = new OmemoHeaderElement_VAxolotl(sid, keys, iv); + OmemoElement_VAxolotl element = new OmemoElement_VAxolotl(header, payload); String expected = "" + @@ -69,7 +69,9 @@ public class OmemoVAxolotlElementTest extends SmackTestSuite { String actual = element.toXML(null).toString(); assertEquals("Serialized xml of OmemoElement must match.", expected, actual); - OmemoVAxolotlElement parsed = new OmemoVAxolotlProvider().parse(TestUtils.getParser(actual)); - assertEquals("Parsed OmemoElement must equal the original.", element.toXML(null).toString(), parsed.toXML(null).toString()); + OmemoElement_VAxolotl parsed = new OmemoVAxolotlProvider().parse(TestUtils.getParser(actual)); + assertEquals("Parsed OmemoElement must equal the original.", + element.toXML(null).toString(), + parsed.toXML(null).toString()); } } diff --git a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/WrapperObjectsTest.java b/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/WrapperObjectsTest.java similarity index 58% rename from smack-omemo/src/test/java/org/jivesoftware/smack/omemo/WrapperObjectsTest.java rename to smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/WrapperObjectsTest.java index a7c64926f..43eb4353c 100644 --- a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/WrapperObjectsTest.java +++ b/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/WrapperObjectsTest.java @@ -14,10 +14,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jivesoftware.smack.omemo; +package org.jivesoftware.smackx.omemo; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertTrue; +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYLENGTH; +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -25,35 +27,20 @@ import static org.junit.Assert.assertNotNull; 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; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.Test; -import org.jxmpp.jid.BareJid; -import org.jxmpp.jid.impl.JidCreate; /** * Test the identityKeyWrapper. */ 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(); @@ -67,41 +54,19 @@ public class WrapperObjectsTest { assertEquals(OmemoElement.TYPE_OMEMO_MESSAGE, c2.getMessageType()); } - @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); - - 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); - ClearTextMessage c = new ClearTextMessage(body, message, information); - - assertEquals(message, c.getOriginalMessage()); - assertEquals(information, c.getMessageInformation()); - assertEquals(body, c.getBody()); - } - @Test public void cipherAndAuthTagTest() throws NoSuchAlgorithmException, CryptoFailedException { Security.addProvider(new BouncyCastleProvider()); - byte[] key = OmemoMessageBuilder.generateKey(); + byte[] key = OmemoMessageBuilder.generateKey(KEYTYPE, KEYLENGTH); byte[] iv = OmemoMessageBuilder.generateIv(); byte[] authTag = OmemoMessageBuilder.generateIv(); - CipherAndAuthTag cat = new CipherAndAuthTag(key, iv, authTag); + CipherAndAuthTag cat = new CipherAndAuthTag(key, iv, authTag, true); assertNotNull(cat.getCipher()); assertArrayEquals(key, cat.getKey()); assertArrayEquals(iv, cat.getIv()); assertArrayEquals(authTag, cat.getAuthTag()); + assertTrue(cat.wasPreKeyEncrypted()); } } diff --git a/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/util/EphemeralTrustCallback.java b/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/util/EphemeralTrustCallback.java new file mode 100644 index 000000000..9b3f393ea --- /dev/null +++ b/smack-omemo/src/test/java/org/jivesoftware/smackx/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.smackx.omemo.util; + +import java.util.HashMap; + +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; +import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; +import org.jivesoftware.smackx.omemo.trust.OmemoTrustCallback; +import org.jivesoftware.smackx.omemo.trust.TrustState; + +/** + * Ephemera Trust Callback used to make trust decisions in tests. + */ +public class EphemeralTrustCallback implements OmemoTrustCallback { + + 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); + } +} diff --git a/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilderTest.java b/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilderTest.java new file mode 100644 index 000000000..7125703e4 --- /dev/null +++ b/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilderTest.java @@ -0,0 +1,79 @@ +/** + * + * 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.smackx.omemo.util; + +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Random; + +import org.jivesoftware.smack.test.util.SmackTestSuite; + +import org.junit.Test; + +public class OmemoMessageBuilderTest extends SmackTestSuite { + + private static final byte[] messageKey = new byte[16]; + private static final byte[] cipherTextWithAuthTag = new byte[35 + 16]; + + public OmemoMessageBuilderTest() { + Random random = new Random(); + random.nextBytes(messageKey); + random.nextBytes(cipherTextWithAuthTag); + } + + @Test + public void testMoveAuthTag() { + // Extract authTag for testing purposes + byte[] authTag = new byte[16]; + System.arraycopy(cipherTextWithAuthTag, 35, authTag, 0, 16); + + byte[] messageKeyWithAuthTag = new byte[16 + 16]; + byte[] cipherTextWithoutAuthTag = new byte[35]; + + OmemoMessageBuilder.moveAuthTag(messageKey, cipherTextWithAuthTag, messageKeyWithAuthTag, cipherTextWithoutAuthTag); + + // Check if first n - 16 bytes of cipherText got copied over to cipherTextWithoutAuthTag correctly + byte[] checkCipherText = new byte[35]; + System.arraycopy(cipherTextWithAuthTag, 0, checkCipherText, 0, 35); + assertTrue(Arrays.equals(checkCipherText, cipherTextWithoutAuthTag)); + + byte[] checkMessageKey = new byte[16]; + System.arraycopy(messageKeyWithAuthTag, 0, checkMessageKey, 0, 16); + assertTrue(Arrays.equals(checkMessageKey, messageKey)); + + byte[] checkAuthTag = new byte[16]; + System.arraycopy(messageKeyWithAuthTag, 16, checkAuthTag, 0, 16); + assertTrue(Arrays.equals(checkAuthTag, authTag)); + } + + @Test(expected = IllegalArgumentException.class) + public void testCheckIllegalMessageKeyWithAuthTagLength() { + byte[] illegalMessageKey = new byte[16 + 15]; // too short + byte[] cipherTextWithoutAuthTag = new byte[35]; // ok + + OmemoMessageBuilder.moveAuthTag(messageKey, cipherTextWithAuthTag, illegalMessageKey, cipherTextWithoutAuthTag); + } + + @Test(expected = IllegalArgumentException.class) + public void testCheckIllegalCipherTextWithoutAuthTagLength() { + byte[] messageKeyWithAuthTag = new byte[16 + 16]; // ok + byte[] illegalCipherTextWithoutAuthTag = new byte[39]; // too long + + OmemoMessageBuilder.moveAuthTag(messageKey, cipherTextWithAuthTag, messageKeyWithAuthTag, illegalCipherTextWithoutAuthTag); + } +}