Permalink
Switch branches/tags
Nothing to show
Find file Copy path
169 lines (84 sloc) 10.8 KB

Readium LCP DRM support in Readium-2

This document describes how a R2 based app should handle DRMs. It details the Readium LCP support, but other DRM modules should be handled the same way.

A protected publication may be imported in an R2 app in two ways:

  • import of a protected EPUB, i.e. an EPUB containing a DRM license;
  • import of a DRM licence, e.g. a .lcpl file

These resources may be imported via side loading (Open with ... from a web page on a mobile app, Open file ... in a desktop app), via an OPDS feed or any other supported way to access extenal resources.

To get more details about Reading system behavior in a Readium LCP context, please read the Readium LCP spec, specifically section 7 (Reading System Behavior).

Being ready

A Readium LCP compliant app must embed a root certificate provided by the DRM administrator.

A Readium LCP compliant app must update the Readium LCP certificate revocation list (CRL) on a regular basis (e.g. on a weekly basis, at the time the app is launched).

A Readium LCP compliant app must generate a unique device id and human readable device name at install time.

A quick note about the device ID: both Android and iOS "launcher app" default open-source implementations use UUID coupled with a per-app-install persistent storage (i.e. device "id" gets wiped and renewed if app is removed then reinstalled).

Android sample 1 and Android sample 2

iOS sample 1 and iOS sample 2

Check if the EPUB is DRM protected

An LCP protected publication is signaled by the presence of a license document (META-INF/license.lcpl) plus certain specific values in META-INF/encryption.xml which indicate which resources are encrypted and with which algorthm.

As the licence document is mandatory, the app must raise an error if this file is missing but the content is declared encrypted. Also, the app should check that all resources referenced in encryption.xml are found in the EPUB archive.

Import a protected publication

An app which imports or opens an EPUB protected by LCP (i.e. containing its license) will follow these steps:

1/ Validate the license structure and check its profile identifier

The app checks that the license is valid (EDRLab provides a JSON schema for LCP licenses in the EDRLab github, lcp-testing-tools). It the license is invalid, the user gets a notification like "This Readium LCP license is invalid, the publication cannot be processed".

The app checks that the profile identifier in the license can be processed. If it is not the case, the user gets a notification like "This Readium LCP license has a profile identifier that this app cannot handle, the publication cannot be processed".

2/ Get the passphrase associated with the license

Each LCP license is associated with a passphrase. Providers usually provide the same passphrase for all licenses issued to the same user, but this is not a MUST (this is a SHOULD): a provider may for instance decide to use one passphrase per ebook collection or per user subscription. The passphrase may be changed from time to time, usually at the request of the user. In such a case, every license generated by the provider for the user will usually be updated to handle the new passphrase, but this not required by the specification.

The app must therefore do the following:

a/ If the user id has been indicated in the license (it is highly recommended but not required), check if one or more passphrase hash associated with licenses from the same user (by origin + user id) have been stored. If one or more values are found, call the r2-lcp-client library (C++) with the json license and the array of passphrase hash as parameters. The lib returns the correct passphrase, if any, or an error if none is correct. If ok jump to 3/.

b/ Check if a passphrase hash has already been stored for the license. If yes, the app calls the r2-lcp-client library (C++) with the json license and the passphrase hash as parameters. The lib returns the passphrase hash if it can decrypt the license key_check, or an error if the passphrase is incorrect. If ok jump to 3/.

c/ Display the hint and ask the passphrase to the user. The app the calls the r2-lcp-client library (C++) with the license and the passphrase hash as parameters. The lib returns the passphrase hash if it can decrypt the license key_check, or an error if the passphrase is incorrect. Loop until the user enters the correct passphrase or quits.

If the user has entered a good passphrase for the license, store the (license id, origin, user id, passphrase hash) tuple. It will allow later for checks a/ and b/. Note that a record will only be created when the user enters a new passphrase, meaning that this storage cannot be used as a catalog of all licenses acquired by the user.

