Project/codestates-final-project

리펙토링 및 개선 - 4 / RSA 적용

fullfish 2022. 6. 1. 15:43

클라이언트단에서 서버단으로 정보보낼 때 중간에 password 탈취에 대응하기위해

RSA를 적용해서 password 암호화를 해주었다

 

RSA에 대해 내가 쓴 글

 

RSA

RSA란? 현재 SSL/TLS에 가장 많이 사용되는 공개키 암호화 알고리즘 전세계 대부분의 인터넷 뱅킹에서 사용 대칭키가 아닌 공개키와 개인키가 한 쌍을 이룸 공개키로 암호화한 내용은 개인키로만

fullfish.tistory.com

 

회원가입 흐름

  1. 클라이언트에서 email과 nickname 그리고 createKey: true(키 생성위해)를 보냄
  2. 서버에서 이메일 중복확인
  3. 중복이면 중복 에러
  4. 중복아니면 키를 생성
  5. 서버에서 첫 요청시와 그 다음 요청시에 값을 다 입력했는지 검증
  6. db d,e,N저장 (email nickname ,d , e, N 있는 유저 생성)
  7. 클라이언트로 공개키 (e, N)보냄
  8. 클라이언트에서 password 암호화
  9. 서버로 password 암호화한것과 email보냄
  10. 서버에서 아까 email을 이용해서 키들 받아옴
  11. 서버에서 복호화한후 Bcrypt를 이용해서 hashing하고 db에 아직 null값인 password에 저장

코드

// 클라이언트
export const signUpApi = async (email, nickname, password) => {
  const res = await signCustomApi.post('up', {
    createKey: true,
    nickname,
    email,
  });
  let encrypted = [];
  const e = BigInt(Number(JSON.parse(res.data.data.e)));
  const N = BigInt(Number(JSON.parse(res.data.data.N)));
  BigInt.prototype.toJSON = function () {
    return this.toString();
  };
  console.time('암호화');
  for (let i = 0; i < password.length; i++) {
    let a = BigInt(password[i].charCodeAt(0));
    encrypted[i] = JSON.stringify(power(a, e, N));
  }
  console.timeEnd('암호화');
  const res2 = await signCustomApi.post('up', {
    email,
    nickname,
    password: encrypted,
  });
  return res2;
};

export const signInApi = async (email, password) => {
  const result = await signCustomApi.post('in', {
    email,
    password,
  });
  try {
    if (result.data.accessToken) {
      sessionStorage.setItem('user', JSON.stringify(result.data));
    }
    return result.data;
  } catch (err) {
    console.log(err);
  }
};
// 서버
 up: {
    post: async (req, res) => {
      try {
        const { email, nickname, createKey } = req.body;
        let password = req.body.password;
        const userInfo = await user.findOne({
          where: {
            email,
          },
        });
        // 가입 안되어 있을 경우 키 생성
        if (createKey === true) {
          // 이미 가입되었을 경우
          if (userInfo) {
            await slack.slack("Signup Post 409");
            return res.status(409).send({ message: "email already exists" });
          }
          let [e, N, d] = RSA.createKey();
          const payload = {
            email,
            nickname,
            password: "temp",
            d,
            e,
            N,
          };
          await user.create(payload);
          BigInt.prototype.toJSON = function () {
            return this.toString();
          };
          e = JSON.stringify(e);
          N = JSON.stringify(N);
          return res.status(201).send({ data: { e: e, N: N } });
        } else {
          if (!email || !password || !nickname) {
            await slack.slack("Signup Post 422");
            return res.status(422).send({ message: "insufficient parameters supplied" });
          }
          let passwordBigIntArr = [];
          console.time("복호화");
          for (let i = 0; i < password.length; i++) {
            passwordBigIntArr[i] = BigInt(Number(JSON.parse(password[i])));
          }
          console.timeEnd("복호화");
          let d = BigInt(userInfo.dataValues.d);
          let N = BigInt(userInfo.dataValues.N);
          const passwordDecryptedArr = passwordBigIntArr.map((ele) => {
            return String.fromCharCode(Number(power(ele, d, N)));
          });
          password = passwordDecryptedArr.join("");
          //     //? 방법 1 salt 생성 후 소금 치기
          bcrypt.genSalt(13, async function (err, salt) {
            bcrypt.hash(password, salt, async function (err, hash) {
              userInfo.password = hash;
              const result = await userInfo.save();
              await slack.slack("User Post 201", `id : ${result.dataValues.id}`);
              return res.status(201).send({ data: { id: result.dataValues.id } });
            });
          });
          //     //? 방법 2 salt 자동 생성
          // bcrypt.hash(password, 13, async function (err, hash) {
          // userInfo.password = hash;
          // const result = await userInfo.save();
          // await slack.slack("User Post 201", `id : ${result.dataValues.id}`);
          // return res.status(201).send({ data: { id: result.dataValues.id } });
          // });
          //     //? ---
        }
      } catch (err) {
        await slack.slack("Signup Post 501");
        res.status(501).send("Signup Post");
      }
    },
  },

