1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2025-09-09 17:19:39 +02:00

Add OMEMO support

This commit adds the modules smack-omemo and smack-omemo-signal.
smack-omemo is licensed under the Apache license like the rest of the smack project.
smack-omemo-signal on the other hand is licensed under the GPLv3.
Due to the fact, that smack-omemo is not of much use without smack-omemo-signal,
the OMEMO feature can currently only be used by GPLv3 compatible software.
This may change in the future, when a more permissively licensed module becomes available.

Fixes SMACK-743.
This commit is contained in:
vanitasvitae 2017-06-02 12:26:37 +02:00 committed by Florian Schmaus
parent ce36fb468c
commit e86700b040
95 changed files with 11770 additions and 22 deletions

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

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

View file

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

View file

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

View file

@ -0,0 +1,64 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo;
public class OmemoFingerprint implements CharSequence {
private final String fingerprintString;
public OmemoFingerprint(String fingerprintString) {
this.fingerprintString = fingerprintString;
}
@Override
public int length() {
return fingerprintString.length();
}
@Override
public char charAt(int index) {
return fingerprintString.charAt(index);
}
@Override
public CharSequence subSequence(int start, int end) {
return fingerprintString.subSequence(start, end);
}
public CharSequence subSequence(int start) {
return fingerprintString.subSequence(start, fingerprintString.length() - 1);
}
@Override
public String toString() {
return fingerprintString;
}
@Override
public boolean equals(Object other) {
if (!(other instanceof OmemoFingerprint)) {
return false;
}
OmemoFingerprint otherFingerprint = (OmemoFingerprint) other;
return this.toString().trim().equals(otherFingerprint.toString().trim());
}
@Override
public int hashCode() {
return toString().hashCode();
}
}

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,48 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.listener;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag;
import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation;
/**
* Listener interface that allows implementations to receive decrypted OMEMO messages.
*
* @author Paul Schaub
*/
public interface OmemoMessageListener {
/**
* Gets called, whenever an OmemoMessage has been received and was successfully decrypted.
*
* @param decryptedBody Decrypted body
* @param encryptedMessage Encrypted Message
* @param wrappingMessage Wrapping carbon message, in case the message was a carbon copy, else null.
* @param omemoInformation Information about the messages encryption etc.
*/
void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation);
/**
* Gets called, whenever an OmemoElement without a body (an OmemoKeyTransportElement) is received.
*
* @param cipherAndAuthTag transported Cipher along with an optional AuthTag
* @param message Message that contained the KeyTransport
* @param wrappingMessage Wrapping message (eg. carbon), or null
* @param omemoInformation Information about the messages encryption etc.
*/
void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation);
}

View file

@ -0,0 +1,54 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.listener;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag;
import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation;
import org.jxmpp.jid.BareJid;
/**
* Listener interface that allows implementations to receive decrypted OMEMO MUC messages.
* @author Paul Schaub
*/
public interface OmemoMucMessageListener {
/**
* Gets called whenever an OMEMO message has been received in a MultiUserChat and successfully decrypted.
* @param muc MultiUserChat the message was sent in
* @param from the bareJid of the sender
* @param decryptedBody the decrypted Body of the message
* @param message the original message with encrypted element
* @param wrappingMessage in case of a carbon copy, this is the wrapping message
* @param omemoInformation information about the encryption of the message
*/
void onOmemoMucMessageReceived(MultiUserChat muc, BareJid from, String decryptedBody, Message message,
Message wrappingMessage, OmemoMessageInformation omemoInformation);
/**
* Gets called, whenever an OmemoElement without a body (an OmemoKeyTransportElement) is received.
*
* @param muc MultiUserChat the message was sent in
* @param from bareJid of the sender
* @param cipherAndAuthTag transportedKey along with an optional authTag
* @param message Message that contained the KeyTransport
* @param wrappingMessage Wrapping message (eg. carbon), or null
* @param omemoInformation Information about the messages encryption etc.
*/
void onOmemoKeyTransportReceived(MultiUserChat muc, BareJid from, CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation);
}

