core, ui: security code verification in channels (verify member key) (#7255)

This commit is contained in:
spaced4ndy
2026-07-15 10:46:31 +00:00
committed by GitHub
parent 6e2995cab6
commit 0fef34914d
28 changed files with 250 additions and 88 deletions
@@ -2535,7 +2535,8 @@ data class GroupMember (
var activeConn: Connection? = null,
val supportChat: GroupSupportChat? = null,
val memberChatVRange: VersionRange,
val relayLink: String? = null
val relayLink: String? = null,
val memberVerifiedCode: SecurityCode? = null
): NamedChat {
val id: String get() = "#$groupId @$groupMemberId"
val ready get() = activeConn?.connStatus == ConnStatus.Ready
@@ -2555,7 +2556,7 @@ data class GroupMember (
override val shortDescr: String? get() = memberProfile.shortDescr
override val image: String? get() = memberProfile.image
val contactLink: String? = memberProfile.contactLink
val verified get() = activeConn?.connectionCode != null
val verified get() = memberVerifiedCode != null || activeConn?.connectionCode != null
// the badge shown for a member's name; a badge that expired over a month ago (ExpiredOld) is not shown
val nameBadge: LocalBadge? get() {
val badge = memberProfile.localBadge
@@ -12,6 +12,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
@@ -29,12 +30,14 @@ fun VerifyCodeView(
connectionVerified: Boolean,
verify: suspend (String?) -> Pair<Boolean, String>?,
close: () -> Unit,
verifyDescription: StringResource = MR.strings.to_verify_compare,
) {
if (connectionCode != null) {
VerifyCodeLayout(
displayName,
connectionCode,
connectionVerified,
verifyDescription,
verifyCode = { newCode ->
val res = verify(newCode)
if (res != null) {
@@ -54,6 +57,7 @@ private fun VerifyCodeLayout(
displayName: String,
connectionCode: String,
connectionVerified: Boolean,
verifyDescription: StringResource,
verifyCode: suspend (String?) -> Boolean,
) {
ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) {
@@ -90,7 +94,7 @@ private fun VerifyCodeLayout(
}
Text(
generalGetString(MR.strings.to_verify_compare),
generalGetString(verifyDescription),
Modifier.padding(bottom = DEFAULT_PADDING)
)
@@ -126,7 +126,7 @@ fun ModalData.GroupChatInfoView(
withBGApi {
val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, member.groupMemberId)
val stats = r?.second
val (_, code) = if (member.memberActive) {
val (_, code) = if ((member.memberActive || (groupInfo.useRelays && member.memberCurrent)) && member.memberRole != GroupMemberRole.Relay) {
val memCode = chatModel.controller.apiGetGroupMemberCode(rhId, groupInfo.apiId, member.groupMemberId)
member to memCode?.second
} else {
@@ -214,11 +214,12 @@ fun GroupMemberInfoView(
verify = { code ->
chatModel.controller.apiVerifyGroupMember(rhId, mem.groupId, mem.groupMemberId, code)?.let { r ->
val (verified, existingCode) = r
val copy = mem.copy(
activeConn = mem.activeConn?.copy(
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
)
)
val code = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
val copy = if (groupInfo.useRelays) {
mem.copy(memberVerifiedCode = code)
} else {
mem.copy(activeConn = mem.activeConn?.copy(connectionCode = code))
}
withContext(Dispatchers.Main) {
chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, copy)
}
@@ -229,6 +230,7 @@ fun GroupMemberInfoView(
}
},
close,
verifyDescription = if (groupInfo.useRelays) MR.strings.to_verify_channel_member_key else MR.strings.to_verify_compare,
)
}
}
@@ -541,16 +543,19 @@ fun GroupMemberInfoLayout(
member.memberRole != GroupMemberRole.Relay &&
((groupInfo.fullGroupPreferences.support.on && member.memberRole < GroupMemberRole.Moderator)
|| member.supportChat != null)
val canVerifyCode = connectionCode != null && member.memberRole != GroupMemberRole.Relay
val canSyncConn = cStats != null && cStats.ratchetSyncAllowed
if (member.memberActive) {
if ((member.memberActive || (groupInfo.useRelays && member.memberCurrent))
&& (showMemberSupportChat || canVerifyCode || canSyncConn)) {
SectionView {
if (showMemberSupportChat) {
SupportChatButton()
}
if (connectionCode != null && !(groupInfo.useRelays && member.memberRole == GroupMemberRole.Relay)) {
if (canVerifyCode) {
VerifyCodeButton(member.verified, verifyClicked)
}
if (cStats != null && cStats.ratchetSyncAllowed) {
if (canSyncConn) {
SynchronizeConnectionButton(syncMemberConnection)
}
// } else if (developerTools) {
@@ -558,11 +563,6 @@ fun GroupMemberInfoLayout(
// }
}
SectionDividerSpaced()
} else if (groupInfo.useRelays && member.memberCurrent && showMemberSupportChat) {
SectionView {
SupportChatButton()
}
SectionDividerSpaced()
}
if (member.contactLink != null) {
@@ -79,7 +79,7 @@ fun MemberSupportChatAppBar(
withBGApi {
val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, scopeMember_.groupMemberId)
val stats = r?.second
val code = if (scopeMember_.memberActive) {
val code = if ((scopeMember_.memberActive || (groupInfo.useRelays && scopeMember_.memberCurrent)) && scopeMember_.memberRole != GroupMemberRole.Relay) {
val memCode = chatModel.controller.apiGetGroupMemberCode(rhId, groupInfo.apiId, scopeMember_.groupMemberId)
memCode?.second
} else {
@@ -1013,6 +1013,7 @@
<string name="mark_code_verified">Mark verified</string>
<string name="clear_verification">Clear verification</string>
<string name="to_verify_compare">To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</string>
<string name="to_verify_channel_member_key">To verify keys with this subscriber, compare (or scan) the code on your devices.</string>
<string name="is_verified">%s is verified</string>
<string name="is_not_verified">%s is not verified</string>