diff --git a/.gitignore b/.gitignore index e7b2ad4d..4c04ed86 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ bin/ .build Packages/ .swiftpm/ +Package.resolved diff --git a/Documentation/Index.md b/Documentation/Index.md index 0e8261f2..8a525fa6 100644 --- a/Documentation/Index.md +++ b/Documentation/Index.md @@ -3,10 +3,10 @@ - [SQLite.swift Documentation](#sqliteswift-documentation) - [Installation](#installation) - [Swift Package Manager](#swift-package-manager) + - [Using SQLite.swift with SQLCipher](#using-sqliteswift-with-sqlcipher) - [Carthage](#carthage) - [CocoaPods](#cocoapods) - [Requiring a specific version of SQLite](#requiring-a-specific-version-of-sqlite) - - [Using SQLite.swift with SQLCipher](#using-sqliteswift-with-sqlcipher) - [Manual](#manual) - [Getting Started](#getting-started) - [Connecting to a Database](#connecting-to-a-database) @@ -119,7 +119,79 @@ process of downloading, compiling, and linking dependencies. $ swift build ``` +#### Using SQLite.swift with SQLCipher + +If you want to use [SQLCipher][] with SQLite.swift you can specify the `SQLCipher` trait when consuming SQLite.swift. + +```swift +depdencies: [ + .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.4", traits: ["SQLCipher"]) +] +``` + +As of Xcode 16.4 (16F6), there's no direct way in the Xcode UI to select trait variations so you'll need to use a local wrapper package to pull in the SQLite.swift dependency with the `SQLCipher` trait enabled: + +```swift +// swift-tools-version: 6.1 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "AppDependencies", + platforms: [ + .macOS(.v10_14), + .iOS(.v13), + .macCatalyst(.v13), + .watchOS(.v8), + .tvOS(.v15), + .visionOS(.v1) + ], + products: [ + .library( + name: "AppDependencies", + targets: ["AppDependencies"]), + ], + dependencies: [ + .package( + url: "https://github.com/stephencelis/SQLite.swift.git", + from: "0.15.4", + traits: ["SQLCipher"]) + ], + targets: [ + .target( + name: "AppDependencies", + dependencies: [ + .product( + name: "SQLite", + package: "SQLite.swift") + ] + ) + ] +) +``` + +Within Xcode add your local `AppDependencies` wrapper package as a package dependency and SQLite.swift with SQLCipher functionality will be accessible. + +Using the `SQLCipher` trait will cause SQLite.swift to include a dependency on SQLCipher.swift and enable `Connection` methods to set and change the database key: + +```swift +import SQLite + +let db = try Connection("path/to/encrypted.sqlite3") +try db.key("secret") +try db.rekey("new secret") // changes encryption key on already encrypted db +``` + +To encrypt an existing database: + +```swift +let db = try Connection("path/to/unencrypted.sqlite3") +try db.sqlcipher_export(.uri("encrypted.sqlite3"), key: "secret") +``` + [Swift Package Manager]: https://swift.org/package-manager +[SQLCipher]: https://www.zetetic.net/sqlcipher/ ### Carthage @@ -191,41 +263,9 @@ end See the [sqlite3 podspec][sqlite3pod] for more details. -#### Using SQLite.swift with SQLCipher - -If you want to use [SQLCipher][] with SQLite.swift you can require the -`SQLCipher` subspec in your Podfile (SPM is not supported yet, see [#1084](https://github.com/stephencelis/SQLite.swift/issues/1084)): - -```ruby -target 'YourAppTargetName' do - # Make sure you only require the subspec, otherwise you app might link against - # the system SQLite, which means the SQLCipher-specific methods won't work. - pod 'SQLite.swift/SQLCipher', '~> 0.15.4' -end -``` - -This will automatically add a dependency to the SQLCipher pod as well as -extend `Connection` with methods to change the database key: - -```swift -import SQLite - -let db = try Connection("path/to/encrypted.sqlite3") -try db.key("secret") -try db.rekey("new secret") // changes encryption key on already encrypted db -``` - -To encrypt an existing database: - -```swift -let db = try Connection("path/to/unencrypted.sqlite3") -try db.sqlcipher_export(.uri("encrypted.sqlite3"), key: "secret") -``` - [CocoaPods]: https://cocoapods.org [CocoaPods Installation]: https://guides.cocoapods.org/using/getting-started.html#getting-started [sqlite3pod]: https://github.com/clemensg/sqlite3pod -[SQLCipher]: https://www.zetetic.net/sqlcipher/ ### Manual diff --git a/Package.swift b/Package.swift index 56925d18..0f049506 100644 --- a/Package.swift +++ b/Package.swift @@ -1,18 +1,33 @@ -// swift-tools-version:5.9 +// swift-tools-version: 6.1 import PackageDescription let deps: [Package.Dependency] = [ - .github("swiftlang/swift-toolchain-sqlite", exact: "1.0.4") + .github("swiftlang/swift-toolchain-sqlite", exact: "1.0.4"), + .github("sqlcipher/SQLCipher.swift.git", from: "4.11.0") ] +let applePlatforms: [PackageDescription.Platform] = [.iOS, .macOS, .watchOS, .tvOS, .visionOS] + +let sqlcipherTraitTargetCondition: TargetDependencyCondition? = .when(platforms: applePlatforms, traits: ["SQLCipher"]) + +let sqlcipherTraitBuildSettingCondition: BuildSettingCondition? = .when(platforms: applePlatforms, traits: ["SQLCipher"]) + let targets: [Target] = [ .target( name: "SQLite", dependencies: [ - .product(name: "SwiftToolchainCSQLite", package: "swift-toolchain-sqlite", condition: .when(platforms: [.linux, .windows, .android])) + .product(name: "SwiftToolchainCSQLite", package: "swift-toolchain-sqlite", condition: .when(platforms: [.linux, .windows, .android])), + .product(name: "SQLCipher", package: "SQLCipher.swift", condition: sqlcipherTraitTargetCondition) ], exclude: [ "Info.plist" + ], + cSettings: [ + .define("SQLITE_HAS_CODEC", to: nil, sqlcipherTraitBuildSettingCondition) + ], + swiftSettings: [ + .define("SQLITE_HAS_CODEC", sqlcipherTraitBuildSettingCondition), + .define("SQLITE_SWIFT_SQLCIPHER", sqlcipherTraitBuildSettingCondition) ] ) ] @@ -29,6 +44,9 @@ let testTargets: [Target] = [ ], resources: [ .copy("Resources") + ], + swiftSettings: [ + .define("SQLITE_SWIFT_SQLCIPHER", sqlcipherTraitBuildSettingCondition) ] ) ] @@ -48,8 +66,12 @@ let package = Package( targets: ["SQLite"] ) ], + traits: [ + .trait(name: "SQLCipher", description: "Enables SQLCipher encryption when a key is supplied to Connection") + ], dependencies: deps, - targets: targets + testTargets + targets: targets + testTargets, + swiftLanguageModes: [.v5], ) extension Package.Dependency { @@ -57,4 +79,8 @@ extension Package.Dependency { static func github(_ repo: String, exact ver: Version) -> Package.Dependency { .package(url: "https://github.com/\(repo)", exact: ver) } + + static func github(_ repo: String, from ver: Version) -> Package.Dependency { + .package(url: "https://github.com/\(repo)", from: ver) + } } diff --git a/README.md b/README.md index a52b7ae5..4195c639 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ syntax _and_ intent. - [Full-text search][] support - [Well-documented][See Documentation] - Extensively tested - - [SQLCipher][] support via CocoaPods + - [SQLCipher][] support via Swift Package Manager - [Schema query/migration][] - Works on [Linux](Documentation/Linux.md) (with some limitations) - Active support at diff --git a/Sources/SQLite/Extensions/Cipher.swift b/Sources/SQLite/Extensions/Cipher.swift index 03194ef1..95e15877 100644 --- a/Sources/SQLite/Extensions/Cipher.swift +++ b/Sources/SQLite/Extensions/Cipher.swift @@ -5,6 +5,19 @@ import SQLCipher /// @see [sqlcipher api](https://www.zetetic.net/sqlcipher/sqlcipher-api/) extension Connection { + /// Granularitly of SQLCipher log outputs + /// Each log level is more verbose than the last + /// + /// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_log_level + public enum CipherLogLevel: String { + case none + case error + case warn + case info + case debug + case trace + } + /// - Returns: the SQLCipher version /// /// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_version @@ -12,6 +25,29 @@ extension Connection { (try? scalar("PRAGMA cipher_version")) as? String } + /// - Returns: the SQLCipher fips status: 1 for fips mode, 0 for non-fips mode + /// The FIPS status will not be initialized until the database connection has been keyed + /// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_fips_status + public var cipherFipsStatus: String? { + (try? scalar("PRAGMA cipher_fips_status")) as? String + } + + /// - Returns: The compiled crypto provider. + /// The database must be keyed before requesting the name of the crypto provider. + /// + /// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_provider + public var cipherProvider: String? { + (try? scalar("PRAGMA cipher_provider")) as? String + } + + /// - Returns: the version number provided from the compiled crypto provider. + /// This value, if known, is available only after the database has been keyed. + /// + /// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_provider_version + public var cipherProviderVersion: String? { + (try? scalar("PRAGMA cipher_provider_version")) as? String + } + /// Specify the key for an encrypted database. This routine should be /// called right after sqlite3_open(). /// @@ -82,6 +118,43 @@ extension Connection { try detach(schemaName) } + /// When using Commercial or Enterprise SQLCipher packages you must call + /// `PRAGMA cipher_license` with a valid license code prior to executing + /// cryptographic operations on an encrypted database. + /// Failure to provide a license code, or use of an expired trial code, + /// will result in an `SQLITE_AUTH (23)` error code reported from the SQLite API + /// License Codes will activate SQLCipher Commercial or Enterprise packages + /// from Zetetic: https://www.zetetic.net/sqlcipher/buy/ + /// 15-day free trials are available by request: https://www.zetetic.net/sqlcipher/trial/ + /// + /// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_license + /// - Parameter license: base64 SQLCipher license code to activate SQLCipher commercial + public func applyLicense(_ license: String) throws { + try run("PRAGMA cipher_license = '\(license)'") + } + + /// Instructs SQLCipher to log internal debugging and operational information + /// to the sepecified log target (device) using `os_log` + /// The supplied logLevel will determine the granularity of the logs output + /// Available logLevel options are: NONE, ERROR, WARN, INFO, DEBUG, TRACE + /// Note that each level is more verbose than the last, + /// and particularly with DEBUG and TRACE the logging system will generate + /// a significant log volume + /// + /// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_log + /// - Parameter logLevel: CipherLogLevel The granularity to use for the logging system - defaults to `DEBUG` + public func enableCipherLogging(logLevel: CipherLogLevel = .debug) throws { + try run("PRAGMA cipher_log = device") + try run("PRAGMA cipher_log_level = \(logLevel.rawValue.uppercased())") + } + + /// Instructs SQLCipher to disable logging internal debugging and operational information + /// + /// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_log + public func disableCipherLogging() throws { + try run("PRAGMA cipher_log_level = \(CipherLogLevel.none.rawValue.uppercased())") + } + // MARK: - private private func _key_v2(db: String, keyPointer: UnsafePointer, diff --git a/Tests/SQLiteTests/Extensions/CipherTests.swift b/Tests/SQLiteTests/Extensions/CipherTests.swift index bc89cfa2..a93cf49f 100644 --- a/Tests/SQLiteTests/Extensions/CipherTests.swift +++ b/Tests/SQLiteTests/Extensions/CipherTests.swift @@ -107,12 +107,26 @@ class CipherTests: XCTestCase { XCTAssertEqual(1, try conn.scalar("SELECT count(*) FROM foo") as? Int64) } + func test_cipher_provider() throws { + XCTAssertEqual("commoncrypto", db1.cipherProvider) + } + + func test_cipher_provider_version() throws { + XCTAssertNotNil(db1.cipherProviderVersion) + } + + func test_cipher_fips_status() throws { + let fipsStatusString = db1.cipherFipsStatus + XCTAssertNotNil(fipsStatusString) + XCTAssertEqual(0, Int(fipsStatusString!)) + } + private func keyData(length: Int = 64) -> NSData { let keyData = NSMutableData(length: length)! let result = SecRandomCopyBytes(kSecRandomDefault, length, keyData.mutableBytes.assumingMemoryBound(to: UInt8.self)) XCTAssertEqual(0, result) - return NSData(data: keyData) + return NSData(data: keyData as Data) } } #endif