102 lines
4.2 KiB
Swift
102 lines
4.2 KiB
Swift
//
|
|
// 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))!
|
|
}
|
|
}
|