Skip to content

Commit 488d81e

Browse files
committed
implementation of PRAGMA cipher_integrity_check and associated tests
1 parent de558b0 commit 488d81e

7 files changed

Lines changed: 424 additions & 78 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# SQLCipher Change Log
22
All notable changes to this project will be documented in this file.
33

4+
## [unreleased] - (TBD - [unreleased])
5+
- Adds PRAGMA cipher_integrity_check to perform external verification of page HMACs
6+
47
## [4.1.0] - (March 2019 - [4.1.0 changes])
58
- Defer reading salt from header until key derivation is triggered
69
- Clarify usage of sqlite3_rekey for plaintext databases in header

src/crypto.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,11 @@ int sqlcipher_codec_pragma(sqlite3* db, int iDb, Parse *pParse, const char *zLef
629629
pragma = sqlite3_mprintf("PRAGMA cipher_default_kdf_algorithm = %s;", SQLCIPHER_PBKDF2_HMAC_SHA512_LABEL);
630630
}
631631
codec_vdbe_return_string(pParse, "pragma", pragma, P4_DYNAMIC);
632+
}else
633+
if( sqlite3StrICmp(zLeft,"cipher_integrity_check")==0 ){
634+
if(ctx) {
635+
sqlcipher_codec_ctx_integrity_check(ctx, pParse, "cipher_integrity_check");
636+
}
632637
}else {
633638
return 0;
634639
}

src/crypto.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ int sqlcipher_get_mem_security(void);
289289

290290
int sqlcipher_find_db_index(sqlite3 *db, const char *zDb);
291291

292+
int sqlcipher_codec_ctx_integrity_check(codec_ctx *, Parse *, char *);
293+
292294
#endif
293295
#endif
294296
/* END SQLCIPHER */

src/crypto_impl.c

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,6 +1254,87 @@ static int sqlcipher_check_connection(const char *filename, char *key, int key_s
12541254
return rc;
12551255
}
12561256

