implementation of PRAGMA cipher_integrity_check and associated tests

This commit is contained in:
Stephen Lombardo 2019-05-15 10:24:27 -04:00
parent de558b049f
commit 488d81e8da
7 changed files with 424 additions and 78 deletions

View File

@ -1,6 +1,9 @@
# SQLCipher Change Log
All notable changes to this project will be documented in this file.
## [unreleased] - (TBD - [unreleased])
- Adds PRAGMA cipher_integrity_check to perform external verification of page HMACs
## [4.1.0] - (March 2019 - [4.1.0 changes])
- Defer reading salt from header until key derivation is triggered
- Clarify usage of sqlite3_rekey for plaintext databases in header

View File

@ -629,6 +629,11 @@ int sqlcipher_codec_pragma(sqlite3* db, int iDb, Parse *pParse, const char *zLef
pragma = sqlite3_mprintf("PRAGMA cipher_default_kdf_algorithm = %s;", SQLCIPHER_PBKDF2_HMAC_SHA512_LABEL);
}
codec_vdbe_return_string(pParse, "pragma", pragma, P4_DYNAMIC);
}else
if( sqlite3StrICmp(zLeft,"cipher_integrity_check")==0 ){
if(ctx) {
sqlcipher_codec_ctx_integrity_check(ctx, pParse, "cipher_integrity_check");
}
}else {
return 0;
}

View File

@ -289,6 +289,8 @@ int sqlcipher_get_mem_security(void);
int sqlcipher_find_db_index(sqlite3 *db, const char *zDb);
int sqlcipher_codec_ctx_integrity_check(codec_ctx *, Parse *, char *);
#endif
#endif
/* END SQLCIPHER */

View File

