Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add additional SQLCipher configuration options #497

Merged
merged 11 commits into from
Mar 8, 2019
21 changes: 21 additions & 0 deletions GRDB/Core/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,27 @@ public struct Configuration {
///
/// Default: nil
public var passphrase: String?

// Valid options for the cipher_page_size setting
public enum CipherPageSize: Int {
case pageSize1K = 1024
case pageSize2K = 2048
case pageSize4K = 4096
case pageSize8K = 8192
case pageSize16K = 16384
case pageSize32K = 32768
case pageSize64K = 65536
}

/// The cipher_page_size for encrypted databases
///
/// Default: .pageSize1K - this corresponds to the default used until now in SQLCipher/GRDBCipher 3
public var cipherPageSize: CipherPageSize = .pageSize1K
groue marked this conversation as resolved.
Show resolved Hide resolved

/// The kdf_iter setting for encrypted database
///
/// Default: 64000 - this corresponds to the default used until now in SQLCipher/GRDBCipher 3
public var KDFIterations: Int = 64000
groue marked this conversation as resolved.
Show resolved Hide resolved
#endif


Expand Down
32 changes: 32 additions & 0 deletions GRDB/Core/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ public final class Database {
try Database.validateSQLCipher(sqliteConnection)
if let passphrase = configuration.passphrase {
try Database.set(passphrase: passphrase, forConnection: sqliteConnection)
try Database.set(cipherPageSize: configuration.cipherPageSize, forConnection: sqliteConnection)
try Database.set(kdfIterations: configuration.KDFIterations, forConnection: sqliteConnection)
}
#endif
try Database.validateDatabaseFormat(sqliteConnection)
Expand Down Expand Up @@ -261,6 +263,36 @@ extension Database {
throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection)))
}
}

private static func set(cipherPageSize: Configuration.CipherPageSize, forConnection sqliteConnection: SQLiteConnection) throws {
var sqliteStatement: SQLiteStatement? = nil
let code = sqlite3_prepare_v2(sqliteConnection, "PRAGMA cipher_page_size = \(cipherPageSize.rawValue)", -1, &sqliteStatement, nil)
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection)))
}
defer {
groue marked this conversation as resolved.
Show resolved Hide resolved
sqlite3_finalize(sqliteStatement)
}
let step = sqlite3_step(sqliteStatement)
if step != SQLITE_DONE {
throw DatabaseError(resultCode: .SQLITE_MISUSE, message: "Unable to set cipher_page_size")
groue marked this conversation as resolved.
Show resolved Hide resolved
}
}

private static func set(kdfIterations: Int, forConnection sqliteConnection: SQLiteConnection) throws {
var sqliteStatement: SQLiteStatement? = nil
let code = sqlite3_prepare_v2(sqliteConnection, "PRAGMA kdf_iter = \(kdfIterations)", -1, &sqliteStatement, nil)
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection)))
}
defer {
sqlite3_finalize(sqliteStatement)
}
let step = sqlite3_step(sqliteStatement)
if step != SQLITE_DONE {
throw DatabaseError(resultCode: .SQLITE_MISUSE, message: "Unable to set kdf_iter")
groue marked this conversation as resolved.
Show resolved Hide resolved
}
}
#endif

