본문 바로가기
자바/개념

[Java/개념] 암호화 & 복호화 알고리즘 사용 예시 - Cipher, Base64

by drCode 2025. 5. 21.
728x90
반응형

 

AES/ECB/PKCS5Padding

 

데이터를 송수신할 때, 암호화가 필요하다면 어떻게 해야할까?

 

API 를 요청하는 기관으로부터 데이터를 수신하는데, 특정 데이터는 암호화되어 Base64로 인코딩 되어 데이터를 수신한다고 한다.

 

우선  AES 알고리즘을 사용한 복호화 방식은 아래와 같았다.

 

1. 암호화를 풀기 위한 키 문자열을 바이트화 시켜서 키 크기(256)만큼 8로 나눈 값만큼의 길이의 내용을 키에 대입 

2. 인코딩 된 문자열 Base64로 디코딩하기 

3. Cipher Instance를 알고리즘/모드/패딩 방식을 정해서 Cipher 객체에 Cipher모드와 SecretKeySpec 을 키를 이용해 초기화

4. Cipher.doFinal 을 이용해 복호화된 바이트 배열을 얻고 UTF-8로 변환한 문자열 얻기

 

import org.apache.commons.codec.binary.Base64;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

 

위의 4개는 import  해야한다.

 

byte[] key = null;
final int AES_KEY_SIZE_256 = 256;

String keyStr = "abcdefghijkmlnopqrstuvwxyz012345";  // 임시로 만든 키 

key = keyStr.getBytes("UTF-8");

// AES-256 으로 암&복호화를 할 예정이기에 256으로 했다.
// 32바이트로 설정하기 위해 8로 나눈다.
// 암호화 알고리즘이 요구하는 키 길이에 맞추기 위해서이다.
key = Arrays.copyOf(key, AES_KEY_SIZE_256 / 8);

