본문 바로가기
깃허브 링크!
node.js

node.js의 crypto 모듈(암호화) ,(해시,솔트) 등등

 

더보기

오늘 해볼것은 

로그인 가입을 만드는데 단방향 암호화를 추가하여 보안을 강화하고 토큰 기능을 혼합하여 사용자 인증을 처리하는 코드이다.

 

새로운 단어
1. 단방향 암호화란?
단방향 암호화는 원본 데이터를 암호화하여 암호화된 값(해시 값)을 생성하는 과정입니다. 이때, 원본 값을 알아내는 것은 거의 불가능합니다. 단방향 암호화는 주로 비밀번호 저장과 같은 보안 요구사항에 사용됩니다.

2. 해시란?
해시는 임의의 크기의 데이터를 고정된 크기의 고유한 값으로 변환하는 함수입니다. 해시 함수는 일반적으로 암호학적으로 안전하며 충돌 가능성이 낮은 결과를 생성합니다. 해시 함수를 사용하여 데이터를 해싱하면 고유한 해시 값이 생성되며, 입력 데이터가 조금이라도 다르면 결과 해시 값도 크게 달라집니다.

3. 해싱 작업이란?
해싱 작업은 원본 데이터를 해시 함수에 입력하여 해시 값을 생성하는 과정입니다. 원본 데이터에 대한 고유한 해시 값을 얻을 수 있습니다. 일반적으로 비밀번호 저장, 데이터 무결성 검증 등에서 사용됩니다. 해싱은 단방향 암호화의 한 형태입니다.

4. crypto란?
crypto는 Node.js의 내장 모듈로, 다양한 암호화 기능을 제공합니다. 이 모듈을 사용하여 데이터를 암호화하고 복호화할 수 있습니다. 비밀번호와 같은 중요한 정보를 보호하기 위해 암호화를 사용합니다.

5.솔트란?
솔트는 암호화 과정에서 사용되는 임의의 값을 말합니다. 솔트는 해시 함수에 추가되어 암호화된 결과를 더욱 안전하게 만듭니다. 솔트 값을 암호화된 데이터와 함께 저장하여 나중에 비밀번호를 검증할 때 사용합니다. 각 사용자마다 다른 솔트 값을 가지도록 하여 공격자가 솔트 값을 알아내기 어렵게 만듭니다.
(비밀번호에 추가되는 임의의 데이터)라고 보면 된다.


6. pbkdf2?
"password-Based Key Derivation Function 2"의 약어로 비밀번호 기반의 키 파생 함수
이 함수는 주어진 비밀번호와 솔트를 사용하여 보안 강도가 높은 키를 생성하는 데 사용된다.

이 메서드는 비동기적으로 동작하며 콜백 함수를 통해 결과를 반환하고 콜백 함수는 오류(err)와 해시 값(hash)을 
매개변수로 받는다.

crypto.pbkdf2 메서드는 crypto 모듈에 포함된 함수로 pbkdf2 알고리즘을 구현한다.
이 알고리즘은 비밀번호를 안전하게 해싱하기 위해 사용된다.

 

 

배웠지만 아직 생소한 단어

1. promise?
javascript 의 내장 객체로 비동기(다음 작업을 기다리지 않고 바로 다른 작업 수행) 작업의 결과를 나타내는데 사용
여기에서는 resolve와 reject가 사용되는데 두개다 promise객체의 메서드이다.
resolve는 promise 객체가 성공 상태로 이행될 때 호출되며, 성공한 결과 값을 전달하고
reject는 promise 객체가 실패 상태로 거부될 때 호출되며, 실패한 이유를 전달해준다. 

2. router?
express 프레임워크에서 url 경로에 따라 들어오는 요청을 처리하는 역할을 수행하는 기능이다.
express 애플리케이션은 라우터를 사용하여 요청이 특정 경로에 도달했을 때 어떤 동작을 수행할지를 정의

라우터는 특정 경로에 대한 요청을 처리하기 위해 http 메서드 (get,post,put,delete등)와 url 패턴을 매칭하여 해당 요청을
처리할 핸들러 함수를 실행한다. 

 

 

오늘 배운 코드 분석 -1

1.crypto.randomBytes?
node.js의 crypto 모듈에 있는 메서드로 임의의 바이트를 생성하는 기능을 제공한다.
첫 번째 매개변수로 생성할 바이트의 길이를 제공하고 두 번째 매개변수로 콜백 함수를 전달한다.

