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:
Stefan Ceriu
2022-07-01 13:56:52 +03:00
committed by GitHub
parent 266ca656fc
commit a6f9d1ba89
30 changed files with 1604 additions and 98 deletions
@@ -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 }
}
@@ -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")
]
}
}
@@ -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)
}
}
@@ -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 {