@@ -8,42 +8,50 @@ package org.readium.r2.lcp
88
99import org.readium.r2.lcp.auth.LcpPassphraseAuthentication
1010import org.readium.r2.lcp.license.model.LicenseDocument
11+ import org.readium.r2.shared.publication.encryption.Encryption
1112import org.readium.r2.shared.publication.encryption.encryption
12- import org.readium.r2.shared.publication.flatten
13+ import org.readium.r2.shared.publication.epub.EpubEncryptionParser
1314import org.readium.r2.shared.publication.protection.ContentProtection
1415import org.readium.r2.shared.publication.services.contentProtectionServiceFactory
1516import org.readium.r2.shared.util.AbsoluteUrl
1617import org.readium.r2.shared.util.DebugError
1718import org.readium.r2.shared.util.ThrowableError
1819import org.readium.r2.shared.util.Try
20+ import org.readium.r2.shared.util.Url
1921import org.readium.r2.shared.util.asset.Asset
20- import org.readium.r2.shared.util.asset.AssetRetriever
22+ import org.readium.r2.shared.util.asset.AssetOpener
2123import org.readium.r2.shared.util.asset.ContainerAsset
2224import org.readium.r2.shared.util.asset.ResourceAsset
25+ import org.readium.r2.shared.util.data.Container
2326import org.readium.r2.shared.util.data.ReadError
27+ import org.readium.r2.shared.util.data.decodeRwpm
28+ import org.readium.r2.shared.util.data.decodeXml
29+ import org.readium.r2.shared.util.data.readDecodeOrElse
2430import org.readium.r2.shared.util.flatMap
31+ import org.readium.r2.shared.util.format.Format
32+ import org.readium.r2.shared.util.format.Trait
2533import org.readium.r2.shared.util.getOrElse
34+ import org.readium.r2.shared.util.resource.Resource
2635import org.readium.r2.shared.util.resource.TransformingContainer
2736
2837internal class LcpContentProtection (
2938 private val lcpService : LcpService ,
3039 private val authentication : LcpAuthenticating ,
31- private val assetRetriever : AssetRetriever
40+ private val assetOpener : AssetOpener
3241) : ContentProtection {
3342
34- override val scheme: ContentProtection .Scheme =
35- ContentProtection .Scheme .Lcp
36-
37- override suspend fun supports (
38- asset : Asset
39- ): Try <Boolean , Nothing > =
40- Try .success(lcpService.isLcpProtected(asset))
41-
4243 override suspend fun open (
4344 asset : Asset ,
4445 credentials : String? ,
4546 allowUserInteraction : Boolean
46- ): Try <ContentProtection .Asset , ContentProtection .OpenError > {
47+ ): Try <ContentProtection .OpenResult , ContentProtection .OpenError > {
48+ if (
49+ ! asset.format.conformsTo(Trait .LCP_PROTECTED ) &&
50+ ! asset.format.conformsTo(Format .LCP_LICENSE_DOCUMENT )
51+ ) {
52+ return Try .failure(ContentProtection .OpenError .AssetNotSupported ())
53+ }
54+
4755 return when (asset) {
4856 is ContainerAsset -> openPublication(asset, credentials, allowUserInteraction)
4957 is ResourceAsset -> openLicense(asset, credentials, allowUserInteraction)
@@ -54,7 +62,7 @@ internal class LcpContentProtection(
5462 asset : ContainerAsset ,
5563 credentials : String? ,
5664 allowUserInteraction : Boolean
57- ): Try <ContentProtection .Asset , ContentProtection .OpenError > {
65+ ): Try <ContentProtection .OpenResult , ContentProtection .OpenError > {
5866 val license = retrieveLicense(asset, credentials, allowUserInteraction)
5967 return createResultAsset(asset, license)
6068 }
@@ -71,40 +79,73 @@ internal class LcpContentProtection(
7179 return lcpService.retrieveLicense(asset, authentication, allowUserInteraction)
7280 }
7381
74- private fun createResultAsset (
82+ private suspend fun createResultAsset (
7583 asset : ContainerAsset ,
7684 license : Try <LcpLicense , LcpError >
77- ): Try <ContentProtection .Asset , ContentProtection .OpenError > {
85+ ): Try <ContentProtection .OpenResult , ContentProtection .OpenError > {
7886 val serviceFactory = LcpContentProtectionService
7987 .createFactory(license.getOrNull(), license.failureOrNull())
8088
81- val decryptor = LcpDecryptor (license.getOrNull())
89+ val encryptionData =
90+ when {
91+ asset.format.conformsTo(Trait .EPUB ) -> parseEncryptionDataEpub(asset.container)
92+ else -> parseEncryptionDataRpf(asset.container)
93+ }
94+ .getOrElse { return Try .failure(ContentProtection .OpenError .Reading (it)) }
95+
96+ val decryptor = LcpDecryptor (license.getOrNull(), encryptionData)
8297
8398 val container = TransformingContainer (asset.container, decryptor::transform)
8499
85- val protectedFile = ContentProtection .Asset (
86- mediaType = asset.mediaType,
87- container = container,
100+ val protectedFile = ContentProtection .OpenResult (
101+ asset = ContainerAsset (
102+ format = asset.format,
103+ container = container
104+ ),
88105 onCreatePublication = {
89- decryptor.encryptionData = (manifest.readingOrder + manifest.resources + manifest.links)
90- .flatten()
91- .mapNotNull {
92- it.properties.encryption?.let { enc -> it.url() to enc }
93- }
94- .toMap()
95-
96106 servicesBuilder.contentProtectionServiceFactory = serviceFactory
97107 }
98108 )
99109
100110 return Try .success(protectedFile)
101111 }
102112
113+ private suspend fun parseEncryptionDataEpub (container : Container <Resource >): Try <Map <Url , Encryption >, ReadError> {
114+ val encryptionResource = container[Url (" META-INF/encryption.xml" )!! ]
115+ ? : return Try .failure(ReadError .Decoding (" Missing encryption.xml" ))
116+
117+ val encryptionDocument = encryptionResource
118+ .readDecodeOrElse(
119+ decode = { it.decodeXml() },
120+ recover = { return Try .failure(it) }
121+ )
122+
123+ return Try .success(EpubEncryptionParser .parse(encryptionDocument))
124+ }
125+
126+ private suspend fun parseEncryptionDataRpf (container : Container <Resource >): Try <Map <Url , Encryption >, ReadError> {
127+ val manifestResource = container[Url (" manifest.json" )!! ]
128+ ? : return Try .failure(ReadError .Decoding (" Missing manifest" ))
129+
130+ val manifest = manifestResource
131+ .readDecodeOrElse(
132+ decode = { it.decodeRwpm() },
133+ recover = { return Try .failure(it) }
134+ )
135+
136+ val encryptionData = manifest
137+ .let { (it.readingOrder + it.resources) }
138+ .mapNotNull { link -> link.properties.encryption?.let { link.url() to it } }
139+ .toMap()
140+
141+ return Try .success(encryptionData)
142+ }
143+
103144 private suspend fun openLicense (
104145 licenseAsset : ResourceAsset ,
105146 credentials : String? ,
106147 allowUserInteraction : Boolean
107- ): Try <ContentProtection .Asset , ContentProtection .OpenError > {
148+ ): Try <ContentProtection .OpenResult , ContentProtection .OpenError > {
108149 val license = retrieveLicense(licenseAsset, credentials, allowUserInteraction)
109150
110151 val licenseDoc = license.getOrNull()?.license
@@ -145,14 +186,14 @@ internal class LcpContentProtection(
145186
146187 val asset =
147188 if (link.mediaType != null ) {
148- assetRetriever.retrieve (
189+ assetOpener.open (
149190 url,
150191 mediaType = link.mediaType
151192 )
152193 .map { it as ContainerAsset }
153194 .mapFailure { it.wrap() }
154195 } else {
155- assetRetriever.retrieve (url)
196+ assetOpener.open (url)
156197 .mapFailure { it.wrap() }
157198 .flatMap {
158199 if (it is ContainerAsset ) {
@@ -172,13 +213,13 @@ internal class LcpContentProtection(
172213 return asset.flatMap { createResultAsset(it, license) }
173214 }
174215
175- private fun AssetRetriever.RetrieveError .wrap (): ContentProtection .OpenError =
216+ private fun AssetOpener.OpenError .wrap (): ContentProtection .OpenError =
176217 when (this ) {
177- is AssetRetriever . RetrieveError .FormatNotSupported ->
218+ is AssetOpener . OpenError .FormatNotSupported ->
178219 ContentProtection .OpenError .AssetNotSupported (this )
179- is AssetRetriever . RetrieveError .Reading ->
220+ is AssetOpener . OpenError .Reading ->
180221 ContentProtection .OpenError .Reading (cause)
181- is AssetRetriever . RetrieveError .SchemeNotSupported ->
222+ is AssetOpener . OpenError .SchemeNotSupported ->
182223 ContentProtection .OpenError .AssetNotSupported (this )
183224 }
184225}
0 commit comments