From f0f25ae12ec8c3bf52fa865e60ac32682ee97a3f Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 5 Mar 2025 12:35:33 -0500 Subject: [PATCH] Use the new avatar color hash algorithm. --- .../conversation/colors/AvatarColorHash.kt | 23 +++++------- .../securesms/database/RecipientTable.kt | 4 +-- .../colors/AvatarColorHashTest.kt | 36 +++++++++++++++++++ 3 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 app/src/test/java/org/thoughtcrime/securesms/conversation/colors/AvatarColorHashTest.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColorHash.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColorHash.kt index 6f6094de27..dec8a689ba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColorHash.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColorHash.kt @@ -5,7 +5,9 @@ package org.thoughtcrime.securesms.conversation.colors +import org.signal.core.util.CryptoUtil import org.thoughtcrime.securesms.groups.GroupId +import org.whispersystems.signalservice.api.push.ServiceId /** * Stolen from iOS. Utilizes a simple hash to map different characteristics to an avatar color index. @@ -17,13 +19,13 @@ object AvatarColorHash { * * Uppercase is necessary here because iOS utilizes uppercase UUIDs by default. */ - fun forAddress(serviceId: String?, e164: String?): AvatarColor { - if (!serviceId.isNullOrEmpty()) { - return forSeed(serviceId.toString().uppercase()) + fun forAddress(serviceId: ServiceId?, e164: String?): AvatarColor { + if (serviceId != null) { + return forData(serviceId.toByteArray()) } if (!e164.isNullOrEmpty()) { - return forSeed(e164) + return forData(e164.toByteArray(Charsets.UTF_8)) } return AvatarColor.A100 @@ -33,22 +35,15 @@ object AvatarColorHash { return forData(group.decodedId) } - fun forSeed(seed: String): AvatarColor { - return forData(seed.toByteArray()) - } - @JvmStatic fun forCallLink(rootKey: ByteArray): AvatarColor { return forIndex(rootKey.first().toInt()) } private fun forData(data: ByteArray): AvatarColor { - var hash = 0 - for (value in data) { - hash = hash.rotateLeft(3) xor value.toInt() - } - - return forIndex(hash) + val hash = CryptoUtil.sha256(data) + val firstByte: Byte = hash[0] + return forIndex(firstByte.toInt()) } private fun forIndex(index: Int): AvatarColor { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index 2d78545385..abdad1eb60 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -4098,7 +4098,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da PNI_COLUMN to pni?.toString(), PNI_SIGNATURE_VERIFIED to pniVerified.toInt(), STORAGE_SERVICE_ID to Base64.encodeWithPadding(StorageSyncHelper.generateKey()), - AVATAR_COLOR to AvatarColorHash.forAddress((aci ?: pni)?.toString(), e164).serialize() + AVATAR_COLOR to AvatarColorHash.forAddress((aci ?: pni), e164).serialize() ) if (pni != null || aci != null) { @@ -4155,7 +4155,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da } if (isInsert) { - put(AVATAR_COLOR, AvatarColorHash.forAddress(contact.proto.signalAci?.toString() ?: contact.proto.signalPni?.toString(), contact.proto.e164).serialize()) + put(AVATAR_COLOR, AvatarColorHash.forAddress(contact.proto.signalAci ?: contact.proto.signalPni, contact.proto.e164).serialize()) } } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversation/colors/AvatarColorHashTest.kt b/app/src/test/java/org/thoughtcrime/securesms/conversation/colors/AvatarColorHashTest.kt new file mode 100644 index 0000000000..9ad29fa179 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/conversation/colors/AvatarColorHashTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.conversation.colors + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.signal.core.util.Base64 +import org.thoughtcrime.securesms.groups.GroupId +import org.whispersystems.signalservice.api.push.ServiceId.ACI +import org.whispersystems.signalservice.api.push.ServiceId.PNI + +class AvatarColorHashTest { + + @Test + fun `hash test vector - ACI`() { + assertEquals(AvatarColor.A140, AvatarColorHash.forAddress(ACI.parseOrThrow("a025bf78-653e-44e0-beb9-deb14ba32487"), null)) + } + + @Test + fun `hash test vector - PNI`() { + assertEquals(AvatarColor.A200, AvatarColorHash.forAddress(PNI.parseOrThrow("11a175e3-fe31-4eda-87da-e0bf2a2e250b"), null)) + } + + @Test + fun `hash test vector - E164`() { + assertEquals(AvatarColor.A150, AvatarColorHash.forAddress(null, "+12135550124")) + } + + @Test + fun `hash test vector - GroupId`() { + assertEquals(AvatarColor.A130, AvatarColorHash.forGroupId(GroupId.V2.push(Base64.decode("BwJRIdomqOSOckHjnJsknNCibCZKJFt+RxLIpa9CWJ4=")))) + } +}