Swift에서 CryptoKit을 사용하면 AES256 암호화 및 복호화가 가능합니다. 그런데 CryptoKit은 GCM block mode만 지원하고 있으며, CBC block mode는 CryptoSwift와 같은 외부 라이브러리를 사용해서 암호화 및 복호화를 할 수 있습니다.
먼저 플레인 텍스트(plain text)를 블럭으로 나눕니다. 카운터 기반 암호화 방식이며, 각 블럭은 고유한 카운터 값을 갖게 됩니다. IV는 암호화를 위해 사용하는 초기값이며, 메시지에 대응하는 고유한 값을 갖고 있어야 합니다. 플레인 텍스트를 블록으로 나누고, 각 블록과 해당 블럭의 카운터를 조합해서 암호화합니다. 그리고 다시 인증 태그에 XOR 연산을 수행해서 태그 값을 계산합니다. 모든 블록에서 이뤄지며 CBC와 달리 순차적으로 이뤄질 필요가 없기 때문에 병렬 처리가 가능합니다.
가장 많이 사용하는 것으로 알려진 방식으로 우선 GCM처럼 플레인 텍스트를 블럭으로 나눕니다. 그리고 첫 블럭을 IV 값으로 XOR 연산을 해서 암호화합니다. 암호화된 이 블럭과 다음 블럭 둘의 XOR 연산을 다시 진행합니다. 이를 반복하게 되는데, 나눠진 블록들이 연쇄적으로 암호화되기 때문에 순차적으로 처리함에 따라 병렬 처리가 어렵습니다.
AES256 기준 PDF 파일을 txt 형식 파일로 암호화해보려고 합니다. 그런데 대칭키와 IV를 지정한 문자열로 설정하려고 합니다. CryptoSwift 라이브러리를 사용했습니다.
import Foundation
import CryptoSwift
enum CBCEncryptorError: Error {
case cannotCreateFileURL
case cannotLoadRawData
case cannotLoadKeyBytes
case cannotLoadIVBytes
}
protocol CBCEncryptor {
var encryptedData: Data? { get }
func encrypt() throws
}
final class DefaultCBCEncryptor: CBCEncryptor {
private(set) var encryptedData: Data?
init() {
self.encryptedData = nil
}
func encrypt() throws {
do {
let data = try loadData()
let key = try generateSymmetricKey(from: "Cera")
let iv = try generateIV(from: "BangBus")
let aes = try createAES(key: key, iv: iv)
let encryptedData = try aes.encrypt(data.bytes)
let base64EncodedData = Data(encryptedData).base64EncodedString()
print(base64EncodedData)
} catch let error {
throw error
}
}
private func loadData() throws -> Data {
guard let dataPath = Bundle.main.path(forResource: "Lorem_ipsum", ofType: "pdf") else { throw CBCEncryptorError.cannotCreateFileURL }
let fileURL = URL(fileURLWithPath: dataPath)
do {
let data = try Data(contentsOf: fileURL)
return data
} catch {
throw CBCEncryptorError.cannotLoadRawData
}
}
private func generateSymmetricKey(from string: String) throws -> Data {
let keyData = string.data(using: .utf8)
guard let keyBytes = keyData?.bytes else { throw CBCEncryptorError.cannotLoadKeyBytes }
let paddedKey = addPadding(keyBytes, size: 32, paddingByte: 0)
return Data(paddedKey)
}
private func generateIV(from string: String) throws -> Data {
let ivData = string.data(using: .utf8)
guard let ivBytes = ivData?.bytes else { throw CBCEncryptorError.cannotLoadIVBytes }
let paddedIV = addPadding(ivBytes, size: 16, paddingByte: 0)
return Data(paddedIV)
}
private func createAES(key: Data, iv: Data) throws -> CryptoSwift.AES {
return try AES(key: key.bytes, blockMode: CBC(iv: iv.bytes), padding: .pkcs7)
}
private func addPadding(_ array: [UInt8], size: Int, paddingByte: UInt8) -> [UInt8] {
var paddedArray = array
if paddedArray.count < size {
let paddingCount = size - paddedArray.count
let paddingBytes = Array(repeating: paddingByte, count: paddingCount)
paddedArray.append(contentsOf: paddingBytes)
}
return paddedArray
}
}
암호화 시 대칭키는 256비트, IV는 128비트 길이를 가져야 합니다. "Cera", "BangBus"는 그 길이보다 짧기 때문에 addPadding()
메소드를 사용해서 길이를 맞춰줍니다. 각각의 길이를 맞추지 않으면 유효하지 않은 키 사이즈, 유효하지 않은 IV 값이라는 오류가 발생합니다.
txt 확장자 파일에 암호화된 문자열이 저장되어 있는 형태에서 아래 객체를 사용해 복호화가 가능합니다. 암호화와 동일하게 대칭키와 IV의 길이를 적합한 길이가 되도록 padding을 추가합니다.
import Foundation
import CryptoSwift
enum CBCDecryptorError: Error {
case cannotCreateFileURL
case cannotLoadRawData
case cannotLoadKeyBytes
case cannotLoadIVBytes
case cannotFindData
}
protocol CBCDecryptor {
var decryptedData: Data? { get }
func decrypt() throws
}
final class DefaultCBCDecryptor: CBCDecryptor {
private(set) var decryptedData: Data?
init() {
self.decryptedData = nil
}
func decrypt() throws {
do {
let encryptedString = try loadData()
guard let encryptedData = Data(base64Encoded: encryptedString) else { throw CBCDecryptorError.cannotFindData }
let key = try generateSymmetricKey(from: "Cera")
let iv = try generateIV(from: "BangBus")
let aes = try createAES(key: key, iv: iv)
let target = encryptedData
let decryptedData = try aes.decrypt(target.bytes)
self.decryptedData = Data(decryptedData)
} catch let error {
throw error
}
}
private func loadData() throws -> String {
guard let dataPath = Bundle.main.path(forResource: "image", ofType: "txt") else { throw CBCDecryptorError.cannotCreateFileURL }
let fileURL = URL(fileURLWithPath: dataPath)
do {
let contents = try String(contentsOf: fileURL, encoding: .utf8)
return contents.trimmingCharacters(in: .whitespacesAndNewlines)
} catch {
throw CBCDecryptorError.cannotLoadRawData
}
}
private func generateSymmetricKey(from string: String) throws -> Data {
let keyData = string.data(using: .utf8)
guard let keyBytes = keyData?.bytes else { throw CBCDecryptorError.cannotLoadKeyBytes }
let paddedKey = addPadding(keyBytes, size: 32, paddingByte: 0)
return Data(paddedKey)
}
private func generateIV(from string: String) throws -> Data {
let ivData = string.data(using: .utf8)
guard let ivBytes = ivData?.bytes else { throw CBCDecryptorError.cannotLoadIVBytes }
let paddedIV = addPadding(ivBytes, size: 16, paddingByte: 0)
return Data(paddedIV)
}
private func createAES(key: Data, iv: Data) throws -> CryptoSwift.AES {
return try AES(key: key.bytes, blockMode: CBC(iv: iv.bytes), padding: .pkcs7)
}
private func addPadding(_ array: [UInt8], size: Int, paddingByte: UInt8) -> [UInt8] {
var paddedArray = array
if paddedArray.count < size {
let paddingCount = size - paddedArray.count
let paddingBytes = Array(repeating: paddingByte, count: paddingCount)
paddedArray.append(contentsOf: paddingBytes)
}
return paddedArray
}
}
loadData()
메소드의 반환값을 보면 .trimmingCharacters(in: .whitespacesAndNewlines)
가 보이는데, 문자열의 앞뒤에 있는 여백을 없애준다고 합니다. 여백이 있을 경우 복호화가 되지 않습니다.
위 코드는 padding에 비어있는 값을 넣어줬지만 좋은 방식은 아니라고 생각합니다. 보안상 무작위로 생성된 문자열을 넣는 것이 더 합리적입니다. 어려운 내용은 아니기 때문에 이 코드에서 생략했지만 실제 사용해서 주의해야 하는 부분입니다.