Path: blob/main/SignalServiceKit/Usernames/UsernameValidationManager.swift
1 views
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import LibSignalClient
extension Usernames {
public enum Validation {
public enum Shims {
public typealias MessageProcessor = _UsernameValidationManager_MessageProcessorShim
public typealias StorageServiceManager = _UsernameValidationManager_StorageServiceManagerShim
}
enum Wrappers {
typealias MessageProcessor = _UsernameValidationManager_MessageProcessorWrapper
typealias StorageServiceManager = _UsernameValidationManager_StorageServiceManagerWrapper
}
}
}
public protocol UsernameValidationManager {
func validateUsername() async throws -> Bool
}
// MARK: -
public class UsernameValidationManagerImpl: UsernameValidationManager {
struct Context {
let database: any DB
let localUsernameManager: LocalUsernameManager
let messageProcessor: Usernames.Validation.Shims.MessageProcessor
let storageServiceManager: Usernames.Validation.Shims.StorageServiceManager
let usernameLinkManager: UsernameLinkManager
let whoAmIManager: WhoAmIManager
}
// MARK: Init
private let context: Context
private let taskQueue = ConcurrentTaskQueue(concurrentLimit: 1)
private var logger: UsernameLogger { .shared }
init(context: Context) {
self.context = context
}
// MARK: Username Validation
public func validateUsername() async throws -> Bool {
do {
return try await taskQueue.run {
return try await self._validateUsername()
}
} catch {
logger.error("Error validating username and/or link: \(error)")
throw error
}
}
private func _validateUsername() async throws -> Bool {
logger.info("Validating username.")
try await ensureUsernameStateUpToDate()
let localUsernameState = self.context.database.read { tx in
return self.context.localUsernameManager.usernameState(tx: tx)
}
switch localUsernameState {
case .unset:
// If we validate that we have no local username we can skip
// validating the username link as it's irrelevant.
return try await validateLocalUsernameAgainstService(localUsername: nil)
case let .available(username, usernameLink):
// If we have a username and we're in a good state, try and
// validate both the username and the link.
guard try await validateLocalUsernameAgainstService(localUsername: username) else {
return false
}
return try await validateLocalUsernameLinkAgainstService(
localUsername: username,
localUsernameLink: usernameLink,
)
case let .linkCorrupted(username):
// If we have a username but know our link is broken, no need to
// validate the link. (What would we even validate?)
return try await validateLocalUsernameAgainstService(localUsername: username)
case .usernameAndLinkCorrupted:
// If we know we're in a bad state, we can skip validation.
return false
}
}
/// Ensure that we have the latest local state regarding our username.
///
/// All of a user's devices can update the username and username link.
/// Consequently, before we do any comparison of local and remote state, we
/// should ensure we have the latest state from any linked devices.
///
/// We first finish message processing, specifically because we might find a
/// "fetch latest" sync message telling us to restore from Storage Service.
/// We then wait for any in-progress restores.
///
/// After these steps, we can be confident that we have the latest on our
/// username.
private func ensureUsernameStateUpToDate() async throws {
try await self.context.messageProcessor.waitForFetchingAndProcessing()
try await self.context.storageServiceManager.waitForPendingRestores()
}
/// Validate the local username against the value stored on the service.
///
/// - Returns
/// A promise that resolves with the local username (if any), if the local
/// value matches the service. The promise rejects if the local username
/// does not match the service.
private func validateLocalUsernameAgainstService(
localUsername: String?,
) async throws -> Bool {
let whoAmIResponse = try await self.context.whoAmIManager.makeWhoAmIRequest()
let validationSucceeded: Bool = {
self.logger.info("Comparing usernames; local: \(localUsername != nil), remote: \(whoAmIResponse.usernameHash != nil)")
switch (localUsername, whoAmIResponse.usernameHash) {
case (nil, nil):
// Both missing -> good
return true
case (nil, .some), (.some, nil):
// One missing, one set -> bad
return false
case let (.some(localUsername), .some(remoteUsernameHash)):
// Both present -> check the values
guard
let hashedLocalUsername = try? Usernames.HashedUsername(
forUsername: localUsername,
)
else {
return false
}
return hashedLocalUsername.hashString == remoteUsernameHash
}
}()
if validationSucceeded {
self.logger.info("Username validated successfully.")
} else {
self.logger.warn("Username validation failed: marking local username as corrupted!")
await self.context.database.awaitableWrite { tx in
self.context.localUsernameManager.setLocalUsernameCorrupted(
tx: tx,
)
}
}
return validationSucceeded
}
private func validateLocalUsernameLinkAgainstService(
localUsername: String,
localUsernameLink: Usernames.UsernameLink,
) async throws -> Bool {
let validationSucceeded: Bool
do {
let usernameForLocalLink = try await self.context.usernameLinkManager.decryptEncryptedLink(
link: localUsernameLink,
)
if usernameForLocalLink == nil {
self.logger.warn("Couldn't find our own username link")
}
validationSucceeded = (usernameForLocalLink == localUsername)
} catch
LibSignalClient.SignalError.usernameLinkInvalidEntropyDataLength,
LibSignalClient.SignalError.usernameLinkInvalid
{
self.logger.warn("Couldn't parse our own username link")
validationSucceeded = false
}
if validationSucceeded {
self.logger.info("Successfully validated our own username link")
} else {
self.logger.warn("Couldn't validate our own username link; marking invalid")
await self.context.database.awaitableWrite { tx in
self.context.localUsernameManager.setLocalUsernameWithCorruptedLink(
username: localUsername,
tx: tx,
)
}
}
return validationSucceeded
}
}
// MARK: - Protocolized Wrappers
// MARK: MessageProcessor
public protocol _UsernameValidationManager_MessageProcessorShim {
func waitForFetchingAndProcessing() async throws(CancellationError)
}
class _UsernameValidationManager_MessageProcessorWrapper: Usernames.Validation.Shims.MessageProcessor {
private let messageProcessor: MessageProcessor
init(_ messageProcessor: MessageProcessor) {
self.messageProcessor = messageProcessor
}
func waitForFetchingAndProcessing() async throws(CancellationError) {
try await messageProcessor.waitForFetchingAndProcessing()
}
}
// MARK: StorageServiceManager
public protocol _UsernameValidationManager_StorageServiceManagerShim {
func waitForPendingRestores() async throws
}
class _UsernameValidationManager_StorageServiceManagerWrapper: Usernames.Validation.Shims.StorageServiceManager {
private let storageServiceManager: StorageServiceManager
init(_ storageServiceManager: StorageServiceManager) {
self.storageServiceManager = storageServiceManager
}
func waitForPendingRestores() async throws {
try await storageServiceManager.waitForPendingRestores()
}
}