반응형

[마지막 수정일 : 2018.03.09]

원문 : https://www.raywenderlich.com/185370/basic-ios-security-keychain-hashing

기본 iOS 보안: 키체인과 해싱 (Basic iOS Security: Keychain and Hashing)


소프트웨어 개발의 가장 중요한것 중 하나인 앱 보안(application security)은 또한 가장 신비롭고 무서운것(따라서 전염병 처럼 피하게 됩니다) 중의 하나로 간주됩니다. 사용자는 앱이 올바르게 실행되며, 정보를 비공개로 유지하고, 잠재적인 위협으로부터 그 정보를 보호하길 기대합니다.

이 튜토리얼에서, 여러분은 iOS 보안의 기본 사항에 대해서 배우게 될 것입니다. 여러분은 몇가지 기본 암호화 해싱 메소드로 키체인(keychain - 사용자의 개인 정보를 비공개하고 보호됩니다)에 사용자가 입력한 것을 안전하게 저장하는 작업을 할 것입니다.

애플은 여러분의 앱을 안전하게 유지하는데 도움을 주는 몇가지 API를 가지고 있고, 키체인으로 작업 하는 동안 알아볼 것입니다. 또한 CryptoSwift(암호화 알고리즘을 구현한 검증된 오픈소스 라이브러리)를 사용할 것입니다.

시작하기(Getting Started)

자료 다운로드 해서 시작 프로텍트를 사용합니다. (원문에서는 튜토리얼의 위쪽과 아랫쪽에 있는 자료 다운로드 버튼 사용)

샘플 앱으로 사용자는 로그인하고 친구들의 사진을 볼수 있습니다. 앱의 대부분은 이미 작업되어 있습니다: 여러분이 할일은 앱을 보호하는 것입니다.

다운로드한 자료를 압축해제하고, CocoaPod 의존성을 모두 포함하기 위해 Friendvatars.xcworkspace를 열어 줍니다. 빌드하고 실행하여 로그인 화면이 열리는지 확인합니다.


지금은, 로그인을 탭하면 아무런 반응이 없습니다. 이는 사용자의 신원을 저장하는 방법이 없기 때문입니다. 여러분이 그것을 가장 먼저 추가할 것입니다.

보안이 중요한 이유(Why Security is important)

코드를 살펴보기 전에, 여러분의 앱에서 보안이 왜 필요한지를 이해해야 합니다. 이메일, 비밀번호, 은행 계정 정보와 같은 개인 사용자 데이터를 저장하는 앱에서 보안은 특히 중요합니다.


애플은 왜 보안을 중요하게 여길까요? 여러분이 찍은 사진 부터, 하루동안 걸은 걸음 수 까지, 아이폰에 개인 데이터를 많이 저장합니다. 이러한 데이터를 안전하게 유지하는 것은 매우 중요합니다.

iOS 생태계를 공격하는 사람은 누구이고, 무엇을 원할까요? 공격자는 아마도 범죄자, 사업 경쟁자, 심지어는 친구나 친척 일것입니다. 모든 공격자가 같은 것을 원하는 것은 아닙니다. 일부는 피해를 입히거나 정보를 손상시키는 것을 원하고, 다른 누군가는 생일날 어떤 선물을 받고 싶은지 알고 싶을 수 있습니다.

앱에서 데이터를 잠재적인 위험에 대비해서 보호하는것이 여러분의 일입니다. 다행스럽게도, 애플은 이러한 작업을 간단히 처리할 수 있는 강력한 API를 많이 내장하고 있습니다.

애플의 키체인(Apple’s Keychain)

애플 개발자들에게 가장 중요한 요소중 하나는 메타 데이터 및 민감한 정보를 저장하는 특별한 데이터베이스인 iOS 키체인(iOS Keychain)입니다. 키체인(Keychain)을 사용하는 것은 앱에서 비밀번호 같은 작은 데이터를 저장하는 가장 좋은 방법입니다.

왜 더 간단한 방법보다 키체인(Keychain)을 사용하나요? 사용자의 비밀번호를 UserDefaults에 base-64 인코딩해서 저장하는 것으로 충분하니 않나요? 절대 아닙니다! 그런식으로 저장된 비밀번호를 알아내는 것은 공격자에게 사소한 일입니다. 보안은 어렵고, 자신만의 해결책을 사용하는 것은 좋은 생각이 아닙니다. 여러분의 앱이 금융기관용 앱이 아니더라도 개인 정보를 저장하는 것은 가볍게 처리해서는 안됩니다.


