Browse Source

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
storerework
Paul Schaub 1 year ago
parent
commit
1f731f6318
No known key found for this signature in database GPG Key ID: 62BEE9264BF17311
96 changed files with 6702 additions and 5275 deletions
  1. +2
    -0
      build.gradle
  2. +197
    -117
      documentation/extensions/omemo.md
  3. +43
    -0
      documentation/extensions/omemo_migration_4.2.0_head.md
  4. +1
    -0
      smack-integration-test/build.gradle
  5. +3
    -37
      smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoIntegrationTest.java
  6. +149
    -0
      smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoMessageListener.java
  7. +75
    -0
      smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java
  8. +102
    -0
      smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java
  9. +0
    -81
      smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoInitializationTest.java
  10. +0
    -156
      smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoIntegrationTestHelper.java
  11. +0
    -109
      smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoKeyTransportTest.java
  12. +74
    -0
      smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMamDecryptionTest.java
  13. +235
    -0
      smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java
  14. +0
    -193
      smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessageSendingTest.java
  15. +0
    -195
      smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoSessionRenegotiationTest.java
  16. +0
    -163
      smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java
  17. +79
    -0
      smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/SessionRenegotiationIntegrationTest.java
  18. +2
    -1
      smack-omemo-signal/build.gradle
  19. +60
    -0
      smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalCachingOmemoStore.java
  20. +3
    -6
      smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalFileBasedOmemoStore.java
  21. +30
    -39
      smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoKeyUtil.java
  22. +162
    -0
      smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoRatchet.java
  23. +29
    -30
      smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoService.java
  24. +0
    -142
      smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoSession.java
  25. +3
    -2
      smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStore.java
  26. +82
    -40
      smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStoreConnector.java
  27. +0
    -91
      smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoMessageBuilderTest.java
  28. +0
    -218
      smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalFileBasedOmemoStoreTest.java
  29. +93
    -0
      smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/LegacySignalOmemoKeyUtilTest.java
  30. +58
    -0
      smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoKeyUtilTest.java
  31. +5
    -20
      smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoManagerTest.java
  32. +2
    -2
      smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreConnectorTest.java
  33. +83
    -0
      smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreTest.java
  34. +2
    -0
      smack-omemo/build.gradle
  35. +446
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/CachingOmemoStore.java
  36. +407
    -470
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/FileBasedOmemoStore.java
  37. +98
    -47
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoConfiguration.java
  38. +654
    -382
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoManager.java
  39. +213
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessage.java
  40. +197
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoRatchet.java
  41. +1023
    -925
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java
  42. +265
    -428
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoStore.java
  43. +170
    -2
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement.java
  44. +43
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement_VAxolotl.java
  45. +0
    -207
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleVAxolotlElement.java
  46. +5
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListElement.java
  47. +8
    -2
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListElement_VAxolotl.java
  48. +19
    -120
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoElement.java
  49. +44
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoElement_VAxolotl.java
  50. +83
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement.java
  51. +27
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement_VAxolotl.java
  52. +84
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoKeyElement.java
  53. +0
    -86
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoVAxolotlElement.java
  54. +11
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CryptoFailedException.java
  55. +33
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoIdentityKeyException.java
  56. +10
    -1
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoRawSessionException.java
  57. +66
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/StaleDeviceException.java
  58. +6
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UndecidedOmemoIdentityException.java
  59. +93
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UntrustedOmemoIdentityException.java
  60. +7
    -2
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CipherAndAuthTag.java
  61. +0
    -63
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/ClearTextMessage.java
  62. +23
    -3
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoCachedDeviceList.java
  63. +10
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoDevice.java
  64. +0
    -141
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoMessageInformation.java
  65. +0
    -266
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoSession.java
  66. +29
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/listener/OmemoCarbonCopyStanzaReceivedListener.java
  67. +25
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/listener/OmemoMessageStanzaReceivedListener.java
  68. +3
    -16
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/listener/package-info.java
  69. +10
    -17
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/OmemoMessageListener.java
  70. +5
    -25
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/OmemoMucMessageListener.java
  71. +4
    -4
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoBundleVAxolotlProvider.java
  72. +4
    -4
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoDeviceListVAxolotlProvider.java
  73. +20
    -24
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoVAxolotlProvider.java
  74. +15
    -5
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/OmemoFingerprint.java
  75. +27
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/OmemoTrustCallback.java
  76. +23
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/TrustState.java
  77. +23
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/trust/package-info.java
  78. +49
    -0
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/MessageOrOmemoMessage.java
  79. +2
    -2
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoConstants.java
  80. +19
    -80
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoKeyUtil.java
  81. +124
    -90
      smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilder.java
  82. +0
    -47
      smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoKeyUtilTest.java
  83. +10
    -3
      smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/DeviceListTest.java
  84. +4
    -4
      smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoBundleVAxolotlElementTest.java
  85. +15
    -25
      smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoConfigurationTest.java
  86. +4
    -4
      smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoDeviceListVAxolotlElementTest.java
  87. +1
    -1
      smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoDeviceTest.java
  88. +1
    -1
      smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoExceptionsTest.java
  89. +2
    -2
      smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoFingerprintTest.java
  90. +71
    -82
      smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoKeyUtilTest.java
  91. +91
    -0
      smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoServiceTest.java
  92. +345
    -0
      smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java
  93. +13
    -11
      smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoVAxolotlElementTest.java
  94. +6
    -41
      smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/WrapperObjectsTest.java
  95. +59
    -0
      smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/util/EphemeralTrustCallback.java
  96. +79
    -0
      smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilderTest.java

