Theme:

비밀번호를 데이터베이스에 저장할 때 "해싱해야 한다"는 건 알겠는데, 자바에서 구체적으로 어떤 클래스를 어떻게 쓰면 되는 걸까요?

보안은 백엔드 개발자가 반드시 알아야 하는 분야입니다. Java는 JCA(Java Cryptography Architecture)와 JCE(Java Cryptography Extension)라는 표준 보안 프레임워크를 제공합니다. 해싱, 암호화, 서명, TLS까지 순서대로 알아봅니다.

JCA/JCE 아키텍처

PLAINTEXT
┌──────────────────────┐
│  애플리케이션 코드      │
├──────────────────────┤
│  JCA API              │  MessageDigest, Cipher, Signature, KeyStore...
│  (java.security,      │
│   javax.crypto)       │
├──────────────────────┤
│  SPI (Service Provider│  실제 알고리즘 구현
│   Interface)          │
├──────────────────────┤
│  Provider             │  SunJCE, BouncyCastle 등
└──────────────────────┘

API 사용자는 알고리즘 이름만 지정하면, Provider가 실제 구현을 제공합니다.

해싱 — MessageDigest

기본 해싱

JAVA
import java.security.MessageDigest;

public static String sha256(String input) throws Exception {
    MessageDigest md = MessageDigest.getInstance("SHA-256");
    byte[] hash = md.digest(input.getBytes(StandardCharsets.UTF_8));

    // 바이트 배열을 16진수 문자열로 변환
    return HexFormat.of().formatHex(hash);
}

파일 해싱

JAVA
public static String hashFile(Path path) throws Exception {
    MessageDigest md = MessageDigest.getInstance("SHA-256");
    try (InputStream is = Files.newInputStream(path)) {
        byte[] buffer = new byte[8192];
        int bytesRead;
        while ((bytesRead = is.read(buffer)) != -1) {
            md.update(buffer, 0, bytesRead);
        }
    }
    return HexFormat.of().formatHex(md.digest());
}

주요 해시 알고리즘

알고리즘출력 크기용도
MD5128bit체크섬 (보안 목적 사용 금지)
SHA-1160bit레거시 (보안 목적 사용 금지)
SHA-256256bit일반적인 보안 용도
SHA-512512bit더 높은 보안

비밀번호 해싱 — 일반 해시를 쓰면 안 되는 이유

JAVA
// 나쁜 예 — SHA-256은 너무 빨라서 무차별 대입에 취약
String hashed = sha256(password);

// 좋은 예 — PBKDF2 (느린 해시 함수)
public static String hashPassword(String password, byte[] salt)
        throws Exception {
    SecretKeyFactory factory =
        SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
    KeySpec spec = new PBEKeySpec(
        password.toCharArray(),
        salt,
        310_000,  // 반복 횟수 (높을수록 느림)
        256       // 출력 키 길이(비트)
    );
    byte[] hash = factory.generateSecret(spec).getEncoded();
    return HexFormat.of().formatHex(hash);
}

// 솔트 생성
byte[] salt = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(salt);

대칭 암호화 — Cipher (AES)

같은 키로 암호화와 복호화를 수행합니다.

AES-GCM (권장)

JAVA
public class AesGcmCrypto {
    private static final int GCM_IV_LENGTH = 12;   // 바이트
    private static final int GCM_TAG_LENGTH = 128;  // 비트

    // 키 생성
    public static SecretKey generateKey() throws Exception {
        KeyGenerator kg = KeyGenerator.getInstance("AES");
        kg.init(256); // 256비트 키
        return kg.generateKey();
    }

    // 암호화
    public static byte[] encrypt(byte[] plaintext, SecretKey key)
            throws Exception {
        byte[] iv = new byte[GCM_IV_LENGTH];
        SecureRandom.getInstanceStrong().nextBytes(iv);

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
        cipher.init(Cipher.ENCRYPT_MODE, key, spec);

        byte[] ciphertext = cipher.doFinal(plaintext);

        // IV + 암호문을 함께 반환 (IV는 비밀이 아님)
        byte[] result = new byte[iv.length + ciphertext.length];
        System.arraycopy(iv, 0, result, 0, iv.length);
        System.arraycopy(ciphertext, 0, result, iv.length, ciphertext.length);
        return result;
    }

    // 복호화
    public static byte[] decrypt(byte[] encrypted, SecretKey key)
            throws Exception {
        byte[] iv = Arrays.copyOfRange(encrypted, 0, GCM_IV_LENGTH);
        byte[] ciphertext = Arrays.copyOfRange(
            encrypted, GCM_IV_LENGTH, encrypted.length);

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
        cipher.init(Cipher.DECRYPT_MODE, key, spec);

        return cipher.doFinal(ciphertext);
    }
}

왜 ECB가 아닌 GCM인가

모드특징
ECB같은 블록 → 같은 암호문 (패턴 노출, 사용 금지)
CBCIV 사용, 패턴 숨김. 하지만 무결성 보장 안 됨
GCMIV + 인증 태그 (기밀성 + 무결성 + 인증 모두 제공)

비대칭 암호화 — RSA

