LoginSignup
9
9

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-07-05

以下の気づきを残す.

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

背景

SQLite3には「パスワード暗号化機能」が無い.
欠点を補う方法はいくつか挙げられているが,「有料である」,「自力でのビルドが必要」など課題が多い.
万年素人からGeekへの道
SQLiteの暗号化

一方,Pythonには暗号化ライブラリpycryptoがあるため,活用の余地がある.
Pythonの暗号化において,文字列を暗号化・復号する例は多い.
Pythonで暗号化と復号化
【Python】pycryptoで投げるたけでAES暗号化復号してくれる関数作った。
Pythonでの暗号化/復号化(AEC-DES,RSA)

しかし,ファイルを暗号化する例は見られない.
(どんなファイルも表現しようとすれば文字列だろとは言えるが)

そこで本稿では,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

    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の仕様上起こっている.データ量が16バイトの倍数でないと暗号化できない

  • ソースコードでは「文字数を16で割った余り」から不足文字数を割り出し,'_'で補完することで16バイトの倍数に修正している(1文字1バイトっぽい?).例えば20文字なら不足分12文字を補う

  • SQLite3のDBファイルは文字コードがUTF16だった.AESのパスワードとIVはUTF8で計算しているが,DBファイルだけUTF16用の別処理をしているのはそのせい

  • パスワードもIVも16バイト刻みに直さなきゃいけないのクッソ面倒

ソースコード

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を送った場合,そもそも文字が追加されていないので何もせずデータを返すべきなのに全文字を消去していた
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
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()

結果

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

============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の暗号化ライブラリ使用例は対象が文字列ばっかり
  • ファイルごと暗号化できんじゃね?
  • で き た
9
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
9