본문 바로가기
BackEnd/JAVA

RSA를 이용한 민감정보 암호화 적용

by sorryisme 2025. 12. 15.

1. HTTPS만으로 충분할까?

일반적으로 웹 애플리케이션 보안은 SSL/TLS(HTTPS) 전송 계층 암호화와 서버 내부의 단방향 해시(Hashing) 저장 방식이면 충분하다고 생각했습니다. 하지만 최근 애플리케이션이 복잡해지고, 다양한 보안사고등으로 인해 평문 노출/유출에 대한 우려가 생겼습니다.

  • 실수로 비밀번호 등을 로그로 찍는 순간, 비밀번호가 로그 파일에 남을 수 있습니다.
  • 로그에 접근 가능한 내부자나, 프록시 등을 탈취 가능성을 완전히 배제할 수 없습니다.

내부 의사결정 후 브라우저에서 서버로 전송되는 구간에서도 데이터를 암호화하기로 결정했습니다.

 

2. 기술 검토: 대칭키 vs 비대칭키

과거 다른 프로젝트에서 세션 기반의 대칭키(AES) 방식을 통해 비밀번호를 암호화하는 것을 본 적이 있습니다.

  • 기존 방식: 서버가 AES 키를 생성해 세션에 저장하고, 클라이언트에 내려주면 클라이언트가 그 키로 암호화하여 전송.
  • 문제점: 대칭키 방식은 복호화 키가 브라우저에 노출됩니다. 공격자가 브라우저 메모리나 네트워크를 분석해 키를 탈취하면 복호화할 수 있습니다.

위 문제로 인해 비대칭키(RSA) 암호화 방식을 채택했습니다.

  • 공개키(Public Key): 브라우저에 전송. 암호화만 가능.
  • 개인키(Private Key): 서버 세션에만 저장. 복호화만 가능.
    공개키가 탈취되어도 개인키 없이는 복호화가 불가능하므로 훨씬 안전합니다.

3. 구현 과정 (Spring Framework& Java)

구현의 핵심은 키 쌍(Key Pair) 생성AOP를 활용한 자동 복호화입니다. (로그인의 경우 스프링 시큐리티 필터를 통한 처리를 수행했습니다)

1) 키 생성 및 관리 
초기 구현 단계라 static 유틸리티 메서드로 작성했으나, 추후 유연성을 위해 Bean으로 등록하여 관리할 예정입니다.

public static String generateKeyForSession(HttpSession session, int inactiveSeconds) throws Exception {
    // 1. 이미 세션에 공개키가 있다면 재사용 (불필요한 연산 방지)
    PublicKey savedKey = (PublicKey) session.getAttribute(RSACryptoUtils.RSA_PUBLIC_SESSION_KEY);

    if (savedKey != null) {
        return Base64.getEncoder().encodeToString(savedKey.getEncoded());
    }

    // 2. 키 쌍(KeyPair) 생성 (RSA 2048bit)
    KeyPair pair = generateKeyPair();

    // 3. 세션에 개인키/공개키 저장 및 만료 시간 설정
    session.setAttribute(RSA_PRIVATE_SESSION_KEY, pair.getPrivate());
    session.setAttribute(RSA_PUBLIC_SESSION_KEY, pair.getPublic());
    session.setMaxInactiveInterval(inactiveSeconds);

    // 4. 클라이언트에는 공개키만 Base64로 인코딩하여 전달
    return Base64.getEncoder().encodeToString(pair.getPublic().getEncoded());
}

 

2) 클라이언트 암호화 수행

자바스크립트에서는 JSEncrypt를 통해서 암호화를 수행하여 전송하였습니다

const publicKey =`<c:out value="${publicKey}"/>`;
const encryptor = new JSEncrypt();
encryptor.setPublicKey(publicKey);

encryptor.encrypt($("#password").val());

 

3) AOP를 활용한 투명한 복호화
커스텀 어노테이션(@EncryptedPassword)과 AOP를 사용하여 비즈니스 로직 침범 없이 복호화를 수행하도록 처리했습니다.

@Around("@annotation(com.example.security.EncryptedPassword)")
public Object decryptPassword(ProceedingJoinPoint joinPoint) throws Throwable {
    Object[] args = joinPoint.getArgs();
    // ... 인자 검증 로직 ...

    // 세션에서 개인키 가져오기
    PrivateKey privateKey = (PrivateKey) session.getAttribute(RSA_PRIVATE_SESSION_KEY);

    // 복호화 수행
    String encryptedValue = (String) targetArg;
    String decryptedPassword = RSACryptoUtils.decrypt(encryptedValue, privateKey);

    // 원본 객체 필드에 평문 비밀번호 주입 (Reflection)
    field.set(arg, decryptedPassword);

    return joinPoint.proceed(args);
}

 

4. 주요 고려 사항 및 트러블 슈팅

Q1. 복호화 실패 시 예외 처리는?

  • 로그인 시: 복호화 실패는 곧 '비밀번호 불일치'와 같습니다. 비밀번호 불일치 시에도 큰 리스크가 없어서 로그인이 되도록 했습니다.
  • 회원가입/비밀번호 변경 시: 키 만료 등으로 복호화가 실패 시 비밀번호가 오입력 될 가능성도 있어서
    익셉션을 발생시키고, 프론트엔드에서 키 재발급 요청 후 다시 시도하도록 유도했습니다.

Q2. 세션 만료(키 소멸) 시 UX는?

  • 유저가 회원가입 페이지를 열어두고 일정 시간 자리를 비워 세션(키)가 만료될 수 있습니다. 허나 클라이언트에는 암호화 키가 남아있어 전송 시점에서 키 페어 불일치가 발생합니다.
  • 사용자가 액션 시 서버에서 Exception 발생 → 내부적으로 즉시 키를 재발급하고 클라이언트에게 다시 시도를 유도 했습니다.

Q3. 성능 이슈와 발급 시점 최적화
RSA 키 생성 CPU 연산 비용이 꽤 비싼 작업입니다. 특히 Node.js 같은 싱글 스레드 환경이나 트래픽이 몰리는 상황에서는 병목이 될 수 있습니다.

  • 모든 유저에게 발급? (X): 메인 페이지 접속 시 무조건 키를 발급하는 것은 낭비입니다.
  • 필요할 때만 발급 (O): 로그인 모달이 뜨거나, 비밀번호 입력 input 창에 포커스(Focus)가 가는 시점에 키 발급 API를 호출하도록 최적화했습니다. 이를 통해 불필요한 서버 리소스 소모를 막을 수 있었습니다.

5. 결론

SSL이 전송 구간을 지켜주지만, 애플리케이션 레벨에서의 암호화는 실수로 인한 데이터 유출을 막는 수단입니다. 물론 암호화 로직 추가는 서버 리소스 사용량 증가이 발생할 수 있지만 요즘 같은 시대에 이제는 사소한 영역 또한 모두 보안조치가 필요한 것 같습니다.