본문 바로가기
FRONT-END/iOS

[iOS] Swift API Key 암호화하기

by 랄라J 2024. 3. 30.

나는 현재 개인 프로젝트에서 오픈 API를 사용해 데이터를 받아오는 과정을 진행 중이다!

근데 이제 API Key를 github에 올리면 문제가 되니까 암호화하는 방법을 다시금 찾아보고 적용하는데

분명 해봤는데 왜 이렇게 오래 걸리지... 뭐였지...? 이게 최선이었나? 하는 마음이 들어 정리하고자 한다! 😂

 

방법 1. static 변수 활용하기

하나의 파일을 만들어서 사용할 API Key를 static 변수로 묶어 전역적으로 사용하는 방법이다.

class보다는 struct, enum을 활용해 사용한다.

struct APIKey {
    static let aApiKey = "aksdlakdl1aksdla"
    static let bApiKey = "aksdlakdl1aksdla"
}

// 사용 시 APIKey.aApiKey로 접근해 사용
enum Secrets {
    static let aApiKey = "aksdlakdl1aksdla"
    static let bApiKey = "aksdlakdl1aksdla"
}

// 사용 시 Secrets.aApiKey로 접근해 사용

하지만 이 방법은 코드를 git에 올릴 때 매번 신경을 써줘야 한다.

.gitignore에 해당 특정 파일을 추가해 git에 올리지 않으면 또 괜찮을 것 같기도 해서 실험해 봤다.

 

- New File > Other 내 Empty 클릭 후. gitignore 파일 생성

우선, .gitignore 파일이 git remote에 push 된 상태여야 동작하는 것을 인지하고 있어야 한다.

때문에 먼저 .gitignore 파일 생성 후에 git remote 연결하고 -> push까지 진행해 줬다.

 

그리고 APIKey를 저장할 파일 하나 생성한다.

//  Secrets.swift

struct APIKey {
    static let aApiKey = "aksdlakdl1aksdla"
    static let bApiKey = "aksdlakdl1aksdla"
}

 

# .gitignore 파일

Secrets.swift

terminal에서 git status로 확인해 가면서 진행했다.

위에 git status 결과는 .gitignore에 Secrets.swift를 추가하지 않았을 때 결과고,

아래  git status 결과는 .gitignore에 Secrets.swift를 추가했을 때의 결과다.

즉, 해당 파일을 .gitignore에 추가해 git에 올릴 때는 해당 파일을 제외하고 올리면 된다.

 

해당 방법의 장점은 아래와 같다.

- 코드 내 직접 변수로 선언하기 때문에 접근과 사용이 매우 간편하다. 

- 컴파일 시간에 포함되기 때문에 실행 시간에 추가적인 처리가 필요 없다.

 


방법 2. info.plist 활용하기

static 방법과 유사하다고도 볼 수 있는데, .gitignore에 숨길 .plist 파일을 추가하는 방법이다.

 

- New File > Resource 내 Property List 클릭 후 Secrets.plist 파일 생성

 

- .gitignore에 숨길 파일명 추가

# .gitignore 파일

Secrets.plist

앞서 본 것과 같이 위 결과는 .gitignore에 잘 추가되지 않았을 때, 잘 추가되었을 때다.

 

그리고 이 Secrets.plist에 추가된 APIKey 정보를 가져와 사용하는 방법에는 Bundle을 확장해 사용하는 방법이 있다.

//  Bundle+Extension.swift
import Foundation

extension Bundle {
    var apiKey: String? {
        guard let path = Bundle.main.path(forResource: "Info", ofType: "plist"),
              let rawData = try? Data(contentsOf: URL(fileURLWithPath: path)),
              let realData = try? PropertyListSerialization.propertyList(from: rawData, format: nil) as? [String: Any],
              let key = realData["apiKey"] as? String else { return nil }
        return key
    }
}

// 사용 시 : Bundle.main.apiKey

 

해당 방법의 장점은 아래와 같다.

- 애플리케이션의 구성 정보와 함께 중앙에서 관리되므로 여러 설정값을 한 곳에서 관리할 수 있다.

 

해당 방법의 단점은 아래와 같다. (사실 1단계도 마찬가지...!)

Secrets.plist 파일을 프로젝트에 추가하고, .gitignore에 해당 파일을 추가해 git에서 추적되지 않도록 했지만

project.pbxproj 파일은 프로젝트에 대한 모든 설정과 파일 참조를 포함하고 있다.

그렇기 때문에 다른 팀원이 프로젝트를 열 때, Secrets.plist 파일이 없다면 누락된 것으로 인식되어 에러가 발생할 수 있다.

 

이를 해결하기 위해서 제안되는 방법으로는

- Secrets.plist.template와 같은 템플릿 파일을 제공해, 이 파일을 복사하여 개인적인 Secets.plist를 생성하도록 할 수 있다. 이 템플릿 파일은 실제 키 값을 제외한 기본 구조만 포함되며, 프로젝트에 포함되어 git에 커밋될 수 있다.

- 스크립트를 사용해 프로젝트 빌드 과정에서 Secrets.plist 파일이 존재하지 않을 경우 경로를 띄우거나, 자동으로 기본 템플릿을 생성하는 스크립트를 추가해 프로젝트 설정의 일관성을 유지할 수 있다.

 


방법 3. .xcconfig 활용하기

이 또한 방법 1, 방법 2와 유사하지만 환경에 따라 세분화할 수 있고 조금 더 보안을 곁들인 느낌이다.

 

- New File > Other 내 Configuration Settings File 클릭 후 Secrets.xcconfig 파일 생성

 

//  Secrets.xcconfig

