request device list when encountering unknown device

This commit is contained in:
Daniel Gultsch 2023-02-28 11:33:53 +01:00
parent c3f5273813
commit cf17a2ac6d
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
9 changed files with 195 additions and 12 deletions

View file

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "a619bdeae0408fc2250a0bf2b9ab1f4e", "identityHash": "983b8fb918cf0019a31e3a59b37dc368",
"entities": [ "entities": [
{ {
"tableName": "account", "tableName": "account",
@ -362,7 +362,7 @@
}, },
{ {
"tableName": "axolotl_device_list_item", "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": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@ -381,6 +381,12 @@
"columnName": "deviceId", "columnName": "deviceId",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": false "notNull": false
},
{
"fieldPath": "confirmedInPep",
"columnName": "confirmedInPep",
"affinity": "INTEGER",
"notNull": true
} }
], ],
"primaryKey": { "primaryKey": {
@ -2364,7 +2370,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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')"
] ]
} }
} }

View file

@ -1,5 +1,6 @@
package im.conversations.android.axolotl; package im.conversations.android.axolotl;
import com.google.common.base.Objects;
import org.jxmpp.jid.BareJid; import org.jxmpp.jid.BareJid;
import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.SignalProtocolAddress;
@ -26,4 +27,18 @@ public class AxolotlAddress extends SignalProtocolAddress {
SignalProtocolAddress.class.getSimpleName(), SignalProtocolAddress.class.getSimpleName(),
AxolotlAddress.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);
}
} }

View file

