인턴 근무도 벌써... 한 달이 지났군
회원가입, 로그인 기능을 구현하면서 비밀번호 암호화 단계에서 배운 내용을 벨로그에도 정리해보기로!
단방향 암호화 기법
해시함수(해시 알고리즘)을 이용하여 고정된 길이의 암호화 된 문자열로 바꿔버리는 것
단방향 해시 함수의 다이제스트(digest)로 사용자의 패스워드를 저장
원본 메시지를 알면 암호화 된 메시지를 구하긴 쉽지만, 암호화 된 메시지로는 원본 메시지를 구할 수 없다 (단방향성)
→ 수학적인 연산을 통해 원본 메시지를 변환하여 암호화 한 메시지
레인보우 공격 (rainbow attack)
- 동일한 메시지가 동일한 다이제스트를 갖는다면, 공격자가 전처리 된 다이제스트를 가능한 많이 확보한 다음 탈취한 다이제스트와 비교하여 원본 메시지를 찾아낼 가능성이 있다
- 이와 같은 다이제스트 목록을 레인보우 테이블(rainbow table)이라고 한다
해시 함수의 빠른 처리 속도는 사용자보다 공격자에게 더 큰 편의성을 제공한다
해시 함수 → 짧은 시간에 데이터를 검색하기 위해 설계된 것
때문에 해시 함수의 빠른 처리로 공격자도 매우 빠른 속도로 임의의 문자열의 다이제스트와 해킹할 대상의 다이제스트를 비교할 수 있다
솔트(salt): 단방향 함수에서 다이제스트를 생성할 때 추가되는 바이트 단위의 임의의 문자열
원본 메시지에 문자열(salt)을 추가하여 다이제스트를 생성하는 것
입력한 패스워스트의 다이제스트 생성 → 생성된 다이제스트를 입력값으로 하여 다이제스트 생성 → 이를 반복
→ 입력한 패스워드를 동일한 횟수만큼 해시해야만 입력한 패스워드의 일치 여부를 확인할 수 있다
많이 사용하는 function(라이브러리): PBKDF2, bcrypt, scrypt 등
임의의 길이 데이터를 고정된 길이의 데이터로 매핑하는 함수
SHA-256, SHA-512 등을 사용하기를 권고함
단방향은 복호화 할 수 없고, 양방향은 복호화 할 수 있다
대부분의 사이트는 비밀번호를 찾을 때 원래의 비밀번호를 알려주는 것이 아니라 재설정을 한다.
패스워드를 복호화를 하는 순간 외부에 노출이 되기 때문에 단방향 암호화를 사용한다. (= 복호화를 할 이유가 없다!)
Node.js에 내장되어 있는 내장 모듈 중 하나
→ 문자열을 암호화, 복호화, 해싱할 수 있도록 도와준다
createHash(), update(), digest()
createHash()
: 사용할 알고리즘update()
: 암호화 할 비밀번호digest()
: 인코딩 방식 → salt 값을 DB에 저장해야 한다
// 비밀번호 암호화 예시
// 1. generateAuthCode()를 통해서 12자리의 숫자로만 이루어진 salt 값을 생성
const salt: string = generateAuthCode(100000000000, 999999999999, true);
// 2. 비밀번호 해싱
const hashPassword: string = crypto
// 알고리즘: SHA-256 방식 사용
.createHash('sha256')
// 암호화 할 비밀번호 값과 salt 값을 같이 해싱
.update(password + salt)
// 인코딩 방식: hex
.digest('hex');
// 3. DB에 회원 정보 등록 → salt 값도 함께 create 해준다
const member: User = await User.create({
email,
password: hashPassword,
salt,
});
salt 값을 생성할 때 자체적으로 랜덤한 숫자를 만들어서 사용할 수도 있지만, crypto 내장 함수를 활용하는 방법도 있다.
crypto.randomBytes()
메소드를 통해서 salt를 반환하는 함수를 작성const salt = crypto.randomBytes(34).toString('base64');
솔트의 길이는 32바이트 이상이어야 솔트와 다이제스트를 추측하기 어렵다.
salt 값을 DB에 같이 저장해야 하는 이유 → 로그인 과정에서 필요
// 로그인 예시
// 1. 유저의 DB에 저장된 salt 값을 찾는다
const salt: User = await User.findOne({
attributes: ['salt'],
raw: true,
where: {
email,
},
});
// 2. 로그인 시 입력한 패스워드와 DB에서 찾은 salt 값으로 비밀번호를 해싱해준다
const hashPassword: string = crypto
.createHash('sha256')
.update(password + +salt['salt'])
.digest('hex');
// 3. 입력받은 패스워드로 해싱한 값과 DB에 저장된 패스워드 값을 비교해 로그인 성공 여부를 판단한다
if (user.password == hashPassword) {
/* 로그인 성공 */
} else {
/* 로그인 실패*/
}
솔트와 패스워드를 해시함수에 넣는 과정을 반복
→ 출력 값을 아주 느리게 산출되도록 한다
암호화 방법에는
pbkdf, scrypt, bcrypt
세 가지가 있음
scrypt
방식이 더 안전하다고 하지만 서버에 엄청난 부하가 가해지는 역효과가 발생할 수 있다
pdkdf()
pdkdf()
함수는 5개의 인자가 필요한데 아래와 같다.
crypto.pbkdf2(password, buffer.toString('인코딩 방식'), 반복횟수, digest길이, '암호화 알고리즘')
// 예시
crypto.pbkdf2(password, salt, 9999, 64, 'sha512', function(err, hashed) {
if(err) {
console.log(err)
} else {
console.log(hashed.toString('base64'))
}
})
참고 사이트
https://velog.io/@kaitlin_k/%EC%95%94%ED%98%B8%ED%99%94-%EB%B0%A9%EC%8B%9D
bcrypt.hashpw(password, bcrypt.gensalt())
Blowfish 암호를 기반으로 설계된 암호화 함수로, 가장 강력한 해시 메커니즘 중 하나
좀 더 느리게 설게뙨 암호화 방식이기 때문에 암호화 해독이 조금 더 어렵다 (SHA family는 연산 속도가 매우 빠르다! 공격자의 속도를 늦출 수록 암호화 해독이 어려워진다)
동작 매커니즘: 키 스트레칭
필요한 값: 입력한 암호, salt(bcrypt를 활용한 랜덤 생성)
genSaltSync(rounds, minor), hashSync(data, salt)
2차 비밀번호 설정에는 bycrpt 방식을 사용해 암호화를 했다
// 1. salt를 생성한다
const saltRounds = 10; // default가 10이다
const salt = bycrpt.genSaltSync(saltRounds);
// 2. 해싱한다
const hashedPassword = bcrypt.hashSync(secondaryPassword, salt);
// 3. 암호화 한 2차 비밀번호를 DB의 user 테이블에 업데이트 해준다
userInfo.update({
secondaryPassword: hashedPassword,
}, {
// 로그인 과정에서 식별한 회원 이메일
where: { email: accessToken['userEmail'] }
})
compareSync(data, encrypted)
설정한 2차 비밀번호는 마이페이지에서 회원정보를 불러올 때 bycrpt의 compareSync()
함수를 통해 비교한다
// 1. 유저 정보를 식별
const userInfo: User = await User.findOne({
where: { email: accessToken['userEmail'] },
});
// 2. 패스워드 확인
if (bcrypt.compareSync(secondaryPassword, userInfo.secondaryPassword)) {
/* 성공 */
} else {
/* 실패 */
}
salt를 만들 때 사용하는 rounds의 값은 아래에서 선택 가능하다
디폴트는 10이고, 적절한 rounds 값을 선택해 공격에 대비할 수 있다
rounds=8 // ~40hashes/sec
rounds=9 // ~20hashes/sec
rounds=10 // ~10hashes/sec
rounds=11 // ~5hashes/sec
rounds=12 // 2~3hashes/sec
rounds=13 // ~1 sec/hash
rounds=14 // ~1.5 sec/hash
rounds=15 // ~3 sec/hash
rounds=25 // ~1 hour/hash
rounds=31 // 2~3 days/hash