특히나 Swift로 키체인으로 직접 작업하는 것은 복잡합니다. 대부분 C언어로 작성된 보안 프레임워크를 사용해야 합니다.

다행스럽게도, Apple의 샘플 코드인 GenericKeychain에서 Swift로 감싸놓아 이러한 로우레벨(low level)의 API를 사용하지 않아도됩니다. KeychainPasswordItem은 Keychain을 Swift에서 사용하기 쉬운 인터페이스를 제공하고, 시작 프로젝트에 이미 포함되어 있습니다.

이제 코드를 볼 시간입니다.

키체인 사용하기(Using the Keychain)

AuthViewController.swift을 열어 주세요. 이 뷰컨트롤러는 여러분이 처음 봤던 로그인 화면을 담당합니다. Actions 섹션으로 스크롤을 아래로 내려면, signInButtonPressed가 아무것도 하지않는 것을 알 수 있을 것입니다. 그것을 바꿀 시간입니다. Helpers 섹션의 아래에 다음을 추가하세요.

private func signIn() {
  // 1
  view.endEditing(true)
  
  // 2
  guard let email = emailField.text, email.count > 0 else {
    return
  }
  guard let password = passwordField.text, password.count > 0 else {
    return
  }
  
  // 3
  let name = UIDevice.current.name
  let user = User(name: name, email: email)
}

다음은 어떤 일을 하는지에 대한 설명입니다.

  1. 사용자가 어떤 동작을 했는지 확인하기 위해 키보드를 닫습니다.
  2. 사용자가 입력한 이메일과 비밀번호를 사용합니다. 둘 중 하나라도 길이가 0이면, 계속 진행하지 않습니다. 실제 앱에서는 여기에서 사용자에게 오류를 보여줘야 합니다.
  3. 사용자 이름을 지정합니다. 이 튜토리얼에서는 디바이스의 이름을 가져와서 사용합니다.

주의 
여러분의 Mac(시뮬레이터에 의해 사용되는)의 이름을 변경할 수 있습니다. 시스템 설정(System Preferences) -> 공유(Sharing)로 이동하여 맨 위에 있는 컴퓨터의 이름을 변경합니다. 추가적으로 아이폰의 이름을 변경 할 수 있습니다. 설정(Settings) -> 일반(General) -> 정보(About) -> 이름(Name)

이제 signInButtonPressed에 다음을 추가합니다.

signIn()

signInButtonPressed가 눌러졌을때 signIn 메소드를 호출합니다.

textFieldShouldReturn을 찾고 아래 case TextFieldIdTag.password.rawValue아래에 있는 break문을 다음으로 교체합니다.

signIn()

이제 비밀번호 필드에 포커스가 있고 텍스트가 입력되어 있는 동안에 사용자가 키보드의 return을 탭할때 signIn()이 호출됩니다.

signIn()은 아직 완성되지 않았습니다. 아직 사용자 객체와 비밀번호 저장이 필요합니다. 여러분은 이것을 처리하는 클래스를 구현할 것입니다.

앱에서 인증 관련된 로직을 처리하는 정적(static) 클래스인 AuthController.swift을 열어 주세요.

먼저, 파일의 맨위에 있는 isSignedIn위에 다음을 추가합니다.

static let serviceName = "FriendvatarsService"

키체인(Keychain)에서 앱의 데이터를 식별하는데 사용되는 서비스 이름을 정의하였습니다. 이 상수를 사용하려면, 다음과 같이 클래스의 끝에 signIn메소드를 생성하세요.

class func signIn(_ user: User, password: String) throws {
  try KeychainPasswordItem(service: serviceName, account: user.email).savePassword(password)
  
  Settings.currentUser = user
}

이 메소드는 사용자의 로그인 정보를 키체인(Keychain)에 안전하게 저장합니다. 고유한 식별자(account)로 정의된 서비스 이름으로 KeychainPasswordItem을 만듭니다.

