Encrypt the PDF Search Database on iOS

By default, SQLite databases created by PSPDFKit aren’t encrypted, but they’re still protected by iOS data protection, just like all the data in an application is. If you need to add a level of security, there are SQLite extensions that enable database encryption. These extensions encrypt database content and SQLite metadata.

One such extension is the SQLite Encryption Extension. This extension requires an SEE license.

Another alternative implementation is SQLCipher, which is an open source project with a BSD-style license. To integrate SQLCipher, follow the instructions for either the commercial edition or the community edition.

You can read more about how PSPDFKit integrates with your custom SQLite library in the relevant guide.

Encrypting PSPDFKit Databases

SQLite database encryption is currently supported by PDFLibrary. This guide outlines how to enable the encryption feature. If encryption is enabled, the main database will be encrypted.

Step 1 — Registering a DatabaseEncryptionProvider Implementation

The standard SQLite library doesn’t support encryption out of the box. You therefore have to integrate a third-party option into your codebase. DatabaseEncryptionProvider acts as a bridge between this third-party code and PDFLibrary. We’ll use SQLCipher as an example, but the implementation should be similar — if not identical — for other providers.

To integrate SQLCipher, follow the instructions for either the commercial edition or the community edition.

Once SQLCipher is correctly set up, you have to add an implementation of the DatabaseEncryptionProvider protocol. For SQLCipher, the implementation will look like this:

class YourEncryptionProvider: NSObject, DatabaseEncryptionProvider {

    public func encryptDatabase(_ db: UnsafeMutableRawPointer, withKey keyData: Data) -> Bool {
        let data = keyData as NSData
        assert(data.length == 32, "Danger: key is 32 byte long, hence it will not be pbkdf2'ed by SQLCipher!")
        let error = sqlite3_key(OpaquePointer(db), data.bytes, Int32(data.length))
        return error == SQLITE_OK
    }

    public func reEncryptDatabase(_ db: UnsafeMutableRawPointer, withKey keyData: Data) -> Bool {
        let data = keyData as NSData
        assert(data.length == 32, "Danger: key is 32 byte long, hence it will not be pbkdf2'ed by SQLCipher!")
        let error = sqlite3_rekey(OpaquePointer(db), data.bytes, Int32(data.length))
        return error == SQLITE_OK
    }
}
@interface YourEncryptionProvider : NSObject <PSPDFDatabaseEncryptionProvider>
@end

@implementation YourEncryptionProvider
- (BOOL)encryptDatabase:(void *)db withKey:(NSData *)keyData {
  NSAssert([keyData length] == 32, @"Danger: key is 32 byte long, hence it will not be pbkdf2'ed by SQLCipher!");
  int err = sqlite3_key(db, [keyData bytes], (int)[keyData length]);
  return (err == SQLITE_OK);
}

- (BOOL)reEncryptDatabase:(void *)db withKey:(NSData *)keyData {
  NSAssert([keyData length] == 32, @"Danger: key is 32 byte long, hence it will not be pbkdf2'ed by SQLCipher!");
  int err = sqlite3_rekey(db, [keyData bytes], (int)[keyData length]);
  return (err == SQLITE_OK);
}
@end

Finally, you must register your encryption provider with PSPDFKit. To do so, set the databaseEncryptionProvider property on SDK. You have to do this early on. We recommend the following setup code:

func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
    SDK.setLicenseKey("YOUR_LICENSE_KEY_GOES_HERE")
    SDK.sharedInstance.databaseEncryptionProvider = YourEncryptionProvider()

    return true
}
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [PSPDFKitGlobal setLicenseKey:@"YOUR_LICENSE_KEY_GOES_HERE"];
  PSPDFKitGlobal.sharedInstance.databaseEncryptionProvider = [YourEncryptionProvider new];

  return YES;
}

That’s it. PSPDFKit is now configured to support encryption.

Step 2 — Using an Encrypted PDFLibrary

To use an encrypted PDFLibrary, call the PDFLibrary.encryptedLibrary(withPath:encryptionKeyProvider:error:) factory method. The first argument is a path to a directory of your choice. The second parameter is the so-called encryption key provider. An encryption key provider is a simple block that returns the key in the form of an NSData instance. The provider block will be called whenever the library needs access to the encryption key. This ensures we only keep the encryption key in memory for a short amount of time. Your encryption key provider implementation should always fetch the key from secure storage, e.g. Apple’s keychain. Your key provider implementation should also be free from side effects in the sense that it should always return the same key on every call.

❗Important: Depending on the driver you chose, there might be different requirements for the key provided by the encryption key provider block!

PSPDFKit doesn’t process the key in any way, rather it passes it directly to your DatabaseEncryptionProvider implementation. For example, if you use SQLCipher, your key will be treated as a passphrase. By default, SQLCipher will derive a key by using PBKDF2 with 64,000 iterations. However, if the key is exactly 32-bytes long, it’ll be used directly as the encryption key. Other drivers might have similar pitfalls, so be cautious and read the applicable documentation!

The returned instance of the factory is either nil or an encrypted PDFLibrary instance. nil is returned if the path points to an existing library that either isn’t encrypted or is encrypted with a different key. Otherwise, a new instance of PDFLibrary, with the PDFLibrary.isEncrypted flag set to true, will be returned. Once this is done, you can use the library as usual.

Step 3 — Using an Encrypted Library by Default (Optional)

You can override the default library. To do so, create a library (either encrypted or unencrypted) and call SDK.sharedInstance.library = yourCustomLibrary. Please note that you should do this early on, e.g. in application(_:didFinishLaunchingWithOptions:). If you change the library property later in your app’s lifecycle, some parts of SDK might still use the previous instance!

Step 4 — Verifying the Encryption (Optional)

Always ensure your library is properly encrypted. An easy way to do this is the hexdump command. Run hexdump -C /path/to/your/encrypted/library/index.sqlite. If the database is encrypted, the output should contain no human-readable text. You can also use the sqlite3 command to attempt to read from an encrypted library.