데이터 흐름이

클라이언트 -> 서버 -> 클라이언트 -> 서버 -> 클라이언트이다

 

공개키를 클라이언트가 가지고 있으면 더욱 더 좋겠지만 s3에 저장하는것에 어려움을 느껴 db에 저장했다

 

BigInt 타입은 서버와 클라이언트간에 전송이 안되어서 

BigInt.prototype.toJSON = function () {
    return this.toString();
};

를 이용해서 json화 시켜주었으며 받은 쪽에서는 다시 BigInt화 시켜주었다

 

로그인 흐름

  1. 클라이언트에서 이메일과 키확인용(공개키 받기위해) 보냄
  2. 서버에서 이메일 존재 확인
  3. 없으면 에러
  4. 있으면 클라이언트로 e, N 보냄
  5. 서버에서 첫 요청시와 그 다음 요청시에 값을 다 입력했는지 검증
  6. 클라이언트에서 password 암호화
  7. 서버로 password암호화한것과 email보냄
  8. 서버에서 email 이용해서 개인 받아옴
  9. 복호화함
  10. Bcrypt검증해서 틀리면 비번이 틀린거
  11. 맞으면 로그인

코드

// 클라이언트
export const signInApi = async (email, password) => {
  const res = await signCustomApi.post('in', {
    checkKey: true,
    email,
  });
  let encrypted = [];
  const e = BigInt(Number(JSON.parse(res.data.data.e)));
  const N = BigInt(Number(JSON.parse(res.data.data.N)));
  BigInt.prototype.toJSON = function () {
    return this.toString();
  };
  for (let i = 0; i < password.length; i++) {
    let a = BigInt(password[i].charCodeAt(0));
    encrypted[i] = JSON.stringify(power(a, e, N));
  }
  const res2 = await signCustomApi.post('in', {
    email,
    password: encrypted,
  });
  try {
    if (res2.data.accessToken) {
      sessionStorage.setItem('user', JSON.stringify(res2.data));
    }
    return res2.data;
  } catch (err) {
    console.log(err);
  }
};
// 서버
in: {
    post: async (req, res) => {
      try {
        const { email, checkKey } = req.body;
        let password = req.body.password;
        if (!email) {
          await slack.slack("Signin Post 422");
          return res.status(422).send({ message: "insufficient parameters supplied" });
        }
        const userInfo = await user.findOne({
          where: {
            email,
          },
        });
        //데이터베이스에 email이 없을때
        if (!userInfo) {
          await slack.slack("Signin Post 400");
          return res.status(400).send({ message: "Wrong email" });
        }
        let e = 0;
        let N = 0;
        if (checkKey === true) {
          e = userInfo.dataValues.e;
          N = userInfo.dataValues.N;
          return res.status(200).send({ data: { e: e, N: N } });
        } else {
          let passwordBigIntArr = [];
          for (let i = 0; i < password.length; i++) {
            passwordBigIntArr[i] = BigInt(Number(JSON.parse(password[i])));
          }

          let d = BigInt(userInfo.dataValues.d);
          let N = BigInt(userInfo.dataValues.N);
          const passwordDecryptedArr = passwordBigIntArr.map((ele) => {
            return String.fromCharCode(Number(power(ele, d, N)));
          });
          password = passwordDecryptedArr.join("");
          bcrypt.compare(password, userInfo.password, async function (err, result) {
            //데이터베이스에 email 있지만 비밀번호가 틀릴때
            if (result === false) {
              await slack.slack("Signin Post 400");
              return res.status(400).send({ message: "Wrong password" });
            }

            //데이터 베이스에 회원정보가 있을 경우
            else {
              const payload = {
                id: userInfo.id,
                email,
              };
              const accessToken = jwt.sign(payload, process.env.ACCESS_SECRET, {
                expiresIn: "30m",
              });
              const refreshToken = jwt.sign(payload, process.env.REFRESH_SECRET, {
                expiresIn: "6h",
              });
              res.cookie("refreshToken", refreshToken, {
                sameSite: "Strict",
                httpOnly: true,
                secure: false, // https로 바꾼후에 true로 바꿔야함
              });
              await slack.slack("Signin Post 200", `id : ${userInfo.id}`);
              res.status(200).send({
                data: { id: userInfo.id },
                accessToken: accessToken,
              });
            }
          });
        }
      } catch (err) {
        await slack.slack("Signin Post 501");
        res.status(501).send("Signin Post");
      }
    },
  },