View file

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

View file

@ -0,0 +1,26 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Classes and interfaces for OMEMO Encryption. This module consists of the
* XMPP logic and some abstract crypto classes that have to be implemented
* using concrete crypto libraries (like signal-protocol-java or olm).
* See smack-omemo-signal for a concrete implementation (GPL licensed).
*
* @author Paul Schaub
* @see <a href="https://conversations.im/xeps/multi-end.html">XEP-0384: OMEMO</a>
*/
package org.jivesoftware.smackx.omemo;

View file

@ -0,0 +1,99 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.provider;
import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smackx.omemo.element.OmemoBundleVAxolotlElement;
import org.xmlpull.v1.XmlPullParser;
import java.util.HashMap;
import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.BUNDLE;
import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.IDENTITY_KEY;
import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.PRE_KEYS;
import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.PRE_KEY_ID;
import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.PRE_KEY_PUB;
import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.SIGNED_PRE_KEY_ID;
import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.SIGNED_PRE_KEY_PUB;
import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.SIGNED_PRE_KEY_SIG;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
/**
* Smack ExtensionProvider that parses OMEMO bundle element into OmemoBundleElement objects.
*
* @author Paul Schaub
*/
public class OmemoBundleVAxolotlProvider extends ExtensionElementProvider<OmemoBundleVAxolotlElement> {
@Override
public OmemoBundleVAxolotlElement parse(XmlPullParser parser, int initialDepth) throws Exception {
boolean stop = false;
boolean inPreKeys = false;
int signedPreKeyId = -1;
String signedPreKey = null;
String signedPreKeySignature = null;
String identityKey = null;
HashMap<Integer, String> preKeys = new HashMap<>();
while (!stop) {
int tag = parser.next();
String name = parser.getName();
switch (tag) {
case START_TAG:
// <signedPreKeyPublic>
if (name.equals(SIGNED_PRE_KEY_PUB)) {
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).equals(SIGNED_PRE_KEY_ID)) {
int id = Integer.parseInt(parser.getAttributeValue(i));
signedPreKey = parser.nextText();
signedPreKeyId = id;
}
}
}
// <bundleGetSignedPreKeySignature>
else if (name.equals(SIGNED_PRE_KEY_SIG)) {
signedPreKeySignature = parser.nextText();
}
// <deserializeIdentityKey>
else if (name.equals(IDENTITY_KEY)) {
identityKey = parser.nextText();
}
// <deserializeECPublicKeys>
else if (name.equals(PRE_KEYS)) {
inPreKeys = true;
}
// <preKeyPublic preKeyId='424242'>
else if (inPreKeys && name.equals(PRE_KEY_PUB)) {
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).equals(PRE_KEY_ID)) {
preKeys.put(Integer.parseInt(parser.getAttributeValue(i)),
parser.nextText());
}
}
}
break;
case END_TAG:
if (name.equals(BUNDLE)) {
stop = true;
}
break;
}
}
return new OmemoBundleVAxolotlElement(signedPreKeyId, signedPreKey, signedPreKeySignature, identityKey, preKeys);
}
}

View file

@ -0,0 +1,66 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.provider;
import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smackx.omemo.element.OmemoDeviceListVAxolotlElement;
import org.xmlpull.v1.XmlPullParser;
import static org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement.DEVICE;
import static org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement.ID;
import static org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement.LIST;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import java.util.HashSet;
import java.util.Set;
/**
* Smack ExtensionProvider that parses OMEMO device list element into OmemoDeviceListElement objects.
*
* @author Paul Schaub
*/
public class OmemoDeviceListVAxolotlProvider extends ExtensionElementProvider<OmemoDeviceListVAxolotlElement> {
@Override
public OmemoDeviceListVAxolotlElement parse(XmlPullParser parser, int initialDepth) throws Exception {
Set<Integer> deviceListIds = new HashSet<>();
boolean stop = false;
while (!stop) {
int tag = parser.next();
String name = parser.getName();
switch (tag) {
case START_TAG:
if (name.equals(DEVICE)) {
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).equals(ID)) {
Integer deviceId = Integer.parseInt(parser.getAttributeValue(i));
deviceListIds.add(deviceId);
}
}
}
break;
case END_TAG:
if (name.equals(LIST)) {
stop = true;
}
break;
}
}
return new OmemoDeviceListVAxolotlElement(deviceListIds);
}
}

