본문 바로가기

iOS/Swift

Swift CommonCrypto로 AES 암/복호화 하기! (without CryptoSwift)

💫 서론

이번에 파일을 암호화/복호화를 해야 할 일이 있었는데 구글링 했을 땐 대부분 CryptoSwift 라이브러리를 사용하고 있었다. 하지만 내가 해야 되는 것은 복호화만 간단히 하면 되어서 라이브러리를 사용하기에는 오버스펙인 것 같아 내장 모듈인 CommonCrypto를 사용하여 직접 구현하면서 정리하게 되었다.

 

💫 요구사항

 

이 사이트는 온라인에서 AES를 암호화/복호화 하는 사이트인데 이 것을 목표로 요구사항을 작성하였다.

- AES 암호화

- 128bit, 192bit, 256bit 키 알고리즘

- CBC, ECB 모드 지원

- IV 지원

- Base64, Hex 출력 포맷 지원

- 파일지원 x

- Padding은 pkcs7만 지원

 

💫 목표!! (결과값)

Hello, Crypto! 라는 문구를 12345678901234567890123456789012 Key 값과 1234123412341234 IV 값을 이용해 G+0E3vGU6D2I2Kae8Aq/HA== 라는 암호화된 Base64값을 얻고 그 값으로 복호화하여 다시 Hello, Crypto! 라는 문구를 얻어야 한다.

 

💫 정의

공통 상수

// smaple
private let SECRET_KEY = "12345678901234567890123456789012"
private let IV = "1234123412341234"
private let beEncryptedText = "Hello, Crypto!"

우선 키값, IV값, 암호화 할 문구를 정의한다.

 

Key Sizes

/// Key sizes
enum CryptoKeySize: CaseIterable {
    /// 128 bit AES key size.
    case aes128
    /// 192 bit AES key size.
    case aes192
    /// 256 bit AES key size.
    case aes256
    
    var bytes: Int {
        switch self {
        case .aes128: return 16
        case .aes192: return 24
        case .aes256: return 32
        }
    }
}

유요한 알고리즘의 키 값(AES-128, AES-192, AES-356)의 길이를 정의한다.

 

Errors

/// Errors
enum CrpytoError: Error {
    case invalidKeySize
    case invalidIVSize
    case encryptionFailed
    case decryptionFailed
}

키 사이즈, iv 사이즈, 암호화 실패, 복호화 실패 Error 열거형을 정의한다.

 

BlockMode

protocol BlockMode {}

struct CBC: BlockMode {
    let iv: Data?
}

struct ECB: BlockMode { }

CBC와 ECB 블럭모드 구조체를 정의한다.

 

Data Extension

extension Data {
    var hexString: String {
        map { String(format: "%02hhx", $0) }.joined()
    }
    
    var base64String: String {
        base64EncodedString()
    }
}

리턴한 데이터를 base64 또는 hex 문자열로 바꿔주는 코드를 작성한다.

 

💫 CCCrypt(...) 이해하기

우선 CommonCrypto의 핵심이 되는 CCCrypt 함수를 이해해보자..!

😵

1. CCOperation

암호화할지 복호화할지 선택

 

2.  CCAlgorithm

알고리즘 선택

kCCAlgorithmAES128은 deprecated되었고 이 포스트의 경우에는 kCCAlgorithmAES만 사용하게 될 예정이다.

 

3.  CCOptions

패딩이나 블록모드 선택

이 포스트의 경우 kCCOptionPKCS7Padding만 사용하게 될 예정이다.

 

4. key

앞서 정의한 Key를 Data로 만들고 이 값의 주소를 넘겨주면 된다.

 

5. keyLength

앞서 정의한 Key의 길이를 넘겨주면 된다.

 

6. iv

앞서 정의한 IV를 Data로 만들고 이 값의 주소를 넘겨주면 된다.

nil을 넘겨주면 무조건 ECB모드로 암호화/복호화가 진행된다.

 

7. dataIn