@ -8,16 +8,19 @@ public class AxolotlPayload {
public final AxolotlAddress axolotlAddress; public final AxolotlAddress axolotlAddress;
public final IdentityKey identityKey; public final IdentityKey identityKey;
public final boolean preKeyMessage; public final boolean preKeyMessage;
public final boolean inDeviceList;
public final byte[] payload; public final byte[] payload;
public AxolotlPayload( public AxolotlPayload(
AxolotlAddress axolotlAddress, AxolotlAddress axolotlAddress,
final IdentityKey identityKey, final IdentityKey identityKey,
final boolean preKeyMessage, final boolean preKeyMessage,
final boolean inDeviceList,
byte[] payload) { byte[] payload) {
this.axolotlAddress = axolotlAddress; this.axolotlAddress = axolotlAddress;
this.identityKey = identityKey; this.identityKey = identityKey;
this.preKeyMessage = preKeyMessage; this.preKeyMessage = preKeyMessage;
this.inDeviceList = inDeviceList;
this.payload = payload; this.payload = payload;
} }

View file

@ -2,6 +2,10 @@ package im.conversations.android.axolotl;
import android.os.Build; import android.os.Build;
import com.google.common.base.Optional; 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 eu.siacs.conversations.xmpp.jingle.OmemoVerification;
import im.conversations.android.AbstractAccountService; import im.conversations.android.AbstractAccountService;
import im.conversations.android.database.AxolotlDatabaseStore; 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.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException; import java.security.NoSuchProviderException;
import java.util.HashSet;
import java.util.Set;
import javax.crypto.BadPaddingException; import javax.crypto.BadPaddingException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException; import javax.crypto.IllegalBlockSizeException;
@ -21,6 +27,7 @@ import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -49,12 +56,21 @@ public class AxolotlService extends AbstractAccountService {
private final SignalProtocolStore signalProtocolStore; private final SignalProtocolStore signalProtocolStore;
private PostDecryptionHook postDecryptionHook;
private final Set<AxolotlAddress> freshSessions = new HashSet<>();
private final Multimap<BareJid, Integer> devicesNotInPep = ArrayListMultimap.create();
public AxolotlService( public AxolotlService(
final Account account, final ConversationsDatabase conversationsDatabase) { final Account account, final ConversationsDatabase conversationsDatabase) {
super(account, conversationsDatabase); super(account, conversationsDatabase);
this.signalProtocolStore = new AxolotlDatabaseStore(account, conversationsDatabase); this.signalProtocolStore = new AxolotlDatabaseStore(account, conversationsDatabase);
} }
public void setPostDecryptionHook(final PostDecryptionHook postDecryptionHook) {
this.postDecryptionHook = postDecryptionHook;
}
private AxolotlSession buildReceivingSession( private AxolotlSession buildReceivingSession(
final Jid from, final IdentityKey identityKey, final Header header) { final Jid from, final IdentityKey identityKey, final Header header) {
final Optional<Integer> sid = header.getSourceDevice(); final Optional<Integer> sid = header.getSourceDevice();
@ -88,8 +104,9 @@ public class AxolotlService extends AbstractAccountService {
public AxolotlPayload decrypt(final Jid from, final Encrypted encrypted) public AxolotlPayload decrypt(final Jid from, final Encrypted encrypted)
throws AxolotlDecryptionException { throws AxolotlDecryptionException {
final AxolotlPayload axolotlPayload;
try { try {
return decryptOrThrow(from, encrypted); axolotlPayload = decryptOrThrow(from, encrypted);
} catch (final IllegalArgumentException } catch (final IllegalArgumentException
| NotEncryptedForThisDeviceException | NotEncryptedForThisDeviceException
| InvalidMessageException | InvalidMessageException
@ -110,6 +127,8 @@ public class AxolotlService extends AbstractAccountService {
| BadPaddingException e) { | BadPaddingException e) {
throw new AxolotlDecryptionException(e); throw new AxolotlDecryptionException(e);
} }
registerForHook(axolotlPayload);
return axolotlPayload;
} }
private AxolotlPayload decryptOrThrow(final Jid from, final Encrypted encrypted) private AxolotlPayload decryptOrThrow(final Jid from, final Encrypted encrypted)
@ -151,9 +170,10 @@ public class AxolotlService extends AbstractAccountService {
throw new OutdatedSenderException( throw new OutdatedSenderException(
"Key did not contain auth tag. Sender needs to update their OMEMO client"); "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) { if (payload == null) {
return new AxolotlPayload( return new AxolotlPayload(
session.axolotlAddress, session.identityKey, preKeyMessage, null); session.axolotlAddress, session.identityKey, preKeyMessage, inDeviceList, null);
} }
final byte[] key = new byte[16]; final byte[] key = new byte[16];
final byte[] authTag = 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); System.arraycopy(authTag, 0, payloadWithAuthTag, payloadAsBytes.length, authTag.length);
final byte[] decryptedPayload = cipher.doFinal(payloadWithAuthTag); final byte[] decryptedPayload = cipher.doFinal(payloadWithAuthTag);
return new AxolotlPayload( 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() { public SignalProtocolStore getSignalProtocolStore() {
@ -212,4 +269,10 @@ public class AxolotlService extends AbstractAccountService {
super(message); super(message);
} }
} }
public interface PostDecryptionHook {
void executeHook(final Set<AxolotlAddress> freshSessions);
void executeHook(final Multimap<BareJid, Integer> devicesNotInPep);
}
} }

View file

@ -6,6 +6,7 @@ import androidx.room.OnConflictStrategy;
import androidx.room.Query; import androidx.room.Query;
import androidx.room.Transaction; import androidx.room.Transaction;
import com.google.common.collect.Collections2; 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.AxolotlDeviceListEntity;
import im.conversations.android.database.entity.AxolotlDeviceListItemEntity; import im.conversations.android.database.entity.AxolotlDeviceListItemEntity;
import im.conversations.android.database.entity.AxolotlIdentityEntity; import im.conversations.android.database.entity.AxolotlIdentityEntity;
@ -35,14 +36,34 @@ public abstract class AxolotlDao {
@Insert @Insert
protected abstract void insert(Collection<AxolotlDeviceListItemEntity> entities); protected abstract void insert(Collection<AxolotlDeviceListItemEntity> entities);
@Insert(onConflict = OnConflictStrategy.IGNORE)
protected abstract void insertUnconfirmed(Collection<AxolotlDeviceListItemEntity> entities);
@Transaction @Transaction
public void setDeviceList(Account account, BareJid from, Set<Integer> deviceIds) { public void setDeviceList(Account account, BareJid from, Set<Integer> deviceIds) {
final var listId = insert(AxolotlDeviceListEntity.of(account.id, from)); final var listId = insert(AxolotlDeviceListEntity.of(account.id, from));
insert( insert(
Collections2.transform( 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( @Query(
"SELECT EXISTS(SELECT deviceId FROM axolotl_device_list JOIN axolotl_device_list_item" "SELECT EXISTS(SELECT deviceId FROM axolotl_device_list JOIN axolotl_device_list_item"
+ " ON axolotl_device_list.id=axolotl_device_list_item.deviceListId WHERE" + " ON axolotl_device_list.id=axolotl_device_list_item.deviceListId WHERE"
@ -50,6 +71,10 @@ public abstract class AxolotlDao {
public abstract boolean hasDeviceId( public abstract boolean hasDeviceId(
final long account, final BareJid address, final int deviceId); 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 @Transaction
public void setDeviceListError( public void setDeviceListError(
final Account account, final BareJid address, Condition condition) { final Account account, final BareJid address, Condition condition) {

View file

@ -29,10 +29,14 @@ public class AxolotlDeviceListItemEntity {
public Integer deviceId; 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(); final var entity = new AxolotlDeviceListItemEntity();
entity.deviceListId = deviceListId; entity.deviceListId = deviceListId;
entity.deviceId = deviceId; entity.deviceId = deviceId;
entity.confirmedInPep = confirmedInPep;
return entity; return entity;
} }
} }

View file

@ -47,7 +47,12 @@ public class Transformer {
} }
public boolean transform(final MessageTransformation transformation) { 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;
});
} }
/** /**

View file

@ -2,9 +2,12 @@ package im.conversations.android.xmpp.manager;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet; 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.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; 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.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.KeyHelper; 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); private static final Logger LOGGER = LoggerFactory.getLogger(AxolotlManager.class);
@ -64,6 +67,7 @@ public class AxolotlManager extends AbstractManager {
this.axolotlService = this.axolotlService =
new AxolotlService( new AxolotlService(
connection.getAccount(), ConversationsDatabase.getInstance(context)); connection.getAccount(), ConversationsDatabase.getInstance(context));
this.axolotlService.setPostDecryptionHook(this);
} }
public AxolotlService getAxolotlService() { public AxolotlService getAxolotlService() {
@ -301,7 +305,6 @@ public class AxolotlManager extends AbstractManager {
signedPreKeyRecord.getKeyPair().getPublicKey(), signedPreKeyRecord.getKeyPair().getPublicKey(),
signedPreKeyRecord.getSignature()); signedPreKeyRecord.getSignature());
bundle.addPreKeys(getDatabase().axolotlDao().getPreKeys(getAccount().id)); bundle.addPreKeys(getDatabase().axolotlDao().getPreKeys(getAccount().id));
LOGGER.info("bundle {}", bundle);
return bundle; return bundle;
} }
@ -492,4 +495,63 @@ public class AxolotlManager extends AbstractManager {
private SignalProtocolStore signalProtocolStore() { private SignalProtocolStore signalProtocolStore() {
return this.axolotlService.getSignalProtocolStore(); 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());
}
} }

View file

@ -70,7 +70,7 @@ public class MessageProcessor extends XmppConnection.Delegate implements Consume
return; return;
} }
LOGGER.info( LOGGER.debug(
"Message from {} with {} in level {}", "Message from {} with {} in level {}",
message.getFrom(), message.getFrom(),
message.getExtensionIds(), message.getExtensionIds(),