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

import Foundation
public import LibSignalClient

public struct GroupV2Params {
    public let groupSecretParams: GroupSecretParams
    public let groupSecretParamsData: Data
    public let groupPublicParams: GroupPublicParams
    public let groupPublicParamsData: Data

    public init(groupSecretParams: GroupSecretParams) throws {
        self.groupSecretParams = groupSecretParams
        self.groupSecretParamsData = groupSecretParams.serialize()
        let groupPublicParams = try groupSecretParams.getPublicParams()
        self.groupPublicParams = groupPublicParams
        self.groupPublicParamsData = groupPublicParams.serialize()
    }
}

// MARK: -

public extension TSGroupModelV2 {
    func groupV2Params() throws -> GroupV2Params {
        return try GroupV2Params(groupSecretParams: try GroupSecretParams(contents: secretParamsData))
    }
}

// MARK: -

public extension GroupV2Params {

    private func encryptString(_ value: String) throws -> Data {
        return try encryptBlob(Data(value.utf8))
    }

    private func decryptString(_ data: Data) throws -> String {
        let plaintext = try decryptBlob(data)
        guard let string = String(bytes: plaintext, encoding: .utf8) else {
            throw OWSAssertionError("Could not decrypt value.")
        }
        return string
    }

    private func encryptBlob(_ plaintext: Data) throws -> Data {
        let clientZkGroupCipher = ClientZkGroupCipher(groupSecretParams: groupSecretParams)
        let ciphertext = try clientZkGroupCipher.encryptBlob(plaintext: plaintext)
        assert(ciphertext != plaintext)
        assert(!ciphertext.isEmpty)

        if plaintext.count <= Self.decryptedBlobCacheMaxItemSize {
            let cacheKey = (ciphertext + groupSecretParamsData)
            Self.decryptedBlobCache.setObject(plaintext, forKey: cacheKey)
        }

        return ciphertext
    }

    private static let decryptedBlobCache = LRUCache<Data, Data>(
        maxSize: 16,
        shouldEvacuateInBackground: true,
    )
    private static let decryptedBlobCacheMaxItemSize: UInt = 4 * 1024

    private func decryptBlob(_ ciphertext: Data) throws -> Data {
        let cacheKey = (ciphertext + groupSecretParamsData)
        if let plaintext = Self.decryptedBlobCache.object(forKey: cacheKey) {
            return plaintext
        }

        let clientZkGroupCipher = ClientZkGroupCipher(groupSecretParams: groupSecretParams)
        let plaintext = try clientZkGroupCipher.decryptBlob(blobCiphertext: ciphertext)
        assert(ciphertext != plaintext)

        if plaintext.count <= Self.decryptedBlobCacheMaxItemSize {
            Self.decryptedBlobCache.setObject(plaintext, forKey: cacheKey)
        }
        return plaintext
    }

    func aci(for userId: Data) throws -> Aci {
        guard let aci = try serviceId(for: userId) as? Aci else {
            throw ServiceIdError.wrongServiceIdKind
        }
        return aci
    }

    func serviceId(for userId: Data) throws -> ServiceId {
        let uuidCiphertext = try UuidCiphertext(contents: userId)
        return try serviceId(for: uuidCiphertext)
    }

    func aci(for uuidCiphertext: UuidCiphertext) throws -> Aci {
        guard let aci = try serviceId(for: uuidCiphertext) as? Aci else {
            throw ServiceIdError.wrongServiceIdKind
        }
        return aci
    }

    private static let decryptedServiceIdCache = LRUCache<Data, ServiceId>(
        maxSize: Int(RemoteConfig.current.maxGroupSizeHardLimit),
        nseMaxSize: Int(RemoteConfig.current.maxGroupSizeHardLimit),
    )

