Rework support for XEP-0384: OMEMO Encryption

Changes:

    Rework integration tests
    New structure of base integration test classes
    bump dependency on signal-protocol-java from 2.4.0 to 2.6.2
    Introduced CachingOmemoStore implementations
    Use CachingOmemoStore classes in integration tests
    Removed OmemoSession classes (replaced with more logical OmemoRatchet classes)
    Consequently also removed load/storeOmemoSession methods from OmemoStore
    Removed some clutter from KeyUtil classes
    Moved trust decision related code from OmemoStore to TrustCallback
    Require authenticated connection for many functions
    Add async initialization function in OmemoStore
    Refactor omemo test package (/java/org/jivesoftware/smack/omemo -> /java/org/jivesoftware/smackx)
    Remove OmemoStore method isFreshInstallation() as well as defaultDeviceId related stuff
    FileBasedOmemoStore: Add cleaner methods to store/load base data types (Using tryWithResource, only for future releases, once Android API gets bumped)
    Attempt to make OmemoManager thread safe
    new logic for getInstanceFor() deviceId determination
    OmemoManagers encrypt methods now don't throw exceptions when encryption for some devices fails. Instead message gets encrypted when possible and more information about failures gets returned alongside the message itself
    Added OmemoMessage class for that purpose
    Reworked entire OmemoService class
    Use safer logic for creating trust-ignoring messages (like ratchet-update messages)
    Restructure elements/provider in order to prepare for OMEMO namespace bumps
    Remove OmemoManager.regenerate() methods in favor of getInstanceFor(connection, randomDeviceId)
    Removed some unnecessary configuration options
    Prepare for support of more AES message key types
    Simplify session creation
    Where possible, avoid side effects in methods
    Add UntrustedOmemoIdentityException
    Add TrustState enum
    More improved tests
This commit is contained in:
Paul Schaub 2018-06-13 12:29:16 +02:00
parent f290197f6a
commit 1f731f6318
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
96 changed files with 6915 additions and 5488 deletions

View File

@ -548,10 +548,12 @@ task clirrRootReport(type: org.kordamp.gradle.clirr.ClirrReportTask) {
} }
task integrationTest { task integrationTest {
description 'Verify correct functionality of Smack by running some integration tests.'
dependsOn project(':smack-integration-test').tasks.run dependsOn project(':smack-integration-test').tasks.run
} }
task omemoSignalIntTest { task omemoSignalIntTest {
description 'Run integration tests of the smack-omemo module in combination with smack-omemo-signal.'
dependsOn project(':smack-omemo-signal-integration-test').tasks.run dependsOn project(':smack-omemo-signal-integration-test').tasks.run
} }

View File

