개인 프로젝트/프로젝트

RSA 암호화 방식을 이용한 로그인 처리

Leo.K 2022. 6. 18. 01:09

프로젝트를 진행하는 와중에 이번에는 필자가 로그인을 담당하게 되었다. 평소에 다른 학원 사람들과 꾸준히 세미 프로젝트를 해왔어서 그런지 단순한 로그인 기능 구현은 어려움이 없었다. 후에 소셜 로그인은 팀원들과 함께 공부하여 함께 구현해보기로 했는데, 회의날까지 약간의 시간이 남아서 로그인 처리에 대한 보안을 조금 강화하고자 한다. 

이를 위해 SSL을 구현해볼까 고민해보았지만,, 최대한 돈이 들지 않는 범주에서 하기 위해서 RSA를 구현해보기로 했다. 그렇다면 RSA에 대해서 한 번 정리하고 코드에 대한 리뷰를 보도록 하자.

[ RSA ]

RSA란 암호화와 인증을 할 수 있는 공개키 암호 시스템이다. 이것저것 자격증을 공부하면서 많이 들어보았는데, 이렇게 직접 구현해보니 느낌이 색달랐던 것 같다. 이것은 1977년 RonRivest와 Adi Shamir, Leonard Adleman에 의해서 개발되었고, 이 세사람 이름의 앞 글자를 따서 RSA라는 이름이 명명되었다고 한다. 

그렇다면 어디에 사용하고 왜 사용하는 것일까? 

기본적으로 필자가 지금껏 구현했던 로그인 처리는 정말 단순하게 데이터만 넘겨주고 받는 시스템이었다. Web서버에 SSL(Secure Socket Layer)설치 없이 로그인 처리를 할 때 사용자가 입력한 아이디와 비밀번호를 평문으로 전송하는 경우 중간에서 정보를 가로채어 가로챈 계정 정보를 권한이 없는 사용자가 시스템에 로그인 한 후 시스템을 손상시킬 수 있는 보안적 위협이 있다. 

이와 같은 보안 문제가 발생하는 것을 방지하기 위하여 RSA암호화 방식을 사용한다. 이 방식을 사용한다고 해서 위의 방식처럼 서버로 전송한 평문을 가로챌 수 없다는 말은 아니다. 암호화된 평문을 가로채더라도 이를 복호화 하기 위해선 이에 대응되는 개인키로만 가능한데, 이 개인키를 서버에만 저장함으로써 해석의 여지를 주지 않늗다.

[ 작동원리 ]

  1. 사용자가 로그인을 하기 위해서 로그인 form을 호출하는 요청이 서버에 들어오면 이 시점에서 서버에서는 공개키와 개인키를 생성한다. 이 중에서 공개키는 클라이언트 단에 hidden처리해서 보이지 않게 하고(소스코드 보기로 확인할 수 있지만 암호화 되어 있다), 개인키는 사용자의 요청 정보가 담긴 request 객체에 포함된 session에 저장한다.
  2. 사용자가 로그인 폼에 아이디 비밀번호를 입력하고 로그인 버튼을 누르면 서버로 전송하기 직전에 자바스크립트 함수로 가로챈다. 필자는 send()함수 내에서 f.submit()으로 전송하기 직전에 암호화를 진행했다. 
  3. 1에서 서버가 공개키를 hidden시켜놓았다고 했다(세션트래킹). 이 공개키를 이용해서 사용자가 입력한 데이터를 암호화 하여 서버로 전송한다. 이 경우 사용하는 함수를 사용하기 위해서는 별도의 라이브러리가 필요하다.
  4. 서버에서는 전달받은 암호화된 평문을 세션에 저장해둔 개인키를 사용하여 복호화 한다. 
  5. DB에 저장된 사용자 id와 pwd가 일치하는지 확인한다. 

 

