Skip to main content

Một số lưu ý bảo mật cần có cho app iOS


Lời nói đầu

Như chúng ta đã biết, bảo mật là một trong những khía cạnh quan trọng nhất trong phát triển phần mềm. Và khi người dùng sử dụng ứng dụng, đồng nghĩa với việc họ đang đặt trọn niềm tin cũng như sự kì vọng của mình vào các lập trình viên, rằng dữ liệu của họ luôn được đảm bảo riêng tư.
Để không phụ niềm tin và sự kì vọng ấy, chúng ta nên có một cách thức nào đấy để làm cho dữ liệu của mình ít nhất không thể an toàn tuyệt đối, thì chí ít cũng phải để cho những phần tử phá hoại phải mất công động não, lao tâm khổ tứ một chút.
May mắn thay, cho người dùng và cũng cho các lập trình viên, trong bài viết nay tôi sẽ đề cập đến sai lầm thường gặp trong việc bảo mật dữ liệu trong ứng dụng và một số cách để khắc phục điều này.

Sai lầm thứ nhất: Lưu dữ liệu nhạy cảm "nhầm" chỗ

Là một người làm app, và cũng clone app khá nhiều - thỉnh thoảng tôi thường dạo quanh Apple Store ngắm nghía một vài app và nhận ra rằng: Có rất nhiều ứng dụng đã và đang lặp đi lặp lại một lỗi rất phổ biến đó là lưu trữ dữ liệu nhạy cảm không đúng nơi đúng chỗ - lưu dữ liệu ngay tại UserDefault.
UserDefault là một nơi cực kì tiện mà các lập trình viên iOS thường sử dụng để lưu dữ liệu, tuy nhiên thực chất nó chỉ là một tệp chứa danh sách các thuộc tính dưới dạng key-value được đặt trong thư mục Preferences của ứng dụng. Và đương nhiên là nó không được mã hóa dưới bất kì hình thức nào.
Về cơ bản bằng cách sử dụng một vài ứng dụng bất kì của bên thứ 3 là ta có thể dễ dàng xem được dữ liệu bên trong UserDefault của bất kì ứng dụng nào tải về từ Apple Store mà không cần phải JailBreak thiết bị - một ví dụ đó là ứng dụng iMazing
Lý do tôi viết bài này vì nhận ra rằng có rất nhiều ứng dụng đã và đang mắc phải sai lầm này, một số dữ liệu nhạy cảm được lưu trong UserDefault có thể bao gồm: access token, renewable subscription flag, số coin/tiền trong tài khoản (app/game), trạng thái đã mua một số vật phẩm (yes/no),...
Tất cả các dữ liệu này có thể bị truy cập, bị thay đổi bất cứ lúc nào và gây ra thiệt hại cho ứng dụng, bằng việc sử dụng một cách miễn phí các tính năng được trả phí hoặc nhiều hơn thế nữa.

Giải pháp:

Chúng ta phải luôn ghi nhớ rằng UserDefault được thiết kế chỉ để lưu một vài dữ liệu đơn giản vô thưởng vô phạt kiểu như: có nên hiển thị màn tutorial không hay sở thích của người dùng là gì, nên hiển thị màn hình nào lần đầu khởi động app,..
Để lưu trữ các dữ liệu nhạy cảm, đòi hỏi tính bảo mật cao chúng ta nên sử dụng các phương thức mà Apple recommend mà KeyChain là một ví dụ. KeyChain giúp chúng ta giải quyết vấn đề này bằng cách cung cấp cho ta một cách để lưu trữ một lượng dữ liệu nhỏ trong cơ sở dữ liệu được mã hóa của hệ thống được gọi là keychain.

Keychain Services API