@ -1254,6 +1254,87 @@ cleanup:
return rc;
}
int sqlcipher_codec_ctx_integrity_check(codec_ctx *ctx, Parse *pParse, char *column) {
Pgno page = 1;
int i, trans_rc, rc = 0;
char *result;
unsigned char *hmac_out = NULL;
sqlite3_file *fd = sqlite3PagerFile(ctx->pBt->pBt->pPager);
i64 file_sz;
Vdbe *v = sqlite3GetVdbe(pParse);
sqlite3VdbeSetNumCols(v, 1);
sqlite3VdbeSetColName(v, 0, COLNAME_NAME, column, SQLITE_STATIC);
if(fd == NULL || fd->pMethods == 0) {
sqlite3VdbeAddOp4(v, OP_String8, 0, 1, 0, "database file is undefined", P4_TRANSIENT);
sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1);
goto cleanup;
}
if(!(ctx->flags & CIPHER_FLAG_HMAC)) {
sqlite3VdbeAddOp4(v, OP_String8, 0, 1, 0, "HMAC is not enabled, unable to integrity check", P4_TRANSIENT);
sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1);
goto cleanup;
}
if((rc = sqlcipher_codec_key_derive(ctx)) != SQLITE_OK) {
sqlite3VdbeAddOp4(v, OP_String8, 0, 1, 0, "unable to derive keys", P4_TRANSIENT);
sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1);
goto cleanup;
}
/* establish an exclusive lock on the database */
if((trans_rc = sqlite3BtreeBeginTrans(ctx->pBt, 2, 0)) != SQLITE_OK) {
sqlite3VdbeAddOp4(v, OP_String8, 0, 1, 0, "unable to lock database", P4_TRANSIENT);
sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1);
goto cleanup;
}
sqlite3OsFileSize(fd, &file_sz);
hmac_out = sqlcipher_malloc(ctx->hmac_sz);
for(page = 1; page <= file_sz / ctx->page_sz; page++) {
int offset = (page - 1) * ctx->page_sz;
int payload_sz = ctx->page_sz - ctx->reserve_sz + ctx->iv_sz;
int read_sz = ctx->page_sz;
if(page==1) {
int page1_offset = ctx->plaintext_header_sz ? ctx->plaintext_header_sz : FILE_HEADER_SZ;
read_sz = read_sz - page1_offset;
payload_sz = payload_sz - page1_offset;
offset += page1_offset;
}
sqlcipher_memset(ctx->buffer, 0, ctx->page_sz);
sqlcipher_memset(hmac_out, 0, ctx->hmac_sz);
if(sqlite3OsRead(fd, ctx->buffer, read_sz, offset) != SQLITE_OK) {
result = sqlite3_mprintf("error reading %d bytes from file page %d at offset %d\n", read_sz, page, offset);
sqlite3VdbeAddOp4(v, OP_String8, 0, 1, 0, result, P4_DYNAMIC);
sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1);
} else if(sqlcipher_page_hmac(ctx, ctx->read_ctx, page, ctx->buffer, payload_sz, hmac_out) != SQLITE_OK) {
result = sqlite3_mprintf("HMAC operation failed for page %d", page);
sqlite3VdbeAddOp4(v, OP_String8, 0, 1, 0, result, P4_DYNAMIC);
sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1);
} else if(sqlcipher_memcmp(ctx->buffer + payload_sz, hmac_out, ctx->hmac_sz) != 0) {
result = sqlite3_mprintf("HMAC verification failed for page %d", page);
sqlite3VdbeAddOp4(v, OP_String8, 0, 1, 0, result, P4_DYNAMIC);
sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1);
}
}
if(file_sz % ctx->page_sz != 0) {
result = sqlite3_mprintf("page %d has an invalid size of %d bytes", page, file_sz - ((file_sz / ctx->page_sz) * ctx->page_sz));
sqlite3VdbeAddOp4(v, OP_String8, 0, 1, 0, result, P4_DYNAMIC);
sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1);
}
cleanup:
if(trans_rc == SQLITE_OK) sqlite3BtreeRollback(ctx->pBt, SQLITE_OK, 0);
if(hmac_out != NULL) sqlcipher_free(hmac_out, ctx->hmac_sz);
return SQLITE_OK;
}
int sqlcipher_codec_ctx_migrate(codec_ctx *ctx) {
int i, pass_sz, keyspec_sz, nRes, user_version, rc, oflags;
Db *pDb = 0;

View File

@ -531,84 +531,6 @@ do_test custom-pagesize-must-match {
db close
file delete -force test.db
# 1. create a database and insert a bunch of data, close the database
# 2. seek to the middle of a database page and write some junk
# 3. Open the database and verify that the database is no longer readable
do_test hmac-tamper-resistence {
sqlite_orig db test.db
execsql {
PRAGMA key = 'testkey';
CREATE table t1(a,b);
BEGIN;
}
for {set i 1} {$i<=1000} {incr i} {
set r [expr {int(rand()*500000)}]
execsql "INSERT INTO t1 VALUES($i,'value $r');"
}
execsql {
COMMIT;
}
db close
# write some junk into the hmac segment, leaving
# the page data valid but with an invalid signature
hexio_write test.db 1000 0000
sqlite_orig db test.db
catchsql {
PRAGMA key = 'testkey';
SELECT count(*) FROM t1;
}
} {1 {file is not a database}}
db close
file delete -force test.db
# 1. create a database and insert a bunch of data, close the database
# 2. seek to the middle of a database page and write some junk
# 3. Open the database and verify that the database is still readable
do_test nohmac-not-tamper-resistent {
sqlite_orig db test.db
execsql {
PRAGMA key = 'testkey';
PRAGMA cipher_use_hmac = OFF;
PRAGMA cipher_page_size = 1024;
CREATE table t1(a,b);
BEGIN;
}
for {set i 1} {$i<=1000} {incr i} {
set r [expr {int(rand()*500000)}]
execsql "INSERT INTO t1 VALUES($i,'value $r');"
}
execsql {
COMMIT;
}
db close
# write some junk into the middle of the page
hexio_write test.db 2560 00
sqlite_orig db test.db
execsql {
PRAGMA key = 'testkey';
PRAGMA cipher_use_hmac = OFF;
PRAGMA cipher_page_size = 1024;
SELECT count(*) FROM t1;
}
} {1000}
db close
file delete -force test.db
# 1. create a database with WAL journal mode
# 2. create table and insert operations should work

View File

@ -0,0 +1,332 @@
# SQLCipher
# codec.test developed by Stephen Lombardo (Zetetic LLC)
# sjlombardo at zetetic dot net
# http://zetetic.net
#
# Copyright (c) 2018, ZETETIC LLC
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the ZETETIC LLC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY ZETETIC LLC ''AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL ZETETIC LLC BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# This file implements regression tests for SQLite library. The
# focus of this script is testing code cipher features.
#
# NOTE: tester.tcl has overridden the definition of sqlite3 to
# automatically pass in a key value. Thus tests in this file
# should explicitly close and open db with sqlite_orig in order
# to bypass default key assignment.
set testdir [file dirname $argv0]
source $testdir/tester.tcl
source $testdir/sqlcipher.tcl
# 1. create a database and insert a bunch of data, close the database
# 2. seek to the middle of the first database page and write some junk
# 3. Open the database and verify that the database is no longer readable
do_test hmac-tamper-resistence-first-page {
sqlite_orig db test.db
execsql {
PRAGMA key = 'testkey';
CREATE table t1(a,b);
BEGIN;
}
for {set i 1} {$i<=1000} {incr i} {
set r [expr {int(rand()*500000)}]
execsql "INSERT INTO t1 VALUES($i,'value $r');"
}
execsql {
COMMIT;
}
db close
# write some junk into the hmac segment, leaving
# the page data valid but with an invalid signature
hexio_write test.db 1000 0000
sqlite_orig db test.db
catchsql {
PRAGMA key = 'testkey';
SELECT count(*) FROM t1;
}
} {1 {file is not a database}}
db close
file delete -force test.db
# 1. create a database and insert a bunch of data, close the database
# 2. seek to the middle of a database page and write some junk
# 3. Open the database and verify that the database is still readable
do_test nohmac-not-tamper-resistent {
sqlite_orig db test.db
execsql {
PRAGMA key = 'testkey';
PRAGMA cipher_use_hmac = OFF;
PRAGMA cipher_page_size = 1024;
CREATE table t1(a,b);
BEGIN;
}
for {set i 1} {$i<=1000} {incr i} {
set r [expr {int(rand()*500000)}]
execsql "INSERT INTO t1 VALUES($i,'value $r');"
}
execsql {
COMMIT;
}
db close
# write some junk into the middle of the page
hexio_write test.db 2560 00
sqlite_orig db test.db
execsql {
PRAGMA key = 'testkey';
PRAGMA cipher_use_hmac = OFF;
PRAGMA cipher_page_size = 1024;
SELECT count(*) FROM t1;
}
} {1000}
db close
file delete -force test.db
# 1. create a database and insert a bunch of data, close the database
# 2. seek to the middle of a database page (not the first page) and write bad data
# 3. Open the database and verify that the database is no longer readable
do_test hmac-tamper-resistence {
sqlite_orig db test.db
execsql {
PRAGMA key = 'testkey';
CREATE table t1(a,b);
BEGIN;
}
for {set i 1} {$i<=1000} {incr i} {
set r [expr {int(rand()*500000)}]
execsql "INSERT INTO t1 VALUES($i,'value $r');"
}
execsql {
COMMIT;
}
db close
# write some junk into the hmac segment, leaving
# the page data valid but with an invalid signature
hexio_write test.db 16500 0000
sqlite_orig db test.db
catchsql {
PRAGMA key = 'testkey';
SELECT count(*) FROM t1;
}
} {1 {database disk image is malformed}}
db close
file delete -force test.db
# try cipher_integrity_check on an in-memory database
# which should fail because the file doesn't exist
do_test memory-integrity-check-should-fail {
sqlite_orig db :memory:
execsql {
PRAGMA key = 'testkey';
CREATE TABLE t1(a,b);
INSERT INTO t1(a,b) values (1,2);
PRAGMA cipher_integrity_check;
}
} {{database file is undefined}}
db close
# try cipher_integrity_check on a valid 1.1.8 database
# should fail because version 1.0 doesn't use HMAC
do_test version-1-integrity-check-fail-no-hmac {
file copy -force $sampleDir/sqlcipher-1.1.8-testkey.db test.db
sqlite_orig db test.db
execsql {
PRAGMA key = 'testkey';
PRAGMA cipher_compatibility = 1;
PRAGMA cipher_integrity_check;
}
} {{HMAC is not enabled, unable to integrity check}}
db close
file delete -force test.db
# try cipher_integrity_check on a valid 2 database
do_test version-2-integrity-check-valid {
file copy -force $sampleDir/sqlcipher-2.0-le-testkey.db test.db
sqlite_orig db test.db
execsql {
PRAGMA key = 'testkey';
PRAGMA cipher_compatibility = 2;
PRAGMA cipher_integrity_check;
}
} {}
db close
file delete -force test.db
# try cipher_integrity_check on a corrupted version 2 database
do_test version-2-integrity-check-invalid {
file copy -force $sampleDir/sqlcipher-2.0-le-testkey.db test.db
hexio_write test.db 8202 00
hexio_write test.db 10250 00
sqlite_orig db test.db
execsql {
PRAGMA key = 'testkey';
PRAGMA cipher_compatibility = 2;
PRAGMA cipher_integrity_check;
}
} {{HMAC verification failed for page 9} {HMAC verification failed for page 11}}
db close
file delete -force test.db
# try cipher_integrity_check on a valid version 3 database
do_test version-3-integrity-check-valid {
file copy -force $sampleDir/sqlcipher-3.0-testkey.db test.db
sqlite_orig db test.db
execsql {
PRAGMA key = 'testkey';
PRAGMA cipher_compatibility = 3;
PRAGMA cipher_integrity_check;
}
} {}
db close
file delete -force test.db
# try cipher_integrity_check on a corrupted version 3 database
do_test version-3-integrity-check-invalid {
file copy -force $sampleDir/sqlcipher-3.0-testkey.db test.db
hexio_write test.db 8202 00
hexio_write test.db 10250 00
sqlite_orig db test.db
execsql {
PRAGMA key = 'testkey';
PRAGMA cipher_compatibility = 3;
PRAGMA cipher_integrity_check;
}
} {{HMAC verification failed for page 9} {HMAC verification failed for page 11}}
db close
file delete -force test.db
# try cipher_integrity_check on a valid version 4 database
do_test version-4-integrity-check-valid {
file copy -force $sampleDir/sqlcipher-4.0-testkey.db test.db
sqlite_orig db test.db
execsql {
PRAGMA key = 'testkey';
PRAGMA cipher_integrity_check;
}
} {}
db close
file delete -force test.db
# try cipher_integrity_check on a corrupted version 4 database
do_test version-4-integrity-check-invalid {
file copy -force $sampleDir/sqlcipher-4.0-testkey.db test.db
# corrupt page data
hexio_write test.db 5120 00
# corrupt iv
hexio_write test.db 12208 00
# corrupt the mac segment
hexio_write test.db 16320 00
sqlite_orig db test.db
execsql {
PRAGMA key = 'testkey';
PRAGMA cipher_integrity_check;
}
} {{HMAC verification failed for page 2} {HMAC verification failed for page 3} {HMAC verification failed for page 4}}
db close
file delete -force test.db
# try cipher_integrity_check on a corrupted version 4 database
do_test version-4-integrity-check-invalid-last-page {
file copy -force $sampleDir/sqlcipher-4.0-testkey.db test.db
hexio_write test.db 978944 0000
sqlite_orig db test.db
execsql {
PRAGMA key = 'testkey';
PRAGMA cipher_integrity_check;
}
} {{page 240 has an invalid size of 2 bytes}}
db close
file delete -force test.db
# verify cipher_integrity_check works on a plaintext header db
do_test integrity-check-plaintext-header {
sqlite_orig db test.db
set rc {}
execsql {
PRAGMA key = 'test';
PRAGMA cipher_plaintext_header_size = 32;
CREATE TABLE t1(a,b);
INSERT INTO t1(a,b) VALUES (1,2);
}
lappend rc [execsql {
PRAGMA cipher_integrity_check;
}]
lappend rc [string equal [hexio_read test.db 16 5] "1000010150"]
hexio_write test.db 120 00
hexio_write test.db 5120 00
lappend rc [execsql {
PRAGMA cipher_integrity_check;
}]
} {{} 1 {{HMAC verification failed for page 1} {HMAC verification failed for page 2}}}
file delete -force test.db
# verify database locking for cipher_integrity_check
do_test integrity-check-locking {
sqlite_orig db test.db
sqlite_orig db2 test.db
execsql {
PRAGMA key = 'test';
CREATE TABLE t1(a,b);
BEGIN EXCLUSIVE;
INSERT INTO t1(a,b) VALUES (1,2);
}
execsql {
PRAGMA key = 'test';
PRAGMA cipher_integrity_check;
} db2
} {{unable to lock database}}
sqlite_orig db test.db
sqlite_orig db2 test.db
file delete -force test.db
finish_test

View File

@ -48,5 +48,6 @@ slave_test_file $testdir/sqlcipher-rekey.test
slave_test_file $testdir/sqlcipher-plaintext-header.test
slave_test_file $testdir/sqlcipher-rekey.test
slave_test_file $testdir/sqlcipher-pragmas.test
slave_test_file $testdir/sqlcipher-integrity.test
finish_test