POMOTODO : 암호화

2022. 1. 5. 17:04프로그래밍/개인프로젝트

POMOTODO를 만들면서 작성한 코드를 복습하기 위해 작성하는 글입니다.

 

POMOTODO.kr

 

What is POMOTODO?


 

사용자의 정보를 받을 때 비밀번호를 직접 받으면 보안에 굉장히 취약하다는 문제가 있다
그래서 입력받은 비밀번호를 해쉬값으로 변경해서 서버에 저장해야한다

해쉬? : 입력값을 일정한 로직에 따라 사람이 알 수 없는 문자로 변환해줌

내생각 : 

- hash를 사전에 검색하면 나오는 정의
    1.'#'이라는 기호
    2. '고기와 감자를 잘게 다져 섞어 요리하여 따뜻하게 차려 낸 것' 이라는 요리법  
- 해시함수에 대한 정의
   1. '하나의 주어진 출력에 대하여 이 출력으로 사상시키는 하나의 입력을 찾는 것이 계산적으로 불가능하고....,'

즉, 요리에 빗대서 쉽게 생각해보면 고기와 감자를 잘게다져서 섞으면 어떤재료들이 어떤 형태로 들어갔는지 사람은 알 수 없게 되는것처럼 
입력값을 사람이 알 수 없는 문자들로 변환해주는 작업이라고 할 수 있다

 

md5를 사용하는 방법 (사용금지 - 단순 공부용)

1. 모듈설치
    npm istall md5
 
2. server에 md5 로드
 let md5 = require('md5')​
3. 사용예제 :
   md5('입력값')을 출력해보면 입력값에 따라 암호화된 문자를 출력해준다
   (사용자가 입력하는 값을 md5()로 묶어주고 서버와 대조해서 맞으면 로그인하도록 해준다)
4. 사용상의 문제 : 
   하지만 암호화된 해쉬값을 입력값으로 변환해주는 프로그램을 사용하면 중요 정보가 노출될 수 있으므로 
   단독으로 사용하면 매우 위험하다
   그리고 보안이 이미 뚫렸으므로 md5는 암호화로서의 가치가 없어졌다고 할 수 있다

 

해쉬값변환을 통한 정보노출에 대한 해결책 salt

랜덤생성되는 암호화 키값인 salt와 '사용자가 입력한 비밀번호'를 더해서 '암호화된 해쉬값'을 만들면
해쉬값 변환 프로그램을 통해 알 수가 없다
db에 '암호화된 해쉬값','salt값'을 넣어두고 유저의 '입력값'+'salt값' = '암호화된해쉬값' 인지 검사하도록 구현한다
salt값을 생성해주는 pbkdf2를 사용하도록 해보자

pbkdf2 (Password-Based Key Derivation Function 2)

pbkdf2는 키 스트레칭 기법 (중첩암호화기법)을 사용하기 위해 만들어진 함수이다

1. 모듈설치
    npm install pbkdf2-password

2. server.js에 코드작성
 
let bkfd2Password = require('pbkdf2-password')
let hasher = bkfd2Password();
hasher({password:'비밀번호'}, function(err, pass, salt, hash){
   console.log(err, pass, salt, hash);
   //콘솔로그로 찍으면  err = undefined, pass:입력한비밀번호값, salt: 랜덤생성, hash: 입력비밀번호+salt값에 대한 해쉬값 을 출력해준다
})​

3.  회원가입부분과 로그인 부분에 코드를 추가하였다

회원가입

 

app.post('/signupResult',function(req, res){
        db.collection('users').findOne({id: req.body.loginId}, function(err,result){
            if(result == null){
                // console.log('아이디가 없음') 
                //서버에 아이디가 없으면 if, 있으면 else 출력 -> 없으면 가입을 진행해주고 있으면 얼럿창띄운다음에 회원가입페이지로 보내기
                hasher({password: req.body.password}, function(err, pass, salt, hash){
                    // console.log(err, pass, salt, hash);
                    // err = undefined, pass:입력한비밀번호값, salt: 랜덤번호생성, hash: 입력비밀번호+salt값에 대한 해쉬값 을 출력해준다
                    // db의 컬렉션 만들어서 데이터 저장하기
                    db.collection('users').insertOne({ id : req.body.loginId, hashPassword : hash, saltPassword : salt, email : req.body.email, number : req.body.number, gender : req.body.gender,birthday : req.body.birthday, }, function(err, result){
                        console.log('db user create')
                    })
                    db.collection('pomodoro').insertOne({ id : req.body.loginId, content: '', contentHTML:''}, function(err, result){
                        console.log('db pomodoro create')
                    })
                    db.collection('todolist').insertOne({ id : req.body.loginId, todoList: '', todoListHTML:''}, function(err, result){
                        console.log('db todolist create')
                    })
                    db.collection('not-todolist').insertOne({ id : req.body.loginId, notTodoList: '', notTodoListHTML:''}, function(err, result){
                        console.log('db not-todolist create')
                    })
                    res.send("<script>alert('WELCOME !');location.href='/login';</script>");
                })
            }else{
                // console.log('아이디가 있음')
                res.send("<script>alert('this id is already in use.');location.href='/signup';</script>");
            }
        });
    })

