ForestEXOR Games Blog

引退プログラマーは最新技術に着いて行けない

SQLite使ってみる その3 vfsを使った暗号化編

今もプロの現場でゲームを作っておられる友人様から
「暗号化しろや」
というお言葉を頂きましたので必死こいて調べました。

SQLiteの暗号化を調べているとSQLCipherとかwxSqlite3とか別物を使った
有料やライセンスが面倒な物ばかり引っ掛かってきます。
しかしvfs機能を使えば普通のSQLiteでも暗号化はできるようです。
ちなみにSQLite自体はパブリックドメインの親切仕様です。

参考パクらせて頂いたサイト
c++ - Using SQLite with std::iostream - Stack Overflow



正直ほとんどぱくっていて自分でも安全の確証が無いので…
このプログラムによって引き起こされた一切の責任を負いません、自己責任でお願いします。

その1のようにいちいち個別で説明していると長くなるので一気に書きます。

注意点としては、実はopenからcloseまでずっとファイルを開きっぱなしというわけでは
無いようです、割りと頻繁にxOpenとxCloseメソッドが呼び出されているので頻繁にファイルを開閉しているみたいです。
中にはxOpenの後再びxOpenが呼び出されるパターンもあったのでdeleteせず再びnewして
いる可能性のある恐ろしい事が内部で起こっています。
しかし調べた所メモリリークはしていなかった不思議、
ですがそんな危険を避ける為にshared_ptrを使いましょう。
(僕は老害プログラマーな為ほとんど使った事がないのですがどうやら世間では普通に使うのが常識らしいですけど)

恐らくvfs.szOsFileでサイズ指定して使う場合はダウンキャストしている事から、
malloc的な物でメモリを確保しているのだと思われます。
なので独自実装の構造体のコンストラクタやデストラクタが呼び出されない為にどの
タイミングでメモリが開放されているのか不明…

暗号化に関して適当なので個々で独自に実装を変えてください。

#include <stdio.h>
#include <crtdbg.h>
#include "sqlite3.h"
#pragma comment( lib, "sqlite3-x86.dll" )

#include <memory>
#include <string>
#include <fstream>
#include <iostream>
#include <iterator>

// 恐らくvfsを使う時にユーザーがが独自に実装する必要がある構造体
struct SQLiteFile : sqlite3_file{
	std::shared_ptr<std::fstream>	pStream;
	int						iLockLevel;
};

int xClose( sqlite3_file* pFileBase ){
	::printf_s("xClose\n");
	auto pFile = static_cast<SQLiteFile*>(pFileBase);

	// 暗号化
	pFile->pStream->seekg( 0, std::ios_base::beg );
	pFile->pStream->seekp( 0, std::ios_base::beg );
	std::string str( (std::istreambuf_iterator<char>((*pFile->pStream))), std::istreambuf_iterator<char>() );
	for( size_t i(0), Size(str.size()) ; i < Size ; i++ ){
		str[i] ^= 0xAB;
	}
	// 暗号結果を上書き
	pFile->pStream->seekg( 0, std::ios_base::beg );
	pFile->pStream->seekp( 0, std::ios_base::beg );
	pFile->pStream->write( str.data(), str.size() );
	pFile->pStream->close();

	pFile->pStream = nullptr;
	return SQLITE_OK;
}

int xRead( sqlite3_file* pFileBase, void* buf, int iAmt, sqlite3_int64 iOfst ){
	::printf_s("xRead\n");
	auto pFile = static_cast<SQLiteFile*>(pFileBase);

	pFile->pStream->sync();
	pFile->pStream->seekg( iOfst, std::ios::beg );
	if( pFile->pStream->fail() ){
		pFile->pStream->clear();
		memset( static_cast<char*>(buf), 0, iAmt );
		return SQLITE_IOERR_SHORT_READ;
	}

	pFile->pStream->read( static_cast<char*>(buf), iAmt );

	const auto gcount = pFile->pStream->gcount();
	if( gcount < iAmt ){
		memset( static_cast<char*>(buf) + gcount, 0, static_cast<size_t>(iAmt - gcount) );
		return SQLITE_IOERR_SHORT_READ;
	}

	return SQLITE_OK;
}

int xWrite( sqlite3_file* pFileBase, const void* buf, int iAmt, sqlite3_int64 iOfst ){
	::printf_s("xWrite\n");
	auto pFile = static_cast<SQLiteFile*>(pFileBase);

	pFile->pStream->sync();
	pFile->pStream->seekp( iOfst, std::ios::beg );
	if( pFile->pStream->fail() ){
		pFile->pStream->clear();
		return SQLITE_IOERR_WRITE;
	}

	pFile->pStream->write( static_cast<const char*>(buf), iAmt );

	if( pFile->pStream->fail() ){
		pFile->pStream->clear();
		return SQLITE_IOERR_WRITE;
	}

	return SQLITE_OK;
}

int xTruncate( sqlite3_file* pFileBase, sqlite3_int64 size ){
	::printf_s("xTruncate\n");
	return SQLITE_OK;
}

int xSync( sqlite3_file* pFileBase, int flags ){
	::printf_s("xSync\n");
	auto pFile = static_cast<SQLiteFile*>(pFileBase);
	return pFile->pStream->sync();
}

int xFileSize( sqlite3_file* pFileBase, sqlite3_int64* pSize ){
	::printf_s("xFileSize\n");
	auto pFile = static_cast<SQLiteFile*>(pFileBase);

	(*pSize) = pFile->pStream->seekg( 0, std::ios::end ).tellg();

	if( pFile->pStream->fail() ){
		pFile->pStream->clear();
	}

	return SQLITE_OK;
}

int xLock( sqlite3_file* pFileBase, int iLevel ){
	::printf_s("xLock\n");
	auto pFile = static_cast<SQLiteFile*>(pFileBase);
	pFile->iLockLevel = iLevel;
	return SQLITE_OK;
}

int xUnlock( sqlite3_file* pFileBase, int iLevel ){
	::printf_s("xUnlock\n");
	auto pFile = static_cast<SQLiteFile*>(pFileBase);
	pFile->iLockLevel = iLevel;
	return SQLITE_OK;
}

int xCheckReservedLock( sqlite3_file* pFileBase, int* pResOut ){
	::printf_s("xCheckReservedLock\n");
	auto pFile = static_cast<SQLiteFile*>(pFileBase);
	(*pResOut) = (pFile->iLockLevel >= 1);
	return SQLITE_OK;
}

int xFileControl( sqlite3_file* pFileBase, int op, void* pArg ){
	::printf_s("xFileControl\n");
	auto pFile = static_cast<SQLiteFile*>(pFileBase);

	switch(op){
	case SQLITE_FCNTL_LOCKSTATE:
		(*reinterpret_cast<int*>(pArg)) = pFile->iLockLevel;
		break;
	case SQLITE_FCNTL_SIZE_HINT:
		break;
	case SQLITE_FCNTL_CHUNK_SIZE:
		break;
	case SQLITE_GET_LOCKPROXYFILE:	return SQLITE_ERROR;
	case SQLITE_SET_LOCKPROXYFILE:	return SQLITE_ERROR;
	case SQLITE_LAST_ERRNO:			return SQLITE_ERROR;
	}

	return SQLITE_OK;
}

int xSectorSize( sqlite3_file* ){
	::printf_s("xSectorSize\n");
	return 512;
}

int xDeviceCharacteristics( sqlite3_file* ){
	::printf_s("xDeviceCharacteristics\n");
	return (SQLITE_IOCAP_ATOMIC | SQLITE_IOCAP_SAFE_APPEND | SQLITE_IOCAP_SEQUENTIAL);
}

