Spring

[Spring] Server-Client 간 암, 복호화 구현

lakelight 2022. 8. 18. 10:33
728x90
반응형

이미지 출처: https://www.entrust.com/ko/resources/hsm/faq/what-is-data-encryption

 

서버와 클라이언트 간에 데이터를 전송할 때 암호화를 해서 데이터를 보내려고 합니다.
이때 어떻게 암, 복화화를 하면 좋을까요?

데이터 암, 복호화에 사용할 알고리즘은 두 가지 입니다.
AESRSA입니다. 그럼 이 두가지에 대한 개념에 대해 먼저 알아보겠습니다.

 

AES

대칭키 알고리즘입니다. 즉, 같은 키로 암호화와 복호화를 진행합니다.
대칭키 알고리즘은 속도가 빠르지만 키 배송에 대한 문제가 있습니다.
또한 사용자가 증가할 수록 키교환을 해야하기 때문에 많은 키를 관리해야할 수도 있습니다.

RSA

공개키 암호화 방식으로, 서로 다른 키로 암호화와 복호화를 진행합니다.
공개키를 이용해서 암호화를 하고, 개인키를 이용해서 복호화를 진행합니다.
대칭키 알고리즘에 비해 속도가 느립니다.

 

Server-Client 간 암, 복호화 전체적인 흐름

  1. 클라이언트 로그인 요청 → 서버에 있는 RSA비밀키와 대응되는 RSA공개키를 전송
    • 클라이언트가 데이터를 전송할 때만 암호화를 진행할 것이기 때문에 공개키는 클라이언트가, 비밀키는 서버가 가지고 있습니다.
  2. 클라이언트 데이터 요청  요청 데이터를 AES 대칭키로 암호화하고, AES 키를 서버에게 받은 RSA공개키로 암호화
    • 클라이언트는 AES로 암호화된 데이터와, RSA로 암호화된 AES키를 서버에 요청합니다.
  3. 서버 복호화  RSA로 암호화된 AES키를 RSA비밀키로 복호화, 복호화된 AES키를 이용해 데이터 복호화
    • 복호화된 데이터를 사용하여 클라이언트가 필요한 데이터 응답합니다.

 

Server-Client 간 암, 복호화 코드 구현

[User.class] 회원관리를 위한 클래스

package hooyn.rsa.entity;

import lombok.Data;
import lombok.Getter;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
@Getter
@Data
public class User {
    @Id @GeneratedValue
    @Column(name = "user_id")
    private Long id;

    private String email;
    private String password;

    public User(String email, String password) {
        this.email = email;
        this.password = password;
    }

    public User() {

    }
}

 

[UserRepository.interface]

package hooyn.rsa.repository;

import hooyn.rsa.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
    User findByEmail(String email);
}

 

[AESUtil.class] AES 암복호화를 위한 클래스

package hooyn.rsa.core;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class AESUtil {
    public static String encrypt(String text, String key) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
        IvParameterSpec ivParamSpec = new IvParameterSpec(key.substring(0, 16).getBytes());
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivParamSpec);

        byte[] encrypted = cipher.doFinal(text.getBytes("UTF-8"));
        return Base64.getEncoder().encodeToString(encrypted);
    }

    public static String decrypt(String cipherText, String key) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
        IvParameterSpec ivParamSpec = new IvParameterSpec(key.substring(0, 16).getBytes());
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivParamSpec);

        byte[] decodedBytes = Base64.getDecoder().decode(cipherText);
        byte[] decrypted = cipher.doFinal(decodedBytes);
        return new String(decrypted, "UTF-8");
    }
}

 

[RSAUtil.class] RSA 암복호화를 위한 클래스

package hooyn.rsa.core;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

public class RSAUtil {
    private static KeyPair keyPair = null;
    public static KeyPair genRSAKeyPair() throws NoSuchAlgorithmException {
        if(keyPair!=null){
            return keyPair;
        }
        SecureRandom secureRandom = new SecureRandom();
        KeyPairGenerator gen;
        gen = KeyPairGenerator.getInstance("RSA");
        gen.initialize(1024, secureRandom);
        keyPair = gen.genKeyPair();
        return keyPair;
    }

    public static String getDecryptText(String encrypted, PrivateKey privateKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, UnsupportedEncodingException {
        Cipher cipher = Cipher.getInstance("RSA");
        byte[] byteEncrypted = Base64.getDecoder().decode(encrypted.getBytes());
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] bytePlain = cipher.doFinal(byteEncrypted);
        String decrypted = new String(bytePlain, "utf-8");
        return decrypted;
    }