+ 2
- 0
build.gradle View File

@@ -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
}



+ 197
- 117
documentation/extensions/omemo.md View File

@@ -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.

```
SignalOmemoService.setup();
```
Storing Keys
------------

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.
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.

```
//set path in case we want to use a file-based store (default)
OmemoConfiguration.setFileBasedOmemoStoreDefaultPath(new File("path/to/your/store"));
```
* 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.

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.
* 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.

```
OmemoManager omemoManager = OmemoManager.getInstanceFor(connection);
```
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.

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.
It most certainly makes sense to store the data of the used `OmemoStore` in a secure way (for example an
encrypted database).

```
omemoManager.addOmemoMessageListener(new OmemoMessageListener() {
@Overwrite
public void omOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
System.out.println(decryptedBody);
}
});
Handling Trust Decisions
------------------------

omemoManager.addOmemoMucMessageListener(new OmemoMucMessageListener() {
@Overwrite
public void onOmemoMucMessageReceived(MultiUserChat muc, BareJid from, String decryptedBody, Message message,
Message wrappingMessage, OmemoMessageInformation omemoInformation) {
System.out.println(decryptedBody);
}
});
```
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.

Usage
-----
3. Get an instance of the OmemoManager for your connection

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.
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:

```
omemoManager.trustOmemoIdentity(trustedDevice, trustedFingerprint);
omemoManager.distrustOmemoIdentity(untrustedDevice, untrustedFingerprint);
List<OmemoDevice> devices = manager.getDevicesOf(contactsBareJid);
```

The trust decision should be made by the user based on comparing fingerprints.
You can get fingerprints of your own and contacts devices:
To get the OmemoFingerprint of a device, you can call

```
OmemoFingerprint myFingerprint = omemoManager.getFingerprint();
OmemoFingerprint otherFingerprint = omemoStore.getFingerprint(omemoManager, otherDevice);
OmemoFingerprint fingerprint = manager.getFingerprint(device);
```

To encrypt a message for a single contact or a MUC, you do as follows:
This fingerprint can now be displayed to the user who can decide whether to trust the device, or not.