// 복호화 준비 시작
Cipher decryptCipher.getInstance("AES/ECB/PKCS5Padding");
decryptCipher.init(Cipher.DECRYPTE_MODE, new SecretKeySpec(key, "AES");
// 복호화 준비 완료

String trgtMsg = "암호화 후 인코딩 된 텍스트";

byte[] decodedTrgtMsg = Base64.decodeBase64(trgtMsg);

// 여기서 doFinal로 복호화하기 전, 바이트의 길이가 16으로 나누어 떨어져야 한다.
System.out.println(" 길이 : " + decodedTrgtMsg.length % 16); // 0이 아니면 에러가 발생한다.

byte[] decrytedMsgArr = decryptCipher.doFinal(decodedTrgtMsg);
String decrytedMsg = new String(decrytedMsgArr, "UTF-8");

System.out.println(" decrytedMsg : " + decrytedMsg); // 복호화된 텍스트

// 디코딩 및 복호화 끝

 

대칭키 암호화인 AES는 정해진 길이의 키만 받는다.

키 사이즈(bit) 바이트 수(byte) 설명
128비트 16바이트 AES-128
192비트 24바이트 AES-192
256비트 32바이트 AES-256

 

Arrays.copyOf()  과정은 지정된 길이로 바이트 배열을 잘라내거나 0으로 패딩하기 위함이다.

길이가 짧으면 뒤를 0으로 채워서 강제로 길이를 맞추는 용도이다.

 

만약 길이가 맞지 않으면 암/복호화 과정에서 예외가 발생한다.

 

그런데, 왜 16으로 나눈 나머지가 아니면(16의 배수가 아니면) 에러가 발생할까?

 

🔐 AES는 항상 "블록 단위"로 동작하는 블록 암호화 알고리즘

그리고 AES는 고정 블록 크기인 16바이트 (128비트) 단위로 데이터를 처리

 

AES의 구조적 특징은 아래와 같다.

구분 내용
암호화 알고리즘 AES (Advanced Encryption Standard)
블록 크기 항상 16바이트 (128비트)
키 크기 128, 192, 256 비트 (16, 24, 32바이트)

 

🔸 여기서 말하는 "블록 크기"는 입력 데이터의 크기

즉, 암호화할 평문은 16바이트 단위로 쪼개져서 암호화됨


✅ 그래서 왜 16의 배수가 되어야 하나?

Cipher.doFinal()은 입력 데이터를 AES 블록 크기(16바이트)에 맞춰야 한다.

그렇지 않으면 IllegalBlockSizeException 예외가 발생

 

평문의 길이는 16의 배수가 아닐때, 이는 패딩(Padding)이 필요한 경우다.

복호화 시에는 패딩을 반드시 제거해야한다.

 

패딩 종류로는 아래와 같다.

PKCS5Padding, PKCS7Padding : 일반적인 패딩 방식

NoPadding : 사용 시, 반드시 입력 길이가 16의 배수여야 한다.

 

Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");

 

PKCS5Padding을 사용하면 내부적으로 패딩이 추가되어 자동으로 길이를 맞춰주는데, 

복호화 할 때 예외가 발생할 수 있는 상황은 아래와 같다.

- 잘못된 키로 복호화할 때 → 패딩이 이상하게 나올 수 있다.

BadPaddingException 발생 또는

IllegalBlockSizeException (길이 불일치)

 

그래서 바이트의 길이는 16의 배수여야 하는 이유는..

- AES는 16바이트(128비트) 단위로 데이터를 처리하기 때문에, 암호화 대상도 반드시 16의 배수여야 한다.

- 이를 보완하기 위해 **패딩(Padding)**이 필요하고, 대부분의 경우 자동으로 처리된다 (PKCS5Padding 등).

- NoPadding을 사용할 때는 직접 블록 크기 맞추기 필요

- 블록 크기(16바이트)와 키 크기(128, 192, 256비트)는 별개 개념이니 혼동 주의!

 

키를 만들 때는..

위의 예처럼 키를 직접 문자열로 쓰기 보다는, SecureRandom이나 KeyGenerator 로 키를 생성하는 것이 더 안전하다.

SecureRandom random = new SecureRandom();
byte[] key = new byte[32]; // 256비트
random.nextBytes(key);
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey key = keyGen.generateKey();

 

 

이제 AES-256 으로 암호화하는 방법은 다음과 같다.

 

byte[] key = null;
final int AES_KEY_SIZE_256 = 256;

String keyStr = "abcdefghijkmlnopqrstuvwxyz012345";  // 임시로 만든 키 

key = keyStr.getBytes("UTF-8");

// AES-256 으로 암&복호화를 할 예정이기에 256으로 했다.
// 32바이트로 설정하기 위해 8로 나눈다.
// 암호화 알고리즘이 요구하는 키 길이에 맞추기 위해서이다.
key = Arrays.copyOf(key, AES_KEY_SIZE_256 / 8);

// 암호화 준비 시작
Cipher encryptCipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
encryptCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"));
// 암호화 준비 종료

 

여기서 getInstance 사용 시, "알고리즘/모드/패딩" 순으로 파라미터를 넣으면 된다.

 

AES : 알고리즘

ECB : 모드

PKCS5Padding : 패딩

 

파라미터로 넣을 수 있는 알고리즘의 종류는 아래와 같다.

AES (Advanced Encryption Standard)

DES, DESede (Triple DES)

RSA

Blowfish

RC2

ChaCha20 (JDK 11 이상) 등

 

파라미터로 넣을 수 있는 모드의 종류는 아래와 같다.

ECB (Electronic Codebook) - 가장 단순하지만 보안상 권장되지 않는다.

CBC (Cipher Block Chaining) - IV(초기화 벡터) 필요

CFB (cipher Feedback)

OFB (Output Feedback)

CTR (Counter mode)

GCM (Galois/Counter Mode - AES 전용, 인증 가능)

 

파라미터로 넣을 수 있는 모드의 종류는 아래와 같다.

NoPadding : 패딩 없음 → 입력 길이가 블록 크기의 배수여야 함

PKCS5Padding : 일반적으로 사용되는 블록 패딩

PKCS7Padding : PKCS5와 동일 (Java에서는 PKCS5로 표기)

ISO10126Padding, ZeroBytePadding  등 (라이브러리에 따라 다름)

 

알고리즘 모드 패딩 전체 문자열

알고리즘 모드 패딩 전쳋 문자열
AES ECB PKCS5Padding "AES/ECB/PKCS5Padding"
AES CBC PKCS5Padding "AES/CBC/PKCS5Padding"
AES GCM NoPadding "AES/GCM/NoPadding"
DES CBC PKCS5Padding "DES/CBC/PKCS5Padding"
Triple DES ECB PKCS5Padding "DESede/ECB/PKCS5Padding"
Blowfish CFB NoPadding "Blowfish/CFB/NoPadding"
RSA ECB PKCS1Padding "RSA/ECB/PKCS1Padding"

 

※ 주의 사항

1. CBC, GCM 등은 초기화 벡터(IV) 가 필요하다.

 - cipher.init(......, ivParameterSpec) 형태로 초기화 해야한다.

2. AES/ECB/PKCS5Padding 처럼 모드와 패딩을 생략할 경우, 기본값은 ECB 모드와 PKCS5Padding 이 사용된다.

Cipher.getInstance("AES")  // == Cipher.getInstance("AES/ECB/PKCS5Padding")

3. 사용할 수 있는 조합은 JDK에 포함된 security provider 또는 BouncyCastle 등 외부 provider에 따라 다르다.

 

사용 가능한 trasnformation 목록을 확인하는 방법은 아래처럼 출력해보면 된다.

for (Provider provider : Security.getProviders()) {
    for (Provider.Service service : provider.getServices()) {
        if ("Cipher".equals(service.getType())) {
            System.out.println(service.getAlgorithm());
        }
    }
}

 

 

알고리즘/모드/ 패딩 별 사용 예시

✅ 1. AES / ECB / PKCS5Padding (가장 기본)

🔹 특징

  • 간단하지만 보안 취약
  • IV 불필요
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class AES_ECB_Example {
    public static void main(String[] args) throws Exception {
        String key = "0123456789abcdef"; // 16바이트 (128bit)
        String plainText = "테스트 문자열";

        // 암호화
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
        cipher.init(Cipher.ENCRYPT_MODE, keySpec);
        byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));
        String encryptedBase64 = Base64.getEncoder().encodeToString(encrypted);
        System.out.println("암호화 결과: " + encryptedBase64);

        // 복호화
        cipher.init(Cipher.DECRYPT_MODE, keySpec);
        byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedBase64));
        System.out.println("복호화 결과: " + new String(decrypted, "UTF-8"));
    }
}
 