Dưới đây là cách mà chúng ta lưu mật khẩu của người dùng trong Keychain:
class KeychainService {
    func save(_ password: String, for account: String) {
        let password = password.data(using: String.Encoding.utf8)!
        let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                    kSecAttrAccount as String: account,
                                    kSecValueData as String: password]
        let status = SecItemAdd(query as CFDictionary, nil)
        guard status == errSecSuccess else { return print("save error")
    }
}
Với keyword kSecClassGenericPassword hệ thống sẽ hiểu rằng đây là dữ liệu kiểu mật khẩu và tiến hành mã hóa. Sau đó chúng ta sử dụng SecItemAdd() để lưu dữ liệu vào Keychain với query đã tạo ở bước trước đó.
Cách lấy mật khẩu ra rất đơn giản:
func retrivePassword(for account: String) -> String? {
    let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                kSecAttrAccount as String: account,
                                kSecMatchLimit as String: kSecMatchLimitOne,
                                kSecReturnData as String: kCFBooleanTrue]
    
    
    var retrivedData: AnyObject? = nil
    let _ = SecItemCopyMatching(query as CFDictionary, &retrivedData)
    
    
    guard let data = retrivedData as? Data else {return nil}
    return String(data: data, encoding: String.Encoding.utf8)
}
Chúng ta có thể viết một đoạn code test nho nhỏ để kiểm tra việc lưu vào và lấy ra có đúng như ý muốn hay không như sau:
func testPaswordRetrive() {
    let  password = "123456"
    let account = "User"
    keyChainService.save(password, for: account)
    XCTAssertEqual(keyChainService.retrivePassword(for: account), password)
}
Nếu bạn thấy việc sử dụng Keychain hơi phức tạp thì có rất nhiều mã nguồn mở trên Gihub sẵn sáng giúp bạn giải quyết vấn đề này, trong đó có SAMKeychainSwiftKeychainWrapper

Sai lầm thứ hai: Lưu mật khẩu và xác thực

Một sai lầm nữa thường được lặp đi lặp lại đó là việc lập tình viên lưu lại mật khẩu trong app dưới dạng mật khẩu gốc hoặc tiến hành request API bằng username và mật khẩu gốc.
Nếu bạn đang lưu mật khẩu bằng Keychain tuy có tốt hơn việc lưu trong UserDefault một chút nhưng lời khuyên là luôn phải lưu chúng lại dưới dãng đã được mã hóa bước đầu.
Giả sử cho dù hacker có hack thành công vào Keychain hoặc bắt được gói tin chúng ta gửi đi khi login thì dữ liệu mà chúng nhận được cũng là dữ liệu đã bị mã hóa.
Một cách đơn giản cho việc mã hóa bước đầu dữ liệu là tiến hành lưu chúng lại dưới dạng dữ liệu băm (hash).

Mã hóa dữ liệu nhạy cảm:

Chúng ta có thể sử dụng một mã nguồn mở có tên CryptoSwift để băm các dữ liệu mà chúng ta cho là nhạy cảm cần được mã hóa. Cách sử dụng rất là đơn giản:
func saveEncryptedPassword(_ password: String, for account: String) {
    let salt = Array("salty".utf8)
    let key = try! HKDF(password: Array(password.utf8), salt: salt, variant: .sha256).calculate().toHexString()
    keychainService.save(key, for: account)
}
Phương thức trên nhận chuỗi username và password và lưu chúng vào keychain dưới dạng một chuỗi mã hash thay vì lưu trực tiếp dữ liệu thô. Có thể hiểu như sau: salt: là một chuỗi duy nhất dùng để trộn vào chuỗi password .sha256 là phương thức băm HKDFhàm dẫn xuất dựa trên hash-based message authentication code
salt đơn giản là để hacker gặp khó khăn hơn trong việc giải mã dữ liệu. Vì nếu không có salt, với một máy tính cấu hình mạnh và một bộ dữ liệu lớn chứa các mật khẩu thường dùng, hacker có thể ngược từ mã hash ra mật khẩu.
Bây giờ chúng ta có thể tiến hành request lên server với key đã được mã hóa như sau:
authManager.login(key, user)
Tất nhiên là phía client và server phải dùng cùng một mã salt và phương thức băm để có thể giải mã ra dữ liệu đồng nhất.
Bằng cách áp dụng phương pháp này, chúng ta đã đưa ứng dụng của mình lên một tầm cao mới của sự bảo mật, tuy không thể đảm bảo 100% là ứng dụng sẽ không thể bị hack, nhưng cũng đã làm cho việc hack ứng dụng khó khăn hơn rất nhiều.

Kết

