Trong hầu hết các trường hợp, khi xây dựng ứng dụng trò chuyện, việc cung cấp đủ sự riêng tư và bảo mật cho người dùng của bạn là rất quan trọng. Điều này có thể được thực hiện bằng các phương pháp mật mã hóa như mã hóa từ đầu đến cuối. Mã hóa từ đầu đến cuối đang trở thành một yêu cầu phổ biến, vì nó được tích hợp trong các ứng dụng trò chuyện lớn như WhatsApp và Telegram.
Trong bài viết này, bạn sẽ tìm hiểu cách mã hóa từ đầu đến cuối cơ bản hoạt động trong một ứng dụng trò chuyện iOS bằng cách sử dụng CryptoKit của Apple cho các phương pháp mã hóa an toàn và cấp cao và SDK chat iOS linh hoạt của Stream cho các thành phần mạng và giao diện người dùng trò chuyện sẵn có.
1

Vui lòng lưu ý rằng hướng dẫn này rất cơ bản và chỉ mang tính tham khảo, và việc tự triển khai giao thức mã hóa của riêng bạn không được khuyến khích. Các thuật toán được sử dụng có thể chứa gây một số lỗi nhất định nếu không được triển khai đúng cách với sự giúp đỡ của các chuyên gia bảo mật. Tuy nhiên, việc đọc bài viết này có thể là một bước khởi đầu đáng để thử

Bạn có thể tìm thấy dự án đã hoàn thành trên GitHub: encrypted-ios-chat.

End-to-End Encryption là gì?

End-to-End Encryption - Mã hóa từ đầu đến cuối là một hệ thống truyền thông trong đó chỉ những người tham gia trò chuyện mới có thể đọc được các tin nhắn. Không có kẻ nghe trộm nào có thể truy cập vào các khóa mật mã hóa cần thiết để giải mã cuộc trò chuyện — không cả một công ty nào quản lý dịch vụ nhắn tin đó.

Lexicon Hacker: End-to-End Encryption là gì

CryptoKit của Apple là gì?

CryptoKit là một framework Swift mới giúp việc thực hiện các hoạt động mật mã hóa dễ dàng và an toàn hơn bao giờ hết, dù bạn chỉ cần tính một hàm hash hoặc đang triển khai một giao thức xác thực phức tạp hơn.

WWDC19: Mật mã và Ứng dụng của Bạn
Lexicon Hacker: End-to-End Encryption là gì

Những gì bạn cần

  • Xcode 11 hoặc mới hơn
  • iOS 13 hoặc mới hơn

Các Phương pháp Mã hóa Cơ bản

Trước khi chúng ta đến phần trò chuyện, hãy bắt đầu bằng cách xác định các phương pháp mã hóa cơ bản chúng ta cần. Chúng ta sẽ tích hợp các phương pháp này trong phần tiếp theo.

1. Tạo một private key

Một private key là rất quan trọng cho mã hóa từ đầu đến cuối. Nó được sử dụng cùng với một public key để tạo ra một khóa đối xứng được sử dụng để mã hóa và giải mã dữ liệu. Mỗi người dùng trong ứng dụng của bạn đều có một private key và công khai. Họ chia sẻ public key của họ cho nhau để họ có thể tạo ra cùng một khóa đối xứng. Alice chia sẻ public key của mình với Bob, và Bob sử dụng private key của mình và public key của Alice để tạo ra một khóa đối xứng. Ngược lại, Bob chia sẻ public key của mình với Alice, và Alice sử dụng private key của mình với public key của Bob để tạo ra cùng một khóa đối xứng. Miễn là private key của Alice và Bob là bí mật với mỗi người trong số họ, không ai có thể tạo ra cùng một khóa đối xứng để giải mã cuộc trò chuyện của họ.

import CryptoKit

func generatePrivateKey() -> P256.KeyAgreement.PrivateKey {
    let privateKey = P256.KeyAgreement.PrivateKey()
    return privateKey
}

Để tạo một private key, chúng ta sẽ sử dụng P256.KeyAgreement.PrivateKey() initializer.