@ -3,6 +3,9 @@ Encrypting messages with OMEMO
[Back](index.md) [Back](index.md)
About OMEMO
-----------
OMEMO ([XEP-0384](https://xmpp.org/extensions/xep-0384.html)) is an adaption 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 of the Signal protocol for XMPP. It provides an important set of
cryptographic properties including but not restricted to cryptographic properties including but not restricted to
@ -22,18 +25,44 @@ It does NOT provide a server side message archive, so that a new device could
fetch old chat history. fetch old chat history.
Most implementations of OMEMO use the signal-protocol libraries provided by Most implementations of OMEMO use the signal-protocol libraries provided by
OpenWhisperSystems. Unlike Smack, those libraries are licensed under the GPL, OpenWhisperSystems. Unlike Smack, those libraries are licensed under the GPLv3,
which prevents a Apache licensed OMEMO implementation using those libraries (see which prevents a Apache licensed OMEMO implementation using those libraries (see
[licensing situation](https://github.com/igniterealtime/Smack/wiki/OMEMO-libsignal-Licensing-Situation)). [licensing situation](https://github.com/igniterealtime/Smack/wiki/OMEMO-libsignal-Licensing-Situation)).
The module smack-omemo therefore contains no code related to signal-protocol. The module smack-omemo therefore contains no code related to signal-protocol.
However, almost all functionality is encapsulated in that module. If you want However, almost all functionality is encapsulated in that module. If you want
to use OMEMO in a GPL client, you can use the smack-omemo-signal to use OMEMO in a GPLv3 licensed client, you can use the smack-omemo-signal
Smack module, which binds the signal-protocol library to smack-omemo. Smack module, which binds the signal-protocol library to smack-omemo.
It is also possible, to port smack-omemo to other libraries implementing the It is also possible, to port smack-omemo to other libraries implementing the
double ratchet algorithm. double ratchet algorithm.
Requirements Understanding the Double Ratchet Algorithm
------------ ------------------------------------------
In the context of OMEMO encryption, a *recipient* is a not a user, but a users *device* (a user might have
multiple devices of course).
Unlike in PGP, each device capable of OMEMO has its own identity key and publishes its own key bundle.
It is not advised to migrate OMEMO identities from one device to another, as it might damage the ratchet
if not done properly (more on that later). Sharing one identity key between multiple devices is not the purpose of
OMEMO. If a contact has three OMEMO capable devices, you will see three different OMEMO identities and their
fingerprints.
OMEMO utilizes multiple layers of encryption when encrypting a message.
The body of the message is encrypted with a symmetric message key (AES-128-GCM) producing a *payload*.
The message key is encrypted for each recipient using the double ratchet algorithm.
For that purpose, the sending device creates a session with the recipient device (if there was no session already).
Upon receiving a message, the recipient selects the encrypted key addressed to them and decrypts it with their
counterpart of the OMEMO session. The decrypted key gets then used to decrypt the message.
One important consequence of forward secrecy is, that whenever an OMEMO message gets decrypted,
the state of the ratchet changes and the key used to decrypt the message gets deleted.
There is no way to recover this key a second time. The result is, that every message can be decrypted
exactly once.
In order to provide the best user experience, it is therefore advised to implement a client side message archive,
since solutions like MAM cannot be used to fetch old, already once decrypted OMEMO messages.
Server-side Requirements
------------------------
In order to use OMEMO encryption, your server and the servers of your chat 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)) partners must support PEP ([XEP-0163](http://xmpp.org/extensions/xep-0163.html))
@ -42,170 +71,221 @@ Optionally your server should support Message Carbons ([XEP-0280](http://xmpp.or
and Message Archive Management ([XEP-0313](http://xmpp.org/extensions/xep-0313.html)) and Message Archive Management ([XEP-0313](http://xmpp.org/extensions/xep-0313.html))
to achieve message synchronization across all (on- and offline) devices. to achieve message synchronization across all (on- and offline) devices.
Setup Client-side Requirements
----- ------------------------
First you need to setup a OmemoService, for example the libsignal one: If you are want to run smack-omemo related code on the Windows platform, you might have to install the
[Java Cryptography Extension](http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html).
This is needed to generate cryptographically strong keys.
Storing Keys
------------
smack-omemo needs to create, store and delete some information like keys and session states during operation.
For that purpose the `OmemoStore` class is used. There are multiple implementations with different properties.
* The `(Signal)FileBasedOmemoStore` stores all information in individual files organized in a directory tree.
While this is the most basic and easy to use implementation, it is not the best solution in terms of performance.
* The `(Signal)CachingOmemoStore` is a multi-purpose store implementation. It can be used to wrap another
`(Signal)OmemoStore` implementation to provide a caching layer (for example in order to reduce access to a database backend or
a the file system of the `FileBasedOmemoStore`. It is therefore advised to wrap persistent `(Signal)OmemoStore`
implementations with a `(Signal)CachingOmemoStore`.
On the other hand it can also be used standalone as an ephemeral `OmemoStore`, which "forgets" all stored information
once the program terminates. This comes in handy for testing purposes.
If you are unhappy with the `(Signal)FileBasedOmemoStore`, you can implement your own store (for example with a
SQL database) by extending the `(Signal)OmemoStore` class.
It most certainly makes sense to store the data of the used `OmemoStore` in a secure way (for example an
encrypted database).
Handling Trust Decisions
------------------------
In order for a cryptographic system to make sense, decisions must be made whether to *trust* an identity or not.
For technical reasons those decisions cannot be stored within the `OmemoStore`. Instead a client must implement
the `OmemoTrustCallback`. This interface provides methods to mark `OmemoFingerprints` as trusted or untrusted and to
query trust decisions.
In order to provide security, a client should communicate to the user, that it is important for them to compare
fingerprints through an external channel (reading it out on the phone, scanning QR codes...) before starting to chat.
While not implemented in smack-omemo, it is certainly for the client to implement different trust models like
[Blind Trust Before Verification](https://gultsch.de/trust.html).
Basic Setup
-----------
Before you can start to send and receive messages, some preconditions have to be met. These steps should be executed
in the order as presented below. In this example we will use components from the *smack-omemo-signal* module.
1. Register an OmemoService
The `OmemoService` class is responsible for handling incoming messages and manages access to the Double Ratchet.
```
SignalOmemoService.setup();
```
The `setup()` method registers the service as a singleton. You can later access the instance
by calling `SignalOmemoService.getInstace()`. The service can only be registered once.
Subsequent calls will throw an `IllegalStateException`.
2. Set an OmemoStore
Now you have to decide, what `OmemoStore` implementation you want to use to store and access
keys and session states. In this example we'll use the `SignalFileBasedOmemoStore` wrapped in a
`SignalCachingOmemoStore` for better performance.
```
SignalOmemoService service = SignalOmemoService.getInstace();
service.setOmemoStoreBackend(new SignalCachingOmemoStore(new SignalFileBasedOmemoStore(new File("/path/to/store"))));
```
Just like the `OmemoService` instance, the `OmemoStore` instance can only be set once.
3. Get an instance of the OmemoManager for your connection
For the greater part of OMEMO related actions, you'll use the `OmemoManager`. The `OmemoManager` represents
your OMEMO device. While it is possible to have multiple `OmemoManager`s per `XMPPConnection`, you really
only need one.
```
OmemoManager manager = OmemoManager.getInstanceFor(connection);
```
If for whatever reason you decide to use multiple `OmemoManager`s at once,
it is highly advised to get them like this:
```
OmemoManager first = OmemoManager.getInstanceFor(connection, firstId);
OmemoManager second = OmemoManager.getInstanceFor(connection, secondId);
```
4. Set an OmemoTrustCallback
As stated above, the `OmemoTrustCallback` is used to query trust decisions. Set the callback like this:
```
manager.setTrustCallback(trustCallback);
```
If you use multiple `OmemoManager`s each `OmemoManager` MUST have its own callback.
5. Set listeners for OMEMO messages.
To get notified of incoming OMEMO encrypted messages, you need to register corresponding listeners.
There are two types of listeners.
* `OmemoMessageListener` is used to listen for incoming encrypted OMEMO single chat messages and
KeyTransportMessages.
* `OmemoMucMessageListener` is used to listen for encrypted OMEMO messages sent in a MultiUserChat.
Note that an incoming message might not have a body. That might be the case for
[KeyTransportMessages](https://xmpp.org/extensions/xep-0384.html#usecases-keysend)
or messages sent to update the ratchet. You can check, whether a received message is such a message by calling
`OmemoMessage.Received.isKeyTransportMessage()`, which will return true if the message has no body.
The received message will include the senders device and fingerprint, which you can use in
`OmemoManager.isTrustedOmemoIdentity(device, fingerprint)` to determine, if the message was sent by a trusted device.
6. Initialize the manager(s)
Ideally all above steps should be executed *before* `connection.login()` gets called. That way you won't miss
any offline messages. If the connection is not yet logged in, now is the time to do so.
```
connection.login();
manager.initialize();
```
Since a lot of keys are generated in this step, this might take a little longer on older devices.
You might want to use the asynchronous call `OmemoManager.initializeAsync(initializationFinishedCallback)`
instead to prevent the thread from blocking.
Send Messages
-------------
Encrypting a message for a contact really means to encrypt the message for all trusted devices of the contact, as well
as all trusted devices of the user itself (except the sending device). The encryption process will fail if there are
devices for which the user has not yet made a trust decision.
### Make Trust Decisions
To get a list of all devices of a contact, you can do the following:
``` ```
SignalOmemoService.setup(); List<OmemoDevice> devices = manager.getDevicesOf(contactsBareJid);
``` ```
As a first step you have to prepare the OmemoStore. To get the OmemoFingerprint of a device, you can call
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) OmemoFingerprint fingerprint = manager.getFingerprint(device);
OmemoConfiguration.setFileBasedOmemoStoreDefaultPath(new File("path/to/your/store"));
``` ```
For each device you need an OmemoManager. This fingerprint can now be displayed to the user who can decide whether to trust the device, or not.
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); // Trust
manager.trustOmemoIdentity(device, fingerprint);
// Distrust
manager.distrustOmemoIdentity(device, fingerprint);
``` ```
As soon as the connection is authenticated, the module generates some keys and ### Encrypt a Message
announces OMEMO support.
To get updated with new OMEMO messages, you should register message listeners.
Currently only Message bodies can be encrypted.
``` ```
omemoManager.addOmemoMessageListener(new OmemoMessageListener() { String secret = "Mallory is a twerp!";
@Overwrite OmemoMessage.Sent encrypted = manager.encrypt(contactsBareJid, secret);
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 The encrypted message will contain some information about the message. It might for example happen, that the encryption
----- failed for some recipient devices. For that reason the encrypted message will contain a map of skipped devices and
the reasons.
Before you can encrypt a message for a device, you have to trust its identity. ### Encrypt a Message for a MultiUserChat
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. A MultiUserChat must fulfill some criteria in order to be OMEMO capable.
The MUC must be non-anonymous. Furthermore all members of the MUC must have subscribed to one another.
You can check for the non-anonymity like follows:
``` ```
omemoManager.trustOmemoIdentity(trustedDevice, trustedFingerprint); manager.multiUserChatSupportsOmemo(muc);
omemoManager.distrustOmemoIdentity(untrustedDevice, untrustedFingerprint);
``` ```
The trust decision should be made by the user based on comparing fingerprints. Encryption is then done analog to single message encryption:
You can get fingerprints of your own and contacts devices:
``` ```
OmemoFingerprint myFingerprint = omemoManager.getFingerprint(); OmemoMessage.Sent encrypted = manager.encrypt(multiUserChat, secret);
OmemoFingerprint otherFingerprint = omemoStore.getFingerprint(omemoManager, otherDevice);
``` ```
To encrypt a message for a single contact or a MUC, you do as follows: ### Sending an encrypted Message
To send the message, it has to be wrapped in a `Message` object. That can conveniently be done like follows.
``` ```
Message encryptedSingleMessage = omemoManager.encrypt(bobsBareJid, "Hi Bob!"); Message message = encrypted.asMessage(contactsJid);
connection.sendStanza(message):
Message encryptedMucMessage = omemoManager.encrypt(multiUserChat, "Hi everybody!");
``` ```
Note: It may happen, that smack-omemo is unable to create a session with a device. This will add a [Message Processing Hint](https://xmpp.org/extensions/xep-0334.html) for MAM,
In case we could not create a single valid session for a recipient, a an [Explicit Message Encryption](https://xmpp.org/extensions/xep-0380.html) hint for OMEMO,
CannotCreateOmemoSessionException will be thrown. This exception contains information as well as an optional cleartext hint about OMEMO to the message.
about which sessions could (not) be created and why. If you want to ignore those devices,
you can encrypt the message for all remaining devices like this:
```
Message encryptedMessage = omemoManager.encryptForExistingSession(cannotEstablishSessionException, "Hi there!");
```
The resulting message can then be sent via the ChatManager/MultiUserChatManager.
You may want to generate a new identity sometime in the future. That's pretty straight
forward. No need to manually publish bundles etc.
```
omemoManager.regenerate();
```
In case your device list gets filled with old unused identities, you can clean it up.
This will remove all active devices from the device list and only publish the device
you are using right now.
```
omemoManager.purgeDevices();
```
If you want to find out, whether a server, MUC or contacts resource supports OMEMO,
you can use the following methods:
```
boolean serverCan = omemoManager.serverSupportsOmemo(serverJid);
boolean mucCan = omemoManager.multiUserChatSupportsOmemo(mucJid);
boolean resourceCan = omemoManager.resourceSupportsOmemo(contactsResourceJid);
```
It might happen, that the server you or your contact are using is not delivering devicelist updates correctly.
In such a case smack-omemo cannot fetch bundles or send messages to devices it hasn\'t seen before. To mitigate this, it
might help to explicitly request the latest device list from the server.
```
omemoManager.requestDeviceListUpdateFor(contactJid);
```
If you want to decrypt a MamQueryResult, you can do so using the following method:
````
List<ClearTextMessage> decryptedMamQuery = omemoManager.decryptMamQueryResult(mamQueryResult);
````
Note, that you cannot decrypt an OMEMO encrypted message twice for reasons of forward secrecy.
A ClearTextMessage contains the decrypted body of the message, as well as additional information like if/how the message was encrypted in the first place.
Unfortunately due to the fact that you cannot decrypt messages twice, you have to keep track of the message history locally on the device and ideally also keep track of the last received message, so you can query the server only for messages newer than that.
Configuration Configuration
------------- -------------
smack-omemo has some configuration options that can be changed on runtime via the `OmemoConfiguration` class: 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) * 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) * 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. * 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. * 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. * setRepairBrokenSessionsWithPreKeyMessages when set to true, whenever a message arrives, which cannot be decrypted, smack-omemo will respond with a preKeyMessage which discards the old session and builds a fresh one.
* 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. * setCompleteSessionWithEmptyMessage when set to true, whenever a preKeyMessage arrives, smack-omemo will respond with an empty message to complete the session.
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 Integration Tests
----------------- -----------------

View File

@ -0,0 +1,43 @@
Migrating smack-omemo from 4.2.1 to 4.x.x
=========================================
The implementation of smack-omemo and smack-omemo-signal was originally started as an
academic project under pressure of time.
For that reason, the API was not perfect when OMEMO support was first introduced in
Smack in version 4.2.1.
Many issues of smack-omemo have been resolved over the course of the last year in
a major effort, which is why smack-omemo and smack-omemo-signalwere excluded from
the 4.2.2 release.
During this time major parts of the implementation were redone and the API changed
as a consequence of that. This guide will go through all notable changes in order
to make the process of upgrading as easy and straight forward as possible.
## Trust
One major change is, that the OmemoStore implementations no longer store trust decisions.
Methods related to trust have been removed from OmemoStore implementations.
Instead the client is now responsible to store those.
Upon startup, the client now must pass an `OmemoTrustCallback` to the `OmemoManager`
which is used to access and change trust decisions.
It is recommended for the client to store trust decisions as tuples of (omemo device,
fingerprint of identityKey, trust state).
When querying a trust decision (aka. "Is this fingerprint trusted for that device?),
the local fingerprint should be compared to the provided fingerprint.
The method signatures for setting and querying trust from inside the OmemoManager are
still the same. Internally they access the `OmemoTrustCallback` set by the client.
## Encryption
Message encryption in smack-omemo 4.2.1 was ugly. Encryption for multiple devices
could fail because session negotiation could go wrong, which resulted in an
exception, which contained all devices with working sessions.
That exception could then be used in
`OmemoManager.encryptForExistingSessions(CannotEstablishOmemoSessionException exception, String message)`,
to encrypt the message for all devices with a session.
The new API is

View File

@ -13,6 +13,7 @@ dependencies {
compile project(':smack-experimental') compile project(':smack-experimental')
compile project(':smack-omemo') compile project(':smack-omemo')
compile project(':smack-debug') compile project(':smack-debug')
compile project(path: ":smack-omemo", configuration: "testRuntime")
compile 'org.reflections:reflections:0.9.9-RC1' compile 'org.reflections:reflections:0.9.9-RC1'
compile 'eu.geekplace.javapinning:java-pinning-java7:1.1.0-alpha1' compile 'eu.geekplace.javapinning:java-pinning-java7:1.1.0-alpha1'
// Note that the junit-vintage-engine runtime dependency is not // Note that the junit-vintage-engine runtime dependency is not

View File

@ -16,40 +16,21 @@
*/ */
package org.jivesoftware.smackx.omemo; package org.jivesoftware.smackx.omemo;
import java.io.File;
import java.util.logging.Level;
import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.XMPPException;
import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.TestNotPossibleException;
import org.junit.AfterClass;
import org.junit.BeforeClass;
/** /**
* Super class for OMEMO integration tests. * Super class for OMEMO integration tests.
*/ */
public abstract class AbstractOmemoIntegrationTest extends AbstractSmackIntegrationTest { public abstract class AbstractOmemoIntegrationTest extends AbstractSmackIntegrationTest {
private static final File storePath;
static {
String userHome = System.getProperty("user.home");
if (userHome != null) {
File f = new File(userHome);
storePath = new File(f, ".config/smack-integration-test/store");
} else {
storePath = new File("int_test_omemo_store");
}
}
public AbstractOmemoIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { public AbstractOmemoIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException {
super(environment); super(environment);
if (OmemoConfiguration.getFileBasedOmemoStoreDefaultPath() == null) {
OmemoConfiguration.setFileBasedOmemoStoreDefaultPath(storePath);
}
// Test for server support // Test for server support
if (!OmemoManager.serverSupportsOmemo(connection, connection.getXMPPServiceDomain())) { if (!OmemoManager.serverSupportsOmemo(connection, connection.getXMPPServiceDomain())) {
throw new TestNotPossibleException("Server does not support OMEMO (PubSub)"); throw new TestNotPossibleException("Server does not support OMEMO (PubSub)");
@ -59,23 +40,8 @@ public abstract class AbstractOmemoIntegrationTest extends AbstractSmackIntegrat
if (!OmemoService.isServiceRegistered()) { if (!OmemoService.isServiceRegistered()) {
throw new TestNotPossibleException("No OmemoService registered."); throw new TestNotPossibleException("No OmemoService registered.");
} }
OmemoConfiguration.setCompleteSessionWithEmptyMessage(true);
OmemoConfiguration.setRepairBrokenSessionsWithPrekeyMessages(true);
} }
@BeforeClass
public void beforeTest() {
LOGGER.log(Level.INFO, "START EXECUTION");
OmemoIntegrationTestHelper.deletePath(storePath);
before();
}
@AfterClass
public void afterTest() {
after();
OmemoIntegrationTestHelper.deletePath(storePath);
LOGGER.log(Level.INFO, "END EXECUTION");
}
public abstract void before();
public abstract void after();
} }

View File

@ -0,0 +1,149 @@
/**
*
* Copyright 2018 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smackx.carbons.packet.CarbonExtension;
import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener;
import org.igniterealtime.smack.inttest.util.ResultSyncPoint;
import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint;
/**
* Convenience class. This listener is used so that implementers of OmemoMessageListener don't have to implement
* both messages. Instead they can just overwrite the message they want to implement.
*/
public class AbstractOmemoMessageListener implements OmemoMessageListener {
@Override
public void onOmemoMessageReceived(Stanza stanza, OmemoMessage.Received decryptedMessage) {
// Override me
}
@Override
public void onOmemoCarbonCopyReceived(CarbonExtension.Direction direction, Message carbonCopy, Message wrappingMessage, OmemoMessage.Received decryptedCarbonCopy) {
// Override me
}
private static class SyncPointListener extends AbstractOmemoMessageListener {
protected final ResultSyncPoint<?,?> syncPoint;
SyncPointListener(ResultSyncPoint<?,?> syncPoint) {
this.syncPoint = syncPoint;
}
public ResultSyncPoint<?, ?> getSyncPoint() {
return syncPoint;
}
}
static class MessageListener extends SyncPointListener {
protected final String expectedMessage;
MessageListener(String expectedMessage, SimpleResultSyncPoint syncPoint) {
super(syncPoint);
this.expectedMessage = expectedMessage;
}
MessageListener(String expectedMessage) {
this(expectedMessage, new SimpleResultSyncPoint());
}
@Override
public void onOmemoMessageReceived(Stanza stanza, OmemoMessage.Received received) {
SimpleResultSyncPoint srp = (SimpleResultSyncPoint) syncPoint;
if (received.isKeyTransportMessage()) {
return;
}
if (received.getBody().equals(expectedMessage)) {
srp.signal();
} else {
srp.signalFailure("Received decrypted message was not equal to sent message.");
}
}
}
static class PreKeyMessageListener extends MessageListener {
PreKeyMessageListener(String expectedMessage, SimpleResultSyncPoint syncPoint) {
super(expectedMessage, syncPoint);
}
PreKeyMessageListener(String expectedMessage) {
this(expectedMessage, new SimpleResultSyncPoint());
}
@Override
public void onOmemoMessageReceived(Stanza stanza, OmemoMessage.Received received) {
SimpleResultSyncPoint srp = (SimpleResultSyncPoint) syncPoint;
if (received.isKeyTransportMessage()) {
return;
}
if (received.isPreKeyMessage()) {
if (received.getBody().equals(expectedMessage)) {
srp.signal();
} else {
srp.signalFailure("Received decrypted message was not equal to sent message.");
}
} else {
srp.signalFailure("Received message was not a PreKeyMessage.");
}
}
}
static class KeyTransportListener extends SyncPointListener {
KeyTransportListener(SimpleResultSyncPoint resultSyncPoint) {
super(resultSyncPoint);
}
KeyTransportListener() {
this(new SimpleResultSyncPoint());
}
@Override
public void onOmemoMessageReceived(Stanza stanza, OmemoMessage.Received received) {
SimpleResultSyncPoint s = (SimpleResultSyncPoint) syncPoint;
if (received.isKeyTransportMessage()) {
s.signal();
}
}
}
static class PreKeyKeyTransportListener extends KeyTransportListener {
PreKeyKeyTransportListener(SimpleResultSyncPoint resultSyncPoint) {
super(resultSyncPoint);
}
PreKeyKeyTransportListener() {
this(new SimpleResultSyncPoint());
}
@Override
public void onOmemoMessageReceived(Stanza stanza, OmemoMessage.Received received) {
SimpleResultSyncPoint s = (SimpleResultSyncPoint) syncPoint;
if (received.isPreKeyMessage()) {
if (received.isKeyTransportMessage()) {
s.signal();
}
}
}
}
}

View File

@ -0,0 +1,75 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import java.util.logging.Level;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.TestNotPossibleException;
import org.junit.AfterClass;
import org.junit.BeforeClass;
/**
* Abstract OMEMO integration test framing, which creates and initializes two OmemoManagers (for conOne and conTwo).
* Both users subscribe to one another and trust their identities.
* After the test traces in PubSub and in the users Rosters are removed.
*/
public abstract class AbstractTwoUsersOmemoIntegrationTest extends AbstractOmemoIntegrationTest {
protected OmemoManager alice, bob;
public AbstractTwoUsersOmemoIntegrationTest(SmackIntegrationTestEnvironment environment)
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException, TestNotPossibleException {
super(environment);
}
@BeforeClass
public void setup() throws Exception {
alice = OmemoManagerSetupHelper.prepareOmemoManager(conOne);
bob = OmemoManagerSetupHelper.prepareOmemoManager(conTwo);
LOGGER.log(Level.FINE, "Alice: " + alice.getOwnDevice() + " Bob: " + bob.getOwnDevice());
assertFalse(alice.getDeviceId().equals(bob.getDeviceId()));
// Subscribe presences
OmemoManagerSetupHelper.syncSubscribePresence(alice.getConnection(), bob.getConnection(), "bob", null);
OmemoManagerSetupHelper.syncSubscribePresence(bob.getConnection(), alice.getConnection(), "alice", null);
OmemoManagerSetupHelper.trustAllIdentitiesWithTests(alice, bob); // Alice trusts Bob's devices
OmemoManagerSetupHelper.trustAllIdentitiesWithTests(bob, alice); // Bob trusts Alice' and Mallory's devices
assertEquals(bob.getOwnFingerprint(), alice.getActiveFingerprints(bob.getOwnJid()).get(bob.getOwnDevice()));
assertEquals(alice.getOwnFingerprint(), bob.getActiveFingerprints(alice.getOwnJid()).get(alice.getOwnDevice()));
}
@AfterClass
public void cleanUp() {
alice.stopStanzaAndPEPListeners();
bob.stopStanzaAndPEPListeners();
OmemoManagerSetupHelper.cleanUpPubSub(alice);
OmemoManagerSetupHelper.cleanUpRoster(alice);
OmemoManagerSetupHelper.cleanUpPubSub(bob);
OmemoManagerSetupHelper.cleanUpRoster(bob);
}
}

View File

@ -0,0 +1,102 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smackx.omemo.element.OmemoBundleElement;
import org.igniterealtime.smack.inttest.SmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.TestNotPossibleException;
/**
* Simple OMEMO message encryption integration test.
* During this test Alice sends an encrypted message to Bob. Bob decrypts it and sends a response to Alice.
* It is checked whether the messages can be decrypted, and if used up pre-keys result in renewed bundles.
*/
public class MessageEncryptionIntegrationTest extends AbstractTwoUsersOmemoIntegrationTest {
public MessageEncryptionIntegrationTest(SmackIntegrationTestEnvironment environment)
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException, TestNotPossibleException {
super(environment);
}
/**
* This test checks whether the following actions are performed.
*
* Alice publishes bundle A1
* Bob publishes bundle B1
*
* Alice sends message to Bob (preKeyMessage)
* Bob publishes bundle B2
* Alice still has A1
*
* Bob responds to Alice (normal message)
* Alice still has A1
* Bob still has B2
* @throws Exception
*/
@SmackIntegrationTest
public void messageTest() throws Exception {
OmemoBundleElement a1 = alice.getOmemoService().getOmemoStoreBackend().packOmemoBundle(alice.getOwnDevice());
OmemoBundleElement b1 = bob.getOmemoService().getOmemoStoreBackend().packOmemoBundle(bob.getOwnDevice());
// Alice sends message(s) to bob
// PreKeyMessage A -> B
final String body1 = "One is greater than zero (for small values of zero).";
AbstractOmemoMessageListener.PreKeyMessageListener listener1 =
new AbstractOmemoMessageListener.PreKeyMessageListener(body1);
bob.addOmemoMessageListener(listener1);
OmemoMessage.Sent e1 = alice.encrypt(bob.getOwnJid(), body1);
alice.getConnection().sendStanza(e1.asMessage(bob.getOwnJid()));
listener1.getSyncPoint().waitForResult(10 * 1000);
bob.removeOmemoMessageListener(listener1);
OmemoBundleElement a1_ = alice.getOmemoService().getOmemoStoreBackend().packOmemoBundle(alice.getOwnDevice());
OmemoBundleElement b2;
synchronized (bob.LOCK) { // Circumvent race condition where bundle gets replenished after getting stored in b2
b2 = bob.getOmemoService().getOmemoStoreBackend().packOmemoBundle(bob.getOwnDevice());
}
assertEquals("Alice sent bob a preKeyMessage, so her bundle MUST still be the same.", a1, a1_);
assertNotEquals("Bob just received a preKeyMessage from alice, so his bundle must have changed.", b1, b2);
// Message B -> A
final String body3 = "The german words for 'leek' and 'wimp' are the same.";
AbstractOmemoMessageListener.MessageListener listener3 =
new AbstractOmemoMessageListener.MessageListener(body3);
alice.addOmemoMessageListener(listener3);
OmemoMessage.Sent e3 = bob.encrypt(alice.getOwnJid(), body3);
bob.getConnection().sendStanza(e3.asMessage(alice.getOwnJid()));
listener3.getSyncPoint().waitForResult(10 * 1000);
alice.removeOmemoMessageListener(listener3);
OmemoBundleElement a1__ = alice.getOmemoService().getOmemoStoreBackend().packOmemoBundle(alice.getOwnDevice());
OmemoBundleElement b2_ = bob.getOmemoService().getOmemoStoreBackend().packOmemoBundle(bob.getOwnDevice());
assertEquals("Since alice initiated the session with bob, at no time he sent a preKeyMessage, " +
"so her bundle MUST still be the same.", a1_, a1__);
assertEquals("Bob changed his bundle earlier, but at this point his bundle must be equal to " +
"after the first change.", b2, b2_);
}
}

View File

@ -1,81 +0,0 @@
/**
*
* Copyright 2017 Florian Schmaus, Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import static junit.framework.TestCase.assertNotNull;
import static junit.framework.TestCase.assertTrue;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.cleanServerSideTraces;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.setUpOmemoManager;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.util.OmemoConstants;
import org.jivesoftware.smackx.pubsub.PubSubException;
import org.jivesoftware.smackx.pubsub.PubSubException.NotAPubSubNodeException;
import org.igniterealtime.smack.inttest.SmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.TestNotPossibleException;
public class OmemoInitializationTest extends AbstractOmemoIntegrationTest {
private OmemoManager alice;
private OmemoStore<?,?,?,?,?,?,?,?,?> store;
@Override
public void before() {
alice = OmemoManager.getInstanceFor(conOne, 666);
store = OmemoService.getInstance().getOmemoStoreBackend();
}
public OmemoInitializationTest(SmackIntegrationTestEnvironment environment) throws TestNotPossibleException, XMPPErrorException, NotConnectedException, NoResponseException, InterruptedException {
super(environment);
}
/**
* Tests, if the initialization is done properly.
* @throws NotAPubSubNodeException
*/
@SmackIntegrationTest
public void initializationTest() throws XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, SmackException.NotLoggedInException, CorruptedOmemoKeyException, NotAPubSubNodeException {
// test keys.
setUpOmemoManager(alice);
assertNotNull("IdentityKey must not be null after initialization.", store.loadOmemoIdentityKeyPair(alice));
assertTrue("We must have " + OmemoConstants.TARGET_PRE_KEY_COUNT + " preKeys.",
store.loadOmemoPreKeys(alice).size() == OmemoConstants.TARGET_PRE_KEY_COUNT);
assertNotNull("Our signedPreKey must not be null.", store.loadCurrentSignedPreKeyId(alice));
// Is deviceId published?
assertTrue("Published deviceList must contain our deviceId.",
OmemoService.fetchDeviceList(alice, alice.getOwnJid())
.getDeviceIds().contains(alice.getDeviceId()));
assertTrue("Our fingerprint must be of correct length.",
OmemoService.getInstance().getOmemoStoreBackend().getFingerprint(alice).length() == 64);
}
@Override
public void after() {
alice.shutdown();
cleanServerSideTraces(alice);
}
}

View File

@ -1,156 +0,0 @@
/**
*
* Copyright 2017 Florian Schmaus, Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertNotNull;
import static junit.framework.TestCase.assertTrue;
import java.io.File;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterEntry;
import org.jivesoftware.smackx.omemo.element.OmemoBundleElement;
import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.internal.CachedDeviceList;
import org.jivesoftware.smackx.omemo.util.OmemoConstants;
import org.jivesoftware.smackx.pubsub.PubSubException;
import org.jivesoftware.smackx.pubsub.PubSubException.NotAPubSubNodeException;
import org.jivesoftware.smackx.pubsub.PubSubManager;
/**
* Class containing some helper methods for OmemoIntegrationTests.
*/
final class OmemoIntegrationTestHelper {
private static final Logger LOGGER = Logger.getLogger(OmemoIntegrationTestHelper.class.getSimpleName());
static void cleanServerSideTraces(OmemoManager omemoManager) {
cleanUpPubSub(omemoManager);
cleanUpRoster(omemoManager);
}
static void deletePath(File storePath) {
FileBasedOmemoStore.deleteDirectory(storePath);
}
static void deletePath(OmemoManager omemoManager) {
OmemoService.getInstance().getOmemoStoreBackend().purgeOwnDeviceKeys(omemoManager);
}
static void cleanUpPubSub(OmemoManager omemoManager) {
PubSubManager pm = PubSubManager.getInstance(omemoManager.getConnection(),omemoManager.getOwnJid());
try {
omemoManager.requestDeviceListUpdateFor(omemoManager.getOwnJid());
} catch (SmackException.NotConnectedException | InterruptedException | SmackException.NoResponseException e) {
// ignore
}
CachedDeviceList deviceList = OmemoService.getInstance().getOmemoStoreBackend().loadCachedDeviceList(omemoManager, omemoManager.getOwnJid());
for (int id : deviceList.getAllDevices()) {
try {
pm.getLeafNode(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(id)).deleteAllItems();
} catch (InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException | PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException | NotAPubSubNodeException e) {
// Silent
}
try {
pm.deleteNode(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(id));
} catch (SmackException.NoResponseException | InterruptedException | SmackException.NotConnectedException | XMPPException.XMPPErrorException e) {
// Silent
}
}
try {
pm.getLeafNode(OmemoConstants.PEP_NODE_DEVICE_LIST).deleteAllItems();
} catch (InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException | PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException | NotAPubSubNodeException e) {
// Silent
}
try {
pm.deleteNode(OmemoConstants.PEP_NODE_DEVICE_LIST);
} catch (SmackException.NoResponseException | InterruptedException | SmackException.NotConnectedException | XMPPException.XMPPErrorException e) {
// Silent
}
}
static void cleanUpRoster(OmemoManager omemoManager) {
Roster roster = Roster.getInstanceFor(omemoManager.getConnection());
for (RosterEntry r : roster.getEntries()) {
try {
roster.removeEntry(r);
} catch (InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException | XMPPException.XMPPErrorException | SmackException.NotLoggedInException e) {
// Silent
}
}
}
/**
* Let Alice subscribe to Bob.
* @param alice
* @param bob
* @throws SmackException.NotLoggedInException
* @throws XMPPException.XMPPErrorException
* @throws SmackException.NotConnectedException
* @throws InterruptedException
* @throws SmackException.NoResponseException
*/
static void subscribe(OmemoManager alice, OmemoManager bob, String nick)
throws SmackException.NotLoggedInException, XMPPException.XMPPErrorException,
SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException {
Roster aliceRoster = Roster.getInstanceFor(alice.getConnection());
Roster bobsRoster = Roster.getInstanceFor(bob.getConnection());
bobsRoster.setSubscriptionMode(Roster.SubscriptionMode.accept_all);
aliceRoster.createEntry(bob.getOwnJid(), nick, null);
}
static void unidirectionalTrust(OmemoManager alice, OmemoManager bob) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, CannotEstablishOmemoSessionException {
// Fetch deviceList
alice.requestDeviceListUpdateFor(bob.getOwnJid());
LOGGER.log(Level.INFO, "Current deviceList state: " + alice.getOwnDevice() + " knows " + bob.getOwnDevice() + ": "
+ OmemoService.getInstance().getOmemoStoreBackend().loadCachedDeviceList(alice, bob.getOwnJid()));
assertTrue("Trusting party must know the others device at this point.",
alice.getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(alice, bob.getOwnJid())
.getActiveDevices().contains(bob.getDeviceId()));
// Create sessions
alice.buildSessionsWith(bob.getOwnJid());
assertTrue("Trusting party must have a session with the other end at this point.",
!alice.getOmemoService().getOmemoStoreBackend().loadAllRawSessionsOf(alice, bob.getOwnJid()).isEmpty());
// Trust the other party
alice.getOmemoService().getOmemoStoreBackend().trustOmemoIdentity(alice, bob.getOwnDevice(),
alice.getOmemoService().getOmemoStoreBackend().getFingerprint(alice, bob.getOwnDevice()));
}
static void setUpOmemoManager(OmemoManager omemoManager) throws CorruptedOmemoKeyException, InterruptedException, SmackException.NoResponseException, SmackException.NotConnectedException, XMPPException.XMPPErrorException, SmackException.NotLoggedInException, PubSubException.NotALeafNodeException, NotAPubSubNodeException {
omemoManager.initialize();
OmemoBundleElement bundle = OmemoService.fetchBundle(omemoManager, omemoManager.getOwnDevice());
assertNotNull("Bundle must not be null.", bundle);
assertEquals("Published Bundle must equal our local bundle.", bundle, omemoManager.getOmemoService().getOmemoStoreBackend().packOmemoBundle(omemoManager));
}
}

View File

@ -1,109 +0,0 @@
/**
*
* Copyright 2017 Florian Schmaus, Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.cleanServerSideTraces;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.setUpOmemoManager;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.subscribe;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.unidirectionalTrust;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import java.util.logging.Level;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.chat2.ChatManager;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.omemo.element.OmemoElement;
import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag;
import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation;
import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener;
import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder;
import org.igniterealtime.smack.inttest.SmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.TestNotPossibleException;
import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint;
/**
* Test keyTransportMessages.
*/
public class OmemoKeyTransportTest extends AbstractOmemoIntegrationTest {
private OmemoManager alice, bob;
public OmemoKeyTransportTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException {
super(environment);
}
@Override
public void before() {
alice = OmemoManager.getInstanceFor(conOne, 11111);
bob = OmemoManager.getInstanceFor(conTwo, 222222);
}
@SmackIntegrationTest
public void keyTransportTest() throws Exception {
final SimpleResultSyncPoint syncPoint = new SimpleResultSyncPoint();
subscribe(alice, bob, "Bob");
subscribe(bob, alice, "Alice");
setUpOmemoManager(alice);
setUpOmemoManager(bob);
unidirectionalTrust(alice, bob);
unidirectionalTrust(bob, alice);
final byte[] key = OmemoMessageBuilder.generateKey();
final byte[] iv = OmemoMessageBuilder.generateIv();
bob.addOmemoMessageListener(new OmemoMessageListener() {
@Override
public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
// Don't care
}
@Override
public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
LOGGER.log(Level.INFO, "Received a keyTransportMessage.");
assertTrue("Key must match the one we sent.", Arrays.equals(key, cipherAndAuthTag.getKey()));
assertTrue("IV must match the one we sent.", Arrays.equals(iv, cipherAndAuthTag.getIv()));
syncPoint.signal();
}
});
OmemoElement keyTransportElement = alice.createKeyTransportElement(key, iv, bob.getOwnDevice());
Message message = new Message(bob.getOwnJid());
message.addExtension(keyTransportElement);
ChatManager.getInstanceFor(alice.getConnection()).chatWith(bob.getOwnJid().asEntityBareJidIfPossible())
.send(message);
// TODO: Should use 'timeout' field instead of hardcoded '10 * 1000'.
syncPoint.waitForResult(10 * 1000);
}
@Override
public void after() {
alice.shutdown();
bob.shutdown();
cleanServerSideTraces(alice);
cleanServerSideTraces(bob);
}
}

View File

@ -0,0 +1,74 @@
/**
*
* Copyright 2018 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import static org.junit.Assert.assertEquals;
import java.util.List;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smackx.mam.MamManager;
import org.jivesoftware.smackx.mam.element.MamPrefsIQ;
import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException;
import org.jivesoftware.smackx.omemo.util.MessageOrOmemoMessage;
import org.igniterealtime.smack.inttest.SmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.TestNotPossibleException;
/**
* This test sends a message from Alice to Bob, while Bob has automatic decryption disabled.
* Then Bob fetches his Mam archive and decrypts the result.
*/
public class OmemoMamDecryptionTest extends AbstractTwoUsersOmemoIntegrationTest {
public OmemoMamDecryptionTest(SmackIntegrationTestEnvironment environment)
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException, TestNotPossibleException {
super(environment);
MamManager bobsMamManager = MamManager.getInstanceFor(conTwo);
if (!bobsMamManager.isSupported()) {
throw new TestNotPossibleException("Test is not possible, because MAM is not supported on the server.");
}
}
@SmackIntegrationTest
public void mamDecryptionTest() throws XMPPException.XMPPErrorException, SmackException.NotLoggedInException,
SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException,
CryptoFailedException, UndecidedOmemoIdentityException {
// Make sure, Bobs server stores messages in the archive
MamManager bobsMamManager = MamManager.getInstanceFor(bob.getConnection());
bobsMamManager.enableMamForAllMessages();
bobsMamManager.setDefaultBehavior(MamPrefsIQ.DefaultBehavior.always);
// Prevent bob from automatically decrypting MAM messages.
bob.stopStanzaAndPEPListeners();
String body = "This message will be stored in MAM!";
OmemoMessage.Sent encrypted = alice.encrypt(bob.getOwnJid(), body);
alice.getConnection().sendStanza(encrypted.asMessage(bob.getOwnJid()));
MamManager.MamQuery query = bobsMamManager.queryArchive(MamManager.MamQueryArgs.builder().limitResultsToJid(alice.getOwnJid()).build());
assertEquals(1, query.getMessageCount());
List<MessageOrOmemoMessage> decryptedMamQuery = bob.decryptMamQueryResult(query);
assertEquals(1, decryptedMamQuery.size());
assertEquals(body, decryptedMamQuery.get(decryptedMamQuery.size() - 1).getOmemoMessage().getBody());
}
}

View File

@ -0,0 +1,235 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.HashMap;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.roster.PresenceEventListener;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterEntry;
import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
import org.jivesoftware.smackx.omemo.util.EphemeralTrustCallback;
import org.jivesoftware.smackx.omemo.util.OmemoConstants;
import org.jivesoftware.smackx.pubsub.PubSubException;
import org.jivesoftware.smackx.pubsub.PubSubManager;
import com.google.common.collect.Maps;
import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.FullJid;
import org.jxmpp.jid.Jid;
public class OmemoManagerSetupHelper {
/**
* Synchronously subscribes presence.
* @param subscriber connection of user which subscribes.
* @param target connection of user which gets subscribed.
* @param targetNick nick of the subscribed user.
* @param targetGroups groups of the user.
* @throws Exception
*/
public static void syncSubscribePresence(final XMPPConnection subscriber,
final XMPPConnection target,
String targetNick,
String[] targetGroups)
throws Exception {
final SimpleResultSyncPoint subscribed = new SimpleResultSyncPoint();
Roster subscriberRoster = Roster.getInstanceFor(subscriber);
Roster targetRoster = Roster.getInstanceFor(target);
targetRoster.setSubscriptionMode(Roster.SubscriptionMode.accept_all);
subscriberRoster.addPresenceEventListener(new PresenceEventListener() {
@Override
public void presenceAvailable(FullJid address, Presence availablePresence) {
}
@Override
public void presenceUnavailable(FullJid address, Presence presence) {
}
@Override
public void presenceError(Jid address, Presence errorPresence) {
subscribed.signalFailure();
}
@Override
public void presenceSubscribed(BareJid address, Presence subscribedPresence) {
subscribed.signal();
}
@Override
public void presenceUnsubscribed(BareJid address, Presence unsubscribedPresence) {
}
});
subscriberRoster.createEntry(target.getUser().asBareJid(), targetNick, targetGroups);
subscribed.waitForResult(10 * 1000);
}
public static void trustAllIdentities(OmemoManager alice, OmemoManager bob)
throws InterruptedException, SmackException.NotConnectedException, SmackException.NotLoggedInException,
SmackException.NoResponseException, CannotEstablishOmemoSessionException, CorruptedOmemoKeyException,
XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException {
Roster roster = Roster.getInstanceFor(alice.getConnection());
if (alice.getOwnJid() != bob.getOwnJid() &&
(!roster.iAmSubscribedTo(bob.getOwnJid()) || !roster.isSubscribedToMyPresence(bob.getOwnJid()))) {
throw new IllegalStateException("Before trusting identities of a user, we must be subscribed to one another.");
}
alice.requestDeviceListUpdateFor(bob.getOwnJid());
HashMap<OmemoDevice, OmemoFingerprint> fingerprints = alice.getActiveFingerprints(bob.getOwnJid());
for (OmemoDevice device : fingerprints.keySet()) {
OmemoFingerprint fingerprint = fingerprints.get(device);
alice.trustOmemoIdentity(device, fingerprint);
}
}
public static void trustAllIdentitiesWithTests(OmemoManager alice, OmemoManager bob)
throws InterruptedException, SmackException.NotConnectedException, SmackException.NotLoggedInException,
SmackException.NoResponseException, CannotEstablishOmemoSessionException, CorruptedOmemoKeyException,
XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException {
alice.requestDeviceListUpdateFor(bob.getOwnJid());
HashMap<OmemoDevice, OmemoFingerprint> fps1 = alice.getActiveFingerprints(bob.getOwnJid());
assertFalse(fps1.isEmpty());
assertAllDevicesAreUndecided(alice, fps1);
assertAllDevicesAreUntrusted(alice, fps1);
trustAllIdentities(alice, bob);
HashMap<OmemoDevice, OmemoFingerprint> fps2 = alice.getActiveFingerprints(bob.getOwnJid());
assertEquals(fps1.size(), fps2.size());
assertTrue(Maps.difference(fps1, fps2).areEqual());
assertAllDevicesAreDecided(alice, fps2);
assertAllDevicesAreTrusted(alice, fps2);
}
public static OmemoManager prepareOmemoManager(XMPPConnection connection) throws Exception {
final OmemoManager manager = OmemoManager.getInstanceFor(connection, OmemoManager.randomDeviceId());
manager.setTrustCallback(new EphemeralTrustCallback());
if (connection.isAuthenticated()) {
manager.initialize();
} else {
throw new AssertionError("Connection must be authenticated.");
}
return manager;
}
public static void assertAllDevicesAreUndecided(OmemoManager manager, HashMap<OmemoDevice, OmemoFingerprint> devices) {
for (OmemoDevice device : devices.keySet()) {
// All fingerprints MUST be neither decided, nor trusted.
assertFalse(manager.isDecidedOmemoIdentity(device, devices.get(device)));
}
}
public static void assertAllDevicesAreUntrusted(OmemoManager manager, HashMap<OmemoDevice, OmemoFingerprint> devices) {
for (OmemoDevice device : devices.keySet()) {
// All fingerprints MUST be neither decided, nor trusted.
assertFalse(manager.isTrustedOmemoIdentity(device, devices.get(device)));
}
}
public static void assertAllDevicesAreDecided(OmemoManager manager, HashMap<OmemoDevice, OmemoFingerprint> devices) {
for (OmemoDevice device : devices.keySet()) {
// All fingerprints MUST be neither decided, nor trusted.
assertTrue(manager.isDecidedOmemoIdentity(device, devices.get(device)));
}
}
public static void assertAllDevicesAreTrusted(OmemoManager manager, HashMap<OmemoDevice, OmemoFingerprint> devices) {
for (OmemoDevice device : devices.keySet()) {
// All fingerprints MUST be neither decided, nor trusted.
assertTrue(manager.isTrustedOmemoIdentity(device, devices.get(device)));
}
}
public static void cleanUpPubSub(OmemoManager omemoManager) {
PubSubManager pm = PubSubManager.getInstance(omemoManager.getConnection(),omemoManager.getOwnJid());
try {
omemoManager.requestDeviceListUpdateFor(omemoManager.getOwnJid());
} catch (SmackException.NotConnectedException | InterruptedException | SmackException.NoResponseException | PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException e) {
// ignore
}
OmemoCachedDeviceList deviceList = OmemoService.getInstance().getOmemoStoreBackend()
.loadCachedDeviceList(omemoManager.getOwnDevice(), omemoManager.getOwnJid());
for (int id : deviceList.getAllDevices()) {
try {
pm.getLeafNode(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(id)).deleteAllItems();
} catch (InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException |
PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException |
PubSubException.NotAPubSubNodeException e) {
// Silent
}
try {
pm.deleteNode(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(id));
} catch (SmackException.NoResponseException | InterruptedException | SmackException.NotConnectedException
| XMPPException.XMPPErrorException e) {
// Silent
}
}
try {
pm.getLeafNode(OmemoConstants.PEP_NODE_DEVICE_LIST).deleteAllItems();
} catch (InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException |
PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException |
PubSubException.NotAPubSubNodeException e) {
// Silent
}
try {
pm.deleteNode(OmemoConstants.PEP_NODE_DEVICE_LIST);
} catch (SmackException.NoResponseException | InterruptedException | SmackException.NotConnectedException |
XMPPException.XMPPErrorException e) {
// Silent
}
}
public static void cleanUpRoster(OmemoManager omemoManager) {
Roster roster = Roster.getInstanceFor(omemoManager.getConnection());
for (RosterEntry r : roster.getEntries()) {
try {
roster.removeEntry(r);
} catch (InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException |
XMPPException.XMPPErrorException | SmackException.NotLoggedInException e) {
// Silent
}
}
}
}

View File

@ -1,193 +0,0 @@
/**
*
* Copyright 2017 Florian Schmaus, Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertNotSame;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.cleanServerSideTraces;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.setUpOmemoManager;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.subscribe;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.unidirectionalTrust;
import java.security.NoSuchAlgorithmException;
import java.util.logging.Level;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.chat2.ChatManager;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.omemo.element.OmemoBundleElement;
import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException;
import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag;
import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation;
import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener;
import org.jivesoftware.smackx.pubsub.PubSubException;
import org.jivesoftware.smackx.pubsub.PubSubException.NotAPubSubNodeException;
import junit.framework.TestCase;
import org.igniterealtime.smack.inttest.SmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.TestNotPossibleException;
import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint;
/**
* Test message sending.
*/
public class OmemoMessageSendingTest extends AbstractOmemoIntegrationTest {
private OmemoManager alice, bob;
private OmemoStore<?,?,?,?,?,?,?,?,?> store;
public OmemoMessageSendingTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, TestNotPossibleException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
super(environment);
}
@Override
public void before() {
alice = OmemoManager.getInstanceFor(conOne, 123);
bob = OmemoManager.getInstanceFor(conTwo, 345);
store = OmemoService.getInstance().getOmemoStoreBackend();
}
/**
* This Test tests sending and receiving messages.
* Alice and Bob create fresh devices, then they add another to their rosters.
* Next they build sessions with one another and Alice sends a message to Bob.
* After receiving and successfully decrypting the message, its tested, if Bob
* publishes a new Bundle. After that Bob replies to the message and its tested,
* whether Alice can decrypt the message and if she does NOT publish a new Bundle.
*
* @throws CorruptedOmemoKeyException
* @throws InterruptedException
* @throws SmackException.NoResponseException
* @throws SmackException.NotConnectedException
* @throws XMPPException.XMPPErrorException
* @throws SmackException.NotLoggedInException
* @throws PubSubException.NotALeafNodeException
* @throws CannotEstablishOmemoSessionException
* @throws UndecidedOmemoIdentityException
* @throws NoSuchAlgorithmException
* @throws CryptoFailedException
* @throws NotAPubSubNodeException
*/
@SmackIntegrationTest
public void messageSendingTest()
throws CorruptedOmemoKeyException, InterruptedException, SmackException.NoResponseException,
SmackException.NotConnectedException, XMPPException.XMPPErrorException,
SmackException.NotLoggedInException, PubSubException.NotALeafNodeException,
CannotEstablishOmemoSessionException, UndecidedOmemoIdentityException, NoSuchAlgorithmException,
CryptoFailedException, PubSubException.NotAPubSubNodeException {
final String alicesSecret = "Hey Bob! I love you!";
final String bobsSecret = "I love you too, Alice."; //aww <3
final SimpleResultSyncPoint messageOneSyncPoint = new SimpleResultSyncPoint();
final SimpleResultSyncPoint messageTwoSyncPoint = new SimpleResultSyncPoint();
// Subscribe to one another
subscribe(alice, bob, "Bob");
subscribe(bob, alice,"Alice");
// initialize OmemoManagers
setUpOmemoManager(alice);
setUpOmemoManager(bob);
// Save initial bundles
OmemoBundleElement aliceBundle = store.packOmemoBundle(alice);
OmemoBundleElement bobsBundle = store.packOmemoBundle(bob);
// Trust
unidirectionalTrust(alice, bob);
unidirectionalTrust(bob, alice);
// Register messageListeners
bob.addOmemoMessageListener(new OmemoMessageListener() {
@Override
public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
LOGGER.log(Level.INFO,"Bob received message: " + decryptedBody);
if (decryptedBody.trim().equals(alicesSecret.trim())) {
messageOneSyncPoint.signal();
} else {
messageOneSyncPoint.signal(new Exception("Received message must equal sent message."));
}
}
@Override
public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
}
});
alice.addOmemoMessageListener(new OmemoMessageListener() {
@Override
public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
LOGGER.log(Level.INFO, "Alice received message: " + decryptedBody);
if (decryptedBody.trim().equals(bobsSecret.trim())) {
messageTwoSyncPoint.signal();
} else {
messageTwoSyncPoint.signal(new Exception("Received message must equal sent message."));
}
}
@Override
public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
}
});
// Prepare Alice message for Bob
Message encryptedA = alice.encrypt(bob.getOwnJid(), alicesSecret);
ChatManager.getInstanceFor(alice.getConnection()).chatWith(bob.getOwnJid().asEntityBareJidIfPossible())
.send(encryptedA);
try {
messageOneSyncPoint.waitForResult(10 * 1000);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Exception while waiting for message: " + e, e);
TestCase.fail("Bob must have received Alice message.");
}
// Check if Bob published a new Bundle
assertNotSame("Bob must have published another bundle at this point, since we used a PreKeyMessage.",
bobsBundle, OmemoService.fetchBundle(alice, bob.getOwnDevice()));
// Prepare Bobs response
Message encryptedB = bob.encrypt(alice.getOwnJid(), bobsSecret);
ChatManager.getInstanceFor(bob.getConnection()).chatWith(alice.getOwnJid().asEntityBareJidIfPossible())
.send(encryptedB);
try {
messageTwoSyncPoint.waitForResult(10 * 1000);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Exception while waiting for response: " + e, e);
TestCase.fail("Alice must have received a response from Bob.");
}
assertEquals("Alice must not have published a new bundle, since we built the session using Bobs bundle.",
aliceBundle, OmemoService.fetchBundle(bob, alice.getOwnDevice()));
}
@Override
public void after() {
alice.shutdown();
bob.shutdown();
cleanServerSideTraces(alice);
cleanServerSideTraces(bob);
}
}

View File

@ -1,195 +0,0 @@
/**
*
* Copyright 2017 Florian Schmaus, Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.fail;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.cleanServerSideTraces;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.setUpOmemoManager;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.subscribe;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.unidirectionalTrust;
import java.util.logging.Level;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.chat2.ChatManager;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag;
import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation;
import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener;
import org.igniterealtime.smack.inttest.SmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.TestNotPossibleException;
import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint;
/**
* Test session renegotiation.
*/
public class OmemoSessionRenegotiationTest extends AbstractOmemoIntegrationTest {
private OmemoManager alice, bob;
private OmemoStore<?,?,?,?,?,?,?,?,?> store;
public OmemoSessionRenegotiationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, TestNotPossibleException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
super(environment);
}
@Override
public void before() {
alice = OmemoManager.getInstanceFor(conOne, 1337);
bob = OmemoManager.getInstanceFor(conTwo, 1009);
store = OmemoService.getInstance().getOmemoStoreBackend();
}
@SmackIntegrationTest
public void sessionRenegotiationTest() throws Exception {
final boolean[] phaseTwo = new boolean[1];
final SimpleResultSyncPoint sp1 = new SimpleResultSyncPoint();
final SimpleResultSyncPoint sp2 = new SimpleResultSyncPoint();
final SimpleResultSyncPoint sp3 = new SimpleResultSyncPoint();
final SimpleResultSyncPoint sp4 = new SimpleResultSyncPoint();
final String m1 = "1: Alice says hello to bob.";
final String m2 = "2: Bob replies to Alice.";
final String m3 = "3. This message will arrive but Bob cannot decrypt it.";
final String m4 = "4. This message is readable by Bob again.";
subscribe(alice, bob, "Bob");
subscribe(bob, alice, "Alice");
setUpOmemoManager(alice);
setUpOmemoManager(bob);
unidirectionalTrust(alice, bob);
unidirectionalTrust(bob, alice);
OmemoMessageListener first = new OmemoMessageListener() {
@Override
public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
LOGGER.log(Level.INFO, "Bob received OMEMO message: " + decryptedBody);
assertEquals("Received message MUST match the one we sent.", decryptedBody, m1);
sp1.signal();
}
@Override
public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
}
};
bob.addOmemoMessageListener(first);
ChatManager.getInstanceFor(alice.getConnection()).chatWith(bob.getOwnJid().asEntityBareJidIfPossible())
.send(alice.encrypt(bob.getOwnJid(), m1));
sp1.waitForResult(10 * 1000);
bob.removeOmemoMessageListener(first);
OmemoMessageListener second = new OmemoMessageListener() {
@Override
public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
LOGGER.log(Level.INFO, "Alice received OMEMO message: " + decryptedBody);
assertEquals("Reply must match the message we sent.", decryptedBody, m2);
sp2.signal();
}
@Override
public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
}
};
alice.addOmemoMessageListener(second);
ChatManager.getInstanceFor(bob.getConnection()).chatWith(alice.getOwnJid().asEntityBareJidIfPossible())
.send(bob.encrypt(alice.getOwnJid(), m2));
sp2.waitForResult(10 * 1000);
alice.removeOmemoMessageListener(second);
store.forgetOmemoSessions(bob);
store.removeAllRawSessionsOf(bob, alice.getOwnJid());
OmemoMessageListener third = new OmemoMessageListener() {
@Override
public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
fail("Bob should not have received a decipherable message: " + decryptedBody);
}
@Override
public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
}
};
bob.addOmemoMessageListener(third);
OmemoMessageListener fourth = new OmemoMessageListener() {
@Override
public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
}
@Override
public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
LOGGER.log(Level.INFO, "Alice received preKeyMessage.");
sp3.signal();
}
};
alice.addOmemoMessageListener(fourth);
ChatManager.getInstanceFor(alice.getConnection()).chatWith(bob.getOwnJid().asEntityBareJidIfPossible())
.send(alice.encrypt(bob.getOwnJid(), m3));
sp3.waitForResult(10 * 1000);
bob.removeOmemoMessageListener(third);
alice.removeOmemoMessageListener(fourth);
OmemoMessageListener fifth = new OmemoMessageListener() {
@Override
public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
LOGGER.log(Level.INFO, "Bob received an OMEMO message: " + decryptedBody);
assertEquals("The received message must match the one we sent.",
decryptedBody, m4);
sp4.signal();
}
@Override
public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) {
}
};
bob.addOmemoMessageListener(fifth);
ChatManager.getInstanceFor(alice.getConnection()).chatWith(bob.getOwnJid().asEntityBareJidIfPossible())
.send(alice.encrypt(bob.getOwnJid(), m4));
sp4.waitForResult(10 * 1000);
}
@Override
public void after() {
alice.shutdown();
bob.shutdown();
cleanServerSideTraces(alice);
cleanServerSideTraces(bob);
}
}

