// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. // // stringlint:disable import Foundation import CommonCrypto import SessionUtilitiesKit // MARK: - Encryption public extension Crypto.Generator { private static var hmac256KeyLength: Int { 32 } private static var hmac256OutputLength: Int { 32 } private static var aesCBCIvLength: Int { 16 } private static var aesKeySize: Int { 32 } static func encryptAttachment( plaintext: Data, using dependencies: Dependencies ) -> Crypto.Generator<(ciphertext: Data, encryptionKey: Data, digest: Data)> { return Crypto.Generator( id: "encryptAttachment", args: [plaintext] ) { // Due to paddedSize, we need to divide by two. guard plaintext.count < (UInt.max / 2) else { Log.error("[Crypto] Attachment data too long to encrypt.") throw CryptoError.encryptionFailed } guard var iv: [UInt8] = dependencies.crypto.generate(.randomBytes(aesCBCIvLength)), var encryptionKey: [UInt8] = dependencies.crypto.generate(.randomBytes(aesKeySize)), var hmacKey: [UInt8] = dependencies.crypto.generate(.randomBytes(hmac256KeyLength)) else { Log.error("[Crypto] Failed to generate random data.") throw CryptoError.encryptionFailed } // The concatenated key for storage var outKey: Data = Data() outKey.append(Data(encryptionKey)) outKey.append(Data(hmacKey)) // Apply any padding let desiredSize: Int = max(541, Int(floor(pow(1.05, ceil(log(Double(plaintext.count)) / log(1.05)))))) var paddedAttachmentData: [UInt8] = Array(plaintext) if desiredSize > plaintext.count { paddedAttachmentData.append(contentsOf: [UInt8](repeating: 0, count: desiredSize - plaintext.count)) } var numBytesEncrypted: size_t = 0 var bufferData: [UInt8] = Array(Data(count: paddedAttachmentData.count + kCCBlockSizeAES128)) let cryptStatus: CCCryptorStatus = CCCrypt( CCOperation(kCCEncrypt), CCAlgorithm(kCCAlgorithmAES128), CCOptions(kCCOptionPKCS7Padding), &encryptionKey, encryptionKey.count, &iv, &paddedAttachmentData, paddedAttachmentData.count, &bufferData, bufferData.count, &numBytesEncrypted ) guard cryptStatus == kCCSuccess else { Log.error("[Crypto] Failed to encrypt attachment with status: \(cryptStatus).") throw CryptoError.encryptionFailed } guard cryptStatus == kCCSuccess else { Log.error("[Crypto] Failed to encrypt attachment with status: \(cryptStatus).") throw CryptoError.encryptionFailed } guard bufferData.count >= numBytesEncrypted else { Log.error("[Crypto] ciphertext has unexpected length: \(bufferData.count) < \(numBytesEncrypted).") throw CryptoError.encryptionFailed } let ciphertext: [UInt8] = Array(bufferData[0.. Crypto.Generator { return Crypto.Generator( id: "decryptAttachment", args: [ciphertext, key, digest, unpaddedSize] ) { guard ciphertext.count >= aesCBCIvLength + hmac256OutputLength else { Log.error("[Crypto] Attachment shorter than crypto overhead."); throw CryptoError.decryptionFailed } // key: 32 byte AES key || 32 byte Hmac-SHA256 key. var encryptionKey: [UInt8] = Array(key[0..= numBytesDecrypted else { Log.error("[Crypto] Attachment paddedPlaintext has unexpected length: \(bufferData.count) < \(numBytesDecrypted).") throw CryptoError.decryptionFailed } let paddedPlaintext: [UInt8] = Array(bufferData[0.. 0 else { guard paddedPlaintext.contains(where: { $0 != 0x00 }) else { // [case 2] The bytes were all 0's. We assume it was all padding and the actual // attachment data was indeed empty. The downside here would be if a legacy client // was intentionally sending an attachment consisting of just 0's. This seems unlikely, // and would only affect iOS clients from before commit: // // 6eeb78157a044e632adc3daf6254aceacd53e335 // Author: Michael Kirk // Date: Thu Oct 26 15:08:25 2017 -0700 // // Include size in attachment pointer return Data() } // [case 1] There was something besides 0 in our data, assume it wasn't padding. return Data(paddedPlaintext) } guard unpaddedSize <= paddedPlaintext.count else { Log.error("[Crypto] Decrypted attachment was smaller than the expected size (\(unpaddedSize) < \(paddedPlaintext.count)), decryption was invalid.") throw CryptoError.decryptionFailed } // If the `paddedPlaintext` is the same length as the `unpaddedSize` then just return it guard unpaddedSize != paddedPlaintext.count else { return Data(paddedPlaintext) } return Data(paddedPlaintext[0..