 Apple Lover Developer & Artist

영속적인 디자인에 현대의 공감을 채워넣는 공방입니다

 Apple/iOS Dev Challenges

[Challenge] 데이터 암복호화 모델 개발

singularis7 2023. 12. 18. 17:27
반응형

Overview

대칭키 암호화 알고리즘을 사용해 데이터 암복호화 기능이 적용된 모델 객체를 개발해 본다.

보안

현대의 애플리케이션은 로컬에서 독자적으로 동작하지 않는다. 클라우드 인프라를 바탕으로 네트워크 통신을 통해 데이터를 처리한다. 네트워크 환경에서 개인정보 등과 같은 중요 정보를 담은 데이터가 외부에 노출될 위험성도 높아졌다. 현대 컴퓨팅 세계에서 보안은 소프트웨어 설계 관점에서 신경 써야 할 요소가 되었다.

애플리케이션의 개발 과정에서 보안은 전반적인 설계 철학에 가까운 것 같다. 보안 분야의 세부 범주도 넓기에 하나의 보안 기능을 구현했다고 끝날일이 아니다. 기획 단계부터 구현, 테스트, 출시까지 개발 생애주기 전반에 걸쳐서 의도적으로 보안을 고려한 설계가 필요하다.

앱 개발자의 설계 책임

보안성을 고려한 애플리케이션을 개발하기 위해 개발자가 신경써야할 영역이 있다. 네트워크 통신 등의 수단으로 외부에서 들어온 데이터를 처리하는 과정이다. 외부의 데이터는 잠재적으로 신뢰할 수 없는 데이터다. 따라서 유효성을 검증하고 데이터를 안전하게 불러와서 신뢰할 수 있는 데이터 영역에 저장해야 한다. 반대로 애플리케이션의 데이터를 외부로 내보내야 할 수 있다. 데이터의 수신자를 검증하고 안전한 네트워크 채널을 통해 통신하는지 확인해야 한다. 한마디로 정리하자면 애플리케이션 개발자는 앱 내부에 신뢰할 수 없는 영역과 신뢰할 수 있는 영역의 경계를 설계해야 하는 것이다.

출처: 애플 개발자 문서 중 Security Overview 발췌

보안 설계 수단

앱 개발자가 사용할 수 있는 보안 설계 수단이 있다. 가장 쉬운 방법은 애플이 미리 개발해 둔 보안 서비스를 사용하는 것이다.

예를 들어, 키체인을 사용하면 암호화 키, 패스워드, 인증서 및 기타 보안 관련 정보 등 주요 데이터를 안전하게 관리할 수 있다. 키체인은 다른 앱이 접근할 수 없는 보안 저장소에 데이터를 저장하며 사용자가 로그인하거나 장치의 잠금이 해제되었을 때만 접근할 수 있도록 설계되었다.

안전한 보안 통신 채널을 확보하고자 URL 로딩 시스템을 사용할 수 있다. 고수준 API를 통해 손쉽게 SSL 및 TLS 등의 보안 통신을 사용할 수 있다. 소켓 스트림을 사용하여 자체 보안 프로토콜을 정의할 수도 있다.

데이터 암호화

엄청난 금전적 이해관계를 다루는 핵심 데이터가 있다고 가정해 본다. 데이터 소유주와 관련 없는 제삼자의 관점에서 데이터를 탈취해서 이익을 얻을 수 있다면 수단과 방법을 가리지 않고 데이터를 얻고자 할 것이다.

데이터 암호화 시스템을 잘 활용하면 데이터 접근 권한이 없는 제삼자가 데이터를 탈취하더라도 해석할 수 없도록 보호할 수 있다. 해시를 사용하면 데이터의 수정 사항을 확인할 수 있다. 해시와 공개키 암호화 기술의 결합을 통해 디지털 서명을 만들어서 데이터 소스를 증명하는 수단으로도 활용할 수 있다.

Common Cryptor

애플 플랫폼은 개발자가 앱 내에서 암호화 솔루션을 개발할 수 있도록 포괄적인 저수준 암호화 API를 제공하고 있다. 대표적으로 Common Cryptor이다.

Common Cryptor 라이브러리는 macOS와 iOS에 포함된 Apple의 오픈소스 암호화 라이브러리이다. 메시지 다이제스트, 해시 기반 메시지 인증 코드, 대칭키 데이터 암복호화과 난수 생성 등의 유틸리티 기능 등을 제공하고 있다.