    public static String getEncryptText(String plainText, PublicKey publicKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] bytePlain = cipher.doFinal(plainText.getBytes());
        String encrypted = Base64.getEncoder().encodeToString(bytePlain);
        return encrypted;
    }

    public static PrivateKey getPrivateKeyFromBase64String(final String keyString) throws NoSuchAlgorithmException, InvalidKeySpecException {
        System.out.println("keyString = " + keyString);

        final String privateKeyString =
                keyString.replaceAll("\\n",  "").replaceAll("-{5}[a-zA-Z]*-{5}", "");

        System.out.println("privateKeyString = " + privateKeyString);

        KeyFactory keyFactory = KeyFactory.getInstance("RSA");

        PKCS8EncodedKeySpec keySpecPKCS8 =
                new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyString));

        return keyFactory.generatePrivate(keySpecPKCS8);
    }

    public static PublicKey getPublicKeyFromBase64String(final String keyString) throws NoSuchAlgorithmException, InvalidKeySpecException {

        final String publicKeyString =
                keyString.replaceAll("\\n",  "").replaceAll("-{5}[ a-zA-Z]*-{5}", "");

        KeyFactory keyFactory = KeyFactory.getInstance("RSA");

        X509EncodedKeySpec keySpecX509 =
                new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyString));

        return keyFactory.generatePublic(keySpecX509);
    }
}

 

[UserController.class] 기능을 사용하기 위한 API Controller

package hooyn.rsa.controller;

import hooyn.rsa.controller.request.DataDecryptionRequest;
import hooyn.rsa.controller.request.DataEncryptionRequest;
import hooyn.rsa.controller.request.JoinRequest;
import hooyn.rsa.controller.request.LoginRequest;
import hooyn.rsa.core.AESUtil;
import hooyn.rsa.core.RSAUtil;
import hooyn.rsa.entity.User;
import hooyn.rsa.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;

import java.security.*;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequiredArgsConstructor
public class UserController {

    private final UserRepository userRepository;

    @PostMapping("/join")
    @Transactional
    public String join(@RequestBody JoinRequest request){
        User user = new User(request.getEmail(), request.getPassword());

        User user1 = userRepository.save(user);
        return user1.toString();
    }

    @PostMapping("/login")
    public Map<String, String> login(@RequestBody LoginRequest request) throws NoSuchAlgorithmException {
        User byEmail = userRepository.findByEmail(request.getEmail());
        Map<String, String> response = new HashMap<>();

        // RSA에서 필요한 공개키와 개인키를 가져옵니다.
        KeyPair keyPair = RSAUtil.genRSAKeyPair();
        PublicKey publicKey = keyPair.getPublic();

        System.out.println("publicKey = " + publicKey);

        // 공개키를 Base64 인코딩한 문자일을 만듭니다.
        byte[] bytePublicKey = publicKey.getEncoded();
        String base64PublicKey = Base64.getEncoder().encodeToString(bytePublicKey);

        if(request.getPassword().equals(byEmail.getPassword())){
            response.put("RSAKey", base64PublicKey);
            response.put("message", "로그인 성공");
            return response;
        } else {
            response.put("RSAKey", null);
            response.put("message", "로그인 실패");
            return response;
        }
    }

    //client
    @PostMapping("/data/en")
    public Map<String, String> dataEncryption(@RequestBody DataEncryptionRequest request) throws Exception {
        Map<String, String> response = new HashMap<>();
        String aesKey = RandomStringUtils.randomAlphanumeric(32);

        String encrypt = AESUtil.encrypt(request.getData(), aesKey);

        PublicKey publicKey = RSAUtil.getPublicKeyFromBase64String(request.getRsaKey());
        String encryptAESKey = RSAUtil.getEncryptText(aesKey, publicKey);
        response.put("EncryptedAESKey", encryptAESKey);
        response.put("EncryptedData", encrypt);

        return response;
    }

    @PostMapping("/data/de")
    public Map<String, String> dataDecryption(@RequestBody DataDecryptionRequest request) throws Exception {
        Map<String, String> response = new HashMap<>();

        String decryptAesKey = RSAUtil.getDecryptText(request.getEncryptedAESKey(), RSAUtil.genRSAKeyPair().getPrivate());
        String data = AESUtil.decrypt(request.getEncryptedData(), decryptAesKey);

        response.put("data", data);

        return response;
    }
}

 

[UserControllerView.class] HTML 화면을 보여주기 위한 Controller

package hooyn.rsa.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class UserControllerView {
    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String loginForm() {
        return "login";
    }
}

 

