From 2ed8aab69fe74e64496c0b5902aca60b24043dcb Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Wed, 25 May 2022 15:43:12 +0200 Subject: [PATCH] perf(ios): optimize swift code --- .../Classes/SwiftNativeCryptoIosPlugin.swift | 225 +++++++++++------- .../ios/Classes/ciphers/AESCipher.swift | 47 +++- .../ios/Classes/kdf/Pbkdf2.swift | 55 +++++ .../ios/Classes/keys/SecretKey.swift | 17 ++ .../ios/Classes/protocols/Cipher.swift | 8 + .../ios/Classes/protocols/Key.swift | 4 + .../ios/Classes/protocols/KeyDerivation.swift | 5 + .../ios/Classes/utils/CipherAlgorithm.swift | 10 + .../ios/Classes/utils/HashAlgorithm.swift | 37 +++ .../ios/Classes/utils/KdfAlgorithm.swift | 4 + .../ios/Classes/utils/NativeCryptoError.swift | 10 + .../ios/Classes/utils/Task.swift | 44 ++++ 12 files changed, 370 insertions(+), 96 deletions(-) diff --git a/packages/native_crypto_ios/ios/Classes/SwiftNativeCryptoIosPlugin.swift b/packages/native_crypto_ios/ios/Classes/SwiftNativeCryptoIosPlugin.swift index 00fdbb7..c9b1a84 100644 --- a/packages/native_crypto_ios/ios/Classes/SwiftNativeCryptoIosPlugin.swift +++ b/packages/native_crypto_ios/ios/Classes/SwiftNativeCryptoIosPlugin.swift @@ -3,93 +3,144 @@ import UIKit @available(iOS 13.0, *) public class SwiftNativeCryptoIosPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "plugins.hugop.cl/native_crypto", binaryMessenger: registrar.messenger()) - let instance = SwiftNativeCryptoIosPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "digest": - let args : NSDictionary = call.arguments as! NSDictionary - - let data : Data = (args["data"] as! FlutterStandardTypedData).data - let algo : String = args["algorithm"] as! String - let algorithm : HashAlgorithm? = HashAlgorithm.init(rawValue: algo) - // TODO(hpcl): check if algorithm is null - // TODO(hpcl): check if digest is null - result(FlutterStandardTypedData.init(bytes: Hash.digest(data: data, algorithm: algorithm!))) - case "generateSecretKey": - let args : NSDictionary = call.arguments as! NSDictionary - - let bitsCount : NSNumber = args["bitsCount"] as! NSNumber - // TODO(hpcl): check if secure random is null - result(FlutterStandardTypedData.init(bytes: Key.fromSecureRandom(bitsCount: bitsCount.intValue))) - case "generateKeyPair": - result(FlutterStandardTypedData.init(bytes: KeyPair.fromCurve())) - case "pbkdf2": - let args : NSDictionary = call.arguments as! NSDictionary - - let password : String = args["password"] as! String - let salt : String = args["salt"] as! String - let keyBytesCount : NSNumber = args["keyBytesCount"] as! NSNumber - let iterations : NSNumber = args["iterations"] as! NSNumber - let algo : String = args["algorithm"] as! String - let algorithm : HashAlgorithm? = HashAlgorithm.init(rawValue: algo) - // TODO(hpcl): check if algorithm is null - // TODO(hpcl): check if derivation is null - result(FlutterStandardTypedData.init(bytes: Key.fromPBKDF2(password: password, salt: salt, keyBytesCount: keyBytesCount.intValue, iterations: iterations.intValue, algorithm: algorithm!)!)) - case "encrypt": - let args : NSDictionary = call.arguments as! NSDictionary - - let data : Data = (args["data"] as! FlutterStandardTypedData).data - let key : Data = (args["key"] as! FlutterStandardTypedData).data - let algo : String = args["algorithm"] as! String - // TODO(hpcl): check if algorithm is null - // TODO(hpcl): check if ciphertext is null - var ciphertext : Data - switch algo { - case "aes": - ciphertext = AESCipher.encrypt(plaintext: data, key: key)! - case "chachapoly": - ciphertext = CHACHACipher.encrypt(plaintext: data, key: key)! - default: - ciphertext = Data.init(); - } - result(FlutterStandardTypedData.init(bytes: ciphertext)) - case "decrypt": - let args : NSDictionary = call.arguments as! NSDictionary - - let data : Data = (args["data"] as! FlutterStandardTypedData).data - let key : Data = (args["key"] as! FlutterStandardTypedData).data - let algo : String = args["algorithm"] as! String - // TODO(hpcl): check if algorithm is null - // TODO(hpcl): check if ciphertext is null - var ciphertext : Data - switch algo { - case "aes": - ciphertext = AESCipher.decrypt(ciphertext: data, key: key)! - case "chachapoly": - ciphertext = CHACHACipher.decrypt(ciphertext: data, key: key)! - default: - ciphertext = Data.init(); - } - result(FlutterStandardTypedData.init(bytes: ciphertext)) - case "generateSharedSecretKey": - let args : NSDictionary = call.arguments as! NSDictionary - - let salt : Data = (args["salt"] as! FlutterStandardTypedData).data - let keyBytesCount : NSNumber = args["keyBytesCount"] as! NSNumber - let ephemeralPrivateKey : Data = (args["ephemeralPrivateKey"] as! FlutterStandardTypedData).data - let otherPublicKey : Data = (args["otherPublicKey"] as! FlutterStandardTypedData).data - let hkdfAlgorithm : String = args["hkdfAlgorithm"] as! String - let algorithm : HashAlgorithm? = HashAlgorithm.init(rawValue: hkdfAlgorithm) - // TODO(hpcl): check if algorithm is null - // TODO(hpcl): check if generated key is null - result(FlutterStandardTypedData.init(bytes: ECDH.generateSharedSecretKey(salt: salt, hash: algorithm!, keyBytesCount: keyBytesCount.intValue, privateKey: ephemeralPrivateKey, publicKey: otherPublicKey)!)) + static let name: String = "plugins.hugop.cl/native_crypto" - default: result(FlutterMethodNotImplemented) + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: name, binaryMessenger: registrar.messenger()) + let instance = SwiftNativeCryptoIosPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) } - } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "digest": _call(task: handleDigest(call: call), result: result) + case "generateSecretKey": _call(task: handleGenerateSecretKey(call: call), result: result) + case "pbkdf2": _call(task: handlePbkdf2(call: call), result: result) + case "encryptAsList": _call(task: handleEncryptAsList(call: call), result: result) + case "decryptAsList": _call(task: handleDecryptAsList(call: call), result: result) + case "encrypt": _call(task: handleCrypt(call: call, forEncryption: true), result: result) + case "decrypt": _call(task: handleCrypt(call: call, forEncryption: false), result: result) + default: result(FlutterMethodNotImplemented) + } + } + + private func _call(task: Task, result: @escaping FlutterResult) { + task.call() + task.finalize(callback: {(task: Task) in + if (task.isSuccessful()) { + result(task.getResult()!) + } else { + let exception: Error = task.getException() + let message = exception.localizedDescription + result(FlutterError(code: "native_crypto", message: message, details: nil)) + } + }) + } + + private func handleDigest(call: FlutterMethodCall) -> Task { + return Task(task: { + let args : NSDictionary = call.arguments as! NSDictionary + + let data : Data = (args["data"] as! FlutterStandardTypedData).data + let algorithm : String = args["algorithm"] as! String + + return FlutterStandardTypedData.init(bytes: try HashAlgorithm.digest(data: data, algorithm: algorithm)) + }) + } + + private func handleGenerateSecretKey(call: FlutterMethodCall) -> Task { + return Task(task: { + let args : NSDictionary = call.arguments as! NSDictionary + + let bitsCount : NSNumber = args["bitsCount"] as! NSNumber + + return FlutterStandardTypedData.init(bytes: SecretKey(fromSecureRandom: bitsCount.intValue).bytes) + }) + } + + private func handlePbkdf2(call: FlutterMethodCall) -> Task { + return Task(task: { + let args : NSDictionary = call.arguments as! NSDictionary + + let password : String = args["password"] as! String + let salt : String = args["salt"] as! String + let keyBytesCount : NSNumber = args["keyBytesCount"] as! NSNumber + let iterations : NSNumber = args["iterations"] as! NSNumber + let algorithm : String = args["algorithm"] as! String + + let pbkdf2 : Pbkdf2 = Pbkdf2(keyBytesCount: keyBytesCount.intValue, iterations: iterations.intValue) + pbkdf2.hash = HashAlgorithm.init(rawValue: algorithm) ?? pbkdf2.hash + pbkdf2.initialize(password: password, salt: salt) + + return FlutterStandardTypedData.init(bytes: try pbkdf2.derive().bytes) + }) + } + + private func handleEncryptAsList(call: FlutterMethodCall) -> Task> { + return Task(task: { + let args : NSDictionary = call.arguments as! NSDictionary + + let data : Data = (args["data"] as! FlutterStandardTypedData).data + let key : Data = (args["key"] as! FlutterStandardTypedData).data + let algorithm : String = args["algorithm"] as! String + + let cipherAlgorithm : CipherAlgorithm? = CipherAlgorithm.init(rawValue: algorithm) + var cipher : Cipher + if (cipherAlgorithm != nil) { + cipher = cipherAlgorithm!.getCipher + } else { + throw NativeCryptoError.cipherError + } + + return try cipher.encryptAsList(data: data, key: key) + }) + } + + private func handleDecryptAsList(call: FlutterMethodCall) -> Task { + return Task(task: { + let args : NSDictionary = call.arguments as! NSDictionary + + let data = args["data"] as! NSArray + let key : Data = (args["key"] as! FlutterStandardTypedData).data + let algorithm : String = args["algorithm"] as! String + + let iv = (data[0] as! FlutterStandardTypedData).data + let encrypted = (data[1] as! FlutterStandardTypedData).data + + let cipherAlgorithm : CipherAlgorithm? = CipherAlgorithm.init(rawValue: algorithm) + var cipher : Cipher + if (cipherAlgorithm != nil) { + cipher = cipherAlgorithm!.getCipher + } else { + throw NativeCryptoError.cipherError + } + + return FlutterStandardTypedData.init(bytes: try cipher.decryptAsList(data: [iv, encrypted], key: key)) + }) + } + + private func handleCrypt(call: FlutterMethodCall, forEncryption: Bool) -> Task { + return Task(task: { + let args : NSDictionary = call.arguments as! NSDictionary + + let data : Data = (args["data"] as! FlutterStandardTypedData).data + let key : Data = (args["key"] as! FlutterStandardTypedData).data + let algorithm : String = args["algorithm"] as! String + + let cipherAlgorithm : CipherAlgorithm? = CipherAlgorithm.init(rawValue: algorithm) + var cipher : Cipher + if (cipherAlgorithm != nil) { + cipher = cipherAlgorithm!.getCipher + } else { + throw NativeCryptoError.cipherError + } + + if (forEncryption) { + return FlutterStandardTypedData.init(bytes: try cipher.encrypt(data: data, key: key)) + } else { + return FlutterStandardTypedData.init(bytes: try cipher.decrypt(data: data, key: key)) + } + }) + } + } diff --git a/packages/native_crypto_ios/ios/Classes/ciphers/AESCipher.swift b/packages/native_crypto_ios/ios/Classes/ciphers/AESCipher.swift index a52a925..2110ff5 100644 --- a/packages/native_crypto_ios/ios/Classes/ciphers/AESCipher.swift +++ b/packages/native_crypto_ios/ios/Classes/ciphers/AESCipher.swift @@ -1,28 +1,57 @@ // -// AES.swift +// AESCipher.swift // native_crypto_ios // // Created by Hugo Pointcheval on 25/05/2022. // import Foundation +import CryptoKit -class AES : Cipher { +class AESCipher : Cipher { + var algorithm: CipherAlgorithm = CipherAlgorithm.aes + /// Encrypts plaintext with key using AES GCM @available(iOS 13.0, *) - static func encrypt(plaintext: Data, key: Data) -> Data? { - let symmetricKey = SymmetricKey.init(data: key) - let encrypted = try? AES.GCM.seal(plaintext, using: symmetricKey) - return encrypted?.combined + func encrypt(data: Data, key: Data) throws -> Data { + let symmetricKey : SymmetricKey = SymmetricKey.init(data: key) + let encrypted : AES.GCM.SealedBox? = try? AES.GCM.seal(data, using: symmetricKey) + + let encryptedData : Data? = encrypted?.combined + if (encryptedData == nil) { + throw NativeCryptoError.encryptionError + } + return encryptedData! } /// Decrypts ciphertext with key using AES GCM @available(iOS 13.0, *) - static func decrypt(ciphertext: Data, key: Data) -> Data? { + func decrypt(data: Data, key: Data) throws -> Data { let symmetricKey = SymmetricKey.init(data: key) - let sealedBox = try? AES.GCM.SealedBox(combined: ciphertext) - if (sealedBox == nil) { return nil } + let sealedBox = try? AES.GCM.SealedBox(combined: data) + if (sealedBox == nil) { return Data.init() } let decryptedData = try? AES.GCM.open(sealedBox!, using: symmetricKey) + if (decryptedData == nil) { + throw NativeCryptoError.decryptionError + } + return decryptedData! + } + + func encryptAsList(data: Data, key: Data) throws -> [Data] { + let encryptedData = try encrypt(data: data, key: key) + + let iv = encryptedData.prefix(12) + let data = encryptedData.suffix(from: 12) + + return [iv, data] + } + + func decryptAsList(data: [Data], key: Data) throws -> Data { + var encryptedData = data.first! + let data = data.last! + encryptedData.append(data) + + let decryptedData = try decrypt(data: encryptedData, key: key) return decryptedData } } diff --git a/packages/native_crypto_ios/ios/Classes/kdf/Pbkdf2.swift b/packages/native_crypto_ios/ios/Classes/kdf/Pbkdf2.swift index 1c52aa9..8a34a1e 100644 --- a/packages/native_crypto_ios/ios/Classes/kdf/Pbkdf2.swift +++ b/packages/native_crypto_ios/ios/Classes/kdf/Pbkdf2.swift @@ -6,3 +6,58 @@ // import Foundation +import CommonCrypto + +class Pbkdf2 : KeyDerivation { + var algorithm: KdfAlgorithm = KdfAlgorithm.pbkdf2 + + var keyBytesCount: Int + var iterations: Int + var hash: HashAlgorithm = HashAlgorithm.HashSHA256 + + var password: String? = nil + var salt: String? = nil + + init(keyBytesCount: Int, iterations: Int) { + self.keyBytesCount = keyBytesCount + self.iterations = iterations + } + + func initialize(password: String, salt: String) { + self.password = password + self.salt = salt + } + + func derive() throws -> SecretKey { + if (password == nil || salt == nil) { + throw NativeCryptoError.pbkdf2Error + } + + let passwordData = password!.data(using: .utf8)! + let saltData = salt!.data(using: .utf8)! + + var derivedKeyData = Data(repeating: 0, count: keyBytesCount) + let localDerivedKeyData = derivedKeyData + + let status = derivedKeyData.withUnsafeMutableBytes { (derivedKeyBytes: UnsafeMutableRawBufferPointer) in + saltData.withUnsafeBytes { (saltBytes: UnsafeRawBufferPointer) in + CCKeyDerivationPBKDF( + CCPBKDFAlgorithm(kCCPBKDF2), + password, + passwordData.count, + saltBytes.bindMemory(to: UInt8.self).baseAddress, + saltData.count, + hash.pbkdf2identifier, + UInt32(iterations), + derivedKeyBytes.bindMemory(to: UInt8.self).baseAddress, + localDerivedKeyData.count) + } + } + + if (status != kCCSuccess) { + throw NativeCryptoError.pbkdf2Error + } + + return SecretKey(derivedKeyData) + } +} diff --git a/packages/native_crypto_ios/ios/Classes/keys/SecretKey.swift b/packages/native_crypto_ios/ios/Classes/keys/SecretKey.swift index df97a3d..6325143 100644 --- a/packages/native_crypto_ios/ios/Classes/keys/SecretKey.swift +++ b/packages/native_crypto_ios/ios/Classes/keys/SecretKey.swift @@ -6,3 +6,20 @@ // import Foundation +import CryptoKit + +class SecretKey : Key { + var bytes: Data + + init(_ bytes: Data) { + self.bytes = bytes + } + + init(fromSecureRandom bitsCount: Int) { + let symmetricKey = SymmetricKey.init(size: SymmetricKeySize(bitCount: bitsCount)) + bytes = symmetricKey.withUnsafeBytes + { + return Data(Array($0)) + } + } +} diff --git a/packages/native_crypto_ios/ios/Classes/protocols/Cipher.swift b/packages/native_crypto_ios/ios/Classes/protocols/Cipher.swift index 9c2cc81..f1291f9 100644 --- a/packages/native_crypto_ios/ios/Classes/protocols/Cipher.swift +++ b/packages/native_crypto_ios/ios/Classes/protocols/Cipher.swift @@ -6,3 +6,11 @@ // import Foundation + +protocol Cipher { + var algorithm: CipherAlgorithm { get } + func encrypt(data: Data, key: Data) throws -> Data + func decrypt(data: Data, key: Data) throws -> Data + func encryptAsList(data: Data, key: Data) throws -> [Data] + func decryptAsList(data: [Data], key: Data) throws-> Data +} diff --git a/packages/native_crypto_ios/ios/Classes/protocols/Key.swift b/packages/native_crypto_ios/ios/Classes/protocols/Key.swift index 19bea38..9fc9199 100644 --- a/packages/native_crypto_ios/ios/Classes/protocols/Key.swift +++ b/packages/native_crypto_ios/ios/Classes/protocols/Key.swift @@ -6,3 +6,7 @@ // import Foundation + +protocol Key { + var bytes: Data { get set } +} diff --git a/packages/native_crypto_ios/ios/Classes/protocols/KeyDerivation.swift b/packages/native_crypto_ios/ios/Classes/protocols/KeyDerivation.swift index 3313121..df3a0d5 100644 --- a/packages/native_crypto_ios/ios/Classes/protocols/KeyDerivation.swift +++ b/packages/native_crypto_ios/ios/Classes/protocols/KeyDerivation.swift @@ -6,3 +6,8 @@ // import Foundation + +protocol KeyDerivation { + var algorithm : KdfAlgorithm { get } + func derive() throws -> SecretKey +} diff --git a/packages/native_crypto_ios/ios/Classes/utils/CipherAlgorithm.swift b/packages/native_crypto_ios/ios/Classes/utils/CipherAlgorithm.swift index 0617eb0..afb9094 100644 --- a/packages/native_crypto_ios/ios/Classes/utils/CipherAlgorithm.swift +++ b/packages/native_crypto_ios/ios/Classes/utils/CipherAlgorithm.swift @@ -6,3 +6,13 @@ // import Foundation + +enum CipherAlgorithm : String { + case aes = "aes" + + var getCipher: Cipher { + switch self { + case .aes: return AESCipher() + } + } +} diff --git a/packages/native_crypto_ios/ios/Classes/utils/HashAlgorithm.swift b/packages/native_crypto_ios/ios/Classes/utils/HashAlgorithm.swift index 1dbe5f7..38b7740 100644 --- a/packages/native_crypto_ios/ios/Classes/utils/HashAlgorithm.swift +++ b/packages/native_crypto_ios/ios/Classes/utils/HashAlgorithm.swift @@ -6,3 +6,40 @@ // import Foundation +import CommonCrypto +import CryptoKit + +enum HashAlgorithm: String { + case HashSHA256 = "sha256" + case HashSHA384 = "sha384" + case HashSHA512 = "sha512" + + var pbkdf2identifier: UInt32 { + switch self { + case .HashSHA256: return CCPBKDFAlgorithm(kCCPRFHmacAlgSHA256) + case .HashSHA384: return CCPBKDFAlgorithm(kCCPRFHmacAlgSHA384) + case .HashSHA512: return CCPBKDFAlgorithm(kCCPRFHmacAlgSHA512) + } + } + + @available(iOS 13.0, *) + func digest(data: Data) -> Data { + switch self { + case .HashSHA256: + return Data(SHA256.hash(data: data)) + case .HashSHA384: + return Data(SHA384.hash(data: data)) + case .HashSHA512: + return Data(SHA512.hash(data: data)) + } + } + + @available(iOS 13.0, *) + static func digest(data: Data, algorithm: String) throws -> Data { + let algo = HashAlgorithm.init(rawValue: algorithm) + if (algo == nil) { + throw NativeCryptoError.messageDigestError + } + return algo!.digest(data: data) + } +} diff --git a/packages/native_crypto_ios/ios/Classes/utils/KdfAlgorithm.swift b/packages/native_crypto_ios/ios/Classes/utils/KdfAlgorithm.swift index 7629866..d6af3c1 100644 --- a/packages/native_crypto_ios/ios/Classes/utils/KdfAlgorithm.swift +++ b/packages/native_crypto_ios/ios/Classes/utils/KdfAlgorithm.swift @@ -6,3 +6,7 @@ // import Foundation + +enum KdfAlgorithm { + case pbkdf2 +} diff --git a/packages/native_crypto_ios/ios/Classes/utils/NativeCryptoError.swift b/packages/native_crypto_ios/ios/Classes/utils/NativeCryptoError.swift index 540909d..a2ac159 100644 --- a/packages/native_crypto_ios/ios/Classes/utils/NativeCryptoError.swift +++ b/packages/native_crypto_ios/ios/Classes/utils/NativeCryptoError.swift @@ -6,3 +6,13 @@ // import Foundation + +enum NativeCryptoError : Error { +case decryptionError +case encryptionError +case messageDigestError +case pbkdf2Error +case cipherError +case resultError +case exceptionError +} diff --git a/packages/native_crypto_ios/ios/Classes/utils/Task.swift b/packages/native_crypto_ios/ios/Classes/utils/Task.swift index db3fa79..4d11070 100644 --- a/packages/native_crypto_ios/ios/Classes/utils/Task.swift +++ b/packages/native_crypto_ios/ios/Classes/utils/Task.swift @@ -6,3 +6,47 @@ // import Foundation + +class Task { + + var task: () throws -> T + private var successful: Bool = false + private var result: T? = nil + private var exception: Error? = nil + + init(task: @escaping () throws -> T) { + self.task = task + } + + func isSuccessful() -> Bool { + return successful + } + + func getResult() -> T? { + return result + } + + func getException() -> Error { + if (exception != nil) { + return exception! + } else { + return NativeCryptoError.exceptionError + } + } + + func call() { + do { + result = try task() + exception = nil + successful = true + } catch { + exception = error + result = nil + successful = false + } + } + + func finalize(callback: (_ task: Task) -> Void) { + callback(self) + } +}