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

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