```
Message encryptedSingleMessage = omemoManager.encrypt(bobsBareJid, "Hi Bob!");
// Trust
manager.trustOmemoIdentity(device, fingerprint);

Message encryptedMucMessage = omemoManager.encrypt(multiUserChat, "Hi everybody!");
// Distrust
manager.distrustOmemoIdentity(device, fingerprint);
```

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:
### Encrypt a Message

Currently only Message bodies can be encrypted.
```
Message encryptedMessage = omemoManager.encryptForExistingSession(cannotEstablishSessionException, "Hi there!");
String secret = "Mallory is a twerp!";
OmemoMessage.Sent encrypted = manager.encrypt(contactsBareJid, secret);
```

The resulting message can then be sent via the ChatManager/MultiUserChatManager.
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.

You may want to generate a new identity sometime in the future. That's pretty straight
forward. No need to manually publish bundles etc.
### 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.regenerate();
manager.multiUserChatSupportsOmemo(muc);
```

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.
Encryption is then done analog to single message encryption:

```
omemoManager.purgeDevices();
OmemoMessage.Sent encrypted = manager.encrypt(multiUserChat, secret);
```

If you want to find out, whether a server, MUC or contacts resource supports OMEMO,
you can use the following methods:
### Sending an encrypted Message

```
boolean serverCan = omemoManager.serverSupportsOmemo(serverJid);
boolean mucCan = omemoManager.multiUserChatSupportsOmemo(mucJid);
boolean resourceCan = omemoManager.resourceSupportsOmemo(contactsResourceJid);
```
To send the message, it has to be wrapped in a `Message` object. That can conveniently be done like follows.

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);
Message message = encrypted.asMessage(contactsJid);
connection.sendStanza(message):
```

If you want to decrypt a MamQueryResult, you can do so using the following method:
````
List<ClearTextMessage> 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
-----------------


+ 43
- 0
documentation/extensions/omemo_migration_4.2.0_head.md View File

@@ -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





+ 1
- 0
smack-integration-test/build.gradle View File

@@ -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


+ 3
- 37
smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoIntegrationTest.java View File

@@ -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.");
}
}

@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");
OmemoConfiguration.setCompleteSessionWithEmptyMessage(true);
OmemoConfiguration.setRepairBrokenSessionsWithPrekeyMessages(true);
}

public abstract void before();

public abstract void after();
}

+ 149
- 0
smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoMessageListener.java View File

@@ -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();
}
}
}
}
}

+ 75
- 0
smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java View File

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

+ 102
- 0
smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java View File

@@ -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_);
}
}

+ 0
- 81
smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoInitializationTest.java View File

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

+ 0
- 156
smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoIntegrationTestHelper.java View File

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

+ 0
- 109
smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoKeyTransportTest.java View File

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

+ 74
- 0
smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMamDecryptionTest.java View File

@@ -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<MessageOrOmemoMessage> decryptedMamQuery = bob.decryptMamQueryResult(query);

assertEquals(1, decryptedMamQuery.size());
assertEquals(body, decryptedMamQuery.get(decryptedMamQuery.size() - 1).getOmemoMessage().getBody());
}
}

+ 235
- 0
smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java View File

@@ -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<OmemoDevice, OmemoFingerprint> 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<OmemoDevice, OmemoFingerprint> fps1 = alice.getActiveFingerprints(bob.getOwnJid());

assertFalse(fps1.isEmpty());
assertAllDevicesAreUndecided(alice, fps1);
assertAllDevicesAreUntrusted(alice, fps1);

trustAllIdentities(alice, bob);

HashMap<OmemoDevice, OmemoFingerprint> 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<OmemoDevice, OmemoFingerprint> 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<OmemoDevice, OmemoFingerprint> 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<OmemoDevice, OmemoFingerprint> 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<OmemoDevice, OmemoFingerprint> 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
}
}
}
}

+ 0
- 193
smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessageSendingTest.java View File

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

+ 0
- 195
smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoSessionRenegotiationTest.java View File

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

+ 0
- 163
smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java View File

@@ -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;