View File

@ -1,163 +0,0 @@
/**
*
* Copyright 2017 Florian Schmaus, Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNotNull;
import static junit.framework.TestCase.assertNotSame;
import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.assertTrue;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.cleanServerSideTraces;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.deletePath;
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.setUpOmemoManager;
import java.util.Date;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.igniterealtime.smack.inttest.SmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.TestNotPossibleException;
/**
* Test the OmemoStore.
*/
public class OmemoStoreTest extends AbstractOmemoIntegrationTest {
private OmemoManager alice;
private OmemoManager bob;
public OmemoStoreTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException {
super(environment);
}
@Override
public void before() {
alice = OmemoManager.getInstanceFor(conOne);
bob = OmemoManager.getInstanceFor(conOne);
}
@SmackIntegrationTest
public void storeTest() throws Exception {
// ########### PRE-INITIALIZATION ############
assertEquals("Creating an OmemoManager without MUST have set the default deviceId.", alice.getDeviceId(), OmemoService.getInstance().getOmemoStoreBackend().getDefaultDeviceId(alice.getOwnJid()));
assertEquals("OmemoManager must be equal, since both got created without giving a deviceId.", alice, bob);
OmemoService.getInstance().getOmemoStoreBackend().setDefaultDeviceId(alice.getOwnJid(), -1); //Reset default deviceId
alice.shutdown();
alice = OmemoManager.getInstanceFor(conOne);
assertNotSame("Instantiating OmemoManager without deviceId MUST assign random deviceId.", alice.getDeviceId(), bob.getDeviceId());
OmemoStore<?,?,?,?,?,?,?,?,?> store = OmemoService.getInstance().getOmemoStoreBackend();
OmemoFingerprint finger = new OmemoFingerprint("FINGER");
// DefaultDeviceId
store.setDefaultDeviceId(alice.getOwnJid(), 777);
assertEquals("defaultDeviceId setting/getting must equal.", 777, store.getDefaultDeviceId(alice.getOwnJid()));
// Trust/Distrust/Decide
bob.shutdown();
bob = OmemoManager.getInstanceFor(conTwo, 998);
assertFalse("Bobs device MUST be undecided at this point",
store.isDecidedOmemoIdentity(alice, bob.getOwnDevice(), finger));
assertFalse("Bobs device MUST not be trusted at this point",
store.isTrustedOmemoIdentity(alice, bob.getOwnDevice(), finger));
store.trustOmemoIdentity(alice, bob.getOwnDevice(), finger);
assertTrue("Bobs device MUST be trusted at this point.",
store.isTrustedOmemoIdentity(alice, bob.getOwnDevice(), finger));
assertTrue("Bobs device MUST be decided at this point.",
store.isDecidedOmemoIdentity(alice, bob.getOwnDevice(), finger));
store.distrustOmemoIdentity(alice, bob.getOwnDevice(), finger);
assertFalse("Bobs device MUST be untrusted at this point.",
store.isTrustedOmemoIdentity(alice, bob.getOwnDevice(), finger));
// Dates
assertNull("Date of last received message must be null when no message was received ever.",
store.getDateOfLastReceivedMessage(alice, bob.getOwnDevice()));
Date now = new Date();
store.setDateOfLastReceivedMessage(alice, bob.getOwnDevice(), now);
assertEquals("Date of last received message must match the one we set.",
now, store.getDateOfLastReceivedMessage(alice, bob.getOwnDevice()));
assertNull("Date of last signed preKey renewal must be null.",
store.getDateOfLastSignedPreKeyRenewal(alice));
store.setDateOfLastSignedPreKeyRenewal(alice, now);
assertEquals("Date of last signed preKey renewal must match our date.",
now, store.getDateOfLastSignedPreKeyRenewal(alice));
// Keys
assertNull("IdentityKeyPair must be null at this point.",
store.loadOmemoIdentityKeyPair(alice));
assertNull("IdentityKey of contact must be null at this point.",
store.loadOmemoIdentityKey(alice, bob.getOwnDevice()));
assertEquals("PreKeys list must be of length 0 at this point.",
0, store.loadOmemoPreKeys(alice).size());
assertEquals("SignedPreKeys list must be of length 0 at this point.",
0, store.loadOmemoSignedPreKeys(alice).size());
assertNotNull("Generated IdentityKeyPair must not be null.",
store.generateOmemoIdentityKeyPair());
assertEquals("Generated PreKey list must be of correct length.",
100, store.generateOmemoPreKeys(1, 100).size());
// LastPreKeyId
assertEquals("LastPreKeyId must be 0 at this point.",
0, store.loadLastPreKeyId(alice));
store.storeLastPreKeyId(alice, 1234);
Thread.sleep(100);
assertEquals("LastPreKeyId set/get must equal.", 1234, store.loadLastPreKeyId(alice));
store.storeLastPreKeyId(alice, 0);
// CurrentSignedPreKeyId
assertEquals("CurrentSignedPreKeyId must be 0 at this point.",
0, store.loadCurrentSignedPreKeyId(alice));
store.storeCurrentSignedPreKeyId(alice, 554);
Thread.sleep(100);
assertEquals("CurrentSignedPreKeyId must match the value we set.",
554, store.loadCurrentSignedPreKeyId(alice));
store.storeCurrentSignedPreKeyId(alice, 0);
deletePath(alice);
// ################# POST-INITIALIZATION #################
setUpOmemoManager(alice);
// Keys
assertNotNull("IdentityKeyPair must not be null after initialization",
store.loadOmemoIdentityKeyPair(alice));
assertNotSame("LastPreKeyId must not be 0 after initialization.",
0, store.loadLastPreKeyId(alice));
assertNotSame("currentSignedPreKeyId must not be 0 after initialization.",
0, store.loadCurrentSignedPreKeyId(alice));
assertNotNull("The last PreKey must not be null.",
store.loadOmemoPreKey(alice, store.loadLastPreKeyId(alice) - 1));
assertNotNull("The current signedPreKey must not be null.",
store.loadOmemoSignedPreKey(alice, store.loadCurrentSignedPreKeyId(alice)));
}
@Override
public void after() {
cleanServerSideTraces(alice);
cleanServerSideTraces(bob);
alice.shutdown();
bob.shutdown();
}
}

View File

@ -0,0 +1,79 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.igniterealtime.smack.inttest.SmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.TestNotPossibleException;
public class SessionRenegotiationIntegrationTest extends AbstractTwoUsersOmemoIntegrationTest {
public SessionRenegotiationIntegrationTest(SmackIntegrationTestEnvironment environment)
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException, TestNotPossibleException {
super(environment);
}
@SmackIntegrationTest
public void sessionRenegotiationTest() throws Exception {
boolean prevRepairProperty = OmemoConfiguration.getRepairBrokenSessionsWithPreKeyMessages();
OmemoConfiguration.setRepairBrokenSessionsWithPrekeyMessages(true);
boolean prevCompleteSessionProperty = OmemoConfiguration.getCompleteSessionWithEmptyMessage();
OmemoConfiguration.setCompleteSessionWithEmptyMessage(false);
// send PreKeyMessage -> Success
final String body1 = "P = NP is true for all N,P from the set of complex numbers, where P is equal to 0";
AbstractOmemoMessageListener.PreKeyMessageListener listener1 =
new AbstractOmemoMessageListener.PreKeyMessageListener(body1);
OmemoMessage.Sent e1 = alice.encrypt(bob.getOwnJid(), body1);
bob.addOmemoMessageListener(listener1);
alice.getConnection().sendStanza(e1.asMessage(bob.getOwnJid()));
listener1.getSyncPoint().waitForResult(10 * 1000);
bob.removeOmemoMessageListener(listener1);
// Remove the session on Bobs side.
synchronized (bob.LOCK) {
bob.getOmemoService().getOmemoStoreBackend().removeRawSession(bob.getOwnDevice(), alice.getOwnDevice());
}
// Send normal message -> fail, bob repairs session with preKeyMessage
final String body2 = "P = NP is also true for all N,P from the set of complex numbers, where N is equal to 1.";
AbstractOmemoMessageListener.PreKeyKeyTransportListener listener2 =
new AbstractOmemoMessageListener.PreKeyKeyTransportListener();
OmemoMessage.Sent e2 = alice.encrypt(bob.getOwnJid(), body2);
alice.addOmemoMessageListener(listener2);
alice.getConnection().sendStanza(e2.asMessage(bob.getOwnJid()));
listener2.getSyncPoint().waitForResult(10 * 1000);
alice.removeOmemoMessageListener(listener2);
// Send normal message -> success
final String body3 = "P = NP would be a disaster for the world of cryptography.";
AbstractOmemoMessageListener.MessageListener listener3 = new AbstractOmemoMessageListener.MessageListener(body3);
OmemoMessage.Sent e3 = alice.encrypt(bob.getOwnJid(), body3);
bob.addOmemoMessageListener(listener3);
alice.getConnection().sendStanza(e3.asMessage(bob.getOwnJid()));
listener3.getSyncPoint().waitForResult(10 * 1000);
bob.removeOmemoMessageListener(listener3);
OmemoConfiguration.setRepairBrokenSessionsWithPrekeyMessages(prevRepairProperty);
OmemoConfiguration.setCompleteSessionWithEmptyMessage(prevCompleteSessionProperty);
}
}

View File

@ -10,7 +10,8 @@ dependencies {
compile project(":smack-im") compile project(":smack-im")
compile project(":smack-extensions") compile project(":smack-extensions")
compile project(":smack-omemo") compile project(":smack-omemo")
compile 'org.whispersystems:signal-protocol-java:2.4.0' compile 'org.whispersystems:signal-protocol-java:2.6.2'
testCompile project(path: ":smack-core", configuration: "testRuntime") testCompile project(path: ":smack-core", configuration: "testRuntime")
testCompile project(path: ":smack-omemo", configuration: "testRuntime")
} }

View File

@ -0,0 +1,60 @@
/**
*
* Copyright 2017 Paul Schaub
*
* This file is part of smack-omemo-signal.
*
* smack-omemo-signal is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jivesoftware.smackx.omemo.signal;
import org.jivesoftware.smackx.omemo.CachingOmemoStore;
import org.jivesoftware.smackx.omemo.OmemoStore;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.SessionCipher;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.state.PreKeyBundle;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
/**
* Implementation of the CachingOmemoStore for smack-omemo-signal.
* This Store implementation can either be used as a proxy wrapping a persistent SignalOmemoStore in order to prevent
* excessive storage access, or it can be used standalone as an ephemeral store, which doesn't persist its contents.
*/
public class SignalCachingOmemoStore extends CachingOmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord,
SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> {
/**
* Create a new SignalCachingOmemoStore as a caching layer around a persisting OmemoStore
* (eg. a SignalFileBasedOmemoStore).
* @param wrappedStore other store implementation that gets wrapped
*/
public SignalCachingOmemoStore(OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord,
SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> wrappedStore) {
super(wrappedStore);
}
/**
* Create a new SignalCachingOmemoStore as an ephemeral standalone OmemoStore.
*/
public SignalCachingOmemoStore() {
super(new SignalOmemoKeyUtil());
}
}

View File

@ -42,18 +42,15 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord;
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class SignalFileBasedOmemoStore public class SignalFileBasedOmemoStore
extends FileBasedOmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> { extends FileBasedOmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord,
SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> {
public SignalFileBasedOmemoStore() {
super();
}
public SignalFileBasedOmemoStore(File base) { public SignalFileBasedOmemoStore(File base) {
super(base); super(base);
} }
@Override @Override
public OmemoKeyUtil<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> keyUtil() { public OmemoKeyUtil<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, ECPublicKey, PreKeyBundle> keyUtil() {
return new SignalOmemoKeyUtil(); return new SignalOmemoKeyUtil();
} }
} }

View File

