티스토리 뷰

반응형

Photo by Alex Motoc on Unsplash

 

 

가장 널리 쓰이는 대칭키 암호화 알고리즘중 하나인 AES(Advanced Encryption Standard) 대해 간략히 알아보고 실제 Golang 코드를 들여다 보자

 

개요

 

비밀키 하나로 메시지를 암호화하고 다시 원래 메시지로 복호화 하는걸 대칭키(symmetric) 방식이라고 한다. 중에서도 시점 보안성과 성능을 충분히 만족하는 녀석이 AES라고 보면 되겠다.

 

AES-128, AES-192, AES-256

 

그런데 AES 비밀키(secret key) 길이(= 비트수) 128, 192, 256으로 나눌 있다. AES-128만으로도 충분히 보안이 우수하지만 AES-256이면 보안성은 더욱 나으면서도 성능도 나쁘지 않으니 성능에 매우 민감한 상황이 아니라면 AES-256 가면 된다고 생각한다.

 

참고링크: https://www.ubiqsecurity.com/blog/128bit-or-256bit-encryption-which-to-use/

 

AES 블록 암호화 선택

 

참고링크: https://stackoverflow.com/a/1220869/3382699

참고링크: https://security.stackexchange.com/a/52674

 

한 줄 요약: AES-256-GCM을 쓰자

 

ECB 그냥 쓰지말자.

CTR 병렬처리가 가능하다 == 빠르다

- CBC, OFB, CFB 순차적이다

CBC, OFB, CFB 비슷하지만 OFB, CFB 암호화만 필요하기에 저장 공간 이점이 있다.

XTR 스트림 데이터가 아닌 하드디스크/RAM등의 데이터 암호화에 널리 쓰인다

- 반대로 OFB, CFB 스트리밍 데이터의 암호화에 좋다.

OCB 암호화와 인증이 한번에 이루어져서 제일 좋지만 특허로 묶여있다.

- GCM OCB 유사하지만 특허에 묶여있지 않기에 현시점 최선의 선택

 

AES-256-GCM in Golang

GitHub: https://github.com/nicewook/golang-aes-gsm-example

 

가지 방법을 정리해 본다. 위 GitHub을 clone하여 각각 aes-gsm1, aes-gsm2, aes-gsm3 디렉토리에서 아래와 같이 테스트할 수 있다. 코드가 어렵지 않으니 쉽게 이해할 수 있을 것이다. 

$ go test . -v

바로 코드로 들어가보자. 차이점은 Seal() 메서드이다. 

  // Seal encrypts and authenticates plaintext, authenticates the
  // additional data and appends the result to dst, returning the updated
  // slice. The nonce must be NonceSize() bytes long and unique for all
  // time, for a given key.
  //
  // To reuse plaintext's storage for the encrypted output, use plaintext[:0]
  // as dst. Otherwise, the remaining capacity of dst must not overlap plaintext.
  Seal(dst, nonce, plaintext, additionalData []byte) []byte

 

TL;DR

 

- 번째 방법: dst nil 넣으면 암호화된 byte slice nonce 정보가 없기에 별도의 방법으로 이를 기억해두거나 전달해줘야 한다.

- 번째 방법: dst nonce 넣어주는 방법. 그러면 nonce 암호화된 byte slice 더해져서 리턴된다. , nonce 정보까지 포함되는 것이다. 

- 번째 방법: additionalData 까지 추가해주는 방법. additionalData 번째 secret key라고 생각하면 편하다. 추가적인 보안이다

 

번째 방법. 파라미터 dst nil 넣기

 

1) aes.NewCipher() secretKey 사용하는 새로운 block 생성한 다음

2) cipher.NewGCM() 으로 GCM cipher 생성한다

3) 임의의 nonce 12 바이트 생성한 다음

4) Seal() 메서드로 암호화 해준다.

 

이때 암호화된 ciphertext에는 nonce 정보가 포함되지 않기에 이를 별도로 리턴해주도록 했다.

이후에 복호화를 때에도 nonce 값을 알아야만 복호화가 가능하다.

이렇게 사용해야 하는 경우가 있을까 궁금하다.

 

// aes-gsm1/aes-gsm1.go - 복호화 하는 AES256GSMDecrypt() 함수는 생략

func AES256GSMEncrypt(secretKey []byte, plaintext []byte) ([]byte, []byte, error) {
  if len(secretKey) != 32 {
    return nil, nil, fmt.Errorf("secret key is not for AES-256: total %d bits", 8*len(secretKey))
  }
  // prepare AES-256-GSM cipher
  block, err := aes.NewCipher(secretKey)
  if err != nil {
    return nil, nil, err
  }
  aesgcm, err := cipher.NewGCM(block)
  if err != nil {
    return nil, nil, err
  }
  // make random nonce
  nonce := make([]byte, 12)
  if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
    return nil, nil, err
  }
  // encrypt plaintext
  ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)
  fmt.Println("--\nencryption start")
  fmt.Printf("nonce to use: %x\n", nonce)
  fmt.Printf("ciphertext: %x\n", ciphertext)
  return ciphertext, nonce, nil
}

 

번째 방법. 파라미터 dst nonce 넣어주기

 

번째 방법과 다른 부분은 Seal 메서드의 파라미터로 nonce 넣어준 것이다.

이렇게 하면 nonce 바이트 슬라이스에, 생성된 암호 바이트 슬라이스를 붙여서 ciphertext 리턴한다.

, ciphertext 내에 nonce 들어가 있는 것이다.

 

// aes-gsm2/aes-gsm2.go - AES256GSMEncrypt()

  // encrypt plaintext
  ciphertext := aesgcm.Seal(nonce, nonce, plaintext, nil)
  fmt.Println("--\nencryption start")
  fmt.Printf("nonce to use: %x\n", nonce)
  fmt.Printf("ciphertext: %x\n", ciphertext)
  return ciphertext, nil // nonce is included in ciphertext. no need to return

 

