Merge branch '4.4'

This commit is contained in:
Florian Schmaus 2021-03-19 09:47:07 +01:00
commit 4fefa92e40
22 changed files with 288 additions and 132 deletions

View File

@ -148,7 +148,7 @@ allprojects {
jxmppVersion = '[1.0.0, 1.0.999]' jxmppVersion = '[1.0.0, 1.0.999]'
miniDnsVersion = '[1.0.0, 1.0.999]' miniDnsVersion = '[1.0.0, 1.0.999]'
smackMinAndroidSdk = 19 smackMinAndroidSdk = 19
junitVersion = '5.6.2' junitVersion = '5.7.1'
commonsIoVersion = '2.6' commonsIoVersion = '2.6'
bouncyCastleVersion = '1.68' bouncyCastleVersion = '1.68'
guavaVersion = '30.1-jre' guavaVersion = '30.1-jre'

View File

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2015 Florian Schmaus * Copyright 2015-2021 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -44,11 +44,20 @@ public abstract class AbstractListFilter implements StanzaFilter {
* @param filters the filters to add. * @param filters the filters to add.
*/ */
protected AbstractListFilter(StanzaFilter... filters) { protected AbstractListFilter(StanzaFilter... filters) {
this(new ArrayList<StanzaFilter>(Arrays.asList(filters)));
}
/**
* Creates an filter using the specified filters.
*
* @param filters the filters to add.
*/
protected AbstractListFilter(List<StanzaFilter> filters) {
Objects.requireNonNull(filters, "Parameter must not be null."); Objects.requireNonNull(filters, "Parameter must not be null.");
for (StanzaFilter filter : filters) { for (StanzaFilter filter : filters) {
Objects.requireNonNull(filter, "Parameter must not be null."); Objects.requireNonNull(filter, "Parameter must not be null.");
} }
this.filters = new ArrayList<StanzaFilter>(Arrays.asList(filters)); this.filters = filters;
} }
/** /**

View File

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2003-2007 Jive Software. * Copyright 2003-2007 Jive Software, 2021 Florian Schmaus.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,6 +17,8 @@
package org.jivesoftware.smack.filter; package org.jivesoftware.smack.filter;
import java.util.List;
import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.Stanza;
/** /**
@ -44,6 +46,15 @@ public class AndFilter extends AbstractListFilter implements StanzaFilter {
super(filters); super(filters);
} }
/**
* Creates an AND filter using the specified filters.
*
* @param filters the filters to add.
*/
public AndFilter(List<StanzaFilter> filters) {
super(filters);
}
@Override @Override
public boolean accept(Stanza packet) { public boolean accept(Stanza packet) {
for (StanzaFilter filter : filters) { for (StanzaFilter filter : filters) {

View File

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2003-2007 Jive Software, 2016-2019 Florian Schmaus. * Copyright 2003-2007 Jive Software, 2016-2021 Florian Schmaus.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -47,4 +47,8 @@ public class RandomUtil {
public static int nextSecureRandomInt() { public static int nextSecureRandomInt() {
return SECURE_RANDOM.get().nextInt(); return SECURE_RANDOM.get().nextInt();
} }
public static void fillWithSecureRandom(byte[] bytes) {
SECURE_RANDOM.get().nextBytes(bytes);
}
} }

View File

@ -183,6 +183,10 @@ public abstract class FileTransfer {
protected void setException(Exception exception) { protected void setException(Exception exception) {
this.exception = exception; this.exception = exception;
Status currentStatus = getStatus();
if (currentStatus != Status.error) {
updateStatus(currentStatus, Status.error);
}
} }
protected void setStatus(Status status) { protected void setStatus(Status status) {

View File

@ -40,6 +40,7 @@ import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.filetransfer.FileTransferException.NoAcceptableTransferMechanisms; import org.jivesoftware.smackx.filetransfer.FileTransferException.NoAcceptableTransferMechanisms;
import org.jivesoftware.smackx.filetransfer.FileTransferException.NoStreamMethodsOfferedException; import org.jivesoftware.smackx.filetransfer.FileTransferException.NoStreamMethodsOfferedException;
import org.jivesoftware.smackx.formtypes.FormFieldRegistry;
import org.jivesoftware.smackx.si.packet.StreamInitiation; import org.jivesoftware.smackx.si.packet.StreamInitiation;
import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.ListSingleFormField; import org.jivesoftware.smackx.xdata.ListSingleFormField;
@ -66,6 +67,9 @@ public final class FileTransferNegotiator extends Manager {
private static final String STREAM_INIT_PREFIX = "jsi_"; private static final String STREAM_INIT_PREFIX = "jsi_";
static final String STREAM_DATA_FIELD_NAME = "stream-method"; static final String STREAM_DATA_FIELD_NAME = "stream-method";
static {
FormFieldRegistry.addLookasideFieldRegistryEntry(STREAM_DATA_FIELD_NAME, FormField.Type.list_single);
}
private static final Random randomGenerator = new Random(); private static final Random randomGenerator = new Random();

View File

@ -324,6 +324,10 @@ public class OutgoingFileTransfer extends FileTransfer {
transferThread.start(); transferThread.start();
} }
public void setCallback(NegotiationProgress negotiationProcess) {
this.callback = negotiationProcess;
}
private void handleXMPPException(XMPPErrorException e) { private void handleXMPPException(XMPPErrorException e) {
StanzaError error = e.getStanzaError(); StanzaError error = e.getStanzaError();
if (error != null) { if (error != null) {

View File

@ -36,6 +36,8 @@ public class FormFieldRegistry {
private static final Map<String, FormField.Type> CLARK_NOTATION_FIELD_REGISTRY = new ConcurrentHashMap<>(); private static final Map<String, FormField.Type> CLARK_NOTATION_FIELD_REGISTRY = new ConcurrentHashMap<>();
private static final Map<String, FormField.Type> LOOKASIDE_FIELD_REGISTRY = new ConcurrentHashMap<>();
@SuppressWarnings("ReferenceEquality") @SuppressWarnings("ReferenceEquality")
public static void register(DataForm dataForm) { public static void register(DataForm dataForm) {
// TODO: Also allow forms of type 'result'? // TODO: Also allow forms of type 'result'?
@ -98,11 +100,11 @@ public class FormFieldRegistry {
public static FormField.Type lookup(String formType, String fieldName) { public static FormField.Type lookup(String formType, String fieldName) {
if (formType == null) { if (formType == null) {
if (!XmlUtil.isClarkNotation(fieldName)) { if (XmlUtil.isClarkNotation(fieldName)) {
return null; return CLARK_NOTATION_FIELD_REGISTRY.get(fieldName);
} }
return CLARK_NOTATION_FIELD_REGISTRY.get(fieldName); return LOOKASIDE_FIELD_REGISTRY.get(fieldName);
} }
synchronized (REGISTRY) { synchronized (REGISTRY) {
@ -122,4 +124,7 @@ public class FormFieldRegistry {
return lookup(null, fieldName); return lookup(null, fieldName);
} }
public static void addLookasideFieldRegistryEntry(String fieldName, FormField.Type formFieldType) {
LOOKASIDE_FIELD_REGISTRY.put(fieldName, formFieldType);
}
} }

View File

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2003-2007 Jive Software. 2020 Florian Schmaus * Copyright 2003-2007 Jive Software. 2020-2021 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -149,6 +149,8 @@ public class MultiUserChat {
private EntityFullJid myRoomJid; private EntityFullJid myRoomJid;
private StanzaCollector messageCollector; private StanzaCollector messageCollector;
private DiscoverInfo mucServiceDiscoInfo;
/** /**
* Used to signal that the reflected self-presence was received <b>and</b> processed by us. * Used to signal that the reflected self-presence was received <b>and</b> processed by us.
*/ */
@ -342,7 +344,8 @@ public class MultiUserChat {
private Presence enter(MucEnterConfiguration conf) throws NotConnectedException, NoResponseException, private Presence enter(MucEnterConfiguration conf) throws NotConnectedException, NoResponseException,
XMPPErrorException, InterruptedException, NotAMucServiceException { XMPPErrorException, InterruptedException, NotAMucServiceException {
final DomainBareJid mucService = room.asDomainBareJid(); final DomainBareJid mucService = room.asDomainBareJid();
if (!multiUserChatManager.providesMucService(mucService)) { mucServiceDiscoInfo = multiUserChatManager.getMucServiceDiscoInfo(mucService);
if (mucServiceDiscoInfo == null) {
throw new NotAMucServiceException(this); throw new NotAMucServiceException(this);
} }
// We enter a room by sending a presence packet where the "to" // We enter a room by sending a presence packet where the "to"
@ -757,6 +760,10 @@ public class MultiUserChat {
throw new MucNotJoinedException(this); throw new MucNotJoinedException(this);
} }
// TODO: Consider adding a origin-id to the presence, once it is moved form smack-experimental into
// smack-extensions, in case the MUC service does not support stable IDs, and modify
// reflectedLeavePresenceFilters accordingly.
// We leave a room by sending a presence packet where the "to" // We leave a room by sending a presence packet where the "to"
// field is in the form "roomName@service/nickname" // field is in the form "roomName@service/nickname"
Presence leavePresence = connection.getStanzaFactory().buildPresenceStanza() Presence leavePresence = connection.getStanzaFactory().buildPresenceStanza()
@ -764,14 +771,19 @@ public class MultiUserChat {
.to(myRoomJid) .to(myRoomJid)
.build(); .build();
StanzaFilter reflectedLeavePresenceFilter = new AndFilter( List<StanzaFilter> reflectedLeavePresenceFilters = new ArrayList<>(3);
StanzaTypeFilter.PRESENCE, reflectedLeavePresenceFilters.add(StanzaTypeFilter.PRESENCE);
new StanzaIdFilter(leavePresence), reflectedLeavePresenceFilters.add(new OrFilter(
new OrFilter( new AndFilter(FromMatchesFilter.createFull(myRoomJid), PresenceTypeFilter.UNAVAILABLE,
new AndFilter(FromMatchesFilter.createFull(myRoomJid), PresenceTypeFilter.UNAVAILABLE, MUCUserStatusCodeFilter.STATUS_110_PRESENCE_TO_SELF), MUCUserStatusCodeFilter.STATUS_110_PRESENCE_TO_SELF),
new AndFilter(fromRoomFilter, PresenceTypeFilter.ERROR) new AndFilter(fromRoomFilter, PresenceTypeFilter.ERROR)));
)
); boolean supportsStableId = mucServiceDiscoInfo.containsFeature(MultiUserChatConstants.STABLE_ID_FEATURE);
if (supportsStableId) {
reflectedLeavePresenceFilters.add(new StanzaIdFilter(leavePresence));
}
StanzaFilter reflectedLeavePresenceFilter = new AndFilter(reflectedLeavePresenceFilters);
// Reset occupant information first so that we are assume that we left the room even if sendStanza() would // Reset occupant information first so that we are assume that we left the room even if sendStanza() would
// throw. // throw.

View File

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2020 Florian Schmaus * Copyright 2020-2021 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -20,4 +20,6 @@ public class MultiUserChatConstants {
public static final String NAMESPACE = "http://jabber.org/protocol/muc"; public static final String NAMESPACE = "http://jabber.org/protocol/muc";
public static final String STABLE_ID_FEATURE = NAMESPACE + "#stable_id";
} }

View File

@ -1,6 +1,6 @@
/** /**
* *
* Copyright © 2014-2020 Florian Schmaus * Copyright © 2014-2021 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -137,7 +137,7 @@ public final class MultiUserChatManager extends Manager {
private static final StanzaFilter INVITATION_FILTER = new AndFilter(StanzaTypeFilter.MESSAGE, new StanzaExtensionFilter(new MUCUser()), private static final StanzaFilter INVITATION_FILTER = new AndFilter(StanzaTypeFilter.MESSAGE, new StanzaExtensionFilter(new MUCUser()),
new NotFilter(MessageTypeFilter.ERROR)); new NotFilter(MessageTypeFilter.ERROR));
private static final ExpirationCache<DomainBareJid, Void> KNOWN_MUC_SERVICES = new ExpirationCache<>( private static final ExpirationCache<DomainBareJid, DiscoverInfo> KNOWN_MUC_SERVICES = new ExpirationCache<>(
100, 1000 * 60 * 60 * 24); 100, 1000 * 60 * 60 * 24);
private final Set<InvitationListener> invitationsListeners = new CopyOnWriteArraySet<InvitationListener>(); private final Set<InvitationListener> invitationsListeners = new CopyOnWriteArraySet<InvitationListener>();
@ -396,16 +396,23 @@ public final class MultiUserChatManager extends Manager {
*/ */
public boolean providesMucService(DomainBareJid domainBareJid) throws NoResponseException, public boolean providesMucService(DomainBareJid domainBareJid) throws NoResponseException,
XMPPErrorException, NotConnectedException, InterruptedException { XMPPErrorException, NotConnectedException, InterruptedException {
boolean contains = KNOWN_MUC_SERVICES.containsKey(domainBareJid); return getMucServiceDiscoInfo(domainBareJid) != null;
if (!contains) { }
if (serviceDiscoveryManager.supportsFeature(domainBareJid,
MUCInitialPresence.NAMESPACE)) { DiscoverInfo getMucServiceDiscoInfo(DomainBareJid mucServiceAddress)
KNOWN_MUC_SERVICES.put(domainBareJid, null); throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
return true; DiscoverInfo discoInfo = KNOWN_MUC_SERVICES.get(mucServiceAddress);
} if (discoInfo != null) {
return discoInfo;
} }
return contains; discoInfo = serviceDiscoveryManager.discoverInfo(mucServiceAddress);
if (!discoInfo.containsFeature(MUCInitialPresence.NAMESPACE)) {
return null;
}
KNOWN_MUC_SERVICES.put(mucServiceAddress, discoInfo);
return discoInfo;
} }
/** /**

View File

@ -16,6 +16,7 @@
*/ */
package org.igniterealtime.smack; package org.igniterealtime.smack;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -90,7 +91,13 @@ public class XmppConnectionStressTest {
MessageBuilder messageBuilder = fromConnection.getStanzaFactory().buildMessageStanza(); MessageBuilder messageBuilder = fromConnection.getStanzaFactory().buildMessageStanza();
messageBuilder.to(toConnection.getUser()); messageBuilder.to(toConnection.getUser());
int payloadChunkCount = random.nextInt(configuration.maxPayloadChunks) + 1; final int payloadChunkCount;
if (configuration.maxPayloadChunks == 0) {
payloadChunkCount = 0;
} else {
payloadChunkCount = random.nextInt(configuration.maxPayloadChunks) + 1;
}
for (int c = 0; c < payloadChunkCount; c++) { for (int c = 0; c < payloadChunkCount; c++) {
int payloadChunkSize = random.nextInt(configuration.maxPayloadChunkSize) + 1; int payloadChunkSize = random.nextInt(configuration.maxPayloadChunkSize) + 1;
String payloadCunk = StringUtils.randomString(payloadChunkSize, random); String payloadCunk = StringUtils.randomString(payloadChunkSize, random);
@ -128,6 +135,13 @@ public class XmppConnectionStressTest {
Map<XMPPConnection, Map<EntityFullJid, boolean[]>> receiveMarkers = new ConcurrentHashMap<>(connections.size()); Map<XMPPConnection, Map<EntityFullJid, boolean[]>> receiveMarkers = new ConcurrentHashMap<>(connections.size());
for (XMPPConnection connection : connections) { for (XMPPConnection connection : connections) {
final Map<EntityFullJid, boolean[]> myReceiveMarkers = new HashMap<>(connections.size());
receiveMarkers.put(connection, myReceiveMarkers);
for (XMPPConnection otherConnection : connections) {
boolean[] fromMarkers = new boolean[configuration.messagesPerConnection];
myReceiveMarkers.put(otherConnection.getUser(), fromMarkers);
}
connection.addSyncStanzaListener(new StanzaListener() { connection.addSyncStanzaListener(new StanzaListener() {
@Override @Override
public void processStanza(Stanza stanza) { public void processStanza(Stanza stanza) {
@ -139,17 +153,7 @@ public class XmppConnectionStressTest {
Integer messageNumber = (Integer) extension.getProperty(MESSAGE_NUMBER_PROPERTY); Integer messageNumber = (Integer) extension.getProperty(MESSAGE_NUMBER_PROPERTY);
Map<EntityFullJid, boolean[]> myReceiveMarkers = receiveMarkers.get(connection);
if (myReceiveMarkers == null) {
myReceiveMarkers = new HashMap<>(connections.size());
receiveMarkers.put(connection, myReceiveMarkers);
}
boolean[] fromMarkers = myReceiveMarkers.get(from); boolean[] fromMarkers = myReceiveMarkers.get(from);
if (fromMarkers == null) {
fromMarkers = new boolean[configuration.messagesPerConnection];
myReceiveMarkers.put(from, fromMarkers);
}
// Sanity check: All markers before must be true, all markers including the messageNumber marker must be false. // Sanity check: All markers before must be true, all markers including the messageNumber marker must be false.
for (int i = 0; i < fromMarkers.length; i++) { for (int i = 0; i < fromMarkers.length; i++) {
@ -173,7 +177,9 @@ public class XmppConnectionStressTest {
exceptionMessage.append(i); exceptionMessage.append(i);
exceptionMessage.append("\nMessage with id ").append(stanza.getStanzaId()) exceptionMessage.append("\nMessage with id ").append(stanza.getStanzaId())
.append(" from ").append(from) .append(" from ").append(from)
.append(" to ").append(stanza.getTo()); .append(" to ").append(stanza.getTo())
.append('\n');
exceptionMessage.append("From Markers: ").append(Arrays.toString(fromMarkers)).append('\n');
Exception exception = new Exception(exceptionMessage.toString()); Exception exception = new Exception(exceptionMessage.toString());
receiveExceptions.put(connection, exception); receiveExceptions.put(connection, exception);
@ -188,15 +194,14 @@ public class XmppConnectionStressTest {
fromMarkers[messageNumber] = true; fromMarkers[messageNumber] = true;
if (myReceiveMarkers.size() != connections.size()) {
return;
}
for (boolean[] markers : myReceiveMarkers.values()) { for (boolean[] markers : myReceiveMarkers.values()) {
if (BooleansUtils.contains(markers, false)) { if (BooleansUtils.contains(markers, false)) {
// There is at least one message we did not receive yet, therefore do not signal the
// receivedSemaphore.
return; return;
} }
} }
// All markers set to true, this means we received all messages. // All markers set to true, this means we received all messages.
receivedSemaphore.release(); receivedSemaphore.release();
} }
@ -276,6 +281,7 @@ public class XmppConnectionStressTest {
XMPPConnection connection = connections.get(i); XMPPConnection connection = connections.get(i);
EntityFullJid connectionAddress = connection.getUser(); EntityFullJid connectionAddress = connection.getUser();
connectionIds.put(connectionAddress, i); connectionIds.put(connectionAddress, i);
sb.append(i).append(": ").append(connection).append('\n');
} }
for (Map.Entry<XMPPConnection, Map<EntityFullJid, boolean[]>> entry : receiveMarkers.entrySet()) { for (Map.Entry<XMPPConnection, Map<EntityFullJid, boolean[]>> entry : receiveMarkers.entrySet()) {
@ -295,7 +301,7 @@ public class XmppConnectionStressTest {
sb.append(markerToConnectionId) sb.append(markerToConnectionId)
.append(" is missing ").append(numberOfFalseMarkers) .append(" is missing ").append(numberOfFalseMarkers)
.append(" messages from ").append(markerFromConnectionId) .append(" messages from ").append(markerFromConnectionId)
.append(" :"); .append(": ");
for (int i = 0; i < marker.length; i++) { for (int i = 0; i < marker.length; i++) {
if (marker[i]) { if (marker[i]) {
continue; continue;

View File

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2015-2020 Florian Schmaus * Copyright 2015-2021 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -26,6 +26,7 @@ import java.util.Arrays;
import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.filetransfer.FileTransfer.Status;
import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
@ -98,9 +99,12 @@ public class FileTransferIntegrationTest extends AbstractSmackIntegrationTest {
oft.sendStream(new ByteArrayInputStream(dataToSend), "hello.txt", dataToSend.length, "A greeting"); oft.sendStream(new ByteArrayInputStream(dataToSend), "hello.txt", dataToSend.length, "A greeting");
int duration = 0; int duration = 0;
while (!oft.isDone()) { while (!oft.isDone()) {
switch (oft.getStatus()) { Status status = oft.getStatus();
switch (status) {
case error: case error:
throw new Exception("FileTransfer error: " + oft.getError()); FileTransfer.Error error = oft.getError();
Exception exception = oft.getException();
throw new Exception("FileTransfer error: " + error, exception);
default: default:
LOGGER.info("FileTransfer status: " + oft.getStatus() + ". Progress: " + oft.getProgress()); LOGGER.info("FileTransfer status: " + oft.getStatus() + ". Progress: " + oft.getProgress());
break; break;

View File

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2017 Paul Schaub * Copyright 2017 Paul Schaub, 2021 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,7 +17,9 @@
package org.jivesoftware.smackx.omemo; package org.jivesoftware.smackx.omemo;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.logging.Level; import java.util.logging.Level;
@ -25,6 +27,7 @@ import java.util.logging.Logger;
import javax.crypto.BadPaddingException; import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException; import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import org.jivesoftware.smackx.omemo.element.OmemoElement; import org.jivesoftware.smackx.omemo.element.OmemoElement;
import org.jivesoftware.smackx.omemo.element.OmemoKeyElement; import org.jivesoftware.smackx.omemo.element.OmemoKeyElement;
@ -172,11 +175,10 @@ public abstract class OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey,
byte[] encryptedBody = payloadAndAuthTag(element, cipherAndAuthTag.getAuthTag()); byte[] encryptedBody = payloadAndAuthTag(element, cipherAndAuthTag.getAuthTag());
try { try {
String plaintext = new String(cipherAndAuthTag.getCipher().doFinal(encryptedBody), StandardCharsets.UTF_8); return cipherAndAuthTag.decrypt(encryptedBody);
return plaintext; } catch (IllegalBlockSizeException | BadPaddingException | InvalidKeyException | NoSuchAlgorithmException
} catch (IllegalBlockSizeException | BadPaddingException e) { | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
throw new CryptoFailedException("decryptMessageElement could not decipher message body: " throw new CryptoFailedException("decryptMessageElement could not decipher message body", e);
+ e.getMessage());
} }
} }

View File

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2017 Paul Schaub * Copyright 2017 Paul Schaub, 2021 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,10 +16,6 @@
*/ */
package org.jivesoftware.smackx.omemo.exceptions; package org.jivesoftware.smackx.omemo.exceptions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** /**
* Exception gets thrown when some cryptographic function failed. * Exception gets thrown when some cryptographic function failed.
* *
@ -27,20 +23,18 @@ import java.util.List;
*/ */
public class CryptoFailedException extends Exception { public class CryptoFailedException extends Exception {
private static final long serialVersionUID = 3466888654338119924L; private static final long serialVersionUID = 1;
private final ArrayList<Exception> exceptions = new ArrayList<>(); public CryptoFailedException(String message, Exception wrappedException) {
super(message, wrappedException);
}
public CryptoFailedException(String message) { public CryptoFailedException(String message) {
super(message); this(message, null);
} }
public CryptoFailedException(Exception e) { public CryptoFailedException(Exception e) {
super(e); this("Crypto failed " + e.getMessage(), e);
exceptions.add(e);
} }
public List<Exception> getExceptions() {
return Collections.unmodifiableList(exceptions);
}
} }

View File

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2017 Paul Schaub, 2019 Florian Schmaus * Copyright 2017 Paul Schaub, 2019-2021 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,18 +16,14 @@
*/ */
package org.jivesoftware.smackx.omemo.internal; package org.jivesoftware.smackx.omemo.internal;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.CIPHERMODE; import java.nio.charset.StandardCharsets;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher; import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException; import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
/** /**
* Encapsulate Cipher and AuthTag. * Encapsulate Cipher and AuthTag.
@ -45,21 +41,10 @@ public class CipherAndAuthTag {
this.wasPreKey = wasPreKey; this.wasPreKey = wasPreKey;
} }
public Cipher getCipher() throws CryptoFailedException { public String decrypt(byte[] ciphertext) throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException,
NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException {
Cipher cipher; byte[] plaintext = OmemoAesCipher.decryptAesGcmNoPadding(ciphertext, key, iv);
try { return new String(plaintext, StandardCharsets.UTF_8);
cipher = Cipher.getInstance(CIPHERMODE);
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 e) {
throw new CryptoFailedException(e);
}
return cipher;
} }
public byte[] getAuthTag() { public byte[] getAuthTag() {

View File

@ -0,0 +1,99 @@
/**
*
* Copyright 2017 Paul Schaub, 2019-2021 Florian Schmaus
*
* 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.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.jivesoftware.smack.util.RandomUtil;
import org.jivesoftware.smackx.omemo.util.OmemoConstants;
import org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder;
public class OmemoAesCipher {
static {
byte[] iv = OmemoMessageBuilder.generateIv();
byte[] key = new byte[16];
RandomUtil.fillWithSecureRandom(key);
try {
encryptAesGcmNoPadding("This is just a test", key, iv);
} catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException
| InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
String message = "Unable to perform " + OmemoConstants.Crypto.CIPHERMODE
+ " operation requires by OMEMO. Ensure that a suitable crypto provider for is available."
+ " For example Bouncycastle on Android (BouncyCastleProvider)";
throw new AssertionError(message);
}
}
private enum CipherOpmode {
encrypt(Cipher.ENCRYPT_MODE),
decrypt(Cipher.DECRYPT_MODE),
;
public final int opmodeInt;
CipherOpmode(int opmodeInt) {
this.opmodeInt = opmodeInt;
}
}
private static byte[] performCipherOperation(CipherOpmode opmode, byte[] input, byte[] key,
byte[] initializationVector)
throws IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException,
NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
SecretKey secretKey = new SecretKeySpec(key, OmemoConstants.Crypto.KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(initializationVector);
Cipher cipher = Cipher.getInstance(OmemoConstants.Crypto.CIPHERMODE);
cipher.init(opmode.opmodeInt, secretKey, ivSpec);
byte[] ciphertext = cipher.doFinal(input);
return ciphertext;
}
public static byte[] decryptAesGcmNoPadding(byte[] ciphertext, byte[] key, byte[] initializationVector)
throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException,
NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException {
return performCipherOperation(CipherOpmode.decrypt, ciphertext, key, initializationVector);
}
public static byte[] encryptAesGcmNoPadding(byte[] plaintext, byte[] key, byte[] initializationVector)
throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
return performCipherOperation(CipherOpmode.encrypt, plaintext, key, initializationVector);
}
public static byte[] encryptAesGcmNoPadding(String plaintext, byte[] key, byte[] initializationVector)
throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
return encryptAesGcmNoPadding(plaintextBytes, key, initializationVector);
}
}

View File

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2017 Paul Schaub, 2019 Florian Schmaus * Copyright 2017 Paul Schaub, 2019-2021 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,26 +16,20 @@
*/ */
package org.jivesoftware.smackx.omemo.util; package org.jivesoftware.smackx.omemo.util;
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.KEYLENGTH;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE; import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList; import java.util.ArrayList;
import javax.crypto.BadPaddingException; import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException; import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator; import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException; import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.jivesoftware.smack.util.RandomUtil;
import org.jivesoftware.smackx.omemo.OmemoRatchet; import org.jivesoftware.smackx.omemo.OmemoRatchet;
import org.jivesoftware.smackx.omemo.OmemoService; import org.jivesoftware.smackx.omemo.OmemoService;
import org.jivesoftware.smackx.omemo.element.OmemoElement; import org.jivesoftware.smackx.omemo.element.OmemoElement;
@ -47,6 +41,7 @@ import org.jivesoftware.smackx.omemo.exceptions.NoIdentityKeyException;
import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException; import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException;
import org.jivesoftware.smackx.omemo.exceptions.UntrustedOmemoIdentityException; import org.jivesoftware.smackx.omemo.exceptions.UntrustedOmemoIdentityException;
import org.jivesoftware.smackx.omemo.internal.CiphertextTuple; import org.jivesoftware.smackx.omemo.internal.CiphertextTuple;
import org.jivesoftware.smackx.omemo.internal.OmemoAesCipher;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
import org.jivesoftware.smackx.omemo.trust.OmemoTrustCallback; import org.jivesoftware.smackx.omemo.trust.OmemoTrustCallback;
@ -159,16 +154,7 @@ public class OmemoMessageBuilder<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_
} }
// Encrypt message body // Encrypt message body
SecretKey secretKey = new SecretKeySpec(messageKey, KEYTYPE); byte[] ciphertext = OmemoAesCipher.encryptAesGcmNoPadding(message, messageKey, initializationVector);
IvParameterSpec ivSpec = new IvParameterSpec(initializationVector);
Cipher cipher = Cipher.getInstance(CIPHERMODE);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
byte[] body;
byte[] ciphertext;
body = message.getBytes(StandardCharsets.UTF_8);
ciphertext = cipher.doFinal(body);
byte[] clearKeyWithAuthTag = new byte[messageKey.length + 16]; byte[] clearKeyWithAuthTag = new byte[messageKey.length + 16];
byte[] cipherTextWithoutAuthTag = new byte[ciphertext.length - 16]; byte[] cipherTextWithoutAuthTag = new byte[ciphertext.length - 16];
@ -278,9 +264,8 @@ public class OmemoMessageBuilder<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_
* @return initialization vector * @return initialization vector
*/ */
public static byte[] generateIv() { public static byte[] generateIv() {
SecureRandom random = new SecureRandom();
byte[] iv = new byte[12]; byte[] iv = new byte[12];
random.nextBytes(iv); RandomUtil.fillWithSecureRandom(iv);
return iv; return iv;
} }
} }

View File

@ -22,7 +22,6 @@ 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.KEYTYPE;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@ -61,7 +60,6 @@ public class WrapperObjectsTest extends SmackTestSuite {
CipherAndAuthTag cat = new CipherAndAuthTag(key, iv, authTag, true); CipherAndAuthTag cat = new CipherAndAuthTag(key, iv, authTag, true);
assertNotNull(cat.getCipher());
assertArrayEquals(key, cat.getKey()); assertArrayEquals(key, cat.getKey());
assertArrayEquals(iv, cat.getIv()); assertArrayEquals(iv, cat.getIv());
assertArrayEquals(authTag, cat.getAuthTag()); assertArrayEquals(authTag, cat.getAuthTag());

View File

@ -1143,16 +1143,27 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
} }
} }
catch (Exception e) { catch (Exception e) {
// The exception can be ignored if the the connection is 'done' // Set running to false since this thread will exit here and notifyConnectionError() will wait until
// or if the it was caused because the socket got closed. // the reader and writer thread's 'running' value is false. Hence we need to set it to false before calling
if (!done) { // notifyConnetctionError() below, even though run() also sets it to false. Therefore, do not remove this.
// Set running to false since this thread will exit here and notifyConnectionError() will wait until running = false;
// the reader and writer thread's 'running' value is false.
running = false; String ignoreReasonThread = null;
// Close the connection and notify connection listeners of the
// error. boolean writerThreadWasShutDown = packetWriter.queue.isShutdown();
notifyConnectionError(e); if (writerThreadWasShutDown) {
ignoreReasonThread = "writer";
} else if (done) {
ignoreReasonThread = "reader";
} }
if (ignoreReasonThread != null) {
LOGGER.log(Level.FINER, "Ignoring " + e + " as " + ignoreReasonThread + " was already shut down");
return;
}
// Close the connection and notify connection listeners of the error.
notifyConnectionError(e);
} }
} }
} }

View File

@ -73,6 +73,12 @@ public final class StaxXmlPullParser implements XmlPullParser {
return namespaceContext.getNamespaceURI(prefix); return namespaceContext.getNamespaceURI(prefix);
} }
@Override
public String getNamespace() {
String prefix = getPrefix();
return getNamespace(prefix);
}
@Override @Override
public int getDepth() { public int getDepth() {
return depth; return depth;
@ -106,13 +112,6 @@ public final class StaxXmlPullParser implements XmlPullParser {
return xmlStreamReader.getText(); return xmlStreamReader.getText();
} }
@Override
public String getNamespace() {
NamespaceContext namespaceContext = xmlStreamReader.getNamespaceContext();
String prefix = getPrefix();
return namespaceContext.getNamespaceURI(prefix);
}
@Override @Override
public String getName() { public String getName() {
QName qname = getQName(); QName qname = getQName();

View File

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2019 Florian Schmaus. * Copyright 2019-2021 Florian Schmaus.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -28,7 +28,14 @@ public class SmackXmlParser {
xmlPullParserFactoryServiceLoader = ServiceLoader.load(XmlPullParserFactory.class); xmlPullParserFactoryServiceLoader = ServiceLoader.load(XmlPullParserFactory.class);
} }
private static XmlPullParserFactory xmlPullParserFactory;
public static XmlPullParserFactory getXmlPullParserFactory() { public static XmlPullParserFactory getXmlPullParserFactory() {
final XmlPullParserFactory xmlPullParserFactory = SmackXmlParser.xmlPullParserFactory;
if (xmlPullParserFactory != null) {
return xmlPullParserFactory;
}
Iterator<XmlPullParserFactory> iterator = xmlPullParserFactoryServiceLoader.iterator(); Iterator<XmlPullParserFactory> iterator = xmlPullParserFactoryServiceLoader.iterator();
if (!iterator.hasNext()) { if (!iterator.hasNext()) {
throw new IllegalStateException( throw new IllegalStateException(
@ -37,6 +44,10 @@ public class SmackXmlParser {
return iterator.next(); return iterator.next();
} }
public static void setXmlPullParserFactory(XmlPullParserFactory xmlPullParserFactory) {
SmackXmlParser.xmlPullParserFactory = xmlPullParserFactory;
}
/** /**
* Creates a new XmlPullParser suitable for parsing XMPP. This means in particular that * Creates a new XmlPullParser suitable for parsing XMPP. This means in particular that
* FEATURE_PROCESS_NAMESPACES is enabled. * FEATURE_PROCESS_NAMESPACES is enabled.