search
LoginSignup
7

More than 3 years have passed since last update.

posted at

updated at

Organization

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

以下の気づきを残す.

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

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
What you can do with signing up
7