You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 4 Next »

Readium 2 DRM Support

Readium 2 support for DRM currently is limited to LCP only. However, it is possible to change Readium 2 libraries to add a custom DRM. This section describes the way Readium 2 decrypts protected books and the changes that were made to enable Adobe DRM support.

Definition of DRMs (brands and schemes) can be found in NYPL’s fork of R2Shared framework, which was altered to include Adobe DRM.

DRM support is built into NYPL’s fork of R2Streamer framework, where DRMDecoder is responsible for actual decoding of a stream. DRMDecoder calls FullDRMInputStream to decode input stream, which, in return, calls DRM.license.decipher function to decrypt the stream.

When the user opens a book, LibraryService has to parse the book before presenting it on screen. When LibraryService parses the book, it creates two base objects used later in the process, Container and Publication. During parsing, the DRM method of the book is defined in scanForDRM() function, and the container stores this DRM method as a property. At that point, only the brand of the DRM is defined, actual code for decrypting is defined later, when the publication is loaded.

In our solution, LibraryService creates an array of DRM services to decode .epub files: LCPLibraryService (added only in Readium 2 app and not used in our solution) and AdobeDRMLibraryService. AdobeDRMLibraryService loads publication, adding AdobeDRMLicense to the DRM described above, as a method to decrypt anything that is passed to it. AdobeDRMLicense creates AdobeDRMContainer for each publication, and this container bridges Swift functions with C++ functions of Adobe software.

To support Adobe DRM in Readium 2, two libraries were forked and changed:


The changes in their code are not to be submitted upstream to the main Readium 2 repositories; this code is for SimplyE use only.

When Readium 2 team modifies the code in these libraries, we need to update our libraries to apply the changes as well. You can find changes made in the libraries below in bold.

Changes in R2Shared

In `DRM/DRM.swift` the new brand and scheme for Adobe DRM (added code is bold):

public struct DRM {
    public let brand: Brand
    public let scheme: Scheme
    
    /// The license will be filled when passed back to the DRM module.
    
    public var license: DRMLicense?
    public enum Brand: String {
        case lcp
        case adobe
    }

    public enum Scheme: String {
        case lcp = "http://readium.org/2014/01/lcp"
        case adobe = "http://ns.adobe.com/adept"
    }

    public init(brand: Brand) {
        self.brand = brand
        switch brand {
        case .lcp:
            scheme = .lcp
        case .adobe:
            scheme = .adobe
        }
    }
}


Commit:

https://github.com/NYPL-Simplified/r2-shared-swift/commit/229e057d87f81d3f57b0090832d0fee5f9fb7aee

In `Publication/Publication.swift` changed `public let otherCollections...` to `public var otherCollections` to make table of contents update possible.

Commit:

https://github.com/NYPL-Simplified/r2-shared-swift/commit/f23debe8e9a5c2b2f1b3923b316f96916cfb3475

Changes in R2Streamer

In `Parser/EPUB/EPUBParser.swift` return Adobe DRM (added code is bold):

 private static func scanForDRM(in container: Container) -> DRM? {  
  /// LCP.
  // Check if a LCP license file is present in the container.
  if ((try? container.data(relativePath: EPUBConstant.lcplFilePath)) != nil) {
     return DRM(brand: .lcp)
   }
   return DRM(brand: .adobe)
   // return nil
  }


    private static func scanForDRM(in container: Container) -> DRM? {

        /// LCP.

        // Check if a LCP license file is present in the container.

         if ((try? container.data(relativePath: EPUBConstant.lcplFilePath)) != nil) {

             return DRM(brand: .lcp)

         }

         return DRM(brand: .adobe)

         // return nil

     }

Commit:

https://github.com/NYPL-Simplified/r2-streamer-swift/commit/9a53944a6bac0280eb4902155647c1e5bf0e5a9e

