Readium Logo

Content Protection

Summary

Offers a way to support more content protection technologies in Readium 2.

Motivation

DRMs and other protection technologies can be vastly different, therefore we don’t want to make a generic system fitting the needs of every arbitrary DRM. Instead, we should only focus on the features that are actually used in Readium components, such as resource decryption and rights consumption.

We want to:

Peripheral features should be handled by reading apps themselves, which control the supported DRMs, for example:

Developer Guide

A Content Protection can be different things:

Unlocking a Publication

Since it’s usually possible to read the metadata of a publication without unlocking its protection, the credentials are not always necessary. If the provided credentials are incorrect or missing, the Publication can be returned in a restricted state: parts of its manifest are readable, but not all of its resources.

However, if you need to render the publication to the user, you can set the allowUserInteraction parameter to true. If the given credentials are incorrect, then the Content Protection will be allowed to ask the user for its credentials.

streamer.open(asset, allowUserInteraction = true, credentials = "open sesame")

Some Content Protections – e.g. ZIP encryption – prevent reading a publication’s metadata even in a restricted state without the proper credentials. In this case, a Publication.OpeningError.IncorrectCredentials error will be returned.

Using Third-Party Protections

Format-specific protections are natively handled by the Streamer, since they are tied to the asset format. However, for third-party technologies such as a DRM, you need to register them by providing a ContentProtection instance to the Streamer. Here’s an example for LCP:

streamer = Streamer(
    contentProtections: [
        // The provided `authentication` argument is part of the LCP library
        // and is used to retrieve the user passphrase when needed.
        lcpService.contentProtection(authentication)
    ]
)

Rendering a Publication

To render the publication, reading apps must first check if it is restricted. Navigators must refuse to be created with a restricted publication.

if (!publication.isRestricted) {
    presentNavigator(publication)
}

Consuming User Rights

Some Content Protection technologies support user rights, such as copy or print. It’s possible to consume these rights using the UserRights API, for example, to copy a text selection:

if (publication.rights.copy(text)) {
    pasteboard.add(text)
}

Sometimes, you need to know whether the copy action is allowed before actually consuming the right, for example to know if you can display a sharing popup. In which case, you can use canCopy(text: String). For a more general purpose, the property canCopy indicates whether the action is at all possible and can be used to grey out a “Copy” button.

Backward Compatibility and Migration

Mobile (Swift and Kotlin)

This new API is not backward-compatible with the current support for LCP and reading apps will need to update their integration.

Reference Guide

The Readium toolkit offers different tools to address the various Content Protection technologies:

Content Protections

Content Protections can be grouped in two categories:

Readium supports only a single enabled Content Protection per publication, because cumulating rights consumption or decryption from different sources can’t be done blindly. Third-party protections provided to the Streamer take precedence (in order) over any format-specific protection.

Format-Specific Protections

There are currently only two format-specific protections recognized by Readium: PDF and ZIP.

When a password protection is used, the credentials parameter provided to the Streamer is used to unlock the protected asset. In case of incorrect credentials:

ZIP Protection

There are many encryption methods supported in ZIP, some proprietary. Readium can’t reasonably handle all of it, and the supported protections will depend on the underlying ZIP library used. Traditional PKWARE encryption, even though not really secure, is widely used to add password protection to a ZIP, and should therefore be supported if possible.

PDF Protection

PDF supports encryption protected by password and user permissions.

If a PDF contains user permissions, such as “copy”, then the Streamer will expose them through a UserRights instance attached to the ContentProtectionService. Only a subset of PDF permissions are supported: the ones used in Readium. For example, the “changes allowed” permission doesn’t make sense since we can’t edit a PDF in Readium.

Note that Readium supports only publication-level protections. If a ZIP package contains a password-protected PDF file, then the PDF won’t be readable in Readium.

Third-Party Protections

Third-party protections can be widely different, therefore the Readium toolkit avoids making assumptions about the way they work. This means that only the core features used in Readium – publication locking, resources transformation and rights consumption – need to be implemented with protection-agnostic interfaces. Any peripheral features, such as managing loans or presenting license information, are out of scope for Readium. They should be handled by reading apps themselves, using the APIs provided by the Content Protection library.

A third-party protection library (or bridge) should implement the ContentProtection interface, which will be registered to the Streamer and used when parsing a publication.

Unlocking a Publication

A protected publication can be opened in two states: restricted or unrestricted. A restricted publication has a limited access to its manifest and resources and can’t be rendered with a Navigator. It is usually only used to import a publication to the user’s bookshelf.