    func serviceId(for uuidCiphertext: UuidCiphertext) throws -> ServiceId {
        let cacheKey = (uuidCiphertext.serialize() + groupSecretParamsData)
        if let plaintext = Self.decryptedServiceIdCache.object(forKey: cacheKey) {
            return plaintext
        }

        let clientZkGroupCipher = ClientZkGroupCipher(groupSecretParams: self.groupSecretParams)
        let serviceId = try clientZkGroupCipher.decrypt(uuidCiphertext)

        Self.decryptedServiceIdCache.setObject(serviceId, forKey: cacheKey)
        return serviceId
    }

    func userId(for serviceId: ServiceId) throws -> Data {
        let clientZkGroupCipher = ClientZkGroupCipher(groupSecretParams: self.groupSecretParams)
        let uuidCiphertext = try clientZkGroupCipher.encrypt(serviceId)
        let userId = uuidCiphertext.serialize()

        let cacheKey = (userId + groupSecretParamsData)
        Self.decryptedServiceIdCache.setObject(serviceId, forKey: cacheKey)

        return userId
    }

    private static let decryptedProfileKeyCache = LRUCache<Data, Data>(
        maxSize: Int(RemoteConfig.current.maxGroupSizeHardLimit),
        nseMaxSize: Int(RemoteConfig.current.maxGroupSizeHardLimit),
    )

    func profileKey(forProfileKeyCiphertext profileKeyCiphertext: ProfileKeyCiphertext, aci: Aci) throws -> Data {
        let cacheKey = (profileKeyCiphertext.serialize() + aci.serviceIdBinary + groupSecretParamsData)
        if let plaintext = Self.decryptedProfileKeyCache.object(forKey: cacheKey) {
            return plaintext
        }

        let clientZkGroupCipher = ClientZkGroupCipher(groupSecretParams: self.groupSecretParams)
        let profileKey = try clientZkGroupCipher.decryptProfileKey(profileKeyCiphertext: profileKeyCiphertext, userId: aci)
        let plaintext = profileKey.serialize()

        Self.decryptedProfileKeyCache.setObject(plaintext, forKey: cacheKey)
        return plaintext
    }
}

// MARK: -

public extension GroupV2Params {
    func decryptDisappearingMessagesTimer(_ ciphertext: Data?) -> DisappearingMessageToken {
        guard let ciphertext else {
            // Treat a missing value as disabled.
            return DisappearingMessageToken.disabledToken
        }
        do {
            let blobProtoData = try decryptBlob(ciphertext)
            let blobProto = try GroupsProtoGroupAttributeBlob(serializedData: blobProtoData)
            if let blobContent = blobProto.content {
                switch blobContent {
                case .disappearingMessagesDuration(let value):
                    return .token(forProtoExpireTimerSeconds: value)
                default:
                    owsFailDebug("Invalid disappearing messages value.")
                }
            }
        } catch {
            owsFailDebug("Could not decrypt and parse disappearing messages state: \(error).")
        }
        return DisappearingMessageToken.disabledToken
    }

    func encryptDisappearingMessagesTimer(_ token: DisappearingMessageToken) throws -> Data {
        do {
            let duration = (
                token.isEnabled
                    ? token.durationSeconds
                    : 0,
            )
            var blobBuilder = GroupsProtoGroupAttributeBlob.builder()
            blobBuilder.setContent(GroupsProtoGroupAttributeBlobOneOfContent.disappearingMessagesDuration(duration))
            let blobData = try blobBuilder.buildSerializedData()
            let encryptedTimerData = try encryptBlob(blobData)
            return encryptedTimerData
        } catch {
            owsFailDebug("Error: \(error)")
            throw error
        }
    }

    func decryptGroupName(_ ciphertext: Data?) -> String? {
        guard let ciphertext else {
            // Treat a missing value as no value.
            return nil
        }
        do {
            let blobProtoData = try decryptBlob(ciphertext)
            let blobProto = try GroupsProtoGroupAttributeBlob(serializedData: blobProtoData)
            if let blobContent = blobProto.content {
                switch blobContent {
                case .title(let value):
                    return value
                default:
                    owsFailDebug("Invalid group name value.")
                }
            }
        } catch {
            owsFailDebug("Could not decrypt group name: \(error).")
        }
        return nil
    }

