암호화와 복호화
평문(plaintext)를 암호문(ciphertext)로 변환하는 과정을 암호화라 한다. 반대의 경우는 복호화라 한다. 암호화 키와 복호화 키가 같다면 대칭키 암호라 한다. 반대의 경우는 비대칭 키 암호라 한다. 암, 복호화 과정에서 메시지를 블럭 단위로 나누어 처리하는 것을 블럭 암호, 비트 단위로 처리하는 것을 스트림 암호라 한다.
블럭 암호 운용 방식
블럭 암호 운용 방식은 하나의 키와 초기화벡터로 블럭 암호를 반복적으로 안전하게 이용하는 절차를 말한다. 키(key)는 암호화, 복호화 시에 사용되며 128비트, 256비트와 같이 알고리즘에서 요구하는 길이를 사용해야 하므로 16바이트 또는 32바이트가 되어야 한다. 일반적으로 ASCII 환경에서는 영어, 숫자, 기타 기호등은 한 글자당 1바이트이며, 한글은 2바이트다. 그러나 UTF-8 환경에서는 영어, 숫자, 기타 기호는 똑같이 1바이트이며, 한글은 3바이트가 된다.
초기화벡터(Initialization Vector, IV) 는 첫 블럭을 암호화할때 사용되는 값을 의미한다. (CBC 모드에서 가장 첫 블럭은 1단계 앞의 암호문 블럭이 존재하지 않기 때문) CBC 모드에서는 평문이 반드시 1단계 앞의 암호문과 XOR 연산 후 암호화 된다. IV는 키와 마찬가지로 송수신자 사이에 미리 약속되어 있어야 하지만 공개된 값을 사용해도 무방하다. IV는 반복되어 사용되어서는 안된다. IV가 같은 경우 비슷한 두개의 평문을 암호화했을 때 앞부분의 블록들이 서로 같게 되어 보안 문제가 발생하기 때문이다. 때문에 암호화 때마다 랜덤 비트열을 사용하는 것이 보통이다. 블럭 암호의 경우 비트 블럭 단위로 나누기 때문에 마지막 블럭이 기준 비트를 만족하지 못하는 경우를 채워줘야 한다. 이를 패딩이라고 한다. (ex. 300비트를 128비트 단위로 나누는 경우 128 + 128 + 44 이기 때문에 마지막 44 블럭을 128비트에 맞춰 패딩해야한다.)
SEED 128과 AES 128 블럭 암호 알고리즘
SEED 128은 KISA(한국인터넷진흥원)에서 개발했다. AES 128은 벨기에 인에 의해 개발됐으며 미국을 비롯한 나라에서 사용중이다. 두 알고리즘 모두 128비트 블럭 단위로 동작된다. (192, 256비트도 지원) 두 알고리즘 모두 대칭키 암호다.
CryptoSwift
Swift에서 AES 128 암호 알고리즘 사용을 위해 CryptoSwift를 사용할 수 있다.
import CryptoSwift
let key = "p a s s w o r d ".bytes // 16바이트의 키 생성
let iv = "d r o w s s a p ".bytes // 16바이트의 초기화벡터 생성
let plaintext = "안녕하세요. 1급 비밀을 전달합니다. 블라블라~"
// ECB 모드
do {
// 암호화
let encrypted = try AES(key: key, blockMode: .ECB, padding: .pkcs7).encrypt(plaintext.bytes)
print(encrypted) // [160, 48, 156, 182, 178, 99, 10, 81, 132, 75, 65, 167, 151, 163, 131, 118, 252, 184, 125, 76, 106, 241, 152, 29, 51, 113, 88, 10, 2, 245, 37, 152, 16, 96, 137, 91, 243, 30, 157, 63, 86, 157, 30, 80, 11, 199, 127, 35, 102, 80, 31, 241, 128, 128, 187, 248, 100, 17, 98, 44, 209, 96, 230, 204]
print(encrypted.toHexString()) // a0309cb6b2630a51844b41a797a38376fcb87d4c6af1981d3371580a02f525981060895bf31e9d3f569d1e500bc77f2366501ff18080bbf86411622cd160e6cc
// 복호화
let decrypted = try AES(key: key, blockMode: .ECB, padding: .pkcs7).decrypt(encrypted)
print(decrypted) // [236, 149, 136, 235, 133, 149, 237, 149, 152, 236, 132, 184, 236, 154, 148, 46, 32, 49, 234, 184, 137, 32, 235, 185, 132, 235, 176, 128, 236, 157, 132, 32, 236, 160, 132, 235, 139, 172, 237, 149, 169, 235, 139, 136, 235, 139, 164, 46, 32, 235, 184, 148, 235, 157, 188, 235, 184, 148, 235, 157, 188, 126]
print(String(bytes: decrypted, encoding: .utf8)!) // 안녕하세요. 1급 비밀을 전달합니다. 블라블라~
} catch {
print(error)
}
// CBC 모드
do {
// 암호화
let encrypted = try AES(key: key, blockMode: .CBC(iv: iv), padding: .pkcs7).encrypt(plaintext.bytes)
print(encrypted) // [187, 181, 61, 114, 215, 25, 208, 113, 242, 222, 198, 94, 250, 255, 108, 195, 230, 76, 51, 224, 220, 231, 116, 144, 240, 253, 48, 174, 33, 240, 238, 228, 190, 29, 215, 133, 139, 126, 54, 96, 132, 18, 230, 253, 43, 49, 124, 121, 166, 240, 76, 9, 160, 156, 63, 248, 219, 40, 184, 194, 99, 33, 83, 34]
print(encrypted.toHexString()) // bbb53d72d719d071f2dec65efaff6cc3e64c33e0dce77490f0fd30ae21f0eee4be1dd7858b7e36608412e6fd2b317c79a6f04c09a09c3ff8db28b8c263215322
// 복호화
let decrypted = try AES(key: key, blockMode: .CBC(iv: iv), padding: .pkcs7).decrypt(encrypted)
print(decrypted) // [236, 149, 136, 235, 133, 149, 237, 149, 152, 236, 132, 184, 236, 154, 148, 46, 32, 49, 234, 184, 137, 32, 235, 185, 132, 235, 176, 128, 236, 157, 132, 32, 236, 160, 132, 235, 139, 172, 237, 149, 169, 235, 139, 136, 235, 139, 164, 46, 32, 235, 184, 148, 235, 157, 188, 235, 184, 148, 235, 157, 188, 126]
print(String(bytes: decrypted, encoding: .utf8)!) // 안녕하세요. 1급 비밀을 전달합니다. 블라블라~
} catch {
print(error)
}
// 간소화 모드 (CBC 알고리즘)
do {
// 암호화
let aes = try AES(key: "p a s s w o r d ", iv: "d r o w s s a p ")
let encrypted = try aes.encrypt("안녕하세요. 1급 비밀을 전달합니다. 블라블라~".bytes)
print(encrypted) // [187, 181, 61, 114, 215, 25, 208, 113, 242, 222, 198, 94, 250, 255, 108, 195, 230, 76, 51, 224, 220, 231, 116, 144, 240, 253, 48, 174, 33, 240, 238, 228, 190, 29, 215, 133, 139, 126, 54, 96, 132, 18, 230, 253, 43, 49, 124, 121, 166, 240, 76, 9, 160, 156, 63, 248, 219, 40, 184, 194, 99, 33, 83, 34]
print(encrypted.toHexString()) // bbb53d72d719d071f2dec65efaff6cc3e64c33e0dce77490f0fd30ae21f0eee4be1dd7858b7e36608412e6fd2b317c79a6f04c09a09c3ff8db28b8c263215322
// 복호화
let decrypted = try aes.decrypt(encrypted)
print(decrypted) // [236, 149, 136, 235, 133, 149, 237, 149, 152, 236, 132, 184, 236, 154, 148, 46, 32, 49, 234, 184, 137, 32, 235, 185, 132, 235, 176, 128, 236, 157, 132, 32, 236, 160, 132, 235, 139, 172, 237, 149, 169, 235, 139, 136, 235, 139, 164, 46, 32, 235, 184, 148, 235, 157, 188, 235, 184, 148, 235, 157, 188, 126]
print(String(bytes: decrypted, encoding: .utf8)!) // 안녕하세요. 1급 비밀을 전달합니다. 블라블라~
} catch {
print(error)
}
SEED 128
...
Objective-C Bridging Header.h
...
import CryptoSwift
// SEED 사용
var key = "passwordpassword".bytes // 16바이트의 키 생성
var iv = "passwordpassword".bytes // 16바이트의 초기화벡터 생성
var plaintext = "안녕하세요.".bytes // 암호화될 평문 생성 (16바이트로 생성됨)
var ciphertext = " ".bytes // 출력버퍼 생성 (입력버퍼보다 16바이트 크게 확보하는 것이 좋음 -> 암호화될 평문(입력 버퍼): 16바이트 // 출력될 암호문(출력 버퍼): 32바이트
print(ciphertext) // [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32]
var plaintext_ = " ".bytes // 복호화시 필요한 출력버퍼 생성 (위와 동일하게 16바이트 추가 확보)
print(plaintext_) // [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32]
// 암호화
// KISA_SEED_CBC_ENCRYPT 함수는 암호화가 성공하면 암호문의 길이를 리턴해준다. 암호문은 ciphertext 변수에 바로 적용된다. 왜냐? & 연산자를 썼기 때문.
var outlen = KISA_SEED_CBC_ENCRYPT(&key, &iv, &plaintext, 16, &ciphertext) // C로 된 함수가 포팅되기 때문에 UnsafeMutablePointer<UInt8> 이 인자값으로 쓰인다. 하지만 Swift에서는 포인터 개념이 없기 때문에 & 연산자를 통해 메모리 주소값을 바로 보낼 수 있다. (이 SEED 함수 쓰려고 4일동안 찾아다니다 혼자 시도해본게 성공 ㅡ.ㅡ 아무도 안알려주고 어디에도 정보가 없다...)
print(ciphertext) // [18, 69, 23, 167, 212, 215, 104, 251, 46, 225, 184, 4, 97, 49, 195, 181, 130, 108, 249, 15, 28, 183, 249, 111, 176, 64, 134, 20, 161, 159, 32, 10]
// 처음과 다르게 ciphertext의 바이트가 변경된 것을 알 수 있다! -> 고로 암호문이 입력되었음을 알 수 있다.
// 복호화
outlen = KISA_SEED_CBC_DECRYPT(&key, &iv, &ciphertext, UInt32(outlen), &plaintext_)
print(plaintext_) // [236, 149, 136, 235, 133, 149, 237, 149, 152, 236, 132, 184, 236, 154, 148, 46, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16]
// 마찬가지로 plaintext_의 바이트가 처음과 다름을 알 수 있다! -> 고로 암호문이 복호화 되었음을 알 수 있다.
// 마지막 작업으로 복호화된 문장의 패딩을 셀프로 제거해야한다 ㅡ.ㅡ...
let count = Int(outlen) // outlen은 UInt32기 때문에 편의를 위해 Int로 변경. (참고로 outlen은 아까 말했듯 암,복호화 성공시 암호문의 길이를 리턴해준다. 여기서 Outlen은 16, 아까 암호화했을땐 outlen이 32였다.)
var removedPadding = [UInt8]() // 패딩이 제거된 바이트들이 저장될 UInt8 배열 생성
// 패딩은 제거하고 실제 필요한 바이트들만 배열에 담는다.
for i in 0...(count - 1) {
removedPadding.append(plaintext_[i])
}
// 패딩이 제거된 바이트들을 인코딩하면 완성!!!
print(String(bytes: removedPadding, encoding: .utf8)!) // 안녕하세요.
BIP 0039
BIP는 결정론적 지갑을 생성하기 위해 연상 기호 코드나 문장을 실행하는 것이라 할 수 있다. 결정론적 지갑은 Seed라고 하는 단일 시작 지점에서 키를 파생시키는 시스템이다. 이 Seed는 별다른 정보 요구 없이 지갑을 쉽게 백업하고 복원할 수 있게 한다. Seed는 어떤 경우에는 개인키에 대한 정보없이 공개키를 만들 수 있게 해주기도 한다. Seed는 일반적으로 사람이 읽을 수 있는 연상 기호 코드로 연속된다. 연상 기호 코드는 2진수 또는 16진수 처리와 비교해서 종이에 쓰거나 말로 할 수 있을 정도로 사람과의 상호작용에 우월하다. (ex. 한국 -> kor, 25인치 검정색 TV -> 25-BL-TV) BIP는 연상 기호를 생성하는 부분과 이진법 시드로 변환하는 두 가지로 나뉘며, 시드가 나중에 결정성 지갑을 생성하는데 사용될 수 있다.
비트연산
Swift로 따지면 &&(AND), ||(OR) 이 있으며 비트연산시에는 &(AND), |(OR), ^(XOR)로 사용된다.
// 비트연산시 결과
// AND 연산자는 모든 입력값이 1인 경우에만 1을 출력
0 & 0 // 0
0 & 1 // 0
1 & 0 // 0
1 & 1 // 1
// OR 연산자는 입력값 중 1이 있다면 1을 출력
0 | 0 // 0
0 | 1 // 1
1 | 0 // 1
1 | 1 // 1
// XOR 연산자는 입력값이 같지 않다면 1을 출력
0 ^ 0 // 0
0 ^ 1 // 1
1 ^ 0 // 1
1 ^ 1 // 0