int xOpen( sqlite3_vfs* pVFS, const char* zName, sqlite3_file* pFileBase, int flags, int* pOutFlags ){
	::printf_s("xOpen\n");

	// ファイル操作関連のメソッドの定義
	static sqlite3_io_methods methods;
	methods.iVersion = 1;
	methods.xClose = &xClose;
	methods.xRead = &xRead;
	methods.xWrite = &xWrite;
	methods.xTruncate = &xTruncate;
	methods.xSync = &xSync;
	methods.xFileSize = &xFileSize;
	methods.xLock = &xLock;
	methods.xUnlock = &xUnlock;
	methods.xCheckReservedLock = &xCheckReservedLock;
	methods.xFileControl = &xFileControl;
	methods.xSectorSize = &xSectorSize;
	methods.xDeviceCharacteristics = &xDeviceCharacteristics;
	pFileBase->pMethods = &methods;

	if( nullptr != pOutFlags ){
		(*pOutFlags) = flags;
	}

	auto pFile = static_cast<SQLiteFile*>(pFileBase);
	pFile->pStream = std::shared_ptr<std::fstream>( new std::fstream() );
	pFile->iLockLevel = 0;

	// .dbファイルを開く
	pFile->pStream->open( zName, (std::fstream::in | std::fstream::out | std::fstream::binary) );
	if( true == pFile->pStream->is_open() ){
		// 復号化
		std::string str( (std::istreambuf_iterator<char>((*pFile->pStream))), std::istreambuf_iterator<char>() );
		for( size_t i(0), Size(str.size()) ; i < Size ; i++ ){
			str[i] ^= 0xAB;
		}
		// 復号結果を上書き
		pFile->pStream->seekg( 0, std::ios_base::beg );
		pFile->pStream->seekp( 0, std::ios_base::beg );
		pFile->pStream->write( str.data(), str.size() );
	}else{
		if( 0 == (flags & SQLITE_OPEN_CREATE) ){
			return SQLITE_CANTOPEN;
		}else{
			// CREATEフラグが立っていたら新規作成で開く
			std::ofstream ofs( zName, std::fstream::binary );
			if( false == ofs.is_open() ){
				return SQLITE_ERROR;
			}
			ofs.close();	// とりあえずファイルを作成
			// 改めて作ったファイルを開く
			pFile->pStream->open( zName, (std::fstream::in | std::fstream::out | std::fstream::binary) );
			if( false == pFile->pStream->is_open() ){
				return SQLITE_ERROR;
			}
		}
	}

	return SQLITE_OK;
}

