|
| 1 | +#if SQLITE_HAS_CODEC |
| 2 | +#if GRDBCIPHER // CocoaPods (SQLCipher subspec) |
| 3 | +import SQLCipher |
| 4 | +#elseif SQLCipher |
| 5 | +import SQLCipher |
| 6 | +#else |
| 7 | +#error("No SQLCipher library configured") |
| 8 | +#endif |
| 9 | +import Foundation |
| 10 | + |
| 11 | +extension Database { |
| 12 | + |
| 13 | + /// Destination of SQLCipher logs. |
| 14 | + /// |
| 15 | + /// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_log> |
| 16 | + public struct SQLCipherLogTarget: Sendable { |
| 17 | + public var rawValue: String |
| 18 | + public init(rawValue: String) { |
| 19 | + self.rawValue = rawValue |
| 20 | + } |
| 21 | + |
| 22 | + public static let stdout = Self(rawValue: "stdout") |
| 23 | + public static let stderr = Self(rawValue: "stderr") |
| 24 | + public static let device = Self(rawValue: "device") |
| 25 | + public static func file(_ path: String) -> Self { Self(rawValue: path) } |
| 26 | + } |
| 27 | + |
| 28 | + /// Granularitly of SQLCipher log outputs. |
| 29 | + /// |
| 30 | + /// Each log level is more verbose than the last. With ``debug`` and |
| 31 | + /// ``trace`` the logging system will generate a significant log volume. |
| 32 | + /// |
| 33 | + /// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_log_level> |
| 34 | + /// |
| 35 | + /// ## Topics |
| 36 | + /// |
| 37 | + /// ### Log Levels |
| 38 | + /// |
| 39 | + /// - ``none`` |
| 40 | + /// - ``error`` |
| 41 | + /// - ``warn`` |
| 42 | + /// - ``info`` |
| 43 | + /// - ``debug`` |
| 44 | + /// - ``trace`` |
| 45 | + public struct SQLCipherLogLevel: RawRepresentable, Sendable { |
| 46 | + public var rawValue: String |
| 47 | + public init(rawValue: String) { |
| 48 | + self.rawValue = rawValue |
| 49 | + } |
| 50 | + |
| 51 | + public static let none = Self(rawValue: "NONE") |
| 52 | + public static let error = Self(rawValue: "ERROR") |
| 53 | + public static let warn = Self(rawValue: "WARN") |
| 54 | + public static let info = Self(rawValue: "INFO") |
| 55 | + public static let debug = Self(rawValue: "DEBUG") |
| 56 | + public static let trace = Self(rawValue: "TRACE") |
| 57 | + } |
| 58 | + |
| 59 | + /// - Returns: the SQLCipher version |
| 60 | + /// |
| 61 | + /// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_version> |
| 62 | + public var cipherVersion: String { |
| 63 | + get throws { try String.fetchOne(self, sql: "PRAGMA cipher_version")! } |
| 64 | + } |
| 65 | + |
| 66 | + /// - Returns: the SQLCipher fips status: 1 for fips mode, 0 for non-fips mode |
| 67 | + /// The FIPS status will not be initialized until the database connection has been keyed |
| 68 | + /// |
| 69 | + /// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_fips_status> |
| 70 | + public var cipherFipsStatus: String? { |
| 71 | + get throws { try String.fetchOne(self, sql: "PRAGMA cipher_fips_status") } |
| 72 | + } |
| 73 | + |
| 74 | + /// - Returns: The compiled crypto provider. |
| 75 | + /// The database must be keyed before requesting the name of the crypto provider. |
| 76 | + /// |
| 77 | + /// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_provider> |
| 78 | + public var cipherProvider: String? { |
| 79 | + get throws { try String.fetchOne(self, sql: "PRAGMA cipher_provider") } |
| 80 | + } |
| 81 | + |
| 82 | + /// - Returns: the version number provided from the compiled crypto provider. |
| 83 | + /// This value, if known, is available only after the database has been keyed. |
| 84 | + /// |
| 85 | + /// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_provider_version> |
| 86 | + public var cipherProviderVersion: String? { |
| 87 | + get throws { try String.fetchOne(self, sql: "PRAGMA cipher_provider_version") } |
| 88 | + } |
| 89 | + |
| 90 | + /// Sets the passphrase used to crypt and decrypt an SQLCipher database. |
| 91 | + /// |
| 92 | + /// Call this method from `Configuration.prepareDatabase`, |
| 93 | + /// as in the example below: |
| 94 | + /// |
| 95 | + /// var config = Configuration() |
| 96 | + /// config.prepareDatabase { db in |
| 97 | + /// try db.usePassphrase("secret") |
| 98 | + /// } |
| 99 | + public func usePassphrase(_ passphrase: String) throws { |
| 100 | + guard var data = passphrase.data(using: .utf8) else { |
| 101 | + throw DatabaseError(message: "invalid passphrase") |
| 102 | + } |
| 103 | + defer { |
| 104 | + data.resetBytes(in: 0..<data.count) |
| 105 | + } |
| 106 | + try usePassphrase(data) |
| 107 | + } |
| 108 | + |
| 109 | + /// Sets the passphrase used to crypt and decrypt an SQLCipher database. |
| 110 | + /// |
| 111 | + /// Call this method from `Configuration.prepareDatabase`, |
| 112 | + /// as in the example below: |
| 113 | + /// |
| 114 | + /// var config = Configuration() |
| 115 | + /// config.prepareDatabase { db in |
| 116 | + /// try db.usePassphrase(passphraseData) |
| 117 | + /// } |
| 118 | + public func usePassphrase(_ passphrase: Data) throws { |
| 119 | + let code = passphrase.withUnsafeBytes { |
| 120 | + sqlite3_key(sqliteConnection, $0.baseAddress, CInt($0.count)) |
| 121 | + } |
| 122 | + guard code == SQLITE_OK else { |
| 123 | + throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection))) |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + /// Changes the passphrase used by an SQLCipher encrypted database. |
| 128 | + public func changePassphrase(_ passphrase: String) throws { |
| 129 | + guard var data = passphrase.data(using: .utf8) else { |
| 130 | + throw DatabaseError(message: "invalid passphrase") |
| 131 | + } |
| 132 | + defer { |
| 133 | + data.resetBytes(in: 0..<data.count) |
| 134 | + } |
| 135 | + try changePassphrase(data) |
| 136 | + } |
| 137 | + |
| 138 | + /// Changes the passphrase used by an SQLCipher encrypted database. |
| 139 | + public func changePassphrase(_ passphrase: Data) throws { |
| 140 | + // FIXME: sqlite3_rekey is discouraged. |
| 141 | + // |
| 142 | + // https://github.com/ccgus/fmdb/issues/547#issuecomment-259219320 |
| 143 | + // |
| 144 | + // > We (Zetetic) have been discouraging the use of sqlite3_rekey in |
| 145 | + // > favor of attaching a new database with the desired encryption |
| 146 | + // > options and using sqlcipher_export() to migrate the contents and |
| 147 | + // > schema of the original db into the new one: |
| 148 | + // > 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/ |
| 149 | + let code = passphrase.withUnsafeBytes { |
| 150 | + sqlite3_rekey(sqliteConnection, $0.baseAddress, CInt($0.count)) |
| 151 | + } |
| 152 | + guard code == SQLITE_OK else { |
| 153 | + throw DatabaseError(resultCode: code, message: lastErrorMessage) |
| 154 | + } |
| 155 | + } |
| 156 | + |
| 157 | + /// When using Commercial or Enterprise SQLCipher packages you must call |
| 158 | + /// `PRAGMA cipher_license` with a valid license code prior to executing |
| 159 | + /// cryptographic operations on an encrypted database. |
| 160 | + /// Failure to provide a license code, or use of an expired trial code, |
| 161 | + /// will result in an `SQLITE_AUTH (23)` error code reported from the SQLite API |
| 162 | + /// License Codes will activate SQLCipher Commercial or Enterprise packages |
| 163 | + /// from Zetetic: <https://www.zetetic.net/sqlcipher/buy/> |
| 164 | + /// 15-day free trials are available by request: <https://www.zetetic.net/sqlcipher/trial/> |
| 165 | + /// |
| 166 | + /// Call this method from ``Configuration/prepareDatabase(_:)``, |
| 167 | + /// as in the example below: |
| 168 | + /// |
| 169 | + /// ```swift |
| 170 | + /// var config = Configuration() |
| 171 | + /// config.prepareDatabase { db in |
| 172 | + /// try db.applySQLCipherLicense(license) |
| 173 | + /// } |
| 174 | + /// ``` |
| 175 | + /// |
| 176 | + /// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_license> |
| 177 | + /// - Parameter license: base64 SQLCipher license code to activate SQLCipher commercial |
| 178 | + public func applySQLCipherLicense(_ license: String) throws { |
| 179 | + try execute(sql: "PRAGMA cipher_license = '\(license)'") |
| 180 | + } |
| 181 | + |
| 182 | + /// Instructs SQLCipher to log internal debugging and operational information. |
| 183 | + /// |
| 184 | + /// The supplied ``logLevel`` determines the granularity of the logs output. |
| 185 | + /// |
| 186 | + /// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_log> |
| 187 | + /// - Parameter logLevel: The granularity to use for the logging system - defaults to `DEBUG`. |
| 188 | + /// - Parameter target: The destination of SQLCipher logs - defaults to `.device`. |
| 189 | + public func enableCipherLogging( |
| 190 | + logLevel: SQLCipherLogLevel = .debug, |
| 191 | + target: SQLCipherLogTarget = .device |
| 192 | + ) throws { |
| 193 | + // Pragma do not support SQL arguments. We need to generate SQL that contains literal values: |
| 194 | + // PRAGMA cipher_log = '/path/to/file' |
| 195 | + // PRAGMA cipher_log_level = 'xxx' |
| 196 | + let context = SQLGenerationContext(self, argumentsSink: .literalValues) |
| 197 | + try execute(sql: SQL("PRAGMA cipher_log = \(target.rawValue)").sql(context)) |
| 198 | + try execute(sql: SQL("PRAGMA cipher_log_level = \(logLevel.rawValue.uppercased())").sql(context)) |
| 199 | + } |
| 200 | + |
| 201 | + /// Instructs SQLCipher to disable logging internal debugging and operational information. |
| 202 | + /// |
| 203 | + /// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_log> |
| 204 | + public func disableCipherLogging() throws { |
| 205 | + try execute(sql: "PRAGMA cipher_log_level = NONE") |
| 206 | + } |
| 207 | + |
| 208 | + internal func validateSQLCipher() throws { |
| 209 | + // https://discuss.zetetic.net/t/important-advisory-sqlcipher-with-xcode-8-and-new-sdks/1688 |
| 210 | + // |
| 211 | + // > In order to avoid situations where SQLite might be used |
| 212 | + // > improperly at runtime, we strongly recommend that |
| 213 | + // > applications institute a runtime test to ensure that the |
| 214 | + // > application is actually using SQLCipher on the active |
| 215 | + // > connection. |
| 216 | + if try String.fetchOne(self, sql: "PRAGMA cipher_version") == nil { |
| 217 | + throw DatabaseError(resultCode: .SQLITE_MISUSE, message: """ |
| 218 | + GRDB is not linked against SQLCipher. \ |
| 219 | + Check https://discuss.zetetic.net/t/important-advisory-sqlcipher-with-xcode-8-and-new-sdks/1688 |
| 220 | + """) |
| 221 | + } |
| 222 | + } |
| 223 | + |
| 224 | + internal func dropAllDatabaseObjects() throws { |
| 225 | + // SQLCipher does not support the backup API: |
| 226 | + // https://discuss.zetetic.net/t/using-the-sqlite-online-backup-api/2631 |
| 227 | + // So we'll drop all database objects one after the other. |
| 228 | + |
| 229 | + // Prevent foreign keys from messing with drop table statements |
| 230 | + let foreignKeysEnabled = try Bool.fetchOne(self, sql: "PRAGMA foreign_keys")! |
| 231 | + if foreignKeysEnabled { |
| 232 | + try execute(sql: "PRAGMA foreign_keys = OFF") |
| 233 | + } |
| 234 | + |
| 235 | + try throwingFirstError( |
| 236 | + execute: { |
| 237 | + // Remove all database objects, one after the other |
| 238 | + try inTransaction { |
| 239 | + let sql = "SELECT type, name FROM sqlite_master WHERE name NOT LIKE 'sqlite_%'" |
| 240 | + while let row = try Row.fetchOne(self, sql: sql) { |
| 241 | + let type: String = row["type"] |
| 242 | + let name: String = row["name"] |
| 243 | + try execute(sql: "DROP \(type) \(name.quotedDatabaseIdentifier)") |
| 244 | + } |
| 245 | + return .commit |
| 246 | + } |
| 247 | + }, |
| 248 | + finally: { |
| 249 | + // Restore foreign keys if needed |
| 250 | + if foreignKeysEnabled { |
| 251 | + try execute(sql: "PRAGMA foreign_keys = ON") |
| 252 | + } |
| 253 | + }) |
| 254 | + } |
| 255 | +} |
| 256 | + |
| 257 | +#endif |
0 commit comments