암호화 사용하기

이재홍 http**://www.pyrasis.com 2014.12.17 ~ 2015.02.07

AES 대칭키 알고리즘 사용하기

다음은 crypto/aes 패키지에서 제공하는 대칭키 알고리즘 함수입니다.

  • func NewCipher(key []byte) (cipher.Block, error): 대칭키 암호화 블록 생성
  • func (c *aesCipher) Encrypt(dst, src []byte): 평문을 AES 알고리즘으로 암호화
  • func (c *aesCipher) Decrypt(dst, src []byte): AES 알고리즘으로 암호화된 데이터를 평문으로 복호화

이번에는 AES 대칭키 알고리즘을 사용하여 데이터를 암호화하고 복호화해보겠습니다.

package main

import (
	"crypto/aes"
	"fmt"
)

func main() {
	key := "Hello, Key 12345" // 16바이트
	s := "Hello, world! 12"   // 16바이트

	block, err := aes.NewCipher([]byte(key)) // AES 대칭키 암호화 블록 생성
	if err != nil {
		fmt.Println(err)
		return
	}

	ciphertext := make([]byte, len(s))
	block.Encrypt(ciphertext, []byte(s)) // 평문을 AES 알고리즘으로 암호화
	fmt.Printf("%x\n", ciphertext)

	plaintext := make([]byte, len(s))
	block.Decrypt(plaintext, ciphertext) // AES 알고리즘으로 암호화된 데이터를 평문으로 복호화
	fmt.Println(string(plaintext))
}

실행 결과

a20455c66b97529fa756a0c9a7d2f329
Hello, world! 12

AES는 블록 암호화 알고리즘이므로 키와 암호화할 데이터의 크기가 일정해야 합니다. 여기서는 키와 데이터 모두 16바이트로 만들었습니다.

aes.NewCipher 함수에 키를 넣으면 암호화 블록(cipher.Block)이 리턴됩니다. 그리고 Encrypt 함수에 데이터(s)와 암호화된 데이터을 저장할 슬라이스(ciphertext)를 넣으면 암호화가 됩니다. 마찬가지로 Decrypt 함수에 암호화된 데이터와 복호화된 데이터를 저장할 슬라이스를 넣으면 복호화가 됩니다.

실무에서는 예제보다 데이터의 크기가 훨씬 큽니다. 즉, 길이가 긴 데이터는 16바이트씩 잘라서 암호화하면 됩니다. 하지만 데이터를 잘라서 암호화하면 보안에 취약해지며 이런 방식을 ECB(Electronic Codebook)라고 합니다.

블록 암호 운용 방식
블록 암호 운용 방식에 대한 자세한 내용은 다음 링크를 참조하기 바랍니다.

긴 데이터를 안전하게 암호화하기 위해 대칭키 알고리즘은 다양한 운용 방식을 제공합니다. 그중 CBC(Cipher Block Chaining) 방식을 사용하여 암호화를 해보겠습니다.

다음은 crypto/cipher 패키지에서 제공하는 암호화 운용 모드 함수입니다.

  • func NewCBCEncrypter(b Block, iv []byte) BlockMode: 암호화 블록과 초기화 벡터로 암호화 블록 모드 인스턴스 생성
  • func (x *cbcEncrypter) CryptBlocks(dst, src []byte): 암호화 블록 모드 인스턴스로 암호화
  • func NewCBCDecrypter(b Block, iv []byte) BlockMode: 암호화 블록과 초기화 벡터로 복호화 블록 모드 인스턴스 생성
  • func (x *cbcDecrypter) CryptBlocks(dst, src []byte): 복호화 블록 모드 인스턴스로 복호화

다음은 io 패키지에서 제공하는 읽기 함수입니다.

  • func ReadFull(r Reader, buf []byte) (n int, err error): io.Reader에서 buf의 길이만큼 데이터를 읽음
package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"fmt"
	"io"
)

func encrypt(b cipher.Block, plaintext []byte) []byte {
	if mod := len(plaintext) % aes.BlockSize; mod != 0 { // 블록 크기의 배수가 되어야함
		padding := make([]byte, aes.BlockSize-mod)   // 블록 크기에서 모자라는 부분을
		plaintext = append(plaintext, padding...)    // 채워줌
	}

	ciphertext := make([]byte, aes.BlockSize+len(plaintext)) // 초기화 벡터 공간(aes.BlockSize)만큼 더 생성
	iv := ciphertext[:aes.BlockSize] // 부분 슬라이스로 초기화 벡터 공간을 가져옴
	if _, err := io.ReadFull(rand.Reader, iv); err != nil { // 랜덤 값을 초기화 벡터에 넣어줌
		fmt.Println(err)
		return nil
	}

	mode := cipher.NewCBCEncrypter(b, iv) // 암호화 블록과 초기화 벡터를 넣어서 암호화 블록 모드 인스턴스 생성
	mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext) // 암호화 블록 모드 인스턴스로
                                                                // 암호화

	return ciphertext
}