In `Parser/EPUB/EPUBEncryptionParser.swift` added Adobe encryption scheme (added code in bold):

    func parseEncryptions() -> [String: Encryption] {

        guard let document = document else {

            return [:]

        }

        var encryptions: [String: Encryption] = [:]

        

        // Loop through <EncryptedData> elements..

        for encryptedDataElement in document.xpath("./enc:EncryptedData") {

            guard let algorithm = encryptedDataElement.firstChild(xpath: "enc:EncryptionMethod")?.attr("Algorithm"),

                var resourceURI = encryptedDataElement.firstChild(xpath:"enc:CipherData/enc:CipherReference")?.attr("URI")?.removingPercentEncoding else

            {

                continue

            }

            resourceURI = normalize(base: "/", href: resourceURI)

            var encryption = Encryption(algorithm: algorithm)

            // LCP. Tag LCP protected resources.

            let keyInfoURI = encryptedDataElement.firstChild(xpath: "ds:KeyInfo/ds:RetrievalMethod")?.attr("URI")

            if keyInfoURI == "license.lcpl#/encryption/content_key",

                drm?.brand == DRM.Brand.lcp

            {

                 encryption.scheme = drm?.scheme.rawValue

             }

             // LCP END.

             

             if drm?.brand == .adobe {

                 encryption.scheme = drm?.scheme.rawValue

             }

. . .

Commit:

https://github.com/NYPL-Simplified/r2-streamer-swift/commit/08c6d6a204cebff5137f868794a412b453437a5b

In `Parser/EPUB/EPUBEncryptionParser.swift` changed how Adobe encryption scheme is assigned: 

scheme = drm?.scheme.rawValue

In `Parser/EPUB/EPUBParser.swift` added `links` to the initialized publication

publication.links = links

Commit:

https://github.com/NYPL-Simplified/r2-streamer-swift/commit/ffffd5e37f7e422d42aa7478b845a313d8767187

Adobe DRM Support

Solution Files

Adobe DRM support files can be found in the `Simplified/Reader2/AdobeDRM` folder of the solution.

AdobeDRMLibraryService.swift


Conforms to DRMLibraryService protocol, that defines the brand of a DRM and methods to check if the file can be fulfilled with this DRM service, to load DRM license into DRM structure. Effectively, that part of the code creates AdobeDRMLicense for a publication.

AdobeDRMLicense.swift

Conforms to DRMLicense protocol; it initialises AdobeDRMContainer and decrypts data calling AdobeDRMContainer decode(data: Data) function. License (in terms of Readium 2) is the part of DRM that performs actual decryption.

AdobeDRMContainer.h, AdobeDRMContainer.mm

AdobeDRMContainer wraps C++ code of the project, initialises internal objects required by Adobe DRM Connector library and provides data decoding for AdobeDRMLicense.

Adding Adobe DRM Support in SimplyE

In `Simplified/Reader2/Internal/LibrayService.swift` init adds AdobeDRMLibraryService to drmLibraryServices array (added code in bold):

  init(publicationServer: PublicationServer) {

    self.publicationServer = publicationServer

    #if LCP

    drmLibraryServices.append(LCPLibraryService())

    #endif

    

    drmLibraryServices.append(AdobeDRMLibraryService())

  }

Decrypting Table of Contents

The current version of Readium 2 parses navigation document without decrypting it. As a result, the table of contents appears blank in the app. To show table of contents in the app, additional decryption was required, and LibraryService.swift parsePublication(at url: URL) function was modified (added code is bold):

  private func parsePublication(at url: URL) -> PubBox? {

    do {

      guard let (pubBox, parsingCallback) = try Publication.parse(at: url) else {

        return nil

      }

      let (publication, container) = pubBox

      // TODO: SIMPLY-2840

      // Parse .ncx document to update TOC and page list if publication doesn't contain TOC

      // -- the code below should be removed as described in SIMPLY-2840 --

      if publication.tableOfContents.isEmpty {

        publication.otherCollections.append(contentsOf: parseNCXDocument(in: container, links: publication.links))

      }

      // -- end of cleanup --

      items[url.lastPathComponent] = (container, parsingCallback)

      return (publication, container)

    } catch {

      log(.error, "Error parsing publication at '\(url.absoluteString)': \(error.localizedDescription)")

      return nil

    }

  }

}

And parseNCXDocument(from container: Container, to publication: inout Publication) function was added in an extension to LibraryService. 

The way Readium 2 creates Publication and Container objects may change in the future, the idea is to decrypt ncxDocumentData as shown below:

Original code:

  private func parseNCXDocument(in container: Container, links: [Link]) -> [PublicationCollection] {

      // Get the link in the readingOrder pointing to the NCX document.

      guard let ncxLink = links.first(withType: .ncx),

          let ncxDocumentData = try? container.data(relativePath: ncxLink.href) else

      {

          return []

      }

      let ncx = NCXParser(data: ncxDocumentData, at: ncxLink.href)

      func makeCollection(_ type: NCXParser.NavType, role: String) -> PublicationCollection? {

          let links = ncx.links(for: type)

          guard !links.isEmpty else {

              return nil

          }

          return PublicationCollection(role: role, links: links)

      }

      return [

          makeCollection(.tableOfContents, role: "toc"),

          makeCollection(.pageList, role: "pageList")

      ].compactMap { $0 }

  }

Modified code decrypts ncx data (in bold):

  private func parseNCXDocument(in container: Container, links: [Link]) -> [PublicationCollection] {

      // Get the link in the readingOrder pointing to the NCX document.

      guard let ncxLink = links.first(withType: .ncx),

          let ncxDocumentData = try? container.data(relativePath: ncxLink.href) else

      {

          return []

      }

      // this part is added to decrypt ncx data

      var data = ncxDocumentData

      let license = AdobeDRMLicense(for: container)

      if let optionalDecipheredData = try? license.decipher(ncxDocumentData),

          let decipheredData = optionalDecipheredData {

          data = decipheredData

      }

      // NCXParser here parses data instead of ncxDocumentData

      let ncx = NCXParser(data: data, at: ncxLink.href)

      func makeCollection(_ type: NCXParser.NavType, role: String) -> PublicationCollection? {

          let links = ncx.links(for: type)

          guard !links.isEmpty else {

              return nil

          }

          return PublicationCollection(role: role, links: links)

      }

      return [

          makeCollection(.tableOfContents, role: "toc"),

          makeCollection(.pageList, role: "pageList")

      ].compactMap { $0 }

  }

This workaround is temporary, the version of Readium 2 that supports multiple DRM services won’t need additional parsing of the navigation document, and this workaround should be removed.

Once multiple DRM support is done, it’s safe to remove:

  • LibraryService extension in `Simplified/Reader2/Internal/LibraryService.swift` marked with // TODO: SIMPLY-2840 // This extension should be removed as a part of the cleanup;
  • Code in parsePublication(at url: URL) marked with // TODO: SIMPLY-2840.
  • No labels