단방향 암호화는 평문을 암호화 할 수는 있지만 암호화된 문자를 다시 평문으로 즉, 복호화가 불가능한 암호화 방법이다. 주로 해시 알고리즘을 이용하여 단방향 암호화를 구현하며, 단방향 암호화를 사용하는 주된 이유는 메시지 또는 파일의 무결성(integrity)을 보장하기 위해서이다.
해시 알고리즘은 동일한 평문에 대해서 항상 동일 해시값을 갖는다. 예를 들어 'hello world' 라는 평문을 해시 함수를 통해 단방향 암호화 시 항상 'good'이라는 해시 값을 가진다.(가정임) 따라서 특정 해시 알고리즘에 대해서 어떠한 평문이 어떤한 해시값을 갖는지 알 수가 있다. 즉, 해시 값이 'good'이라면 암호화 전 평문은 'hello world'가 되는 셈이다. 이런 특징을 이용하여 해시 함수의 해시 값들을 대량으로 정리한 테이블이 존재하며, 이를 레인보우 테이블이라고 부른다. 그리고 레이보우 테이블을 이용하여 사용자의 정보를 해킹하는 공격을 레인보우 공격이라고 한다.
단방향 암호화와 달리 양방향 암호화는 암호화된 값을 다시 암호화 하기 이전의 값으로 되돌릴 수 있다. 이를 '복호화'라고 하며 양방향 암호화는 암호화 알고리즘과 키를 이용해서 암호화를 진행하는데, 이 키를 통해서 암호화 된 값을 보호할 수 있다.
대칭키 방식은 암호화 할 때 사용하는 키와 암호문으로부터 평문을 복호화 할 때 사용하는 키가 동일한 암호 시스템이다. 따라서 암호화를 진행할 때 사용한 키를 모른다면 해당 암호문은 다시 복호화 할 수 없다.
비대칭키 암호화에서는 암호화 때 사용하는 키와 복호화 할 때 사용하는 키를 다르게 사용한다. 일반적으로 다른 사람들에게 공개하는 Public Key와 절대 노출을 하지 않는 Private Key가 있으며, 이 두 개의 키를 Key Pair라고 부른다.
Bcrypt는 레인보우 테이블 공격을 방지하기 위해 '솔팅'과 '키 스트레칭'을 적용한 해시함수이다.
- 솔팅 : 해시함수를 통해 단반향 암호화를 진행할 때 원래 데이터에 추가적으로 임의의 데이터를 추가해서 암호화를 진행하는 방식이다. 예를 들어 'hello world'에 솔팅을 하게되면 해시값은 기존처럼 'good'이라는 해시값이 아닌 다른 값이 나오게 된다.
- 키 스트레칭 : 해시함수를 통해 단방향 암호화를 진행하여 해시값이 나오면 그 해시값을 다시 해시함수를 돌려 또 다른 해시값을 얻고, 다시 또 해시함수를 돌리는 작업을 반복하여 계속해서 해시값을 변경 시키는 작업이다.
const bcrypt = require("bcrypt"); // (1) bcrypt 모듈 import
const password = 'password'; // (2) 암호화 할 평문
const saltRounds = 12; // (3) Cost Factor, 즉 키 스트레칭수. 보통 8~12로 설정하며 12일 경우 2의 12승 = 1,024
const makeHashFunc = async (password, saltRounds) => {
return await bcrypt.hash(password, saltRounds); // (4) hash 메소드를 통해서 암호화
}
const hashedPassword = makeHashFunc(password, saltRounds) //
console.log(hashedPassword)
=> b'$2b$12$76taFAFPE9ydE0ZsuWkIZexWVjLBbTTHWc509/OLI5nM9d5r3fkRG' (5) 암호화된 해시값
Bcrypt는 단방향 해시 알고리즘이므로 복호화가 불가능 하다. Bcrypt의 검증은 암호화된 값이 가지고 있는 알고리즘, Cost Factor, Salt를 이용한다. 비교하고 싶은 평문을 암호화된 값이 가지고 있는 알고리즘, Cost Factor, Salt을 이용하여 해시를 진행한 후 암호화된 값과의 비교를 통해 검증을 진행한다.
const checkHash = async (password, hashedPassword) => {
return await bcrypt.compare(password, hashedPassword) // (1)comparemethod로 평문과 암호화된 값 비교
}
const checkResultFunc = async () => {
const hashedPassword = await makeHash("password", 12);
const result = await checkHash("password", hashedPassword);
console.log(result);
};
checkResultFunc();
=> true(or false) // (2)평 문과 암호화된 값을 비교해서 같으면 true를 다르면 false가 return
Header는 JWT의 첫번째 구성 요소이고 alg, typ 정보를 담고 있다.
alg : Signature을 만드는데 사용한 알고리즘 정보
typ : Token의 타입
// Header 예시
{
"alg" : "HS256",
"typ" : "JWT"
}
실질적으로 전달해야 하는 정보들을 가지고 있다. Payload에 담긴 정보 하나 하나를 Claim이라고 하는데, 3가지 종류의 Claim이 존재한다.
Registered Claims : WT 표준으로 지정된 Claim으로 모두 사용 할 필요 없이적절히 상황게 맞게 사용하면 된다.
☞ iss: 토큰 발급자
☞ sub: 토큰 제목
☞ aud: 토큰 대상자
☞ exp: 토큰 만료시간
☞ iat: 토큰 발급 시간
☞ nbf: 토큰 활성화 시간
☞ jti: JWT의 고유 식별자
Public Claims : JWT를 사용하는 사람들이 공개적으로 정의할 수 있다.
Private Claims : Public Claims과 달리 오직 사용자와 서버 사이에서만 합의하여 사용하는 Claim
// Payload의 구성 예시
{
"exp": "1245678900", //Registered Claims
"https://velopert.com/jwt_claims/is_admin": true, //Public Claims
"user_id" : 12345123 //Private Claims
}
Signature는 JWT의 서명 부분으로 Header의 인코딩된 내용과 Payload의 인코딩된 내용을 더한 뒤에 Secret Key와 알고리즘을 이용하여 암호된 값을 나타낸다.
const jwt = require('jsonwebtoken'); // (1) jsonwebtoken 라이브러리 import
const payLoad = { accessToken: jwt }; // (2) 실제로 전달할 내용인 Payload 정의
const secretKey = process.env.JWTSECRET_KEY // (3)Secret Key는 노출되면 안 되기 때문에 환경변수로 관리해 주어야 한다.
const jwtToken = jwt.sign(payLoad, secretKey); // (4)sign 메소드로 JWT 발급, 첫번째 인자로 Payload가 두번째 인자로 Secret Key가 들어 가며 세번째 인자로 option을 추가 할 수 있는데, option이 존재하지 않으면 HS256 알고리즘으로 JWT가 발급된다.
console.log(jwtToken)
=> 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE2NTA1NTYxMzZ9.YAMgUMLhiVUwkRTr2rpOrIyWN0cTGLxsxZBqLAaKWUU'
const decoded = jwt.verify(jwtToken, secretKey); // (1)verify 메소드로 JWT의 Payload 확인, 첫번째 인자로 JWT가 두번째 인자로 토큰을 만들 때 사용한 Secret Key가 들어감
console.log(decoded)
=> { accessToken: jwt, iat: 1650555667 } // (2) JWT가 가지고 있던 Payload