// // StreamCryptor.swift // Cryptor // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) import CommonCrypto #elseif os(Linux) import OpenSSL #endif /// /// Encrypts or decrypts return results as they become available. /// /// - Note: The underlying cipher may be a block or a stream cipher. /// /// Use for large files or network streams. /// /// For small, in-memory buffers Cryptor may be easier to use. /// public class StreamCryptor { #if os(Linux) // // Key sizes // static let kCCKeySizeAES128 = 16 static let kCCKeySizeAES192 = 24 static let kCCKeySizeAES256 = 32 static let kCCKeySizeDES = 8 static let kCCKeySize3DES = 24 static let kCCKeySizeMinCAST = 5 static let kCCKeySizeMaxCAST = 16 static let kCCKeySizeMinRC2 = 1 static let kCCKeySizeMaxRC2 = 128 static let kCCKeySizeMinBlowfish = 8 static let kCCKeySizeMaxBlowfish = 56 // // Block sizes // static let kCCBlockSizeAES128 = 16 static let kCCBlockSizeDES = 8 static let kCCBlockSize3DES = 8 static let kCCBlockSizeCAST = 8 static let kCCBlockSizeRC2 = 8 static let kCCBlockSizeBlowfish = 8 #endif /// /// Enumerates Cryptor operations /// public enum Operation { /// Encrypting case encrypt /// Decrypting case decrypt #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) /// Convert to native `CCOperation` func nativeValue() -> CCOperation { switch self { case .encrypt: return CCOperation(kCCEncrypt) case .decrypt: return CCOperation(kCCDecrypt) } } #elseif os(Linux) /// Convert to native value func nativeValue() -> UInt32 { switch self { case .encrypt: return 0 case .decrypt: return 1 } } #endif } /// /// Enumerates valid key sizes. /// public enum ValidKeySize { case fixed(Int) case discrete([Int]) case range(Int, Int) /// /// Determines if a given `keySize` is valid for this algorithm. /// /// - Parameter keySize: The size to test for validity. /// /// - Returns: True if valid, false otherwise. /// func isValidKeySize(keySize: Int) -> Bool { switch self { case .fixed(let fixed): return (fixed == keySize) case .range(let min, let max): return ((keySize >= min) && (keySize <= max)) case .discrete(let values): return values.contains(keySize) } } /// /// Determines the next valid key size; that is, the first valid key size larger /// than the given value. /// /// - Parameter keySize: The size for which the `next` size is desired. /// /// - Returns: Will return `nil` if the passed in `keySize` is greater than the max. /// func paddedKeySize(keySize: Int) -> Int? { switch self { case .fixed(let fixed): return (keySize <= fixed) ? fixed : nil case .range(let min, let max): return (keySize > max) ? nil : ((keySize < min) ? min : keySize) case .discrete(let values): return values.sorted().reduce(nil) { answer, current in return answer ?? ((current >= keySize) ? current : nil) } } } } /// /// Maps CommonCryptoOptions onto a Swift struct. /// public struct Options: OptionSet { public typealias RawValue = Int public let rawValue: RawValue /// Convert from a native value (i.e. `0`, `kCCOptionpkcs7Padding`, `kCCOptionECBMode`) public init(rawValue: RawValue) { self.rawValue = rawValue } /// Convert from a native value (i.e. `0`, `kCCOptionpkcs7Padding`, `kCCOptionECBMode`) public init(_ rawValue: RawValue) { self.init(rawValue: rawValue) } /// No options public static let none = Options(rawValue: 0) #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) /// Use padding. Needed unless the input is a integral number of blocks long. public static var pkcs7Padding = Options(rawValue:kCCOptionPKCS7Padding) /// Electronic Code Book Mode. Don't use this. public static var ecbMode = Options(rawValue:kCCOptionECBMode) #elseif os(Linux) /// Use padding. Needed unless the input is a integral number of blocks long. public static var pkcs7Padding = Options(rawValue:0x0001) /// Electronic Code Book Mode. Don't use this. public static var ecbMode = Options(rawValue:0x0002) #endif } /// /// Enumerates available algorithms /// public enum Algorithm { /// Advanced Encryption Standard /// - Note: aes and aes128 are equivalent. case aes, aes128, aes192, aes256 /// Data Encryption Standard case des /// Triple des case tripleDes /// cast case cast /// rc2 case rc2 /// blowfish case blowfish /// Blocksize, in bytes, of algorithm. public var blockSize: Int { switch self { case .aes, .aes128, .aes192, .aes256: return kCCBlockSizeAES128 case .des: return kCCBlockSizeDES case .tripleDes: return kCCBlockSize3DES case .cast: return kCCBlockSizeCAST case .rc2: return kCCBlockSizeRC2 case .blowfish: return kCCBlockSizeBlowfish } } public var defaultKeySize: Int { switch self { case .aes, .aes128: return kCCKeySizeAES128 case .aes192: return kCCKeySizeAES192 case .aes256: return kCCKeySizeAES256 case .des: return kCCKeySizeDES case .tripleDes: return kCCKeySize3DES case .cast: return kCCKeySizeMinCAST case .rc2: return kCCKeySizeMinRC2 case .blowfish: return kCCKeySizeMinBlowfish } } #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) /// Native, CommonCrypto constant for algorithm. func nativeValue() -> CCAlgorithm { switch self { case .aes, .aes128, .aes192, .aes256: return CCAlgorithm(kCCAlgorithmAES) case .des: return CCAlgorithm(kCCAlgorithmDES) case .tripleDes: return CCAlgorithm(kCCAlgorithm3DES) case .cast: return CCAlgorithm(kCCAlgorithmCAST) case .rc2: return CCAlgorithm(kCCAlgorithmRC2) case .blowfish: return CCAlgorithm(kCCAlgorithmBlowfish) } } #elseif os(Linux) /// Native, OpenSSL function for algorithm. func nativeValue(options: Options) -> OpaquePointer? { if options == .pkcs7Padding || options == .none { switch self { case .aes, .aes128: return .init(EVP_aes_128_cbc()) case .aes192: return .init(EVP_aes_192_cbc()) case .aes256: return .init(EVP_aes_256_cbc()) case .des: return .init(EVP_des_cbc()) case .tripleDes: return .init(EVP_des_ede3_cbc()) case .cast: return .init(EVP_cast5_cbc()) case .rc2: return .init(EVP_rc2_cbc()) case .blowfish: return .init(EVP_bf_cbc()) } } if options == .ecbMode { switch self { case .aes, .aes128: return .init(EVP_aes_128_ecb()) case .aes192: return .init(EVP_aes_192_ecb()) case .aes256: return .init(EVP_aes_256_ecb()) case .des: return .init(EVP_des_ecb()) case .tripleDes: return .init(EVP_des_ede3_ecb()) case .cast: return .init(EVP_cast5_ecb()) case .rc2: return .init(EVP_rc2_ecb()) case .blowfish: return .init(EVP_bf_ecb()) } } fatalError("Unsupported options and/or algorithm.") } #endif /// /// Determines the valid key size for this algorithm /// /// - Returns: Valid key size for this algorithm. /// func validKeySize() -> ValidKeySize { #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) switch self { case .aes, .aes128, .aes192, .aes256: return .discrete([kCCKeySizeAES128, kCCKeySizeAES192, kCCKeySizeAES256]) case .des: return .fixed(kCCKeySizeDES) case .tripleDes: return .fixed(kCCKeySize3DES) case .cast: return .range(kCCKeySizeMinCAST, kCCKeySizeMaxCAST) case .rc2: return .range(kCCKeySizeMinRC2, kCCKeySizeMaxRC2) case .blowfish: return .range(kCCKeySizeMinBlowfish, kCCKeySizeMaxBlowfish) } #elseif os(Linux) switch self { case .aes, .aes128: return .fixed(kCCKeySizeAES128) case .aes192: return .fixed(kCCKeySizeAES192) case .aes256: return .fixed(kCCKeySizeAES256) case .des: return .fixed(kCCKeySizeDES) case .tripleDes: return .fixed(kCCKeySize3DES) case .cast: return .range(kCCKeySizeMinCAST, kCCKeySizeMaxCAST) case .rc2: return .range(kCCKeySizeMinRC2, kCCKeySizeMaxRC2) case .blowfish: return .range(kCCKeySizeMinBlowfish, kCCKeySizeMaxBlowfish) } #endif } /// /// Tests if a given keySize is valid for this algorithm /// /// - Parameter keySize: The key size to be validated. /// /// - Returns: True if valid, false otherwise. /// func isValidKeySize(keySize: Int) -> Bool { return self.validKeySize().isValidKeySize(keySize: keySize) } /// /// Calculates the next, if any, valid keySize greater or equal to a given `keySize` for this algorithm /// /// - Parameter keySize: Key size for which the next size is requested. /// /// - Returns: Next key size or nil /// func paddedKeySize(keySize: Int) -> Int? { return self.validKeySize().paddedKeySize(keySize: keySize) } } /// /// The status code resulting from the last method call to this Cryptor. /// Used to get additional information when optional chaining collapes. /// public internal(set) var status: Status = .success /// /// Context obtained. True if we have it, false otherwise. /// private var haveContext: Bool = false #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) /// CommonCrypto Context private var context = UnsafeMutablePointer.allocate(capacity: 1) #elseif os(Linux) /// OpenSSL Cipher Context private let context: OpaquePointer? = .init(EVP_CIPHER_CTX_new()) /// Operation private var operation: Operation = .encrypt /// The algorithm private var algorithm: Algorithm #endif // MARK: Lifecycle Methods /// /// Default Initializer /// /// - Parameters: /// - operation: The operation to perform see Operation (Encrypt, Decrypt) /// - algorithm: The algorithm to use see Algorithm (AES, des, tripleDes, cast, rc2, blowfish) /// - keyBuffer: Pointer to key buffer /// - keyByteCount: Number of bytes in the key /// - ivBuffer: Initialization vector buffer /// - ivLength: Length of the ivBuffer /// /// - Returns: New StreamCryptor instance. /// public init(operation: Operation, algorithm: Algorithm, options: Options, keyBuffer: [UInt8], keyByteCount: Int, ivBuffer: UnsafePointer, ivLength: Int = 0) throws { guard algorithm.isValidKeySize(keySize: keyByteCount) else { throw CryptorError.invalidKeySize } guard options.contains(.ecbMode) || ivLength == algorithm.blockSize else { throw CryptorError.invalidIVSizeOrLength } #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) let rawStatus = CCCryptorCreate(operation.nativeValue(), algorithm.nativeValue(), CCOptions(options.rawValue), keyBuffer, keyByteCount, ivBuffer, self.context) if let status = Status.fromRaw(status: rawStatus) { self.status = status } else { throw CryptorError.fail(rawStatus, "Cryptor init returned unexpected status.") } self.haveContext = true #elseif os(Linux) self.algorithm = algorithm self.operation = operation var rawStatus: Int32 switch self.operation { case .encrypt: rawStatus = EVP_EncryptInit_ex(.make(optional: self.context), .make(optional: algorithm.nativeValue(options: options)), nil, keyBuffer, ivBuffer) case .decrypt: rawStatus = EVP_DecryptInit_ex(.make(optional: self.context), .make(optional: algorithm.nativeValue(options: options)), nil, keyBuffer, ivBuffer) } if rawStatus == 0 { let errorCode = ERR_get_error() if let status = Status.fromRaw(status: errorCode) { self.status = status } else { throw CryptorError.fail(Int32(errorCode), "Cryptor init returned unexpected status.") } } self.haveContext = true // Default to no padding... var needPadding: Int32 = 0 if options == .pkcs7Padding { needPadding = 1 } // Note: This call must be AFTER the init call above... EVP_CIPHER_CTX_set_padding(.make(optional: self.context), needPadding) self.status = Status.success #endif } /// /// Creates a new StreamCryptor /// /// - Parameters: /// - operation: The operation to perform see Operation (Encrypt, Decrypt) /// - algorithm: The algorithm to use see Algorithm (AES, des, tripleDes, cast, rc2, blowfish) /// - key: A byte array containing key data /// - iv: A byte array containing initialization vector /// /// - Returns: New StreamCryptor instance. /// public convenience init(operation: Operation, algorithm: Algorithm, options: Options, key: [UInt8], iv: [UInt8]) throws { guard let paddedKeySize = algorithm.paddedKeySize(keySize: key.count) else { throw CryptorError.invalidKeySize } try self.init(operation:operation, algorithm:algorithm, options:options, keyBuffer:CryptoUtils.zeroPad(byteArray:key, blockSize: paddedKeySize), keyByteCount:paddedKeySize, ivBuffer:iv, ivLength:iv.count) } /// /// Creates a new StreamCryptor /// /// - Parameters: /// - operation: The operation to perform see Operation (Encrypt, Decrypt) /// - algorithm: The algorithm to use see Algorithm (AES, des, tripleDes, cast, rc2, blowfish) /// - key: A string containing key data (will be interpreted as UTF8) /// - iv: A string containing initialization vector data (will be interpreted as UTF8) /// /// - Returns: New StreamCryptor instance. /// public convenience init(operation: Operation, algorithm: Algorithm, options: Options, key: String, iv: String) throws { let keySize = key.utf8.count guard let paddedKeySize = algorithm.paddedKeySize(keySize: keySize) else { throw CryptorError.invalidKeySize } try self.init(operation:operation, algorithm:algorithm, options:options, keyBuffer:CryptoUtils.zeroPad(string: key, blockSize: paddedKeySize), keyByteCount:paddedKeySize, ivBuffer:iv, ivLength:iv.utf8.count) } /// /// Cleanup /// deinit { // Ensure we've got a context before attempting to get rid of it... if self.haveContext == false { return } #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) // Ensure we've got a context before attempting to get rid of it... if self.context.pointee == nil { return } let rawStatus = CCCryptorRelease(self.context.pointee) if let status = Status.fromRaw(status: rawStatus) { if status != .success { NSLog("WARNING: CCCryptoRelease failed with status \(rawStatus).") } } else { fatalError("CCCryptorUpdate returned unexpected status.") } #if swift(>=4.1) context.deallocate() #else context.deallocate(capacity: 1) #endif self.haveContext = false #elseif os(Linux) EVP_CIPHER_CTX_free(.make(optional: self.context)) self.haveContext = false #endif } // MARK: Public Methods /// /// Add the contents of an Data buffer to the current encryption/decryption operation. /// /// - Parameters: /// - dataIn: The input data /// - byteArrayOut: Output data /// /// - Returns: A tuple containing the number of output bytes produced and the status (see Status) /// public func update(dataIn: Data, byteArrayOut: inout [UInt8]) -> (Int, Status) { let dataOutAvailable = byteArrayOut.count var dataOutMoved = 0 #if swift(>=5.0) dataIn.withUnsafeBytes() { _ = update(bufferIn: $0.baseAddress!, byteCountIn: dataIn.count, bufferOut: &byteArrayOut, byteCapacityOut: dataOutAvailable, byteCountOut: &dataOutMoved) } #else dataIn.withUnsafeBytes() { (buffer: UnsafePointer) in _ = update(bufferIn: buffer, byteCountIn: dataIn.count, bufferOut: &byteArrayOut, byteCapacityOut: dataOutAvailable, byteCountOut: &dataOutMoved) } #endif return (dataOutMoved, self.status) } /// /// Add the contents of an NSData buffer to the current encryption/decryption operation. /// /// - Parameters: /// - dataIn: The input data /// - byteArrayOut: Output data /// /// - Returns: A tuple containing the number of output bytes produced and the status (see Status) /// public func update(dataIn: NSData, byteArrayOut: inout [UInt8]) -> (Int, Status) { let dataOutAvailable = byteArrayOut.count var dataOutMoved = 0 var ptr = dataIn.bytes.assumingMemoryBound(to: UInt8.self).pointee _ = update(bufferIn: &ptr, byteCountIn: dataIn.length, bufferOut: &byteArrayOut, byteCapacityOut: dataOutAvailable, byteCountOut: &dataOutMoved) return (dataOutMoved, self.status) } /// /// Add the contents of a byte array to the current encryption/decryption operation. /// /// - Parameters: /// - byteArrayIn: The input data /// - byteArrayOut: Output data /// /// - Returns: A tuple containing the number of output bytes produced and the status (see Status) /// public func update(byteArrayIn: [UInt8], byteArrayOut: inout [UInt8]) -> (Int, Status) { let dataOutAvailable = byteArrayOut.count var dataOutMoved = 0 _ = update(bufferIn: byteArrayIn, byteCountIn: byteArrayIn.count, bufferOut: &byteArrayOut, byteCapacityOut: dataOutAvailable, byteCountOut: &dataOutMoved) return (dataOutMoved, self.status) } /// /// Add the contents of a string (interpreted as UTF8) to the current encryption/decryption operation. /// /// - Parameters: /// - byteArrayIn: The input data /// - byteArrayOut: Output data /// /// - Returns: A tuple containing the number of output bytes produced and the status (see Status) /// public func update(stringIn: String, byteArrayOut: inout [UInt8]) -> (Int, Status) { let dataOutAvailable = byteArrayOut.count var dataOutMoved = 0 _ = update(bufferIn: stringIn, byteCountIn: stringIn.utf8.count, bufferOut: &byteArrayOut, byteCapacityOut: dataOutAvailable, byteCountOut: &dataOutMoved) return (dataOutMoved, self.status) } /// /// Retrieves all remaining encrypted or decrypted data from this cryptor. /// /// - Note: If the underlying algorithm is an block cipher and the padding option has /// not been specified and the cumulative input to the cryptor has not been an integral /// multiple of the block length this will fail with an alignment error. /// /// - Note: This method updates the status property /// /// - Parameter byteArrayOut: The output bffer /// /// - Returns: a tuple containing the number of output bytes produced and the status (see Status) /// public func final(byteArrayOut: inout [UInt8]) -> (Int, Status) { let dataOutAvailable = byteArrayOut.count var dataOutMoved = 0 _ = final(bufferOut: &byteArrayOut, byteCapacityOut: dataOutAvailable, byteCountOut: &dataOutMoved) return (dataOutMoved, self.status) } // MARK: - Low-level interface /// /// Update the buffer /// /// - Parameters: /// - bufferIn: Pointer to input buffer /// - inByteCount: Number of bytes contained in input buffer /// - bufferOut: Pointer to output buffer /// - outByteCapacity: Capacity of the output buffer in bytes /// - outByteCount: On successful completion, the number of bytes written to the output buffer /// /// - Returns: Status of the update /// public func update(bufferIn: UnsafeRawPointer, byteCountIn: Int, bufferOut: UnsafeMutablePointer, byteCapacityOut: Int, byteCountOut: inout Int) -> Status { if self.status == .success { #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) let rawStatus = CCCryptorUpdate(self.context.pointee, bufferIn, byteCountIn, bufferOut, byteCapacityOut, &byteCountOut) if let status = Status.fromRaw(status: rawStatus) { self.status = status } else { fatalError("CCCryptorUpdate returned unexpected status.") } #elseif os(Linux) var rawStatus: Int32 var outLength: Int32 = 0 switch self.operation { case .encrypt: rawStatus = EVP_EncryptUpdate(.make(optional: self.context), bufferOut, &outLength, bufferIn.assumingMemoryBound(to: UInt8.self), Int32(byteCountIn)) case .decrypt: rawStatus = EVP_DecryptUpdate(.make(optional: self.context), bufferOut, &outLength, bufferIn.assumingMemoryBound(to: UInt8.self), Int32(byteCountIn)) } byteCountOut = Int(outLength) if rawStatus == 0 { let errorCode = ERR_get_error() if let status = Status.fromRaw(status: errorCode) { self.status = status } else { fatalError("Cryptor update returned unexpected status.") } } else { self.status = Status.success } #endif } return self.status } /// /// Retrieves all remaining encrypted or decrypted data from this cryptor. /// /// - Note: If the underlying algorithm is an block cipher and the padding option has /// not been specified and the cumulative input to the cryptor has not been an integral /// multiple of the block length this will fail with an alignment error. /// /// - Note: This method updates the status property /// /// - Parameters: /// - bufferOut: Pointer to output buffer /// - outByteCapacity: Capacity of the output buffer in bytes /// - outByteCount: On successful completion, the number of bytes written to the output buffer /// /// - Returns: Status of the update /// public func final(bufferOut: UnsafeMutablePointer, byteCapacityOut: Int, byteCountOut: inout Int) -> Status { if self.status == Status.success { #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) let rawStatus = CCCryptorFinal(self.context.pointee, bufferOut, byteCapacityOut, &byteCountOut) if let status = Status.fromRaw(status: rawStatus) { self.status = status } else { fatalError("CCCryptorUpdate returned unexpected status.") } #elseif os(Linux) var rawStatus: Int32 var outLength: Int32 = Int32(byteCapacityOut) switch self.operation { case .encrypt: rawStatus = EVP_EncryptFinal_ex(.make(optional: self.context), bufferOut, &outLength) case .decrypt: rawStatus = EVP_DecryptFinal_ex(.make(optional: self.context), bufferOut, &outLength) } byteCountOut = Int(outLength) if rawStatus == 0 { let errorCode = ERR_get_error() if let status = Status.fromRaw(status: errorCode) { self.status = status } else { fatalError("Cryptor final returned unexpected status.") } } else { self.status = Status.success } #endif } return self.status } /// /// Determines the number of bytes that will be output by this Cryptor if inputBytes of additional /// data is input. /// /// - Parameters: /// - inputByteCount: Number of bytes that will be input. /// - isFinal: True if buffer to be input will be the last input buffer, false otherwise. /// /// - Returns: The final output length /// public func getOutputLength(inputByteCount: Int, isFinal: Bool = false) -> Int { #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) return CCCryptorGetOutputLength(self.context.pointee, inputByteCount, isFinal) #elseif os(Linux) if inputByteCount == 0 { return self.algorithm.blockSize } return (inputByteCount + self.algorithm.blockSize - (inputByteCount % self.algorithm.blockSize)) #endif } }