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

import Foundation

/// When validating a downloaded attachment, we might either have a plaintext
/// hash or encrypted blob hash (digest) to validate against, either of which ensure
/// the file we download is the same as the one the sender intended (or ourselves,
/// if we are the sender).
public enum AttachmentIntegrityCheck: Equatable {
    case digestSHA256Ciphertext(Data)
    case sha256ContentHash(Data)

    var isEmpty: Bool {
        switch self {
        case .digestSHA256Ciphertext(let data):
            return data.isEmpty
        case .sha256ContentHash(let data):
            return data.isEmpty
        }
    }
}

public struct PendingAttachment {
    let blurHash: String?
    let sha256ContentHash: Data
    let encryptedByteCount: UInt32
    let unencryptedByteCount: UInt32
    let mimeType: String
    let encryptionKey: Data
    let digestSHA256Ciphertext: Data
    let localRelativeFilePath: String
    private(set) var renderingFlag: AttachmentReference.RenderingFlag
    let sourceFilename: String?
    let validatedContentType: Attachment.ContentType
    let orphanRecordId: OrphanedAttachmentRecord.RowId

    mutating func removeBorderlessRenderingFlagIfPresent() {
        switch renderingFlag {
        case .borderless:
            renderingFlag = .default
        default:
            return
        }
    }
}

public struct RevalidatedAttachment {
    let validatedContentType: Attachment.ContentType
    /// Revalidation might _change_ the mimeType we report.
    let mimeType: String
    let blurHash: String?
    /// Orphan record for any created ancillary files, such as the audio waveform.
    let orphanRecordId: OrphanedAttachmentRecord.RowId
}

public protocol ValidatedInlineMessageBody {
    /// The (possibly truncated) body to inline in the message.
    var inlinedBody: MessageBody { get }
}

public protocol ValidatedMessageBody: ValidatedInlineMessageBody {
    /// If the original text didn't fit inline, the pending attachment that
    /// should be used to create the oversize text Attachment.
    var oversizeText: PendingAttachment? { get }
}

public protocol AttachmentContentValidator {

    /// Validate and prepare a DataSource's contents, based on the provided mimetype.
    /// Returns a PendingAttachment with validated contents, ready to be inserted.
    /// Note the content type may be `invalid`; we can still create an Attachment from these.
    /// Errors are thrown if data reading/parsing fails.
    func validateDataSourceContents(
        _ dataSource: DataSourcePath,
        mimeType: String,
        renderingFlag: AttachmentReference.RenderingFlag,
        sourceFilename: String?,
    ) async throws -> PendingAttachment

    /// Validate and prepare a Data's contents, based on the provided mimetype.
    /// Returns a PendingAttachment with validated contents, ready to be inserted.
    /// Note the content type may be `invalid`; we can still create an Attachment from these.
    /// Errors are thrown if data parsing fails.
    func validateDataContents(
        _ data: Data,
        mimeType: String,
        renderingFlag: AttachmentReference.RenderingFlag,
        sourceFilename: String?,
    ) async throws -> PendingAttachment

    /// Validate and prepare an encrypted attachment file's contents, based on the provided mimetype.
    /// Returns a PendingAttachment with validated contents, ready to be inserted.
    /// Note the content type may be `invalid`; we can still create an Attachment from these.
    /// Errors are thrown if data reading/parsing/decryption fails.
    ///
    /// - Parameter plaintextLength: If provided, the decrypted file will be truncated
    /// after this length. If nil, it is assumed the encrypted file has no custom padding (anything besides PKCS7)
    /// and will not be truncated after decrypting.
    func validateDownloadedContents(
        ofEncryptedFileAt fileUrl: URL,
        attachmentKey: AttachmentKey,
        plaintextLength: UInt32?,
        integrityCheck: AttachmentIntegrityCheck,
        mimeType: String,
        renderingFlag: AttachmentReference.RenderingFlag,
        sourceFilename: String?,
    ) async throws -> PendingAttachment