1257+
int sqlcipher_codec_ctx_integrity_check(codec_ctx *ctx, Parse *pParse, char *column) {
1258+
Pgno page = 1;
1259+
int i, trans_rc, rc = 0;
1260+
char *result;
1261+
unsigned char *hmac_out = NULL;
1262+
sqlite3_file *fd = sqlite3PagerFile(ctx->pBt->pBt->pPager);
1263+
i64 file_sz;
1264+
1265+
Vdbe *v = sqlite3GetVdbe(pParse);
1266+
sqlite3VdbeSetNumCols(v, 1);
1267+
sqlite3VdbeSetColName(v, 0, COLNAME_NAME, column, SQLITE_STATIC);
1268+
1269+
if(fd == NULL || fd->pMethods == 0) {
1270+
sqlite3VdbeAddOp4(v, OP_String8, 0, 1, 0, "database file is undefined", P4_TRANSIENT);
1271+
sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1);
1272+
goto cleanup;
1273+
}
1274+
1275+
if(!(ctx->flags & CIPHER_FLAG_HMAC)) {
1276+
sqlite3VdbeAddOp4(v, OP_String8, 0, 1, 0, "HMAC is not enabled, unable to integrity check", P4_TRANSIENT);
1277+
sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1);
1278+
goto cleanup;
1279+
}
1280+
1281+
if((rc = sqlcipher_codec_key_derive(ctx)) != SQLITE_OK) {
1282+
sqlite3VdbeAddOp4(v, OP_String8, 0, 1, 0, "unable to derive keys", P4_TRANSIENT);
1283+
sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1);
1284+
goto cleanup;
1285+
}
1286+
1287+
/* establish an exclusive lock on the database */
1288+
if((trans_rc = sqlite3BtreeBeginTrans(ctx->pBt, 2, 0)) != SQLITE_OK) {
1289+
sqlite3VdbeAddOp4(v, OP_String8, 0, 1, 0, "unable to lock database", P4_TRANSIENT);
1290+
sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1);
1291+
goto cleanup;
1292+
}
1293+
1294+
sqlite3OsFileSize(fd, &file_sz);
1295+
hmac_out = sqlcipher_malloc(ctx->hmac_sz);
1296+
1297+
for(page = 1; page <= file_sz / ctx->page_sz; page++) {
1298+
int offset = (page - 1) * ctx->page_sz;
1299+
int payload_sz = ctx->page_sz - ctx->reserve_sz + ctx->iv_sz;
1300+
int read_sz = ctx->page_sz;
1301+
1302+
if(page==1) {
1303+
int page1_offset = ctx->plaintext_header_sz ? ctx->plaintext_header_sz : FILE_HEADER_SZ;
1304+
read_sz = read_sz - page1_offset;
1305+
payload_sz = payload_sz - page1_offset;
1306+
offset += page1_offset;
1307+
}
1308+
1309+
sqlcipher_memset(ctx->buffer, 0, ctx->page_sz);
1310+
sqlcipher_memset(hmac_out, 0, ctx->hmac_sz);
1311+
if(sqlite3OsRead(fd, ctx->buffer, read_sz, offset) != SQLITE_OK) {
1312+
result = sqlite3_mprintf("error reading %d bytes from file page %d at offset %d\n", read_sz, page, offset);
1313+
sqlite3VdbeAddOp4(v, OP_String8, 0, 1, 0, result, P4_DYNAMIC);
1314+
sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1);
1315+
} else if(sqlcipher_page_hmac(ctx, ctx->read_ctx, page, ctx->buffer, payload_sz, hmac_out) != SQLITE_OK) {
1316+
result = sqlite3_mprintf("HMAC operation failed for page %d", page);
1317+
sqlite3VdbeAddOp4(v, OP_String8, 0, 1, 0, result, P4_DYNAMIC);
1318+
sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1);
1319+
} else if(sqlcipher_memcmp(ctx->buffer + payload_sz, hmac_out, ctx->hmac_sz) != 0) {
1320+
result = sqlite3_mprintf("HMAC verification failed for page %d", page);
1321+
sqlite3VdbeAddOp4(v, OP_String8, 0, 1, 0, result, P4_DYNAMIC);
1322+
sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1);
1323+
}
1324+
}
1325+
1326+
if(file_sz % ctx->page_sz != 0) {
1327+
result = sqlite3_mprintf("page %d has an invalid size of %d bytes", page, file_sz - ((file_sz / ctx->page_sz) * ctx->page_sz));
1328+
sqlite3VdbeAddOp4(v, OP_String8, 0, 1, 0, result, P4_DYNAMIC);
1329+
sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1);
1330+
}
1331+
1332+
cleanup:
1333+
if(trans_rc == SQLITE_OK) sqlite3BtreeRollback(ctx->pBt, SQLITE_OK, 0);
1334+
if(hmac_out != NULL) sqlcipher_free(hmac_out, ctx->hmac_sz);
1335+
return SQLITE_OK;
1336+
}
1337+
12571338
int sqlcipher_codec_ctx_migrate(codec_ctx *ctx) {
12581339
int i, pass_sz, keyspec_sz, nRes, user_version, rc, oflags;
12591340
Db *pDb = 0;

test/sqlcipher-core.test

Lines changed: 0 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -531,84 +531,6 @@ do_test custom-pagesize-must-match {
531531
db close
532532
file delete -force test.db
533533

534-
# 1. create a database and insert a bunch of data, close the database
535-
# 2. seek to the middle of a database page and write some junk
536-
# 3. Open the database and verify that the database is no longer readable
537-
do_test hmac-tamper-resistence {
538-
sqlite_orig db test.db
539-
540-
execsql {
541-
PRAGMA key = 'testkey';
542-
CREATE table t1(a,b);
543-
BEGIN;
544-
}
545-
546-
for {set i 1} {$i<=1000} {incr i} {
547-
set r [expr {int(rand()*500000)}]
548-
execsql "INSERT INTO t1 VALUES($i,'value $r');"
549-
}
550-
551-
execsql {
552-
COMMIT;
553-
}
554-
555-
db close
556-
557-
# write some junk into the hmac segment, leaving
558-
# the page data valid but with an invalid signature
559-
hexio_write test.db 1000 0000
560-
561-
sqlite_orig db test.db
562-
563-
catchsql {
564-
PRAGMA key = 'testkey';
565-
SELECT count(*) FROM t1;
566-
}
567-
568-
} {1 {file is not a database}}
569-
db close
570-
file delete -force test.db
571-
572-
# 1. create a database and insert a bunch of data, close the database
573-
# 2. seek to the middle of a database page and write some junk
574-
# 3. Open the database and verify that the database is still readable
575-
do_test nohmac-not-tamper-resistent {
576-
sqlite_orig db test.db
577-
578-
execsql {
579-
PRAGMA key = 'testkey';
580-
PRAGMA cipher_use_hmac = OFF;
581-
PRAGMA cipher_page_size = 1024;
582-
CREATE table t1(a,b);
583-
BEGIN;
584-
}
585-
586-
for {set i 1} {$i<=1000} {incr i} {
587-
set r [expr {int(rand()*500000)}]
588-
execsql "INSERT INTO t1 VALUES($i,'value $r');"
589-
}
590-
591-
execsql {
592-
COMMIT;
593-
}
594-
595-
db close
596-
597-
# write some junk into the middle of the page
598-
hexio_write test.db 2560 00
599-
600-
sqlite_orig db test.db
601-
602-
execsql {
603-
PRAGMA key = 'testkey';
604-
PRAGMA cipher_use_hmac = OFF;
605-
PRAGMA cipher_page_size = 1024;
606-
SELECT count(*) FROM t1;
607-
}
608-
609-
} {1000}
610-
db close
611-
file delete -force test.db
612534

613535
# 1. create a database with WAL journal mode
614536
# 2. create table and insert operations should work

0 commit comments

Comments
 (0)