Programmer's Progress

도서 정보 제공 웹 서비스 - RSA, SHA-512 보안기능 제공하기 본문

Web Service/도서 정보 제공 웹 서비스

도서 정보 제공 웹 서비스 - RSA, SHA-512 보안기능 제공하기

Blanc et Noir 2021. 12. 2. 12:55

현재 개발 중인 웹 서비스는 보안을 전혀 신경 쓰지 않았었다.

클라이언트에서 서버로 데이터를 전송할 때, 서버에서 모델을 통해 DB에 데이터를 저장할 때 등등

만약 개발이 완료된 웹서비스가 보안 기능을 전혀 제공하지 않는다면 문제가 생길 것이다.

 

네트워크 보안 수업을 들으면서 SSL을 통해 대칭 암호키를 비밀리에 주고받을 수 있다면 좋겠지만

지금 당장으로는 지식이 부족하여 적용이 어렵다고 판단했고, 비대칭키 암호화를 통해 이를 부분 해결하기로 했다.

 

 

 

 

다음과 같이 패키지를 구성하였다.

 

 

 

원리는 간단하다.

 

로그인 버튼을 누를때마다 서버는 클라이언트에게 랜덤 한 공개키 값을 보내주고

그에 대응되는 비밀키를 세션에 저장한다.

 

클라이언트는 전달받은 공개키로 비밀번호를 암호화한 후에 AJAX요청으로 암호화된 비밀번호와

암호화되지 않은 ID를 전달한다.

 

서버는 클라이언트의 세션객체에 담긴 비밀키로 복호화 후에 DB에 질의 후 로그인 여부를 결정한다.

 

 

 

 

 

이때 중요한 점이 있다.

바로 비밀번호는 절대로 복호화되지 않는 암호화 방식으로 DB에 저장해야 한다는 것이다.

왜냐하면 DB관리자가 비밀번호를 복호화하여 해당 사용자 인척 로그인하여 여러 가지 일들을 수행할 수 있기 때문이다.

또한, 만약 DB의 정보가 탈취되었다고 했을 때, 이 비밀번호가 복호화 가능하다면 해커가 상당한 이득을 취할 것이다.

 

 

따라서 비밀번호는 애초에 복호화하지 않는다는 것을 가정하여 단방향 암호화를 진행한다.

이때 사용하기로 결정한 해시함수가 바로 SHA-512이다.

 

 

 

 

SHA-512는 임의의 길이의 문자열을 받아서 512비트의 블록으로 해시값을 만들어내는데

만약 비밀번호를 그대로 해싱한다면 레인보우 테이블 공격에 취약할 수 있다.

그렇기에 사용자마다 SALT값을 랜덤 하게 지정하고, 입력받은 평문과 SALT를 덧붙여서 해싱함으로써

해당 공격에 대한 저항성을 얻도록 하는 것이 일반적이다.

 

 

 

 

 

이 SALT값은 고정되지 않는다. 물론 회원 가입할 때 DB에 SALT도 같이 저장하기는 하지만

비밀번호 변경시에는 SALT값도 같이 변경함으로써 비밀번호와 비밀번호 찾기 질문의 답을 재설정하도록 해

새로운 SALT와 함께 다시 해싱하여 DB에 저장할 것이다.

 

 

 

 

package encrypt;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;

public class SHA{
	public static String getSalt() { 
		String salt=""; 
		try { 
			SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
			byte[] bytes = new byte[16];
			random.nextBytes(bytes);
			salt = new String(Base64.getEncoder().encode(bytes));
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace(); 
		}
		return salt; 
	}
	public static String SHA512(String plaintext, String salt) {
		String hash = null;
		try {
			MessageDigest md = MessageDigest.getInstance("SHA-512");
			md.update((plaintext+salt).getBytes());
			hash = String.format("%0128x", new BigInteger(1, md.digest()));
		} catch (Exception e) {
			e.printStackTrace();
		}
		return hash;
	}
	public static String DSHA512(String plaintext, String salt) {
		return SHA512(SHA512(plaintext,salt),salt);
	}
}

SHA-512의 기능을 담당할 SHA.java 소스코드

 

 

 

 

package encrypt;

import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

import javax.crypto.Cipher;

