Backend
인증(Authentication)과 권한(Authorization)
인증(Authentication)과 권한(인가: Authorization)
인증(Authentication, Auth)
인증이란 누군가의 신원을 확인하는 것을 말하며, 특정 클라이언트가 주장하는 사용자임이 맞는지를 확인하는 과정이다. 유저가 누구인지 확인하는 절차로, 보통 사용자 이름와 암호를 통해 인증하게 된다.
권한(Authorization, 인가)
클라이언트가 하고자 하는 행동이 허가되어 가능한 행동인지를 확인하는 것을 말하며, 접근 가능 대상과 편집 및 삭제가 가능한 대상 등 웹의 어느 부분에 접근이 가능한지 등을 체크한다. 유저에 대한 권한을 부여하는 것이기도 하며, 사용자가 인증된 후에 주로 일어나는 과정이다.
비밀번호를 원형으로 저장하지 않는 방법
사용자를 인증하기 위해 사용자 이름과 암호, 패스워드, 비밀번호를 받았다면 그 사용자의 이름을 저장해야 한다. 그리고 절대로 암호를 원형으로 데이터베이스에 저장해서는 안된다. 다시 한번 강조하는데,
절대로 암호를 원형으로 저장해서는 안된다.
대신 우리는 암호를
해시(Hash)
할 것이다. 암호를 데이터베이스에 그대로 저장하는 대신, 해시 함수라는 것으로 암호를 처리한다. 이후 해시 함수의 결과를 데이터베이스에 저장하는 것이다.해시 함수(Hash Function)란? 임의의 길이를 갖는 데이터를 고정된 길이의 데이터로 매핑(변환)하는 함수다. 해시 함수에 의해 얻어지는 값은 해시 값, 해시 체크섬 또는 해시라고 한다. 암호 알고리즘과 달리 키(key)를 사용하지 않아 같은 입력에 항상 같은 출력이 나오게 된다. 주로 데이터의 오류나 변조를 탐지하여 데이터 무결성을 제공하기 위해 사용한다.
사용자 등록 시에 작성한 암호를 해싱하여 결과값(해시 값)을 데이터베이스에 저장해두고, 사용자가 인증을 요구할 때 작성한 암호의 해시 값을 데이터베이스에 저장된 값과 비교한 뒤 동일하다면 인증되는 것이다.
암호화 해시 함수(cryptographic hash function)
해시 함수는 하나의 특정 함수를 의미하는 것이 아니라 정의에 부합하는 수많은 해시 함수를 가진 집합이다. 여러 종류가 있고 여러 용도가 있다. 모든 해시 함수가 암호 저장에 적합한 것은 아니다. 패스워드에 사용하는 해시 함수는 특정 몇 가지 해시 함수만을 사용한다. 암호화 해시 함수의 몇 가지 조건을 알아보자.
단방향 함수
단방향 함수(일방향 함수, one-way function)란? 계산하기는 쉽지만 역을 구하는 것은 어려운 함수다. 결과값이 주어졌을 때 입력값을 구하는 것이 어려운 함수이다. ‘쉽다’, ‘어렵다’는 의미도 수학적으로 엄밀히 정의해야 한다. - 위키백과
보통 암호화 해시 함수(Cryptographic Hash Functions)는 변환이 불가능한
단방향 함수(One-way Function)
여야 한다. 즉, 역추적이 불가능해야 한다.아주 간단한 단방향 함수의 예시로는 절댓값 함수를 들 수 있다. 입력 값을 모르는 변수 x가 절댓값 함수를 통과하자 결과값으로 100이 나왔다면, 변수 x는 100일까 -100일까? 경우의 수는 둘 뿐이지만 이 또한 단방향 함수라고 할 수 있다.
눈사태 효과 (쇄도 효과, 산사태 효과)
다음은 입력 값에 작은 변화가 있을 때 출력 값이 크게 변해야 한다는 점이다. ‘qwer1234’이라는 암호가 ‘qwor1234’로 바뀌었을 때의 두 해시 값이 완전히 달라야 한다.
충돌 저항성(collision resistance)
해시 충돌에 대해 안전해야 한다는 뜻인데, 충돌이 거의 일어나서는 안된다. 거의 일어나서는 안된다는 말은 계산상 어려워야 한다는 뜻이고, 정말 거의 일어나서는 안되는 수준이어야 한다. 똑같은 값이 두 개 출력되는 충돌이 일어날 확률이 ~ 정도여야 한다.
해시 충돌(Hash Collision)이란? 해시 함수가 서로 다른 두 개의 입력값에 대해 동일한 출력값을 내는 상황을 의미한다. 해시 함수의 입력값 수가 무한하다고 가정한다면 비둘기집 원리에 의해 해시 충돌은 항상 존재할 수 밖에 없다. 해시 충돌은 함수를 이용한 자료구조나 알고리즘의 효율성을 떨어뜨리기 때문에 해시 충돌이 자주 발생하지 않도록 구성되어야 한다. 특히 암호학적 해시 함수의 경우 해시 함수의 안전성을 깨뜨리는 충돌 공격이 가능하기 때문에 의도적 해시 충돌을 만드는 것이 어렵도록 만들어야 한다.
결정적 함수(Deterministic functino)
입력 값이 같으면 출력 값도 항상 같음을 보장해야 하는 함수를 말한다. 올바른 암호를 적었는데도 해시 값이 데이터베이스에 저장되어 있는 해시 값과 다르게 나온다면 인증을 보장할 수 없다.
느린 함수
암호화 해시 함수의 경우 예상과 다르게 느린 함수(deliberately Slow)를 사용해야 한다. 빠른 해시 함수를 사용한다면 훨씬 쉽게 침입을 시도할 수 있다. 고의적으로 느린 함수를 사용한다면 이 속도를 늦출 수 있다. 암호화 해시 함수 중에서 사용자 패스워드를 위한 함수가 아니어서 빠르게 작동하는 함수도 존재한다. 서명한 쿠키에 이용되는 함수는 SHA-256 이라는 해시 함수다. 너무 빠른 해시 함수이기 때문에 암호를 해시하는 데는 적합하지 않다.
솔트 (암호학)
암호학에서 솔트(salt)는 데이터, 비밀번호, 통과암호를 해시 처리하는 단방향 함수의 추가 입력으로 사용되는 랜덤 데이터이다. 솔트는 스토리지에서 비밀번호를 보호하기 위해 사용된다. 역사적으로 비밀번호는 시스템에 평문으로 저장되지만 시간이 지남에 따라 추가적인 보호 방법이 개발되어 시스템으로부터 사용자의 비밀번호 읽기를 보호한다. 솔트는 이러한 방식의 하나이다. - 위키백과
암호를 해시할 때, 암호를 역산하거나 해시 함수의 허점을 알아내기 어렵도록 만들게끔 하는 추가 안전 장치를 솔트라고 한다.
패스워드에 대한 세 가지 진실
- 많은 사람들은 여러 웹사이트에 같은 암호를 사용한다.
- 많이 사용되는 암호 백만 개가 존재한다.
‘123456’
이 조사된 모든 암호의 4%를 차지하는 설문 결과가 있다.
- 암호를 저장하는데 적합한 해시 알고리즘은 몇 가지 뿐이다.
bcrypt
: 정말 대부분의 앱이 이 알고리즘을 사용한다.
그렇다면, 데이터베이스 침입해서 사용자의 암호 해시 값을 알아내어서 많이 사용되는 암호를 대부분의 앱이 사용하는 암호화 해시 알고리즘에 넣어 나온 결과값과 대조하는, 역방향 조회 테이블을 생성하는 것을 막을 수 없다는 것이다.
📏 Hash(password + salt) ⇒ hashed
역방향 조회 테이블을 만들 수 없게 하는 것이 바로 암호 솔트(Password Salt)이다. 누군가의 암호를 해시할 때 암호를 해시하면서 임의의 값을 넣는 것이다. 사용자마다 무작위로 생성된 솔트는 아주 다른 출력값을 만들어낸다. 해커는 솔트를 알아 낼 수 없기 때문에 미리 계산해둔 조회 테이블을 사용할 수 없게 된다.
위에 언급한 bcrypt 알고리즘에도 솔트를 생성해주는 과정이 있다.
Bcrypt
bcrypt는 사용할 수 있는 몇 안되는 암호화 해시 함수 알고리즘 중 하나로, 아주 흔히 사용된다. 1999년에 만들어진 암호이며 이 알고리즘은 거의 모든 프로그래밍 언어로 구현되어 있다.
두 패키지의 차이점은
bcryptjs
는 오로지 자바스크립트로만 작성되어서 클라이언트 측에서도 실행이 가능하다는 점이다. bcrypt
패키지는 서버를 대상으로 작성된 패키지이다.
어떤 것을 사용해도 무방하나
bcrypt
패키지를 설치해서 데모 코드를 작성해보자. 그리고 이 패키지는 Promise를 지원하기 때문에 async-await 비동기 구문을 사용할 수 있다.bcrypt
에는 API가 몇 가지 없는데, genSalt()
와 hash()
이 두 메서드가 암호화된 해시 값을 생성하기 위해 알아야 할 메서드이다.예제 코드
// Usage Step#1 bcrypt.genSalt(saltRounds, function(err, salt) { bcrypt.hash(myPlaintextPassword, salt, function(err, hash) { // Store hash in your password DB. }); });
genSalt()
는 솔트 값을 생성해주는 메서드이다.
saltRounds
의 Round
는 해시의 난이도를 뜻한다. 해시를 계산하는데 걸리는 시간(해시 난이도)을 조절할 수 있는 bcrypt
알고리즘의 핵심 기능이다. saltRounds
에 넣는 숫자가 낮아질 수록 해시 과정은 더 빠르게 처리되며 이 속도는 기하급수적으로 늘릴 수도 있다. 숫자를 전달하는 목적 자체는 과정을 느리게 만드는 것에 있다. 요즘은 saltRounds
에 12를 전달하는 것이 권장 값이라고 한다.사용하기
const bcrypt = require('bcrypt'); const hashPassword = async () => { const salt = await bcrypt.genSalt(12); console.log(salt); } hashPassword();
$ node index.js $2b$10$.o7mp2lhmgcKAiYGU4eTVu
- 콘솔에 출력된 결과값이 자동으로 생성된 솔트
const bcrypt = require('bcrypt'); const hashPassword = async (password) => { const salt = await bcrypt.genSalt(12); const hash = await bcrypt.hash(password, salt) console.log(salt); console.log(hash); } hashPassword('qwer1234');
$ node index.js $2b$10$.T5MREqV6V1Gmlpl/1aF8e $2b$10$.T5MREqV6V1Gmlpl/1aF8eKV1rd7ZAPFEA7hkKYD9ZJiYVV/YRBly
- 위가 솔트값, 아래가 패스워드 ‘qwer1234’를 솔트와 함께 해싱한 해시 값
해시 값에 솔트 값이 포함되어 있는 것이
bcrypt
의 특징이다. 해시 출력 값에 포함되어 있기 때문에 우리는 따로 솔트를 저장할 필요가 없다. 자동 생성되는 솔트는 계속해서 값이 바뀌기 때문에 다시 패스워드를 해싱해도 출력값은 동일하지 않다. 그렇다면 어떻게 데이터베이스의 해시 값과 사용자의 인증 암호를 비교할 수 있을까?
// Load hash from your password DB. bcrypt.compare(myPlaintextPassword, hash, function(err, result) { // result == true }); bcrypt.compare(someOtherPlaintextPassword, hash, function(err, result) { // result == false });
compare()
메서드는 암호를 비교하기 위한 메서드
const bcrypt = require('bcrypt'); const hashPassword = async (password) => { const salt = await bcrypt.genSalt(12); const hash = await bcrypt.hash(password, salt) console.log(salt); console.log(hash); } const login = async (password, hashedPassword) => { const result = await bcrypt.compare(password, hashedPassword); console.log(result); } // hashPassword('qwer1234'); login('qwer1234', '$2b$10$.T5MREqV6V1Gmlpl/1aF8eKV1rd7ZAPFEA7hkKYD9ZJiYVV/YRBly');
- 콘솔에 출력된 해시 값을 넣어 검증한 결과와, 값을 바꾸어 검증한 결과
bcrypt
솔트는 공개되어도 상관없다. 솔트의 목적은 무작위성의 부여이기 때문이다. 우리가 정해야 할 것은 saltRound
즉, 해시 난이도(횟수)만 전달하면 된다. 12에서 17정도로만 올려도 콘솔이 출력되는 속도가 많이 느려지는 것을 확인할 수 있다.