📱Mobile/🔥Swift

[Swift] SwiftUI KeyChain Service 예제

후누스 토르발즈 2021. 10. 30. 02:04
반응형

 

 

 

KeyChain Services

키체인 서비스란?

  1. 사용자를 대신하여 소량의 데이터를 안전하게 저장할 수 있습니다.
  2. 대부분의 사람들은 수많은 온라인 계정을 관리하고, 일반적으로 여러 계정에 걸쳐 간단한 암호를 재활용하기에 안전하지 않습니다. 이에 대응하여 키체인 서비스 API는 앱에 키체인이라는 암호화된 데이터베이스사용자 데이터의 작은 비트를 저장하는 메커니즘을 제공하여 이 문제를 해결하는데 도움이 됩니다. 비밀번호가 안전하게 기억되면 사용자가 복잡한 비밀번호를 자유롭게 선택할 수 있습니다.
  3. 비밀번호에 국한되지 않고 인증서, 키 신뢰 서비스로 관리하는 암호화 키인증서도 보관이 가능하며 이것들를 통해 사용자는 보안 통신에 참여하고 다른 사용자 및 장치와 신뢰를 구축할 수 있습니다.

 

 

API Components

Keychain Items

 키체인에 저장하는 아이템에 기밀 정보를 포함합니다

암호나 암호화 키와 같은 비밀을 저장하려면 키체인 아이템을 패키징합니다. 데이터 자체와 함께 아이템의 액세스 가능성을 제어하고 검색 가능하게 만들기 위해 공개적으로 표시되는 속성 집합을 제공합니다. 키체인 서비스는 디스크에 저장된 암호화된 데이터베이스인 키체인에서 데이터 암호화 및 저장(데이터 속성 포함)을 처리합니다. 나중에 승인된 프로세스는 키체인 서비스를 이용하여 아이템을 찾고 데이터를 해독합니다.

 

 

 

 

Item Class Keys and Values

 키체인 아이템은 암호, 암호화 키 및 인증서와 같이 보유하는 데이터의 종류에 따라 다양한 클래스로 분류됩니다.

  • Item Class Keys
    • kSecClass: CFString 값이 아이템의 클래스인 딕셔너리 키
  • Item Class Values: CFString
    • kSecClassGenericPassword: 일반 암호 아이템을 나타내는 값
    • kSecClassInternetPassword: 인터넷 비밀번호 아이템을 나타내는 값
    • kSecClassCertificate: 인증서 아이템을 나타내는 값
    • kSecClassKey: 암호화 키 아이템을 나타내는 값
    • kSecClassIdentity: ID 아이템을 나타내는 값

 

Password Attribute Keys

  • kSecAttrService: 값이 아이템의 서비스를 나타내는 문자열인 키
    • Bundle.main.bundleIdentifier: 수신기의 번들 식별자 (Playground에서 실행시에는 얻을 수 없다.)
  • kSecAttrAccount: 값이 아이템의 계정 이름을 나타내는 문자열인 키 (아이디)
    • kSecAttrGeneric: 값이 아이템의 사용자 정의 속성을 나타내는 키 (패스워드)
  • kSecMatchLimit: 값이 일치 제한을 나타내는 키
    • kSecMatchLimitOne: 한 아이템과 정확히 일치하는 값
    • kSecMatchLimitAll: 아이템 수에 제한이 없는 일치에 해당하는 값입니다.
  • kSecReturnAttributes: 아이템 특성을 반환할지 여부를 나타내는 부울 키
  • kSecReturnData: 값이 아이템 데이터를 반환할지 여부를 나타내는 부울인 키

 

Adding Keychain Items

  • SecItemAdd(_:_:): 하나 이상의 아이템을 키 체인에 추가

Keychain Item Search

  • SecItemCopyMatching(_:_:): 검색 쿼리와 일치하는 하나 이상의 키 체인 아이템을 반환하거나 특정 키체인 아이템의 특성을 복사

Keychain Item Modification

  • SecItemUpdate(_:_:): 검색 쿼리와 일치하는 아이템을 수정
  • SecItemDelete(_:): 검색 쿼리와 일치하는 아이템을 삭제

 

 

 

*CF(Core Foundation: Foundation 프레임워크와 원활하게 연결된 저수준 함수, 기본 데이터 유형 및 다양한 컬렉션 유형에 액세스합니다.)

  • CFTypeRef: Core Foundation 개체에 대한 입력되지 않은 "일반" 참조, 모든 "CF 개체"의 기본 "유형" 및 이에 대한 다형성 기능

  • CFString: 효율적인 문자열 조작 및 문자열 변환 기능 모음을 제공. 원활한 유니코드 지원을 제공하고 Cocoa와 C 기반 프로그램 간의 데이터 공유를 용이하게 함. CFString 객체는 변경할 수 없음.
  • CFDictionary: 딕셔너리를 처음 생성할 때 키-값 쌍을 설정하고 나중에 수정할 수 없는 정적 딕셔너리를 생성
  • CFData: 및 파생된 변경 가능한 유형인 CFMutableData 는 데이터 개체, 바이트 버퍼용 개체 지향 래퍼에 대한 지원을 제공합니다.

 

Security Framework Result Codes

  • errSecSuccess: OSStatus  에러 없음

 

 

 

 

User.swift

class User: Encodable, Decodable{
    var userId: String
    var password: String
    
    init(userId:String, password:String) {
        self.userId = userId
        self.password = password
    }
}

 

 

StorageManager.swift

import Foundation
import Security

final class StorageManager {
  // MARK: Shared instance
  static let shared = StorageManager()
  private init() { }