이 앱의 경우에, 사용자의 이메일은 키체인(Keychain)의 식별자로 사용되지만, 다른 예제에서는 사용자 ID나 사용자 이름을 고유한 식별자로 사용할 수 있습니다. 마지막으로, Settings.currentUser을 user로 설정합니다. - 이것은 UserDefaults에 저장됩니다.

이 메소드가 완벽한 것으로 간주해서는 안됩니다(should not). 사용자의 비밀번호를 직접 저장하는 것은 가장 좋은 방법이 아닙니다. 예를 들어, 공격자가 애플의 키체인(Keychain)에 침투하게 되면, 사용자의 비밀번호를 읽을수 있습니다. 더 나은 방법은 사용자의 식별자로 만든 해시를 저장하는 것입니다.

AuthController.swift의 맨 위에 Foundation 가져온 아래에 다음을 추가 합니다.

import CryptoSwift

CryptoSwift는 Swift로 작성된 많은 표준 암호화 알고리즘에서 가장 인기있는 것들 중 하나 입니다. 암호화는 어렵고 올바르게 사용하는게 필요합니다. 인기있는 암호화 라이브러리를 사용하는 것은 여러분이 직접 표준 해싱 함수를 구현할 필요가 없다는 것을 의미합니다. 최고의 암호화는 검토를 위해 공개되어 있습니다.

주의
애플의 CommonCrypto 프래임워크는 만은 유용한 해싱 함수를 제공하지만, Swift로 작업하기가 쉽지 않습니다. 이것이 이 튜토리얼에서 CryptoSwift 라이브러리를 선택한 이유 입니다.

다음으로 signIn 위에 다음을 추가합니다.

class func passwordHash(from email: String, password: String) -> String {
  let salt = "x4vV8bGgqqmQwgCoyXFQj+(o.nUNQhVP7ND"
  return "\(password).\(email).\(salt)".sha256()
}

이 메소드는 이메일과 비밀번호를 가져와서 해쉬된 문자열을 반환합니다. salt는 공통 비밀번호를 만드는데 사용하는 고유한 문자열 이며, 흔치 않습니다. sha256()는 입력한 문자열을 SHA-2 해시 타입으로 만들어주는 CryptoSwift 메소드입니다.

이전 예제에서, 공격자가 망가진 키체인(Keychain)에서 해시를 찾을 수 있습니다. 공격자는 공통으로 사용되는 비밀번호 테이블을 만들고 그 해시들과 비교할 수 있습니다. 만약 사용자의 입력을 salting하지 않고 해시된 경우에, 공격자의 해시 테이블에 비밀번호가 존재하게 되며, 그 비밀번호는 위험하게 됩니다.

salt를 결합시키면 공격의 복잡성이 커집니다. 게다가 사용자의 이메일과 비밀번호를 salt와 결합하여 해시를 만들면 쉽게 해킹당하지 않을 것입니다.

주의 
서버 백앤드 인증의 경우, 앱과 서버는 동일한 salt를 공유합니다. 같은 방법으로 해시를 만들고 식별을 위해 두 해시를 비교합니다.

signIn(_:password:)로 돌아가서, savePassword 호출하는 줄을 다음으로 교체합니다.

let finalHash = passwordHash(from: user.email, password: password)
try KeychainPasswordItem(service: serviceName, account: user.email).savePassword(finalHash)

signIn은 이제 원래 비밀번호보다 더 강력한 해시를 저장합니다. 이제 뷰컨트롤러에 추가할 시간입니다.

AuthViewController.swift로 돌아가서 signIn()의 아래에 다음을 추가하세요.

do {
  try AuthController.signIn(user, password: password)
} catch {
  print("Error signing in: \(error.localizedDescription)")
}

이렇게 하면 사용자와 해시된 비밀번호를 저장할 것이며 앱에 로그인 되는데 좀 더 시간이 걸릴 것입니다. AppController.swift는 인증이 변경될때 알 수 있는 방법이 필요합니다.

AuthController.swift에 정적 변수 isSignedIn가 있는 것을 알고 있을지도 모릅니다. 현재, 사용자가 로그인 하더라도 항상 false를 반환합니다.

AuthController.swift에서, isSignedIn을 다음으로 업데이트 합니다.

static var isSignedIn: Bool {
  // 1
  guard let currentUser = Settings.currentUser else {
    return false
  }
  
  do {
    // 2
    let password = try KeychainPasswordItem(service: serviceName, account: currentUser.email).readPassword()
    return password.count > 0
  } catch {
    return false
  }
}

