RSA (비대칭 암호화 알고리즘)

DobbyisCoding·2021년 3월 22일
0

사용하는 목적
WEB 서버에 SSL 설치 없이 로그인처리할때 평문으로 전송할경우 중간에서 정보를 가로채어 가로챈 계정정보를 권한이 없는 사용자가 시스템에 로그인 한후 시스템을 손상시킬수도 있습니다.

이와 같은 보안 문제가 발생하는 것을 방지하기 위해서 RSA암호화 방식을 사용합니다.

사실 그냥 SSL을 사용하면 더안전하게 아무런 처리없이 간단하게 사용할수 있지만 사용하는 서비스들에 대해서 전부 인증서를 등록(인증서별로 1년단위로 고정적으로 돈이 많이든다. ㅎㅎ)하여 사용 할수 없으니 가장 쉽고 돈안들이면서 안전하게 로그인 할수있는 방식이다.

RSA 란 ?
RSA란 암호화와 인증을 할수있는 공개키 암호시스템이다. 이것은 1977년 RonRivest와 Adi Shamir, Leonard Adleman에 의해서 개발되었다.

RSA 암호화 알고리즘을 통해서 평문을 암호화하여 서버로 전송하여 중간에 계정정보를 가로채더라도 암호화 키가 없으면 해석이 불가능하다.

기본 작동 원리
1. 서버측에서 RSA 공개키와 개인키(암호키)를 생성하여 개인키는 세션에 저장하고 공개키는 HTML 로그인 폼 페이지에 Input[type=hidden] value 값에 셋팅한다.

  1. 로그인 폼은 사용자가 아이디 패스워드를 넣고 전송을 하면 전송하기전에 중간에 자바스크립트가 가로챈다.

  2. 입력된 사용자 아이디,패스워드를 서버에서 전달받은 공개키로 RSA 암호화하여 서버로 전송한다.

  3. 로그인폼에서 전달받은 사용자 아이디,패스워드를 세션에 저장된 RSA 개인키로 복호화 한다.

  4. Database에 저장된 사용자 아이디와 패스워드가 일치하는지 확인한다.

    Spring Framework RSA 키생성
    로그인 폼 화면을 출력할때 공개키와 개인키를 생성하여 공개키는 Input hidden에 값을 셋팅하고 개인키는 세션에 저장한다.

로그인 처리전 로그인 페이지로 이동시 공개키와 개인키를 생성하여 세션에 담고 공개키 값을 클라이언트로 전달한다.

@RequestMapping("login.do")
public String MoveLogin(@ModelAttribute Member member, HttpServletRequest request,HttpServletResponse response)throws GeneralSecurityException {
		HttpSession session = request.getSession();
	if(member.getM_email() != null) return "index";
	else {
		KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
		generator.initialize(1024);
		KeyPair keyPair = generator.genKeyPair();
		KeyFactory keyFactory = KeyFactory.getInstance("RSA");
		PublicKey publicKey = keyPair.getPublic();
		PrivateKey privateKey = keyPair.getPrivate();
		//System.out.println("#### publicKey: "+publicKey+", #### privateKey :"+privateKey);
			
		session.setAttribute("_RSA_WEB_Key_", privateKey);   //세션에 RSA 개인키를 세션에 저장한다.
		RSAPublicKeySpec publicSpec = (RSAPublicKeySpec) keyFactory.getKeySpec(publicKey, RSAPublicKeySpec.class);
		String publicKeyModulus = publicSpec.getModulus().toString(16);
		String publicKeyExponent = publicSpec.getPublicExponent().toString(16);
	 
		request.setAttribute("RSAModulus", publicKeyModulus);  //로그인 폼에 Input Hidden에 값을 넘겨 준다
		request.setAttribute("RSAExponent", publicKeyExponent);   //로그인 폼에 Input Hidden에 값을 넘겨 준다

		return "login";
		}
		
	}

로그인 페이지에 서버에서 전달된 공개키를 클라이언트에 히든으로 세팅해준다

<form id="LoginS" name="loginS"  >    
	    <h2>여행시작하기,</h2>
	    <input type="hidden" id="RSAModulus" value="${RSAModulus}" /><!-- 서버에서 전달한값 세팅. -->
		<input type="hidden" id="RSAExponent" value="${RSAExponent}" /><!-- 서버에서 전달한값 세팅 -->
	    <label>
	      <span>이메일</span>
	      <input type="text" name="m_email" id="l_email" required="required"/>
	    </label>
	    <label>
	      <span>비밀번호</span>
	      <input type="password" name="m_pwd" id="l_pwd" required="required"/>
	    </label>
		<a href="findIdPwd">
	    	<p class="forgot-pass">이메일 또는 비밀번호가 기억이 안나시나요?</p>
	    </a>
	
	    <button type="button" id="ms_login" class="submit" onclick="loginCheck()" >시작 </button><!-- class="submit" onclick="loginCheck()" -->
	    <!-- 네이버 로그인 창으로 이동 -->
		<div id="naver_id_login" style="text-align:center"><a href="${url}">
		<img style="max-height: 40px;border-radius: 30px;width: 45%;" width="223" src="https://developers.naver.com/doc/review_201802/CK_bEFnWMeEBjXpQ5o8N_20180202_7aot50.png"/></a></div>
		<br>
  </form>

RSA 자바스크립트 라이브러리

