Nate 해킹사건 때문에 데이터 암호화를 보다 더 강화하기로 했다. 물론 AES로 하자고 그래서 하는데, 부득불 mysql의 aes_encrypt / aes_decrypt와 호환되게 한다고 해서 하다가 몇가지 꼼수를 부리게 되었다.

AES

표준 암호화/ 복호화 알고리즘은 미국 어디선가 공모해서 여러가지 응모한 알고리즘 중에 rijndael을 선택하였고, 그중에 128bit 블럭 알고리즘은 표준으로 정하고 AES(Advanced Encrypt Standard)라고 이름을 붙혔다.

결국 rijndael-128 == AES 인 것이다.

rijndael 의 모드

  • ECB: 각 블럭은 서로 독립적이다. MySQL에서 쓰는 모드는 이거다.
  • CFB, CBC, CTR 등 나머지 모드는 약간 미묘한 차이가 있지만, MySQL은 ECB를 쓰기에 나중에 시간나면 공부하자.

MySQL의 AES의 제한사항은

  • 128 비트 밖에 지원하지 못한다. 196, 256등 다른 비트를 지원하지 못하며 지원하려면 소스를 다시 컴파일해야한다.
  • 다시 컴파일 한다고 그래도 한가지 방식밖에 못쓴다. ㅡㅡ
  • initialization vector를 사용하지 않는다. mcrypt에서는 random iv를 사용해서 보안을 한층 더 강화할 수 있을 것 같은데…
  • 블럭 비트의 여유 바이트를 맞추기 위해서 데이터에 약간의 추가 데이터를 더하는데, 안의 데이터는 추가할 데이터의 길이만큼을 chr(length(pading)))을 더해서 맞춘다.
  • 패딩 길이는 0은 없다. 1부터 시작한다. 0이 없는 이유는 0이 붙으면 다시 decrypt할때 어디까지 잘라내야할 지 곤란하기 때문이다.

구현

  • rijndael은 그냥 mcrypt를 쓴다. MySQL은 aes 소스 자체를 포함하고 있다. 이게 mcrypt인지 아닌지는 모름..
  • AES == rijndael-128 / ecb

Python 버전

참고로 python에서 padding은 \x00을 사용한다.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def aes_encrypt(data, key):
    """MySQL aes_encrypt compatible encrypt function"""
    import mcrypt
    import binascii
 
    m = mcrypt.MCRYPT('rijndael-128', 'ecb')
 
    m.init(key)
 
    # MySQL은 여기다 padding을 붙여서 진행하니까...
    # 아래 공식은 다음 주소에서 확인
    # 그리고 padding하는 것은 padding하는 문자의 chr(x)의 값을 padding 한다.
    expected_length = 16 * ((len(data) / 16) + 1)
    padding_length = expected_length - len(data)
 
    data = data + chr(padding_length) * padding_length
    enc = m.encrypt(data)
    return binascii.hexlify(enc).upper()
 
def aes_decrypt(data, key):
    """MySQL aes_decrypt compatible decrypt function"""
    import mcrypt
    import binascii
 
    m = mcrypt.MCRYPT('rijndael-128', 'ecb')
 
    data = binascii.unhexlify(data)
    m.init(key)
    dec = m.decrypt(data)
 
    if '\x00' in dec:   # Python-mysql은 padding으로 \x00을 사용한다.
        dec = dec[:dec.index('\x00')]
 
    # MySQL은 Padding으로 더해진 문자열 갯수를 사용한다.
    last = dec[len(dec) - 1]
    dec = dec[:-ord(last)]
 
    return dec

PHP 버전

php 버전의 문제는 bin2hex는 php 버전이 있지만, 역은 undocumented다. 그래서 구현해줘야한다.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static public function aes_encrypt($data, $key){
  $expected_length = 16 * (floor(strlen($data) / 16) +1);
  $padding_length = $expected_length - strlen($data);
  $data = $data . str_repeat(chr($padding_length), $padding_length);
  $enc = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_ECB);
  return strtoupper(bin2hex($enc));
}
 
static public function aes_decrypt($data, $key){
  // bin2hex의 역 함수가 php는 없어서 새로 만듦
  if(!function_exists('hex2bin')){
    function hex2bin($h)
      {
        if (!is_string($h)) return null;
        $r='';
        for ($a=0; $a<strlen($h); $a+=2) { $r.=chr(hexdec($h{$a}.$h{($a+1)})); }
        return $r;
      }
  }
 
  $data = hex2bin($data);
  $dec = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_ECB);
  $last = $dec[strlen($dec) - 1];
  $dec = substr($dec, 0, strlen($dec) - ord($last));
  return $dec;
}

참고

  • MySQL의 AES가 128면 불안해 하는 사람(^^)들이 있다면, MySQL의 aes 함수들은 포기하고 libmcrypt를 이용해서 stored procedure를 만드는 것이 좋다.
  • 근데 굳이 MySQL의 command line에서 이것을 쓴다는 것은 글쎄 개인적인 소견으론 바람직하지 않는 것 같다.
  • MySQL에서 사용하는 것을 포기한다면 initialization vector를 사용하는 것이 좋겠다.
  • 음 다음에 데이터 암호화 한다면 아래처럼 해볼듯…
    • 랜덤 iv 생성 -> 데이터 암호화 (with salt) -> iv 자체도 암호화 -> 약간 섞어서 최종 데이터 생성 –> bin2hex