Readium makes no assumption about the way a third-party protection can unlock a publication. It could for example:

ContentProtection is allowed to prompt the user for its credentials only if the allowUserInteraction parameter is set to true. The rationale is that a reading app might want to import a collection of publications, in which case the user should not be asked for all its credentials. However, background requests are allowed at all time.

Note that if, for a given third-party protection, a restricted publication can’t be used to create its Manifest at all, then the parsing should fail with an IncorrectCredentials error, like with a password-protected ZIP.

API Reference (r2-shared)

UserRights Interface

Manages consumption of user rights and permissions.

Copy
Print
Implementations

ContentProtectionService Interface (implements Publication.Service)

Provides information about a publication’s content protection and manages user rights.

Properties
Publication Helpers
Web Service
content-protection Route
{
  "isRestricted": false,
  "error": {
    "en": "The publication has expired."
  },
  "name": {
    "en": "Readium LCP"
  },
  "rights": {
    "canCopy": true,
    "canPrint": false
  }
}
rights/copy Route

If peek is true, then it’s equivalent to calling publication.rights.canCopy(...): the right is not consumed.

Response

Status Code Description
200 The copy is allowed.
403 The copy is forbidden.
rights/print Route

If peek is true, then it’s equivalent to calling publication.rights.canPrint(...): the right is not consumed.

Response

Status Code Description
200 The print is allowed.
403 The print is forbidden.

API Reference (r2-streamer)

Streamer Extensions

Constructor

Two new arguments are added to the constructor: contentProtections and onAskCredentials.

Methods

There are three new parameters added to Streamer::open(): allowUserInteraction, credentials, and sender.

ContentProtection Interface

Bridge between a Content Protection technology and the Readium toolkit.

Its responsibilities are to:

Methods
ProtectedAsset Class

Holds the result of opening a PublicationAsset with a ContentProtection.

All the constructor parameters are public.

Extensibility for Third-Party Protections

While the presented protection features are only limited to the ones used by Readium components, the architecture is opened to third-party extensions.

Publication-Scoped Extensions

ContentProtectionService is a place of choice for extensibility, because it is attached to the Publication object. You can add any extra features and, optionally, expose them as publication helpers.

Here’s an example of how to expose the license of the LCP DRM inside the Publication:

class LCPContentProtectionService: ContentProtectionService {

    let license: LCPLicense

}

extension Publication {

    var lcpLicense: LCPLicense? {
        findService(ContentProtectionService.self)?.license
    }

}

// Then the reading app can access the license from the `Publication` itself:
publication.lcpLicense?.renew()

Globally-Scoped Extensions

You can also add global features in your implementation of the ContentProtection interface. This class is instanciated by reading apps, so you have full control over the exposed API.

A common use case would be to require the reading app to implement a custom callback interface to ask the user for the credentials when requested. Here’s an example with LCP:

interface LCPAuthenticating {
    requestPassphrase(license: LCPAuthenticatedLicense): String?
}

class LCPContentProtection extends ContentProtection {
    authentication: LCPAuthenticating

    constructor(authentication: LCPAuthenticating) {
        this.authentication = authentication
    }
}

Rationale and Alternatives

Unlocking is Only Done During Parsing

Compared to the current implementation, a Publication is opened either unrestricted, or restricted. There’s no way to unlock an existing Publication without reparsing it. The reasons for this choice are:

Peripheral Features

Peripheral features were initially considered to be integrated in a DRM-agnostic interface: displaying rights information, renewing loans, etc. However, it was guided by the LCP specification, and it became clear that it wouldn’t really be agnostic.

For example, LCP is limiting copy by a character count. But a DRM could restrict word count, or number of copies. Therefore, it’s not easy to display localized information about the remaining copy right available.

Besides, these features don’t live in Readium components but in reading apps. You are welcome to create your own DRM-agnostic adapter for such features.

Future Possibilities

Invalidating the Publication Asset

A Content Protection technology might alter the publication asset during the lifetime of the Publication object. For example, by injecting an updated license file after renewing a loan. This could potentially be an issue for opened file handlers in the leaf Fetcher accessing the publication asset.

A solution would be the introduction of an InvalidatingFetcher, which would be able to recreate its child Fetcher on-demand. The ContentProtectionService would keep a reference to this InvalidatingFetcher, and call invalidate() every time the publication asset is updated.

InvalidatingFetcher Class

Constructor
Methods