Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Usernames/UsernameLinkManager.swift
1 views
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import LibSignalClient

/// Manages transforming between username links and plaintext usernames.
///
/// Username links do not directly encode a username. Instead, they encode
/// "entropy data" and a "handle UUID", which can be used (with support from the
/// service) to produce a plaintext username.
///
/// Specifically, the server stores an encrypted form of the user's username
/// which can be retrieved using the handle and decrypted using the entropy.
/// Importantly, the entropy is never made available to the server, and
/// consequently the usernames themselves are not exposed to the server.
///
/// This indirection allows the user to rotate their username link without
/// changing their username, by instead providing the server with a new
/// encrypted username blob derived from new entropy data (which will
/// correspond to a new handle).
///
/// Assuming a given link is not outdated, i.e. the link's creator has not
/// rotated their link, the plaintext username is available by fetching the
/// encrypted username blob from the service using the handle in the link, and
/// decrypting it using the entropy in the link.
public protocol UsernameLinkManager {
    /// Generate the encrypted username.
    ///
    /// To be used in a link, this username should be uploaded to the service in
    /// exchange for a handle.
    ///
    /// - Parameter existingEntropy
    /// Specific entropy to use when encrypting the username. If this is passed,
    /// the `entropy` return value will be equivalent to it.
    func generateEncryptedUsername(
        username: String,
        existingEntropy: Data?,
    ) throws -> (entropy: Data, encryptedUsername: Data)

    /// Uses the given link to fetch an encrypted username and decrypt it into a
    /// plaintext username.
    func decryptEncryptedLink(link: Usernames.UsernameLink) async throws -> String?
}

public final class UsernameLinkManagerImpl: UsernameLinkManager {
    private let db: any DB
    private let apiClient: UsernameApiClient

    init(
        db: any DB,
        apiClient: UsernameApiClient,
    ) {
        self.db = db
        self.apiClient = apiClient
    }

    public func generateEncryptedUsername(
        username: String,
        existingEntropy: Data?,
    ) throws -> (entropy: Data, encryptedUsername: Data) {
        let lscUsername = try LibSignalClient.Username(username)
        let (entropyBytes, encryptedUsernameBytes) = try lscUsername.createLink(previousEntropy: existingEntropy)

        return (
            entropy: entropyBytes,
            encryptedUsername: encryptedUsernameBytes,
        )
    }

    public func decryptEncryptedLink(
        link: Usernames.UsernameLink,
    ) async throws -> String? {
        return try await self.apiClient.getUsernameLink(handle: link.handle, entropy: link.entropy)?.value
    }
}