<!-- RSA 자바스크립트 라이브러리 -->
<script type="text/javascript" src="../js/RSA/jsbn.js"></script>
<script type="text/javascript" src="../js/RSA/rsa.js"></script>
<script type="text/javascript" src="../js/RSA/prng4.js"></script>
<script type="text/javascript" src="../js/RSA/rng.js"></script>

RSA 암호화 스크립트 로직

$("#ms_login").click(function(){
	//사용자 계정정보 암호화전 평문
	var uemail = $("#l_email").val();
	var pwd = $("#l_pwd").val();
	
	//alert(uemail +"##"+ pwd);
	//RSA 암호화 생성
	var rsa = new RSAKey();
	rsa.setPublic($("#RSAModulus").val(), $("#RSAExponent").val());
	
	//사용자 계정정보를 암호화 처리
	uemail = rsa.encrypt(uemail);
	pwd = rsa.encrypt(pwd); 
	//alert(uemail +"##"+ pwd);
	$.ajax({ 
		  type: "POST",  
		  url: "login.proc",  
		  data: {m_email :uemail, m_pwd: pwd},  //사용자 암호화된 계정정보를 서버로 전송
		  dataType:"json",
		  success: function(msg){    
			 
			  if(msg.state == "true")
			  {
				  location.href = "../"; 
			  }
			  else if(msg.state == "false")
			  {
				 salert("이메일 또는 비밀번호를 다시 확인해주세요.");
			  }
			  else
			  {
				 salert("잘못된 경로로 접근하였습니다. 암호화 인증에 실패하였습니다."); 
			  } 
		  } 
	});
});

암호화된 정보를 받아 서버에서 복호화 하는 로직

public String decryptRsa(PrivateKey privateKey, String securedValue) {
		 String decryptedValue = "";
		 try{
			Cipher cipher = Cipher.getInstance("RSA");
		   /**
			* 암호화 된 값은 byte 배열이다.
			* 이를 문자열 폼으로 전송하기 위해 16진 문자열(hex)로 변경한다. 
			* 서버측에서도 값을 받을 때 hex 문자열을 받아서 이를 다시 byte 배열로 바꾼 뒤에 복호화 과정을 수행한다.
			*/
			byte[] encryptedBytes = hexToByteArray(securedValue);
			cipher.init(Cipher.DECRYPT_MODE, privateKey);
			byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
			decryptedValue = new String(decryptedBytes, "utf-8"); // 문자 인코딩 주의.
		 }catch(Exception e)
		 {
			 System.out.println("decryptRsa Exception Error : "+e.getMessage());
		 }
			return decryptedValue;
	} 
	/** 
	 * 16진 문자열을 byte 배열로 변환한다. 
	 */
	 public static byte[] hexToByteArray(String hex) {
		if (hex == null || hex.length() % 2 != 0) {
			return new byte[]{};
		}
	 
		byte[] bytes = new byte[hex.length() / 2];
		for (int i = 0; i < hex.length(); i += 2) {
			byte value = (byte)Integer.parseInt(hex.substring(i, i + 2), 16);
			bytes[(int) Math.floor(i / 2)] = value;
		}
		return bytes;
	}
	 /* 로그인 체크 */
	 @RequestMapping(value = "/login.proc",headers="Accept=application/json",method = RequestMethod.POST)
	 public @ResponseBody JSONObject loginChk(HttpServletRequest request) 
	 {
	 	Member loginMember = new Member();
	 	JSONObject listObj = new JSONObject();
	 	
	 	String uemail = request.getParameter("m_email");
	 	String pwd = request.getParameter("m_pwd");
	 	HttpSession session = request.getSession();
	 	System.out.println("###암호화 된 아이디: "+uemail+", ###암호화 된 비밀번호: "+pwd);
	 	PrivateKey privateKey = (PrivateKey) session.getAttribute("_RSA_WEB_Key_");  //로그인전에 세션에 저장된 개인키를 가져온다.
	 	if (privateKey == null) 
	 	{ 
	 		listObj.put("state", "false"); 
	 		System.out.println("#### False1");
	 		//json 형태의 데이터를 DB나 웹서버에서 가져올때는 json데이터를 넣어주지않고 적절하게 가공만 하면 되므로 문제되지 않지만 다음과 같이 직접적으로 Generic 타입 선언 없이 put으로만 일방적으로 넣었을때 경고가 나타난것이다.
	 	}
	 	else
	 	{
	 		try
	 		{
	 			System.out.println("3");
	 			//암호화처리된 사용자계정정보를 복호화 처리한다.
	 			String _uemail = decryptRsa(privateKey, uemail);
	 			String _pwd = decryptRsa(privateKey, pwd);
	 			//복호화 처리된 계정정보를 member에 담아서 myBatis와 연동한다.
	 			//System.out.println("##### _uid : "+_uemail+", #### _pwd : "+_pwd);
	 			loginMember.setM_email(_uemail);
	 			loginMember.setM_pwd(_pwd);
	 			Member m1 = service.login(loginMember, null, servletContext);
	 			if(m1.getM_email().length() > 0) { 
	 				  session.setAttribute("LOGINUSER", m1);
	 				  System.out.println("#### 정상 작동");
	 			}else { 
	 				listObj.put("state", "false");
	 			} 
	 			listObj.put("state", "true");
	 			//iBatis 처리 및 로그인후 session 처리
	 		}
	 		catch(Exception e)
	 		{
	 			listObj.put("state", "false");
	 			System.out.println("login ERROR : "+e.getMessage()); 
	 		}
	 	} 
	 	System.out.println("#### 정상 작동");
	 	return listObj;  
	 }
     ```
profile
Keep going

0개의 댓글