Skip to content
Daniel Weck edited this page May 5, 2017 · 27 revisions

LCP / LSD client library, app integration

This document describes how to integrate Readium LCP+LSD software components into a e-reader application ("reading system") built using the Readium SDK. The LCP (Licensed Content Protection) and LSD (License Status Document) technologies implement resource encryption and DRM (Digital Rights Managements) for EPUB publications.

General information about Readium LCP and LSD is provided on the EDRLab website:
https://www.edrlab.org/readium/readium-lcp/

The actual LCP and LSD specifications are hosted at Readium's GitHub:
https://readium.github.io/readium-lcp-specification/
and:
https://readium.github.io/readium-lsd-specification/

Further information about Readium projects is available at:
https://readium.github.io

Note that some client-side LCP / LSD functionality depends on the existence of associated online services. For more information about the open-source Go server implementation, see:
https://github.com/readium/readium-lcp-server/
(more information will be available soon)

High-level overview

In order to illustrate how to connect application-level code with library-level LCP and LSD APIs, this document showcases a sample app with minimal functionality, known as a "Readium SDK launcher app". As part of the Readium SDK open-source ecosystem, there are "launcher apps" for several native platforms, namely: Android, iOS, and OSX (Windows support is incomplete at this stage). This document currently shows how to integrate LCP + LSD functionality only in an Android app. Documentation for iOS will be created at a later stage, but the principles are broadly the same.

Irrespective of the targeted platform, once the "launcher" app has started the user selects a file to open (3 options):

  1. EPUB file (.epub extension) not protected with LCP, i.e. no META-INF/license.lcpl resource is found inside the zipped container:
    • the app loads and renders the EPUB normally.
  2. EPUB file protected with LCP (the license exists inside the zip directory as META-INF/license.lcpl)
    • the app verifies the authenticity of the license (validation of the associated certificate, and verification of the signature)
    • the app asks the user to enter the passphrase that was used to secure access to this particular publication's license (and the passphrase is saved into secure storage for future reuse),
    • the app checks the LSD for potential rights updates (this step requires network access, but fails gracefully if no web connectivity is available), and then ; if needed ; the app downloads the updated LCP license, and injects META-INF/license.lcpl back into the EPUB archive,
    • the app loads the EPUB and decrypts publication resources in order to render the publication,
    • the app verifies the certificate revocation list at regular intervals (this step requires network access, but fails gracefully if no web connectivity is available).
  3. LCP license (standalone JSON file with .lcpl extension), containing a download link for the associated encrypted EPUB publication:
    • the app downloads the "raw" encrypted publication (i.e. not tied to any particular license),
    • the app injects the license into the acquired EPUB (file path META-INF/license.lcpl inside the zip archive),
    • goto (2).

Note that a "launcher app" is designed to test and/or demonstrate API usage, and as such does not represent a real-world user experience. For example, the Android sample app exposes extremely basic (and naive) LSD user interface affordances to return and renew a license, in order to emulate the ebook library lending model at a very low API level.

Source tree organisation

Here is a breakdown of the required GitHub repositories and branches:

Android "launcher app"

SDKLauncher-Android, feature/lcp branch:
https://github.com/readium/SDKLauncher-Android/tree/feature/lcp
=> Java codebase, Android-Studio project that composes the modules listed below (the corresponding Git submodules are imported automatically when initialising the top-level repository).

Readium SDK library

readium-sdk, feature/lcp branch:
https://github.com/readium/readium-sdk/tree/develop
=> C++ code, with platform-specific integration layers (i.e. JNI/C++ for Android).

Readium LCP / LSD client library

readium-lcp-client, develop branch:
https://github.com/readium/readium-lcp-client/tree/develop
=> C++ code, with JNI/C++ integration layer for Android, and platform-specific (Java) implementation of some components.

Readium JS, shared codebase

Readium's "rendering engine" (i.e. HTML/CSS+Javascript code common to both non-native and browser apps) contains no LCP or LSD -specific customisations. In other words, apps with LCP and LSD support depend on standard ReadiumJS code, as follows:

readium-shared-js, develop branch:
https://github.com/readium/readium-shared-js/tree/develop

readium-cfi-js, develop branch:
https://github.com/readium/readium-cfi-js/tree/develop
(this is a Git submodule dependency that is automatically imported by readium-shared-js)

Readium SDK dependency

Strictly-speaking, the core of the LCP client library does not depend on the Readium SDK. In other words, the Readium SDK (and / or the Readium JS "rendering" engine) is not required when building an application with LCP + LSD functionality.