다음은 어떤 일을 하는지에 대한 설명입니다.

  1. 곧, 여러분은 UserDefaults에 저장된 현재 사용자를 확인합니다. 만약 사용자가 없으면, 키체인(Keychain)에 있는 비밀번호 해시를 찾을 수 있는 식별자가 없는 것이며, 따라서 로그인 되지 않았다는 것을 나타냅니다.
  2. 키체인(Keychain)에 있는 비밀번호 해시를 읽어서, 비밀번호가 있고 공백이 없는 경우에, 사용자는 로그인 된 것으로 간주합니다.

이제 AppController.swift에 있는 handleAuthState가 올바르게 동작하지만, 로그인 한 후에 UI를 제대로 업데이트하기 위해서는 앱을 다시 실행해야 합니다. 대신에, 인증과 같은 상태가 변경된 것을 앱에게 알려주는 좋은 방법은 알림(notifications)을 사용하는 것입니다.

AuthController.swift 아래에 다음을 추가하세요.

extension Notification.Name {
  
  static let loginStatusChanged = Notification.Name("com.razeware.auth.changed")
  
}

사용자 정의 알림을 만들때 일반적으로 앱의 번들 식별자로 사용하는 역방향 도메인 식별자를 사용하는 것이 좋습니다. 고유한 식별자를 사용하면 디버깅 할 때 도움이 될수 있어서 로그(logs)에서 언급된 다른 프레임워크로부터 알림이 눈에 띄게 됩니다.

사용자정의 알림 이름을 사용하기 위해, signIn(_:password:)의 아래에 다음을 추가합니다.

NotificationCenter.default.post(name: .loginStatusChanged, object: nil)

앱의 다른 부분에서 감지할 수 있는 알림을 보냅니다.

AppController.swift 에서, show(in:)위에 init메소드를 추가합니다.

init() {
  NotificationCenter.default.addObserver(
    self,
    selector: #selector(handleAuthState),
    name: .loginStatusChanged,
    object: nil
  )
}

이렇게 하면 AppController가 로그인 알림 옵져버로 등록됩니다. 알림이 발생할때 handleAuthState가 호출될 것입니다.

빌드하고 실행합니다. 이메일과 비밀번호 조합하여 로그인 한 후, 친구 목록이 표시됩니다.


여러분은 친구들의 이름만 있으며, 아바타가 아무것도 없다는 것을 알게 될것 입니다. 보기에 매우 유쾌하지 않습니다. 아마도 로그아웃과 미완성된 앱에 대해 잊어버릴지 모릅니다. 오, 로그아웃 버튼이 동작하지 않습니다. 별 1개의 리뷰를 남기고 개발자에게 줄 시간입니다.


로그인 기능은 훌륭하지만, 앱에서 로그아웃 할수 있는 방법은 없습니다. 인증 상태 변경 신호를 보내는 알림이 있기에, 실제로 이것을 하기에는 꽤 쉽습니다.

AuthController.swift로 돌아가서, signIn(_:password:) 아래에 다음 메소드를 추가합니다.

class func signOut() throws {
  // 1
  guard let currentUser = Settings.currentUser else {
    return
  }
  
  // 2
  try KeychainPasswordItem(service: serviceName, account: currentUser.email).deleteItem()
  
  // 3
  Settings.currentUser = nil
  NotificationCenter.default.post(name: .loginStatusChanged, object: nil)
}

이것은 매우 간단하지만, 다음은 상세 설명입니다.

  1. 현재 사용자를 저장했는지 확인하고, 그렇지 않으면 빨리 벗어납니다.
  2. 키체인(Keychain)에서 비밀번호를 삭제합니다.
  3. 사용자 객체를 정리하고 알림을 보냅니다.

이것을 연결하려면, FriendsViewController.swift로 건너뛰고 signOut에 다음을 추가합니다.

try? AuthController.signOut()

로그아웃 버튼을 누를때, 로그인 한 사용자의 데이터를 지우기위해 새 메소드가 호출됩니다.

앱의 오류를 처리하는 것이 좋지만, 이 튜토리얼에서는 오류를 무시할 것입니다.