public class RSA2048{
	public static String keyToString(Key key) {
		return Base64.getEncoder().encodeToString(key.getEncoded());
	}
	public static String encrypt(String plaintext, String publickey){
		try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            byte[] publickeyBytes = Base64.getDecoder().decode(publickey.getBytes());
            X509EncodedKeySpec publickeySpec = new X509EncodedKeySpec(publickeyBytes);
            Key key = keyFactory.generatePublic(publickeySpec);
            
		    byte[] plaintextBytes = plaintext.getBytes();
		    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
		    cipher.init(Cipher.ENCRYPT_MODE,key);
		    byte[] ciphertextBytes = cipher.doFinal(plaintextBytes);
		    return encode(ciphertextBytes);
		}catch(Exception e) {
			e.printStackTrace();
			return null;
		}
	}
	public static String decrypt(String ciphertext, String privatekey){
	    try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            byte[] privatekeyBytes = Base64.getDecoder().decode(privatekey.getBytes());
            PKCS8EncodedKeySpec privatekeySpec = new PKCS8EncodedKeySpec(privatekeyBytes);
            Key key = keyFactory.generatePrivate(privatekeySpec);
            
		    byte[] ciphertextBytes = decode(ciphertext);
		    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
		    cipher.init(Cipher.DECRYPT_MODE,key);
		    byte[] plaintextBytes = cipher.doFinal(ciphertextBytes);
		    return new String(plaintextBytes,"UTF8");
	    }catch(Exception e) {
			e.printStackTrace();
			return null;
	    }
	}
	private static String encode(byte[] data){
	    return Base64.getEncoder().encodeToString(data);
	}
	private static byte[] decode(String data){
	    return Base64.getDecoder().decode(data);
	}
	public static KeyPair createKey() {
		KeyPairGenerator gen;
		KeyPair keypair = null;
		try {
			gen = KeyPairGenerator.getInstance("RSA");
			gen.initialize(2048);
			keypair = gen.genKeyPair();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return keypair;
	}
}

RSA-2048 암호화, 복호화를 담당하는 RSA 2048.java 소스코드

 

 

 

 

 

 

 

$(document).ready(function(){
	function check(str) {
		var regExp = /^[a-z0-9_]{8,16}$/;		
		if(!regExp.test(str)) {
			return false; 
		} else { 
			return true; 
		} 
	}
    $(document).on("click","#LOGIN_BUTTON",function(e){
    	var CUSTOMER_ID = $("#CUSTOMER_ID").val();
    	var CUSTOMER_PW = $("#CUSTOMER_PW").val();
        if(!check(CUSTOMER_ID)||!check(CUSTOMER_PW)){
        	alert("아이디와 비밀번호는 알파벳과 숫자로 8자리이상 16자리이하 입니다.");
        }else{
        	$.ajax({
        		"url":"/LibraryService/customer/getPublicKey.do",
        		"dataType":"JSON",
        		"type":"POST",
        		"success":function(result){
                	CUSTOMER_PW = encryptByRSA2048(CUSTOMER_PW,result[0].PUBLICKEY);
                	$.ajax({
                		"url":"/LibraryService/customer/login.do",
                		"type":"POST",
                		"dataType":"JSON",
                		"data":{
                			"CUSTOMER_ID":CUSTOMER_ID,
                			"CUSTOMER_PW":CUSTOMER_PW
                		},
                		"success":function(result){
                			if(result[0].FLAG=="FALSE"){
                				alert(result[0].CONTENT);
                			}else{
                				location.href = "/LibraryService/resource/jsp/main.jsp";
                			}
                		},
                		"error":function(){
                			alert("에러");
                		}
                	});
        		},
        		"error":function(){
        			console.log("에러");
        		}
        	});
        }
    });
})

login.js 소스코드, 사용자는 먼저 AJAX요청으로 서버의 공개키를 요구한다.

getPublicKey.do 요청을 수행하는 것에 유의하자.

 

 

 

 

 

 

 

 

클라이언트 상에서 RSA 암호화를 진행하는 것은 JSEncrypt.js 오픈소스를 이용했다.

 

JSEncrypt

Introduction When browsing the internet looking for a good solution to RSA Javascript encryption, there is a whole slew of libraries that basically take the fantastic work done by Tom Wu @ http://www-cs-students.stanford.edu/~tjw/jsbn/ and then modify that

travistidwell.com

 

 

 

 

 

function encryptByRSA2048(plaintext, publickey){
    var rsa = new JSEncrypt({default_key_size: 2048});
    rsa.setPrivateKey(publickey);
    return rsa.encrypt(plaintext);
}

function decryptByRSA2048(ciphertext, privatekey){
    var rsa = new JSEncrypt({default_key_size: 2048});
    rsa.setPrivateKey(privatekey);
    return rsa.decrypt(ciphertext);
}

