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

import CommonCrypto
import Foundation

/// Computes (and un-computes) attachment padding.
///
/// In order to obsfucate attachment size on the wire, we round up
/// attachment plaintext bytes to the nearest power of 1.05. This number was
/// selected as it provides a good balance between number of buckets and
/// wasted bytes on the wire.
///
/// This type can compute that padding, and it can also "reverse" the
/// process and determine the maximum plaintext size that will fit within a
/// particular encrypted size limit.
struct PaddingBucket {
    private enum Constants {
        static let paddingMultiplier = 1.05
        static let smallestBucketNumber: Int = 129 // => 541 bytes

        static let ivLength = UInt64(Cryptography.Constants.aescbcIVLength)
        static let hmacLength = UInt64(Cryptography.Constants.hmac256OutputLength)
        static let blockLength = UInt64(kCCBlockSizeAES128)
    }

    let bucketNumber: Int

    /// The plaintext size with padding.
    let plaintextSize: UInt64

    /// The encrypted size with padding & encryption overhead.
    let encryptedSize: UInt64

    init?(bucketNumber: Int) {
        self.bucketNumber = max(bucketNumber, Constants.smallestBucketNumber)
        let plaintextSize = UInt64(exactly: floor(pow(Constants.paddingMultiplier, Double(self.bucketNumber))))
        guard let plaintextSize else {
            return nil
        }
        self.plaintextSize = plaintextSize
        let encryptedSize = Self.addingEncryptionOverhead(to: plaintextSize)
        guard let encryptedSize else {
            return nil
        }
        self.encryptedSize = encryptedSize
    }

    static func addingEncryptionOverhead(to paddedValue: UInt64) -> UInt64? {
        let result = paddedValue.addingReportingOverflow(
            Constants.ivLength
                + Constants.blockLength
                - paddedValue % Constants.blockLength
                + Constants.hmacLength,
        )
        if result.overflow {
            return nil
        }
        return result.partialValue
    }

    static func forUnpaddedPlaintextSize(_ unpaddedPlaintextSize: UInt64) -> PaddingBucket? {
        let bucketNumber: Int
        if unpaddedPlaintextSize == 0 {
            bucketNumber = 0
        } else {
            bucketNumber = Int(ceil(log(Double(unpaddedPlaintextSize)) / log(Constants.paddingMultiplier)))
        }
        return PaddingBucket(bucketNumber: bucketNumber)
    }

    static func forEncryptedSizeLimit(_ encryptedSize: UInt64) -> PaddingBucket {
        let worstCasePlaintextLimit = encryptedSize.subtractingReportingOverflow(
            Constants.ivLength
                // When computing the `encryptedSize`, we add 1 to 16 bytes of
                // `blockLength` padding. We always subtract 16 here (as a worst case) and
                // then check the next bucket to handle values near the boundary.
                + Constants.blockLength
                + Constants.hmacLength,
        )
        if worstCasePlaintextLimit.overflow || worstCasePlaintextLimit.partialValue == 0 {
            return PaddingBucket(bucketNumber: 0)!
        }
        // Taking the `floor(...)` here may cause us to pick a bucket one smaller
        // than we should when `encryptedSize` is exactly the size of a bucket.
        // (This happens when `plaintextSize` has a fractional component that gets
        // floored.) We already need to check the next bucket to handle the PKCS7
        // padding, so we rely on that check to handle this off-by-one as well.
        let worstCaseBucketNumber = floor(log(Double(worstCasePlaintextLimit.partialValue)) / log(Constants.paddingMultiplier))
        // We check one optimistic bucket because the minimum spacing is 27 bytes
        // (which is larger than the 15 + 1 worst-case bytes mentioned above).
        let optimisticPaddingBucket = PaddingBucket(bucketNumber: Int(worstCaseBucketNumber) + 1)
        if let optimisticPaddingBucket, optimisticPaddingBucket.encryptedSize <= encryptedSize {
            return optimisticPaddingBucket
        }
        // By definition, this bucket can't overflow the encrypted size limit.
        return PaddingBucket(bucketNumber: Int(worstCaseBucketNumber))!
    }
}