mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
vector-im/element-x-ios/issues/31 - Implement simple SaS sesson verification
* vector-im/element-x-ios/issues/31 - Session verification unit and UI tests * Fix ImageAnonymizer tests when running on Apple Silicon * Rename SessionVerification to SessionVerificationScreen
This commit is contained in:
@@ -27,6 +27,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
private let client: Client
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
private var sessionVerificationControllerProxy: SessionVerificationControllerProxy?
|
||||
|
||||
private(set) var rooms: [RoomProxy] = [] {
|
||||
didSet {
|
||||
@@ -96,11 +97,25 @@ class ClientProxy: ClientProxyProtocol {
|
||||
return Data(bytes: bytes, count: bytes.count)
|
||||
}
|
||||
|
||||
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError> {
|
||||
await Task.detached {
|
||||
do {
|
||||
let sessionVerificationController = try self.client.getSessionVerificationController()
|
||||
return .success(SessionVerificationControllerProxy(sessionVerificationController: sessionVerificationController))
|
||||
} catch {
|
||||
return .failure(.failedRetrievingSessionVerificationController)
|
||||
}
|
||||
}
|
||||
.value
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
fileprivate func didReceiveSyncUpdate() {
|
||||
Benchmark.logElapsedDurationForIdentifier("ClientSync", message: "Received sync update")
|
||||
|
||||
callbacks.send(.receivedSyncUpdate)
|
||||
|
||||
Task.detached {
|
||||
await self.updateRooms()
|
||||
}
|
||||
|
||||
@@ -12,11 +12,13 @@ import Combine
|
||||
|
||||
enum ClientProxyCallback {
|
||||
case updatedRoomsList
|
||||
case receivedSyncUpdate
|
||||
}
|
||||
|
||||
enum ClientProxyError: Error {
|
||||
case failedRetrievingAvatarURL
|
||||
case failedRetrievingDisplayName
|
||||
case failedRetrievingSessionVerificationController
|
||||
}
|
||||
|
||||
protocol ClientProxyProtocol {
|
||||
@@ -33,4 +35,6 @@ protocol ClientProxyProtocol {
|
||||
func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource
|
||||
|
||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data
|
||||
|
||||
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError>
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@ class RoomProxy: RoomProxyProtocol {
|
||||
backwardStream = room.startLiveEventListener()
|
||||
}
|
||||
|
||||
deinit {
|
||||
room.setDelegate(delegate: nil)
|
||||
}
|
||||
|
||||
var id: String {
|
||||
room.id()
|
||||
}
|
||||
|
||||
@@ -7,13 +7,64 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
class UserSession: UserSessionProtocol {
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var checkForSessionVerificationControllerCancellable: AnyCancellable?
|
||||
|
||||
let clientProxy: ClientProxyProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let callbacks = PassthroughSubject<UserSessionCallback, Never>()
|
||||
private(set) var sessionVerificationController: SessionVerificationControllerProxyProtocol?
|
||||
|
||||
init(clientProxy: ClientProxyProtocol, mediaProvider: MediaProviderProtocol) {
|
||||
self.clientProxy = clientProxy
|
||||
self.mediaProvider = mediaProvider
|
||||
|
||||
setupSessionVerificationWatchdog()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupSessionVerificationWatchdog() {
|
||||
checkForSessionVerificationControllerCancellable = clientProxy.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
if case .receivedSyncUpdate = callback {
|
||||
self?.attemptSessionVerification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func attemptSessionVerification() {
|
||||
Task {
|
||||
switch await clientProxy.sessionVerificationControllerProxy() {
|
||||
case .success(let sessionVerificationController):
|
||||
tearDownSessionVerificationControllerWatchdog()
|
||||
|
||||
if !sessionVerificationController.isVerified {
|
||||
callbacks.send(.sessionVerificationNeeded)
|
||||
}
|
||||
|
||||
self.sessionVerificationController = sessionVerificationController
|
||||
|
||||
sessionVerificationController.callbacks.sink { callback in
|
||||
switch callback {
|
||||
case .finished:
|
||||
self.callbacks.send(.didVerifySession)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed getting session verification controller with error: \(error). Will retry on the next sync update.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func tearDownSessionVerificationControllerWatchdog() {
|
||||
checkForSessionVerificationControllerCancellable = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,18 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
enum UserSessionCallback {
|
||||
case sessionVerificationNeeded
|
||||
case didVerifySession
|
||||
}
|
||||
|
||||
protocol UserSessionProtocol {
|
||||
var clientProxy: ClientProxyProtocol { get }
|
||||
var mediaProvider: MediaProviderProtocol { get }
|
||||
|
||||
var sessionVerificationController: SessionVerificationControllerProxyProtocol? { get }
|
||||
|
||||
var callbacks: PassthroughSubject<UserSessionCallback, Never> { get }
|
||||
}
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// MockSessionVerificationControllerProxy.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 07/06/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import Combine
|
||||
|
||||
struct MockSessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol {
|
||||
var callbacks = PassthroughSubject<SessionVerificationControllerProxyCallback, Never>()
|
||||
|
||||
var isVerified: Bool = false
|
||||
|
||||
func requestVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
|
||||
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 2.0) {
|
||||
callbacks.send(.receivedVerificationData(Self.emojis))
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func approveVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
|
||||
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 2.0) {
|
||||
callbacks.send(.finished)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func declineVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
|
||||
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 2.0) {
|
||||
callbacks.send(.cancelled)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func cancelVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
|
||||
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 2.0) {
|
||||
callbacks.send(.cancelled)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
static var emojis: [SessionVerificationEmoji] {
|
||||
[SessionVerificationEmoji(symbol: "🦋", description: "Butterfly"),
|
||||
SessionVerificationEmoji(symbol: "🐘", description: "Elephant"),
|
||||
SessionVerificationEmoji(symbol: "🦋", description: "Butterfly"),
|
||||
SessionVerificationEmoji(symbol: "🎂", description: "Cake"),
|
||||
SessionVerificationEmoji(symbol: "🎂", description: "Cake"),
|
||||
SessionVerificationEmoji(symbol: "🏁", description: "Flag"),
|
||||
SessionVerificationEmoji(symbol: "🌏", description: "Globe")
|
||||
]
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
//
|
||||
// SessionVerificationControllerProxy.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 06/06/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import Combine
|
||||
|
||||
private class WeakSessionVerificationControllerProxy: SessionVerificationControllerDelegate {
|
||||
private weak var proxy: SessionVerificationControllerProxy?
|
||||
|
||||
init(proxy: SessionVerificationControllerProxy) {
|
||||
self.proxy = proxy
|
||||
}
|
||||
|
||||
// MARK: - SessionVerificationControllerDelegate
|
||||
|
||||
func didReceiveVerificationData(data: [MatrixRustSDK.SessionVerificationEmoji]) {
|
||||
proxy?.didReceiveData(data)
|
||||
}
|
||||
|
||||
func didFail() {
|
||||
proxy?.didFail()
|
||||
}
|
||||
|
||||
func didCancel() {
|
||||
proxy?.didCancel()
|
||||
}
|
||||
|
||||
func didFinish() {
|
||||
proxy?.didFinish()
|
||||
}
|
||||
}
|
||||
|
||||
class SessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol {
|
||||
private let sessionVerificationController: SessionVerificationController
|
||||
|
||||
init(sessionVerificationController: SessionVerificationController) {
|
||||
self.sessionVerificationController = sessionVerificationController
|
||||
sessionVerificationController.setDelegate(delegate: WeakSessionVerificationControllerProxy(proxy: self))
|
||||
}
|
||||
|
||||
deinit {
|
||||
sessionVerificationController.setDelegate(delegate: nil)
|
||||
}
|
||||
|
||||
let callbacks = PassthroughSubject<SessionVerificationControllerProxyCallback, Never>()
|
||||
|
||||
var isVerified: Bool {
|
||||
sessionVerificationController.isVerified()
|
||||
}
|
||||
|
||||
func requestVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
|
||||
await Task.detached {
|
||||
do {
|
||||
try self.sessionVerificationController.requestVerification()
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedRequestingVerification)
|
||||
}
|
||||
}
|
||||
.value
|
||||
}
|
||||
|
||||
func approveVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
|
||||
await Task.detached {
|
||||
do {
|
||||
try self.sessionVerificationController.approveVerification()
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedApprovingVerification)
|
||||
}
|
||||
}
|
||||
.value
|
||||
}
|
||||
|
||||
func declineVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
|
||||
await Task.detached {
|
||||
do {
|
||||
try self.sessionVerificationController.declineVerification()
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedDecliningVerification)
|
||||
}
|
||||
}
|
||||
.value
|
||||
}
|
||||
|
||||
func cancelVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
|
||||
await Task.detached {
|
||||
do {
|
||||
try self.sessionVerificationController.cancelVerification()
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedCancellingVerification)
|
||||
}
|
||||
}
|
||||
.value
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
fileprivate func didReceiveData(_ data: [MatrixRustSDK.SessionVerificationEmoji]) {
|
||||
callbacks.send(.receivedVerificationData(data.map { emoji in
|
||||
SessionVerificationEmoji(symbol: emoji.symbol(), description: emoji.description())
|
||||
}))
|
||||
}
|
||||
|
||||
fileprivate func didFail() {
|
||||
callbacks.send(.failed)
|
||||
}
|
||||
|
||||
fileprivate func didFinish() {
|
||||
callbacks.send(.finished)
|
||||
}
|
||||
|
||||
fileprivate func didCancel() {
|
||||
callbacks.send(.cancelled)
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// SessionVerificationControllerProxyProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 07/06/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
enum SessionVerificationControllerProxyError: Error {
|
||||
case failedRequestingVerification
|
||||
case failedApprovingVerification
|
||||
case failedDecliningVerification
|
||||
case failedCancellingVerification
|
||||
}
|
||||
|
||||
enum SessionVerificationControllerProxyCallback {
|
||||
case receivedVerificationData([SessionVerificationEmoji])
|
||||
case finished
|
||||
case cancelled
|
||||
case failed
|
||||
}
|
||||
|
||||
struct SessionVerificationEmoji: Hashable {
|
||||
let symbol: String
|
||||
let description: String
|
||||
}
|
||||
|
||||
protocol SessionVerificationControllerProxyProtocol {
|
||||
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> { get }
|
||||
|
||||
var isVerified: Bool { get }
|
||||
|
||||
func requestVerification() async -> Result<Void, SessionVerificationControllerProxyError>
|
||||
|
||||
func approveVerification() async -> Result<Void, SessionVerificationControllerProxyError>
|
||||
|
||||
func declineVerification() async -> Result<Void, SessionVerificationControllerProxyError>
|
||||
|
||||
func cancelVerification() async -> Result<Void, SessionVerificationControllerProxyError>
|
||||
}
|
||||
@@ -18,7 +18,9 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
init(roomProxy: RoomProxyProtocol) {
|
||||
self.roomProxy = roomProxy
|
||||
|
||||
self.roomProxy.callbacks.sink { [weak self] callback in
|
||||
self.roomProxy.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch callback {
|
||||
|
||||
Reference in New Issue
Block a user