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

Allow Database Connection Configuration #508

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one expection:
- [#499](https://github.com/groue/GRDB.swift/pull/499): Extract EncodableRecord from MutablePersistableRecord
- [#502](https://github.com/groue/GRDB.swift/pull/502): Rename Future to DatabaseFuture
- [#503](https://github.com/groue/GRDB.swift/pull/503): IFNULL support for association aggregates
- [#508](https://github.com/groue/GRDB.swift/pull/508) by [@michaelkirk-signal](https://github.com/michaelkirk-signal): Allow Database Connection Configuration
- [#510](https://github.com/groue/GRDB.swift/pull/510) by [@charlesmchen-signal](https://github.com/charlesmchen-signal): Expose DatabaseRegion(table:) initializer

### Fixed

- [#511](https://github.com/groue/GRDB.swift/pull/511) by [@charlesmchen-signal](https://github.com/charlesmchen-signal): Fix DatabasePool.setupMemoryManagement()


### Breaking Changes

- Swift 4.0 and Swift 4.1 are no longer supported
Expand Down
25 changes: 13 additions & 12 deletions GRDB/Core/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,22 @@ public struct Configuration {
/// Default: nil
public var passphrase: String?

/// The cipher_page_size setting for the encrypted database.
#endif

/// If set, allows custom configuration to be run every time
/// a new connection is opened.
///
/// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_page_size
/// This block is run after the Database's connection has opened, but
/// before that connection has been made available to any read/write
/// API's.
///
/// Default: 1024 - this corresponds to the default used by SQLCipher 3
public var cipherPageSize: Int = 1024

/// The kdf_iter setting for the encrypted database.
///
/// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#kdf_iter
/// For example:
///
/// Default: 64000 - this corresponds to the default used by SQLCipher 3
public var kdfIterations: Int = 64000
#endif

/// var config = Configuration()
/// config.prepareDatabase = { db in
/// try db.execute(sql: "PRAGMA kdf_iter = 10000")
/// }
public var prepareDatabase: ((Database) throws -> Void)?

// MARK: - Transactions

Expand Down
179 changes: 67 additions & 112 deletions GRDB/Core/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,28 +166,9 @@ public final class Database {
// MARK: - Initializer

init(path: String, configuration: Configuration, schemaCache: DatabaseSchemaCache) throws {
let sqliteConnection = try Database.openConnection(path: path, flags: configuration.SQLiteOpenFlags)
do {
try Database.activateExtendedCodes(sqliteConnection)
#if SQLITE_HAS_CODEC
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)
} catch {
Database.closeConnection(sqliteConnection)
throw error
}

self.sqliteConnection = sqliteConnection
self.sqliteConnection = try Database.openConnection(path: path, flags: configuration.SQLiteOpenFlags)
self.configuration = configuration
self.schemaCache = schemaCache

configuration.SQLiteConnectionDidOpen?()
}

deinit {
Expand Down Expand Up @@ -221,97 +202,6 @@ extension Database {
}
throw DatabaseError(resultCode: .SQLITE_INTERNAL) // WTF SQLite?
}

private static func activateExtendedCodes(_ sqliteConnection: SQLiteConnection) throws {
let code = sqlite3_extended_result_codes(sqliteConnection, 1)
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection)))
}
}

#if SQLITE_HAS_CODEC
private static func validateSQLCipher(_ sqliteConnection: SQLiteConnection) throws {
// https://discuss.zetetic.net/t/important-advisory-sqlcipher-with-xcode-8-and-new-sdks/1688
//
// > In order to avoid situations where SQLite might be used
// > improperly at runtime, we strongly recommend that
// > applications institute a runtime test to ensure that the
// > application is actually using SQLCipher on the active
// > connection.
var sqliteStatement: SQLiteStatement? = nil
let code = sqlite3_prepare_v2(sqliteConnection, "PRAGMA cipher_version", -1, &sqliteStatement, nil)
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection)))
}
defer {
sqlite3_finalize(sqliteStatement)
}
if sqlite3_step(sqliteStatement) != SQLITE_ROW || (sqlite3_column_text(sqliteStatement, 0) == nil) {
throw DatabaseError(resultCode: .SQLITE_MISUSE, message: """
GRDB is not linked against SQLCipher. \
Check https://discuss.zetetic.net/t/important-advisory-sqlcipher-with-xcode-8-and-new-sdks/1688
""")
}
}

