프로그래밍에 대한 공부를 하다보면 인터넷 상에 입력하는 내 개인정보에 대한 불안감이 점점 더 커진다. 지금은 많은 사람들이 보안에 대해서 굉장히 큰 경각심을 가지고, 또 그에 부응하는 많은 보안 프로그램들이 나온다. 여러가지 알고리즘을 생각하는 보안 업계의 개발자들은 정말 대단하다고 생각한다.
사실 유저 개인정보 해싱에 대해 중요하다라는 얘기만 들어봤지 한번도 시도해본적이 없다. 아직 접하지 못했고 또 기한이 정해져있는 프로젝트라는 점에서 안정적인 것을 선호했다. 검색을 해보니 가장 많이 나오는게 NodeJS의 내장 모듈인 Crypto였다. 참고한 사이트는 내가 정말 존경하는 Zero Cho 님의 Crypto 강의이다.
코드를 작성하기 전에 먼저 암호화 방법 부터 공부하였다.
양방향 암호화엔 두가지 방법이 존재한다.
대칭형 암호화
- 키가 있으면 복호화가 가능한 암호화 방식이다. 다만 암호화된 데이터를 클라이언트로 보낼 때 키도 같이 보내야 한다는 단점이다. 이는 굉장히 위험한 방법이다.
비대칭형 암호화
- 마찬가지로 복호화가 가능한 방식이다. 그러나 암호화 할 때 키와 복호화 할 때 키가 서로 다르다. 이는 특정 키(A)로 암호화 한 것은 암호화 할 때와는 다른 특정 키(B)로만 복호화 할 수 있으므로 암호화가 필요한 곳에서만 A를 가지고 있으면 되고 복호화가 필요한 곳에서만 B를 가지고 있으면 된다.
클라이언트가 구글로 부터 발급받은 OAuth 액세스토큰을 서버 보내고 서버에선 유저 정보를 요청할 때 돌아오는 응답에 있는 ID로 유저를 구분하여 소셜 로그인을 구현하려 했다. 나는 이 ID를 DB에 저장하려 했는데 아무래도 유저 고유의 식별문자라 해싱이 필요할 듯 싶었다.
참고로 이 방법은 단방향 암호화를 적용하기에 적당했다.
import axios from 'axios';
import crypto from 'crypto';
import { OAuths } from '../../models/oauth';
import { Users } from '../../models/user';
const oAuthHandler = async (req, req) => {
const accessToken = req.headers.authorization;
//? google oauth를 통해 정보를 받아온다
if (!accessToken) return res.status(401).json({message: 'unauthorized'});
const oAuthData = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo?access_token=' + accessToken, {
headers: {
authorization: `token ${accessToken}`,
accept: 'application/json'
}})
.then(data => {
return data.data;
}).catch(e => console.log('oAuth token expired'));
//? 받아온 정보의 email과 고유 id로 식별
const { email, id } = oAuthData;
먼저 구글 API/ 액세스토큰을 이용해 유저 정보를 받아온다.
만약 응답받은 데이터의 email로 가입된 유저가 DB에 없다면 소셜 회원가입 알고리즘으로 넘어간다.
그리고 ID를 유저 식별용으로 등록 할 건데 해싱이 필요하다.
const salt = await crypto.randByte(64).then(d => d.toString('base64')).catch(e => {console.log('hashingerror')});
const hashedId = await crypto.pbkdf2(id, salt, 100000, 64, 'sha512').then(d => d.toString('base64')).catch(e => {console.log('hashingerror')});
// 생략
크립토의 randByte 메소드를 사용하여 64바이트 길이의 salt를 생성 해준다. salt는 buffer 형식을 가지고 있으므로 base64 문자열로 바꿔준다.
pbkdf2 메소드를 사용하여 인자를 순서대로 (해싱할 값, 솔트, 해시함수 반복 횟수, 알고리즘)을 넣어주면 해싱된 값이 나온다. (참고로 반복 횟수는 100000 처럼 딱 떨어지는 것이 아닌 103414 와 같은 수를 넣는게 좋다고 한다.)
위의 과정을 거칠 때 마다 ID에 따라 다른 salt 값을 생성하므로 특정 데이터를 비교할 때 salt값이 꼭 필요하다. 그러므로 salt값을 저장해두어야 한다.
// 생략
//? oauth id와 email로 된 user가 존재하지 않을 경우 회원가입
if (!oAuthInfo && !userInfo && hashedId) {
const nickName = '시인' + Math.random().toString(36).slice(2);
await Users.create({email, nickName, introduction: null, avatarUrl: null, authCode: null, status: 1}).then( async (d) => {
await OAuths.create({userId: d.id, oAuthId: hashedId, platform: 'google', salt});
});
};
// 생략
}
위와 같이 데이터를 생성해주면,
위와 같이 DB에 해싱된 값이 입력되는 것을 볼 수 있다.
//생략
if (userInfo && oAuthInfo) {
//? OAuth 테이블에서 뽑아온 정보
const { userId, oAuthId } = oAuthInfo;
const savedSalt = oAuthInfo.salt;
if (userId !== userInfo.id) return res.status(401).json({message: 'unauthorized'});
const decodedId = await pbkdf2(id, savedSalt, 100000, 64, 'sha512').then(d => d.toString('base64')).catch(e => {console.log('hashingerror')});
if (decodedId !== oAuthId) return res.status(401).json({message: 'unauthorized'});
};
//생략
위와 같은 방식으로 똑같은 알고리즘에 똑같은 솔트를 넣고 해싱하여 비교하면 된다. 만약 저장된 데이터의 id와 로그인 시 요청된 id가 같다면 해시 값도 같을 것이다.