티스토리 뷰
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)
'golang' 카테고리의 다른 글
Golang Gin Gonic - 1. Basic REST API (1) | 2021.09.14 |
---|---|
Golang: AES-GSM에서 additional data의 활용 (0) | 2021.09.08 |
Golang: 사용자의 비밀번호를 저장하고 인증하는 서버 만들기 (0) | 2021.08.18 |
Golang: Marshaling 된 JSON을 Indent하여 보기 (0) | 2021.08.04 |
Golang: next permutation을 구현해보자 (0) | 2021.05.17 |
- Total
- Today
- Yesterday
- 독서후기
- websocket
- go
- golang
- folklore
- ChatGPT
- notion
- Shortcut
- 잡학툰
- 클린 애자일
- OpenAI
- 2023
- 인텔리제이
- 노션
- intellij
- Gin
- strange
- pool
- agile
- Bug
- JIRA
- 체호프
- 영화
- bun
- 독서
- solid
- postgres
- github
- API
- 제이펍
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |