ikeikeikeike's unk blog.

http://github-awards.com/users/ikeikeikeike

Codeigniterで保存されているパスワードを復号化してDjangoに移行してみよう

いきなりですが

こちらのサイトによると最近流行りのPHPフレームワークTOP2は Laravel, Phalcon なんだとか

http://www.sitepoint.com/best-php-frameworks-2014/

多分昨今のPHP界隈は Codeigniter から Laravel, Phalcon あたりに旧システムを移植する話がどんどん増えてきてる頃合い(良く分かってないです)

せっかく移行するなら言語を変えたいと思ってる人もいてもいいはずですよね。

んでCodeigniterのパスワードは復号化できるのでPythonで復号化処理を書いちゃえばみなさんの重い腰もあがりやすいはず (Djangoは復号化出来ない, パスワードチェックはhash値の比較だったかな)

もくじ

  • Codeigniter復号化処理
  • Django inspectdb Command
  • South パッケージ
  • Django Custom User

いろいろ端折っているのでimportエラー他はそれなりに出ますよ

Codeigniter復号化処理

まず下記のような復号化処理を定義しておきます

# -*- coding: utf-8 -*-
import base64
import logging
import hashlib
import itertools

import mcrypt  #  XXX: pip install python-mcrypt


logger = logging.getLogger(__name__)


class DecodeError(Exception):
    """ CrypterDecode error """


class Codeigniter(object):

    def __init__(self, bit='rijndael-256', mode='cbc'):
        # AES:Rijndael 256bit, CBC modeを使用
        self.crypter = mcrypt.MCRYPT(bit, mode)

    def encode(self):
        raise NotImplementedError("Not implemented error: Codeigniter.encode")

    def decode(self, string, key='my-secret-key'):
        """ Codeigniterで保存されているパスワードの復号化 """
        data = ''
        hashkey = hashlib.md5(key).hexdigest()

        try:
            ## Codeigniter独自(っぽい)の復号化前処理
            # data_str: 元のバイト列を返す
            # key_str:  hash文字数が超えたらcycleして初めからhash文字を返す
            iters = zip(base64.b64decode(string), itertools.cycle(hashlib.sha1(hashkey).hexdigest()))
            for data_str, key_str in iters:
                temp = ord(data_str) - ord(key_str)
                if temp < 0:
                    temp += 256
                data += chr(temp)
        except TypeError as err:
            logger.exception(err)
            raise DecodeError(str(err))

        ## 以下mcrypt使用時のお約束
        init_vect = data[0:self.crypter.get_iv_size()]
        data = data[self.crypter.get_iv_size():]

        try:
            self.crypter.init(hashkey.ljust(24, '\0'), init_vect)
        except ValueError as err:
            logger.exception(err)
            raise DecodeError(str(err))

        return self.crypter.decrypt(data)[0:32].rstrip('\0')
実行方法
>>> codeigniter = Codeigniter()
>>> codeigniter.decode(encrypted_password)
'output raw password'
Django inspectdb Command

次に app の作成, Database から Django のモデル定義を生成するコマンドを実行

http://docs.djangoproject.jp/en/latest/ref/django-admin.html#inspectdb

$ python manage.py startapp egg
$ python manage.py inspectdb > egg/models.py

生成後のディレクトリ構成は下記のようになっている

$ tree
.
├── development.db
├── egg
│   ├── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── ham
│   ├── __init__.py
│   ├── __init__.pyc
│   ├── settings.py
│   ├── settings.pyc
│   ├── urls.py
│   └── wsgi.py
└── manage.py
South パッケージ

migration ツールの South を実行して Codeigniter のDatabase定義を修正する準備

$ python manage.py schemamigration [django_app] --initial

これで、定義は終了したのでmodelsを修正すれば、south がDatabaseの改修をしてくれます。

$ python manage.py schemamigration [django_app] --auto
$ python manage.py migrate

Django Custom User

最後に先ほど定義した復号化処理を Django user model へ差し込めば移行処理が完了

# -*- coding: utf-8 -*-
from django.db import models
from django.utils import timezone
from django.contrib.auth import hashers
from django.utils.encoding import python_2_unicode_compatible

from . import crypter


@python_2_unicode_compatible
class BaseUser(models.Model):
    """ 各UserModelの親クラス

    passwordのアルゴリズムを旧フレームワークから移植している
    """
    encrypted_password = models.CharField(u'パスワード', max_length=128L)
    last_login = models.DateTimeField(u'ログイン日時', default=timezone.now)

    is_active = True

    REQUIRED_FIELDS = []

    class Meta:
        abstract = True

    def get_username(self):
        """ AccountのID識別子を返却 """
        return getattr(self, self.USERNAME_FIELD)

    def __str__(self):
        return self.get_username()

    def natural_key(self):
        return (self.get_username(), )

    def is_anonymous(self):
        return False

    def is_authenticated(self):
        return True

    def set_password(self, raw_password):
        """ パスワードを設定する """
        self.encrypted_password = hashers.make_password(raw_password)

    def check_password(self, raw_password):
        """ raw passwordのチェック

        旧passwordが正しい場合は即Django暗号化方式のパスワードへ更新する
        """

        def setter(raw_password):
            """ password更新 """
            self.set_password(raw_password)
            self.save(update_fields=["encrypted_password"])

        if self.has_usable_password():
            # Django password
            return hashers.check_password(raw_password, self.encrypted_password, setter)
        else:
            # Old framework password
            try:
                result = crypter.codeigniter.decode(self.encrypted_password) == raw_password
            except crypter.DecodeError:
                return False

            if result:
                setter(raw_password)

            return result

    def set_unusable_password(self):
        """ 有効なhash値にならないパスワードをセット """
        self.encrypted_password = hashers.make_password(None)

    def has_usable_password(self):
        """ 有効なパスワードか """
        return hashers.is_password_usable(self.encrypted_password)

    def get_full_name(self):
        raise NotImplementedError()

    def get_short_name(self):
        raise NotImplementedError()

    def _get_encrypted_password(self):
        return self.encrypted_password

    def _set_encrypted_password(self, value):
        self.encrypted_password = value

    password = property(_get_encrypted_password, _set_encrypted_password)
実行方法

テキトーにrunserverしてログインしてみてください

$ python manage.py runserver

うん、これでDjangoに移行できるかな?

稚拙なことしか書いてないですが、是非トライしてみてください。