mirror of
https://github.com/vanitasvitae/Smack.git
synced 2024-10-04 21:49:33 +02:00
Add OMEMO support
This commit adds the modules smack-omemo and smack-omemo-signal. smack-omemo is licensed under the Apache license like the rest of the smack project. smack-omemo-signal on the other hand is licensed under the GPLv3. Due to the fact, that smack-omemo is not of much use without smack-omemo-signal, the OMEMO feature can currently only be used by GPLv3 compatible software. This may change in the future, when a more permissively licensed module becomes available. Fixes SMACK-743.
This commit is contained in:
parent
ce36fb468c
commit
e86700b040
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -18,6 +18,7 @@ core/build/
|
|||
debug/build/
|
||||
experimental/build/
|
||||
extensions/build/
|
||||
out/
|
||||
|
||||
bin/
|
||||
core/bin
|
||||
|
|
81
build.gradle
81
build.gradle
|
@ -40,7 +40,11 @@ allprojects {
|
|||
// build, causing unnecessary rebuilds.
|
||||
builtDate = (new java.text.SimpleDateFormat("yyyy-MM-dd")).format(new Date())
|
||||
oneLineDesc = 'An Open Source XMPP (Jabber) client library'
|
||||
javadocAllProjects = subprojects - project(':smack-integration-test')
|
||||
integrationTestProjects = [
|
||||
':smack-integration-test',
|
||||
':smack-omemo-signal-integration-test',
|
||||
].collect{ project(it) }
|
||||
javadocAllProjects = subprojects - integrationTestProjects
|
||||
// A dirty hack used for Gradle's jacoco plugin, since is not
|
||||
// hable to handle the case when a (sub)project has no unit
|
||||
// tests. :-(
|
||||
|
@ -56,6 +60,7 @@ allprojects {
|
|||
':smack-resolver-dnsjava',
|
||||
':smack-resolver-javax',
|
||||
':smack-resolver-minidns',
|
||||
':smack-omemo-signal-integration-test',
|
||||
].collect{ project(it) }
|
||||
projectsWithUnitTests = subprojects - projectsWithoutUnitTests
|
||||
androidProjects = [
|
||||
|
@ -67,11 +72,17 @@ allprojects {
|
|||
':smack-sasl-provided',
|
||||
':smack-extensions',
|
||||
':smack-experimental',
|
||||
':smack-omemo',
|
||||
':smack-omemo-signal',
|
||||
].collect{ project(it) }
|
||||
androidBootClasspathProjects = [
|
||||
':smack-android',
|
||||
':smack-android-extensions',
|
||||
].collect{ project(it) }
|
||||
gplLicensedProjects = [
|
||||
':smack-omemo-signal',
|
||||
':smack-omemo-signal-integration-test',
|
||||
].collect{ project(it) }
|
||||
androidBootClasspath = getAndroidRuntimeJar()
|
||||
androidJavadocOffline = getAndroidJavadocOffline()
|
||||
junitVersion = '4.11'
|
||||
|
@ -350,14 +361,6 @@ subprojects {
|
|||
developerConnection 'scm:git:https://github.com/igniterealtime/Smack.git'
|
||||
}
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name 'The Apache Software License, Version 2.0'
|
||||
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||
distribution 'repo'
|
||||
}
|
||||
}
|
||||
|
||||
developers {
|
||||
developer {
|
||||
id 'flow'
|
||||
|
@ -387,8 +390,17 @@ subprojects {
|
|||
}
|
||||
}
|
||||
|
||||
// No need to ever clirr smack-integration-test
|
||||
project(':smack-integration-test').clirr.enabled = false
|
||||
// There is no need to ever clirr integration test projects and the
|
||||
// smack-repl project.
|
||||
configure(integrationTestProjects + project(':smack-repl')) {
|
||||
clirr {
|
||||
enabled false
|
||||
}
|
||||
}
|
||||
|
||||
// Disable clirr on omemo modules
|
||||
project(':smack-omemo').clirr.enabled = false
|
||||
project(':smack-omemo-signal').clirr.enabled = false
|
||||
|
||||
subprojects*.jar {
|
||||
manifest {
|
||||
|
@ -396,6 +408,49 @@ subprojects*.jar {
|
|||
}
|
||||
}
|
||||
|
||||
configure(subprojects - gplLicensedProjects) {
|
||||
checkstyle {
|
||||
configProperties.checkstyleLicenseHeader = "header"
|
||||
}
|
||||
uploadArchives {
|
||||
repositories {
|
||||
mavenDeployer {
|
||||
pom.project {
|
||||
licenses {
|
||||
license {
|
||||
name 'The Apache Software License, Version 2.0'
|
||||
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||
distribution 'repo'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configure(gplLicensedProjects) {
|
||||
checkstyle {
|
||||
configProperties.checkstyleLicenseHeader = "${project.name}-gplv3-license-header"
|
||||
}
|
||||
uploadArchives {
|
||||
repositories {
|
||||
mavenDeployer {
|
||||
pom.project {
|
||||
licenses {
|
||||
license {
|
||||
name 'GNU General Public License, version 3 or any later version'
|
||||
url 'https://www.gnu.org/licenses/gpl.txt'
|
||||
distribution 'repo'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
configure(androidBootClasspathProjects) {
|
||||
compileJava {
|
||||
options.bootClasspath = androidBootClasspath
|
||||
|
@ -436,6 +491,10 @@ task integrationTest {
|
|||
dependsOn project(':smack-integration-test').tasks.run
|
||||
}
|
||||
|
||||
task omemoSignalIntTest {
|
||||
dependsOn project(':smack-omemo-signal-integration-test').tasks.run
|
||||
}
|
||||
|
||||
def getGitCommit() {
|
||||
def dotGit = new File("$projectDir/.git")
|
||||
if (!dotGit.isDirectory()) return 'non-git build'
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</module>
|
||||
<module name="SuppressionCommentFilter"/>
|
||||
<module name="Header">
|
||||
<property name="headerFile" value="config/header.txt"/>
|
||||
<property name="headerFile" value="config/${checkstyleLicenseHeader}.txt"/>
|
||||
<property name="ignoreLines" value="3"/>
|
||||
<property name="fileExtensions" value="java"/>
|
||||
</module>
|
||||
|
|
20
config/smack-omemo-signal-gplv3-license-header.txt
Normal file
20
config/smack-omemo-signal-gplv3-license-header.txt
Normal file
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 20XX John Doe
|
||||
*
|
||||
* This file is part of smack-omemo-signal.
|
||||
*
|
||||
* smack-omemo-signal is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 20XX John Doe
|
||||
*
|
||||
* This file is part of smack-omemo-signal-integration-test.
|
||||
*
|
||||
* smack-omemo-signal-integration-test is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
|
@ -91,9 +91,10 @@ Experimental Smack Extensions and currently supported XEPs of smack-experimental
|
|||
| JSON Containers | [XEP-0335](http://xmpp.org/extensions/xep-0335.html) | Encapsulation of JSON data within XMPP Stanzas. |
|
||||
| [Internet of Things - Discovery](iot.md) | [XEP-0347](http://xmpp.org/extensions/xep-0347.html) | Describes how Things can be installed and discovered by their owners. |
|
||||
| Client State Indication | [XEP-0352](http://xmpp.org/extensions/xep-0352.html) | A way for the client to indicate its active/inactive state. |
|
||||
| [Push Notifications](pushnotifications.md) | [XEP-0357](http://xmpp.org/extensions/xep-0357.html) | Defines a way to manage push notifications from an XMPP Server. |
|
||||
| [Push Notifications](pushnotifications.md) | [XEP-0357](http://xmpp.org/extensions/xep-0357.html) | Defines a way to manage push notifications from an XMPP Server. |
|
||||
| HTTP File Upload | [XEP-0363](http://xmpp.org/extensions/xep-0363.html) | Protocol to request permissions to upload a file to an HTTP server and get a shareable URL. |
|
||||
| [Multi-User Chat Light](muclight.md) | [XEP-xxxx](http://mongooseim.readthedocs.io/en/latest/open-extensions/xeps/xep-muc-light.html) | Multi-User Chats for mobile XMPP applications and specific enviroment. |
|
||||
| [OMEMO End Encryption (omemo.md) | [XEP-0384](http://xmpp.org/extensions/xep-0384.html) | Encrypt messages using OMEMO encryption (currently only with smack-omemo-signal -> GPLv3). |
|
||||
| Google GCM JSON payload | n/a | Semantically the same as XEP-0335: JSON Containers |
|
||||
|
||||
|
||||
|
|
217
documentation/extensions/omemo.md
Normal file
217
documentation/extensions/omemo.md
Normal file
|
@ -0,0 +1,217 @@
|
|||
Encrypting messages with OMEMO
|
||||
==============================
|
||||
|
||||
[Back](index.md)
|
||||
|
||||
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
|
||||
|
||||
* Confidentiality
|
||||
* Integrity
|
||||
* Authenticity
|
||||
* Forward secrecy
|
||||
* Future secrecy (break-in recovery)
|
||||
* Plausible deniability
|
||||
|
||||
Contrary to OTR it is capable of multi-end-to-multi-end encryption and
|
||||
message synchronization across multiple devices. It also allows the sender
|
||||
to send a message while the recipient is offline.
|
||||
|
||||
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,
|
||||
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
|
||||
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
|
||||
------------
|
||||
|
||||
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))
|
||||
to store and exchange key bundles.
|
||||
Optionally your server should support Message Carbons ([XEP-0280](http://xmpp.org/extensions/xep-0280.html))
|
||||
and Message Archive Management ([XEP-0313](http://xmpp.org/extensions/xep-0313.html))
|
||||
to achieve message synchronization across all (on- and offline) devices.
|
||||
|
||||
Setup
|
||||
-----
|
||||
|
||||
First you need to setup a OmemoService, for example the libsignal one:
|
||||
|
||||
```
|
||||
SignalOmemoService.setup();
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
//set path in case we want to use a file-based store (default)
|
||||
OmemoConfiguration.setFileBasedOmemoStoreDefaultPath(new File("path/to/your/store"));
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
OmemoManager omemoManager = OmemoManager.getInstanceFor(connection);
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
omemoManager.addOmemoMessageListener(new OmemoMessageListener() {
|
||||
@Overwrite
|
||||
public void omOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
|
||||
System.out.println(decryptedBody);
|
||||
}
|
||||
});
|
||||
|
||||
omemoManager.addOmemoMucMessageListener(new OmemoMucMessageListener() {
|
||||
@Overwrite
|
||||
public void onOmemoMucMessageReceived(MultiUserChat muc, BareJid from, String decryptedBody, Message message,
|
||||
Message wrappingMessage, OmemoMessageInformation omemoInformation) {
|
||||
System.out.println(decryptedBody);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
omemoManager.trustOmemoIdentity(trustedDevice, trustedFingerprint);
|
||||
omemoManager.distrustOmemoIdentity(untrustedDevice, untrustedFingerprint);
|
||||
```
|
||||
|
||||
The trust decision should be made by the user based on comparing fingerprints.
|
||||
You can get fingerprints of your own and contacts devices:
|
||||
|
||||
```
|
||||
OmemoFingerprint myFingerprint = omemoManager.getFingerprint();
|
||||
OmemoFingerprint otherFingerprint = omemoStore.getFingerprint(omemoManager, otherDevice);
|
||||
```
|
||||
|
||||
To encrypt a message for a single contact or a MUC, you do as follows:
|
||||
|
||||
```
|
||||
Message encryptedSingleMessage = omemoManager.encrypt(bobsBareJid, "Hi Bob!");
|
||||
|
||||
Message encryptedMucMessage = omemoManager.encrypt(multiUserChat, "Hi everybody!");
|
||||
```
|
||||
|
||||
Note: It may happen, that smack-omemo is unable to create a session with a device.
|
||||
In case we could not create a single valid session for a recipient, a
|
||||
CannotCreateOmemoSessionException will be thrown. This exception contains information
|
||||
about which sessions could (not) be created and why. If you want to ignore those devices,
|
||||
you can encrypt the message for all remaining devices like this:
|
||||
|
||||
```
|
||||
Message encryptedMessage = omemoManager.encryptForExistingSession(cannotEstablishSessionException, "Hi there!");
|
||||
```
|
||||
|
||||
The resulting message can then be sent via the ChatManager/MultiUserChatManager.
|
||||
|
||||
You may want to generate a new identity sometime in the future. That's pretty straight
|
||||
forward. No need to manually publish bundles etc.
|
||||
|
||||
```
|
||||
omemoManager.regenerate();
|
||||
```
|
||||
|
||||
In case your device list gets filled with old unused identities, you can clean it up.
|
||||
This will remove all active devices from the device list and only publish the device
|
||||
you are using right now.
|
||||
|
||||
```
|
||||
omemoManager.purgeDevices();
|
||||
```
|
||||
|
||||
If you want to find out, whether a server, MUC or contacts resource supports OMEMO,
|
||||
you can use the following methods:
|
||||
|
||||
```
|
||||
boolean serverCan = omemoManager.serverSupportsOmemo(serverJid);
|
||||
boolean mucCan = omemoManager.multiUserChatSupportsOmemo(mucJid);
|
||||
boolean resourceCan = omemoManager.resourceSupportsOmemo(contactsResourceJid);
|
||||
```
|
||||
|
||||
It might happen, that the server you or your contact are using is not delivering devicelist updates correctly.
|
||||
In such a case smack-omemo cannot fetch bundles or send messages to devices it hasn\'t seen before. To mitigate this, it
|
||||
might help to explicitly request the latest device list from the server.
|
||||
```
|
||||
omemoManager.requestDeviceListUpdateFor(contactJid);
|
||||
```
|
||||
|
||||
If you want to decrypt a MamQueryResult, you can do so using the following method:
|
||||
````
|
||||
List<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.
|
||||
|
||||
|
||||
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
|
||||
|
||||
Integration Tests
|
||||
-----------------
|
||||
smack-omemo comes with a set of integration tests. Lets say you want to run the integration test suite for smack-omemo-signal.
|
||||
You can do so by using the following gradle task:
|
||||
|
||||
```
|
||||
gradle omemoSignalIntTest
|
||||
```
|
|
@ -23,4 +23,7 @@ include 'smack-core',
|
|||
'smack-android-extensions',
|
||||
'smack-java7',
|
||||
'smack-integration-test',
|
||||
'smack-omemo',
|
||||
'smack-omemo-signal',
|
||||
'smack-omemo-signal-integration-test',
|
||||
'smack-repl'
|
||||
|
|
|
@ -13,8 +13,10 @@ dependencies {
|
|||
testCompile "org.jxmpp:jxmpp-jid:$jxmppVersion:tests"
|
||||
testCompile "junit:junit:$junitVersion"
|
||||
testCompile 'xmlunit:xmlunit:1.5'
|
||||
testCompile 'org.powermock:powermock-module-junit4:1.5.5'
|
||||
testCompile 'org.powermock:powermock-api-mockito:1.5.5'
|
||||
testCompile "org.powermock:powermock-module-junit4:1.6.4"
|
||||
testCompile "org.powermock:powermock-module-junit4-rule:1.6.4"
|
||||
testCompile "org.powermock:powermock-api-mockito:1.6.4"
|
||||
testCompile "org.powermock:powermock-classloading-xstream:1.6.4"
|
||||
testCompile 'com.jamesmurty.utils:java-xmlbuilder:0.6'
|
||||
testCompile 'net.iharder:base64:2.3.8'
|
||||
}
|
||||
|
|
|
@ -20,5 +20,6 @@
|
|||
<className>org.jivesoftware.smack.android.AndroidSmackInitializer</className>
|
||||
<className>org.jivesoftware.smack.java7.Java7SmackInitializer</className>
|
||||
<className>org.jivesoftware.smack.im.SmackImInitializer</className>
|
||||
<className>org.jivesoftware.smackx.omemo.OmemoInitializer</className>
|
||||
</optionalStartupClasses>
|
||||
</smack>
|
||||
|
|
|
@ -11,6 +11,7 @@ dependencies {
|
|||
compile project(':smack-tcp')
|
||||
compile project(':smack-extensions')
|
||||
compile project(':smack-experimental')
|
||||
compile project(':smack-omemo')
|
||||
compile 'org.reflections:reflections:0.9.9-RC1'
|
||||
compile 'eu.geekplace.javapinning:java-pinning-java7:1.1.0-alpha1'
|
||||
compile "junit:junit:$junitVersion"
|
||||
|
|
|
@ -196,10 +196,14 @@ public final class Configuration {
|
|||
return addTestPackage(enabledTest.getPackage().getName());
|
||||
}
|
||||
|
||||
public Builder addTestPackage(String testPackage) {
|
||||
private void ensureTestPackagesIsSet(int length) {
|
||||
if (testPackages == null) {
|
||||
testPackages = new HashSet<>();
|
||||
testPackages = new HashSet<>(length);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder addTestPackage(String testPackage) {
|
||||
ensureTestPackagesIsSet(4);
|
||||
testPackages.add(testPackage);
|
||||
return this;
|
||||
}
|
||||
|
@ -260,10 +264,12 @@ public final class Configuration {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder setTestPackages(String testPackagesString) {
|
||||
public Builder addTestPackages(String testPackagesString) {
|
||||
if (testPackagesString != null) {
|
||||
String[] testPackagesArray = testPackagesString.split(",");
|
||||
testPackages = new HashSet<>(testPackagesArray.length);
|
||||
|
||||
ensureTestPackagesIsSet(testPackagesArray.length);
|
||||
|
||||
for (String s : testPackagesArray) {
|
||||
testPackages.add(s.trim());
|
||||
}
|
||||
|
@ -271,6 +277,19 @@ public final class Configuration {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder addTestPackages(String[] testPackagesString) {
|
||||
if (testPackagesString == null) {
|
||||
return this;
|
||||
}
|
||||
|
||||
ensureTestPackagesIsSet(testPackagesString.length);
|
||||
|
||||
for (String testPackage : testPackagesString) {
|
||||
testPackages.add(testPackage);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Configuration build() throws KeyManagementException, NoSuchAlgorithmException {
|
||||
return new Configuration(service, serviceTlsPin, securityMode, replyTimeout, debug, accountOneUsername,
|
||||
accountOnePassword, accountTwoUsername, accountTwoPassword, accountThreeUsername, accountThreePassword, enabledTests, disabledTests,
|
||||
|
@ -280,7 +299,7 @@ public final class Configuration {
|
|||
|
||||
private static final String SINTTEST = "sinttest.";
|
||||
|
||||
public static Configuration newConfiguration()
|
||||
public static Configuration newConfiguration(String[] testPackages)
|
||||
throws IOException, KeyManagementException, NoSuchAlgorithmException {
|
||||
Properties properties = new Properties();
|
||||
|
||||
|
@ -330,7 +349,9 @@ public final class Configuration {
|
|||
builder.setDebug(properties.getProperty("debug"));
|
||||
builder.setEnabledTests(properties.getProperty("enabledTests"));
|
||||
builder.setDisabledTests(properties.getProperty("disabledTests"));
|
||||
builder.setTestPackages(properties.getProperty("testPackages"));
|
||||
|
||||
builder.addTestPackages(properties.getProperty("testPackages"));
|
||||
builder.addTestPackages(testPackages);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ public class SmackIntegrationTestFramework {
|
|||
|
||||
public static void main(String[] args) throws IOException, KeyManagementException,
|
||||
NoSuchAlgorithmException, SmackException, XMPPException, InterruptedException {
|
||||
Configuration config = Configuration.newConfiguration();
|
||||
Configuration config = Configuration.newConfiguration(args);
|
||||
|
||||
SmackIntegrationTestFramework sinttest = new SmackIntegrationTestFramework(config);
|
||||
TestRunResult testRunResult = sinttest.run();
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
*
|
||||
* 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 org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest;
|
||||
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
|
||||
import org.igniterealtime.smack.inttest.TestNotPossibleException;
|
||||
import org.jivesoftware.smack.SmackException;
|
||||
import org.jivesoftware.smack.XMPPException;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Super class for OMEMO integration tests.
|
||||
*/
|
||||
public abstract class AbstractOmemoIntegrationTest extends AbstractSmackIntegrationTest {
|
||||
|
||||
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)");
|
||||
}
|
||||
|
||||
//Check for OmemoService
|
||||
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");
|
||||
}
|
||||
|
||||
public abstract void before();
|
||||
|
||||
public abstract void after();
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
*
|
||||
* 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 org.igniterealtime.smack.inttest.SmackIntegrationTest;
|
||||
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
|
||||
import org.igniterealtime.smack.inttest.TestNotPossibleException;
|
||||
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 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;
|
||||
|
||||
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.
|
||||
*/
|
||||
@SmackIntegrationTest
|
||||
public void initializationTest() throws XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, SmackException.NotLoggedInException, CorruptedOmemoKeyException {
|
||||
//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,0 +1,155 @@
|
|||
/**
|
||||
*
|
||||
* 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 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.PubSubAssertionError;
|
||||
import org.jivesoftware.smackx.pubsub.PubSubException;
|
||||
import org.jivesoftware.smackx.pubsub.PubSubManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static junit.framework.TestCase.assertEquals;
|
||||
import static junit.framework.TestCase.assertNotNull;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
|
||||
/**
|
||||
* 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 | PubSubAssertionError.DiscoInfoNodeAssertionError e) {
|
||||
//Silent
|
||||
}
|
||||
|
||||
try {
|
||||
pm.deleteNode(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(id));
|
||||
} catch (SmackException.NoResponseException | InterruptedException | SmackException.NotConnectedException | XMPPException.XMPPErrorException | PubSubAssertionError e) {
|
||||
//Silent
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
pm.getLeafNode(OmemoConstants.PEP_NODE_DEVICE_LIST).deleteAllItems();
|
||||
} catch (InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException | PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException | PubSubAssertionError.DiscoInfoNodeAssertionError e) {
|
||||
//Silent
|
||||
}
|
||||
|
||||
try {
|
||||
pm.deleteNode(OmemoConstants.PEP_NODE_DEVICE_LIST);
|
||||
} catch (SmackException.NoResponseException | InterruptedException | SmackException.NotConnectedException | XMPPException.XMPPErrorException | PubSubAssertionError 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 {
|
||||
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,0 +1,112 @@
|
|||
/**
|
||||
*
|
||||
* 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 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;
|
||||
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 java.util.Arrays;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
try {
|
||||
syncPoint.waitForResult(10 * 1000);
|
||||
} catch (TimeoutException e) {
|
||||
TestCase.fail("We MUST have received the keyTransportMessage within 10 seconds.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void after() {
|
||||
alice.shutdown();
|
||||
bob.shutdown();
|
||||
cleanServerSideTraces(alice);
|
||||
cleanServerSideTraces(bob);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
/**
|
||||
*
|
||||
* 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 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;
|
||||
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 java.security.NoSuchAlgorithmException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@SmackIntegrationTest
|
||||
public void messageSendingTest() throws CorruptedOmemoKeyException, InterruptedException, SmackException.NoResponseException, SmackException.NotConnectedException, XMPPException.XMPPErrorException, SmackException.NotLoggedInException, PubSubException.NotALeafNodeException, CannotEstablishOmemoSessionException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, CryptoFailedException {
|
||||
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,0 +1,193 @@
|
|||
/**
|
||||
*
|
||||
* 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 org.igniterealtime.smack.inttest.SmackIntegrationTest;
|
||||
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
|
||||
import org.igniterealtime.smack.inttest.TestNotPossibleException;
|
||||
import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint;
|
||||
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 java.util.logging.Level;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 messagewe 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) {
|
||||