ex)
const createSalt = ()=>{
    // 암호화에 시간이 좀 걸리기때문에 
    return new Promise((resolve,reject)=>{
        // 랜덤 바이트 길이는 64
        crypto.randomBytes(64, (err, result)=>{
            if(err) reject(err);
            // 실패시 err 객체 reject메서드로 반환 
            // 성공하면 resolve 메서드로 결과를 16진수로 변환해서 반환
            resolve(result.toString("hex"));
        })
    });
};​

위의 코드 해석 

  1. 64는 crypto.randomBytes 메서드가 생성할 바이트의 길이를 나타냅니다. 이 값은 임의로 선택한 값으로, 보안적인 요구사항에 맞게 조정할 수 있습니다.
  2. err와 result는 crypto.randomBytes 메서드의 콜백 함수에 전달되는 매개변수입니다. err은 에러 객체이고, result는 생성된 랜덤한 데이터를 나타냅니다.
  3. if (err) reject(err)는 crypto.randomBytes 메서드 실행 중 에러가 발생했을 경우, reject 메서드를 호출하여 프로미스를 실패 상태로 변경합니다. 즉, err 값이 존재하면 reject가 호출되어 프로미스가 실패합니다.
  4. resolve(result.toString("hex"))는 crypto.randomBytes 메서드가 성공적으로 실행되고 생성된 랜덤한 데이터를 16진수로 변환한 후, resolve 메서드를 호출하여 프로미스를 성공 상태로 변경합니다. 이렇게 프로미스가 성공하면 생성된 랜덤한 데이터를 반환합니다.

 

 

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

const test = async()=>{
    const salt = await createSalt();
    const hash = await createHash(salt, pw);
    console.log(hash);
}


createHash 함수는 salt 와 password를 매개변수로 받는다.
함수 내부에서 promise를 생성하여 비동기 작업을 수행하고 resolve와 reject 콜백 함수를 인자로 받는다. 

crypto.pbkdf2 메서드를 사용하여 비밀번호를 해싱
이 메서드는 password, salt, 165165, 64(해시값의 바이트 길이), "sha256" 해싱 알고리즘을 인자로 받고
crypto.pbkdf2 메서드의 콜백함수에서 오류와 해시값을 받는다.

오류가 있는 경우 reject를 호출하여 promise를 실패 상태로 설정

해시 값을 16 진수로 변환하고 변환된 값을 resolve를 호출하여 promise를 성공 상태로 설정

-------- const test = async() 부분
createHash 함수를 사용하는 test 함수를 정의한다. 
이 함수는 async 키워드를 사용하여 비동기 함수로 만들어 준다.

test 함수 내부에서 await 키워드를 사용하여 createSalt 함수를 호출하고 결과를 salt 변수에 할당한다.
이후 await를 사용하여 createHash 함수를 호출하고 결과를 hash 변수에 할당한다.

이후 hash 값을 출력한다. 

 

 

 

오늘 배운 코드 내용 분석-3
const mysql = mysql2.createPool({
    user : "root",
    password : "nonbanonfj",
    database : "test13",
    multipleStatements : true
})​

위의 코드는 node.js 에서 mysql 데이터베이스에 연결하기 위해 mysql2 모듈을 사용하여 풀(pool)을 생성하는 부분이다.
'user' = mysql 서버에 연결할 사용자 이름을 나타내는 문자열 
'password'  = mysql 서버에 연결할 사용자의 비밀번호를 나타내는 문자열
'database' = 연결할 mysql 데이터베이스의 이름을 나타내는 문자열
'multipleStatements' = 여러 sql 문을 한 번에 실행할 수 있는지 여부를 나타내는 불리언 값

 

오늘 배운 코드 내용 분석-4
// 테이블 초기화
const userInit = 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))")

}}

userInit();


위의 코드는 userInit 라는 비동기 함수를 정의하고 이 함수 내에서 mysql 데이터베이스에 대한 초기화 작업을 수행

userinit 함수 내부에서는 try-catch 문을 사용하여 데이터베이스에서 사용자 정보를 조회하는 쿼리를 실행
만약 쿼리 실행 중에 오류가 발생하면 catch 블록으로 이동하여 create Table 쿼리를 실행


 

오늘 배운 코드 내용 분석-5
app.get("/",(req,res)=>{
    res.render("join");
})

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

 

위의 코드는 express 프레임워크를 사용하여 두 개의 라우트를 정의하는 부분

