Compare commits

...

7 Commits

Author SHA1 Message Date
Michael Kirk
747a6352d6 example docs 2019-04-04 16:03:30 -06:00
Michael Kirk
dcc3db2730 Revert "update sqlcipher"
This reverts commit 26b8682e63.
2019-04-04 15:57:21 -06:00
Michael Kirk
29c1c5481c inline setupConnection, re-order method calls to call validateFormat later, convert static->instance methods where possible 2019-04-04 15:55:53 -06:00
Michael Kirk
ebc7a9d8e0 fix #if scope 2019-04-04 15:42:19 -06:00
Michael Kirk
99065a4faf prepareDB block 2019-04-02 10:47:31 -06:00
Michael Kirk
26b8682e63 update sqlcipher 2019-03-31 14:27:46 -06:00
Gwendal Roué
d41f6b4466 Makefile: support for Xcode 10.2 2019-03-28 07:08:18 +01:00
5 changed files with 161 additions and 133 deletions

View File

@ -82,21 +82,23 @@ public struct Configuration {
/// Default: nil
public var passphrase: String?
/// The cipher_page_size setting for the encrypted database.
///
/// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_page_size
///
/// 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
///
/// Default: 64000 - this corresponds to the default used by SQLCipher 3
public var kdfIterations: Int = 64000
#endif
// If set, allows custom configuration to be run every time
// a new connection is opened.
//
// 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.
//
// For example:
//
// var config = Configuration()
// config.prepareDatabase = { db in
// db.execute(sql: "PRAGMA kdf_iter = '10000';")
// }
//
public var prepareDatabase: ((Database) throws -> Void)?
// MARK: - Transactions

View File

@ -166,28 +166,10 @@ 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 {
@ -221,103 +203,12 @@ 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 {
// MARK: - Database Setup
/// This method must be called after database initialization
func setup() throws {
// Setup trace first, so that setup queries are traced.
@ -327,6 +218,18 @@ extension Database {
setupDefaultFunctions()
setupDefaultCollations()
observationBroker.installCommitAndRollbackHooks()
try activateExtendedCodes()
#if SQLITE_HAS_CODEC
try validateSQLCipher()
if let passphrase = configuration.passphrase {
try setCipherPassphrase(passphrase)
}
#endif
try configuration.prepareDatabase?(self)
try validateFormat()
configuration.SQLiteConnectionDidOpen?()
}
private func setupTrace() {
@ -431,6 +334,67 @@ 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.
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 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.
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 {

View File

@ -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()
throw error
}
}
}

View File

@ -51,7 +51,11 @@ endif
TEST_ACTIONS = clean build build-for-testing test-without-building
# When adding support for an Xcode version, look for available devices with `instruments -s devices`
ifeq ($(XCODEVERSION),10.1)
ifeq ($(XCODEVERSION),10.2)
MAX_SWIFT_VERSION = 5
MAX_IOS_DESTINATION = "platform=iOS Simulator,name=iPhone XS,OS=12.2"
MIN_IOS_DESTINATION = "platform=iOS Simulator,name=iPhone 4s,OS=9.0"
else ifeq ($(XCODEVERSION),10.1)
MAX_SWIFT_VERSION = 4.2
MAX_IOS_DESTINATION = "platform=iOS Simulator,name=iPhone XS,OS=12.1"
MIN_IOS_DESTINATION = "platform=iOS Simulator,name=iPhone 4s,OS=9.0"

View File

@ -343,7 +343,9 @@ class EncryptionTests: GRDBTestCase {
func testCipherPageSize() throws {
do {
dbConfiguration.passphrase = "secret"
dbConfiguration.cipherPageSize = 8192
dbConfiguration.prepareDatabase = { db in
try db.execute(sql: "PRAGMA cipher_page_size = 8192")
}
let dbQueue = try makeDatabaseQueue(filename: "test.sqlite")
try dbQueue.inDatabase({ db in
@ -352,7 +354,10 @@ class EncryptionTests: GRDBTestCase {
}
do {
dbConfiguration.cipherPageSize = 4096
dbConfiguration.passphrase = "secret"
dbConfiguration.prepareDatabase = { db in
try db.execute(sql: "PRAGMA cipher_page_size = 4096")
}
let dbPool = try makeDatabasePool(filename: "testpool.sqlite")
try dbPool.write({ db in
@ -371,7 +376,9 @@ class EncryptionTests: GRDBTestCase {
func testCipherKDFIterations() throws {
do {
dbConfiguration.passphrase = "secret"
dbConfiguration.kdfIterations = 128000
dbConfiguration.prepareDatabase = { db in
try db.execute(sql: "PRAGMA kdf_iter = 128000")
}
let dbQueue = try makeDatabaseQueue(filename: "test.sqlite")
try dbQueue.inDatabase { db in
@ -380,8 +387,11 @@ class EncryptionTests: GRDBTestCase {
}
do {
dbConfiguration.kdfIterations = 128000
dbConfiguration.passphrase = "secret"
dbConfiguration.prepareDatabase = { db in
try db.execute(sql: "PRAGMA kdf_iter = 128000")
}
let dbPool = try makeDatabasePool(filename: "testpool.sqlite")
try dbPool.write { db in
XCTAssertEqual(try Int.fetchOne(db, sql: "PRAGMA kdf_iter"), 128000)
@ -395,6 +405,49 @@ class EncryptionTests: GRDBTestCase {
}
}
}
func testCipherWithMismatchedKDFIterations() throws {
do {
dbConfiguration.passphrase = "secret"
dbConfiguration.prepareDatabase = { db in
try db.execute(sql: "PRAGMA kdf_iter = 128000")
}
let dbPool = try makeDatabasePool(filename: "testpool.sqlite")
try dbPool.write { db in
XCTAssertEqual(try Int.fetchOne(db, sql: "PRAGMA kdf_iter"), 128000)
try db.execute(sql: "CREATE TABLE data(value INTEGER)")
try db.execute(sql: "INSERT INTO data(value) VALUES(1)")
}
try dbPool.read { db in
XCTAssertEqual(try Int.fetchOne(db, sql: "PRAGMA kdf_iter"), 128000)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT value FROM data"), 1)
}
}
do {
dbConfiguration.passphrase = "secret"
dbConfiguration.prepareDatabase = { db in
try db.execute(sql: "PRAGMA kdf_iter = 64000")
}
do {
let dbPool = try makeDatabasePool(filename: "testpool.sqlite")
try dbPool.read { db in
XCTAssertEqual(try Int.fetchOne(db, sql: "PRAGMA kdf_iter"), 64000)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT value FROM data"), 1)
}
XCTFail("Expected error")
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode, .SQLITE_NOTADB)
XCTAssertEqual(error.message!, "file is not a database")
XCTAssertTrue(error.sql == nil)
XCTAssertEqual(error.description, "SQLite error 26: file is not a database")
}
}
}
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