private static func validateDatabaseFormat(_ sqliteConnection: SQLiteConnection) throws {
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7303,6 +7303,30 @@ try clearDBQueue.inDatabase { db in
// Now the copy is done, and the clear-text database can be deleted.
```

**Advanced configuration options for SQLCipher**
groue marked this conversation as resolved.
Show resolved Hide resolved
There are two advanced configuration options that you can set for configuring SQLCipher that control aspects of the encryption and key generation process.

```swift
var configuartion = Configuration()
groue marked this conversation as resolved.
Show resolved Hide resolved
configuration.passphrase = "secret"
configuration.cipherPageSize = .pageSize4K
configuration.KDFIterations = 128000
groue marked this conversation as resolved.
Show resolved Hide resolved
let dbQueue = try DatabaseQueue(path: "...", configuration: configuration)
```
***cipherPageSize***
The `cipherPageSize` is used to adjust the page size for the encrypted database (this corresponds to the SQLCipher `PRAGMA cipher_page_size` configuration option). Increasing the page size can noticeably improve performance for certain queries that access large numbers of pages.
groue marked this conversation as resolved.
Show resolved Hide resolved

WARNING: The same `cipherPageSize` must be supplied every time that the database file is open; attempting to access the database without setting the proper `cipherPageSize` will result in the `SQLite error 26: file is encrypted or is not a database` error being thrown.

The default `cipherPageSize` in the current version of SQLCipher used in GRDB.swift is `.pageSize1K`.

***KDFIterations***
The `KDFIterations` value is used to adjust the number of iterations that the PBKDF2 key derivation is run to derive the key from the `passphrase` supplied (this corresponds to the SQLCipher `PRAGMA kdf_iter` configuration option).

WARNING: The same `KDFIterations` must be supplied every time that the database file is open; attempting to access the database without setting the proper `KDFIterations` will result in the `SQLite error 26: file is encrypted or is not a database` error being thrown.

The default `KDFIterations` in the current version of SQLCipher used in GRDB.swift is `64000`. It is not recommend to reduce the number of iterations used from the default.


## Backup

Expand Down
56 changes: 56 additions & 0 deletions Tests/GRDBTests/EncryptionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,62 @@ class EncryptionTests: GRDBTestCase {
}
}

func testCipherPageSize() throws {
groue marked this conversation as resolved.
Show resolved Hide resolved
do {
dbConfiguration.passphrase = "secret"
dbConfiguration.cipherPageSize = .pageSize8K

let dbQueue = try makeDatabaseQueue(filename: "test.sqlite")
try dbQueue.inDatabase({ db in
XCTAssertEqual(try Int.fetchOne(db, "PRAGMA cipher_page_size")!, 8192)
})
}

do {
dbConfiguration.cipherPageSize = .pageSize4K

let dbQueue = try makeDatabasePool(filename: "testpool.sqlite")
try dbQueue.write({ db in
XCTAssertEqual(try Int.fetchOne(db, "PRAGMA cipher_page_size")!, 4096)
try db.execute("CREATE TABLE data(value INTEGER)")
try db.execute("INSERT INTO data(value) VALUES(1)")
})
try dbQueue.read({ db in
XCTAssertEqual(try Int.fetchOne(db, "PRAGMA cipher_page_size")!, 4096)
XCTAssertEqual(try Int.fetchOne(db, "SELECT value FROM data"), 1)
})

}
}

func testCipherKDFSettings() throws {
do {
dbConfiguration.passphrase = "secret"
dbConfiguration.KDFIterations = 128000

let dbQueue: DatabaseQueue? = try makeDatabaseQueue(filename: "test.sqlite")
try dbQueue!.inDatabase { db in
XCTAssertEqual(try Int.fetchOne(db, "PRAGMA kdf_iter"), 128000)
}
}

do {
dbConfiguration.KDFIterations = 128000

let dbQueue: DatabasePool? = try makeDatabasePool(filename: "testpool.sqlite")
try dbQueue!.write { db in
XCTAssertEqual(try Int.fetchOne(db, "PRAGMA kdf_iter"), 128000)
try db.execute("CREATE TABLE data(value INTEGER)")
try db.execute("INSERT INTO data(value) VALUES(1)")
}

try dbQueue!.read { db in
XCTAssertEqual(try Int.fetchOne(db, "PRAGMA kdf_iter"), 128000)
XCTAssertEqual(try Int.fetchOne(db, "SELECT value FROM data"), 1)
}
}
}

func testExportPlainTextDatabaseToEncryptedDatabase() throws {
// See https://discuss.zetetic.net/t/how-to-encrypt-a-plaintext-sqlite-database-to-use-sqlcipher-and-avoid-file-is-encrypted-or-is-not-a-database-errors/868?source_topic_id=939
do {
Expand Down