목차
1. 단방향 암호화
1. 단방향 암호화의 개념
- 암호화된 데이터를 다시 평문으로 복호화할 수 없는 암호화 방식을 의미.
- 주로 해시(Hash) 함수를 사용하여 데이터를 고정된 길이의 해시 값으로 변환.
- 대표적인 알고리즘: SHA-256, BCrypt 등.
- 입력값이 같다면 항상 동일한 해시 값이 출력되므로, 이를 통해 데이터의 일치 여부를 검증함.
- 주의사항: 출력된 해시 값의 길이가 길다고 해서 무조건 안전한 것은 아님.
- 입력값 자체가 ‘123456’처럼 단순하고 짧으면, 생성 가능한 해시 값의 경우의 수도 적어짐.
- 따라서 해시 길이 자체가 암호화 강도를 절대적으로 보장하지는 않음.
2. 충돌 저항성 (Collision Resistance)
- 서로 다른 두 입력값이 우연히 동일한 해시값을 갖는 경우(충돌)를 찾아내는 것이 현실적으로 불가능해야 한다는 성질임.
- 이론적으로는 해시 충돌을 찾을 가능성이 존재하지만, 이를 계산하는 데 천문학적인 시간과 비용이 소요되어 사실상 불가능함을 의미함.
- 강도에 따라 약한 충돌 저항성과 강한 충돌 저항성으로 분류됨.
3. 솔팅 (Salting)
- 해시 함수 적용 전, 원본 데이터에 무작위 문자열(Salt)을 추가하여 해싱하는 기법임.
- 동일한 원본 데이터라도 Salt에 따라 매번 다른 해시 값이 생성되므로, 레인보우 테이블(Rainbow Table) 공격을 효과적으로 방어함.
- 레인보우 테이블: 해커가 미리 계산해 둔 입력값과 해시 값의 매핑 테이블.
- 동작 과정
- 1) 사용자별로 고유한 무작위 문자열(Salt) 생성 (예:
8z#k9!a). - 2) 비밀번호 등 입력 값의 앞이나 뒤에 Salt를 결합함.
- 3) 결합된 문자열을 해싱함.
- 4) DB에 생성된 해시 값과 Salt 값을 함께 저장함 (Salt 유실 시 검증 불가).
- 1) 사용자별로 고유한 무작위 문자열(Salt) 생성 (예:
- Salt 값 자체는 비밀이 아니므로 DB에 평문으로 저장해도 무방하나, 반드시 사용자마다 서로 다른 값을 부여해야 함.
- (참고) BCrypt 알고리즘은 내부적으로 Salt 생성 및 관리를 자동으로 수행해주므로 개발 편의성과 보안성이 높음.
4. 주요 해시 알고리즘 (SHA-256 vs BCrypt)
1) SHA-256
- NSA(미국 국가안보국)에서 설계한 SHA-2 계열의 해시 함수.
- 어떠한 입력 값이라도 256비트(16진수 기준 64자) 길이의 값으로 출력함.
- 특징: 복잡한 수학 연산을 통해 결과 값을 매우 빠르게 도출함 (초당 수억~수십억 번 계산 가능).
- 장점: 대용량 파일의 위변조 검증, 블록체인 등 데이터 무결성 확인에 효과적임.
- 단점: 연산 속도가 너무 빨라 GPU를 이용한 무차별 대입 공격(Brute Force)에 취약하므로, 비밀번호 저장용으로는 부적합함.
flowchart LR
A[입력] --> B[SHA-256 블록 처리]
B --> C[256-bit 해시 출력]
2) BCrypt
- 1999년에 오직 비밀번호 저장을 목적으로 설계된 해시 함수임.
- 의도적으로 해시 계산에 시간이 걸리도록 설계하여 무차별 대입 공격을 차단함.
- 특징: 알고리즘 내부적으로 임의의 Salt를 생성하여 비밀번호와 결합 후 해싱함 (Spring Security 기본 권장).
- 장점: 보안성이 매우 높음.
- 단점: CPU 사용량이 많아 동시 접속자가 폭증할 경우 서버 리소스 부담이 될 수 있음.
flowchart TD
A[비밀번호] --> B[Salt 자동 생성]
B --> C[Cost Factor 적용]
C --> D[반복적인 Blowfish 기반 연산]
D --> E[Salt 포함 해시 결과]
(참고) 해시 연산이 빠르면 왜 위험할까?
flowchart LR
A[공격자] --> B[비밀번호 후보 생성]
B --> C[해시 계산]
C --> D{해시 일치?}
D -->|No| B
D -->|Yes| E[비밀번호 발견]
style C fill:#ffcccc
- 공격자는 초당 수십억 번의 연산이 가능한 장비를 이용해 무차별 대입을 시도함.
- SHA-256 같은 고속 알고리즘은 8자리 비밀번호가 단 몇 초~분 만에 뚫릴 위험이 있음.
- 따라서 비밀번호 보호를 위해서는 연산 속도를 조절할 수 있는 BCrypt 등을 사용해야 함.
2. 양방향 암호화
1. 양방향 암호화의 개념
- 암호화와 복호화가 모두 가능한 방식으로, SSH, HTTPs 프로토콜 등에서 널리 사용됨.
- 대표 알고리즘: AES, RSA.
- 키(Key)를 사용하여 암호화 및 복호화를 수행하며, 키 관리 방식에 따라 대칭키와 비대칭키로 구분됨.
- 대칭키 방식: 암호화와 복호화에 동일한 키를 사용함.
- 비대칭키 방식: 암호화(공개키)와 복호화(개인키)에 서로 다른 키를 사용함.
2. AES 대칭키 암호화
- 미국 표준 기술 연구소(NIST)에서 제정한 표준 대칭키 암호화 알고리즘임.
- 데이터를 128비트 블록으로 나누고, 복잡한 수학적 연산(치환, 이동, 믹싱, XOR)을 반복하는 블록 암호(Block Cipher) 방식임.
- 키 길이에 따라 AES-128, AES-192, AES-256으로 나뉘며, 키가 길수록 라운드 횟수가 증가해 보안성이 강화됨.
- 장점: 구조가 단순하여 처리 속도가 매우 빠르고, 현재까지 알려진 효율적인 해킹 방법이 없음.
- 단점: 송신자와 수신자가 동일한 키를 공유해야 하므로, 키 배송(전달) 과정에서의 보안 취약점이 존재함.
3. RSA 비대칭키 암호화
- 전자상거래, 인터넷 뱅킹 등에서 널리 쓰이는 공개키 기반 암호화 알고리즘임.
- ‘매우 큰 두 소수의 곱을 구하기는 쉽지만, 그 곱을 소인수분해하는 것은 매우 어렵다’는 수학적 난제에 기반함.
- 장점: 공개키는 공개되어도 무방하므로 키 배송 문제가 해결되며, 개인키를 이용한 전자 서명(신원 인증)이 가능함.
- 단점: 복잡한 수학 연산으로 인해 AES 대비 처리 속도가 매우 느리며, 보안성을 위해 매우 긴 키(2048비트 이상)를 사용해야 함.
4. [실전] Java 구현 예제 (AES/RSA)
Spring Boot 4.0.2 (Java 17+) 환경에서 외부 라이브러리 없이 javax.crypto, java.security 패키지만을 활용한 구현임.
- 변환(Transformation) 문자열 구조:
알고리즘 / 운용모드 / 패딩 - AES-256 설정:
AES/GCM/NoPadding- GCM 모드: 데이터의 기밀성뿐만 아니라 무결성(인증)까지 동시에 보장하며 병렬 처리가 가능함.
- NoPadding: GCM은 스트림 암호처럼 동작하므로 패딩이 불필요함.
- IV(초기화 벡터): 매 암호화마다 랜덤 생성하여 동일 평문도 다른 암호문이 되도록 함.
- RSA 설정:
RSA/ECB/OAEPWithSHA-256AndMGF1Padding- OAEP Padding: 기존 PKCS1Padding의 취약점을 보완한 최신 표준 패딩 방식임.
package com.example.demo.util;
import org.springframework.stereotype.Component;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
@Component
public class CryptoUtils {
// 1. AES-256 Encryption (Symmetric Key)
private static final String AES_ALG = "AES";
private static final String AES_TRANSFORMATION = "AES/GCM/NoPadding";
private static final int GCM_IV_LENGTH = 12; // GCM 표준 권장 IV 길이
private static final int GCM_TAG_LENGTH = 128; // 인증 태그 길이 (bits)
// AES 암호화 (IV 자동 생성 및 포함)
public String encryptAes(String plainText, String secretKeyStr) {
try {
byte[] keyBytes = secretKeyStr.getBytes(StandardCharsets.UTF_8);
SecretKey secretKey = new SecretKeySpec(keyBytes, AES_ALG);
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
// IV와 암호문을 결합하여 반환
byte[] combined = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length);
return Base64.getEncoder().encodeToString(combined);
} catch (Exception e) {
throw new RuntimeException("Failed to encrypt data using AES.", e);
}
}
// AES 복호화 (IV 분리 후 복호화)
public String decryptAes(String cipherText, String secretKeyStr) {
try {
byte[] combined = Base64.getDecoder().decode(cipherText);
byte[] keyBytes = secretKeyStr.getBytes(StandardCharsets.UTF_8);
SecretKey secretKey = new SecretKeySpec(keyBytes, AES_ALG);
byte[] iv = new byte[GCM_IV_LENGTH];
byte[] encrypted = new byte[combined.length - GCM_IV_LENGTH];
System.arraycopy(combined, 0, iv, 0, iv.length);
System.arraycopy(combined, iv.length, encrypted, 0, encrypted.length);
Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
return new String(cipher.doFinal(encrypted), StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("Failed to decrypt data using AES.", e);
}
}
// 2. RSA Encryption (Asymmetric Key)
private static final String RSA_ALG = "RSA";
private static final String RSA_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
// RSA 암호화 (Public Key 사용)
public String encryptRsa(String plainText, String publicKeyBase64) {
try {
byte[] keyBytes = Base64.getDecoder().decode(publicKeyBase64);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALG);
PublicKey publicKey = keyFactory.generatePublic(spec);
Cipher cipher = Cipher.getInstance(RSA_TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException("Failed to encrypt using RSA Public Key.", e);
}
}
// RSA 복호화 (Private Key 사용)
public String decryptRsa(String cipherText, String privateKeyBase64) {
try {
byte[] keyBytes = Base64.getDecoder().decode(privateKeyBase64);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALG);
PrivateKey privateKey = keyFactory.generatePrivate(spec);
Cipher cipher = Cipher.getInstance(RSA_TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(cipherText));
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("Failed to decrypt using RSA Private Key.", e);
}
}
}
3. HMAC 데이터 검증
HMAC (Hash-based Message Authentication Code)
- 원본 데이터에 비밀키를 결합한 후 해시 함수를 적용하여 생성하는 메시지 인증 코드임.
- 클라이언트가 보낸 메시지의 무결성(위변조 여부)과 인증(발신자 확인)을 동시에 보장하기 위해 사용됨.
- 동작 과정
- 1) 발신자가 원본 메시지 + 비밀키를 조합해 해시 값(MAC)을 생성함.
- 2) 원본 메시지와 MAC을 수신자에게 전송함.
- 3) 수신자는 자신이 가진 비밀키로 동일하게 해시 값을 생성하여, 수신된 MAC과 일치하는지 비교함.
- 활용 사례: API 인증(헤더 서명), JWT(Signature 검증), 세션 쿠키 위변조 방지 등.
- 장점: 구현이 단순하고 연산 효율이 좋음.
- 단점: 송수신자가 비밀키를 안전하게 공유해야 하는 키 관리 이슈가 존재함.
[실전] Java HMAC-SHA256 구현 예제
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class HmacExample {
private static final String HMAC_SHA256 = "HmacSHA256";
public static String generateHmac(String message, String secretKey) {
try {
// 1. 알고리즘과 비밀키 설정
SecretKeySpec signingKey = new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
// 2. Mac 인스턴스 초기화
Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(signingKey);
// 3. HMAC 계산 및 Base64 인코딩 반환
byte[] rawHmac = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(rawHmac);
} catch (Exception e) {
throw new RuntimeException("Failed to generate HMAC.", e);
}
}
}