본문 바로가기
■Development■/《Security》

[Security] 대칭키 암복호화 완복 가이드 : 개념부터 Java 구현까지

by 은스타 2019. 9. 22.
반응형

대칭키 암호화 완벽 가이드: 개념부터 Java 구현까지

안녕하세요! 오늘은 정보 보안의 기본 중의 기본인 대칭키 암호화(Symmetric Key Encryption)에 대해 알아보겠습니다. 복잡한 암호화 개념을 쉽게 이해하고, Java에서 실제로 구현하는 방법까지 단계별로 살펴볼 예정입니다. 보안에 관심 있는 개발자나 학생들에게 유용한 내용이 될 것입니다.


목차

  1. 대칭키 암호화란?
  2. 대칭키 암호화의 작동 원리
  3. 주요 대칭키 암호화 알고리즘
  4. 대칭키 vs 비대칭키 암호화
  5. Java에서의 대칭키 암호화 구현
  6. 대칭키 암호화의 장단점과 활용 사례
  7. 대칭키 암호화 보안 강화 방법
  8. 자주 묻는 질문 (FAQ)

 

#1. 대칭키 암호화란?

대칭키 암호화는 데이터를 암호화하고 복호화할 때 동일한 키(비밀키)를 사용하는 암호화 방식입니다. 이는 마치 하나의 열쇠로 문을 잠그고 여는 것과 같은 원리입니다.

 

대칭키 암호화의 핵심 요소

  • 평문(Plaintext): 원본 메시지 또는 데이터
  • 암호문(Ciphertext): 암호화된 데이터
  • 암호화 알고리즘(Encryption Algorithm): 평문을 암호문으로 변환하는 수학적 과정
  • 대칭키(Symmetric Key): 암호화와 복호화에 모두 사용되는 비밀 값
  • 초기화 벡터(Initialization Vector, IV): 일부 알고리즘에서 사용되는 추가 입력값으로, 같은 평문이라도 다른 암호문을 생성하게 함

 

#2. 대칭키 암호화의 작동 원리

대칭키 암호화는 다음과 같은 기본 과정으로 작동합니다:

1. 암호화 과정

  1. 암호화하려는 원본 데이터(평문)를 준비합니다.
  2. 암호화 알고리즘과 비밀키를 사용하여 평문을 암호문으로 변환합니다.
  3. 생성된 암호문은 외부로 전송하거나 저장합니다.

2. 복호화 과정

  1. 수신자는 암호문을 받습니다.
  2. 동일한 암호화 알고리즘과 동일한 비밀키를 사용하여 암호문을 원래의 평문으로 복원합니다.

이러한 암호화 방식은 오랫동안 사용되어 왔으며, 컴퓨터가 등장하기 전부터 다양한 형태로 존재했습니다. 가장 유명한 고전적 예시는 시저 암호(Caesar cipher)로, 알파벳을 일정한 수만큼 이동시켜 암호화하는 방식입니다.

 

#3. 주요 대칭키 암호화 알고리즘

현대의 대칭키 암호화는 복잡한 수학적 연산을 기반으로 하며, 다양한 알고리즘이 개발되어 있습니다. 가장 널리 사용되는 알고리즘들을 살펴보겠습니다:

1. DES (Data Encryption Standard)

  • 개발 시기: 1970년대
  • 키 길이: 56비트
  • 특징: 한때 표준이었으나 현재는 키 길이가 짧아 보안에 취약함
  • 상태: 현재 사용하지 않는 것을 권장

2. 3DES (Triple DES)

  • 개발 시기: DES의 취약점을 보완하기 위해 개발
  • 키 길이: 168비트(실질적으로는 112비트)
  • 특징: DES 알고리즘을 3번 적용
  • 상태: 여전히 사용되지만 점차 AES로 대체 중

3. AES (Advanced Encryption Standard)

  • 개발 시기: 2001년에 NIST에 의해 표준으로 채택
  • 키 길이: 128, 192, 256비트
  • 특징: 현재 가장 널리 사용되는 대칭키 암호화 알고리즘
  • 상태: 현재 표준으로 사용됨

4. Blowfish

  • 개발 시기: 1993년
  • 키 길이: 32-448비트
  • 특징: 빠른 속도와 효율적인 성능
  • 상태: 여전히 사용되지만 후속작인 Twofish가 더 권장됨

5. Twofish

  • 개발 시기: 1998년
  • 키 길이: 최대 256비트
  • 특징: Blowfish의 후속 알고리즘, AES 공모에서 최종 후보 중 하나였음
  • 상태: 현재도 안전하다고 간주됨