A_API_KEY = aksdlakdl1aksdla
B_API_KEY = asdqwjelkjqe2132

("" 없이 사용한다는 점에 유의하기, 다음 정보 입력 시 ,도 불필요)

 

- 프로젝트 configurations 세팅

 

- info.plist에 환경 변수를 활용해 api key를 설정

 

- .gitignore에 숨길 .xcconfig 파일만 추가

(info.plist는 xcconfig의 환경 변수를 사용하기 때문에 올릴 필요 없는 것)

# .gitignore 파일

*.xcconfig

 

- 사용 시에는 2단계처럼 Bundle을 확장해 구성해서 사용하면 된다.

 

해당 방법의 장점은 아래와 같다.

- 다양한 개발 환경에 대한 구성을 별도의 파일로 관리할 수 있고, Xcode 프로젝트 설정에서 쉽게 변경할 수 있다.

 

해당 방법의 단점은 아래와 같다. 

- API 키와 같은 값을 코드에서 직접 참조하기 위해 추가적인 코드 작성이 필요할 수 있다. Info.plist를 통한 접근보다 직관적이지 않을 수 있다.

- 다수의 .xcconfig 파일을 관리하고 적용하기 위해 Xcode 빌드 설정에 대한 이해가 필요하고 프로젝트 설정 복잡성이 증가할 수 있다.

 


방법 4. keychain활용하기

keychain은 이미 클라이언트에 존재하거나, 앱이 서버에서 받은 민감한 정보를 안전하게 저장하고 관리하기 위해 사용하는 저장 방식이다.

 

사실 보안을 위한 정보를 저장하기 위한 방법으로 바로 keychain이 떠올랐다.

하지만 어차피 코드를 통해 암호화하는 것이다 보니 암호화할 데이터를 코드에 적어야 하는데 어떻게 보안이 된다는 건지에 대한 의문이 들어 이 포스팅을 시작하게 되었다.

 

우선 코드로 바로 적는 것이 아니라 안전하게 정보를 전달하기 위한 방법으로는

1. 위에 작성한 방법대로 구성 파일 분리 후 .gitignore에 추가하고 앱 빌드 시 이 파일로부터 값을 읽어 keychain에 저장한 후 사용하기

(하지만 이 방법도 최종 빌드된 앱 내에 정보가 포함되기 때문에 완벽한 보안을 보장하기 어렵다.)

2. 앱 첫 실행 시 서버로부터 안전하게 받아오기

위 두 가지가 있는데 2번은 서버가 필요하니 결국 1번을 통해서 처리해도 완벽한 보안을 보장하기는 어렵다고 한다.

 

그래도 구현하는 과정을 코드로 알아보자면

import Foundation
import Security

class KeychainHelper {

    @discardableResult
    static func save(key: String, data: Data) -> OSStatus {
        let query = [
            kSecClass as String       : kSecClassGenericPassword,
            kSecAttrAccount as String : key,
            kSecValueData as String   : data ] as [String : Any]

        SecItemDelete(query as CFDictionary)

        return SecItemAdd(query as CFDictionary, nil)
    }

    static func load(key: String) -> Data? {
        let query = [
            kSecClass as String       : kSecClassGenericPassword,
            kSecAttrAccount as String : key,
            kSecReturnData as String  : kCFBooleanTrue!,
            kSecMatchLimit as String  : kSecMatchLimitOne ] as [String : Any]

        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)

        if status == noErr {
            return item as? Data
        }
        return nil
    }
}

 

// API 키 저장
if let apiKeyData = "your_secret_api_key".data(using: .utf8) {
    let saveStatus = KeychainHelper.save(key: "UniqueKeyForAPIKey", data: apiKeyData)
    print(saveStatus == errSecSuccess ? "API Key 저장 성공" : "API Key 저장 실패")
}

// API 키 검색
if let loadedApiKeyData = KeychainHelper.load(key: "UniqueKeyForAPIKey"),
   let apiKey = String(data: loadedApiKeyData, encoding: .utf8) {
    print("검색된 API Key: \(apiKey)")
} else {
    print("API Key를 찾을 수 없음")
}

 

결국 위 모든 방법 다 동일한 단점을 가지고 있다.

- 해당 파일이 git에 올라가지 않으니, 협업자들 간의 파일 또는 해당 파일의 내용을 수동으로 공유해야 해 다른 곳에 저장해야 한다.

- 해당 파일을 잃어버리면 다시 해당 파일을 작성해야 하는 번거로움도 발생한다.

- 특히, 협업하는 입장에서 다른 곳에 저장하더라도 변경된 키를 공유하지 않아 에러가 발생하는 등 휴먼에러가 발생하기 쉽다.


 

사실 https://nshipster.com/secrets/ 이 글에 따르면 클라이언트에 저장하는 것 자체가 안티 패턴으로 보아야 한다고 한다.

Restating our original question: “How do I store secrets securely on the client?”

Our answer: “Don’t (but if you must, obfuscation wouldn’t hurt).”

모바일 앱에서 비밀을 유지하는 방법은 서버에 비밀을 저장하는 것이라고 말한다.

즉, 서버를 통해 암호화되어야 하는 정보를 관리하도록 권장한다.

그 외로는 클라이언트를 활용하지 않고 CI를 활용해 환경 변수를 사용해 런타임에 키를 주입하는 방식이 있는데, 추후 알아보는 걸로..!

 

결국 나는 .xcconfig + info.plist를 활용하는 정도로 API Key를 보안하기로 하고 적용했다.

혹시 해당 내용에 대해 더 잘 알고 계신 분이 있다면 잘못된 내용에 대해 언제든 피드백 주시면 감사하겠습니다 🐥

반응형

댓글