diff --git a/CHANGELOG.md b/CHANGELOG.md index 07c0c2e..bf68545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/crypto.c b/src/crypto.c index 6ba85ae..64d1eae 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -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; } diff --git a/src/crypto.h b/src/crypto.h index b32922d..efb9213 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -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 */ diff --git a/src/crypto_impl.c b/src/crypto_impl.c index 2f8797c..53d7705 100644 --- a/src/crypto_impl.c +++ b/src/crypto_impl.c @@ -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; diff --git a/test/sqlcipher-core.test b/test/sqlcipher-core.test index 63ba1b1..f7c9403 100644 --- a/test/sqlcipher-core.test +++ b/test/sqlcipher-core.test @@ -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 diff --git a/test/sqlcipher-integrity.test b/test/sqlcipher-integrity.test new file mode 100644 index 0000000..ec6856b --- /dev/null +++ b/test/sqlcipher-integrity.test @@ -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 diff --git a/test/sqlcipher.test b/test/sqlcipher.test index ce0dcc4..200564c 100644 --- a/test/sqlcipher.test +++ b/test/sqlcipher.test @@ -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