python(外部モジュールなし)による暗号化・復号化簡易プログラム

# pip不要(外部モジュールなし)、import不要、フリーの暗号化・復号化プログラム
pythonのみで動作します。

:::note warn
頑強ではありません。総当たり法でコロッと逝きます。
最後に挙げた総当たり法に対する改良版はちょっとだけ抵抗します。
高度な暗号化が必要ならばcryptography等のきちんとした暗号化ライブラリを使ってください。
:::

そう簡単には破れることもない(改良版の方)コンパクトな暗号化・復号化を利用したい人向けです。以下の2点が理由で作りました。

<ul>
<li>暗号化ライブラリのインストールをしたくない</li>
できるだけ標準ライブラリでないものをpipをしなくても済むプログラムを作るため。動作環境の安定化。
<li>暗号化ライブラリのimportをしたくない</li>
配布用の実行ファイル(pyinstallerによりコンパイルしたもの)の巨大化を回避するため。一瞬で起動する実行ファイルが欲しかった。
</ul>

以下の自作プログラムで使っています。OpenAIのAPIキーを暗号化・復号化するために利用していますが、日本語が含まれていても動作します。平文と暗号文は'utf-8'です。

https://qiita.com/ngktakanobu/items/fe41d1f6f492a118c77f

プログラム開発の際は「奥村晴彦、改訂新版C言語による標準アルゴリズム事典　技術評論社」のcrypt.cを参考にしています。元のプログラムも記事のプログラムもフリーソフトです。可読文字列と数値の配列との相互変換の部分はオリジナルです(この部分はpythonのコードとしては非効率な気がします)。

# コピペ用のソース
改良版は2行だけの変更です。こちらの解説は無駄にはなりません。

https://gist.github.com/TakanobuSaito/e81216c2e39920c10ac047d9e65e6b52

# 改変可能な部分
acryptseedを変更することで別の乱数系列を使うことになるので暗号化可読文字列も変わります。ただし暗号化・復号化で同じacryptseedを指定する必要があります。
code62strの要素を入れ替えたり変更したりすることが可能。code62strの64の要素はユニークである必要があります。

# 呼び出し例
```python
estr1 = cryptencode('本日は晴天なり', 3276)
estr2 = cryptencode('A', 3276)
estr3 = cryptencode('AB', 3276)
estr4 = cryptencode('ABC', 3276)
print(cryptdecode(estr1, 3276))
print(cryptdecode(estr2, 3276))
print(cryptdecode(estr3, 3276))
print(cryptdecode(estr4, 3276))
```

# 暗号化プログラム解説
標準ライブラリのrandomだけimportする必要があります。
まずstr1.encodeでバイト列に変換します。random.seed(acryptseed)で乱数系列を指定して、for文の中でバイト列を、random.randint(0,255)で発生した0～255のバイト値で排他的論理和演算します。(len1 & 256)とも排他的論理和をしているのは、同じ平文が長さによって異なる暗号化文字列となるようにする工夫です。
　random.seedで同じ整数値を指定すると、同じ乱数系列が発生します。また排他的論理和はビット反転なので、もう一回反転すると元に戻ります。そのことを利用して復号化します。
```python
    str1byte = str1.encode('utf-8')
    len1 = len(str1byte)
    random.seed(acryptseed)
    str1crypt = []
    for i in range(len1):
         str1crypt.append(str1byte[i] ^ random.randint(0,255))
```
8bitの暗号化バイト列は、そのままでは文字列にはなりません。8bitバイト列を6bit単位で切り出して、可読文字列に変換します。アルファベット大文字26、アルファベット小文字が26、数字が10、あとは適当な記号'-'と'='を追加して合計64なので6bitでちょうど足ります。
　可読文字列への変換部分はpythonプログラムとしては非効率な気がします。
```python
    astr2 = ""
    for i in range(len1 * 8):
        j = int(i / 8)
        k = i - j * 8
        if i % 6 == 0:
            if(i > 0):
                astr2 += code62str[bit]
            if (str1crypt[j] & (0x80 >> k)) != 0:
                bit = 0x20
            else:
                bit = 0
        else:
            if (str1crypt[j] & (0x80 >> k)) != 0:
                bit |= 0x20 >> (i % 6)
    astr2 += code62str[bit]
    return astr2
```


# 復号化プログラム解説
標準ライブラリのrandomだけimportする必要があります。
まず、暗号化した可読文字列を6bit整数配列に変換します。ここでは、6bit数字列を可読文字列に変換する表であるcode62str表から自動的に作った逆変換の表str2code6を参照しています。
```python
    # 文字列を6bit整数配列にする
    len1 = len(str1)
    random.seed(acryptseed)
    code6 = []
    for i in range(len1):
        code6.append(str2code6[str1[i]])
```
6bit数字列を8bit数字列に再編成します。
```python
    # 6bit整数配列を8bit整数配列(バイト列)に変換する
    code8 = []
    bit = 0
    k = 0
    for i in range(len1):
        for j in range(6):
            if k % 8 == 0:
                if (i != 0 or j != 0):
                    code8.append(bit)
                if (code6[i] & (0x20 >> j)) != 0:
                    bit = 0x80 >> (k % 8)
                else:
                    bit = 0
            else:
                if (code6[i] & (0x20 >> j)) != 0:
                    bit |= (0x80 >> (k % 8))
            k += 1
    if(k % 8 == 0):
        code8.append(bit)
    len8 = len(code8)
```
最後に、バイト列を暗号化(encode)時と同じ乱数系列を排他的論理和することで元の平文を得て返します。
暗号化とは逆順で平文の長さlen8を使った(len8 % 256)との排他的論理和を取っています。
```python
    code8decode = []
    for i in range(len8):
        code8decode.append(code8[i] ^ random.randint(0,255))
    return  bytes(code8decode).decode('utf-8')
```
以上、簡易暗号化・復号化プログラムでした。

総当たり法にとても弱いので多少改良したバージョンを以下に示します。排他的論理和演算の部分を変更しています。

## 改良版暗号化の排他的論理和演算
平文のバイト列の長さと排他的論理和を行うことで、総当たり法がやりにくくなっています。
```python
        str1crypt.append((str1byte[i] ^ random.randint(0,255)) ^ (len1 % 256))
```

## 改良版復号化の排他的論理和演算
暗号化と同じように平文のバイト列の長さと排他的論理和演算を行っています。演算の順序に注意してください。

```python
        code8decode.append((code8[i] ^ (len8 % 256)) ^ random.randint(0,255))
```

## 改良版のソース

https://gist.github.com/TakanobuSaito/3360af0ea35d43d47722c3ef147b878c

## 呼び出し例
以下の呼び出し例を新旧バージョンで比べて見ると、改良の効果が分かります。
```python
estr2 = cryptencode('ABC', 3276)
estr3 = cryptencode('ABCD', 3276)
estr4 = cryptencode('ABCDE', 3276)

print(estr2)
print(estr3)
print(estr4)

print(cryptdecode(estr2, 3276))
print(cryptdecode(estr3, 3276))
print(cryptdecode(estr4, 3276))
```
比較表
<table>
<tr><th>平文</th><th>元のバージョンの暗号化文字列</th><th>改良版の暗号化文字列</th></tr>
<tr><td>ABC</td><td>TPTU</td><td>TBHX</td></tr>
<tr><td>ABCD</td><td>TPTUBa</td><td>SP9YAa</td></tr>
<tr><td>ABCDE</td><td>TPTUBam</td><td>S55ZAqy</td></tr>
</table>