1
0
Fork 0
mirror of https://codeberg.org/Mercury-IM/Smack synced 2024-11-24 15:22:07 +01: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:
vanitasvitae 2017-06-02 12:26:37 +02:00 committed by Florian Schmaus
parent ce36fb468c
commit e86700b040
95 changed files with 11770 additions and 22 deletions

1
.gitignore vendored
View file

@ -18,6 +18,7 @@ core/build/
debug/build/
experimental/build/
extensions/build/
out/
bin/
core/bin

View file

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

View file

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

View 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
*/

View file

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

View file

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

View 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
```

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,159 @@
/**
*
* 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.XMPPException;
import java.util.Date;
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.deletePath;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.setUpOmemoManager;
/**
* Test the OmemoStore.
*/
public class OmemoStoreTest extends AbstractOmemoIntegrationTest {
private OmemoManager alice;
private OmemoManager bob;
public OmemoStoreTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException {
super(environment);
}
@Override
public void before() {
alice = OmemoManager.getInstanceFor(conOne);
bob = OmemoManager.getInstanceFor(conOne);
}
@SmackIntegrationTest
public void storeTest() throws Exception {
//########### PRE-INITIALIZATION ############
assertEquals("Creating an OmemoManager without MUST have set the default deviceId.", alice.getDeviceId(), OmemoService.getInstance().getOmemoStoreBackend().getDefaultDeviceId(alice.getOwnJid()));
assertEquals("OmemoManager must be equal, since both got created without giving a deviceId.", alice, bob);
OmemoService.getInstance().getOmemoStoreBackend().setDefaultDeviceId(alice.getOwnJid(), -1); //Reset default deviceId
alice.shutdown();
alice = OmemoManager.getInstanceFor(conOne);
assertNotSame("Instantiating OmemoManager without deviceId MUST assign random deviceId.", alice.getDeviceId(), bob.getDeviceId());
OmemoStore<?,?,?,?,?,?,?,?,?> store = OmemoService.getInstance().getOmemoStoreBackend();
OmemoFingerprint finger = new OmemoFingerprint("FINGER");
//DefaultDeviceId
store.setDefaultDeviceId(alice.getOwnJid(), 777);
assertEquals("defaultDeviceId setting/getting must equal.", 777, store.getDefaultDeviceId(alice.getOwnJid()));
//Trust/Distrust/Decide
bob.shutdown();
bob = OmemoManager.getInstanceFor(conTwo, 998);
assertFalse("Bobs device MUST be undecided at this point",
store.isDecidedOmemoIdentity(alice, bob.getOwnDevice(), finger));
assertFalse("Bobs device MUST not be trusted at this point",
store.isTrustedOmemoIdentity(alice, bob.getOwnDevice(), finger));
store.trustOmemoIdentity(alice, bob.getOwnDevice(), finger);
assertTrue("Bobs device MUST be trusted at this point.",
store.isTrustedOmemoIdentity(alice, bob.getOwnDevice(), finger));
assertTrue("Bobs device MUST be decided at this point.",
store.isDecidedOmemoIdentity(alice, bob.getOwnDevice(), finger));
store.distrustOmemoIdentity(alice, bob.getOwnDevice(), finger);
assertFalse("Bobs device MUST be untrusted at this point.",
store.isTrustedOmemoIdentity(alice, bob.getOwnDevice(), finger));
//Dates
assertNull("Date of last received message must be null when no message was received ever.",
store.getDateOfLastReceivedMessage(alice, bob.getOwnDevice()));
Date now = new Date();
store.setDateOfLastReceivedMessage(alice, bob.getOwnDevice(), now);
assertEquals("Date of last reveived message must match the one we set.",
now, store.getDateOfLastReceivedMessage(alice, bob.getOwnDevice()));
assertNull("Date of last signed prekey renewal must be null.",
store.getDateOfLastSignedPreKeyRenewal(alice));
store.setDateOfLastSignedPreKeyRenewal(alice, now);
assertEquals("Date of last signed prekey renewal must match our date.",
now, store.getDateOfLastSignedPreKeyRenewal(alice));
//Keys
assertNull("IdentityKeyPair must be null at this point.",
store.loadOmemoIdentityKeyPair(alice));
assertNull("IdentityKey of contact must be null at this point.",
store.loadOmemoIdentityKey(alice, bob.getOwnDevice()));
assertEquals("PreKeys list must be of length 0 at this point.",
0, store.loadOmemoPreKeys(alice).size());
assertEquals("SignedPreKeys list must be of length 0 at this point.",
0, store.loadOmemoSignedPreKeys(alice).size());
assertNotNull("Generated IdentityKeyPair must not be null.",
store.generateOmemoIdentityKeyPair());
assertEquals("Generated PreKey list must be of correct length.",
100, store.generateOmemoPreKeys(1, 100).size());
//LastPreKeyId
assertEquals("LastPreKeyId must be 0 at this point.",
0, store.loadLastPreKeyId(alice));
store.storeLastPreKeyId(alice, 1234);
Thread.sleep(100);
assertEquals("LastPreKeyId set/get must equal.", 1234, store.loadLastPreKeyId(alice));
store.storeLastPreKeyId(alice, 0);
//CurrentSignedPreKeyId
assertEquals("CurrentSignedPreKeyId must be 0 at this point.",
0, store.loadCurrentSignedPreKeyId(alice));
store.storeCurrentSignedPreKeyId(alice, 554);
Thread.sleep(100);
assertEquals("CurrentSignedPreKeyId must match the value we set.",
554, store.loadCurrentSignedPreKeyId(alice));
store.storeCurrentSignedPreKeyId(alice, 0);
deletePath(alice);
//################# POST-INITIALIZATION #################
setUpOmemoManager(alice);
//Keys
assertNotNull("IdentityKeyPair must not be null after initialization",
store.loadOmemoIdentityKeyPair(alice));
assertNotSame("LastPreKeyId must not be 0 after initialization.",
0, store.loadLastPreKeyId(alice));
assertNotSame("currentSignedPreKeyId must not be 0 after initialization.",
0, store.loadCurrentSignedPreKeyId(alice));
assertNotNull("The last PreKey must not be null.",
store.loadOmemoPreKey(alice, store.loadLastPreKeyId(alice) - 1));
assertNotNull("The current signedPreKey must not be null.",
store.loadOmemoSignedPreKey(alice, store.loadCurrentSignedPreKeyId(alice)));
}
@Override
public void after() {
alice.shutdown();
bob.shutdown();
}
}

View file

@ -0,0 +1 @@
../../../../../../../../smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/package-info.java

View file

@ -0,0 +1 @@
int_test_omemo_store/

View file

@ -0,0 +1,17 @@
apply plugin: 'application'
description = """\
Smack integration tests for OMEMO using libsignal."""
mainClassName = 'org.igniterealtime.smack.inttest.smack_omemo_signal.SmackOmemoSignalIntegrationTestFramework'
applicationDefaultJvmArgs = ["-enableassertions"]
dependencies {
compile project(':smack-integration-test')
compile project(':smack-omemo-signal')
}
run {
// Pass all system properties down to the "application" run
systemProperties System.getProperties()
}

View file

@ -0,0 +1,52 @@
/**
*
* Copyright 2017 Paul Schaub
*
* 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
*/
package org.igniterealtime.smack.inttest.smack_omemo_signal;
import org.igniterealtime.smack.inttest.SmackIntegrationTestFramework;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.signal.SignalOmemoService;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
public class SmackOmemoSignalIntegrationTestFramework {
public static void main(String[] args) throws InvalidKeyException, NoSuchPaddingException,
InvalidAlgorithmParameterException, IllegalBlockSizeException,
BadPaddingException, NoSuchAlgorithmException, NoSuchProviderException, SmackException,
InterruptedException, CorruptedOmemoKeyException, KeyManagementException, IOException, XMPPException {
SignalOmemoService.acknowledgeLicense();
SignalOmemoService.setup();
final String[] smackOmemoPackages = new String[] { "org.jivesoftware.smackx.omemo" };
SmackIntegrationTestFramework.main(smackOmemoPackages);
}
}

View file

@ -0,0 +1,25 @@
/**
*
* Copyright 2017 Paul Schaub
*
* 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
*/
/**
* The Smack Integration Test Framework for smack-omemo-signal.
*/
package org.igniterealtime.smack.inttest.smack_omemo_signal;

677
smack-omemo-signal/LICENSE Normal file
View file

@ -0,0 +1,677 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{one line to give the program's name and a brief idea of what it does.}
Copyright (C) {year} {name of author}
This program 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, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
{project} Copyright (C) {year} {fullname}
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View file

@ -0,0 +1,16 @@
// Although the osgi plugin is already applied by the root project's
// subprojects closure, we need to re-apply it here so that the
// manifest is a OsgiManifest. Possible caused by
// evaluationDependsOnChildren in the root project.
apply plugin: 'osgi'
apply plugin: 'checkstyle'
apply plugin: 'maven'
dependencies {
compile project(":smack-im")
compile project(":smack-extensions")
compile project(":smack-omemo")
compile 'org.whispersystems:signal-protocol-java:2.4.0'
testCompile project(path: ":smack-core", configuration: "testRuntime")
}

View file

@ -0,0 +1,58 @@
/**
*
* Copyright 2017 Paul Schaub
*
* This file is part of smack-omemo-signal.
*
* smack-omemo-signal is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jivesoftware.smackx.omemo.signal;
import org.jivesoftware.smackx.omemo.FileBasedOmemoStore;
import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.SessionCipher;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.state.PreKeyBundle;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import java.io.File;
/**
* Implementation of a FileBasedOmemoStore for the smack-omemo-signal module.
*
* @author Paul Schaub
*/
@SuppressWarnings("unused")
public class SignalFileBasedOmemoStore
extends FileBasedOmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> {
public SignalFileBasedOmemoStore() {
super();
}
public SignalFileBasedOmemoStore(File base) {
super(base);
}
@Override
public OmemoKeyUtil<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> keyUtil() {
return new SignalOmemoKeyUtil();
}
}

View file

@ -0,0 +1,226 @@
/**
*
* Copyright 2017 Paul Schaub
*
* This file is part of smack-omemo-signal.
*
* smack-omemo-signal is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jivesoftware.smackx.omemo.signal;
import org.jivesoftware.smackx.omemo.OmemoFingerprint;
import org.jivesoftware.smackx.omemo.OmemoManager;
import org.jivesoftware.smackx.omemo.OmemoStore;
import org.jivesoftware.smackx.omemo.element.OmemoBundleVAxolotlElement;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.internal.OmemoSession;
import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.SessionCipher;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.state.PreKeyBundle;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.KeyHelper;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
/**
* Concrete implementation of the KeyUtil for an implementation using the Signal library.
*
* @author Paul Schaub
*/
public class SignalOmemoKeyUtil extends OmemoKeyUtil<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord,
SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> {
@Override
public IdentityKeyPair generateOmemoIdentityKeyPair() {
return KeyHelper.generateIdentityKeyPair();
}
@Override
public HashMap<Integer, PreKeyRecord> generateOmemoPreKeys(int currentPreKeyId, int count) {
List<PreKeyRecord> preKeyRecords = KeyHelper.generatePreKeys(currentPreKeyId, count);
HashMap<Integer, PreKeyRecord> hashMap = new HashMap<>();
for (PreKeyRecord p : preKeyRecords) {
hashMap.put(p.getId(), p);
}
return hashMap;
}
@Override
public SignedPreKeyRecord generateOmemoSignedPreKey(IdentityKeyPair identityKeyPair, int currentPreKeyId) throws CorruptedOmemoKeyException {
try {
return KeyHelper.generateSignedPreKey(identityKeyPair, currentPreKeyId);
} catch (InvalidKeyException e) {
throw new CorruptedOmemoKeyException(e.getMessage());
}
}
@Override
public SignedPreKeyRecord signedPreKeyFromBytes(byte[] data) throws IOException {
return new SignedPreKeyRecord(data);
}
@Override
public byte[] signedPreKeyToBytes(SignedPreKeyRecord signedPreKeyRecord) {
return signedPreKeyRecord.serialize();
}
@Override
public OmemoSession<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher>
createOmemoSession(OmemoManager omemoManager, OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> omemoStore,
OmemoDevice contact, IdentityKey identityKey) {
return new SignalOmemoSession(omemoManager, omemoStore, contact, identityKey);
}
@Override
public OmemoSession<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher>
createOmemoSession(OmemoManager omemoManager, OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> omemoStore, OmemoDevice from) {
return new SignalOmemoSession(omemoManager, omemoStore, from);
}
@Override
public SessionRecord rawSessionFromBytes(byte[] data) throws IOException {
return new SessionRecord(data);
}
@Override
public byte[] rawSessionToBytes(SessionRecord session) {
return session.serialize();
}
@Override
public IdentityKeyPair identityKeyPairFromBytes(byte[] data) throws CorruptedOmemoKeyException {
try {
return new IdentityKeyPair(data);
} catch (InvalidKeyException e) {
throw new CorruptedOmemoKeyException(e.getMessage());
}
}
@Override
public IdentityKey identityKeyFromBytes(byte[] data) throws CorruptedOmemoKeyException {
try {
return new IdentityKey(data, 0);
} catch (InvalidKeyException e) {
throw new CorruptedOmemoKeyException(e.getMessage());
}
}
@Override
public ECPublicKey ellipticCurvePublicKeyFromBytes(byte[] data) throws CorruptedOmemoKeyException {
try {
return Curve.decodePoint(data, 0);
} catch (InvalidKeyException e) {
throw new CorruptedOmemoKeyException(e.getMessage());
}
}
@Override
public byte[] preKeyToBytes(PreKeyRecord preKeyRecord) {
return preKeyRecord.serialize();
}
@Override
public PreKeyRecord preKeyFromBytes(byte[] bytes) throws IOException {
return new PreKeyRecord(bytes);
}
@Override
public PreKeyBundle bundleFromOmemoBundle(OmemoBundleVAxolotlElement bundle, OmemoDevice contact, int preKeyId) throws CorruptedOmemoKeyException {
return new PreKeyBundle(0,
contact.getDeviceId(),
preKeyId,
BUNDLE.preKeyPublic(bundle, preKeyId),
BUNDLE.signedPreKeyId(bundle),
BUNDLE.signedPreKeyPublic(bundle),
BUNDLE.signedPreKeySignature(bundle),
BUNDLE.identityKey(bundle));
}
@Override
public byte[] signedPreKeySignatureFromKey(SignedPreKeyRecord signedPreKey) {
return signedPreKey.getSignature();
}
@Override
public int signedPreKeyIdFromKey(SignedPreKeyRecord signedPreKey) {
return signedPreKey.getId();
}
@Override
public byte[] identityKeyPairToBytes(IdentityKeyPair identityKeyPair) {
return identityKeyPair.serialize();
}
@Override
public IdentityKey identityKeyFromPair(IdentityKeyPair identityKeyPair) {
return identityKeyPair.getPublicKey();
}
@Override
public byte[] identityKeyForBundle(IdentityKey identityKey) {
return identityKey.getPublicKey().serialize();
}
@Override
public byte[] identityKeyToBytes(IdentityKey identityKey) {
return identityKey.serialize();
}
@Override
public byte[] preKeyPublicKeyForBundle(ECPublicKey preKey) {
return preKey.serialize();
}
@Override
public byte[] preKeyForBundle(PreKeyRecord preKeyRecord) {
return preKeyRecord.getKeyPair().getPublicKey().serialize();
}
@Override
public byte[] signedPreKeyPublicForBundle(SignedPreKeyRecord signedPreKey) {
return signedPreKey.getKeyPair().getPublicKey().serialize();
}
@Override
public OmemoFingerprint getFingerprint(IdentityKey identityKey) {
String fp = identityKey.getFingerprint();
//Cut "(byte)0x" prefixes, remove spaces and commas, cut first two digits.
fp = fp.replace("(byte)0x", "").replace(",", "").replace(" ", "").substring(2);
return new OmemoFingerprint(fp);
}
@Override
public SignalProtocolAddress omemoDeviceAsAddress(OmemoDevice contact) {
return new SignalProtocolAddress(contact.getJid().asBareJid().toString(), contact.getDeviceId());
}
@Override
public OmemoDevice addressAsOmemoDevice(SignalProtocolAddress address) throws XmppStringprepException {
return new OmemoDevice(JidCreate.bareFrom(address.getName()), address.getDeviceId());
}
}

View file

@ -0,0 +1,111 @@
/**
*
* Copyright 2017 Paul Schaub
*
* This file is part of smack-omemo-signal.
*
* smack-omemo-signal is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jivesoftware.smackx.omemo.signal;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smackx.omemo.OmemoManager;
import org.jivesoftware.smackx.omemo.OmemoService;
import org.jivesoftware.smackx.omemo.OmemoStore;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.SessionBuilder;
import org.whispersystems.libsignal.SessionCipher;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.UntrustedIdentityException;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.state.PreKeyBundle;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.logging.Level;
/**
* Concrete implementation of the OmemoService using the Signal library.
*
* @author Paul Schaub
*/
@SuppressWarnings("unused")
public final class SignalOmemoService extends OmemoService<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> {
private static SignalOmemoService INSTANCE;
private static boolean LICENSE_ACKNOWLEDGED = false;
public static void setup() throws InvalidKeyException, XMPPErrorException, NoSuchPaddingException, InvalidAlgorithmParameterException, UnsupportedEncodingException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, NoSuchProviderException, SmackException, InterruptedException, CorruptedOmemoKeyException {
if (!LICENSE_ACKNOWLEDGED) {
throw new IllegalStateException("smack-omemo-signal is licensed under the terms of the GPLv3. Please be aware that you " +
"can only use this library within the terms of the GPLv3. See for example " +
"https://www.gnu.org/licenses/quick-guide-gplv3 for more details. Please call " +
"SignalOmemoService.acknowledgeLicense() prior to the setup() method in order to prevent " +
"this exception.");
}
if (INSTANCE == null) {
INSTANCE = new SignalOmemoService();
}
setInstance(INSTANCE);
}
@Override
public OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> createDefaultOmemoStoreBackend() {
return new SignalFileBasedOmemoStore();
}
private SignalOmemoService()
throws SmackException, InterruptedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException,
NoSuchPaddingException, InvalidAlgorithmParameterException, UnsupportedEncodingException,
IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, NoSuchProviderException,
java.security.InvalidKeyException {
super();
}
public static void acknowledgeLicense() {
LICENSE_ACKNOWLEDGED = true;
}
@Override
protected void processBundle(OmemoManager omemoManager, PreKeyBundle preKeyBundle, OmemoDevice contact) throws CorruptedOmemoKeyException {
SignalOmemoStoreConnector connector = new SignalOmemoStoreConnector(omemoManager, getOmemoStoreBackend());
SessionBuilder builder = new SessionBuilder(connector, connector, connector, connector,
getOmemoStoreBackend().keyUtil().omemoDeviceAsAddress(contact));
try {
builder.process(preKeyBundle);
LOGGER.log(Level.INFO, "Session built with " + contact);
getOmemoStoreBackend().getOmemoSessionOf(omemoManager, contact); //method puts session in session map.
} catch (org.whispersystems.libsignal.InvalidKeyException e) {
throw new CorruptedOmemoKeyException(e.getMessage());
} catch (UntrustedIdentityException e) {
// This should never happen.
throw new AssertionError(e);
}
}
}

View file

@ -0,0 +1,141 @@
/**
*
* Copyright 2017 Paul Schaub
*
* This file is part of smack-omemo-signal.
*
* smack-omemo-signal is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jivesoftware.smackx.omemo.signal;
import org.jivesoftware.smackx.omemo.OmemoManager;
import org.jivesoftware.smackx.omemo.OmemoStore;
import org.jivesoftware.smackx.omemo.element.OmemoElement;
import org.jivesoftware.smackx.omemo.exceptions.NoRawSessionException;
import org.jivesoftware.smackx.omemo.internal.CiphertextTuple;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.internal.OmemoSession;
import org.whispersystems.libsignal.DuplicateMessageException;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.InvalidVersionException;
import org.whispersystems.libsignal.LegacyMessageException;
import org.whispersystems.libsignal.NoSessionException;
import org.whispersystems.libsignal.SessionCipher;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.UntrustedIdentityException;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.protocol.CiphertextMessage;
import org.whispersystems.libsignal.protocol.PreKeySignalMessage;
import org.whispersystems.libsignal.protocol.SignalMessage;
import org.whispersystems.libsignal.state.PreKeyBundle;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Concrete implementation of the OmemoSession using the Signal library.
*
* @author Paul Schaub
*/
public class SignalOmemoSession extends OmemoSession<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> {
private static final Logger LOGGER = Logger.getLogger(SignalOmemoSession.class.getName());
/**
* Constructor used when the remote user initialized the session using a PreKeyOmemoMessage.
*
* @param omemoManager omemoManager
* @param omemoStore omemoStoreConnector that can be used to get information from
* @param remoteContact omemoDevice of the remote contact
* @param identityKey identityKey of the remote contact
*/
SignalOmemoSession(OmemoManager omemoManager, OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> omemoStore,
OmemoDevice remoteContact, IdentityKey identityKey) {
super(omemoManager, omemoStore, remoteContact, identityKey);
}
/**
* Constructor used when we initiate a new Session with the remote user.
*
* @param omemoManager omemoManager
* @param omemoStore omemoStore used to get information from
* @param remoteContact omemoDevice of the remote contact
*/
SignalOmemoSession(OmemoManager omemoManager,
OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> omemoStore,
OmemoDevice remoteContact) {
super(omemoManager, omemoStore, remoteContact);
}
@Override
public SessionCipher createCipher(OmemoDevice contact) {
SignalOmemoStoreConnector connector = new SignalOmemoStoreConnector(omemoManager, omemoStore);
return new SessionCipher(connector, connector, connector, connector,
omemoStore.keyUtil().omemoDeviceAsAddress(contact));
}
@Override
public CiphertextTuple encryptMessageKey(byte[] messageKey) {
CiphertextMessage ciphertextMessage;
ciphertextMessage = cipher.encrypt(messageKey);
int type = (ciphertextMessage.getType() == CiphertextMessage.PREKEY_TYPE ?
OmemoElement.TYPE_OMEMO_PREKEY_MESSAGE : OmemoElement.TYPE_OMEMO_MESSAGE);
return new CiphertextTuple(ciphertextMessage.serialize(), type);
}
@Override
public byte[] decryptMessageKey(byte[] encryptedKey) throws NoRawSessionException {
byte[] decryptedKey = null;
try {
try {
PreKeySignalMessage message = new PreKeySignalMessage(encryptedKey);
if (!message.getPreKeyId().isPresent()) {
LOGGER.log(Level.WARNING, "PreKeySignalMessage did not contain a PreKeyId");
return null;
}
LOGGER.log(Level.INFO, "PreKeySignalMessage received, new session ID: " + message.getSignedPreKeyId() + "/" + message.getPreKeyId().get());
IdentityKey messageIdentityKey = message.getIdentityKey();
if (this.identityKey != null && !this.identityKey.equals(messageIdentityKey)) {
LOGGER.log(Level.INFO, "Had session with fingerprint " + getFingerprint() +
", received message with different fingerprint " + omemoStore.keyUtil().getFingerprint(messageIdentityKey) +
". Silently drop the message.");
} else {
this.identityKey = messageIdentityKey;
decryptedKey = cipher.decrypt(message);
this.preKeyId = message.getPreKeyId().get();
}
} catch (InvalidMessageException | InvalidVersionException e) {
SignalMessage message = new SignalMessage(encryptedKey);
decryptedKey = cipher.decrypt(message);
} catch (InvalidKeyIdException e) {
throw new NoRawSessionException(e);
}
catch (InvalidKeyException | UntrustedIdentityException e) {
LOGGER.log(Level.SEVERE, "Error decrypting message header, " + e.getClass().getName() + ": " + e.getMessage());
}
} catch (InvalidMessageException | NoSessionException e) {
throw new NoRawSessionException(e);
} catch (LegacyMessageException | DuplicateMessageException e) {
LOGGER.log(Level.SEVERE, "Error decrypting message header, " + e.getClass().getName() + ": " + e.getMessage());
}
return decryptedKey;
}
}

View file

@ -0,0 +1,50 @@
/**
*
* Copyright 2017 Paul Schaub
*
* This file is part of smack-omemo-signal.
*
* smack-omemo-signal is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jivesoftware.smackx.omemo.signal;
import org.jivesoftware.smackx.omemo.OmemoStore;
import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.SessionCipher;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.state.PreKeyBundle;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
/**
* Implementation of the OmemoStore using the Signal library.
*
* @author Paul Schaub
*/
@SuppressWarnings("unused")
public abstract class SignalOmemoStore
extends OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> {
private final SignalOmemoKeyUtil signalKeyUtil = new SignalOmemoKeyUtil();
@Override
public OmemoKeyUtil<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> keyUtil() {
return signalKeyUtil;
}
}

View file

@ -0,0 +1,228 @@
/**
*
* Copyright 2017 Paul Schaub
*
* This file is part of smack-omemo-signal.
*
* smack-omemo-signal is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jivesoftware.smackx.omemo.signal;
import org.jivesoftware.smackx.omemo.OmemoManager;
import org.jivesoftware.smackx.omemo.OmemoStore;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.SessionCipher;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.state.IdentityKeyStore;
import org.whispersystems.libsignal.state.PreKeyBundle;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.PreKeyStore;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Class that adapts libsignal-protocol-java's Store classes to the OmemoStore class.
*
* @author Paul Schaub
*/
public class SignalOmemoStoreConnector
implements IdentityKeyStore, SessionStore, PreKeyStore, SignedPreKeyStore {
private static final Logger LOGGER = Logger.getLogger(SignalOmemoStoreConnector.class.getName());
private final OmemoManager omemoManager;
private final OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher>
omemoStore;
public SignalOmemoStoreConnector(OmemoManager omemoManager, OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> store) {
this.omemoManager = omemoManager;
this.omemoStore = store;
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
try {
return omemoStore.loadOmemoIdentityKeyPair(omemoManager);
} catch (CorruptedOmemoKeyException e) {
LOGGER.log(Level.SEVERE, "getIdentityKeyPair has failed: " + e, e);
return null;
}
}
/**
* We don't use this.
* @return dummy
*/
@Override
public int getLocalRegistrationId() {
return 0;
}
@Override
public void saveIdentity(SignalProtocolAddress signalProtocolAddress, IdentityKey identityKey) {
try {
omemoStore.storeOmemoIdentityKey(omemoManager, omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress), identityKey);
} catch (XmppStringprepException e) {
throw new AssertionError(e);
}
}
@Override
public boolean isTrustedIdentity(SignalProtocolAddress signalProtocolAddress, IdentityKey identityKey) {
//Disable internal trust management. Instead we use OmemoStore.isTrustedOmemoIdentity() before encrypting for a
//recipient.
return true;
}
@Override
public PreKeyRecord loadPreKey(int i) throws InvalidKeyIdException {
PreKeyRecord pr = omemoStore.loadOmemoPreKey(omemoManager, i);
if (pr == null) {
throw new InvalidKeyIdException("No PreKey with Id " + i + " found!");
}
return pr;
}
@Override
public void storePreKey(int i, PreKeyRecord preKeyRecord) {
omemoStore.storeOmemoPreKey(omemoManager, i, preKeyRecord);
}
@Override
public boolean containsPreKey(int i) {
try {
return (loadPreKey(i) != null);
} catch (InvalidKeyIdException e) {
LOGGER.log(Level.WARNING, "containsPreKey has failed: " + e.getMessage());
return false;
}
}
@Override
public void removePreKey(int i) {
omemoStore.removeOmemoPreKey(omemoManager, i);
}
@Override
public SessionRecord loadSession(SignalProtocolAddress signalProtocolAddress) {
try {
SessionRecord s = omemoStore.loadRawSession(omemoManager, omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress));
return (s != null ? s : new SessionRecord());
} catch (XmppStringprepException e) {
throw new AssertionError(e);
}
}
@Override
public List<Integer> getSubDeviceSessions(String s) {
HashMap<Integer, SessionRecord> contactsSessions;
try {
contactsSessions = omemoStore.loadAllRawSessionsOf(omemoManager, JidCreate.bareFrom(s));
} catch (XmppStringprepException e) {
throw new AssertionError(e);
}
if (contactsSessions != null) {
return new ArrayList<>(contactsSessions.keySet());
}
return new ArrayList<>();
}
@Override
public void storeSession(SignalProtocolAddress signalProtocolAddress, SessionRecord sessionRecord) {
try {
omemoStore.storeRawSession(omemoManager, omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress), sessionRecord);
} catch (XmppStringprepException e) {
throw new AssertionError(e);
}
}
@Override
public boolean containsSession(SignalProtocolAddress signalProtocolAddress) {
try {
return omemoStore.containsRawSession(omemoManager, omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress));
} catch (XmppStringprepException e) {
throw new AssertionError(e);
}
}
@Override
public void deleteSession(SignalProtocolAddress signalProtocolAddress) {
try {
omemoStore.removeRawSession(omemoManager, omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress));
} catch (XmppStringprepException e) {
throw new AssertionError(e);
}
}
@Override
public void deleteAllSessions(String s) {
try {
omemoStore.removeAllRawSessionsOf(omemoManager, JidCreate.bareFrom(s));
} catch (XmppStringprepException e) {
throw new AssertionError(e);
}
}
@Override
public SignedPreKeyRecord loadSignedPreKey(int i) throws InvalidKeyIdException {
SignedPreKeyRecord spkr = omemoStore.loadOmemoSignedPreKey(omemoManager, i);
if (spkr == null) {
throw new InvalidKeyIdException("No SignedPreKey with Id " + i + " found!");
}
return spkr;
}
@Override
public List<SignedPreKeyRecord> loadSignedPreKeys() {
HashMap<Integer, SignedPreKeyRecord> signedPreKeyRecordHashMap = omemoStore.loadOmemoSignedPreKeys(omemoManager);
List<SignedPreKeyRecord> signedPreKeyRecordList = new ArrayList<>();
signedPreKeyRecordList.addAll(signedPreKeyRecordHashMap.values());
return signedPreKeyRecordList;
}
@Override
public void storeSignedPreKey(int i, SignedPreKeyRecord signedPreKeyRecord) {
omemoStore.storeOmemoSignedPreKey(omemoManager, i, signedPreKeyRecord);
}
@Override
public boolean containsSignedPreKey(int i) {
try {
return loadSignedPreKey(i) != null;
} catch (InvalidKeyIdException e) {
LOGGER.log(Level.WARNING, "containsSignedPreKey has failed: " + e.getMessage());
return false;
}
}
@Override
public void removeSignedPreKey(int i) {
omemoStore.removeOmemoSignedPreKey(omemoManager, i);
}
}

View file

@ -0,0 +1,26 @@
/**
*
* Copyright 2017 Paul Schaub
*
* This file is part of smack-omemo-signal.
*
* smack-omemo-signal is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
/**
* Concrete implementation of OMEMO for smack using the signal-protocol-java library.
* @author Paul Schaub
*/
package org.jivesoftware.smackx.omemo.signal;

View file

@ -0,0 +1,105 @@
/**
*
* Copyright 2017 Paul Schaub
*
* This file is part of smack-omemo-signal.
*
* smack-omemo-signal is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jivesoftware.smack.omemo;
import org.jivesoftware.smack.DummyConnection;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.test.util.TestUtils;
import org.jivesoftware.smackx.omemo.OmemoManager;
import org.jivesoftware.smackx.omemo.element.OmemoElement;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.provider.OmemoVAxolotlProvider;
import org.jivesoftware.smackx.omemo.signal.SignalOmemoService;
import org.junit.Test;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
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.assertTrue;
/**
* Test OmemoManager functionality.
*/
public class OmemoManagerTest extends SmackTestSuite {
@Test
public void instantiationTest() throws CorruptedOmemoKeyException, NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException, InterruptedException, XMPPException.XMPPErrorException, NoSuchPaddingException, BadPaddingException, InvalidAlgorithmParameterException, NoSuchProviderException, IllegalBlockSizeException, SmackException {
SignalOmemoService.acknowledgeLicense();
SignalOmemoService.setup();
DummyConnection dummy = new DummyConnection();
DummyConnection silly = new DummyConnection();
OmemoManager a = OmemoManager.getInstanceFor(dummy, 123);
OmemoManager b = OmemoManager.getInstanceFor(dummy, 234);
OmemoManager c = OmemoManager.getInstanceFor(silly, 123);
OmemoManager d = OmemoManager.getInstanceFor(dummy, 123);
assertNotNull(a);
assertNotNull(b);
assertNotNull(c);
assertNotNull(d);
assertEquals(123, a.getDeviceId());
assertEquals(234, b.getDeviceId());
assertFalse(a == b);
assertFalse(a == c);
assertFalse(b == c);
assertTrue(a == d);
}
@Test
public void randomDeviceIdTest() {
int a = OmemoManager.randomDeviceId();
int b = OmemoManager.randomDeviceId();
assertNotSame(a, b); // This is highly unlikely
assertTrue(a > 0);
assertTrue(b > 0);
}
@Test
public void stanzaRecognitionTest() throws Exception {
String omemoXML = "<encrypted xmlns='eu.siacs.conversations.axolotl'><header sid='1009'><key rid='1337'>MwohBfRqBm2atj3fT0/KUDg59Cnvfpgoe/PLNIu1xgSXujEZEAAYACIwKh6TTC7VBQZcCcKnQlO+6s1GQ9DIRKH4JU7XrJ+JJnkPUwJ4VLSeOEQD7HmFbhQPTLZO0u/qlng=</key><iv>sN0amy4e2NBrlb4G/OjNIQ==</iv></header><payload>4xVUAeg4M0Mhk+5n3YG1x12Dw/cYTc0Z</payload></encrypted>";
OmemoElement omemoElement = new OmemoVAxolotlProvider().parse(TestUtils.getParser(omemoXML));
Message m = new Message();
m.addExtension(omemoElement);
Message n = new Message();
assertTrue(OmemoManager.stanzaContainsOmemoElement(m));
assertFalse(OmemoManager.stanzaContainsOmemoElement(n));
}
}

View file

@ -0,0 +1,88 @@
/**
*
* Copyright 2017 Paul Schaub
*
* This file is part of smack-omemo-signal.
*
* smack-omemo-signal is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jivesoftware.smack.omemo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder;
import org.junit.Test;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.SessionCipher;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.state.PreKeyBundle;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Security;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.CIPHERMODE;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.PROVIDER;
import static org.junit.Assert.assertArrayEquals;
/**
* Test the OmemoMessageBuilder.
*/
public class OmemoMessageBuilderTest extends SmackTestSuite {
@Test
public void setTextTest() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, UnsupportedEncodingException, IllegalBlockSizeException, BadPaddingException, NoSuchProviderException, InvalidKeyException {
Security.addProvider(new BouncyCastleProvider());
String message = "Hello World!";
byte[] key = OmemoMessageBuilder.generateKey();
byte[] iv = OmemoMessageBuilder.generateIv();
SecretKey secretKey = new SecretKeySpec(key, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
OmemoMessageBuilder<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher>
mb = new OmemoMessageBuilder<>(null, null, key, iv);
mb.setMessage(message);
byte[] expected = cipher.doFinal(message.getBytes(StringUtils.UTF8));
byte[] messageKey = new byte[16];
System.arraycopy(mb.getMessageKey(),0, messageKey, 0, 16);
byte[] messagePlusTag = new byte[mb.getCiphertextMessage().length + 16];
System.arraycopy(mb.getCiphertextMessage(),0,messagePlusTag,0,mb.getCiphertextMessage().length);
System.arraycopy(mb.getMessageKey(), 16, messagePlusTag, mb.getCiphertextMessage().length, 16);
assertArrayEquals(key, messageKey);
assertArrayEquals(expected, messagePlusTag);
}
}

View file

@ -0,0 +1,217 @@
/**
*
* Copyright 2017 Paul Schaub
*
* This file is part of smack-omemo-signal.
*
* smack-omemo-signal is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jivesoftware.smack.omemo;
import org.jivesoftware.smackx.omemo.FileBasedOmemoStore;
import org.jivesoftware.smackx.omemo.OmemoConfiguration;
import org.jivesoftware.smackx.omemo.OmemoManager;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.internal.CachedDeviceList;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.signal.SignalFileBasedOmemoStore;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import java.io.File;
import java.util.Date;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNotNull;
import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertArrayEquals;
import static org.powermock.api.mockito.PowerMockito.when;
/**
* Test the file-based signalOmemoStore.
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest({OmemoManager.class})
public class SignalFileBasedOmemoStoreTest {
private static File storePath;
private static SignalFileBasedOmemoStore omemoStore;
private static OmemoManager omemoManager;
private void deleteStore() {
FileBasedOmemoStore.deleteDirectory(storePath);
}
@BeforeClass
public static void setup() throws XmppStringprepException {
String userHome = System.getProperty("user.home");
if (userHome != null) {
File f = new File(userHome);
storePath = new File(f, ".config/smack-integration-test/store");
} else {
storePath = new File("int_test_omemo_store");
}
OmemoConfiguration.setFileBasedOmemoStoreDefaultPath(storePath);
omemoStore = new SignalFileBasedOmemoStore();
OmemoDevice device = new OmemoDevice(JidCreate.bareFrom("storeTest@server.tld"), 55155);
omemoManager = PowerMockito.mock(OmemoManager.class);
when(omemoManager.getDeviceId()).thenReturn(device.getDeviceId());
when(omemoManager.getOwnJid()).thenReturn(device.getJid());
when(omemoManager.getOwnDevice()).thenReturn(device);
}
@Before
public void before() {
deleteStore();
}
@After
public void after() {
deleteStore();
}
@Test
public void isFreshInstallationTest() {
assertTrue(omemoStore.isFreshInstallation(omemoManager));
omemoStore.storeOmemoIdentityKeyPair(omemoManager, omemoStore.generateOmemoIdentityKeyPair());
assertFalse(omemoStore.isFreshInstallation(omemoManager));
omemoStore.purgeOwnDeviceKeys(omemoManager);
assertTrue(omemoStore.isFreshInstallation(omemoManager));
}
@Test
public void defaultDeviceIdTest() throws XmppStringprepException {
assertEquals(-1, omemoStore.getDefaultDeviceId(omemoManager.getOwnJid()));
omemoStore.setDefaultDeviceId(omemoManager.getOwnJid(), 55);
assertEquals(55, omemoStore.getDefaultDeviceId(omemoManager.getOwnJid()));
assertEquals(-1, omemoStore.getDefaultDeviceId(JidCreate.bareFrom("randomGuy@server.tld")));
}
@Test
public void cachedDeviceListTest() throws XmppStringprepException {
OmemoDevice bob = new OmemoDevice(JidCreate.bareFrom("bob@builder.tv"), 666);
OmemoDevice craig = new OmemoDevice(JidCreate.bareFrom("craig@southpark.tv"), 3333333);
CachedDeviceList bobsList = new CachedDeviceList();
assertEquals(0, bobsList.getAllDevices().size());
bobsList.getActiveDevices().add(bob.getDeviceId());
bobsList.getActiveDevices().add(777);
bobsList.getInactiveDevices().add(888);
CachedDeviceList craigsList = new CachedDeviceList();
craigsList.addDevice(craig.getDeviceId());
assertEquals(3, bobsList.getAllDevices().size());
assertEquals(2, bobsList.getActiveDevices().size());
assertTrue(bobsList.getInactiveDevices().contains(888));
assertTrue(bobsList.getActiveDevices().contains(777));
assertTrue(bobsList.getAllDevices().contains(888));
assertEquals(0, craigsList.getInactiveDevices().size());
assertEquals(1, craigsList.getActiveDevices().size());
assertEquals(1, craigsList.getAllDevices().size());
assertEquals(craig.getDeviceId(), craigsList.getActiveDevices().iterator().next().intValue());
}
@Test
public void omemoIdentityKeyPairTest() throws CorruptedOmemoKeyException {
assertNull(omemoStore.loadOmemoIdentityKeyPair(omemoManager));
omemoStore.storeOmemoIdentityKeyPair(omemoManager, omemoStore.generateOmemoIdentityKeyPair());
IdentityKeyPair ikp = omemoStore.loadOmemoIdentityKeyPair(omemoManager);
assertNotNull(ikp);
assertTrue(omemoStore.keyUtil().getFingerprint(ikp.getPublicKey()).equals(omemoStore.getFingerprint(omemoManager)));
}
@Test
public void signedPreKeyTest() throws CorruptedOmemoKeyException {
assertEquals(0, omemoStore.loadOmemoSignedPreKeys(omemoManager).size());
IdentityKeyPair ikp = omemoStore.generateOmemoIdentityKeyPair();
SignedPreKeyRecord spk = omemoStore.generateOmemoSignedPreKey(ikp, 14);
omemoStore.storeOmemoSignedPreKey(omemoManager, 14, spk);
assertEquals(1, omemoStore.loadOmemoSignedPreKeys(omemoManager).size());
assertNotNull(omemoStore.loadOmemoSignedPreKey(omemoManager, 14));
assertArrayEquals(spk.serialize(), omemoStore.loadOmemoSignedPreKey(omemoManager, 14).serialize());
assertNull(omemoStore.loadOmemoSignedPreKey(omemoManager, 13));
assertEquals(0, omemoStore.loadCurrentSignedPreKeyId(omemoManager));
omemoStore.storeCurrentSignedPreKeyId(omemoManager, 15);
assertEquals(15, omemoStore.loadCurrentSignedPreKeyId(omemoManager));
omemoStore.removeOmemoSignedPreKey(omemoManager, 14);
assertNull(omemoStore.loadOmemoSignedPreKey(omemoManager, 14));
assertNull(omemoStore.getDateOfLastSignedPreKeyRenewal(omemoManager));
Date now = new Date();
omemoStore.setDateOfLastSignedPreKeyRenewal(omemoManager, now);
assertEquals(now, omemoStore.getDateOfLastSignedPreKeyRenewal(omemoManager));
}
@Test
public void preKeyTest() {
assertEquals(0, omemoStore.loadOmemoPreKeys(omemoManager).size());
assertNull(omemoStore.loadOmemoPreKey(omemoManager, 12));
omemoStore.storeOmemoPreKeys(omemoManager,
omemoStore.generateOmemoPreKeys(1, 20));
assertNotNull(omemoStore.loadOmemoPreKey(omemoManager, 12));
assertEquals(20, omemoStore.loadOmemoPreKeys(omemoManager).size());
omemoStore.removeOmemoPreKey(omemoManager, 12);
assertNull(omemoStore.loadOmemoPreKey(omemoManager, 12));
assertEquals(19, omemoStore.loadOmemoPreKeys(omemoManager).size());
assertEquals(0, omemoStore.loadLastPreKeyId(omemoManager));
omemoStore.storeLastPreKeyId(omemoManager, 35);
assertEquals(35, omemoStore.loadLastPreKeyId(omemoManager));
}
@Test
public void trustingTest() throws XmppStringprepException, CorruptedOmemoKeyException {
OmemoDevice bob = new OmemoDevice(JidCreate.bareFrom("bob@builder.tv"), 555);
IdentityKey bobsKey = omemoStore.generateOmemoIdentityKeyPair().getPublicKey();
assertFalse(omemoStore.isDecidedOmemoIdentity(omemoManager, bob, bobsKey));
assertFalse(omemoStore.isTrustedOmemoIdentity(omemoManager, bob, bobsKey));
omemoStore.trustOmemoIdentity(omemoManager, bob, bobsKey);
assertTrue(omemoStore.isDecidedOmemoIdentity(omemoManager, bob, omemoStore.keyUtil().getFingerprint(bobsKey)));
assertTrue(omemoStore.isTrustedOmemoIdentity(omemoManager, bob, omemoStore.keyUtil().getFingerprint(bobsKey)));
assertNull(omemoStore.loadOmemoIdentityKey(omemoManager, bob));
omemoStore.storeOmemoIdentityKey(omemoManager, bob, bobsKey);
assertNotNull(omemoStore.loadOmemoIdentityKey(omemoManager, bob));
IdentityKey bobsOtherKey = omemoStore.generateOmemoIdentityKeyPair().getPublicKey();
assertFalse(omemoStore.isTrustedOmemoIdentity(omemoManager, bob, bobsOtherKey));
assertFalse(omemoStore.isDecidedOmemoIdentity(omemoManager, bob, bobsOtherKey));
omemoStore.distrustOmemoIdentity(omemoManager, bob, omemoStore.keyUtil().getFingerprint(bobsKey));
assertTrue(omemoStore.isDecidedOmemoIdentity(omemoManager, bob, bobsKey));
assertFalse(omemoStore.isTrustedOmemoIdentity(omemoManager, bob, bobsKey));
assertNull(omemoStore.getDateOfLastReceivedMessage(omemoManager, bob));
Date now = new Date();
omemoStore.setDateOfLastReceivedMessage(omemoManager, bob, now);
assertEquals(now, omemoStore.getDateOfLastReceivedMessage(omemoManager, bob));
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,45 @@
/**
*
* Copyright 2017 Paul Schaub
*
* This file is part of smack-omemo-signal.
*
* smack-omemo-signal is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jivesoftware.smack.omemo;
import org.jivesoftware.smackx.omemo.signal.SignalOmemoStoreConnector;
import org.junit.Test;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertTrue;
/**
* Test some functionality of the SignalOmemoStoreConnector.
*/
public class SignalOmemoStoreConnectorTest {
@Test
public void getLocalRegistrationIdTest() {
SignalOmemoStoreConnector connector = new SignalOmemoStoreConnector(null, null);
assertEquals("RegistrationId must always be 0.", 0, connector.getLocalRegistrationId());
}
@Test
public void isTrustedIdentityTest() {
SignalOmemoStoreConnector connector = new SignalOmemoStoreConnector(null, null);
assertTrue("All identities must be trusted by default.", connector.isTrustedIdentity(null, null));
}
}

14
smack-omemo/build.gradle Normal file
View file

@ -0,0 +1,14 @@
// Although the osgi plugin is already applied by the root project's
// subprojects closure, we need to re-apply it here so that the
// manifest is a OsgiManifest. Possible caused by
// evaluationDependsOnChildren in the root project.
apply plugin: 'osgi'
dependencies {
compile project(":smack-im")
compile project(":smack-extensions")
compile project(":smack-experimental")
compile "org.bouncycastle:bcprov-jdk15on:1.57"
testCompile project(path: ":smack-core", configuration: "testRuntime")
}

View file

@ -0,0 +1,939 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.internal.CachedDeviceList;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jxmpp.jid.BareJid;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Like a rocket!
*
* @author Paul Schaub
*/
public abstract class FileBasedOmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>
extends OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> {
private static final Logger LOGGER = Logger.getLogger(FileBasedOmemoStore.class.getSimpleName());
private final FileHierarchy hierarchy;
public FileBasedOmemoStore() {
this(OmemoConfiguration.getFileBasedOmemoStoreDefaultPath());
}
public FileBasedOmemoStore(File basePath) {
super();
if (basePath == null) {
throw new IllegalStateException("No FileBasedOmemoStoreDefaultPath set in OmemoConfiguration.");
}
this.hierarchy = new FileHierarchy(basePath);
}
@Override
public boolean isFreshInstallation(OmemoManager omemoManager) {
File userDirectory = hierarchy.getUserDeviceDirectory(omemoManager);
File[] files = userDirectory.listFiles();
return files == null || files.length == 0;
}
@Override
public int getDefaultDeviceId(BareJid user) {
try {
return readInt(hierarchy.getDefaultDeviceIdPath(user));
} catch (IOException e) {
return -1;
}
}
@Override
public void setDefaultDeviceId(BareJid user, int defaultDeviceId) {
File defaultDeviceIdPath = hierarchy.getDefaultDeviceIdPath(user);
if (defaultDeviceIdPath == null) {
LOGGER.log(Level.SEVERE, "defaultDeviceIdPath is null!");
}
try {
writeInt(defaultDeviceIdPath, defaultDeviceId);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not write defaultDeviceId: " + e, e);
}
}
@Override
public int loadLastPreKeyId(OmemoManager omemoManager) {
try {
int l = readInt(hierarchy.getLastPreKeyIdPath(omemoManager));
return l == -1 ? 0 : l;
} catch (IOException e) {
return 0;
}
}
@Override
public void storeLastPreKeyId(OmemoManager omemoManager, int currentPreKeyId) {
try {
writeInt(hierarchy.getLastPreKeyIdPath(omemoManager), currentPreKeyId);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not write lastPreKeyId: " + e, e);
}
}
@Override
public T_IdKeyPair loadOmemoIdentityKeyPair(OmemoManager omemoManager) throws CorruptedOmemoKeyException {
File identityKeyPairPath = hierarchy.getIdentityKeyPairPath(omemoManager);
try {
byte[] bytes = readBytes(identityKeyPairPath);
return bytes != null ? keyUtil().identityKeyPairFromBytes(bytes) : null;
} catch (IOException e) {
return null;
}
}
@Override
public void storeOmemoIdentityKeyPair(OmemoManager omemoManager, T_IdKeyPair identityKeyPair) {
File identityKeyPairPath = hierarchy.getIdentityKeyPairPath(omemoManager);
try {
writeBytes(identityKeyPairPath, keyUtil().identityKeyPairToBytes(identityKeyPair));
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not write omemoIdentityKeyPair: " + e, e);
}
}
@Override
public T_IdKey loadOmemoIdentityKey(OmemoManager omemoManager, OmemoDevice device) throws CorruptedOmemoKeyException {
File identityKeyPath = hierarchy.getContactsIdentityKeyPath(omemoManager, device);
try {
byte[] bytes = readBytes(identityKeyPath);
return bytes != null ? keyUtil().identityKeyFromBytes(bytes) : null;
} catch (IOException e) {
return null;
}
}
@Override
public void storeOmemoIdentityKey(OmemoManager omemoManager, OmemoDevice device, T_IdKey t_idKey) {
File identityKeyPath = hierarchy.getContactsIdentityKeyPath(omemoManager, device);
try {
writeBytes(identityKeyPath, keyUtil().identityKeyToBytes(t_idKey));
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not write omemoIdentityKey of " + device + ": " + e, e);
}
}
@Override
public boolean isTrustedOmemoIdentity(OmemoManager omemoManager, OmemoDevice device, OmemoFingerprint fingerprint) {
File trustPath = hierarchy.getContactsTrustPath(omemoManager, device);
try {
String depositedFingerprint = new String(readBytes(trustPath), StringUtils.UTF8);
return depositedFingerprint.length() > 2
&& depositedFingerprint.charAt(0) == '1'
&& new OmemoFingerprint(depositedFingerprint.substring(2)).equals(fingerprint);
} catch (IOException e) {
return false;
}
}
@Override
public boolean isDecidedOmemoIdentity(OmemoManager omemoManager, OmemoDevice device, OmemoFingerprint fingerprint) {
File trustPath = hierarchy.getContactsTrustPath(omemoManager, device);
try {
String depositedFingerprint = new String(readBytes(trustPath), StringUtils.UTF8);
return depositedFingerprint.length() > 2
&& (depositedFingerprint.charAt(0) == '1' || depositedFingerprint.charAt(0) == '2')
&& new OmemoFingerprint(depositedFingerprint.substring(2)).equals(fingerprint);
} catch (IOException e) {
return false;
}
}
@Override
public void trustOmemoIdentity(OmemoManager omemoManager, OmemoDevice device, OmemoFingerprint fingerprint) {
File trustPath = hierarchy.getContactsTrustPath(omemoManager, device);
try {
writeBytes(trustPath, ("1 " + fingerprint.toString()).getBytes(StringUtils.UTF8));
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not trust " + device + ": " + e, e);
}
}
@Override
public void distrustOmemoIdentity(OmemoManager omemoManager, OmemoDevice device, OmemoFingerprint fingerprint) {
File trustPath = hierarchy.getContactsTrustPath(omemoManager, device);
try {
writeBytes(trustPath, ("2 " + fingerprint.toString()).getBytes(StringUtils.UTF8));
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not distrust " + device + ": " + e, e);
}
}
@Override
public void setDateOfLastReceivedMessage(OmemoManager omemoManager, OmemoDevice from, Date date) {
File lastMessageReceived = hierarchy.getLastMessageReceivedDatePath(omemoManager, from);
try {
writeLong(lastMessageReceived, date.getTime());
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not write date of last received message from " + from + ": " + e, e);
}
}
@Override
public Date getDateOfLastReceivedMessage(OmemoManager omemoManager, OmemoDevice from) {
File lastMessageReceived = hierarchy.getLastMessageReceivedDatePath(omemoManager, from);
try {
long date = readLong(lastMessageReceived);
return date != -1 ? new Date(date) : null;
} catch (IOException e) {
return null;
}
}
@Override
public void setDateOfLastSignedPreKeyRenewal(OmemoManager omemoManager, Date date) {
File lastSignedPreKeyRenewal = hierarchy.getLastSignedPreKeyRenewal(omemoManager);
try {
writeLong(lastSignedPreKeyRenewal, date.getTime());
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not write date of last singedPreKey renewal for "
+ omemoManager.getOwnDevice() + ": " + e, e);
}
}
@Override
public Date getDateOfLastSignedPreKeyRenewal(OmemoManager omemoManager) {
File lastSignedPreKeyRenewal = hierarchy.getLastSignedPreKeyRenewal(omemoManager);
try {
long date = readLong(lastSignedPreKeyRenewal);
return date != -1 ? new Date(date) : null;
} catch (IOException e) {
return null;
}
}
@Override
public T_PreKey loadOmemoPreKey(OmemoManager omemoManager, int preKeyId) {
File preKeyPath = hierarchy.getPreKeyPath(omemoManager, preKeyId);
try {
byte[] bytes = readBytes(preKeyPath);
return bytes != null ? keyUtil().preKeyFromBytes(bytes) : null;
} catch (IOException e) {
return null;
}
}
@Override
public void storeOmemoPreKey(OmemoManager omemoManager, int preKeyId, T_PreKey t_preKey) {
File preKeyPath = hierarchy.getPreKeyPath(omemoManager, preKeyId);
try {
writeBytes(preKeyPath, keyUtil().preKeyToBytes(t_preKey));
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not write preKey with id " + preKeyId + ": " + e, e);
}
}
@Override
public void removeOmemoPreKey(OmemoManager omemoManager, int preKeyId) {
File preKeyPath = hierarchy.getPreKeyPath(omemoManager, preKeyId);
preKeyPath.delete();
}
@Override
public int loadCurrentSignedPreKeyId(OmemoManager omemoManager) {
File currentSignedPreKeyIdPath = hierarchy.getCurrentSignedPreKeyIdPath(omemoManager);
try {
int i = readInt(currentSignedPreKeyIdPath);
return i == -1 ? 0 : i;
} catch (IOException e) {
return 0;
}
}
@Override
public void storeCurrentSignedPreKeyId(OmemoManager omemoManager, int currentSignedPreKeyId) {
File currentSignedPreKeyIdPath = hierarchy.getCurrentSignedPreKeyIdPath(omemoManager);
try {
writeInt(currentSignedPreKeyIdPath, currentSignedPreKeyId);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not write currentSignedPreKeyId "
+ currentSignedPreKeyId + " for " + omemoManager.getOwnDevice() + ": "
+ e, e);
}
}
@Override
public HashMap<Integer, T_PreKey> loadOmemoPreKeys(OmemoManager omemoManager) {
File preKeyDirectory = hierarchy.getPreKeysDirectory(omemoManager);
HashMap<Integer, T_PreKey> preKeys = new HashMap<>();
if (preKeyDirectory == null) {
return preKeys;
}
File[] keys = preKeyDirectory.listFiles();
for (File f : keys != null ? keys : new File[0]) {
try {
byte[] bytes = readBytes(f);
if (bytes == null) {
continue;
}
T_PreKey p = keyUtil().preKeyFromBytes(bytes);
preKeys.put(Integer.parseInt(f.getName()), p);
} catch (IOException e) {
//Do nothing
}
}
return preKeys;
}
@Override
public T_SigPreKey loadOmemoSignedPreKey(OmemoManager omemoManager, int signedPreKeyId) {
File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(omemoManager), Integer.toString(signedPreKeyId));
try {
byte[] bytes = readBytes(signedPreKeyPath);
return bytes != null ? keyUtil().signedPreKeyFromBytes(bytes) : null;
} catch (IOException e) {
return null;
}
}
@Override
public HashMap<Integer, T_SigPreKey> loadOmemoSignedPreKeys(OmemoManager omemoManager) {
File signedPreKeysDirectory = hierarchy.getSignedPreKeysDirectory(omemoManager);
HashMap<Integer, T_SigPreKey> signedPreKeys = new HashMap<>();
if (signedPreKeysDirectory == null) {
return signedPreKeys;
}
File[] keys = signedPreKeysDirectory.listFiles();
for (File f : keys != null ? keys : new File[0]) {
try {
byte[] bytes = readBytes(f);
if (bytes == null) {
continue;
}
T_SigPreKey p = keyUtil().signedPreKeyFromBytes(bytes);
signedPreKeys.put(Integer.parseInt(f.getName()), p);
} catch (IOException e) {
//Do nothing
}
}
return signedPreKeys;
}
@Override
public void storeOmemoSignedPreKey(OmemoManager omemoManager, int signedPreKeyId, T_SigPreKey signedPreKey) {
File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(omemoManager), Integer.toString(signedPreKeyId));
try {
writeBytes(signedPreKeyPath, keyUtil().signedPreKeyToBytes(signedPreKey));
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not write signedPreKey " + signedPreKey
+ " for " + omemoManager.getOwnDevice() + ": " + e, e);
}
}
@Override
public void removeOmemoSignedPreKey(OmemoManager omemoManager, int signedPreKeyId) {
File signedPreKeyPath = new File(hierarchy.getSignedPreKeysDirectory(omemoManager), Integer.toString(signedPreKeyId));
signedPreKeyPath.delete();
}
@Override
public T_Sess loadRawSession(OmemoManager omemoManager, OmemoDevice device) {
File sessionPath = hierarchy.getContactsSessionPath(omemoManager, device);
try {
byte[] bytes = readBytes(sessionPath);
return bytes != null ? keyUtil().rawSessionFromBytes(bytes) : null;
} catch (IOException e) {
return null;
}
}
@Override
public HashMap<Integer, T_Sess> loadAllRawSessionsOf(OmemoManager omemoManager, BareJid contact) {
File contactsDirectory = hierarchy.getContactsDir(omemoManager, contact);
HashMap<Integer, T_Sess> sessions = new HashMap<>();
String[] devices = contactsDirectory.list();
for (String deviceId : devices != null ? devices : new String[0]) {
int id;
try {
id = Integer.parseInt(deviceId);
} catch (NumberFormatException e) {
continue;
}
OmemoDevice device = new OmemoDevice(contact, id);
File session = hierarchy.getContactsSessionPath(omemoManager, device);
try {
byte[] bytes = readBytes(session);
if (bytes == null) {
continue;
}
T_Sess s = keyUtil().rawSessionFromBytes(bytes);
sessions.put(id, s);
} catch (IOException e) {
//Do nothing
}
}
return sessions;
}
@Override
public void storeRawSession(OmemoManager omemoManager, OmemoDevice device, T_Sess session) {
File sessionPath = hierarchy.getContactsSessionPath(omemoManager, device);
try {
writeBytes(sessionPath, keyUtil().rawSessionToBytes(session));
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not write session between our device " + omemoManager.getOwnDevice()
+ " and their device " + device + ": " + e.getMessage());
}
}
@Override
public void removeRawSession(OmemoManager omemoManager, OmemoDevice device) {
File sessionPath = hierarchy.getContactsSessionPath(omemoManager, device);
sessionPath.delete();
}
@Override
public void removeAllRawSessionsOf(OmemoManager omemoManager, BareJid contact) {
File contactsDirectory = hierarchy.getContactsDir(omemoManager, contact);
String[] devices = contactsDirectory.list();
for (String deviceId : devices != null ? devices : new String[0]) {
int id;
try {
id = Integer.parseInt(deviceId);
} catch (NumberFormatException e) {
continue;
}
OmemoDevice device = new OmemoDevice(contact, id);
File session = hierarchy.getContactsSessionPath(omemoManager, device);
session.delete();
}
}
@Override
public boolean containsRawSession(OmemoManager omemoManager, OmemoDevice device) {
File session = hierarchy.getContactsSessionPath(omemoManager, device);
return session.exists();
}
@Override
public CachedDeviceList loadCachedDeviceList(OmemoManager omemoManager, BareJid contact) {
CachedDeviceList cachedDeviceList = new CachedDeviceList();
if (contact == null) {
return null;
}
//active
File activeDevicesPath = hierarchy.getContactsActiveDevicesPath(omemoManager, contact);
try {
cachedDeviceList.getActiveDevices().addAll(readIntegers(activeDevicesPath));
} catch (IOException e) {
// Don't worry...
}
//inactive
File inactiveDevicesPath = hierarchy.getContactsInactiveDevicesPath(omemoManager, contact);
try {
cachedDeviceList.getInactiveDevices().addAll(readIntegers(inactiveDevicesPath));
} catch (IOException e) {
//It's ok :)
}
return cachedDeviceList;
}
@Override
public void storeCachedDeviceList(OmemoManager omemoManager, BareJid contact, CachedDeviceList deviceList) {
if (contact == null) {
return;
}
File activeDevices = hierarchy.getContactsActiveDevicesPath(omemoManager, contact);
try {
writeIntegers(activeDevices, deviceList.getActiveDevices());
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not write active devices of deviceList of "
+ contact + ": " + e.getMessage());
}
File inactiveDevices = hierarchy.getContactsInactiveDevicesPath(omemoManager, contact);
try {
writeIntegers(inactiveDevices, deviceList.getInactiveDevices());
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not write inactive devices of deviceList of "
+ contact + ": " + e.getMessage());
}
}
@Override
public void purgeOwnDeviceKeys(OmemoManager omemoManager) {
File deviceDirectory = hierarchy.getUserDeviceDirectory(omemoManager);
deleteDirectory(deviceDirectory);
}
private void writeInt(File target, int i) throws IOException {
if (target == null) {
throw new IOException("Could not write integer to null-path.");
}
FileHierarchy.createFile(target);
IOException io = null;
DataOutputStream out = null;
try {
out = new DataOutputStream(new FileOutputStream(target));
out.writeInt(i);
} catch (IOException e) {
io = e;
} finally {
if (out != null) {
out.close();
}
}
if (io != null) {
throw io;
}
}
private int readInt(File target) throws IOException {
if (target == null) {
throw new IOException("Could not read integer from null-path.");
}
IOException io = null;
int i = -1;
DataInputStream in = null;
try {
in = new DataInputStream(new FileInputStream(target));
i = in.readInt();
} catch (IOException e) {
io = e;
} finally {
if (in != null) {
in.close();
}
}
if (io != null) {
throw io;
}
return i;
}
private void writeLong(File target, long i) throws IOException {
if (target == null) {
throw new IOException("Could not write long to null-path.");
}
FileHierarchy.createFile(target);
IOException io = null;
DataOutputStream out = null;
try {
out = new DataOutputStream(new FileOutputStream(target));
out.writeLong(i);
} catch (IOException e) {
io = e;
} finally {
if (out != null) {
out.close();
}
}
if (io != null) {
throw io;
}
}
private long readLong(File target) throws IOException {
if (target == null) {
throw new IOException("Could not read long from null-path.");
}
IOException io = null;
long l = -1;
DataInputStream in = null;
try {
in = new DataInputStream(new FileInputStream(target));
l = in.readLong();
} catch (IOException e) {
io = e;
} finally {
if (in != null) {
in.close();
}
}
if (io != null) {
throw io;
}
return l;
}
private void writeBytes(File target, byte[] bytes) throws IOException {
if (target == null) {
throw new IOException("Could not write bytes to null-path.");
}
//Create file
FileHierarchy.createFile(target);
IOException io = null;
DataOutputStream out = null;
try {
out = new DataOutputStream(new FileOutputStream(target));
out.write(bytes);
} catch (IOException e) {
io = e;
} finally {
if (out != null) {
out.close();
}
}
if (io != null) {
throw io;
}
}
private byte[] readBytes(File target) throws IOException {
if (target == null) {
throw new IOException("Could not read bytes from null-path.");
}
byte[] b = null;
IOException io = null;
DataInputStream in = null;
try {
in = new DataInputStream(new FileInputStream(target));
b = new byte[in.available()];
in.read(b);
} catch (IOException e) {
io = e;
} finally {
if (in != null) {
in.close();
}
}
if (io != null) {
throw io;
}
return b;
}
private void writeIntegers(File target, Set<Integer> integers) throws IOException {
if (target == null) {
throw new IOException("Could not write integers to null-path.");
}
IOException io = null;
DataOutputStream out = null;
try {
out = new DataOutputStream(new FileOutputStream(target));
for (int i : integers) {
out.writeInt(i);
}
} catch (IOException e) {
io = e;
} finally {
if (out != null) {
out.close();
}
}
if (io != null) {
throw io;
}
}
private Set<Integer> readIntegers(File target) throws IOException {
if (target == null) {
throw new IOException("Could not write integers to null-path.");
}
HashSet<Integer> integers = new HashSet<>();
IOException io = null;
DataInputStream in = null;
try {
in = new DataInputStream(new FileInputStream(target));
try {
while (true) {
integers.add(in.readInt());
}
} catch (EOFException e) {
//Reached end of the list.
}
} catch (IOException e) {
io = e;
} finally {
if (in != null) {
in.close();
}
}
if (io != null) {
throw io;
}
return integers;
}
public static void deleteDirectory(File root) {
File[] currList;
Stack<File> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
if (stack.lastElement().isDirectory()) {
currList = stack.lastElement().listFiles();
if (currList != null && currList.length > 0) {
for (File curr : currList) {
stack.push(curr);
}
} else {
stack.pop().delete();
}
} else {
stack.pop().delete();
}
}
}
/**
* This class represents the directory structure of the FileBasedOmemoStoreV2.
* The directory looks as follows:
*
* OMEMO_Store/
* 'romeo@montague.lit'/ //Our bareJid
* ...
* 'juliet@capulet.lit'/ //Our other bareJid
* defaultDeviceId
* '13371234'/ //deviceId
* identityKeyPair //Our identityKeyPair
* lastPreKeyId //Id of the last preKey we generated
* currentSignedPreKeyId //Id of the currently used signedPreKey
* lastSignedPreKeyRenewal //Date of when the signedPreKey was last renewed.
* preKeys/ //Our preKeys
* '1'
* '2'
* ...
* signedPreKeys/ //Our signedPreKeys
* '1'
* '2'
* ...
* contacts/
* 'romeo@capulet.lit'/ //Juliets contact Romeo
* activeDevice //List of Romeos active devices
* inactiveDevices //List of his inactive devices
* 'deviceId'/ //Romeos deviceId
* identityKey //Romeos identityKey
* session //Our session with romeo
* trust //Records about the trust in romeos device
* (lastReceivedMessageDate) //Only, for our own other devices:
* //date of the last received message
*
*/
public static class FileHierarchy {
static final String STORE = "OMEMO_Store";
static final String CONTACTS = "contacts";
static final String DEFAULT_DEVICE_ID = "defaultDeviceId";
static final String IDENTITY_KEY = "identityKey";
static final String IDENTITY_KEY_PAIR = "identityKeyPair";
static final String PRE_KEYS = "preKeys";
static final String LAST_MESSAGE_RECEVIED_DATE = "lastMessageReceivedDate";
static final String LAST_PRE_KEY_ID = "lastPreKeyId";
static final String SIGNED_PRE_KEYS = "signedPreKeys";
static final String CURRENT_SIGNED_PRE_KEY_ID = "currentSignedPreKeyId";
static final String LAST_SIGNED_PRE_KEY_RENEWAL = "lastSignedPreKeyRenewal";
static final String SESSION = "session";
static final String DEVICE_LIST_ACTIVE = "activeDevices";
static final String DEVICE_LIST_INAVTIVE = "inactiveDevices";
static final String TRUST = "trust";
File basePath;
FileHierarchy(File basePath) {
this.basePath = basePath;
basePath.mkdirs();
}
File getStoreDirectory() {
return createDirectory(basePath, STORE);
}
File getUserDirectory(BareJid bareJid) {
return createDirectory(getStoreDirectory(), bareJid.toString());
}
File getUserDeviceDirectory(OmemoManager omemoManager) {
return createDirectory(getUserDirectory(omemoManager.getOwnJid()),
Integer.toString(omemoManager.getDeviceId()));
}
File getContactsDir(OmemoManager omemoManager) {
return createDirectory(getUserDeviceDirectory(omemoManager), CONTACTS);
}
File getContactsDir(OmemoManager omemoManager, BareJid contact) {
return createDirectory(getContactsDir(omemoManager), contact.toString());
}
File getContactsDir(OmemoManager omemoManager, OmemoDevice omemoDevice) {
return createDirectory(getContactsDir(omemoManager, omemoDevice.getJid()),
Integer.toString(omemoDevice.getDeviceId()));
}
File getIdentityKeyPairPath(OmemoManager omemoManager) {
return new File(getUserDeviceDirectory(omemoManager), IDENTITY_KEY_PAIR);
}
File getPreKeysDirectory(OmemoManager omemoManager) {
return createDirectory(getUserDeviceDirectory(omemoManager), PRE_KEYS);
}
File getPreKeyPath(OmemoManager omemoManager, int preKeyId) {
return new File(getPreKeysDirectory(omemoManager), Integer.toString(preKeyId));
}
File getLastMessageReceivedDatePath(OmemoManager omemoManager, OmemoDevice device) {
return new File(getContactsDir(omemoManager, device), LAST_MESSAGE_RECEVIED_DATE);
}
File getLastPreKeyIdPath(OmemoManager omemoManager) {
return new File(getUserDeviceDirectory(omemoManager), LAST_PRE_KEY_ID);
}
File getSignedPreKeysDirectory(OmemoManager omemoManager) {
return createDirectory(getUserDeviceDirectory(omemoManager), SIGNED_PRE_KEYS);
}
File getCurrentSignedPreKeyIdPath(OmemoManager omemoManager) {
return new File(getUserDeviceDirectory(omemoManager), CURRENT_SIGNED_PRE_KEY_ID);
}
File getLastSignedPreKeyRenewal(OmemoManager omemoManager) {
return new File(getUserDeviceDirectory(omemoManager), LAST_SIGNED_PRE_KEY_RENEWAL);
}
File getDefaultDeviceIdPath(BareJid bareJid) {
return new File(getUserDirectory(bareJid), DEFAULT_DEVICE_ID);
}
File getContactsIdentityKeyPath(OmemoManager omemoManager, OmemoDevice omemoDevice) {
return new File(getContactsDir(omemoManager, omemoDevice), IDENTITY_KEY);
}
File getContactsSessionPath(OmemoManager omemoManager, OmemoDevice omemoDevice) {
return new File(getContactsDir(omemoManager, omemoDevice), SESSION);
}
File getContactsActiveDevicesPath(OmemoManager omemoManager, BareJid contact) {
return new File(getContactsDir(omemoManager, contact), DEVICE_LIST_ACTIVE);
}
File getContactsInactiveDevicesPath(OmemoManager omemoManager, BareJid contact) {
return new File(getContactsDir(omemoManager, contact), DEVICE_LIST_INAVTIVE);
}
File getContactsTrustPath(OmemoManager omemoManager, OmemoDevice omemoDevice) {
return new File(getContactsDir(omemoManager, omemoDevice), TRUST);
}
private static File createFile(File f) throws IOException {
File p = f.getParentFile();
createDirectory(p);
f.createNewFile();
return f;
}
private static File createFile(File dir, String filename) throws IOException {
return createFile(new File(dir, filename));
}
private static File createDirectory(File dir, String subdir) {
File f = new File(dir, subdir);
return createDirectory(f);
}
private static File createDirectory(File f) {
if (f.exists() && f.isDirectory()) {
return f;
}
f.mkdirs();
return f;
}
}
}

View file

@ -0,0 +1,166 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import java.io.File;
/**
* Contains OMEMO related configuration options.
*
* @author Paul Schaub
*/
public final class OmemoConfiguration {
/**
* Ignore own other stale devices that we did not receive a message from for a period of time.
* Ignoring means do not encrypt messages for them. This helps to mitigate stale devices that threaten
* forward secrecy by never advancing ratchets.
*/
private static boolean IGNORE_STALE_DEVICES = true;
private static int IGNORE_STALE_DEVICE_AFTER_HOURS = 24 * 7; //One week
/**
* Delete stale devices from the device list after a period of time.
*/
private static boolean DELETE_STALE_DEVICES = true;
private static int DELETE_STALE_DEVICE_AFTER_HOURS = 24 * 7 * 4; //4 weeks
/**
* Upload a new signed prekey in intervals. This improves forward secrecy. Old keys are kept for some more time and
* then deleted.
*/
private static boolean RENEW_OLD_SIGNED_PREKEYS = false;
private static int RENEW_OLD_SIGNED_PREKEYS_AFTER_HOURS = 24 * 7; //One week
private static int MAX_NUMBER_OF_STORED_SIGNED_PREKEYS = 4;
/**
* Add a plaintext body hint about omemo encryption to the message.
*/
private static boolean ADD_OMEMO_HINT_BODY = true;
/**
* Add Explicit Message Encryption hint (XEP-0380) to the message.
*/
private static boolean ADD_EME_ENCRYPTION_HINT = true;
/**
* Add MAM storage hint to allow the server to store messages that do not contain a body.
*/
private static boolean ADD_MAM_STORAGE_HINT = true;
private static File FILE_BASED_OMEMO_STORE_DEFAULT_PATH = null;
public static void setIgnoreStaleDevices(boolean ignore) {
IGNORE_STALE_DEVICES = ignore;
}
public static boolean getIgnoreStaleDevices() {
return IGNORE_STALE_DEVICES;
}
public static void setIgnoreStaleDevicesAfterHours(int hours) {
if (hours <= 0) {
throw new IllegalArgumentException("Hours must be greater than 0.");
}
IGNORE_STALE_DEVICE_AFTER_HOURS = hours;
}
public static int getIgnoreStaleDevicesAfterHours() {
return IGNORE_STALE_DEVICE_AFTER_HOURS;
}
public static void setDeleteStaleDevices(boolean delete) {
DELETE_STALE_DEVICES = delete;
}
public static boolean getDeleteStaleDevices() {
return DELETE_STALE_DEVICES;
}
public static void setDeleteStaleDevicesAfterHours(int hours) {
if (hours <= 0) {
throw new IllegalArgumentException("Hours must be greater than 0.");
}
DELETE_STALE_DEVICE_AFTER_HOURS = hours;
}
public static int getDeleteStaleDevicesAfterHours() {
return DELETE_STALE_DEVICE_AFTER_HOURS;
}
public static void setRenewOldSignedPreKeys(boolean renew) {
RENEW_OLD_SIGNED_PREKEYS = renew;
}
public static boolean getRenewOldSignedPreKeys() {
return RENEW_OLD_SIGNED_PREKEYS;
}
public static void setRenewOldSignedPreKeysAfterHours(int hours) {
if (hours <= 0) {
throw new IllegalArgumentException("Hours must be greater than 0.");
}
RENEW_OLD_SIGNED_PREKEYS_AFTER_HOURS = hours;
}
public static int getRenewOldSignedPreKeysAfterHours() {
return RENEW_OLD_SIGNED_PREKEYS_AFTER_HOURS;
}
public static void setMaxNumberOfStoredSignedPreKeys(int number) {
if (number <= 0) {
throw new IllegalArgumentException("Number must be greater than 0.");
}
MAX_NUMBER_OF_STORED_SIGNED_PREKEYS = number;
}
public static int getMaxNumberOfStoredSignedPreKeys() {
return MAX_NUMBER_OF_STORED_SIGNED_PREKEYS;
}
public static void setAddOmemoHintBody(boolean addHint) {
ADD_OMEMO_HINT_BODY = addHint;
}
public static boolean getAddOmemoHintBody() {
return ADD_OMEMO_HINT_BODY;
}
public static void setAddEmeEncryptionHint(boolean addHint) {
ADD_EME_ENCRYPTION_HINT = addHint;
}
public static boolean getAddEmeEncryptionHint() {
return ADD_EME_ENCRYPTION_HINT;
}
public static void setAddMAMStorageProcessingHint(boolean addStorageHint) {
ADD_MAM_STORAGE_HINT = addStorageHint;
}
public static boolean getAddMAMStorageProcessingHint() {
return ADD_MAM_STORAGE_HINT;
}
public static void setFileBasedOmemoStoreDefaultPath(File path) {
FILE_BASED_OMEMO_STORE_DEFAULT_PATH = path;
}
public static File getFileBasedOmemoStoreDefaultPath() {
return FILE_BASED_OMEMO_STORE_DEFAULT_PATH;
}
}

View file

@ -0,0 +1,64 @@
/**
*
* 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;
public class OmemoFingerprint implements CharSequence {
private final String fingerprintString;
public OmemoFingerprint(String fingerprintString) {
this.fingerprintString = fingerprintString;
}
@Override
public int length() {
return fingerprintString.length();
}
@Override
public char charAt(int index) {
return fingerprintString.charAt(index);
}
@Override
public CharSequence subSequence(int start, int end) {
return fingerprintString.subSequence(start, end);
}
public CharSequence subSequence(int start) {
return fingerprintString.subSequence(start, fingerprintString.length() - 1);
}
@Override
public String toString() {
return fingerprintString;
}
@Override
public boolean equals(Object other) {
if (!(other instanceof OmemoFingerprint)) {
return false;
}
OmemoFingerprint otherFingerprint = (OmemoFingerprint) other;
return this.toString().trim().equals(otherFingerprint.toString().trim());
}
@Override
public int hashCode() {
return toString().hashCode();
}
}

View file

@ -0,0 +1,38 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import org.jivesoftware.smack.initializer.UrlInitializer;
/**
* Initializer class that registers omemo providers.
*
* @author Paul Schaub
*/
@SuppressWarnings("unused")
public class OmemoInitializer extends UrlInitializer {
@Override
protected String getProvidersUrl() {
return "classpath:org.jivesoftware.smackx.omemo/omemo.providers";
}
@Override
protected String getConfigUrl() {
return "classpath:org.jivesoftware.smackx.omemo/omemo.xml";
}
}

View file

@ -0,0 +1,887 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.Manager;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smackx.carbons.CarbonManager;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.eme.element.ExplicitMessageEncryptionElement;
import org.jivesoftware.smackx.hints.element.StoreHint;
import org.jivesoftware.smackx.mam.MamManager;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.jivesoftware.smackx.muc.MultiUserChatManager;
import org.jivesoftware.smackx.muc.RoomInfo;
import org.jivesoftware.smackx.omemo.element.OmemoDeviceListVAxolotlElement;
import org.jivesoftware.smackx.omemo.element.OmemoElement;
import org.jivesoftware.smackx.omemo.element.OmemoVAxolotlElement;
import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
import org.jivesoftware.smackx.omemo.exceptions.NoOmemoSupportException;
import org.jivesoftware.smackx.omemo.exceptions.NoRawSessionException;
import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException;
import org.jivesoftware.smackx.omemo.internal.CachedDeviceList;
import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag;
import org.jivesoftware.smackx.omemo.internal.ClearTextMessage;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation;
import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener;
import org.jivesoftware.smackx.omemo.listener.OmemoMucMessageListener;
import org.jivesoftware.smackx.pep.PEPListener;
import org.jivesoftware.smackx.pep.PEPManager;
import org.jivesoftware.smackx.pubsub.EventElement;
import org.jivesoftware.smackx.pubsub.ItemsExtension;
import org.jivesoftware.smackx.pubsub.PayloadItem;
import org.jivesoftware.smackx.pubsub.PubSubException;
import org.jivesoftware.smackx.pubsub.packet.PubSub;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.DomainBareJid;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.EntityFullJid;
import org.jxmpp.jid.FullJid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.BODY_OMEMO_HINT;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.PEP_NODE_DEVICE_LIST_NOTIFY;
/**
* Manager that allows sending messages encrypted with OMEMO.
* This class also provides some methods useful for a client that implements OMEMO.
*
* @author Paul Schaub
*/
public final class OmemoManager extends Manager {
private static final Logger LOGGER = Logger.getLogger(OmemoManager.class.getName());
private static final WeakHashMap<XMPPConnection, WeakHashMap<Integer,OmemoManager>> INSTANCES = new WeakHashMap<>();
private final OmemoService<?, ?, ?, ?, ?, ?, ?, ?, ?> service;
private final HashSet<OmemoMessageListener> omemoMessageListeners = new HashSet<>();
private final HashSet<OmemoMucMessageListener> omemoMucMessageListeners = new HashSet<>();
private OmemoService<?,?,?,?,?,?,?,?,?>.OmemoStanzaListener omemoStanzaListener;
private OmemoService<?,?,?,?,?,?,?,?,?>.OmemoCarbonCopyListener omemoCarbonCopyListener;
private int deviceId;
/**
* Private constructor to prevent multiple instances on a single connection (which probably would be bad!).
*
* @param connection connection
*/
private OmemoManager(XMPPConnection connection, int deviceId) {
super(connection);
setConnectionListener();
this.deviceId = deviceId;
service = OmemoService.getInstance();
}
/**
* Get an instance of the OmemoManager for the given connection and deviceId.
*
* @param connection Connection
* @param deviceId deviceId of the Manager. If the deviceId is null, a random id will be generated.
* @return an OmemoManager
*/
public synchronized static OmemoManager getInstanceFor(XMPPConnection connection, Integer deviceId) {
WeakHashMap<Integer,OmemoManager> managersOfConnection = INSTANCES.get(connection);
if (managersOfConnection == null) {
managersOfConnection = new WeakHashMap<>();
INSTANCES.put(connection, managersOfConnection);
}
if (deviceId == null || deviceId < 1) {
deviceId = randomDeviceId();
}
OmemoManager manager = managersOfConnection.get(deviceId);
if (manager == null) {
manager = new OmemoManager(connection, deviceId);
managersOfConnection.put(deviceId, manager);
}
return manager;
}
/**
* Get an instance of the OmemoManager for the given connection.
* This method creates the OmemoManager for the stored defaultDeviceId of the connections user.
* If there is no such id is stored, it uses a fresh deviceId and sets that as defaultDeviceId instead.
*
* @param connection connection
* @return OmemoManager
*/
public synchronized static OmemoManager getInstanceFor(XMPPConnection connection) {
BareJid user;
if (connection.getUser() != null) {
user = connection.getUser().asBareJid();
} else {
//This might be dangerous
try {
user = JidCreate.bareFrom(((AbstractXMPPConnection) connection).getConfiguration().getUsername());
} catch (XmppStringprepException e) {
throw new AssertionError("Username is not a valid Jid. " +
"Use OmemoManager.gerInstanceFor(Connection, deviceId) instead.");
}
}
int defaulDeviceId = OmemoService.getInstance().getOmemoStoreBackend().getDefaultDeviceId(user);
if (defaulDeviceId < 1) {
defaulDeviceId = randomDeviceId();
OmemoService.getInstance().getOmemoStoreBackend().setDefaultDeviceId(user, defaulDeviceId);
}
return getInstanceFor(connection, defaulDeviceId);
}
/**
* Initializes the OmemoManager. This method is called automatically once the client logs into the server successfully.
*
* @throws CorruptedOmemoKeyException
* @throws InterruptedException
* @throws SmackException.NoResponseException
* @throws SmackException.NotConnectedException
* @throws XMPPException.XMPPErrorException
* @throws SmackException.NotLoggedInException
* @throws PubSubException.NotALeafNodeException
*/
public void initialize() throws CorruptedOmemoKeyException, InterruptedException, SmackException.NoResponseException,
SmackException.NotConnectedException, XMPPException.XMPPErrorException, SmackException.NotLoggedInException,
PubSubException.NotALeafNodeException {
getOmemoService().initialize(this);
}
/**
* OMEMO encrypt a cleartext message for a single recipient.
*
* @param to recipients barejid
* @param message text to encrypt
* @return encrypted message
* @throws CryptoFailedException when something crypto related fails
* @throws UndecidedOmemoIdentityException When there are undecided devices
* @throws NoSuchAlgorithmException
* @throws InterruptedException
* @throws CannotEstablishOmemoSessionException when we could not create session withs all of the recipients devices.
* @throws SmackException.NotConnectedException
* @throws SmackException.NoResponseException
*/
public Message encrypt(BareJid to, String message) throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, InterruptedException, CannotEstablishOmemoSessionException, SmackException.NotConnectedException, SmackException.NoResponseException {
Message m = new Message();
m.setBody(message);
OmemoVAxolotlElement encrypted = getOmemoService().processSendingMessage(this, to, m);
return finishMessage(encrypted);
}
/**
* OMEMO encrypt a cleartext message for multiple recipients.
*
* @param recipients recipients barejids
* @param message text to encrypt
* @return encrypted message.
* @throws CryptoFailedException When something crypto related fails
* @throws UndecidedOmemoIdentityException When there are undecided devices.
* @throws NoSuchAlgorithmException
* @throws InterruptedException
* @throws CannotEstablishOmemoSessionException When there is one recipient, for whom we failed to create a session
* with every one of their devices.
* @throws SmackException.NotConnectedException
* @throws SmackException.NoResponseException
*/
public Message encrypt(ArrayList<BareJid> recipients, String message) throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, InterruptedException, CannotEstablishOmemoSessionException, SmackException.NotConnectedException, SmackException.NoResponseException {
Message m = new Message();
m.setBody(message);
OmemoVAxolotlElement encrypted = getOmemoService().processSendingMessage(this, recipients, m);
return finishMessage(encrypted);
}
/**
* Encrypt a message for all recipients in the MultiUserChat.
*
* @param muc multiUserChat
* @param message message to send
* @return encrypted message
* @throws UndecidedOmemoIdentityException when there are undecided devices.
* @throws NoSuchAlgorithmException
* @throws CryptoFailedException
* @throws XMPPException.XMPPErrorException
* @throws SmackException.NotConnectedException
* @throws InterruptedException
* @throws SmackException.NoResponseException
* @throws NoOmemoSupportException When the muc doesn't support OMEMO.
* @throws CannotEstablishOmemoSessionException when there is a user for whom we could not create a session
* with any of their devices.
*/
public Message encrypt(MultiUserChat muc, String message) throws UndecidedOmemoIdentityException, NoSuchAlgorithmException, CryptoFailedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, NoOmemoSupportException, CannotEstablishOmemoSessionException {
if (!multiUserChatSupportsOmemo(muc.getRoom())) {
throw new NoOmemoSupportException();
}
Message m = new Message();
m.setBody(message);
ArrayList<BareJid> recipients = new ArrayList<>();
for (EntityFullJid e : muc.getOccupants()) {
recipients.add(muc.getOccupant(e).getJid().asBareJid());
}
return encrypt(recipients, message);
}
/**
* Encrypt a message for all users we could build a session with successfully in a previous attempt.
* This method can come in handy as a fallback when encrypting a message fails due to devices we cannot
* build a session with.
*
* @param exception CannotEstablishSessionException from a previous encrypt(user(s), message) call.
* @param message message we want to send.
* @return encrypted message
* @throws CryptoFailedException
* @throws UndecidedOmemoIdentityException when there are undecided identities.
*/
public Message encryptForExistingSessions(CannotEstablishOmemoSessionException exception, String message) throws CryptoFailedException, UndecidedOmemoIdentityException {
Message m = new Message();
m.setBody(message);
OmemoVAxolotlElement encrypted = getOmemoService().encryptOmemoMessage(this, exception.getSuccesses(), m);
return finishMessage(encrypted);
}
/**
* Decrypt an OMEMO message. This method comes handy when dealing with messages that were not automatically
* decrypted by smack-omemo, eg. MAM query messages.
* @param sender sender of the message
* @param omemoMessage message
* @return decrypted message
* @throws InterruptedException Exception
* @throws SmackException.NoResponseException Exception
* @throws SmackException.NotConnectedException Exception
* @throws CryptoFailedException When decryption fails
* @throws XMPPException.XMPPErrorException Exception
* @throws CorruptedOmemoKeyException When the used keys are invalid
* @throws NoRawSessionException When there is no double ratchet session found for this message
*/
public ClearTextMessage decrypt(BareJid sender, Message omemoMessage) throws InterruptedException, SmackException.NoResponseException, SmackException.NotConnectedException, CryptoFailedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException, NoRawSessionException {
return getOmemoService().processLocalMessage(this, sender, omemoMessage);
}
/**
* Return a list of all OMEMO messages that were found in the MAM query result, that could be successfully decrypted.
* Normal cleartext messages are also added to this list.
*
* @param mamQueryResult mamQueryResult
* @return list of decrypted OmemoMessages
* @throws InterruptedException Exception
* @throws XMPPException.XMPPErrorException Exception
* @throws SmackException.NotConnectedException Exception
* @throws SmackException.NoResponseException Exception
*/
public List<ClearTextMessage> decryptMamQueryResult(MamManager.MamQueryResult mamQueryResult) throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException {
List<ClearTextMessage> l = new ArrayList<>();
l.addAll(getOmemoService().decryptMamQueryResult(this, mamQueryResult));
return l;
}
/**
* Trust that a fingerprint belongs to an OmemoDevice.
* The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must
* be of length 64.
* @param device device
* @param fingerprint fingerprint
*/
public void trustOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) {
getOmemoService().getOmemoStoreBackend().trustOmemoIdentity(this, device, fingerprint);
}
/**
* Distrust the fingerprint/OmemoDevice tuple.
* The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must
* be of length 64.
* @param device device
* @param fingerprint fingerprint
*/
public void distrustOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) {
getOmemoService().getOmemoStoreBackend().distrustOmemoIdentity(this, device, fingerprint);
}
/**
* Returns true, if the fingerprint/OmemoDevice tuple is trusted, otherwise false.
* The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must
* be of length 64.
* @param device device
* @param fingerprint fingerprint
* @return
*/
public boolean isTrustedOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) {
return getOmemoService().getOmemoStoreBackend().isTrustedOmemoIdentity(this, device, fingerprint);
}
/**
* Returns true, if the fingerprint/OmemoDevice tuple is decided by the user.
* The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must
* be of length 64.
* @param device device
* @param fingerprint fingerprint
* @return
*/
public boolean isDecidedOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) {
return getOmemoService().getOmemoStoreBackend().isDecidedOmemoIdentity(this, device, fingerprint);
}
/**
* Clear all other devices except this one from our device list and republish the list.
*
* @throws InterruptedException
* @throws SmackException
* @throws XMPPException.XMPPErrorException
* @throws CorruptedOmemoKeyException
*/
public void purgeDevices() throws SmackException, InterruptedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException {
getOmemoService().publishDeviceIdIfNeeded(this,true);
getOmemoService().publishBundle(this);
}
/**
* Generate fresh identity keys and bundle and publish it to the server.
* @throws SmackException
* @throws InterruptedException
* @throws XMPPException.XMPPErrorException
* @throws CorruptedOmemoKeyException
*/
public void regenerate() throws SmackException, InterruptedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException {
//create a new identity and publish new keys to the server
getOmemoService().regenerate(this, null);
getOmemoService().publishDeviceIdIfNeeded(this,false);
getOmemoService().publishBundle(this);
}
/**
* Send a ratchet update message. This can be used to advance the ratchet of a session in order to maintain forward
* secrecy.
*
* @param recipient recipient
* @throws UndecidedOmemoIdentityException When the trust of session with the recipient is not decided yet
* @throws CorruptedOmemoKeyException When the used identityKeys are corrupted
* @throws CryptoFailedException When something fails with the crypto
* @throws CannotEstablishOmemoSessionException When we can't establish a session with the recipient
*/
public void sendRatchetUpdateMessage(OmemoDevice recipient)
throws CorruptedOmemoKeyException, UndecidedOmemoIdentityException, CryptoFailedException,
CannotEstablishOmemoSessionException {
getOmemoService().sendOmemoRatchetUpdateMessage(this, recipient, false);
}
/**
* Create a new KeyTransportElement. This message will contain the AES-Key and IV that can be used eg. for encrypted
* Jingle file transfer.
*
* @param aesKey AES key to transport
* @param iv Initialization vector
* @param to list of recipient devices
* @return KeyTransportMessage
* @throws UndecidedOmemoIdentityException When the trust of session with the recipient is not decided yet
* @throws CorruptedOmemoKeyException When the used identityKeys are corrupted
* @throws CryptoFailedException When something fails with the crypto
* @throws CannotEstablishOmemoSessionException When we can't establish a session with the recipient
*/
public OmemoVAxolotlElement createKeyTransportElement(byte[] aesKey, byte[] iv, OmemoDevice ... to)
throws UndecidedOmemoIdentityException, CorruptedOmemoKeyException, CryptoFailedException,
CannotEstablishOmemoSessionException {
return getOmemoService().prepareOmemoKeyTransportElement(this, aesKey, iv, to);
}
/**
* Create a new Message from a encrypted OmemoMessageElement.
* Add ourselves as the sender and the encrypted element.
* Also tell the server to store the message despite a possible missing body.
* The body will be set to a hint message that we are using OMEMO.
*
* @param encrypted OmemoMessageElement
* @return Message containing the OMEMO element and some additional information
*/
Message finishMessage(OmemoVAxolotlElement encrypted) {
if (encrypted == null) {
return null;
}
Message chatMessage = new Message();
chatMessage.setFrom(connection().getUser().asBareJid());
chatMessage.addExtension(encrypted);
if (OmemoConfiguration.getAddOmemoHintBody()) {
chatMessage.setBody(BODY_OMEMO_HINT);
}
if (OmemoConfiguration.getAddMAMStorageProcessingHint()) {
StoreHint.set(chatMessage);
}
if (OmemoConfiguration.getAddEmeEncryptionHint()) {
chatMessage.addExtension(new ExplicitMessageEncryptionElement(OMEMO_NAMESPACE_V_AXOLOTL, OMEMO));
}
return chatMessage;
}
/**
* Returns true, if the contact has any active devices published in a deviceList.
*
* @param contact contact
* @return true if contact has at least one OMEMO capable device.
* @throws SmackException.NotConnectedException
* @throws InterruptedException
* @throws SmackException.NoResponseException
*/
public boolean contactSupportsOmemo(BareJid contact) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
getOmemoService().refreshDeviceList(this, contact);
return !getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(this, contact)
.getActiveDevices().isEmpty();
}
/**
* Returns true, if the device resource has announced OMEMO support.
* Throws an IllegalArgumentException if the provided FullJid does not have a resource part.
*
* @param fullJid jid of a resource
* @return true if resource supports OMEMO
* @throws XMPPException.XMPPErrorException if
* @throws SmackException.NotConnectedException something
* @throws InterruptedException goes
* @throws SmackException.NoResponseException wrong
*/
public boolean resourceSupportsOmemo(FullJid fullJid) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
if (fullJid.hasNoResource()) {
throw new IllegalArgumentException("Jid " + fullJid + " has no resource part.");
}
return ServiceDiscoveryManager.getInstanceFor(connection()).discoverInfo(fullJid).containsFeature(PEP_NODE_DEVICE_LIST_NOTIFY);
}
/**
* Returns true, if the MUC with the EntityBareJid multiUserChat is non-anonymous and members only (prerequisite
* for OMEMO encryption in MUC).
*
* @param multiUserChat EntityBareJid of the MUC
* @return true if chat supports OMEMO
* @throws XMPPException.XMPPErrorException if
* @throws SmackException.NotConnectedException something
* @throws InterruptedException goes
* @throws SmackException.NoResponseException wrong
*/
public boolean multiUserChatSupportsOmemo(EntityBareJid multiUserChat) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
RoomInfo roomInfo = MultiUserChatManager.getInstanceFor(connection()).getRoomInfo(multiUserChat);
return roomInfo.isNonanonymous() && roomInfo.isMembersOnly();
}
/**
* Returns true, if the Server supports PEP.
*
* @param connection XMPPConnection
* @param server domainBareJid of the server to test
* @return true if server supports pep
* @throws XMPPException.XMPPErrorException
* @throws SmackException.NotConnectedException
* @throws InterruptedException
* @throws SmackException.NoResponseException
*/
public static boolean serverSupportsOmemo(XMPPConnection connection, DomainBareJid server) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
return ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(server).containsFeature(PubSub.NAMESPACE);
}
/**
* Return the fingerprint of our identity key.
*
* @return fingerprint
*/
public OmemoFingerprint getOurFingerprint() {
return getOmemoService().getOmemoStoreBackend().getFingerprint(this);
}
public OmemoFingerprint getFingerprint(OmemoDevice device) throws CannotEstablishOmemoSessionException {
if (device.equals(getOwnDevice())) {
return getOurFingerprint();
}
return getOmemoService().getOmemoStoreBackend().getFingerprint(this, device);
}
/**
* Return all fingerprints of active devices of a contact.
* @param contact contact
* @return HashMap of deviceIds and corresponding fingerprints.
*/
public HashMap<OmemoDevice, OmemoFingerprint> getActiveFingerprints(BareJid contact) {
HashMap<OmemoDevice, OmemoFingerprint> fingerprints = new HashMap<>();
CachedDeviceList deviceList = getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(this, contact);
for (int id : deviceList.getActiveDevices()) {
OmemoDevice device = new OmemoDevice(contact, id);
OmemoFingerprint fingerprint = null;
try {
fingerprint = getFingerprint(device);
} catch (CannotEstablishOmemoSessionException e) {
LOGGER.log(Level.WARNING, "Could not build session with device " + id
+ " of user " + contact + ": " + e.getMessage());
}
if (fingerprint != null) {
fingerprints.put(device, fingerprint);
}
}
return fingerprints;
}
public void addOmemoMessageListener(OmemoMessageListener listener) {
omemoMessageListeners.add(listener);
}
public void removeOmemoMessageListener(OmemoMessageListener listener) {
omemoMessageListeners.remove(listener);
}
public void addOmemoMucMessageListener(OmemoMucMessageListener listener) {
omemoMucMessageListeners.add(listener);
}
public void removeOmemoMucMessageListener(OmemoMucMessageListener listener) {
omemoMucMessageListeners.remove(listener);
}
/**
* Build OMEMO sessions with devices of contact.
*
* @param contact contact we want to build session with.
* @throws InterruptedException
* @throws CannotEstablishOmemoSessionException
* @throws SmackException.NotConnectedException
* @throws SmackException.NoResponseException
*/
public void buildSessionsWith(BareJid contact) throws InterruptedException, CannotEstablishOmemoSessionException, SmackException.NotConnectedException, SmackException.NoResponseException {
getOmemoService().buildOrCreateOmemoSessionsFromBundles(this, contact);
}
/**
* Request a deviceList update from contact contact.
*
* @param contact contact we want to obtain the deviceList from.
* @throws SmackException.NotConnectedException
* @throws InterruptedException
* @throws SmackException.NoResponseException
*/
public void requestDeviceListUpdateFor(BareJid contact) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
getOmemoService().refreshDeviceList(this, contact);
}
/**
* Rotate the signedPreKey published in our OmemoBundle. This should be done every now and then (7-14 days).
* The old signedPreKey should be kept for some more time (a month or so) to enable decryption of messages
* that have been sent since the key was changed.
*
* @throws CorruptedOmemoKeyException When the IdentityKeyPair is damaged.
* @throws InterruptedException XMPP error
* @throws XMPPException.XMPPErrorException XMPP error
* @throws SmackException.NotConnectedException XMPP error
* @throws SmackException.NoResponseException XMPP error
* @throws PubSubException.NotALeafNodeException if the bundle node on the server is a CollectionNode
*/
public void rotateSignedPreKey() throws CorruptedOmemoKeyException, InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException, PubSubException.NotALeafNodeException {
//generate key
getOmemoService().getOmemoStoreBackend().changeSignedPreKey(this);
//publish
getOmemoService().publishDeviceIdIfNeeded(this, false);
getOmemoService().publishBundle(this);
}
/**
* Return true, if the given Stanza contains an OMEMO element 'encrypted'.
* @param stanza stanza
* @return true if stanza has extension 'encrypted'
*/
public static boolean stanzaContainsOmemoElement(Stanza stanza) {
return stanza.hasExtension(OmemoElement.ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL);
}
/**
* Throw an IllegalStateException if no OmemoService is set.
*/
private void throwIfNoServiceSet() {
if (service == null) {
throw new IllegalStateException("No OmemoService set in OmemoManager.");
}
}
private void setConnectionListener() {
connection().addConnectionListener(new ConnectionListener() {
@Override
public void connected(XMPPConnection connection) {
LOGGER.log(Level.INFO, "connected");
}
@Override
public void authenticated(XMPPConnection connection, boolean resumed) {
LOGGER.log(Level.INFO, "authenticated. Resumed: " + resumed);
if (resumed) {
return;
}
try {
initialize();
} catch (InterruptedException | CorruptedOmemoKeyException | PubSubException.NotALeafNodeException | SmackException.NotLoggedInException | SmackException.NoResponseException | SmackException.NotConnectedException | XMPPException.XMPPErrorException e) {
LOGGER.log(Level.SEVERE, "connectionListener.authenticated() failed to initialize OmemoManager: "
+ e.getMessage());
}
}
@Override
public void connectionClosed() {
}
@Override
public void connectionClosedOnError(Exception e) {
connectionClosed();
}
@Override
public void reconnectionSuccessful() {
}
@Override
public void reconnectingIn(int seconds) {
}
@Override
public void reconnectionFailed(Exception e) {
}
});
}
public static int randomDeviceId() {
int i = new Random().nextInt(Integer.MAX_VALUE);
if (i == 0) {
return randomDeviceId();
}
return Math.abs(i);
}
/**
* Return the BareJid of the user.
*
* @return bareJid
*/
public BareJid getOwnJid() {
EntityFullJid fullJid = connection().getUser();
if (fullJid == null) return null;
return fullJid.asBareJid();
}
/**
* Return the deviceId of this OmemoManager.
*
* @return deviceId
*/
public int getDeviceId() {
return deviceId;
}
/**
* Return the OmemoDevice of the user.
*
* @return omemoDevice
*/
public OmemoDevice getOwnDevice() {
return new OmemoDevice(getOwnJid(), getDeviceId());
}
void setDeviceId(int nDeviceId) {
INSTANCES.get(connection()).remove(getDeviceId());
INSTANCES.get(connection()).put(nDeviceId, this);
this.deviceId = nDeviceId;
}
/**
* Notify all registered OmemoMessageListeners about a received OmemoMessage.
*
* @param decryptedBody decrypted Body element of the message
* @param encryptedMessage unmodified message as it was received
* @param wrappingMessage message that wrapped the incoming message
* @param messageInformation information about the messages encryption (used identityKey, carbon...)
*/
void notifyOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation messageInformation) {
for (OmemoMessageListener l : omemoMessageListeners) {
l.onOmemoMessageReceived(decryptedBody, encryptedMessage, wrappingMessage, messageInformation);
}
}
void notifyOmemoKeyTransportMessageReceived(CipherAndAuthTag cipherAndAuthTag, Message transportingMessage,
Message wrappingMessage, OmemoMessageInformation information) {
for (OmemoMessageListener l : omemoMessageListeners) {
l.onOmemoKeyTransportReceived(cipherAndAuthTag, transportingMessage, wrappingMessage, information);
}
}
/**
* Notify all registered OmemoMucMessageListeners of an incoming OmemoMessageElement in a MUC.
*
* @param muc MultiUserChat the message was received in
* @param from BareJid of the user that sent the message
* @param decryptedBody decrypted body
* @param message original message with encrypted content
* @param wrappingMessage wrapping message (in case of carbon copy)
* @param omemoInformation information about the encryption of the message
*/
void notifyOmemoMucMessageReceived(MultiUserChat muc, BareJid from, String decryptedBody, Message message,
Message wrappingMessage, OmemoMessageInformation omemoInformation) {
for (OmemoMucMessageListener l : omemoMucMessageListeners) {
l.onOmemoMucMessageReceived(muc, from, decryptedBody, message,
wrappingMessage, omemoInformation);
}
}
void notifyOmemoMucKeyTransportMessageReceived(MultiUserChat muc, BareJid from, CipherAndAuthTag cipherAndAuthTag,
Message transportingMessage, Message wrappingMessage,
OmemoMessageInformation messageInformation) {
for (OmemoMucMessageListener l : omemoMucMessageListeners) {
l.onOmemoKeyTransportReceived(muc, from, cipherAndAuthTag,
transportingMessage, wrappingMessage, messageInformation);
}
}
/**
* Remove all active stanza listeners of this manager from the connection.
* This is somewhat the counterpart of initialize().
*/
public void shutdown() {
PEPManager.getInstanceFor(connection()).removePEPListener(deviceListUpdateListener);
connection().removeAsyncStanzaListener(omemoStanzaListener);
CarbonManager.getInstanceFor(connection()).removeCarbonCopyReceivedListener(omemoCarbonCopyListener);
}
/**
* Get our connection.
*
* @return the connection of this manager
*/
XMPPConnection getConnection() {
return connection();
}
/**
* Return the OMEMO service object.
*
* @return omemoService
*/
OmemoService<?,?,?,?,?,?,?,?,?> getOmemoService() {
throwIfNoServiceSet();
return service;
}
PEPListener deviceListUpdateListener = new PEPListener() {
@Override
public void eventReceived(EntityBareJid from, EventElement event, Message message) {
for (ExtensionElement items : event.getExtensions()) {
if (!(items instanceof ItemsExtension)) {
continue;
}
for (ExtensionElement item : ((ItemsExtension) items).getItems()) {
if (!(item instanceof PayloadItem<?>)) {
continue;
}
PayloadItem<?> payloadItem = (PayloadItem<?>) item;
if (!(payloadItem.getPayload() instanceof OmemoDeviceListVAxolotlElement)) {
continue;
}
//Device List <list>
OmemoDeviceListVAxolotlElement omemoDeviceListElement = (OmemoDeviceListVAxolotlElement) payloadItem.getPayload();
int ourDeviceId = getDeviceId();
getOmemoService().getOmemoStoreBackend().mergeCachedDeviceList(OmemoManager.this, from, omemoDeviceListElement);
if (from == null) {
//Unknown sender, no more work to do.
//TODO: This DOES happen for some reason. Figure out when...
continue;
}
if (!from.equals(getOwnJid())) {
//Not our deviceList, so nothing more to do
continue;
}
if (omemoDeviceListElement.getDeviceIds().contains(ourDeviceId)) {
//We are on the list. Nothing more to do
continue;
}
//Our deviceList and we are not on it! We don't want to miss all the action!!!
LOGGER.log(Level.INFO, "Our deviceId was not on the list!");
Set<Integer> deviceListIds = omemoDeviceListElement.copyDeviceIds();
//enroll at the deviceList
deviceListIds.add(ourDeviceId);
omemoDeviceListElement = new OmemoDeviceListVAxolotlElement(deviceListIds);
try {
OmemoService.publishDeviceIds(OmemoManager.this, omemoDeviceListElement);
} catch (SmackException | InterruptedException | XMPPException.XMPPErrorException e) {
//TODO: It might be dangerous NOT to retry publishing our deviceId
LOGGER.log(Level.SEVERE,
"Could not publish our device list after an update without our id was received: "
+ e.getMessage());
}
}
}
}
};
OmemoService<?,?,?,?,?,?,?,?,?>.OmemoStanzaListener getOmemoStanzaListener() {
if (omemoStanzaListener == null) {
omemoStanzaListener = getOmemoService().createStanzaListener(this);
}
return omemoStanzaListener;
}
OmemoService<?,?,?,?,?,?,?,?,?>.OmemoCarbonCopyListener getOmemoCarbonCopyListener() {
if (omemoCarbonCopyListener == null) {
omemoCarbonCopyListener = getOmemoService().createOmemoCarbonCopyListener(this);
}
return omemoCarbonCopyListener;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,789 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterEntry;
import org.jivesoftware.smackx.omemo.element.OmemoBundleVAxolotlElement;
import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement;
import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.internal.CachedDeviceList;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.internal.OmemoSession;
import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil;
import org.jxmpp.jid.BareJid;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.TARGET_PRE_KEY_COUNT;
/**
* Class that presents some methods that are used to load/generate/store keys and session data needed for OMEMO.
*
* @param <T_IdKeyPair> IdentityKeyPair class
* @param <T_IdKey> IdentityKey class
* @param <T_PreKey> PreKey class
* @param <T_SigPreKey> SignedPreKey class
* @param <T_Sess> Session class
* @param <T_Addr> Address class
* @param <T_ECPub> Elliptic Curve PublicKey class
* @param <T_Bundle> Bundle class
* @param <T_Ciph> Cipher class
* @author Paul Schaub
*/
public abstract class OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> {
private static final Logger LOGGER = Logger.getLogger(OmemoStore.class.getName());
private final WeakHashMap<OmemoManager, HashMap<OmemoDevice, OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>>>
omemoSessions = new WeakHashMap<>();
/**
* Create a new OmemoStore.
*/
public OmemoStore() {
}
/**
* Return true if this is a fresh installation.
*
* @param omemoManager omemoManager of our device.
* @return true or false.
*/
public abstract boolean isFreshInstallation(OmemoManager omemoManager);
/**
* Check, if our freshly generated deviceId is available (unique) in our deviceList.
*
* @param omemoManager omemoManager of our device.
* @param id our deviceId.
* @return true if list did not contain our id, else false
*/
boolean isAvailableDeviceId(OmemoManager omemoManager, int id) {
LOGGER.log(Level.INFO, "Check if id " + id + " is available...");
//Lookup local cached device list
BareJid ownJid = omemoManager.getOwnJid();
CachedDeviceList cachedDeviceList = loadCachedDeviceList(omemoManager, ownJid);
if (cachedDeviceList == null) {
cachedDeviceList = new CachedDeviceList();
}
//Does the list already contain that id?
return !cachedDeviceList.contains(id);
}
/**
* Generate a new Identity (deviceId, identityKeys, preKeys...).
*
* @param omemoManager omemoManager of our device we want to regenerate.
* @throws CorruptedOmemoKeyException in case something goes wrong
*/
void regenerate(OmemoManager omemoManager) throws CorruptedOmemoKeyException {
LOGGER.log(Level.INFO, "Regenerating with deviceId " + omemoManager.getDeviceId() + "...");
int nextPreKeyId = 1;
storeOmemoIdentityKeyPair(omemoManager, generateOmemoIdentityKeyPair());
storeOmemoPreKeys(omemoManager, generateOmemoPreKeys(nextPreKeyId, TARGET_PRE_KEY_COUNT));
storeLastPreKeyId(omemoManager, OmemoKeyUtil.addInBounds(nextPreKeyId, TARGET_PRE_KEY_COUNT));
storeCurrentSignedPreKeyId(omemoManager, -1); //Set back to no-value default
changeSignedPreKey(omemoManager);
initializeOmemoSessions(omemoManager);
}
/**
* Merge the received OmemoDeviceListElement with the one we already have. If we had none, the received one is saved.
*
* @param omemoManager omemoManager of our device.
* @param contact Contact we received the list from.
* @param list List we received.
*/
void mergeCachedDeviceList(OmemoManager omemoManager, BareJid contact, OmemoDeviceListElement list) {
CachedDeviceList cached = loadCachedDeviceList(omemoManager, contact);
if (cached == null) {
cached = new CachedDeviceList();
}
if (list != null) {
cached.merge(list.getDeviceIds());
}
storeCachedDeviceList(omemoManager, contact, cached);
}
/**
* Renew our singed preKey. This should be done once every 7-14 days.
* The old signed PreKey should be kept for around a month or so (look it up in the XEP).
*
* @param omemoManager omemoManager of our device.
* @throws CorruptedOmemoKeyException when our identityKey is invalid
*/
void changeSignedPreKey(OmemoManager omemoManager) throws CorruptedOmemoKeyException {
int lastSignedPreKeyId = loadCurrentSignedPreKeyId(omemoManager);
if (lastSignedPreKeyId == -1) lastSignedPreKeyId = 0;
try {
T_SigPreKey newSignedPreKey = generateOmemoSignedPreKey(loadOmemoIdentityKeyPair(omemoManager), lastSignedPreKeyId + 1);
storeOmemoSignedPreKey(omemoManager, lastSignedPreKeyId + 1, newSignedPreKey);
storeCurrentSignedPreKeyId(omemoManager, lastSignedPreKeyId + 1);
setDateOfLastSignedPreKeyRenewal(omemoManager);
removeOldSignedPreKeys(omemoManager);
} catch (CorruptedOmemoKeyException e) {
LOGGER.log(Level.INFO, "Couldn't change SignedPreKey: " + e.getMessage());
throw e;
}
}
/**
* Remove the oldest signedPreKey until there are only MAX_NUMBER_OF_STORED_SIGNED_PREKEYS left.
*
* @param omemoManager omemoManager of our device.
*/
private void removeOldSignedPreKeys(OmemoManager omemoManager) {
if (OmemoConfiguration.getMaxNumberOfStoredSignedPreKeys() <= 0) {
return;
}
int currentId = loadCurrentSignedPreKeyId(omemoManager);
if (currentId == -1) currentId = 0;
HashMap<Integer, T_SigPreKey> signedPreKeys = loadOmemoSignedPreKeys(omemoManager);
for (int i : signedPreKeys.keySet()) {
if (i <= currentId - OmemoConfiguration.getMaxNumberOfStoredSignedPreKeys()) {
LOGGER.log(Level.INFO, "Remove signedPreKey " + i + ".");
removeOmemoSignedPreKey(omemoManager, i);
}
}
}
/**
* Pack a OmemoBundleElement containing our key material.
* If we used up n preKeys since we last published our bundle, generate n new preKeys and add them to the bundle.
* We should always publish TARGET_PRE_KEY_COUNT keys.
*
* @param omemoManager omemoManager of our device.
* @return OmemoBundleElement
* @throws CorruptedOmemoKeyException when a key could not be loaded
*/
OmemoBundleVAxolotlElement packOmemoBundle(OmemoManager omemoManager) throws CorruptedOmemoKeyException {
int currentSignedPreKeyId = loadCurrentSignedPreKeyId(omemoManager);
if (currentSignedPreKeyId == -1) currentSignedPreKeyId = 0;
T_SigPreKey currentSignedPreKey = loadOmemoSignedPreKey(omemoManager, currentSignedPreKeyId);
T_IdKeyPair identityKeyPair = loadOmemoIdentityKeyPair(omemoManager);
HashMap<Integer, T_PreKey> preKeys = loadOmemoPreKeys(omemoManager);
int newKeysCount = TARGET_PRE_KEY_COUNT - preKeys.size();
if (newKeysCount > 0) {
int lastPreKeyId = loadLastPreKeyId(omemoManager);
if (lastPreKeyId == -1) lastPreKeyId = 0;
HashMap<Integer, T_PreKey> newKeys = generateOmemoPreKeys(lastPreKeyId + 1, newKeysCount);
storeOmemoPreKeys(omemoManager, newKeys);
preKeys.putAll(newKeys);
storeLastPreKeyId(omemoManager, lastPreKeyId + newKeysCount);
}
return new OmemoBundleVAxolotlElement(
currentSignedPreKeyId,
keyUtil().signedPreKeyPublicForBundle(currentSignedPreKey),
keyUtil().signedPreKeySignatureFromKey(currentSignedPreKey),
keyUtil().identityKeyForBundle(keyUtil().identityKeyFromPair(identityKeyPair)),
keyUtil().preKeyPublisKeysForBundle(preKeys)
);
}
/**
* Preload all OMEMO sessions for our devices and our contacts from existing raw sessions.
*
* @param omemoManager omemoManager of our device.
*/
void initializeOmemoSessions(OmemoManager omemoManager) {
//Get HashMap of our omemoSessions
HashMap<OmemoDevice, OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>>
sessions = omemoSessions.get(omemoManager);
if (sessions == null) {
sessions = new HashMap<>();
omemoSessions.put(omemoManager, sessions);
}
//Sessions with our own devices
HashMap<Integer, T_Sess> ourRawSessions = loadAllRawSessionsOf(omemoManager, omemoManager.getOwnJid());
ourRawSessions.remove(omemoManager.getDeviceId()); //Just to make sure we have no session with ourselves...
sessions.putAll(createOmemoSessionsFromRawSessions(omemoManager, omemoManager.getOwnJid(), ourRawSessions));
//Sessions with contacts
for (RosterEntry rosterEntry : Roster.getInstanceFor(omemoManager.getConnection()).getEntries()) {
HashMap<Integer, T_Sess> contactDevices = loadAllRawSessionsOf(omemoManager, rosterEntry.getJid().asBareJid());
sessions.putAll(createOmemoSessionsFromRawSessions(omemoManager, rosterEntry.getJid().asBareJid(), contactDevices));
}
}
/**
* Forget all omemoSessions of the omemoManager from cache.
* This will not remove the sessions from persistent memory!
*
* @param omemoManager omemoManager we want to forget sessions from.
*/
void forgetOmemoSessions(OmemoManager omemoManager) {
omemoSessions.remove(omemoManager);
}
/**
* Create a new concrete OmemoSession with a contact.
*
* @param omemoManager omemoManager of our device.
* @param device device to establish the session with
* @param identityKey identityKey of the device
* @return concrete OmemoSession
*/
private OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>
createOmemoSession(OmemoManager omemoManager, OmemoDevice device, T_IdKey identityKey) {
return keyUtil().createOmemoSession(omemoManager, this, device, identityKey);
}
/**
* Return the OmemoSession for the OmemoDevice. If there is no OmemoSession for the device yet,
* build one from local raw session material.
*
* @param omemoManager omemoManager of our device.
* @param device OmemoDevice
* @return OmemoSession
*/
public OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>
getOmemoSessionOf(OmemoManager omemoManager, OmemoDevice device) {
HashMap<OmemoDevice, OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>>
sessions = omemoSessions.get(omemoManager);
if (sessions == null) {
sessions = new HashMap<>();
omemoSessions.put(omemoManager, sessions);
}
OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>
session = sessions.get(device);
//No OmemoSession found
if (session == null) {
T_IdKey identityKey = null;
try {
identityKey = loadOmemoIdentityKey(omemoManager, device);
} catch (CorruptedOmemoKeyException e) {
LOGGER.log(Level.WARNING, "getOmemoSessionOf could not load identityKey of " + device + ": " + e.getMessage());
}
if (identityKey != null) {
session = createOmemoSession(omemoManager, device, identityKey);
} else {
LOGGER.log(Level.INFO, "getOmemoSessionOf couldn't find an identityKey for " + device
+ ". Initiate session without.");
session = createOmemoSession(omemoManager, device, null);
}
sessions.put(device, session);
}
if (session.getIdentityKey() == null) {
try {
session.setIdentityKey(loadOmemoIdentityKey(omemoManager, device));
} catch (CorruptedOmemoKeyException e) {
LOGGER.log(Level.WARNING, "Can't update IdentityKey of " + device + ": " + e.getMessage());
}
}
return session;
}
/**
* Create OmemoSession objects for all T_Sess objects of the contact.
* The T_Sess objects will be wrapped inside a OmemoSession for every device of the contact.
*
* @param omemoManager omemoManager of our device.
* @param contact BareJid of the contact
* @param rawSessions HashMap of Integers (deviceIds) and T_Sess sessions.
* @return HashMap of OmemoContacts and OmemoSessions
*/
private HashMap<OmemoDevice, OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>>
createOmemoSessionsFromRawSessions(OmemoManager omemoManager, BareJid contact, HashMap<Integer, T_Sess> rawSessions) {
HashMap<OmemoDevice, OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>>
sessions = new HashMap<>();
for (Map.Entry<Integer, T_Sess> sessionEntry : rawSessions.entrySet()) {
OmemoDevice omemoDevice = new OmemoDevice(contact, sessionEntry.getKey());
try {
T_IdKey identityKey = loadOmemoIdentityKey(omemoManager, omemoDevice);
if (identityKey != null) {
sessions.put(omemoDevice, createOmemoSession(omemoManager, omemoDevice, identityKey));
} else {
LOGGER.log(Level.WARNING, "IdentityKey of " + omemoDevice + " is null. Is this even possible at this point?");
}
} catch (CorruptedOmemoKeyException e1) {
LOGGER.log(Level.WARNING, "buildOmemoSessionFor could not create a session for " + omemoDevice +
": " + e1.getMessage());
}
}
return sessions;
}
// *sigh*
/**
* Return the id of the last generated preKey.
* This is used to generate new preKeys without preKeyId collisions.
*
* @param omemoManager omemoManager of our device.
* @return id of the last preKey
*/
public abstract int loadLastPreKeyId(OmemoManager omemoManager);
/**
* Store the id of the last preKey we generated.
*
* @param omemoManager omemoManager of our device.
* @param currentPreKeyId the id of the last generated PreKey
*/
public abstract void storeLastPreKeyId(OmemoManager omemoManager, int currentPreKeyId);
/**
* Generate a new IdentityKeyPair. We should always have only one pair and usually keep this for a long time.
*
* @return identityKeyPair
*/
public T_IdKeyPair generateOmemoIdentityKeyPair() {
return keyUtil().generateOmemoIdentityKeyPair();
}
/**
* Load our identityKeyPair from storage.
*
* @param omemoManager omemoManager of our device.
* @return identityKeyPair
* @throws CorruptedOmemoKeyException Thrown, if the stored key is damaged (*hands up* not my fault!)
*/
public abstract T_IdKeyPair loadOmemoIdentityKeyPair(OmemoManager omemoManager) throws CorruptedOmemoKeyException;
/**
* Store our identityKeyPair in storage. It would be a cool feature, if the key could be stored in a encrypted
* database or something similar.
*
* @param omemoManager omemoManager of our device.
* @param identityKeyPair identityKeyPair
*/
public abstract void storeOmemoIdentityKeyPair(OmemoManager omemoManager, T_IdKeyPair identityKeyPair);
/**
* Load the public identityKey of the device.
*
* @param omemoManager omemoManager of our device.
* @param device device
* @return identityKey
* @throws CorruptedOmemoKeyException when the key in question is corrupted and cant be deserialized.
*/
public abstract T_IdKey loadOmemoIdentityKey(OmemoManager omemoManager, OmemoDevice device) throws CorruptedOmemoKeyException;
/**
* Store the public identityKey of the device.
*
* @param omemoManager omemoManager of our device.
* @param device device
* @param key identityKey
*/
public abstract void storeOmemoIdentityKey(OmemoManager omemoManager, OmemoDevice device, T_IdKey key);
/**
* Decide, whether a identityKey of a device is trusted or not.
* If you want to use this module, you should memorize, whether the user has trusted this key or not, since
* the owner of the identityKey will be able to read sent messages when this method returned 'true' for their
* identityKey. Either you let the user decide whether you trust a key every time you see a new key, or you
* implement something like 'blind trust' (see https://gultsch.de/trust.html).
*
* @param omemoManager omemoManager of our device.
* @param device Owner of the key
* @param identityKey identityKey
* @return true, if the user trusts the key and wants to send messages to it, otherwise false
*/
public boolean isTrustedOmemoIdentity(OmemoManager omemoManager, OmemoDevice device, T_IdKey identityKey) {
return isTrustedOmemoIdentity(omemoManager, device, keyUtil().getFingerprint(identityKey));
}
public abstract boolean isTrustedOmemoIdentity(OmemoManager omemoManager, OmemoDevice device, OmemoFingerprint fingerprint);
/**
* Did the user yet made a decision about whether to trust or distrust this device?
*
* @param omemoManager omemoManager of our device.
* @param device device
* @param identityKey IdentityKey
* @return true, if the user either trusted or distrusted the device. Return false, if the user did not yet decide.
*/
public boolean isDecidedOmemoIdentity(OmemoManager omemoManager, OmemoDevice device, T_IdKey identityKey) {
return isDecidedOmemoIdentity(omemoManager, device, keyUtil().getFingerprint(identityKey));
}
public abstract boolean isDecidedOmemoIdentity(OmemoManager omemoManager, OmemoDevice device, OmemoFingerprint fingerprint);
/**
* Trust an OmemoIdentity. This involves marking the key as trusted.
*
* @param omemoManager omemoManager of our device.
* @param device device
* @param identityKey identityKey
*/
public void trustOmemoIdentity(OmemoManager omemoManager, OmemoDevice device, T_IdKey identityKey) {
trustOmemoIdentity(omemoManager, device, keyUtil().getFingerprint(identityKey));
}
public abstract void trustOmemoIdentity(OmemoManager omemoManager, OmemoDevice device, OmemoFingerprint identityKeyFingerprint);
/**
* Distrust an OmemoIdentity. This involved marking the key as distrusted.
*
* @param omemoManager omemoManager of our device.
* @param device device
* @param identityKey identityKey
*/
public void distrustOmemoIdentity(OmemoManager omemoManager, OmemoDevice device, T_IdKey identityKey) {
distrustOmemoIdentity(omemoManager, device, keyUtil().getFingerprint(identityKey));
}
public abstract void distrustOmemoIdentity(OmemoManager omemoManager, OmemoDevice device, OmemoFingerprint fingerprint);
/**
* Set the date in millis of the last message that was received from device 'from' to 'date'.
*
* @param omemoManager omemoManager of our device.
* @param from device in question
* @param date date of the last received message
*/
public abstract void setDateOfLastReceivedMessage(OmemoManager omemoManager, OmemoDevice from, Date date);
/**
* Set the date in millis of the last message that was received from device 'from' to now.
*
* @param omemoManager omemoManager of our device.
* @param from device in question
*/
public void setDateOfLastReceivedMessage(OmemoManager omemoManager, OmemoDevice from) {
this.setDateOfLastReceivedMessage(omemoManager, from, new Date());
}
/**
* Return the date in millis of the last message that was received from device 'from'.
*
* @param omemoManager omemoManager of our device.
* @param from device in question
* @return date if existent as long, otherwise -1
*/
public abstract Date getDateOfLastReceivedMessage(OmemoManager omemoManager, OmemoDevice from);
/**
* Set the date in millis of the last time the signed preKey was renewed.
*
* @param omemoManager omemoManager of our device.
* @param date date
*/
public abstract void setDateOfLastSignedPreKeyRenewal(OmemoManager omemoManager, Date date);
/**
* Store the date of the last preKey renewal in the omemoStore.
*
* @param omemoManager omemoManager of our device.
*/
public void setDateOfLastSignedPreKeyRenewal(OmemoManager omemoManager) {
setDateOfLastSignedPreKeyRenewal(omemoManager, new Date());
}
/**
* Get the date in millis of the last time the signed preKey was renewed.
*
* @param omemoManager omemoManager of our device.
* @return date if existent, otherwise null
*/
public abstract Date getDateOfLastSignedPreKeyRenewal(OmemoManager omemoManager);
/**
* Generate 'count' new PreKeys beginning with id 'startId'.
* These preKeys are published and can be used by contacts to establish sessions with us.
*
* @param startId start id
* @param count how many keys do we want to generate
* @return Map of new preKeys
*/
public HashMap<Integer, T_PreKey> generateOmemoPreKeys(int startId, int count) {
return keyUtil().generateOmemoPreKeys(startId, count);
}
/**
* Load the preKey with id 'preKeyId' from storage.
*
* @param omemoManager omemoManager of our device.
* @param preKeyId id of the key to be loaded
* @return loaded preKey
*/
public abstract T_PreKey loadOmemoPreKey(OmemoManager omemoManager, int preKeyId);
/**
* Store a PreKey in storage.
*
* @param omemoManager omemoManager of our device.
* @param preKeyId id of the key
* @param preKey key
*/
public abstract void storeOmemoPreKey(OmemoManager omemoManager, int preKeyId, T_PreKey preKey);
/**
* Store a whole bunch of preKeys.
*
* @param omemoManager omemoManager of our device.
* @param preKeyHashMap HashMap of preKeys
*/
public void storeOmemoPreKeys(OmemoManager omemoManager, HashMap<Integer, T_PreKey> preKeyHashMap) {
for (Map.Entry<Integer, T_PreKey> e : preKeyHashMap.entrySet()) {
storeOmemoPreKey(omemoManager, e.getKey(), e.getValue());
}
}
/**
* remove a preKey from storage. This is called, when a contact used one of our preKeys to establish a session
* with us.
*
* @param omemoManager omemoManager of our device.
* @param preKeyId id of the used key that will be deleted
*/
public abstract void removeOmemoPreKey(OmemoManager omemoManager, int preKeyId);
/**
* Return the id of the currently used signed preKey.
* This is used to avoid collisions when generating a new signedPreKey.
*
* @param omemoManager omemoManager of our device.
* @return id
*/
public abstract int loadCurrentSignedPreKeyId(OmemoManager omemoManager);
/**
* Store the id of the currently used signedPreKey.
*
* @param omemoManager omemoManager of our device.
* @param currentSignedPreKeyId if of the signedPreKey that is currently in use
*/
public abstract void storeCurrentSignedPreKeyId(OmemoManager omemoManager, int currentSignedPreKeyId);
/**
* Return all our current OmemoPreKeys.
*
* @param omemoManager omemoManager of our device.
* @return Map containing our preKeys
*/
public abstract HashMap<Integer, T_PreKey> loadOmemoPreKeys(OmemoManager omemoManager);
/**
* Return the signedPreKey with the id 'singedPreKeyId'.
*
* @param omemoManager omemoManager of our device.
* @param signedPreKeyId id of the key
* @return key
*/
public abstract T_SigPreKey loadOmemoSignedPreKey(OmemoManager omemoManager, int signedPreKeyId);
/**
* Load all our signed PreKeys.
*
* @param omemoManager omemoManager of our device.
* @return HashMap of our singedPreKeys
*/
public abstract HashMap<Integer, T_SigPreKey> loadOmemoSignedPreKeys(OmemoManager omemoManager);
/**
* Generate a new signed preKey.
*
* @param identityKeyPair identityKeyPair used to sign the preKey
* @param signedPreKeyId id that the preKey will have
* @return signedPreKey
* @throws CorruptedOmemoKeyException when something goes wrong
*/
public T_SigPreKey generateOmemoSignedPreKey(T_IdKeyPair identityKeyPair, int signedPreKeyId) throws CorruptedOmemoKeyException {
return keyUtil().generateOmemoSignedPreKey(identityKeyPair, signedPreKeyId);
}
/**
* Store a signedPreKey in storage.
*
* @param omemoManager omemoManager of our device.
* @param signedPreKeyId id of the signedPreKey
* @param signedPreKey the key itself
*/
public abstract void storeOmemoSignedPreKey(OmemoManager omemoManager, int signedPreKeyId, T_SigPreKey signedPreKey);
/**
* Remove a signedPreKey from storage.
*
* @param omemoManager omemoManager of our device.
* @param signedPreKeyId id of the key that will be removed
*/
public abstract void removeOmemoSignedPreKey(OmemoManager omemoManager, int signedPreKeyId);
/**
* Load the crypto-lib specific session object of the device from storage.
*
* @param omemoManager omemoManager of our device.
* @param device device whose session we want to load
* @return crypto related session
*/
public abstract T_Sess loadRawSession(OmemoManager omemoManager, OmemoDevice device);
/**
* Load all crypto-lib specific session objects of contact 'contact'.
*
* @param omemoManager omemoManager of our device.
* @param contact BareJid of the contact we want to get all sessions from
* @return HashMap of deviceId and sessions of the contact
*/
public abstract HashMap<Integer, T_Sess> loadAllRawSessionsOf(OmemoManager omemoManager, BareJid contact);
/**
* Store a crypto-lib specific session to storage.
*
* @param omemoManager omemoManager of our device.
* @param device OmemoDevice whose session we want to store
* @param session session
*/
public abstract void storeRawSession(OmemoManager omemoManager, OmemoDevice device, T_Sess session);
/**
* Remove a crypto-lib specific session from storage.
*
* @param omemoManager omemoManager of our device.
* @param device device whose session we want to delete
*/
public abstract void removeRawSession(OmemoManager omemoManager, OmemoDevice device);
/**
* Remove all crypto-lib specific session of a contact.
*
* @param omemoManager omemoManager of our device.
* @param contact BareJid of the contact
*/
public abstract void removeAllRawSessionsOf(OmemoManager omemoManager, BareJid contact);
/**
* Return true, if we have a session with the device, otherwise false.
* Hint for Signal: Do not try 'return getSession() != null' since this will create a new session.
*
* @param omemoManager omemoManager of our device.
* @param device device
* @return true if we have session, otherwise false
*/
public abstract boolean containsRawSession(OmemoManager omemoManager, OmemoDevice device);
/**
* Load a list of deviceIds from contact 'contact' from the local cache.
*
* @param omemoManager omemoManager of our device.
* @param contact contact we want to get the deviceList of
* @return CachedDeviceList of the contact
*/
public abstract CachedDeviceList loadCachedDeviceList(OmemoManager omemoManager, BareJid contact);
/**
* Store the DeviceList of the contact in local storage.
* See this as a cache.
*
* @param omemoManager omemoManager of our device.
* @param contact Contact
* @param deviceList list of the contacts devices' ids.
*/
public abstract void storeCachedDeviceList(OmemoManager omemoManager, BareJid contact, CachedDeviceList deviceList);
/**
* Delete this device's IdentityKey, PreKeys, SignedPreKeys and Sessions.
*
* @param omemoManager omemoManager of our device.
*/
public abstract void purgeOwnDeviceKeys(OmemoManager omemoManager);
/**
* Return a concrete KeyUtil object that we can use as a utility to create keys etc.
*
* @return KeyUtil object
*/
public abstract OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> keyUtil();
/**
* Return our identityKeys fingerprint.
*
* @param omemoManager omemoManager of our device.
* @return fingerprint of our identityKeyPair
*/
public OmemoFingerprint getFingerprint(OmemoManager omemoManager) {
try {
return keyUtil().getFingerprint(keyUtil().identityKeyFromPair(loadOmemoIdentityKeyPair(omemoManager)));
} catch (CorruptedOmemoKeyException e) {
LOGGER.log(Level.WARNING, "getFingerprint failed due to corrupted identityKeyPair: " + e.getMessage());
return null;
}
}
/**
* Return the default deviceId for a user.
* The defaultDeviceId will be used when the OmemoManager gets instantiated without passing a specific deviceId.
* If no default id is set, return -1;
*
* @param user user
* @return defaultDeviceId or -1
*/
public abstract int getDefaultDeviceId(BareJid user);
/**
* Set the default deviceId of a user.
*
* @param user user
* @param defaultDeviceId defaultDeviceId
*/
public abstract void setDefaultDeviceId(BareJid user, int defaultDeviceId);
/**
* Return the fingerprint of the given devices announced identityKey.
*
* @param omemoManager omemoManager of our device.
* @param device device
* @throws CannotEstablishOmemoSessionException if we cannot establish a session
* @return fingerprint of the identityKey
*/
public OmemoFingerprint getFingerprint(OmemoManager omemoManager, OmemoDevice device) throws CannotEstablishOmemoSessionException {
T_IdKey idKey;
try {
idKey = loadOmemoIdentityKey(omemoManager, device);
if (idKey == null) {
OmemoService.getInstance().buildSessionFromOmemoBundle(omemoManager, device, true);
}
idKey = loadOmemoIdentityKey(omemoManager, device);
} catch (CorruptedOmemoKeyException e) {
LOGGER.log(Level.WARNING, "getFingerprint failed due to corrupted identityKey: " + e.getMessage());
return null;
}
return keyUtil().getFingerprint(idKey);
}
}

View file

@ -0,0 +1,56 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.element;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.util.XmlStringBuilder;
/**
* Class that represents an OMEMO Bundle element.
* TODO: Move functionality to here.
*
* @author Paul Schaub
*/
public abstract class OmemoBundleElement implements ExtensionElement {
public static final String BUNDLE = "bundle";
public static final String SIGNED_PRE_KEY_PUB = "signedPreKeyPublic";
public static final String SIGNED_PRE_KEY_ID = "signedPreKeyId";
public static final String SIGNED_PRE_KEY_SIG = "signedPreKeySignature";
public static final String IDENTITY_KEY = "identityKey";
public static final String PRE_KEYS = "prekeys";
public static final String PRE_KEY_PUB = "preKeyPublic";
public static final String PRE_KEY_ID = "preKeyId";
@Override
public abstract XmlStringBuilder toXML();
@Override
public boolean equals(Object other) {
if (!(other instanceof OmemoBundleElement)) {
return false;
}
OmemoBundleElement otherOmemoBundleElement = (OmemoBundleElement) other;
return toXML().equals(otherOmemoBundleElement.toXML());
}
@Override
public int hashCode() {
return this.toXML().hashCode();
}
}

View file

@ -0,0 +1,207 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.element;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smack.util.stringencoder.Base64;
import java.util.HashMap;
import java.util.Map;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL;
/**
* OMEMO device bundle as described here:
* https://xmpp.org/extensions/xep-0384.html#usecases-announcing (Example 3).
*
* @author Paul Schaub
*/
public class OmemoBundleVAxolotlElement extends OmemoBundleElement {
private final int signedPreKeyId;
private final String signedPreKeyB64;
private byte[] signedPreKey;
private final String signedPreKeySignatureB64;
private byte[] signedPreKeySignature;
private final String identityKeyB64;
private byte[] identityKey;
private final HashMap<Integer, String> preKeysB64;
private HashMap<Integer, byte[]> preKeys;
/**
* Constructor to create a Bundle Element from base64 Strings.
*
* @param signedPreKeyId id
* @param signedPreKeyB64 base64 encoded signedPreKey
* @param signedPreKeySigB64 base64 encoded signedPreKeySignature
* @param identityKeyB64 base64 encoded identityKey
* @param preKeysB64 HashMap of base64 encoded preKeys
*/
public OmemoBundleVAxolotlElement(int signedPreKeyId, String signedPreKeyB64, String signedPreKeySigB64, String identityKeyB64, HashMap<Integer, String> preKeysB64) {
this.signedPreKeyId = signedPreKeyId;
this.signedPreKeyB64 = signedPreKeyB64;
this.signedPreKeySignatureB64 = signedPreKeySigB64;
this.identityKeyB64 = identityKeyB64;
this.preKeysB64 = preKeysB64;
}
/**
* Constructor to create a Bundle Element from decoded byte arrays.
*
* @param signedPreKeyId id
* @param signedPreKey signedPreKey
* @param signedPreKeySig signedPreKeySignature
* @param identityKey identityKey
* @param preKeys HashMap of preKeys
*/
public OmemoBundleVAxolotlElement(int signedPreKeyId, byte[] signedPreKey, byte[] signedPreKeySig, byte[] identityKey, HashMap<Integer, byte[]> preKeys) {
this.signedPreKeyId = signedPreKeyId;
this.signedPreKey = signedPreKey;
this.signedPreKeyB64 = Base64.encodeToString(signedPreKey);
this.signedPreKeySignature = signedPreKeySig;
this.signedPreKeySignatureB64 = Base64.encodeToString(signedPreKeySignature);
this.identityKey = identityKey;
this.identityKeyB64 = Base64.encodeToString(identityKey);
this.preKeys = preKeys;
this.preKeysB64 = new HashMap<>();
for (int id : preKeys.keySet()) {
preKeysB64.put(id, Base64.encodeToString(preKeys.get(id)));
}
}
/**
* Return the signedPreKey of the OmemoBundleElement.
*
* @return signedPreKey as byte array
*/
public byte[] getSignedPreKey() {
if (signedPreKey == null) {
signedPreKey = Base64.decode(signedPreKeyB64);
}
return this.signedPreKey.clone();
}
/**
* Return the id of the signedPreKey in the bundle.
*
* @return id of signedPreKey
*/
public int getSignedPreKeyId() {
return this.signedPreKeyId;
}
/**
* Get the signature of the signedPreKey.
*
* @return signature as byte array
*/
public byte[] getSignedPreKeySignature() {
if (signedPreKeySignature == null) {
signedPreKeySignature = Base64.decode(signedPreKeySignatureB64);
}
return signedPreKeySignature.clone();
}
/**
* Return the public identityKey of the bundles owner.
* This can be used to check the signedPreKeys signature.
* The fingerprint of this key is, what the user has to verify.
*
* @return public identityKey as byte array
*/
public byte[] getIdentityKey() {
if (identityKey == null) {
identityKey = Base64.decode(identityKeyB64);
}
return this.identityKey.clone();
}
/**
* Return the HashMap of preKeys in the bundle.
* The map uses the preKeys ids as key and the preKeys as value.
*
* @return preKeys
*/
public HashMap<Integer, byte[]> getPreKeys() {
if (preKeys == null) {
preKeys = new HashMap<>();
for (int id : preKeysB64.keySet()) {
preKeys.put(id, Base64.decode(preKeysB64.get(id)));
}
}
return this.preKeys;
}
/**
* Return a single preKey from the map.
*
* @param id id of the preKey
* @return the preKey
*/
public byte[] getPreKey(int id) {
return getPreKeys().get(id);
}
@Override
public String getElementName() {
return BUNDLE;
}
@Override
public XmlStringBuilder toXML() {
XmlStringBuilder sb = new XmlStringBuilder(this).rightAngleBracket();
sb.halfOpenElement(SIGNED_PRE_KEY_PUB).attribute(SIGNED_PRE_KEY_ID, signedPreKeyId).rightAngleBracket()
.append(signedPreKeyB64).closeElement(SIGNED_PRE_KEY_PUB);
sb.openElement(SIGNED_PRE_KEY_SIG).append(signedPreKeySignatureB64).closeElement(SIGNED_PRE_KEY_SIG);
sb.openElement(IDENTITY_KEY).append(identityKeyB64).closeElement(IDENTITY_KEY);
sb.openElement(PRE_KEYS);
for (Map.Entry<Integer, String> p : this.preKeysB64.entrySet()) {
sb.halfOpenElement(PRE_KEY_PUB).attribute(PRE_KEY_ID, p.getKey()).rightAngleBracket()
.append(p.getValue()).closeElement(PRE_KEY_PUB);
}
sb.closeElement(PRE_KEYS);
sb.closeElement(this);
return sb;
}
@Override
public String getNamespace() {
return OMEMO_NAMESPACE_V_AXOLOTL;
}
@Override
public String toString() {
String out = "OmemoBundleElement[\n";
out += SIGNED_PRE_KEY_PUB + " " + SIGNED_PRE_KEY_ID + "=" + signedPreKeyId + ": " + signedPreKeyB64 + "\n";
out += SIGNED_PRE_KEY_SIG + ": " + signedPreKeySignatureB64 + "\n";
out += IDENTITY_KEY + ": " + identityKeyB64 + "\n";
out += PRE_KEYS + " (" + preKeysB64.size() + ")\n";
for (Map.Entry<Integer, String> e : preKeysB64.entrySet()) {
out += PRE_KEY_PUB + " " + PRE_KEY_ID + "=" + e.getKey() + ": " + e.getValue() + "\n";
}
return out;
}
}

View file

@ -0,0 +1,81 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.element;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.XmlStringBuilder;
/**
* A OMEMO device list update containing the IDs of all active devices of a contact.
*
* @author Paul Schaub
*/
public abstract class OmemoDeviceListElement implements ExtensionElement {
public static final String DEVICE = "device";
public static final String ID = "id";
public static final String LIST = "list";
/**
* Unmodifiable set of device IDs.
*/
private final Set<Integer> deviceIds;
public OmemoDeviceListElement(Set<Integer> deviceIds) {
deviceIds = Objects.requireNonNull(deviceIds);
this.deviceIds = Collections.unmodifiableSet(deviceIds);
}
public Set<Integer> getDeviceIds() {
return deviceIds;
}
public Set<Integer> copyDeviceIds() {
return new HashSet<>(deviceIds);
}
@Override
public String getElementName() {
return LIST;
}
@Override
public final XmlStringBuilder toXML() {
XmlStringBuilder sb = new XmlStringBuilder(this).rightAngleBracket();
for (Integer id : deviceIds) {
sb.halfOpenElement(DEVICE).attribute(ID, id).closeEmptyElement();
}
sb.closeElement(this);
return sb;
}
@Override
public final String toString() {
String out = "OmemoDeviceListElement[";
for (int i : deviceIds) {
out += i + ",";
}
return out.substring(0, out.length() - 1) + "]";
}
}

View file

@ -0,0 +1,39 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.element;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL;
import java.util.Set;
/**
* The OMEMO device list element with the legacy Axolotl namespace.
*
* @author Paul Schaub
*/
public class OmemoDeviceListVAxolotlElement extends OmemoDeviceListElement {
public OmemoDeviceListVAxolotlElement(Set<Integer> deviceIds) {
super(deviceIds);
}
@Override
public String getNamespace() {
return OMEMO_NAMESPACE_V_AXOLOTL;
}
}

View file

@ -0,0 +1,194 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.element;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.NamedElement;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smack.util.stringencoder.Base64;
import java.util.ArrayList;
/**
* Class that represents a OmemoElement.
* TODO: Move functionality here.
*
* @author Paul Schaub
*/
public abstract class OmemoElement implements ExtensionElement {
public static final int TYPE_OMEMO_PREKEY_MESSAGE = 1;
public static final int TYPE_OMEMO_MESSAGE = 0;
public static final String ENCRYPTED = "encrypted";
public static final String HEADER = "header";
public static final String SID = "sid";
public static final String KEY = "key";
public static final String RID = "rid";
public static final String PREKEY = "prekey";
public static final String IV = "iv";
public static final String PAYLOAD = "payload";
protected final OmemoElement.OmemoHeader header;
protected final byte[] payload;
/**
* Create a new OmemoMessageElement from a header and a payload.
*
* @param header header of the message
* @param payload payload
*/
public OmemoElement(OmemoElement.OmemoHeader header, byte[] payload) {
this.header = Objects.requireNonNull(header);
this.payload = payload;
}
public OmemoElement.OmemoHeader getHeader() {
return header;
}
/**
* Return the payload of the message.
*
* @return payload
*/
public byte[] getPayload() {
if (payload == null) {
return null;
}
return payload.clone();
}
public boolean isKeyTransportElement() {
return payload == null;
}
public boolean isMessageElement() {
return payload != null;
}
/**
* Header element of the message. The header contains information about the sender and the encrypted keys for
* the recipients, as well as the iv element for AES.
*/
public static class OmemoHeader implements NamedElement {
private final int sid;
private final ArrayList<Key> keys;
private final byte[] iv;
public OmemoHeader(int sid, ArrayList<OmemoHeader.Key> keys, byte[] iv) {
this.sid = sid;
this.keys = keys;
this.iv = iv;
}
/**
* Return the deviceId of the sender of the message.
*
* @return senders id
*/
public int getSid() {
return sid;
}
public ArrayList<OmemoHeader.Key> getKeys() {
return new ArrayList<>(keys);
}
public byte[] getIv() {
return iv != null ? iv.clone() : null;
}
@Override
public String getElementName() {
return HEADER;
}
@Override
public CharSequence toXML() {
XmlStringBuilder sb = new XmlStringBuilder(this);
sb.attribute(SID, getSid()).rightAngleBracket();
for (OmemoHeader.Key k : getKeys()) {
sb.element(k);
}
sb.openElement(IV).append(Base64.encodeToString(getIv())).closeElement(IV);
return sb.closeElement(this);
}
/**
* Small class to collect key (byte[]), its id and whether its a prekey or not.
*/
public static class Key implements NamedElement {
final byte[] data;
final int id;
final boolean preKey;
public Key(byte[] data, int id) {
this.data = data;
this.id = id;
this.preKey = false;
}
public Key(byte[] data, int id, boolean preKey) {
this.data = data;
this.id = id;
this.preKey = preKey;
}
public int getId() {
return this.id;
}
public byte[] getData() {
return this.data;
}
public boolean isPreKey() {
return this.preKey;
}
@Override
public String toString() {
return Integer.toString(id);
}
@Override
public String getElementName() {
return KEY;
}
@Override
public CharSequence toXML() {
XmlStringBuilder sb = new XmlStringBuilder(this);
if (isPreKey()) {
sb.attribute(PREKEY, true);
}
sb.attribute(RID, getId());
sb.rightAngleBracket();
sb.append(Base64.encodeToString(getData()));
sb.closeElement(this);
return sb;
}
}
}
}

View file

@ -0,0 +1,86 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.element;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smack.util.stringencoder.Base64;
import java.io.UnsupportedEncodingException;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL;
/**
* An OMEMO (PreKey)WhisperMessage element.
*
* @author Paul Schaub
*/
public class OmemoVAxolotlElement extends OmemoElement {
/**
* Create a new OmemoMessageElement from a header and a payload.
*
* @param header header of the message
* @param payload payload
*/
public OmemoVAxolotlElement(OmemoHeader header, byte[] payload) {
super(header, payload);
}
@Override
public String getElementName() {
return ENCRYPTED;
}
@Override
public XmlStringBuilder toXML() {
XmlStringBuilder sb = new XmlStringBuilder(this).rightAngleBracket();
sb.element(header);
if (payload != null) {
sb.openElement(PAYLOAD).append(Base64.encodeToString(payload)).closeElement(PAYLOAD);
}
sb.closeElement(this);
return sb;
}
@Override
public String getNamespace() {
return OMEMO_NAMESPACE_V_AXOLOTL;
}
@Override
public String toString() {
try {
StringBuilder s = new StringBuilder("Encrypted:\n")
.append(" header: sid: ").append(getHeader().getSid()).append('\n');
for (OmemoHeader.Key k : getHeader().getKeys()) {
s.append(" key: prekey: ").append(k.isPreKey()).append(" rid: ")
.append(k.getId()).append(' ')
.append(new String(k.getData(), StringUtils.UTF8)).append('\n');
}
s.append(" iv: ").append(new String(getHeader().getIv(), StringUtils.UTF8)).append('\n');
s.append(" payload: ").append(new String(getPayload(), StringUtils.UTF8));
return s.toString();
} catch (UnsupportedEncodingException e) {
// UTF-8 must be supported on all platforms claiming to be java compatible.
throw new AssertionError(e);
}
}
}

View file

@ -0,0 +1,23 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Classes that represent OMEMO related stanzas.
*
* @author Paul Schaub
* @see <a href="https://conversations.im/xeps/multi-end.html">XEP-0384: OMEMO</a>
*/
package org.jivesoftware.smackx.omemo.element;

View file

@ -0,0 +1,92 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.exceptions;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jxmpp.jid.BareJid;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* Exception gets thrown when we are unable to establish a session with a device for some reason.
*
* @author Paul Schaub
*/
public class CannotEstablishOmemoSessionException extends Exception {
private static final long serialVersionUID = 3165844730283295249L;
private final HashMap<BareJid, HashMap<OmemoDevice, Throwable>> failures = new HashMap<>();
private final HashMap<BareJid, ArrayList<OmemoDevice>> successes = new HashMap<>();
public CannotEstablishOmemoSessionException(OmemoDevice failed, Throwable reason) {
super();
getFailsOfContact(failed.getJid()).put(failed, reason);
}
public void addFailures(CannotEstablishOmemoSessionException otherFailures) {
for (Map.Entry<BareJid, HashMap<OmemoDevice, Throwable>> entry : otherFailures.getFailures().entrySet()) {
getFailsOfContact(entry.getKey()).putAll(entry.getValue());
}
}
public void addSuccess(OmemoDevice success) {
getSuccessesOfContact(success.getJid()).add(success);
}
public HashMap<BareJid, HashMap<OmemoDevice, Throwable>> getFailures() {
return failures;
}
public HashMap<BareJid, ArrayList<OmemoDevice>> getSuccesses() {
return successes;
}
private HashMap<OmemoDevice, Throwable> getFailsOfContact(BareJid contact) {
HashMap<OmemoDevice, Throwable> h = failures.get(contact);
if (h == null) {
h = new HashMap<>();
failures.put(contact, h);
}
return h;
}
private ArrayList<OmemoDevice> getSuccessesOfContact(BareJid contact) {
ArrayList<OmemoDevice> suc = successes.get(contact);
if (suc == null) {
suc = new ArrayList<>();
successes.put(contact, suc);
}
return suc;
}
/**
* Return true, if there is at least one recipient, which would not be able to decipher the message on any of
* their devices.
* @return boolean
*/
public boolean requiresThrowing() {
for (Map.Entry<BareJid, HashMap<OmemoDevice, Throwable>> entry : failures.entrySet()) {
ArrayList<OmemoDevice> suc = successes.get(entry.getKey());
if (suc == null || suc.isEmpty()) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,30 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.exceptions;
/**
* Exception gets thrown, when an invalid key is encountered.
*
* @author Paul Schaub
*/
public class CorruptedOmemoKeyException extends Exception {
private static final long serialVersionUID = -965658520562690429L;
public CorruptedOmemoKeyException(String message) {
super(message);
}
}

View file

@ -0,0 +1,35 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.exceptions;
/**
* Exception gets thrown when some cryptographic function failed.
*
* @author Paul Schaub
*/
public class CryptoFailedException extends Exception {
private static final long serialVersionUID = 3466888654338119924L;
public CryptoFailedException(String message) {
super(message);
}
public CryptoFailedException(Exception e) {
super(e);
}
}

View file

@ -0,0 +1,55 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.exceptions;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
public final class MultipleCryptoFailedException extends CryptoFailedException {
/**
*
*/
private static final long serialVersionUID = 1L;
private final List<CryptoFailedException> cryptoFailedExceptions;
private MultipleCryptoFailedException(String message, List<CryptoFailedException> cryptoFailedExceptions) {
super(message);
this.cryptoFailedExceptions = Collections.unmodifiableList(cryptoFailedExceptions);
if (cryptoFailedExceptions.isEmpty()) {
throw new IllegalArgumentException("Exception list must not be empty.");
}
}
public static MultipleCryptoFailedException from(List<CryptoFailedException> cryptoFailedExceptions) {
StringBuilder sb = new StringBuilder("Multiple CryptoFailedExceptions: ");
Iterator<CryptoFailedException> it = cryptoFailedExceptions.iterator();
while (it.hasNext()) {
sb.append(it.next());
if (it.hasNext()) {
sb.append(", ");
}
}
return new MultipleCryptoFailedException(sb.toString(), cryptoFailedExceptions);
}
public List<CryptoFailedException> getCryptoFailedExceptions() {
return cryptoFailedExceptions;
}
}

View file

@ -0,0 +1,24 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.exceptions;
/**
* Exception that indicates, that a MUC does not support OMEMO.
*/
public class NoOmemoSupportException extends Exception {
private static final long serialVersionUID = 1L;
}

View file

@ -0,0 +1,31 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.exceptions;
/**
* Exception that gets thrown whenever a OmemoMessage arrives that no OmemoSession was found for to decrypt it.
*
* @author Paul Schaub
*/
public class NoRawSessionException extends Exception {
private static final long serialVersionUID = 3466888654338119954L;
public NoRawSessionException(Exception e) {
super(e);
}
}

View file

@ -0,0 +1,54 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.exceptions;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import java.util.HashSet;
/**
* Exception that is thrown when the user tries to encrypt a message for a undecided device.
*
* @author Paul Schaub
*/
public class UndecidedOmemoIdentityException extends Exception {
private static final long serialVersionUID = -6591706422506879747L;
private final HashSet<OmemoDevice> devices = new HashSet<>();
public UndecidedOmemoIdentityException(OmemoDevice contact) {
super();
this.devices.add(contact);
}
/**
* Return the HashSet of untrusted devices.
*
* @return untrusted devices
*/
public HashSet<OmemoDevice> getUntrustedDevices() {
return this.devices;
}
/**
* Add all untrusted devices of another Exception to this Exceptions HashSet of untrusted devices.
*
* @param other other Exception
*/
public void join(UndecidedOmemoIdentityException other) {
this.devices.addAll(other.getUntrustedDevices());
}
}

View file

@ -0,0 +1,23 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Exceptions.
*
* @author Paul Schaub
* @see <a href="https://conversations.im/xeps/multi-end.html">XEP-0384: OMEMO</a>
*/
package org.jivesoftware.smackx.omemo.exceptions;

View file

@ -0,0 +1,124 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.internal;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
/**
* This class is used to represent device lists of contacts.
* There are active devices (a set of device ids, which was published with the last device list update)
* and inactive devices (set of devices that once were active, but are not included in recent list updates).
* Both kinds are cached by the client. When a device that was active in the last update is not included in
* a new update, it becomes an inactive device. Vice versa, inactive devices can also become active again, by
* being included in the latest device list update.
* <p>
* The client ensures, that his own device id is on the list of active devices, as soon as he gets online.
*
* @author Paul Schaub
*/
public class CachedDeviceList implements Serializable {
private static final long serialVersionUID = 3153579238321261203L;
private final Set<Integer> activeDevices;
private final Set<Integer> inactiveDevices;
public CachedDeviceList() {
this.activeDevices = new HashSet<>();
this.inactiveDevices = new HashSet<>();
}
/**
* Returns all active devices.
* Active devices are all devices that were in the latest DeviceList update.
*
* @return active devices
*/
public Set<Integer> getActiveDevices() {
return activeDevices;
}
/**
* Return all inactive devices.
* Inactive devices are devices which were in a past DeviceList update once, but were not included in
* the latest update.
*
* @return inactive devices
*/
public Set<Integer> getInactiveDevices() {
return inactiveDevices;
}
/**
* Returns an OmemoDeviceListElement containing all devices (active and inactive).
*
* @return all devices
*/
public Set<Integer> getAllDevices() {
Set<Integer> all = new HashSet<>();
all.addAll(activeDevices);
all.addAll(inactiveDevices);
return all;
}
/**
* Merge a device list update into the CachedDeviceList.
* The source code should be self explanatory.
*
* @param deviceListUpdate received device list update
*/
public void merge(Set<Integer> deviceListUpdate) {
inactiveDevices.addAll(activeDevices);
activeDevices.clear();
activeDevices.addAll(deviceListUpdate);
inactiveDevices.removeAll(activeDevices);
}
/**
* Add a device to the list of active devices.
*
* @param deviceId deviceId that will be added
*/
public void addDevice(int deviceId) {
activeDevices.add(deviceId);
}
/**
* Returns true if deviceId is either in the list of active or inactive devices.
*
* @param deviceId id
* @return true or false
*/
public boolean contains(int deviceId) {
return activeDevices.contains(deviceId) || inactiveDevices.contains(deviceId);
}
@Override
public String toString() {
String out = "active: [";
for (int id : activeDevices) {
out += id + " ";
}
out += "] inacitve: [";
for (int id : inactiveDevices) {
out += id + " ";
}
out += "]";
return out;
}
}

View file

@ -0,0 +1,84 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.internal;
import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.CIPHERMODE;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.PROVIDER;
/**
* Encapsulate Cipher and AuthTag.
*
* @author Paul Schaub
*/
public class CipherAndAuthTag {
private final byte[] key, iv, authTag;
public CipherAndAuthTag(byte[] key, byte[] iv, byte[] authTag) throws CryptoFailedException {
this.authTag = authTag;
this.key = key;
this.iv = iv;
}
public Cipher getCipher() throws CryptoFailedException {
Cipher cipher;
try {
cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
} catch (NoSuchAlgorithmException | java.security.InvalidKeyException |
InvalidAlgorithmParameterException |
NoSuchPaddingException | NoSuchProviderException e) {
throw new CryptoFailedException(e);
}
return cipher;
}
public byte[] getAuthTag() {
if (authTag != null) {
return authTag.clone();
}
return null;
}
public byte[] getKey() {
if (key != null) {
return key.clone();
}
return null;
}
public byte[] getIv() {
if (iv != null) {
return iv.clone();
}
return null;
}
}

View file

@ -0,0 +1,67 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.internal;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.TYPE_OMEMO_PREKEY_MESSAGE;
/**
* Bundles a decrypted ciphertext together with information about the message type.
*
* @author Paul Schaub
*/
public class CiphertextTuple {
private final byte[] ciphertext;
private final int messageType;
/**
* Create a new CiphertextTuple.
*
* @param ciphertext ciphertext
* @param type type
*/
public CiphertextTuple(byte[] ciphertext, int type) {
this.ciphertext = ciphertext;
this.messageType = type;
}
/**
* Return the ciphertext.
*
* @return ciphertext
*/
public byte[] getCiphertext() {
return ciphertext;
}
/**
* Return the messageType.
*
* @return messageType
*/
public int getMessageType() {
return this.messageType;
}
/**
* Returns true if this is a preKeyMessage.
*
* @return preKeyMessage?
*/
public boolean isPreKeyMessage() {
return getMessageType() == TYPE_OMEMO_PREKEY_MESSAGE;
}
}

View file

@ -0,0 +1,63 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.internal;
import org.jivesoftware.smack.packet.Message;
/**
* Class that bundles a decrypted message together with the original message and some information about the encryption.
*
* @author Paul Schaub
*/
public class ClearTextMessage {
private final String body;
private final Message encryptedMessage;
private final OmemoMessageInformation messageInformation;
public ClearTextMessage(String message, Message original, OmemoMessageInformation messageInfo) {
this.body = message;
this.encryptedMessage = original;
this.messageInformation = messageInfo;
}
/**
* Return the body of the decrypted message.
*
* @return plaintext body
*/
public String getBody() {
return body;
}
/**
* Return the original Message object.
*
* @return original message
*/
public Message getOriginalMessage() {
return encryptedMessage;
}
/**
* Return the OmemoMessageInformation.
*
* @return omemoMessageInformation
*/
public OmemoMessageInformation getMessageInformation() {
return messageInformation;
}
}

View file

@ -0,0 +1,36 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.internal;
/**
* Wrapper for IdentityKey objects.
*
* @author Paul Schaub
*/
public class IdentityKeyWrapper {
private final Object identityKey;
public IdentityKeyWrapper(Object wrapped) {
identityKey = wrapped;
}
public Object getIdentityKey() {
return identityKey;
}
}

View file

@ -0,0 +1,77 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.internal;
import org.jxmpp.jid.BareJid;
/**
* Class that combines a BareJid and a deviceId.
*
* @author Paul Schaub
*/
public class OmemoDevice {
private final BareJid jid;
private final int deviceId;
/**
* Create a new OmemoDevice.
*
* @param jid jid of the contact
* @param deviceId deviceId if the device.
*/
public OmemoDevice(BareJid jid, int deviceId) {
this.jid = jid;
this.deviceId = deviceId;
}
/**
* Return the BareJid of the device owner.
*
* @return bareJid
*/
public BareJid getJid() {
return this.jid;
}
/**
* Return the OMEMO device Id of the device.
*
* @return deviceId
*/
public int getDeviceId() {
return this.deviceId;
}
@Override
public String toString() {
return jid.toString() + ":" + deviceId;
}
@Override
public boolean equals(Object other) {
return other instanceof OmemoDevice &&
((OmemoDevice) other).getJid().equals(this.getJid()) &&
((OmemoDevice) other).getDeviceId() == this.getDeviceId();
}
@Override
public int hashCode() {
Integer i;
i = jid.hashCode() + deviceId;
return i.hashCode();
}
}

View file

@ -0,0 +1,141 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.internal;
/**
* Class that contains information about a decrypted message (eg. which key was used, if it was a carbon...).
*
* @author Paul Schaub
*/
public class OmemoMessageInformation {
private boolean isOmemoMessage;
private IdentityKeyWrapper senderIdentityKey;
private OmemoDevice senderDevice;
private CARBON carbon = CARBON.NONE;
/**
* Empty constructor.
*/
// TOOD Move this class into smackx.omemo and make this constructor package protected. -Flow
public OmemoMessageInformation() {
}
/**
* Creates a new OmemoMessageInformation object. Its assumed, that this is about an OMEMO message.
*
* @param senderIdentityKey identityKey of the sender device
* @param senderDevice device that sent the message
* @param carbon Carbon type
*/
public OmemoMessageInformation(IdentityKeyWrapper senderIdentityKey, OmemoDevice senderDevice, CARBON carbon) {
this.senderIdentityKey = senderIdentityKey;
this.senderDevice = senderDevice;
this.carbon = carbon;
this.isOmemoMessage = true;
}
/**
* Create a new OmemoMessageInformation.
*
* @param senderIdentityKey identityKey of the sender device
* @param senderDevice device that sent the message
* @param carbon Carbon type
* @param omemo is this an omemo message?
*/
public OmemoMessageInformation(IdentityKeyWrapper senderIdentityKey, OmemoDevice senderDevice, CARBON carbon, boolean omemo) {
this(senderIdentityKey, senderDevice, carbon);
this.isOmemoMessage = omemo;
}
/**
* Return the sender devices identityKey.
*
* @return identityKey
*/
public IdentityKeyWrapper getSenderIdentityKey() {
return senderIdentityKey;
}
/**
* Set the sender devices identityKey.
*
* @param senderIdentityKey identityKey
*/
public void setSenderIdentityKey(IdentityKeyWrapper senderIdentityKey) {
this.senderIdentityKey = senderIdentityKey;
}
/**
* Return the sender device.
*
* @return sender device
*/
public OmemoDevice getSenderDevice() {
return senderDevice;
}
/**
* Return true, if this is (was) an OMEMO message.
* @return true if omemo
*/
public boolean isOmemoMessage() {
return this.isOmemoMessage;
}
/**
* Set the sender device.
*
* @param senderDevice sender device
*/
public void setSenderDevice(OmemoDevice senderDevice) {
this.senderDevice = senderDevice;
}
/**
* Return the carbon type.
*
* @return carbon type
*/
public CARBON getCarbon() {
return carbon;
}
/**
* Set the carbon type.
*
* @param carbon carbon type
*/
public void setCarbon(CARBON carbon) {
this.carbon = carbon;
}
/**
* Types of Carbon Messages.
*/
public enum CARBON {
NONE, //No carbon
SENT, //Sent carbon
RECV //Received Carbon
}
@Override
public String toString() {
return (senderDevice != null ? senderDevice.toString() : "") + " " + carbon;
}
}

View file

@ -0,0 +1,264 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.internal;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.omemo.OmemoFingerprint;
import org.jivesoftware.smackx.omemo.OmemoManager;
import org.jivesoftware.smackx.omemo.OmemoStore;
import org.jivesoftware.smackx.omemo.element.OmemoElement;
import org.jivesoftware.smackx.omemo.element.OmemoElement.OmemoHeader.Key;
import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
import org.jivesoftware.smackx.omemo.exceptions.MultipleCryptoFailedException;
import org.jivesoftware.smackx.omemo.exceptions.NoRawSessionException;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
/**
* This class represents a OMEMO session between us and another device.
*
* @param <T_IdKeyPair> IdentityKeyPair class
* @param <T_IdKey> IdentityKey class
* @param <T_PreKey> PreKey class
* @param <T_SigPreKey> SignedPreKey class
* @param <T_Sess> Session class
* @param <T_Addr> Address class
* @param <T_ECPub> Elliptic Curve PublicKey class
* @param <T_Bundle> Bundle class
* @param <T_Ciph> Cipher class
* @author Paul Schaub
*/
public abstract class OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> {
protected final T_Ciph cipher;
protected final OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore;
protected final OmemoDevice remoteDevice;
protected final OmemoManager omemoManager;
protected T_IdKey identityKey;
protected int preKeyId = -1;
/**
* Constructor used when we establish the session.
*
* @param omemoManager OmemoManager of our device
* @param omemoStore OmemoStore where we want to store the session and get key information from
* @param remoteDevice the OmemoDevice we want to establish the session with
* @param identityKey identityKey of the recipient
*/
public OmemoSession(OmemoManager omemoManager,
OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore,
OmemoDevice remoteDevice, T_IdKey identityKey) {
this(omemoManager, omemoStore, remoteDevice);
this.identityKey = identityKey;
}
/**
* Another constructor used when they establish the session with us.
*
* @param omemoManager OmemoManager of our device
* @param omemoStore OmemoStore we want to store the session and their key in
* @param remoteDevice identityKey of the partner
*/
public OmemoSession(OmemoManager omemoManager, OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore,
OmemoDevice remoteDevice) {
this.omemoManager = omemoManager;
this.omemoStore = omemoStore;
this.remoteDevice = remoteDevice;
this.cipher = createCipher(remoteDevice);
}
/**
* Try to decrypt the transported message key using the double ratchet session.
*
* @param element omemoElement
* @param keyId our keyId
* @return tuple of cipher generated from the unpacked message key and the authtag
* @throws CryptoFailedException if decryption using the double ratchet fails
* @throws NoRawSessionException if we have no session, but the element was NOT a PreKeyMessage
*/
public CipherAndAuthTag decryptTransportedKey(OmemoElement element, int keyId) throws CryptoFailedException,
NoRawSessionException {
byte[] unpackedKey = null;
List<CryptoFailedException> decryptExceptions = new ArrayList<>();
List<Key> keys = element.getHeader().getKeys();
// Find key with our ID.
for (OmemoElement.OmemoHeader.Key k : keys) {
if (k.getId() == keyId) {
try {
unpackedKey = decryptMessageKey(k.getData());
break;
} catch (CryptoFailedException e) {
// There might be multiple keys with our id, but we can only decrypt one.
// So we can't throw the exception, when decrypting the first duplicate which is not for us.
decryptExceptions.add(e);
}
}
}
if (unpackedKey == null) {
if (!decryptExceptions.isEmpty()) {
throw MultipleCryptoFailedException.from(decryptExceptions);
}
throw new CryptoFailedException("Transported key could not be decrypted, since no provided message key. Provides keys: " + keys);
}
byte[] messageKey = new byte[16];
byte[] authTag = null;
if (unpackedKey.length == 32) {
authTag = new byte[16];
//copy key part into messageKey
System.arraycopy(unpackedKey, 0, messageKey, 0, 16);
//copy tag part into authTag
System.arraycopy(unpackedKey, 16, authTag, 0,16);
} else if (element.isKeyTransportElement() && unpackedKey.length == 16) {
messageKey = unpackedKey;
} else {
throw new CryptoFailedException("MessageKey has wrong length: "
+ unpackedKey.length + ". Probably legacy auth tag format.");
}
return new CipherAndAuthTag(messageKey, element.getHeader().getIv(), authTag);
}
/**
* Use the symmetric key in cipherAndAuthTag to decrypt the payload of the omemoMessage.
* The decrypted payload will be the body of the returned Message.
*
* @param element omemoElement containing a payload.
* @param cipherAndAuthTag cipher and authentication tag.
* @return Message containing the decrypted payload in its body.
* @throws CryptoFailedException
*/
public static Message decryptMessageElement(OmemoElement element, CipherAndAuthTag cipherAndAuthTag) throws CryptoFailedException {
if (!element.isMessageElement()) {
throw new IllegalArgumentException("decryptMessageElement cannot decrypt OmemoElement which is no MessageElement!");
}
if (cipherAndAuthTag.getAuthTag() == null || cipherAndAuthTag.getAuthTag().length != 16) {
throw new CryptoFailedException("AuthenticationTag is null or has wrong length: "
+ (cipherAndAuthTag.getAuthTag() == null ? "null" : cipherAndAuthTag.getAuthTag().length));
}
byte[] encryptedBody = new byte[element.getPayload().length + 16];
byte[] payload = element.getPayload();
System.arraycopy(payload, 0, encryptedBody, 0, payload.length);
System.arraycopy(cipherAndAuthTag.getAuthTag(), 0, encryptedBody, payload.length, 16);
try {
String plaintext = new String(cipherAndAuthTag.getCipher().doFinal(encryptedBody), StringUtils.UTF8);
Message decrypted = new Message();
decrypted.setBody(plaintext);
return decrypted;
} catch (UnsupportedEncodingException | IllegalBlockSizeException | BadPaddingException e) {
throw new CryptoFailedException("decryptMessageElement could not decipher message body: "
+ e.getMessage());
}
}
/**
* Try to decrypt the message.
* First decrypt the message key using our session with the sender.
* Second use the decrypted key to decrypt the message.
* The decrypted content of the 'encrypted'-element becomes the body of the clear text message.
*
* @param element OmemoElement
* @param keyId the key we want to decrypt (usually our own device id)
* @return message as plaintext
* @throws CryptoFailedException
* @throws NoRawSessionException
*/
// TODO find solution for what we actually want to decrypt (String, Message, List<ExtensionElements>...)
public Message decryptMessageElement(OmemoElement element, int keyId) throws CryptoFailedException, NoRawSessionException {
if (!element.isMessageElement()) {
throw new IllegalArgumentException("OmemoElement is not a messageElement!");
}
CipherAndAuthTag cipherAndAuthTag = decryptTransportedKey(element, keyId);
return decryptMessageElement(element, cipherAndAuthTag);
}
/**
* Create a new SessionCipher used to encrypt/decrypt keys. The cipher typically implements the ratchet and KDF-chains.
*
* @param contact OmemoDevice
* @return SessionCipher
*/
public abstract T_Ciph createCipher(OmemoDevice contact);
/**
* Get the id of the preKey used to establish the session.
*
* @return id
*/
public int getPreKeyId() {
return this.preKeyId;
}
/**
* Encrypt a message key for the recipient. This key can be deciphered by the recipient with its corresponding
* session cipher. The key is then used to decipher the message.
*
* @param messageKey serialized key to encrypt
* @return A CiphertextTuple containing the ciphertext and the messageType
* @throws CryptoFailedException
*/
public abstract CiphertextTuple encryptMessageKey(byte[] messageKey) throws CryptoFailedException;
/**
* Decrypt a messageKey using our sessionCipher. We can use that key to decipher the actual message.
* Same as encryptMessageKey, just the other way round.
*
* @param encryptedKey encrypted key
* @return serialized decrypted key or null
* @throws CryptoFailedException when decryption fails.
* @throws NoRawSessionException when no session was found in the double ratchet library
*/
public abstract byte[] decryptMessageKey(byte[] encryptedKey) throws CryptoFailedException, NoRawSessionException;
/**
* Return the identityKey of the session.
*
* @return identityKey
*/
public T_IdKey getIdentityKey() {
return identityKey;
}
/**
* Set the identityKey of the remote device.
* @param identityKey identityKey
*/
public void setIdentityKey(T_IdKey identityKey) {
this.identityKey = identityKey;
}
/**
* Return the fingerprint of the contacts identityKey.
*
* @return fingerprint or null
*/
public OmemoFingerprint getFingerprint() {
return (this.identityKey != null ? omemoStore.keyUtil().getFingerprint(this.identityKey) : null);
}
}

View file

@ -0,0 +1,23 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Classes that are used internally to arrange objects.
*
* @author Paul Schaub
* @see <a href="https://conversations.im/xeps/multi-end.html">XEP-0384: OMEMO</a>
*/
package org.jivesoftware.smackx.omemo.internal;

View file

@ -0,0 +1,48 @@
/**
*
* 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.listener;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag;
import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation;
/**
* Listener interface that allows implementations to receive decrypted OMEMO messages.
*
* @author Paul Schaub
*/
public interface OmemoMessageListener {
/**
* Gets called, whenever an OmemoMessage has been received and was successfully decrypted.
*
* @param decryptedBody Decrypted body
* @param encryptedMessage Encrypted Message
* @param wrappingMessage Wrapping carbon message, in case the message was a carbon copy, else null.
* @param omemoInformation Information about the messages encryption etc.
*/
void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation);
/**
* Gets called, whenever an OmemoElement without a body (an OmemoKeyTransportElement) is received.
*
* @param cipherAndAuthTag transported Cipher along with an optional AuthTag
* @param message Message that contained the KeyTransport
* @param wrappingMessage Wrapping message (eg. carbon), or null
* @param omemoInformation Information about the messages encryption etc.
*/
void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation);
}

View file

@ -0,0 +1,54 @@
/**
*
* 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.listener;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag;
import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation;
import org.jxmpp.jid.BareJid;
/**
* Listener interface that allows implementations to receive decrypted OMEMO MUC messages.
* @author Paul Schaub
*/
public interface OmemoMucMessageListener {
/**
* Gets called whenever an OMEMO message has been received in a MultiUserChat and successfully decrypted.
* @param muc MultiUserChat the message was sent in
* @param from the bareJid of the sender
* @param decryptedBody the decrypted Body of the message
* @param message the original message with encrypted element
* @param wrappingMessage in case of a carbon copy, this is the wrapping message
* @param omemoInformation information about the encryption of the message
*/
void onOmemoMucMessageReceived(MultiUserChat muc, BareJid from, String decryptedBody, Message message,
Message wrappingMessage, OmemoMessageInformation omemoInformation);
/**
* Gets called, whenever an OmemoElement without a body (an OmemoKeyTransportElement) is received.
*
* @param muc MultiUserChat the message was sent in
* @param from bareJid of the sender
* @param cipherAndAuthTag transportedKey along with an optional authTag
* @param message Message that contained the KeyTransport
* @param wrappingMessage Wrapping message (eg. carbon), or null
* @param omemoInformation Information about the messages encryption etc.
*/
void onOmemoKeyTransportReceived(MultiUserChat muc, BareJid from, CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation);
}

View file

@ -0,0 +1,23 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Callbacks and listeners.
*
* @author Paul Schaub
* @see <a href="https://conversations.im/xeps/multi-end.html">XEP-XXXX: OMEMO</a>
*/
package org.jivesoftware.smackx.omemo.listener;

View file

@ -0,0 +1,26 @@
/**
*
* 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.
*/
/**
* Classes and interfaces for OMEMO Encryption. This module consists of the
* XMPP logic and some abstract crypto classes that have to be implemented
* using concrete crypto libraries (like signal-protocol-java or olm).
* See smack-omemo-signal for a concrete implementation (GPL licensed).
*
* @author Paul Schaub
* @see <a href="https://conversations.im/xeps/multi-end.html">XEP-0384: OMEMO</a>
*/
package org.jivesoftware.smackx.omemo;

View file

@ -0,0 +1,99 @@
/**
*
* 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.provider;
import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smackx.omemo.element.OmemoBundleVAxolotlElement;
import org.xmlpull.v1.XmlPullParser;
import java.util.HashMap;
import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.BUNDLE;
import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.IDENTITY_KEY;
import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.PRE_KEYS;
import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.PRE_KEY_ID;
import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.PRE_KEY_PUB;
import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.SIGNED_PRE_KEY_ID;
import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.SIGNED_PRE_KEY_PUB;
import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.SIGNED_PRE_KEY_SIG;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
/**
* Smack ExtensionProvider that parses OMEMO bundle element into OmemoBundleElement objects.
*
* @author Paul Schaub
*/
public class OmemoBundleVAxolotlProvider extends ExtensionElementProvider<OmemoBundleVAxolotlElement> {
@Override
public OmemoBundleVAxolotlElement parse(XmlPullParser parser, int initialDepth) throws Exception {
boolean stop = false;
boolean inPreKeys = false;
int signedPreKeyId = -1;
String signedPreKey = null;
String signedPreKeySignature = null;
String identityKey = null;
HashMap<Integer, String> preKeys = new HashMap<>();
while (!stop) {
int tag = parser.next();
String name = parser.getName();
switch (tag) {
case START_TAG:
// <signedPreKeyPublic>
if (name.equals(SIGNED_PRE_KEY_PUB)) {
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).equals(SIGNED_PRE_KEY_ID)) {
int id = Integer.parseInt(parser.getAttributeValue(i));
signedPreKey = parser.nextText();
signedPreKeyId = id;
}
}
}
// <bundleGetSignedPreKeySignature>
else if (name.equals(SIGNED_PRE_KEY_SIG)) {
signedPreKeySignature = parser.nextText();
}
// <deserializeIdentityKey>
else if (name.equals(IDENTITY_KEY)) {
identityKey = parser.nextText();
}
// <deserializeECPublicKeys>
else if (name.equals(PRE_KEYS)) {
inPreKeys = true;
}
// <preKeyPublic preKeyId='424242'>
else if (inPreKeys && name.equals(PRE_KEY_PUB)) {
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).equals(PRE_KEY_ID)) {
preKeys.put(Integer.parseInt(parser.getAttributeValue(i)),
parser.nextText());
}
}
}
break;
case END_TAG:
if (name.equals(BUNDLE)) {
stop = true;
}
break;
}
}
return new OmemoBundleVAxolotlElement(signedPreKeyId, signedPreKey, signedPreKeySignature, identityKey, preKeys);
}
}

View file

@ -0,0 +1,66 @@
/**
*
* 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.provider;
import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smackx.omemo.element.OmemoDeviceListVAxolotlElement;
import org.xmlpull.v1.XmlPullParser;
import static org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement.DEVICE;
import static org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement.ID;
import static org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement.LIST;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import java.util.HashSet;
import java.util.Set;
/**
* Smack ExtensionProvider that parses OMEMO device list element into OmemoDeviceListElement objects.
*
* @author Paul Schaub
*/
public class OmemoDeviceListVAxolotlProvider extends ExtensionElementProvider<OmemoDeviceListVAxolotlElement> {
@Override
public OmemoDeviceListVAxolotlElement parse(XmlPullParser parser, int initialDepth) throws Exception {
Set<Integer> deviceListIds = new HashSet<>();
boolean stop = false;
while (!stop) {
int tag = parser.next();
String name = parser.getName();
switch (tag) {
case START_TAG:
if (name.equals(DEVICE)) {
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).equals(ID)) {
Integer deviceId = Integer.parseInt(parser.getAttributeValue(i));
deviceListIds.add(deviceId);
}
}
}
break;
case END_TAG:
if (name.equals(LIST)) {
stop = true;
}
break;
}
}
return new OmemoDeviceListVAxolotlElement(deviceListIds);
}
}

View file

@ -0,0 +1,95 @@
/**
*
* 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.provider;
import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smack.util.stringencoder.Base64;
import org.jivesoftware.smackx.omemo.element.OmemoVAxolotlElement;
import org.xmlpull.v1.XmlPullParser;
import java.util.ArrayList;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.ENCRYPTED;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.HEADER;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.IV;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.KEY;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.PAYLOAD;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.PREKEY;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.RID;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.SID;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
/**
* Smack ExtensionProvider that parses incoming OMEMO Message element into OmemoMessageElement objects.
*
* @author Paul Schaub
*/
public class OmemoVAxolotlProvider extends ExtensionElementProvider<OmemoVAxolotlElement> {
@Override
public OmemoVAxolotlElement parse(XmlPullParser parser, int initialDepth) throws Exception {
boolean inEncrypted = true;
int sid = -1;
ArrayList<OmemoVAxolotlElement.OmemoHeader.Key> keys = new ArrayList<>();
byte[] iv = null;
byte[] payload = null;
while (inEncrypted) {
int tag = parser.next();
String name = parser.getName();
switch (tag) {
case START_TAG:
switch (name) {
case HEADER:
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).equals(SID)) {
sid = Integer.parseInt(parser.getAttributeValue(i));
}
}
break;
case KEY:
boolean prekey = false;
int rid = -1;
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).equals(PREKEY)) {
prekey = Boolean.parseBoolean(parser.getAttributeValue(i));
} else if (parser.getAttributeName(i).equals(RID)) {
rid = Integer.parseInt(parser.getAttributeValue(i));
}
}
keys.add(new OmemoVAxolotlElement.OmemoHeader.Key(Base64.decode(parser.nextText()), rid, prekey));
break;
case IV:
iv = Base64.decode(parser.nextText());
break;
case PAYLOAD:
payload = Base64.decode(parser.nextText());
break;
}
break;
case END_TAG:
if (name.equals(ENCRYPTED)) {
inEncrypted = false;
}
break;
}
}
OmemoVAxolotlElement.OmemoHeader header = new OmemoVAxolotlElement.OmemoHeader(sid, keys, iv);
return new OmemoVAxolotlElement(header, payload);
}
}

View file

@ -0,0 +1,23 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Provider classes that parse OMEMO related stanzas into objects.
*
* @author Paul Schaub
* @see <a href="https://conversations.im/xeps/multi-end.html">XEP-0384: OMEMO</a>
*/
package org.jivesoftware.smackx.omemo.provider;

View file

@ -0,0 +1,63 @@
/**
*
* 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.util;
/**
* Some constants related to OMEMO.
* @author Paul Schaub
*/
public final class OmemoConstants {
//Constants
/**
* Omemo related namespace.
*/
public static final String OMEMO_NAMESPACE_V_AXOLOTL = "eu.siacs.conversations.axolotl";
public static final String OMEMO = "OMEMO";
//PubSub Node names
public static final String PEP_NODE_DEVICE_LIST = OMEMO_NAMESPACE_V_AXOLOTL + ".devicelist";
public static final String PEP_NODE_DEVICE_LIST_NOTIFY = PEP_NODE_DEVICE_LIST + "+notify";
public static final String PEP_NODE_BUNDLES = OMEMO_NAMESPACE_V_AXOLOTL + ".bundles";
/**
* How many preKeys do we want to publish?
*/
public static final int TARGET_PRE_KEY_COUNT = 100;
/**
* Return the node name of the PEP node containing the device bundle of the device with device id deviceId.
*
* @param deviceId id of the device
* @return node name of the devices bundle node
*/
public static String PEP_NODE_BUNDLE_FROM_DEVICE_ID(int deviceId) {
return PEP_NODE_BUNDLES + ":" + deviceId;
}
public static final String BODY_OMEMO_HINT = "I sent you an OMEMO encrypted message but your client doesnt seem to support that. Find more information on https://conversations.im/omemo";
/**
* Information about the keys used for message encryption.
*/
public static final class Crypto {
public static final String KEYTYPE = "AES";
public static final int KEYLENGTH = 128;
public static final String CIPHERMODE = "AES/GCM/NoPadding";
public static final String PROVIDER = "BC";
}
}

View file

@ -0,0 +1,450 @@
/**
*
* 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.util;
import org.jivesoftware.smackx.omemo.OmemoFingerprint;
import org.jivesoftware.smackx.omemo.OmemoManager;
import org.jivesoftware.smackx.omemo.OmemoStore;
import org.jivesoftware.smackx.omemo.element.OmemoBundleVAxolotlElement;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.internal.OmemoSession;
import org.jxmpp.stringprep.XmppStringprepException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Class that is used to convert bytes to keys and vice versa.
*
* @param <T_IdKeyPair> IdentityKeyPair class
* @param <T_IdKey> IdentityKey class
* @param <T_PreKey> PreKey class
* @param <T_SigPreKey> SignedPreKey class
* @param <T_Sess> Session class
* @param <T_Addr> Address class
* @param <T_ECPub> Elliptic Curve PublicKey class
* @param <T_Bundle> Bundle class
* @param <T_Ciph> Cipher class
* @author Paul Schaub
*/
public abstract class OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> {
private static final Logger LOGGER = Logger.getLogger(OmemoKeyUtil.class.getName());
public final Bundle BUNDLE = new Bundle();
/**
* Bundle related methods.
*/
public class Bundle {
/**
* Extract an IdentityKey from a OmemoBundleElement.
*
* @param bundle OmemoBundleElement
* @return identityKey
* @throws CorruptedOmemoKeyException if the key is damaged/malformed
*/
public T_IdKey identityKey(OmemoBundleVAxolotlElement bundle) throws CorruptedOmemoKeyException {
return identityKeyFromBytes(bundle.getIdentityKey());
}
/**
* Extract a signedPreKey from an OmemoBundleElement.
*
* @param bundle OmemoBundleElement
* @return singedPreKey
* @throws CorruptedOmemoKeyException if the key is damaged/malformed
*/
public T_ECPub signedPreKeyPublic(OmemoBundleVAxolotlElement bundle) throws CorruptedOmemoKeyException {
return signedPreKeyPublicFromBytes(bundle.getSignedPreKey());
}
/**
* Extract the id of the transported signedPreKey from the bundle.
*
* @param bundle OmemoBundleElement
* @return signedPreKeyId
*/
public int signedPreKeyId(OmemoBundleVAxolotlElement bundle) {
return bundle.getSignedPreKeyId();
}
/**
* Extract the signature of the signedPreKey in the bundle as a byte array.
*
* @param bundle OmemoBundleElement
* @return signature
*/
public byte[] signedPreKeySignature(OmemoBundleVAxolotlElement bundle) {
return bundle.getSignedPreKeySignature();
}
/**
* Extract the preKey with id 'keyId' from the bundle.
*
* @param bundle OmemoBundleElement
* @param keyId id of the preKey
* @return the preKey
* @throws CorruptedOmemoKeyException when the key cannot be parsed from bytes
*/
public T_ECPub preKeyPublic(OmemoBundleVAxolotlElement bundle, int keyId) throws CorruptedOmemoKeyException {
return preKeyPublicFromBytes(bundle.getPreKey(keyId));
}
/**
* Break up the OmemoBundleElement into a list of crypto-lib specific bundles (T_PreKey).
* In case of the signal library, we break the OmemoBundleElement in ~100 PreKeyBundles (one for every transported
* preKey).
*
* @param bundle OmemoBundleElement containing multiple PreKeys
* @param contact Contact that the bundle belongs to
* @return a HashMap with one T_Bundle per preKey and the preKeyId as key
* @throws CorruptedOmemoKeyException when one of the keys cannot be parsed
*/
public HashMap<Integer, T_Bundle> bundles(OmemoBundleVAxolotlElement bundle, OmemoDevice contact) throws CorruptedOmemoKeyException {
HashMap<Integer, T_Bundle> bundles = new HashMap<>();
for (int deviceId : bundle.getPreKeys().keySet()) {
try {
bundles.put(deviceId, bundleFromOmemoBundle(bundle, contact, deviceId));
} catch (CorruptedOmemoKeyException e) {
LOGGER.log(Level.INFO, "Cannot parse PreKeyBundle: " + e.getMessage());
}
}
if (bundles.size() == 0) {
throw new CorruptedOmemoKeyException("Bundle contained no valid preKeys.");
}
return bundles;
}
}
/**
* Deserialize an identityKeyPair from a byte array.
*
* @param data byte array
* @return IdentityKeyPair (T_IdKeyPair)
* @throws CorruptedOmemoKeyException if the key is damaged of malformed
*/
public abstract T_IdKeyPair identityKeyPairFromBytes(byte[] data) throws CorruptedOmemoKeyException;
/**
* Deserialize an identityKey from a byte array.
*
* @param data byte array
* @return identityKey (T_IdKey)
* @throws CorruptedOmemoKeyException if the key is damaged or malformed
*/
public abstract T_IdKey identityKeyFromBytes(byte[] data) throws CorruptedOmemoKeyException;
/**
* Serialize an identityKey into bytes.
*
* @param identityKey idKey
* @return bytes
*/
public abstract byte[] identityKeyToBytes(T_IdKey identityKey);
/**
* Deserialize an elliptic curve public key from bytes.
*
* @param data bytes
* @return elliptic curve public key (T_ECPub)
* @throws CorruptedOmemoKeyException if the key is damaged or malformed
*/
public abstract T_ECPub ellipticCurvePublicKeyFromBytes(byte[] data) throws CorruptedOmemoKeyException;
/**
* Deserialize a public preKey from bytes.
*
* @param data preKey as bytes
* @return preKey
* @throws CorruptedOmemoKeyException if the key is damaged or malformed
*/
public T_ECPub preKeyPublicFromBytes(byte[] data) throws CorruptedOmemoKeyException {
return ellipticCurvePublicKeyFromBytes(data);
}
/**
* Serialize a preKey into a byte array.
*
* @param preKey preKey
* @return byte[]
*/
public abstract byte[] preKeyToBytes(T_PreKey preKey);
/**
* Deserialize a preKey from a byte array.
*
* @param bytes byte array
* @return deserialized preKey
* @throws IOException when something goes wrong
*/
public abstract T_PreKey preKeyFromBytes(byte[] bytes) throws IOException;
/**
* Generate 'count' new PreKeys beginning with id 'startId'.
* These preKeys are published and can be used by contacts to establish sessions with us.
*
* @param startId start id
* @param count how many keys do we want to generate
* @return Map of new preKeys
*/
public abstract HashMap<Integer, T_PreKey> generateOmemoPreKeys(int startId, int count);
/**
* Generate a new signed preKey.
*
* @param identityKeyPair identityKeyPair used to sign the preKey
* @param signedPreKeyId id that the preKey will have
* @return signedPreKey
* @throws CorruptedOmemoKeyException when the identityKeyPair is invalid
*/
public abstract T_SigPreKey generateOmemoSignedPreKey(T_IdKeyPair identityKeyPair, int signedPreKeyId) throws CorruptedOmemoKeyException;
/**
* Deserialize a public signedPreKey from bytes.
*
* @param data bytes
* @return signedPreKey
* @throws CorruptedOmemoKeyException if the key is damaged or malformed
*/
public T_ECPub signedPreKeyPublicFromBytes(byte[] data) throws CorruptedOmemoKeyException {
return ellipticCurvePublicKeyFromBytes(data);
}
/**
* Deserialize a signedPreKey from a byte array.
*
* @param data byte array
* @return deserialized signed preKey
* @throws IOException when something goes wrong
*/
public abstract T_SigPreKey signedPreKeyFromBytes(byte[] data) throws IOException;
/**
* Serialize a signedPreKey into a byte array.
*
* @param sigPreKey signedPreKey
* @return byte array
*/
public abstract byte[] signedPreKeyToBytes(T_SigPreKey sigPreKey);
/**
* Build a crypto-lib specific PreKeyBundle (T_Bundle) using a PreKey from the OmemoBundleElement 'bundle'.
* The PreKeyBundle will contain the identityKey, signedPreKey and signature, as well as a preKey
* from the OmemoBundleElement.
*
* @param bundle OmemoBundleElement
* @param contact Contact that the bundle belongs to
* @param keyId id of the preKey that will be selected from the OmemoBundleElement and that the PreKeyBundle will contain
* @return PreKeyBundle (T_PreKey)
* @throws CorruptedOmemoKeyException if some key is damaged or malformed
*/
public abstract T_Bundle bundleFromOmemoBundle(OmemoBundleVAxolotlElement bundle, OmemoDevice contact, int keyId) throws CorruptedOmemoKeyException;
/**
* Extract the signature from a signedPreKey.
*
* @param signedPreKey signedPreKey
* @return signature as byte array
*/
public abstract byte[] signedPreKeySignatureFromKey(T_SigPreKey signedPreKey);
/**
* Generate a new IdentityKeyPair. We should always have only one pair and usually keep this for a long time.
*
* @return identityKeyPair
*/
public abstract T_IdKeyPair generateOmemoIdentityKeyPair();
/**
* return the id of the given signedPreKey.
*
* @param signedPreKey key
* @return id of the key
*/
public abstract int signedPreKeyIdFromKey(T_SigPreKey signedPreKey);
/**
* serialize an identityKeyPair into bytes.
*
* @param identityKeyPair identityKeyPair
* @return byte array
*/
public abstract byte[] identityKeyPairToBytes(T_IdKeyPair identityKeyPair);
/**
* Extract the public identityKey from an identityKeyPair.
*
* @param pair keyPair
* @return public key of the pair
*/
public abstract T_IdKey identityKeyFromPair(T_IdKeyPair pair);
/**
* Prepare an identityKey for transport in an OmemoBundleElement (serialize it).
*
* @param identityKey identityKey that will be transported
* @return key as byte array
*/
public abstract byte[] identityKeyForBundle(T_IdKey identityKey);
/**
* Prepare an elliptic curve preKey for transport in an OmemoBundleElement.
*
* @param preKey key
* @return key as byte array
*/
public abstract byte[] preKeyPublicKeyForBundle(T_ECPub preKey);
/**
* Prepare a preKey for transport in an OmemoBundleElement.
*
* @param preKey preKey
* @return key as byte array
*/
public abstract byte[] preKeyForBundle(T_PreKey preKey);
/**
* Prepare a whole bunche of preKeys for transport.
*
* @param preKeyHashMap HashMap of preKeys
* @return HashMap of byte arrays but with the same keyIds as key
*/
public HashMap<Integer, byte[]> preKeyPublisKeysForBundle(HashMap<Integer, T_PreKey> preKeyHashMap) {
HashMap<Integer, byte[]> out = new HashMap<>();
for (Map.Entry<Integer, T_PreKey> e : preKeyHashMap.entrySet()) {
out.put(e.getKey(), preKeyForBundle(e.getValue()));
}
return out;
}
/**
* Prepare a public signedPreKey for transport in a bundle.
*
* @param signedPreKey signedPrekey
* @return signedPreKey as byte array
*/
public abstract byte[] signedPreKeyPublicForBundle(T_SigPreKey signedPreKey);
/**
* Return the fingerprint of an identityKey.
*
* @param identityKey identityKey
* @return fingerprint of the key
*/
public abstract OmemoFingerprint getFingerprint(T_IdKey identityKey);
/**
* Create a new crypto-specific Session object.
*
* @param omemoManager omemoManager of our device.
* @param omemoStore omemoStore where we can save the session, get keys from etc.
* @param from the device we want to create the session with.
* @return a new session
*/
public abstract OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>
createOmemoSession(OmemoManager omemoManager, OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore,
OmemoDevice from);
/**
* Create a new concrete OmemoSession with a contact.
*
* @param omemoManager omemoManager of our device.
* @param omemoStore omemoStore
* @param device device to establish the session with
* @param identityKey identityKey of the device
* @return concrete OmemoSession
*/
public abstract OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>
createOmemoSession(OmemoManager omemoManager, OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore,
OmemoDevice device, T_IdKey identityKey);
/**
* Deserialize a raw OMEMO Session from bytes.
*
* @param data bytes
* @return raw OMEMO Session
* @throws IOException when something goes wrong
*/
public abstract T_Sess rawSessionFromBytes(byte[] data) throws IOException;
/**
* Serialize a raw OMEMO session into a byte array.
*
* @param session raw session
* @return byte array
*/
public abstract byte[] rawSessionToBytes(T_Sess session);
/**
* Convert an OmemoDevice to a crypto-lib specific contact format.
*
* @param contact omemoContact
* @return crypto-lib specific contact object
*/
public abstract T_Addr omemoDeviceAsAddress(OmemoDevice contact);
/**
* Convert a crypto-lib specific contact object into an OmemoDevice.
*
* @param address contact
* @return as OmemoDevice
* @throws XmppStringprepException if the address is not a valid BareJid
*/
public abstract OmemoDevice addressAsOmemoDevice(T_Addr address) throws XmppStringprepException;
public static String prettyFingerprint(OmemoFingerprint fingerprint) {
return prettyFingerprint(fingerprint.toString());
}
/**
* Split the fingerprint in blocks of 8 characters with spaces between.
*
* @param ugly fingerprint as continuous string
* @return fingerprint with spaces for better readability
*/
public static String prettyFingerprint(String ugly) {
if (ugly == null) return null;
String pretty = "";
for (int i = 0; i < 8; i++) {
if (i != 0) pretty += " ";
pretty += ugly.substring(8 * i, 8 * (i + 1));
}
return pretty;
}
/**
* Add integers modulo MAX_VALUE.
*
* @param value base integer
* @param added value that is added to the base value
* @return (value plus added) modulo Integer.MAX_VALUE
*/
public static int addInBounds(int value, int added) {
int avail = Integer.MAX_VALUE - value;
if (avail < added) {
return added - avail;
} else {
return value + added;
}
}
}

View file

@ -0,0 +1,252 @@
/**
*
* 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.util;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.omemo.OmemoManager;
import org.jivesoftware.smackx.omemo.OmemoStore;
import org.jivesoftware.smackx.omemo.element.OmemoVAxolotlElement;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException;
import org.jivesoftware.smackx.omemo.internal.CiphertextTuple;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.internal.OmemoSession;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.util.ArrayList;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.CIPHERMODE;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYLENGTH;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.PROVIDER;
/**
* Class used to build OMEMO messages.
*
* @param <T_IdKeyPair> IdentityKeyPair class
* @param <T_IdKey> IdentityKey class
* @param <T_PreKey> PreKey class
* @param <T_SigPreKey> SignedPreKey class
* @param <T_Sess> Session class
* @param <T_Addr> Address class
* @param <T_ECPub> Elliptic Curve PublicKey class
* @param <T_Bundle> Bundle class
* @param <T_Ciph> Cipher class
* @author Paul Schaub
*/
public class OmemoMessageBuilder<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> {
private final OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore;
private final OmemoManager omemoManager;
private byte[] messageKey = generateKey();
private byte[] initializationVector = generateIv();
private byte[] ciphertextMessage;
private final ArrayList<OmemoVAxolotlElement.OmemoHeader.Key> keys = new ArrayList<>();
/**
* Create a OmemoMessageBuilder.
*
* @param omemoManager OmemoManager of our device.
* @param omemoStore OmemoStore.
* @param aesKey AES key that will be transported to the recipient. This is used eg. to encrypt the body.
* @param iv IV
* @throws NoSuchPaddingException
* @throws BadPaddingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
* @throws IllegalBlockSizeException
* @throws UnsupportedEncodingException
* @throws NoSuchProviderException
* @throws InvalidAlgorithmParameterException
*/
public OmemoMessageBuilder(OmemoManager omemoManager,
OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore,
byte[] aesKey, byte[] iv)
throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException,
UnsupportedEncodingException, NoSuchProviderException, InvalidAlgorithmParameterException {
this.omemoStore = omemoStore;
this.omemoManager = omemoManager;
this.messageKey = aesKey;
this.initializationVector = iv;
}
/**
* Create a new OmemoMessageBuilder with random IV and AES key.
*
* @param omemoManager omemoManager of our device.
* @param omemoStore omemoStore.
* @param message Messages body.
* @throws NoSuchPaddingException
* @throws BadPaddingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
* @throws IllegalBlockSizeException
* @throws UnsupportedEncodingException
* @throws NoSuchProviderException
* @throws InvalidAlgorithmParameterException
*/
public OmemoMessageBuilder(OmemoManager omemoManager,
OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore, String message)
throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException,
UnsupportedEncodingException, NoSuchProviderException, InvalidAlgorithmParameterException {
this.omemoManager = omemoManager;
this.omemoStore = omemoStore;
this.setMessage(message);
}
/**
* Create an AES messageKey and use it to encrypt the message.
* Optionally append the Auth Tag of the encrypted message to the messageKey afterwards.
*
* @param message content of the message
* @throws NoSuchPaddingException When no Cipher could be instantiated.
* @throws NoSuchAlgorithmException when no Cipher could be instantiated.
* @throws NoSuchProviderException when BouncyCastle could not be found.
* @throws InvalidAlgorithmParameterException when the Cipher could not be initialized
* @throws InvalidKeyException when the generated key is invalid
* @throws UnsupportedEncodingException when UTF8 is unavailable
* @throws BadPaddingException when cipher.doFinal gets wrong padding
* @throws IllegalBlockSizeException when cipher.doFinal gets wrong Block size.
*/
public void setMessage(String message) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, InvalidKeyException, UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException {
if (message == null) {
return;
}
//Encrypt message body
SecretKey secretKey = new SecretKeySpec(messageKey, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(initializationVector);
Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
byte[] body;
byte[] ciphertext;
body = (message.getBytes(StringUtils.UTF8));
ciphertext = cipher.doFinal(body);
byte[] clearKeyWithAuthTag = new byte[messageKey.length + 16];
byte[] cipherTextWithoutAuthTag = new byte[ciphertext.length - 16];
System.arraycopy(messageKey, 0, clearKeyWithAuthTag, 0, 16);
System.arraycopy(ciphertext, 0, cipherTextWithoutAuthTag, 0, cipherTextWithoutAuthTag.length);
System.arraycopy(ciphertext, ciphertext.length - 16, clearKeyWithAuthTag, 16, 16);
ciphertextMessage = cipherTextWithoutAuthTag;
messageKey = clearKeyWithAuthTag;
}
/**
* Add a new recipient device to the message.
*
* @param device recipient device
* @throws CryptoFailedException when encrypting the messageKey fails
* @throws UndecidedOmemoIdentityException
* @throws CorruptedOmemoKeyException
*/
public void addRecipient(OmemoDevice device) throws CryptoFailedException, UndecidedOmemoIdentityException, CorruptedOmemoKeyException {
addRecipient(device, false);
}
/**
* Add a new recipient device to the message.
* @param device recipient device
* @param ignoreTrust ignore current trust state? Useful for keyTransportMessages that are sent to repair a session
* @throws CryptoFailedException
* @throws UndecidedOmemoIdentityException
* @throws CorruptedOmemoKeyException
*/
public void addRecipient(OmemoDevice device, boolean ignoreTrust) throws
CryptoFailedException, UndecidedOmemoIdentityException, CorruptedOmemoKeyException {
OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> session =
omemoStore.getOmemoSessionOf(omemoManager, device);
if (session != null) {
if (!ignoreTrust && !omemoStore.isDecidedOmemoIdentity(omemoManager, device, session.getIdentityKey())) {
//Warn user of undecided device
throw new UndecidedOmemoIdentityException(device);
}
if (!ignoreTrust && omemoStore.isTrustedOmemoIdentity(omemoManager, device, session.getIdentityKey())) {
//Encrypt key and save to header
CiphertextTuple encryptedKey = session.encryptMessageKey(messageKey);
keys.add(new OmemoVAxolotlElement.OmemoHeader.Key(encryptedKey.getCiphertext(), device.getDeviceId(), encryptedKey.isPreKeyMessage()));
}
}
}
/**
* Assemble an OmemoMessageElement from the current state of the builder.
*
* @return OmemoMessageElement
*/
public OmemoVAxolotlElement finish() {
OmemoVAxolotlElement.OmemoHeader header = new OmemoVAxolotlElement.OmemoHeader(
omemoManager.getDeviceId(),
keys,
initializationVector
);
return new OmemoVAxolotlElement(header, ciphertextMessage);
}
/**
* Generate a new AES key used to encrypt the message.
*
* @return new AES key
* @throws NoSuchAlgorithmException
*/
public static byte[] generateKey() throws NoSuchAlgorithmException {
KeyGenerator generator = KeyGenerator.getInstance(KEYTYPE);
generator.init(KEYLENGTH);
return generator.generateKey().getEncoded();
}
/**
* Generate a 16 byte initialization vector for AES encryption.
*
* @return iv
*/
public static byte[] generateIv() {
SecureRandom random = new SecureRandom();
byte[] iv = new byte[16];
random.nextBytes(iv);
return iv;
}
public byte[] getCiphertextMessage() {
return ciphertextMessage;
}
public byte[] getMessageKey() {
return messageKey;
}
}

View file

@ -0,0 +1,23 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Helper classes and other stuff.
*
* @author Paul Schaub
* @see <a href="https://conversations.im/xeps/multi-end.html">XEP-XXXX: OMEMO</a>
*/
package org.jivesoftware.smackx.omemo.util;

View file

@ -0,0 +1,19 @@
<?xml version="1.0"?>
<!-- Providers for OMEMO -->
<smackProviders>
<extensionProvider>
<elementName>encrypted</elementName>
<namespace>eu.siacs.conversations.axolotl</namespace>
<className>org.jivesoftware.smackx.omemo.provider.OmemoVAxolotlProvider</className>
</extensionProvider>
<extensionProvider>
<elementName>list</elementName>
<namespace>eu.siacs.conversations.axolotl</namespace>
<className>org.jivesoftware.smackx.omemo.provider.OmemoDeviceListVAxolotlProvider</className>
</extensionProvider>
<extensionProvider>
<elementName>bundle</elementName>
<namespace>eu.siacs.conversations.axolotl</namespace>
<className>org.jivesoftware.smackx.omemo.provider.OmemoBundleVAxolotlProvider</className>
</extensionProvider>
</smackProviders>

View file

@ -0,0 +1,5 @@
<smack>
<startupClasses>
<className>org.jivesoftware.smackx.omemo.OmemoManager</className>
</startupClasses>
</smack>

View file

@ -0,0 +1,70 @@
/**
*
* Copyright the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.omemo;
import org.jivesoftware.smackx.omemo.internal.CachedDeviceList;
import org.junit.Test;
import java.util.HashSet;
import java.util.Set;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* Test behavior of device lists.
*
* @author Paul Schaub
*/
public class DeviceListTest {
/**
* Test, whether deviceList updates are correctly merged into the cached device list.
* IDs in the update become active devices, active IDs that were not in the update become inactive.
* Inactive IDs that were not in the update stay inactive.
*/
@Test
public void mergeDeviceListsTest() {
CachedDeviceList cached = new CachedDeviceList();
assertNotNull(cached.getActiveDevices());
assertNotNull(cached.getInactiveDevices());
cached.getInactiveDevices().add(1);
cached.getInactiveDevices().add(2);
cached.getActiveDevices().add(3);
Set<Integer> update = new HashSet<>();
update.add(4);
update.add(1);
cached.merge(update);
assertTrue(cached.getActiveDevices().contains(1) &&
!cached.getActiveDevices().contains(2) &&
!cached.getActiveDevices().contains(3) &&
cached.getActiveDevices().contains(4));
assertTrue(!cached.getInactiveDevices().contains(1) &&
cached.getInactiveDevices().contains(2) &&
cached.getInactiveDevices().contains(3) &&
!cached.getInactiveDevices().contains(4));
assertTrue(cached.getAllDevices().size() == 4);
}
}

View file

@ -0,0 +1,96 @@
/**
*
* Copyright the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.omemo;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.test.util.TestUtils;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.stringencoder.Base64;
import org.jivesoftware.smackx.omemo.element.OmemoBundleVAxolotlElement;
import org.jivesoftware.smackx.omemo.provider.OmemoBundleVAxolotlProvider;
import org.junit.Test;
import java.util.Arrays;
import java.util.HashMap;
import static junit.framework.TestCase.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* Test serialization and parsing of the OmemoBundleVAxolotlElement.
*/
public class OmemoBundleVAxolotlElementTest extends SmackTestSuite {
@Test
public void serializationTest() throws Exception {
int signedPreKeyId = 420;
String signedPreKeyB64 = Base64.encodeToString("SignedPreKey".getBytes(StringUtils.UTF8));
String signedPreKeySigB64 = Base64.encodeToString("SignedPreKeySignature".getBytes(StringUtils.UTF8));
String identityKeyB64 = Base64.encodeToString("IdentityKey".getBytes(StringUtils.UTF8));
int preKeyId1 = 220, preKeyId2 = 284;
String preKey1B64 = Base64.encodeToString("FirstPreKey".getBytes(StringUtils.UTF8)),
preKey2B64 = Base64.encodeToString("SecondPreKey".getBytes(StringUtils.UTF8));
HashMap<Integer, String> preKeysB64 = new HashMap<>();
preKeysB64.put(preKeyId1, preKey1B64);
preKeysB64.put(preKeyId2, preKey2B64);
OmemoBundleVAxolotlElement bundle = new OmemoBundleVAxolotlElement(signedPreKeyId,
signedPreKeyB64, signedPreKeySigB64, identityKeyB64, preKeysB64);
assertEquals("ElementName must match.", "bundle", bundle.getElementName());
assertEquals("Namespace must match.", "eu.siacs.conversations.axolotl", bundle.getNamespace());
String expected =
"<bundle xmlns='eu.siacs.conversations.axolotl'>" +
"<signedPreKeyPublic signedPreKeyId='420'>" +
signedPreKeyB64 +
"</signedPreKeyPublic>" +
"<signedPreKeySignature>" +
signedPreKeySigB64 +
"</signedPreKeySignature>" +
"<identityKey>" +
identityKeyB64 +
"</identityKey>" +
"<prekeys>" +
"<preKeyPublic preKeyId='220'>" +
preKey1B64 +
"</preKeyPublic>" +
"<preKeyPublic preKeyId='284'>" +
preKey2B64 +
"</preKeyPublic>" +
"</prekeys>" +
"</bundle>";
String actual = bundle.toXML().toString();
assertEquals("Bundles XML must match.", expected, actual);
byte[] signedPreKey = "SignedPreKey".getBytes(StringUtils.UTF8);
byte[] signedPreKeySig = "SignedPreKeySignature".getBytes(StringUtils.UTF8);
byte[] identityKey = "IdentityKey".getBytes(StringUtils.UTF8);
byte[] firstPreKey = "FirstPreKey".getBytes(StringUtils.UTF8);
byte[] secondPreKey = "SecondPreKey".getBytes(StringUtils.UTF8);
OmemoBundleVAxolotlElement parsed = new OmemoBundleVAxolotlProvider().parse(TestUtils.getParser(actual));
assertTrue("B64-decoded signedPreKey must match.", Arrays.equals(signedPreKey, parsed.getSignedPreKey()));
assertEquals("SignedPreKeyId must match", signedPreKeyId, parsed.getSignedPreKeyId());
assertTrue("B64-decoded signedPreKey signature must match.", Arrays.equals(signedPreKeySig, parsed.getSignedPreKeySignature()));
assertTrue("B64-decoded identityKey must match.", Arrays.equals(identityKey, parsed.getIdentityKey()));
assertTrue("B64-decoded first preKey must match.", Arrays.equals(firstPreKey, parsed.getPreKey(220)));
assertTrue("B64-decoded second preKey must match.", Arrays.equals(secondPreKey, parsed.getPreKey(284)));
assertEquals("toString outputs must match.", bundle.toString(), parsed.toString());
}
}

View file

@ -0,0 +1,112 @@
/**
*
* Copyright the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.omemo;
import junit.framework.TestCase;
import org.jivesoftware.smackx.omemo.OmemoConfiguration;
import org.junit.Test;
import java.io.File;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
/**
* Test the OmemoConfiguration class.
*/
public class OmemoConfigurationTest {
@Test
public void omemoConfigurationTest() {
@SuppressWarnings("unused") OmemoConfiguration configuration = new OmemoConfiguration();
// Default Store Path
File storePath = new File("test");
assertNull("getFileBasedOmemoStoreDefaultPath MUST return null at this point.",
OmemoConfiguration.getFileBasedOmemoStoreDefaultPath());
OmemoConfiguration.setFileBasedOmemoStoreDefaultPath(storePath);
assertEquals("FileBasedOmemoStoreDefaultPath must equal the one we set.", storePath.getAbsolutePath(),
OmemoConfiguration.getFileBasedOmemoStoreDefaultPath().getAbsolutePath());
// EME
OmemoConfiguration.setAddEmeEncryptionHint(false);
assertEquals(false, OmemoConfiguration.getAddEmeEncryptionHint());
OmemoConfiguration.setAddEmeEncryptionHint(true);
assertEquals(true, OmemoConfiguration.getAddEmeEncryptionHint());
// MAM
OmemoConfiguration.setAddMAMStorageProcessingHint(false);
assertEquals(false, OmemoConfiguration.getAddMAMStorageProcessingHint());
OmemoConfiguration.setAddMAMStorageProcessingHint(true);
assertEquals(true, OmemoConfiguration.getAddMAMStorageProcessingHint());
// Body hint
OmemoConfiguration.setAddOmemoHintBody(false);
assertEquals(false, OmemoConfiguration.getAddOmemoHintBody());
OmemoConfiguration.setAddOmemoHintBody(true);
assertEquals(true, OmemoConfiguration.getAddOmemoHintBody());
// Delete stale devices
OmemoConfiguration.setDeleteStaleDevices(false);
assertEquals(false, OmemoConfiguration.getDeleteStaleDevices());
OmemoConfiguration.setDeleteStaleDevices(true);
assertEquals(true, OmemoConfiguration.getDeleteStaleDevices());
OmemoConfiguration.setDeleteStaleDevicesAfterHours(25);
assertEquals(25, OmemoConfiguration.getDeleteStaleDevicesAfterHours());
try {
OmemoConfiguration.setDeleteStaleDevicesAfterHours(-3);
TestCase.fail("OmemoConfiguration.setDeleteStaleDevicesAfterHours should not accept values <= 0.");
} catch (IllegalArgumentException e) {
// Expected.
}
// Ignore stale device
OmemoConfiguration.setIgnoreStaleDevices(false);
assertEquals(false, OmemoConfiguration.getIgnoreStaleDevices());
OmemoConfiguration.setIgnoreStaleDevices(true);
assertEquals(true, OmemoConfiguration.getIgnoreStaleDevices());
OmemoConfiguration.setIgnoreStaleDevicesAfterHours(44);
assertEquals(44, OmemoConfiguration.getIgnoreStaleDevicesAfterHours());
try {
OmemoConfiguration.setIgnoreStaleDevicesAfterHours(-5);
TestCase.fail("OmemoConfiguration.setIgnoreStaleDevicesAfterHours should not accept values <= 0.");
} catch (IllegalArgumentException e) {
// Expected
}
// Renew signedPreKeys
OmemoConfiguration.setRenewOldSignedPreKeys(false);
assertEquals(false, OmemoConfiguration.getRenewOldSignedPreKeys());
OmemoConfiguration.setRenewOldSignedPreKeys(true);
assertEquals(true, OmemoConfiguration.getRenewOldSignedPreKeys());
OmemoConfiguration.setRenewOldSignedPreKeysAfterHours(77);
assertEquals(77, OmemoConfiguration.getRenewOldSignedPreKeysAfterHours());
try {
OmemoConfiguration.setRenewOldSignedPreKeysAfterHours(0);
TestCase.fail("OmemoConfiguration.setRenewOldSignedPreKeysAfterHours should not accept values <= 0");
} catch (IllegalArgumentException e) {
// Expected
}
OmemoConfiguration.setMaxNumberOfStoredSignedPreKeys(6);
assertEquals(6, OmemoConfiguration.getMaxNumberOfStoredSignedPreKeys());
try {
OmemoConfiguration.setMaxNumberOfStoredSignedPreKeys(0);
TestCase.fail("OmemoConfiguration.setMaxNumberOfStoredSignedPreKeys should not accept values <= 0");
} catch (IllegalArgumentException e) {
//Expected
}
}
}

View file

@ -0,0 +1,56 @@
/**
*
* Copyright the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.omemo;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.test.util.TestUtils;
import org.jivesoftware.smackx.omemo.element.OmemoDeviceListVAxolotlElement;
import org.jivesoftware.smackx.omemo.provider.OmemoDeviceListVAxolotlProvider;
import org.junit.Test;
import org.xmlpull.v1.XmlPullParser;
import java.util.HashSet;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* Test serialization and parsing of DeviceListElement.
*/
public class OmemoDeviceListVAxolotlElementTest extends SmackTestSuite {
@Test
public void serializationTest() throws Exception {
HashSet<Integer> ids = new HashSet<>();
ids.add(1234);
ids.add(9876);
OmemoDeviceListVAxolotlElement element = new OmemoDeviceListVAxolotlElement(ids);
String xml = element.toXML().toString();
XmlPullParser parser = TestUtils.getParser(xml);
OmemoDeviceListVAxolotlElement parsed = new OmemoDeviceListVAxolotlProvider().parse(parser);
assertTrue("Parsed element must equal the original.", parsed.getDeviceIds().equals(element.getDeviceIds()));
assertEquals("Generated XML must match.",
"<list xmlns='eu.siacs.conversations.axolotl'>" +
"<device id='1234'/>" +
"<device id='9876'/>" +
"</list>",
xml);
}
}

View file

@ -0,0 +1,62 @@
/**
*
* Copyright the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.omemo;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.junit.Assert;
import org.junit.Test;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Test the OmemoDevice class.
*
* @author Paul Schaub
*/
public class OmemoDeviceTest {
/**
* Test, if the equals() method works as intended.
*/
@Test
public void testEquals() {
BareJid romeo, juliet, guyUnderTheBalcony;
try {
romeo = JidCreate.bareFrom("romeo@shakespeare.lit");
guyUnderTheBalcony = JidCreate.bareFrom("romeo@shakespeare.lit/underTheBalcony");
juliet = JidCreate.bareFrom("juliet@shakespeare.lit");
} catch (XmppStringprepException e) {
Assert.fail(e.getMessage());
return;
}
OmemoDevice r = new OmemoDevice(romeo, 1);
OmemoDevice g = new OmemoDevice(guyUnderTheBalcony, 1);
OmemoDevice r2 = new OmemoDevice(romeo, 2);
OmemoDevice j = new OmemoDevice(juliet, 3);
OmemoDevice j2 = new OmemoDevice(juliet, 1);
assertTrue(r.equals(g));
assertFalse(r.equals(r2));
assertFalse(j.equals(j2));
assertFalse(j2.equals(r2));
}
}

View file

@ -0,0 +1,104 @@
/**
*
* Copyright the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.omemo;
import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException;
import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
import org.jivesoftware.smackx.omemo.exceptions.MultipleCryptoFailedException;
import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.junit.Test;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import java.util.ArrayList;
import static junit.framework.TestCase.fail;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Test Omemo related Exceptions.
*/
public class OmemoExceptionsTest {
@Test
public void undecidedOmemoIdentityExceptionTest() throws XmppStringprepException {
OmemoDevice alice = new OmemoDevice(JidCreate.bareFrom("alice@server.tld"), 1234);
OmemoDevice bob = new OmemoDevice(JidCreate.bareFrom("bob@server.tld"), 5678);
OmemoDevice mallory = new OmemoDevice(JidCreate.bareFrom("mallory@server.tld"), 9876);
UndecidedOmemoIdentityException u = new UndecidedOmemoIdentityException(alice);
assertTrue(u.getUntrustedDevices().contains(alice));
assertTrue(u.getUntrustedDevices().size() == 1);
UndecidedOmemoIdentityException v = new UndecidedOmemoIdentityException(bob);
v.getUntrustedDevices().add(mallory);
assertTrue(v.getUntrustedDevices().size() == 2);
assertTrue(v.getUntrustedDevices().contains(bob));
assertTrue(v.getUntrustedDevices().contains(mallory));
u.getUntrustedDevices().add(bob);
u.join(v);
assertTrue(u.getUntrustedDevices().size() == 3);
}
@Test
public void cannotEstablishOmemoSessionExceptionTest() throws XmppStringprepException {
OmemoDevice alice1 = new OmemoDevice(JidCreate.bareFrom("alice@server.tld"), 1234);
OmemoDevice alice2 = new OmemoDevice(JidCreate.bareFrom("alice@server.tld"), 2345);
OmemoDevice bob = new OmemoDevice(JidCreate.bareFrom("bob@server.tld"), 5678);
CannotEstablishOmemoSessionException c = new CannotEstablishOmemoSessionException(alice1, null);
assertEquals(1, c.getFailures().size());
assertTrue(c.getFailures().containsKey(alice1.getJid()));
c.addSuccess(alice2);
assertFalse(c.requiresThrowing());
c.addFailures(new CannotEstablishOmemoSessionException(bob, null));
assertTrue(c.requiresThrowing());
assertEquals(1, c.getSuccesses().size());
assertEquals(2, c.getFailures().size());
c.getSuccesses().remove(alice2.getJid());
c.addFailures(new CannotEstablishOmemoSessionException(alice2, null));
assertEquals(2, c.getFailures().size());
}
@Test
public void multipleCryptoFailedExceptionTest() {
CryptoFailedException e1 = new CryptoFailedException("Fail");
CryptoFailedException e2 = new CryptoFailedException("EpicFail");
ArrayList<CryptoFailedException> l = new ArrayList<>();
l.add(e1); l.add(e2);
MultipleCryptoFailedException m = MultipleCryptoFailedException.from(l);
assertEquals(2, m.getCryptoFailedExceptions().size());
assertTrue(m.getCryptoFailedExceptions().contains(e1));
assertTrue(m.getCryptoFailedExceptions().contains(e2));
ArrayList<CryptoFailedException> el = new ArrayList<>();
try {
MultipleCryptoFailedException m2 = MultipleCryptoFailedException.from(el);
fail("MultipleCryptoFailedException must not allow empty list.");
} catch (IllegalArgumentException e) {
// Expected
}
}
}

View file

@ -0,0 +1,38 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.omemo;
import org.jivesoftware.smackx.omemo.OmemoFingerprint;
import org.junit.Test;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertNotSame;
/**
* Test the OmemoFingerprint class.
*/
public class OmemoFingerprintTest {
@Test
public void fingerprintTest() {
OmemoFingerprint first = new OmemoFingerprint("FINGER");
OmemoFingerprint second = new OmemoFingerprint("TOE");
assertNotSame(first, second);
assertEquals(first, new OmemoFingerprint("FINGER"));
}
}

View file

@ -0,0 +1,46 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.omemo;
import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil;
import org.junit.Test;
import static junit.framework.TestCase.assertEquals;
/**
* Test KeyUtil functions.
*
* @author Paul Schaub
*/
public class OmemoKeyUtilTest {
@Test
public void testAddInBounds() {
int high = Integer.MAX_VALUE - 2;
int max = Integer.MAX_VALUE;
assertEquals(OmemoKeyUtil.addInBounds(high, 3), 1);
assertEquals(OmemoKeyUtil.addInBounds(1,2), 3);
assertEquals(OmemoKeyUtil.addInBounds(max, 5), 5);
}
@Test
public void testPrettyFingerprint() {
String ugly = "FFFFFFFFEEEEEEEEDDDDDDDDCCCCCCCCBBBBBBBBAAAAAAAA9999999988888888";
String pretty = OmemoKeyUtil.prettyFingerprint(ugly);
assertEquals(pretty, "FFFFFFFF EEEEEEEE DDDDDDDD CCCCCCCC BBBBBBBB AAAAAAAA 99999999 88888888");
}
}

View file

@ -0,0 +1,73 @@
/**
*
* Copyright the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.omemo;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.test.util.TestUtils;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.stringencoder.Base64;
import org.jivesoftware.smackx.omemo.element.OmemoElement;
import org.jivesoftware.smackx.omemo.element.OmemoVAxolotlElement;
import org.jivesoftware.smackx.omemo.provider.OmemoVAxolotlProvider;
import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder;
import org.junit.Test;
import java.util.ArrayList;
import static org.junit.Assert.assertEquals;
/**
* Test serialization and parsing of OmemoVAxolotlElements.
*/
public class OmemoVAxolotlElementTest extends SmackTestSuite {
@Test
public void serializationTest() throws Exception {
byte[] payload = "This is payload.".getBytes(StringUtils.UTF8);
int keyId1 = 8;
int keyId2 = 33333;
byte[] keyData1 = "KEYDATA".getBytes(StringUtils.UTF8);
byte[] keyData2 = "DATAKEY".getBytes(StringUtils.UTF8);
int sid = 12131415;
byte[] iv = OmemoMessageBuilder.generateIv();
ArrayList<OmemoElement.OmemoHeader.Key> keys = new ArrayList<>();
keys.add(new OmemoElement.OmemoHeader.Key(keyData1, keyId1));
keys.add(new OmemoElement.OmemoHeader.Key(keyData2, keyId2, true));
OmemoVAxolotlElement.OmemoHeader header = new OmemoElement.OmemoHeader(sid, keys, iv);
OmemoVAxolotlElement element = new OmemoVAxolotlElement(header, payload);
String expected =
"<encrypted xmlns='eu.siacs.conversations.axolotl'>" +
"<header sid='12131415'>" +
"<key rid='8'>" + Base64.encodeToString(keyData1) + "</key>" +
"<key prekey='true' rid='33333'>" + Base64.encodeToString(keyData2) + "</key>" +
"<iv>" + Base64.encodeToString(iv) + "</iv>" +
"</header>" +
"<payload>" +
Base64.encodeToString(payload) +
"</payload>" +
"</encrypted>";
String actual = element.toXML().toString();
assertEquals("Serialized xml of OmemoElement must match.", expected, actual);
OmemoVAxolotlElement parsed = new OmemoVAxolotlProvider().parse(TestUtils.getParser(actual));
assertEquals("Parsed OmemoElement must equal the original.", element.toXML().toString(), parsed.toXML().toString());
}
}

View file

@ -0,0 +1,105 @@
/**
*
* Copyright the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.omemo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.omemo.element.OmemoElement;
import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag;
import org.jivesoftware.smackx.omemo.internal.CiphertextTuple;
import org.jivesoftware.smackx.omemo.internal.ClearTextMessage;
import org.jivesoftware.smackx.omemo.internal.IdentityKeyWrapper;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation;
import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder;
import org.junit.Test;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.impl.JidCreate;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
/**
* Test the identityKeyWrapper.
*/
public class WrapperObjectsTest {
@Test
public void identityKeyWrapperTest() {
Object pseudoKey = new Object();
IdentityKeyWrapper wrapper = new IdentityKeyWrapper(pseudoKey);
assertEquals(pseudoKey, wrapper.getIdentityKey());
}
@Test
public void ciphertextTupleTest() {
byte[] c = OmemoMessageBuilder.generateIv();
CiphertextTuple c1 = new CiphertextTuple(c, OmemoElement.TYPE_OMEMO_PREKEY_MESSAGE);
assertTrue(c1.isPreKeyMessage());
assertArrayEquals(c, c1.getCiphertext());
assertEquals(OmemoElement.TYPE_OMEMO_PREKEY_MESSAGE, c1.getMessageType());
CiphertextTuple c2 = new CiphertextTuple(c, OmemoElement.TYPE_OMEMO_MESSAGE);
assertFalse(c2.isPreKeyMessage());
assertEquals(OmemoElement.TYPE_OMEMO_MESSAGE, c2.getMessageType());
}
@Test
public void clearTextMessageTest() throws Exception {
Object pseudoKey = new Object();
IdentityKeyWrapper wrapper = new IdentityKeyWrapper(pseudoKey);
BareJid senderJid = JidCreate.bareFrom("bob@server.tld");
OmemoDevice sender = new OmemoDevice(senderJid, 1234);
OmemoMessageInformation information = new OmemoMessageInformation(wrapper, sender, OmemoMessageInformation.CARBON.NONE);
assertTrue("OmemoInformation must state that the message is an OMEMO message.",
information.isOmemoMessage());
assertEquals(OmemoMessageInformation.CARBON.NONE, information.getCarbon());
assertEquals(sender, information.getSenderDevice());
assertEquals(wrapper, information.getSenderIdentityKey());
String body = "Decrypted Body";
Message message = new Message(senderJid, body);
ClearTextMessage c = new ClearTextMessage(body, message, information);
assertEquals(message, c.getOriginalMessage());
assertEquals(information, c.getMessageInformation());
assertEquals(body, c.getBody());
}
@Test
public void cipherAndAuthTagTest() throws NoSuchAlgorithmException, CryptoFailedException {
Security.addProvider(new BouncyCastleProvider());
byte[] key = OmemoMessageBuilder.generateKey();
byte[] iv = OmemoMessageBuilder.generateIv();
byte[] authTag = OmemoMessageBuilder.generateIv();
CipherAndAuthTag cat = new CipherAndAuthTag(key, iv, authTag);
assertNotNull(cat.getCipher());
assertArrayEquals(key, cat.getKey());
assertArrayEquals(iv, cat.getIv());
assertArrayEquals(authTag, cat.getAuthTag());
}
}