View file

@ -0,0 +1,95 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.provider;
import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smack.util.stringencoder.Base64;
import org.jivesoftware.smackx.omemo.element.OmemoVAxolotlElement;
import org.xmlpull.v1.XmlPullParser;
import java.util.ArrayList;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.ENCRYPTED;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.HEADER;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.IV;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.KEY;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.PAYLOAD;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.PREKEY;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.RID;
import static org.jivesoftware.smackx.omemo.element.OmemoElement.SID;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
/**
* Smack ExtensionProvider that parses incoming OMEMO Message element into OmemoMessageElement objects.
*
* @author Paul Schaub
*/
public class OmemoVAxolotlProvider extends ExtensionElementProvider<OmemoVAxolotlElement> {
@Override
public OmemoVAxolotlElement parse(XmlPullParser parser, int initialDepth) throws Exception {
boolean inEncrypted = true;
int sid = -1;
ArrayList<OmemoVAxolotlElement.OmemoHeader.Key> keys = new ArrayList<>();
byte[] iv = null;
byte[] payload = null;
while (inEncrypted) {
int tag = parser.next();
String name = parser.getName();
switch (tag) {
case START_TAG:
switch (name) {
case HEADER:
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).equals(SID)) {
sid = Integer.parseInt(parser.getAttributeValue(i));
}
}
break;
case KEY:
boolean prekey = false;
int rid = -1;
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).equals(PREKEY)) {
prekey = Boolean.parseBoolean(parser.getAttributeValue(i));
} else if (parser.getAttributeName(i).equals(RID)) {
rid = Integer.parseInt(parser.getAttributeValue(i));
}
}
keys.add(new OmemoVAxolotlElement.OmemoHeader.Key(Base64.decode(parser.nextText()), rid, prekey));
break;
case IV:
iv = Base64.decode(parser.nextText());
break;
case PAYLOAD:
payload = Base64.decode(parser.nextText());
break;
}
break;
case END_TAG:
if (name.equals(ENCRYPTED)) {
inEncrypted = false;
}
break;
}
}
OmemoVAxolotlElement.OmemoHeader header = new OmemoVAxolotlElement.OmemoHeader(sid, keys, iv);
return new OmemoVAxolotlElement(header, payload);
}
}

View file

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

View file

@ -0,0 +1,63 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.util;
/**
* Some constants related to OMEMO.
* @author Paul Schaub
*/
public final class OmemoConstants {
//Constants
/**
* Omemo related namespace.
*/
public static final String OMEMO_NAMESPACE_V_AXOLOTL = "eu.siacs.conversations.axolotl";
public static final String OMEMO = "OMEMO";
//PubSub Node names
public static final String PEP_NODE_DEVICE_LIST = OMEMO_NAMESPACE_V_AXOLOTL + ".devicelist";
public static final String PEP_NODE_DEVICE_LIST_NOTIFY = PEP_NODE_DEVICE_LIST + "+notify";
public static final String PEP_NODE_BUNDLES = OMEMO_NAMESPACE_V_AXOLOTL + ".bundles";
/**
* How many preKeys do we want to publish?
*/
public static final int TARGET_PRE_KEY_COUNT = 100;
/**
* Return the node name of the PEP node containing the device bundle of the device with device id deviceId.
*
* @param deviceId id of the device
* @return node name of the devices bundle node
*/
public static String PEP_NODE_BUNDLE_FROM_DEVICE_ID(int deviceId) {
return PEP_NODE_BUNDLES + ":" + deviceId;
}
public static final String BODY_OMEMO_HINT = "I sent you an OMEMO encrypted message but your client doesnt seem to support that. Find more information on https://conversations.im/omemo";
/**
* Information about the keys used for message encryption.
*/
public static final class Crypto {
public static final String KEYTYPE = "AES";
public static final int KEYLENGTH = 128;
public static final String CIPHERMODE = "AES/GCM/NoPadding";
public static final String PROVIDER = "BC";
}
}

