SQLite3 DBをファイルごとパスワード暗号化・復号する

以下の気づきを残す．

* Pythonの文字列暗号化は<b>ファイル自体にも効く</b>
* SQLite3のDBをファイルごと暗号化することは可能
* ~~文字列って掛け算できたの~~

# 背景
SQLite3には「パスワード暗号化機能」が無い．
欠点を補う方法はいくつか挙げられているが，「有料である」，「自力でのビルドが必要」など課題が多い．
[万年素人からGeekへの道](http://d.hatena.ne.jp/shinriyo/20130531/p1)
[SQLiteの暗号化](https://qiita.com/koorinonaka/items/300fbd4db1c0dc5fb574)

一方，Pythonには暗号化ライブラリpycryptoがあるため，活用の余地がある．
Pythonの暗号化において，文字列を暗号化・復号する例は多い．
[Pythonで暗号化と復号化](https://qiita.com/teitei_tk/items/0b8bae99a8700452b718)
[【Python】pycryptoで投げるたけでAES暗号化復号してくれる関数作った。](https://qiita.com/Azunyan1111/items/01a4df7a1c162dbb6f08)
[Pythonでの暗号化/復号化(AEC-DES,RSA)](http://yut.hatenablog.com/entry/20110207/1297070966)

しかし，<b>ファイルを暗号化する例</b>は見られない．
（どんなファイルも表現しようとすれば文字列だろとは言えるが）

そこで本稿では，pycryptoを用いてSQLite3のDBファイル自体をパスワード暗号化・復号できることを確かめ，例を示す．

「文字しか暗号化できないとかクソ」と嘆く人がいれば，ぜひ見て頂きたい．

# やったこと
* SQLite3でDBを構築し，データを確認する
* DBファイル(sample.db)をPython3のpycryptoでAES暗号化し，db.cipherとして暗号ファイル作成
* 暗号ファイル(db.cipher)をAES復号で元に戻し，sample.dbとして上書き
* 再びSQLite3で接続し，データが元通りであることを確認する

# ファイル構成
* cipher.py: 一連の処理をする
* AES_Cipher_SQLite.py: 暗号化・復号処理をする
* Database.py: DB処理をする

cipher.pyを実行する．cipher.pyがAES_Cipher_SQLite.pyやDatabase.pyを呼び出しながら処理を行う（なので同じディレクトリに配置すること）．

##注意点
* PyCharmではpycryptoのインストールができなかったので，(恐らく)同じ機能を持つpycryptodomeを使っている
>[Can't install pycrypto #263](https://github.com/dlitz/pycrypto/issues/263)  
Highly recommend NOT to use pycrypto. It is old and not maintained and contains many vulnerabilities. Use pycryptodome (https://github.com/Legrandin/pycryptodome) instead - it is compatible and up to date

* 16バイト刻みで云々という処理はpycryptoの仕様上起こっている．<b>データ量が16バイトの倍数でないと暗号化できない</b>
* ソースコードでは「文字数を16で割った余り」から不足文字数を割り出し，'_'で補完することで16バイトの倍数に修正している(1文字1バイトっぽい？)．例えば20文字なら不足分12文字を補う
* <b>SQLite3のDBファイルは文字コードがUTF16</b>だった．AESのパスワードとIVはUTF8で計算しているが，DBファイルだけUTF16用の別処理をしているのはそのせい
* ~~パスワードもIVも16バイト刻みに直さなきゃいけないのクッソ面倒~~


##ソースコード

```python:cipher.py
from AES_Cipher_SQLite import AES_Cipher_SQLite
from Database import Database

# DBファイル作成，データ取得
db = Database()
db.insertTest()
db.getAllAccounts()

# 送ったパスワードを暗号化・復号に用いる
cipherDB = AES_Cipher_SQLite('myPassWord')

# バイナリモードでDBファイル読み込み
with open("sample.db", "rb") as fileData:
    contents = fileData.read()  # DBファイルのバイナリ取得(エンコードはutf16)
print('===binary Data===')
print(contents)

# DBバイナリデータが16バイト刻みか確かめ，
# 不足していればバイト文字で補う
contents_shortageChars = cipherDB.getShortageChars(contents)
contents = cipherDB.get16ByteIncrementsData_fromUTF16(
    contents, contents_shortageChars
)
print('===after increment===')
print(contents)

# AES暗号化
encrypted = cipherDB.AES_Encryption(contents)
print('===after Encryption===')
print(encrypted)

# バイナリモードで暗号ファイルを作成
with open("db.cipher", "wb") as fileData:
    # 暗号化されたDBバイナリデータ(16バイト刻み)を書き込み
    fileData.write(encrypted)

# バイナリモードで暗号ファイルを読み込み
with open("db.cipher", "rb") as fileData:
    # DBファイルのバイナリ取得(エンコードはutf16)
    cipher_data = fileData.read()

# AES復号
decrypted = cipherDB.AES_Decryption(cipher_data)
print('===after Decryption===')
print(decrypted)

# DBバイナリデータ(16バイト刻み)を生のDBバイナリデータに修正
# 16バイト刻みにするために補った分のバイト文字を消去
decoded = cipherDB.getRawByteData(decrypted, contents_shortageChars)
print('===after Fix===')
print(decoded)

# バイナリモードでDBファイル(復号したほう)を上書き
with open("sample.db", "wb") as fileData:
    # 暗号化されたDBバイナリデータ(16バイト刻み)を書き込み
    fileData.write(decrypted)

# 再びDBデータ取得
db.getAllAccounts()
```

↓AES_Cipher_SQLite.pyは以下のバグを修正済み．

* getShortageChars()に引数として送ったデータが元々16バイト刻みだった場合，文字を追加する必要が無いのに16文字追加していた
* getRawByteData()に0を送った場合，そもそも文字が追加されていないので何もせずデータを返すべきなのに全文字を消去していた


```python:AES_Cipher_SQLite.py
# -*- coding: utf-8 -*-
from Cryptodome.Cipher import AES


class AES_Cipher_SQLite:
    def __init__(self, keyPhrase: str):
        # AES暗号化に使うパスワード
        keyPhrase_shortageChars = self.getShortageChars(keyPhrase)
        self.PassWord = self.get16ByteIncrementsData(
            keyPhrase, keyPhrase_shortageChars
        ).encode('utf8')
        
        # AES暗号化に使うIV
        ivPhrase = 'iv'
        ivPhrase_shortageChars = self.getShortageChars(ivPhrase)
        self.iv = self.get16ByteIncrementsData(
            ivPhrase, ivPhrase_shortageChars
        ).encode('utf8')
    
    # 16バイト刻みの文字数となるために何文字不足しているかを返す
    def getShortageChars(self, text: str) -> int:
        # 16バイト刻みなら不足数は0
        if (len(text) % 16) == 0:
            nearChars = len(text)
        else:
            nearChars = len(text) + (16 - (len(text) % 16))
        
        return nearChars - len(text)
    
    # 不足分したバイト数をstring文字で補い，16バイト刻みにして返す
    def get16ByteIncrementsData(self, text: str, shortage: int) -> str:
        return text + '_' * shortage
    
    # 不足したバイト数をバイト文字で補い，16バイト刻みにして返す
    def get16ByteIncrementsData_fromUTF16(
            self, text: bytes, shortage: int) -> bytes:
        return text + b'_' * shortage
    
    # バイト文字から補った分を消去して返す
    def getRawByteData(self, text: bytes, chars: int) -> bytes:
        # 元々補っていなかった場合は何もしない
        if chars == 0:
            return text
        else:
            return text[:-chars]
    
    # AESでDBバイナリデータ(16バイト刻み)を暗号化
    def AES_Encryption(self, targetData: bytes) -> bytes:
        obj = AES.new(self.PassWord, AES.MODE_CBC, self.iv)
        encryptedData = obj.encrypt(targetData)
        return encryptedData
    
    # AESでDBバイナリデータ(16バイト刻み)を復号
    def AES_Decryption(self, targetData: bytes) -> bytes:
        obj = AES.new(self.PassWord, AES.MODE_CBC, self.iv)
        decrypted = obj.decrypt(targetData)
        return decrypted
```


```python:Database.py
# -*- coding: utf-8 -*-
import sqlite3


class Database:
    def __init__(self):
        self.dbName = 'sample.db'
        self.accounts_table = 'Accounts'
        con = sqlite3.connect(self.dbName)
        
        # もしテーブルが無ければ作る
        # integerはprimary keyにすると自動でauto incrementになっている
        # アカウントテーブル=====================================
        con.execute("create table if not exists %s("
                    "id integer primary key,"  # 連番(主キー)
                    "user string not null,"  # ユーザ名
                    "password string not null)"  # パスワード
                    % self.accounts_table)
        
        con.commit()  # SQLを確定
        con.close()
    
    # 全データ表示
    def getAllAccounts(self):
        con = sqlite3.connect(self.dbName)
        cur = con.cursor()
        cur.execute("select name from sqlite_master where type='table'")
        print('============Tables ============')
        for row in cur:
            print(row[0])
        
        cur.execute("select * from %s" % (self.accounts_table))
        print('============Values in Accounts Table ============')
        for row in cur:
            print(row)
        con.close()
    
    # データ挿入
    def insertTest(self):
        con = sqlite3.connect(self.dbName)
        con.execute(
            "insert into %s(user,password) values("
            "'myUserFirst@mail.com',"
            "'password_myUserFirst')"
            % (self.accounts_table))
        con.execute(
            "insert into %s(user,password) values("
            "'secondName',"
            "'doublePass')"
            % (self.accounts_table))
        
        con.commit()
        con.close()
```



# 結果
バイナリデータが長すぎるので，データ文字列は最初と最後だけ表示している．

```console
============Tables ============
Accounts
============Values in Accounts Table ============
(1, 'myUserFirstf@mail.com', 'passwordaa_myUserFirst')
(2, 'secondName', 'doublePass')

===binary Data===
b'SQLite format 3\x00\x10...\x0079myUserFirstf@mail.compasswordaa_myUserFirst'

===after increment===
b'SQLite format 3\x00\x10...\x0079myUserFirstf@mail.compasswordaa_myUserFirst'

===after Encryption===
b'\xcd\xb6\t\x127\x00\xd3%\...\xfe&\xe7\x00\xbdOZ\x85cG|;wk\x194\x99\x01Px'

===after Decryption===
b'SQLite format 3\x00\x10...\x0079myUserFirstf@mail.compasswordaa_myUserFirst'

===after Fix===
b'SQLite format 3\x00\x10...\x0079myUserFirstf@mail.compasswordaa_myUserFirst'

============Tables ============
Accounts
============Values in Accounts Table ============
(1, 'myUserFirstf@mail.com', 'passwordaa_myUserFirst')
(2, 'secondName', 'doublePass')
```

* まずDBデータが表示される
* DBファイルのデータを表示(読める)
* ~~データを16バイト刻みにするため'_'を付けて補完~~
* SQLite3のDBファイルは元々16バイト刻みだった（補完されない）
* データを暗号化して表示(読めない)
* 復号して表示(また読める)
* ~~データから補完した余計な文字を抜き取る~~
* 元々16バイト刻みだったので変化なし
* DBデータを表示(できた)

どうやらSQLite3のDBデータは16バイト刻みを維持しているようなので，正直get16ByteIncrementsData_fromUTF16()とgetRawByteData()はいらない

#まとめ
* SQLite3の暗号化はわかりにくい
* ファイルごと暗号化すればいいんじゃね？
* Pythonの暗号化ライブラリ使用例は対象が文字列ばっかり
* ファイルごと暗号化できんじゃね？
* で　き　た