안녕하세요

 

 

개발을 하다보면 외부 API를 사용해야하는 경우가 많이 발생합니다.

 

 

보통의 경우 API키를 통해 인증을 진행하고 해당서비스를 이용하게됩니다.

 

 

인증키를 노출하게되면 원치않는 과금이 발생할 수 도 있기에 클라이언트 환경에 API키를 포함할 경우 각별한 주의가 필요합니다.

 

 

오늘은 클라이언트 환경에서 API키를 보유해야 하는 경우 어떠한 방법들이 있는지 다뤄보겠습니다.

 

 

방법1: xcconfig, info.plist에 포함하는 경우

 

대부분의 경우 이 방법을 많이 사용하실 것 같습니다.

 

 

swift코드에 포함하게 될 경우 깃허브에 해당파일을 올려 즉시 노출이 발생하게되기 때문에

 

 

xcconfig와 같은 파일에 키를 저장하고 해당 파일을 ignore하는 방식을 많이 사용합니다.

 

 

하지만, 아시다시피 방법은 사실 전혀 안전하지 못합니다.

 

 

xcconfig를 사용하는 경우에도 결국 런타임에 해당 값을 사용하려면 info.plist로 값을 불러와야합니다.

 

 

info.plist의 경우 리소스이기 때문에 빌드시 기계어로 변경되지 않고 원본이 빌드결과물에 포함되게 됩니다.

빌드결과물 폴더

 

iOS애플리케이션의 경우 빌드 결과물을 확인할 수 있습니다.

 

 

따라서 단순히 패키지 내용 살펴보기를 통해 공격자가 API키를 획득할 수 있습니다.

 

 

방법2: swift파일에 직접 포함하는 경우

 

저는 한동안 주로 이 방법을 사용해 왔습니다.

 

 

먼저 Swift파일에 API키를 정적 상수로 보유하는 타입을 구현하고 해당 파일을 ignore하는 방식을 사용했습니다.

 

 

이 방식 info plist를 활용하는 방법보다 안전한 이유는 API키가 컴파일되어 바이너리화 되기 때문입니다.

 

 

하지만, 아시다시피 iOS애플리케이션은 빌드결과물에 접근할 수 있습니다.

 

 

어렵지만 공격자 역공학을 사용하면 API키를 알아낼 수 있습니다.

 

 

자 그럼 어떻게 해야할까요...

 

 

방법3: 난독화를 통해 해결하기

 

역공학을 하는 것을 완전히 막을 수 있는 방법은 없습니다.

 

 

따라서 저희가 할 수 있는 것은 역공학과정이 최대한 어렵도록 코드를 난독화(obfuscation)하는 방법밖에 없습니다.

 

 

별거아닌것 같지만, 난독화된 기계어를 역공학 하는 과정은 매우 어려울 것이라고 판단됩니다.

 

 

API키를 바이너리 파일에 포함하되, 난독화를 통해 역공학을 어렵게한다! 이것이 핵심입니다.

 

 

GYB를 사용하여 코드 난독화 하기

 

Swift언어를 개발한 개발자들은 반복되는 코드(보일러 플레이트)를 효과적으로 생성하기 위해 GYB라는 기술을 사용합니다.

(GYB는 Generate your boilerplate의 약자입니다.)

 

 

해당 기술은 Python을 사용하여 코드를 생성하는 템플릿 프로그램입니다.

 

 

파이썬 코드를 사용하여 반복되는 코드들을 한번에 생성하는 것입니다.

 

 

예를들어 Encodable의 경우 아래와 같이 타입별로 encode함수를 생성해야 합니다.

 

mutating func encode(_ value: Bool) throws
mutating func encode(_ value: String) throws
mutating func encode(_ value: Double) throws
mutating func encode(_ value: Float) throws
mutating func encode(_ value: Int) throws

 

 

해당 함수들을 타입마다 개발자가 직접생성한다면 매우 괴로운 작업이 될 것 입니다.

 

 

유지 보수도 어렵고요.

 

 

GYB를 사용하면 아래와 같이 간단한 코드로 순식간에 생성할 수 있습니다.

 

%{
codable_types = ['Bool', 'String', 'Double', 'Float',
                 'Int', 'Int8', 'Int16', 'Int32', 'Int64',
                 'UInt', 'UInt8', 'UInt16', 'UInt32', 'UInt64']
}%


% for type in codable_types:
  mutating func encode(_ value: ${type}) throws
% end

 

 

 

GYB문법과 관련된 내용을 간단하게 설명하자면 다음과 같습니다.

 

The sequence %{ code }% evaluates a block of Python code
- 일종의 코드 실행 블록으로 쓰인다.
- 함수선언 및 정의, 전역변수 초기화 등등

The sequence % code: ... % end manages control flow
- 보통 if, for, while 같은 제어 흐름 구문을 나타낸다.

The sequence ${ code } substitutes the result of an expression
- ${ ... }는 문자열 안에서 변수나 표현식의 값을 삽입(substitute)하는 용도

 

좀 더 자세하게 알고 싶다면 아래글을 추천합니다.

https://nshipster.com/swift-gyb/

 

 

난독화의 진행과정은 다음과 같습니다.

 

 

1. 빌드 전에 API키를 빌드 스크립트의 환경 변수로 등록한다.

2. 랜덤한 바이너리(salt)를 생성하고 변수로 저장한다.

3. API키와 랜덤 바이너리(salt)를 XOR연산을 수행하여 암호문(Cyper)을 생성한다.

4. 생성한 암호문을 Swift코드의 변수에 할당한다.

5. 런타임에 해당 변수에 접근시 salt를 사용해 암호문을 평문으로 복호화한다.

 

 

더보기

XOR연산을 통한 암호화

XOR연산을 동일한 값으로 두번 시행하면 시도전의 평문이 도출됩니다.

 

1011 ^ 0100 = 1111(암호문)

1111 ^ 0100 = 1011

 

 

해당과정을 통해 결과적으로 아래와 같은 Swift파일이 빌드과정에서 생성됩니다.

 

enum Secret {

    private static let salt: [UInt8] = [
        0xcc, 0x4f, 0x0a, 0xba, 0x58, 0x03, 0x3d, 0xad, 
        0x6e, 0xfa, 0x05, 0x9d, 0xef, 0xfa, 0xd4, 0x36, 
        0x5e, 0x94, 0x0f, 0xbc, 0x0d, 0xca, 0xf6, 0x68, 
        0x99, 0x3a, 0x5d, 0x3c, 0xa4, 0xa4, 0x5a, 0x98, 
        0xdb, 0xe9, 0x58, 0xaf, 0xa9, 0x26, 0x2d, 0x59, 
        0xa6, 0xc8, 0x15, 0xc6, 0x92, 0x51, 0xa5, 0x06, 
        0x56, 0xb6, 0x5a, 0xd5, 0xa6, 0x4c, 0x74, 0x89, 
        0x21, 0x57, 0x38, 0x44, 0x54, 0x2d, 0xdf, 0x78, 
    ]

    static var apiKey: String {
        let encoded: [UInt8] = [
            0xee, 0xa3, 0x9f, 0x32, 0xb3, 0x86, 0xa8, 0x40, 
            0xfb, 0x62, 0xe9, 0x19, 0x57, 0x16, 0x4e, 0xa2, 
            0xb5, 0x24, 0x97, 0x56, 0xbd, 0x5b, 0x1a, 0xe2, 
            0x2c, 0xd1, 0xd6, 0xb4, 0x4f, 0x2f, 0xfe, 0xb6, 
            0xf9, 
        ]
        return decode(encoded, cipher: salt)
    }
    
    private static func decode(_ encoded: [UInt8], cipher: [UInt8]) -> String {
        String(decoding: encoded.enumerated().map { (offset, element) in
            element ^ cipher[offset % cipher.count]
        }, as: UTF8.self)
    }
}

 

 

GYB를 사용한 구현과정 상세

GYB는 Homebrew에서 Install하여 독립적으로 사용할 수 있습니다.

 

brew install nshipster/formulae/gyb

※ 해당 프로그램은 python 2.7을 사용합니다, 설치후 gyb.py에서 python3를 사용하도록 변경하면 .gyb파일에서 파이썬3 문법을 사용할 수 있습니다.

 

 