C언어로 개발되어 있기에 Swift 프로젝트에서 사용하려면 몇 가지 단계를 거쳐야 한다. Objective-C 언어를 통해 Common Cryptor C코드를 랩핑 한 뒤에 Bridging Header를 활용해 Swift로 불러오는 방법이다.

다음은 Common Cryptor 라이브러리를 사용해 대칭키 암호화 알고리즘인 AES256-CBC 모드를 사용한 암복호화 기능을 구현한 후 Swift에서 불러오는 프로젝트를 구현해 보겠다.

대칭키 암복호화 API 개발

Xcode에서 새로운 macOS CLI 프로젝트를 생성할 것이다. 프로젝트 이름음 CryptoPlayground로 정했으며 Swift 환경의 프로젝트를 생성했다.

Objective-C API Wrapping

CryptoManager 클래스를 생성할 것이다. NSObject를 상속받은 Objective-C 클래스를 생성한다. 파일의 생성이 완료되면 Xcode가 브릿징 헤더를 생성할지 물어보는 경고창을 띄울 것이다. 이 때는 그냥 생성하기 눌러주면 된다.

자동으로 생성된 브릿징 헤더 파일을 열어서 CryptoManager 인터페이스가 담긴 헤더 파일을 import 해준다. 코드는 다음과 같다.

//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

#import "CryptoManager.h"

Objective-C 암복호화 API 구현

브릿징 헤더를 통해 Swift와 Objective-C 간의 상호운용성 설정을 완료했다면 개발 환경의 준비가 완료된 것이다. 지금부터는 실제로 암복호화 기능을 수행하는 API를 개발해 볼 것이다.

CryptoManager.h 헤더 파일을 수정하여 AES256-CBC를 활용한 암호화 및 복호화 인터페이스를 추가해 보겠다. 암호화 및 복호화 기능을 구현하기 위해서는 3가지 데이터가 필요하다. 암호화할 평문 데이터, 대칭키, 암호화 알고리즘에 사용될 초기 벡터값이다. 필자는 다음과 같이 암복호화 메서드를 구현해 보았다.

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface CryptoManager : NSObject

- (nullable NSData *)encryptAES256CBCWithPlainData:(nonnull NSData *)data key:(nonnull NSString *)key iv:(nonnull NSString *)iv;
- (nullable NSData *)decryptAES256CBCWithCipheredData:(nonnull NSData *)data key:(nonnull NSString *)key iv:(nonnull NSString *)iv;

@end

NS_ASSUME_NONNULL_END

두 개의 인스턴스 메서드가 선언되었다. nullable, nonnull 키워드는 Swift 상호운용성 개발 환경에서 Objective-C API가 Swift 옵셔널 개념을 지원하도록 선언해 준 것이다. 암호화 API를 구현해 보겠다.

#import "CryptoManager.h"
#import <CommonCrypto/CommonCryptor.h>

@implementation CryptoManager

- (nullable NSData *)encryptAES256CBCWithPlainData:(nonnull NSData *)data key:(nonnull NSString *)key iv:(nonnull NSString *)iv {

    char keyCString[kCCKeySizeAES256 + 1];
    memset(keyCString, 0, sizeof(keyCString));

    char ivCString[kCCBlockSizeAES128 + 1];
    memset(ivCString, 0, sizeof(ivCString));

    [key getCString:keyCString maxLength:sizeof(keyCString) encoding:NSUTF8StringEncoding];
    [iv getCString:ivCString maxLength:sizeof(ivCString) encoding:NSUTF8StringEncoding];

    NSUInteger bufferSize = data.length + kCCBlockSizeAES128;
    NSUInteger encryptedBytesSize = 0;

    void *bufferPtr = malloc(bufferSize);
    if (bufferPtr == NULL) {
        return nil;
    }

        // ...

}

Common Crypto 라이브러리에서 암복호화 연산을 수행하는 API가 있다. 바로 CCCrypto이다. 이 함수는 데이터의 암복호화 과정에서 사용하고 싶은 알고리즘 구성을 파라미터로 넘겨주면 개발자가 제공한 버퍼 메모리에 암복호화 연산이 수행된 결과 값을 담아준다. CCCrypto는 C언어 API이기 때문에 Objective-C 오브젝트인 NSString이나 NSData를 바로 사용할 수 없다. C언어의 타입으로 변경해주어야 한다.