func decrypt(b cipher.Block, ciphertext []byte) []byte {
	if len(ciphertext)%aes.BlockSize != 0 { // 블록 크기의 배수가 아니면 리턴
		fmt.Println("암호화된 데이터의 길이는 블록 크기의 배수가 되어야합니다.")
		return nil
	}

	iv := ciphertext[:aes.BlockSize]        // 부분 슬라이스로 초기화 벡터 공간을 가져옴
	ciphertext = ciphertext[aes.BlockSize:] // 부분 슬라이스로 암호화된 데이터를 가져옴

	plaintext := make([]byte, len(ciphertext)) // 평문 데이터를 저장할 공간 생성
	mode := cipher.NewCBCDecrypter(b, iv)      // 암호화 블록과 초기화 벡터를 넣어서
                                                   // 복호화 블록 모드 인스턴스 생성
	mode.CryptBlocks(plaintext, ciphertext)    // 복호화 블록 모드 인스턴스로 복호화

	return plaintext
}

func main() {
	key := "Hello Key 123456" // 16바이트

	s := `동해 물과 백두산이 마르고 닳도록
하느님이 보우하사 우리나라 만세.
무궁화 삼천리 화려강산
대한 사람, 대한으로 길이 보전하세.`

	block, err := aes.NewCipher([]byte(key)) // AES 대칭키 암호화 블록 생성
	if err != nil {
		fmt.Println(err)
		return
	}

	ciphertext := encrypt(block, []byte(s)) // 평문을 AES 알고리즘으로 암호화
	fmt.Printf("%x\n", ciphertext)

	plaintext := decrypt(block, ciphertext) // AES 알고리즘 암호문을 평문으로 복호화
	fmt.Println(string(plaintext))

}

실행 결과

6b39496c974990a9890f49ec962f92cd00f3009f724e3e6a7bcdd7b37867... (생략)
동해 물과 백두산이 마르고 닳도록... (생략)

먼저 블록 암호화는 암호화할 데이터의 길이가 블록 크기(aes.BlockSize)의 배수라야 합니다. 따라서 다음과 같이 블록 크기의 배수가 될 수 있도록 모자라는 부분을 채워줍니다.

if mod := len(plaintext) % aes.BlockSize; mod != 0 { // 블록 크기의 배수가 되어야함
	padding := make([]byte, aes.BlockSize-mod)   // 블록 크기에서 모자라는 부분을
	plaintext = append(plaintext, padding...)    // 채워줌
}

CBC 운용 방식은 초기화 벡터(Initialization Vector, IV)를 첫 블록에 배치시키고, 각 블록은 이전 블록의 암호화 결과와 XOR됩니다. 따라서 초기화 벡터는 암호화할 때마다 매번 다른 값으로 생성해야 합니다.

ciphertext := make([]byte, aes.BlockSize+len(plaintext)) // 초기화 벡터 공간(aes.BlockSize)만큼 더 생성
iv := ciphertext[:aes.BlockSize] // 부분 슬라이스로 초기화 벡터 공간을 가져옴
if _, err := io.ReadFull(rand.Reader, iv); err != nil { // 랜덤 값을 초기화 벡터에 넣어줌
	fmt.Println(err)
	return nil
}

암호화 데이터를 저장할 슬라이스는 초기화 벡터가 들어갈 공간(aes.BlockSize)만큼 더 생성해줍니다. 그리고 rand.Readerio.ReadFull 함수로 읽으면 랜덤 값을 얻을 수 있습니다. 이 랜덤 값을 초기화 벡터 iv에 넣어줍니다.

cipher.NewCBCEncrypter 함수에 암호화 블록 b와 초기화 벡터 iv를 넣어준 뒤 CryptBlocks 함수로 암호화를 하면 됩니다.

mode := cipher.NewCBCEncrypter(b, iv) // 암호화 블록과 초기화 벡터를 넣어서 암호화 블록 모드 인스턴스 생성
mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext) // 암호화 블록 모드 인스턴스로 암호화

이번에는 복호화를 알아보겠습니다. 복호화를 할 때 암호화된 데이터의 길이는 블록 크기의 배수가 되어야 합니다. 따라서 복호화하기 전에 항상 길이를 검사합니다.

if len(ciphertext)%aes.BlockSize != 0 { // 블록 크기의 배수가 아니면 리턴
	fmt.Println("암호화된 데이터의 길이는 블록 크기의 배수가 되어야합니다.")
	return nil
}

암호화된 데이터(ciphertext)의 첫 블록에서 iv를 꺼냅니다. 그리고 cipher.NewCBCDecrypter 함수에 암호화 블록 b와 초기화 벡터 iv를 넣어준 뒤 CryptBlocks 함수로 복호화를 하면 됩니다.

iv := ciphertext[:aes.BlockSize]        // 부분 슬라이스로 초기화 벡터 공간을 가져옴
ciphertext = ciphertext[aes.BlockSize:] // 부분 슬라이스로 암호화된 데이터를 가져옴

plaintext := make([]byte, len(ciphertext)) // 평문 데이터를 저장할 공간 생성
mode := cipher.NewCBCDecrypter(b, iv)      // 암호화 블록과 초기화 벡터를 넣어서
                                           // 복호화 블록 모드 인스턴스 생성
mode.CryptBlocks(plaintext, ciphertext)    // 복호화 블록 모드 인스턴스로 복호화

저작권 안내

이 웹사이트에 게시된 모든 글의 무단 복제 및 도용을 금지합니다.
  • 블로그, 게시판 등에 퍼가는 것을 금지합니다.
  • 비공개 포스트에 퍼가는 것을 금지합니다.
  • 글 내용, 그림을 발췌 및 요약하는 것을 금지합니다.
  • 링크 및 SNS 공유는 허용합니다.

Published

01 June 2015