Ngoài ra, tôi đã chọn thuật toán P256 vì tính sẵn có trên nhiều nền tảng, vì nó được hỗ trợ bởi API Crypto Web trong trường hợp bạn cần một phiên bản web. Ngoài ra, đây là sự cân bằng tốt giữa hiệu suất và bảo mật. Sự ưu tiên này có thể thay đổi theo thời gian khi các thuật toán mới trở nên có sẵn.

PS: public key có thể được trích xuất từ private key bằng cách sử dụng privateKey.publicKey. Chúng ta sẽ đề cập đến điều này trong phần tích hợp của bài viết này.

private key phải được lưu ở một nơi an toàn. Để làm điều này, bạn có thể cần chuyển đổi nó thành định dạng chuỗi để lưu trữ và chuyển đổi lại để thực hiện các hoạt động mật mã. Bạn có thể làm điều này bằng cách sử dụng phương thức exportPrivateKey sau đây:

import Foundation
import CryptoKit

func exportPrivateKey(_ privateKey: P256.KeyAgreement.PrivateKey) -> String {
    let rawPrivateKey = privateKey.rawRepresentation
    let privateKeyBase64 = rawPrivateKey.base64EncodedString()
    let percentEncodedPrivateKey = privateKeyBase64.addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
    return percentEncodedPrivateKey
}

Và phương thức importPrivateKey:

import Foundation
import CryptoKit

func importPrivateKey(_ privateKey: String) throws -> P256.KeyAgreement.PrivateKey {
    let privateKeyBase64 = privateKey.removingPercentEncoding!
    let rawPrivateKey = Data(base64Encoded: privateKeyBase64)!
    return try P256.KeyAgreement.PrivateKey(rawRepresentation: rawPrivateKey)
}

2. Rút Trích Khóa Đối xứng

Khi một người dùng (người gửi tin nhắn) có public key của người dùng khác mà nó muốn trò chuyện (người nhận tin nhắn), người đó có thể tạo ra khóa đối xứng bằng cách sử dụng các phương pháp: privateKey.sharedSecretFromKeyAgreement(with: publicKey)sharedSecret.hkdfDerivedSymmetricKey.

import Foundation
import CryptoKit

func deriveSymmetricKey(privateKey: P256.KeyAgreement.PrivateKey, publicKey: P256.KeyAgreement.PublicKey) throws -> SymmetricKey {
    let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: publicKey)
    
    let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(
        using: SHA256.self,
        salt: "My Key Agreement Salt".data(using: .utf8)!,
        sharedInfo: Data(),
        outputByteCount: 32
    )
    
    return symmetricKey
}

Quá trình này được gọi là Trò chuyện Khóa Diffie-Hellman và đảm bảo rằng khóa đối xứng có thể được chia sẻ giữa hai người dùng mà không bao giờ rời khỏi thiết bị của họ.

3. Mã Hóa Văn Bản

Bây giờ, chúng ta sử dụng khóa đối xứng đó để mã hóa văn bản bằng cách sử dụng phương thức AES.GCM.seal. Chúng ta cũng mã hóa nó thành một chuỗi Base64, để nó có thể được gửi như văn bản bình thường bằng cách sử dụng Stream Chat SDK trong bước tích hợp.
Sau khi văn bản được mã hóa và mã hóa, nó có thể được an toàn gửi qua mạng.

4. Giải Mã Văn Bản

Sau khi người nhận nhận được văn bản đã được mã hóa, họ sẽ giải mã nó từ Base64 và giải mã nó bằng cách sử dụng các phương thức AES.GCM.SealedBoxAES.GCM.open.

import Foundation
import CryptoKit

func decrypt(text: String, symmetricKey: SymmetricKey) -> String {
    do {
        guard let data = Data(base64Encoded: text) else {
            return "Could not decode text: \(text)"
        }
        
        let sealedBox = try AES.GCM.SealedBox(combined: data)
        let decryptedData = try AES.GCM.open(sealedBox, using: symmetricKey)
        
        guard let text = String(data: decryptedData, encoding: .utf8) else {
            return "Could not decode data: \(decryptedData)"
        }
        
        return text
    } catch let error {
        return "Error decrypting message: \(error.localizedDescription)"
    }
}

