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

import Foundation
import LibSignalClient

/// Represents an envelope that's already been decrypted.
///
/// This might be a message from one of our linked devices, or it might be a
/// message from another user.
///
/// Identified and unidentified envelopes take different paths to produce
/// this object, but they both produce this type so that most downstream
/// logic doesn't need to care about the source of a message.
///
/// For identified envelopes, `sourceAci` is taken directly from the
/// `ValidatedIncomingEnvelope`, and then we decrypt the envelope payload to
/// populate `plaintextData`.
///
/// For unidentified envelopes, we first decrypt the envelope payload which
/// gives us the `sourceAci` and a nested payload. We then decrypt that
/// nested payload to populate `plaintextData`.
class DecryptedIncomingEnvelope {
    let envelope: SSKProtoEnvelope
    let timestamp: UInt64
    let serverTimestamp: UInt64
    let sourceAci: Aci
    let sourceDeviceId: DeviceId
    let localIdentity: OWSIdentity
    let wasReceivedByUD: Bool
    let plaintextData: Data
    let content: SSKProtoContent?

    init(
        validatedEnvelope: ValidatedIncomingEnvelope,
        updatedEnvelope: SSKProtoEnvelope,
        sourceAci: Aci,
        sourceDeviceId: DeviceId,
        wasReceivedByUD: Bool,
        plaintextData: Data,
        isPlaintextCipher: Bool?,
    ) throws {
        self.envelope = updatedEnvelope
        self.timestamp = validatedEnvelope.timestamp
        self.serverTimestamp = validatedEnvelope.serverTimestamp
        self.sourceAci = sourceAci
        self.sourceDeviceId = sourceDeviceId
        self.localIdentity = validatedEnvelope.localIdentity
        self.wasReceivedByUD = wasReceivedByUD
        self.plaintextData = plaintextData
        self.content = {
            do {
                return try SSKProtoContent(serializedData: plaintextData)
            } catch {
                owsFailDebug("Failed to deserialize content proto: \(error)")
                return nil
            }
        }()

        let hasDecryptionError = (
            content?.decryptionErrorMessage != nil,
        )
        let hasAnythingElse = (
            content?.dataMessage != nil
                || content?.syncMessage != nil
                || content?.callMessage != nil
                || content?.nullMessage != nil
                || content?.receiptMessage != nil
                || content?.typingMessage != nil
                || content?.storyMessage != nil
                || content?.pniSignatureMessage != nil
                || content?.senderKeyDistributionMessage != nil
                || content?.unknownFields != nil,
        )
        if hasDecryptionError, hasAnythingElse {
            throw OWSGenericError("Message content must contain one type.")
        }
        if let isPlaintextCipher, isPlaintextCipher != hasDecryptionError {
            throw OWSGenericError("Plaintext ciphers must have decryption errors.")
        }

        // This prevents replay attacks by the service.
        if let dataMessage = content?.dataMessage, dataMessage.timestamp != timestamp {
            throw OWSGenericError("Data messages must have matching timestamps \(sourceAci)")
        }
        if let typingMessage = content?.typingMessage, typingMessage.timestamp != timestamp {
            throw OWSGenericError("Typing messages must have matching timestamps \(sourceAci)")
        }
    }
}