request device list when encountering unknown device
This commit is contained in:
parent
c3f5273813
commit
cf17a2ac6d
|
@ -2,7 +2,7 @@
|
|||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "a619bdeae0408fc2250a0bf2b9ab1f4e",
|
||||
"identityHash": "983b8fb918cf0019a31e3a59b37dc368",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "account",
|
||||
|
@ -362,7 +362,7 @@
|
|||
},
|
||||
{
|
||||
"tableName": "axolotl_device_list_item",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `deviceListId` INTEGER NOT NULL, `deviceId` INTEGER, FOREIGN KEY(`deviceListId`) REFERENCES `axolotl_device_list`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `deviceListId` INTEGER NOT NULL, `deviceId` INTEGER, `confirmedInPep` INTEGER NOT NULL, FOREIGN KEY(`deviceListId`) REFERENCES `axolotl_device_list`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
|
@ -381,6 +381,12 @@
|
|||
"columnName": "deviceId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "confirmedInPep",
|
||||
"columnName": "confirmedInPep",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
|
@ -2364,7 +2370,7 @@
|
|||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a619bdeae0408fc2250a0bf2b9ab1f4e')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '983b8fb918cf0019a31e3a59b37dc368')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package im.conversations.android.axolotl;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import org.jxmpp.jid.BareJid;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
|
||||
|
@ -26,4 +27,18 @@ public class AxolotlAddress extends SignalProtocolAddress {
|
|||
SignalProtocolAddress.class.getSimpleName(),
|
||||
AxolotlAddress.class.getSimpleName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
if (!super.equals(o)) return false;
|
||||
AxolotlAddress that = (AxolotlAddress) o;
|
||||
return Objects.equal(jid, that.jid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(super.hashCode(), jid);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,16 +8,19 @@ public class AxolotlPayload {
|
|||
public final AxolotlAddress axolotlAddress;
|
||||
public final IdentityKey identityKey;
|
||||
public final boolean preKeyMessage;
|
||||
public final boolean inDeviceList;
|
||||
public final byte[] payload;
|
||||
|
||||
public AxolotlPayload(
|
||||
AxolotlAddress axolotlAddress,
|
||||
final IdentityKey identityKey,
|
||||
final boolean preKeyMessage,
|
||||
final boolean inDeviceList,
|
||||
byte[] payload) {
|
||||
this.axolotlAddress = axolotlAddress;
|
||||
this.identityKey = identityKey;
|
||||
this.preKeyMessage = preKeyMessage;
|
||||
this.inDeviceList = inDeviceList;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,10 @@ package im.conversations.android.axolotl;
|
|||
|
||||
import android.os.Build;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Multimap;
|
||||
import eu.siacs.conversations.xmpp.jingle.OmemoVerification;
|
||||
import im.conversations.android.AbstractAccountService;
|
||||
import im.conversations.android.database.AxolotlDatabaseStore;
|
||||
|
@ -14,6 +18,8 @@ import im.conversations.android.xmpp.model.axolotl.Payload;
|
|||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
|
@ -21,6 +27,7 @@ import javax.crypto.NoSuchPaddingException;
|
|||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import org.jxmpp.jid.BareJid;
|
||||
import org.jxmpp.jid.Jid;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -49,12 +56,21 @@ public class AxolotlService extends AbstractAccountService {
|
|||
|
||||
private final SignalProtocolStore signalProtocolStore;
|
||||
|
||||
private PostDecryptionHook postDecryptionHook;
|
||||
|
||||
private final Set<AxolotlAddress> freshSessions = new HashSet<>();
|
||||
private final Multimap<BareJid, Integer> devicesNotInPep = ArrayListMultimap.create();
|
||||
|
||||
public AxolotlService(
|
||||
final Account account, final ConversationsDatabase conversationsDatabase) {
|
||||
super(account, conversationsDatabase);
|
||||
this.signalProtocolStore = new AxolotlDatabaseStore(account, conversationsDatabase);
|
||||
}
|
||||
|
||||
public void setPostDecryptionHook(final PostDecryptionHook postDecryptionHook) {
|
||||
this.postDecryptionHook = postDecryptionHook;
|
||||
}
|
||||
|
||||
private AxolotlSession buildReceivingSession(
|
||||
final Jid from, final IdentityKey identityKey, final Header header) {
|
||||
final Optional<Integer> sid = header.getSourceDevice();
|
||||
|
@ -88,8 +104,9 @@ public class AxolotlService extends AbstractAccountService {
|
|||
|
||||
public AxolotlPayload decrypt(final Jid from, final Encrypted encrypted)
|
||||
throws AxolotlDecryptionException {
|
||||
final AxolotlPayload axolotlPayload;
|
||||
try {
|
||||
return decryptOrThrow(from, encrypted);
|
||||
axolotlPayload = decryptOrThrow(from, encrypted);
|
||||
} catch (final IllegalArgumentException
|
||||
| NotEncryptedForThisDeviceException
|
||||
| InvalidMessageException
|
||||
|
@ -110,6 +127,8 @@ public class AxolotlService extends AbstractAccountService {
|
|||
| BadPaddingException e) {
|
||||
throw new AxolotlDecryptionException(e);
|
||||
}
|
||||
registerForHook(axolotlPayload);
|
||||
return axolotlPayload;
|
||||
}
|
||||
|
||||
private AxolotlPayload decryptOrThrow(final Jid from, final Encrypted encrypted)
|
||||
|
@ -151,9 +170,10 @@ public class AxolotlService extends AbstractAccountService {
|
|||
throw new OutdatedSenderException(
|
||||
"Key did not contain auth tag. Sender needs to update their OMEMO client");
|
||||
}
|
||||
final var inDeviceList = database.axolotlDao().hasDeviceId(account, session.axolotlAddress);
|
||||
if (payload == null) {
|
||||
return new AxolotlPayload(
|
||||
session.axolotlAddress, session.identityKey, preKeyMessage, null);
|
||||
session.axolotlAddress, session.identityKey, preKeyMessage, inDeviceList, null);
|
||||
}
|
||||
final byte[] key = new byte[16];
|
||||
final byte[] authTag = new byte[16];
|
||||
|
@ -175,7 +195,44 @@ public class AxolotlService extends AbstractAccountService {
|
|||
System.arraycopy(authTag, 0, payloadWithAuthTag, payloadAsBytes.length, authTag.length);
|
||||
final byte[] decryptedPayload = cipher.doFinal(payloadWithAuthTag);
|
||||
return new AxolotlPayload(
|
||||
session.axolotlAddress, session.identityKey, preKeyMessage, decryptedPayload);
|
||||
session.axolotlAddress,
|
||||
session.identityKey,
|
||||
preKeyMessage,
|
||||
inDeviceList,
|
||||
decryptedPayload);
|
||||
}
|
||||
|
||||
private void registerForHook(final AxolotlPayload axolotlPayload) {
|
||||
synchronized (this.freshSessions) {
|
||||
if (axolotlPayload.preKeyMessage) {
|
||||
this.freshSessions.add(axolotlPayload.axolotlAddress);
|
||||
}
|
||||
}
|
||||
synchronized (this.devicesNotInPep) {
|
||||
if (!axolotlPayload.inDeviceList) {
|
||||
this.devicesNotInPep.put(
|
||||
axolotlPayload.axolotlAddress.getJid(),
|
||||
axolotlPayload.axolotlAddress.getDeviceId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void executePostDecryptionHook() {
|
||||
final var hook = this.postDecryptionHook;
|
||||
if (hook == null) {
|
||||
return;
|
||||
}
|
||||
final Set<AxolotlAddress> freshSessions;
|
||||
synchronized (this.freshSessions) {
|
||||
freshSessions = ImmutableSet.copyOf(this.freshSessions);
|
||||
this.freshSessions.clear();
|
||||
}
|
||||
final Multimap<BareJid, Integer> devicesNotInPep;
|
||||
synchronized (this.devicesNotInPep) {
|
||||
devicesNotInPep = ImmutableMultimap.copyOf(this.devicesNotInPep);
|
||||
}
|
||||
hook.executeHook(freshSessions);
|
||||
hook.executeHook(devicesNotInPep);
|
||||
}
|
||||
|
||||
public SignalProtocolStore getSignalProtocolStore() {
|
||||
|
@ -212,4 +269,10 @@ public class AxolotlService extends AbstractAccountService {
|
|||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
public interface PostDecryptionHook {
|
||||
void executeHook(final Set<AxolotlAddress> freshSessions);
|
||||
|
||||
void executeHook(final Multimap<BareJid, Integer> devicesNotInPep);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.room.OnConflictStrategy;
|
|||
import androidx.room.Query;
|
||||
import androidx.room.Transaction;
|
||||
import com.google.common.collect.Collections2;
|
||||
import im.conversations.android.axolotl.AxolotlAddress;
|
||||
import im.conversations.android.database.entity.AxolotlDeviceListEntity;
|
||||
import im.conversations.android.database.entity.AxolotlDeviceListItemEntity;
|
||||
import im.conversations.android.database.entity.AxolotlIdentityEntity;
|
||||
|
@ -35,14 +36,34 @@ public abstract class AxolotlDao {
|
|||
@Insert
|
||||
protected abstract void insert(Collection<AxolotlDeviceListItemEntity> entities);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
protected abstract void insertUnconfirmed(Collection<AxolotlDeviceListItemEntity> entities);
|
||||
|
||||
@Transaction
|
||||
public void setDeviceList(Account account, BareJid from, Set<Integer> deviceIds) {
|
||||
final var listId = insert(AxolotlDeviceListEntity.of(account.id, from));
|
||||
insert(
|
||||
Collections2.transform(
|
||||
deviceIds, deviceId -> AxolotlDeviceListItemEntity.of(listId, deviceId)));
|
||||
deviceIds,
|
||||
deviceId -> AxolotlDeviceListItemEntity.of(listId, deviceId, true)));
|
||||
}
|
||||
|
||||
@Transaction
|
||||
public void setUnconfirmedDevices(
|
||||
final Account account, final BareJid address, Set<Integer> unconfirmedDeviceIds) {
|
||||
final Long listId = getDeviceListId(account.id, address);
|
||||
if (listId == null) {
|
||||
return;
|
||||
}
|
||||
insertUnconfirmed(
|
||||
Collections2.transform(
|
||||
unconfirmedDeviceIds,
|
||||
deviceId -> AxolotlDeviceListItemEntity.of(listId, deviceId, false)));
|
||||
}
|
||||
|
||||
@Query("SELECT id FROM axolotl_device_list WHERE accountId=:account AND address=:address")
|
||||
abstract Long getDeviceListId(long account, final BareJid address);
|
||||
|
||||
@Query(
|
||||
"SELECT EXISTS(SELECT deviceId FROM axolotl_device_list JOIN axolotl_device_list_item"
|
||||
+ " ON axolotl_device_list.id=axolotl_device_list_item.deviceListId WHERE"
|
||||
|
@ -50,6 +71,10 @@ public abstract class AxolotlDao {
|
|||
public abstract boolean hasDeviceId(
|
||||
final long account, final BareJid address, final int deviceId);
|
||||
|
||||
public boolean hasDeviceId(final Account account, final AxolotlAddress axolotlAddress) {
|
||||
return hasDeviceId(account.id, axolotlAddress.getJid(), axolotlAddress.getDeviceId());
|
||||
}
|
||||
|
||||
@Transaction
|
||||
public void setDeviceListError(
|
||||
final Account account, final BareJid address, Condition condition) {
|
||||
|
|
|
@ -29,10 +29,14 @@ public class AxolotlDeviceListItemEntity {
|
|||
|
||||
public Integer deviceId;
|
||||
|
||||
public static AxolotlDeviceListItemEntity of(final long deviceListId, final int deviceId) {
|
||||
public boolean confirmedInPep;
|
||||
|
||||
public static AxolotlDeviceListItemEntity of(
|
||||
final long deviceListId, final int deviceId, final boolean confirmedInPep) {
|
||||
final var entity = new AxolotlDeviceListItemEntity();
|
||||
entity.deviceListId = deviceListId;
|
||||
entity.deviceId = deviceId;
|
||||
entity.confirmedInPep = confirmedInPep;
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,12 @@ public class Transformer {
|
|||
}
|
||||
|
||||
public boolean transform(final MessageTransformation transformation) {
|
||||
return database.runInTransaction(() -> transform(database, transformation));
|
||||
return database.runInTransaction(
|
||||
() -> {
|
||||
final var sendDeliveryReceipts = transform(database, transformation);
|
||||
axolotlService.executePostDecryptionHook();
|
||||
return sendDeliveryReceipts;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,9 +2,12 @@ package im.conversations.android.xmpp.manager;
|
|||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
@ -51,7 +54,7 @@ import org.whispersystems.libsignal.state.SignalProtocolStore;
|
|||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.util.KeyHelper;
|
||||
|
||||
public class AxolotlManager extends AbstractManager {
|
||||
public class AxolotlManager extends AbstractManager implements AxolotlService.PostDecryptionHook {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AxolotlManager.class);
|
||||
|
||||
|
@ -64,6 +67,7 @@ public class AxolotlManager extends AbstractManager {
|
|||
this.axolotlService =
|
||||
new AxolotlService(
|
||||
connection.getAccount(), ConversationsDatabase.getInstance(context));
|
||||
this.axolotlService.setPostDecryptionHook(this);
|
||||
}
|
||||
|
||||
public AxolotlService getAxolotlService() {
|
||||
|
@ -301,7 +305,6 @@ public class AxolotlManager extends AbstractManager {
|
|||
signedPreKeyRecord.getKeyPair().getPublicKey(),
|
||||
signedPreKeyRecord.getSignature());
|
||||
bundle.addPreKeys(getDatabase().axolotlDao().getPreKeys(getAccount().id));
|
||||
LOGGER.info("bundle {}", bundle);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
|
@ -492,4 +495,63 @@ public class AxolotlManager extends AbstractManager {
|
|||
private SignalProtocolStore signalProtocolStore() {
|
||||
return this.axolotlService.getSignalProtocolStore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeHook(final Set<AxolotlAddress> freshSessions) {
|
||||
for (final AxolotlAddress axolotlAddress : freshSessions) {
|
||||
LOGGER.info(
|
||||
"fresh session from {}/{}",
|
||||
axolotlAddress.getJid(),
|
||||
axolotlAddress.getDeviceId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeHook(Multimap<BareJid, Integer> devicesNotInPep) {
|
||||
for (final Map.Entry<BareJid, Collection<Integer>> entries :
|
||||
devicesNotInPep.asMap().entrySet()) {
|
||||
if (entries.getValue().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
// Warning. This will leak our resource to anyone who knows our jid + device id
|
||||
// TODO we could limit this to addresses in our roster; however the point of this
|
||||
// exercise is mostly to improve reliability with people not in our roster
|
||||
confirmDeviceInPep(entries.getKey(), ImmutableSet.copyOf(entries.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
private void confirmDeviceInPep(final BareJid address, final Set<Integer> devices) {
|
||||
final var deviceListFuture = this.fetchDeviceIds(address);
|
||||
final var caughtDeviceListFuture =
|
||||
Futures.catching(
|
||||
deviceListFuture,
|
||||
Exception.class,
|
||||
(Function<Exception, Set<Integer>>) input -> Collections.emptySet(),
|
||||
MoreExecutors.directExecutor());
|
||||
Futures.addCallback(
|
||||
caughtDeviceListFuture,
|
||||
new FutureCallback<>() {
|
||||
@Override
|
||||
public void onSuccess(final Set<Integer> devicesInPep) {
|
||||
final Set<Integer> unconfirmedDevices =
|
||||
Sets.difference(devices, devicesInPep);
|
||||
if (unconfirmedDevices.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
LOGGER.info(
|
||||
"Found unconfirmed devices for {}: {}",
|
||||
address,
|
||||
unconfirmedDevices);
|
||||
getDatabase()
|
||||
.axolotlDao()
|
||||
.setUnconfirmedDevices(getAccount(), address, unconfirmedDevices);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Throwable throwable) {
|
||||
LOGGER.error("Could not confirm device list for {}", address, throwable);
|
||||
}
|
||||
},
|
||||
getDatabase().getQueryExecutor());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ public class MessageProcessor extends XmppConnection.Delegate implements Consume
|
|||
return;
|
||||
}
|
||||
|
||||
LOGGER.info(
|
||||
LOGGER.debug(
|
||||
"Message from {} with {} in level {}",
|
||||
message.getFrom(),
|
||||
message.getExtensionIds(),
|
||||
|
|
Loading…
Reference in a new issue