Compare commits
7 Commits
GRDB-4.0
...
mkirk/GRDB
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
747a6352d6 | ||
|
|
dcc3db2730 | ||
|
|
29c1c5481c | ||
|
|
ebc7a9d8e0 | ||
|
|
99065a4faf | ||
|
|
26b8682e63 | ||
|
|
d41f6b4466 |
@ -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
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
6
Makefile
6
Makefile
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user