우선 NSString 타입의 Key와 IV값을 C언어 문자 배열로 변환해 보겠다. Key와 IV 값을 담아줄 C언어 기본 타입의 문자 배열을 선언했다. 여기서 문자 배열의 크기를 정하는 과정이 중요하다.

AES256 알고리즘에서 사용되는 키 길이는 이름에서 보이는 것처럼 256비트이다. 라이브러리에는 암복호화 연산에 사용될 키 길이나 블록의 크기가 상수값으로 선언되어 있다. kCCKeySizeAES256을 통해 키 길이를 불러온 후 CString의 마지막 지점에 null 문자를 넣어줄 공간 하나를 추가 배정했다.

CBC 모드에서 사용되는 초기 백터값(IV)의 경우 암복호화 블럭 연산에 사용되기 때문에 블럭 크기와 동일해야 한다. 블럭의 크기 또한 상수값으로 선언되어있다. AES 키 길이와 상관없이 모두 128비트로 동일하기 때문에 kCCBlockSizeAES128 사용하면 된다. 마찬가지로 String의 마지막 지점을 의미하는 null 문자를 넣어줄 공간 하나를 추가 배정해 주었다.

이제 NSString 오브젝트의 문자열 값을 C 기본 타입의 문자 배열에 할당해주어야 한다. NSString은 자신의 문자열 값을 CString으로 변환해 주는 getCString 메서드를 제공하고 있다. C 기본 타입 문자는 1byte이기 때문에 sizeof로 배열의 크기를 손쉽게 측정할 수 있다. 주의할 점은 인코딩을 UTF8을 선택해야 한다는 점이다.

앞서 CCCrypto 함수를 사용할 때 암복호화 결괏값은 개발자가 제공한 버퍼 메모리에 담긴다고 했다. 암복호화할 데이터의 크기에 따라 버퍼의 공간이 유동적으로 변해야 하기에 malloc을 활용한 동적할당을 해주었다. 여기서도 버퍼 메모리의 크기를 정해줄 때 주의할 점이 있다. AES256 CBC 모드는 128비트 크기의 블록 단위로 데이터를 쪼개서 암복호화 연산을 한다. 따라서 연산을 위해 데이터의 크기는 블록의 크기의 정수배로 나누어 떨어져야한다. 나누어 떨어지지 않을 경우 블럭의 크기에 알맞게 데이터를 채워 넣어주는 개념이 있는데 바로 PKCS7Padding이다. 패딩 데이터를 채워 넣어줄 추가 공간까지 고려해야 하기에 버퍼 사이즈를 결정할 때 본래 데이터 크기에 블록 사이즈인 kCCBlockSizeAES128 를 더해주었다. 이후 암복호화된 최종 데이터 크기를 담아줄 변수 하나를 선언해 주었다.

@implementation CryptoManager

- (nullable NSData *)encryptAES256CBCWithPlainData:(nonnull NSData *)data key:(nonnull NSString *)key iv:(nonnull NSString *)iv {

      // ...

    CCCryptorStatus status = CCCrypt(
        kCCEncrypt, kCCAlgorithmAES, kCCOptionPKCS7Padding,
        keyCString, kCCKeySizeAES256, ivCString,
        data.bytes, data.length,
        bufferPtr, bufferSize, &encryptedBytesSize
    );

    NSData *encryptedData = nil;
    if (status == kCCSuccess) {
        encryptedData = [NSData dataWithBytes:bufferPtr length:encryptedBytesSize];
    }

    free(bufferPtr);
    bufferPtr = NULL;

    return encryptedData;

}

이제 CCCrypto를 사용할 준비가 되었다. AES256-CBC모드를 사용한 암호화 연산을 수행하도록 파라미터를 구성해 보겠다.

첫 번째 인자에 수행할 연산의 종류를 선언할 수 있다. kCCEncrypt 혹은 kCCDecrypt를 통해 암호화나 복호화 연산을 수행할 수 있다.

두 번째 인자에는 암복호화에 사용될 알고리즘을 선언할 수 있다. 예를 들어 AES, DES 등의 알고리즘을 선언할 수 있다. 이번에는 AES 알고리즘을 사용하고자 kCCAlgorithmAES를 사용했다.

세 번째 인자에는 암복호화 과정의 세부 옵션을 선언할 수 있다. 예를 들어 PKCS7패딩의 사용 여부나 ECB 모드의 사용 여부를 결정할 수 있다. 기본값은 CBC 모드이기에 PKCS7 패딩만을 사용하도록 설정해 주었다.