[login.html] 테스트를 위한 HTML 구성 - fetch 사용하여 구현

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>로그인</title>
</head>
<body>
<br>
<div>
    <div class="input-box">
        <label for="username">아이디   </label>
        <input id="username_join" type="text" name="username" placeholder="아이디">
    </div>

    <div class="input-box">
        <label for="password">비밀번호</label>
        <input id="password_join" type="password" name="password" placeholder="비밀번호">
    </div>
    <input type="button" style="WIDTH: 250pt; HEIGHT: 45pt" id="join_button" onclick="button_click1();" value="Join"/>
</div>
<br><br><br>
<div>
    <div class="input-box">
        <label for="username">아이디   </label>
        <input id="username" type="text" name="username" placeholder="아이디">
    </div>

    <div class="input-box">
        <label for="password">비밀번호</label>
        <input id="password" type="password" name="password" placeholder="비밀번호">
    </div>
    <input type="button" style="WIDTH: 250pt; HEIGHT: 45pt" id="login_button" onclick="button_click2();" value="Login"/>
</div>
<br><br><br>
<div>
    <input type="button" style="WIDTH: 250pt; HEIGHT: 45pt" id="encryption" onclick="button_click3();" value="Client To Server [Data Send]"/>
    <br>
    <textarea style="WIDTH: 250pt; HEIGHT: 45pt" id="data"></textarea>
</div>
<br><br><br>
<div>
    <input type="button" style="WIDTH: 250pt; HEIGHT: 45pt" id="decryption" onclick="button_click4();" value="Data Decoding At Server [Data Receive]"/>
</div>
<br><br><br>
  <script>

      var rsaKeyInfo;
      var requestInfo;

      const sleep = async (ms) => {
          return new Promise(
              (resolve, reject) =>
                  setTimeout(
                      () => resolve(),
                      ms * 1000
                  )
          );
      };

      async function button_click1(){
          console.log("회원 정보 입력");

          await sleep(1);
          console.log("...");
          await sleep(1);

          fetch("http://localhost:8080/join", {
              method: "POST",
              headers: {
                  "Content-Type": "application/json",
              },
              body: JSON.stringify({
                  email: document.getElementById("username_join").value,
                  password: document.getElementById("password_join").value
              }),
          })
              .then((response) => console.log(response));

          console.log("회원가입 성공");
      }

      async function button_click2(){
          console.log("로그인 정보 입력")
          await sleep(1);

          fetch("http://localhost:8080/login", {
              method: "POST",
              headers: {
                  "Content-Type": "application/json",
              },
              body: JSON.stringify({
                  email: document.getElementById("username").value,
                  password: document.getElementById("password").value
              }),
          })
              .then((response) => response.json())
              .then((data) => rsaKeyInfo=data);

          console.log("...");
          await sleep(1);
          console.log("로그인 성공");
          console.log(rsaKeyInfo);
      }

      async function button_click3(){
          console.log("Client에서 Server로 데이터 암호화 후 키와 함께 전송")
          await sleep(1);

          var rsaKeyString = JSON.stringify(rsaKeyInfo);
          var rsaParsing = rsaKeyString.split('"');
          var rsaKeyData = rsaParsing[3];//RSA 키 받아오기

          fetch("http://localhost:8080/data/en", {
              method: "POST",
              headers: {
                  "Content-Type": "application/json",
              },
              body: JSON.stringify({
                  rsaKey: rsaKeyData,
                  data: document.getElementById('data').value
              }),
          })
              .then((response) => response.json())
              .then((data) => requestInfo=data);

          console.log("...");
          await sleep(1);
          console.log("데이터 전송 완료")
          console.log(requestInfo);
      }

      async function button_click4(){
          console.log("서버에서 데이터 복호화 처리")
          await sleep(1);

          var requestString = JSON.stringify(requestInfo);
          var requestParsing = requestString.split('"');
          var encryptedAESKeydata = requestParsing[3];
          var encryptedDatadata = requestParsing[7];

          fetch("http://localhost:8080/data/de", {
              method: "POST",
              headers: {
                  "Content-Type": "application/json",
              },
              body: JSON.stringify({
                  encryptedAESKey: encryptedAESKeydata,
                  encryptedData: encryptedDatadata
              }),
          })
              .then((response) => response.json())
              .then((data) => console.log(data));
      }
  </script>
</body>
</html>

 

Git Link - 더 자세한 코드를 위한 Github Link

 

GitHub - hooyn/DataEncryptionAndDecryption: 트윔 신입 교육 - RSA를 이용한 데이터 암호화, 복호화

트윔 신입 교육 - RSA를 이용한 데이터 암호화, 복호화. Contribute to hooyn/DataEncryptionAndDecryption development by creating an account on GitHub.

github.com

 

728x90
반응형