  // MARK: Keychain
  private let service = Bundle.main.bundleIdentifier
  private let account = "Service"

  private lazy var query: [CFString: Any]? = {
    guard let service = self.service else { return nil }
    return [kSecClass: kSecClassGenericPassword,
            kSecAttrService: service,
            kSecAttrAccount: account]
  }()

  func createUser(_ user: User) -> Bool {
    guard let data = try? JSONEncoder().encode(user),
    let service = self.service else { return false }
    let query: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
                                  kSecAttrService: service,
                                  kSecAttrAccount: account,
                                  kSecAttrGeneric: data]

    return SecItemAdd(query as CFDictionary, nil) == errSecSuccess
  }

  func readUser() -> User? {
    guard let service = self.service else { return nil }
    let query: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
                                  kSecAttrService: service,
                                  kSecAttrAccount: account,
                                  kSecMatchLimit: kSecMatchLimitOne,
                                  kSecReturnAttributes: true,
                                  kSecReturnData: true]

    var item: CFTypeRef?
    if SecItemCopyMatching(query as CFDictionary, &item) != errSecSuccess { return nil }

    guard let existingItem = item as? [String: Any],
        let data = existingItem[kSecAttrGeneric as String] as? Data,
        let user = try? JSONDecoder().decode(User.self, from: data) else { return nil }

    return user
  }

  func updateUser(_ user: User) -> Bool {
    guard let query = self.query,
      let data = try? JSONEncoder().encode(user) else { return false }
    
    let attributes: [CFString: Any] = [kSecAttrAccount: account,
                                     kSecAttrGeneric: data]

    return SecItemUpdate(query as CFDictionary, attributes as CFDictionary) == errSecSuccess
  }

  func deleteUser() -> Bool {
    guard let query = self.query else { return false }
    return SecItemDelete(query as CFDictionary) == errSecSuccess
  }
}

 

 

ContentView.swift

import SwiftUI
import CryptoKit

struct FieldStyle: ViewModifier {
    let lightGreyColor = Color(red: 240.0/255.0, green: 240.0/255.0, blue: 240.0/255.0, opacity: 1.0)
    func body(content: Content) -> some View {
        return content
            .padding()
            .background(lightGreyColor)
            .cornerRadius(5.0)
            .padding(.bottom, 5)
            .disableAutocorrection(true)
    }
}

struct ContentView: View {
    @State private var userId: String = ""
    @State private var password: String = ""    // password_1234
    @State private var isLogin: Bool = false
    @State private var showingAlert: Bool = false
    
    var body: some View {
        NavigationView{
            VStack{
                Text("WellCome!")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .padding(.bottom, 40)
                TextField("Userid", text: $userId)
                    .modifier(FieldStyle())
                    .autocapitalization(.none)
                SecureField("Password", text: $password){
                    login(userId, password)
                }
                    .modifier(FieldStyle())
                NavigationLink(destination: MainView(userId: self.$userId, isLogin: self.$isLogin), isActive: $isLogin){
                    Button(action: {
                        login(userId, password)
                    }
                    ){
                        Text("LOGIN")
                            .font(.headline)
                            .foregroundColor(.white)
                            .padding()
                            .frame(width: 280, height: 45)
                            .background(Color.blue)
                            .cornerRadius(10.0)
                    }
                }
                Spacer()
            }.padding()
            .padding(.top, 120)
            .ignoresSafeArea()
            .alert(isPresented: $showingAlert) {
                Alert(title: Text("불일치"), message: Text("아이디 또는 패스워드가 잘못되었습니다."), dismissButton: .default(Text("닫기")))
            }
        }
    }
    
    
    init() {
        readUser()  // data 검색
        deleteUser()    // kSecAttrAccount의 값이 "Service"인 data 삭제
        readUser()  // data 검색
        createUser()    // kSecAttrAccount의 값을 "Service"로 SecAddItem
        readUser()  // data 검색
//        updateUser()    //  kSecAttrAccount의 값이 "Service"인것에 SecItemUpdate
        readUser()
    }
    
    func readUser() {
        guard let user = StorageManager.shared.readUser() else {
            print("Read Failed")
            return
        }
        print("read user.id : ", user.userId)
        print("read user.password : ", user.password)
    }
    
    func createUser() {
        let user = User(userId: "LEE", password:toSHA256("password_1234"))
        guard StorageManager.shared.createUser(user) else {
            print("Create Failed")
            return
        }
    }
    
    func updateUser() {
        let user = User(userId: "LEE", password:toSHA256("password_123"))
        guard StorageManager.shared.updateUser(user) else {
            print("Create Failed")
            return
        }
    }
    
    func deleteUser() {
        guard StorageManager.shared.deleteUser() else {
            print("Delete Failed")
            return
        }
    }
    
    func login(_ userId: String, _ password: String){
        if userId == "" || password == "" {
            showingAlert = true
            return
        }
        guard let user = StorageManager.shared.readUser() else {
            showingAlert = true
            return
        }
        if(userId == user.userId && toSHA256(password) == user.password){
            self.password = ""
            isLogin = true
        } else {
            showingAlert = true
        }
    }
    
    func toSHA256(_ password: String) -> String {
        let data = password.data(using: .utf8)
        let sha256 = SHA256.hash(data: data!)
        // 이 시퀀스의 각 요소로 지정된 변환을 호출하는 nil이 아닌 결과를 포함하는 배열을 반환.
        // 2자리 Hex String 16진수 소문자로
        let shaData = sha256.compactMap{String(format: "%02x", $0)}.joined()
        return shaData
    }
}

 

 

참조: https://daeun28.github.io/ios%EC%82%AC%EC%9A%A9%EB%B2%95/post21/

반응형