blog-go-crypto/block at main · vompressor/blog-go-crypto
Contribute to vompressor/blog-go-crypto development by creating an account on GitHub.
코드

go 는 암호화를 위한 표준 라이브러리를 제공합니다.

crypto

crypto · pkg.go.dev

golang.org/x/crypto

crypto · pkg.go.dev

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 모드의 암호화는 다음 과정을 거칩니다.

  1. 긴 평문을 암호화 블록 사이즈로 나눈다.
  2. 각 블록은 암호화 되기 전에 이전 블록의 암호화 결과와 XOR 된다.
    첫 블록은 초기화 벡터 (iv) 를 XOR 한다.

여기서 iv 라는 개념이 등장하는데 iv 는 암호화에 사용하는 블록 사이즈와 길이가 같은 임의의 값으로 암호화에 사용된 iv 와 복호화에 사용되는 iv 가 같아야 복호화가 정상적으로 이루어집니다. 이러한 특성으로 iv 를 제 2의 키로 사용할 수 있습니다.

CBC 의 복호화는 다음 과정을 거칩니다.

  1. 암호문을 블록 사이즈로 나눈다.
  2. 각 블록을 복호화 한 후 전 암호 블록과 XOR하여 평문 블록을 얻는다.
    첫 블록은 iv 를 XOR 한다.

CBC 는 암호화 전 평문의 길이를 블록의 배수로 맞춰줘야 합니다. 모자란 길이를 채우기 위해 임의의 값을 붙여주는데 이를 패딩 (Padding) 이라고 합니다.

Hello How are you
// 위 평문은 17 Byte 이다. 만약 AES-256-CBC 로
// 암호화 하고자 하면 15 바이트가 모자르다.

Hello How are you000000000000000
// 패딩의 예시
// 모자라는 바이트를 '0'으로 채웠다.

패딩 또한 여러가지 표준이 존재합니다.

Padding (cryptography) - Wikipedia

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)
}