TIL.Bcrypt

블로피시 암호에 기반을 둔 암호화 해시 함수

Blowfish는 키(key)방식의 대칭형 블록 암호이다.
소프트웨어에서 양호한 암호화 속도를 제공하지만 현재는 고급 암호화 표준이 더 많은 눈길을 끌고 있다.

단방향 해시함수의 취약점을 보완하기위하여 등장


단방향 해시함수의 취약점

  • 레인보우테이블 공격
    • 보통 해시함수를 이용하여 저장된 비밀번호로 부터 원래의 비밀번호를 추출해 내는데 사용
  • 무차별 대입 공격(brute force attack)
    • 해시함수는 원래 짧은 시간에 데이터를 검색하기 위해 설계된 것으로 해시함수의 빠른 처리 속도로 인해 해커는 매우 빠른 속도로 임의의 문자열의 다이제스트와 해킹할 대상의 다이제스트를 비교할수있다.
      • 해시함수를 사용하여 변환가능한 모든 해시값을 저장시켜 놓은 표
      • MD5를 사용한 경우 일반적인 장비를 이용하여 초당 56억개의 다이제스트를 대입할수 있다.

단방향 해시함수 보완

  • 솔팅(Salting)

    • 패스워드에 임의의 문자열인 salt를 추가하여 다이제스트를 생성하는것으로 같은 팽스워드라도 각기다른 salt가 들어가 다이제스트가 다르게 생성되어 rainbow table을 무의미하게 만든다.
  • 키 스트레칭(Key stretching)

    • 해시를 여러번 반복하여 시간을 늘림으로써 bruteforce attack 에 대비하는것이다
      • 패스워드 다이제스트 생성 → 생성된다이제스트를 입력값으로 하여 또 다이제스트를 생성 이를 반복
      • 만약 원래대로라면 1초에 56억개를 대입하여 비교할 수있는데 키스트레칭을 적용하여 1초에 5번정도만 비교할 수있게 한다면 하나의 다이제스트를 해킹하는데 오래 걸릴 것이다.

bcrypt의 해시문자열은 다음과 같다

$2b$[cost]$[22 character salt][31 character hash]

ex)

$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
 \/ \/ \____________________/\_____________________________/
Alg Cost       Salt                        Hash

Alg : 2a = 해시알고리즘 식별자 (bcrypt)

Cost : 10 = Cost factor : 2**10 =⇒ 1.024 rounds

Salt : N9qo8uLOickgx2ZMRZoMye = 16바이트(128비트) 솔트

Hash : IjZAgcfl7p92ldGxad68LJZdL17lhWy :24 바이트 해시


알고리즘

Function bcrypt
   Input:
      cost:     Number (4..31)                      log2(Iterations). e.g. 12 ==> 212 = 4,096 iterations
      salt:     array of Bytes (16 bytes)           random salt
      password: array of Bytes (1..72 bytes)        UTF-8 encoded password
   Output:
      hash:     array of Bytes (24 bytes)

//Initialize Blowfish state with expensive key setup algorithm
   //P: array of 18 subkeys (UInt32[18])
   //S: Four substitution boxes (S-boxes), S0...S3. Each S-box is 1,024 bytes (UInt32[256])
   P, S ← EksBlowfishSetup(cost, salt, password)

//Repeatedly encrypt the text "OrpheanBeholderScryDoubt" 64 times
   ctext ← "OrpheanBeholderScryDoubt"  //24 bytes ==> three 64-bit blocks
   repeat (64)
      ctext ← EncryptECB(P, S, ctext) //encrypt using standard Blowfish in ECB mode

//24-byte ctext is resulting password hash
   return Concatenate(cost, salt, ctext)