✅ 2. AES / CBC / PKCS5Padding

🔹 특징

  • IV 필요
  • ECB보다 보안 우수
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class AES_CBC_Example {
    public static void main(String[] args) throws Exception {
        String key = "0123456789abcdef"; // 16바이트
        String iv = "fedcba9876543210"; // 16바이트 IV
        String plainText = "테스트 문자열";

        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
        IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());

        // 암호화
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
        byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));
        String encryptedBase64 = Base64.getEncoder().encodeToString(encrypted);
        System.out.println("암호화 결과: " + encryptedBase64);

        // 복호화
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
        byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedBase64));
        System.out.println("복호화 결과: " + new String(decrypted, "UTF-8"));
    }
}

✅ 3. AES / GCM / NoPadding (권장, JDK 8 이상)

🔹 특징

  • 보안성 우수 (무결성 검증 포함)
  • IV 필수, 태그 자동 포함
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;

public class AES_GCM_Example {
    public static void main(String[] args) throws Exception {
        String key = "0123456789abcdef0123456789abcdef"; // 32바이트 = 256비트
        String plainText = "테스트 문자열";

        byte[] iv = new byte[12]; // GCM은 보통 12바이트 IV 사용
        new SecureRandom().nextBytes(iv);

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
        GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv); // 128-bit 인증 태그

        // 암호화
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec);
        byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));
        String encryptedBase64 = Base64.getEncoder().encodeToString(encrypted);
        System.out.println("암호화 결과: " + encryptedBase64);

        // 복호화
        cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);
        byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedBase64));
        System.out.println("복호화 결과: " + new String(decrypted, "UTF-8"));
    }
}

✅ 4. RSA / ECB / PKCS1Padding (비대칭)

🔹 특징

  • 공개키 암호 방식
  • 보통은 AES와 조합하여 키 전달용으로만 사용
import javax.crypto.Cipher;
import java.security.*;
import java.util.Base64;

public class RSA_Example {
    public static void main(String[] args) throws Exception {
        String plainText = "테스트 문자열";

        KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
        kpg.initialize(2048);
        KeyPair keyPair = kpg.generateKeyPair();

        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");

        // 암호화 (공개키)
        cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
        byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));
        String encryptedBase64 = Base64.getEncoder().encodeToString(encrypted);
        System.out.println("암호화 결과: " + encryptedBase64);

        // 복호화 (개인키)
        cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
        byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedBase64));
        System.out.println("복호화 결과: " + new String(decrypted, "UTF-8"));
    }
}

 

 

728x90
반응형

댓글