빌드하고 실행합니다. 그리고 나서 Sign Out버튼을 누릅니다.


이제 여러분은 앱에서 인증 작업을 하는 완벽하는 예제를 가졌습니다.

해시(Hashing)

여러분은 인증 설정하는 멋진 일을 했습니다. 그러나 재미있는 것은 아직 끝나지 않았습니다. 이제 친구의 뷰 안에 이름 앞에 빈 공간을 표시할 것입니다.

FriendsViewController.swift에, 사용자 모델 객체의 목록이 있습니다. 뷰에 각 사용자에 대한 아바타 이미지를 보여주려 합니다. User에는 name과 email 두가지 속성만 있기에, 이미지를 어떻게 보여줘야 할까요?

이메일 주소를 가져와 아바타 이미지와 연결 시켜주는 서비스가 있는 것으로 알고 있습니다. 
Gravatar! 이전에 Gravatar에 대해서 들어보지 못했다면, 그것은 일반적으로 전세계적으로 블로그와 포럼에서 이메일 주소와 아바타를 연결시키기 위해 사용됩니다. 이것은 간단해서 사용자는 자신이 가입한 모든 포럼이나 사이트에 새로운 아바타를 업로드할 필요가 없습니다.

각 사용자들은 자신의 이메일에 이미 연결된 아바타를 가지고 있습니다. 따라서 해야할 일은 Gravatar에 요청하고 이미지를 얻어오는 것입니다. 그렇게 하기 위해, 요청 URL을 만들기 위해 이메일의 MD5 해시를 생성할 것입니다.

Gravatar의 사이트에 있는 문서를 보면, 요청을 하기 위해 해시된 이메일 주소가 필요하다는 것을 알 수 있습니다. 이것은 CryptoSwift를 활용할수 있기 때문에 쉬운일이 될것 입니다. tableView(_:cellForRowAt:)에 Gavatar에 관련된 주석이 있는 곳에 다음을 추가하세요.

// 1
let emailHash = user.email.trimmingCharacters(in: .whitespacesAndNewlines)
                          .lowercased()
                          .md5()

// 2
if let url = URL(string: "https://www.gravatar.com/avatar/" + emailHash) {
  URLSession.shared.dataTask(with: url) { data, response, error in
    guard let data = data, let image = UIImage(data: data) else {
      return
    }
    
    // 3
    self.imageCache.setObject(image, forKey: user.email as NSString)
    
    DispatchQueue.main.async {
      // 4
      self.tableView.reloadRows(at: [indexPath], with: .automatic)
    }
  }.resume()
}

다음은 상세 설명입니다.

  1. 먼저 Gravator의 문서에 따라 전자메일을 표준화 하고나서 MD5해시를 생성합니다.
  2. Gravator URL과 URLSession을 구성합니다. 반환된 데이터로부터 UIImage를 불러옵니다.
  3. 이미지를 캐시하여 이메일 주소를 반복적으로 가져오지는 것을 방지합니다.
  4. 테이블 뷰의 행(row)을 다시 로드하면 아바타 이미지가 표시됩니다.

빌드하고 실행하세요. 이제 여러분은 친구들의 아바타 이미지와 이름을 볼수 있습니다.


주의
여러분의 이메일이 파란색 G에 기본 흰색을 반환하면, Gravatar 사이트에 가서 여러분의 아바타를 업로드하고 친구들을 가입시키세요.

여기에서 어디로 가야 하나요?(Where to Go From Here?)

이제 여러분은 기본 iOS 보안과 인증을 처리하는 완벽한 앱을 가지게 되었고, Gravatar가 제공하는 아바타를 볼수 있습니다. 보안의 중요성과, iOS 키체인, 일반 텍스트 값 대신에 해시를 저장하는 것 같은 좋은 사례를 배웠습니다. 여러분이 즐거운 시간이 되었기를 희망합니다.

앱 보안에 대해 다른 방법에 관심이 있으면, 애플 제품에서 생체 인식 센서를 사용하는 방법을 배우세요

프레임 워크에 대해서 더 자세히 보길 원한다면, 애플 보안 프레임워크(Apple’s Security framework)에 관해 읽을 수 있습니다.

마지막으로, CryptoSwift가 제공하는 더 많은 알고리즘을 살펴보세요.

반응형
Posted by 까칠코더
,