6. ChaCha20

  • 개발 시기: 2008년
  • 키 길이: 256비트
  • 특징: 스트림 암호로, 특히 모바일 환경에서 효율적
  • 상태: 현대 암호화 프로토콜에서 널리 사용됨 (예: TLS)

이 중에서 현재 가장 많이 사용되고 권장되는 알고리즘은 AES입니다. AES는 미국 정부의 표준으로 채택되었으며, 금융 기관, 클라우드 서비스, 통신 등 다양한 분야에서 사용되고 있습니다.

 

#4. 대칭키 vs 비대칭키 암호화

대칭키 암호화를 더 잘 이해하기 위해, 또 다른 주요 암호화 방식인 비대칭키 암호화(Asymmetric Key Encryption)와 비교해 보겠습니다.

대칭키 암호화 (Symmetric Key Encryption)

  • 키의 수: 하나의 키만 사용 (암호화와 복호화에 동일한 키)
  • 속도: 빠름
  • 리소스 사용: 적음
  • 키 분배: 어려움 (안전한 채널이 필요)
  • 보안: 키 길이에 따라 다름
  • 적합한 용도: 대량 데이터 암호화, 세션 암호화

비대칭키 암호화 (Asymmetric Key Encryption)

  • 키의 수: 두 개의 키 사용 (공개키와 개인키)
  • 속도: 대칭키에 비해 느림
  • 리소스 사용: 많음
  • 키 분배: 용이함 (공개키는 공유 가능)
  • 보안: 일반적으로 높음
  • 적합한 용도: 디지털 서명, 키 교환, 인증

실제 사용 예시

대부분의 현대 보안 시스템에서는 두 방식을 조합하여 사용합니다:

  • 하이브리드 암호화: 비대칭키로 대칭키를 안전하게 전송하고, 실제 데이터는 대칭키로 암호화
  • TLS/SSL: 웹 브라우저와 서버 간의 보안 통신에 하이브리드 암호화 사용
  • PGP(Pretty Good Privacy): 이메일 암호화에 두 방식을 조합하여 사용

 

#5. Java에서의 대칭키 암호화 구현

이제 Java를 사용하여 대칭키 암호화를 구현하는 방법을 살펴보겠습니다. Java는 javax.crypto 패키지를 통해 다양한 암호화 기능을 제공합니다.

필요한 준비물

  • JDK 8 이상 설치
  • 기본 자바 개발 환경 (IDE 또는 텍스트 편집기)

1. AES 암호화 및 복호화 기본 예제

아래는 Java에서 AES 알고리즘을 사용한 기본적인 암호화 및 복호화 코드입니다:

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;

public class AESExample {

    public static void main(String[] args) throws Exception {
        // 암호화할 원본 텍스트
        String originalText = "안녕하세요! 이것은 암호화될 텍스트입니다.";
        System.out.println("원본 텍스트: " + originalText);

        // 키 생성
        SecretKey key = generateKey(128);

        // 초기화 벡터(IV) 생성
        byte[] iv = generateIV();

        // 암호화
        String encryptedText = encrypt(originalText, key, iv);
        System.out.println("암호화된 텍스트: " + encryptedText);

        // 복호화
        String decryptedText = decrypt(encryptedText, key, iv);
        System.out.println("복호화된 텍스트: " + decryptedText);
    }

    // AES 키 생성 메소드
    public static SecretKey generateKey(int keySize) throws Exception {
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(keySize);
        return keyGenerator.generateKey();
    }

    // 초기화 벡터(IV) 생성 메소드
    public static byte[] generateIV() {
        byte[] iv = new byte[16]; // AES 블록 크기는 16바이트
        new SecureRandom().nextBytes(iv);
        return iv;
    }

    // 암호화 메소드
    public static String encrypt(String plainText, SecretKey key, byte[] iv) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
        byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));

        // Base64로 인코딩하여 문자열로 반환
        return Base64.getEncoder().encodeToString(encrypted);
    }

    // 복호화 메소드
    public static String decrypt(String encryptedText, SecretKey key, byte[] iv) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);

        // Base64 디코딩 후 복호화
        byte[] decodedEncryptedText = Base64.getDecoder().decode(encryptedText);
        byte[] decrypted = cipher.doFinal(decodedEncryptedText);

        return new String(decrypted, StandardCharsets.UTF_8);
    }
}

이 코드는 다음과 같은 기능을 수행합니다:

  1. 128비트 AES 키를 생성합니다.
  2. 초기화 벡터(IV)를 생성합니다.
  3. 주어진 텍스트를 AES-CBC 모드로 암호화합니다.
  4. 암호화된 데이터를 Base64로 인코딩하여 문자열로 변환합니다.
  5. 암호화된 텍스트를 다시 복호화하여 원본 텍스트를 복원합니다.

2. 문자열 키를 사용한 AES 암호화 예제

실제 사용 시에는 문자열 형태의 비밀번호를 키로 변환하여 사용하는 경우가 많습니다. 다음은 이를 구현한 예제입니다:

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Base64;

public class AESWithPasswordExample {

    public static void main(String[] args) throws Exception {
        // 암호화할 원본 텍스트
        String originalText = "이것은 비밀번호로 암호화될 텍스트입니다.";
        System.out.println("원본 텍스트: " + originalText);

        // 비밀번호와 솔트
        String password = "매우강력한비밀번호";
        byte[] salt = generateSalt();

        // 초기화 벡터
        byte[] iv = generateIV();

        // 비밀번호로부터 키 생성
        SecretKey key = getKeyFromPassword(password, salt);

        // 암호화
        String encryptedText = encrypt(originalText, key, iv);
        System.out.println("암호화된 텍스트: " + encryptedText);

        // 복호화
        String decryptedText = decrypt(encryptedText, key, iv);
        System.out.println("복호화된 텍스트: " + decryptedText);
    }

    // 솔트 생성 메소드
    public static byte[] generateSalt() {
        byte[] salt = new byte[16];
        new SecureRandom().nextBytes(salt);
        return salt;
    }

    // 초기화 벡터 생성 메소드
    public static byte[] generateIV() {
        byte[] iv = new byte[16];
        new SecureRandom().nextBytes(iv);
        return iv;
    }

    // 비밀번호로부터 키 생성 메소드
    public static SecretKey getKeyFromPassword(String password, byte[] salt) throws Exception {
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 65536, 256);
        SecretKey tmp = factory.generateSecret(spec);
        return new SecretKeySpec(tmp.getEncoded(), "AES");
    }

    // 암호화 메소드
    public static String encrypt(String plainText, SecretKey key, byte[] iv) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
        byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));

        return Base64.getEncoder().encodeToString(encrypted);
    }

    // 복호화 메소드
    public static String decrypt(String encryptedText, SecretKey key, byte[] iv) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);

        byte[] decodedEncryptedText = Base64.getDecoder().decode(encryptedText);
        byte[] decrypted = cipher.doFinal(decodedEncryptedText);

        return new String(decrypted, StandardCharsets.UTF_8);
    }
}

이 예제에서는 PBKDF2WithHmacSHA256 알고리즘을 사용하여 비밀번호와 솔트로부터 안전한 키를 생성합니다. 이는 단순한, 짧은 비밀번호라도 암호학적으로 강력한 키로 변환해 주는 방법입니다.

3. 파일 암호화 예제

다음은 파일을 AES로 암호화하고 복호화하는 예제입니다:

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;

public class FileEncryptionExample {

    public static void main(String[] args) throws Exception {
        // 원본 파일과 암호화된 파일 경로
        String inputFile = "원본파일.txt";
        String encryptedFile = "암호화된파일.enc";
        String decryptedFile = "복호화된파일.txt";

        // 키와 IV 생성
        SecretKey key = AESExample.generateKey(256);
        byte[] iv = AESExample.generateIV();

        // 파일 암호화
        encryptFile(inputFile, encryptedFile, key, iv);
        System.out.println("파일 암호화 완료!");

        // 파일 복호화
        decryptFile(encryptedFile, decryptedFile, key, iv);
        System.out.println("파일 복호화 완료!");
    }

    // 파일 암호화 메소드
    public static void encryptFile(String inputFile, String outputFile, SecretKey key, byte[] iv) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);

        try (FileInputStream inputStream = new FileInputStream(inputFile);
             FileOutputStream outputStream = new FileOutputStream(outputFile)) {

            // IV를 파일 시작 부분에 저장
            outputStream.write(iv);

            byte[] buffer = new byte[64];
            int bytesRead;

            while ((bytesRead = inputStream.read(buffer)) != -1) {
                byte[] output = cipher.update(buffer, 0, bytesRead);
                if (output != null) {
                    outputStream.write(output);
                }
            }

            byte[] finalOutput = cipher.doFinal();
            if (finalOutput != null) {
                outputStream.write(finalOutput);
            }
        }
    }

    // 파일 복호화 메소드
    public static void decryptFile(String inputFile, String outputFile, SecretKey key, byte[] iv) throws Exception {
        try (FileInputStream inputStream = new FileInputStream(inputFile);
             FileOutputStream outputStream = new FileOutputStream(outputFile)) {

            // 파일에서 IV 읽기 (첫 16바이트)
            byte[] fileIv = new byte[16];
            inputStream.read(fileIv);

            // IV로 Cipher 초기화
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            IvParameterSpec ivSpec = new IvParameterSpec(fileIv);
            cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);

            byte[] buffer = new byte[64];
            int bytesRead;

            while ((bytesRead = inputStream.read(buffer)) != -1) {
                byte[] output = cipher.update(buffer, 0, bytesRead);
                if (output != null) {
                    outputStream.write(output);
                }
            }

            byte[] finalOutput = cipher.doFinal();
            if (finalOutput != null) {
                outputStream.write(finalOutput);
            }
        }
    }
}

이 예제에서는 다음과 같은 작업을 수행합니다:

  1. 파일을 읽어 AES-CBC 모드로 암호화하고 새 파일에 저장합니다.
  2. 초기화 벡터(IV)를 암호화된 파일의 시작 부분에 저장합니다.
  3. 암호화된 파일을 읽어 복호화하고 원본 파일을 복원합니다.

 

#6. 대칭키 암호화의 장단점과 활용 사례

대칭키 암호화는 많은 장점을 가지고 있지만, 몇 가지 제한사항도 있습니다. 이를 이해하고 적절한 상황에서 활용하는 것이 중요합니다.

장점

  1. 속도: 비대칭키 암호화보다 훨씬 빠름
  2. 효율성: 리소스 사용량이 적어 대용량 데이터 암호화에 적합
  3. 보안성: 충분히 긴 키와 현대적인 알고리즘을 사용하면 매우 안전함
  4. 구현 용이성: 상대적으로 구현이 간단함

단점

  1. 키 분배 문제: 안전한 키 교환 방법이 필요함
  2. 키 관리 복잡성: 통신 상대마다 다른 키를 관리해야 함
  3. 확장성: 다수의 사용자 간 통신에서 관리가 어려워짐
  4. 부인 방지 기능 부재: 누가 메시지를 암호화했는지 증명할 수 없음

주요 활용 사례

  1. 데이터 암호화: 데이터베이스 필드, 파일 시스템, 디스크 볼륨 등
  2. 통신 암호화: TLS/SSL에서 세션 데이터 암호화
  3. 토큰 및 쿠키 보안: 웹 애플리케이션에서 토큰 암호화
  4. 비밀번호 저장: 해싱과 함께 사용하여 비밀번호 보호
  5. 클라우드 데이터 보호: 클라우드에 저장되는 민감한 정보 보호

 

#7. 대칭키 암호화 보안 강화 방법

대칭키 암호화를 더 안전하게 사용하기 위한 몇 가지 핵심 방법을 살펴보겠습니다:

1. 강력한 키 사용

  • 충분한 키 길이: AES의 경우 최소 128비트, 가능하면 256비트 사용
  • 무작위성: 키는 암호학적으로 안전한 난수 생성기(CSPRNG)로 생성
  • 키 교체: 주기적으로 키를 변경하여 위험 최소화

2. 적절한 암호화 모드 선택

  • CBC (Cipher Block Chaining): 가장 일반적인 모드, IV 필요
  • GCM (Galois/Counter Mode): 인증 기능이 포함된 모드, 권장됨
  • CTR (Counter): 병렬 처리에 효율적인 모드
  • ECB (Electronic Codebook): 보안상 취약하므로 사용 지양

3. 초기화 벡터(IV) 관리

  • 무작위 IV: 매 암호화마다 새로운 랜덤 IV 사용
  • IV 전송: IV는 암호문과 함께 전송해도 괜찮음 (비밀이 아님)
  • IV 길이: 알고리즘 블록 크기와 동일해야 함 (AES의 경우 16바이트)

4. 패딩 방식 고려

  • PKCS5Padding/PKCS7Padding: 가장 일반적인 패딩 방식
  • 패딩 오라클 공격 방지: 패딩 오류를 구분할 수 없게 처리

5. 키 파생 및 저장

  • KDF (Key Derivation Function): PBKDF2, Argon2 등을 사용하여 비밀번호에서 키 파생
  • HSM (Hardware Security Module): 중요한 키는 하드웨어 보안 모듈에 저장
  • 키 저장소: Java KeyStore 등의 안전한 저장소 사용

6. 추가 보안 계층

  • 데이터 인증: HMAC 등을 사용하여 암호문의 무결성 검증
  • 솔트 사용: 키 파생 시 솔트를 사용하여 사전 공격 방지
  • 하이브리드 암호화: 비대칭키 암호화와 함께 사용하여 키 교환 문제 해결

 

#8. 자주 묻는 질문 (FAQ)

Q: AES와 DES 중 어떤 것을 사용해야 할까요?

A: 현대 애플리케이션에서는 AES를 사용하는 것이 강력하게 권장됩니다. DES는 키 길이가 짧아(56비트) 현재의 컴퓨팅 파워로 쉽게 깨질 수 있습니다. AES는 미국 정부 표준이며 현재까지 심각한 취약점이 발견되지 않았습니다.

Q: Java에서 가장 안전한 대칭키 암호화 방법은 무엇인가요?

A: Java에서는 AES-GCM 모드를 사용하는 것이 가장 권장됩니다. 이 모드는 암호화와 함께 인증(authenticity)도 제공하므로 데이터의 기밀성과 무결성을 모두 보장합니다. 키 크기는 256비트를 사용하고, 키 생성에는 SecureRandom을 활용하세요.

Q: 암호화 키를 어떻게 안전하게 보관해야 하나요?

A: 암호화 키를 안전하게 보관하는 방법은 다음과 같습니다:

  1. Java KeyStore 사용: 비밀번호로 보호된 키 저장소
  2. 환경 변수 대신 비밀 관리 서비스 사용: AWS KMS, HashiCorp Vault 등
  3. 하드코딩 금지: 소스 코드에 키를 절대 포함하지 마세요
  4. 메모리 보호: 키를 사용한 후 메모리에서 안전하게 제거 (char[] 배열 사용 후 0으로 덮어쓰기)
  5. 접근 제한: 암호화 키에 대한 접근을 최소한의 필요한 시스템이나 사용자로 제한

Q: 대칭키와 비대칭키 암호화를 함께 사용하는 방법은 무엇인가요?

A: 대칭키와 비대칭키 암호화를 함께 사용하는 가장 일반적인 방법은 다음과 같습니다:

  1. 대용량 데이터를 대칭키로 암호화
  2. 대칭키를 수신자의 공개키로 암호화
  3. 암호화된 데이터와 암호화된 대칭키를 함께 전송
  4. 수신자는 자신의 개인키로 대칭키를 복호화한 후, 해당 대칭키로 데이터를 복호화

이러한 하이브리드 접근 방식은 SSL/TLS, PGP 등 많은 보안 프로토콜에서 사용됩니다.

Q: 초기화 벡터(IV)는 왜 필요한가요?

A: 초기화 벡터(IV)는 동일한 평문이 항상 동일한 암호문으로 변환되는 것을 방지합니다. 이것은 패턴 분석 공격을 어렵게 만듭니다. IV는 비밀이 아니므로 암호문과 함께 전송해도 됩니다. 하지만 매번 새로운 랜덤 IV를 사용하는 것이 중요합니다.


결론

대칭키 암호화는 그 속도와 효율성 때문에 현대 정보 보안의 중요한 기둥입니다. 특히 AES와 같은 현대적인 알고리즘은 적절하게 구현되었을 때 매우 안전합니다.

Java에서 대칭키 암호화를 구현할 때는 다음 사항을 기억하세요:

  1. 안전한 알고리즘(AES)과 충분한 키 길이(256비트) 사용
  2. 적절한 암호화 모드(GCM 또는 CBC) 선택
  3. 안전한 키 생성 및 관리 방법 적용
  4. 초기화 벡터(IV)를 올바르게 사용
  5. 가능하면 키 교환에 비대칭키 암호화 활용

암호화는 정보 보안의 한 측면일 뿐이며, 전체 시스템 보안을 위해서는 인증, 권한 부여, 감사, 보안 코딩 관행 등 다양한 접근 방식이 필요합니다.

이 글을 통해 대칭키 암호화에 대한 이해를 높이고, Java 애플리케이션에서 안전한 암호화를 구현하는 데 도움이 되기를 바랍니다. 보안은 결코 완벽할 수 없지만, 모범 사례를 따름으로써 위험을 크게 줄일 수 있습니다.

긴 글 읽어주셔서 감사합니다.

끝.

반응형