로그인

- login.ejs

<!--login.ejs-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png">
    <link rel="icon" type="image/png" sizes="32x32" href="favicon/favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="16x16" href="favicon/favicon-16x16.png">
    <link rel="manifest" href="favicon/site.webmanifest">
    <link rel="mask-icon" href="favicon/safari-pinned-tab.svg" color="#5bbad5">
    <meta name="msapplication-TileColor" content="#da532c">
    <meta name="theme-color" content="#ffffff">
    
    <title>POMOTODO</title>
    <link rel="stylesheet" href="login-signup.css" />
    <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script> 
    <script defer src="login.js"></script>
</head>
<body>
    <div class="container">
      <h1>log in</h1>
      <div class="login-title-underline"></div>
      <form method="post" name="loginForm" id="authForm" action="/loginPost">
          <div class="form-list">
            <input type="text" class="form-list" id="loginId" name="loginId" placeholder="username" minlength="5" maxlength="10" autocomplete='off' autofocus>
          </div>
          <div class="form-list">
            <input type="password" class="form-list" id="loginPw" name="loginPassword" placeholder="password" minlength="5" maxlength="20" autocomplete='off'onkeydown="if(window.event.keyCode==13){printName()}">
          </div>
          <div class="id-check"><%= loginFeedback %></div>
          <button type="button" class="form-list" id="form-login" onclick='printName()' >login</button>
          <button type="button" class="form-list" id="login-toHome" onclick="location.href='/'">home</button>
          <button type="button" class="form-list" id="login-toSignup" onclick="location.href='/signup'">sign up</button>
          <button type="button" class="form-list" id="form-pomotodo" onclick="window.open('https://github.com/coqoa/POMOTODO/blob/main/README.md')">what is POMOTODO?</button>
      </form>
    </div>
</body>
</html>

- login-signup.css(login.ejs와 signup.ejs에 첨부하는 css파일)

/* login-signup.css */
@import url("https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
/* font-family: 'Ubuntu', sans-serif; */
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+KR:wght@100;200;300;400;500;600;700&display=swap");
/* font-family: 'IBM Plex Sans KR', sans-serif; */

:root {
  --backgroundColor: #f0f0f0;
  --contentColor: #f5f5f5;
  --blackShadow: #cfcfcf;
  --whiteShadow: #f2f2f2;
  --lightgrayShadow: #d9d9d9;
  --bluetext: #221974;
  --redtext: #ff8585;
  --hoverredtext: #f75858;
  --graytext: #c7c8ca;
  --beigetext: #f5f5dc;
  --greentext: #76c576;
}
body {
  /* background-color: #e9f0fa; */
  background-color: var(--backgroundColor);
}
.container {
  /* background-color: #e9f0fa; */
  background-color: var(--contentColor);
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 350px;
  height: 600px;
  box-shadow: 7px 7px 10px var(--blackShadow), -4px -4px 5px var(--whiteShadow);
  border-radius: 15px;
  zoom: 80%;
}
/* #signupForm, */
/* 로그인폼 레이아웃 */
#authForm {
  position: relative;
  top: 20%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
/* 로그인 폼 내 각 요소에 관한 코드 */
.form-list {
  font-family: "Ubuntu", "IBM Plex Sans KR";
  font-size: 17px;
  font-weight: 400;
  font-style: italic;
  color: var(--bluetext);
  padding: 5px;
  margin: 3px;
  border: 0;
  border-radius: 1px;
  text-align: center;
  background-color: transparent;
}
h1 {
  font-family: "Ubuntu", "IBM Plex Sans KR";
  font-size: 40px;
  font-weight: 500;
  font-style: italic;
  color: var(--bluetext);
  position: relative;
  top: 30px;
  right: 85px;
  text-align: center;
  margin-bottom: 150px;
}
label {
  position: relative;
  top: 0px;
  right: 100px;
}
/*------------------------------------------------ ⬆︎ login, signup공통적용 ------------------------------------------------ */

/* ⬇︎ 개별적용 */
/*------------------------------------------------ ⬇︎ login ------------------------------------------------ */

.login-title-underline {
  position: relative;
  top: -125px;
  left: 45px;
  height: 5px;
  width: 95px;
  border-bottom: 3px solid var(--bluetext);
}
#loginPw,
#loginId {
  position: relative;
  top: 20px;
  width: 250px;
  height: 40px;
  border: 0;
  margin-bottom: 30px;
  background-color: white;
  /* border: 1px solid white; */
  border-radius: 15px;
  box-shadow: inset 2px 2px 3px var(--blackShadow),
    inset -2px -2px 2px var(--whiteShadow);
}
#loginPw {
  top: 26px;
}
#form-login {
  font-size: 20px;
  font-weight: 500;
  position: relative;
  top: 25px;
  margin-top: 20px;
  width: 260px;
  height: 50px;
  color: var(--bluetext);
  background-color: var(--contentColor);
  box-shadow: 4px 4px 6px var(--blackShadow), -3px -3px 5px var(--whiteShadow);
  border-radius: 15px;
}
#form-login:hover {
  cursor: pointer;
  color: var(--hoverredtext);
}
#form-login:active {
  box-shadow: inset 2px 2px 3px var(--blackShadow), inset -2px -2px 2px #ffffff;
}

/* 플래시 메세지 */
.id-check {
  position: absolute;
  top: 210px;
  font-family: "Ubuntu", "IBM Plex Sans KR";
  font-size: 12px;
  font-weight: 500;
  font-style: italic;
  color: var(--redtext);
}
#login-toHome,
#login-toSignup,
#form-pomotodo {
  font-size: 15px;
  position: relative;
  color: var(--graytext);
  top: 40px;
}
#login-toHome:hover,
#login-toSignup:hover,
#form-pomotodo:hover {
  cursor: pointer;
  color: black;
}
/*------------------------------------------------ ⬇︎ signup ------------------------------------------------ */
#signup-container {
  height: 650px;
}
/* .form-list label {
  color: gray;
} */

/* 라벨위치 미세조정 */
.label-id {
  right: 128px;
}
.label-email {
  right: 115px;
}
.label-number {
  right: 106px;
}
.label-birthday {
  right: 104px;
}

.signup-title {
  top: 0px;
  right: 70px;
}
.signup-title-underline {
  position: relative;
  top: -150px;
  left: 42px;
  height: 5px;
  width: 130px;
  border-bottom: 3px solid var(--bluetext);
  margin-bottom: 40px;
}
.gender-label input[type="radio"] {
  display: none;
}
.gender-label input[type="radio"] + span {
  position: relative;
  width: 50px;
  top: 15px;
  left: 101px;
  display: inline-block;
  background: none;
  border: 1px solid lightgray;
  border-radius: 10px;
  padding: 0px 10px;
  margin-bottom: 20px;
  text-align: center;
  height: 35px;
  line-height: 33px;
  font-weight: 500;
  cursor: pointer;
  color: lightgray;
  z-index: 100;
}
.gender-label input[type="radio"]:checked + span {
  border: 1px solid var(--bluetext);
  background: none;
  color: var(--bluetext);
}
#signupId,
#signupPassword,
#signupPasswordCheck,
#signupEmail,
#signupNumber,
#signupBirthday {
  font-size: 14px;
  position: relative;
  width: 250px;
  height: 20px;
  background-color: white;
  /* border: 1px solid white; */
  margin: 3px;
  border-radius: 7px;
  border: 0;
  box-shadow: inset 2px 2px 3px var(--blackShadow),
    inset -2px -2px 2px var(--whiteShadow);
}
#form-signup {
  font-size: 18px;
  font-weight: 500;
  position: relative;
  top: -20px;
  margin-top: 20px;
  width: 260px;
  height: 35px;
  color: var(--bluetext);
  background-color: var(--contentColor);
  box-shadow: 4px 4px 6px var(--blackShadow), -3px -3px 5px var(--whiteShadow);
  border-radius: 8px;
  cursor: pointer;
}
#form-signup:hover {
  color: var(--redtext);
}
#form-signup:active {
  box-shadow: inset 2px 2px 3px var(--blackShadow), inset -2px -2px 2px #ffffff;
}
#signup-toHome,
#signup-toLogin {
  font-size: 15px;
  position: relative;
  color: var(--graytext);
}
#signup-toHome {
  top: -12px;
  left: -40px;
}
#signup-toLogin {
  top: -45px;
  left: 39px;
}
#signup-toHome:hover,
#signup-toLogin:hover {
  cursor: pointer;
  color: black;
}

/* 회원가입 입력값체크*/
.validate-id {
  display: block;
  font-size: 12px;
  position: absolute;
  top: 60px;
  left: 115px;
  color: var(--redtext);
}

.message {
  display: none;
  font-size: 12px;
}
#password-message-match,
#number-message-count,
#birthday-message-count,
#email-check-match {
  color: var(--greentext);
}
#password-message-unmatch,
#password-message-length,
#number-message,
#birthday-message,
#email-check-unmatch {
  color: var(--redtext);
}
#password-message-match,
#password-message-unmatch,
#password-message-length,
#number-message,
#number-message-count,
#birthday-message,
#birthday-message-count,
#email-check-match,
#email-check-unmatch {
  position: absolute;
  top: 175px;
  left: 26%;
}
#password-message-unmatch {
  left: 30%;
}

#email-check-match,
#email-check-unmatch {
  top: 250px;
  left: 28%;
}
#number-message,
#number-message-count {
  top: 320px;
  left: 27%;
}
#birthday-message,
#birthday-message-count {
  top: 385px;
  left: 27%;
}
#number-message-count,
#birthday-message-count,
#email-check-match,
#password-message-match {
  left: 49%;
}
@media screen and (max-width: 551px) {
  .container {
    zoom: 70%;
    top: 350px;
  }
}

- server.js

//server.js   
     // login버튼누르면 요청받는 post
     app.post('/loginPost', passport.authenticate('local', { // 로컬방식으로 인증
        failureRedirect : '/fail' ,
        failureFlash : true
         //실패시 /fail페이지로 이동시켜주세요
        }), function(req, res){
            req.session.save(function(){
                // 성공시 redirect해서 홈페이지로 보내주기
                res.redirect('/') 
            })
    });
    
    // 비밀번호 검사하는 passport 미들웨어 설정
    //new LocalStrategy( { 설정 }, function(){ 아이디비번 검사하는 코드 } )
    passport.use(new LocalStrategy({
         usernameField: 'loginId', //사용자가 제출한 id가 어디 적혔는지(input의 name 속성값)
         passwordField: 'loginPassword', //사용자가 제출한 pw가 어디 적혔는지(input의 name 속성값)
         session: true,  //로그인 후 세션을 저장할지 ?
         passReqToCallback: false, //사용자가 입력한 id,pw외에 다른정보도 검증해보고 싶으면

    }, function (inputId, inputPw, done) {
         // console.log(inputId, inputPw) -> 사용자가 입력한 id/pw가 콘솔로그로 출력됨
        db.collection('users').findOne({ id: inputId }, function (err, user) {
             //입력한 id에 대한 정보를 결과에 담아옴
             if (err) return done(err) //에러처리문법
            if (!user) { //db에 없는 아이디일때?
                return done(null, false, { message: 'ID does not exist' })
                 //done은 3개의 파라미터를 가질수 있음, 1: 서버에러, 2: 성공시 사용자db, 3:에러메시지
            }
            if(user){ // db에 아이디가 존재하면?
                hasher({password:inputPw, salt: user.saltPassword}, function(err, pass, salt, hash){
                     // console.log(err, pass, salt, hash);
                     // err = undefined, pass:입력한비밀번호값, salt: 랜덤번호생성, hash: 입력비밀번호+salt값에 대한 해쉬값 을 출력해준다
                     // 입력비밀번호와 유저의 salt를 가져와서 hash로 만들고 그 해쉬값이 서버의 해쉬값과 일치하다면  로그인성공
                    if (hash == user.hashPassword) { // 인증완료 - 로그인
                        return done(null, user)
                    } else { // 비밀번호가 틀렸을 때
                        return done(null, false, { message: 'password incorrect' })
                    }
                })
            }
        })
    }));