You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/SessionMessagingKit/Crypto/Crypto+Attachments.swift

257 lines
11 KiB
Swift

// 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
Merge remote-tracking branch 'origin/feature/swift-package-manager' into feature/groups-rebuild # Conflicts: # Podfile # Podfile.lock # Session.xcodeproj/project.pbxproj # Session/Calls/Call Management/SessionCall.swift # Session/Calls/Call Management/SessionCallManager.swift # Session/Calls/CallVC.swift # Session/Conversations/ConversationVC+Interaction.swift # Session/Conversations/ConversationVC.swift # Session/Conversations/ConversationViewModel.swift # Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift # Session/Conversations/Settings/ThreadSettingsViewModel.swift # Session/Emoji/Emoji+Available.swift # Session/Home/GlobalSearch/GlobalSearchViewController.swift # Session/Home/HomeVC.swift # Session/Home/HomeViewModel.swift # Session/Home/New Conversation/NewDMVC.swift # Session/Media Viewing & Editing/DocumentTitleViewController.swift # Session/Media Viewing & Editing/GIFs/GifPickerCell.swift # Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift # Session/Media Viewing & Editing/ImagePickerController.swift # Session/Media Viewing & Editing/MediaTileViewController.swift # Session/Media Viewing & Editing/PhotoCapture.swift # Session/Media Viewing & Editing/PhotoCaptureViewController.swift # Session/Media Viewing & Editing/PhotoLibrary.swift # Session/Media Viewing & Editing/SendMediaNavigationController.swift # Session/Meta/AppDelegate.swift # Session/Meta/AppEnvironment.swift # Session/Meta/MainAppContext.swift # Session/Meta/SessionApp.swift # Session/Notifications/NotificationPresenter.swift # Session/Notifications/PushRegistrationManager.swift # Session/Notifications/SyncPushTokensJob.swift # Session/Notifications/UserNotificationsAdaptee.swift # Session/Onboarding/LandingVC.swift # Session/Onboarding/LinkDeviceVC.swift # Session/Onboarding/Onboarding.swift # Session/Onboarding/RegisterVC.swift # Session/Onboarding/RestoreVC.swift # Session/Settings/HelpViewModel.swift # Session/Settings/NukeDataModal.swift # Session/Shared/FullConversationCell.swift # Session/Shared/OWSBezierPathView.m # Session/Utilities/BackgroundPoller.swift # Session/Utilities/MockDataGenerator.swift # SessionMessagingKit/Configuration.swift # SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift # SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift # SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift # SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift # SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift # SessionMessagingKit/Database/Models/Attachment.swift # SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift # SessionMessagingKit/Database/Models/Interaction.swift # SessionMessagingKit/Database/Models/Profile.swift # SessionMessagingKit/Database/Models/SessionThread.swift # SessionMessagingKit/File Server/FileServerAPI.swift # SessionMessagingKit/Jobs/AttachmentDownloadJob.swift # SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift # SessionMessagingKit/Jobs/DisappearingMessagesJob.swift # SessionMessagingKit/Jobs/FailedMessageSendsJob.swift # SessionMessagingKit/Jobs/MessageSendJob.swift # SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift # SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift # SessionMessagingKit/Messages/Message.swift # SessionMessagingKit/Open Groups/Crypto/Crypto+OpenGroupAPI.swift # SessionMessagingKit/Open Groups/Models/SOGSMessage.swift # SessionMessagingKit/Open Groups/OpenGroupAPI.swift # SessionMessagingKit/Open Groups/OpenGroupManager.swift # SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+LegacyClosedGroups.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver.swift # SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift # SessionMessagingKit/Sending & Receiving/MessageSender.swift # SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift # SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift # SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift # SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupAPI+Poller.swift # SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift # SessionMessagingKit/Utilities/ProfileManager.swift # SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift # SessionMessagingKitTests/LibSession/LibSessionSpec.swift # SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift # SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift # SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift # SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift # SessionMessagingKitTests/Utilities/CryptoSMKSpec.swift # SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift # SessionNotificationServiceExtension/NSENotificationPresenter.swift # SessionNotificationServiceExtension/NotificationServiceExtension.swift # SessionShareExtension/ShareAppExtensionContext.swift # SessionShareExtension/ShareNavController.swift # SessionShareExtension/ThreadPickerVC.swift # SessionSnodeKit/Crypto/Crypto+SessionSnodeKit.swift # SessionSnodeKit/Models/DeleteAllBeforeResponse.swift # SessionSnodeKit/Models/DeleteAllMessagesResponse.swift # SessionSnodeKit/Models/DeleteMessagesResponse.swift # SessionSnodeKit/Models/RevokeSubkeyRequest.swift # SessionSnodeKit/Models/RevokeSubkeyResponse.swift # SessionSnodeKit/Models/SendMessageResponse.swift # SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift # SessionSnodeKit/Models/UpdateExpiryAllResponse.swift # SessionSnodeKit/Models/UpdateExpiryResponse.swift # SessionSnodeKit/Networking/SnodeAPI.swift # SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift # SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift # SessionTests/Database/DatabaseSpec.swift # SessionTests/Settings/NotificationContentViewModelSpec.swift # SessionUIKit/Components/ToastController.swift # SessionUIKit/Style Guide/Values.swift # SessionUtilitiesKit/Crypto/Crypto+SessionUtilitiesKit.swift # SessionUtilitiesKit/Crypto/Crypto.swift # SessionUtilitiesKit/Database/Models/Identity.swift # SessionUtilitiesKit/Database/Models/Job.swift # SessionUtilitiesKit/Database/Storage.swift # SessionUtilitiesKit/Database/Types/Migration.swift # SessionUtilitiesKit/General/AppContext.swift # SessionUtilitiesKit/General/Data+Utilities.swift # SessionUtilitiesKit/General/Logging.swift # SessionUtilitiesKit/General/SNUserDefaults.swift # SessionUtilitiesKit/General/String+Trimming.swift # SessionUtilitiesKit/General/String+Utilities.swift # SessionUtilitiesKit/General/TimestampUtils.swift # SessionUtilitiesKit/General/UIEdgeInsets.swift # SessionUtilitiesKit/JobRunner/JobRunner.swift # SessionUtilitiesKit/LibSession/LibSessionError.swift # SessionUtilitiesKit/Media/DataSource.swift # SessionUtilitiesKit/Meta/SessionUtilitiesKit.h # SessionUtilitiesKit/Networking/NetworkType.swift # SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift # SessionUtilitiesKit/Utilities/BackgroundTaskManager.swift # SessionUtilitiesKit/Utilities/BencodeResponse.swift # SessionUtilitiesKit/Utilities/CExceptionHelper.mm # SessionUtilitiesKit/Utilities/FileManagerType.swift # SessionUtilitiesKit/Utilities/KeychainStorageType.swift # SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift # SessionUtilitiesKitTests/Database/Utilities/PersistableRecordUtilitiesSpec.swift # SessionUtilitiesKitTests/General/SessionIdSpec.swift # SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift # SignalUtilitiesKit/Configuration.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalInputAccessoryView.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift # SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCropViewController.swift # SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift # SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift # SignalUtilitiesKit/Meta/SignalUtilitiesKit.h # SignalUtilitiesKit/Shared View Controllers/OWSViewController.swift # SignalUtilitiesKit/Shared Views/CircleView.swift # SignalUtilitiesKit/Shared Views/TappableView.swift # SignalUtilitiesKit/Utilities/AppSetup.swift # SignalUtilitiesKit/Utilities/Bench.swift # SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift # _SharedTestUtilities/CommonMockedExtensions.swift # _SharedTestUtilities/MockCrypto.swift # _SharedTestUtilities/Mocked.swift # _SharedTestUtilities/SynchronousStorage.swift
10 months ago
var iv: [UInt8] = dependencies[singleton: .crypto].generate(.randomBytes(aesCBCIvLength)),
var encryptionKey: [UInt8] = dependencies[singleton: .crypto].generate(.randomBytes(aesKeySize)),
var hmacKey: [UInt8] = dependencies[singleton: .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..<numBytesEncrypted])
var encryptedPaddedData: [UInt8] = (iv + ciphertext)
// compute hmac of: iv || encrypted data
guard encryptedPaddedData.count < (UInt.max / 2) else {
Log.error("[Crypto] Attachment data too long to encrypt.")
throw CryptoError.encryptionFailed
}
guard hmacKey.count < (UInt.max / 2) else {
Log.error("[Crypto] Hmac key is too long.")
throw CryptoError.encryptionFailed
}
var hmacDataBuffer: [UInt8] = Array(Data(count: Int(CC_SHA256_DIGEST_LENGTH)))
CCHmac(
CCHmacAlgorithm(kCCHmacAlgSHA256),
&hmacKey,
hmacKey.count,
&encryptedPaddedData,
encryptedPaddedData.count,
&hmacDataBuffer
)
let hmac: [UInt8] = Array(hmacDataBuffer[0..<hmac256OutputLength])
encryptedPaddedData.append(contentsOf: hmac)
// compute digest of: iv || encrypted data || hmac
guard encryptedPaddedData.count < UInt32.max else {
Log.error("[Crypto] Attachment data too long to encrypt.")
throw CryptoError.encryptionFailed
}
var digest: [UInt8] = Array(Data(count: Int(CC_SHA256_DIGEST_LENGTH)))
CC_SHA256(&encryptedPaddedData, UInt32(encryptedPaddedData.count), &digest)
return (Data(encryptedPaddedData), outKey, Data(digest))
}
}
}
// MARK: - Decryption
public extension Crypto.Generator {
static func decryptAttachment(
ciphertext: Data,
key: Data,
digest: Data,
unpaddedSize: UInt
) -> Crypto.Generator<Data> {
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..<aesKeySize])
var hmacKey: [UInt8] = Array(key[aesKeySize...])
// ciphertext: IV || Ciphertext || truncated MAC(IV||Ciphertext)
var iv: [UInt8] = Array(ciphertext[0..<aesCBCIvLength])
var encryptedAttachment: [UInt8] = Array(ciphertext[aesCBCIvLength..<ciphertext.count - hmac256OutputLength])
let hmac: [UInt8] = Array(ciphertext[(ciphertext.count - hmac256OutputLength)...])
// Verify hmac of: iv || encrypted data
var dataToAuth: [UInt8] = (iv + encryptedAttachment)
var hmacDataBuffer: [UInt8] = Array(Data(count: Int(CC_SHA256_DIGEST_LENGTH)))
CCHmac(
CCHmacAlgorithm(kCCHmacAlgSHA256),
&hmacKey,
hmacKey.count,
&dataToAuth,
dataToAuth.count,
&hmacDataBuffer
)
let generatedHmac: [UInt8] = Array(hmacDataBuffer[0..<hmac256OutputLength])
let isHmacEqual: Bool = {
guard hmac.count == generatedHmac.count else { return false }
var isEqual: UInt8 = 0
(0..<hmac.count).forEach { index in
// Rather than returning as soon as we find a discrepency, we compare the rest of
// the byte stream to maintain a constant time comparison
isEqual |= hmac[index] ^ generatedHmac[index]
}
return (isEqual == 0)
}()
guard isHmacEqual else {
Log.error("[Crypto] Bad HMAC on decrypting payload.")
throw CryptoError.decryptionFailed
}
// Verify digest of: iv || encrypted data || hmac
dataToAuth += generatedHmac
var generatedDigest: [UInt8] = Array(Data(count: Int(CC_SHA256_DIGEST_LENGTH)))
CC_SHA256(&dataToAuth, UInt32(dataToAuth.count), &generatedDigest)
let isDigestEqual: Bool = {
guard digest.count == generatedDigest.count else { return false }
var isEqual: UInt8 = 0
(0..<digest.count).forEach { index in
// Rather than returning as soon as we find a discrepency, we compare the rest of
// the byte stream to maintain a constant time comparison
isEqual |= digest[index] ^ generatedDigest[index]
}
return (isEqual == 0)
}()
guard isDigestEqual else {
Log.error("[Crypto] Bad digest on decrypting payload.")
throw CryptoError.decryptionFailed
}
var numBytesDecrypted: size_t = 0
var bufferData: [UInt8] = Array(Data(count: ciphertext.count + kCCBlockSizeAES128))
let cryptStatus: CCCryptorStatus = CCCrypt(
CCOperation(kCCDecrypt),
CCAlgorithm(kCCAlgorithmAES128),
CCOptions(kCCOptionPKCS7Padding),
&encryptionKey, encryptionKey.count,
&iv,
&encryptedAttachment, encryptedAttachment.count,
&bufferData, bufferData.count,
&numBytesDecrypted
)
guard cryptStatus == kCCSuccess else {
Log.error("[Crypto] Failed to decrypt attachment with status: \(cryptStatus).")
throw CryptoError.decryptionFailed
}
guard bufferData.count >= numBytesDecrypted else {
Log.error("[Crypto] Attachment paddedPlaintext has unexpected length: \(bufferData.count) < \(numBytesDecrypted).")
throw CryptoError.decryptionFailed
}
let paddedPlaintext: [UInt8] = Array(bufferData[0..<numBytesDecrypted])
// Legacy iOS clients didn't set the unpaddedSize on attachments.
// So an unpaddedSize of 0 could mean one of two things:
// [case 1] receiving a legacy attachment from before padding was introduced
// [case 2] receiving a modern attachment of length 0 that just has some null padding (e.g. an empty group sync)
guard unpaddedSize > 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 <michael.code@endoftheworl.de>
// 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..<Int(unpaddedSize)])
}
}
}