mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			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.
		
		
		
		
		
			
		
			
				
	
	
		
			257 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			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
 | 
						|
                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..<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)])
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |