본문 바로가기

블록체인_9기/💚 Node.js

32강_230516_Node.js(암호화, Hash 암호화, crypto, 해시화, salt, 키 스트레칭 기법, Bcrypt)

728x90

 

 

 

 


암호화

  • 암호화의 종류
    • 단방향 암호화
    • 양방향 암호화
      • 비대칭형 암호화
      • 대칭형 암호화

단방향 암호화

  • 복호화할 수 없는 암호화 방법
    • 원본 값을 알 수 없다.
    • 복호화?
      • 암호문을 원본으로 변경해주는 것이다.
  • 비밀번호를 만들 때 사용하고 원본의 값을 알아낼 수 없기에 안전하다.
    • 다만, 몇 가지 알고리즘은 뚫려 MD5, SHA1, SHA-180은 사용하면 안된다.
  • 원래의 값을 알 수 없도록 복잡한 알고리즘으로 암호화를 시켜주기 때문에 원본 값을 복호화 할 수 없는 것

양방향 암호화

  • 암호화할 때 사용하는 키와 복호화할 때 사용하는 키의 동일성에 대한 기준으로 구분한다.
  • 비대칭형 암호화
  • 대칭형 암호화
    • 키가 있으면 해당 키로 암호화된 문자열을 복호화할 수 있다.
    • 하지만 웹에서는 암호화된 문자열을 서버에서 클라이언트로 보낼 때 해당 키도 같이 보내주어야 한다.
      • 때문에 클라이언트에서 어떤 일이 일어날지 몰라 해당 키를 클라이언트에게 보내줄 수 없다.

 

단방향 암호화 방법

  • 가장 간단한 방법으로 해쉬 함수를 사용하는 방법이 있다.
  •  해쉬함수?
    • 같은 입력 값에 같은 출력값이 나오는게 보장되지만, 출력 값으로 입력 값을 유추할 수 없는 것을 의미한다.
    • 아래와 같은 예시를 확인하면, 사용자가 입력한 비밀번호를 넘겨서 데이터베이스 저장해 놓는다면 실제 데이터베이스가 노출되어도 입력값을 알 수 없어 보안에 안전하다.
입력 값 출력 값
qwe fhklqfnn83jrnlfni4gn492nnlnb29b24p244nf94m2p9fm2
123 89uwfnibm4m2f9b2794n208nv4f82j8rj9fjn04v4o42jf04
  • 단방향 암호화는 Hash 알고리즘을 사용한다.
    • 알고리즘을 통해 데이터를 고정된 크기의 고유한 값으로 바꿔주는 것이다.
    • 예를 들어, Hash 알고리즘에서 길이를 6으로 설정했다면,
      • 123456 => fg4fgl
      • 3245 => 3njn45
      • 이처럼 길이가 달라도, 6자의 비밀번호로 변경된다.

 

 

 


Hash 암호화

  • 단방향 암호화 기술이다.
  • 암호화가 진행되기 전 문장을 암호화된 문장으로 변경해준다.
  • Hash 알고리즘은 종류가 다양하고, 모든 사람에게 공개되어 있다.
  • Hash 알고리즘마다 Hash 길이가 다르고 이미 보안이 뚫린 해쉬가 다수 존재한다.
    • 따라서  MD5, SHA1, SHA-180은 사용하면 안된다.
  • Hash 알고리즘은 특정 입력에 대해 항상 같은 Hash 값을 리턴한다.
  • Hash함수는 속도가 빠르다. 이 속도는 해커가 암호화된 문장을 해석하는데 유리하게 작용한다.
    • 해커는 가능한 모든 입력 값에 대한 출력 값을 정리해놓고 정리해놓은 출력 값과 실제 데이터베이스에 저장되어 있는 값을 일일히 비교하는 작업으로 비밀번호를 탈취할 수 있기 때문이다.
    • 이렇게 입력값에 대한 출력값을 저장해 놓은 것을 " 레인보우 테이블 "이라 한다.

Hash 구문

  • createHash(algorithm[,options]): 사용할 해시 알고리즘을 입력한다.
    • SHA256, SHA 512등의 알고리즘을 입력하여 사용한다.
    • SHA256: 데이터를 256bit (32byte)의 고정 크기 해시 값으로 변환해주는 알고리즘이다.
  • update(data[,inputEncoding]): 변환할 문자열을 입력한다.
  • digest([encoding]): 인코딩할 알고리즘을 넣어준다. 변환된 문자열을 반환한다.
    • base64, hex,  latin1이 주로 사용된다.
      • base64: 직역 시, 64진법 이라는 뜻이다. 8비트 이진 데이터를 문자 코드에 영향을 받지 않는 공통 ASCII 영역의 문자들로만 이루어진 일련의 문자열로 바꾸는 인코딩 방식이다.
      • hex: 16진수의 문자열로 변환한다.

 

 

 


crypto

 

암호화의 종류

SHA

  • 미국 국가안보국(NSA)가 1993년에 처음으로 설계해 미국 국가 표준으로 지정된 알고리즘이다.
  • Hash 함수를 적용한 단방향 암호화 알고리즘으로 복호화가 불가능하다.
  • GPU를 이용한 연산속도가 매우 빠르기 때문에 비밀번호 암호화에 권장되지 않는다.
    • GPU속도가 빠를수록 공격자의 하드웨어를 통한 오프라인 brute force에 더 취약하다.
  • SHA의 종류
    • SHA-1
    • SHA-2
      • SHA256, SHA384, SHA512
    • SHA-3
      • SHA3-256, SHA3-384, SHA3-512 등

PBKDF2

  • 해시함수의 컨테이너 역할을 한다.
  • 검증된 해시함수만을 사용한다.
  • 해시함수와 salt를 적용 후 해시함수의 반복횟수를 지정하여 암호화 할 수 있다.
  • 가장 많이 사용되는 함수이다. ISO 표준에 적합해 NITS에서 승인된 알고리즘이다.
pbkdf2_hmac(해시함수(sha256..), password, salt, iteration, DLen)

const createHash = (salt,pw)=>{
    return new Promise((resolve, reject)=>{
        crypto.pbkdf2(
            pw,             // 해싱할 값을 문자열로 넣어주고 전달
            salt,           // salt 값
            165165,         // 키 스트레칭 반복 횟수. 반복횟수가 많아질수록 이렇게 암호가 되는데 시간도 오래 걸린다.
            64,             // 해시 값의 바이트. 64byte
            "sha256",        // 해시화 알고리즘
            (err, hash)=>{
                if(err) reject(err);
                resolve(hash.toString("hex"));
            }            
        )
    })

}
 

PBKDF2 테스트

패스워드기반 키생성 (PBKDF2) 사용자가 입력하는 패스워드를 직접 비밀키로 사용하는 것은 고정된 키를 사용하게 되어 사전공격 등의 방법이 가능하므로 보안성에 문제가 많다. 이를 해결하기

cris.joongbu.ac.kr

Bcrypt

  • Blowfish 암호를 기반으로 설계된 암호화 함수이며, 현재까지 사용중인 가장 강력한 해시 메커니즘이다.
  • .NET 및 Java를 포함한 많은 플랫폼과 언어에서 사용할 수 있다.
  • 반복횟수를 늘려 연산속도를 늦출 수 있으므로 연산능력이 증가하더라도 brute-forece 공격에 대비할 수 있다.

Scrypt

  • 오프라인 brute forece 공격에 대해 더 강력하지만, 많은 메모리와 CPU를 사용한다.
  • OpenSSL 1.1 이상을 제공하는 시스템에서만 작동한다.
  • 여러 언어의 라이브러리로 제공된다.
  • 다른 암호기반 KDF에 비해 많은 양의 메모리를 사용하도록 설계되었다.
  • 하드웨어 구현을 하는데 크기와 비용이 훨씬 더 비싸기 때문에, 주어진 자원에서 공격자가 사용할 수 있는 병렬처리의 양이 한정적이다.

 