Note also that the hash algorithm may depend on the LCP profile used in the license; therefore an evolution of the lib could calculate the hash value from the clear passphrase.

3/ Validate the license integrity

Before checking the license status, it's good to verify the license integrity.

The app calls the r2-lcp-client library (createContext()) with the license, the passphrase hash and CRL as parameters.

The r2-lcp-client library will verify the license integrity and create a DRM context, i.e.:

  • check the signature of the provider certificate using the embedded root certificate;
  • check that the provider certificate is not in the CRL;
  • check that the provider certificate was not expired when the license was last updated;
  • validate the signature of the license;
  • check the user key;
  • check the date rights. An LCP license handles a datetime start and datetime end, which must be compared with the system datetime.
  • return a "context" structure, to be used later in decryption calls.

See the Readium LCP spec section 5.5 for additional details.

4/ Check the license status

An LCP license may contain a "status" link, i.e. a link to a status document. If it is the case and if the app is online, the app must:

1/ Fetch the status document.

If the Status Document is unavailable or if the client is unable to obtain an internet connection, it MUST NOT block the user from accessing the Publication tied to the License Document. Jump to step 7.

2/ Validate the structure of the status document. If the structure is not valid, the app must jump to the next step, as if no status link was present in the license.

3/ Check that the status is "ready" or "active".

If this is not the case (revoked, returned, cancelled, expired), the app will notify the user and stop there. The message to the user must be clear about the status of the license: don't display "expired" if the status is "revoked". The date and time corresponding to the new status should be displayed (e.g. "The license expired on 01 January 2018").

If the license has been revoked, the user message should display the number of devices which registered to the server. This count can be calculated from the number of "register" events in the status document. If no event is logged in the status document, no such message should appear (certainly not "The license was registered by 0 devices").

5/ Get an updated license if needed

If the license timestamp in the 'updated' object of the Status Document is more recent than the timestamp contained in the local copy of the License Document, the client MUST download the License Document again.

It must then validate again the license structure and integrity. The expiration date is tested again at this time (the license take precedence over the status document).

If the license is ok, the app replaces the previous copy with the new one.

Note 1: it implies that if the user has changed his passphrase on the provider's end, the "old" passphrase is still ok until the 'updated' timestamp of the status document is modified (e.g. after a loan return).

Note 2: if the status value of a Status Document contradicts the corresponding up-to-date License Document, the up-to-date License Document takes precedence.

6/ Register the device / license

If the app is online, it must silently (= non-blocking for the user):

1/ check if the device / license is already registered. If it is the case, the app moves on.

2/ call the "register" link associated with the license id, passing a device id and device name as parameters. In case of error, the app must let the user read the publication.

3/ If the registration was successful, store the fact the the device / license has been registered.

7/ Open the publication

Import a DRM license

An app which imports a DRM license will follow these steps (see the previous section for more details):

1/ Validate the license structure and check its profile identifier

2/ Get the passphrase associated with the license

3/ Validate the license integrity

4/ Check the license status

5/ Get an updated license if needed

6/ Fetch the encrypted publication

In the LCP use case, the app will use the "publication" link. It will store the encrypted publication and insert the license as META-INF/license.lcpl. In case of error, the user is notified and the app stops there.

7/ Register the device / license

8/ Open the publication

Open a protected publication stored in the app catalog

The process is a simpler than when the protected publication is imported, as some information about the license is stored in the database, especially the license identifier.

The process start on step 4 (Check the license status), followed by step 5 (Get an updated license if needed) if needed. If everything is ok, the publication can be read.

Decrypt a publication

For each encrypted resource or chunk, the app will call r2-lcp-lib (C++), passing the context previously initialized and the encrypted content as parameters.

Check the print & copy rights

Each time the user decides to print a page or copy a range of characters, the app will

1/ check the current counter vs the corresponding right

The app will verify that the stored counter plus the additional volume to be printed or copied does not exceed the rights expressed in the license.

2/ store the new counter