    func encryptGroupName(_ value: String) throws -> Data {
        do {
            var blobBuilder = GroupsProtoGroupAttributeBlob.builder()
            blobBuilder.setContent(GroupsProtoGroupAttributeBlobOneOfContent.title(value))
            let blobData = try blobBuilder.buildSerializedData()
            let encryptedTimerData = try encryptBlob(blobData)
            return encryptedTimerData
        } catch {
            owsFailDebug("Error: \(error)")
            throw error
        }
    }

    func decryptGroupDescription(_ ciphertext: Data?) -> String? {
        guard let ciphertext else {
            // Treat a missing value as no value.
            return nil
        }
        do {
            let blobProtoData = try decryptBlob(ciphertext)
            let blobProto = try GroupsProtoGroupAttributeBlob(serializedData: blobProtoData)
            if let blobContent = blobProto.content {
                switch blobContent {
                case .descriptionText(let value):
                    return value
                default:
                    owsFailDebug("Invalid group description value.")
                }
            }
        } catch {
            owsFailDebug("Could not decrypt group name: \(error).")
        }
        return nil
    }

    func encryptGroupDescription(_ value: String) throws -> Data {
        do {
            var blobBuilder = GroupsProtoGroupAttributeBlob.builder()
            blobBuilder.setContent(GroupsProtoGroupAttributeBlobOneOfContent.descriptionText(value))
            let blobData = try blobBuilder.buildSerializedData()
            let ciphertext = try encryptBlob(blobData)
            return ciphertext
        } catch {
            owsFailDebug("Error: \(error)")
            throw error
        }
    }

    func decryptGroupAvatar(_ ciphertext: Data) throws -> Data? {
        do {
            let blobProtoData = try decryptBlob(ciphertext)
            let blobProto = try GroupsProtoGroupAttributeBlob(serializedData: blobProtoData)
            if let blobContent = blobProto.content {
                switch blobContent {
                case .avatar(let value):
                    return value
                default:
                    owsFailDebug("Invalid group avatar value.")
                }
            }
            return nil
        } catch {
            owsFailDebug("Error: \(error)")
            throw error
        }
    }

    func encryptGroupAvatar(_ value: Data) throws -> Data {
        do {
            var blobBuilder = GroupsProtoGroupAttributeBlob.builder()
            blobBuilder.setContent(GroupsProtoGroupAttributeBlobOneOfContent.avatar(value))
            let blobData = try blobBuilder.buildSerializedData()
            return try encryptBlob(blobData)
        } catch {
            owsFailDebug("Error: \(error)")
            throw error
        }
    }

    func decryptMemberLabel(_ ciphertext: Data) throws -> String? {
        do {
            let decryptedLabel = try decryptString(ciphertext)
            return decryptedLabel.filterStringForDisplay().trimToGlyphCount(24).trimToUtf8ByteCount(96)
        } catch {
            owsFailDebug("Error: \(error)")
            throw error
        }
    }

    func encryptMemberLabel(_ value: String) throws -> Data {
        do {
            return try encryptString(value)
        } catch {
            owsFailDebug("Error: \(error)")
            throw error
        }
    }

    func decryptMemberLabelEmoji(_ ciphertext: Data) throws -> String? {
        do {
            let decryptedEmoji = try decryptString(ciphertext)
            owsAssertDebug(decryptedEmoji.containsOnlyEmoji)
            guard decryptedEmoji.lengthOfBytes(using: .utf8) <= 48 else {
                throw OWSAssertionError("member label emoji is too long.")
            }
            return decryptedEmoji.filterStringForDisplay()
        } catch {
            owsFailDebug("Error: \(error)")
            throw error
        }
    }

    func encryptMemberLabelEmoji(_ value: String) throws -> Data {
        owsAssertDebug(value.containsOnlyEmoji)
        do {
            return try encryptString(value)
        } catch {
            owsFailDebug("Error: \(error)")
            throw error
        }
    }
}