In concrete terms, the connexion between the LCP library and the Readium SDK is based on the conceptual abstractions known as Content Filter (to decrypt resource streams originating from the EPUB package) and Content Module (to initialise specific DRM schemes, and register associated Content Filter(s)):
https://github.com/readium/readium-sdk/blob/feature/lcp/ePub3/ePub/content_module.h
https://github.com/readium/readium-sdk/blob/feature/lcp/ePub3/ePub/filter.h
The concrete LCP implementations of Content Module and Content Filter are located in:
https://github.com/readium/readium-lcp-client/tree/develop/src/lcp-content-filter

A form of "dependency injection" (or "inversion of control" design pattern) is implemented via the ContentModuleManager::Instance()->RegisterContentModule() function:
https://github.com/readium/readium-lcp-client/blob/develop/src/lcp-content-filter/LcpContentModule.cpp#L185
https://github.com/readium/readium-sdk/blob/feature/lcp/ePub3/ePub/content_module_manager.h#L60
...and the FilterManager::Instance()->RegisterFilter() function:
https://github.com/readium/readium-lcp-client/blob/develop/src/lcp-content-filter/LcpContentFilter.cpp#L286
https://github.com/readium/readium-lcp-client/blob/develop/src/lcp-content-filter/LcpContentModule.cpp#L54
https://github.com/readium/readium-sdk/blob/develop/ePub3/ePub/filter_manager.h#L87
https://github.com/readium/readium-sdk/blob/develop/ePub3/ePub/filter_manager_impl.h#L38

API hooks between application and LCP/LSD library

Root certificate

A reading system application capable of handling protected publications delivered in the LCP ecosystem needs to use a common certificate in order to validate licenses generated with individual provider certificates.

In the Android sample app / Readium SDK "launcher" app, this root certificate is the lcp.crt file:
https://github.com/readium/SDKLauncher-Android/blob/feature/lcp/SDKLauncher-Android/app/src/main/assets/lcp/lcp.crt

The "LCP service" which is used to perform various LCP or LSD -related operations is initialised using the ServiceFactory, where the root certificate is passed as a parameter of the factory function:
https://github.com/readium/SDKLauncher-Android/blob/feature/lcp/SDKLauncher-Android/app/src/main/java/org/readium/sdk/android/launcher/ContainerList.java#L260

Secure storage, HTTP downloader

The StorageProvider and NetProvider instances are application-specific components written in Java (i.e. concrete per-platform implementations of common abstract interfaces defined at the native c++ level). They are used by the LCP library's C++ code to securely store user passphrases, and to emit simple HTTP GET requests (typically: to download a resource).

The Java (application code) and C++ (native code) are connected using JNI, which is why some functionality in the Android project requires additional boilerplate source code. See for example the files in these folders from the LCP + LSD client library:
https://github.com/readium/readium-lcp-client/tree/develop/platform/android/lib/src/main/java/org/readium/sdk/lcp
https://github.com/readium/readium-lcp-client/tree/develop/platform/android/lcp/src/main/jni

LCP CredentialHandler

The CredentialHandler interface contains a single decrypt() function, which essentially consists in a callback invoked from the native C++ library code to notify the application that the license associated with the publication requires a user passphrase:
https://github.com/readium/readium-lcp-client/blob/develop/platform/android/lib/src/main/java/org/readium/sdk/lcp/CredentialHandler.java
(the low-level library effectively aborts the process of loading the EPUB, and hands-over control back to the application)

To control the execution flow, the native layer raises an exception (ContentModuleExceptionDecryptFlow), which is handled gracefully on the receiving end to abort the ongoing operation:
https://github.com/readium/readium-lcp-client/blob/develop/src/lcp-content-filter/LcpContentModule.cpp#L139
https://github.com/readium/readium-sdk/blob/develop/Platform/Android/epub3/src/main/jni/epub3.cpp#L484

In the Android app, an input dialog is presented to the user (showPassphraseDialog()):
https://github.com/readium/SDKLauncher-Android/blob/feature/lcp/SDKLauncher-Android/app/src/main/java/org/readium/sdk/android/launcher/ContainerList.java#L184

...and the EPUB-loading procedure is started once again if the given passphrase successfully unlocks the license (see mLicense.decrypt(passPhrase);):
https://github.com/readium/SDKLauncher-Android/blob/feature/lcp/SDKLauncher-Android/app/src/main/java/org/readium/sdk/android/launcher/ContainerList.java#L203

LSD StatusDocumentHandler

The StatusDocumentHandler interface also contains a single function (process()), which also consists in a callback invoked from the native C++ library code to notify the application that the license associated with the publication contains an LSD link that needs to be checked:
https://github.com/readium/readium-lcp-client/blob/develop/platform/android/lib/src/main/java/org/readium/sdk/lcp/StatusDocumentHandler.java
(the low-level library effectively aborts the process of loading the EPUB, and hands-over control back to the application)