공개키로 암호화, 개인키로 복호화합니다.

JAVA
// 키 쌍 생성
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
KeyPair keyPair = kpg.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();

// 암호화 (공개키)
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encrypted = cipher.doFinal("비밀 메시지".getBytes(StandardCharsets.UTF_8));

// 복호화 (개인키)
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decrypted = cipher.doFinal(encrypted);
String message = new String(decrypted, StandardCharsets.UTF_8);

RSA는 대칭키에 비해 느리므로, 실무에서는 대칭키를 RSA로 암호화해서 교환하고, 실제 데이터는 대칭키(AES)로 암호화합니다 (하이브리드 암호화).

전자 서명 — Signature

JAVA
// 서명 생성 (개인키)
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(privateKey);
signer.update(data);
byte[] signature = signer.sign();

// 서명 검증 (공개키)
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(publicKey);
verifier.update(data);
boolean isValid = verifier.verify(signature);

서명의 목적:

  • 인증: 서명자가 개인키 소유자임을 증명
  • 무결성: 데이터가 변조되지 않았음을 확인
  • 부인 방지: 서명자가 서명 사실을 부인할 수 없음

HMAC — 대칭키 기반 메시지 인증

JAVA
public static byte[] hmacSha256(byte[] data, SecretKey key)
        throws Exception {
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(key);
    return mac.doFinal(data);
}

// API 요청 서명 검증
SecretKey apiKey = new SecretKeySpec(
    "my-secret".getBytes(), "HmacSHA256");
byte[] expectedMac = hmacSha256(requestBody, apiKey);
boolean valid = MessageDigest.isEqual(expectedMac, receivedMac);
// 주의: Arrays.equals 대신 isEqual을 사용 (타이밍 공격 방지)

KeyStore — 키 관리

JAVA
// KeyStore 생성 및 키 저장
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(null, null); // 새 KeyStore 초기화

// 비밀키 저장
ks.setKeyEntry("my-aes-key", secretKey,
    "password".toCharArray(), null);

// 파일로 저장
try (OutputStream os = Files.newOutputStream(Path.of("keystore.p12"))) {
    ks.store(os, "storePassword".toCharArray());
}

// 키 로드
try (InputStream is = Files.newInputStream(Path.of("keystore.p12"))) {
    ks.load(is, "storePassword".toCharArray());
}
SecretKey loaded = (SecretKey) ks.getKey(
    "my-aes-key", "password".toCharArray());

TLS/HTTPS

SSLContext 설정

JAVA
// 커스텀 TrustStore로 HTTPS 클라이언트 설정
KeyStore trustStore = KeyStore.getInstance("PKCS12");
try (InputStream is = Files.newInputStream(Path.of("truststore.p12"))) {
    trustStore.load(is, "password".toCharArray());
}

TrustManagerFactory tmf = TrustManagerFactory.getInstance(
    TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());

// HttpClient에 적용
HttpClient client = HttpClient.newBuilder()
    .sslContext(sslContext)
    .build();

mTLS (상호 인증)

JAVA
// 클라이언트 인증서 설정
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(Files.newInputStream(Path.of("client.p12")),
    "password".toCharArray());

KeyManagerFactory kmf = KeyManagerFactory.getInstance(
    KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, "password".toCharArray());

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(),
    new SecureRandom());

SecureRandom — 암호학적 난수

JAVA
// 기본 사용
SecureRandom random = new SecureRandom();
byte[] token = new byte[32];
random.nextBytes(token);

// 강력한 인스턴스 (블로킹될 수 있음)
SecureRandom strong = SecureRandom.getInstanceStrong();

// 토큰 생성
String sessionToken = HexFormat.of().formatHex(token);

java.util.Random은 시드를 알면 전체 시퀀스를 예측할 수 있으므로, 보안 목적으로는 반드시 SecureRandom을 사용합니다.

실무 주의사항

  1. 직접 암호 알고리즘을 구현하지 마세요 — 표준 라이브러리를 사용합니다.
  2. ECB 모드 사용 금지 — GCM 또는 최소 CBC를 사용합니다.
  3. 키를 코드에 하드코딩하지 마세요 — 환경 변수, KeyStore, 시크릿 매니저를 사용합니다.
  4. MD5, SHA-1은 보안 목적으로 사용 금지 — SHA-256 이상을 사용합니다.
  5. 비밀번호에 일반 해시 사용 금지 — PBKDF2, bcrypt, Argon2를 사용합니다.
  6. 타이밍 공격 주의 — MAC 비교 시 MessageDigest.isEqual()을 사용합니다.

정리

  • JCA/JCE는 알고리즘 독립적인 보안 API를 제공하며, Provider 기반으로 동작합니다.
  • MessageDigest는 해싱, Cipher는 암호화, Signature는 서명에 사용합니다.
  • AES-GCM은 기밀성과 무결성을 동시에 제공하는 권장 암호화 모드입니다.
  • KeyStore로 키와 인증서를 안전하게 관리합니다.
  • 보안 코드에서는 반드시 SecureRandom을 사용하고, 검증된 알고리즘과 모드를 선택합니다.
댓글 로딩 중...