Incoming session verification support (#3428)

* Fixes #1227 - Add support for receiving and interacting with incoming session verification requests.
* Fix a couple of random small warnings
* Move static view config to the view state
* Update snapshots
This commit is contained in:
Stefan Ceriu
2024-10-29 15:21:17 +02:00
committed by GitHub
parent cf5e9fb313
commit d77bb935b7
104 changed files with 995 additions and 483 deletions
@@ -50,6 +50,8 @@ class ClientProxy: ClientProxyProtocol {
let secureBackupController: SecureBackupControllerProtocol
private(set) var sessionVerificationController: SessionVerificationControllerProxyProtocol?
private static var roomCreationPowerLevelOverrides: PowerLevels {
.init(usersDefault: nil,
eventsDefault: nil,
@@ -157,7 +159,16 @@ class ClientProxy: ClientProxyProtocol {
updateVerificationState(client.encryption().verificationState())
verificationStateListenerTaskHandle = client.encryption().verificationStateListener(listener: VerificationStateListenerProxy { [weak self] verificationState in
self?.updateVerificationState(verificationState)
guard let self else { return }
updateVerificationState(verificationState)
// The session verification controller requires the user's identity which
// isn't available before a keys query response. Use the verification
// state updates as an aproximation for when that happens.
Task {
await self.buildSessionVerificationControllerProxyIfPossible(verificationState: verificationState)
}
})
sendQueueListenerTaskHandle = client.subscribeToSendQueueStatus(listener: SendQueueRoomErrorListenerProxy { [weak self] roomID, error in
@@ -484,7 +495,8 @@ class ClientProxy: ClientProxyProtocol {
func roomPreviewForIdentifier(_ identifier: String, via: [String]) async -> Result<RoomPreviewDetails, ClientProxyError> {
do {
let roomPreview = try await client.getRoomPreviewFromRoomId(roomId: identifier, viaServers: via)
return .success(.init(roomPreview))
let roomPreviewInfo = try roomPreview.info()
return .success(.init(roomPreviewInfo))
} catch let error as ClientError where error.code == .forbidden {
return .failure(.roomPreviewIsPrivate)
} catch {
@@ -561,16 +573,6 @@ class ClientProxy: ClientProxyProtocol {
}
}
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError> {
do {
let sessionVerificationController = try await client.getSessionVerificationController()
return .success(SessionVerificationControllerProxy(sessionVerificationController: sessionVerificationController))
} catch {
MXLog.error("Failed retrieving session verification controller proxy with error: \(error)")
return .failure(.sdkError(error))
}
}
func logout() async -> URL? {
do {
return try await client.logout().flatMap(URL.init(string:))
@@ -728,6 +730,19 @@ class ClientProxy: ClientProxyProtocol {
verificationStateSubject.send(verificationState)
}
private func buildSessionVerificationControllerProxyIfPossible(verificationState: VerificationState) async {
guard sessionVerificationController == nil, verificationState != .unknown else {
return
}
do {
let sessionVerificationController = try await client.getSessionVerificationController()
self.sessionVerificationController = SessionVerificationControllerProxy(sessionVerificationController: sessionVerificationController)
} catch {
MXLog.error("Failed retrieving session verification controller proxy with error: \(error)")
}
}
private func loadUserAvatarURLFromCache() {
loadCachedAvatarURLTask = Task {
@@ -1079,17 +1094,17 @@ private class SendQueueRoomErrorListenerProxy: SendQueueRoomErrorListener {
}
private extension RoomPreviewDetails {
init(_ roomPreview: RoomPreview) {
self = RoomPreviewDetails(roomID: roomPreview.roomId,
name: roomPreview.name,
canonicalAlias: roomPreview.canonicalAlias,
topic: roomPreview.topic,
avatarURL: roomPreview.avatarUrl.flatMap(URL.init(string:)),
memberCount: UInt(roomPreview.numJoinedMembers),
isHistoryWorldReadable: roomPreview.isHistoryWorldReadable,
isJoined: roomPreview.isJoined,
isInvited: roomPreview.isInvited,
isPublic: roomPreview.isPublic,
canKnock: roomPreview.canKnock)
init(_ roomPreviewInfo: RoomPreviewInfo) {
self = RoomPreviewDetails(roomID: roomPreviewInfo.roomId,
name: roomPreviewInfo.name,
canonicalAlias: roomPreviewInfo.canonicalAlias,
topic: roomPreviewInfo.topic,
avatarURL: roomPreviewInfo.avatarUrl.flatMap(URL.init(string:)),
memberCount: UInt(roomPreviewInfo.numJoinedMembers),
isHistoryWorldReadable: roomPreviewInfo.isHistoryWorldReadable,
isJoined: roomPreviewInfo.membership == .joined,
isInvited: roomPreviewInfo.membership == .invited,
isPublic: roomPreviewInfo.joinRule == .public,
canKnock: roomPreviewInfo.joinRule == .knock)
}
}
@@ -115,6 +115,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
var secureBackupController: SecureBackupControllerProtocol { get }
var sessionVerificationController: SessionVerificationControllerProxyProtocol? { get }
func isOnlyDeviceLeft() async -> Result<Bool, ClientProxyError>
func startSync()
@@ -155,8 +157,6 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
func setUserAvatar(media: MediaInfo) async -> Result<Void, ClientProxyError>
func removeUserAvatar() async -> Result<Void, ClientProxyError>
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError>
func deactivateAccount(password: String?, eraseData: Bool) async -> Result<Void, ClientProxyError>
@@ -246,10 +246,8 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
#if targetEnvironment(simulator)
// This gets called for no reason on simulators, where CallKit
// isn't even supported. Ignore
return
#endif
// isn't even supported, ignore it.
#else
if let ongoingCallID {
actionsSubject.send(.endCall(roomID: ongoingCallID.roomID))
}
@@ -257,6 +255,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
tearDownCallSession(sendEndCallAction: false)
action.fulfill()
#endif
}
// MARK: - Private
@@ -18,6 +18,10 @@ private class WeakSessionVerificationControllerProxy: SessionVerificationControl
// MARK: - SessionVerificationControllerDelegate
func didReceiveVerificationRequest(details: MatrixRustSDK.SessionVerificationRequestDetails) {
proxy?.didReceiveVerificationRequest(details: details)
}
func didReceiveVerificationData(data: MatrixRustSDK.SessionVerificationData) {
switch data {
// We can handle only emojis for now
@@ -54,86 +58,142 @@ class SessionVerificationControllerProxy: SessionVerificationControllerProxyProt
init(sessionVerificationController: SessionVerificationController) {
self.sessionVerificationController = sessionVerificationController
sessionVerificationController.setDelegate(delegate: WeakSessionVerificationControllerProxy(proxy: self))
}
deinit {
sessionVerificationController.setDelegate(delegate: nil)
}
let callbacks = PassthroughSubject<SessionVerificationControllerProxyCallback, Never>()
let actions = PassthroughSubject<SessionVerificationControllerProxyAction, Never>()
func acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) async -> Result<Void, SessionVerificationControllerProxyError> {
MXLog.info("Acknowledging verification request")
do {
try await sessionVerificationController.acknowledgeVerificationRequest(senderId: details.senderID, flowId: details.flowID)
return .success(())
} catch {
MXLog.error("Failed requesting session verification with error: \(error)")
return .failure(.failedAcknowledgingVerificationRequest)
}
}
func acceptVerificationRequest() async -> Result<Void, SessionVerificationControllerProxyError> {
MXLog.info("Accepting verification request")
do {
try await sessionVerificationController.acceptVerificationRequest()
return .success(())
} catch {
MXLog.error("Failed requesting session verification with error: \(error)")
return .failure(.failedAcceptingVerificationRequest)
}
}
func requestVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
sessionVerificationController.setDelegate(delegate: WeakSessionVerificationControllerProxy(proxy: self))
MXLog.info("Requesting session verification")
do {
try await sessionVerificationController.requestVerification()
return .success(())
} catch {
MXLog.error("Failed requesting session verification with error: \(error)")
return .failure(.failedRequestingVerification)
}
}
func startSasVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
MXLog.info("Starting SAS verification")
do {
try await sessionVerificationController.startSasVerification()
return .success(())
} catch {
MXLog.error("Failed starting SAS verification with error: \(error)")
return .failure(.failedStartingSasVerification)
}
}
func approveVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
MXLog.info("Approving verification")
do {
try await sessionVerificationController.approveVerification()
return .success(())
} catch {
MXLog.error("Failed approving verification with error: \(error)")
return .failure(.failedApprovingVerification)
}
}
func declineVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
MXLog.info("Declining verification")
do {
try await sessionVerificationController.declineVerification()
return .success(())
} catch {
MXLog.error("Failed declining verification with error: \(error)")
return .failure(.failedDecliningVerification)
}
}
func cancelVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
MXLog.info("Cancelling verification")
do {
try await sessionVerificationController.cancelVerification()
return .success(())
} catch {
MXLog.error("Failed cancelling verification with error: \(error)")
return .failure(.failedCancellingVerification)
}
}
// MARK: - Private
fileprivate func didReceiveVerificationRequest(details: MatrixRustSDK.SessionVerificationRequestDetails) {
MXLog.info("Received verification request \(details)")
let details = SessionVerificationRequestDetails(senderID: details.senderId,
flowID: details.flowId,
deviceID: details.deviceId,
displayName: details.displayName,
firstSeenDate: Date(timeIntervalSince1970: TimeInterval(details.firstSeenTimestamp / 1000)))
actions.send(.receivedVerificationRequest(details: details))
}
fileprivate func didAcceptVerificationRequest() {
callbacks.send(.acceptedVerificationRequest)
MXLog.info("Accepted verification request")
actions.send(.acceptedVerificationRequest)
}
fileprivate func didStartSasVerification() {
callbacks.send(.startedSasVerification)
MXLog.info("Started SAS verification")
actions.send(.startedSasVerification)
}
fileprivate func didReceiveData(_ data: [MatrixRustSDK.SessionVerificationEmoji]) {
callbacks.send(.receivedVerificationData(data.map { emoji in
MXLog.info("Received verification data")
actions.send(.receivedVerificationData(data.map { emoji in
SessionVerificationEmoji(symbol: emoji.symbol(), description: emoji.description())
}))
}
fileprivate func didFail() {
callbacks.send(.failed)
actions.send(.failed)
}
fileprivate func didFinish() {
callbacks.send(.finished)
actions.send(.finished)
}
fileprivate func didCancel() {
callbacks.send(.cancelled)
actions.send(.cancelled)
}
}
@@ -7,8 +7,11 @@
import Combine
import Foundation
import MatrixRustSDK
enum SessionVerificationControllerProxyError: Error {
case failedAcknowledgingVerificationRequest
case failedAcceptingVerificationRequest
case failedRequestingVerification
case failedStartingSasVerification
case failedApprovingVerification
@@ -16,7 +19,8 @@ enum SessionVerificationControllerProxyError: Error {
case failedCancellingVerification
}
enum SessionVerificationControllerProxyCallback {
enum SessionVerificationControllerProxyAction {
case receivedVerificationRequest(details: SessionVerificationRequestDetails)
case acceptedVerificationRequest
case startedSasVerification
case receivedVerificationData([SessionVerificationEmoji])
@@ -25,6 +29,14 @@ enum SessionVerificationControllerProxyCallback {
case failed
}
struct SessionVerificationRequestDetails {
let senderID: String
let flowID: String
let deviceID: String
let displayName: String?
let firstSeenDate: Date
}
struct SessionVerificationEmoji: Hashable {
let symbol: String
let description: String
@@ -36,7 +48,11 @@ struct SessionVerificationEmoji: Hashable {
// sourcery: AutoMockable
protocol SessionVerificationControllerProxyProtocol {
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> { get }
var actions: PassthroughSubject<SessionVerificationControllerProxyAction, Never> { get }
func acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) async -> Result<Void, SessionVerificationControllerProxyError>
func acceptVerificationRequest() async -> Result<Void, SessionVerificationControllerProxyError>
func requestVerification() async -> Result<Void, SessionVerificationControllerProxyError>