해시 함수 종류

  • MD5, SHA-1, HAS-180 : 이미 보안이 뚫린 해시함수
  • SHA-256, SHA-512 : 특정 입력값에 대해 항상 같은 해시 값을 리턴(레인보우 어택에 취약)
  • PBKDF2(Password-Based Key Derivation Function)해시함수의 컨테이너인 PBKDF2는 솔트를 적용한 후 해시함수의 반복 횟수를 임의로 선택할 수 있다. PBKDF2는 아주 가볍고 구현하기 쉬우며, SHA와 같이 검증된 해시함수만을 사용한다.(Django에서 사용)
  • bcrypt (Key Derivation Functions)bcrypt는 처음부터 패스워드 저장을 위해서 설계되었다. bcrypt는 보안에 집착하기로 유명한 OpenBSD에서 기본 암호 인증 메커니즘으로 사용되고 있고 미래에 PBKDF2보다 더 경쟁력이 있다고 여겨진다. 입력값을 72byte로 해야 하는 부분이 PBKDF2와 scrypt와 다르다.
  • scrypt (Key Derivation Functions)scrypt는 상대적으로 최신 알고리즘이며 나머지 둘보다 더 경쟁력 있는 것으로 평가되나 아직 덜 확산되어 있다. scrypt는 다이제스트를 생성할 때 메모리 오버헤드를 갖도록 설계되어, brute force attack을 시도할 때 병렬화 처리가 매우 어렵다. 따라서 PBKDF2보다 안전하다고 평가되며 미래에 bcrypt에 비해 더 경쟁력이 있다고 여겨진다. scrypt는 보안에 아주 민감한 사용자들을 위한 백업 솔루션을 제공하는 Tarsnap에서도 사용하고 있다.

사용자 인터페이스 고려사항

  • 패스워드의 길이를 제한 두지 않는다
    • 제한을 둔다면 평문저장이 의심될분만아니라 brute force의 범위를 좁힐수있기 때문
  • 최소길이는 제한하는것이 좋다.
  • 특수문자 대소문자를 제한하지 않는다
  • 패스워드(&민감정보)를 입력하는 POST 요청은 반드시 SSL로 보낸다.
  • 패스워드 실패 횟수 제한을 걸어 둔다.

사용방법

bcrypt.hashpw(비밀번호.encode(’utf-8’), bcrypt.gensalt())

bcrypt.hashpw(비밀번호.encode, gensalt())
이 함수는 암호나 다른 어떤 string도 hashing해준다. 두 가지 인자를 받는다.
A string(bytes)
Salt : bcrypt.gensalt()를 통해 자동으로 랜덤값을 받을수있다.

예제

import bcrypt
password = '1234'
hash_password = bcrypt.hashpw(password.encode('utf-8'),bcrypt.gensalt())
hash_password
'2a$12acusMujY4QblouspQ.681uJaTU1T.rfsxOalWzUx90yyrV46im.1G'

예제 타입

password = str
hash_password = str
qassword.encode(’utf-8’) = byte

해시패스워드 과정

패스워드를 1번 인자로 받고 encode매서드로 byte타입으로 변경후 gensalt를 통해 문자열 추가후 암호화

💡 현재 3.9버전에서는 decode를 안써도 byte타입에서 str타입으로 변환 =⇒ 라이브러리의 문제로 보임 3.9 이하의 버전에서는 hash_password = bcrypt.hashpw(password.encode('utf-8'),bcrypt.gensalt()).decode(’utf-8’) 으로 타입변환을 해주어야한다. 이유는 추후 비밀번호를 비교하는과정에서 byte타입이 아닌 str 타입으로 비교하기 때문에

bcrypt.checkpw()

비밀번호가 데이터 베이스에 저장된 값과 동일한지 확인해볼수 있는 함수이다.

b = '1234'
bcrypt.checkpw(b.encode('utf-8'), hash_password)
True
bcrypt.checkpw(b.encode('utf-8'), hash_password)
True
d = '12345'
bcrypt.checkpw(d.encode('utf-8'), hash_password)
False

이미 위에서 실행한

import bcrypt
password = '1234'
hash_password = bcrypt.hashpw(password.encode('utf-8'),bcrypt.gensalt())
hash_password
'2a$12acusMujY4QblouspQ.681uJaTU1T.rfsxOalWzUx90yyrV46im.1G'

이곳의 hash_password에 salt값이 있기 때문에 hashpw인자에서 gensalt()를 추가 실행할 필요가 없다.

로그인 시에는 저장 되어있는 salt값과 매칭이 되는지 확인 하기 때문에 password를 만들때만 gensalt한다.

좋은 웹페이지 즐겨찾기