As with the above CredentialHandler, the native layer raises the ContentModuleExceptionDecryptFlow exception to control the execution flow:
https://github.com/readium/readium-lcp-client/blob/develop/src/lcp-content-filter/LcpContentModule.cpp#L149
https://github.com/readium/readium-sdk/blob/develop/Platform/Android/epub3/src/main/jni/epub3.cpp#L484

In the Android app, a modal cancellable dialog is displayed, informing the user about the ongoing LSD checking (launchStatusDocumentProcessing() is an async operation):
https://github.com/readium/SDKLauncher-Android/blob/feature/lcp/SDKLauncher-Android/app/src/main/java/org/readium/sdk/android/launcher/ContainerList.java#L434

Note that if the modal dialog is cancelled by the user, the EPUB-loading operation stops. However, if the LSD checks fail because of external circumstances such as network failure / timeout, the EPUB-loading operation continues as if no LSD link had been present in the LCP license. Either way, the onStatusDocumentProcessingComplete_() callback is invoked to signify the end of the process:
https://github.com/readium/SDKLauncher-Android/blob/feature/lcp/SDKLauncher-Android/app/src/main/java/org/readium/sdk/android/launcher/ContainerList.java#L519

The actual process consisting of requested the LSD JSON document, and of checking its status updates, is implemented entirely in this Java class:
https://github.com/readium/readium-lcp-client/blob/develop/platform/android/lib/src/main/java/org/readium/sdk/lcp/StatusDocumentProcessing.java
Note that the NetProvider C++ abstraction is not used here. Instead, the fine-grain handling of HTTP requests + responses (e.g. special processing of headers) is written direcly using the available platform APIs (Java, Ion library). Also note that the return + renew protocol is only implemented for testing / demonstration purposes. In a real-world situation, there would probably be an intermediary to deal with the library lending scenario, rather than having the reading system app directly interacting with the HTTP service endpoints.

Standalone license, EPUB download, LCP injection

In the Android Readium SDK "launcher" app, the function downloadAndOpenSelectedBook() demonstrates how to proceed:
https://github.com/readium/SDKLauncher-Android/blob/feature/lcp/SDKLauncher-Android/app/src/main/java/org/readium/sdk/android/launcher/ContainerList.java#L722
Note that in this sample app, the downloaded EPUB filename simply matches that of the original license file, so that alphabetical sorting in the file browser reveals the files side-by-side. The original *.lcpl file is not deleted.

Note that the NetProvider C++ abstraction is not used here, to avoid unnecessary JNI calls. Instead, the HTTP request is created and the response is handled directly in application code (Java, using the Ion library), including the incremental progress report which informs the user about the size of the downloading EPUB publication:
https://github.com/readium/SDKLauncher-Android/blob/feature/lcp/SDKLauncher-Android/app/src/main/java/org/readium/sdk/android/launcher/ContainerList.java#L833

The license injection ; i.e. adding the META-INF/license.lcpl file to the EPUB zip archive ; is implemented in C++ via the LCP service utility function injectLicense(), which saves implementors having to write this code for each target platform:
https://github.com/readium/SDKLauncher-Android/blob/feature/lcp/SDKLauncher-Android/app/src/main/java/org/readium/sdk/android/launcher/ContainerList.java#L914

One final implementation note: the ENABLE_NET_PROVIDER_ACQUISITION preprocessor directive is deactivated by default, and thereby excludes legacy code (from the C++ build) that was originally used to implement an abstraction layer for downloading a LCP-linked EPUB. The decision was made to "backport" this abstraction layer into the application domain (i.e. as centralized, concrete platform-specific code), so that the overall source tree is easily debuggable and maintainable (this is particularly relevant with Android Java + JNI / C++). See for example the "Acquisition" files in:
https://github.com/readium/readium-lcp-client/tree/develop/platform/android/lib/src/main/java/org/readium/sdk/lcp
and:
https://github.com/readium/readium-lcp-client/tree/develop/platform/android/lcp/src/main/jni

Device ID (LSD registration)

In the Android Readium SDK "launcher" app, a unique identifier (String id = UUID.randomUUID().toString();) is created the first time the LSD registration is performed:
https://github.com/readium/SDKLauncher-Android/blob/feature/lcp/SDKLauncher-Android/app/src/main/java/org/readium/sdk/android/launcher/ContainerList.java#L466
The value is stored in local storage for future re-use (m_context.getSharedPreferences("DEVICE_ID", Context.MODE_PRIVATE);), and is therefore re-created if the app is uninstalled. Additionally, the StatusDocumentProcessing.IDeviceIDManager instance handles the tracking of already-registered LSD documents, so that the client app doesn't generate unnecessary register HTTP requests with the LSD server.

A real-world reading system would probably need to override this behaviour with a stricter definition of "device identifier", suitable for the retail chain / publisher infrastructure, and/or depending on the targeted Android platform.