diff --git a/.gitignore b/.gitignore index 989af3d92..742156b3d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ core/build/ debug/build/ experimental/build/ extensions/build/ +out/ bin/ core/bin diff --git a/build.gradle b/build.gradle index 9ca176231..7bbae4c1d 100644 --- a/build.gradle +++ b/build.gradle @@ -40,7 +40,11 @@ allprojects { // build, causing unnecessary rebuilds. builtDate = (new java.text.SimpleDateFormat("yyyy-MM-dd")).format(new Date()) oneLineDesc = 'An Open Source XMPP (Jabber) client library' - javadocAllProjects = subprojects - project(':smack-integration-test') + integrationTestProjects = [ + ':smack-integration-test', + ':smack-omemo-signal-integration-test', + ].collect{ project(it) } + javadocAllProjects = subprojects - integrationTestProjects // A dirty hack used for Gradle's jacoco plugin, since is not // hable to handle the case when a (sub)project has no unit // tests. :-( @@ -56,6 +60,7 @@ allprojects { ':smack-resolver-dnsjava', ':smack-resolver-javax', ':smack-resolver-minidns', + ':smack-omemo-signal-integration-test', ].collect{ project(it) } projectsWithUnitTests = subprojects - projectsWithoutUnitTests androidProjects = [ @@ -67,11 +72,17 @@ allprojects { ':smack-sasl-provided', ':smack-extensions', ':smack-experimental', + ':smack-omemo', + ':smack-omemo-signal', ].collect{ project(it) } androidBootClasspathProjects = [ ':smack-android', ':smack-android-extensions', ].collect{ project(it) } + gplLicensedProjects = [ + ':smack-omemo-signal', + ':smack-omemo-signal-integration-test', + ].collect{ project(it) } androidBootClasspath = getAndroidRuntimeJar() androidJavadocOffline = getAndroidJavadocOffline() junitVersion = '4.11' @@ -350,14 +361,6 @@ subprojects { developerConnection 'scm:git:https://github.com/igniterealtime/Smack.git' } - licenses { - license { - name 'The Apache Software License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - distribution 'repo' - } - } - developers { developer { id 'flow' @@ -387,8 +390,17 @@ subprojects { } } -// No need to ever clirr smack-integration-test -project(':smack-integration-test').clirr.enabled = false +// There is no need to ever clirr integration test projects and the +// smack-repl project. +configure(integrationTestProjects + project(':smack-repl')) { + clirr { + enabled false + } +} + +// Disable clirr on omemo modules +project(':smack-omemo').clirr.enabled = false +project(':smack-omemo-signal').clirr.enabled = false subprojects*.jar { manifest { @@ -396,6 +408,49 @@ subprojects*.jar { } } +configure(subprojects - gplLicensedProjects) { + checkstyle { + configProperties.checkstyleLicenseHeader = "header" + } + uploadArchives { + repositories { + mavenDeployer { + pom.project { + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + } + } + } + } +} + +configure(gplLicensedProjects) { + checkstyle { + configProperties.checkstyleLicenseHeader = "${project.name}-gplv3-license-header" + } + uploadArchives { + repositories { + mavenDeployer { + pom.project { + licenses { + license { + name 'GNU General Public License, version 3 or any later version' + url 'https://www.gnu.org/licenses/gpl.txt' + distribution 'repo' + } + } + } + } + } +} + +} + configure(androidBootClasspathProjects) { compileJava { options.bootClasspath = androidBootClasspath @@ -436,6 +491,10 @@ task integrationTest { dependsOn project(':smack-integration-test').tasks.run } +task omemoSignalIntTest { + dependsOn project(':smack-omemo-signal-integration-test').tasks.run +} + def getGitCommit() { def dotGit = new File("$projectDir/.git") if (!dotGit.isDirectory()) return 'non-git build' diff --git a/config/checkstyle.xml b/config/checkstyle.xml index d5ce5464e..27a71807f 100644 --- a/config/checkstyle.xml +++ b/config/checkstyle.xml @@ -8,7 +8,7 @@ - + diff --git a/config/smack-omemo-signal-gplv3-license-header.txt b/config/smack-omemo-signal-gplv3-license-header.txt new file mode 100644 index 000000000..e2ecef5d1 --- /dev/null +++ b/config/smack-omemo-signal-gplv3-license-header.txt @@ -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 + */ diff --git a/config/smack-omemo-signal-integration-test-gplv3-license-header.txt b/config/smack-omemo-signal-integration-test-gplv3-license-header.txt new file mode 100644 index 000000000..b77cfaf36 --- /dev/null +++ b/config/smack-omemo-signal-integration-test-gplv3-license-header.txt @@ -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 + */ diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index d2764a2d0..29036cc9f 100644 --- a/documentation/extensions/index.md +++ b/documentation/extensions/index.md @@ -92,9 +92,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 | diff --git a/documentation/extensions/omemo.md b/documentation/extensions/omemo.md new file mode 100644 index 000000000..898ba9c4c --- /dev/null +++ b/documentation/extensions/omemo.md @@ -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 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 +``` diff --git a/settings.gradle b/settings.gradle index 9d5284329..a0024efb2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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' diff --git a/smack-core/build.gradle b/smack-core/build.gradle index 46867ea91..6d5c5267c 100644 --- a/smack-core/build.gradle +++ b/smack-core/build.gradle @@ -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' } diff --git a/smack-core/src/main/resources/org.jivesoftware.smack/smack-config.xml b/smack-core/src/main/resources/org.jivesoftware.smack/smack-config.xml index 03ffe987c..fd4468b90 100644 --- a/smack-core/src/main/resources/org.jivesoftware.smack/smack-config.xml +++ b/smack-core/src/main/resources/org.jivesoftware.smack/smack-config.xml @@ -20,5 +20,6 @@ org.jivesoftware.smack.android.AndroidSmackInitializer org.jivesoftware.smack.java7.Java7SmackInitializer org.jivesoftware.smack.im.SmackImInitializer + org.jivesoftware.smackx.omemo.OmemoInitializer diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiatorTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiatorTest.java index 7a7bab2f2..cd3e5201d 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiatorTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiatorTest.java @@ -61,6 +61,8 @@ public class Socks5ClientForInitiatorTest { static final DomainBareJid proxyJID = JidTestUtil.MUC_EXAMPLE_ORG; static final String loopbackAddress = InetAddress.getLoopbackAddress().getHostAddress(); + private static final int GET_SOCKET_TIMEOUT = 90 * 1000; + int proxyPort = 7890; String sessionID = "session_id"; @@ -111,7 +113,7 @@ public class Socks5ClientForInitiatorTest { connection, sessionID, targetJID); try { - socks5Client.getSocket(10000); + socks5Client.getSocket(GET_SOCKET_TIMEOUT); fail("exception should be thrown"); } @@ -175,7 +177,7 @@ public class Socks5ClientForInitiatorTest { Socks5ClientForInitiator socks5Client = new Socks5ClientForInitiator(streamHost, digest, connection, sessionID, targetJID); - Socket socket = socks5Client.getSocket(10000); + Socket socket = socks5Client.getSocket(GET_SOCKET_TIMEOUT); // verify test data InputStream in = socket.getInputStream(); @@ -224,7 +226,7 @@ public class Socks5ClientForInitiatorTest { try { - socks5Client.getSocket(10000); + socks5Client.getSocket(GET_SOCKET_TIMEOUT); fail("exception should be thrown"); } diff --git a/smack-integration-test/build.gradle b/smack-integration-test/build.gradle index 461039058..07c3a10e0 100644 --- a/smack-integration-test/build.gradle +++ b/smack-integration-test/build.gradle @@ -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" diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java index 523fc65d4..8418e2d95 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java @@ -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(); } diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java index 9e9feec00..4026ac6e2 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java @@ -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(); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java index c1ad1dce6..e337dd776 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java @@ -31,6 +31,7 @@ import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smackx.forward.packet.Forwarded; import org.jivesoftware.smackx.mam.MamManager.MamQueryResult; +import org.jivesoftware.smackx.mam.element.MamPrefsIQ; import org.jxmpp.jid.EntityBareJid; public class MamIntegrationTest extends AbstractSmackIntegrationTest { @@ -55,6 +56,9 @@ public class MamIntegrationTest extends AbstractSmackIntegrationTest { EntityBareJid userOne = conOne.getUser().asEntityBareJid(); EntityBareJid userTwo = conTwo.getUser().asEntityBareJid(); + //Make sure MAM is archiving messages + mamManagerConTwo.updateArchivingPreferences(null, null, MamPrefsIQ.DefaultBehavior.always); + Message message = new Message(userTwo); String messageId = message.setStanzaId(); String messageBody = "test message"; diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoIntegrationTest.java new file mode 100644 index 000000000..e7055eeaf --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoIntegrationTest.java @@ -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(); +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoInitializationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoInitializationTest.java new file mode 100644 index 000000000..c00ff6f56 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoInitializationTest.java @@ -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); + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoIntegrationTestHelper.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoIntegrationTestHelper.java new file mode 100644 index 000000000..6728bdf40 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoIntegrationTestHelper.java @@ -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)); + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoKeyTransportTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoKeyTransportTest.java new file mode 100644 index 000000000..63e7156b7 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoKeyTransportTest.java @@ -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); + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessageSendingTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessageSendingTest.java new file mode 100644 index 000000000..5ba84736b --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessageSendingTest.java @@ -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); + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoSessionRenegotiationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoSessionRenegotiationTest.java new file mode 100644 index 000000000..c3e8bdf73 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoSessionRenegotiationTest.java @@ -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); + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java new file mode 100644 index 000000000..5b172a87a --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java @@ -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(); + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/package-info.java new file mode 120000 index 000000000..2af0aa211 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/package-info.java @@ -0,0 +1 @@ +../../../../../../../../smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/package-info.java \ No newline at end of file diff --git a/smack-omemo-signal-integration-test/.gitignore b/smack-omemo-signal-integration-test/.gitignore new file mode 100644 index 000000000..ebf26cf4c --- /dev/null +++ b/smack-omemo-signal-integration-test/.gitignore @@ -0,0 +1 @@ +int_test_omemo_store/ diff --git a/smack-omemo-signal-integration-test/build.gradle b/smack-omemo-signal-integration-test/build.gradle new file mode 100644 index 000000000..f70ed0260 --- /dev/null +++ b/smack-omemo-signal-integration-test/build.gradle @@ -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() +} diff --git a/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/SmackOmemoSignalIntegrationTestFramework.java b/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/SmackOmemoSignalIntegrationTestFramework.java new file mode 100644 index 000000000..0656b68b4 --- /dev/null +++ b/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/SmackOmemoSignalIntegrationTestFramework.java @@ -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); + } + +} diff --git a/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/package-info.java b/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/package-info.java new file mode 100644 index 000000000..4534652d5 --- /dev/null +++ b/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/package-info.java @@ -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; diff --git a/smack-omemo-signal/LICENSE b/smack-omemo-signal/LICENSE new file mode 100644 index 000000000..f27031ae8 --- /dev/null +++ b/smack-omemo-signal/LICENSE @@ -0,0 +1,677 @@ + + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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 . + +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 +. + + 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 +. + diff --git a/smack-omemo-signal/build.gradle b/smack-omemo-signal/build.gradle new file mode 100644 index 000000000..9ec09844d --- /dev/null +++ b/smack-omemo-signal/build.gradle @@ -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") +} diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalFileBasedOmemoStore.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalFileBasedOmemoStore.java new file mode 100644 index 000000000..8a2bf3bac --- /dev/null +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalFileBasedOmemoStore.java @@ -0,0 +1,58 @@ +/** + * + * Copyright 2017 Paul Schaub + * + * This file is part of smack-omemo-signal. + * + * smack-omemo-signal is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jivesoftware.smackx.omemo.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 { + + public SignalFileBasedOmemoStore() { + super(); + } + + public SignalFileBasedOmemoStore(File base) { + super(base); + } + + @Override + public OmemoKeyUtil keyUtil() { + return new SignalOmemoKeyUtil(); + } +} diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoKeyUtil.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoKeyUtil.java new file mode 100644 index 000000000..708e90f53 --- /dev/null +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoKeyUtil.java @@ -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 { + + @Override + public IdentityKeyPair generateOmemoIdentityKeyPair() { + return KeyHelper.generateIdentityKeyPair(); + } + + @Override + public HashMap generateOmemoPreKeys(int currentPreKeyId, int count) { + List preKeyRecords = KeyHelper.generatePreKeys(currentPreKeyId, count); + HashMap 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 + createOmemoSession(OmemoManager omemoManager, OmemoStore omemoStore, + OmemoDevice contact, IdentityKey identityKey) { + return new SignalOmemoSession(omemoManager, omemoStore, contact, identityKey); + } + + @Override + public OmemoSession + createOmemoSession(OmemoManager omemoManager, OmemoStore omemoStore, OmemoDevice from) { + return new SignalOmemoSession(omemoManager, omemoStore, from); + } + + @Override + public SessionRecord rawSessionFromBytes(byte[] data) throws IOException { + 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()); + } +} diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoService.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoService.java new file mode 100644 index 000000000..96771796d --- /dev/null +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoService.java @@ -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 { + + 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 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); + } + } +} diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoSession.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoSession.java new file mode 100644 index 000000000..6e0de85e6 --- /dev/null +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoSession.java @@ -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 { + private static final Logger LOGGER = Logger.getLogger(SignalOmemoSession.class.getName()); + + /** + * Constructor used when the remote user initialized the session using a PreKeyOmemoMessage. + * + * @param omemoManager omemoManager + * @param omemoStore omemoStoreConnector that can be used to get information from + * @param remoteContact omemoDevice of the remote contact + * @param identityKey identityKey of the remote contact + */ + SignalOmemoSession(OmemoManager omemoManager, OmemoStore omemoStore, + OmemoDevice remoteContact, IdentityKey identityKey) { + super(omemoManager, omemoStore, remoteContact, identityKey); + } + + /** + * Constructor used when we initiate a new Session with the remote user. + * + * @param omemoManager omemoManager + * @param omemoStore omemoStore used to get information from + * @param remoteContact omemoDevice of the remote contact + */ + SignalOmemoSession(OmemoManager omemoManager, + OmemoStore omemoStore, + OmemoDevice remoteContact) { + super(omemoManager, omemoStore, remoteContact); + } + + @Override + public SessionCipher createCipher(OmemoDevice contact) { + SignalOmemoStoreConnector connector = new SignalOmemoStoreConnector(omemoManager, omemoStore); + return new SessionCipher(connector, connector, connector, connector, + omemoStore.keyUtil().omemoDeviceAsAddress(contact)); + } + + @Override + public CiphertextTuple encryptMessageKey(byte[] messageKey) { + CiphertextMessage ciphertextMessage; + ciphertextMessage = cipher.encrypt(messageKey); + int type = (ciphertextMessage.getType() == CiphertextMessage.PREKEY_TYPE ? + OmemoElement.TYPE_OMEMO_PREKEY_MESSAGE : OmemoElement.TYPE_OMEMO_MESSAGE); + return new CiphertextTuple(ciphertextMessage.serialize(), type); + } + + @Override + public byte[] decryptMessageKey(byte[] encryptedKey) throws NoRawSessionException { + byte[] decryptedKey = null; + try { + try { + PreKeySignalMessage message = new PreKeySignalMessage(encryptedKey); + if (!message.getPreKeyId().isPresent()) { + LOGGER.log(Level.WARNING, "PreKeySignalMessage did not contain a PreKeyId"); + return null; + } + LOGGER.log(Level.INFO, "PreKeySignalMessage received, new session ID: " + message.getSignedPreKeyId() + "/" + message.getPreKeyId().get()); + IdentityKey messageIdentityKey = message.getIdentityKey(); + if (this.identityKey != null && !this.identityKey.equals(messageIdentityKey)) { + LOGGER.log(Level.INFO, "Had session with fingerprint " + getFingerprint() + + ", received message with different fingerprint " + omemoStore.keyUtil().getFingerprint(messageIdentityKey) + + ". Silently drop the message."); + } else { + this.identityKey = messageIdentityKey; + decryptedKey = cipher.decrypt(message); + this.preKeyId = message.getPreKeyId().get(); + } + } catch (InvalidMessageException | InvalidVersionException e) { + SignalMessage message = new SignalMessage(encryptedKey); + decryptedKey = cipher.decrypt(message); + } catch (InvalidKeyIdException e) { + throw new NoRawSessionException(e); + } + catch (InvalidKeyException | UntrustedIdentityException e) { + LOGGER.log(Level.SEVERE, "Error decrypting message header, " + e.getClass().getName() + ": " + e.getMessage()); + } + } catch (InvalidMessageException | NoSessionException e) { + throw new NoRawSessionException(e); + } catch (LegacyMessageException | DuplicateMessageException e) { + LOGGER.log(Level.SEVERE, "Error decrypting message header, " + e.getClass().getName() + ": " + e.getMessage()); + } + return decryptedKey; + } +} diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStore.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStore.java new file mode 100644 index 000000000..adc413720 --- /dev/null +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStore.java @@ -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 { + + private final SignalOmemoKeyUtil signalKeyUtil = new SignalOmemoKeyUtil(); + + @Override + public OmemoKeyUtil keyUtil() { + return signalKeyUtil; + } +} diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStoreConnector.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStoreConnector.java new file mode 100644 index 000000000..63740291a --- /dev/null +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStoreConnector.java @@ -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 + omemoStore; + + public SignalOmemoStoreConnector(OmemoManager omemoManager, OmemoStore 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 getSubDeviceSessions(String s) { + HashMap 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 loadSignedPreKeys() { + HashMap signedPreKeyRecordHashMap = omemoStore.loadOmemoSignedPreKeys(omemoManager); + List 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); + } +} diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/package-info.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/package-info.java new file mode 100644 index 000000000..804c7937c --- /dev/null +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/package-info.java @@ -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; diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoManagerTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoManagerTest.java new file mode 100644 index 000000000..22fb0be24 --- /dev/null +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoManagerTest.java @@ -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 = "
MwohBfRqBm2atj3fT0/KUDg59Cnvfpgoe/PLNIu1xgSXujEZEAAYACIwKh6TTC7VBQZcCcKnQlO+6s1GQ9DIRKH4JU7XrJ+JJnkPUwJ4VLSeOEQD7HmFbhQPTLZO0u/qlng=sN0amy4e2NBrlb4G/OjNIQ==
4xVUAeg4M0Mhk+5n3YG1x12Dw/cYTc0Z
"; + 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)); + } +} diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoMessageBuilderTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoMessageBuilderTest.java new file mode 100644 index 000000000..7c7debf66 --- /dev/null +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/OmemoMessageBuilderTest.java @@ -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 + mb = new OmemoMessageBuilder<>(null, null, key, iv); + mb.setMessage(message); + + byte[] expected = cipher.doFinal(message.getBytes(StringUtils.UTF8)); + byte[] messageKey = new byte[16]; + System.arraycopy(mb.getMessageKey(),0, messageKey, 0, 16); + byte[] messagePlusTag = new byte[mb.getCiphertextMessage().length + 16]; + System.arraycopy(mb.getCiphertextMessage(),0,messagePlusTag,0,mb.getCiphertextMessage().length); + System.arraycopy(mb.getMessageKey(), 16, messagePlusTag, mb.getCiphertextMessage().length, 16); + + assertArrayEquals(key, messageKey); + assertArrayEquals(expected, messagePlusTag); + } +} diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalFileBasedOmemoStoreTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalFileBasedOmemoStoreTest.java new file mode 100644 index 000000000..7037687fd --- /dev/null +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalFileBasedOmemoStoreTest.java @@ -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)); + } +} diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoKeyUtilTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoKeyUtilTest.java new file mode 100644 index 000000000..095e39b1f --- /dev/null +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoKeyUtilTest.java @@ -0,0 +1,165 @@ +/** + * + * 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.test.util.SmackTestSuite; +import org.jivesoftware.smack.test.util.TestUtils; +import org.jivesoftware.smackx.omemo.element.OmemoBundleVAxolotlElement; +import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; +import org.jivesoftware.smackx.omemo.provider.OmemoBundleVAxolotlProvider; +import org.jivesoftware.smackx.omemo.signal.SignalOmemoKeyUtil; +import org.junit.Test; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.stringprep.XmppStringprepException; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.state.PreKeyBundle; +import org.whispersystems.libsignal.state.PreKeyRecord; +import org.whispersystems.libsignal.state.SignedPreKeyRecord; + +import java.util.HashMap; +import java.util.Iterator; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertNotSame; +import static junit.framework.TestCase.assertTrue; +import static junit.framework.TestCase.fail; + +/** + * Test SignalOmemoKeyUtil methods. + * + * @author Paul Schaub + */ +public class SignalOmemoKeyUtilTest extends SmackTestSuite { + + private final SignalOmemoKeyUtil keyUtil = new SignalOmemoKeyUtil(); + + @Test + public void generateOmemoIdentityKeyPairTest() { + IdentityKeyPair ikp = keyUtil.generateOmemoIdentityKeyPair(); + assertNotNull("IdentityKeyPair must not be null.", ikp); + assertNotNull("PrivateKey must not be null.", ikp.getPrivateKey()); + assertNotNull("PublicKey must not be null.", ikp.getPublicKey()); + } + + @Test + public void omemoIdentityKeyPairSerializationTest() { + IdentityKeyPair ikp = keyUtil.generateOmemoIdentityKeyPair(); + byte[] bytes = keyUtil.identityKeyPairToBytes(ikp); + assertNotNull("serialized identityKeyPair must not be null.", + bytes); + assertNotSame("serialized identityKeyPair must not be of length 0.", + 0, bytes.length); + try { + IdentityKeyPair ikp2 = keyUtil.identityKeyPairFromBytes(bytes); + assertTrue("Deserialized IdentityKeyPairs PublicKey must equal the originals one.", + ikp.getPublicKey().equals(ikp2.getPublicKey())); + } catch (CorruptedOmemoKeyException e) { + fail("Caught exception while deserializing IdentityKeyPair."); + } + } + + @Test + public void omemoIdentityKeySerializationTest() { + IdentityKey k = keyUtil.generateOmemoIdentityKeyPair().getPublicKey(); + + try { + assertEquals("Deserialized IdentityKey must equal the original one.", + k, keyUtil.identityKeyFromBytes(keyUtil.identityKeyToBytes(k))); + } catch (CorruptedOmemoKeyException e) { + fail("Caught exception while serializing and deserializing identityKey (" + e + "): " + e.getMessage()); + } + } + + @Test + public void generateOmemoPreKeysTest() { + HashMap pks = + keyUtil.generateOmemoPreKeys(1, 20); + assertTrue("There must be 20 preKeys.", pks.size() == 20); + assertTrue("PreKey ids must be within boundaries [1, 20]", pks.keySet().contains(1) && pks.keySet().contains(20)); + } + + @Test + public void generateOmemoSignedPreKeyTest() { + IdentityKeyPair ikp = keyUtil.generateOmemoIdentityKeyPair(); + try { + SignedPreKeyRecord spk = keyUtil.generateOmemoSignedPreKey(ikp, 1); + assertNotNull("SignedPreKey must not be null.", spk); + assertEquals("SignedPreKeyId must match.", 1, spk.getId()); + assertEquals("singedPreKeyId must match here also.", 1, keyUtil.signedPreKeyIdFromKey(spk)); + } catch (CorruptedOmemoKeyException e) { + fail("Caught an exception while generating signedPreKey (" + e + "): " + e.getMessage()); + } + } + + @Test + public void getFingerprintTest() { + IdentityKeyPair ikp = keyUtil.generateOmemoIdentityKeyPair(); + IdentityKey ik = ikp.getPublicKey(); + assertTrue("Length of fingerprint must be 64.", + keyUtil.getFingerprint(ik).length() == 64); + } + + @Test + public void addressToDeviceTest() { + SignalProtocolAddress address = new SignalProtocolAddress("test@server.tld",1337); + try { + OmemoDevice device = keyUtil.addressAsOmemoDevice(address); + assertEquals(device, new OmemoDevice(JidCreate.bareFrom("test@server.tld"), 1337)); + } catch (XmppStringprepException e) { + fail("Could not convert address to device: " + e + " " + e.getMessage()); + } + } + + @Test + public void deviceToAddressTest() { + try { + OmemoDevice device = new OmemoDevice(JidCreate.bareFrom("test@server.tld"), 1337); + SignalProtocolAddress address = keyUtil.omemoDeviceAsAddress(device); + assertEquals(address, new SignalProtocolAddress("test@server.tld", 1337)); + } catch (XmppStringprepException e) { + fail("Could not convert device to address: " + e + " " + e.getMessage()); + } + } + + @Test + public void bundlesFromOmemoBundleTest() throws Exception { + OmemoDevice device = new OmemoDevice(JidCreate.bareFrom("test@test.tld"), 1337); + String bundleXML = "BYKq4s6+plpjAAuCnGO+YFpLP71tMUPgj9ZZmkMSko4ETYMUtzWpc5USMCXStUrCbXFeHTOX3xkBTrU6/MuE/16s4ql1vRN0+JLtYPgZtTm3hb2dHwLA5BUzeTRGjSZwig==BY3AYRje4YBA6W4uuAXYNKzbII/UJbw7qE8kWHI15etiBbzKUJbnqYW19h2dWCyLMbYEpF8r477Ukv9wqMayERQEBeit9Pz31QxklV69BZ0qIxktnUO5TYAgHacFWDYsDnhdBSlbqC8nOpG4TMqvZmCPr6TCPNRcuuoO8Fp2rLGwLFYzBWYsJTsJLtmOgChiz4ilS/cgoEptnfv87tuvq5VpZFV+BY/xq67AkvgIaUO1NbROJeG+r6CcpzByoKvpIaPYyaw/BVRkNWaoocepKEqah95F1DG/uTE1iNEgIZ40wnGd39g/BWMI2ivYBIziOiJsnxJHmiUNN1GcPs3vP/E4vn7hu10BBd7QSMnxJULdKHohRhxUW/DVVRhdaY9SSX16j+CJF8YdBSgQ8NXIkq9fZrtYEdV6qkz5EK7YXVRAiIAFaaDuwUZHBf9Q2r9P4P15GvIiaHWTEU5gLyk/A8ys6Pzz01pLuu9ZBVU6/JKCXqaNa4ApbPFxYExxKuQKuRctk8a1brNcRbJUBfFGHormRpE7x92Eo3IcZcyhxa1//lKyLCNLdlL5Gg1PBd/Je4PdYYJy+6gXrcy7CRqDxBHVgPKN9AOiGxpRX7gkBVtdD2xyJnxPYNJPCT7sYdCXAoD7pMLgf27Dj0dU9vU3BX41BkuSp/qGYDlEzsuE5Tlia1IjzmYsiZRcjAp8D2tqBRY9W9zotVhB7DV2s/I7RYFzzg/Rok0AjU6ODs+iBUtFBb4DW8bURvMuh21PzHGqQlQm6eaI2S4pPLD482yV65IUBSFOrkueqrJDACBIUDpaYiOV51fUuFit4dGYYkvV3StyBT402/OG5FLw2jt+cpYepykpoRVPbI+bWcUx42CqSlwxBeMDEcZ23jnocObmU+esIhAGUvEVCyeiqq+n29Ex38FwBYUDDsKjORZTuZ1ImIIcwhL2peK1K+kTS+QhqCufoIRJBcC/x3Q3zZKv2DKaZlTWpM2Qzg8UogXJ2MmyKQzNI6RJBad8sDrpoVujQTlenKtSfc7JbWlXq5MGDb71q+5DCo88BYlAA5ZyhfiKLFE/U6lufiokNmQjGYP5eMCKhZsuv9BXBbK+LNKsLizmJtd6iEd+QUDdBEgmxIylkTyAS2gxghEHBZ+9oZGHWkRJXPnzT54+UPhQY0vpUdzGltMvneZHqfMLBRRXzcCruX3Gb+kbBodA9OaHcEx/XYT3dpwKK6hx8mYfBTgeei2VCoKk3dBG0FP45UjDoJBV9wQiDn2pW9xwTMkSBZHFWmtevdvuYAbMOpQ7nAAdv+oJxY+A7GFi2jU/PftPBRn4+vobphaBHjOl4gYrVIPHEGMvsn63pbAVgdx69XQRBaUv1tnXFTkJ2jiFT0vlUjH9upOASZHN4EmXGX9n9UAcBU+13hmRR2dkuIqBKxItFFaIdnaAti3beOnmezR+/VtWBbmmB27Q1B72qhxxW++CyrNHCy0UwiAOdkKOBUKCkyZ0BemHNdH5VhufFn9n4qu6e1pVyYjn47ivQy1xHmQL6eh1BSnbvvDgCRGpu/SkapLOe66hxxeJKw7U160d6vxUkYM6BVaUjCB5ZhooG2umXa4CVu6BjmNDkkUUM19pzangbfEUBZD+gzgJ4jXxjfJtMMuWvHJmr/f5vJ+u7vhH4y7KjYM3BW3zmMGSm5jhMTpSjT8u0dsDnK2pXMRVPTr08xmh7vhJBSE7XKChX5zcJrJtoBTAVtUL/gB9iFFb2rE0fKj2b2UQBXVao8jlCDAeOMr4thch7T8Gl+7h2OhcihFAOqkmzf9MBdPqg07COBd2OInhQqc1yCZbixd1CpEbpcG9NjbxGwRUBTzmunAmQX61OaIlTYdfWQU3VtkVXdiLCcegUIOzg/hwBfRxST8negQ2vxMQLufVXdOM/U5IPHCETGsV2uhkdz4HBTlbSzgCQwwkjD/EbEWfcontJobg9u5Odqn/x9QAmu1jBczhPhwuz7KQJW8KICaOgQ0J/+baVwptpqxOtwjFphQhBc0xu3QbrVWQDlIh2VdrfP/GowUF8CN5Q3iCpuabLhIKBaiPlpNAjMviSv3n3tJ+8vAQS7IORAuYJz8pZ/k7CdthBUDuxRt02ajimnvq8BeBQEies6TNDs/E0uvZ7aLHBJAIBQFVdgojx8r3LOjJbAk3CWhtCxU2DxFQHyoBewfJyTk9BdMJqMd7Rkiu9tcmcG1TDU0XKoEHJYPK3FBfRScvqlBrBSyfLhWFdFkUcyczOpIgwo5M5JrJEWWLBJLrOYZHlkYbBeM0hOY/zjvwbGgFHTLAKplV2A57bKzJOd0qhkc22zk1BRIohETGkJNWCDmZjnq7kgawbPWjjBaok4QMTSynT3QcBZykP11RVcyQQmYD+gxGYzL1aQlKce3+EzPZDunh0ftOBYwQvPfzvB97+QcBfCR2YW1EOIDw8KW5FrGmhw4/JxlPBYNXW8MBYvPvtsjo2LVUBy4JZdRfG1WKq2dNY8gt+OFeBSXp4XWf7UkZnM5IK0nQf2/PqHqkMXBq9s/z0YRWUt44BUzNkOEe1jnuoJ4sQpz9DeBojDr1qfpadPr6UbC9SSozBXONDctFe7rI0h5+erFwpp+LjU9MnVONIhpOsX+aiTQTBRElBbzl1sPRtu3r7kQfjqzXn1LxwnRU7gpWxjVMrplKBdGuy7iMtNqzmLOgG8QH63Jc22Mo7Tyquz4UkeT1F+8jBas8r4IYxDpWYCvwTE+esHgELip8d/C3BJP14W74RjJqBTJCGy3cDLmpDqHaXE9NPaEs1kKibx4fNx4SmEc74xMsBRLV4n9fTnOnt0omE4xfl9XsYlml78F7bs585qiWyAwNBUWflPRltdUAfkQbFWjEbTDc5FBImnSAxZk/GYqyGwB1BfKrwvFbKawM8Y18oPzXd8dNk821fZ3s2r+yXFrsLDlHBbqXgiP75kNoQPZ6MYNUdLvepRLQc1EBm5ZYV9VW56EfBazAZ71zu++p6o0LAJNBJIgKSacrque4veToF850TpQWBQptZxpQZugPAK9CMZnR3p+gF0rqYVihRnUIdWAmhMB9Ba70cNznf57ndU6NY62paZcDTTOZmPPS8/JZqLyP+ZVrBSDQwgSHsNjf3MOh4SRRd5jzq/kcjIlf6JEa1SoX06BnBQ1ATRmYMPCyNt8fu/GZ0UeAYWG+WtiDs0uDLsmklI4eBSAofueQkVpDo+I4SoFMdC8S35EOvOn7zmyOG4stSy4BBcpdJVI1JARw8QeKXhbsMIgFxQzTvMSuQeAyvdYfgFIXBURqmjb1lZU66KyPBlCWrjBbISJyqgMW8OaJOchk39YLBVcQm66sdtSBIYK9KymoaZnSvLQPNftBPi+BPfg20VwhBQDNPKib8FK5YquNUAzB7sirGjdj+El+HrOTlMr0w1omBQ66K4ENDGMAlZc7AqcE9dodeeAWfGzSyRYMto57iGAXBTnfRRbPKKBLyoV/BTeIZhkfs629J462AvxuE3pHgvcaBfyu+Cln9QhDLWz1AqOuYgqkh78LROOk4g326gj378gXBRZovbjk6iAtKaKGLvLWlGGml/SUhMtSJEgjrO4tWd9sBZ6OUOFAbuIPTaOwy0qyA6zZ9uYyxskF6i7EXWNQr1NrBWV8bGYfPvLq7Dla1gEqZv3eFej2UzcMWvFOiwurY7ASBSZQ8prazrspZeNKzJzZc0bp1PEs1odEHsI7PLYCUVQdBbAYn4nIg9EjRh92dTHKfgrTC/oAU/92U2WkDtCS+fs1BehKd0MHqJauFPVQsS37SIFwUXo0OOcMembkOhyMGPF8BS7CeBYN+H0s+GwxIrUc5SmdMZEXTprVZD6RYoM+YyxKBSBc48kcT2EN1Siv/hoX8ozuHSEfQXIS93SNY8+Jg7pzBdr7WFoKkG1m/CsTV7J2G9/yXV1pOupqPyU7Rs5FjVoJBc+i4mSLKDMm+ZxkcWMdVdM4p/MlBOFQLb+NF9j4QxlTBfQslqyOk1QwcdrJRJVUvlHUYGJc115O17sb5HIP7GE2BbHvfsMnJu2y60YmI509hoUkgGN1UqrOMLMwoC8TDqp6BVQDMiH5KfKHZLbhTwXxR4RdsADov1gD2elDd6SO+hIQBaNnLStoh3EygkLfA9tjULQYg6X7L/n1jNQeaFKaGjsaBffy5atUJ49XgzsxXMiAopLhTU0rJtGIId0g+kggLBYaBb5fC0qp2eJq8HvJVkf7MIJk+eBZ3TVasvwCn8t4MhEhBb8H48LSq/nxpOKovpLVYw8X3mIJM7JMk3yYgFUKdL0pBTfHeYAsa2hl/aoA3wslmL9RT+O26P6OWs0J2dif5o5pBf7u5QrY3Wrn0PYaRri5nDL6p6iNHFLSk6781wys0hkpBSBryRkeNrvLgGJgh95g9oWLmrptWVPIGPSzoXrVNlAd"; + OmemoBundleVAxolotlElement bundle = new OmemoBundleVAxolotlProvider().parse(TestUtils.getParser(bundleXML)); + HashMap bundles = keyUtil.BUNDLE.bundles(bundle, device); + + assertEquals("There must be 100 bundles in the HashMap.", 100, bundles.size()); + assertNotNull(keyUtil.BUNDLE.identityKey(bundle)); + + Iterator it = bundles.keySet().iterator(); + while (it.hasNext()) { + assertNotNull(keyUtil.BUNDLE.preKeyPublic(bundle, it.next())); + } + + assertEquals(1, keyUtil.BUNDLE.signedPreKeyId(bundle)); + assertNotNull(keyUtil.BUNDLE.signedPreKeyPublic(bundle)); + assertNotNull(keyUtil.BUNDLE.signedPreKeySignature(bundle)); + } +} diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreConnectorTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreConnectorTest.java new file mode 100644 index 000000000..c1aa64210 --- /dev/null +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smack/omemo/SignalOmemoStoreConnectorTest.java @@ -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)); + } +} diff --git a/smack-omemo/build.gradle b/smack-omemo/build.gradle new file mode 100644 index 000000000..b6b838986 --- /dev/null +++ b/smack-omemo/build.gradle @@ -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") +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/FileBasedOmemoStore.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/FileBasedOmemoStore.java new file mode 100644 index 000000000..7728d435c --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/FileBasedOmemoStore.java @@ -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 + extends OmemoStore { + + 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 loadOmemoPreKeys(OmemoManager omemoManager) { + File preKeyDirectory = hierarchy.getPreKeysDirectory(omemoManager); + HashMap 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 loadOmemoSignedPreKeys(OmemoManager omemoManager) { + File signedPreKeysDirectory = hierarchy.getSignedPreKeysDirectory(omemoManager); + HashMap 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 loadAllRawSessionsOf(OmemoManager omemoManager, BareJid contact) { + File contactsDirectory = hierarchy.getContactsDir(omemoManager, contact); + HashMap 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 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 readIntegers(File target) throws IOException { + if (target == null) { + throw new IOException("Could not write integers to null-path."); + } + + HashSet 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 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; + } + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoConfiguration.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoConfiguration.java new file mode 100644 index 000000000..4635fd6e4 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoConfiguration.java @@ -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; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoFingerprint.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoFingerprint.java new file mode 100644 index 000000000..23b3f7a2c --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoFingerprint.java @@ -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(); + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoInitializer.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoInitializer.java new file mode 100644 index 000000000..cea430df6 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoInitializer.java @@ -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"; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoManager.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoManager.java new file mode 100644 index 000000000..db397a9a0 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoManager.java @@ -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> INSTANCES = new WeakHashMap<>(); + private final OmemoService service; + + private final HashSet omemoMessageListeners = new HashSet<>(); + private final HashSet omemoMucMessageListeners = new HashSet<>(); + + private OmemoService.OmemoStanzaListener omemoStanzaListener; + private OmemoService.OmemoCarbonCopyListener omemoCarbonCopyListener; + + private 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 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 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 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 decryptMamQueryResult(MamManager.MamQueryResult mamQueryResult) throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException { + List 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 getActiveFingerprints(BareJid contact) { + HashMap fingerprints = new HashMap<>(); + CachedDeviceList deviceList = getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(this, contact); + for (int id : deviceList.getActiveDevices()) { + OmemoDevice device = new OmemoDevice(contact, id); + OmemoFingerprint fingerprint = null; + try { + fingerprint = getFingerprint(device); + } catch (CannotEstablishOmemoSessionException e) { + LOGGER.log(Level.WARNING, "Could not build session with device " + id + + " of user " + contact + ": " + e.getMessage()); + } + + 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 + 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 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; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java new file mode 100644 index 000000000..5712c9420 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java @@ -0,0 +1,1379 @@ +/** + * + * 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.bouncycastle.jce.provider.BouncyCastleProvider; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.StanzaListener; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.StanzaFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smackx.carbons.CarbonCopyReceivedListener; +import org.jivesoftware.smackx.carbons.CarbonManager; +import org.jivesoftware.smackx.carbons.packet.CarbonExtension; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; +import org.jivesoftware.smackx.forward.packet.Forwarded; +import org.jivesoftware.smackx.mam.MamManager; +import org.jivesoftware.smackx.muc.MultiUserChat; +import org.jivesoftware.smackx.muc.MultiUserChatManager; +import org.jivesoftware.smackx.omemo.element.OmemoBundleVAxolotlElement; +import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement; +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.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.IdentityKeyWrapper; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; +import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation; +import org.jivesoftware.smackx.omemo.internal.OmemoSession; +import org.jivesoftware.smackx.omemo.util.OmemoConstants; +import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder; +import org.jivesoftware.smackx.pep.PEPManager; +import org.jivesoftware.smackx.pubsub.LeafNode; +import org.jivesoftware.smackx.pubsub.PayloadItem; +import org.jivesoftware.smackx.pubsub.PubSubAssertionError; +import org.jivesoftware.smackx.pubsub.PubSubException; +import org.jivesoftware.smackx.pubsub.PubSubManager; +import org.jxmpp.jid.BareJid; + +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.security.Security; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL; +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID; +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.PEP_NODE_DEVICE_LIST; +import static org.jivesoftware.smackx.omemo.util.OmemoConstants.PEP_NODE_DEVICE_LIST_NOTIFY; + +/** + * This class contains OMEMO related logic and registers listeners etc. + * + * @param IdentityKeyPair class + * @param IdentityKey class + * @param PreKey class + * @param SignedPreKey class + * @param Session class + * @param Address class + * @param Elliptic Curve PublicKey class + * @param Bundle class + * @param Cipher class + * @author Paul Schaub + */ +public abstract class OmemoService { + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + protected static final Logger LOGGER = Logger.getLogger(OmemoService.class.getName()); + + private static OmemoService INSTANCE; + + protected OmemoStore omemoStore; + + public static OmemoService getInstance() { + if (INSTANCE == null) { + throw new IllegalStateException("No OmemoService registered"); + } + return INSTANCE; + } + + /** + * Set singleton instance. Throws an IllegalStateException, if there is already a service set as instance. + * + * @param omemoService instance + */ + protected static void setInstance(OmemoService omemoService) { + if (INSTANCE != null) { + throw new IllegalStateException("An OmemoService is already registered"); + } + INSTANCE = omemoService; + } + + public static boolean isServiceRegistered() { + return INSTANCE != null; + } + + /** + * Return the used omemoStore backend. + * If there is no store backend set yet, set the default one (typically a file-based one). + * + * @return omemoStore backend + */ + public OmemoStore + getOmemoStoreBackend() { + if (omemoStore == null) { + setOmemoStoreBackend(createDefaultOmemoStoreBackend()); + return getOmemoStoreBackend(); + } + return omemoStore; + } + + /** + * Set an omemoStore as backend. Throws an IllegalStateException, if there is already a backend set. + * + * @param omemoStore store. + */ + public void setOmemoStoreBackend( + OmemoStore omemoStore) { + if (this.omemoStore != null) { + throw new IllegalStateException("An OmemoStore backend has already been set."); + } + this.omemoStore = omemoStore; + } + + /** + * Create a default OmemoStore object. + * + * @return default omemoStore. + */ + public abstract OmemoStore + createDefaultOmemoStoreBackend(); + + /** + * Create a new OmemoService object. This should only happen once. + * When the service gets created, it tries a placeholder crypto function in order to test, if all necessary + * algorithms are available on the system. + * + * @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 OmemoService() + throws NoSuchPaddingException, InvalidKeyException, UnsupportedEncodingException, IllegalBlockSizeException, + BadPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException { + + //Check availability of algorithms and encodings needed for crypto + checkAvailableAlgorithms(); + } + + /** + * Initialize OMEMO functionality for OmemoManager omemoManager. + * + * @param omemoManager OmemoManager we'd like to initialize. + * @throws InterruptedException + * @throws CorruptedOmemoKeyException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + * @throws SmackException.NotLoggedInException + * @throws PubSubException.NotALeafNodeException + */ + void initialize(OmemoManager omemoManager) throws InterruptedException, CorruptedOmemoKeyException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException, SmackException.NotLoggedInException, PubSubException.NotALeafNodeException { + if (!omemoManager.getConnection().isAuthenticated()) { + throw new SmackException.NotLoggedInException(); + } + + boolean mustPublishId = false; + if (getOmemoStoreBackend().isFreshInstallation(omemoManager)) { + LOGGER.log(Level.INFO, "No key material found. Looks like we have a fresh installation."); + //Create new key material and publish it to the server + regenerate(omemoManager, omemoManager.getDeviceId()); + mustPublishId = true; + } + + //Get fresh device list from server + mustPublishId |= refreshOwnDeviceList(omemoManager); + + publishDeviceIdIfNeeded(omemoManager, false, mustPublishId); + publishBundle(omemoManager); + + subscribeToDeviceLists(omemoManager); + registerOmemoMessageStanzaListeners(omemoManager); //Wait for new OMEMO messages + getOmemoStoreBackend().initializeOmemoSessions(omemoManager); //Preload existing OMEMO sessions + } + + /** + * Test availability of required algorithms. We do this in advance, so we can simplify exception handling later. + * + * @throws NoSuchPaddingException + * @throws UnsupportedEncodingException + * @throws InvalidAlgorithmParameterException + * @throws NoSuchAlgorithmException + * @throws IllegalBlockSizeException + * @throws BadPaddingException + * @throws NoSuchProviderException + * @throws InvalidKeyException + */ + protected static void checkAvailableAlgorithms() throws NoSuchPaddingException, UnsupportedEncodingException, + InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, + NoSuchProviderException, InvalidKeyException { + //Test crypto functions + new OmemoMessageBuilder<>(null, null, ""); + } + + /** + * Generate a new unique deviceId and regenerate new keys. + * + * @param omemoManager OmemoManager we want to regenerate. + * @param nDeviceId new DeviceId we want to use with the newly generated keys. + * @throws CorruptedOmemoKeyException when freshly generated identityKey is invalid + * (should never ever happen *crosses fingers*) + */ + void regenerate(OmemoManager omemoManager, Integer nDeviceId) throws CorruptedOmemoKeyException { + //Generate unique ID that is not already taken + while (nDeviceId == null || !getOmemoStoreBackend().isAvailableDeviceId(omemoManager, nDeviceId)) { + nDeviceId = OmemoManager.randomDeviceId(); + } + + getOmemoStoreBackend().forgetOmemoSessions(omemoManager); + getOmemoStoreBackend().purgeOwnDeviceKeys(omemoManager); + omemoManager.setDeviceId(nDeviceId); + getOmemoStoreBackend().regenerate(omemoManager); + } + + /** + * Publish a fresh bundle to the server. + * + * @param omemoManager OmemoManager + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + * @throws CorruptedOmemoKeyException + * @throws XMPPException.XMPPErrorException + */ + void publishBundle(OmemoManager omemoManager) + throws SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException, CorruptedOmemoKeyException, XMPPException.XMPPErrorException { + Date lastSignedPreKeyRenewal = getOmemoStoreBackend().getDateOfLastSignedPreKeyRenewal(omemoManager); + if (OmemoConfiguration.getRenewOldSignedPreKeys() && lastSignedPreKeyRenewal != null) { + if (System.currentTimeMillis() - lastSignedPreKeyRenewal.getTime() + > 1000L * 60 * 60 * OmemoConfiguration.getRenewOldSignedPreKeysAfterHours()) { + LOGGER.log(Level.INFO, "Renewing signedPreKey"); + getOmemoStoreBackend().changeSignedPreKey(omemoManager); + } + } else { + getOmemoStoreBackend().setDateOfLastSignedPreKeyRenewal(omemoManager); + } + + //publish + PubSubManager.getInstance(omemoManager.getConnection(), omemoManager.getOwnJid()) + .tryToPublishAndPossibleAutoCreate(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(omemoManager.getDeviceId()), + new PayloadItem<>(getOmemoStoreBackend().packOmemoBundle(omemoManager))); + } + + /** + * Publish our deviceId in case it is not on the list already. + * This method calls publishDeviceIdIfNeeded(omemoManager, deleteOtherDevices, false). + * @param omemoManager OmemoManager + * @param deleteOtherDevices Do we want to remove other devices from the list? + * @throws InterruptedException + * @throws PubSubException.NotALeafNodeException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + */ + void publishDeviceIdIfNeeded(OmemoManager omemoManager, boolean deleteOtherDevices) throws InterruptedException, + PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, SmackException.NoResponseException { + publishDeviceIdIfNeeded(omemoManager, deleteOtherDevices, false); + } + + /** + * Publish our deviceId in case it is not on the list already. + * + * @param omemoManager OmemoManager + * @param deleteOtherDevices Do we want to remove other devices from the list? + * If we do, publish the list with only our id, regardless if we were on the list + * already. + * @param publish Do we want to force publishing our id? + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + * @throws XMPPException.XMPPErrorException + * @throws PubSubException.NotALeafNodeException + */ + void publishDeviceIdIfNeeded(OmemoManager omemoManager, boolean deleteOtherDevices, boolean publish) + throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, + XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException { + + CachedDeviceList deviceList = getOmemoStoreBackend().loadCachedDeviceList(omemoManager, omemoManager.getOwnJid()); + + Set deviceListIds; + if (deviceList == null) { + deviceListIds = new HashSet<>(); + } else { + deviceListIds = new HashSet<>(deviceList.getActiveDevices()); + } + + if (deleteOtherDevices) { + deviceListIds.clear(); + } + + int ourDeviceId = omemoManager.getDeviceId(); + if (deviceListIds.add(ourDeviceId)) { + publish = true; + } + + publish |= removeStaleDevicesIfNeeded(omemoManager, deviceListIds); + + if (publish) { + publishDeviceIds(omemoManager, new OmemoDeviceListVAxolotlElement(deviceListIds)); + } + } + + /** + * Remove stale devices from our device list. + * This does only delete devices, if that's configured in OmemoConfiguration. + * + * @param omemoManager OmemoManager + * @param deviceListIds deviceIds we plan to publish. Stale devices are deleted from that list. + * @return + */ + boolean removeStaleDevicesIfNeeded(OmemoManager omemoManager, Set deviceListIds) { + boolean publish = false; + int ownDeviceId = omemoManager.getDeviceId(); + //Clear devices that we didn't receive a message from for a while + Iterator it = deviceListIds.iterator(); + while (OmemoConfiguration.getDeleteStaleDevices() && it.hasNext()) { + int id = it.next(); + if (id == ownDeviceId) { + //Skip own id + continue; + } + + OmemoDevice d = new OmemoDevice(omemoManager.getOwnJid(), id); + Date date = getOmemoStoreBackend().getDateOfLastReceivedMessage(omemoManager, d); + + if (date == null) { + getOmemoStoreBackend().setDateOfLastReceivedMessage(omemoManager, d); + } else { + if (System.currentTimeMillis() - date.getTime() > 1000L * 60 * 60 * OmemoConfiguration.getDeleteStaleDevicesAfterHours()) { + LOGGER.log(Level.INFO, "Remove device " + id + " because of more than " + + OmemoConfiguration.getDeleteStaleDevicesAfterHours() + " hours of inactivity."); + it.remove(); + publish = true; + } + } + } + return publish; + } + + /** + * Publish the given deviceList to the server. + * + * @param omemoManager OmemoManager + * @param deviceList list of deviceIDs + * @throws InterruptedException Exception + * @throws XMPPException.XMPPErrorException Exception + * @throws SmackException.NotConnectedException Exception + * @throws SmackException.NoResponseException Exception + * @throws PubSubException.NotALeafNodeException Exception + */ + static void publishDeviceIds(OmemoManager omemoManager, OmemoDeviceListElement deviceList) + throws InterruptedException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, SmackException.NoResponseException, PubSubException.NotALeafNodeException { + PubSubManager.getInstance(omemoManager.getConnection(), omemoManager.getOwnJid()) + .tryToPublishAndPossibleAutoCreate(OmemoConstants.PEP_NODE_DEVICE_LIST, new PayloadItem<>(deviceList)); + } + + /** + * Fetch the deviceList node of a contact. + * + * @param omemoManager omemoManager + * @param contact contact + * @return LeafNode + * @throws InterruptedException + * @throws PubSubException.NotALeafNodeException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + * @throws PubSubAssertionError.DiscoInfoNodeAssertionError ejabberd bug: https://github.com/processone/ejabberd/issues/1717 + */ + static LeafNode fetchDeviceListNode(OmemoManager omemoManager, BareJid contact) + throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, SmackException.NoResponseException, PubSubAssertionError.DiscoInfoNodeAssertionError { + return PubSubManager.getInstance(omemoManager.getConnection(), contact).getLeafNode(PEP_NODE_DEVICE_LIST); + } + + /** + * Directly fetch the device list of a contact. + * + * @param omemoManager OmemoManager + * @param contact BareJid of the contact + * @return The OmemoDeviceListElement of the contact + * @throws XMPPException.XMPPErrorException When + * @throws SmackException.NotConnectedException something + * @throws InterruptedException goes + * @throws SmackException.NoResponseException wrong + * @throws PubSubException.NotALeafNodeException when the device lists node is not a LeafNode + * @throws PubSubAssertionError.DiscoInfoNodeAssertionError ejabberd bug: https://github.com/processone/ejabberd/issues/1717 + */ + static OmemoDeviceListElement fetchDeviceList(OmemoManager omemoManager, BareJid contact) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, PubSubException.NotALeafNodeException, PubSubAssertionError.DiscoInfoNodeAssertionError { + return extractDeviceListFrom(fetchDeviceListNode(omemoManager, contact)); + } + + /** + * Refresh our deviceList from the server. + * + * @param omemoManager omemoManager + * @return true, if we should publish our device list again (because its broken or not existent...) + * + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + */ + private boolean refreshOwnDeviceList(OmemoManager omemoManager) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, XMPPException.XMPPErrorException { + try { + getOmemoStoreBackend().mergeCachedDeviceList(omemoManager, omemoManager.getOwnJid(), + fetchDeviceList(omemoManager, omemoManager.getOwnJid())); + + } catch (XMPPException.XMPPErrorException e) { + + if (e.getXMPPError().getCondition() == XMPPError.Condition.item_not_found) { + LOGGER.log(Level.WARNING, "Could not refresh own deviceList, because the node did not exist: " + + e.getMessage()); + return true; + } + + throw e; + + } catch (PubSubException.NotALeafNodeException e) { + LOGGER.log(Level.WARNING, "Could not refresh own deviceList, because the Node is not a LeafNode: " + + e.getMessage()); + } + + catch (PubSubAssertionError.DiscoInfoNodeAssertionError bug) { + LOGGER.log(Level.WARNING, "Caught a PubSubAssertionError when fetching a deviceList node. " + + "This probably means that we're dealing with an ejabberd server and the LeafNode does not exist."); + return true; + } + return false; + } + + /** + * Refresh the deviceList of contact and merge it with the one stored locally. + * @param omemoManager omemoManager + * @param contact contact + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + */ + void refreshDeviceList(OmemoManager omemoManager, BareJid contact) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { + OmemoDeviceListElement omemoDeviceListElement; + try { + omemoDeviceListElement = fetchDeviceList(omemoManager, contact); + } catch (PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException e) { + LOGGER.log(Level.WARNING, "Could not fetch device list of " + contact + ": " + e, e); + return; + } + catch (PubSubAssertionError.DiscoInfoNodeAssertionError bug) { + LOGGER.log(Level.WARNING, "Caught a PubSubAssertionError when fetching a deviceList node. " + + "This probably means that the LeafNode does not exist."); + return; + } + + getOmemoStoreBackend().mergeCachedDeviceList(omemoManager, contact, omemoDeviceListElement); + } + + /** + * Fetch the OmemoBundleElement of the contact. + * + * @param omemoManager OmemoManager + * @param contact the contacts BareJid + * @return the OmemoBundleElement of the contact + * @throws XMPPException.XMPPErrorException When + * @throws SmackException.NotConnectedException something + * @throws InterruptedException goes + * @throws SmackException.NoResponseException wrong + * @throws PubSubException.NotALeafNodeException when the bundles node is not a LeafNode + */ + static OmemoBundleVAxolotlElement fetchBundle(OmemoManager omemoManager, OmemoDevice contact) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, PubSubException.NotALeafNodeException, PubSubAssertionError.DiscoInfoNodeAssertionError { + LeafNode node = PubSubManager.getInstance(omemoManager.getConnection(), contact.getJid()).getLeafNode(PEP_NODE_BUNDLE_FROM_DEVICE_ID(contact.getDeviceId())); + return extractBundleFrom(node); + } + + /** + * Extract the OmemoBundleElement of a contact from a LeafNode. + * + * @param node typically a LeafNode containing the OmemoBundles of a contact + * @return the OmemoBundleElement + * @throws XMPPException.XMPPErrorException When + * @throws SmackException.NotConnectedException something + * @throws InterruptedException goes + * @throws SmackException.NoResponseException wrong + */ + private static OmemoBundleVAxolotlElement extractBundleFrom(LeafNode node) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { + if (node == null) { + return null; + } + try { + return (OmemoBundleVAxolotlElement) ((PayloadItem) node.getItems().get(0)).getPayload(); + } catch (IndexOutOfBoundsException e) { + return null; + } + } + + /** + * Extract the OmemoDeviceListElement of a contact from a node containing his OmemoDeviceListElement. + * + * @param node typically a LeafNode containing the OmemoDeviceListElement of a contact + * @return the extracted OmemoDeviceListElement. + * @throws XMPPException.XMPPErrorException When + * @throws SmackException.NotConnectedException something + * @throws InterruptedException goes + * @throws SmackException.NoResponseException wrong + */ + private static OmemoDeviceListElement extractDeviceListFrom(LeafNode node) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { + if (node == null) { + LOGGER.log(Level.WARNING, "DeviceListNode is null."); + return null; + } + List items = node.getItems(); + if (items.size() > 0) { + OmemoDeviceListVAxolotlElement listElement = (OmemoDeviceListVAxolotlElement) ((PayloadItem) items.get(items.size() - 1)).getPayload(); + if (items.size() > 1) { + node.deleteAllItems(); + node.publish(new PayloadItem<>(listElement)); + } + return listElement; + } + + Set emptySet = Collections.emptySet(); + return new OmemoDeviceListVAxolotlElement(emptySet); + } + + /** + * Subscribe to the device lists of our contacts using PEP. + * + * @param omemoManager omemoManager we want to subscribe with + */ + private static void subscribeToDeviceLists(OmemoManager omemoManager) { + registerDeviceListListener(omemoManager); + ServiceDiscoveryManager.getInstanceFor(omemoManager.getConnection()).addFeature(PEP_NODE_DEVICE_LIST_NOTIFY); + } + + /** + * Build sessions for all devices of the contact that we do not have a session with yet. + * + * @param omemoManager omemoManager + * @param jid the BareJid of the contact + */ + void buildOrCreateOmemoSessionsFromBundles(OmemoManager omemoManager, BareJid jid) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, CannotEstablishOmemoSessionException { + CachedDeviceList devices = getOmemoStoreBackend().loadCachedDeviceList(omemoManager, jid); + CannotEstablishOmemoSessionException sessionException = null; + if (devices == null || devices.getAllDevices().isEmpty()) { + refreshDeviceList(omemoManager, jid); + devices = getOmemoStoreBackend().loadCachedDeviceList(omemoManager, jid); + } + + for (int id : devices.getActiveDevices()) { + OmemoDevice device = new OmemoDevice(jid, id); + if (getOmemoStoreBackend().containsRawSession(omemoManager, device)) { + //We have a session already. + continue; + } + + //Build missing session + try { + buildSessionFromOmemoBundle(omemoManager, device, false); + } catch (CannotEstablishOmemoSessionException e) { + + if (sessionException == null) { + sessionException = e; + } else { + sessionException.addFailures(e); + } + + } catch (CorruptedOmemoKeyException e) { + CannotEstablishOmemoSessionException fail = + new CannotEstablishOmemoSessionException(device, e); + + if (sessionException == null) { + sessionException = fail; + } else { + sessionException.addFailures(fail); + } + } + } + + if (sessionException != null) { + throw sessionException; + } + } + + /** + * Build an OmemoSession for the given OmemoDevice. + * + * @param omemoManager omemoManager + * @param device OmemoDevice + * @param fresh Do we want to build a session even if we already have one? + * @throws CannotEstablishOmemoSessionException when no session could be established + * @throws CorruptedOmemoKeyException when the bundle contained an invalid OMEMO identityKey + */ + public void buildSessionFromOmemoBundle(OmemoManager omemoManager, OmemoDevice device, boolean fresh) throws CannotEstablishOmemoSessionException, CorruptedOmemoKeyException { + + if (device.equals(omemoManager.getOwnDevice())) { + return; + } + + //Do not build sessions with devices we already know... + if (!fresh && getOmemoStoreBackend().containsRawSession(omemoManager, device)) { + getOmemoStoreBackend().getOmemoSessionOf(omemoManager, device); //Make sure its loaded though + return; + } + + OmemoBundleVAxolotlElement bundle; + try { + bundle = fetchBundle(omemoManager, device); + } catch (SmackException | XMPPException.XMPPErrorException | InterruptedException | PubSubAssertionError e) { + throw new CannotEstablishOmemoSessionException(device, e); + } + + HashMap bundles = getOmemoStoreBackend().keyUtil().BUNDLE.bundles(bundle, device); + + //Select random Bundle + int randomIndex = new Random().nextInt(bundles.size()); + T_Bundle randomPreKeyBundle = new ArrayList<>(bundles.values()).get(randomIndex); + //Build raw session + processBundle(omemoManager, randomPreKeyBundle, device); + } + + /** + * Process a received bundle. Typically that includes saving keys and building a session. + * + * @param omemoManager omemoManager that will process the bundle + * @param bundle T_Bundle (depends on used Signal/Olm library) + * @param device OmemoDevice + * @throws CorruptedOmemoKeyException + */ + protected abstract void processBundle(OmemoManager omemoManager, T_Bundle bundle, OmemoDevice device) throws CorruptedOmemoKeyException; + + /** + * Register a PEPListener that listens for deviceList updates. + * + * @param omemoManager omemoManager we want to register with. + */ + private static void registerDeviceListListener(final OmemoManager omemoManager) { + PEPManager.getInstanceFor(omemoManager.getConnection()).removePEPListener(omemoManager.deviceListUpdateListener); + PEPManager.getInstanceFor(omemoManager.getConnection()).addPEPListener(omemoManager.deviceListUpdateListener); + } + + /** + * Process a received message. Try to decrypt it in case we are a recipient device. If we are not a recipient + * device, return null. + * + * @param sender the BareJid of the sender of the message + * @param message the encrypted message + * @param information OmemoMessageInformation object which will contain meta data about the decrypted message + * @return decrypted message or null + * @throws NoRawSessionException + * @throws InterruptedException + * @throws SmackException.NoResponseException + * @throws SmackException.NotConnectedException + * @throws CryptoFailedException + * @throws XMPPException.XMPPErrorException + * @throws CorruptedOmemoKeyException + */ + private Message processReceivingMessage(OmemoManager omemoManager, OmemoDevice sender, OmemoElement message, final OmemoMessageInformation information) + throws NoRawSessionException, InterruptedException, SmackException.NoResponseException, SmackException.NotConnectedException, + CryptoFailedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException { + + ArrayList messageRecipientKeys = message.getHeader().getKeys(); + //Do we have a key with our ID in the message? + for (OmemoVAxolotlElement.OmemoHeader.Key k : messageRecipientKeys) { + //Only decrypt with our deviceID + if (k.getId() != omemoManager.getDeviceId()) { + continue; + } + + Message decrypted = decryptOmemoMessageElement(omemoManager, sender, message, information); + if (sender.equals(omemoManager.getOwnJid()) && decrypted != null) { + getOmemoStoreBackend().setDateOfLastReceivedMessage(omemoManager, sender); + } + return decrypted; + } + + LOGGER.log(Level.INFO, "There is no key with our deviceId. Silently discard the message."); + return null; + } + + /** + * Decrypt a given OMEMO encrypted message. Return null, if there is no OMEMO element in the message, + * otherwise try to decrypt the message and return a ClearTextMessage object. + * + * @param omemoManager omemoManager of the receiving device + * @param sender barejid of the sender + * @param message encrypted message + * @return decrypted message or null + * @throws InterruptedException Exception + * @throws SmackException.NoResponseException Exception + * @throws SmackException.NotConnectedException Exception + * @throws CryptoFailedException When the message could not be decrypted. + * @throws XMPPException.XMPPErrorException Exception + * @throws CorruptedOmemoKeyException When the used OMEMO keys are invalid. + * @throws NoRawSessionException When there is no session to decrypt the message with in the double + * ratchet library + */ + ClearTextMessage processLocalMessage(OmemoManager omemoManager, BareJid sender, Message message) throws InterruptedException, SmackException.NoResponseException, SmackException.NotConnectedException, CryptoFailedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException, NoRawSessionException { + if (OmemoManager.stanzaContainsOmemoElement(message)) { + OmemoElement omemoMessageElement = message.getExtension(OmemoElement.ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); + OmemoMessageInformation info = new OmemoMessageInformation(); + Message decrypted = processReceivingMessage(omemoManager, + new OmemoDevice(sender, omemoMessageElement.getHeader().getSid()), + omemoMessageElement, info); + return new ClearTextMessage(decrypted != null ? decrypted.getBody() : null, message, info); + } else { + LOGGER.log(Level.WARNING, "Stanza does not contain an OMEMO message."); + return null; + } + } + + /** + * Encrypt a clear text message for the given recipient. + * The body of the message will be encrypted. + * + * @param omemoManager omemoManager of the sending device + * @param recipient BareJid of the recipient + * @param message message to encrypt. + * @return OmemoMessageElement + * @throws CryptoFailedException + * @throws UndecidedOmemoIdentityException + * @throws NoSuchAlgorithmException + */ + OmemoVAxolotlElement processSendingMessage(OmemoManager omemoManager, BareJid recipient, Message message) + throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, CannotEstablishOmemoSessionException { + ArrayList recipients = new ArrayList<>(); + recipients.add(recipient); + return processSendingMessage(omemoManager, recipients, message); + } + + /** + * Encrypt a clear text message for the given recipients. + * The body of the message will be encrypted. + * + * @param omemoManager omemoManager of the sending device. + * @param recipients List of BareJids of all recipients + * @param message message to encrypt. + * @return OmemoMessageElement + * @throws CryptoFailedException + * @throws UndecidedOmemoIdentityException + * @throws NoSuchAlgorithmException + */ + OmemoVAxolotlElement processSendingMessage(OmemoManager omemoManager, ArrayList recipients, Message message) + throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, CannotEstablishOmemoSessionException { + + CannotEstablishOmemoSessionException sessionException = null; + //Them - The contact wants to read the message on all their devices. + HashMap> receivers = new HashMap<>(); + for (BareJid recipient : recipients) { + try { + buildOrCreateOmemoSessionsFromBundles(omemoManager, recipient); + } catch (CannotEstablishOmemoSessionException e) { + + if (sessionException == null) { + sessionException = e; + } else { + sessionException.addFailures(e); + } + } + } + + for (BareJid recipient : recipients) { + CachedDeviceList theirDevices = getOmemoStoreBackend().loadCachedDeviceList(omemoManager, recipient); + ArrayList receivingDevices = new ArrayList<>(); + for (int id : theirDevices.getActiveDevices()) { + OmemoDevice recipientDevice = new OmemoDevice(recipient, id); + + if (getOmemoStoreBackend().containsRawSession(omemoManager, recipientDevice)) { + receivingDevices.add(recipientDevice); + } + + if (sessionException != null) { + sessionException.addSuccess(recipientDevice); + } + } + + if (!receivingDevices.isEmpty()) { + receivers.put(recipient, receivingDevices); + } + } + + //Us - We want to read the message on all of our devices + CachedDeviceList ourDevices = getOmemoStoreBackend().loadCachedDeviceList(omemoManager, omemoManager.getOwnJid()); + if (ourDevices == null) { + ourDevices = new CachedDeviceList(); + } + + ArrayList ourReceivingDevices = new ArrayList<>(); + for (int id : ourDevices.getActiveDevices()) { + OmemoDevice ourDevice = new OmemoDevice(omemoManager.getOwnJid(), id); + if (id == omemoManager.getDeviceId()) { + //Don't build session with our exact device. + continue; + } + + Date lastReceived = getOmemoStoreBackend().getDateOfLastReceivedMessage(omemoManager, ourDevice); + if (lastReceived == null) { + getOmemoStoreBackend().setDateOfLastReceivedMessage(omemoManager, ourDevice); + lastReceived = new Date(); + } + + if (OmemoConfiguration.getIgnoreStaleDevices() && System.currentTimeMillis() - lastReceived.getTime() + > 1000L * 60 * 60 * OmemoConfiguration.getIgnoreStaleDevicesAfterHours()) { + LOGGER.log(Level.WARNING, "Refusing to encrypt message for stale device " + ourDevice + + " which was inactive for at least " + OmemoConfiguration.getIgnoreStaleDevicesAfterHours() + + " hours."); + } else { + if (getOmemoStoreBackend().containsRawSession(omemoManager, ourDevice)) { + ourReceivingDevices.add(ourDevice); + } + } + } + + if (!ourReceivingDevices.isEmpty()) { + receivers.put(omemoManager.getOwnJid(), ourReceivingDevices); + } + + if (sessionException != null && sessionException.requiresThrowing()) { + throw sessionException; + } + + return encryptOmemoMessage(omemoManager, receivers, message); + } + + /** + * Decrypt a incoming OmemoMessageElement that was sent by the OmemoDevice 'from'. + * + * @param omemoManager omemoManager of the decrypting device. + * @param from OmemoDevice that sent the message + * @param message Encrypted OmemoMessageElement + * @param information OmemoMessageInformation object which will contain metadata about the encryption + * @return Decrypted message + * @throws CryptoFailedException when decrypting message fails for some reason + * @throws InterruptedException + * @throws CorruptedOmemoKeyException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + * @throws NoRawSessionException + */ + private Message decryptOmemoMessageElement(OmemoManager omemoManager, OmemoDevice from, OmemoElement message, + final OmemoMessageInformation information) + throws CryptoFailedException, InterruptedException, CorruptedOmemoKeyException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, SmackException.NoResponseException, NoRawSessionException { + + CipherAndAuthTag transportedKey = decryptTransportedOmemoKey(omemoManager, from, message, information); + return OmemoSession.decryptMessageElement(message, transportedKey); + } + + /** + * Decrypt a messageKey that was transported in an OmemoElement. + * + * @param omemoManager omemoManager of the receiving device. + * @param sender omemoDevice of the sender. + * @param omemoMessage omemoElement containing the key. + * @param messageInfo omemoMessageInformation that will contain metadata about the encryption. + * @return a CipherAndAuthTag pair + * @throws CryptoFailedException + * @throws NoRawSessionException + * @throws InterruptedException + * @throws CorruptedOmemoKeyException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + */ + private CipherAndAuthTag decryptTransportedOmemoKey(OmemoManager omemoManager, OmemoDevice sender, + OmemoElement omemoMessage, + OmemoMessageInformation messageInfo) + throws CryptoFailedException, NoRawSessionException, InterruptedException, CorruptedOmemoKeyException, + XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException { + + int preKeyCountBefore = getOmemoStoreBackend().loadOmemoPreKeys(omemoManager).size(); + + OmemoSession + session = getOmemoStoreBackend().getOmemoSessionOf(omemoManager, sender); + CipherAndAuthTag cipherAndAuthTag = session.decryptTransportedKey(omemoMessage, omemoManager.getDeviceId()); + + messageInfo.setSenderDevice(sender); + messageInfo.setSenderIdentityKey(new IdentityKeyWrapper(session.getIdentityKey())); + + if (preKeyCountBefore != getOmemoStoreBackend().loadOmemoPreKeys(omemoManager).size()) { + LOGGER.log(Level.INFO, "We used up a preKey. Publish new Bundle."); + publishBundle(omemoManager); + } + return cipherAndAuthTag; + } + + /** + * Encrypt the message and return it as an OmemoMessageElement. + * + * @param omemoManager omemoManager of the encrypting device. + * @param recipients List of devices that will be able to decipher the message. + * @param message Clear text message + * + * @throws CryptoFailedException when some cryptographic function fails + * @throws UndecidedOmemoIdentityException when the identity of one or more contacts is undecided + * + * @return OmemoMessageElement + */ + OmemoVAxolotlElement encryptOmemoMessage(OmemoManager omemoManager, HashMap> recipients, Message message) + throws CryptoFailedException, UndecidedOmemoIdentityException { + + OmemoMessageBuilder + builder; + try { + builder = new OmemoMessageBuilder<>(omemoManager, getOmemoStoreBackend(), message.getBody()); + } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | + NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { + throw new CryptoFailedException(e); + } + + UndecidedOmemoIdentityException undecided = null; + + for (Map.Entry> entry : recipients.entrySet()) { + for (OmemoDevice c : entry.getValue()) { + try { + builder.addRecipient(c); + } catch (CorruptedOmemoKeyException e) { + //TODO: How to react? + LOGGER.log(Level.SEVERE, "encryptOmemoMessage failed to establish a session with device " + + c + ": " + e.getMessage()); + } catch (UndecidedOmemoIdentityException e) { + //Collect all undecided devices + if (undecided == null) { + undecided = e; + } else { + undecided.join(e); + } + } + } + } + + if (undecided != null) { + throw undecided; + } + + return builder.finish(); + } + + /** + * Prepares a keyTransportElement with a random aes key and iv. + * + * @param omemoManager omemoManager of the sending device. + * @param recipients recipients of the omemoKeyTransportElement + * @return KeyTransportElement + * @throws CryptoFailedException + * @throws UndecidedOmemoIdentityException + * @throws CorruptedOmemoKeyException + * @throws CannotEstablishOmemoSessionException + */ + OmemoVAxolotlElement prepareOmemoKeyTransportElement(OmemoManager omemoManager, OmemoDevice... recipients) throws CryptoFailedException, + UndecidedOmemoIdentityException, CorruptedOmemoKeyException, CannotEstablishOmemoSessionException { + + OmemoMessageBuilder + builder; + try { + builder = new OmemoMessageBuilder<>(omemoManager, getOmemoStoreBackend(), null); + + } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | + NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { + throw new CryptoFailedException(e); + } + + for (OmemoDevice r : recipients) { + builder.addRecipient(r); + } + + return builder.finish(); + } + + /** + * Prepare a KeyTransportElement with aesKey and iv. + * + * @param omemoManager OmemoManager of the sending device. + * @param aesKey AES key + * @param iv initialization vector + * @param recipients recipients + * @return KeyTransportElement + * @throws CryptoFailedException + * @throws UndecidedOmemoIdentityException + * @throws CorruptedOmemoKeyException + * @throws CannotEstablishOmemoSessionException + */ + OmemoVAxolotlElement prepareOmemoKeyTransportElement(OmemoManager omemoManager, byte[] aesKey, byte[] iv, OmemoDevice... recipients) throws CryptoFailedException, + UndecidedOmemoIdentityException, CorruptedOmemoKeyException, CannotEstablishOmemoSessionException { + + OmemoMessageBuilder + builder; + try { + builder = new OmemoMessageBuilder<>(omemoManager, getOmemoStoreBackend(), aesKey, iv); + + } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | + NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { + throw new CryptoFailedException(e); + } + + for (OmemoDevice r : recipients) { + builder.addRecipient(r); + } + + return builder.finish(); + } + + /** + * Return a new RatchetUpdateMessage. + * + * @param omemoManager omemoManager of the sending device. + * @param recipient recipient + * @param preKeyMessage if true, a new session will be built for this message (useful to repair broken sessions) + * otherwise the message will be encrypted using the existing session. + * @return OmemoRatchetUpdateMessage + * @throws CannotEstablishOmemoSessionException + * @throws CorruptedOmemoKeyException + * @throws CryptoFailedException + * @throws UndecidedOmemoIdentityException + */ + protected Message getOmemoRatchetUpdateMessage(OmemoManager omemoManager, OmemoDevice recipient, boolean preKeyMessage) throws CannotEstablishOmemoSessionException, CorruptedOmemoKeyException, CryptoFailedException, UndecidedOmemoIdentityException { + if (preKeyMessage) { + buildSessionFromOmemoBundle(omemoManager, recipient, true); + } + + OmemoVAxolotlElement keyTransportElement = prepareOmemoKeyTransportElement(omemoManager, recipient); + Message ratchetUpdateMessage = omemoManager.finishMessage(keyTransportElement); + ratchetUpdateMessage.setTo(recipient.getJid()); + + return ratchetUpdateMessage; + } + + /** + * Send an OmemoRatchetUpdateMessage to recipient. If preKeyMessage is true, the message will be encrypted using a + * freshly built session. This can be used to repair broken sessions. + * + * @param omemoManager omemoManager of the sending device. + * @param recipient recipient + * @param preKeyMessage shall this be a preKeyMessage? + * @throws UndecidedOmemoIdentityException + * @throws CorruptedOmemoKeyException + * @throws CryptoFailedException + * @throws CannotEstablishOmemoSessionException + */ + protected void sendOmemoRatchetUpdateMessage(OmemoManager omemoManager, OmemoDevice recipient, boolean preKeyMessage) throws UndecidedOmemoIdentityException, CorruptedOmemoKeyException, CryptoFailedException, CannotEstablishOmemoSessionException { + Message ratchetUpdateMessage = getOmemoRatchetUpdateMessage(omemoManager, recipient, preKeyMessage); + + try { + omemoManager.getConnection().sendStanza(ratchetUpdateMessage); + + } catch (SmackException.NotConnectedException | InterruptedException e) { + LOGGER.log(Level.WARNING, "sendOmemoRatchetUpdateMessage failed: " + e.getMessage()); + } + } + + /** + * Listen for incoming messages and carbons, decrypt them and pass the cleartext messages to the registered + * OmemoMessageListeners. + * + * @param omemoManager omemoManager we want to register with + */ + private void registerOmemoMessageStanzaListeners(OmemoManager omemoManager) { + omemoManager.getConnection().removeAsyncStanzaListener(omemoManager.getOmemoStanzaListener()); + omemoManager.getConnection().addAsyncStanzaListener(omemoManager.getOmemoStanzaListener(), omemoStanzaFilter); + + CarbonManager.getInstanceFor(omemoManager.getConnection()).removeCarbonCopyReceivedListener(omemoManager.getOmemoCarbonCopyListener()); + CarbonManager.getInstanceFor(omemoManager.getConnection()).addCarbonCopyReceivedListener(omemoManager.getOmemoCarbonCopyListener()); + } + + /** + * StanzaFilter that filters messages containing a OMEMO element. + */ + private final StanzaFilter omemoStanzaFilter = new StanzaFilter() { + @Override + public boolean accept(Stanza stanza) { + return stanza instanceof Message && OmemoManager.stanzaContainsOmemoElement(stanza); + } + }; + + /** + * Try to decrypt a mamQueryResult. Note that OMEMO messages can only be decrypted once on a device, so if you + * try to decrypt a message that has been decrypted earlier in time, the decryption will fail. You should handle + * message history locally when using OMEMO, since you cannot rely on MAM. + * + * @param omemoManager omemoManager of the decrypting device. + * @param mamQueryResult mamQueryResult that shall be decrypted. + * @return list of decrypted messages. + * @throws InterruptedException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + */ + List decryptMamQueryResult(OmemoManager omemoManager, MamManager.MamQueryResult mamQueryResult) + throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException { + List result = new ArrayList<>(); + for (Forwarded f : mamQueryResult.forwardedMessages) { + if (OmemoManager.stanzaContainsOmemoElement(f.getForwardedStanza())) { + //Decrypt OMEMO messages + try { + result.add(processLocalMessage(omemoManager, f.getForwardedStanza().getFrom().asBareJid(), (Message) f.getForwardedStanza())); + } catch (NoRawSessionException | CorruptedOmemoKeyException | CryptoFailedException e) { + LOGGER.log(Level.WARNING, "decryptMamQueryResult failed to decrypt message from " + + f.getForwardedStanza().getFrom() + " due to corrupted session/key: " + e.getMessage()); + } + } else { + //Wrap cleartext messages + Message m = (Message) f.getForwardedStanza(); + result.add(new ClearTextMessage(m.getBody(), m, + new OmemoMessageInformation(null, null, OmemoMessageInformation.CARBON.NONE, false))); + } + } + return result; + } + + /** + * Return the barejid of the user that sent the message inside the MUC. If the message wasn't sent in a MUC, + * return null; + * + * @param omemoManager omemoManager + * @param stanza message + * @return BareJid of the sender. + */ + private static OmemoDevice getSender(OmemoManager omemoManager, Stanza stanza) { + OmemoElement omemoElement = stanza.getExtension(OmemoElement.ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); + BareJid sender = stanza.getFrom().asBareJid(); + if (isMucMessage(omemoManager, stanza)) { + MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(omemoManager.getConnection()); + MultiUserChat muc = mucm.getMultiUserChat(sender.asEntityBareJidIfPossible()); + sender = muc.getOccupant(sender.asEntityFullJidIfPossible()).getJid().asBareJid(); + } + if (sender == null) { + throw new AssertionError("Sender is null."); + } + return new OmemoDevice(sender, omemoElement.getHeader().getSid()); + } + + /** + * Return true, if the user knows a multiUserChat with a jid matching the sender of the stanza. + * @param omemoManager omemoManager of the user + * @param stanza stanza in question + * @return true if MUC message, otherwise false. + */ + private static boolean isMucMessage(OmemoManager omemoManager, Stanza stanza) { + BareJid sender = stanza.getFrom().asBareJid(); + MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(omemoManager.getConnection()); + + return mucm.getJoinedRooms().contains(sender.asEntityBareJidIfPossible()); + } + + OmemoStanzaListener createStanzaListener(OmemoManager omemoManager) { + return new OmemoStanzaListener(omemoManager, this); + } + + /** + * StanzaListener that listens for incoming omemoElements that are NOT send via carbons. + */ + class OmemoStanzaListener implements StanzaListener { + private final OmemoManager omemoManager; + private final OmemoService + service; + + OmemoStanzaListener(OmemoManager omemoManager, + OmemoService service) { + this.omemoManager = omemoManager; + this.service = service; + } + + @Override + public void processStanza(Stanza stanza) throws SmackException.NotConnectedException, InterruptedException { + Message decrypted; + OmemoElement omemoMessage = stanza.getExtension(OmemoElement.ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); + OmemoMessageInformation messageInfo = new OmemoMessageInformation(); + MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(omemoManager.getConnection()); + OmemoDevice senderDevice = getSender(omemoManager, stanza); + try { + //Is it a MUC message... + if (isMucMessage(omemoManager, stanza)) { + + MultiUserChat muc = mucm.getMultiUserChat(stanza.getFrom().asEntityBareJidIfPossible()); + if (omemoMessage.isMessageElement()) { + + decrypted = processReceivingMessage(omemoManager, senderDevice, omemoMessage, messageInfo); + if (decrypted != null) { + omemoManager.notifyOmemoMucMessageReceived(muc, senderDevice.getJid(), decrypted.getBody(), + (Message) stanza, null, messageInfo); + } + + } else if (omemoMessage.isKeyTransportElement()) { + + CipherAndAuthTag cipherAndAuthTag = decryptTransportedOmemoKey(omemoManager, senderDevice, omemoMessage, messageInfo); + if (cipherAndAuthTag != null) { + omemoManager.notifyOmemoMucKeyTransportMessageReceived(muc, senderDevice.getJid(), cipherAndAuthTag, + (Message) stanza, null, messageInfo); + } + } + } + //... or a normal chat message... + else { + if (omemoMessage.isMessageElement()) { + + decrypted = service.processReceivingMessage(omemoManager, senderDevice, omemoMessage, messageInfo); + if (decrypted != null) { + omemoManager.notifyOmemoMessageReceived(decrypted.getBody(), (Message) stanza, null, messageInfo); + } + + } else if (omemoMessage.isKeyTransportElement()) { + + CipherAndAuthTag cipherAndAuthTag = decryptTransportedOmemoKey(omemoManager, senderDevice, omemoMessage, messageInfo); + if (cipherAndAuthTag != null) { + omemoManager.notifyOmemoKeyTransportMessageReceived(cipherAndAuthTag, (Message) stanza, null, messageInfo); + } + } + } + + } catch (CryptoFailedException | CorruptedOmemoKeyException | InterruptedException | SmackException.NotConnectedException | XMPPException.XMPPErrorException | SmackException.NoResponseException e) { + LOGGER.log(Level.WARNING, "internal omemoMessageListener failed to decrypt incoming OMEMO message: " + + e.getMessage()); + + } catch (NoRawSessionException e) { + try { + LOGGER.log(Level.INFO, "Received message with invalid session from " + + senderDevice + ". Send RatchetUpdateMessage."); + service.sendOmemoRatchetUpdateMessage(omemoManager, senderDevice, true); + + } catch (UndecidedOmemoIdentityException | CorruptedOmemoKeyException | CannotEstablishOmemoSessionException | CryptoFailedException e1) { + LOGGER.log(Level.WARNING, "internal omemoMessageListener failed to establish a session for incoming OMEMO message: " + + e.getMessage()); + } + } + } + } + + OmemoCarbonCopyListener createOmemoCarbonCopyListener(OmemoManager omemoManager) { + return new OmemoCarbonCopyListener(omemoManager, this, omemoStanzaFilter); + } + + /** + * StanzaListener that listens for incoming OmemoElements that ARE sent in carbons. + */ + class OmemoCarbonCopyListener implements CarbonCopyReceivedListener { + + private final OmemoManager omemoManager; + private final OmemoService service; + private final StanzaFilter filter; + + public OmemoCarbonCopyListener(OmemoManager omemoManager, + OmemoService service, + StanzaFilter filter) { + this.omemoManager = omemoManager; + this.service = service; + this.filter = filter; + } + + @Override + public void onCarbonCopyReceived(CarbonExtension.Direction direction, Message carbonCopy, Message wrappingMessage) { + if (filter.accept(carbonCopy)) { + OmemoDevice senderDevice = getSender(omemoManager, carbonCopy); + Message decrypted; + MultiUserChatManager mucm = MultiUserChatManager.getInstanceFor(omemoManager.getConnection()); + OmemoElement omemoMessage = carbonCopy.getExtension(OmemoElement.ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); + OmemoMessageInformation messageInfo = new OmemoMessageInformation(); + + if (CarbonExtension.Direction.received.equals(direction)) { + messageInfo.setCarbon(OmemoMessageInformation.CARBON.RECV); + } else { + messageInfo.setCarbon(OmemoMessageInformation.CARBON.SENT); + } + + try { + //Is it a MUC message... + if (isMucMessage(omemoManager, carbonCopy)) { + + MultiUserChat muc = mucm.getMultiUserChat(carbonCopy.getFrom().asEntityBareJidIfPossible()); + if (omemoMessage.isMessageElement()) { + + decrypted = processReceivingMessage(omemoManager, senderDevice, omemoMessage, messageInfo); + if (decrypted != null) { + omemoManager.notifyOmemoMucMessageReceived(muc, senderDevice.getJid(), decrypted.getBody(), + carbonCopy, wrappingMessage, messageInfo); + } + + } else if (omemoMessage.isKeyTransportElement()) { + + CipherAndAuthTag cipherAndAuthTag = decryptTransportedOmemoKey(omemoManager, senderDevice, omemoMessage, messageInfo); + if (cipherAndAuthTag != null) { + omemoManager.notifyOmemoMucKeyTransportMessageReceived(muc, senderDevice.getJid(), cipherAndAuthTag, + carbonCopy, wrappingMessage, messageInfo); + } + } + } + //... or a normal chat message... + else { + if (omemoMessage.isMessageElement()) { + + decrypted = service.processReceivingMessage(omemoManager, senderDevice, omemoMessage, messageInfo); + if (decrypted != null) { + omemoManager.notifyOmemoMessageReceived(decrypted.getBody(), carbonCopy, null, messageInfo); + } + + } else if (omemoMessage.isKeyTransportElement()) { + + CipherAndAuthTag cipherAndAuthTag = decryptTransportedOmemoKey(omemoManager, senderDevice, omemoMessage, messageInfo); + if (cipherAndAuthTag != null) { + omemoManager.notifyOmemoKeyTransportMessageReceived(cipherAndAuthTag, carbonCopy, null, messageInfo); + } + } + } + + } catch (CryptoFailedException | CorruptedOmemoKeyException | InterruptedException | SmackException.NotConnectedException | XMPPException.XMPPErrorException | SmackException.NoResponseException e) { + LOGGER.log(Level.WARNING, "internal omemoMessageListener failed to decrypt incoming OMEMO carbon copy: " + + e.getMessage()); + + } catch (NoRawSessionException e) { + try { + LOGGER.log(Level.INFO, "Received OMEMO carbon copy message with invalid session from " + + senderDevice + ". Send RatchetUpdateMessage."); + service.sendOmemoRatchetUpdateMessage(omemoManager, senderDevice, true); + + } catch (UndecidedOmemoIdentityException | CorruptedOmemoKeyException | CannotEstablishOmemoSessionException | CryptoFailedException e1) { + LOGGER.log(Level.WARNING, "internal omemoMessageListener failed to establish a session for incoming OMEMO carbon message: " + + e.getMessage()); + } + } + } + } + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoStore.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoStore.java new file mode 100644 index 000000000..0090574e5 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoStore.java @@ -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 IdentityKeyPair class + * @param IdentityKey class + * @param PreKey class + * @param SignedPreKey class + * @param Session class + * @param Address class + * @param Elliptic Curve PublicKey class + * @param Bundle class + * @param Cipher class + * @author Paul Schaub + */ +public abstract class OmemoStore { + private static final Logger LOGGER = Logger.getLogger(OmemoStore.class.getName()); + + private final WeakHashMap>> + 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 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 preKeys = loadOmemoPreKeys(omemoManager); + int newKeysCount = TARGET_PRE_KEY_COUNT - preKeys.size(); + + if (newKeysCount > 0) { + int lastPreKeyId = loadLastPreKeyId(omemoManager); + if (lastPreKeyId == -1) lastPreKeyId = 0; + HashMap newKeys = generateOmemoPreKeys(lastPreKeyId + 1, newKeysCount); + storeOmemoPreKeys(omemoManager, newKeys); + preKeys.putAll(newKeys); + storeLastPreKeyId(omemoManager, lastPreKeyId + newKeysCount); + } + + return new OmemoBundleVAxolotlElement( + 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> + sessions = omemoSessions.get(omemoManager); + if (sessions == null) { + sessions = new HashMap<>(); + omemoSessions.put(omemoManager, sessions); + } + + //Sessions with our own devices + HashMap ourRawSessions = loadAllRawSessionsOf(omemoManager, omemoManager.getOwnJid()); + ourRawSessions.remove(omemoManager.getDeviceId()); //Just to make sure we have no session with ourselves... + sessions.putAll(createOmemoSessionsFromRawSessions(omemoManager, omemoManager.getOwnJid(), ourRawSessions)); + + //Sessions with contacts + for (RosterEntry rosterEntry : Roster.getInstanceFor(omemoManager.getConnection()).getEntries()) { + HashMap contactDevices = loadAllRawSessionsOf(omemoManager, rosterEntry.getJid().asBareJid()); + sessions.putAll(createOmemoSessionsFromRawSessions(omemoManager, rosterEntry.getJid().asBareJid(), contactDevices)); + } + } + + /** + * Forget all omemoSessions of the omemoManager from cache. + * This will not remove the sessions from persistent memory! + * + * @param omemoManager omemoManager we want to forget sessions from. + */ + void forgetOmemoSessions(OmemoManager omemoManager) { + omemoSessions.remove(omemoManager); + } + + /** + * Create a new concrete OmemoSession with a contact. + * + * @param omemoManager omemoManager of our device. + * @param device device to establish the session with + * @param identityKey identityKey of the device + * @return concrete OmemoSession + */ + private OmemoSession + createOmemoSession(OmemoManager omemoManager, OmemoDevice device, T_IdKey identityKey) { + return keyUtil().createOmemoSession(omemoManager, this, device, identityKey); + } + + /** + * Return the OmemoSession for the OmemoDevice. If there is no OmemoSession for the device yet, + * build one from local raw session material. + * + * @param omemoManager omemoManager of our device. + * @param device OmemoDevice + * @return OmemoSession + */ + public OmemoSession + getOmemoSessionOf(OmemoManager omemoManager, OmemoDevice device) { + + HashMap> + sessions = omemoSessions.get(omemoManager); + + if (sessions == null) { + sessions = new HashMap<>(); + omemoSessions.put(omemoManager, sessions); + } + + OmemoSession + session = sessions.get(device); + //No OmemoSession found + if (session == null) { + T_IdKey identityKey = null; + try { + identityKey = loadOmemoIdentityKey(omemoManager, device); + } catch (CorruptedOmemoKeyException e) { + LOGGER.log(Level.WARNING, "getOmemoSessionOf could not load identityKey of " + device + ": " + e.getMessage()); + } + + if (identityKey != null) { + session = createOmemoSession(omemoManager, device, identityKey); + + } else { + LOGGER.log(Level.INFO, "getOmemoSessionOf couldn't find an identityKey for " + device + + ". Initiate session without."); + session = createOmemoSession(omemoManager, device, null); + } + + sessions.put(device, session); + } + + if (session.getIdentityKey() == null) { + try { + session.setIdentityKey(loadOmemoIdentityKey(omemoManager, device)); + } catch (CorruptedOmemoKeyException e) { + LOGGER.log(Level.WARNING, "Can't update IdentityKey of " + device + ": " + e.getMessage()); + } + } + return session; + } + + /** + * Create OmemoSession objects for all T_Sess objects of the contact. + * The T_Sess objects will be wrapped inside a OmemoSession for every device of the contact. + * + * @param omemoManager omemoManager of our device. + * @param contact BareJid of the contact + * @param rawSessions HashMap of Integers (deviceIds) and T_Sess sessions. + * @return HashMap of OmemoContacts and OmemoSessions + */ + private HashMap> + createOmemoSessionsFromRawSessions(OmemoManager omemoManager, BareJid contact, HashMap rawSessions) { + + HashMap> + sessions = new HashMap<>(); + + for (Map.Entry sessionEntry : rawSessions.entrySet()) { + OmemoDevice omemoDevice = new OmemoDevice(contact, sessionEntry.getKey()); + try { + T_IdKey identityKey = loadOmemoIdentityKey(omemoManager, omemoDevice); + if (identityKey != null) { + sessions.put(omemoDevice, createOmemoSession(omemoManager, omemoDevice, identityKey)); + } else { + LOGGER.log(Level.WARNING, "IdentityKey of " + omemoDevice + " is null. Is this even possible at this point?"); + } + } catch (CorruptedOmemoKeyException e1) { + LOGGER.log(Level.WARNING, "buildOmemoSessionFor could not create a session for " + omemoDevice + + ": " + e1.getMessage()); + } + } + return sessions; + } + + // *sigh* + + /** + * Return the id of the last generated preKey. + * This is used to generate new preKeys without preKeyId collisions. + * + * @param omemoManager omemoManager of our device. + * @return id of the last preKey + */ + public abstract int loadLastPreKeyId(OmemoManager omemoManager); + + /** + * Store the id of the last preKey we generated. + * + * @param omemoManager omemoManager of our device. + * @param currentPreKeyId the id of the last generated PreKey + */ + public abstract void storeLastPreKeyId(OmemoManager omemoManager, int currentPreKeyId); + + /** + * Generate a new IdentityKeyPair. We should always have only one pair and usually keep this for a long time. + * + * @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 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 preKeyHashMap) { + for (Map.Entry 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 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 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 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 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); + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement.java new file mode 100644 index 000000000..30bd6e059 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement.java @@ -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(); + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleVAxolotlElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleVAxolotlElement.java new file mode 100644 index 000000000..a7a8b2e6c --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleVAxolotlElement.java @@ -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 preKeysB64; + private HashMap preKeys; + + /** + * Constructor to create a Bundle Element from base64 Strings. + * + * @param signedPreKeyId id + * @param signedPreKeyB64 base64 encoded signedPreKey + * @param signedPreKeySigB64 base64 encoded signedPreKeySignature + * @param identityKeyB64 base64 encoded identityKey + * @param preKeysB64 HashMap of base64 encoded preKeys + */ + public OmemoBundleVAxolotlElement(int signedPreKeyId, String signedPreKeyB64, String signedPreKeySigB64, String identityKeyB64, HashMap preKeysB64) { + this.signedPreKeyId = signedPreKeyId; + this.signedPreKeyB64 = signedPreKeyB64; + this.signedPreKeySignatureB64 = signedPreKeySigB64; + this.identityKeyB64 = identityKeyB64; + this.preKeysB64 = preKeysB64; + } + + /** + * Constructor to create a Bundle Element from decoded byte arrays. + * + * @param signedPreKeyId id + * @param signedPreKey signedPreKey + * @param signedPreKeySig signedPreKeySignature + * @param identityKey identityKey + * @param preKeys HashMap of preKeys + */ + public OmemoBundleVAxolotlElement(int signedPreKeyId, byte[] signedPreKey, byte[] signedPreKeySig, byte[] identityKey, HashMap preKeys) { + this.signedPreKeyId = signedPreKeyId; + + this.signedPreKey = signedPreKey; + this.signedPreKeyB64 = Base64.encodeToString(signedPreKey); + + this.signedPreKeySignature = signedPreKeySig; + this.signedPreKeySignatureB64 = Base64.encodeToString(signedPreKeySignature); + + this.identityKey = identityKey; + this.identityKeyB64 = Base64.encodeToString(identityKey); + + this.preKeys = preKeys; + this.preKeysB64 = new HashMap<>(); + for (int id : preKeys.keySet()) { + preKeysB64.put(id, Base64.encodeToString(preKeys.get(id))); + } + } + + /** + * Return the signedPreKey of the OmemoBundleElement. + * + * @return signedPreKey as byte array + */ + public byte[] getSignedPreKey() { + if (signedPreKey == null) { + signedPreKey = Base64.decode(signedPreKeyB64); + } + return this.signedPreKey.clone(); + } + + /** + * Return the id of the signedPreKey in the bundle. + * + * @return id of signedPreKey + */ + public int getSignedPreKeyId() { + return this.signedPreKeyId; + } + + /** + * Get the signature of the signedPreKey. + * + * @return signature as byte array + */ + public byte[] getSignedPreKeySignature() { + if (signedPreKeySignature == null) { + signedPreKeySignature = Base64.decode(signedPreKeySignatureB64); + } + return signedPreKeySignature.clone(); + } + + /** + * Return the public identityKey of the bundles owner. + * This can be used to check the signedPreKeys signature. + * The fingerprint of this key is, what the user has to verify. + * + * @return public identityKey as byte array + */ + public byte[] getIdentityKey() { + if (identityKey == null) { + identityKey = Base64.decode(identityKeyB64); + } + return this.identityKey.clone(); + } + + /** + * Return the HashMap of preKeys in the bundle. + * The map uses the preKeys ids as key and the preKeys as value. + * + * @return preKeys + */ + public HashMap getPreKeys() { + if (preKeys == null) { + preKeys = new HashMap<>(); + for (int id : preKeysB64.keySet()) { + preKeys.put(id, Base64.decode(preKeysB64.get(id))); + } + } + return this.preKeys; + } + + /** + * Return a single preKey from the map. + * + * @param id id of the preKey + * @return the preKey + */ + public byte[] getPreKey(int id) { + return getPreKeys().get(id); + } + + @Override + public String getElementName() { + return BUNDLE; + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder sb = new XmlStringBuilder(this).rightAngleBracket(); + + sb.halfOpenElement(SIGNED_PRE_KEY_PUB).attribute(SIGNED_PRE_KEY_ID, signedPreKeyId).rightAngleBracket() + .append(signedPreKeyB64).closeElement(SIGNED_PRE_KEY_PUB); + + sb.openElement(SIGNED_PRE_KEY_SIG).append(signedPreKeySignatureB64).closeElement(SIGNED_PRE_KEY_SIG); + + sb.openElement(IDENTITY_KEY).append(identityKeyB64).closeElement(IDENTITY_KEY); + + sb.openElement(PRE_KEYS); + for (Map.Entry p : this.preKeysB64.entrySet()) { + sb.halfOpenElement(PRE_KEY_PUB).attribute(PRE_KEY_ID, p.getKey()).rightAngleBracket() + .append(p.getValue()).closeElement(PRE_KEY_PUB); + } + sb.closeElement(PRE_KEYS); + + sb.closeElement(this); + return sb; + } + + @Override + public String getNamespace() { + return OMEMO_NAMESPACE_V_AXOLOTL; + } + + @Override + public String toString() { + String out = "OmemoBundleElement[\n"; + out += SIGNED_PRE_KEY_PUB + " " + SIGNED_PRE_KEY_ID + "=" + signedPreKeyId + ": " + signedPreKeyB64 + "\n"; + out += SIGNED_PRE_KEY_SIG + ": " + signedPreKeySignatureB64 + "\n"; + out += IDENTITY_KEY + ": " + identityKeyB64 + "\n"; + out += PRE_KEYS + " (" + preKeysB64.size() + ")\n"; + for (Map.Entry e : preKeysB64.entrySet()) { + out += PRE_KEY_PUB + " " + PRE_KEY_ID + "=" + e.getKey() + ": " + e.getValue() + "\n"; + } + return out; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListElement.java new file mode 100644 index 000000000..8fc5a814b --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListElement.java @@ -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 deviceIds; + + public OmemoDeviceListElement(Set deviceIds) { + deviceIds = Objects.requireNonNull(deviceIds); + this.deviceIds = Collections.unmodifiableSet(deviceIds); + } + + public Set getDeviceIds() { + return deviceIds; + } + + public Set 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) + "]"; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListVAxolotlElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListVAxolotlElement.java new file mode 100644 index 000000000..197566c5d --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoDeviceListVAxolotlElement.java @@ -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 deviceIds) { + super(deviceIds); + } + + @Override + public String getNamespace() { + return OMEMO_NAMESPACE_V_AXOLOTL; + } + +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoElement.java new file mode 100644 index 000000000..35a8d5997 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoElement.java @@ -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 keys; + private final byte[] iv; + + public OmemoHeader(int sid, ArrayList keys, byte[] iv) { + this.sid = sid; + this.keys = keys; + this.iv = iv; + } + + /** + * Return the deviceId of the sender of the message. + * + * @return senders id + */ + public int getSid() { + return sid; + } + + public ArrayList getKeys() { + return new ArrayList<>(keys); + } + + public byte[] getIv() { + return iv != null ? iv.clone() : null; + } + + @Override + public String getElementName() { + return 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; + } + } + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoVAxolotlElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoVAxolotlElement.java new file mode 100644 index 000000000..1d700b3c6 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoVAxolotlElement.java @@ -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); + } + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/package-info.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/package-info.java new file mode 100644 index 000000000..63f7989a2 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/package-info.java @@ -0,0 +1,23 @@ +/** + * + * Copyright 2017 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Classes that represent OMEMO related stanzas. + * + * @author Paul Schaub + * @see XEP-0384: OMEMO + */ +package org.jivesoftware.smackx.omemo.element; diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CannotEstablishOmemoSessionException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CannotEstablishOmemoSessionException.java new file mode 100644 index 000000000..6c8116212 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CannotEstablishOmemoSessionException.java @@ -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> failures = new HashMap<>(); + private final HashMap> successes = new HashMap<>(); + + public CannotEstablishOmemoSessionException(OmemoDevice failed, Throwable reason) { + super(); + getFailsOfContact(failed.getJid()).put(failed, reason); + } + + public void addFailures(CannotEstablishOmemoSessionException otherFailures) { + for (Map.Entry> entry : otherFailures.getFailures().entrySet()) { + getFailsOfContact(entry.getKey()).putAll(entry.getValue()); + } + } + + public void addSuccess(OmemoDevice success) { + getSuccessesOfContact(success.getJid()).add(success); + } + + public HashMap> getFailures() { + return failures; + } + + public HashMap> getSuccesses() { + return successes; + } + + private HashMap getFailsOfContact(BareJid contact) { + HashMap h = failures.get(contact); + if (h == null) { + h = new HashMap<>(); + failures.put(contact, h); + } + return h; + } + + private ArrayList getSuccessesOfContact(BareJid contact) { + ArrayList 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> entry : failures.entrySet()) { + ArrayList suc = successes.get(entry.getKey()); + if (suc == null || suc.isEmpty()) { + return true; + } + } + return false; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CorruptedOmemoKeyException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CorruptedOmemoKeyException.java new file mode 100644 index 000000000..24f63be40 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CorruptedOmemoKeyException.java @@ -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); + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CryptoFailedException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CryptoFailedException.java new file mode 100644 index 000000000..7212bf883 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CryptoFailedException.java @@ -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); + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/MultipleCryptoFailedException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/MultipleCryptoFailedException.java new file mode 100644 index 000000000..2205cba0e --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/MultipleCryptoFailedException.java @@ -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 cryptoFailedExceptions; + + private MultipleCryptoFailedException(String message, List 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 cryptoFailedExceptions) { + StringBuilder sb = new StringBuilder("Multiple CryptoFailedExceptions: "); + Iterator it = cryptoFailedExceptions.iterator(); + while (it.hasNext()) { + sb.append(it.next()); + if (it.hasNext()) { + sb.append(", "); + } + } + return new MultipleCryptoFailedException(sb.toString(), cryptoFailedExceptions); + } + + public List getCryptoFailedExceptions() { + return cryptoFailedExceptions; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoOmemoSupportException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoOmemoSupportException.java new file mode 100644 index 000000000..0143bfe5f --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoOmemoSupportException.java @@ -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; +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoRawSessionException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoRawSessionException.java new file mode 100644 index 000000000..7cc211ab1 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/NoRawSessionException.java @@ -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); + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UndecidedOmemoIdentityException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UndecidedOmemoIdentityException.java new file mode 100644 index 000000000..367229453 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/UndecidedOmemoIdentityException.java @@ -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 devices = new HashSet<>(); + + public UndecidedOmemoIdentityException(OmemoDevice contact) { + super(); + this.devices.add(contact); + } + + /** + * Return the HashSet of untrusted devices. + * + * @return untrusted devices + */ + public HashSet 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()); + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/package-info.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/package-info.java new file mode 100644 index 000000000..007ed78ac --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/package-info.java @@ -0,0 +1,23 @@ +/** + * + * Copyright 2017 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Exceptions. + * + * @author Paul Schaub + * @see XEP-0384: OMEMO + */ +package org.jivesoftware.smackx.omemo.exceptions; diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CachedDeviceList.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CachedDeviceList.java new file mode 100644 index 000000000..41662e1ff --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CachedDeviceList.java @@ -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. + *

+ * 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 activeDevices; + private final Set 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 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 getInactiveDevices() { + return inactiveDevices; + } + + /** + * Returns an OmemoDeviceListElement containing all devices (active and inactive). + * + * @return all devices + */ + public Set getAllDevices() { + Set 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 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; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CipherAndAuthTag.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CipherAndAuthTag.java new file mode 100644 index 000000000..6d5e49a3d --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CipherAndAuthTag.java @@ -0,0 +1,84 @@ +/** + * + * Copyright 2017 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.omemo.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; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CiphertextTuple.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CiphertextTuple.java new file mode 100644 index 000000000..c94a5bc88 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CiphertextTuple.java @@ -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; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/ClearTextMessage.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/ClearTextMessage.java new file mode 100644 index 000000000..928e4fb5f --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/ClearTextMessage.java @@ -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; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/IdentityKeyWrapper.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/IdentityKeyWrapper.java new file mode 100644 index 000000000..c89bd33f4 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/IdentityKeyWrapper.java @@ -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; + } + + +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoDevice.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoDevice.java new file mode 100644 index 000000000..81102ce25 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoDevice.java @@ -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(); + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoMessageInformation.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoMessageInformation.java new file mode 100644 index 000000000..ada9cc2b6 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoMessageInformation.java @@ -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; + } +} + + diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoSession.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoSession.java new file mode 100644 index 000000000..023728532 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoSession.java @@ -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 IdentityKeyPair class + * @param IdentityKey class + * @param PreKey class + * @param SignedPreKey class + * @param Session class + * @param Address class + * @param Elliptic Curve PublicKey class + * @param Bundle class + * @param Cipher class + * @author Paul Schaub + */ +public abstract class OmemoSession { + + protected final T_Ciph cipher; + protected final OmemoStore omemoStore; + protected final OmemoDevice remoteDevice; + protected final OmemoManager omemoManager; + protected T_IdKey identityKey; + protected int preKeyId = -1; + + /** + * Constructor used when we establish the session. + * + * @param omemoManager OmemoManager of our device + * @param omemoStore OmemoStore where we want to store the session and get key information from + * @param remoteDevice the OmemoDevice we want to establish the session with + * @param identityKey identityKey of the recipient + */ + public OmemoSession(OmemoManager omemoManager, + OmemoStore omemoStore, + OmemoDevice remoteDevice, T_IdKey identityKey) { + this(omemoManager, omemoStore, remoteDevice); + this.identityKey = identityKey; + } + + /** + * Another constructor used when they establish the session with us. + * + * @param omemoManager OmemoManager of our device + * @param omemoStore OmemoStore we want to store the session and their key in + * @param remoteDevice identityKey of the partner + */ + public OmemoSession(OmemoManager omemoManager, OmemoStore omemoStore, + OmemoDevice remoteDevice) { + this.omemoManager = omemoManager; + this.omemoStore = omemoStore; + this.remoteDevice = remoteDevice; + this.cipher = createCipher(remoteDevice); + } + + /** + * Try to decrypt the transported message key using the double ratchet session. + * + * @param element omemoElement + * @param keyId our keyId + * @return tuple of cipher generated from the unpacked message key and the authtag + * @throws CryptoFailedException if decryption using the double ratchet fails + * @throws NoRawSessionException if we have no session, but the element was NOT a PreKeyMessage + */ + public CipherAndAuthTag decryptTransportedKey(OmemoElement element, int keyId) throws CryptoFailedException, + NoRawSessionException { + byte[] unpackedKey = null; + List decryptExceptions = new ArrayList<>(); + List keys = element.getHeader().getKeys(); + // Find key with our ID. + for (OmemoElement.OmemoHeader.Key k : keys) { + if (k.getId() == keyId) { + try { + unpackedKey = decryptMessageKey(k.getData()); + break; + } catch (CryptoFailedException e) { + // There might be multiple keys with our id, but we can only decrypt one. + // So we can't throw the exception, when decrypting the first duplicate which is not for us. + decryptExceptions.add(e); + } + } + } + + if (unpackedKey == null) { + if (!decryptExceptions.isEmpty()) { + throw MultipleCryptoFailedException.from(decryptExceptions); + } + + throw new CryptoFailedException("Transported key could not be decrypted, since no provided message key. Provides keys: " + keys); + } + + byte[] messageKey = new byte[16]; + byte[] authTag = null; + + if (unpackedKey.length == 32) { + authTag = new byte[16]; + //copy key part into messageKey + System.arraycopy(unpackedKey, 0, messageKey, 0, 16); + //copy tag part into authTag + System.arraycopy(unpackedKey, 16, authTag, 0,16); + } else if (element.isKeyTransportElement() && unpackedKey.length == 16) { + messageKey = unpackedKey; + } else { + throw new CryptoFailedException("MessageKey has wrong length: " + + unpackedKey.length + ". Probably legacy auth tag format."); + } + + return new CipherAndAuthTag(messageKey, element.getHeader().getIv(), authTag); + } + + /** + * Use the symmetric key in cipherAndAuthTag to decrypt the payload of the omemoMessage. + * The decrypted payload will be the body of the returned Message. + * + * @param element omemoElement containing a payload. + * @param cipherAndAuthTag cipher and authentication tag. + * @return Message containing the decrypted payload in its body. + * @throws CryptoFailedException + */ + public static Message decryptMessageElement(OmemoElement element, CipherAndAuthTag cipherAndAuthTag) throws CryptoFailedException { + if (!element.isMessageElement()) { + throw new IllegalArgumentException("decryptMessageElement cannot decrypt OmemoElement which is no MessageElement!"); + } + + if (cipherAndAuthTag.getAuthTag() == null || cipherAndAuthTag.getAuthTag().length != 16) { + throw new CryptoFailedException("AuthenticationTag is null or has wrong length: " + + (cipherAndAuthTag.getAuthTag() == null ? "null" : cipherAndAuthTag.getAuthTag().length)); + } + byte[] encryptedBody = new byte[element.getPayload().length + 16]; + byte[] payload = element.getPayload(); + System.arraycopy(payload, 0, encryptedBody, 0, payload.length); + System.arraycopy(cipherAndAuthTag.getAuthTag(), 0, encryptedBody, payload.length, 16); + + try { + String plaintext = new String(cipherAndAuthTag.getCipher().doFinal(encryptedBody), StringUtils.UTF8); + Message decrypted = new Message(); + decrypted.setBody(plaintext); + return decrypted; + + } catch (UnsupportedEncodingException | IllegalBlockSizeException | BadPaddingException e) { + throw new CryptoFailedException("decryptMessageElement could not decipher message body: " + + e.getMessage()); + } + } + + /** + * Try to decrypt the message. + * First decrypt the message key using our session with the sender. + * Second use the decrypted key to decrypt the message. + * The decrypted content of the 'encrypted'-element becomes the body of the clear text message. + * + * @param element OmemoElement + * @param keyId the key we want to decrypt (usually our own device id) + * @return message as plaintext + * @throws CryptoFailedException + * @throws NoRawSessionException + */ + // TODO find solution for what we actually want to decrypt (String, Message, List...) + public Message decryptMessageElement(OmemoElement element, int keyId) throws CryptoFailedException, NoRawSessionException { + if (!element.isMessageElement()) { + throw new IllegalArgumentException("OmemoElement is not a messageElement!"); + } + + CipherAndAuthTag cipherAndAuthTag = decryptTransportedKey(element, keyId); + return decryptMessageElement(element, cipherAndAuthTag); + } + + /** + * Create a new SessionCipher used to encrypt/decrypt keys. The cipher typically implements the ratchet and KDF-chains. + * + * @param contact OmemoDevice + * @return SessionCipher + */ + public abstract T_Ciph createCipher(OmemoDevice contact); + + /** + * Get the id of the preKey used to establish the session. + * + * @return id + */ + public int getPreKeyId() { + return this.preKeyId; + } + + /** + * Encrypt a message key for the recipient. This key can be deciphered by the recipient with its corresponding + * session cipher. The key is then used to decipher the message. + * + * @param messageKey serialized key to encrypt + * @return A CiphertextTuple containing the ciphertext and the messageType + * @throws CryptoFailedException + */ + public abstract CiphertextTuple encryptMessageKey(byte[] messageKey) throws CryptoFailedException; + + /** + * Decrypt a messageKey using our sessionCipher. We can use that key to decipher the actual message. + * Same as encryptMessageKey, just the other way round. + * + * @param encryptedKey encrypted key + * @return serialized decrypted key or null + * @throws CryptoFailedException when decryption fails. + * @throws NoRawSessionException when no session was found in the double ratchet library + */ + public abstract byte[] decryptMessageKey(byte[] encryptedKey) throws CryptoFailedException, NoRawSessionException; + + /** + * Return the identityKey of the session. + * + * @return identityKey + */ + public T_IdKey getIdentityKey() { + return identityKey; + } + + /** + * Set the identityKey of the remote device. + * @param identityKey identityKey + */ + public void setIdentityKey(T_IdKey identityKey) { + this.identityKey = identityKey; + } + + /** + * Return the fingerprint of the contacts identityKey. + * + * @return fingerprint or null + */ + public OmemoFingerprint getFingerprint() { + return (this.identityKey != null ? omemoStore.keyUtil().getFingerprint(this.identityKey) : null); + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/package-info.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/package-info.java new file mode 100644 index 000000000..485f603ea --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/package-info.java @@ -0,0 +1,23 @@ +/** + * + * Copyright 2017 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Classes that are used internally to arrange objects. + * + * @author Paul Schaub + * @see XEP-0384: OMEMO + */ +package org.jivesoftware.smackx.omemo.internal; diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/OmemoMessageListener.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/OmemoMessageListener.java new file mode 100644 index 000000000..ed319e043 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/OmemoMessageListener.java @@ -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); +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/OmemoMucMessageListener.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/OmemoMucMessageListener.java new file mode 100644 index 000000000..019999cdd --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/OmemoMucMessageListener.java @@ -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); +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/package-info.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/package-info.java new file mode 100644 index 000000000..b9eeb58c7 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/listener/package-info.java @@ -0,0 +1,23 @@ +/** + * + * Copyright 2017 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Callbacks and listeners. + * + * @author Paul Schaub + * @see XEP-XXXX: OMEMO + */ +package org.jivesoftware.smackx.omemo.listener; diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/package-info.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/package-info.java new file mode 100644 index 000000000..737ea40e6 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/package-info.java @@ -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 XEP-0384: OMEMO + */ +package org.jivesoftware.smackx.omemo; diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoBundleVAxolotlProvider.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoBundleVAxolotlProvider.java new file mode 100644 index 000000000..1de76e644 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoBundleVAxolotlProvider.java @@ -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 { + @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 preKeys = new HashMap<>(); + + while (!stop) { + int tag = parser.next(); + String name = parser.getName(); + switch (tag) { + case START_TAG: + // + 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; + } + } + } + // + else if (name.equals(SIGNED_PRE_KEY_SIG)) { + signedPreKeySignature = parser.nextText(); + } + // + else if (name.equals(IDENTITY_KEY)) { + identityKey = parser.nextText(); + } + // + else if (name.equals(PRE_KEYS)) { + inPreKeys = true; + } + // + 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); + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoDeviceListVAxolotlProvider.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoDeviceListVAxolotlProvider.java new file mode 100644 index 000000000..69f8cdf75 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoDeviceListVAxolotlProvider.java @@ -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 { + + @Override + public OmemoDeviceListVAxolotlElement parse(XmlPullParser parser, int initialDepth) throws Exception { + Set 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); + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoVAxolotlProvider.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoVAxolotlProvider.java new file mode 100644 index 000000000..47826e23d --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoVAxolotlProvider.java @@ -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 { + + @Override + public OmemoVAxolotlElement parse(XmlPullParser parser, int initialDepth) throws Exception { + boolean inEncrypted = true; + int sid = -1; + ArrayList 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); + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/package-info.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/package-info.java new file mode 100644 index 000000000..aba66090a --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/package-info.java @@ -0,0 +1,23 @@ +/** + * + * Copyright 2017 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Provider classes that parse OMEMO related stanzas into objects. + * + * @author Paul Schaub + * @see XEP-0384: OMEMO + */ +package org.jivesoftware.smackx.omemo.provider; diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoConstants.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoConstants.java new file mode 100644 index 000000000..442da7a24 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoConstants.java @@ -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 doesn’t 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"; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoKeyUtil.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoKeyUtil.java new file mode 100644 index 000000000..5ca08109c --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoKeyUtil.java @@ -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 IdentityKeyPair class + * @param IdentityKey class + * @param PreKey class + * @param SignedPreKey class + * @param Session class + * @param Address class + * @param Elliptic Curve PublicKey class + * @param Bundle class + * @param Cipher class + * @author Paul Schaub + */ +public abstract class OmemoKeyUtil { + 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 bundles(OmemoBundleVAxolotlElement bundle, OmemoDevice contact) throws CorruptedOmemoKeyException { + HashMap 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 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 preKeyPublisKeysForBundle(HashMap preKeyHashMap) { + HashMap out = new HashMap<>(); + for (Map.Entry 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 + createOmemoSession(OmemoManager omemoManager, OmemoStore omemoStore, + OmemoDevice from); + + /** + * Create a new concrete OmemoSession with a contact. + * + * @param omemoManager omemoManager of our device. + * @param omemoStore omemoStore + * @param device device to establish the session with + * @param identityKey identityKey of the device + * @return concrete OmemoSession + */ + public abstract OmemoSession + createOmemoSession(OmemoManager omemoManager, OmemoStore omemoStore, + OmemoDevice device, T_IdKey identityKey); + + /** + * 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; + } + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilder.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilder.java new file mode 100644 index 000000000..e682b865c --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilder.java @@ -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 IdentityKeyPair class + * @param IdentityKey class + * @param PreKey class + * @param SignedPreKey class + * @param Session class + * @param Address class + * @param Elliptic Curve PublicKey class + * @param Bundle class + * @param Cipher class + * @author Paul Schaub + */ +public class OmemoMessageBuilder { + private final OmemoStore omemoStore; + private final OmemoManager omemoManager; + + private byte[] messageKey = generateKey(); + private byte[] initializationVector = generateIv(); + + private byte[] ciphertextMessage; + private final ArrayList 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 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 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 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; + } +} diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/package-info.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/package-info.java new file mode 100644 index 000000000..af65de1e1 --- /dev/null +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/package-info.java @@ -0,0 +1,23 @@ +/** + * + * Copyright 2017 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Helper classes and other stuff. + * + * @author Paul Schaub + * @see XEP-XXXX: OMEMO + */ +package org.jivesoftware.smackx.omemo.util; diff --git a/smack-omemo/src/main/resources/org.jivesoftware.smackx.omemo/omemo.providers b/smack-omemo/src/main/resources/org.jivesoftware.smackx.omemo/omemo.providers new file mode 100644 index 000000000..463807d65 --- /dev/null +++ b/smack-omemo/src/main/resources/org.jivesoftware.smackx.omemo/omemo.providers @@ -0,0 +1,19 @@ + + + + + encrypted + eu.siacs.conversations.axolotl + org.jivesoftware.smackx.omemo.provider.OmemoVAxolotlProvider + + + list + eu.siacs.conversations.axolotl + org.jivesoftware.smackx.omemo.provider.OmemoDeviceListVAxolotlProvider + + + bundle + eu.siacs.conversations.axolotl + org.jivesoftware.smackx.omemo.provider.OmemoBundleVAxolotlProvider + + diff --git a/smack-omemo/src/main/resources/org.jivesoftware.smackx.omemo/omemo.xml b/smack-omemo/src/main/resources/org.jivesoftware.smackx.omemo/omemo.xml new file mode 100644 index 000000000..eb24fca42 --- /dev/null +++ b/smack-omemo/src/main/resources/org.jivesoftware.smackx.omemo/omemo.xml @@ -0,0 +1,5 @@ + + + org.jivesoftware.smackx.omemo.OmemoManager + + diff --git a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/DeviceListTest.java b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/DeviceListTest.java new file mode 100644 index 000000000..daf1383c7 --- /dev/null +++ b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/DeviceListTest.java @@ -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 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); + } +} diff --git a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoBundleVAxolotlElementTest.java b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoBundleVAxolotlElementTest.java new file mode 100644 index 000000000..ea3b74b57 --- /dev/null +++ b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoBundleVAxolotlElementTest.java @@ -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 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 = + "" + + "" + + signedPreKeyB64 + + "" + + "" + + signedPreKeySigB64 + + "" + + "" + + identityKeyB64 + + "" + + "" + + "" + + preKey1B64 + + "" + + "" + + preKey2B64 + + "" + + "" + + ""; + 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()); + } +} diff --git a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoConfigurationTest.java b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoConfigurationTest.java new file mode 100644 index 000000000..171750519 --- /dev/null +++ b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoConfigurationTest.java @@ -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 + } + } +} diff --git a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoDeviceListVAxolotlElementTest.java b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoDeviceListVAxolotlElementTest.java new file mode 100644 index 000000000..dc32f2e34 --- /dev/null +++ b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoDeviceListVAxolotlElementTest.java @@ -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 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.", + "" + + "" + + "" + + "", + xml); + } +} diff --git a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoDeviceTest.java b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoDeviceTest.java new file mode 100644 index 000000000..91d666434 --- /dev/null +++ b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoDeviceTest.java @@ -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)); + } +} diff --git a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoExceptionsTest.java b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoExceptionsTest.java new file mode 100644 index 000000000..2c3dfd4bc --- /dev/null +++ b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoExceptionsTest.java @@ -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 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 el = new ArrayList<>(); + try { + MultipleCryptoFailedException m2 = MultipleCryptoFailedException.from(el); + fail("MultipleCryptoFailedException must not allow empty list."); + } catch (IllegalArgumentException e) { + // Expected + } + } +} diff --git a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoFingerprintTest.java b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoFingerprintTest.java new file mode 100644 index 000000000..95d16d67b --- /dev/null +++ b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoFingerprintTest.java @@ -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")); + } +} diff --git a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoKeyUtilTest.java b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoKeyUtilTest.java new file mode 100644 index 000000000..3bdb1835b --- /dev/null +++ b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoKeyUtilTest.java @@ -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"); + } +} diff --git a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoVAxolotlElementTest.java b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoVAxolotlElementTest.java new file mode 100644 index 000000000..e5a0f33dd --- /dev/null +++ b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/OmemoVAxolotlElementTest.java @@ -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 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 = + "" + + "

" + + "" + Base64.encodeToString(keyData1) + "" + + "" + Base64.encodeToString(keyData2) + "" + + "" + Base64.encodeToString(iv) + "" + + "
" + + "" + + Base64.encodeToString(payload) + + "" + + ""; + + 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()); + } +} diff --git a/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/WrapperObjectsTest.java b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/WrapperObjectsTest.java new file mode 100644 index 000000000..1688c65fe --- /dev/null +++ b/smack-omemo/src/test/java/org/jivesoftware/smack/omemo/WrapperObjectsTest.java @@ -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()); + } +} diff --git a/smack-repl/build.gradle b/smack-repl/build.gradle index c5b05db56..3f412fb50 100644 --- a/smack-repl/build.gradle +++ b/smack-repl/build.gradle @@ -16,6 +16,8 @@ dependencies { testCompile project(path: ":smack-core", configuration: "archives") } -task printClasspath(dependsOn: assemble) << { - println sourceSets.main.runtimeClasspath.asPath +task printClasspath(dependsOn: assemble) { + doLast { + println sourceSets.main.runtimeClasspath.asPath + } }