go
는 암호화를 위한 표준 라이브러리를 제공합니다.
crypto
golang.org/x/crypto
Go의 블록 암호
주로 사용하는 블록 암호화로 AES
DES
를 지원합니다.
AES-128
AES-192
AES-256
DES
3DES
블록 암호
아래 함수는 cipher.Block
인터페이스를 리턴 합니다.
AES
다음 함수로 AES
Cipher
를 얻습니다.
cip, err := aes.NewCipher(key) // 바이트 슬라이스 key
key
의 사이즈에 따라서 AES
의 블럭 사이즈가 결정 됩니다.
AES-128 |
AES-192 |
AES-256 |
|
---|---|---|---|
키 길이 | 16 |
24 |
32 |
DES
다음 함수로 DES
Cipher
를 얻습니다.
des.NewCipher(key)
des.NewTripleDESCipher(key) // 3DES
DES |
3DES |
|
---|---|---|
키 길이 | 8 |
32 |
암호화/복호화
cipher.Block
은 다음 함수가 존재합니다.
type Block interface {
// Cipher 의 블럭 크기를 리턴합니다.
BlockSize() int
// src 를 암호화 하여 dst에 저장합니다.
// src는 블럭사이즈와 길이가 같아야합니다.
Encrypt(dst, src []byte)
// src 를 복호화 하여 dst에 저장합니다.
// src는 블럭사이즈와 길이가 같아야합니다.
Decrypt(dst, src []byte)
}
// 암호화 예제
plaintext := []byte("Hello, world! 12")
key := make([]byte, 32)
rand.Read(key) // 랜덤값 키 생성
ciphertext := make([]byte, len(plaintext))
cip, _ := aes.NewCipher(key)
// 암호화
cip.Encrypt(ciphertext, plaintext)
fmt.Printf("ciphertext: %x\n", ciphertext)
// 복호화
cip.Decrypt(plaintext, ciphertext)
fmt.Printf("plaintext: %s\n", plaintext)
AES-256
은 블록 사이즈가 32바이트 입니다.
이는 한번 암호화할 때 32바이트만 입력을 받는다는 의미이기도 합니다.
실제로 암호화를 할 데이터는 32바이트보다 클 경우가 많습니다.
이때, 데이터를 32바이트 블록들로 나눠서 암호화를 하는 방법이 있습니다. 이를 ECB
모드 라고 합니다.
그러나 같은 키로 모든 블록을 암호화 하면 보안에 취약합니다.
위 이미지는 리눅스 마스코트 펭귄을 ECB
, CBC
모드로 암호화한 결과입니다. ECB
모드는 암호화를 했음에도 펭귄의 윤곽선과 명암이 특정됨을 알 수 있습니다.
그리고 모든 블록을 한 키로 암호화 하기 때문에, 암호화된 모든 블록을 대조하여 키를 찾아내기 쉽습니다. ECB
모드는 보안에 취약하니 사용하지 않는것이 좋습니다.
블록 암호 운용 방식(모드)
큰 데이터를 안전하게 암호화 하기 위해 다양한 모드들이 개발되어 있습니다. 물론 go
표준 라이브러리에도 이러한 운용 모드들이 지원됩니다.
CBC
암호 블록 체인 방식CTR
카운터GCM
갈루와/카운터 모드CFB
암호 피드백OFB
출력 피드백
이중 CBC
모드를 다뤄보겠습니다. 다른 모드들은 추후 다루겠습니다.
CBC Mode
CBC
는 1976년 IBM
에서 개발되어 현재까지 많이 사용하는 암호 모드입니다.
CBC
모드의 암호화는 다음 과정을 거칩니다.
- 긴 평문을 암호화 블록 사이즈로 나눈다.
- 각 블록은 암호화 되기 전에 이전 블록의 암호화 결과와 XOR 된다.
첫 블록은 초기화 벡터 (iv
) 를 XOR 한다.
여기서 iv
라는 개념이 등장하는데 iv
는 암호화에 사용하는 블록 사이즈와 길이가 같은 임의의 값으로 암호화에 사용된 iv
와 복호화에 사용되는 iv
가 같아야 복호화가 정상적으로 이루어집니다. 이러한 특성으로 iv
를 제 2의 키로 사용할 수 있습니다.
CBC
의 복호화는 다음 과정을 거칩니다.
- 암호문을 블록 사이즈로 나눈다.
- 각 블록을 복호화 한 후 전 암호 블록과 XOR하여 평문 블록을 얻는다.
첫 블록은iv
를 XOR 한다.
CBC
는 암호화 전 평문의 길이를 블록의 배수로 맞춰줘야 합니다. 모자란 길이를 채우기 위해 임의의 값을 붙여주는데 이를 패딩 (Padding
) 이라고 합니다.
Hello How are you
// 위 평문은 17 Byte 이다. 만약 AES-256-CBC 로
// 암호화 하고자 하면 15 바이트가 모자르다.
Hello How are you000000000000000
// 패딩의 예시
// 모자라는 바이트를 '0'으로 채웠다.
패딩 또한 여러가지 표준이 존재합니다.
PKCS#7
을 주로 사용합니다.
// 블록의 크기는 16Byte 이다.
// Hello How Are y
// 위 데이터는 15Byte 이다.
// 1 바이트를 체우기 위해 '0x01' 1개를 패딩으로 추가한다.
48656c6c6f20486f7720617265207901
// Hello How Are you
// 위 데이터는 17Byte 이다.
// 15 바이트를 체우기 위해 '0x0f' 15개를 패딩으로 추가한다.
48656c6c6f20486f772061726520796f750f0f0f0f0f0f0f0f0f0f0f0f0f0f0f
// 만약 블록이 16의 배수로 떨어지면
// 0x10을 16개 추가한다.
// 이는 마지막 값이 패딩인지 데이터인지 모호함을 없애기 위함이다.
48656c6c6f20486f772061726520796f10101010101010101010101010101010
// PKCS#7 패딩, 언패딩 예시
func PaddingPKCS7(src []byte, size int) []byte {
if src == nil {
return bytes.Repeat([]byte{byte(size)}, size)
}
padlen := 1
for ((len(src) + padlen) % size) != 0 {
padlen = padlen + 1
}
pad := bytes.Repeat([]byte{byte(padlen)}, padlen)
return append(src, pad...)
}
func UnPaddingPKCS7(src []byte, size int) ([]byte, error) {
if len(src) == 0 {
return nil, errors.New("invalid padding")
}
padlen := int(src[len(src)-1])
if padlen <= 0 || padlen > size {
return nil, errors.New("invalid padding")
}
pad := src[len(src)-padlen:]
for _, n := range pad {
if int(n) != padlen {
return nil, errors.New("invalid padding")
}
}
return src[:len(src)-padlen], nil
}
이제 CBC
모드를 사용하여 긴 평문을 암호화 하겠습니다.
다음 함수로 CBC
모드 인터페이스를 얻습니다.
cip, _ := aes.NewCipher(key)
cipher.NewCBCEncrypter(cip, iv)
cipher.NewCBCDecrypter(cip, iv)
// iv는 cip의 블럭사이즈와 같은 길이의
// 바이트 슬라이스를 입력합니다.
cipher.NewCBCEncrypter(cip, iv)
,cipher.NewCBCDecrypter(cip, iv)
는cipher.BlockMode
인터페이스를 리턴합니다.
type BlockMode interface {
// 블록 사이즈 리턴
BlockSize() int
// 암/복호화 수행
CryptBlocks(dst, src []byte)
}
func main() {
str := `동해 물과 백두산이 마르고 닳도록
하느님이 보우하사 우리나라 만세.
무궁화 삼천리 화려 강산
대한 사람, 대한으로 길이 보전하세.`
// 랜덤 값으로 키 생성
key := make([]byte, 32)
rand.Read(key)
// 원본 데이터
plainText := []byte(str)
fmt.Printf("Source:\n%s\n\n", plainText)
// AES-256 생성
cip, err := aes.NewCipher(key)
if err != nil {
println(err.Error())
return
}
// 랜덤 값으로 iv 생성
iv := make([]byte, cip.BlockSize())
rand.Read(iv)
// CBC Encrypter, Decrypter 생성
cbcEncrypter := cipher.NewCBCEncrypter(cip, iv)
cbcDecrypter := cipher.NewCBCDecrypter(cip, iv)
// 원본 데이터 패딩
padded := PaddingPKCS7(plainText, cbcEncrypter.BlockSize())
// 암호화 데이터를 저장할 슬라이스 생성
cipherText := make([]byte, len(padded))
// 암호화
cbcEncrypter.CryptBlocks(cipherText, padded)
fmt.Printf("CipherText:\n%x\n\n", cipherText)
// 복호화 데이터를 저장할 슬라이스 생성
plainText2 := make([]byte, len(cipherText))
// 복호화
cbcDecrypter.CryptBlocks(plainText2, cipherText)
// 패딩 제거
unpadded, err := UnPaddingPKCS7(plainText2, cbcDecrypter.BlockSize())
if err != nil {
println(err.Error())
return
}
fmt.Printf("PlainText:\n%s\n\n", unpadded)
}