웹 서비스나 내부 시스템에서 중요한 데이터(개인정보, 파일 등)를 보호할 때 자주쓰는 암호화 조합라고 함
AES + CBC 모드 + PKCS5Padding + Base64
암호화 과정
평문(문자열)
AES/CBC/PKCS5로 암호화
> Base64로 문자 형태 변경
> 저장 및 전송
복호화 과정
Base64 문자열
Base64 디코딩
> AES/CBC/PKCS5로 복호화
> 평문 복원
1. AEB
AES(Advanced Encryption Standard)는 대칭 암호키임
대칭키란 암호화/복호화 할 때 쓰는 키가 같다는 뜻
AES는 데이터를 16바이트(128bit) 단위로 끊어서 처리하는 블록 암호
키 길이는 보통 128bit, 192bit, 256bit를 사용 (16, 24, 32 바이트)
블록 사이즈 : 16바이트(고정)
키 길이 : 16/24/32 바이트 중 하나
빠르고, 검증된 알고리즘이라 실무에서 쓴다함 (여기도 씀)
2. CBC 모드 (블록들을 체인처럼 엮어서 암호화)
AES는 기본적으로 "한 블록(16바이트)을 암호화하는 함수"
여러 블록을 이어 암호화하려면 "운영 모드(Mode)"가 필요
그중 하나가 CBC(Cipher Block Chaining)
단순히 각 블록을 AES로만 암호화하면
같은 평문 블록 > 항상 같은 암호문 블록 > 패턴이 그대로 들어날 수 있음(보안 취약)
그래서 CBC는 블록끼리 엮어서 암호화함
2.1 CBC 암호화 방식
1. 첫 블록(P1)을 IV(초기화 백터)와 XOR
2. 그 결과를 AES로 암호화
3. 그 결과를 C1이라고 할 때 (첫 번째 암호화 블록)
C1 = AES_Encrypt(P1 XOR IV)
두 번째 블록부터
평문(P2)과 직전 암호문(C1)을 XOR
AES로 암호화 > C2
C2 = AES_Encrypt(P2 XOR C1)
C3 = AES_Encrypt(P3 XOR C2)
...
여기서 중요한 점
- IV(Initialization Vector) : 첫 블록용 초기값, 길이 16바이트
- 이전 암호문이 다음 블록과 암호에 영향을 미침(체인처럼 연결)
2.2. CBC 복호화
복호화는 반대로 진행함
P1 = AES_Encrypt(C1) XOR IV
P2 = AES_Encrypt(C2) XOR C1
P3 = AES_Encrypt(C3) XOR C2
즉 복호화는 반드시
암호문 전체, 같은 IV, 같은 secretkey가 모두 필요함
3. PKCS5Padding
AEC는 16바이트씩 끊어서 처리하는데 평문 길이가 항상 16의 배수일 것이라는 보장이 없음
이럴 때 부족한 부분을 채우는 규칙이 패딩임
그 중 하나가 PKCS5Padding(PKCS7과 거의 동일함)
3.1 PKCS5Padding 규칙
1. 블록 크기 : 16바이트 (AES 기준)
2. 평문 길이를 16으로 나눈 나머지를 구함
3. 부족한 만큼 n 바이트를 채우는데 n 값 자체를 n번 반복해서 채움
예를 들어
평문 길이가 13바이트라면 = 3바이트 부족
> 0x03 0x03 0x03 추가
평문이 딱 16바이트라면 16바이트를 통째로 패딩으로 추가
> 0x10(16진수 16) 16개
복호화 시에는
마지막 바이트 값을 읽어서 n으로 간주
마지막 n바이트를 잘라냄
이렇게 하면 암호화 전의 원래 데이터를 정확히 복구할 수 있음
4. Base64
AES로 암호화한 결과는 바이너리 데이터임
이걸 그대로
JSON, URL 파라미터, 텍스트 기반 로그, DB의 문자열 컬럼에
넣으면 깨지거나, 제어문자 때문에 문제를 일으킬 수 있음
그래서 보통
바이너리 암호문 > Base64라는 형식으로 인코딩 > 영문 + 숫자 + 몇몇 기호로만 이루어진 문자열 로 바꿈다고 함
예) 바이너리 0x93 0xA1 0xAF
Base64 : k6Gvx93j....
5. 정리
- 평문 문자열 > 바이트로 변환
- PKCS5 패딩 적용
길이를 16의 배수로 맞춤 - AES/CBC/PKCS5Padding으로 암호화
키(Secretkey), IV 사용
결과 : 바이너리 암호문 - Base64 인코딩
바이너리 암호문 > Base64 문자열 - 해당 Base64 문자열을 DB/파일등이 저장
6. 복호화
- Base64 문자열 > 디코딩
Base64 문자열 > 바이너리 암호문 - AES/CBC/PKCS5Padding으로 복호화
암호화할 때와 같은 키, 같은 IV 사용
결과 : 패딩이 붙어 있는 평문 바이트 - PKCS5 패딩 제거
- UTF-8 문자열로 변환
7. 예제 (JAVA)
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* - 대칭키 알고리즘: AES
* - 블록 모드: CBC (Cipher Block Chaining)
* - 패딩 방식: PKCS5Padding
* - 인코딩: UTF-8
* - 암호문 표현: Base64 문자열
*/
public class App {
/**
* 비밀키(Secret Key)
*
* AES 는 16/24/32 바이트 길이 키 사용
* - 16 byte → AES-128
* - 24 byte → AES-192
* - 32 byte → AES-256
*
* 여기서는 16바이트 AES-128
* getBytes("UTF-8") 했을 때 정확히 16바이트가 나와야 됨
*/
private static final String SECRET_KEY = "secretkey1234567"; // 16 bytes → AES-128
/**
* IV(Initialization Vector, 초기화 벡터)
*
* CBC 모드에서는 첫 번째 블록을 암호화할 때 사용할 "초기값"이 필요
* 이 값이 IV 이고, AES 에서는 항상 16바이트(블록 크기와 동일)여야 됨
*
* - 동일한 평문 + 동일한 키를 쓰더라도
* IV 가 다르면 암호문이 달라짐
* - CBC 모드 보안성에 중요한 요소
*/
private static final String IV = "initvector123456"; // 16 bytes
/**
* 문자열을 AES/CBC/PKCS5Padding 방식으로 암호화하고
* 그 결과를 Base64 문자열로 반환
*
* @param plaintext 암호화할 평문 문자열
* @return Base64 로 인코딩된 암호문 문자열
* @throws Exception 예외
*/
public static String encrypt(String plaintext) throws Exception {
// 1. SecretKeySpec 생성
// - 문자열로 된 SECRET_KEY 를 UTF-8 바이트 배열로 변환
// - 알고리즘 이름 "AES" 를 지정하여 SecretKeySpec 객체 생성
// - 이 객체는 Cipher 에서 사용할 실제 "대칭키" 역할
SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes("UTF-8"), "AES");
// 2. IvParameterSpec 생성
// - IV 문자열을 UTF-8 바이트 배열로 변환
// - AES CBC 모드에서 사용할 IV(초기화 벡터)를 지정하는 객체
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes("UTF-8"));
// 3. Cipher 인스턴스 생성
// - "AES/CBC/PKCS5Padding" 이라는 변환(transformation) 문자열을 사용
// = 알고리즘/블록모드/패딩 방식
// - 여기서는:
// 알고리즘: AES
// 블록 모드: CBC
// 패딩: PKCS5Padding (PKCS7과 동일하게 취급됨)
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 4. Cipher 초기화 (암호화 모드)
// - ENCRYPT_MODE: 암호화 모드로 설정
// - keySpec: 위에서 만든 AES SecretKey
// - ivSpec : CBC 모드에서 사용할 IV
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
// 5. 실제 암호화 수행
// - 평문 문자열을 UTF-8 바이트로 변환한 뒤 doFinal() 에 전달
// - doFinal() 은 내부적으로
// 1) PKCS5Padding 적용 (블록 크기 맞추기)
// 2) AES/CBC 모드로 암호화
// 를 모두 수행한 뒤 암호문 바이트 배열을 반환
byte[] encrypted = cipher.doFinal(plaintext.getBytes("UTF-8"));
// 6. 암호문 바이트를 Base64 문자열로 변환
// - 암호문은 바이너리 데이터이므로
// 그대로 문자열로 사용하면 깨질 수 있음
// - Base64 로 인코딩하면 네트워크 전송/DB 저장 등에 안전한 문자열 형태로 사용 가능
return Base64.getEncoder().encodeToString(encrypted);
}
/**
* Base64 로 인코딩된 AES/CBC/PKCS5Padding 암호문을
* 복호화하여 원래 평문 문자열로 되돌림
*
* @param ciphertextBase64 Base64 문자열 형태의 암호문
* @return 복호화된 평문 문자열
* @throws Exception 예외
*/
public static String decrypt(String ciphertextBase64) throws Exception {
// 1. 암호화할 때와 동일한 SecretKeySpec 생성
// - 대칭키 알고리즘이기 때문에
// 암호화와 복호화에 동일한 키를 사용해야 됨
SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes("UTF-8"), "AES");
// 2. 암호화 때 사용한 것과 동일한 IV 를 다시 지정
// - CBC 모드 복호화 시에도 암호화할 때와 같은 IV 가 필요
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes("UTF-8"));
// 3. Cipher 인스턴스 재생성
// - 암호화 때와 동일하게 "AES/CBC/PKCS5Padding" 지정
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 4. Cipher 초기화 (복호화 모드)
// - DECRYPT_MODE: 복호화 모드
// - keySpec : 암호화 때와 동일한 SecretKey
// - ivSpec : 암호화 때와 동일한 IV
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
// 5. Base64 디코딩
// - 문자열 형태로 저장되어 있던 암호문을
// 다시 바이너리 형태(바이트 배열)로 되돌림
byte[] decoded = Base64.getDecoder().decode(ciphertextBase64);
// 6. 실제 복호화 수행
// - doFinal() 은 내부적으로
// 1) AES/CBC 모드로 복호화
// 2) PKCS5Padding 제거
// 를 수행하여 원래 평문 바이트 배열을 반환
byte[] decrypted = cipher.doFinal(decoded);
// 7. 평문 바이트 배열을 UTF-8 문자열로 변환하여 반환
return new String(decrypted, "UTF-8");
}
/**
* 간단한 동작 테스트용 main 메서드
*
* - original 문자열을 encrypt()로 암호화한 뒤
* 그 결과를 decrypt()로 복호화해서
* 원래 문자열과 일치하는지 확인
*/
public static void main(String[] args) throws Exception {
// 테스트용 평문 문자열
String original = "AES CBC PKCS5 테스트 예제 코드123%$#";
// 암호화 수행 → Base64 문자열
String enc = encrypt(original);
// 복호화 수행 → 평문 문자열
String dec = decrypt(enc);
// 결과 출력
System.out.println("원문: " + original);
System.out.println("암호문(Base64): " + enc);
System.out.println("복호문: " + dec);
}
}


- 길이가 안 맞으면 에러가 나거나 자동으로 잘려서 복호화가 불가할 수 있음
- 인코딩 문자열 <------> 바이트 변환은 그냥 UTF-8로 통일하는게 안전
- Base64 두 번 인코딩/디코딩 == 복호화 불가할 수 있음
- 인생이 재미없다면 SecretKey를 잃어버리기
- 블로그 작성하면서도 무슨 말인지 잘 모르겠음
- 그게 뭔데 10덕아
- 화이팅
'기타 > study' 카테고리의 다른 글
| VMWARE - Ubuntu 설치 (0) | 2025.05.13 |
|---|---|
| VMWARE 설치 (3) | 2025.05.12 |
| Spring Boot + React CORS 연동 (로컬 환경) (0) | 2025.04.13 |
| JWT - JSON Web Token (0) | 2025.03.24 |
| 코딩 기초 트레이닝 - 프로그래머스(차근차근 시작해 보세요 Day 25) (0) | 2025.01.31 |