1. app.get("/", (req,res) => {...} : 루트 경로인 (현재파일)에 get 요청이 들어왔을때 처리하는 라우트
    res.render("join")을 통해 "join" 이라는 템플릿을 렌더링하여 클라이언트에 응답한다. 
    이 라우트는 주로 회원가입 페이지를 보여주는 역할을 할수 있다.

2. app.get("/login", (req,res) =>{...} : "/login" 경로에 get 요청이 들어왔을때 처리하는 라우트
    res.render("login")을 통해 "login"이라는 템플릿을 렌더링하여 클라이언트에 응답한다.
    이 라우트는 주로 로그인 페이지를 보여주는 역할을 할수 있다. 

** app.get이 app.listen 보다 밑에 있어도 상관이 없다.

 

오늘 배운 코드 내용 분석-6(last)
app.post("/join",async(req,res)=>{
    const {user_id,user_pw} = req.body;
    const salt = await createSalt();
    const hash = await createHash(salt, req.body.user_pw);
    await mysql.query("INSERT INTO users (user_id,user_pw,salt)VALUES(?,?,?)",[user_id,hash, salt]);
    res.redirect('/login');

})

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("유저 없음"); 
    }
})​


위 코드는 express 애플리케이션에서 post 요청을 처리하는 핸들러들을 정의한 부분

app.post("join",async (req,res) =>{...} : /join 경로로 post 요청이 오면 실행되는 핸들러를 정의

요청의 body(login.ejs , join.ejs)에서 user_id와 user_pw를 추출하고 createSalt() 함수를 사용하여 
salt를 생성하고 생성된 salt와 요청의 user_pw를 사용하여 hash를 생성한다.
그리고 생성된 값들을 mysql에 insert쿼리를 사용하여 저장하고 마지막으로 '/login'경로로 리다이렉트 한다.

join부분은 회원가입하는 과정

--------------- app.post('/login',async(req,res)=>{...}  이 부분은 로그인 하는 과정

login 경로로 post 요청이 오면 실행되는 핸들러 정의
요청의 body에서 user_id와 user_pw를 추출하고 mysql에서 user_id에 해당하는 사용자 정보를 조회한다.
조회된 결과에서 salt를 추출하고 추출된 salt와 요청의 user.pw를 사용하여 hash를 생성한다.
생성된 hash와 조회된 사용자의 user_pw를 비교하여 로그인 결과를 반환한다.

이러한 방식으로 회원가입(join)과 로그인(login)을 처리하는 라우트 핸들러를 정의하고 있다.

 

 

 

 

 

 

--------------------- page 폴더의 login ejs와 join.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">
    <title>Document</title>
</head>
<body>
    <form action="/login" method="post">
        <label for="">아이디</label>
        <input type="text" name="user_id"> <br>
        <label for="">비밀번호</label> <br>
        <input type="text" name="user_pw"> <br>
        <button>로그인</button>
    </form>
</body>
</html>

join.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">
    <title>Document</title>
</head>
<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>
</html>

---------------------

 

 

 

 

crypto 모듈로 암호화

 let hashA = crypto.createHash("sha256"); 
// node.js의  crypto 모듈을 사용하여 sha256해시 객체를 생성하는 코드(널리 사용되는 해시 함수 중 하나로 보안에 강력)

해시 객체 생성한 후
- update() 메서드 사용하여 암호화할 문자열을 전달(해시 객체에 입력) ex) hashA.update("password")
- 그 후에 digest() 메서드를 이용하여 최종해시 값을 반환 예를 들어 hashA.digest("hex")는 해시 값을 16진수로 반환 

정리하자면 
위의 코드에서는 sha-256해시 객체를 생성한 후 update() 메서드로 비밀번호를 암호화하고 digest() 메서드로 
최종 해시 값을 16진수로 반환하여 hashString 변수에 저장하는 과정을 나타낸다.
이를 통해 단방향 암호화를 수행하고 원본값을 알수 없는  해시 값을 얻을수 있다.

이해하기 어려울수 있는데 간략하게
우리가 사용하는 네이버 같은 사이트들이 비밀번호 찾기를 시도하면 
비밀번호를 알려주거나 메일로 전송 해주지 않고 비밀번호 변경을 시켜준다.
원본 비밀번호를 알수가 없기 때문에

 

 

 

지금까지의 과정이 어떻게 이뤄지는지 

 

mysql에 test13파일을 만들었고 tables 폴더안에 users 파일을 만들었다.