crypto 모듈을 이용하여 암호화 제작

#. 폴더의 경로는 다음과 같다.

1. app.js_사용할 모듈을 불러온다.(crypto, express, path, mysql2, ejs)

const crypto = require("crypto");
const express = require("express");
const path = require("path");
const mysql2 = require("mysql2/promise");

2.app.js_salt값을 생성하는 함수를 생성한다.

  • 해당 함수를 이용하여 회원가입 시, 모든 계정에 salt값이 들어가게 된다.
// salt 값을 만들어주는 함수
const createSalt = ()=>{
    // 암호화에 시간이 좀 걸리기 때문에
    return new Promise((resolve, reject)=>{
        crypto.randomBytes(64,(err,result)=>{
            if(err) reject(err);
            // 실패[] 시 err 객체 reject메서드로 반환
            // 성공하면 resolve 메소드로 결과를 16진수로 변환해서 반환
            resolve(result.toString("hex"));
        })
    })
}

3. app.js_Hash를 생성하는 함수를 만든다.

  • crypto의 'pbkdf2' 메소드를 사용하여 키 스트레칭을 적용한다.
    • 키 스트레칭 기법은 아래의 bcrypt에서 자세히 설명하도록 한다.
const createHash = (salt,pw)=>{
    return new Promise((resolve, reject)=>{
        crypto.pbkdf2(
            pw,             // 해싱할 값을 문자열로 넣어주고 전달
            salt,           // salt 값
            165165,         // 키 스트레칭 반복 횟수. 반복횟수가 많아질수록 이렇게 암호가 되는데 시간도 오래 걸린다.
            64,             // 해시 값의 바이트. 64byte
            "sha256",        // 해시화 알고리즘
            (err, hash)=>{
                if(err) reject(err);
                resolve(hash.toString("hex"));
            }            
        )
    })
}

4. app.js_서버 인스턴스를 생성한다.

const app = express();

5. app.js_body객체 사용 / view엔진 경로설정 / view엔진 ejs사용 설정

  • body객체 사용
app.use(express.urlencoded({extended:false}));
  • view엔진 경로설정
app.set("views", path.join(__dirname,"page"));
  • view엔진 ejs사용 설정
app.set("view engine", "ejs");

6. app.js_mysql을 연결한다.

const mysql = mysql2.createPool({
    user : "root",
    password : "000000",
    database : "test5",
    multipleStatements : true
})

7. app.js_데이터베이스의 테이블을 초기화 한다.

// 테이블 초기화
const usersInit = async ()=>{
    try {
        await mysql.query("SELECT * FROM users");
    } catch (error) {
        await mysql.query("CREATE TABLE users(id INT AUTO_INCREMENT PRIMARY KEY, user_id VARCHAR(20), user_pw VARCHAR(128), salt VARCHAR(128))")
    }
}
usersInit();

8. app.js_루트경로('/')의 페이지와 ('/login')경로의 페이지를 render한다.

app.get('/',(req,res)=>{
    res.render("join");
})

app.get('/login',(req,res)=>{
    res.render("login");
})

9. app.js_회원가입 페이지에서 salt와 hash생성 함수를 이용하여 계정을 생성한다.

  • users 테이블의 salt 컬럼에 넣을 salt값을 생성하고, pw 컬럼에 들어갈 hash 값을 생성한다.
app.post("/join",async(req,res)=>{
    const {user_id, user_pw} = req.body;
    const salt = await createSalt();
    const hash = await createHash(salt, user_pw);
    await mysql.query("INSERT INTO users(user_id,user_pw,salt)VALUE(?,?,?)",[user_id,hash,salt])
    res.redirect("/login")
})

10.app.js_로그인 페이지에서 계정정보를 비교하여 로그인을 진행한다.

  • 로그인 시도 시, 먼저 로그인창에서 입력한 id값을 기준으로 테이블에서 계정을 찾는다.
    • 계정이 없을 경우, 유저가 없다는 에러메시지가 뜨고 
    • 있을 경우, 아래의 검사과정을 거치게 된다.
      • 반환 받은 계정 값의 salt 값과, 로그인창에서 입력한 비밀번호로 제작한 salt값을 비교한다.
        • 회원가입 시 입력했던 비밀번호와 현재 입력한 비밀번호가 같다면 hash값도 동일한 값이 나올것이다.
        • 따라서, 두 값이 동일하면 로그인을 성공시키고
        • 두 값이 동일하지 않다면 비밀번호가 틀렸다는 메시지를 보낸다. 
app.post("/login", async(req,res)=>{
    const {user_id, user_pw } = req.body;
    const [result] = await mysql.query("SELECT * FROM users WHERE user_id = ?", [user_id]);
    if(result[0]?.salt){
        const salt = result[0].salt;
        const hash = await createHash(salt, user_pw);
        if(hash == result[0].user_pw){
            res.send("로그인 됨")
        }
        else{
            res.send("비밀번호 틀렸음")
        }
    }
    else{
        res.send("유저 없음")
    }

})

11. page_ join.ejs와 login.ejs의 코드이다. 

  • join.ejs
<body>
    <form action="/join" method="post">
        <label for="">아이디</label> <br>
        <input type="text" name="user_id"> <br><br>
        <label for="">비밀번호</label> <br>
        <input type="text" name="user_pw"> <br><br>
        <button>가입하기</button>
    </form>
</body>
  • login.jes
<body>
    <form action="/login" method="post">
        <label for="">아이디</label> <br>
        <input type="text" name="user_id"> <br>
        <label for="">비밀번호</label> <br>
        <input type="text" name="user_pw"> <br>
        <button>로그인</button>
    </form>
</body>

 

 

 


bcrypt

  • 키(key)방식의 대칭형 블록 암호에 기반을 둔 암호화 해시 함수이다.
  • 레인보우 테이블 공격을 방지하기 위해 salt와 키 스트레칭을 적용한 대표적인 예이다.
  • 보안에 집착하기로 유명한 OpenBSD에서 사용하고 있다.
  • 반복횟수를 늘려 연산속도를 늦출 수 있기 때문에 연산능력이 증가해도 공격에 대비할 수 있다.

구조

$2b$12$76taFAFPE9ydE0ZsuWkIZexWVjLBbTTHWc509/OLI5nM9d5r3fkRG
 \/ \/ \____________________/\_____________________________/
Alg Cost       Salt                        Hash
  • $2a$[cost]$[salt][hash]
    • $2a$: 해시 알고리즘 식별자. 고정값이다.
    • [cost]: (ex. 12 = 2^12) Cost Factor로 키 스트레칭(Key Stretching)의 횟수이다.
      • 기본적으로 많이 사용하는 횟수가 10이다. 이 수보다 많으면 많이 느려질 수 있다.
    • [salt]: 인코딩 된 salt값이다. 알고리즘에서 문자열의 일부분을 salt값으로 사용한다.
      • ex. 76taFAFPE9ydE0ZsuWkIZe
      • 16byte 크기의 salt이며, Base64로 인코딩된 22개의 문자이다.
    • [hash]: 비밀번호와 salt값을 합하고 해시해서 인코딩된 값이다.
      • ex. xWVjLBbTTHWc509/OLI5nM9d5r3fkRG
      • 24Byte의 해시 값, Base64로 인코딩된 31개의 문자이다.
      •  

Salt

출처:&nbsp;https://d2.naver.com/helloworld/318732

  • 복호화를 방해하기 위해 단방향 암호화시 소금을 뿌려 해커가 복호화 하는 것을 방해하는 방법이다.
  • 입력으로 들어가는 비밀번호에 추가 문자열을 덧붙이는 방법이다.
    • 따라서, 유저1과 유저2의 비밀번호 입력 값이 같더라도 발급된 해쉬 값을 달라, A의 정보가 탈취 당했어도, B의 정보는 안전한 것이다.

 

키 스트레칭 기법 (Key Stretching)

  • 키 스트레칭 기법은 salt와 password를 해시함수에 넣는 과정을 반복해 해커가 복호화 하는 것을 귀찮게 만드는 방법이다.
  • 따라서, 출력 값을 아주 느리게 산출될 수 있도록 한다.

 

brypto 모듈을 이용하여 암호화 제작

#. 폴더의 경로는 다음과 같다.

1. app.js_사용할 모듈을 불러온다.(bcrypt, express, path, mysql2, ejs)

const express = require("express");
const path = require("path");

2. app.js_body객체 사용 / view엔진 경로설정 / view엔진 ejs사용 설정

  • body객체 사용
app.use(express.urlencoded({extended:false}));
  • view엔진 경로설정
app.set("views", path.join(__dirname,"page"));
  • view엔진 ejs사용 설정
app.set("view engine", "ejs");
  • 서버 인스턴스 생성
const app = express();

3. config.js_mysql을 연결한다.

const mysql2 = require("mysql2/promise");

const mysql = mysql2.createPool({
    user : "root",
    password : "000000",
    database : "test7",
    multipleStatements : false
})

module.exports = mysql;

4. usersModel.js_mysql에 적용시킬 쿼리문을 작성한 함수를 설정한다.

  • usersInit: 테이블 생성 함수
exports.usersInit = async()=>{
    try {
        await mysql.query("SELECT * FROM users");
    } catch (error) {
        await mysql.query("CREATE TABLE users(id INT AUTO_INCREMENT PRIMARY KEY, user_id VARCHAR(20), user_pw VARCHAR(128))")
    }
}
  • userSelect: 계정 선택 함수
exports.userSelect = async(user_id)=>{
    console.log(user_id,'믿지안아');
    try {
        const [result] = await mysql.query("SELECT * FROM users WHERE user_id = ?", [user_id]);
        console.log("result",result)
        return result[0];
    } catch (error) {
        console.log(error);
    }
}
  • userInsert: 계정 추가 함수
exports.userInsert = async(user_id,user_pw)=>{
    try {
        // 일단 중복되는 아이디인지 확인을 먼저 하자
        const [user] = await mysql.query("SELECT * FROM users WHERE user_id = ?", [user_id])
        if(user.length != 0){
            // 이미 존재하는 아이디
            let err = new Error("중복 아이디임");
            console.log(err)
            return err;
        }

        // 중복되지 않았으면 회원가입 정상적으로
        await mysql.query("INSERT INTO users(user_id, user_pw)VALUES(?,?)",[user_id,user_pw]);
    } catch (error) {
        console.log(error);
    }
}

5. index.js_' usersModel.js '에서 작성한 함수들을 불러오고 내보낸다.

const { usersInit, userInsert, userSelect } = require("./usersModel");

usersInit();

module.exports = { userInsert, userSelect };

6. usersController.js_controller에서 작성된 함수를 불러오고 사용할 모듈을 가져온다.

const { userInsert, userSelect } = require("../models");
const bcrypt = require("bcrypt");

7. usersController.js_hash를 생성하는 함수를 설정하고 검증을 할 수 있는 함수를 설정한다.

  • createHash: hash값을 생성하는 함수이다.
const createHash = (password)=>{
    return new Promise((resolve,reject)=>{
        // hash메소드로 해시값을 만들어 줄 수 있다.
        bcrypt.hash(password, 10, (err, data)=>{
            if(err) reject(err);
            resolve(data);
        })
    })
}
  • compare: 문자열과 해시 값을 전달해주고 매개변수로 검증 결과를 확인한다.
    • 검증결과를 true, false값으로 반환한다.