[ 소스 코드 분석 ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");//RSA알고리즘으로 키 쌍을 생성
generator.initialize(1024);//키의 크기는 1024
KeyPair keyPair       = generator.genKeyPair();
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey   = keyPair.getPublic(); //공유키
PrivateKey privateKey = keyPair.getPrivate();//개인키
 
session.setAttribute("RSA_WEB_KEY", privateKey);//세션에 개인키를 저장
RSAPublicKeySpec publicSpec = keyFactory.getKeySpec(publicKey, RSAPublicKeySpec.class);
 
String publicKeyModulus    = publicSpec.getModulus().toString(16);
String publicKeyExponenet  = publicSpec.getPublicExponent().toString(16);
 
request.setAttribute("RSAModulus", publicKeyModulus);
request.setAttribute("RSAExponent", publicKeyExponenet);
cs
 

로그인 폼을 호출하기 전에 공개키와 개인키를 생성한다. 

  • KeyPairGenerator(공개 추상 클래스) -> 공개키와 개인키의 쌍을 생성하는 데 사용된다. 이는 getInstance()메서드를 사용하여 구성된다. 특정 알고리즘에 대한 KeyPairGenerator는 이 알고리즘과 함께 사용할 수 있는 공개/개인 키 쌍을 생성한다. 또한 생성된 각 키와 알고리즘별 매개변수를 연결한다. 여기서 말하는 알고리즘은 1번 행에 "RSA"이다. 
  • 2행을 보면 초기화가 되고 있는 것을 볼 수 있는데, 이는 오버로딩 된 초기화 메서드 중에서 키의 사이즈를 부여하는 것이다. 키 사이즈는 1024, 2048 등이 있다. 
    • 조금은 수학적이긴 하지만 RSA의 암호화를 하는 과정을 살펴보자 
    • RSA방식으로 암/복호화를 하기 위해선 먼저 키를 만들어야 하는데, 그 키를 생성하는 과정은 아래와 같다. 
      • 소수 p, q를 준비한다.
      • p-1, q-1과 각각 서로소(두 수 사이의 공약수가 1뿐인 관계)인 정수 e를 준비한다.
      • (e * d) mod (p-1)*(q-1) = 1을 만족하는 d를 찾는다. 
      • N = pq를 계산한 후에 N과 e를 공개하는데, 이들이 바로 공개키이다. 그리고 공개하지 않는 d가 바로 개인키이다. 
      • 이후에 p, q, p-1, q-1은 사용하지도 않고, 남겨봐야 보안에 결점이 생기므로 삭제한다.  
      • RSA에서 키의 사이즈를 말할 때, 주로 N의 사이즈를 의미하며 사이즈가 필자가 적요한 것처럼 1024인 경우라면 정수 N의 크기가 2^1023 ~ 2^1024의 사이의 범위에 있다는 것이다.
  • 3행을 보면 그냥 알 수 있듯이 개인키/공개키의 쌍을 생성하는 메서드이다.
  • 4행을 보자, 지정된 알고리즘 "RSA"의 공개/개인키를 변환하는 개체를 반환하고, 이 객체를 사용해 9행에서 생성한 공개키를 RSAPublicKeySpec타입으로 변환한다.  즉, 바이트 배열을 서버로 전송하기 위해 16진 문자열로 바꾼다.
  • 5-6행은 생성한 키의 쌍에서 각각 개인키 공유키로 나누어서 저장하는 것이다.
  • 11~15행을 보자 사용자에게 공개할 공개키의 내용중 계수(N)와 공개지수(e)를 16진수로 저장한다. 16진수로 저장한 데이터를 포워딩하여 로그인 폼에 hidden속성을 이용하여 세션 트래킹 해주고, 이 값을 이용해 사용자가 입력한 데이터를 암호화 한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private String decryptRSA(PrivateKey privateKey, String m_id) {
    // TODO Auto-generated method stub
    String decryptedValue = "";
    try {
        Cipher cipher = Cipher.getInstance("RSA");
        
        byte[] encryptedBytes = hexToByteArray(m_id);
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
        decryptedValue = new String(decryptedBytes, "utf-8");
    } catch (Exception e) {
        // TODO: handle exception
        System.out.println("# 복호화 에러 발생 : " + e.getMessage());
    }
    return decryptedValue;
}
 
private byte[] hexToByteArray(String m_id) {
    // TODO Auto-generated method stub
    if(m_id == null || m_id.length()%2 != 0) {
        return new byte[] {};
    }
    
    byte[] bytes = new byte[m_id.length()/2];
    for(int i=0; i< m_id.length(); i+=2) {
        byte val = (byte) Integer.parseInt(m_id.substring(i, i+2), 16);
        bytes[(int) Math.floor(i/2)] = val;
    }
    
    return bytes;
}
cs

클라이언트로부터 들어온 암호화된 값을 받아서 이제 복호화하는 과정을 거칠 것이다. RSA로 암호화되는 값은 byte배열이다. 이를 문자열 폼으로 전송하기 위해서 16진 문자열로 변경해서 전송한 것이다. 물론 서버에서 수신한 데이터 또한 16진 문자열을 받기 때문에 이를 다시 원래의 상태인 byte배열로 바꾼뒤에 복호화 과정을 수행해야 한다.