@ -21,25 +21,18 @@
package org.jivesoftware.smackx.omemo.signal; package org.jivesoftware.smackx.omemo.signal;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.TreeMap;
import org.jivesoftware.smackx.omemo.OmemoFingerprint; import org.jivesoftware.smackx.omemo.element.OmemoBundleElement;
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.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.internal.OmemoSession; import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil; import org.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.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException; 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.Curve;
import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.state.PreKeyBundle; import org.whispersystems.libsignal.state.PreKeyBundle;
@ -54,7 +47,7 @@ import org.whispersystems.libsignal.util.KeyHelper;
* @author Paul Schaub * @author Paul Schaub
*/ */
public class SignalOmemoKeyUtil extends OmemoKeyUtil<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, public class SignalOmemoKeyUtil extends OmemoKeyUtil<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord,
SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> { SessionRecord, ECPublicKey, PreKeyBundle> {
@Override @Override
public IdentityKeyPair generateOmemoIdentityKeyPair() { public IdentityKeyPair generateOmemoIdentityKeyPair() {
@ -62,17 +55,18 @@ public class SignalOmemoKeyUtil extends OmemoKeyUtil<IdentityKeyPair, IdentityKe
} }
@Override @Override
public HashMap<Integer, PreKeyRecord> generateOmemoPreKeys(int currentPreKeyId, int count) { public TreeMap<Integer, PreKeyRecord> generateOmemoPreKeys(int currentPreKeyId, int count) {
List<PreKeyRecord> preKeyRecords = KeyHelper.generatePreKeys(currentPreKeyId, count); List<PreKeyRecord> preKeyRecords = KeyHelper.generatePreKeys(currentPreKeyId, count);
HashMap<Integer, PreKeyRecord> hashMap = new HashMap<>(); TreeMap<Integer, PreKeyRecord> map = new TreeMap<>();
for (PreKeyRecord p : preKeyRecords) { for (PreKeyRecord p : preKeyRecords) {
hashMap.put(p.getId(), p); map.put(p.getId(), p);
} }
return hashMap; return map;
} }
@Override @Override
public SignedPreKeyRecord generateOmemoSignedPreKey(IdentityKeyPair identityKeyPair, int currentPreKeyId) throws CorruptedOmemoKeyException { public SignedPreKeyRecord generateOmemoSignedPreKey(IdentityKeyPair identityKeyPair, int currentPreKeyId)
throws CorruptedOmemoKeyException {
try { try {
return KeyHelper.generateSignedPreKey(identityKeyPair, currentPreKeyId); return KeyHelper.generateSignedPreKey(identityKeyPair, currentPreKeyId);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
@ -82,6 +76,7 @@ public class SignalOmemoKeyUtil extends OmemoKeyUtil<IdentityKeyPair, IdentityKe
@Override @Override
public SignedPreKeyRecord signedPreKeyFromBytes(byte[] data) throws IOException { public SignedPreKeyRecord signedPreKeyFromBytes(byte[] data) throws IOException {
if (data == null) return null;
return new SignedPreKeyRecord(data); return new SignedPreKeyRecord(data);
} }
@ -90,21 +85,9 @@ public class SignalOmemoKeyUtil extends OmemoKeyUtil<IdentityKeyPair, IdentityKe
return signedPreKeyRecord.serialize(); return signedPreKeyRecord.serialize();
} }
@Override
public OmemoSession<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher>
createOmemoSession(OmemoManager omemoManager, OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> omemoStore,
OmemoDevice contact, IdentityKey identityKey) {
return new SignalOmemoSession(omemoManager, omemoStore, contact, identityKey);
}
@Override
public OmemoSession<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher>
createOmemoSession(OmemoManager omemoManager, OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> omemoStore, OmemoDevice from) {
return new SignalOmemoSession(omemoManager, omemoStore, from);
}
@Override @Override
public SessionRecord rawSessionFromBytes(byte[] data) throws IOException { public SessionRecord rawSessionFromBytes(byte[] data) throws IOException {
if (data == null) return null;
return new SessionRecord(data); return new SessionRecord(data);
} }
@ -115,6 +98,7 @@ public class SignalOmemoKeyUtil extends OmemoKeyUtil<IdentityKeyPair, IdentityKe
@Override @Override
public IdentityKeyPair identityKeyPairFromBytes(byte[] data) throws CorruptedOmemoKeyException { public IdentityKeyPair identityKeyPairFromBytes(byte[] data) throws CorruptedOmemoKeyException {
if (data == null) return null;
try { try {
return new IdentityKeyPair(data); return new IdentityKeyPair(data);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
@ -124,6 +108,7 @@ public class SignalOmemoKeyUtil extends OmemoKeyUtil<IdentityKeyPair, IdentityKe
@Override @Override
public IdentityKey identityKeyFromBytes(byte[] data) throws CorruptedOmemoKeyException { public IdentityKey identityKeyFromBytes(byte[] data) throws CorruptedOmemoKeyException {
if (data == null) return null;
try { try {
return new IdentityKey(data, 0); return new IdentityKey(data, 0);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
@ -133,6 +118,7 @@ public class SignalOmemoKeyUtil extends OmemoKeyUtil<IdentityKeyPair, IdentityKe
@Override @Override
public ECPublicKey ellipticCurvePublicKeyFromBytes(byte[] data) throws CorruptedOmemoKeyException { public ECPublicKey ellipticCurvePublicKeyFromBytes(byte[] data) throws CorruptedOmemoKeyException {
if (data == null) return null;
try { try {
return Curve.decodePoint(data, 0); return Curve.decodePoint(data, 0);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
@ -147,11 +133,13 @@ public class SignalOmemoKeyUtil extends OmemoKeyUtil<IdentityKeyPair, IdentityKe
@Override @Override
public PreKeyRecord preKeyFromBytes(byte[] bytes) throws IOException { public PreKeyRecord preKeyFromBytes(byte[] bytes) throws IOException {
if (bytes == null) return null;
return new PreKeyRecord(bytes); return new PreKeyRecord(bytes);
} }
@Override @Override
public PreKeyBundle bundleFromOmemoBundle(OmemoBundleVAxolotlElement bundle, OmemoDevice contact, int preKeyId) throws CorruptedOmemoKeyException { public PreKeyBundle bundleFromOmemoBundle(OmemoBundleElement bundle, OmemoDevice contact, int preKeyId)
throws CorruptedOmemoKeyException {
return new PreKeyBundle(0, return new PreKeyBundle(0,
contact.getDeviceId(), contact.getDeviceId(),
preKeyId, preKeyId,
@ -208,20 +196,23 @@ public class SignalOmemoKeyUtil extends OmemoKeyUtil<IdentityKeyPair, IdentityKe
} }
@Override @Override
public OmemoFingerprint getFingerprint(IdentityKey identityKey) { public OmemoFingerprint getFingerprintOfIdentityKey(IdentityKey identityKey) {
if (identityKey == null) {
return null;
}
String fp = identityKey.getFingerprint(); String fp = identityKey.getFingerprint();
// Cut "(byte)0x" prefixes, remove spaces and commas, cut first two digits. // Cut "(byte)0x" prefixes, remove spaces and commas, cut first two digits.
fp = fp.replace("(byte)0x", "").replace(",", "").replace(" ", "").substring(2); fp = fp.replace("(byte)0x", "").replace(",", "")
.replace(" ", "").substring(2);
return new OmemoFingerprint(fp); return new OmemoFingerprint(fp);
} }
@Override @Override
public SignalProtocolAddress omemoDeviceAsAddress(OmemoDevice contact) { public OmemoFingerprint getFingerprintOfIdentityKeyPair(IdentityKeyPair identityKeyPair) {
return new SignalProtocolAddress(contact.getJid().asBareJid().toString(), contact.getDeviceId()); if (identityKeyPair == null) {
} return null;
}
@Override return getFingerprintOfIdentityKey(identityKeyPair.getPublicKey());
public OmemoDevice addressAsOmemoDevice(SignalProtocolAddress address) throws XmppStringprepException {
return new OmemoDevice(JidCreate.bareFrom(address.getName()), address.getDeviceId());
} }
} }

View File

@ -0,0 +1,162 @@
/**
*
* 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 java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smackx.omemo.OmemoManager;
import org.jivesoftware.smackx.omemo.OmemoRatchet;
import org.jivesoftware.smackx.omemo.OmemoStore;
import org.jivesoftware.smackx.omemo.element.OmemoElement;
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.UntrustedOmemoIdentityException;
import org.jivesoftware.smackx.omemo.internal.CiphertextTuple;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
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;
public class SignalOmemoRatchet
extends OmemoRatchet<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord,
SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> {
private static final Logger LOGGER = Logger.getLogger(OmemoRatchet.class.getName());
private final SignalOmemoStoreConnector storeConnector;
SignalOmemoRatchet(OmemoManager omemoManager,
OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord,
SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle,
SessionCipher> store) {
super(omemoManager, store);
this.storeConnector = new SignalOmemoStoreConnector(omemoManager, store);
}
@Override
public byte[] doubleRatchetDecrypt(OmemoDevice sender, byte[] encryptedKey)
throws CorruptedOmemoKeyException, NoRawSessionException, CryptoFailedException,
UntrustedOmemoIdentityException {
SessionCipher cipher = getCipher(sender);
byte[] decryptedKey;
// Try to handle the message as a PreKeySignalMessage...
try {
PreKeySignalMessage preKeyMessage = new PreKeySignalMessage(encryptedKey);
if (!preKeyMessage.getPreKeyId().isPresent()) {
throw new CryptoFailedException("PreKeyMessage did not contain a preKeyId.");
}
IdentityKey messageIdentityKey = preKeyMessage.getIdentityKey();
IdentityKey previousIdentityKey = store.loadOmemoIdentityKey(storeConnector.getOurDevice(), sender);
if (previousIdentityKey != null &&
!previousIdentityKey.getFingerprint().equals(messageIdentityKey.getFingerprint())) {
throw new UntrustedOmemoIdentityException(sender,
store.keyUtil().getFingerprintOfIdentityKey(previousIdentityKey),
store.keyUtil().getFingerprintOfIdentityKey(messageIdentityKey));
}
try {
decryptedKey = cipher.decrypt(preKeyMessage);
}
catch (UntrustedIdentityException e) {
throw new AssertionError("Signals trust management MUST be disabled.");
}
catch (LegacyMessageException | InvalidKeyException e) {
throw new CryptoFailedException(e);
}
catch (InvalidKeyIdException e) {
throw new NoRawSessionException(sender, e);
}
catch (DuplicateMessageException e) {
LOGGER.log(Level.INFO, "Decryption of PreKeyMessage from " + sender +
" failed, since the message has been decrypted before.");
return null;
}
} catch (InvalidVersionException | InvalidMessageException noPreKeyMessage) {
// ...if that fails, handle it as a SignalMessage
try {
SignalMessage message = new SignalMessage(encryptedKey);
decryptedKey = getCipher(sender).decrypt(message);
}
catch (UntrustedIdentityException e) {
throw new AssertionError("Signals trust management MUST be disabled.");
}
catch (InvalidMessageException | NoSessionException e) {
throw new NoRawSessionException(sender, e);
}
catch (LegacyMessageException e) {
throw new CryptoFailedException(e);
}
catch (DuplicateMessageException e1) {
LOGGER.log(Level.INFO, "Decryption of SignalMessage from " + sender +
" failed, since the message has been decrypted before.");
return null;
}
}
return decryptedKey;
}
@Override
public CiphertextTuple doubleRatchetEncrypt(OmemoDevice recipient, byte[] messageKey) {
CiphertextMessage ciphertextMessage;
try {
ciphertextMessage = getCipher(recipient).encrypt(messageKey);
} catch (UntrustedIdentityException e) {
throw new AssertionError("Signals trust management MUST be disabled.");
}
// TODO: Figure out, if this is enough...
int type = (ciphertextMessage.getType() == CiphertextMessage.PREKEY_TYPE ?
OmemoElement.TYPE_OMEMO_PREKEY_MESSAGE : OmemoElement.TYPE_OMEMO_MESSAGE);
return new CiphertextTuple(ciphertextMessage.serialize(), type);
}
private SessionCipher getCipher(OmemoDevice device) {
return new SessionCipher(storeConnector, storeConnector, storeConnector, storeConnector,
SignalOmemoStoreConnector.asAddress(device));
}
}

View File

@ -20,21 +20,8 @@
*/ */
package org.jivesoftware.smackx.omemo.signal; package org.jivesoftware.smackx.omemo.signal;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.logging.Level; import java.util.logging.Level;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smackx.omemo.OmemoManager; import org.jivesoftware.smackx.omemo.OmemoManager;
import org.jivesoftware.smackx.omemo.OmemoService; import org.jivesoftware.smackx.omemo.OmemoService;
import org.jivesoftware.smackx.omemo.OmemoStore; import org.jivesoftware.smackx.omemo.OmemoStore;
@ -59,16 +46,27 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord;
* @author Paul Schaub * @author Paul Schaub
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class SignalOmemoService extends OmemoService<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> { public final class SignalOmemoService
extends OmemoService<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord,
SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> {
private static SignalOmemoService INSTANCE; private static SignalOmemoService INSTANCE;
private static boolean LICENSE_ACKNOWLEDGED = false; private static boolean LICENSE_ACKNOWLEDGED = false;
public static void setup() throws InvalidKeyException, XMPPErrorException, NoSuchPaddingException, InvalidAlgorithmParameterException, UnsupportedEncodingException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, NoSuchProviderException, SmackException, InterruptedException, CorruptedOmemoKeyException { @Override
protected SignalOmemoRatchet instantiateOmemoRatchet(
OmemoManager manager,
OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord,
SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> store) {
return new SignalOmemoRatchet(manager, getOmemoStoreBackend());
}
public static void setup() {
if (!LICENSE_ACKNOWLEDGED) { if (!LICENSE_ACKNOWLEDGED) {
throw new IllegalStateException("smack-omemo-signal is licensed under the terms of the GPLv3. Please be aware that you " + throw new IllegalStateException("smack-omemo-signal is licensed under the terms of the GPLv3. " +
"can only use this library within the terms of the GPLv3. See for example " + "Please be aware that you can only use this library within the terms of the GPLv3. " +
"https://www.gnu.org/licenses/quick-guide-gplv3 for more details. Please call " + "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 " + "SignalOmemoService.acknowledgeLicense() prior to the setup() method in order to prevent " +
"this exception."); "this exception.");
} }
@ -79,15 +77,13 @@ public final class SignalOmemoService extends OmemoService<IdentityKeyPair, Iden
} }
@Override @Override
public OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> createDefaultOmemoStoreBackend() { public OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord,
return new SignalFileBasedOmemoStore(); SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher>
createDefaultOmemoStoreBackend() {
return new SignalCachingOmemoStore();
} }
private SignalOmemoService() private SignalOmemoService() {
throws SmackException, InterruptedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException,
NoSuchPaddingException, InvalidAlgorithmParameterException, UnsupportedEncodingException,
IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, NoSuchProviderException,
java.security.InvalidKeyException {
super(); super();
} }
@ -96,14 +92,17 @@ public final class SignalOmemoService extends OmemoService<IdentityKeyPair, Iden
} }
@Override @Override
protected void processBundle(OmemoManager omemoManager, PreKeyBundle preKeyBundle, OmemoDevice contact) throws CorruptedOmemoKeyException { protected void processBundle(OmemoManager omemoManager,
PreKeyBundle contactsBundle,
OmemoDevice contactsDevice)
throws CorruptedOmemoKeyException {
SignalOmemoStoreConnector connector = new SignalOmemoStoreConnector(omemoManager, getOmemoStoreBackend()); SignalOmemoStoreConnector connector = new SignalOmemoStoreConnector(omemoManager, getOmemoStoreBackend());
SessionBuilder builder = new SessionBuilder(connector, connector, connector, connector, SessionBuilder builder = new SessionBuilder(connector, connector, connector, connector,
getOmemoStoreBackend().keyUtil().omemoDeviceAsAddress(contact)); SignalOmemoStoreConnector.asAddress(contactsDevice));
try { try {
builder.process(preKeyBundle); builder.process(contactsBundle);
LOGGER.log(Level.INFO, "Session built with " + contact); LOGGER.log(Level.FINE, "Session built with " + contactsDevice);
getOmemoStoreBackend().getOmemoSessionOf(omemoManager, contact); //method puts session in session map.
} catch (org.whispersystems.libsignal.InvalidKeyException e) { } catch (org.whispersystems.libsignal.InvalidKeyException e) {
throw new CorruptedOmemoKeyException(e.getMessage()); throw new CorruptedOmemoKeyException(e.getMessage());
} catch (UntrustedIdentityException e) { } catch (UntrustedIdentityException e) {

View File

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

View File

@ -40,12 +40,13 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord;
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public abstract class SignalOmemoStore public abstract class SignalOmemoStore
extends OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> { extends OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord,
SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> {
private final SignalOmemoKeyUtil signalKeyUtil = new SignalOmemoKeyUtil(); private final SignalOmemoKeyUtil signalKeyUtil = new SignalOmemoKeyUtil();
@Override @Override
public OmemoKeyUtil<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> keyUtil() { public OmemoKeyUtil<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, ECPublicKey, PreKeyBundle> keyUtil() {
return signalKeyUtil; return signalKeyUtil;
} }
} }

View File

@ -21,15 +21,17 @@
package org.jivesoftware.smackx.omemo.signal; package org.jivesoftware.smackx.omemo.signal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.TreeMap;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.jivesoftware.smackx.omemo.OmemoManager; import org.jivesoftware.smackx.omemo.OmemoManager;
import org.jivesoftware.smackx.omemo.OmemoStore; import org.jivesoftware.smackx.omemo.OmemoStore;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException; import org.jxmpp.stringprep.XmppStringprepException;
import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKey;
@ -57,21 +59,28 @@ public class SignalOmemoStoreConnector
private static final Logger LOGGER = Logger.getLogger(SignalOmemoStoreConnector.class.getName()); private static final Logger LOGGER = Logger.getLogger(SignalOmemoStoreConnector.class.getName());
private final OmemoManager omemoManager; private final OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord,
private final OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher>
omemoStore; omemoStore;
private final OmemoManager omemoManager;
public SignalOmemoStoreConnector(OmemoManager omemoManager, OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> store) { public SignalOmemoStoreConnector(OmemoManager omemoManager, OmemoStore<IdentityKeyPair,
IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey,
PreKeyBundle, SessionCipher> store) {
this.omemoManager = omemoManager; this.omemoManager = omemoManager;
this.omemoStore = store; this.omemoStore = store;
} }
OmemoDevice getOurDevice() {
return omemoManager.getOwnDevice();
}
@Override @Override
public IdentityKeyPair getIdentityKeyPair() { public IdentityKeyPair getIdentityKeyPair() {
try { try {
return omemoStore.loadOmemoIdentityKeyPair(omemoManager); return omemoStore.loadOmemoIdentityKeyPair(getOurDevice());
} catch (CorruptedOmemoKeyException e) { } catch (CorruptedOmemoKeyException e) {
LOGGER.log(Level.SEVERE, "getIdentityKeyPair has failed: " + e, e); LOGGER.log(Level.SEVERE, "IdentityKeyPair seems to be invalid.", e);
return null; return null;
} }
} }
@ -86,33 +95,41 @@ public class SignalOmemoStoreConnector
} }
@Override @Override
public void saveIdentity(SignalProtocolAddress signalProtocolAddress, IdentityKey identityKey) { public boolean saveIdentity(SignalProtocolAddress signalProtocolAddress, IdentityKey identityKey) {
OmemoDevice device;
try { try {
omemoStore.storeOmemoIdentityKey(omemoManager, omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress), identityKey); device = asOmemoDevice(signalProtocolAddress);
} catch (XmppStringprepException e) { } catch (XmppStringprepException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
omemoStore.storeOmemoIdentityKey(getOurDevice(), device, identityKey);
return true;
} }
@Override @Override
public boolean isTrustedIdentity(SignalProtocolAddress signalProtocolAddress, IdentityKey identityKey) { public boolean isTrustedIdentity(SignalProtocolAddress signalProtocolAddress,
// Disable internal trust management. Instead we use OmemoStore.isTrustedOmemoIdentity() before encrypting for a IdentityKey identityKey,
// recipient. Direction direction) {
// Disable internal trust management. Instead we use OmemoStore.isTrustedOmemoIdentity() before encrypting
// for a recipient.
return true; return true;
} }
@Override @Override
public PreKeyRecord loadPreKey(int i) throws InvalidKeyIdException { public PreKeyRecord loadPreKey(int i) throws InvalidKeyIdException {
PreKeyRecord pr = omemoStore.loadOmemoPreKey(omemoManager, i); PreKeyRecord preKey = omemoStore.loadOmemoPreKey(getOurDevice(), i);
if (pr == null) {
throw new InvalidKeyIdException("No PreKey with Id " + i + " found!"); if (preKey == null) {
throw new InvalidKeyIdException("No PreKey with Id " + i + " found.");
} }
return pr;
return preKey;
} }
@Override @Override
public void storePreKey(int i, PreKeyRecord preKeyRecord) { public void storePreKey(int i, PreKeyRecord preKeyRecord) {
omemoStore.storeOmemoPreKey(omemoManager, i, preKeyRecord); omemoStore.storeOmemoPreKey(getOurDevice(), i, preKeyRecord);
} }
@Override @Override
@ -120,96 +137,113 @@ public class SignalOmemoStoreConnector
try { try {
return (loadPreKey(i) != null); return (loadPreKey(i) != null);
} catch (InvalidKeyIdException e) { } catch (InvalidKeyIdException e) {
LOGGER.log(Level.WARNING, "containsPreKey has failed: " + e.getMessage());
return false; return false;
} }
} }
@Override @Override
public void removePreKey(int i) { public void removePreKey(int i) {
omemoStore.removeOmemoPreKey(omemoManager, i); omemoStore.removeOmemoPreKey(getOurDevice(), i);
} }
@Override @Override
public SessionRecord loadSession(SignalProtocolAddress signalProtocolAddress) { public SessionRecord loadSession(SignalProtocolAddress signalProtocolAddress) {
OmemoDevice device;
try { try {
SessionRecord s = omemoStore.loadRawSession(omemoManager, omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress)); device = asOmemoDevice(signalProtocolAddress);
return (s != null ? s : new SessionRecord());
} catch (XmppStringprepException e) { } catch (XmppStringprepException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
SessionRecord record = omemoStore.loadRawSession(getOurDevice(), device);
if (record != null) {
return record;
} else {
return new SessionRecord();
}
} }
@Override @Override
public List<Integer> getSubDeviceSessions(String s) { public List<Integer> getSubDeviceSessions(String s) {
HashMap<Integer, SessionRecord> contactsSessions; BareJid jid;
try { try {
contactsSessions = omemoStore.loadAllRawSessionsOf(omemoManager, JidCreate.bareFrom(s)); jid = JidCreate.bareFrom(s);
} catch (XmppStringprepException e) { } catch (XmppStringprepException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
if (contactsSessions != null) {
return new ArrayList<>(contactsSessions.keySet()); return new ArrayList<>(omemoStore.loadAllRawSessionsOf(getOurDevice(), jid).keySet());
}
return new ArrayList<>();
} }
@Override @Override
public void storeSession(SignalProtocolAddress signalProtocolAddress, SessionRecord sessionRecord) { public void storeSession(SignalProtocolAddress signalProtocolAddress, SessionRecord sessionRecord) {
OmemoDevice device;
try { try {
omemoStore.storeRawSession(omemoManager, omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress), sessionRecord); device = asOmemoDevice(signalProtocolAddress);
} catch (XmppStringprepException e) { } catch (XmppStringprepException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
omemoStore.storeRawSession(getOurDevice(), device, sessionRecord);
} }
@Override @Override
public boolean containsSession(SignalProtocolAddress signalProtocolAddress) { public boolean containsSession(SignalProtocolAddress signalProtocolAddress) {
OmemoDevice device;
try { try {
return omemoStore.containsRawSession(omemoManager, omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress)); device = asOmemoDevice(signalProtocolAddress);
} catch (XmppStringprepException e) { } catch (XmppStringprepException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
return omemoStore.containsRawSession(getOurDevice(), device);
} }
@Override @Override
public void deleteSession(SignalProtocolAddress signalProtocolAddress) { public void deleteSession(SignalProtocolAddress signalProtocolAddress) {
OmemoDevice device;
try { try {
omemoStore.removeRawSession(omemoManager, omemoStore.keyUtil().addressAsOmemoDevice(signalProtocolAddress)); device = asOmemoDevice(signalProtocolAddress);
} catch (XmppStringprepException e) { } catch (XmppStringprepException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
omemoStore.removeRawSession(getOurDevice(), device);
} }
@Override @Override
public void deleteAllSessions(String s) { public void deleteAllSessions(String s) {
BareJid jid;
try { try {
omemoStore.removeAllRawSessionsOf(omemoManager, JidCreate.bareFrom(s)); jid = JidCreate.bareFrom(s);
} catch (XmppStringprepException e) { } catch (XmppStringprepException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
omemoStore.removeAllRawSessionsOf(getOurDevice(), jid);
} }
@Override @Override
public SignedPreKeyRecord loadSignedPreKey(int i) throws InvalidKeyIdException { public SignedPreKeyRecord loadSignedPreKey(int i) throws InvalidKeyIdException {
SignedPreKeyRecord spkr = omemoStore.loadOmemoSignedPreKey(omemoManager, i); SignedPreKeyRecord signedPreKeyRecord = omemoStore.loadOmemoSignedPreKey(getOurDevice(), i);
if (spkr == null) { if (signedPreKeyRecord == null) {
throw new InvalidKeyIdException("No SignedPreKey with Id " + i + " found!"); throw new InvalidKeyIdException("No signed preKey with id " + i + " found.");
} }
return spkr; return signedPreKeyRecord;
} }
@Override @Override
public List<SignedPreKeyRecord> loadSignedPreKeys() { public List<SignedPreKeyRecord> loadSignedPreKeys() {
HashMap<Integer, SignedPreKeyRecord> signedPreKeyRecordHashMap = omemoStore.loadOmemoSignedPreKeys(omemoManager);
List<SignedPreKeyRecord> signedPreKeyRecordList = new ArrayList<>(); TreeMap<Integer, SignedPreKeyRecord> signedPreKeyRecordHashMap =
signedPreKeyRecordList.addAll(signedPreKeyRecordHashMap.values()); omemoStore.loadOmemoSignedPreKeys(getOurDevice());
return signedPreKeyRecordList; return new ArrayList<>(signedPreKeyRecordHashMap.values());
} }
@Override @Override
public void storeSignedPreKey(int i, SignedPreKeyRecord signedPreKeyRecord) { public void storeSignedPreKey(int i, SignedPreKeyRecord signedPreKeyRecord) {
omemoStore.storeOmemoSignedPreKey(omemoManager, i, signedPreKeyRecord); omemoStore.storeOmemoSignedPreKey(getOurDevice(), i, signedPreKeyRecord);
} }
@Override @Override
@ -224,6 +258,14 @@ public class SignalOmemoStoreConnector
@Override @Override
public void removeSignedPreKey(int i) { public void removeSignedPreKey(int i) {
omemoStore.removeOmemoSignedPreKey(omemoManager, i); omemoStore.removeOmemoSignedPreKey(getOurDevice(), i);
}
private static OmemoDevice asOmemoDevice(SignalProtocolAddress address) throws XmppStringprepException {
return new OmemoDevice(JidCreate.bareFrom(address.getName()), address.getDeviceId());
}
public static SignalProtocolAddress asAddress(OmemoDevice device) {
return new SignalProtocolAddress(device.getJid().toString(), device.getDeviceId());
} }
} }

View File

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

View File

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

View File

@ -0,0 +1,93 @@
/**
*
* Copyright 2017 Paul Schaub
*
* This file is part of smack-omemo-signal.
*
* smack-omemo-signal is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jivesoftware.smackx.omemo;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertNotNull;
import static junit.framework.TestCase.assertNotSame;
import static junit.framework.TestCase.assertTrue;
import static junit.framework.TestCase.fail;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.signal.SignalOmemoKeyUtil;
import org.junit.Test;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
/**
* Test SignalOmemoKeyUtil methods.
*
* @author Paul Schaub
*/
public class LegacySignalOmemoKeyUtilTest extends SmackTestSuite {
private final SignalOmemoKeyUtil keyUtil = new SignalOmemoKeyUtil();
@Test
public void omemoIdentityKeyPairSerializationTest() throws CorruptedOmemoKeyException {
IdentityKeyPair ikp = keyUtil.generateOmemoIdentityKeyPair();
byte[] bytes = keyUtil.identityKeyPairToBytes(ikp);
assertNotNull("serialized identityKeyPair must not be null.",
bytes);
assertNotSame("serialized identityKeyPair must not be of length 0.",
0, bytes.length);
IdentityKeyPair ikp2 = keyUtil.identityKeyPairFromBytes(bytes);
assertTrue("Deserialized IdentityKeyPairs PublicKey must equal the originals one.",
ikp.getPublicKey().equals(ikp2.getPublicKey()));
}
@Test
public void omemoIdentityKeySerializationTest() {
IdentityKey k = keyUtil.generateOmemoIdentityKeyPair().getPublicKey();
try {
assertEquals("Deserialized IdentityKey must equal the original one.",
k, keyUtil.identityKeyFromBytes(keyUtil.identityKeyToBytes(k)));
} catch (CorruptedOmemoKeyException e) {
fail("Caught exception while serializing and deserializing identityKey (" + e + "): " + e.getMessage());
}
}
@Test
public void generateOmemoSignedPreKeyTest() {
IdentityKeyPair ikp = keyUtil.generateOmemoIdentityKeyPair();
try {
SignedPreKeyRecord spk = keyUtil.generateOmemoSignedPreKey(ikp, 1);
assertNotNull("SignedPreKey must not be null.", spk);
assertEquals("SignedPreKeyId must match.", 1, spk.getId());
assertEquals("singedPreKeyId must match here also.", 1, keyUtil.signedPreKeyIdFromKey(spk));
} catch (CorruptedOmemoKeyException e) {
fail("Caught an exception while generating signedPreKey (" + e + "): " + e.getMessage());
}
}
@Test
public void getFingerprintTest() {
IdentityKeyPair ikp = keyUtil.generateOmemoIdentityKeyPair();
IdentityKey ik = ikp.getPublicKey();
assertTrue("Length of fingerprint must be 64.",
keyUtil.getFingerprintOfIdentityKey(ik).length() == 64);
}
}

View File

@ -0,0 +1,58 @@
/**
*
* Copyright 2017 Paul Schaub
*
* This file is part of smack-omemo-signal.
*
* smack-omemo-signal is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jivesoftware.smackx.omemo;
import java.util.Arrays;
import java.util.Collection;
import org.jivesoftware.smackx.omemo.signal.SignalOmemoKeyUtil;
import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.state.PreKeyBundle;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
/**
* smack-omemo-signal implementation of {@link OmemoKeyUtilTest}.
* This class executes tests of its super class with available implementations of {@link OmemoKeyUtil}.
* So far this includes {@link SignalOmemoKeyUtil}.
*/
@RunWith(value = Parameterized.class)
public class SignalOmemoKeyUtilTest
extends OmemoKeyUtilTest<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, ECPublicKey, PreKeyBundle> {
public SignalOmemoKeyUtilTest(OmemoKeyUtil<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, ECPublicKey, PreKeyBundle> keyUtil) {
super(keyUtil);
}
@Parameterized.Parameters
public static Collection<Object[]> getParameters() {
return Arrays.asList(new Object[][] {
{ new SignalOmemoKeyUtil()}
});
}
}

View File

@ -18,7 +18,7 @@
* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
package org.jivesoftware.smack.omemo; package org.jivesoftware.smackx.omemo;
import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertFalse;
@ -26,26 +26,11 @@ import static junit.framework.TestCase.assertNotNull;
import static junit.framework.TestCase.assertNotSame; import static junit.framework.TestCase.assertNotSame;
import static junit.framework.TestCase.assertTrue; import static junit.framework.TestCase.assertTrue;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import org.jivesoftware.smack.DummyConnection; import org.jivesoftware.smack.DummyConnection;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.test.util.SmackTestSuite; import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.test.util.TestUtils; 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.element.OmemoElement;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.provider.OmemoVAxolotlProvider; import org.jivesoftware.smackx.omemo.provider.OmemoVAxolotlProvider;
import org.jivesoftware.smackx.omemo.signal.SignalOmemoService; import org.jivesoftware.smackx.omemo.signal.SignalOmemoService;
@ -54,10 +39,10 @@ import org.junit.Test;
/** /**
* Test OmemoManager functionality. * Test OmemoManager functionality.
*/ */
public class OmemoManagerTest extends SmackTestSuite { public class SignalOmemoManagerTest extends SmackTestSuite {
@Test @Test
public void instantiationTest() throws CorruptedOmemoKeyException, NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException, InterruptedException, XMPPException.XMPPErrorException, NoSuchPaddingException, BadPaddingException, InvalidAlgorithmParameterException, NoSuchProviderException, IllegalBlockSizeException, SmackException { public void instantiationTest() {
SignalOmemoService.acknowledgeLicense(); SignalOmemoService.acknowledgeLicense();
SignalOmemoService.setup(); SignalOmemoService.setup();
@ -73,8 +58,8 @@ public class OmemoManagerTest extends SmackTestSuite {
assertNotNull(c); assertNotNull(c);
assertNotNull(d); assertNotNull(d);
assertEquals(123, a.getDeviceId()); assertEquals(Integer.valueOf(123), a.getDeviceId());
assertEquals(234, b.getDeviceId()); assertEquals(Integer.valueOf(234), b.getDeviceId());
assertFalse(a == b); assertFalse(a == b);
assertFalse(a == c); assertFalse(a == c);

View File

@ -18,7 +18,7 @@
* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
package org.jivesoftware.smack.omemo; package org.jivesoftware.smackx.omemo;
import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertTrue; import static junit.framework.TestCase.assertTrue;
@ -41,6 +41,6 @@ public class SignalOmemoStoreConnectorTest {
@Test @Test
public void isTrustedIdentityTest() { public void isTrustedIdentityTest() {
SignalOmemoStoreConnector connector = new SignalOmemoStoreConnector(null, null); SignalOmemoStoreConnector connector = new SignalOmemoStoreConnector(null, null);
assertTrue("All identities must be trusted by default.", connector.isTrustedIdentity(null, null)); assertTrue("All identities must be trusted by default.", connector.isTrustedIdentity(null, null, null));
} }
} }

View File

@ -0,0 +1,83 @@
/**
*
* Copyright 2017 Paul Schaub
*
* This file is part of smack-omemo-signal.
*
* smack-omemo-signal is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jivesoftware.smackx.omemo;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import org.jivesoftware.smackx.omemo.signal.SignalCachingOmemoStore;
import org.jivesoftware.smackx.omemo.signal.SignalFileBasedOmemoStore;
import org.jivesoftware.smackx.omemo.signal.SignalOmemoKeyUtil;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.jxmpp.stringprep.XmppStringprepException;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.SessionCipher;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.state.PreKeyBundle;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
/**
* smack-omemo-signal implementation of {@link OmemoStoreTest}.
* This class executes tests of its super class with available implementations of {@link OmemoStore}.
* So far this includes {@link SignalFileBasedOmemoStore}, {@link SignalCachingOmemoStore}.
*/
@RunWith(value = Parameterized.class)
public class SignalOmemoStoreTest extends OmemoStoreTest<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> {
public SignalOmemoStoreTest(OmemoStore<IdentityKeyPair, IdentityKey, PreKeyRecord, SignedPreKeyRecord, SessionRecord, SignalProtocolAddress, ECPublicKey, PreKeyBundle, SessionCipher> store)
throws XmppStringprepException {
super(store);
}
/**
* We are running this Test with multiple available OmemoStore implementations.
* @return
* @throws IOException
*/
@Parameterized.Parameters
public static Collection<Object[]> getParameters() throws IOException {
TemporaryFolder temp = initStaticTemp();
return Arrays.asList(new Object[][] {
// Simple file based store
{ new SignalFileBasedOmemoStore(temp.newFolder("sigFileBased"))},
// Ephemeral caching store
{ new SignalCachingOmemoStore()},
// Caching file based store
{ new SignalCachingOmemoStore(new SignalFileBasedOmemoStore(temp.newFolder("cachingSigFileBased")))}
});
}
@Test
public void keyUtilTest() {
assertTrue(store.keyUtil() instanceof SignalOmemoKeyUtil);
}
}

View File

@ -9,5 +9,7 @@ dependencies {
compile project(":smack-extensions") compile project(":smack-extensions")
compile project(":smack-experimental") compile project(":smack-experimental")
compile "org.bouncycastle:bcprov-jdk15on:1.59"
testCompile project(path: ":smack-core", configuration: "testRuntime") testCompile project(path: ":smack-core", configuration: "testRuntime")
} }

View File

@ -0,0 +1,446 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import java.util.Date;
import java.util.HashMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil;
import org.jxmpp.jid.BareJid;
/**
* This class implements the Proxy Pattern in order to wrap an OmemoStore with a caching layer.
* This reduces access to the underlying storage layer (eg. database, filesystem) by only accessing it for
* missing/updated values.
*
* Alternatively this implementation can be used as an ephemeral keystore without a persisting backend.
*
* @param <T_IdKeyPair>
* @param <T_IdKey>
* @param <T_PreKey>
* @param <T_SigPreKey>
* @param <T_Sess>
* @param <T_Addr>
* @param <T_ECPub>
* @param <T_Bundle>
* @param <T_Ciph>
*/
public class CachingOmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>
extends OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> {
private final HashMap<OmemoDevice, KeyCache<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess>> caches = new HashMap<>();
private final OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> persistent;
private final OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_ECPub, T_Bundle> keyUtil;
public CachingOmemoStore(OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_ECPub, T_Bundle> keyUtil) {
if (keyUtil == null) {
throw new IllegalArgumentException("KeyUtil MUST NOT be null!");
}
this.keyUtil = keyUtil;
persistent = null;
}
public CachingOmemoStore(OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> wrappedStore) {
if (wrappedStore == null) {
throw new NullPointerException("Wrapped OmemoStore MUST NOT be null!");
}
this.keyUtil = null;
persistent = wrappedStore;
}
@Override
public SortedSet<Integer> localDeviceIdsOf(BareJid localUser) {
if (persistent != null) {
return persistent.localDeviceIdsOf(localUser);
} else {
return new TreeSet<>(); //TODO: ?
}
}
@Override
public T_IdKeyPair loadOmemoIdentityKeyPair(OmemoDevice userDevice)
throws CorruptedOmemoKeyException {
T_IdKeyPair pair = getCache(userDevice).identityKeyPair;
if (pair == null && persistent != null) {
pair = persistent.loadOmemoIdentityKeyPair(userDevice);
if (pair != null) {
getCache(userDevice).identityKeyPair = pair;
}
}
return pair;
}
@Override
public void storeOmemoIdentityKeyPair(OmemoDevice userDevice, T_IdKeyPair identityKeyPair) {
getCache(userDevice).identityKeyPair = identityKeyPair;
if (persistent != null) {
persistent.storeOmemoIdentityKeyPair(userDevice, identityKeyPair);
}
}
@Override
public void removeOmemoIdentityKeyPair(OmemoDevice userDevice) {
getCache(userDevice).identityKeyPair = null;
if (persistent != null) {
persistent.removeOmemoIdentityKeyPair(userDevice);
}
}
@Override
public T_IdKey loadOmemoIdentityKey(OmemoDevice userDevice, OmemoDevice contactsDevice)
throws CorruptedOmemoKeyException {
T_IdKey idKey = getCache(userDevice).identityKeys.get(contactsDevice);
if (idKey == null && persistent != null) {
idKey = persistent.loadOmemoIdentityKey(userDevice, contactsDevice);
if (idKey != null) {
getCache(userDevice).identityKeys.put(contactsDevice, idKey);
}
}
return idKey;
}
@Override
public void storeOmemoIdentityKey(OmemoDevice userDevice, OmemoDevice device, T_IdKey t_idKey) {
getCache(userDevice).identityKeys.put(device, t_idKey);
if (persistent != null) {
persistent.storeOmemoIdentityKey(userDevice, device, t_idKey);
}
}
@Override
public void removeOmemoIdentityKey(OmemoDevice userDevice, OmemoDevice contactsDevice) {
getCache(userDevice).identityKeys.remove(contactsDevice);
if (persistent != null) {
persistent.removeOmemoIdentityKey(userDevice, contactsDevice);
}
}
@Override
public void setDateOfLastReceivedMessage(OmemoDevice userDevice, OmemoDevice from, Date date) {
getCache(userDevice).lastMessagesDates.put(from, date);
if (persistent != null) {
persistent.setDateOfLastReceivedMessage(userDevice, from, date);
}
}
@Override
public Date getDateOfLastReceivedMessage(OmemoDevice userDevice, OmemoDevice from) {
Date last = getCache(userDevice).lastMessagesDates.get(from);
if (last == null && persistent != null) {
last = persistent.getDateOfLastReceivedMessage(userDevice, from);
if (last != null) {
getCache(userDevice).lastMessagesDates.put(from, last);
}
}
return last;
}
@Override
public void setDateOfLastDeviceIdPublication(OmemoDevice userDevice, OmemoDevice contactsDevice, Date date) {
getCache(userDevice).lastDeviceIdPublicationDates.put(contactsDevice, date);
if (persistent != null) {
persistent.setDateOfLastReceivedMessage(userDevice, contactsDevice, date);
}
}
@Override
public Date getDateOfLastDeviceIdPublication(OmemoDevice userDevice, OmemoDevice contactsDevice) {
Date last = getCache(userDevice).lastDeviceIdPublicationDates.get(contactsDevice);
if (last == null && persistent != null) {
last = persistent.getDateOfLastDeviceIdPublication(userDevice, contactsDevice);
if (last != null) {
getCache(userDevice).lastDeviceIdPublicationDates.put(contactsDevice, last);
}
}
return last;
}
@Override
public void setDateOfLastSignedPreKeyRenewal(OmemoDevice userDevice, Date date) {
getCache(userDevice).lastRenewalDate = date;
if (persistent != null) {
persistent.setDateOfLastSignedPreKeyRenewal(userDevice, date);
}
}
@Override
public Date getDateOfLastSignedPreKeyRenewal(OmemoDevice userDevice) {
Date lastRenewal = getCache(userDevice).lastRenewalDate;
if (lastRenewal == null && persistent != null) {
lastRenewal = persistent.getDateOfLastSignedPreKeyRenewal(userDevice);
if (lastRenewal != null) {
getCache(userDevice).lastRenewalDate = lastRenewal;
}
}
return lastRenewal;
}
@Override
public T_PreKey loadOmemoPreKey(OmemoDevice userDevice, int preKeyId) {
T_PreKey preKey = getCache(userDevice).preKeys.get(preKeyId);
if (preKey == null && persistent != null) {
preKey = persistent.loadOmemoPreKey(userDevice, preKeyId);
if (preKey != null) {
getCache(userDevice).preKeys.put(preKeyId, preKey);
}
}
return preKey;
}
@Override
public void storeOmemoPreKey(OmemoDevice userDevice, int preKeyId, T_PreKey t_preKey) {
getCache(userDevice).preKeys.put(preKeyId, t_preKey);
if (persistent != null) {
persistent.storeOmemoPreKey(userDevice, preKeyId, t_preKey);
}
}
@Override
public void removeOmemoPreKey(OmemoDevice userDevice, int preKeyId) {
getCache(userDevice).preKeys.remove(preKeyId);
if (persistent != null) {
persistent.removeOmemoPreKey(userDevice, preKeyId);
}
}
@Override
public TreeMap<Integer, T_PreKey> loadOmemoPreKeys(OmemoDevice userDevice) {
TreeMap<Integer, T_PreKey> preKeys = getCache(userDevice).preKeys;
if (preKeys.isEmpty() && persistent != null) {
preKeys.putAll(persistent.loadOmemoPreKeys(userDevice));
}
return new TreeMap<>(preKeys);
}
@Override
public T_SigPreKey loadOmemoSignedPreKey(OmemoDevice userDevice, int signedPreKeyId) {
T_SigPreKey sigPreKey = getCache(userDevice).signedPreKeys.get(signedPreKeyId);
if (sigPreKey == null && persistent != null) {
sigPreKey = persistent.loadOmemoSignedPreKey(userDevice, signedPreKeyId);
if (sigPreKey != null) {
getCache(userDevice).signedPreKeys.put(signedPreKeyId, sigPreKey);
}
}
return sigPreKey;
}
@Override
public TreeMap<Integer, T_SigPreKey> loadOmemoSignedPreKeys(OmemoDevice userDevice) {
TreeMap<Integer, T_SigPreKey> sigPreKeys = getCache(userDevice).signedPreKeys;
if (sigPreKeys.isEmpty() && persistent != null) {
sigPreKeys.putAll(persistent.loadOmemoSignedPreKeys(userDevice));
}
return new TreeMap<>(sigPreKeys);
}
@Override
public void storeOmemoSignedPreKey(OmemoDevice userDevice,
int signedPreKeyId,
T_SigPreKey signedPreKey) {
getCache(userDevice).signedPreKeys.put(signedPreKeyId, signedPreKey);
if (persistent != null) {
persistent.storeOmemoSignedPreKey(userDevice, signedPreKeyId, signedPreKey);
}
}
@Override
public void removeOmemoSignedPreKey(OmemoDevice userDevice, int signedPreKeyId) {
getCache(userDevice).signedPreKeys.remove(signedPreKeyId);
if (persistent != null) {
persistent.removeOmemoSignedPreKey(userDevice, signedPreKeyId);
}
}
@Override
public T_Sess loadRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice) {
HashMap<Integer, T_Sess> contactSessions = getCache(userDevice).sessions.get(contactsDevice.getJid());
if (contactSessions == null) {
contactSessions = new HashMap<>();
getCache(userDevice).sessions.put(contactsDevice.getJid(), contactSessions);
}
T_Sess session = contactSessions.get(contactsDevice.getDeviceId());
if (session == null && persistent != null) {
session = persistent.loadRawSession(userDevice, contactsDevice);
if (session != null) {
contactSessions.put(contactsDevice.getDeviceId(), session);
}
}
return session;
}
@Override
public HashMap<Integer, T_Sess> loadAllRawSessionsOf(OmemoDevice userDevice, BareJid contact) {
HashMap<Integer, T_Sess> sessions = getCache(userDevice).sessions.get(contact);
if (sessions == null) {
sessions = new HashMap<>();
getCache(userDevice).sessions.put(contact, sessions);
}
if (sessions.isEmpty() && persistent != null) {
sessions.putAll(persistent.loadAllRawSessionsOf(userDevice, contact));
}
return new HashMap<>(sessions);
}
@Override
public void storeRawSession(OmemoDevice userDevice, OmemoDevice contactsDevicece, T_Sess session) {
HashMap<Integer, T_Sess> sessions = getCache(userDevice).sessions.get(contactsDevicece.getJid());
if (sessions == null) {
sessions = new HashMap<>();
getCache(userDevice).sessions.put(contactsDevicece.getJid(), sessions);
}
sessions.put(contactsDevicece.getDeviceId(), session);
if (persistent != null) {
persistent.storeRawSession(userDevice, contactsDevicece, session);
}
}
@Override
public void removeRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice) {
HashMap<Integer, T_Sess> sessions = getCache(userDevice).sessions.get(contactsDevice.getJid());
if (sessions != null) {
sessions.remove(contactsDevice.getDeviceId());
}
if (persistent != null) {
persistent.removeRawSession(userDevice, contactsDevice);
}
}
@Override
public void removeAllRawSessionsOf(OmemoDevice userDevice, BareJid contact) {
getCache(userDevice).sessions.remove(contact);
if (persistent != null) {
persistent.removeAllRawSessionsOf(userDevice, contact);
}
}
@Override
public boolean containsRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice) {
HashMap<Integer, T_Sess> sessions = getCache(userDevice).sessions.get(contactsDevice.getJid());
return (sessions != null && sessions.get(contactsDevice.getDeviceId()) != null) ||
(persistent != null && persistent.containsRawSession(userDevice, contactsDevice));
}
@Override
public OmemoCachedDeviceList loadCachedDeviceList(OmemoDevice userDevice, BareJid contact) {
OmemoCachedDeviceList list = getCache(userDevice).deviceLists.get(contact);
if (list == null && persistent != null) {
list = persistent.loadCachedDeviceList(userDevice, contact);
if (list != null) {
getCache(userDevice).deviceLists.put(contact, list);
}
}
return list == null ? new OmemoCachedDeviceList() : new OmemoCachedDeviceList(list);
}
@Override
public void storeCachedDeviceList(OmemoDevice userDevice,
BareJid contact,
OmemoCachedDeviceList deviceList) {
getCache(userDevice).deviceLists.put(contact, new OmemoCachedDeviceList(deviceList));
if (persistent != null) {
persistent.storeCachedDeviceList(userDevice, contact, deviceList);
}
}
@Override
public void purgeOwnDeviceKeys(OmemoDevice userDevice) {
caches.remove(userDevice);
if (persistent != null) {
persistent.purgeOwnDeviceKeys(userDevice);
}
}
@Override
public OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_ECPub, T_Bundle>
keyUtil() {
if (persistent != null) {
return persistent.keyUtil();
} else {
return keyUtil;
}
}
/**
* Return the {@link KeyCache} object of an {@link OmemoManager}.
* @param device
* @return
*/
private KeyCache<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess> getCache(OmemoDevice device) {
KeyCache<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess> cache = caches.get(device);
if (cache == null) {
cache = new KeyCache<>();
caches.put(device, cache);
}
return cache;
}
/**
* Cache that stores values for an {@link OmemoManager}.
* @param <T_IdKeyPair>
* @param <T_IdKey>
* @param <T_PreKey>
* @param <T_SigPreKey>
* @param <T_Sess>
*/
private static class KeyCache<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess> {
private T_IdKeyPair identityKeyPair;
private final TreeMap<Integer, T_PreKey> preKeys = new TreeMap<>();
private final TreeMap<Integer, T_SigPreKey> signedPreKeys = new TreeMap<>();
private final HashMap<BareJid, HashMap<Integer, T_Sess>> sessions = new HashMap<>();
private final HashMap<OmemoDevice, T_IdKey> identityKeys = new HashMap<>();
private final HashMap<OmemoDevice, Date> lastMessagesDates = new HashMap<>();
private final HashMap<OmemoDevice, Date> lastDeviceIdPublicationDates = new HashMap<>();
private final HashMap<BareJid, OmemoCachedDeviceList> deviceLists = new HashMap<>();
private Date lastRenewalDate = null;
}
}

View File

@ -16,8 +16,6 @@
*/ */
package org.jivesoftware.smackx.omemo; package org.jivesoftware.smackx.omemo;
import java.io.File;
/** /**
* Contains OMEMO related configuration options. * Contains OMEMO related configuration options.
* *
@ -33,37 +31,6 @@ public final class OmemoConfiguration {
private static boolean IGNORE_STALE_DEVICES = true; private static boolean IGNORE_STALE_DEVICES = true;
private static int IGNORE_STALE_DEVICE_AFTER_HOURS = 24 * 7; //One week 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) { public static void setIgnoreStaleDevices(boolean ignore) {
IGNORE_STALE_DEVICES = ignore; IGNORE_STALE_DEVICES = ignore;
} }
@ -83,6 +50,12 @@ public final class OmemoConfiguration {
return IGNORE_STALE_DEVICE_AFTER_HOURS; return IGNORE_STALE_DEVICE_AFTER_HOURS;
} }
/**
* 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
public static void setDeleteStaleDevices(boolean delete) { public static void setDeleteStaleDevices(boolean delete) {
DELETE_STALE_DEVICES = delete; DELETE_STALE_DEVICES = delete;
} }
@ -102,14 +75,39 @@ public final class OmemoConfiguration {
return DELETE_STALE_DEVICE_AFTER_HOURS; return DELETE_STALE_DEVICE_AFTER_HOURS;
} }
/**
* 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;
/**
* Decide, whether signed preKeys are automatically rotated or not.
* It is highly recommended to rotate signed preKeys to preserve forward secrecy.
*
* @param renew automatically rotate signed preKeys?
*/
public static void setRenewOldSignedPreKeys(boolean renew) { public static void setRenewOldSignedPreKeys(boolean renew) {
RENEW_OLD_SIGNED_PREKEYS = renew; RENEW_OLD_SIGNED_PREKEYS = renew;
} }
/**
* Determine, whether signed preKeys are automatically rotated or not.
*
* @return auto-rotate signed preKeys?
*/
public static boolean getRenewOldSignedPreKeys() { public static boolean getRenewOldSignedPreKeys() {
return RENEW_OLD_SIGNED_PREKEYS; return RENEW_OLD_SIGNED_PREKEYS;
} }
/**
* Set the interval in hours, after which the published signed preKey should be renewed.
* This value should be between one or two weeks.
*
* @param hours hours after which signed preKeys should be rotated.
*/
public static void setRenewOldSignedPreKeysAfterHours(int hours) { public static void setRenewOldSignedPreKeysAfterHours(int hours) {
if (hours <= 0) { if (hours <= 0) {
throw new IllegalArgumentException("Hours must be greater than 0."); throw new IllegalArgumentException("Hours must be greater than 0.");
@ -117,10 +115,23 @@ public final class OmemoConfiguration {
RENEW_OLD_SIGNED_PREKEYS_AFTER_HOURS = hours; RENEW_OLD_SIGNED_PREKEYS_AFTER_HOURS = hours;
} }
/**
* Get the interval in hours, after which the published signed preKey should be renewed.
* This value should be between one or two weeks.
*
* @return hours after which signed preKeys should be rotated.
*/
public static int getRenewOldSignedPreKeysAfterHours() { public static int getRenewOldSignedPreKeysAfterHours() {
return RENEW_OLD_SIGNED_PREKEYS_AFTER_HOURS; return RENEW_OLD_SIGNED_PREKEYS_AFTER_HOURS;
} }
/**
* Set the maximum number of signed preKeys that are cached until the oldest one gets deleted.
* This number should not be too small in order to prevent message loss, but also not too big
* to preserve forward secrecy.
*
* @param number number of cached signed preKeys.
*/
public static void setMaxNumberOfStoredSignedPreKeys(int number) { public static void setMaxNumberOfStoredSignedPreKeys(int number) {
if (number <= 0) { if (number <= 0) {
throw new IllegalArgumentException("Number must be greater than 0."); throw new IllegalArgumentException("Number must be greater than 0.");
@ -128,39 +139,79 @@ public final class OmemoConfiguration {
MAX_NUMBER_OF_STORED_SIGNED_PREKEYS = number; MAX_NUMBER_OF_STORED_SIGNED_PREKEYS = number;
} }
/**
* Return the maximum number of signed preKeys that are cached until the oldest one gets deleted.
* @return max number of cached signed preKeys.
*/
public static int getMaxNumberOfStoredSignedPreKeys() { public static int getMaxNumberOfStoredSignedPreKeys() {
return MAX_NUMBER_OF_STORED_SIGNED_PREKEYS; return MAX_NUMBER_OF_STORED_SIGNED_PREKEYS;
} }
/**
* Add a plaintext body hint about omemo encryption to the message.
*/
private static boolean ADD_OMEMO_HINT_BODY = true;
/**
* Decide, whether an OMEMO message should carry a plaintext hint about OMEMO encryption.
* Eg. "I sent you an OMEMO encrypted message..."
*
* @param addHint shall we add a hint?
*/
public static void setAddOmemoHintBody(boolean addHint) { public static void setAddOmemoHintBody(boolean addHint) {
ADD_OMEMO_HINT_BODY = addHint; ADD_OMEMO_HINT_BODY = addHint;
} }
/**
* Determine, whether an OMEMO message should carry a plaintext hint about OMEMO encryption.
*
* @return true, if a hint is added to the message.
*/
public static boolean getAddOmemoHintBody() { public static boolean getAddOmemoHintBody() {
return ADD_OMEMO_HINT_BODY; return ADD_OMEMO_HINT_BODY;
} }
public static void setAddEmeEncryptionHint(boolean addHint) { private static boolean REPAIR_BROKEN_SESSIONS_WITH_PREKEY_MESSAGES = true;
ADD_EME_ENCRYPTION_HINT = addHint;
/**
* Determine, whether incoming messages, which have broken sessions should automatically be answered by an empty
* preKeyMessage in order to establish a new session.
*
* @return true if session should be repaired automatically.
*/
public static boolean getRepairBrokenSessionsWithPreKeyMessages() {
return REPAIR_BROKEN_SESSIONS_WITH_PREKEY_MESSAGES;
} }
public static boolean getAddEmeEncryptionHint() { /**
return ADD_EME_ENCRYPTION_HINT; * Decide, whether incoming messages, which have broken sessions should automatically be answered by an empty
* preKeyMessage in order to establish a new session.
*
* @param repair repair sessions?
*/
public static void setRepairBrokenSessionsWithPrekeyMessages(boolean repair) {
REPAIR_BROKEN_SESSIONS_WITH_PREKEY_MESSAGES = repair;
} }
public static void setAddMAMStorageProcessingHint(boolean addStorageHint) { private static boolean COMPLETE_SESSION_WITH_EMPTY_MESSAGE = true;
ADD_MAM_STORAGE_HINT = addStorageHint;
/**
* Determine, whether incoming preKeyMessages should automatically be answered by an empty message in order to
* complete the session.
*
* @return true if sessions should be completed.
*/
public static boolean getCompleteSessionWithEmptyMessage() {
return COMPLETE_SESSION_WITH_EMPTY_MESSAGE;
} }
public static boolean getAddMAMStorageProcessingHint() { /**
return ADD_MAM_STORAGE_HINT; * Decide, whether incoming preKeyMessages should automatically be answered by an empty message in order to
} * complete the session.
*
public static void setFileBasedOmemoStoreDefaultPath(File path) { * @param complete complete the session or not
FILE_BASED_OMEMO_STORE_DEFAULT_PATH = path; */
} public static void setCompleteSessionWithEmptyMessage(boolean complete) {
COMPLETE_SESSION_WITH_EMPTY_MESSAGE = complete;
public static File getFileBasedOmemoStoreDefaultPath() {
return FILE_BASED_OMEMO_STORE_DEFAULT_PATH;
} }
} }

View File

@ -0,0 +1,213 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.BODY_OMEMO_HINT;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.eme.element.ExplicitMessageEncryptionElement;
import org.jivesoftware.smackx.hints.element.StoreHint;
import org.jivesoftware.smackx.omemo.element.OmemoElement;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
import org.jxmpp.jid.Jid;
public class OmemoMessage {
private final OmemoElement element;
private final byte[] messageKey, iv;
OmemoMessage(OmemoElement element, byte[] key, byte[] iv) {
this.element = element;
this.messageKey = key;
this.iv = iv;
}
/**
* Return the original OmemoElement (&lt;encrypted/&gt;).
*
* @return omemoElement
*/
public OmemoElement getElement() {
return element;
}
/**
* Return the messageKey (or transported key in case of a KeyTransportMessage).
*
* @return key
*/
public byte[] getKey() {
return messageKey.clone();
}
/**
* Return the initialization vector belonging to the key.
* @return initialization vector
*/
public byte[] getIv() {
return iv.clone();
}
/**
* Outgoing OMEMO message.
*/
public static class Sent extends OmemoMessage {
private final Set<OmemoDevice> intendedDevices = new HashSet<>();
private final HashMap<OmemoDevice, Throwable> skippedDevices = new HashMap<>();
/**
* Create a new outgoing OMEMO message.
* @param element OmemoElement
* @param key messageKey (or transported key)
* @param iv initialization vector belonging to key
* @param intendedDevices devices the client intended to encrypt the message for
* @param skippedDevices devices which were skipped during encryption process because encryption
* failed for some reason
*/
Sent(OmemoElement element, byte[] key, byte[] iv, Set<OmemoDevice> intendedDevices, HashMap<OmemoDevice, Throwable> skippedDevices) {
super(element, key, iv);
this.intendedDevices.addAll(intendedDevices);
this.skippedDevices.putAll(skippedDevices);
}
/**
* Return a list of all devices the sender originally intended to encrypt the message for.
* @return list of intended recipients.
*/
public Set<OmemoDevice> getIntendedDevices() {
return intendedDevices;
}
/**
* Return a map of all skipped recipients and the reasons for skipping.
* @return map of skipped recipients and reasons for that.
*/
public HashMap<OmemoDevice, Throwable> getSkippedDevices() {
return skippedDevices;
}
/**
* Determine, if some recipients were skipped during encryption.
* @return true if recipients were skipped.
*/
public boolean isMissingRecipients() {
return !getSkippedDevices().isEmpty();
}
/**
* Return the OmemoElement wrapped in a Message ready to be sent.
* The message is addressed to recipient, contains the OmemoElement
* as well as an optional clear text hint as body, a MAM storage hint
* and an EME hint about OMEMO encryption.
*
* @param recipient recipient for the to-field of the message.
* @return Message
*/
public Message asMessage(Jid recipient) {
Message messageStanza = new Message();
messageStanza.setTo(recipient);
messageStanza.addExtension(getElement());
if (OmemoConfiguration.getAddOmemoHintBody()) {
messageStanza.setBody(BODY_OMEMO_HINT);
}
StoreHint.set(messageStanza);
messageStanza.addExtension(new ExplicitMessageEncryptionElement(OMEMO_NAMESPACE_V_AXOLOTL, OMEMO));
return messageStanza;
}
}
/**
* Incoming OMEMO message.
*/
public static class Received extends OmemoMessage {
private final String message;
private final OmemoFingerprint sendersFingerprint;
private final OmemoDevice senderDevice;
private final boolean preKeyMessage;
/**
* Create a new incoming OMEMO message.
* @param element original OmemoElement
* @param key message key (or transported key)
* @param iv respective initialization vector
* @param body decrypted body
* @param sendersFingerprint OmemoFingerprint of the senders identityKey
* @param senderDevice OmemoDevice of the sender
* @param preKeyMessage if this was a preKeyMessage or not
*/
Received(OmemoElement element, byte[] key, byte[] iv, String body, OmemoFingerprint sendersFingerprint, OmemoDevice senderDevice, boolean preKeyMessage) {
super(element, key, iv);
this.message = body;
this.sendersFingerprint = sendersFingerprint;
this.senderDevice = senderDevice;
this.preKeyMessage = preKeyMessage;
}
/**
* Return the decrypted body of the message.
* @return decrypted body
*/
public String getBody() {
return message;
}
/**
* Return the fingerprint of the messages sender device.
* @return fingerprint of sender
*/
public OmemoFingerprint getSendersFingerprint() {
return sendersFingerprint;
}
/**
* Return the OmemoDevice which sent the message.
* @return senderDevice
*/
public OmemoDevice getSenderDevice() {
return senderDevice;
}
/**
* Return true, if this message was sent as a preKeyMessage.
* @return preKeyMessage or not
*/
boolean isPreKeyMessage() {
return preKeyMessage;
}
/**
* Return true, if the message was a KeyTransportMessage.
* A KeyTransportMessage is a OmemoMessage without a payload.
* @return keyTransportMessage?
*/
public boolean isKeyTransportMessage() {
return message == null;
}
}
}

View File

@ -0,0 +1,197 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.omemo.element.OmemoElement;
import org.jivesoftware.smackx.omemo.element.OmemoKeyElement;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
import org.jivesoftware.smackx.omemo.exceptions.MultipleCryptoFailedException;
import org.jivesoftware.smackx.omemo.exceptions.NoRawSessionException;
import org.jivesoftware.smackx.omemo.exceptions.UntrustedOmemoIdentityException;
import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag;
import org.jivesoftware.smackx.omemo.internal.CiphertextTuple;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
public abstract class OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> {
private static final Logger LOGGER = Logger.getLogger(OmemoRatchet.class.getName());
protected final OmemoManager omemoManager;
protected final OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> store;
/**
* Constructor.
*
* @param omemoManager omemoManager
* @param store omemoStore
*/
public OmemoRatchet(OmemoManager omemoManager,
OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> store) {
this.omemoManager = omemoManager;
this.store = store;
}
/**
* Decrypt a double-ratchet-encrypted message key.
*
* @param sender sender of the message.
* @param encryptedKey key encrypted with the ratchet of the sender.
* @return decrypted message key.
*
* @throws CorruptedOmemoKeyException
* @throws NoRawSessionException when no double ratchet session was found.
* @throws CryptoFailedException
* @throws UntrustedOmemoIdentityException
*/
public abstract byte[] doubleRatchetDecrypt(OmemoDevice sender, byte[] encryptedKey)
throws CorruptedOmemoKeyException, NoRawSessionException, CryptoFailedException,
UntrustedOmemoIdentityException;
/**
* Encrypt a messageKey with the double ratchet session of the recipient.
*
* @param recipient recipient of the message.
* @param messageKey key we want to encrypt.
* @return encrypted message key.
*/
public abstract CiphertextTuple doubleRatchetEncrypt(OmemoDevice recipient, byte[] messageKey);
/**
* Try to decrypt the transported message key using the double ratchet session.
*
* @param element omemoElement
* @return tuple of cipher generated from the unpacked message key and the auth-tag
* @throws CryptoFailedException if decryption using the double ratchet fails
* @throws NoRawSessionException if we have no session, but the element was NOT a PreKeyMessage
*/
CipherAndAuthTag retrieveMessageKeyAndAuthTag(OmemoDevice sender, OmemoElement element) throws CryptoFailedException,
NoRawSessionException {
int keyId = omemoManager.getDeviceId();
byte[] unpackedKey = null;
List<CryptoFailedException> decryptExceptions = new ArrayList<>();
List<OmemoKeyElement> keys = element.getHeader().getKeys();
boolean preKey = false;
// Find key with our ID.
for (OmemoKeyElement k : keys) {
if (k.getId() == keyId) {
try {
unpackedKey = doubleRatchetDecrypt(sender, k.getData());
preKey = k.isPreKey();
break;
} catch (CryptoFailedException e) {
// There might be multiple keys with our id, but we can only decrypt one.
// So we can't throw the exception, when decrypting the first duplicate which is not for us.
decryptExceptions.add(e);
} catch (CorruptedOmemoKeyException e) {
decryptExceptions.add(new CryptoFailedException(e));
} catch (UntrustedOmemoIdentityException e) {
LOGGER.log(Level.WARNING, "Received message from " + sender + " contained unknown identityKey. Ignore message.", e);
}
}
}
if (unpackedKey == null) {
if (!decryptExceptions.isEmpty()) {
throw MultipleCryptoFailedException.from(decryptExceptions);
}
throw new CryptoFailedException("Transported key could not be decrypted, since no suitable message key " +
"was provided. Provides keys: " + keys);
}
// Split in AES auth-tag and key
byte[] messageKey = new byte[16];
byte[] authTag = null;
if (unpackedKey.length == 32) {
authTag = new byte[16];
// copy key part into messageKey
System.arraycopy(unpackedKey, 0, messageKey, 0, 16);
// copy tag part into authTag
System.arraycopy(unpackedKey, 16, authTag, 0,16);
} else if (element.isKeyTransportElement() && unpackedKey.length == 16) {
messageKey = unpackedKey;
} else {
throw new CryptoFailedException("MessageKey has wrong length: "
+ unpackedKey.length + ". Probably legacy auth tag format.");
}
return new CipherAndAuthTag(messageKey, element.getHeader().getIv(), authTag, preKey);
}
/**
* Use the symmetric key in cipherAndAuthTag to decrypt the payload of the omemoMessage.
* The decrypted payload will be the body of the returned Message.
*
* @param element omemoElement containing a payload.
* @param cipherAndAuthTag cipher and authentication tag.
* @return decrypted plain text.
* @throws CryptoFailedException if decryption using AES key fails.
*/
static String decryptMessageElement(OmemoElement element, CipherAndAuthTag cipherAndAuthTag)
throws CryptoFailedException {
if (!element.isMessageElement()) {
throw new IllegalArgumentException("decryptMessageElement cannot decrypt OmemoElement which is no MessageElement!");
}
if (cipherAndAuthTag.getAuthTag() == null || cipherAndAuthTag.getAuthTag().length != 16) {
throw new CryptoFailedException("AuthenticationTag is null or has wrong length: "
+ (cipherAndAuthTag.getAuthTag() == null ? "null" : cipherAndAuthTag.getAuthTag().length));
}
byte[] encryptedBody = payloadAndAuthTag(element, cipherAndAuthTag.getAuthTag());
try {
String plaintext = new String(cipherAndAuthTag.getCipher().doFinal(encryptedBody), StringUtils.UTF8);
return plaintext;
} catch (UnsupportedEncodingException | IllegalBlockSizeException | BadPaddingException e) {
throw new CryptoFailedException("decryptMessageElement could not decipher message body: "
+ e.getMessage());
}
}
/**
* Return the concatenation of the payload of the OmemoElement and the given auth tag.
*
* @param element omemoElement (message element)
* @param authTag authTag
* @return payload + authTag
*/
static byte[] payloadAndAuthTag(OmemoElement element, byte[] authTag) {
if (!element.isMessageElement()) {
throw new IllegalArgumentException("OmemoElement has no payload.");
}
byte[] payload = new byte[element.getPayload().length + authTag.length];
System.arraycopy(element.getPayload(), 0, payload, 0, element.getPayload().length);
System.arraycopy(authTag, 0, payload, element.getPayload().length, authTag.length);
return payload;
}
}

View File

@ -16,12 +16,15 @@
*/ */
package org.jivesoftware.smackx.omemo.element; package org.jivesoftware.smackx.omemo.element;
import java.util.HashMap;
import java.util.Map;
import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smack.util.stringencoder.Base64;
/** /**
* Class that represents an OMEMO Bundle element. * Class that represents an OMEMO Bundle element.
* TODO: Move functionality to here.
* *
* @author Paul Schaub * @author Paul Schaub
*/ */
@ -36,8 +39,173 @@ public abstract class OmemoBundleElement implements ExtensionElement {
public static final String PRE_KEY_PUB = "preKeyPublic"; public static final String PRE_KEY_PUB = "preKeyPublic";
public static final String PRE_KEY_ID = "preKeyId"; public static final String PRE_KEY_ID = "preKeyId";
private final int signedPreKeyId;
private final String signedPreKeyB64;
private byte[] signedPreKey;
private final String signedPreKeySignatureB64;
private byte[] signedPreKeySignature;
private final String identityKeyB64;
private byte[] identityKey;
private final HashMap<Integer, String> preKeysB64;
private HashMap<Integer, byte[]> preKeys;
/**
* Constructor to create a Bundle Element from base64 Strings.
*
* @param signedPreKeyId id
* @param signedPreKeyB64 base64 encoded signedPreKey
* @param signedPreKeySigB64 base64 encoded signedPreKeySignature
* @param identityKeyB64 base64 encoded identityKey
* @param preKeysB64 HashMap of base64 encoded preKeys
*/
public OmemoBundleElement(int signedPreKeyId, String signedPreKeyB64, String signedPreKeySigB64, String identityKeyB64, HashMap<Integer, String> preKeysB64) {
this.signedPreKeyId = signedPreKeyId;
this.signedPreKeyB64 = signedPreKeyB64;
this.signedPreKeySignatureB64 = signedPreKeySigB64;
this.identityKeyB64 = identityKeyB64;
this.preKeysB64 = preKeysB64;
}
/**
* Constructor to create a Bundle Element from decoded byte arrays.
*
* @param signedPreKeyId id
* @param signedPreKey signedPreKey
* @param signedPreKeySig signedPreKeySignature
* @param identityKey identityKey
* @param preKeys HashMap of preKeys
*/
public OmemoBundleElement(int signedPreKeyId, byte[] signedPreKey, byte[] signedPreKeySig, byte[] identityKey, HashMap<Integer, byte[]> preKeys) {
this.signedPreKeyId = signedPreKeyId;
this.signedPreKey = signedPreKey;
this.signedPreKeyB64 = Base64.encodeToString(signedPreKey);
this.signedPreKeySignature = signedPreKeySig;
this.signedPreKeySignatureB64 = Base64.encodeToString(signedPreKeySignature);
this.identityKey = identityKey;
this.identityKeyB64 = Base64.encodeToString(identityKey);
this.preKeys = preKeys;
this.preKeysB64 = new HashMap<>();
for (int id : preKeys.keySet()) {
preKeysB64.put(id, Base64.encodeToString(preKeys.get(id)));
}
}
/**
* Return the signedPreKey of the OmemoBundleElement.
*
* @return signedPreKey as byte array
*/
public byte[] getSignedPreKey() {
if (signedPreKey == null) {
signedPreKey = Base64.decode(signedPreKeyB64);
}
return this.signedPreKey.clone();
}
/**
* Return the id of the signedPreKey in the bundle.
*
* @return id of signedPreKey
*/
public int getSignedPreKeyId() {
return this.signedPreKeyId;
}
/**
* Get the signature of the signedPreKey.
*
* @return signature as byte array
*/
public byte[] getSignedPreKeySignature() {
if (signedPreKeySignature == null) {
signedPreKeySignature = Base64.decode(signedPreKeySignatureB64);
}
return signedPreKeySignature.clone();
}
/**
* Return the public identityKey of the bundles owner.
* This can be used to check the signedPreKeys signature.
* The fingerprint of this key is, what the user has to verify.
*
* @return public identityKey as byte array
*/
public byte[] getIdentityKey() {
if (identityKey == null) {
identityKey = Base64.decode(identityKeyB64);
}
return this.identityKey.clone();
}
/**
* Return the HashMap of preKeys in the bundle.
* The map uses the preKeys ids as key and the preKeys as value.
*
* @return preKeys
*/
public HashMap<Integer, byte[]> getPreKeys() {
if (preKeys == null) {
preKeys = new HashMap<>();
for (int id : preKeysB64.keySet()) {
preKeys.put(id, Base64.decode(preKeysB64.get(id)));
}
}
return this.preKeys;
}
/**
* Return a single preKey from the map.
*
* @param id id of the preKey
* @return the preKey
*/
public byte[] getPreKey(int id) {
return getPreKeys().get(id);
}
@Override @Override
public abstract XmlStringBuilder toXML(String enclosingNamespace); public String getElementName() {
return BUNDLE;
}
@Override
public XmlStringBuilder toXML(String enclosingNamespace) {
XmlStringBuilder sb = new XmlStringBuilder(this, enclosingNamespace).rightAngleBracket();
sb.halfOpenElement(SIGNED_PRE_KEY_PUB).attribute(SIGNED_PRE_KEY_ID, signedPreKeyId).rightAngleBracket()
.append(signedPreKeyB64).closeElement(SIGNED_PRE_KEY_PUB);
sb.openElement(SIGNED_PRE_KEY_SIG).append(signedPreKeySignatureB64).closeElement(SIGNED_PRE_KEY_SIG);
sb.openElement(IDENTITY_KEY).append(identityKeyB64).closeElement(IDENTITY_KEY);
sb.openElement(PRE_KEYS);
for (Map.Entry<Integer, String> p : this.preKeysB64.entrySet()) {
sb.halfOpenElement(PRE_KEY_PUB).attribute(PRE_KEY_ID, p.getKey()).rightAngleBracket()
.append(p.getValue()).closeElement(PRE_KEY_PUB);
}
sb.closeElement(PRE_KEYS);
sb.closeElement(this);
return sb;
}
@Override
public String toString() {
String out = "OmemoBundleElement[\n";
out += SIGNED_PRE_KEY_PUB + " " + SIGNED_PRE_KEY_ID + "=" + signedPreKeyId + ": " + signedPreKeyB64 + "\n";
out += SIGNED_PRE_KEY_SIG + ": " + signedPreKeySignatureB64 + "\n";
out += IDENTITY_KEY + ": " + identityKeyB64 + "\n";
out += PRE_KEYS + " (" + preKeysB64.size() + ")\n";
for (Map.Entry<Integer, String> e : preKeysB64.entrySet()) {
out += PRE_KEY_PUB + " " + PRE_KEY_ID + "=" + e.getKey() + ": " + e.getValue() + "\n";
}
return out;
}
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {

View File

@ -0,0 +1,43 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.element;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL;
import java.util.HashMap;
/**
* OMEMO device bundle as described here:
* https://xmpp.org/extensions/xep-0384.html#usecases-announcing (Example 3).
*
* @author Paul Schaub
*/
public class OmemoBundleElement_VAxolotl extends OmemoBundleElement {
public OmemoBundleElement_VAxolotl(int signedPreKeyId, String signedPreKeyB64, String signedPreKeySigB64, String identityKeyB64, HashMap<Integer, String> preKeysB64) {
super(signedPreKeyId, signedPreKeyB64, signedPreKeySigB64, identityKeyB64, preKeysB64);
}
public OmemoBundleElement_VAxolotl(int signedPreKeyId, byte[] signedPreKey, byte[] signedPreKeySig, byte[] identityKey, HashMap<Integer, byte[]> preKeys) {
super(signedPreKeyId, signedPreKey, signedPreKeySig, identityKey, preKeys);
}
@Override
public String getNamespace() {
return OMEMO_NAMESPACE_V_AXOLOTL;
}
}

View File

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

View File

@ -23,6 +23,7 @@ import java.util.Set;
import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList;
/** /**
* A OMEMO device list update containing the IDs of all active devices of a contact. * A OMEMO device list update containing the IDs of all active devices of a contact.
@ -45,6 +46,10 @@ public abstract class OmemoDeviceListElement implements ExtensionElement {
this.deviceIds = Collections.unmodifiableSet(deviceIds); this.deviceIds = Collections.unmodifiableSet(deviceIds);
} }
public OmemoDeviceListElement(OmemoCachedDeviceList cachedList) {
this.deviceIds = Collections.unmodifiableSet(cachedList.getActiveDevices());
}
public Set<Integer> getDeviceIds() { public Set<Integer> getDeviceIds() {
return deviceIds; return deviceIds;
} }

View File

@ -20,17 +20,23 @@ import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_
import java.util.Set; import java.util.Set;
import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList;
/** /**
* The OMEMO device list element with the legacy Axolotl namespace. * The OMEMO device list element with the legacy Axolotl namespace.
* *
* @author Paul Schaub * @author Paul Schaub
*/ */
public class OmemoDeviceListVAxolotlElement extends OmemoDeviceListElement { public class OmemoDeviceListElement_VAxolotl extends OmemoDeviceListElement {
public OmemoDeviceListVAxolotlElement(Set<Integer> deviceIds) { public OmemoDeviceListElement_VAxolotl(Set<Integer> deviceIds) {
super(deviceIds); super(deviceIds);
} }
public OmemoDeviceListElement_VAxolotl(OmemoCachedDeviceList cachedList) {
super(cachedList);
}
@Override @Override
public String getNamespace() { public String getNamespace() {
return OMEMO_NAMESPACE_V_AXOLOTL; return OMEMO_NAMESPACE_V_AXOLOTL;

View File

@ -16,17 +16,13 @@
*/ */
package org.jivesoftware.smackx.omemo.element; package org.jivesoftware.smackx.omemo.element;
import java.util.ArrayList;
import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.NamedElement;
import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smack.util.stringencoder.Base64; import org.jivesoftware.smack.util.stringencoder.Base64;
/** /**
* Class that represents a OmemoElement. * Class that represents an OmemoElement.
* TODO: Move functionality here.
* *
* @author Paul Schaub * @author Paul Schaub
*/ */
@ -35,17 +31,11 @@ public abstract class OmemoElement implements ExtensionElement {
public static final int TYPE_OMEMO_PREKEY_MESSAGE = 1; public static final int TYPE_OMEMO_PREKEY_MESSAGE = 1;
public static final int TYPE_OMEMO_MESSAGE = 0; public static final int TYPE_OMEMO_MESSAGE = 0;
public static final String ENCRYPTED = "encrypted"; public static final String NAME_ENCRYPTED = "encrypted";
public static final String HEADER = "header"; public static final String ATTR_PAYLOAD = "payload";
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; private final OmemoHeaderElement header;
protected final byte[] payload; private final byte[] payload;
/** /**
* Create a new OmemoMessageElement from a header and a payload. * Create a new OmemoMessageElement from a header and a payload.
@ -53,12 +43,12 @@ public abstract class OmemoElement implements ExtensionElement {
* @param header header of the message * @param header header of the message
* @param payload payload * @param payload payload
*/ */
public OmemoElement(OmemoElement.OmemoHeader header, byte[] payload) { public OmemoElement(OmemoHeaderElement header, byte[] payload) {
this.header = Objects.requireNonNull(header); this.header = Objects.requireNonNull(header);
this.payload = payload; this.payload = payload;
} }
public OmemoElement.OmemoHeader getHeader() { public OmemoHeaderElement getHeader() {
return header; return header;
} }
@ -82,113 +72,22 @@ public abstract class OmemoElement implements ExtensionElement {
return payload != null; return payload != null;
} }
/** @Override
* Header element of the message. The header contains information about the sender and the encrypted keys for public XmlStringBuilder toXML(String enclosingNamespace) {
* the recipients, as well as the iv element for AES. XmlStringBuilder sb = new XmlStringBuilder(this, enclosingNamespace).rightAngleBracket();
*/
public static class OmemoHeader implements NamedElement {
private final int sid;
private final ArrayList<Key> keys;
private final byte[] iv;
public OmemoHeader(int sid, ArrayList<OmemoHeader.Key> keys, byte[] iv) { sb.element(header);
this.sid = sid;
this.keys = keys; if (payload != null) {
this.iv = iv; sb.openElement(ATTR_PAYLOAD).append(Base64.encodeToString(payload)).closeElement(ATTR_PAYLOAD);
} }
/** sb.closeElement(this);
* Return the deviceId of the sender of the message. return sb;
* }
* @return senders id
*/
public int getSid() {
return sid;
}
public ArrayList<OmemoHeader.Key> getKeys() { @Override
return new ArrayList<>(keys); public String getElementName() {
} return NAME_ENCRYPTED;
public byte[] getIv() {
return iv != null ? iv.clone() : null;
}
@Override
public String getElementName() {
return HEADER;
}
@Override
public CharSequence toXML(String enclosingNamespace) {
XmlStringBuilder sb = new XmlStringBuilder(this);
sb.attribute(SID, getSid()).rightAngleBracket();
for (OmemoHeader.Key k : getKeys()) {
sb.element(k);
}
sb.openElement(IV).append(Base64.encodeToString(getIv())).closeElement(IV);
return sb.closeElement(this);
}
/**
* Small class to collect key (byte[]), its id and whether its a prekey or not.
*/
public static class Key implements NamedElement {
final byte[] data;
final int id;
final boolean preKey;
public Key(byte[] data, int id) {
this.data = data;
this.id = id;
this.preKey = false;
}
public Key(byte[] data, int id, boolean preKey) {
this.data = data;
this.id = id;
this.preKey = preKey;
}
public int getId() {
return this.id;
}
public byte[] getData() {
return this.data;
}
public boolean isPreKey() {
return this.preKey;
}
@Override
public String toString() {
return Integer.toString(id);
}
@Override
public String getElementName() {
return KEY;
}
@Override
public CharSequence toXML(String enclosingNamespace) {
XmlStringBuilder sb = new XmlStringBuilder(this);
if (isPreKey()) {
sb.attribute(PREKEY, true);
}
sb.attribute(RID, getId());
sb.rightAngleBracket();
sb.append(Base64.encodeToString(getData()));
sb.closeElement(this);
return sb;
}
}
} }
} }

View File

@ -0,0 +1,44 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.element;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL;
/**
* An OMEMO (PreKey)WhisperMessage element.
*
* @author Paul Schaub
*/
public class OmemoElement_VAxolotl extends OmemoElement {
public static final String NAMESPACE = OMEMO_NAMESPACE_V_AXOLOTL;
/**
* Create a new OmemoMessageElement from a header and a payload.
*
* @param header header of the message
* @param payload payload
*/
public OmemoElement_VAxolotl(OmemoHeaderElement_VAxolotl header, byte[] payload) {
super(header, payload);
}
@Override
public String getNamespace() {
return NAMESPACE;
}
}

View File

@ -0,0 +1,83 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.element;
import java.util.ArrayList;
import java.util.List;
import org.jivesoftware.smack.packet.NamedElement;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smack.util.stringencoder.Base64;
/**
* Header element of the message. The header contains information about the sender and the encrypted keys for
* the recipients, as well as the iv element for AES.
*/
public abstract class OmemoHeaderElement implements NamedElement {
public static final String NAME_HEADER = "header";
public static final String ATTR_SID = "sid";
public static final String ATTR_IV = "iv";
private final int sid;
private final List<OmemoKeyElement> keys;
private final byte[] iv;
public OmemoHeaderElement(int sid, List<OmemoKeyElement> 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<OmemoKeyElement> getKeys() {
return new ArrayList<>(keys);
}
public byte[] getIv() {
return iv != null ? iv.clone() : null;
}
@Override
public String getElementName() {
return NAME_HEADER;
}
@Override
public CharSequence toXML(String enclosingNamespace) {
XmlStringBuilder sb = new XmlStringBuilder(this);
sb.attribute(ATTR_SID, getSid()).rightAngleBracket();
for (OmemoKeyElement k : getKeys()) {
sb.element(k);
}
sb.openElement(ATTR_IV).append(Base64.encodeToString(getIv())).closeElement(ATTR_IV);
return sb.closeElement(this);
}
}

View File

@ -0,0 +1,27 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.element;
import java.util.List;
public class OmemoHeaderElement_VAxolotl extends OmemoHeaderElement {
public OmemoHeaderElement_VAxolotl(int sid, List<OmemoKeyElement> keys, byte[] iv) {
super(sid, keys, iv);
}
}

View File

@ -0,0 +1,84 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.element;
import org.jivesoftware.smack.packet.NamedElement;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smack.util.stringencoder.Base64;
/**
* Small class to collect key (byte[]), its id and whether its a preKey or not.
*/
public class OmemoKeyElement implements NamedElement {
public static final String NAME_KEY = "key";
public static final String ATTR_RID = "rid";
public static final String ATTR_PREKEY = "prekey";
private final byte[] data;
private final int id;
private final boolean preKey;
public OmemoKeyElement(byte[] data, int id) {
this.data = data;
this.id = id;
this.preKey = false;
}
public OmemoKeyElement(byte[] data, int id, boolean preKey) {
this.data = data;
this.id = id;
this.preKey = preKey;
}
public int getId() {
return this.id;
}
public byte[] getData() {
return this.data;
}
public boolean isPreKey() {
return this.preKey;
}
@Override
public String toString() {
return Integer.toString(id);
}
@Override
public String getElementName() {
return NAME_KEY;
}
@Override
public CharSequence toXML(String enclosingNamespace) {
XmlStringBuilder sb = new XmlStringBuilder(this);
if (isPreKey()) {
sb.attribute(ATTR_PREKEY, true);
}
sb.attribute(ATTR_RID, getId());
sb.rightAngleBracket();
sb.append(Base64.encodeToString(getData()));
sb.closeElement(this);
return sb;
}
}

View File

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

View File

@ -16,6 +16,10 @@
*/ */
package org.jivesoftware.smackx.omemo.exceptions; package org.jivesoftware.smackx.omemo.exceptions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** /**
* Exception gets thrown when some cryptographic function failed. * Exception gets thrown when some cryptographic function failed.
* *
@ -25,11 +29,18 @@ public class CryptoFailedException extends Exception {
private static final long serialVersionUID = 3466888654338119924L; private static final long serialVersionUID = 3466888654338119924L;
private final ArrayList<Exception> exceptions = new ArrayList<>();
public CryptoFailedException(String message) { public CryptoFailedException(String message) {
super(message); super(message);
} }
public CryptoFailedException(Exception e) { public CryptoFailedException(Exception e) {
super(e); super(e);
exceptions.add(e);
}
public List<Exception> getExceptions() {
return Collections.unmodifiableList(exceptions);
} }
} }

View File

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

View File

@ -16,6 +16,8 @@
*/ */
package org.jivesoftware.smackx.omemo.exceptions; package org.jivesoftware.smackx.omemo.exceptions;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
/** /**
* Exception that gets thrown whenever a OmemoMessage arrives that no OmemoSession was found for to decrypt it. * Exception that gets thrown whenever a OmemoMessage arrives that no OmemoSession was found for to decrypt it.
* *
@ -25,7 +27,14 @@ public class NoRawSessionException extends Exception {
private static final long serialVersionUID = 3466888654338119954L; private static final long serialVersionUID = 3466888654338119954L;
public NoRawSessionException(Exception e) { private final OmemoDevice device;
public NoRawSessionException(OmemoDevice device, Exception e) {
super(e); super(e);
this.device = device;
}
public OmemoDevice getDeviceWithoutSession() {
return device;
} }
} }

View File

@ -0,0 +1,66 @@
/**
*
* Copyright 2018 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.exceptions;
import java.util.Date;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
public class StaleDeviceException extends Exception {
private static final long serialVersionUID = 1L;
private final OmemoDevice device;
private final Date lastMessageDate;
private final Date lastDeviceIdPublication;
/**
* This exception gets thrown if a message cannot be encrypted for a device due to the device being inactive for too long (stale).
*
* @param device OmemoDevice.
* @param lastMessageDate
* @param lastDeviceIdPublicationDate
*/
public StaleDeviceException(OmemoDevice device, Date lastMessageDate, Date lastDeviceIdPublicationDate) {
this.device = device;
this.lastMessageDate = lastMessageDate;
this.lastDeviceIdPublication = lastDeviceIdPublicationDate;
}
/**
* Return the date on which the last OMEMO message sent from the device was received.
* @return last messages date
*/
public Date getLastMessageDate() {
return lastMessageDate;
}
/**
* Return the date of the last time the deviceId was republished after being inactive/non-existent before.
* @return date of last deviceId (re)publication.
*/
public Date getLastDeviceIdPublicationDate() {
return lastDeviceIdPublication;
}
/**
* Return the stale OMEMO device.
* @return stale device
*/
public OmemoDevice getDevice() {
return device;
}
}

View File

@ -16,6 +16,7 @@
*/ */
package org.jivesoftware.smackx.omemo.exceptions; package org.jivesoftware.smackx.omemo.exceptions;
import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
@ -34,6 +35,11 @@ public class UndecidedOmemoIdentityException extends Exception {
this.devices.add(contact); this.devices.add(contact);
} }
public UndecidedOmemoIdentityException(Collection<OmemoDevice> devices) {
super();
this.devices.addAll(devices);
}
/** /**
* Return the HashSet of undecided devices. * Return the HashSet of undecided devices.
* *

View File

@ -0,0 +1,93 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.exceptions;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
/**
* Exception that gets thrown when we try to en-/decrypt a message for an untrusted contact.
* This might either be because the user actively untrusted a device, or we receive a message from a contact
* which contains an identityKey that differs from the one the user trusted.
*/
public class UntrustedOmemoIdentityException extends Exception {
private static final long serialVersionUID = 1L;
private final OmemoDevice device;
private final OmemoFingerprint trustedKey, untrustedKey;
/**
* Constructor for when we receive a message with an identityKey different from the one we trusted.
*
* @param device device which sent the message.
* @param fpTrusted fingerprint of the identityKey we previously had and trusted.
* @param fpUntrusted fingerprint of the new key which is untrusted.
*/
public UntrustedOmemoIdentityException(OmemoDevice device, OmemoFingerprint fpTrusted, OmemoFingerprint fpUntrusted) {
super();
this.device = device;
this.trustedKey = fpTrusted;
this.untrustedKey = fpUntrusted;
}
/**
* Constructor for when encryption fails because the user untrusted a recipients device.
*
* @param device device the user wants to encrypt for, but which has been marked as untrusted.
* @param untrustedKey fingerprint of that device.
*/
public UntrustedOmemoIdentityException(OmemoDevice device, OmemoFingerprint untrustedKey) {
this(device, null, untrustedKey);
}
/**
* Return the device which sent the message.
* @return omemoDevice.
*/
public OmemoDevice getDevice() {
return device;
}
/**
* Return the fingerprint of the key we expected.
* This might return null in case this exception got thrown during encryption process.
* @return
*/
public OmemoFingerprint getTrustedFingerprint() {
return trustedKey;
}
/**
* Return the fingerprint of the unexpected untrusted key.
* @return
*/
public OmemoFingerprint getUntrustedFingerprint() {
return untrustedKey;
}
@Override
public String toString() {
if (trustedKey != null) {
return "Untrusted OMEMO Identity encountered:\n" +
"Fingerprint of trusted key:\n" + trustedKey.blocksOf8Chars() + "\n" +
"Fingerprint of untrusted key:\n" + untrustedKey.blocksOf8Chars();
} else {
return "Untrusted OMEMO Identity encountered:\n" +
"Fingerprint of untrusted key:\n" + untrustedKey.blocksOf8Chars();
}
}
}

View File

@ -23,7 +23,6 @@ import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.PROVIDER;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException; import java.security.NoSuchProviderException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException; import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.IvParameterSpec;
@ -38,11 +37,13 @@ import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
*/ */
public class CipherAndAuthTag { public class CipherAndAuthTag {
private final byte[] key, iv, authTag; private final byte[] key, iv, authTag;
private final boolean wasPreKey;
public CipherAndAuthTag(byte[] key, byte[] iv, byte[] authTag) throws CryptoFailedException { public CipherAndAuthTag(byte[] key, byte[] iv, byte[] authTag, boolean wasPreKey) {
this.authTag = authTag; this.authTag = authTag;
this.key = key; this.key = key;
this.iv = iv; this.iv = iv;
this.wasPreKey = wasPreKey;
} }
public Cipher getCipher() throws CryptoFailedException { public Cipher getCipher() throws CryptoFailedException {
@ -82,4 +83,8 @@ public class CipherAndAuthTag {
} }
return null; return null;
} }
public boolean wasPreKeyEncrypted() {
return wasPreKey;
}
} }

View File

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

View File

@ -32,17 +32,27 @@ import java.util.Set;
* *
* @author Paul Schaub * @author Paul Schaub
*/ */
public class CachedDeviceList implements Serializable { public class OmemoCachedDeviceList implements Serializable {
private static final long serialVersionUID = 3153579238321261203L; private static final long serialVersionUID = 3153579238321261203L;
private final Set<Integer> activeDevices; private final Set<Integer> activeDevices;
private final Set<Integer> inactiveDevices; private final Set<Integer> inactiveDevices;
public CachedDeviceList() { public OmemoCachedDeviceList() {
this.activeDevices = new HashSet<>(); this.activeDevices = new HashSet<>();
this.inactiveDevices = new HashSet<>(); this.inactiveDevices = new HashSet<>();
} }
public OmemoCachedDeviceList(Set<Integer> activeDevices, Set<Integer> inactiveDevices) {
this();
this.activeDevices.addAll(activeDevices);
this.inactiveDevices.addAll(inactiveDevices);
}
public OmemoCachedDeviceList(OmemoCachedDeviceList original) {
this(original.getActiveDevices(), original.getInactiveDevices());
}
/** /**
* Returns all active devices. * Returns all active devices.
* Active devices are all devices that were in the latest DeviceList update. * Active devices are all devices that were in the latest DeviceList update.
@ -90,12 +100,18 @@ public class CachedDeviceList implements Serializable {
} }
/** /**
* Add a device to the list of active devices. * Add a device to the list of active devices and remove it from inactive.
* *
* @param deviceId deviceId that will be added * @param deviceId deviceId that will be added
*/ */
public void addDevice(int deviceId) { public void addDevice(int deviceId) {
activeDevices.add(deviceId); activeDevices.add(deviceId);
inactiveDevices.remove(deviceId);
}
public void addInactiveDevice(int deviceId) {
activeDevices.remove(deviceId);
inactiveDevices.add(deviceId);
} }
/** /**
@ -108,6 +124,10 @@ public class CachedDeviceList implements Serializable {
return activeDevices.contains(deviceId) || inactiveDevices.contains(deviceId); return activeDevices.contains(deviceId) || inactiveDevices.contains(deviceId);
} }
public boolean isActive(int deviceId) {
return getActiveDevices().contains(deviceId);
}
@Override @Override
public String toString() { public String toString() {
String out = "active: ["; String out = "active: [";

View File

@ -16,6 +16,8 @@
*/ */
package org.jivesoftware.smackx.omemo.internal; package org.jivesoftware.smackx.omemo.internal;
import org.jivesoftware.smackx.omemo.util.OmemoConstants;
import org.jxmpp.jid.BareJid; import org.jxmpp.jid.BareJid;
/** /**
@ -74,4 +76,12 @@ public class OmemoDevice {
i = jid.hashCode() + deviceId; i = jid.hashCode() + deviceId;
return i.hashCode(); return i.hashCode();
} }
/**
* Return the name of the PubSub {@link org.jivesoftware.smackx.pubsub.LeafNode} of this device.
* @return node name.
*/
public String getBundleNodeName() {
return OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(getDeviceId());
}
} }

View File

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

View File

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

View File

@ -0,0 +1,29 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.internal.listener;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.carbons.packet.CarbonExtension;
import org.jivesoftware.smackx.omemo.OmemoManager;
/**
* Internal listener for OMEMO encrypted carbon copies.
*/
public interface OmemoCarbonCopyStanzaReceivedListener {
void onOmemoCarbonCopyReceived(CarbonExtension.Direction direction, Message carbonCopy, Message wrappingMessage, OmemoManager.LoggedInOmemoManager omemoManager);
}

View File

@ -0,0 +1,25 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.internal.listener;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smackx.omemo.OmemoManager;
public interface OmemoMessageStanzaReceivedListener {
void onOmemoMessageStanzaReceived(Stanza stanza, OmemoManager.LoggedInOmemoManager omemoManager);
}

View File

@ -14,23 +14,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.jivesoftware.smackx.omemo.internal;
/** /**
* Wrapper for IdentityKey objects. * StanzaListeners used for internal purposes.
* *
* @author Paul Schaub * @author Paul Schaub
* @see <a href="https://conversations.im/xeps/multi-end.html">XEP-0384: OMEMO</a>
*/ */
public class IdentityKeyWrapper { package org.jivesoftware.smackx.omemo.internal.listener;
private final Object identityKey;
public IdentityKeyWrapper(Object wrapped) {
identityKey = wrapped;
}
public Object getIdentityKey() {
return identityKey;
}
}

View File

@ -17,9 +17,9 @@
package org.jivesoftware.smackx.omemo.listener; package org.jivesoftware.smackx.omemo.listener;
import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag; import org.jivesoftware.smackx.carbons.packet.CarbonExtension;
import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation; import org.jivesoftware.smackx.omemo.OmemoMessage;
/** /**
* Listener interface that allows implementations to receive decrypted OMEMO messages. * Listener interface that allows implementations to receive decrypted OMEMO messages.
@ -30,20 +30,13 @@ public interface OmemoMessageListener {
/** /**
* Gets called, whenever an OmemoMessage has been received and was successfully decrypted. * Gets called, whenever an OmemoMessage has been received and was successfully decrypted.
* *
* @param decryptedBody Decrypted body * @param stanza Received (encrypted) stanza.
* @param encryptedMessage Encrypted Message * @param decryptedMessage decrypted OmemoMessage.
* @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); void onOmemoMessageReceived(Stanza stanza, OmemoMessage.Received decryptedMessage);
/** void onOmemoCarbonCopyReceived(CarbonExtension.Direction direction,
* Gets called, whenever an OmemoElement without a body (an OmemoKeyTransportElement) is received. Message carbonCopy,
* Message wrappingMessage,
* @param cipherAndAuthTag transported Cipher along with an optional AuthTag OmemoMessage.Received decryptedCarbonCopy);
* @param message Message that contained the KeyTransport
* @param wrappingMessage Wrapping message (eg. carbon), or null
* @param omemoInformation Information about the messages encryption etc.
*/
void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation);
} }

View File

@ -16,13 +16,9 @@
*/ */
package org.jivesoftware.smackx.omemo.listener; package org.jivesoftware.smackx.omemo.listener;
import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smackx.muc.MultiUserChat; import org.jivesoftware.smackx.muc.MultiUserChat;
import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag; import org.jivesoftware.smackx.omemo.OmemoMessage;
import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation;
import org.jxmpp.jid.BareJid;
/** /**
* Listener interface that allows implementations to receive decrypted OMEMO MUC messages. * Listener interface that allows implementations to receive decrypted OMEMO MUC messages.
@ -33,24 +29,8 @@ public interface OmemoMucMessageListener {
/** /**
* Gets called whenever an OMEMO message has been received in a MultiUserChat and successfully decrypted. * Gets called whenever an OMEMO message has been received in a MultiUserChat and successfully decrypted.
* @param muc MultiUserChat the message was sent in * @param muc MultiUserChat the message was sent in
* @param from the bareJid of the sender * @param stanza Original Stanza
* @param decryptedBody the decrypted Body of the message * @param decryptedOmemoMessage decrypted Omemo 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, void onOmemoMucMessageReceived(MultiUserChat muc, Stanza stanza, OmemoMessage.Received decryptedOmemoMessage);
Message wrappingMessage, OmemoMessageInformation omemoInformation);
/**
* Gets called, whenever an OmemoElement without a body (an OmemoKeyTransportElement) is received.
*
* @param muc MultiUserChat the message was sent in
* @param from bareJid of the sender
* @param cipherAndAuthTag transportedKey along with an optional authTag
* @param message Message that contained the KeyTransport
* @param wrappingMessage Wrapping message (eg. carbon), or null
* @param omemoInformation Information about the messages encryption etc.
*/
void onOmemoKeyTransportReceived(MultiUserChat muc, BareJid from, CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation);
} }

View File

@ -31,7 +31,7 @@ import java.util.HashMap;
import org.jivesoftware.smack.provider.ExtensionElementProvider; import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smackx.omemo.element.OmemoBundleVAxolotlElement; import org.jivesoftware.smackx.omemo.element.OmemoBundleElement_VAxolotl;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
@ -40,9 +40,9 @@ import org.xmlpull.v1.XmlPullParser;
* *
* @author Paul Schaub * @author Paul Schaub
*/ */
public class OmemoBundleVAxolotlProvider extends ExtensionElementProvider<OmemoBundleVAxolotlElement> { public class OmemoBundleVAxolotlProvider extends ExtensionElementProvider<OmemoBundleElement_VAxolotl> {
@Override @Override
public OmemoBundleVAxolotlElement parse(XmlPullParser parser, int initialDepth) throws Exception { public OmemoBundleElement_VAxolotl parse(XmlPullParser parser, int initialDepth) throws Exception {
boolean stop = false; boolean stop = false;
boolean inPreKeys = false; boolean inPreKeys = false;
@ -96,6 +96,6 @@ public class OmemoBundleVAxolotlProvider extends ExtensionElementProvider<OmemoB
break; break;
} }
} }
return new OmemoBundleVAxolotlElement(signedPreKeyId, signedPreKey, signedPreKeySignature, identityKey, preKeys); return new OmemoBundleElement_VAxolotl(signedPreKeyId, signedPreKey, signedPreKeySignature, identityKey, preKeys);
} }
} }

View File

@ -27,7 +27,7 @@ import java.util.Set;
import org.jivesoftware.smack.provider.ExtensionElementProvider; import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smackx.omemo.element.OmemoDeviceListVAxolotlElement; import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement_VAxolotl;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
@ -36,10 +36,10 @@ import org.xmlpull.v1.XmlPullParser;
* *
* @author Paul Schaub * @author Paul Schaub
*/ */
public class OmemoDeviceListVAxolotlProvider extends ExtensionElementProvider<OmemoDeviceListVAxolotlElement> { public class OmemoDeviceListVAxolotlProvider extends ExtensionElementProvider<OmemoDeviceListElement_VAxolotl> {
@Override @Override
public OmemoDeviceListVAxolotlElement parse(XmlPullParser parser, int initialDepth) throws Exception { public OmemoDeviceListElement_VAxolotl parse(XmlPullParser parser, int initialDepth) throws Exception {
Set<Integer> deviceListIds = new HashSet<>(); Set<Integer> deviceListIds = new HashSet<>();
boolean stop = false; boolean stop = false;
while (!stop) { while (!stop) {
@ -63,6 +63,6 @@ public class OmemoDeviceListVAxolotlProvider extends ExtensionElementProvider<Om
break; break;
} }
} }
return new OmemoDeviceListVAxolotlElement(deviceListIds); return new OmemoDeviceListElement_VAxolotl(deviceListIds);
} }
} }

View File

@ -16,14 +16,8 @@
*/ */
package org.jivesoftware.smackx.omemo.provider; package org.jivesoftware.smackx.omemo.provider;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.ENCRYPTED; import static org.jivesoftware.smackx.omemo.element.OmemoElement.ATTR_PAYLOAD;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.HEADER; import static org.jivesoftware.smackx.omemo.element.OmemoElement.NAME_ENCRYPTED;
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.END_TAG;
import static org.xmlpull.v1.XmlPullParser.START_TAG; import static org.xmlpull.v1.XmlPullParser.START_TAG;
@ -31,8 +25,10 @@ import java.util.ArrayList;
import org.jivesoftware.smack.provider.ExtensionElementProvider; import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smack.util.stringencoder.Base64; import org.jivesoftware.smack.util.stringencoder.Base64;
import org.jivesoftware.smackx.omemo.element.OmemoElement_VAxolotl;
import org.jivesoftware.smackx.omemo.element.OmemoVAxolotlElement; import org.jivesoftware.smackx.omemo.element.OmemoHeaderElement;
import org.jivesoftware.smackx.omemo.element.OmemoHeaderElement_VAxolotl;
import org.jivesoftware.smackx.omemo.element.OmemoKeyElement;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
@ -41,13 +37,13 @@ import org.xmlpull.v1.XmlPullParser;
* *
* @author Paul Schaub * @author Paul Schaub
*/ */
public class OmemoVAxolotlProvider extends ExtensionElementProvider<OmemoVAxolotlElement> { public class OmemoVAxolotlProvider extends ExtensionElementProvider<OmemoElement_VAxolotl> {
@Override @Override
public OmemoVAxolotlElement parse(XmlPullParser parser, int initialDepth) throws Exception { public OmemoElement_VAxolotl parse(XmlPullParser parser, int initialDepth) throws Exception {
boolean inEncrypted = true; boolean inEncrypted = true;
int sid = -1; int sid = -1;
ArrayList<OmemoVAxolotlElement.OmemoHeader.Key> keys = new ArrayList<>(); ArrayList<OmemoKeyElement> keys = new ArrayList<>();
byte[] iv = null; byte[] iv = null;
byte[] payload = null; byte[] payload = null;
@ -57,41 +53,41 @@ public class OmemoVAxolotlProvider extends ExtensionElementProvider<OmemoVAxolot
switch (tag) { switch (tag) {
case START_TAG: case START_TAG:
switch (name) { switch (name) {
case HEADER: case OmemoHeaderElement.NAME_HEADER:
for (int i = 0; i < parser.getAttributeCount(); i++) { for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).equals(SID)) { if (parser.getAttributeName(i).equals(OmemoHeaderElement.ATTR_SID)) {
sid = Integer.parseInt(parser.getAttributeValue(i)); sid = Integer.parseInt(parser.getAttributeValue(i));
} }
} }
break; break;
case KEY: case OmemoKeyElement.NAME_KEY:
boolean prekey = false; boolean prekey = false;
int rid = -1; int rid = -1;
for (int i = 0; i < parser.getAttributeCount(); i++) { for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).equals(PREKEY)) { if (parser.getAttributeName(i).equals(OmemoKeyElement.ATTR_PREKEY)) {
prekey = Boolean.parseBoolean(parser.getAttributeValue(i)); prekey = Boolean.parseBoolean(parser.getAttributeValue(i));
} else if (parser.getAttributeName(i).equals(RID)) { } else if (parser.getAttributeName(i).equals(OmemoKeyElement.ATTR_RID)) {
rid = Integer.parseInt(parser.getAttributeValue(i)); rid = Integer.parseInt(parser.getAttributeValue(i));
} }
} }
keys.add(new OmemoVAxolotlElement.OmemoHeader.Key(Base64.decode(parser.nextText()), rid, prekey)); keys.add(new OmemoKeyElement(Base64.decode(parser.nextText()), rid, prekey));
break; break;
case IV: case OmemoHeaderElement.ATTR_IV:
iv = Base64.decode(parser.nextText()); iv = Base64.decode(parser.nextText());
break; break;
case PAYLOAD: case ATTR_PAYLOAD:
payload = Base64.decode(parser.nextText()); payload = Base64.decode(parser.nextText());
break; break;
} }
break; break;
case END_TAG: case END_TAG:
if (name.equals(ENCRYPTED)) { if (name.equals(NAME_ENCRYPTED)) {
inEncrypted = false; inEncrypted = false;
} }
break; break;
} }
} }
OmemoVAxolotlElement.OmemoHeader header = new OmemoVAxolotlElement.OmemoHeader(sid, keys, iv); OmemoHeaderElement_VAxolotl header = new OmemoHeaderElement_VAxolotl(sid, keys, iv);
return new OmemoVAxolotlElement(header, payload); return new OmemoElement_VAxolotl(header, payload);
} }
} }

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.jivesoftware.smackx.omemo; package org.jivesoftware.smackx.omemo.trust;
public class OmemoFingerprint implements CharSequence { public class OmemoFingerprint implements CharSequence {
@ -39,10 +39,6 @@ public class OmemoFingerprint implements CharSequence {
return fingerprintString.subSequence(start, end); return fingerprintString.subSequence(start, end);
} }
public CharSequence subSequence(int start) {
return fingerprintString.subSequence(start, fingerprintString.length() - 1);
}
@Override @Override
public String toString() { public String toString() {
return fingerprintString; return fingerprintString;
@ -57,6 +53,20 @@ public class OmemoFingerprint implements CharSequence {
return this.toString().trim().equals(otherFingerprint.toString().trim()); return this.toString().trim().equals(otherFingerprint.toString().trim());
} }
/**
* Split the fingerprint in blocks of 8 characters with spaces between.
*
* @return Block representation of the fingerprint.
*/
public String blocksOf8Chars() {
StringBuilder pretty = new StringBuilder();
for (int i = 0; i < 8; i++) {
if (i != 0) pretty.append(' ');
pretty.append(this.fingerprintString.substring(8 * i, 8 * (i + 1)));
}
return pretty.toString();
}
@Override @Override
public int hashCode() { public int hashCode() {
return toString().hashCode(); return toString().hashCode();

View File

@ -0,0 +1,27 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.trust;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
public interface OmemoTrustCallback {
TrustState getTrust(OmemoDevice device, OmemoFingerprint fingerprint);
void setTrust(OmemoDevice device, OmemoFingerprint fingerprint, TrustState state);
}

View File

@ -0,0 +1,23 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.trust;
public enum TrustState {
undecided, // User has yet to decide, whether the identity is trusted or not.
untrusted, // User decided NOT to trust this device.
trusted // User decided to trust this device.
}

View File

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

View File

@ -0,0 +1,49 @@
/**
*
* Copyright 2018 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.util;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smackx.omemo.OmemoMessage;
public class MessageOrOmemoMessage {
private final Message message;
private final OmemoMessage.Received omemoMessage;
public MessageOrOmemoMessage(Message message) {
this.message = Objects.requireNonNull(message);
this.omemoMessage = null;
}
public MessageOrOmemoMessage(OmemoMessage.Received omemoMessage) {
this.omemoMessage = Objects.requireNonNull(omemoMessage);
this.message = null;
}
public boolean isOmemoMessage() {
return omemoMessage != null;
}
public Message getMessage() {
return message;
}
public OmemoMessage.Received getOmemoMessage() {
return omemoMessage;
}
}

View File

@ -37,7 +37,7 @@ public final class OmemoConstants {
/** /**
* How many preKeys do we want to publish? * How many preKeys do we want to publish?
*/ */
public static final int TARGET_PRE_KEY_COUNT = 100; public static final int PRE_KEY_COUNT_PER_BUNDLE = 100;
/** /**
* Return the node name of the PEP node containing the device bundle of the device with device id deviceId. * Return the node name of the PEP node containing the device bundle of the device with device id deviceId.
@ -49,7 +49,7 @@ public final class OmemoConstants {
return PEP_NODE_BUNDLES + ":" + deviceId; return PEP_NODE_BUNDLES + ":" + deviceId;
} }
public static final String BODY_OMEMO_HINT = "I sent you an OMEMO encrypted message but your client doesnt seem to support that. Find more information on https://conversations.im/omemo"; 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. * Information about the keys used for message encryption.

View File

@ -19,18 +19,14 @@ package org.jivesoftware.smackx.omemo.util;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.jivesoftware.smackx.omemo.OmemoFingerprint; import org.jivesoftware.smackx.omemo.element.OmemoBundleElement;
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.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.internal.OmemoSession; import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
import org.jxmpp.stringprep.XmppStringprepException;
/** /**
* Class that is used to convert bytes to keys and vice versa. * Class that is used to convert bytes to keys and vice versa.
@ -40,13 +36,11 @@ import org.jxmpp.stringprep.XmppStringprepException;
* @param <T_PreKey> PreKey class * @param <T_PreKey> PreKey class
* @param <T_SigPreKey> SignedPreKey class * @param <T_SigPreKey> SignedPreKey class
* @param <T_Sess> Session class * @param <T_Sess> Session class
* @param <T_Addr> Address class
* @param <T_ECPub> Elliptic Curve PublicKey class * @param <T_ECPub> Elliptic Curve PublicKey class
* @param <T_Bundle> Bundle class * @param <T_Bundle> Bundle class
* @param <T_Ciph> Cipher class
* @author Paul Schaub * @author Paul Schaub
*/ */
public abstract class OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> { public abstract class OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_ECPub, T_Bundle> {
private static final Logger LOGGER = Logger.getLogger(OmemoKeyUtil.class.getName()); private static final Logger LOGGER = Logger.getLogger(OmemoKeyUtil.class.getName());
public final Bundle BUNDLE = new Bundle(); public final Bundle BUNDLE = new Bundle();
@ -63,7 +57,7 @@ public abstract class OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey,
* @return identityKey * @return identityKey
* @throws CorruptedOmemoKeyException if the key is damaged/malformed * @throws CorruptedOmemoKeyException if the key is damaged/malformed
*/ */
public T_IdKey identityKey(OmemoBundleVAxolotlElement bundle) throws CorruptedOmemoKeyException { public T_IdKey identityKey(OmemoBundleElement bundle) throws CorruptedOmemoKeyException {
return identityKeyFromBytes(bundle.getIdentityKey()); return identityKeyFromBytes(bundle.getIdentityKey());
} }
@ -74,7 +68,7 @@ public abstract class OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey,
* @return singedPreKey * @return singedPreKey
* @throws CorruptedOmemoKeyException if the key is damaged/malformed * @throws CorruptedOmemoKeyException if the key is damaged/malformed
*/ */
public T_ECPub signedPreKeyPublic(OmemoBundleVAxolotlElement bundle) throws CorruptedOmemoKeyException { public T_ECPub signedPreKeyPublic(OmemoBundleElement bundle) throws CorruptedOmemoKeyException {
return signedPreKeyPublicFromBytes(bundle.getSignedPreKey()); return signedPreKeyPublicFromBytes(bundle.getSignedPreKey());
} }
@ -84,7 +78,7 @@ public abstract class OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey,
* @param bundle OmemoBundleElement * @param bundle OmemoBundleElement
* @return signedPreKeyId * @return signedPreKeyId
*/ */
public int signedPreKeyId(OmemoBundleVAxolotlElement bundle) { public int signedPreKeyId(OmemoBundleElement bundle) {
return bundle.getSignedPreKeyId(); return bundle.getSignedPreKeyId();
} }
@ -94,7 +88,7 @@ public abstract class OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey,
* @param bundle OmemoBundleElement * @param bundle OmemoBundleElement
* @return signature * @return signature
*/ */
public byte[] signedPreKeySignature(OmemoBundleVAxolotlElement bundle) { public byte[] signedPreKeySignature(OmemoBundleElement bundle) {
return bundle.getSignedPreKeySignature(); return bundle.getSignedPreKeySignature();
} }
@ -106,7 +100,7 @@ public abstract class OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey,
* @return the preKey * @return the preKey
* @throws CorruptedOmemoKeyException when the key cannot be parsed from bytes * @throws CorruptedOmemoKeyException when the key cannot be parsed from bytes
*/ */
public T_ECPub preKeyPublic(OmemoBundleVAxolotlElement bundle, int keyId) throws CorruptedOmemoKeyException { public T_ECPub preKeyPublic(OmemoBundleElement bundle, int keyId) throws CorruptedOmemoKeyException {
return preKeyPublicFromBytes(bundle.getPreKey(keyId)); return preKeyPublicFromBytes(bundle.getPreKey(keyId));
} }
@ -120,7 +114,7 @@ public abstract class OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey,
* @return a HashMap with one T_Bundle per preKey and the preKeyId as key * @return a HashMap with one T_Bundle per preKey and the preKeyId as key
* @throws CorruptedOmemoKeyException when one of the keys cannot be parsed * @throws CorruptedOmemoKeyException when one of the keys cannot be parsed
*/ */
public HashMap<Integer, T_Bundle> bundles(OmemoBundleVAxolotlElement bundle, OmemoDevice contact) throws CorruptedOmemoKeyException { public HashMap<Integer, T_Bundle> bundles(OmemoBundleElement bundle, OmemoDevice contact) throws CorruptedOmemoKeyException {
HashMap<Integer, T_Bundle> bundles = new HashMap<>(); HashMap<Integer, T_Bundle> bundles = new HashMap<>();
for (int deviceId : bundle.getPreKeys().keySet()) { for (int deviceId : bundle.getPreKeys().keySet()) {
try { try {
@ -207,7 +201,7 @@ public abstract class OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey,
* @param count how many keys do we want to generate * @param count how many keys do we want to generate
* @return Map of new preKeys * @return Map of new preKeys
*/ */
public abstract HashMap<Integer, T_PreKey> generateOmemoPreKeys(int startId, int count); public abstract TreeMap<Integer, T_PreKey> generateOmemoPreKeys(int startId, int count);
/** /**
* Generate a new signed preKey. * Generate a new signed preKey.
@ -259,7 +253,7 @@ public abstract class OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey,
* @return PreKeyBundle (T_PreKey) * @return PreKeyBundle (T_PreKey)
* @throws CorruptedOmemoKeyException if some key is damaged or malformed * @throws CorruptedOmemoKeyException if some key is damaged or malformed
*/ */
public abstract T_Bundle bundleFromOmemoBundle(OmemoBundleVAxolotlElement bundle, OmemoDevice contact, int keyId) throws CorruptedOmemoKeyException; public abstract T_Bundle bundleFromOmemoBundle(OmemoBundleElement bundle, OmemoDevice contact, int keyId) throws CorruptedOmemoKeyException;
/** /**
* Extract the signature from a signedPreKey. * Extract the signature from a signedPreKey.
@ -330,7 +324,7 @@ public abstract class OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey,
* @param preKeyHashMap HashMap of preKeys * @param preKeyHashMap HashMap of preKeys
* @return HashMap of byte arrays but with the same keyIds as key * @return HashMap of byte arrays but with the same keyIds as key
*/ */
public HashMap<Integer, byte[]> preKeyPublisKeysForBundle(HashMap<Integer, T_PreKey> preKeyHashMap) { public HashMap<Integer, byte[]> preKeyPublicKeysForBundle(TreeMap<Integer, T_PreKey> preKeyHashMap) {
HashMap<Integer, byte[]> out = new HashMap<>(); HashMap<Integer, byte[]> out = new HashMap<>();
for (Map.Entry<Integer, T_PreKey> e : preKeyHashMap.entrySet()) { for (Map.Entry<Integer, T_PreKey> e : preKeyHashMap.entrySet()) {
out.put(e.getKey(), preKeyForBundle(e.getValue())); out.put(e.getKey(), preKeyForBundle(e.getValue()));
@ -341,7 +335,7 @@ public abstract class OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey,
/** /**
* Prepare a public signedPreKey for transport in a bundle. * Prepare a public signedPreKey for transport in a bundle.
* *
* @param signedPreKey signedPrekey * @param signedPreKey signedPreKey
* @return signedPreKey as byte array * @return signedPreKey as byte array
*/ */
public abstract byte[] signedPreKeyPublicForBundle(T_SigPreKey signedPreKey); public abstract byte[] signedPreKeyPublicForBundle(T_SigPreKey signedPreKey);
@ -352,32 +346,14 @@ public abstract class OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey,
* @param identityKey identityKey * @param identityKey identityKey
* @return fingerprint of the key * @return fingerprint of the key
*/ */
public abstract OmemoFingerprint getFingerprint(T_IdKey identityKey); public abstract OmemoFingerprint getFingerprintOfIdentityKey(T_IdKey identityKey);
/** /**
* Create a new crypto-specific Session object. * Returns the fingerprint of the public key of an identityKeyPair.
* * @param identityKeyPair IdentityKeyPair.
* @param omemoManager omemoManager of our device. * @return fingerprint of the public key.
* @param omemoStore omemoStore where we can save the session, get keys from etc.
* @param from the device we want to create the session with.
* @return a new session
*/ */
public abstract OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> public abstract OmemoFingerprint getFingerprintOfIdentityKeyPair(T_IdKeyPair identityKeyPair);
createOmemoSession(OmemoManager omemoManager, OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore,
OmemoDevice from);
/**
* Create a new concrete OmemoSession with a contact.
*
* @param omemoManager omemoManager of our device.
* @param omemoStore omemoStore
* @param device device to establish the session with
* @param identityKey identityKey of the device
* @return concrete OmemoSession
*/
public abstract OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph>
createOmemoSession(OmemoManager omemoManager, OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore,
OmemoDevice device, T_IdKey identityKey);
/** /**
* Deserialize a raw OMEMO Session from bytes. * Deserialize a raw OMEMO Session from bytes.
@ -396,43 +372,6 @@ public abstract class OmemoKeyUtil<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey,
*/ */
public abstract byte[] rawSessionToBytes(T_Sess session); 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. * Add integers modulo MAX_VALUE.
* *

View File

@ -28,7 +28,6 @@ import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException; import java.security.NoSuchProviderException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.ArrayList; import java.util.ArrayList;
import javax.crypto.BadPaddingException; import javax.crypto.BadPaddingException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException; import javax.crypto.IllegalBlockSizeException;
@ -39,16 +38,21 @@ import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.omemo.OmemoRatchet;
import org.jivesoftware.smackx.omemo.OmemoManager; import org.jivesoftware.smackx.omemo.OmemoService;
import org.jivesoftware.smackx.omemo.OmemoStore; import org.jivesoftware.smackx.omemo.element.OmemoElement;
import org.jivesoftware.smackx.omemo.element.OmemoVAxolotlElement; import org.jivesoftware.smackx.omemo.element.OmemoElement_VAxolotl;
import org.jivesoftware.smackx.omemo.element.OmemoHeaderElement_VAxolotl;
import org.jivesoftware.smackx.omemo.element.OmemoKeyElement;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException; import org.jivesoftware.smackx.omemo.exceptions.NoIdentityKeyException;
import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException; import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException;
import org.jivesoftware.smackx.omemo.exceptions.UntrustedOmemoIdentityException;
import org.jivesoftware.smackx.omemo.internal.CiphertextTuple; import org.jivesoftware.smackx.omemo.internal.CiphertextTuple;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.internal.OmemoSession; import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
import org.jivesoftware.smackx.omemo.trust.OmemoTrustCallback;
/** /**
* Class used to build OMEMO messages. * Class used to build OMEMO messages.
@ -65,22 +69,27 @@ import org.jivesoftware.smackx.omemo.internal.OmemoSession;
* @author Paul Schaub * @author Paul Schaub
*/ */
public class OmemoMessageBuilder<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> { public class OmemoMessageBuilder<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> {
private final OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore;
private final OmemoManager omemoManager;
private byte[] messageKey = generateKey(); private final OmemoDevice userDevice;
private byte[] initializationVector = generateIv(); private final OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> ratchet;
private final OmemoTrustCallback trustCallback;
private byte[] messageKey;
private final byte[] initializationVector;
private byte[] ciphertextMessage; private byte[] ciphertextMessage;
private final ArrayList<OmemoVAxolotlElement.OmemoHeader.Key> keys = new ArrayList<>(); private final ArrayList<OmemoKeyElement> keys = new ArrayList<>();
/** /**
* Create a OmemoMessageBuilder. * Create an OmemoMessageBuilder.
*
* @param userDevice our OmemoDevice
* @param callback trustCallback for querying trust decisions
* @param ratchet our OmemoRatchet
* @param aesKey aes message key used for message encryption
* @param iv initialization vector used for message encryption
* @param message message we want to send
* *
* @param omemoManager OmemoManager of our device.
* @param omemoStore OmemoStore.
* @param aesKey AES key that will be transported to the recipient. This is used eg. to encrypt the body.
* @param iv IV
* @throws NoSuchPaddingException * @throws NoSuchPaddingException
* @throws BadPaddingException * @throws BadPaddingException
* @throws InvalidKeyException * @throws InvalidKeyException
@ -90,23 +99,31 @@ public class OmemoMessageBuilder<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_
* @throws NoSuchProviderException * @throws NoSuchProviderException
* @throws InvalidAlgorithmParameterException * @throws InvalidAlgorithmParameterException
*/ */
public OmemoMessageBuilder(OmemoManager omemoManager, public OmemoMessageBuilder(OmemoDevice userDevice,
OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore, OmemoTrustCallback callback,
byte[] aesKey, byte[] iv) OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> ratchet,
throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, byte[] aesKey,
byte[] iv,
String message)
throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException,
IllegalBlockSizeException,
UnsupportedEncodingException, NoSuchProviderException, InvalidAlgorithmParameterException { UnsupportedEncodingException, NoSuchProviderException, InvalidAlgorithmParameterException {
this.omemoStore = omemoStore; this.userDevice = userDevice;
this.omemoManager = omemoManager; this.trustCallback = callback;
this.ratchet = ratchet;
this.messageKey = aesKey; this.messageKey = aesKey;
this.initializationVector = iv; this.initializationVector = iv;
setMessage(message);
} }
/** /**
* Create a new OmemoMessageBuilder with random IV and AES key. * Create an OmemoMessageBuilder.
*
* @param userDevice our OmemoDevice
* @param callback trustCallback for querying trust decisions
* @param ratchet our OmemoRatchet
* @param message message we want to send
* *
* @param omemoManager omemoManager of our device.
* @param omemoStore omemoStore.
* @param message Messages body.
* @throws NoSuchPaddingException * @throws NoSuchPaddingException
* @throws BadPaddingException * @throws BadPaddingException
* @throws InvalidKeyException * @throws InvalidKeyException
@ -116,30 +133,32 @@ public class OmemoMessageBuilder<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_
* @throws NoSuchProviderException * @throws NoSuchProviderException
* @throws InvalidAlgorithmParameterException * @throws InvalidAlgorithmParameterException
*/ */
public OmemoMessageBuilder(OmemoManager omemoManager, public OmemoMessageBuilder(OmemoDevice userDevice,
OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore, String message) OmemoTrustCallback callback,
OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> ratchet,
String message)
throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException,
UnsupportedEncodingException, NoSuchProviderException, InvalidAlgorithmParameterException { UnsupportedEncodingException, NoSuchProviderException, InvalidAlgorithmParameterException {
this.omemoManager = omemoManager; this(userDevice, callback, ratchet, generateKey(KEYTYPE, KEYLENGTH), generateIv(), message);
this.omemoStore = omemoStore;
this.setMessage(message);
} }
/** /**
* Create an AES messageKey and use it to encrypt the message. * Encrypt the message with the aes key.
* Optionally append the Auth Tag of the encrypted message to the messageKey afterwards. * Move the AuthTag from the end of the cipherText to the end of the messageKey afterwards.
* This prevents an attacker which compromised one recipient device to switch out the cipherText for other recipients.
* @see <a href="https://conversations.im/omemo/audit.pdf">OMEMO security audit</a>.
* *
* @param message content of the message * @param message plaintext message
* @throws NoSuchPaddingException When no Cipher could be instantiated. * @throws NoSuchPaddingException
* @throws NoSuchAlgorithmException when no Cipher could be instantiated. * @throws NoSuchAlgorithmException
* @throws NoSuchProviderException when BouncyCastle could not be found. * @throws NoSuchProviderException
* @throws InvalidAlgorithmParameterException when the Cipher could not be initialized * @throws InvalidAlgorithmParameterException
* @throws InvalidKeyException when the generated key is invalid * @throws InvalidKeyException
* @throws UnsupportedEncodingException when UTF8 is unavailable * @throws UnsupportedEncodingException
* @throws BadPaddingException when cipher.doFinal gets wrong padding * @throws BadPaddingException
* @throws IllegalBlockSizeException when cipher.doFinal gets wrong Block size. * @throws IllegalBlockSizeException
*/ */
public void setMessage(String message) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, InvalidKeyException, UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException { private void setMessage(String message) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, InvalidKeyException, UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException {
if (message == null) { if (message == null) {
return; return;
} }
@ -159,50 +178,71 @@ public class OmemoMessageBuilder<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_
byte[] clearKeyWithAuthTag = new byte[messageKey.length + 16]; byte[] clearKeyWithAuthTag = new byte[messageKey.length + 16];
byte[] cipherTextWithoutAuthTag = new byte[ciphertext.length - 16]; byte[] cipherTextWithoutAuthTag = new byte[ciphertext.length - 16];
System.arraycopy(messageKey, 0, clearKeyWithAuthTag, 0, 16); moveAuthTag(messageKey, ciphertext, clearKeyWithAuthTag, cipherTextWithoutAuthTag);
System.arraycopy(ciphertext, 0, cipherTextWithoutAuthTag, 0, cipherTextWithoutAuthTag.length);
System.arraycopy(ciphertext, ciphertext.length - 16, clearKeyWithAuthTag, 16, 16);
ciphertextMessage = cipherTextWithoutAuthTag; ciphertextMessage = cipherTextWithoutAuthTag;
messageKey = clearKeyWithAuthTag; messageKey = clearKeyWithAuthTag;
} }
/** /**
* Add a new recipient device to the message. * Move the auth tag from the end of the cipherText to the messageKey.
* *
* @param device recipient device * @param messageKey source messageKey without authTag
* @throws CryptoFailedException when encrypting the messageKey fails * @param cipherText source cipherText with authTag
* @throws UndecidedOmemoIdentityException * @param messageKeyWithAuthTag destination messageKey with authTag
* @throws CorruptedOmemoKeyException * @param cipherTextWithoutAuthTag destination cipherText without authTag
*/ */
public void addRecipient(OmemoDevice device) throws CryptoFailedException, UndecidedOmemoIdentityException, CorruptedOmemoKeyException { static void moveAuthTag(byte[] messageKey,
addRecipient(device, false); byte[] cipherText,
byte[] messageKeyWithAuthTag,
byte[] cipherTextWithoutAuthTag) {
// Check dimensions of arrays
if (messageKeyWithAuthTag.length != messageKey.length + 16) {
throw new IllegalArgumentException("Length of messageKeyWithAuthTag must be length of messageKey + " +
"length of AuthTag (16)");
}
if (cipherTextWithoutAuthTag.length != cipherText.length - 16) {
throw new IllegalArgumentException("Length of cipherTextWithoutAuthTag must be length of cipherText " +
"- length of AuthTag (16)");
}
// Move auth tag from cipherText to messageKey
System.arraycopy(messageKey, 0, messageKeyWithAuthTag, 0, 16);
System.arraycopy(cipherText, 0, cipherTextWithoutAuthTag, 0, cipherTextWithoutAuthTag.length);
System.arraycopy(cipherText, cipherText.length - 16, messageKeyWithAuthTag, 16, 16);
} }
/** /**
* Add a new recipient device to the message. * 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 * @param contactsDevice device of the recipient
* @throws CryptoFailedException * @throws NoIdentityKeyException if we have no identityKey of that device. Can be fixed by fetching and
* @throws UndecidedOmemoIdentityException * processing the devices bundle.
* @throws CorruptedOmemoKeyException * @throws CorruptedOmemoKeyException if the identityKey of that device is corrupted.
* @throws UndecidedOmemoIdentityException if the user hasn't yet decided whether to trust that device or not.
* @throws UntrustedOmemoIdentityException if the user has decided not to trust that device.
*/ */
public void addRecipient(OmemoDevice device, boolean ignoreTrust) throws public void addRecipient(OmemoDevice contactsDevice)
CryptoFailedException, UndecidedOmemoIdentityException, CorruptedOmemoKeyException { throws NoIdentityKeyException, CorruptedOmemoKeyException, UndecidedOmemoIdentityException,
OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> session = UntrustedOmemoIdentityException {
omemoStore.getOmemoSessionOf(omemoManager, device);
if (session != null) { OmemoFingerprint fingerprint;
if (!ignoreTrust && !omemoStore.isDecidedOmemoIdentity(omemoManager, device, session.getIdentityKey())) { fingerprint = OmemoService.getInstance().getOmemoStoreBackend().getFingerprint(userDevice, contactsDevice);
// Warn user of undecided device
throw new UndecidedOmemoIdentityException(device); switch (trustCallback.getTrust(contactsDevice, fingerprint)) {
}
case undecided:
throw new UndecidedOmemoIdentityException(contactsDevice);
case trusted:
CiphertextTuple encryptedKey = ratchet.doubleRatchetEncrypt(contactsDevice, messageKey);
keys.add(new OmemoKeyElement(encryptedKey.getCiphertext(), contactsDevice.getDeviceId(), encryptedKey.isPreKeyMessage()));
break;
case untrusted:
throw new UntrustedOmemoIdentityException(contactsDevice, fingerprint);
if (!ignoreTrust && omemoStore.isTrustedOmemoIdentity(omemoManager, device, session.getIdentityKey())) {
// Encrypt key and save to header
CiphertextTuple encryptedKey = session.encryptMessageKey(messageKey);
keys.add(new OmemoVAxolotlElement.OmemoHeader.Key(encryptedKey.getCiphertext(), device.getDeviceId(), encryptedKey.isPreKeyMessage()));
}
} }
} }
@ -211,24 +251,26 @@ public class OmemoMessageBuilder<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_
* *
* @return OmemoMessageElement * @return OmemoMessageElement
*/ */
public OmemoVAxolotlElement finish() { public OmemoElement finish() {
OmemoVAxolotlElement.OmemoHeader header = new OmemoVAxolotlElement.OmemoHeader( OmemoHeaderElement_VAxolotl header = new OmemoHeaderElement_VAxolotl(
omemoManager.getDeviceId(), userDevice.getDeviceId(),
keys, keys,
initializationVector initializationVector
); );
return new OmemoVAxolotlElement(header, ciphertextMessage); return new OmemoElement_VAxolotl(header, ciphertextMessage);
} }
/** /**
* Generate a new AES key used to encrypt the message. * Generate a new AES key used to encrypt the message.
* *
* @param keyType Key Type
* @param keyLength Key Length in bit
* @return new AES key * @return new AES key
* @throws NoSuchAlgorithmException * @throws NoSuchAlgorithmException
*/ */
public static byte[] generateKey() throws NoSuchAlgorithmException { public static byte[] generateKey(String keyType, int keyLength) throws NoSuchAlgorithmException {
KeyGenerator generator = KeyGenerator.getInstance(KEYTYPE); KeyGenerator generator = KeyGenerator.getInstance(keyType);
generator.init(KEYLENGTH); generator.init(keyLength);
return generator.generateKey().getEncoded(); return generator.generateKey().getEncoded();
} }
@ -243,12 +285,4 @@ public class OmemoMessageBuilder<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_
random.nextBytes(iv); random.nextBytes(iv);
return iv; return iv;
} }
public byte[] getCiphertextMessage() {
return ciphertextMessage;
}
public byte[] getMessageKey() {
return messageKey;
}
} }

View File

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

View File

@ -14,15 +14,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.jivesoftware.smack.omemo; package org.jivesoftware.smackx.omemo;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import org.jivesoftware.smackx.omemo.internal.CachedDeviceList; import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList;
import org.junit.Test; import org.junit.Test;
@ -42,7 +43,7 @@ public class DeviceListTest {
*/ */
@Test @Test
public void mergeDeviceListsTest() { public void mergeDeviceListsTest() {
CachedDeviceList cached = new CachedDeviceList(); OmemoCachedDeviceList cached = new OmemoCachedDeviceList();
assertNotNull(cached.getActiveDevices()); assertNotNull(cached.getActiveDevices());
assertNotNull(cached.getInactiveDevices()); assertNotNull(cached.getInactiveDevices());
@ -67,5 +68,11 @@ public class DeviceListTest {
!cached.getInactiveDevices().contains(4)); !cached.getInactiveDevices().contains(4));
assertTrue(cached.getAllDevices().size() == 4); assertTrue(cached.getAllDevices().size() == 4);
assertFalse(cached.contains(17));
cached.addDevice(17);
assertTrue(cached.getActiveDevices().contains(17));
assertNotNull(cached.toString());
} }
} }

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.jivesoftware.smack.omemo; package org.jivesoftware.smackx.omemo;
import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -27,7 +27,7 @@ import org.jivesoftware.smack.test.util.TestUtils;
import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.stringencoder.Base64; import org.jivesoftware.smack.util.stringencoder.Base64;
import org.jivesoftware.smackx.omemo.element.OmemoBundleVAxolotlElement; import org.jivesoftware.smackx.omemo.element.OmemoBundleElement_VAxolotl;
import org.jivesoftware.smackx.omemo.provider.OmemoBundleVAxolotlProvider; import org.jivesoftware.smackx.omemo.provider.OmemoBundleVAxolotlProvider;
import org.junit.Test; import org.junit.Test;
@ -50,7 +50,7 @@ public class OmemoBundleVAxolotlElementTest extends SmackTestSuite {
preKeysB64.put(preKeyId1, preKey1B64); preKeysB64.put(preKeyId1, preKey1B64);
preKeysB64.put(preKeyId2, preKey2B64); preKeysB64.put(preKeyId2, preKey2B64);
OmemoBundleVAxolotlElement bundle = new OmemoBundleVAxolotlElement(signedPreKeyId, OmemoBundleElement_VAxolotl bundle = new OmemoBundleElement_VAxolotl(signedPreKeyId,
signedPreKeyB64, signedPreKeySigB64, identityKeyB64, preKeysB64); signedPreKeyB64, signedPreKeySigB64, identityKeyB64, preKeysB64);
assertEquals("ElementName must match.", "bundle", bundle.getElementName()); assertEquals("ElementName must match.", "bundle", bundle.getElementName());
@ -85,7 +85,7 @@ public class OmemoBundleVAxolotlElementTest extends SmackTestSuite {
byte[] firstPreKey = "FirstPreKey".getBytes(StringUtils.UTF8); byte[] firstPreKey = "FirstPreKey".getBytes(StringUtils.UTF8);
byte[] secondPreKey = "SecondPreKey".getBytes(StringUtils.UTF8); byte[] secondPreKey = "SecondPreKey".getBytes(StringUtils.UTF8);
OmemoBundleVAxolotlElement parsed = new OmemoBundleVAxolotlProvider().parse(TestUtils.getParser(actual)); OmemoBundleElement_VAxolotl parsed = new OmemoBundleVAxolotlProvider().parse(TestUtils.getParser(actual));
assertTrue("B64-decoded signedPreKey must match.", Arrays.equals(signedPreKey, parsed.getSignedPreKey())); assertTrue("B64-decoded signedPreKey must match.", Arrays.equals(signedPreKey, parsed.getSignedPreKey()));
assertEquals("SignedPreKeyId must match", signedPreKeyId, parsed.getSignedPreKeyId()); assertEquals("SignedPreKeyId must match", signedPreKeyId, parsed.getSignedPreKeyId());

View File

@ -14,14 +14,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.jivesoftware.smack.omemo; package org.jivesoftware.smackx.omemo;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.File;
import org.jivesoftware.smackx.omemo.OmemoConfiguration;
import junit.framework.TestCase; import junit.framework.TestCase;
import org.junit.Test; import org.junit.Test;
@ -34,25 +31,6 @@ public class OmemoConfigurationTest {
@Test @Test
public void omemoConfigurationTest() { public void omemoConfigurationTest() {
@SuppressWarnings("unused") OmemoConfiguration configuration = new OmemoConfiguration(); @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 // Body hint
OmemoConfiguration.setAddOmemoHintBody(false); OmemoConfiguration.setAddOmemoHintBody(false);
@ -109,5 +87,17 @@ public class OmemoConfigurationTest {
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
// Expected // Expected
} }
// Repair broken sessions
OmemoConfiguration.setRepairBrokenSessionsWithPrekeyMessages(false);
assertFalse(OmemoConfiguration.getRepairBrokenSessionsWithPreKeyMessages());
OmemoConfiguration.setRepairBrokenSessionsWithPrekeyMessages(true);
assertTrue(OmemoConfiguration.getRepairBrokenSessionsWithPreKeyMessages());
// Complete fresh sessions
OmemoConfiguration.setCompleteSessionWithEmptyMessage(false);
assertFalse(OmemoConfiguration.getCompleteSessionWithEmptyMessage());
OmemoConfiguration.setCompleteSessionWithEmptyMessage(true);
assertTrue(OmemoConfiguration.getCompleteSessionWithEmptyMessage());
} }
} }

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.jivesoftware.smack.omemo; package org.jivesoftware.smackx.omemo;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -24,7 +24,7 @@ import java.util.HashSet;
import org.jivesoftware.smack.test.util.SmackTestSuite; import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.test.util.TestUtils; import org.jivesoftware.smack.test.util.TestUtils;
import org.jivesoftware.smackx.omemo.element.OmemoDeviceListVAxolotlElement; import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement_VAxolotl;
import org.jivesoftware.smackx.omemo.provider.OmemoDeviceListVAxolotlProvider; import org.jivesoftware.smackx.omemo.provider.OmemoDeviceListVAxolotlProvider;
import org.junit.Test; import org.junit.Test;
@ -41,11 +41,11 @@ public class OmemoDeviceListVAxolotlElementTest extends SmackTestSuite {
ids.add(1234); ids.add(1234);
ids.add(9876); ids.add(9876);
OmemoDeviceListVAxolotlElement element = new OmemoDeviceListVAxolotlElement(ids); OmemoDeviceListElement_VAxolotl element = new OmemoDeviceListElement_VAxolotl(ids);
String xml = element.toXML(null).toString(); String xml = element.toXML(null).toString();
XmlPullParser parser = TestUtils.getParser(xml); XmlPullParser parser = TestUtils.getParser(xml);
OmemoDeviceListVAxolotlElement parsed = new OmemoDeviceListVAxolotlProvider().parse(parser); OmemoDeviceListElement_VAxolotl parsed = new OmemoDeviceListVAxolotlProvider().parse(parser);
assertTrue("Parsed element must equal the original.", parsed.getDeviceIds().equals(element.getDeviceIds())); assertTrue("Parsed element must equal the original.", parsed.getDeviceIds().equals(element.getDeviceIds()));
assertEquals("Generated XML must match.", assertEquals("Generated XML must match.",

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.jivesoftware.smack.omemo; package org.jivesoftware.smackx.omemo;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.jivesoftware.smack.omemo; package org.jivesoftware.smackx.omemo;
import static junit.framework.TestCase.fail; import static junit.framework.TestCase.fail;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;

View File

@ -14,12 +14,12 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.jivesoftware.smack.omemo; package org.jivesoftware.smackx.omemo;
import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertNotSame; import static junit.framework.TestCase.assertNotSame;
import org.jivesoftware.smackx.omemo.OmemoFingerprint; import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
import org.junit.Test; import org.junit.Test;

View File

@ -0,0 +1,91 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertTrue;
import java.util.Date;
import java.util.HashSet;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.junit.Test;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
public class OmemoServiceTest extends SmackTestSuite {
private static final long ONE_HOUR = 1000L * 60 * 60;
private static final int IGNORE_STALE = OmemoConfiguration.getIgnoreStaleDevicesAfterHours();
private static final int DELETE_STALE = OmemoConfiguration.getDeleteStaleDevicesAfterHours();
@Test(expected = IllegalStateException.class)
public void getInstanceFailsWhenNullTest() {
OmemoService.getInstance();
}
@Test
public void isServiceRegisteredTest() {
assertFalse(OmemoService.isServiceRegistered());
}
/**
* Test correct functionality of isStale method.
* @throws XmppStringprepException
*/
@Test
public void isStaleDeviceTest() throws XmppStringprepException {
OmemoDevice user = new OmemoDevice(JidCreate.bareFrom("alice@wonderland.lit"), 123);
OmemoDevice other = new OmemoDevice(JidCreate.bareFrom("bob@builder.tv"), 444);
Date now = new Date();
Date ignoreMe = new Date(now.getTime() - ((IGNORE_STALE + 1) * ONE_HOUR));
Date deleteMe = new Date(now.getTime() - ((DELETE_STALE + 1) * ONE_HOUR));
Date imFine = new Date(now.getTime() - ONE_HOUR);
// One hour "old" devices are (probably) not not stale
assertFalse(OmemoService.isStale(user, other, imFine, IGNORE_STALE));
// Devices one hour "older" than max ages are stale
assertTrue(OmemoService.isStale(user, other, ignoreMe, IGNORE_STALE));
assertTrue(OmemoService.isStale(user, other, deleteMe, DELETE_STALE));
// Own device is never stale, no matter how old
assertFalse(OmemoService.isStale(user, user, deleteMe, DELETE_STALE));
// Always return false if date is null.
assertFalse(OmemoService.isStale(user, other, null, DELETE_STALE));
}
@Test
public void removeOurDeviceTest() throws XmppStringprepException {
OmemoDevice a = new OmemoDevice(JidCreate.bareFrom("a@b.c"), 123);
OmemoDevice b = new OmemoDevice(JidCreate.bareFrom("a@b.c"), 124);
HashSet<OmemoDevice> devices = new HashSet<>();
devices.add(a); devices.add(b);
assertTrue(devices.contains(a));
assertTrue(devices.contains(b));
OmemoService.removeOurDevice(a, devices);
assertFalse(devices.contains(a));
assertTrue(devices.contains(b));
}
}

View File

@ -0,0 +1,345 @@
/**
*
* Copyright Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNotNull;
import static junit.framework.TestCase.assertNotSame;
import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.assertSame;
import static junit.framework.TestCase.assertTrue;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.TreeMap;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
import org.junit.AfterClass;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
public abstract class OmemoStoreTest<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> {
protected final OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> store;
private final OmemoDevice alice, bob;
private static final TemporaryFolder tmp = initStaticTemp();
OmemoStoreTest(OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> store)
throws XmppStringprepException {
this.store = store;
alice = new OmemoDevice(JidCreate.bareFrom("alice@wonderland.lit"), 123);
bob = new OmemoDevice(JidCreate.bareFrom("bob@builder.tv"), 987);
}
// Tests
@Test
public void keyUtilNotNull() {
assertNotNull(store.keyUtil());
}
@Test
public void generateOmemoIdentityKeyPairDoesNotReturnNull() {
assertNotNull(store.generateOmemoIdentityKeyPair());
}
@Test
public void identityKeyFromIdentityKeyPairIsNotNull() {
T_IdKeyPair pair = store.generateOmemoIdentityKeyPair();
assertNotNull(store.keyUtil().identityKeyFromPair(pair));
}
@Test
public void storeLoadRemoveOmemoIdentityKeyPair()
throws IOException, CorruptedOmemoKeyException {
T_IdKeyPair before = store.generateOmemoIdentityKeyPair();
assertNull(store.loadOmemoIdentityKeyPair(alice));
store.storeOmemoIdentityKeyPair(alice, before);
T_IdKeyPair after = store.loadOmemoIdentityKeyPair(alice);
assertNotNull(after);
// Fingerprints equal
assertEquals(store.keyUtil().getFingerprintOfIdentityKeyPair(before),
store.keyUtil().getFingerprintOfIdentityKeyPair(after));
// Byte-representation equals
assertTrue(Arrays.equals(
store.keyUtil().identityKeyPairToBytes(before),
store.keyUtil().identityKeyPairToBytes(after)));
// Non-existing keypair
assertNull("Must return null for non-existing key pairs.", store.loadOmemoIdentityKeyPair(bob));
// Deleting works
store.removeOmemoIdentityKeyPair(alice);
assertNull(store.loadOmemoIdentityKeyPair(alice));
}
@Test
public void storeLoadRemoveOmemoIdentityKey()
throws IOException, CorruptedOmemoKeyException {
// Create IdentityKeys and get bytes
T_IdKey keyA1 = store.keyUtil().identityKeyFromPair(store.generateOmemoIdentityKeyPair());
T_IdKey keyB1 = store.keyUtil().identityKeyFromPair(store.generateOmemoIdentityKeyPair());
byte[] bytesA1 = store.keyUtil().identityKeyToBytes(keyA1);
byte[] bytesB = store.keyUtil().identityKeyToBytes(keyB1);
// Not null and not of length 0
assertNotNull("Serialized identityKey cannot be null.", bytesA1);
assertNotNull("Serialized identityKey cannot be null.", bytesB);
assertNotSame("Serialized identityKey must be of length > 0.", 0, bytesA1.length);
assertNotSame("Serialized identityKey must be of length > 0.", 0, bytesB.length);
// Keys do not equal
assertFalse("Generated IdentityKeys must not be equal (ULTRA unlikely).",
Arrays.equals(bytesA1, bytesB));
// Loading must return null before and not null after saving
assertNull("Must return null, the store could not have this key by now.",
store.loadOmemoIdentityKey(alice, bob));
store.storeOmemoIdentityKey(alice, bob, keyA1);
T_IdKey keyA2 = store.loadOmemoIdentityKey(alice, bob);
assertNotNull(keyA2);
// Loaded key must equal stored one
byte[] bytesA2 = store.keyUtil().identityKeyToBytes(keyA2);
assertTrue("Serialized loaded key must equal serialized stored one.",
Arrays.equals(bytesA1, bytesA2));
// Non-existing keys must return null
assertNull("Non-existing keys must be returned as null.", store.loadOmemoIdentityKey(bob, alice));
// Key must vanish when deleted.
store.removeOmemoIdentityKey(alice, bob);
assertNull(store.loadOmemoIdentityKey(alice, bob));
}
@Test
public void generateOmemoPreKeys() {
TreeMap<Integer, T_PreKey> keys = store.generateOmemoPreKeys(31, 49);
assertNotNull("Generated data structure must not be null.", keys);
byte[] lastKey = null;
for (int i = 31; i <= 79; i++) {
assertEquals("Key ids must be ascending order, starting at 31.", Integer.valueOf(i), keys.firstKey());
assertNotNull("Every id must match to a key.", keys.get(keys.firstKey()));
byte[] bytes = store.keyUtil().preKeyToBytes(keys.get(keys.firstKey()));
assertNotNull("Serialized preKey must not be null.", bytes);
assertNotSame("Serialized preKey must not be of length 0.", 0, bytes.length);
if (lastKey != null) {
assertFalse("PreKeys MUST NOT be equal.", Arrays.equals(lastKey, bytes));
}
lastKey = bytes;
keys.remove(keys.firstKey());
}
assertEquals("After deleting 49 keys, there must be no keys left.", 0, keys.size());
}
@Test
public void storeLoadRemoveOmemoPreKeys()
throws IOException, InterruptedException {
TreeMap<Integer, T_PreKey> before = store.generateOmemoPreKeys(1, 10);
assertEquals("The store must have no prekeys before this test.", 0, store.loadOmemoPreKeys(alice).size());
store.storeOmemoPreKeys(alice, before);
TreeMap<Integer, T_PreKey> after = store.loadOmemoPreKeys(alice);
assertNotNull("Loaded preKeys must not be null.", after);
assertEquals("Loaded preKey count must equal stored count.", before.size(), after.size());
// Non-existing key must be returned as null
assertNull("Non-existing preKey must be returned as null.", store.loadOmemoPreKey(alice, 10000));
int last = after.size();
for (int i = 1; i <= last; i++) {
T_PreKey bKey = before.get(i);
T_PreKey aKey = after.get(i);
assertTrue("Loaded keys must equal stored ones.", Arrays.equals(
store.keyUtil().preKeyToBytes(bKey),
store.keyUtil().preKeyToBytes(aKey)));
T_PreKey rKey = store.loadOmemoPreKey(alice, i);
assertNotNull("Randomly accessed preKeys must not be null.", rKey);
assertTrue("Randomly accessed preKeys must equal the stored ones.", Arrays.equals(
store.keyUtil().preKeyToBytes(aKey),
store.keyUtil().preKeyToBytes(rKey)));
store.removeOmemoPreKey(alice, i);
assertNull("PreKey must be null after deletion.", store.loadOmemoPreKey(alice, i));
}
TreeMap<Integer, T_PreKey> postDeletion = store.loadOmemoPreKeys(alice);
assertSame("PreKey count must equal 0 after deletion of all keys.", 0, postDeletion.size());
}
@Test
public void storeLoadRemoveOmemoSignedPreKeys()
throws IOException, CorruptedOmemoKeyException {
TreeMap<Integer, T_SigPreKey> before = store.loadOmemoSignedPreKeys(alice);
assertEquals("At this stage, there must be no signed prekeys in the store.", 0, before.size());
T_IdKeyPair idp = store.generateOmemoIdentityKeyPair();
T_SigPreKey spk = store.generateOmemoSignedPreKey(idp, 125);
assertNotNull("SignedPreKey must not be null.", spk);
assertEquals("ID of signedPreKey must match.", 125, store.keyUtil().signedPreKeyIdFromKey(spk));
byte[] bytes = store.keyUtil().signedPreKeyToBytes(spk);
assertNotNull("Serialized signedPreKey must not be null", bytes);
assertNotSame("Serialized signedPreKey must not be of length 0.", 0, bytes.length);
// Stored key must equal loaded key
store.storeOmemoSignedPreKey(alice, 125, spk);
TreeMap<Integer, T_SigPreKey> after = store.loadOmemoSignedPreKeys(alice);
assertEquals("We must have exactly 1 signedPreKey now.", 1, after.size());
T_SigPreKey spk2 = after.get(after.firstKey());
assertEquals("Id of the stored signedPreKey must match the one we stored.",
125, store.keyUtil().signedPreKeyIdFromKey(spk2));
assertTrue("Serialization of stored and loaded signed preKey must equal.", Arrays.equals(
store.keyUtil().signedPreKeyToBytes(spk), store.keyUtil().signedPreKeyToBytes(spk2)));
// Random access
T_SigPreKey rspk = store.loadOmemoSignedPreKey(alice, 125);
assertTrue("Serialization of stored and randomly accessed signed preKey must equal.", Arrays.equals(
store.keyUtil().signedPreKeyToBytes(spk), store.keyUtil().signedPreKeyToBytes(rspk)));
assertNull("Non-existing signedPreKey must be returned as null.",
store.loadOmemoSignedPreKey(alice, 10000));
// Deleting
store.removeOmemoSignedPreKey(alice, 125);
assertNull("Deleted key must be returned as null.", store.loadOmemoSignedPreKey(alice, 125));
assertEquals(0, store.loadOmemoSignedPreKeys(alice).size());
}
@Test
public void loadStoreDateOfLastSignedPreKeyRenewal() throws IOException {
assertNull("The date of last signed preKey renewal must be null at this stage.",
store.getDateOfLastSignedPreKeyRenewal(alice));
Date before = new Date();
store.setDateOfLastSignedPreKeyRenewal(alice, before);
Date after = store.getDateOfLastSignedPreKeyRenewal(alice);
assertEquals("Dates must equal.", after, before);
}
@Test
public void loadStoreDateOfLastMessageReceived() throws IOException {
assertNull("The date of last message received must be null at this stage.",
store.getDateOfLastReceivedMessage(alice, bob));
Date before = new Date();
store.setDateOfLastReceivedMessage(alice, bob, before);
Date after = store.getDateOfLastReceivedMessage(alice, bob);
assertEquals("Dates must equal.", after, before);
}
@Test
public void loadStoreCachedDeviceList() throws IOException {
Integer[] active = new Integer[] {1,5,999,10};
Integer[] inactive = new Integer[] {6,7,8};
OmemoCachedDeviceList before = new OmemoCachedDeviceList(
new HashSet<>(Arrays.asList(active)),
new HashSet<>(Arrays.asList(inactive)));
assertNotNull("Loading a non-existent cached deviceList must return an empty list.",
store.loadCachedDeviceList(alice, bob.getJid()));
store.storeCachedDeviceList(alice, bob.getJid(), before);
OmemoCachedDeviceList after = store.loadCachedDeviceList(alice, bob.getJid());
assertTrue("Loaded deviceList must not be empty", after.getAllDevices().size() != 0);
assertEquals("Number of entries in active devices must match.", active.length, after.getActiveDevices().size());
assertEquals("Number of entries in inactive devices must match.", inactive.length, after.getInactiveDevices().size());
assertEquals("Number of total entries must match.", active.length + inactive.length, after.getAllDevices().size());
for (Integer a : active) {
assertTrue(after.getActiveDevices().contains(a));
assertTrue(after.getAllDevices().contains(a));
}
for (Integer i : inactive) {
assertTrue(after.getInactiveDevices().contains(i));
assertTrue(after.getAllDevices().contains(i));
}
store.storeCachedDeviceList(alice, bob.getJid(), new OmemoCachedDeviceList());
assertEquals("DeviceList must be empty after overwriting it with empty list.", 0,
store.loadCachedDeviceList(alice, bob.getJid()).getAllDevices().size());
}
@Test
public void loadAllRawSessionsReturnsEmptyMapTest() {
HashMap<Integer, T_Sess> sessions = store.loadAllRawSessionsOf(alice, bob.getJid());
assertNotNull(sessions);
assertEquals(0, sessions.size());
}
@Test
public void loadNonExistentRawSessionReturnsNullTest() {
T_Sess session = store.loadRawSession(alice, bob);
assertNull(session);
}
@Test
public void getFingerprint() throws IOException, CorruptedOmemoKeyException {
assertNull("Method must return null for a non-existent fingerprint.", store.getFingerprint(alice));
store.storeOmemoIdentityKeyPair(alice, store.generateOmemoIdentityKeyPair());
OmemoFingerprint fingerprint = store.getFingerprint(alice);
assertNotNull("fingerprint must not be null", fingerprint);
assertEquals("Fingerprint must be of length 64", 64, fingerprint.length());
store.removeOmemoIdentityKeyPair(alice); //clean up
}
// ##############################################################
// Workaround for https://github.com/junit-team/junit4/issues/671
static TemporaryFolder initStaticTemp() {
try {
return new TemporaryFolder() { { before(); } };
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
@AfterClass
public static void cleanup() throws Exception {
FileBasedOmemoStore.deleteDirectory(tmp.getRoot());
}
// ##############################################################
}

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.jivesoftware.smack.omemo; package org.jivesoftware.smackx.omemo;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -24,9 +24,9 @@ import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.test.util.TestUtils; import org.jivesoftware.smack.test.util.TestUtils;
import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.stringencoder.Base64; import org.jivesoftware.smack.util.stringencoder.Base64;
import org.jivesoftware.smackx.omemo.element.OmemoElement_VAxolotl;
import org.jivesoftware.smackx.omemo.element.OmemoElement; import org.jivesoftware.smackx.omemo.element.OmemoHeaderElement_VAxolotl;
import org.jivesoftware.smackx.omemo.element.OmemoVAxolotlElement; import org.jivesoftware.smackx.omemo.element.OmemoKeyElement;
import org.jivesoftware.smackx.omemo.provider.OmemoVAxolotlProvider; import org.jivesoftware.smackx.omemo.provider.OmemoVAxolotlProvider;
import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder; import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder;
@ -47,12 +47,12 @@ public class OmemoVAxolotlElementTest extends SmackTestSuite {
int sid = 12131415; int sid = 12131415;
byte[] iv = OmemoMessageBuilder.generateIv(); byte[] iv = OmemoMessageBuilder.generateIv();
ArrayList<OmemoElement.OmemoHeader.Key> keys = new ArrayList<>(); ArrayList<OmemoKeyElement> keys = new ArrayList<>();
keys.add(new OmemoElement.OmemoHeader.Key(keyData1, keyId1)); keys.add(new OmemoKeyElement(keyData1, keyId1));
keys.add(new OmemoElement.OmemoHeader.Key(keyData2, keyId2, true)); keys.add(new OmemoKeyElement(keyData2, keyId2, true));
OmemoVAxolotlElement.OmemoHeader header = new OmemoElement.OmemoHeader(sid, keys, iv); OmemoHeaderElement_VAxolotl header = new OmemoHeaderElement_VAxolotl(sid, keys, iv);
OmemoVAxolotlElement element = new OmemoVAxolotlElement(header, payload); OmemoElement_VAxolotl element = new OmemoElement_VAxolotl(header, payload);
String expected = String expected =
"<encrypted xmlns='eu.siacs.conversations.axolotl'>" + "<encrypted xmlns='eu.siacs.conversations.axolotl'>" +
@ -69,7 +69,9 @@ public class OmemoVAxolotlElementTest extends SmackTestSuite {
String actual = element.toXML(null).toString(); String actual = element.toXML(null).toString();
assertEquals("Serialized xml of OmemoElement must match.", expected, actual); assertEquals("Serialized xml of OmemoElement must match.", expected, actual);
OmemoVAxolotlElement parsed = new OmemoVAxolotlProvider().parse(TestUtils.getParser(actual)); OmemoElement_VAxolotl parsed = new OmemoVAxolotlProvider().parse(TestUtils.getParser(actual));
assertEquals("Parsed OmemoElement must equal the original.", element.toXML(null).toString(), parsed.toXML(null).toString()); assertEquals("Parsed OmemoElement must equal the original.",
element.toXML(null).toString(),
parsed.toXML(null).toString());
} }
} }

View File

@ -14,10 +14,12 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.jivesoftware.smack.omemo; package org.jivesoftware.smackx.omemo;
import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertTrue; import static junit.framework.TestCase.assertTrue;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYLENGTH;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@ -25,35 +27,20 @@ import static org.junit.Assert.assertNotNull;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.Security; import java.security.Security;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.omemo.element.OmemoElement; import org.jivesoftware.smackx.omemo.element.OmemoElement;
import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException; import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag; import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag;
import org.jivesoftware.smackx.omemo.internal.CiphertextTuple; 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.jivesoftware.smackx.omemo.util.OmemoMessageBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Test; import org.junit.Test;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.impl.JidCreate;
/** /**
* Test the identityKeyWrapper. * Test the identityKeyWrapper.
*/ */
public class WrapperObjectsTest { public class WrapperObjectsTest {
@Test
public void identityKeyWrapperTest() {
Object pseudoKey = new Object();
IdentityKeyWrapper wrapper = new IdentityKeyWrapper(pseudoKey);
assertEquals(pseudoKey, wrapper.getIdentityKey());
}
@Test @Test
public void ciphertextTupleTest() { public void ciphertextTupleTest() {
byte[] c = OmemoMessageBuilder.generateIv(); byte[] c = OmemoMessageBuilder.generateIv();
@ -67,41 +54,19 @@ public class WrapperObjectsTest {
assertEquals(OmemoElement.TYPE_OMEMO_MESSAGE, c2.getMessageType()); 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 @Test
public void cipherAndAuthTagTest() throws NoSuchAlgorithmException, CryptoFailedException { public void cipherAndAuthTagTest() throws NoSuchAlgorithmException, CryptoFailedException {
Security.addProvider(new BouncyCastleProvider()); Security.addProvider(new BouncyCastleProvider());
byte[] key = OmemoMessageBuilder.generateKey(); byte[] key = OmemoMessageBuilder.generateKey(KEYTYPE, KEYLENGTH);
byte[] iv = OmemoMessageBuilder.generateIv(); byte[] iv = OmemoMessageBuilder.generateIv();
byte[] authTag = OmemoMessageBuilder.generateIv(); byte[] authTag = OmemoMessageBuilder.generateIv();
CipherAndAuthTag cat = new CipherAndAuthTag(key, iv, authTag); CipherAndAuthTag cat = new CipherAndAuthTag(key, iv, authTag, true);
assertNotNull(cat.getCipher()); assertNotNull(cat.getCipher());
assertArrayEquals(key, cat.getKey()); assertArrayEquals(key, cat.getKey());
assertArrayEquals(iv, cat.getIv()); assertArrayEquals(iv, cat.getIv());
assertArrayEquals(authTag, cat.getAuthTag()); assertArrayEquals(authTag, cat.getAuthTag());
assertTrue(cat.wasPreKeyEncrypted());
} }
} }

View File

@ -0,0 +1,59 @@
/**
*
* Copyright the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.util;
import java.util.HashMap;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
import org.jivesoftware.smackx.omemo.trust.OmemoTrustCallback;
import org.jivesoftware.smackx.omemo.trust.TrustState;
/**
* Ephemera Trust Callback used to make trust decisions in tests.
*/
public class EphemeralTrustCallback implements OmemoTrustCallback {
private final HashMap<OmemoDevice, HashMap<OmemoFingerprint, TrustState>> trustStates = new HashMap<>();
@Override
public TrustState getTrust(OmemoDevice device, OmemoFingerprint fingerprint) {
HashMap<OmemoFingerprint, TrustState> states = trustStates.get(device);
if (states != null) {
TrustState state = states.get(fingerprint);
if (state != null) {
return state;
}
}
return TrustState.undecided;
}
@Override
public void setTrust(OmemoDevice device, OmemoFingerprint fingerprint, TrustState state) {
HashMap<OmemoFingerprint, TrustState> states = trustStates.get(device);
if (states == null) {
states = new HashMap<>();
trustStates.put(device, states);
}
states.put(fingerprint, state);
}
}

View File

@ -0,0 +1,79 @@
/**
*
* Copyright the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.util;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import java.util.Random;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.junit.Test;
public class OmemoMessageBuilderTest extends SmackTestSuite {
private static final byte[] messageKey = new byte[16];
private static final byte[] cipherTextWithAuthTag = new byte[35 + 16];
public OmemoMessageBuilderTest() {
Random random = new Random();
random.nextBytes(messageKey);
random.nextBytes(cipherTextWithAuthTag);
}
@Test
public void testMoveAuthTag() {
// Extract authTag for testing purposes
byte[] authTag = new byte[16];
System.arraycopy(cipherTextWithAuthTag, 35, authTag, 0, 16);
byte[] messageKeyWithAuthTag = new byte[16 + 16];
byte[] cipherTextWithoutAuthTag = new byte[35];
OmemoMessageBuilder.moveAuthTag(messageKey, cipherTextWithAuthTag, messageKeyWithAuthTag, cipherTextWithoutAuthTag);
// Check if first n - 16 bytes of cipherText got copied over to cipherTextWithoutAuthTag correctly
byte[] checkCipherText = new byte[35];
System.arraycopy(cipherTextWithAuthTag, 0, checkCipherText, 0, 35);
assertTrue(Arrays.equals(checkCipherText, cipherTextWithoutAuthTag));
byte[] checkMessageKey = new byte[16];
System.arraycopy(messageKeyWithAuthTag, 0, checkMessageKey, 0, 16);
assertTrue(Arrays.equals(checkMessageKey, messageKey));
byte[] checkAuthTag = new byte[16];
System.arraycopy(messageKeyWithAuthTag, 16, checkAuthTag, 0, 16);
assertTrue(Arrays.equals(checkAuthTag, authTag));
}
@Test(expected = IllegalArgumentException.class)
public void testCheckIllegalMessageKeyWithAuthTagLength() {
byte[] illegalMessageKey = new byte[16 + 15]; // too short
byte[] cipherTextWithoutAuthTag = new byte[35]; // ok
OmemoMessageBuilder.moveAuthTag(messageKey, cipherTextWithAuthTag, illegalMessageKey, cipherTextWithoutAuthTag);
}
@Test(expected = IllegalArgumentException.class)
public void testCheckIllegalCipherTextWithoutAuthTagLength() {
byte[] messageKeyWithAuthTag = new byte[16 + 16]; // ok
byte[] illegalCipherTextWithoutAuthTag = new byte[39]; // too long
OmemoMessageBuilder.moveAuthTag(messageKey, cipherTextWithAuthTag, messageKeyWithAuthTag, illegalCipherTextWithoutAuthTag);
}
}