Python 暗号化・復号化

# はじめに
MH ソフトウェア & サービスが開発・公開している、Webアプリケーションサーバ AiRの便利かな?と思われるコードの紹介です。
Webアプリケーションサーバ AiRはPython、Tornado(Webフレームワーク、Webサーバ)、JavaScript、その他のモジュールで構成されています。
***
Webアプリケーションサーバ AiRは、httpとhttps(SSL)で稼働させることが可能です。
https(SSL)では通信内容は暗号化されると言われています。
AiRをhttp通信で使用する場合もあります。

そこで、AiRはPythonとJavaScriptで共通の暗号化・復号化を実現し、JavaScriptからのAjaxデータを暗号化して通信する方法を実現しています。

まず、"MH ソフトウェア & サービス"という文字列を暗号化してみます。
```
# 暗号化キーの長さは16文字です
ENCRYPT_KEY = "1234567890123456"

NORMAL_STR = "MH ソフトウェア & サービス"
encrypted_str = encrypt(NORMAL_STR, ENCRYPT_KEY)
print(f"{encrypted_str=}")
```
```
encrypted_str='beb80c687e1572b9893299376106b08a83029968c4c1b8a28fd8785d76f875e4bb7bcf35289b598316363b6b5306b205'
```
次に暗号化された文字列を復号化してみます。
```
dec = Decrypt()
decrypted_str = dec.core(encrypted_str, ENCRYPT_KEY)
print(f"{decrypted_str=}")
```
```
decrypted_str='MH ソフトウェア & サービス'
```

次に"name"という文字列を暗号化しdictのキーに、さらに暗号化された"MH ソフトウェア & サービス"という文字列を値にしたdictをjson.dumpsで文字列にし、暗号化します。
```
# 暗号化した文字列をjson形式でキーと値に入れて暗号化します
encrypted_name = encrypt("name", ENCRYPT_KEY)
print(f"{encrypted_name=}")
encrypted_json = encrypt(json.dumps({encrypted_name: encrypted_str}), ENCRYPT_KEY)
print(f"{encrypted_json=}")
```
```
encrypted_name='27126826c77d8ebac9b982928795f38a'
encrypted_json='ce7bb1ac32d66c4252dda79d7fc8735129ed1471ab1f9a39fa770ebb6e9bfd289a55c6a7bd0a979ec0980175fb5d13a2be30c74a58ddb8479842fa5f20a069ed3b1d96fbf74d556b00b14d94b088f0b95eabdc2167d766aa52fb316abc5de8551a59c835f52e8b2ccd871d5f3483a581f9763b8ace30a4ffd76aa968ed746fff26fb0b7f98b04b42d0a7e9961abf5556'
```

暗号化されたjson文字列を復号化すると、dict形式に戻ります。
```
decrypted_json = dec.core(encrypted_json, ENCRYPT_KEY)
print(type(decrypted_json), f"{decrypted_json=}")
# キーも値も復号化されます
```
```
<class 'dict'> decrypted_json={'name': 'MH ソフトウェア & サービス'}
```

下記が今回のサンプルのPythonスクリプトです。実際にはAiR内に、"fx"というモジュールに含まれています。