View file

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

View file

@ -0,0 +1,252 @@
/**
*
* Copyright 2017 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.omemo.util;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.omemo.OmemoManager;
import org.jivesoftware.smackx.omemo.OmemoStore;
import org.jivesoftware.smackx.omemo.element.OmemoVAxolotlElement;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException;
import org.jivesoftware.smackx.omemo.internal.CiphertextTuple;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.internal.OmemoSession;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.util.ArrayList;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.CIPHERMODE;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYLENGTH;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.PROVIDER;
/**
* Class used to build OMEMO messages.
*
* @param <T_IdKeyPair> IdentityKeyPair class
* @param <T_IdKey> IdentityKey class
* @param <T_PreKey> PreKey class
* @param <T_SigPreKey> SignedPreKey class
* @param <T_Sess> Session class
* @param <T_Addr> Address class
* @param <T_ECPub> Elliptic Curve PublicKey class
* @param <T_Bundle> Bundle class
* @param <T_Ciph> Cipher class
* @author Paul Schaub
*/
public class OmemoMessageBuilder<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> {
private final OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore;
private final OmemoManager omemoManager;
private byte[] messageKey = generateKey();
private byte[] initializationVector = generateIv();
private byte[] ciphertextMessage;
private final ArrayList<OmemoVAxolotlElement.OmemoHeader.Key> keys = new ArrayList<>();
/**
* Create a OmemoMessageBuilder.
*
* @param omemoManager OmemoManager of our device.
* @param omemoStore OmemoStore.
* @param aesKey AES key that will be transported to the recipient. This is used eg. to encrypt the body.
* @param iv IV
* @throws NoSuchPaddingException
* @throws BadPaddingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
* @throws IllegalBlockSizeException
* @throws UnsupportedEncodingException
* @throws NoSuchProviderException
* @throws InvalidAlgorithmParameterException
*/
public OmemoMessageBuilder(OmemoManager omemoManager,
OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore,
byte[] aesKey, byte[] iv)
throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException,
UnsupportedEncodingException, NoSuchProviderException, InvalidAlgorithmParameterException {
this.omemoStore = omemoStore;
this.omemoManager = omemoManager;
this.messageKey = aesKey;
this.initializationVector = iv;
}
/**
* Create a new OmemoMessageBuilder with random IV and AES key.
*
* @param omemoManager omemoManager of our device.
* @param omemoStore omemoStore.
* @param message Messages body.
* @throws NoSuchPaddingException
* @throws BadPaddingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
* @throws IllegalBlockSizeException
* @throws UnsupportedEncodingException
* @throws NoSuchProviderException
* @throws InvalidAlgorithmParameterException
*/
public OmemoMessageBuilder(OmemoManager omemoManager,
OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore, String message)
throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException,
UnsupportedEncodingException, NoSuchProviderException, InvalidAlgorithmParameterException {
this.omemoManager = omemoManager;
this.omemoStore = omemoStore;
this.setMessage(message);
}
/**
* Create an AES messageKey and use it to encrypt the message.
* Optionally append the Auth Tag of the encrypted message to the messageKey afterwards.
*
* @param message content of the message
* @throws NoSuchPaddingException When no Cipher could be instantiated.
* @throws NoSuchAlgorithmException when no Cipher could be instantiated.
* @throws NoSuchProviderException when BouncyCastle could not be found.
* @throws InvalidAlgorithmParameterException when the Cipher could not be initialized
* @throws InvalidKeyException when the generated key is invalid
* @throws UnsupportedEncodingException when UTF8 is unavailable
* @throws BadPaddingException when cipher.doFinal gets wrong padding
* @throws IllegalBlockSizeException when cipher.doFinal gets wrong Block size.
*/
public void setMessage(String message) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, InvalidKeyException, UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException {
if (message == null) {
return;
}
//Encrypt message body
SecretKey secretKey = new SecretKeySpec(messageKey, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(initializationVector);
Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
byte[] body;
byte[] ciphertext;
body = (message.getBytes(StringUtils.UTF8));
ciphertext = cipher.doFinal(body);
byte[] clearKeyWithAuthTag = new byte[messageKey.length + 16];
byte[] cipherTextWithoutAuthTag = new byte[ciphertext.length - 16];
System.arraycopy(messageKey, 0, clearKeyWithAuthTag, 0, 16);
System.arraycopy(ciphertext, 0, cipherTextWithoutAuthTag, 0, cipherTextWithoutAuthTag.length);
System.arraycopy(ciphertext, ciphertext.length - 16, clearKeyWithAuthTag, 16, 16);
ciphertextMessage = cipherTextWithoutAuthTag;
messageKey = clearKeyWithAuthTag;
}
/**
* Add a new recipient device to the message.
*
* @param device recipient device
* @throws CryptoFailedException when encrypting the messageKey fails
* @throws UndecidedOmemoIdentityException
* @throws CorruptedOmemoKeyException
*/
public void addRecipient(OmemoDevice device) throws CryptoFailedException, UndecidedOmemoIdentityException, CorruptedOmemoKeyException {
addRecipient(device, false);
}
/**
* Add a new recipient device to the message.
* @param device recipient device
* @param ignoreTrust ignore current trust state? Useful for keyTransportMessages that are sent to repair a session
* @throws CryptoFailedException
* @throws UndecidedOmemoIdentityException
* @throws CorruptedOmemoKeyException
*/
public void addRecipient(OmemoDevice device, boolean ignoreTrust) throws
CryptoFailedException, UndecidedOmemoIdentityException, CorruptedOmemoKeyException {
OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> session =
omemoStore.getOmemoSessionOf(omemoManager, device);
if (session != null) {
if (!ignoreTrust && !omemoStore.isDecidedOmemoIdentity(omemoManager, device, session.getIdentityKey())) {
//Warn user of undecided device
throw new UndecidedOmemoIdentityException(device);
}
if (!ignoreTrust && omemoStore.isTrustedOmemoIdentity(omemoManager, device, session.getIdentityKey())) {
//Encrypt key and save to header
CiphertextTuple encryptedKey = session.encryptMessageKey(messageKey);
keys.add(new OmemoVAxolotlElement.OmemoHeader.Key(encryptedKey.getCiphertext(), device.getDeviceId(), encryptedKey.isPreKeyMessage()));
}
}
}
/**
* Assemble an OmemoMessageElement from the current state of the builder.
*
* @return OmemoMessageElement
*/
public OmemoVAxolotlElement finish() {
OmemoVAxolotlElement.OmemoHeader header = new OmemoVAxolotlElement.OmemoHeader(
omemoManager.getDeviceId(),
keys,
initializationVector
);
return new OmemoVAxolotlElement(header, ciphertextMessage);
}
/**
* Generate a new AES key used to encrypt the message.
*
* @return new AES key
* @throws NoSuchAlgorithmException
*/
public static byte[] generateKey() throws NoSuchAlgorithmException {
KeyGenerator generator = KeyGenerator.getInstance(KEYTYPE);
generator.init(KEYLENGTH);
return generator.generateKey().getEncoded();
}
/**
* Generate a 16 byte initialization vector for AES encryption.
*
* @return iv
*/
public static byte[] generateIv() {
SecureRandom random = new SecureRandom();
byte[] iv = new byte[16];
random.nextBytes(iv);
return iv;
}
public byte[] getCiphertextMessage() {
return ciphertextMessage;
}
public byte[] getMessageKey() {
return messageKey;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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