    /// Just validate an encrypted attachment file's contents, based on the provided mimetype.
    /// Returns the validated content type;  does no integrityCheck validation or primary file copy preparation.
    /// Errors are thrown if data reading/parsing/decryption fails.
    func reValidateContents(
        ofEncryptedFileAt fileUrl: URL,
        attachmentKey: AttachmentKey,
        plaintextLength: UInt32,
        mimeType: String,
    ) async throws -> RevalidatedAttachment

    /// Validate and prepare a backup media file's contents, based on the provided mimetype.
    /// Returns a PendingAttachment with validated contents, ready to be inserted.
    /// Note the content type may be `invalid`; we can still create an Attachment from these.
    /// Errors are thrown if data reading/parsing/decryption fails.
    ///
    /// Unlike attachments from the live service, integrityCheck is not required; we can guarantee
    /// correctness for backup media files since they come from the local user.
    ///
    /// Unlike transit tier attachments, backup attachments are encrypted twice: once when uploaded
    /// to the transit tier, and again when copied to the media tier.  This means validating media tier
    /// attachments required decrypting the file twice to allow validating the actual contents of the attachment.
    ///
    /// Strictly speaking we don't usually need content type validation either, but the set of valid
    /// contents can change over time so it is best to re-validate.
    ///
    /// - Parameter outerDecryptionData: The media tier decryption metadata use as the outer layer of encryption.
    /// - Parameter innerDecryptionData: The transit tier decryption metadata.
    /// - Parameter finalEncryptionKey: The encryption key used to encrypt the file in it's final destination.  If the finalEncryptionKey
    /// matches the encryption key in `innerEncryptionData`, this re-encryption will be skipped.
    func validateBackupMediaFileContents(
        fileUrl: URL,
        outerDecryptionData: DecryptionMetadata,
        innerDecryptionData: DecryptionMetadata,
        finalAttachmentKey: AttachmentKey,
        mimeType: String,
        renderingFlag: AttachmentReference.RenderingFlag,
        sourceFilename: String?,
    ) async throws -> PendingAttachment

    /// Truncates the provided message body if necessary for inlining in a message,
    /// dropping any remaining text even if it might otherwise fit in an oversized text
    /// attachment.
    /// It is much preferred to use ``prepareOversizeTextsIfNeeded(from:)``;
    /// this method only exists as a temporary solution for callsites that process bodies
    /// from within inescapable write transactions and therefore cannot do the
    /// double-commit necessary to prepare an oversize text Attachment.
    /// (This method takes a transaction, despite not using it, to nudge callers.
    /// If you're not already in a write tx, you should use prepareOversizeTextsIfNeeded).
    func truncatedMessageBodyForInlining(
        _ body: MessageBody,
        tx: DBWriteTransaction,
    ) -> ValidatedInlineMessageBody

    /// If the provided message body is large enough to require an oversize text
    /// attachment, creates a pending one, alongside the truncated message body.
    /// If not, just returns the message body as is.
    ///
    /// - parameter encryptionKeys: The encryption key to use for the pending attachment
    /// file to create for oversize text, if any. If there is no provided encryption key for a given MessageBody
    /// input, a random key will be used.
    func prepareOversizeTextsIfNeeded<Key: Hashable>(
        from texts: [Key: MessageBody],
        attachmentKeys: [Key: AttachmentKey],
    ) async throws -> [Key: ValidatedMessageBody]

    /// Build a `QuotedReplyAttachmentDataSource` for a reply to a message with the provided attachment.
    /// Throws an error if the provided attachment is non-visual, or if data reading/writing fails.
    func prepareQuotedReplyThumbnail(
        fromOriginalAttachment: AttachmentStream,
        originalReference: AttachmentReference,
    ) async throws -> QuotedReplyAttachmentDataSource

    /// Build a `PendingAttachment` for a reply to a message with the provided attachment stream.
    /// Throws an error if the provided attachment is non-visual, or if data reading/writing fails.
    func prepareQuotedReplyThumbnail(
        fromOriginalAttachmentStream: AttachmentStream,
    ) async throws -> PendingAttachment
}

extension AttachmentContentValidator {

    public func prepareOversizeTextIfNeeded(
        _ body: MessageBody,
    ) async throws -> ValidatedMessageBody {
        return try await prepareOversizeTextsIfNeeded(
            from: ["": body],
            attachmentKeys: [:],
        ).values.first!
    }
}