Sau khi văn bản được giải mã, nó nên có thể đọc được và có thể hiển thị trên giao diện người dùng.

Tích Hợp Stream Chat

Trong phần này, bạn sẽ tìm hiểu cách sử dụng các phương thức đã triển khai ở trên để đạt được mã hóa từ đầu đến cuối với SDK Swift của Stream Chat.
Bạn có thể làm theo bước “iOS Chat SDK Setup” trong hướng dẫn chính thức để có một ứng dụng trò chuyện cơ bản hoạt động trong vài phút.

1. Dữ Liệu Phụ public key

Chúng ta cần xác định một trường publicKey tùy chỉnh cho đối tượng Người dùng để người dùng có thể chia sẻ public key của mình với người khác. Để làm điều này, tạo kiểu PublicKeyExtraData.

import StreamChatClient
import Foundation

struct PublicKeyExtraData: UserExtraDataCodable, Codable {
    var name: String?
    var avatarURL: URL?
    var publicKey: String?
}

Sau đó, bạn nên thiết lập User.extraDataType thành PublicKeyExtraData.self trước khi bạn khởi tạo Stream Chat Client trong AppDelegate.swift:

import UIKit
import StreamChatClient

[...]
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        User.extraDataType = PublicKeyExtraData.self
        Client.configureShared(.init(apiKey: "[api_key]", logOptions: .info))
        return true
    }
[...]

2. Phương thức Custom Set User

Bây giờ, hãy xác định phương thức setUser tùy chỉnh của chúng tôi để người dùng có thể được đăng ký trong Stream Chat cùng với public key của nó.

import Foundation
import CryptoKit
import StreamChatClient

func setUser(userId: String, publicKey: P256.KeyAgreement.PublicKey, completion: @escaping (Result<UserConnection, ClientError>) -> Void) {
    let exportedPublicKey = exportPublicKey(publicKey)
    
    let extraData = PublicKeyExtraData(
        name: userId,
        avatarURL: URL(string: "https://getstream.io/random_png/?id=\(userId)&name=\(userId)"),
        publicKey: exportedPublicKey
    )
    
    let sender = User(id: userId, extraData: extraData)
    
    Client.shared.set(user: sender, token: .development, completion)
}

Ngoài ra, public key phải được xuất dưới định dạng chuỗi. Bạn nên xác định một phương thức exportPublicKey để thực hiện điều này.

import CryptoKit

func exportPublicKey(_ publicKey: P256.KeyAgreement.PublicKey) -> String {
    let rawPublicKey = publicKey.rawRepresentation
    let base64PublicKey = rawPublicKey.base64EncodedString()
    let encodedPublicKey = base64PublicKey.addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
    return encodedPublicKey
}

Chúng tôi đang mã hóa phần trăm public key sau khi mã hóa sang base64 để đảm bảo dấu + không bị bỏ qua trong trường hợp public key được sử dụng trong các URL của các yêu cầu cụ thể.

Trong một bước tương lai, sau khi chúng tôi lấy public key của một người dùng khác, chúng tôi sẽ cần sử dụng phương thức importPublicKey, thực hiện việc ngược lại của exportPublicKey.

import Foundation
import CryptoKit

func importPublicKey(_ publicKey: String) throws -> P256.KeyAgreement.PublicKey {
    let base64PublicKey = publicKey.removingPercentEncoding!
    let rawPublicKey = Data(base64Encoded: base64PublicKey)!
    let publicKey = try P256.KeyAgreement.PublicKey(rawRepresentation: rawPublicKey)
    return publicKey
}

3. Mã hóa Chat View Controller

Bây giờ chúng ta đã tạo ra private key và đăng ký người dùng của mình với một public key, chúng ta cần một Custom ChatViewController mã hóa các tin nhắn trước khi gửi và giải mã các tin nhắn trước khi hiển thị.
Để làm điều này, hãy tạo EncryptedChatViewController, kế thừa từ ChatViewController.

