TM-SGNL-iOS/SignalServiceKit/Messages/Attachments/V2/ContentValidation/AttachmentContentValidator.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

198 lines
8.5 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public protocol PendingAttachment {
var blurHash: String? { get }
var sha256ContentHash: Data { get }
var encryptedByteCount: UInt32 { get }
var unencryptedByteCount: UInt32 { get }
var mimeType: String { get }
var encryptionKey: Data { get }
var digestSHA256Ciphertext: Data { get }
var localRelativeFilePath: String { get }
var renderingFlag: AttachmentReference.RenderingFlag { get }
var sourceFilename: String? { get }
var validatedContentType: Attachment.ContentType { get }
var orphanRecordId: OrphanedAttachmentRecord.IDType { get }
mutating func removeBorderlessRenderingFlagIfPresent()
}
public protocol RevalidatedAttachment {
var validatedContentType: Attachment.ContentType { get }
/// Revalidation might _change_ the mimeType we report.
var mimeType: String { get }
var blurHash: String? { get }
/// Orphan record for any created ancillary files, such as the audio waveform.
var orphanRecordId: OrphanedAttachmentRecord.IDType { get }
}
public enum ValidatedMessageBody {
/// The original body was small enough to send as-is.
case inline(MessageBody)
/// The original body was too large; we truncated and created an attachment with the untruncated text.
case oversize(truncated: MessageBody, fullsize: PendingAttachment)
}
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.
///
/// - Parameter shouldConsume: If true, the source file will be deleted and the DataSource
/// consumed after validation is complete; otherwise the source file will be left as-is.
func validateContents(
dataSource: DataSource,
shouldConsume: Bool,
mimeType: String,
renderingFlag: AttachmentReference.RenderingFlag,
sourceFilename: String?
) 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 validateContents(
data: Data,
mimeType: String,
renderingFlag: AttachmentReference.RenderingFlag,
sourceFilename: String?
) 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 validateContents(
ofEncryptedFileAt fileUrl: URL,
encryptionKey: Data,
plaintextLength: UInt32?,
digestSHA256Ciphertext: Data,
mimeType: String,
renderingFlag: AttachmentReference.RenderingFlag,
sourceFilename: String?
) throws -> PendingAttachment
/// Just validate an encrypted attachment file's contents, based on the provided mimetype.
/// Returns the validated content type; does no digest validation or primary file copy preparation.
/// Errors are thrown if data reading/parsing/decryption fails.
func reValidateContents(
ofEncryptedFileAt fileUrl: URL,
encryptionKey: Data,
plaintextLength: UInt32,
mimeType: String
) 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, digest 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 outerEncryptionData: The media tier encryption metadata use as the outer layer of encryption.
/// - Parameter innerEncryptionData: The transit tier encryption 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 validateContents(
ofBackupMediaFileAt fileUrl: URL,
outerEncryptionData: EncryptionMetadata,
innerEncryptionData: EncryptionMetadata,
finalEncryptionKey: Data,
mimeType: String,
renderingFlag: AttachmentReference.RenderingFlag,
sourceFilename: String?
) throws -> PendingAttachment
/// 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.
func prepareOversizeTextIfNeeded(
from messageBody: MessageBody
) throws -> 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
) 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
) throws -> PendingAttachment
}
extension AttachmentContentValidator {
public func validateContents(
dataSource: DataSource,
shouldConsume: Bool,
mimeType: String,
renderingFlag: AttachmentReference.RenderingFlag,
sourceFilename: String?
) throws -> AttachmentDataSource {
return .from(pendingAttachment: try self.validateContents(
dataSource: dataSource,
shouldConsume: shouldConsume,
mimeType: mimeType,
renderingFlag: renderingFlag,
sourceFilename: sourceFilename
))
}
public func validateContents(
data: Data,
mimeType: String,
renderingFlag: AttachmentReference.RenderingFlag,
sourceFilename: String?
) throws -> AttachmentDataSource {
return .from(pendingAttachment: try self.validateContents(
data: data,
mimeType: mimeType,
renderingFlag: renderingFlag,
sourceFilename: sourceFilename
))
}
public func validateContents(
ofEncryptedFileAt fileUrl: URL,
encryptionKey: Data,
plaintextLength: UInt32,
digestSHA256Ciphertext: Data,
mimeType: String,
renderingFlag: AttachmentReference.RenderingFlag,
sourceFilename: String?
) throws -> AttachmentDataSource {
return .from(pendingAttachment: try self.validateContents(
ofEncryptedFileAt: fileUrl,
encryptionKey: encryptionKey,
plaintextLength: plaintextLength,
digestSHA256Ciphertext: digestSHA256Ciphertext,
mimeType: mimeType,
renderingFlag: renderingFlag,
sourceFilename: sourceFilename
))
}
}