wxSQLite3 암호화 방식 분석
2025년 2월 25일
ref #
개요 #
wxSQLite3은 wxWidgets 라이브러리를 사용하는 애플리케이션 위에서 SQLite3를 쉽게 사용할 수 있도록 도와주며 암호화 기능까지 제공하기 위한 오픈소스 SQLite3 C++ 래퍼이다.
과거(2019 이전)에는 wxWidget 라이브러리와 의존성이 너무 강했기 때문에 많은 개발자들이 이 암호화 래퍼를 SQLCipher처럼 의존성 없는 별도의 버전으로 요청했고, 그 버전이 SQLite3MultipleCiphers 프로젝트가 된 것이다.
1wxSQLite3Database db;
2db.Open(wxT("database.db"));
3db.ExecuteUpdate(wxT("CREATE TABLE test (id INTEGER);"));
코드 분석 #
sqlite에서는 전체 소스코드를 병합한 amalgamation(융합이라는 뜻) 코드를 따로 만들어서 배포하고 있는데, SQLite3MultipleCiphers 프로젝트는 이 파일을 가지고 암호화 관련 코드를 조금 추가해서 빌드하는 프로젝트인 것이다.
sqlite.c에 패치된 코드는 sqlite3patched.c 파일에서 볼 수 있다.
패치는 프로젝트에서 scripts 폴더 안의 patchsqlite3.sh 스크립트로 자동화 되어있다.
1#!/bin/sh
2# Generate patched sqlite3.c from SQLite3 amalgamation and write it to stdout.
3# Usage: ./script/patchsqlite3.sh sqlite3.c >sqlite3patched.c
4
5INPUT="$([ "$#" -eq 1 ] && echo "$1" || echo "sqlite3.c")"
6if ! [ -f "$INPUT" ]; then
7 echo "Usage: $0 <SQLITE3_AMALGAMATION>" >&2
8 echo " e.g.: $0 sqlite3.c" >&2
9 exit 1
10fi
11
12die() {
13 echo "[-]" "$@" >&2
14 exit 2
15}
16
17# 1) Intercept VFS pragma handling
18# 2) Add handling of KEY parameter in ATTACH statements
19sed 's/sqlite3_file_control\(.*SQLITE_FCNTL_PRAGMA\)/sqlite3mcFileControlPragma\1/' "$INPUT" \
20 | sed '/\#endif \/\* SQLITE3\_H \*\//a \ \n\/\* Function prototypes of SQLite3 Multiple Ciphers \*\/\nSQLITE_PRIVATE int sqlite3mcCheckVfs(const char*);\nSQLITE_PRIVATE int sqlite3mcFileControlPragma(sqlite3*, const char*, int, void*);\nSQLITE_PRIVATE int sqlite3mcHandleAttachKey(sqlite3*, const char*, const char*, sqlite3_value*, char**);\nSQLITE_PRIVATE int sqlite3mcHandleMainKey(sqlite3*, const char*);\ntypedef struct PgHdr PgHdrMC;\nSQLITE_PRIVATE void* sqlite3mcPagerCodec(PgHdrMC* pPg);\ntypedef struct Pager PagerMC;\nSQLITE_PRIVATE int sqlite3mcPagerHasCodec(PagerMC* pPager);\nSQLITE_PRIVATE void sqlite3mcInitMemoryMethods();\nSQLITE_PRIVATE int sqlite3mcIsBackupSupported(sqlite3*, const char*, sqlite3*, const char*);\nSQLITE_PRIVATE void sqlite3mcCodecGetKey(sqlite3* db, int nDb, void** zKey, int* nKey);' \
21 | sed '/\#define MAX\_PATHNAME 512/c #if SQLITE3MC\_MAX\_PATHNAME \> 512\n#define MAX_PATHNAME SQLITE3MC\_MAX\_PATHNAME\n#else\n#define MAX_PATHNAME 512\n#endif' \
22 | sed '/pData = pPage->pData;/c \ if( (pData = sqlite3mcPagerCodec(pPage))==0 ) return SQLITE_NOMEM_BKPT;' \
23 | sed '/pData = p->pData;/c \ if( (pData = sqlite3mcPagerCodec(p))==0 ) return SQLITE_NOMEM;' \
24 | sed '/sqlite3_free_filename( zPath );/i \\n \/\* Handle KEY parameter. \*\/\n if( rc==SQLITE_OK ){\n rc = sqlite3mcHandleAttachKey(db, zName, zPath, argv[2], &zErrDyn);\n }' \
25 | sed '/\*ppVfs = sqlite3_vfs_find(zVfs);/i \ \/\* Check VFS. \*\/\n sqlite3mcCheckVfs(zVfs);\n' \
26 | sed '/sqlite3_free_filename(zOpen);/i \\n \/\* Handle encryption related URI parameters. \*\/\n if( rc==SQLITE_OK ){\n rc = sqlite3mcHandleMainKey(db, zOpen);\n }' \
27 | sed '/^ if( sqlite3PCacheIsDirty(pPager->pPCache) ) return 0;/a \ if( sqlite3mcPagerHasCodec(pPager) != 0 ) return 0;' \
28 | sed '/^ }else if( USEFETCH(pPager) ){/c \ }else if( USEFETCH(pPager) && sqlite3mcPagerHasCodec(pPager) == 0 ){' \
29 | sed '/^ if( rc!=SQLITE_OK ) memset(&mem0, 0, sizeof(mem0));/a \\n \/\* Initialize wrapper for memory management.\*\/\n if( rc==SQLITE_OK ) {\n sqlite3mcInitMemoryMethods();\n }\n' \
30 | sed '/Lock the source database handle./i \ \/\* Check whether databases are compatible with backup \*\/\n if (!sqlite3mcIsBackupSupported(pSrcDb, zSrcDb, pDestDb, zDestDb)){\n sqlite3ErrorWithMsg(pDestDb, SQLITE_ERROR, \"backup is not supported with incompatible source and target databases\");\n return NULL;\n }\n' \
31 | sed '/nRes = sqlite3BtreeGetRequestedReserve(pMain)/a \\n \/\* A VACUUM cannot change the pagesize of an encrypted database. \*\/\n if( db->nextPagesize ){\n extern void sqlite3mcCodecGetKey(sqlite3*, int, void**, int*);\n int nKey;\n char *zKey;\n sqlite3mcCodecGetKey(db, iDb, (void**)&zKey, &nKey);\n if( nKey ) db->nextPagesize = 0;\n }' \
- 함수 치환: sqlite3_file_control → sqlite3mcFileControlPragma
- 함수 프로토타입 삽입: sqlite3mc… 함수들을 헤더 끝부분에 선언
- Pager 버퍼를 암복호화: pData에 저장할때 sqlite3mcPagerCodec 호출하여 암복호화 후 저장
- VFS 호환성 체크: sqlite3mcCheckVfs 함수 호출 추가
이외에도 여러 암복호화 관련 안정성, 호환성을 보장하기 위한 코드 추가
초기화 #
SQLite는 SQLITE_OMIT_AUTOINIT(자동초기화를 하지않는 옵션)이 정의되지 않았을때 라이브러리 함수를 호출하면 어떤 함수든 내부에서 자동으로 초기화 함수 sqlite3_initialize 를 호출하게된다.
sqlite3 에서는 SQLITE_EXTRA_INIT 을 이용해서 초기화 함수 하나를 등록할 수 있다.
1// sqlite3mc.c
2// sqlite3mc 초기화 함수를 등록
3#define SQLITE_EXTRA_INIT sqlite3mc_initialize
4
5// sqlite3pathced.c
6SQLITE_API int sqlite3_initialize(void){
7 // call sqlite3MallocInit() -> sqlite3mcInitMemoryMethods();
8
9// sqlite3mc.c에서 SQLITE_EXTRA_INIT을 정의해뒀기 때문에 실행된다.
10#ifdef SQLITE_EXTRA_INIT
11 if( bRunExtraInit ){
12 int SQLITE_EXTRA_INIT(const char*);
13 rc = SQLITE_EXTRA_INIT(0); // sqlite3mc_initialize(NULL);
14 }
15#endif
16 return rc;
17}
- sqlite3mcInitMemoryMethods: SECURE_MEMORY 를 사용하겠다고 세팅이 되면 보안을 위한 메모리 관리 함수들을 등록하는 것이다.
- sqlite3mc_initialize: 컴파일 옵션에 따라 암복호화 모듈이 추가된다. (기본적으로 모든 암호모듈이 세팅됨)
모듈 등록이 완료되면 multicipher vfs 를 디폴트 vfs로 등록한다.
1SQLITE_PRIVATE int sqlite3mc_initialize(const char* arg)
2{
3 int rc = sqlite3mcInitCipherTables();
4#if HAVE_CIPHER_AES_256_CBC
5 if (rc == SQLITE_OK)
6 rc = sqlite3mcRegisterCipher(&mcAES256Descriptor, mcAES256Params, (CODEC_TYPE_AES256 == CODEC_TYPE));
7#endif
8#if HAVE_CIPHER_CHACHA20
9 if (rc == SQLITE_OK)
10 rc = sqlite3mcRegisterCipher(&mcChaCha20Descriptor, mcChaCha20Params, (CODEC_TYPE_CHACHA20 == CODEC_TYPE));
11#endif
12// ...
13// 암호화 말고도 zip 파일 모듈 같은 확장모듈도 등록한다.
14#ifdef SQLITE_ENABLE_ZIPFILE
15 if (rc == SQLITE_OK)
16 rc = sqlite3_auto_extension((void(*)(void)) sqlite3_zipfile_init);
17#endif
18 return rc;
19
20 // 모듈 등록이 완료되면 multicipher vfs 를 디폴트 vfs로 등록한다.
21 if (rc == SQLITE_OK)
22 {
23 rc = sqlite3mc_vfs_create(NULL, 1);
24 }
25 return rc;
26}
예시. wxaes256.c #
mcAES256Descriptor 는 전역 상수로 이미 암복호화에 사용되는 세트들이 초기화 되어있다.
위의 초기화 과정에서 sqlite3mcRegisterCipher 의 첫번째 인자로 전달되며 정상 초기화가 완료됐는지 검사하고 globalCodecDescriptorTable 에 사용 가능한 코덱중 하나로 등록된다.
1typedef struct _CipherDescriptor
2{
3 const char* m_name;
4 AllocateCipher_t m_allocateCipher;
5 FreeCipher_t m_freeCipher;
6 CloneCipher_t m_cloneCipher;
7 GetLegacy_t m_getLegacy;
8 GetPageSize_t m_getPageSize;
9 GetReserved_t m_getReserved;
10 GetSalt_t m_getSalt;
11 GenerateKey_t m_generateKey;
12 EncryptPage_t m_encryptPage;
13 DecryptPage_t m_decryptPage;
14} CipherDescriptor;
15
16SQLITE_PRIVATE const CipherDescriptor mcAES256Descriptor =
17{
18 CIPHER_NAME_AES256,
19 AllocateAES256Cipher,
20 FreeAES256Cipher,
21 CloneAES256Cipher,
22 GetLegacyAES256Cipher,
23 GetPageSizeAES256Cipher,
24 GetReservedAES256Cipher,
25 GetSaltAES256Cipher,
26 GenerateKeyAES256Cipher,
27 EncryptPageAES256Cipher,
28 DecryptPageAES256Cipher
29};
VFS 교체 #
VFS 구조체 #
VFS는 SQLite가 실제 운영체제 파일시스템과 독립적으로 동작하도록 추상화 레이어이다.
운영체제마다 파일입출력 방법이 다르기 때문에 파일오픈, 동적 라이브러리 로드, 시스템콜 등의 함수를 등록하게 해서 하나의 동일한 인터페이스로 모든 운영체제의 로우레벨한 작업이 가능하도록 한다.
1struct sqlite3_vfs {
2 int iVersion; /* Structure version number (currently 3) */
3 int szOsFile; /* Size of subclassed sqlite3_file */
4 int mxPathname; /* Maximum file pathname length */
5 sqlite3_vfs *pNext; /* Next registered VFS */
6 const char *zName; /* Name of this virtual file system */
7 void *pAppData; /* Pointer to application-specific data */
8 int (*xOpen)(sqlite3_vfs*, sqlite3_filename zName, sqlite3_file*,
9 int flags, int *pOutFlags);
10 int (*xDelete)(sqlite3_vfs*, const char *zName, int syncDir);
11 int (*xAccess)(sqlite3_vfs*, const char *zName, int flags, int *pResOut);
12 int (*xFullPathname)(sqlite3_vfs*, const char *zName, int nOut, char *zOut);
13 void *(*xDlOpen)(sqlite3_vfs*, const char *zFilename);
14 void (*xDlError)(sqlite3_vfs*, int nByte, char *zErrMsg);
15 void (*(*xDlSym)(sqlite3_vfs*,void*, const char *zSymbol))(void);
16 void (*xDlClose)(sqlite3_vfs*, void*);
17 int (*xRandomness)(sqlite3_vfs*, int nByte, char *zOut);
18 int (*xSleep)(sqlite3_vfs*, int microseconds);
19 int (*xCurrentTime)(sqlite3_vfs*, double*);
20 int (*xGetLastError)(sqlite3_vfs*, int, char *);
21 /*
22 ** The methods above are in version 1 of the sqlite_vfs object
23 ** definition. Those that follow are added in version 2 or later
24 */
25 int (*xCurrentTimeInt64)(sqlite3_vfs*, sqlite3_int64*);
26 /*
27 ** The methods above are in versions 1 and 2 of the sqlite_vfs object.
28 ** Those below are for version 3 and greater.
29 */
30 int (*xSetSystemCall)(sqlite3_vfs*, const char *zName, sqlite3_syscall_ptr);
31 sqlite3_syscall_ptr (*xGetSystemCall)(sqlite3_vfs*, const char *zName);
32 const char *(*xNextSystemCall)(sqlite3_vfs*, const char *zName);
33 /*
34 ** The methods above are in versions 1 through 3 of the sqlite_vfs object.
35 ** New fields may be appended in future versions. The iVersion
36 ** value will increment whenever this happens.
37 */
38};
39
40// sqlite3mc 의 vfs 구조체.
41struct sqlite3mc_vfs
42{
43 sqlite3_vfs base; /* Multiple Ciphers VFS shim methods */
44 sqlite3_mutex* mutex; /* Mutex to protect pMain */
45 sqlite3mc_file* pMain; /* List of main database files */
46};
xOpen 함수의 3번째 인자로 sqlite3_file이 전달된다. 이 구조체에는 xRead, xWrite 등 파일 입출력을 재정의 하여 포인터로 넣어둘 수 있는데, sqlite3mc는 이 입출력 함수에서 암복호화를 수행하여 SEE를 구현한다.
1typedef struct sqlite3_file sqlite3_file;
2struct sqlite3_file {
3 const struct sqlite3_io_methods *pMethods; /* Methods for an open file */
4};
5
6struct sqlite3_io_methods {
7 int iVersion;
8 int (*xClose)(sqlite3_file*);
9 int (*xRead)(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst);
10 int (*xWrite)(sqlite3_file*, const void*, int iAmt, sqlite3_int64 iOfst);
11 int (*xTruncate)(sqlite3_file*, sqlite3_int64 size);
12 int (*xSync)(sqlite3_file*, int flags);
13 int (*xFileSize)(sqlite3_file*, sqlite3_int64 *pSize);
14 int (*xLock)(sqlite3_file*, int);
15 int (*xUnlock)(sqlite3_file*, int);
16 int (*xCheckReservedLock)(sqlite3_file*, int *pResOut);
17 int (*xFileControl)(sqlite3_file*, int op, void *pArg);
18 int (*xSectorSize)(sqlite3_file*);
19 int (*xDeviceCharacteristics)(sqlite3_file*);
20 /* Methods above are valid for version 1 */
21 int (*xShmMap)(sqlite3_file*, int iPg, int pgsz, int, void volatile**);
22 int (*xShmLock)(sqlite3_file*, int offset, int n, int flags);
23 void (*xShmBarrier)(sqlite3_file*);
24 int (*xShmUnmap)(sqlite3_file*, int deleteFlag);
25 /* Methods above are valid for version 2 */
26 int (*xFetch)(sqlite3_file*, sqlite3_int64 iOfst, int iAmt, void **pp);
27 int (*xUnfetch)(sqlite3_file*, sqlite3_int64 iOfst, void *p);
28 /* Methods above are valid for version 3 */
29 /* Additional methods may be added in future releases */
30};
31
32// sqlite3mc 의 file 구조체. 역시 원래 OS의 file 구조체를 가지고 있어야한다.
33struct sqlite3mc_file
34{
35 sqlite3_file base; /* sqlite3_file I/O methods */
36 sqlite3_file* pFile; /* Real underlying OS file */
37 sqlite3mc_vfs* pVfsMC; /* Pointer to the sqlite3mc_vfs object */
38 const char* zFileName; /* File name */
39 int openFlags; /* Open flags */
40 sqlite3mc_file* pMainNext; /* Next main db file */
41 sqlite3mc_file* pMainDb; /* Main database to which this one is attached */
42 Codec* codec; /* Codec if encrypted */
43 int pageNo; /* Page number (in case of journal files) */
44};
sqlite3mc 코드 #
vfs를 생성해서 등록하는데, os마다 vfs코드를 sqlite3mc 에서 구현할수는 없으니 pAppData 포인터에 저장해두고 필요할때 호출하는 방식이다.
vfs는 여러개를 등록할 수 있고 각 vfs 마다 이름이 있으며 sqlite3_open_v2 함수 호출 시 이름을 지정하여 명시적으로 vfs 골라서 사용할 수 있다.
1SQLITE_API int sqlite3mc_vfs_create(const char* zVfsReal, int makeDefault)
2{
3 static sqlite3_vfs mcVfsTemplate =
4 {
5 3, /* iVersion */
6 0, /* szOsFile */
7 1024, /* mxPathname */
8 0, /* pNext */
9 0, /* zName */
10 0, /* pAppData */
11 mcVfsOpen, /* xOpen */
12 mcVfsDelete, /* xDelete */
13 mcVfsAccess, /* xAccess */
14 mcVfsFullPathname, /* xFullPathname */
15#ifndef SQLITE_OMIT_LOAD_EXTENSION
16 mcVfsDlOpen, /* xDlOpen */
17 mcVfsDlError, /* xDlError */
18 mcVfsDlSym, /* xDlSym */
19 mcVfsDlClose, /* xDlClose */
20#else
21 0, 0, 0, 0,
22#endif
23 mcVfsRandomness, /* xRandomness */
24 mcVfsSleep, /* xSleep */
25 mcVfsCurrentTime, /* xCurrentTime */
26 mcVfsGetLastError, /* xGetLastError */
27 mcVfsCurrentTimeInt64, /* xCurrentTimeInt64 */
28 mcVfsSetSystemCall, /* xSetSystemCall */
29 mcVfsGetSystemCall, /* xGetSystemCall */
30 mcVfsNextSystemCall /* xNextSystemCall */
31 };
32 // 새로운 vfs인데, sqlite3mc_vfs 라서 추가 정보를 담을 수 있다.
33 sqlite3mc_vfs* pVfsNew = 0; /* Newly allocated VFS */
34 // 이미 있던 기존 vfs
35 sqlite3_vfs* pVfsReal = sqlite3_vfs_find(zVfsReal); /* Real VFS */
36 int rc;
37
38 if (pVfsReal)
39 {
40 size_t nPrefix = strlen(SQLITE3MC_VFS_NAME);
41 size_t nRealName = strlen(pVfsReal->zName);
42 size_t nName = nPrefix + nRealName + 1;
43 size_t nByte = sizeof(sqlite3mc_vfs) + nName + 1;
44 pVfsNew = (sqlite3mc_vfs*) sqlite3_malloc64(nByte);
45 if (pVfsNew)
46 {
47 // 새로 만든 sqlite3mc 용 vfs에 이미 있던 vfs 정보로 일부 채움
48 char* zSpace = (char*) &pVfsNew[1]; // 이름은 원래 구조체 뒤에 있다.
49 memset(pVfsNew, 0, nByte);
50
51 // sqlite3mc_vfs 에 원하는 템플릿을 다 넣고 기존 OS의 vfs도 pAppData에 넣는다.
52 // pAppData는 원하는걸 저장할 수 있는 포인터라 mc에서는 이걸 사용해서 원래 OS의 vfs를 저장한다.
53 memcpy(&pVfsNew->base, &mcVfsTemplate, sizeof(sqlite3_vfs));
54 pVfsNew->base.iVersion = pVfsReal->iVersion;
55 pVfsNew->base.pAppData = pVfsReal;
56 pVfsNew->base.mxPathname = pVfsReal->mxPathname;
57 // pVfsNew->base 가 디폴트 vfs라서 sqlite3_open 에서 file이 전달될때 이 사이즈에 맞춰서 전달된다.
58 pVfsNew->base.szOsFile = sizeof(sqlite3mc_file) + pVfsReal->szOsFile;
59
60 pVfsNew->base.zName = (const char*) zSpace;
61 memcpy(zSpace, SQLITE3MC_VFS_NAME, nPrefix);
62 memcpy(zSpace + nPrefix, "-", 1);
63 memcpy(zSpace + nPrefix + 1, pVfsReal->zName, nRealName);
64
65 // 새로운 vfs를 디폴트 vfs로 등록한다. (makeDefault는 1로 들어오기 때문)
66 pVfsNew->mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_RECURSIVE);
67 if (pVfsNew->mutex)
68 {
69 rc = sqlite3_vfs_register(&pVfsNew->base, makeDefault);
70 if (rc != SQLITE_OK)
71 {
72 sqlite3_mutex_free(pVfsNew->mutex);
73 }
74 }
75 // ...
76}
초기화가 완료되면 디폴트 vfs는 sqlite3mc_vfs 가 되고, base에는 mcVfsOpen 이 포함된 vfs 구조체가 저장되며 base.pAppData 에는 원본 OS의 vfs가 저장된다.
파일 오픈 (xOpen) #
사용자 앱에서 vfs 지정 없이 sqlite3_open 등의 함수를 호출하게 되면, sqlite3 에서는 기본으로 등록한 vfs가 호출되고 xOpen으로 등록한 mcVfsOpen 가 호출된다.
파일의 버전에 따라 입출력 메소드 버전이 정해지는 것을 볼 수 있다.
입출력 메소드는 전역으로 이미 테이블화 되어있고
1// 원본을 백업해뒀던 pAppData 에서 가져오는 매크로이다.
2#define REALVFS(p) ((sqlite3_vfs*)(((sqlite3mc_vfs*)(p))->base.pAppData))
3
4// sqlite3mc 의 입출력 메소드 테이블
5static sqlite3_io_methods mcIoMethodsGlobal3 =
6{
7 3, /* iVersion */
8 mcIoClose, /* xClose */
9 mcIoRead, /* xRead */
10 mcIoWrite, /* xWrite */
11 mcIoTruncate, /* xTruncate */
12 mcIoSync, /* xSync */
13 mcIoFileSize, /* xFileSize */
14 mcIoLock, /* xLock */
15 mcIoUnlock, /* xUnlock */
16 mcIoCheckReservedLock, /* xCheckReservedLock */
17 mcIoFileControl, /* xFileControl */
18 mcIoSectorSize, /* xSectorSize */
19 mcIoDeviceCharacteristics, /* xDeviceCharacteristics */ // v1은 여기까지
20 mcIoShmMap, /* xShmMap */
21 mcIoShmLock, /* xShmLock */
22 mcIoShmBarrier, /* xShmBarrier */
23 mcIoShmUnmap, /* xShmUnmap */ // v2는 여기까지
24 mcIoFetch, /* xFetch */
25 mcIoUnfetch, /* xUnfetch */ // v3은 전부다
26};
27
28static sqlite3_io_methods* mcIoMethodsGlobal[] =
29 { 0, &mcIoMethodsGlobal1 , &mcIoMethodsGlobal2 , &mcIoMethodsGlobal3 };
30
31static int mcVfsOpen(sqlite3_vfs* pVfs, const char* zName, sqlite3_file* pFile, int flags, int* pOutFlags)
32{
33 // xOpen의 3번째 인자 pFile은 외부에서 메모리를 할당해주고 내부에서 채워야한다.
34 // pFile size = sizeof(sqlite3mc_file) + pVfsReal->szOsFile;
35 sqlite3mc_vfs* mcVfs = (sqlite3mc_vfs*) pVfs;
36 sqlite3mc_file* mcFile = (sqlite3mc_file*) pFile;
37 // sqlite3mc_file 구조체 초기화
38 // pFile 위치의 메모리는 아직 비워져있다. OS의 xOpen에서 채워준다.
39 mcFile->pFile = (sqlite3_file*) &mcFile[1];
40 mcFile->pVfsMC = mcVfs;
41 mcFile->openFlags = flags;
42 mcFile->zFileName = zName;
43 mcFile->codec = 0;
44 mcFile->pMainDb = 0;
45 mcFile->pMainNext = 0;
46 mcFile->pageNo = 0;
47
48 // ...
49 // 원본 OS의 vfs에서 xOpen을 호출해서 운영체제 종속을 없앤다.
50 // 파일을 읽어와서 mcFile->pFile에 정보를 채워넣는다.
51 rc = REALVFS(pVfs)->xOpen(REALVFS(pVfs), zName, mcFile->pFile, flags, pOutFlags);
52 if (rc == SQLITE_OK)
53 {
54 // 파일의 버전에 맞춰서 메소드 테이블을 가져와서 등록한다.
55 int ioMethodsVersion = mcFile->pFile->pMethods->iVersion;
56 // mcFile->base.pMethods
57 pFile->pMethods = mcIoMethodsGlobal[ioMethodsVersion];
58 if (flags & SQLITE_OPEN_MAIN_DB)
59 {
60 mcMainListAdd(mcFile);
61 }
62 }
63 return rc;
64}
초기화할때 mc의 vfs.base 가 디폴트 vfs가 되고, file 구조체의 크기는 sizeof(sqlite3mc_file) + sizeof(sqlite3_file) 이기 때문에 mcVfsOpen 호출 시 sqlite3 이 할당해준 메모리는 위의 file 크기가 되고 두 구조체가 연속적으로 존재하게 된다.
sqlite3mc_file 구조체는 mcVfsOpen 에서 초기화되고, 연속적으로 있는 sqlite3_file 구조체는 OS의 xOpen에서 초기화된다.
sqlite3이 이해할 수 있는 인터페이스를 유지하기 위해 sqlite3mc_file의 맨 앞에는 sqlite3_file 이 있고, 여기에 입출력 메소드를 저장하면 앞으로 이 file 구조체를 사용할 때마다 base.pMethods 를 사용하여 원하는 기능을 추가하고 내부적으로는 다시 mcFile->pFile->pMethods 를 사용할 것이다
암복호화 지원을 위한 명령어 추가 #
기존 코드는 sqlite3_file_control 을 호출하기로 되어있지만 sqlite3mc 의 코드를 호출하는 것으로 패치되어 있는데, SQLite에서 PRAGMA 명령어를 파싱하는 sqlite3Pragma 함수에서 호출된다.
1SQLITE_PRIVATE void sqlite3Pragma(
2 Parse *pParse,
3 Token *pId1, /* First part of [schema.]id field */
4 Token *pId2, /* Second part of [schema.]id field, or NULL */
5 Token *pValue, /* Token for <value>, or NULL */
6 int minusFlag /* True if a '-' sign preceded <value> */
7){
8 // ...
9 rc = sqlite3mcFileControlPragma(db, zDb, SQLITE_FCNTL_PRAGMA, (void*)aFcntl);
10 if( rc==SQLITE_OK ){
11 sqlite3VdbeSetNumCols(v, 1);
12 sqlite3VdbeSetColName(v, 0, COLNAME_NAME, aFcntl[0], SQLITE_TRANSIENT);
13 returnSingleText(v, aFcntl[0]);
14 sqlite3_free(aFcntl[0]);
15 goto pragma_out;
16 }
17 // ...
18}
일단 기존의 sqlite3_file_control 함수를 호출해보고 찾지 못하는 PRAGMA 명령어라면 자체적인 로직으로 처리하게 된다.
이 자체적인 로직에서는 원래 sqlite에서 지원하지 않는 PRAGMA cipher, PRAGMA hmac_check, PRAGMA key 등의 로직을 파싱할 수 있게 구현되어 있고, 각 명령어는 내부 함수를 호출하여 설정을 저장하게 된다.
1SQLITE_PRIVATE int
2sqlite3mcFileControlPragma(sqlite3* db, const char* zDbName, int op, void* pArg)
3{
4 int rc = sqlite3_file_control(db, zDbName, op, pArg);
5 if (rc == SQLITE_NOTFOUND)
6 {
7 // ...
8 if (sqlite3StrICmp(pragmaName, "cipher") == 0)
9 value = sqlite3mc_config(db, "cipher", cipherId);
10 // ...
11 else if (sqlite3StrICmp(pragmaName, "hmac_check") == 0)
12 else if (sqlite3StrICmp(pragmaName, "mc_legacy_wal") == 0)
13 else if (sqlite3StrICmp(pragmaName, "key") == 0)
14 rc = sqlite3_key_v2(db, zDbName, pragmaValue, -1);
15 // ...
16 else if (sqlite3StrICmp(pragmaName, "hexkey") == 0)
17 int nValue = sqlite3Strlen30(pragmaValue);
18 // ...
19 else if (sqlite3StrICmp(pragmaName, "rekey") == 0)
20 }
21 return rc;
22}
복호화 (xRead) #
xRead 호출과정 #
sqlite3_exec(db, “SELECT * FROM mytable”) 처럼 쿼리를 호출하여 데이터를 읽을 필요가 있을때 내부적으로 여러 함수를 거쳐(쿼리파싱, B-tree 탐색 등) 데이터가 존재하는 페이지 위치를 특정하고 xRead를 호출하게 된다.
쿼리 -> ... -> sqlite3PagerGet(pgno) -> sqlite3OsRead() -> xRead() 이 순서로 호출되는데, 파일을 열 때 입출력메소드를 이미 등록 해뒀기 때문에 xRead는 mcIoRead 가 호출된다.
파일의 flag를 읽어서 읽는 파일의 종류를 확인한 후 종류별로 다른 mcRead... 함수를 호출한다.
1static int mcIoRead(sqlite3_file* pFile, void* buffer, int count, sqlite3_int64 offset)
2{
3 sqlite3mc_file* mcFile = (sqlite3mc_file*) pFile;
4 int rc = REALFILE(pFile)->pMethods->xRead(REALFILE(pFile), buffer, count, offset);
5 if (rc == SQLITE_IOERR_SHORT_READ)
6 {
7 return rc;
8 }
9 // MAIN_DB 인 경우
10 if (mcFile->openFlags & SQLITE_OPEN_MAIN_DB)
11 {
12 rc = mcReadMainDb(pFile, buffer, count, offset);
13 }
14 // ...
15}
.journal 파일은 롤백 저널링 모드라서 트랜잭션 대상 페이지의 사본을 journal 파일에 저장하고 트랜잭션 성공할때 삭제하는 방식이고, .db-wal 파일은 wal 에 먼저 기록 후 나중에 db 파일에 병합하는 모드이다.
1static int mcReadMainDb(sqlite3_file* pFile, void* buffer, int count, sqlite3_int64 offset);
2static int mcReadMainJournal(sqlite3_file* pFile, const void* buffer, int count, sqlite3_int64 offset);
3static int mcReadSubJournal(sqlite3_file* pFile, const void* buffer, int count, sqlite3_int64 offset);
4static int mcReadWal(sqlite3_file* pFile, const void* buffer, int count, sqlite3_int64 offset);
- MainDb: 특정 페이지만 복호화 하면서 읽거나 전체 페이지를 복호화하고 읽는다.
- MainJournal: 연결된 MainDb가 있는지 확인하고 페이지의 첫 4byte로 페이지 번호 추출 후 나머지 페이지 데이터를 복호화한다.
- SubJournal: MainJournal 과 동일
- Wal: wal 파일헤더(32byte)가 아닌 각 프레임마다 존재하는 프레임 헤더(24byte)에서 페이지 번호를 읽어서 복호화
DB파일을 읽으면서 복호화 #
1static int mcReadMainDb(sqlite3_file* pFile, void* buffer, int count, sqlite3_int64 offset)
2{
3 if (mcFile->codec != 0 && sqlite3mcIsEncrypted(mcFile->codec))
4 {
5 // 특정 페이지만 읽기
6 const int deltaOffset = offset % pageSize;
7 const int deltaCount = count % pageSize;
8 if (deltaOffset || deltaCount)
9 {
10 // ...
11 // OS 의 xRead로 파일 그냥 읽기
12 rc = REALFILE(pFile)->pMethods->xRead(REALFILE(pFile), pageBuffer, pageSize, prevOffset);
13
14 pageNo = prevOffset / pageSize + 1;
15 // 해당 페이지 복호화
16 bufferDecrypted = sqlite3mcCodec(mcFile->codec, pageBuffer, pageNo, 3);
17 rc = sqlite3mcGetCodecLastError(mcFile->codec);
18
19 // buffer 로 복사해서 복호화된 DB 데이터 리턴
20 memcpy(buffer, pageBuffer, count);
21 } else {
22 // 전체 페이지 읽기
23 }
24 }
25}
db, journal, wal 파일을 읽을때, 쓸때 sqlite3mcCodec 함수를 호출해서 페이지 암/복호화를 수행한다.
globalCodecDescriptorTable 에 저장된 CipherDescriptor 를 찾아와서 m_decryptPage 함수를 호출하면서 복호화를 하게 된다.
1// cipher_common.c
2SQLITE_PRIVATE int
3sqlite3mcDecrypt(Codec* codec, int page, unsigned char* data, int len)
4{
5 int cipherType = codec->m_readCipherType;
6 void* cipher = codec->m_readCipher;
7 int reserved = (codec->m_readReserved >= 0) ? codec->m_readReserved : codec->m_reserved;
8
9 // 여기에서 글로벌 테이블에 저장된 디스크립터의 복호화 함수 호출
10 return globalCodecDescriptorTable[cipherType-1].m_decryptPage(cipher, page, data, len, reserved, codec->m_hmacCheck);
11}
12
13// codeext.c
14// 모드에 따라 암/복호화/페이지 로드 를 진행한다.
15SQLITE_PRIVATE void*
16sqlite3mcCodec(void* pCodecArg, void* data, Pgno nPageNum, int nMode)
17{
18 // ...
19 switch(nMode)
20 {
21 case 0: /* Undo a "case 7" journal file encryption */
22 case 2: /* Reload a page */
23 case 3: /* Load a page */
24 if (sqlite3mcHasReadCipher(codec))
25 {
26 rc = sqlite3mcDecrypt(codec, nPageNum, (unsigned char*) data, pageSize);
27 if (rc != SQLITE_OK)
28 {
29 mcReportCodecError(sqlite3mcGetBtShared(codec), rc);
30 memset(data, 0, pageSize);
31 }
32 }
33 break;
34 // ...
35 }
36}
복호화 후 db 읽기 #
1PRAGMA compile_options;
2
3PRAGMA key='0';
4PRAGMA journal_mode=wal;
5PRAGMA cipher=aes256cbc;
6
7CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
8INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Sam'), (4, 'Kim'), (5, 'dhk2');
9SELECT * FROM users;
데이터 저장 포맷의 문제일까 #
sqlite3mc_shell.exe 로 db를 만들고, sqlite3.exe로 읽으면 정상적으로 읽을 수 있는 것을 보면 저장 포맷 차이는 아닌 것 같다.
복호화의 문제일까 #
로직을 확인해봤을땐 aes128cbc 암호화 방식을 사용하고 있고 wxaes128.c, 키 생성로직이 동일해서 sqlite3mc 로 읽을 수 있을 것 같았다.
PRAGMA key 명령을 입력할때 생성된 키를 출력하도록 소스코드를 수정하고 Contact.db 를 읽어봤더니 정상적으로 읽히는것을 확인할 수 있었다. 명령어 입력 순서도 중요하다.
1.open Contact.db
2PRAGMA cipher=aes128cbc;
3PRAGMA hexkey="0000000000000000";
4.tables
Session.db는 오픈 후 wal 파일에 작성되다가 커밋이 되지 않아서 wal 파일에만 데이터가 있는데, 복호화하고 읽는 파일은 wal 파일의 데이터를 읽어올 수 없다.
1.open Session.db
2PRAGMA cipher=aes128cbc;
3PRAGMA hexkey="7a4d5dc300000600";
4.tables
5
6.open Session.db_dec_.db
7PRAGMA journal_mode=wal;
8.tables
Wal 파일 복호화 #
mcReadWal → sqlite3mcCodec → DecryptPageAES128Cipher → sqlite3mcAES128 이런 순서대로 복호화가 진행된다.
wal 파일 구조 #
wal 파일은 기본 wal header (32byte) 이후부터 frame header (24byte) + page (4096byte) 형태로 만들어져 있다.
PRAGMA page_size;로 페이지 사이즈를 확인할 수 있고, 변경할수도 있다.
wal 파일은 프레임 크기 단위로 페이지가 기록되며, frame header의 첫 4바이트를 빅엔디안으로 읽으면 페이지 번호인데, 같은 파일에 같은 페이지 번호를 가진 프레임이 여러개 있는 것을 확인할 수 있다.
원래 wal 파일의 데이터는 같은 페이지라도 지우지 않고 중첩해서 쌓아가는데, 그중 변경사항에 대해서만 db 파일에 병합하게 된다.
1 page 구조 #
첫번째 페이지의 구조이다. 아래 복호화 함수에서 0x15 - 0x17 오프셋의 값이 40, 20, 20 임을 확인한다.
복호화 함수 #
mcReadWal #
프레임단위로 버퍼에 넣고 sqlite3mcCodec 함수를 호출한다.
- mcReadWal 로 전달되는 값
1Enter mcReadWal.. buf: 7f74ef78, cnt: 4120, off: 32 2Enter mcReadWal.. buf: 7f74ef78, cnt: 4120, off: 4152 3Enter mcReadWal.. buf: 7f74ef78, cnt: 4120, off: 8272 4Enter mcReadWal.. buf: 7f74ef78, cnt: 4120, off: 12392 5// ... 6!Enter mcReadWal.. buf: 7f7633a8, cnt: 4096, off: 1479136 7* page: 1, buf[ed 9e f0 b8 14 b4 9f 37] 8 Enter DecryptPageAES128Cipher.. data: 7f7633a8, page: 1, len: 4096 9 = data[ed 9e f0 b8 14 b4 9f 37] 10 = page: 1, offset: 16 11* decbuf[53 51 4c 69 74 65 20 66] 12Enter mcReadWal.. buf: 7f762298, cnt: 4096, off: 1013576 13!Enter mcReadWal.. buf: 7f762298, cnt: 4096, off: 1013576 14* page: 20, buf[b2 07 68 0c d9 1e 4f 72] 15 Enter DecryptPageAES128Cipher.. data: 7f762298, page: 20, len: 4096 16 = data[b2 07 68 0c d9 1e 4f 72] 17 = page: 20, offset: 0 18* decbuf[0d 07 c2 00 08 04 0e 00]
전달되는 값을 보면 wal header 이후 오프셋 부터 프레임 크기단위로 계속 읽어오는데, 이때는 복호화를 하지 않는다.
페이지 크기와 동일한 크기만큼 cnt(4096)가 전달됐을 때 복호화 함수를 타게된다.
이때 보면 페이지 위치가 뒤죽박죽인 것을 볼 수 있는데, db 파일에서
1static int mcReadWal(sqlite3_file* pFile, const void* buffer, int count, sqlite3_int64 offset)
2{
3 int rc = SQLITE_OK;
4 sqlite3mc_file* mcFile = (sqlite3mc_file*) pFile;
5 Codec* codec = (mcFile->pMainDb) ? mcFile->pMainDb->codec : 0;
6
7 if (codec != 0 && sqlite3mcIsEncrypted(codec))
8 {
9 const int pageSize = sqlite3mcGetPageSize(codec);
10
11 if (count == pageSize)
12 {
13 int pageNo = 0;
14 unsigned char ac[4];
15
16 // 프레임 가장 앞 4byte가 페이지번호
17 rc = REALFILE(pFile)->pMethods->xRead(REALFILE(pFile), ac, 4, offset - walFrameHeaderSize);
18 if (rc == SQLITE_OK) pageNo = sqlite3Get4byte(ac);
19
20 if (pageNo != 0)
21 {
22 fprintf(stdout, "Enter mcReadWal.. buf: %x, cnt: %d, off: %d\n", buffer, count, offset);
23 void* bufferDecrypted = sqlite3mcCodec(codec, (char*)buffer, pageNo, 3);
24 rc = sqlite3mcGetCodecLastError(codec);
25 }
26 }
27 else if (codec->m_walLegacy != 0 && count == pageSize + walFrameHeaderSize)
28 {
29 int pageNo = sqlite3Get4byte(buffer);
30 if (pageNo != 0)
31 {
32 void* bufferDecrypted = sqlite3mcCodec(codec, (char*)buffer+walFrameHeaderSize, pageNo, 3);
33 rc = sqlite3mcGetCodecLastError(codec);
34 }
35 }
36 // ...
37}
DecryptPageAES128Cipher #
1static int
2DecryptPageAES128Cipher(void* cipher, int page, unsigned char* data, int len, int reserved, int hmacCheck)
3{
4 AES128Cipher* aesCipher = (AES128Cipher*) cipher;
5 int rc = SQLITE_OK;
6 if (aesCipher->m_legacy != 0)
7 {
8 /* Use the legacy encryption scheme */
9 rc = sqlite3mcAES128(aesCipher->m_aes, page, 0, aesCipher->m_key, data, len, data);
10 }
11 else
12 {
13 unsigned char dbHeader[8];
14 int dbPageSize;
15 int offset = 0;
16 if (page == 1)
17 {
18 /* Save (unencrypted) header bytes 16..23 */
19 memcpy(dbHeader, data + 16, 8);
20 /* Determine page size */
21 dbPageSize = (dbHeader[0] << 8) | (dbHeader[1] << 16);
22 /* Check whether the database header is valid */
23 /* If yes, the database follows the new encryption scheme, otherwise use the previous encryption scheme */
24 if ((dbPageSize >= 512) && (dbPageSize <= SQLITE_MAX_PAGE_SIZE) && (((dbPageSize - 1) & dbPageSize) == 0) &&
25 (dbHeader[5] == 0x40) && (dbHeader[6] == 0x20) && (dbHeader[7] == 0x20))
26 {
27 /* Restore encrypted bytes 16..23 for new encryption scheme */
28 memcpy(data + 16, data + 8, 8);
29 offset = 16;
30 }
31 }
32 fprintf(stdout, " = page: %d, offset: %d\n", page, offset);
33 rc = sqlite3mcAES128(aesCipher->m_aes, page, 0, aesCipher->m_key, data + offset, len - offset, data + offset);
34 if (page == 1 && offset != 0)
35 {
36 /* Verify the database header */
37 if (memcmp(dbHeader, data + 16, 8) == 0)
38 {
39 memcpy(data, SQLITE_FILE_HEADER, 16);
40 }
41 }
42 }
43 return rc;
44}
복호화 함수를 보면, 레거시인 경우 그냥 aes로 프레임 헤더를 제외한 전체 데이터를 복호화하게 된다.
1 page 에서 data[16:24] 를 dbHeader 에저장하고, 신규 암호화 방식인지 검사한다.
신규 암호화 방식이 아니라면 그냥 레거시 필드가 세팅되지 않은 레거시 복호화로 보고 전체 데이터를 복호화한다.
신규 암호화 방식인 경우 data[8:16] 를 data[16:24] 에 넣고 복호화한다.
결국 신규 암호화 방식은 1 page가 어차피 “SQLite format 3\0” 으로 고정되어 있는 데이터 버퍼가 있기 때문에 이 공간을 활용해서 data[16:24] 데이터를 data[8:16] 위치에 저장해두고 복호화하면서 복원하는 방식이다.
이 방식을 무시하고 복호화하면 저렇게 data[16:24] 데이터가 깨져서 문제가 발생한다.