private static func set(passphrase: String, forConnection sqliteConnection: SQLiteConnection) throws {
let data = passphrase.data(using: .utf8)!
#if swift(>=5.0)
let code = data.withUnsafeBytes {
sqlite3_key(sqliteConnection, $0.baseAddress, Int32($0.count))
}
#else
let code = data.withUnsafeBytes {
sqlite3_key(sqliteConnection, $0, Int32(data.count))
}
#endif
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection)))
}
}

private static func set(cipherPageSize: Int, forConnection sqliteConnection: SQLiteConnection) throws {
var sqliteStatement: SQLiteStatement? = nil
var code = sqlite3_prepare_v2(sqliteConnection, "PRAGMA cipher_page_size = \(cipherPageSize)", -1, &sqliteStatement, nil)
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection)))
}
defer {
sqlite3_finalize(sqliteStatement)
}
code = sqlite3_step(sqliteStatement)
if code != SQLITE_DONE {
throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection)))
}
}

private static func set(kdfIterations: Int, forConnection sqliteConnection: SQLiteConnection) throws {
var sqliteStatement: SQLiteStatement? = nil
var 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)
}
code = sqlite3_step(sqliteStatement)
if code != SQLITE_DONE {
throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection)))
}
}
#endif

private static func validateDatabaseFormat(_ sqliteConnection: SQLiteConnection) throws {
// Users are surprised when they open a picture as a database and
// see no error (https://github.com/groue/GRDB.swift/issues/54).
//
// So let's fail early if file is not a database, or encrypted with
// another passphrase.
let code = sqlite3_exec(sqliteConnection, "SELECT * FROM sqlite_master LIMIT 1", nil, nil, nil)
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection)))
}
}
}

extension Database {
Expand All @@ -327,6 +217,21 @@ extension Database {
setupDefaultFunctions()
setupDefaultCollations()
observationBroker.installCommitAndRollbackHooks()
try activateExtendedCodes()

#if SQLITE_HAS_CODEC
try validateSQLCipher()
if let passphrase = configuration.passphrase {
try setCipherPassphrase(passphrase)
}
#endif

// Last step before we can start accessing the database.
// This is the opportunity to run SQLCipher configuration
// pragmas such as cipher_page_size, for example.
try configuration.prepareDatabase?(self)
try validateFormat()
configuration.SQLiteConnectionDidOpen?()
}

private func setupTrace() {
Expand Down Expand Up @@ -381,7 +286,7 @@ extension Database {
db.configuration.trace!(sql)
return SQLITE_OK
}

private func setupForeignKeys() throws {
// Foreign keys are disabled by default with SQLite3
if configuration.foreignKeysEnabled {
Expand Down Expand Up @@ -431,6 +336,56 @@ extension Database {
add(collation: .localizedCompare)
add(collation: .localizedStandardCompare)
}

private func activateExtendedCodes() throws {
let code = sqlite3_extended_result_codes(sqliteConnection, 1)
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection)))
}
}

#if SQLITE_HAS_CODEC
private func validateSQLCipher() throws {
// https://discuss.zetetic.net/t/important-advisory-sqlcipher-with-xcode-8-and-new-sdks/1688
//
// > In order to avoid situations where SQLite might be used
// > improperly at runtime, we strongly recommend that
// > applications institute a runtime test to ensure that the
// > application is actually using SQLCipher on the active
// > connection.
if try String.fetchOne(self, sql: "PRAGMA cipher_version") == nil {
throw DatabaseError(resultCode: .SQLITE_MISUSE, message: """
GRDB is not linked against SQLCipher. \
Check https://discuss.zetetic.net/t/important-advisory-sqlcipher-with-xcode-8-and-new-sdks/1688
""")
}
}

private func setCipherPassphrase(_ passphrase: String) throws {
let data = passphrase.data(using: .utf8)!
#if swift(>=5.0)
let code = data.withUnsafeBytes {
sqlite3_key(sqliteConnection, $0.baseAddress, Int32($0.count))
}
#else
let code = data.withUnsafeBytes {
sqlite3_key(sqliteConnection, $0, Int32(data.count))
}
#endif
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection)))
}
}
#endif

private func validateFormat() throws {
// Users are surprised when they open a picture as a database and
// see no error (https://github.com/groue/GRDB.swift/issues/54).
//
// So let's fail early if file is not a database, or encrypted with
// another passphrase.
try makeSelectStatement(sql: "SELECT * FROM sqlite_master LIMIT 1").makeCursor().next()
}
}

extension Database {
Expand Down
7 changes: 6 additions & 1 deletion GRDB/Core/SerializedDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ final class SerializedDatabase {
self.queue = configuration.makeDispatchQueue(defaultLabel: defaultLabel, purpose: purpose)
SchedulingWatchdog.allowDatabase(db, onQueue: queue)
try queue.sync {
try db.setup()
do {
try db.setup()
} catch {
db.close()
groue marked this conversation as resolved.
Show resolved Hide resolved
throw error
}
}
}

Expand Down
14 changes: 14 additions & 0 deletions GRDB/Fixit/GRDB-4.0.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,17 @@ extension ValueObservation {
set { preconditionFailure() }
}
}

extension Configuration {
@available(*, unavailable, message: "Run the PRAGMA cipher_page_size in Configuration.prepareDatabase instead.")
public var cipherPageSize: Int {
get { preconditionFailure() }
set { preconditionFailure() }
}

@available(*, unavailable, message: "Run the PRAGMA kdf_iter in Configuration.prepareDatabase instead.")
public var kdfIterations: Int {
get { preconditionFailure() }
set { preconditionFailure() }
}
}
26 changes: 7 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7377,31 +7377,19 @@ try clearDBQueue.inDatabase { db in

## Advanced configuration options for SQLCipher

There are two advanced configuration options that you can set for configuring SQLCipher that control aspects of the encryption and key generation process.
Some advanced SQLCipher configuration steps must happen very early in the database lifetime, and you will have to use the `configuration.prepareDatabase` property in order to run them correctly:

```swift
var configuration = Configuration()
configuration.passphrase = "secret"
configuration.cipherPageSize = .pageSize4K
configuration.kdfIterations = 128000
configuration.prepareDatabase = { db in
try db.execute(sql: "PRAGMA cipher_page_size = 4096")
try db.execute(sql: "PRAGMA kdf_iter = 128000")
}
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`](https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_page_size) configuration option). Increasing the page size can noticeably improve performance for certain queries that access large numbers of pages.

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

> :point_up: **Note**: 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.

### 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`](https://www.zetetic.net/sqlcipher/sqlcipher-api/#kdf_iter) configuration option).

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.

