회원 비밀번호 암호화 방법 개선

이슈 #889 에서 말씀드렸던 개선안입니다. 몇 년 묵은 소원이었는데 이제야 시간을 내서 작업을 해봤네요.

기존 회원DB 및 다른 모듈들과 100% 호환성을 유지하면서, 회원 로그인 비밀번호 저장시 PBKDF2, bcrypt 등 강력한 암호화(해싱) 함수 사용을 가능하도록 하는 것을 목표로 합니다.

변경한 내역은 크게 3부분으로 나누어 설명할 수 있겠습니다.

#### 첫째, 비밀번호 암호화(해싱) 기능을 전담하는 클래스 추가

새로 추가한 파일은 `classes/security/Password.class.php`입니다. PBKDF2 알고리듬처럼 이미 공개되어 있는 정보를 가져다 쓴 몇몇 부분을 제외하면 모두 제가 직접 구현하였으며, 이 풀리퀘를 받아들이실 경우 저작권은 네이버 XE개발팀에 드리겠습니다.

`Password` 클래스는 아래와 같은 기능을 수행합니다.

- 현재 시스템에서 사용 가능한 해싱 함수의 목록을 작성하고 (`getSupportedAlgorithms()` 메소드) 그 중 가장 강력한 보안을 제공하는 알고리듬을 추천합니다. (`getBestAlgorithm()` 메소드)
- 주어진 비밀번호를 주어진 알고리듬으로 해싱합니다. (`createHash()` 메소드)
- 이렇게 생성하여 DB에 저장해 두었던 해시를 사용자가 입력한 비밀번호와 비교합니다. (`checkPassword()` 메소드)
- 지원하는 해싱 알고리듬 목록은 아래와 같습니다:
  - md5 (기존 XE 기본값)
  - md5+sha1+md5 (기존 XE에서 `$useSha1`을 지정할 경우 사용하던 옵션)
  - PBKDF2 (PKCS#5 v2 표준, sha256 기반) **PHP 5.1.2~5.3.6에서 추천**
  - bcrypt **PHP 5.3.7 이상에서 추천**
  - 그 밖에 지원하던 알고리듬들은 11월 12일에 삭제했습니다.
- 해시를 넘겨주면 해시의 길이, 특정 문자열의 존재여부 등을 통해 어떤 알고리듬으로 생성된 해시인지 자동으로 파악합니다. (`guessAlgorithm()` 메소드) 따라서 새 방식으로 해싱된 비밀번호는 물론, 기존의 어떤 방식으로 해싱된 비밀번호라도 정확하게 확인이 가능합니다.
- 일부 해싱 함수는 무작위로 생성된 솔트를 필요로 합니다. `openssl` 모듈, `mcrypt` 모듈, `/dev/urandom`, `mt_rand()` 등 현재 시스템에서 사용 가능한 가장 강력한 엔트로피 출처를 파악하여 각 함수가 필요로 하는 규격의 솔트를 자동으로 생성합니다. (`createSecureSalt()` 메소드) 이 기능은 비번 암호화 외에도 무작위로 생성된 문자열이 필요한 곳에서는 얼마든지 활용이 가능할 것으로 보입니다.
- 테스트 편의를 위해, XE와 별도로 사용하더라도 모든 기능이 정상적으로 동작하도록 만들었습니다. 그러나 XE 환경에서 사용시 아래에서 설명드릴 설정에 따라 자동으로 알고리듬을 선택하는 등의 헤택(?)이 있습니다.

#### 둘째, `member` 모듈에서 비밀번호 처리시 위의 클래스와 연동합니다.

- `memberModel` 클래스의 `isValidPassword()` 메소드를 고쳐서 `Password` 클래스와 연동하도록 합니다. 기존의 방식으로 암호화된 비번과 새로 추가된 알고리듬까지 이 함수 하나로 모두 지원합니다.
- `memberModel` 클래스에 `hashPassword()` 메소드를 추가하여 `Password` 클래스와 연동하도록 합니다. 문서, 댓글, 게시판 등의 모듈을 보면 비번을 확인할 때는 `memberModel` 클래스의 `isValidPassword()` 메소드를 호출하는데, 비번을 해싱할 때는 모두 각자 `md5()`를 사용하고 있습니다. 지금까지 XE에서 비번 확인 메소드는 제공했지만 비번 해싱 메소드는 별도로 제공하지 않았기 때문입니다. 해싱 메소드를 제공함으로써 앞으로 XE 모듈을 개발하시는 분들의 편의를 도모합니다.
- `memberController` 클래스에서 회원 가입 및 직접 추가, 회원 정보 및 비번 변경, 비번 리셋 등의 작업을 할 때는 언제나 `memberModel` 클래스의 `hashPassword()` 메소드를 호출하도록 하여 일관성을 높였습니다.

#### 셋째, 관리 페이지에서 해싱 방법을 직접 지정할 수 있도록 하였습니다.

회원 설정에 비밀번호 암호화와 관련된 설정 항목 3가지를 추가하였습니다. (스크린샷 참조)

![untitled](https://cloud.githubusercontent.com/assets/164058/5008790/84502b0c-6aa3-11e4-9e67-98b1aec307ce.png)

- 기존에 운영중이던 사이트에 패치를 적용하면 기본값은 md5로 설정됩니다. 기존 버전과의 호환성을 보장하기 위해서입니다. 그러나 패치를 적용한 후에는 목록에서 원하는 알고리듬을 선택하여 변경할 수 있습니다. (PHP 5.3.6 이하에서는 bcrypt 옵션이 나타나지 않습니다.)
- 암호화 소요시간은 bcrypt의 work factor를 의미합니다. 4~16 범위에서 조절할 수 있으며, 기본값은 8입니다. 권장하는 범위는 8~12 사이입니다. (원래 bcrypt는 4~31 범위를 지원하지만, 16이 넘어가면 서버에 무리가 많이 갑니다. 너무 높은 값을 실수로 선택하는 것을 방지하기 위해 화면상에는 16까지만 표시합니다.)
- PBKDF2에도 소요시간 설정이 동일하게 적용됩니다. 같은 설정 하의 bcrypt와 비슷한 시간이 소요되도록 했습니다.
- 암호화 방법 자동 변환 설정은 현재 설정된 알고리듬과 다른 알고리듬으로 해싱된 비밀번호가 발견될 경우 자동으로 변환할지의 여부를 결정합니다. 물론 해싱 함수는 복호화가 불가능하므로, 다음번에 로그인하거나 비밀번호를 변경할 때 변환이 이루어집니다.

#### 기타

- 처음 설치할 때는 서버에서 지원하는 가장 강력한 알고리듬을 자동으로 선택합니다. PHP 5.3.7 이상에서는 bcrypt, 5.3.6 이하에서는 PBKDF2이지만, PHP 모듈이 이상하게 설치되어 있는 경우 기존과 같이 md5로 선택될 수도 있습니다.
- 문서, 댓글, 게시판, 방명록, 위키, textyle 등 다른 모듈에는 영향을 주지 않습니다. (로그인 비밀번호를 제외하면 대부분의 모듈들이 자체적으로 `md5()`를 사용하고, 비번을 확인할 때만 `memberModel`의 `isValidPassword()` 메소드를 호출하고 있습니다. `isValidPassword()` 메소드는 기존의 md5 해시와 100% 호환되기 때문에 괜찮습니다.) 호환성 테스트에 대한 자세한 내용은 #889 를 참조하시기 바랍니다.
- 모든 클래스와 메소드에는 철저하게 주석을 넣고, XE 코딩 스타일에 맞추려고 노력했습니다.

무지막지하게 큰 패치와 스크롤 압박 쩌는 풀리퀘 내용을 읽어주셔서 감사합니다. 호환성 문제가 있다거나, 설정을 조금 다른 방식으로 하면 좋겠다거나, 그 밖에 지적하실 부분이 있으면 얼마든지 알려주세요. 바꿀 건 바꾸고, 고칠 건 고치더라도, XE의 비번저장 보안은 꼭 개선되었으면 좋겠습니다.

그래도 저처럼 코어 개발자님들과 일면식도 없는 사람이 이런 풀리퀘도 넣을 수 있고... 이래저래 자유 소프트웨어 만세입니다 ^__^
오늘 이 부분 코드를 사용하려다가 의문이 들어서 질문해 보아요..
이 부분에서 왜 +/=를 임의로 문자로 치환하셨는지 알 수 있을까요? 혹시 파일명/URI 인코딩 이 이유이면 _- 으로 치환하면 되지 않을까 싶어서요. 아니면 다른것을 의도하신것인지 알고싶습니다..