const compare = (password, hash)=>{
    return new Promise((resolve, reject)=>{
        // compare 메소드를 사용해서 문자열과 해시 값을 전달해주고 매개변수로 검증 결과를 확인한다.
        bcrypt.compare(password, hash, (err,same)=>{
            // if(err) reject(err);
            resolve(same);        
        })
    })
}

8. usersController.js_회원가입을 할 수 있는 함수를 설정한다.

  • 요청한 ejs의 body에서 받은 비밀번호 값을 매개변수로 넣어 hash를 생성한다. 생성된 hash를 데이터베이스의 pw값으로 넣는다.
//회원가입
exports.Signup = async(req,res)=>{
    const {user_id, user_pw} = req.body;
    try {
        const hash = await createHash(user_pw);
        console.log("해시",hash)
        await userInsert(user_id, hash);
        res.redirect('/login');
    } catch (error) {
        console.log(error)
    }
}

9. usersController.js_로그인을 할 수 있는 함수를 설정한다.

  • 요청한 ejs에서 받은 아이디 값으로 데이터베이스에서 계정을 찾아 data에 반환한다.
  • 첫 번째 if문에서 data에 body에서 입력한 id값이 없으면, 아이디가 없다는 화면이 뜨도록 한다.
    • 있다면, 'compare'검증 함수로 데이터베이스의 pw와 사용자가 body에서 입력한 pw의 값이 동일한지 확인한다.
    • 두 번째 if문에서 검증 결과가 false로 리턴되면 비밀번호가 틀렸다는 화면이 뜨도록 한다.
      • 'compare'함수에서 검증 결과가 true로 리턴되면 로그인을 진행한다.
//로그인
exports.Login = async(req,res)=>{
    const { user_id, user_pw } = req.body;
    try {
        const data = await userSelect(user_id);
        console.log("데이터",data);
        console.log("유저아이디",user_id);
        if(!data?.user_id){
            return res.send("아이디 없음")
        }
        const compare_pw = await compare(user_pw, data.user_pw);
        console.log(compare_pw);
        if(!compare_pw){
            return res.send("비밀번호 틀림");
        }

        res.send("로그인됨")
    } catch (error) {
        console.log(error)
    }
}

10. joinRouter.js_url을 '/join'으로 접속 시 join.ejs 페이지가 뜨게 한다. post요청을 받으면 usersController.js의 Singup 함수를 실행한다.

const router = require("express").Router();
const {Signup} = require("../controllers/usersController");

router.get('/',(req,res)=>{
    res.render("join");
})

router.post('/',Signup);

module.exports = router;

11. loginRouter.js_url을 '/login'으로 접속 시 login.ejs 페이지가 뜨게 한다. post요청을 받으면 usersController.js의 Login 함수를 실행한다.

const router = require("express").Router();
const { Login } = require("../controllers/usersController");

router.get('/',(req,res)=>{
    res.render('login');
})

router.post('/',Login);

module.exports = router;

12. page_ join.ejs와 login.ejs의 코드이다. 

  • join.ejs
<body>
    <form action="/join" method="post">
        <label for="">아이디</label> <br>
        <input type="text" name="user_id"> <br>
        <label for="">비밀번호</label> <br>
        <input type="text" name="user_pw"> <br>
        <button>가입신청</button>
    </form>
</body>
  • login.jes
<body>
    <form action="/login" method="post">
        <label for="">아이디</label> <br>
        <input type="text" name="user_id"> <br>
        <label for="">비밀번호</label> <br>
        <input type="text" name="user_pw"> <br>
        <button>로그인</button>
        <a href="/join">회원가입</a>
    </form>
</body>

 

 

 

 

 

 

 

 

728x90