Việc triển khai bảo mật cho ứng dụng là việc không thể bỏ qua, nhất là với lập trình viên, những người luôn đề cao đạo đức nghề nghiệp kèm theo lời thề hypocrat, à quên, hypocrat là lời thề của bác sĩ, trình viên làm gì có lời thề gì, với cả bác sĩ họ có lời thề hẳn hoi mà còn ... nữa là chúng ta =))
Trong khuôn khổ hai ngày lương, à lại nhầm, trong khuôn khổ của bài viết này, tôi đã đề cập một cách ngắn gọn về hai vấn đề bảo mật mà các lập trình viên iOS hay mắc phải và các giải pháp cho vấn đề này. Hy vọng đã phần nào giúp các bạn có cái nhìn thẳng thắn hơn trong việc giữ gìn tính bảo mật cho thông tin user.
Nếu các bạn có bất kỳ ý kiến/ comment/ góp ý nào đừng ngại để lại comment/like bên dưới bài viết, đây là niềm động lực to lớn cho những người viết bài chúng tôi, khi mà áp lực hai ngày lương luôn đè nặng trong tâm trí đôi khi khiến các bài viết mất đi cái tâm và sự chỉnh chu - thứ mà một bài viết không thể thiếu.
Nói thêm một chút, trong bài viết trước (là bài này) hình như tôi có đề cập đến việc sẽ có một bài viết nói thêm về việc áp dụng tính năng thử nghiệm trên một nhóm user nhất định, hãy để lại ý kiến của mình để tôi biết liệu bạn có quan tâm đến vấn đề này hay không cũng như để tăng tương tác.
Thân ái và chúc các bạn có một ngày vui vẻ.

Comments

Popular posts from this blog

Swift Tool Belt, Part 1: Adding a Border, Corner Radius, and Shadow to a UIView with Interface Builder

During my iOS work, I’ve assembled a set of code that I bring with me on every iOS project. I’m not talking about large frameworks or CocoaPods here. These are smaller Swift extensions or control overrides that are applicable to many projects. I think of them as my tool belt. In this post, I’ll show you an extension that will add a border, a corner radius, and a shadow to any UIView, UIButton, or UILabel and allow you to preview what it will look like in Interface Builder. Back in 2014, I wrote a blog post on Expanding User-Defined Runtime Attributes in Xcode where I added a border, corner radius, and shadow to a UIView using Interface Builder’s user-defined runtime attributes. This solution had no type checking—you had to type the property you wanted to modify by hand and often had to look up what it was called. You also had to run your project in order to see the effect of the runtime attribute. Starting with Xcode 6 , there is a new mech...

Alamofire vs URLSession

Alamofire vs URLSession: a comparison for networking in Swift Alamofire and URLSession both help you to make network requests in Swift. The URLSession API is part of the foundation framework, whereas Alamofire needs to be added as an external dependency. Many  developers  doubt  whether it’s needed to include an extra dependency on something basic like networking in Swift. In the end, it’s perfectly doable to implement a networking layer with the great URLSession API’s which are available nowadays. This blog post is here to compare both frameworks and to find out when to add Alamofire as an external dependency. Build better iOS apps faster Looking for a great mobile CI/CD solution that has tons of iOS-specific tools, smooth code signing, and even real device testing? Learn more about Bitrise’s iOS specific solutions! This shows the real power of Alamofire as the framework makes a lot of things easier. What is Alamofire? Where URLSession...

Frame vs Bounds in iOS

This article is a repost of an answer I wrote on Stack Overflow . Short description frame = a view’s location and size using the parent view’s coordinate system ( important for placing the view in the parent) bounds = a view’s location and size using its own coordinate system (important for placing the view’s content or subviews within itself) Details To help me remember frame , I think of a picture frame on a wall . The picture frame is like the border of a view. I can hang the picture anywhere I want on the wall. In the same way, I can put a view anywhere I want inside a parent view (also called a superview). The parent view is like the wall. The origin of the coordinate system in iOS is the top left. We can put our view at the origin of the superview by setting the view frame’s x-y coordinates to (0, 0), which is like hanging our picture in the very top left corner of the wall. To move it right, increase x, to move it down increase y. To help me remember bound...