암호화/복호화 할 데이터 값의 주소를 넘겨주면 된다.

 

8. dataInLength

암호화/복호화 할 데이터 값의 길이를 넘겨주면 된다.

 

9. dataOut

리턴할 버퍼의 주소를 넘겨주면 된다.

 

10. dataOutAvailable

리턴할 버퍼의 가능한 최대 길이를 넘겨주면 된다.

 

11. dataOutMoved

데이터가 암호화/복호화 된 길이를 리턴한다. (inout)

 

💫 AES

생성자

struct AES {
    private let key: Data
    private let iv: Data?
    private let blockMode: BlockMode
    
    init(keyString: String, ivString: String? = nil) throws {
        guard CryptoKeySize.allCases.map({ $0.bytes }).contains(keyString.count) else {
            throw CrpytoError.invalidKeySize
        }
        
        switch ivString?.count {
        case nil, 0:
            self.blockMode = ECB()
            self.iv = nil
        case 16:
            self.iv = Data(ivString!.utf8)
            self.blockMode = CBC(iv: self.iv)
        default:
            throw CrpytoError.invalidIVSize
        }
        
        self.key = Data(keyString.utf8)
    }
}

keyString과 ivString을 생성자로 받아서 AES구조체를 만든다.

키 값의 길이나 iv값의 길이가 유효하지 않으면 Error를 리턴하고 iv값의 길이가 0이면 CBC모드가 아닌 ECB모드로 암호화를 진행한다.

 

🔒 암호화

extension AES {
    func encrypt(_ string: String) throws -> Data {
        let dataToEncrypt = Data(string.utf8)
        
        let dataSize = dataToEncrypt.count
        let keySize = key.count
        let ivSize = iv?.count ?? 0
        let bufferSize = dataSize + keySize + ivSize
        var buffer = Data(count: bufferSize) // 👈 리턴할 데이터
        
        var numberBytesEncrypted: Int = 0 // 👈 암호화된 길이
        
        // 👇 IV 데이터의 주소
        let ivDataBytesBaseAddress: UnsafeRawPointer?
        if blockMode is CBC {
            if let baseAddress = iv?.withUnsafeBytes({ $0.baseAddress }) {
                ivDataBytesBaseAddress = baseAddress
            } else {
                ivDataBytesBaseAddress = nil
            }
        } else {
            ivDataBytesBaseAddress = nil
        }
        
        // 👇 Key 데이터, buffer의 주소
        guard
            let keyBytesBaseAddress = key.withUnsafeBytes({ $0.baseAddress }),
            let bufferBytesBaseAddress = buffer.withUnsafeMutableBytes({ $0.baseAddress })
        else {
            throw CrpytoError.encryptionFailed
        }
        
        do {
            try dataToEncrypt.withUnsafeBytes { dataBytes in
            	// 👇 암호화 할 데이터의 주소
                guard let dataBytesBaseAddress = dataBytes.baseAddress else {
                    throw CrpytoError.encryptionFailed
                }
                
                // 👇 암호화
                let cryptStatus: CCCryptorStatus = CCCrypt(
                    CCOperation(kCCEncrypt),          // 👈 암호화 / 복호화
                    CCAlgorithm(kCCAlgorithmAES),     // 👈 알고리즘
                    CCOptions(kCCOptionPKCS7Padding), // 👈 패딩옵션
                    keyBytesBaseAddress,              // 👈 키 주소
                    keySize,                          // 👈 키의 길이
                    ivDataBytesBaseAddress,           // 👈 IV 주소
                    dataBytesBaseAddress,             // 👈 Input 데이터 주소
                    dataSize,                         // 👈 Input 데이터 길이
                    bufferBytesBaseAddress,           // 👈 Output 데이터 주소
                    bufferSize,                       // 👈 Output 데이터 길이
                    &numberBytesEncrypted             // 👈 암호화된 길이
                )
                
                guard cryptStatus == CCCryptorStatus(kCCSuccess) else {
                    throw CrpytoError.encryptionFailed
                }
            }
        } catch {
            throw CrpytoError.encryptionFailed
        }
        
        // 👇 암호화된 길이만큼만 짤라서 리턴해준다.
        let encryptedData: Data = buffer.prefix(numberBytesEncrypted)
        return encryptedData
    }
}

 

 