コードが長くてすいません。
```
"""
暗号化・復号化サンプル
"""
import binascii
import copy
import datetime
import json
from typing import (
    Any,
    overload,
    Union,
)

from Crypto.Cipher import AES
from Crypto.Util.Padding import (
    pad,
    unpad,
)
import numpy as np


def encrypt(value: Any, encrypt_key: str) -> str:
    """Encrypt

    :sig: (Any, str) -> str
    """
    result = value
    if len(encrypt_key) != 16:
        return result

    if value == "":
        return result

    if any([
        isinstance(value, dict),
        isinstance(value, list),
    ]):
        _value = copy.deepcopy(value)
        result = encrypt(json.dumps(_value, default=json_default), encrypt_key)
    else:
        _key = encrypt_key.encode()
        _cipher = AES.new(_key, AES.MODE_CBC, _key)  # type: ignore
        try:
            _encrypted = _cipher.encrypt(pad(value.encode('utf-8'), 16))  # type: ignore
        except  AttributeError:
            pass

        try:
            result = binascii.hexlify(_encrypted).decode()  # type: ignore
        except UnboundLocalError:
            pass

    return result


def json_default(value: Any) -> Any:
    """Return default date format of json.

    :sig: (Any) -> Any
    """
    result = value
    if isinstance(value, (datetime.datetime, datetime.date)):
        result = value.isoformat()
    elif isinstance(value, np.int64):  # type: ignore
        result = int(value)  # type: ignore
    elif isinstance(value, np.bool_):
        result = bool(value)
    elif isinstance(value, np.ndarray):
        result = list(value)  # type: ignore
    #raise TypeError(repr(value) + " is not JSON serializable")
    return result


# pylint: disable=too-few-public-methods
class Decrypt():
    """
    Decrypt class

    Decryptクラスは、受け取った暗号化されたデータの型を返します。
    json.dumps()でjson文字列の暗号化された文字列は、再帰的に
    """
    @overload
    def core(self, encrypted: dict[str, Any], password: str) -> dict[str, Any]:
        """Decrypt. If decrypted value is list, decrypt inside.

        :sig: (dict[str, Any], str) -> dict[str, Any]
        """

    @overload
    def core(self, encrypted: list[Any], password: str) -> list[Any]:
        """Decrypt. If decrypted value is list, decrypt inside.

        :sig: (list[Any], str) -> list[Any]
        """

    @overload
    def core(self, encrypted: str, password: str) -> str:
        """Decrypt. If decrypted value is list, decrypt inside.

        :sig: (str, str) -> str
        """

    #pylint: disable-next=too-many-branches
    def core(
        self,
        encrypted: Any,
        password: str
    ) -> Union[dict[str, Any], list[Any], str]:
        """Decrypt. If decrypted value is list or dict, decrypt inside.

        If modify this function, modify project://fx/_.js#258 #df473

        :sig: (Any, str) -> Any
        """
        result: Any = encrypted
        if not self._decryptable(encrypted, password):
            if isinstance(result, str):
                try:
                    result = json.loads(result)
                except json.decoder.JSONDecodeError:
                    pass
            return result

        # NOTE: Check encrypted is list or dict?
        if isinstance(encrypted, dict) | isinstance(encrypted, list):
            return self._object(encrypted, password)

        # NOTE: If encrypted is json string, loads to dict or list or number.
        try:
            encrypted = json.loads(encrypted)
        except json.decoder.JSONDecodeError:
            pass

        try:
            _text = binascii.unhexlify(encrypted)
        except binascii.Error:
            return result
        except ValueError:
            # NOTE: "string argument should contain only ASCII characters"
            # at Japanese charactor.
            return result

        _key = password.encode()
        _cipher = AES.new(_key, AES.MODE_CBC, _key)  # type: ignore

        decrypted: Any = None
        try:
            decrypted = unpad(_cipher.decrypt(_text), 16).decode('utf-8')  # type: ignore
        except ValueError:
            pass

        # NOTE: Check decrypted is list or dict?
        if decrypted is not None:
            #self._redecrypt()
            try:
                decrypted_loads = json.loads(decrypted)
            except json.decoder.JSONDecodeError:
                result = decrypted
            else:
                if isinstance(decrypted_loads, dict):
                    result = self._dict(decrypted_loads, password)
                elif isinstance(decrypted_loads, list):
                    result = self._list(decrypted_loads, password)
                else:
                    result = decrypted_loads

        return result

    def _decryptable(self, encrypted: Any, password: str) -> bool:
        """Retrun encryptable.

        :sig: (Any, str) -> bool
        """
        result = False
        if any([
            encrypted is None,
            len(password) != 16,
            isinstance(encrypted, bool),
            isinstance(encrypted, complex),
            isinstance(encrypted, float),
            isinstance(encrypted, datetime.datetime),
            isinstance(encrypted, int),
        ]):
            result = False
        elif any([
            isinstance(encrypted, dict),
            isinstance(encrypted, list),
        ]):
            result = True
        elif len(encrypted) == 0:
            result = False
        else:
            try:
                _loads = json.loads(encrypted)
                # NOTE: If loads is number, can't decrypt.
                if any([
                    isinstance(_loads, complex),
                    isinstance(_loads, float),
                    isinstance(_loads, int),
                ]):
                    result = False
            except json.decoder.JSONDecodeError:
                result = True

        return result

    def _dict(self, encrypted: Any, password: str) -> dict[str, Any]:
        """Decrypt. If decrypted value is dict, decrypt inside.

        :sig: (Any, str) -> dict[str, Any]
        """
        result: dict[str, Any] = {}
        for item in encrypted.items():
            result.update({self.core(item[0], password): self.core(item[1], password)})
        return result

    def _list(self, encrypted: Any, password: str) -> list[Any]:
        """Decrypt. If decrypted value is list, decrypt inside.

        :sig: (Any, str) -> list[Any]
        """
        result = encrypted
        for i, _list in enumerate(result):
            result[i] = self.core(_list, password)
        return result

    def _object(
        self,
        encrypted: Any,
        password: str
    ) -> Union[dict[str, Any], list[Any]]:
        """Decrypt. If decrypted value is list, decrypt inside.

        :sig: (Any, str) -> Union[dict[str, Any], list[Any]]
        """
        result: Any = None
        if isinstance(encrypted, dict):
            result = self._dict(encrypted, password)
        if isinstance(encrypted, list):
            result = self._list(encrypted, password)
        return result

# 暗号化キーの長さは16文字です
ENCRYPT_KEY = "1234567890123456"
# 記号も使えます
# ENCRYPT_KEY =  "#<?_'g(d%Z&9f+[!"

NORMAL_STR = "MH ソフトウェア & サービス"
encrypted_str = encrypt(NORMAL_STR, ENCRYPT_KEY)
print(f"{encrypted_str=}")

dec = Decrypt()
decrypted_str = dec.core(encrypted_str, ENCRYPT_KEY)
print(f"{decrypted_str=}")

# 暗号化した文字列をjson形式でキーと値に入れて暗号化します
encrypted_name = encrypt("name", ENCRYPT_KEY)
print(f"{encrypted_name=}")
encrypted_json = encrypt(json.dumps({encrypted_name: encrypted_str}), ENCRYPT_KEY)
print(f"{encrypted_json=}")

decrypted_json = dec.core(encrypted_json, ENCRYPT_KEY)
print(type(decrypted_json), f"{decrypted_json=}")
# キーも値も復号化されます
```

***
[Webアプリケーションサーバ AiR](https://qiita.com/MH-SS/items/7a99e799e4988fdad2cc)
***
[弊社webページ: AiR](https://mhss.server-on.net/?f401c=Index&ae504=air)