From 1b438117a353ed79a48393da2e34913db2be91bb Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 16 Jan 2023 14:11:47 +0100 Subject: [PATCH] add Entity Caps 2 hash calculation --- .../android/xmpp/EntityCapabilities2.java | 126 ++++++++++++++++++ .../android/xmpp/EntityCapabilitiesTest.java | 106 +++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java diff --git a/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java b/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java new file mode 100644 index 000000000..20290520f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java @@ -0,0 +1,126 @@ +package im.conversations.android.xmpp; + +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.collect.Collections2; +import com.google.common.collect.Ordering; +import com.google.common.hash.Hashing; +import com.google.common.primitives.Bytes; +import im.conversations.android.xmpp.model.data.Data; +import im.conversations.android.xmpp.model.data.Field; +import im.conversations.android.xmpp.model.data.Value; +import im.conversations.android.xmpp.model.disco.info.Feature; +import im.conversations.android.xmpp.model.disco.info.Identity; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; +import java.nio.charset.StandardCharsets; +import java.util.Collection; + +public class EntityCapabilities2 { + + private static final char UNIT_SEPARATOR = 0x1f; + private static final char RECORD_SEPARATOR = 0x1e; + + private static final char GROUP_SEPARATOR = 0x1d; + + private static final char FILE_SEPARATOR = 0x1c; + + public static byte[] hash(final InfoQuery info) { + final String algo = algorithm(info); + return Hashing.sha256().hashString(algo, StandardCharsets.UTF_8).asBytes(); + } + + private static String asHex(final String message) { + return Joiner.on(' ') + .join( + Collections2.transform( + Bytes.asList(message.getBytes(StandardCharsets.UTF_8)), + b -> String.format("%02x", b))); + } + + private static String algorithm(final InfoQuery infoQuery) { + return features(infoQuery.getExtensions(Feature.class)) + + identities(infoQuery.getExtensions(Identity.class)) + + extensions(infoQuery.getExtensions(Data.class)); + } + + private static String identities(final Collection identities) { + return Joiner.on("") + .join( + Ordering.natural() + .sortedCopy( + Collections2.transform( + identities, EntityCapabilities2::identity))) + + FILE_SEPARATOR; + } + + private static String identity(final Identity identity) { + return Strings.nullToEmpty(identity.getCategory()) + + UNIT_SEPARATOR + + Strings.nullToEmpty(identity.getType()) + + UNIT_SEPARATOR + + Strings.nullToEmpty(identity.getLang()) + + UNIT_SEPARATOR + + Strings.nullToEmpty(identity.getIdentityName()) + + UNIT_SEPARATOR + + RECORD_SEPARATOR; + } + + private static String features(Collection features) { + return Joiner.on("") + .join( + Ordering.natural() + .sortedCopy( + Collections2.transform( + features, EntityCapabilities2::feature))) + + FILE_SEPARATOR; + } + + private static String feature(final Feature feature) { + return Strings.nullToEmpty(feature.getVar()) + UNIT_SEPARATOR; + } + + private static String value(final Value value) { + return Strings.nullToEmpty(value.getContent()) + UNIT_SEPARATOR; + } + + private static String values(final Collection values) { + return Joiner.on("") + .join( + Ordering.natural() + .sortedCopy( + Collections2.transform( + values, EntityCapabilities2::value))); + } + + private static String field(final Field field) { + return Strings.nullToEmpty(field.getFieldName()) + + UNIT_SEPARATOR + + values(field.getExtensions(Value.class)) + + RECORD_SEPARATOR; + } + + private static String fields(final Collection fields) { + return Joiner.on("") + .join( + Ordering.natural() + .sortedCopy( + Collections2.transform( + fields, EntityCapabilities2::field))) + + GROUP_SEPARATOR; + } + + private static String extension(final Data data) { + return fields(data.getExtensions(Field.class)); + } + + private static String extensions(final Collection extensions) { + return Joiner.on("") + .join( + Ordering.natural() + .sortedCopy( + Collections2.transform( + extensions, + EntityCapabilities2::extension))) + + FILE_SEPARATOR; + } +} diff --git a/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java b/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java index f2a1d3e05..ed645f28f 100644 --- a/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java +++ b/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java @@ -77,4 +77,110 @@ public class EntityCapabilitiesTest { final String var = BaseEncoding.base64().encode(EntityCapabilities.hash(info)); Assert.assertEquals("q07IKJEyjvHSyhy//CH0CxmKi8w=", var); } + + @Test + public void caps2() throws IOException { + final String xml = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8)); + assertThat(element, instanceOf(InfoQuery.class)); + final InfoQuery info = (InfoQuery) element; + final String var = BaseEncoding.base64().encode(EntityCapabilities2.hash(info)); + Assert.assertEquals("kzBZbkqJ3ADrj7v08reD1qcWUwNGHaidNUgD7nHpiw8=", var); + } + + @Test + public void caps2complex() throws IOException { + final String xml = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " urn:xmpp:dataforms:softwareinfo\n" + + " \n" + + " \n" + + " Tkabber\n" + + " \n" + + " \n" + + " 0.11.1-svn-20111216-mod (Tcl/Tk 8.6b2)\n" + + " \n" + + " \n" + + " Windows\n" + + " \n" + + " \n" + + " XP\n" + + " \n" + + " \n" + + ""; + final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8)); + assertThat(element, instanceOf(InfoQuery.class)); + final InfoQuery info = (InfoQuery) element; + final String var = BaseEncoding.base64().encode(EntityCapabilities2.hash(info)); + Assert.assertEquals("u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY=", var); + } }