복호화 할때에 nonceSize 만큼으로 나누어서 nonce pure ciphertext 나누기만 하면 된다.

ciphertext nonce + pureCiphertext 인것이다.

 

// aes-gsm2/aes-gsm2.go - AES256GSMDecrypt()

  nonceSize := aesgcm.NonceSize()
  nonce, pureCiphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
  // decrypt ciphertext
  plaintext, err := aesgcm.Open(nil, nonce, pureCiphertext, nil)
  if err != nil {
    return nil, err
  }

 

참고. Encrypt에서와 마찬가지로 NonceSize()라는 메서드가 쓰인다.

 

디폴트 값은 12이다. 처음에 NewGCM()으로 생성할때 설정할 수도 있다. 그런데 12바이트(=96 bits)일까? GCM proposal에서 정의된 수식속의 값이 96이다. 다른 값을 쓰게 되면 추가적인 연산이 들어가게 된다.

 

링크: https://crypto.stackexchange.com/a/41610

 

참고. tagSize 뭘까?

 

무결성(integrity) 인증(authentication) 위해 추가하는 것이다. MAC이랄까?

 

링크: https://www.cryptosys.net/pki/manpki/pki_aesgcmauthencryption.html

AES with Galois/Counter Mode (AES-GCM) provides both authenticated encryption (confidentiality and authentication) and the ability to check the integrity and authentication of additional authenticated data (AAD) that is sent in the clear. AES-GCM is specified in NIST Special Publication 800-38D [SP800-38D].

 

 

참고. Golang 소스코드를 보면 NonceSize 디폴트 값은 12, TagSize 디폴트 값은 16이다.

 

// src/crypto/cipher/gcm.go

const (
  gcmBlockSize = 16
  gcmTagSize = 16
  gcmMinimumTagSize = 12 // NIST SP 800-38D recommends tags with 12 or more bytes.
  gcmStandardNonceSize = 12
)

 

번째 방법. additionalData 까지 추가해주기

 

마지막 파라미터인 additionalData 무엇일까? 암호화 때에 이걸 넣어주면, 복호화 때에 같은 값을 넣어줘야만 하는 번째 열쇠라고 생각하면 된다.

그럼 이걸 , 어떤 경우에 사용하는 걸까? 다음 포스팅에 실제로 사용하는 경우 하나를 예로 들어 보겠다.

- 블로그 포스팅 링크: https://jusths.tistory.com/233

 

// aes-gsm3/aes-gsm3.go - AES256GSMEncrypt()

  // encrypt plaintext with second key
  ciphertext := aesgcm.Seal(nonce, nonce, plaintext, secondKey)

// aes-gsm3/aes-gsm3.go - AES256GSMDecrypt()

  // decrypt ciphertext with second key
  plaintext, err := aesgcm.Open(nil, nonce, pureCiphertext, secondKey)

 

마지막으로 프로젝트 루트 디렉토리에서 테스트를 실행한 결과를 공유한다

→ go test ./... -v
=== RUN   TestAES256GSM1
secret key generated: cafb52a24ce3d69e83fde040d754c07b3fda32c9ea0a4bd41710e45eec8c14f0      
--
encryption start
nonce to use: adcd7965dbd87b70abbdc813
ciphertext: 7950c20966ddcb618be6a2d98abcdbe0a5d682d7d429d6bdc0729790eb6d205fa1f6591fd3f66e17
--
decryption start
ciphertext: 7950c20966ddcb618be6a2d98abcdbe0a5d682d7d429d6bdc0729790eb6d205fa1f6591fd3f66e17
nonce: adcd7965dbd87b70abbdc813
plaintext: 746869732073686f756c6420626520656e63727970746564
--- PASS: TestAES256GSM1 (0.00s)
PASS
ok      aes256gsm/aes-gsm1      (cached)
=== RUN   TestAES256GSM2
secret key generated: cafb52a24ce3d69e83fde040d754c07b3fda32c9ea0a4bd41710e45eec8c14f0
--
encryption start
nonce to use: ecef20840fb49979236f424e
ciphertext: ecef20840fb49979236f424e6358db40f212328dcab4516f767389dc7fbc31001638ad224e6f7cbaf0327066e7a2d4fd44ddf1be
--
decryption start
ciphertext: ecef20840fb49979236f424e6358db40f212328dcab4516f767389dc7fbc31001638ad224e6f7cbaf0327066e7a2d4fd44ddf1be
nonce: ecef20840fb49979236f424e
plaintext: 746869732073686f756c6420626520656e63727970746564
--- PASS: TestAES256GSM2 (0.00s)
PASS
ok      aes256gsm/aes-gsm2      (cached)
=== RUN   TestAES256GSM3
secret key generated: cafb52a24ce3d69e83fde040d754c07b3fda32c9ea0a4bd41710e45eec8c14f0
--
encryption start
nonce to use: 607a2f1a8071c95bf0d5e07f
ciphertext: 607a2f1a8071c95bf0d5e07f82825e10d3dd159811b951c21d899a2ea0547966825e153e3b2705751e6eb1df3cd6a72a56f8b519
--
decryption start
ciphertext: 607a2f1a8071c95bf0d5e07f82825e10d3dd159811b951c21d899a2ea0547966825e153e3b2705751e6eb1df3cd6a72a56f8b519
nonce: 607a2f1a8071c95bf0d5e07f
plaintext: 746869732073686f756c6420626520656e63727970746564
--- PASS: TestAES256GSM3 (0.00s)
PASS
ok      aes256gsm/aes-gsm3      (cached)

 

반응형
댓글
댓글쓰기 폼