/* ** 2020-04-20 ** ** The author disclaims copyright to this source code. In place of ** a legal notice, here is a blessing: ** ** May you do good and not evil. ** May you find forgiveness for yourself and forgive others. ** May you share freely, never taking more than you give. ** ****************************************************************************** ** ** This file implements a VFS shim that obfuscates database content ** written to disk by applying a CipherStrategy. ** ** COMPILING ** ** This extension requires SQLite 3.32.0 or later. ** ** ** LOADING ** ** Initialize it using a single API call as follows: ** ** sqlite3_obfsvfs_init(); ** ** Obfsvfs is a VFS Shim. When loaded, "obfsvfs" becomes the new ** default VFS and it uses the prior default VFS as the next VFS ** down in the stack. This is normally what you want. However, it ** complex situations where multiple VFS shims are being loaded, ** it might be important to ensure that obfsvfs is loaded in the ** correct order so that it sequences itself into the default VFS ** Shim stack in the right order. ** ** USING ** ** Open database connections using the sqlite3_open_v2() with ** the SQLITE_OPEN_URI flag and using a URI filename that includes ** the query parameter "key=XXXXXXXXXXX..." where the XXXX... consists ** of 64 hexadecimal digits (32 bytes of content). ** ** Create a new encrypted database by opening a file that does not ** yet exist using the key= query parameter. ** ** LIMITATIONS: ** ** * An obfuscated database must be created as such. There is ** no way to convert an existing database file into an ** obfuscated database file other than to run ".dump" on the ** older database and reimport the SQL text into a new ** obfuscated database. ** ** * There is no way to change the key value, other than to ** ".dump" and restore the database ** ** * The database page size must be exactly 8192 bytes. No other ** database page sizes are currently supported. ** ** * Memory-mapped I/O does not work for obfuscated databases. ** If you think about it, memory-mapped I/O doesn't make any ** sense for obfuscated databases since you have to make a ** copy of the content to deobfuscate anyhow - you might as ** well use normal read()/write(). ** ** * Only the main database, the rollback journal, and WAL file ** are obfuscated. Other temporary files used for things like ** SAVEPOINTs or as part of a large external sort remain ** unobfuscated. ** ** * Requires SQLite 3.32.0 or later.
*/ #include"ObfuscatingVFS.h"
#include <string.h> #include <ctype.h> #include <stdio.h> /* For debugging only */
/* Access to a lower-level VFS that (might) implement dynamic loading, ** access to randomness, etc.
*/ #define ORIGVFS(p) ((sqlite3_vfs*)((p)->pAppData)) #define ORIGFILE(p) ((sqlite3_file*)(((ObfsFile*)(p)) + 1))
/* An open file */ struct ObfsFile {
sqlite3_file base; /* IO methods */ constchar* zFName; /* Original name of the file */ bool inCkpt; /* Currently doing a checkpoint */
ObfsFile* pPartner; /* Ptr from WAL to main-db, or from main-db to WAL */ void* pTemp; /* Temporary storage for encoded pages */
IPCStreamCipherStrategy*
encryptCipherStrategy; /* CipherStrategy for encryption */
IPCStreamCipherStrategy*
decryptCipherStrategy; /* CipherStrategy for decryption */
};
/* ** Methods for ObfsFile
*/ staticint obfsClose(sqlite3_file*); staticint obfsRead(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst); staticint obfsWrite(sqlite3_file*, constvoid*, int iAmt, sqlite3_int64 iOfst); staticint obfsTruncate(sqlite3_file*, sqlite3_int64 size); staticint obfsSync(sqlite3_file*, int flags); staticint obfsFileSize(sqlite3_file*, sqlite3_int64* pSize); staticint obfsLock(sqlite3_file*, int); staticint obfsUnlock(sqlite3_file*, int); staticint obfsCheckReservedLock(sqlite3_file*, int* pResOut); staticint obfsFileControl(sqlite3_file*, int op, void* pArg); staticint obfsSectorSize(sqlite3_file*); staticint obfsDeviceCharacteristics(sqlite3_file*); staticint obfsShmMap(sqlite3_file*, int iPg, int pgsz, int, voidvolatile**); staticint obfsShmLock(sqlite3_file*, int offset, int n, int flags); staticvoid obfsShmBarrier(sqlite3_file*); staticint obfsShmUnmap(sqlite3_file*, int deleteFlag); staticint obfsFetch(sqlite3_file*, sqlite3_int64 iOfst, int iAmt, void** pp); staticint obfsUnfetch(sqlite3_file*, sqlite3_int64 iOfst, void* p);
static constexpr int kKeyBytes = 32; static constexpr int kIvBytes = IPCStreamCipherStrategy::BlockPrefixLength; static constexpr int kClearTextPrefixBytesOnFirstPage = 32; static constexpr int kReservedBytes = 32; static constexpr int kBasicBlockSize = IPCStreamCipherStrategy::BasicBlockSize;
static_assert(kClearTextPrefixBytesOnFirstPage % kBasicBlockSize == 0);
static_assert(kReservedBytes % kBasicBlockSize == 0);
/* Obfuscate a page using p->encryptCipherStrategy. ** ** A new random nonce is created and stored in the last 32 bytes ** of the page. All other bytes of the page are obfuscasted using the ** CipherStrategy. Except, for page-1 (including the SQLite ** database header) the first 32 bytes are not obfuscated ** ** Return a pointer to the obfuscated content, which is held in the ** p->pTemp buffer. Or return a NULL pointer if something goes wrong. ** Errors are reported using NS_WARNING().
*/ staticvoid* obfsEncode(ObfsFile* p, /* File containing page to be obfuscated */
u8* a, /* database page to be obfuscated */ int nByte /* Bytes of content in a[]. Must be a multiple
of kBasicBlockSize. */
) {
u8 aIv[kIvBytes];
u8* pOut; int i;
static_assert((kIvBytes & (kIvBytes - 1)) == 0);
sqlite3_randomness(kIvBytes, aIv);
pOut = (u8*)p->pTemp; if (pOut == nullptr) {
pOut = static_cast<u8*>(sqlite3_malloc64(nByte)); if (pOut == nullptr) {
NS_WARNING(nsPrintfCString("unable to allocate a buffer in which to" " write obfuscated database content for %s",
p->zFName)
.get()); return nullptr;
}
p->pTemp = pOut;
} if (memcmp(a, "SQLite format 3", 16) == 0) {
i = kClearTextPrefixBytesOnFirstPage; if (a[20] != kReservedBytes) {
NS_WARNING(nsPrintfCString("obfuscated database must have reserved-bytes" " set to %d",
kReservedBytes)
.get()); return nullptr;
}
memcpy(pOut, a, kClearTextPrefixBytesOnFirstPage);
} else {
i = 0;
} constint payloadLength = nByte - kReservedBytes - i;
MOZ_ASSERT(payloadLength > 0); // XXX I guess this can be done in-place as well, then we don't need the // temporary page at all, I guess?
p->encryptCipherStrategy->Cipher(
Span{aIv}, Span{a + i, static_cast<unsigned>(payloadLength)},
Span{pOut + i, static_cast<unsigned>(payloadLength)});
memcpy(pOut + nByte - kReservedBytes, aIv, kIvBytes);
return pOut;
}
/* De-obfuscate a page using p->decryptCipherStrategy. ** ** The deobfuscation is done in-place. ** ** For pages that begin with the SQLite header text, the first ** 32 bytes are not deobfuscated.
*/ staticvoid obfsDecode(ObfsFile* p, /* File containing page to be obfuscated */
u8* a, /* database page to be obfuscated */ int nByte /* Bytes of content in a[]. Must be a multiple
of kBasicBlockSize. */
) { int i;
if (memcmp(a, "SQLite format 3", 16) == 0) {
i = kClearTextPrefixBytesOnFirstPage;
} else {
i = 0;
} constint payloadLength = nByte - kReservedBytes - i;
MOZ_ASSERT(payloadLength > 0);
p->decryptCipherStrategy->Cipher(
Span{a + nByte - kReservedBytes, kIvBytes},
Span{a + i, static_cast<unsigned>(payloadLength)},
Span{a + i, static_cast<unsigned>(payloadLength)});
memset(a + nByte - kReservedBytes, 0, kIvBytes);
}
/* ** Close an obfsucated file.
*/ staticint obfsClose(sqlite3_file* pFile) {
ObfsFile* p = (ObfsFile*)pFile; if (p->pPartner) {
MOZ_ASSERT(p->pPartner->pPartner == p);
p->pPartner->pPartner = nullptr;
p->pPartner = nullptr;
}
sqlite3_free(p->pTemp);
/* ** Sync an obfuscated file.
*/ staticint obfsSync(sqlite3_file* pFile, int flags) {
pFile = ORIGFILE(pFile); return pFile->pMethods->xSync(pFile, flags);
}
/* ** Return the current file-size of an obfuscated file.
*/ staticint obfsFileSize(sqlite3_file* pFile, sqlite_int64* pSize) {
ObfsFile* p = (ObfsFile*)pFile;
pFile = ORIGFILE(p); return pFile->pMethods->xFileSize(pFile, pSize);
}
/* ** Lock an obfuscated file.
*/ staticint obfsLock(sqlite3_file* pFile, int eLock) {
pFile = ORIGFILE(pFile); return pFile->pMethods->xLock(pFile, eLock);
}
/* ** Unlock an obfuscated file.
*/ staticint obfsUnlock(sqlite3_file* pFile, int eLock) {
pFile = ORIGFILE(pFile); return pFile->pMethods->xUnlock(pFile, eLock);
}
/* ** Check if another file-handle holds a RESERVED lock on an obfuscated file.
*/ staticint obfsCheckReservedLock(sqlite3_file* pFile, int* pResOut) {
pFile = ORIGFILE(pFile); return pFile->pMethods->xCheckReservedLock(pFile, pResOut);
}
/* ** File control method. For custom operations on an obfuscated file.
*/ staticint obfsFileControl(sqlite3_file* pFile, int op, void* pArg) { int rc;
ObfsFile* p = (ObfsFile*)pFile;
pFile = ORIGFILE(pFile); if (op == SQLITE_FCNTL_PRAGMA) { char** azArg = (char**)pArg;
MOZ_ASSERT(azArg[1] != nullptr); if (azArg[2] != nullptr && sqlite3_stricmp(azArg[1], "page_size") == 0) { /* Do not allow page size changes on an obfuscated database */ return SQLITE_OK;
}
} elseif (op == SQLITE_FCNTL_CKPT_START || op == SQLITE_FCNTL_CKPT_DONE) {
p->inCkpt = op == SQLITE_FCNTL_CKPT_START; if (p->pPartner) {
p->pPartner->inCkpt = p->inCkpt;
}
}
rc = pFile->pMethods->xFileControl(pFile, op, pArg); if (rc == SQLITE_OK && op == SQLITE_FCNTL_VFSNAME) {
*(char**)pArg = sqlite3_mprintf("obfs/%z", *(char**)pArg);
} return rc;
}
/* ** Return the sector-size in bytes for an obfuscated file.
*/ staticint obfsSectorSize(sqlite3_file* pFile) {
pFile = ORIGFILE(pFile); return pFile->pMethods->xSectorSize(pFile);
}
/* ** Return the device characteristic flags supported by an obfuscated file.
*/ staticint obfsDeviceCharacteristics(sqlite3_file* pFile) { int dc;
pFile = ORIGFILE(pFile);
dc = pFile->pMethods->xDeviceCharacteristics(pFile); return dc & ~SQLITE_IOCAP_SUBPAGE_READ; /* All except the
SQLITE_IOCAP_SUBPAGE_READ bit */
}
/* Create a shared memory file mapping */ staticint obfsShmMap(sqlite3_file* pFile, int iPg, int pgsz, int bExtend, voidvolatile** pp) {
pFile = ORIGFILE(pFile); return pFile->pMethods->xShmMap(pFile, iPg, pgsz, bExtend, pp);
}
/* Perform locking on a shared-memory segment */ staticint obfsShmLock(sqlite3_file* pFile, int offset, int n, int flags) {
pFile = ORIGFILE(pFile); return pFile->pMethods->xShmLock(pFile, offset, n, flags);
}
/* ** Translate a single byte of Hex into an integer. ** This routine only works if h really is a valid hexadecimal ** character: 0..9a..fA..F
*/ static u8 obfsHexToInt(int h) {
MOZ_ASSERT((h >= '0' && h <= '9') || (h >= 'a' && h <= 'f') ||
(h >= 'A' && h <= 'F')); #if 1 /* ASCII */
h += 9 * (1 & (h >> 6)); #else/* EBCDIC */
h += 9 * (1 & ~(h >> 4)); #endif return (u8)(h & 0xf);
}
/* ** Open a new file. ** ** If the file is an ordinary database file, or a rollback or WAL journal ** file, and if the key=XXXX parameter exists, then try to open the file ** as an obfuscated database. All other open attempts fall through into ** the lower-level VFS shim. ** ** If the key=XXXX parameter exists but is not 64-bytes of hex key, then ** put an error message in NS_WARNING() and return SQLITE_CANTOPEN.
*/ staticint obfsOpen(sqlite3_vfs* pVfs, constchar* zName, sqlite3_file* pFile, int flags, int* pOutFlags) {
ObfsFile* p;
sqlite3_file* pSubFile;
sqlite3_vfs* pSubVfs; int rc, i; constchar* zKey;
u8 aKey[kKeyBytes];
pSubVfs = ORIGVFS(pVfs); if (flags &
(SQLITE_OPEN_MAIN_DB | SQLITE_OPEN_WAL | SQLITE_OPEN_MAIN_JOURNAL)) {
zKey = sqlite3_uri_parameter(zName, "key");
} else {
zKey = nullptr;
} if (zKey == nullptr) { return pSubVfs->xOpen(pSubVfs, zName, pFile, flags, pOutFlags);
} for (i = 0;
i < kKeyBytes && isxdigit(zKey[i * 2]) && isxdigit(zKey[i * 2 + 1]);
i++) {
aKey[i] = (obfsHexToInt(zKey[i * 2]) << 4) | obfsHexToInt(zKey[i * 2 + 1]);
} if (i != kKeyBytes) {
NS_WARNING(
nsPrintfCString("invalid query parameter on %s: key=%s", zName, zKey)
.get()); return SQLITE_CANTOPEN;
}
p = (ObfsFile*)pFile;
memset(p, 0, sizeof(*p));
auto encryptCipherStrategy = MakeUnique<IPCStreamCipherStrategy>(); auto decryptCipherStrategy = MakeUnique<IPCStreamCipherStrategy>();
auto resetMethods = MakeScopeExit([pFile] { pFile->pMethods = nullptr; });
if (NS_WARN_IF(NS_FAILED(encryptCipherStrategy->Init(
CipherMode::Encrypt, Span{aKey, sizeof(aKey)},
IPCStreamCipherStrategy::MakeBlockPrefix())))) { return SQLITE_ERROR;
}
if (NS_WARN_IF(NS_FAILED(decryptCipherStrategy->Init(
CipherMode::Decrypt, Span{aKey, sizeof(aKey)})))) { return SQLITE_ERROR;
}
#ifdef DEBUG // If the VFS version is higher than the last known one, you should update // this VFS adding appropriate methods for any methods added in the version // change. static constexpr int kLastKnownVfsVersion = 3;
MOZ_ASSERT(pOrig->iVersion <= kLastKnownVfsVersion); #endif
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.