gyb프로그램에 .gyb파일을 전달하여 실행하면 삽입한 코드가 평가됩니다.

 

 

먼저 앞서 만들어진 Swift파일을 생성한 gyb파일(apikey.swift.gyb)입니다.

 

%{
import os

def chunks(seq, size):
    return (seq[i:(i + size)] for i in range(0, len(seq), size))

def encode(string, cipher):
    bytes = string.encode("UTF-8")
    return [bytes[i] ^ cipher[i % len(cipher)] for i in range(0, len(bytes))]
}%

enum Secret {

    private static let salt: [UInt8] = [
    %{ salt = list(os.urandom(64)) }%
    % for chunk in chunks(salt, 8):
        ${"".join(["0x%02x, " % byte for byte in chunk])}
    % end
    ]

    static var apiKey: String {
        let encoded: [UInt8] = [
        % for chunk in chunks(encode(os.environ.get('API_KEY'), salt), 8):
            ${"".join(["0x%02x, " % byte for byte in chunk])}
        % end
        ]
        return decode(encoded, cipher: salt)
    }
    
    private static func decode(_ encoded: [UInt8], cipher: [UInt8]) -> String {
        String(decoding: encoded.enumerated().map { (offset, element) in
            element ^ cipher[offset % cipher.count]
        }, as: UTF8.self)
    }
}

 

여기서 파이썬 문법에 대해서는 다루지 않겠습니다.

 

 

Swift파일을 한번만 생성하는 것보단 빌드마다 달라지는 것이 좀 더 안전한 방법임으로

 

 

Build phase에서 현재 프로젝트내의 .gyb파일에 대해 gyb프로그램을 실행하는 스크립트를 컴파일전에 실행하게 합니다.

 

 

컴파일 전에 실행하는 이유는 새로운 암호문이 빌드결과물에 포함되도록하기 위해서 입니다.

 

※ Compile source실행 전에 해당 스크립트가 실행되야합니다.

 

key.txt는 키-벨류 형태로 API키를 저장한 파일입니다. 파이썬 코드에서 접근할 수 있도록 환경변수에 등록해 줍니다.

 

아래 코드는 key.txt파일 내용입니다.

API_KEY="안녕하세요반갑습니다."

 

더보기

런 스크립트에서 환경변수를 등록해야 하는 이유

Xcode는 샌드박스 형식을 지향합니다.
이를 위해 빌드시 내부적으로 독립된 Shell을 생성하여 스크립트를 실행합니다.

따라서 런 스크립트에서 환경변수를 등록하는 과정이 필요합니다.
외부 쉘을 통해 등록된 환경변수는 해당 과정에서 무의미합니다.

 

자 이제 api key를 출력해보면

 

print(Secret.apiKey)

 

 

 

 

 

Cmd+R을 하면 매번 새로운 암호문을 생성하지만, 런타임에 복호화 되어 동일한 평문이 출력되는 것을 확인할 수 있습니다.

 

 

※ 암호문을 포함하는 Swift파일 역시 ignore해야합니다!

 

 

해당 프로젝트 파일은 아래에 첨부하겠습니다.

 

GYBTest.zip
0.03MB

 

 

GYB설치 및 사용에 관해서는 아래 글을 확인해 주시길 바랍니다.

 

Secret Management on iOS

One of the great unsolved questions in iOS development is, “How do I store secrets securely on the client?”

nshipster.com

 

 

하지만 가장 좋은 방법은 따로 있습니다.

 

 

방법4: 클라이언트에 API키를 포함하지 않기

 

사실 서버로부터 API키를 획득하면 모든 문제가 해결됩니다.

 

 

별도의 인증과정이 있어 단점이 있지만, 안정성에 비할바는 아니라고 생각됩니다.

 

 

아마 현업 프로젝트의 경우 대부분 이 방법을 사용한다고 생각합니다.

 

 

개인 혹은 사이드 프로젝트의 경우 방법3을 고려해볼 필요는 있다고 생각됩니다.

 

 

긴글 읽어주셔서 감사합니다!