💫 서론
이번에 파일을 암호화/복호화를 해야 할 일이 있었는데 구글링 했을 땐 대부분 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값이 나온것을 볼수있다.
'iOS > Swift' 카테고리의 다른 글
UserDefaults / Keychain / Core Data (0) | 2022.08.17 |
---|---|
Swift에서 DI/DIP 이해하기 (0) | 2022.06.21 |
UserDefaults에서 .value(forKey:)와 .object(forKey:)는 뭐가 다른 건가요? (0) | 2022.05.13 |
논리 게이트를 Swift로 구현해 보자! (0) | 2022.05.13 |
Property wrapper (0) | 2022.04.17 |