해당 소스코드는 JSEncrypt.js 오픈소스의 기능을 활용하여 암호화, 복호화를 진행하는 함수이다.

클라이언트는 이 함수를 호출함으로써 RSA-2048 암호화를 진행할 것이다.

 

 

 

 

 

 

 

 

 

		if(action.equals("/login.do")) {
			PrintWriter out = response.getWriter();
			LoginDAO dao = new LoginDAO();
			HttpSession session = request.getSession();
			if(session.getAttribute("CUSTOMER")!=null) {
				JSONObject json = new JSONObject();
				json.put("FLAG", "LOGON");
				json.put("CONTENT", "이미 로그인중입니다.");
				JSONArray array = new JSONArray();
				array.add(json);
				out.print(array);
			}else {
				String CUSTOMER_ID = request.getParameter("CUSTOMER_ID");
				String CUSTOMER_PW = request.getParameter("CUSTOMER_PW");
				
				CUSTOMER_PW = RSA2048.decrypt(CUSTOMER_PW, (String)session.getAttribute("PRIVATEKEY"));
				CustomerVO CUSTOMER = dao.login(CUSTOMER_ID, CUSTOMER_PW);
				
				JSONArray array = new JSONArray();
				JSONObject json = new JSONObject();
				if(CUSTOMER == null) {
					session.invalidate();
					json.put("FLAG", "FALSE");
					json.put("CONTENT", "해당 로그인 정보가 존재하지 않습니다.");
					array.add(json);
					out.print(array);

				}else {					
					session.setAttribute("CUSTOMER", CUSTOMER);
					json.put("FLAG", "TRUE");
					json.put("CONTENT", "로그인에 성공했습니다.");
					array.add(json);
					out.print(array);
				}
			}
			out.flush();
			out.close();
			
		//회원가입 기능
		}

컨트롤러에서는 다음과 같이 전달 받은 값을 RSA 복호화한후에 모델로 전달한다.

 

 

 

 

 

 

 

 

 

package customer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;

import encrypt.SHA;

public class LoginDAO {
	
	private DataSource dataSource;
	private Connection connection;
	
	public LoginDAO() {
		try {
			Context context = new InitialContext();
			Context envContext = (Context) context.lookup("java:/comp/env");
			dataSource = (DataSource) envContext.lookup("jdbc/oracle");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public CustomerVO login(String CUSTOMER_ID, String CUSTOMER_PW){
		CustomerVO customer;
		try {
			String query="";
			query += "SELECT * ";
			query += "FROM CUSTOMER ";
			query += "WHERE CUSTOMER_ID='"+CUSTOMER_ID+"'";
			connection = dataSource.getConnection();
			PreparedStatement pstmt = connection.prepareStatement(query);
			ResultSet rs = pstmt.executeQuery();
			if(rs.next()) {
				if(rs.getString("CUSTOMER_PW").equals(SHA.DSHA512(CUSTOMER_PW,rs.getString("SALT")))) {
					customer = new CustomerVO(rs.getString("CUSTOMER_ID"),rs.getString("CUSTOMER_PW"),rs.getString("CUSTOMER_NAME"),rs.getString("CUSTOMER_PHONE"),rs.getString("CUSTOMER_EMAIL"),rs.getString("CUSTOMER_ADDRESS"),rs.getString("CUSTOMER_BDATE"),rs.getString("KIND_NUMBER"));
				}else {
					customer = null;
				}				
			}else {
				customer = null;
			}
			rs.close();
			pstmt.close();
			connection.close();
			return customer;
		}catch(Exception e) {
			e.printStackTrace();
			return null;
		}
	}
}

모델에서는 전달받은 평문 비밀번호를 SALT를 덧붙여 해싱후에 해당 정보가 DB에 있는지 질의한다.

이때 SALT를 덧붙여 해싱함으로써 레인보우 테이블의 공격에 저항성을 갖게 구성했음에 유의한다.

 

 

 

 

 

 

 

 

이렇게 로그인과 회원가입, 비밀번호 찾기 등에 대하여

비밀번호, 비밀번호 찾기 질문에 대한 답 등을 RSA 암호화 후에 전송하고 나서 유효성을 판단하고 DB에 단방향 해싱인

SHA-512를 적용하여 저장하고 난 이후의 DB의 스냅샷은 다음과 같다.

 

 

 

사용자 비밀번호가 해싱되어 저장되어있음을 알 수 있다.

 

 

비밀번호 찾기 질문에 대한 답 또한 해싱되어 정답을 알 수 없다.

Comments