int main(){
#if defined(DEBUG) | defined(_DEBUG)
	_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
#endif

	// vfsの作成
	sqlite3_vfs* pVFS = nullptr;
	pVFS = sqlite3_vfs_find( "win32" );	// デフォルトのVFSを取得

	// ほとんどのメソッドはデフォルトの物を使う
	sqlite3_vfs vfs;
	memset( &vfs, 0, sizeof(sqlite3_vfs) );
	vfs.iVersion = 3;	// Structure version number (currently 3)らしい
	vfs.szOsFile = sizeof(SQLiteFile);	// 自分で定義した構造体のサイズを入れる
	vfs.mxPathname = pVFS->mxPathname;
	vfs.zName = "myVFS";	// ここは任意
	vfs.pAppData = pVFS->pAppData;
	vfs.xOpen = &xOpen;		// ここは自分で定義
	vfs.xDelete = pVFS->xDelete;
	vfs.xAccess = pVFS->xAccess;
	vfs.xFullPathname = pVFS->xFullPathname;
	vfs.xDlOpen = pVFS->xDlOpen;
	vfs.xDlError = pVFS->xDlError;
	vfs.xDlSym = pVFS->xDlSym;
	vfs.xDlClose = pVFS->xDlClose;
	vfs.xRandomness = pVFS->xRandomness;
	vfs.xSleep = pVFS->xSleep;
	vfs.xCurrentTime = pVFS->xCurrentTime;
	vfs.xGetLastError = pVFS->xGetLastError;
	vfs.xCurrentTimeInt64 = pVFS->xCurrentTimeInt64;

	// vfsを登録
	::printf_s( "sqlite3_vfs_register\n" );
	int iErr = sqlite3_vfs_register( &vfs, 0 );
	if( SQLITE_OK != iErr ){
		::printf_s( "vfs register failed.\n" );
		::getchar();
		return -1;
	}

	// 以下は自作vfsが機能しているかの検証の為その2とほぼ同じです

	char* strDBName = "test.db";
	char* strErrMsg = nullptr;
	sqlite3* pDB = nullptr;

	// 読み取り書き込みモードで開く 読み取り専用の場合は「SQLITE_OPEN_READONLY」
	::printf_s( "sqlite3_open_v2\n" );
	iErr = ::sqlite3_open_v2( strDBName, &pDB, SQLITE_OPEN_READWRITE, "myVFS" );	// 任意で設定したvfs名
	if( SQLITE_OK != iErr ){
		// オープン失敗
		::printf_s( "DB open failed. 1\n%s\n", ::sqlite3_errmsg(pDB) );

		::printf_s( "sqlite3_close\n" );
		iErr = ::sqlite3_close( pDB );
		if( SQLITE_OK != iErr ){
			::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
			::getchar();
			return -1;
		}
		::printf_s( "DB close success.\n" );

		// .dbファイル新規作成で開く
		::printf_s( "sqlite3_open_v2\n" );
		iErr = ::sqlite3_open_v2( strDBName, &pDB, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, "myVFS" );
		if( SQLITE_OK != iErr ){
			::printf_s( "DB open failed. 2\n%s\n", ::sqlite3_errmsg(pDB) );
			::getchar();
			return -1;
		}else{
			::printf_s( ".db file created. DB open success.\n" );
		}

		// テーブル作成
		::printf_s( "sqlite3_exec create table\n" );
		iErr = ::sqlite3_exec( pDB, "create table hoge(id integer, time date)", nullptr, nullptr, &strErrMsg );
		if( SQLITE_OK != iErr ){
			// テーブル作成失敗
			::printf_s( "create table failed.\n" );
			::printf_s( "%s\n", strErrMsg );
			::sqlite3_free( strErrMsg );

			// DBを閉じる
			iErr = ::sqlite3_close( pDB );
			if( SQLITE_OK != iErr ){
				::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
				::getchar();
				return -1;
			}
			::printf_s( "DB close success.\n" );
			::getchar();
			return -1;
		}
		// テーブル作成成功
		::printf_s( "create table success.\n" );

		// レコード挿入
		::printf_s( "sqlite3_exec insert\n" );
		iErr = ::sqlite3_exec( pDB, "insert into hoge(id, time) values(1, CURRENT_TIME)", nullptr, nullptr, &strErrMsg );
		if( SQLITE_OK != iErr ){
			// 挿入失敗
			::printf_s( "%s\n", strErrMsg );
			sqlite3_free( strErrMsg );

			// DBを閉じる
			iErr = ::sqlite3_close( pDB );
			if( SQLITE_OK != iErr ){
				::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
				::getchar();
				return -1;
			}
			::printf_s( "DB close success.\n" );
			::getchar();
			return -1;
		}
		// 挿入成功
		::printf_s( "insert success. 1\n" );

		// レコード挿入その2
		::printf_s( "sqlite3_exec insert\n" );
		iErr = ::sqlite3_exec( pDB, "insert into hoge(id, time) values(2, CURRENT_TIME)", nullptr, nullptr, &strErrMsg );
		if( SQLITE_OK != iErr ){
			::printf_s( "%s\n", strErrMsg );
			sqlite3_free( strErrMsg );
			iErr = ::sqlite3_close( pDB );
			if( SQLITE_OK != iErr ){
				::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
				::getchar();
				return -1;
			}
			::printf_s( "DB close success.\n" );
			::getchar();
			return -1;
		}
		::printf_s( "insert success. 2\n" );
		
	}else{
		// 既存の.dbファイルの読み込みに成功
		::printf_s( "DB open success.\n" );

		// レコード更新
		::printf_s( "sqlite3_exec update\n" );
		iErr = sqlite3_exec( pDB, "update hoge set time = CURRENT_TIME where id = 2", nullptr, nullptr, &strErrMsg );
		if( SQLITE_OK != iErr ){
			// 更新失敗
			::printf_s( "%s\n", strErrMsg );
			sqlite3_free( strErrMsg );

			// DBを閉じる
			iErr = ::sqlite3_close( pDB );
			if( SQLITE_OK != iErr ){
				::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
				::getchar();
				return -1;
			}
			::printf_s( "DB close success.\n" );
			::getchar();
			return -1;
		}
		// 更新成功
		::printf_s( "update success. 1\n" );
	}

	// レコードの抽出と表示
	sqlite3_stmt* stmt = nullptr;	// ステートメント
	::printf_s( "sqlite3_prepare_v2 select\n" );
	iErr = ::sqlite3_prepare_v2( pDB, "select * from hoge", -1, &stmt, nullptr );
	if( SQLITE_OK != iErr ){
		// 抽出失敗
		::printf_s( "%s\n", ::sqlite3_errmsg(pDB) );
		::sqlite3_reset( stmt );
		::sqlite3_finalize( stmt );

		iErr = ::sqlite3_close( pDB );
		if( SQLITE_OK != iErr ){
			::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
			::getchar();
			return -1;
		}
		::printf_s( "DB close success.\n" );
		::getchar();
		return -1;

	}else{
		// 抽出成功
		::printf_s( "select success.\n" );
		for( ; SQLITE_ROW == ::sqlite3_step(stmt) ; ){	// step1回で1レコード取得
			::printf_s("+--------+-----------------+\n| ");
			// レコードの列数取得
			int iColMax = ::sqlite3_column_count(stmt);
			for( int i(0) ; i < iColMax ; i++ ){
				// 列名取得
				const char* name = sqlite3_column_name( stmt, i );
				// 列のデータ型取得
				int iColType = sqlite3_column_type( stmt, i );

				switch( iColType ){
				// INTEGER(int)
				case SQLITE_INTEGER:
					::printf_s( "%s = %d", name, sqlite3_column_int( stmt, i ) );
					break;
				// REAL(double)
				case SQLITE_FLOAT:
					::printf_s( "%s = %f", name, (float)sqlite3_column_double( stmt, i ) );
					break;
				// TEXT(string)
				case SQLITE_TEXT:
					::printf_s( "%s = %s", name, sqlite3_column_text( stmt, i ) );
					break;
				// NULL(nullptr)
				case SQLITE_NULL:
					break;
				// BLOB(不明)
				case SQLITE_BLOB:
					break;
				}

				::printf_s(" | ");
			}// for( int i(0) ; i < iColMax ; i++ )
			::printf_s("\n");
		}// for( ; SQLITE_ROW == ::sqlite3_step(stmt) ; )
		::printf_s("+--------+-----------------+\n");
	}
	// stmtの後始末
	::sqlite3_reset( stmt );
	::sqlite3_finalize( stmt );

	// レコードの削除
	// 削除用に1つレコードを追加
	::printf_s( "sqlite3_exec insert\n" );
	iErr = ::sqlite3_exec( pDB, "insert into hoge(id, time) values(3, CURRENT_TIME)", nullptr, nullptr, &strErrMsg );
	if( SQLITE_OK != iErr ){
		::printf_s( "%s\n", strErrMsg );
		sqlite3_free( strErrMsg );
		iErr = ::sqlite3_close( pDB );
		if( SQLITE_OK != iErr ){
			::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
			::getchar();
			return -1;
		}
		::printf_s( "DB close success.\n" );
		::getchar();
		return -1;
	}
	::printf_s( "insert success.\n" );

	// 削除
	::printf_s( "sqlite3_exec delete\n" );
	iErr = sqlite3_exec( pDB, "delete from hoge where id = 3", nullptr, nullptr, &strErrMsg );
	if( SQLITE_OK != iErr ){
		// 削除失敗
		::printf_s( "%s\n", strErrMsg );
		sqlite3_free( strErrMsg );
		iErr = ::sqlite3_close( pDB );
		if( SQLITE_OK != iErr ){
			::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
			::getchar();
			return -1;
		}
		::printf_s( "DB close success.\n" );
		::getchar();
		return -1;
	}else{
		// 削除成功
		::printf_s( "delete success.\n" );
	}

	// 後始末
	::printf_s( "sqlite3_close\n" );
	iErr = ::sqlite3_close( pDB );
	if( SQLITE_OK != iErr ){
		::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
		::getchar();
		return -1;
	}
	::printf_s( "DB close success.\n" );

	::getchar();
	return 0;
}

次回はアーカイブファイル内にある.dbファイルを読み込む方法を書きたいと思います。