> :point_up: **Note**: 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.
See [PRAGMA cipher_page_size](https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_page_size) and [PRAGMA kdf_iter](https://www.zetetic.net/sqlcipher/sqlcipher-api/#kdf_iter) for more information.


## Backup
Expand Down Expand Up @@ -8669,7 +8657,7 @@ Sample Code
**Thanks**

- [Pierlis](http://pierlis.com), where we write great software.
- [@bellebethcooper](https://github.com/bellebethcooper), [@bfad](https://github.com/bfad), [@cfilipov](https://github.com/cfilipov), [@charlesmchen-signal](https://github.com/charlesmchen-signal), [@Chiliec](https://github.com/Chiliec), [@darrenclark](https://github.com/darrenclark), [@davidkraus](https://github.com/davidkraus), [@fpillet](http://github.com/fpillet), [@gusrota](https://github.com/gusrota), [@hartbit](https://github.com/hartbit), [@kdubb](https://github.com/kdubb), [@kluufger](https://github.com/kluufger), [@KyleLeneau](https://github.com/KyleLeneau), [@Marus](https://github.com/Marus), [@pakko972](https://github.com/pakko972), [@peter-ss](https://github.com/peter-ss), [@pierlo](https://github.com/pierlo), [@pocketpixels](https://github.com/pocketpixels), [@schveiguy](https://github.com/schveiguy), [@SD10](https://github.com/SD10), [@sobri909](https://github.com/sobri909), [@sroddy](https://github.com/sroddy), [@swiftlyfalling](https://github.com/swiftlyfalling), [@valexa](https://github.com/valexa), and [@zmeyc](https://github.com/zmeyc) for their contributions, help, and feedback on GRDB.
- [@bellebethcooper](https://github.com/bellebethcooper), [@bfad](https://github.com/bfad), [@cfilipov](https://github.com/cfilipov), [@charlesmchen-signal](https://github.com/charlesmchen-signal), [@Chiliec](https://github.com/Chiliec), [@darrenclark](https://github.com/darrenclark), [@davidkraus](https://github.com/davidkraus), [@fpillet](http://github.com/fpillet), [@gusrota](https://github.com/gusrota), [@hartbit](https://github.com/hartbit), [@kdubb](https://github.com/kdubb), [@kluufger](https://github.com/kluufger), [@KyleLeneau](https://github.com/KyleLeneau), [@Marus](https://github.com/Marus), [@michaelkirk-signal](https://github.com/michaelkirk-signal), [@pakko972](https://github.com/pakko972), [@peter-ss](https://github.com/peter-ss), [@pierlo](https://github.com/pierlo), [@pocketpixels](https://github.com/pocketpixels), [@schveiguy](https://github.com/schveiguy), [@SD10](https://github.com/SD10), [@sobri909](https://github.com/sobri909), [@sroddy](https://github.com/sroddy), [@swiftlyfalling](https://github.com/swiftlyfalling), [@valexa](https://github.com/valexa), and [@zmeyc](https://github.com/zmeyc) for their contributions, help, and feedback on GRDB.
- [@aymerick](https://github.com/aymerick) and [@kali](https://github.com/kali) because SQL.
- [ccgus/fmdb](https://github.com/ccgus/fmdb) for its excellency.

Expand Down
4 changes: 0 additions & 4 deletions Tests/GRDBTests/DatabasePoolConcurrencyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,6 @@ class DatabasePoolConcurrencyTests: GRDBTestCase {
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode, .SQLITE_BUSY)
XCTAssertEqual(error.message!, "database is locked")
XCTAssertTrue(error.sql == nil)
XCTAssertEqual(error.description, "SQLite error 5: database is locked")
} catch {
XCTFail("Expected DatabaseError")
}
Expand Down Expand Up @@ -880,8 +878,6 @@ class DatabasePoolConcurrencyTests: GRDBTestCase {
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode, .SQLITE_BUSY)
XCTAssertEqual(error.message!, "database is locked")
XCTAssertTrue(error.sql == nil)
XCTAssertEqual(error.description, "SQLite error 5: database is locked")
}
}
}
Expand Down
4 changes: 0 additions & 4 deletions Tests/GRDBTests/DatabaseQueueTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@ class DatabaseQueueTests: GRDBTestCase {
XCTAssert([
"file is encrypted or is not a database",
"file is not a database"].contains(error.message!))
XCTAssertTrue(error.sql == nil)
XCTAssert([
"SQLite error 26: file is encrypted or is not a database",
"SQLite error 26: file is not a database"].contains(error.description))
}
}
#endif
Expand Down
Loading