import StreamChat
import StreamChatClient
import CryptoKit
import UIKit

class EncryptedChatViewController: ChatViewController {
    var symmetricKey: SymmetricKey!

    override func viewDidLoad() {
        super.viewDidLoad()

        presenter?.messagePreparationCallback = {
            var message = $0
            
            do {
                message.text = try encrypt(text: message.text, symmetricKey: self.symmetricKey!)
            } catch let error {
                message.text = "Could not encrypt message: \(error.localizedDescription)"
            }
            
            return message
        }
    }
    
    override func messageCell(at indexPath: IndexPath, message: Message, readUsers: [User]) -> UITableViewCell {
        var message = message

        message.text = decrypt(text: message.text, symmetricKey: symmetricKey)

        return super.messageCell(at: indexPath, message: message, readUsers: readUsers)
    }
}

Như bạn có thể thấy, nó chứa một thuộc tính symmetricKey bổ sung, sẽ được sử dụng cho việc mã hóa và giải mã. Trong viewDidLoad, chúng ta thiết lập messagePreparationCallback, làm mã hóa các tin nhắn trước khi chúng được gửi. Chúng tôi cũng ghi đè phương thức messageCell để giải mã tin nhắn trước khi hiển thị nó.

4. Hiển Thị Trò chuyện được Mã hóa

Sau khi triển khai Custom ChatViewController của chúng tôi, bây giờ chúng ta có thể hiển thị nó.

import Foundation
import CryptoKit
import StreamChatClient
import StreamChatCore
import UIKit

extension UIViewController {
    func presentEncryptedChat(with userId: String, privateKey: P256.KeyAgreement.PrivateKey) {
        Client.shared.queryUsers(filter: .equal("id", to: userId)) { result in
            DispatchQueue.main.async {
                switch result {
                case let .success(users):
                    guard let user = users.first else {
                        return self.alert(title: "Error", message: "The recipient was not found")
                    }
                    
                    guard let publicKey = (user.extraData as? PublicKeyExtraData)?.publicKey else {
                        return self.alert(title: "Error", message: "The recipient doesn't have a public key")
                    }
                    
                    do {
                        let importedPublicKey = try importPublicKey(publicKey)
                        let derivedKey = try deriveSymmetricKey(privateKey: privateKey, publicKey: importedPublicKey)
                        
                        let chatViewController = EncryptedChatViewController()
                        chatViewController.symmetricKey = derivedKey
                        chatViewController.presenter = ChannelPresenter(channel: Client.shared.channel(members: [Client.shared.user, user]))
                        
                        self.navigationController?.pushViewController(chatViewController, animated: true)
                    } catch let error {
                        self.alert(title: "Error", message: "Could not derive key: \(error)")
                    }
                case let .failure(error):
                    self.alert(title: "Error", message: error.localizedDescription)
                }
            }
        }
    }
}

Để hiển thị EncryptedChatViewController, chúng ta truy vấn người dùng chúng ta muốn trò chuyện để lấy public key của họ và tạo ra một khóa đối xứng. Sau đó, chúng ta đặt các thuộc tính symmetricKeypresenter và đẩy view controller vào navigation stack

Bước Tiếp Theo Với CryptoKit

Chúc mừng! Bạn vừa học cách triển khai mã hóa từ đầu đến cuối cơ bản trong ứng dụng trò chuyện iOS của bạn. Quan trọng là phải biết rằng đây là dạng cơ bản nhất của mã hóa từ đầu đến cuối. Nó thiếu một số điều chỉnh bổ sung có thể làm cho nó mạnh mẽ hơn trong thực thế, chẳng hạn như randomized padding, chữ ký số, và bảo mật tịnh tiến vvv. Ngoài ra, để sử dụng trong thực tế, việc nhận sự giúp đỡ từ các chuyên gia bảo mật ứng dụng là rất quan trọng.
nguồn tham khảo : End-to-End Encrypted iOS Chat with Apple’s CryptoKit