네 번째 인자에는 암복호화에 사용할 키값 C언어 문자열을 넘겨줘야 한다. 앞서 변환해 준 C언어 타입의 키값 문자 배열을 넘겨주었다.

다섯 번째 인자에는 키값의 길이를 넘겨주야 한다. AES256 알고리즘의 키길이는 256비트이기에 이에 대응하는 상수값인 kCCKeySizeAES256을 넘겨주었다.

여섯 번째 인자에는 CBC 모드 암복호화에 사용될 초기 백터값 C언어 문자열을 넘겨줘야 한다. 앞서 변환해 준 C언어 타입의 초기 백터값 문자 배열을 넘겨주었다.

일곱 번째와 여덣번째 인자에는 암복호화할 데이터와 데이터의 크기를 넘겨주어야 한다. 보통 NSData 오브젝트 타입을 가진 경우가 많을 텐데 bytes 속성을 통해 데이터 값을 받아올 수 있으며 length 속성을 통해 데이터 값의 크기를 받아올 수 있다.

아홉 번째와 열 번째 인자에는 암복호화 연산이 수행된 결괏값 데이터를 담아줄 버퍼 메모리의 정보를 넘겨준다. 앞서 동적할당해 둔 포인터 변수와 버퍼 크기를 넘겨주면 된다.

마지막 열한 번째 인자에는 암복호화 연산을 통해 생성된 데이터의 최종 크기를 담아줄 변수를 넘겨준다. 변수 주소값을 직접 넘겨주어 CCCrypto API가 변수 주소에 직접 접근해서 데이터를 담아주도록 한다.

함수가 정상적으로 동작하면 kCCSuccess 상태 코드를 반환한다. 암복호화 데이터를 Objective-C와 Swift에서 불러올 수 있도록 NSData 오브젝트로 포장해 준 후 반환해 준다.

마무리로 앞서 동적할당해 둔 메모리 공간을 운영체제에 반환해 주어 메모리 누수가 발생하지 않도록 처리해 주면 끝난다. 여기까지 잘 따라왔다면 복호화 기능의 구현은 별로 어렵지 않다. 위 코드의 첫 번째 인자를 복호화 옵션인 kCCDecrypt로 수정해 주고 변수 네이밍을 복호화와 연관된 데이터를 담았다는 의미로 수정해 주면 된다.

Swift에서 사용해 보기

초기 개발 환경 설정에서 블릿징 헤더를 설정해 둠으로써 Objective-C API가 Swift에 노출되도록 준비해 두었다. 따라서 다음과 같이 CryptoManager 오브젝트를 통해 데이터를 암복호화 하는 예시 코드를 작성해 보았다.

import Foundation


// 평문
let plainMessageData = "Hello World!".data(using: .utf8)!


// 대칭키 암복호화 설정값
let cryptoManager = CryptoManager()
let key = "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEF" // 256비트 == 32바이트 -> 32문자
let iv = "ABCDEFGHIJKLMNOP" // 128비트 == 16바이트 -> 16문자


// AES256 - CBC 모드 암호화 예시
let encryptedData = cryptoManager.encryptAES256CBC(
    withPlainData: plainMessageData, key: key, iv: iv)
let encryptedBase64String = String(data: encryptedData?.base64EncodedData() ?? Data(), encoding: .utf8)
print(encryptedBase64String!)


// AES256 - CBC 모드 복호화 예시
let decryptedData = cryptoManager.decryptAES256CBC(
    withCipheredData: encryptedData!, key: key, iv: iv)
let decryptedMessage = String(data: decryptedData!, encoding: .utf8)
print(decryptedMessage!)

전체적인 코드 흐름은 평문인 "Hello World!"를 AES256 CBC모드를 사용해 암호화시킨 후 base64 인코딩하여 콘솔에 출력한다. 그 후 암호화된 데이터를 복호화하여 평문과 동일하게 출력되는지 살펴본다. 실행 결과는 다음과 같다.

+uVcp8C9eZoCi92+3l/eqw==
Hello World!
Program ended with exit code: 0

인터넷의 AES256 암복호화 연산 사이트에 접속하여 동일한 데이터를 바탕으로 암복호화를 연산했을 때 동일한 결과가 출력되는지 검증해 보았다. 결과는 동일하게 잘 나온다!

Reference

반응형