암호화(encryption)는 데이터를 안전하게 보호하기 위해 그 데이터를 특정 알고리즘을 사용해 변환하는 과정으로 암호화된 데이터는 원래의 의미를 이해할 수 없는 형태로 바뀌며, 이를 암호문(ciphertext)라고 한다. 오직 적절한 키를 가진 사람만이 이를 다시 원래의 데이터(평문, plaintext)로 복호화(Decryption)할 수 있습니다.
대칭키 암호화는 암호화와 복호화에 같은 키를 사용하는 방식으로, 암호화된 데이터를 복호화하기 위해서는 동일한 키가 필요합니다.
작동 방식
키 생성 : 하나의 비밀키(대칭키) 생성
↓
암호화 : 평문(원본 데이터)을 비밀키로 사용하여 암호화
↓
복호화 : 암호화된 데이터를 동일한 비밀키로 복호화하여 원래의 평문으로 변환
대표적인 알고리즘
- AES(Advanced Encryption Standard) : 현재 가장 많이 사용되는 대칭키 암호화 알고리즘
- DES(Data Encryption Standard) : 과거에 많이 사용되었지만, 현재는 보안 취약성 때문에 거의 사용하지 않음
- 3DES(Triple DES) : DES를 세 번 연속 적용하는 방식으로, DES보다 강력한 보안을 제공하지만, 현재의 기준에서는 안전하지 않음
비대칭키 암호화는 두 개의 서로 다른 키(공개키와 개인키)를 사용하는 방식입니다. 한 키로 암호화한 데이터는 다른 키로만 복호화할 수 있습니다.
작동 방식
키 생성 : 공개키와 개인키 한 쌍이 생성, 공개키는 누구나 접근 가능, 개인키는 소유자만이 보관
↓
암호화 :
- 공개키 암호화 : 데이터를 공개키로 암호화, 암호화된 데이터를 복호화 하려면 해당 공개키와 쌍을 이루는 개인키가 필요
- 개인키 암호화 : 개인키로 암호화하면, 공개키로만 복호화 가능
↓
복호화 : 암호화된 데이터를 대응되는 키로만 복호화할 수 있습니다. 예를 들어, 공개키로 암호화된 데이터를 개인키로 복호화하거나, 개인키로 암호화된 데이터를 공개키로 복호화합니다.
대표적인 알고리즘
- RSA(Rivest-Shamir-Adleman) : 가장 널리 사용되는 비대칭키 암호화 알고리즘으로, 공개키 암호화와 디지털 서명에 모두 사용
- ECC(Elliptic Curve Cryptography) : RSA보다 짧은 키 길이로도 높은 보안을 제공하는 알고리즘으로, 모바일 기기와 같은 자원이 제한된 환경에서 자주 사용
- DSA(Digital Signature Algorithm) : 디지털 서명을 생성하는 데 사용되는 표준 알고리즘
해싱(Hashing)은 입력 데이터를 고정된 크기의 고유한 해시값으로 변환하는 과정을 의미합니다. 이 과정에서 사용되는 함수는 해시 함수라고 하며, 해싱의 주요 목적은 데이터의 무결성을 확인하거나, 데이터를 효율적으로 검색 및 인덱싱 하기 위함 입니다. 해싱은 암호화와 다르게 원본 데이터를 해시값으로 변환하는 것은 가능 하지만, 해시값으로부터 원본 데이터를 복구하는 것은 불가능합니다.
대표적인 해시 함수
- MD5(Message Digest Algorithm 5) : 128비트(16바이트)의 해시값 생성, 과거에 주로 사용되었지만 현재는 충돌 공격이 가능하다는 이유로 보안 취약점이 있음
- SHA-1(Secure Hash Algorithm 1) : 160비트(20바이트)의 해시값을 생성, MD5보다 보안성이 뛰어나지만, 마찬가지로 충돌 공격에 취약하다는 이유로 현재 사용이 권장되지 않음
- SHA-2(Secure Hash Algorithm 2) : SHA-256(256비트 해시값), SHA-512(512비트 해시값) 등이 있으며, 현재 널리 사용되는 해시 함수 중 하나
- SHA-3(Secure Hash Algorithm 3) : 최신 해시 표준으로, SHA-2와는 다른 알고리즘 사용, 다양한 해시 크기(224, 256, 384, 512비트)를 지원하며, 높은 보안성 제공
- bcrypt, scrypt, Argon2 : 비밀번호 해싱에 사용되는 함수로, 단순한 해시 함수와 달리 계산을 느리게 하여 공격을 어렵게 만듬
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"io"
"log"
"github.com/gofiber/fiber/v2"
)
// AES 대칭키 암호화 함수
func encryptAES(key []byte, plaintext string) (string, error) {
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return "", err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], []byte(plaintext))
return hex.EncodeToString(ciphertext), nil
}
// AES 복호화 함수
func decryptAES(key []byte, ciphertext string) (string, error) {
cipherText, _ := hex.DecodeString(ciphertext)
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
iv := cipherText[:aes.BlockSize]
cipherText = cipherText[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(cipherText, cipherText)
return string(cipherText), nil
}
// 유저 데이터 생성(암호화)
userroute.Post("/create", func(c *fiber.Ctx) error {
model := encryption.User{}
if err := c.BodyParser(&model); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
key := []byte("thisis32bitlongpassphraseimusing") // 32바이트 키 (AES-256)
encryptedPwd, err := encryptAES(key, model.Pwd)
if err != nil {
return err
}
// 암호화 비밀번호 데이터 할당
model.Pwd = encryptedPwd
// 사용자 데이터 생성
errMsg := model.Create()
if errMsg.Failure {
return c.JSON(errMsg)
}
return c.JSON(model)
})
// 로그인 단계 비밀번호 복호화 검증
userroute.Post("/login", func(c *fiber.Ctx) error {
type Login struct {
Id string `json:"id"`
Pwd string `json:"pwd"`
}
login := Login{}
if err := c.BodyParser(&login); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
model, _ := encryption.FindUserData(login.Id)
key := []byte("thisis32bitlongpassphraseimusing")
decryptedPwd, err := decryptAES(key, model.Pwd)
if err != nil || decryptedPwd != login.Pwd {
return c.Status(401).JSON(fiber.Map{"msg": "비밀번호가 일치하지 않습니다."})
}
return c.JSON(fiber.Map{"msg": "로그인 성공"})
})
위 이미지를 통해 입력된 비밀번호 데이터 "test@111333"이 "efe151b04e3591347550cb55e3a808739980600913612c4fd37c03"으로 암호화 된 것을 확인할 수 있고, 암호화에 사용했던 키를 복호화 과정에서도 동일하게 사용하여 검증할 수 있습니다.
import (
"crypto/rsa"
"crypto/rand"
"log"
"github.com/gofiber/fiber/v2"
"encoding/base64"
)
var keyStore = map[string]*rsa.PrivateKey{} // 사용자의 비밀키를 임시로 저장하는 맵 (실제 구현에서는 안전하게 저장해야 함)
func encryptRSA(publicKey *rsa.PublicKey, plaintext string) (string, error) {
ciphertext, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, []byte(plaintext), nil)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
func decryptRSA(privateKey *rsa.PrivateKey, ciphertext string) (string, error) {
cipherText, _ := base64.StdEncoding.DecodeString(ciphertext)
plaintext, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, cipherText, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}
userroute.Post("/create", func(c *fiber.Ctx) error {
model := encryption.User{}
if err := c.BodyParser(&model); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
log.Print("입력받은 비밀번호 데이터 : ", model.Pwd)
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048) // RSA 키 생성
publicKey := &privateKey.PublicKey
keyStore[model.Id] = privateKey // 비밀키를 임시로 저장 (실제 구현에서는 데이터베이스나 안전한 저장소 사용)
encryptedPwd, err := encryptRSA(publicKey, model.Pwd)
if err != nil {
return err
}
model.Pwd = encryptedPwd
log.Print("암호화된 비밀번호 데이터 : ", model.Pwd)
errMsg := model.Create()
if errMsg.Failure {
return c.JSON(errMsg)
}
return c.JSON(model)
})
userroute.Post("/login", func(c *fiber.Ctx) error {
type Login struct {
Id string `json:"id"`
Pwd string `json:"pwd"`
}
login := Login{}
if err := c.BodyParser(&login); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
model, _ := encryption.FindUserData(login.Id)
privateKey := keyStore[login.Id] // 사용자의 비밀키를 가져옴
if privateKey == nil {
return c.Status(500).JSON(fiber.Map{"msg": "비밀키를 찾을 수 없습니다."})
}
decryptedPwd, err := decryptRSA(privateKey, model.Pwd)
if err != nil || decryptedPwd != login.Pwd {
return c.Status(401).JSON(fiber.Map{"msg": "비밀번호가 일치하지 않습니다."})
}
return c.JSON(fiber.Map{"msg": "로그인 성공"})
})
위 이미지를 통해 입력된 비밀번호 데이터 "test@111333"이 "HJgRX91ExicvLcqjOI18H9w5IZx/lL8orihltxCwbvSXVCZ5AB5BtQ7UUJZTMtDfQZK0A19CxYo0i5F/ANLbAi/CV5jOq7E6WAdmyngNrT1ybEa3+HhQPOFK8GLT94EkNhLATek3SLe4HXNo8+RCIUQlzPJGzSIqOLN/8p3t040aUCFMu+ozAyg65Cuo7ZAenf2Pwj8Oa4ytYGrGnrfQWB0RYac0O/vtBMsNGeazhifc1IsNF3nl8nUckd9pV7kiYfzn4S30xArfhaVop9h9rQPVquLP06xdHtC+iNXMYVyBQ1YTBwxxSa2cmnH+kqPR44jo7rviWD1FSeA3EfWWHw=="으로 암호화 된 것을 확인할 수 있고, 대칭키와 다르게 비대칭키 암호화는 사용자의 비밀키를 DB에 저장하거나 키 관리 서비스 등 안전하게 저장해야합니다.(편의를 위해 위 코드는 Map 저장)
import (
"golang.org/x/crypto/argon2"
"encoding/base64"
"log"
"github.com/gofiber/fiber/v2"
)
// Argon2 해싱 함수
func hashPasswordArgon2(password string) string {
salt := []byte("somesalt") // 실제로는 랜덤한 솔트를 사용해야 함
hash := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)
return base64.RawStdEncoding.EncodeToString(hash)
}
// Argon2 비밀번호 검증 함수
func verifyPasswordArgon2(hashedPwd, plainPwd string) bool {
salt := []byte("somesalt") // 해시할 때 사용한 솔트와 동일해야 함
hash := argon2.IDKey([]byte(plainPwd), salt, 1, 64*1024, 4, 32)
return base64.RawStdEncoding.EncodeToString(hash) == hashedPwd
}
userroute.Post("/create", func(c *fiber.Ctx) error {
model := encryption.User{}
if err := c.BodyParser(&model); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
log.Print("입력받은 비밀번호 데이터 : ", model.Pwd)
hashedPwd := hashPasswordArgon2(model.Pwd)
model.Pwd = hashedPwd
log.Print("해싱된 비밀번호 데이터 : ", model.Pwd)
errMsg := model.Create()
if errMsg.Failure {
return c.JSON(errMsg)
}
return c.JSON(model)
})
userroute.Post("/login", func(c *fiber.Ctx) error {
type Login struct {
Id string `json:"id"`
Pwd string `json:"pwd"`
}
login := Login{}
if err := c.BodyParser(&login); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
model, _ := encryption.FindUserData(login.Id)
if !verifyPasswordArgon2(model.Pwd, login.Pwd) {
return c.Status(401).JSON(fiber.Map{"msg": "비밀번호가 일치하지 않습니다."})
}
return c.JSON(fiber.Map{"msg": "로그인 성공"})
})
``
위 이미지를 통해 입력된 비밀번호 데이터 "test@111333"이 "1nfFlzL6succYEdqzyFHqhKvj3mBa9x208s00cm2gGk"으로 암호화 된 것을 확인할 수 있습니다.
좋은글 구경하고 갑니다~