위에서 설명한 CCCrypt함수를 사용해 암호화된 데이터를 리턴한다.

(data의 주소는 왜인지 정상적으로 리턴되지 않아서 do-catch문에서 작성했다.)

 

🔓 복호화

extension AES {
    func decrypt(_ string: String) throws -> Data {
        let dataToDecrypt = Data(base64Encoded: string)!
        
        let dataSize = dataToDecrypt.count
        let keySize = key.count
        let ivSize = iv?.count ?? 0
        let bufferSize = dataSize + keySize + ivSize
        var buffer = Data(count: bufferSize)
        
        var numberBytesDecrypted: Int = 0
        
        let ivDataBytesBaseAddress: UnsafeRawPointer?
        if blockMode is CBC {
            if let baseAddress = iv?.withUnsafeBytes({ $0.baseAddress }) {
                ivDataBytesBaseAddress = baseAddress
            } else {
                ivDataBytesBaseAddress = nil
            }
        } else {
            ivDataBytesBaseAddress = nil
        }

        guard
            let keyBytesBaseAddress = key.withUnsafeBytes({ $0.baseAddress }),
            let bufferBytesBaseAddress = buffer.withUnsafeMutableBytes({ $0.baseAddress })
        else {
            throw CrpytoError.decryptionFailed
        }
        
        do {
            try dataToDecrypt.withUnsafeBytes { dataBytes in
                guard let dataBytesBaseAddress = dataBytes.baseAddress else {
                    throw CrpytoError.decryptionFailed
                }
                
                let cryptStatus: CCCryptorStatus = CCCrypt(
                    CCOperation(kCCDecrypt),
                    CCAlgorithm(kCCAlgorithmAES),
                    CCOptions(kCCOptionPKCS7Padding),
                    keyBytesBaseAddress,
                    keySize,
                    ivDataBytesBaseAddress,
                    dataBytesBaseAddress,
                    dataSize,
                    bufferBytesBaseAddress,
                    bufferSize,
                    &numberBytesDecrypted
                )

                guard cryptStatus == CCCryptorStatus(kCCSuccess) else {
                    throw CrpytoError.decryptionFailed
                }
            }
        } catch {
            throw CrpytoError.decryptionFailed
        }
        
        let decryptedData: Data = buffer.prefix(numberBytesDecrypted)
        return decryptedData
    }
}

복호화도 암호화 하는 법과 거의 유사하게 작성하면된다.

 



💫 실행해보기

print("Text to be Encrypted:\n\t\(beEncryptedText)")

guard let aes = try? AES(keyString: SECRET_KEY, ivString: IV) else { return }

guard let encrypted = try? aes.encrypt(beEncryptedText) else { return }

let encryptedBase64 = encrypted.base64String
let encryptedHexString = encrypted.hexString
print("Encrypted Output (Base64):\n\t\(encryptedBase64)")
print("Encrypted Output (Hex):\n\t\(encryptedHexString)")

guard let decrypted = try? aes.decrypt(encryptedBase64) else { return }

let decryptedBase64 = decrypted.base64EncodedString()
let decryptedUTF8 = String(data: Data(base64Encoded: decryptedBase64)!, encoding: .utf8)!
print("Decrypted Output (Base64):\n\t\(decryptedBase64)")
print("Decrypted Output (UTF8):\n\t\(decryptedUTF8)")

이렇게 프린트가 찍힌다.

앞서 보여준 온라인 암호화/복호화 사이트를 다시보면..

완전히 같게 동작했다..!!

Output Text Format을 Hex로 바꿔보면 같은 Hex값이 나온것을 볼수있다.