add 'encryption' and 'identityKey' to message version entity

This commit is contained in:
Daniel Gultsch 2023-02-25 12:28:36 +01:00
parent 677cfcd34c
commit cf5910e96e
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
9 changed files with 157 additions and 65 deletions

View file

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "6186e2691813f4fbd804b90fd770e18b",
"identityHash": "a619bdeae0408fc2250a0bf2b9ab1f4e",
"entities": [
{
"tableName": "account",
@ -1834,7 +1834,7 @@
},
{
"tableName": "message_version",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `messageEntityId` INTEGER NOT NULL, `messageId` TEXT, `stanzaId` TEXT, `modification` TEXT, `modifiedBy` TEXT, `modifiedByResource` TEXT, `occupantId` TEXT, `receivedAt` INTEGER, FOREIGN KEY(`messageEntityId`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `messageEntityId` INTEGER NOT NULL, `messageId` TEXT, `stanzaId` TEXT, `modification` TEXT, `modifiedBy` TEXT, `modifiedByResource` TEXT, `occupantId` TEXT, `receivedAt` INTEGER, `encryption` TEXT, `identityKey` BLOB, FOREIGN KEY(`messageEntityId`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
@ -1889,6 +1889,18 @@
"columnName": "receivedAt",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "encryption",
"columnName": "encryption",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "identityKey",
"columnName": "identityKey",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
@ -2352,7 +2364,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, '6186e2691813f4fbd804b90fd770e18b')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a619bdeae0408fc2250a0bf2b9ab1f4e')"
]
}
}

View file

@ -8,6 +8,7 @@ import com.google.common.collect.Iterables;
import im.conversations.android.IDs;
import im.conversations.android.database.ConversationsDatabase;
import im.conversations.android.database.entity.AccountEntity;
import im.conversations.android.database.model.Encryption;
import im.conversations.android.database.model.MessageEmbedded;
import im.conversations.android.database.model.Modification;
import im.conversations.android.database.model.PartType;
@ -83,6 +84,7 @@ public class MessageTransformationTest {
final var message = Iterables.getOnlyElement(messages);
final var onlyContent = Iterables.getOnlyElement(message.contents);
Assert.assertEquals(GREETING, onlyContent.body);
Assert.assertEquals(Encryption.CLEARTEXT,message.encryption);
final var onlyReaction = Iterables.getOnlyElement(message.reactions);
Assert.assertEquals("Y", onlyReaction.reaction);
Assert.assertEquals(REMOTE, onlyReaction.reactionBy);

View file

@ -16,11 +16,12 @@ import im.conversations.android.database.entity.MessageStateEntity;
import im.conversations.android.database.entity.MessageVersionEntity;
import im.conversations.android.database.model.Account;
import im.conversations.android.database.model.ChatIdentifier;
import im.conversations.android.database.model.MessageContent;
import im.conversations.android.database.model.Encryption;
import im.conversations.android.database.model.MessageIdentifier;
import im.conversations.android.database.model.MessageState;
import im.conversations.android.database.model.MessageWithContentReactions;
import im.conversations.android.database.model.Modification;
import im.conversations.android.transformer.MessageContentWrapper;
import im.conversations.android.transformer.MessageTransformation;
import im.conversations.android.xmpp.model.reactions.Reactions;
import im.conversations.android.xmpp.model.stanza.Message;
@ -31,6 +32,7 @@ import org.jxmpp.jid.Jid;
import org.jxmpp.jid.parts.Resourcepart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.IdentityKey;
@Dao
public abstract class MessageDao {
@ -324,19 +326,37 @@ public abstract class MessageDao {
+ " chatId=:chatId AND stanzaId=:stanzaId")
protected abstract MessageIdentifier getByStanzaId(final long chatId, final String stanzaId);
public void insertMessageContent(Long latestVersion, List<MessageContent> contents) {
public void insertMessageContent(
final Long latestVersion, final MessageContentWrapper messageContentWrapper) {
Preconditions.checkNotNull(
latestVersion, "Contents can only be inserted for a specific version");
Preconditions.checkArgument(
contents.size() > 0,
messageContentWrapper.contents.size() > 0,
"If you are trying to insert empty contents something went wrong");
insertMessageContent(
Lists.transform(contents, c -> MessageContentEntity.of(latestVersion, c)));
Lists.transform(
messageContentWrapper.contents,
c -> MessageContentEntity.of(latestVersion, c)));
final int rows =
updateMessageVersionEncryption(
latestVersion,
messageContentWrapper.encryption,
messageContentWrapper.identityKey);
if (rows != 1) {
throw new IllegalStateException(
"We expected to update encryption information on exactly 1 row");
}
}
@Insert
protected abstract void insertMessageContent(Collection<MessageContentEntity> contentEntities);
@Query(
"UPDATE message_version SET encryption=:encryption,identityKey=:identityKey WHERE"
+ " id=:messageVersionId")
protected abstract int updateMessageVersionEncryption(
long messageVersionId, Encryption encryption, IdentityKey identityKey);
public void insertMessageState(
ChatIdentifier chatIdentifier,
final String messageId,
@ -402,9 +422,10 @@ public abstract class MessageDao {
@Query(
"SELECT message.id as"
+ " id,sentAt,outgoing,toBare,toResource,fromBare,fromResource,modification,latestVersion"
+ " as version,inReplyToMessageEntityId FROM message JOIN message_version ON"
+ " message.latestVersion=message_version.id WHERE message.chatId=:chatId AND"
+ " latestVersion IS NOT NULL ORDER BY message.receivedAt")
+ " as version,inReplyToMessageEntityId,encryption,identityKey FROM message JOIN"
+ " message_version ON message.latestVersion=message_version.id WHERE"
+ " message.chatId=:chatId AND latestVersion IS NOT NULL ORDER BY"
+ " message.receivedAt")
public abstract List<MessageWithContentReactions> getMessages(long chatId);
public void setInReplyTo(

View file

@ -1,17 +1,20 @@
package im.conversations.android.database.entity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Index;
import androidx.room.PrimaryKey;
import com.google.common.base.Preconditions;
import im.conversations.android.database.model.Encryption;
import im.conversations.android.database.model.Modification;
import im.conversations.android.transformer.MessageTransformation;
import im.conversations.android.xmpp.model.stanza.Message;
import java.time.Instant;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.parts.Resourcepart;
import org.whispersystems.libsignal.IdentityKey;
@Entity(
tableName = "message_version",
@ -36,6 +39,8 @@ public class MessageVersionEntity {
public Resourcepart modifiedByResource;
public String occupantId;
public Instant receivedAt;
@Nullable public Encryption encryption;
@Nullable public IdentityKey identityKey;
// the version order is determined by the receivedAt
// the actual display time and display order comes from the parent MessageEntity

View file

@ -0,0 +1,7 @@
package im.conversations.android.database.model;
public enum Encryption {
OMEMO,
CLEARTEXT,
PGP
}

View file

@ -1,8 +1,5 @@
package im.conversations.android.database.model;
import com.google.common.collect.ImmutableList;
import java.util.List;
public class MessageContent {
public final String language;
@ -27,7 +24,4 @@ public class MessageContent {
public static MessageContent file(final String url) {
return new MessageContent(null, PartType.FILE, null, url);
}
public static final List<MessageContent> RETRACTION =
ImmutableList.of(new MessageContent(null, PartType.RETRACTION, null, null));
}

View file

@ -14,6 +14,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jxmpp.jid.Jid;
import org.whispersystems.libsignal.IdentityKey;
public class MessageWithContentReactions {
@ -31,6 +32,8 @@ public class MessageWithContentReactions {
public Modification modification;
public long version;
public Long inReplyToMessageEntityId;
public Encryption encryption;
public IdentityKey identityKey;
@Relation(
entity = MessageEntity.class,

View file

@ -0,0 +1,92 @@
package im.conversations.android.transformer;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import im.conversations.android.axolotl.AxolotlPayload;
import im.conversations.android.database.model.Encryption;
import im.conversations.android.database.model.MessageContent;
import im.conversations.android.database.model.PartType;
import im.conversations.android.xmpp.model.jabber.Body;
import im.conversations.android.xmpp.model.oob.OutOfBandData;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import org.whispersystems.libsignal.IdentityKey;
public class MessageContentWrapper {
public static final MessageContentWrapper RETRACTION =
new MessageContentWrapper(
ImmutableList.of(new MessageContent(null, PartType.RETRACTION, null, null)),
Encryption.CLEARTEXT,
null);
public final List<MessageContent> contents;
public final Encryption encryption;
public final IdentityKey identityKey;
private MessageContentWrapper(
List<MessageContent> contents, Encryption encryption, IdentityKey identityKey) {
if (encryption == Encryption.OMEMO) {
Preconditions.checkArgument(
Objects.nonNull(identityKey),
"OMEMO encrypted content must provide an identity key");
}
this.contents = contents;
this.encryption = encryption;
this.identityKey = identityKey;
}
public static MessageContentWrapper parseCleartext(final MessageTransformation transformation) {
final Collection<Body> bodies = transformation.getExtensions(Body.class);
final Collection<OutOfBandData> outOfBandData =
transformation.getExtensions(OutOfBandData.class);
final ImmutableList.Builder<MessageContent> messageContentBuilder = ImmutableList.builder();
if (bodies.size() == 1 && outOfBandData.size() == 1) {
final String text = Iterables.getOnlyElement(bodies).getContent();
final String url = Iterables.getOnlyElement(outOfBandData).getURL();
if (!Strings.isNullOrEmpty(url) && url.equals(text)) {
return cleartext(ImmutableList.of(MessageContent.file(url)));
}
}
// TODO verify that body is not fallback
for (final Body body : bodies) {
final String text = body.getContent();
if (Strings.isNullOrEmpty(text)) {
continue;
}
messageContentBuilder.add(MessageContent.text(text, body.getLang()));
}
for (final OutOfBandData data : outOfBandData) {
final String url = data.getURL();
if (Strings.isNullOrEmpty(url)) {
continue;
}
messageContentBuilder.add(MessageContent.file(url));
}
return cleartext(messageContentBuilder.build());
}
private static MessageContentWrapper cleartext(final List<MessageContent> contents) {
return new MessageContentWrapper(contents, Encryption.CLEARTEXT, null);
}
public static MessageContentWrapper ofAxolotl(final AxolotlPayload payload) {
if (payload.hasPayload()) {
return new MessageContentWrapper(
ImmutableList.of(MessageContent.text(payload.payloadAsString(), null)),
Encryption.OMEMO,
payload.identityKey);
}
throw new IllegalArgumentException(
String.format("%s does not have payload", payload.getClass().getSimpleName()));
}
public boolean isEmpty() {
return this.contents.isEmpty();
}
}

View file

@ -1,32 +1,24 @@
package im.conversations.android.transformer;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import im.conversations.android.axolotl.AxolotlDecryptionException;
import im.conversations.android.axolotl.AxolotlService;
import im.conversations.android.database.ConversationsDatabase;
import im.conversations.android.database.model.Account;
import im.conversations.android.database.model.ChatIdentifier;
import im.conversations.android.database.model.MessageContent;
import im.conversations.android.database.model.MessageIdentifier;
import im.conversations.android.database.model.MessageState;
import im.conversations.android.database.model.Modification;
import im.conversations.android.xmpp.model.DeliveryReceipt;
import im.conversations.android.xmpp.model.axolotl.Encrypted;
import im.conversations.android.xmpp.model.correction.Replace;
import im.conversations.android.xmpp.model.jabber.Body;
import im.conversations.android.xmpp.model.markers.Displayed;
import im.conversations.android.xmpp.model.muc.user.MultiUserChat;
import im.conversations.android.xmpp.model.oob.OutOfBandData;
import im.conversations.android.xmpp.model.reactions.Reactions;
import im.conversations.android.xmpp.model.reply.Reply;
import im.conversations.android.xmpp.model.retract.Retract;
import im.conversations.android.xmpp.model.stanza.Message;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -89,13 +81,12 @@ public class Transformer {
final Reactions reactions = transformation.getExtension(Reactions.class);
final Retract retract = transformation.getExtension(Retract.class);
final Encrypted encrypted = transformation.getExtension(Encrypted.class);
final List<MessageContent> contents;
final MessageContentWrapper contents;
if (encrypted != null) {
try {
final var payload = axolotlService.decrypt(transformation.from, encrypted);
if (payload.hasPayload()) {
contents =
ImmutableList.of(MessageContent.text(payload.payloadAsString(), null));
contents = MessageContentWrapper.ofAxolotl(payload);
} else {
return true;
}
@ -107,7 +98,7 @@ public class Transformer {
} else {
// TODO we need to remove fallbacks for reactions, retractions and potentially other
// things
contents = parseContent(transformation);
contents = MessageContentWrapper.parseCleartext(transformation);
}
final boolean identifiableSender =
@ -131,7 +122,8 @@ public class Transformer {
.getOrCreateVersion(
chat, transformation, retract.getId(), Modification.RETRACTION);
database.messageDao()
.insertMessageContent(messageIdentifier.version, MessageContent.RETRACTION);
.insertMessageContent(
messageIdentifier.version, MessageContentWrapper.RETRACTION);
return true;
} else if (contents.isEmpty()) {
LOGGER.info("Received message from {} w/o contents", transformation.from);
@ -173,42 +165,6 @@ public class Transformer {
return true;
}
protected List<MessageContent> parseContent(final MessageTransformation transformation) {
final var encrypted = transformation.getExtension(Encrypted.class);
final var encryptedWithPayload = encrypted != null && encrypted.hasPayload();
final Collection<Body> bodies = transformation.getExtensions(Body.class);
final Collection<OutOfBandData> outOfBandData =
transformation.getExtensions(OutOfBandData.class);
final ImmutableList.Builder<MessageContent> messageContentBuilder = ImmutableList.builder();
// TODO decrypt
if (bodies.size() == 1 && outOfBandData.size() == 1) {
final String text = Iterables.getOnlyElement(bodies).getContent();
final String url = Iterables.getOnlyElement(outOfBandData).getURL();
if (!Strings.isNullOrEmpty(url) && url.equals(text)) {
return ImmutableList.of(MessageContent.file(url));
}
}
// TODO verify that body is not fallback
for (final Body body : bodies) {
final String text = body.getContent();
if (Strings.isNullOrEmpty(text)) {
continue;
}
messageContentBuilder.add(MessageContent.text(text, body.getLang()));
}
for (final OutOfBandData data : outOfBandData) {
final String url = data.getURL();
if (Strings.isNullOrEmpty(url)) {
continue;
}
messageContentBuilder.add(MessageContent.file(url));
}
return messageContentBuilder.build();
}
private void transformMessageState(
final ChatIdentifier chat, final MessageTransformation transformation) {
final var displayed = transformation.getExtension(Displayed.class);