@@ -8,42 +8,50 @@ package org.readium.r2.lcp
8
8
9
9
import org.readium.r2.lcp.auth.LcpPassphraseAuthentication
10
10
import org.readium.r2.lcp.license.model.LicenseDocument
11
+ import org.readium.r2.shared.publication.encryption.Encryption
11
12
import org.readium.r2.shared.publication.encryption.encryption
12
- import org.readium.r2.shared.publication.flatten
13
+ import org.readium.r2.shared.publication.epub.EpubEncryptionParser
13
14
import org.readium.r2.shared.publication.protection.ContentProtection
14
15
import org.readium.r2.shared.publication.services.contentProtectionServiceFactory
15
16
import org.readium.r2.shared.util.AbsoluteUrl
16
17
import org.readium.r2.shared.util.DebugError
17
18
import org.readium.r2.shared.util.ThrowableError
18
19
import org.readium.r2.shared.util.Try
20
+ import org.readium.r2.shared.util.Url
19
21
import 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
21
23
import org.readium.r2.shared.util.asset.ContainerAsset
22
24
import org.readium.r2.shared.util.asset.ResourceAsset
25
+ import org.readium.r2.shared.util.data.Container
23
26
import 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
24
30
import org.readium.r2.shared.util.flatMap
31
+ import org.readium.r2.shared.util.format.Format
32
+ import org.readium.r2.shared.util.format.Trait
25
33
import org.readium.r2.shared.util.getOrElse
34
+ import org.readium.r2.shared.util.resource.Resource
26
35
import org.readium.r2.shared.util.resource.TransformingContainer
27
36
28
37
internal class LcpContentProtection (
29
38
private val lcpService : LcpService ,
30
39
private val authentication : LcpAuthenticating ,
31
- private val assetRetriever : AssetRetriever
40
+ private val assetOpener : AssetOpener
32
41
) : ContentProtection {
33
42
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
-
42
43
override suspend fun open (
43
44
asset : Asset ,
44
45
credentials : String? ,
45
46
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
+
47
55
return when (asset) {
48
56
is ContainerAsset -> openPublication(asset, credentials, allowUserInteraction)
49
57
is ResourceAsset -> openLicense(asset, credentials, allowUserInteraction)
@@ -54,7 +62,7 @@ internal class LcpContentProtection(
54
62
asset : ContainerAsset ,
55
63
credentials : String? ,
56
64
allowUserInteraction : Boolean
57
- ): Try <ContentProtection .Asset , ContentProtection .OpenError > {
65
+ ): Try <ContentProtection .OpenResult , ContentProtection .OpenError > {
58
66
val license = retrieveLicense(asset, credentials, allowUserInteraction)
59
67
return createResultAsset(asset, license)
60
68
}
@@ -71,40 +79,73 @@ internal class LcpContentProtection(
71
79
return lcpService.retrieveLicense(asset, authentication, allowUserInteraction)
72
80
}
73
81
74
- private fun createResultAsset (
82
+ private suspend fun createResultAsset (
75
83
asset : ContainerAsset ,
76
84
license : Try <LcpLicense , LcpError >
77
- ): Try <ContentProtection .Asset , ContentProtection .OpenError > {
85
+ ): Try <ContentProtection .OpenResult , ContentProtection .OpenError > {
78
86
val serviceFactory = LcpContentProtectionService
79
87
.createFactory(license.getOrNull(), license.failureOrNull())
80
88
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)
82
97
83
98
val container = TransformingContainer (asset.container, decryptor::transform)
84
99
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
+ ),
88
105
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
-
96
106
servicesBuilder.contentProtectionServiceFactory = serviceFactory
97
107
}
98
108
)
99
109
100
110
return Try .success(protectedFile)
101
111
}
102
112
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
+
103
144
private suspend fun openLicense (
104
145
licenseAsset : ResourceAsset ,
105
146
credentials : String? ,
106
147
allowUserInteraction : Boolean
107
- ): Try <ContentProtection .Asset , ContentProtection .OpenError > {
148
+ ): Try <ContentProtection .OpenResult , ContentProtection .OpenError > {
108
149
val license = retrieveLicense(licenseAsset, credentials, allowUserInteraction)
109
150
110
151
val licenseDoc = license.getOrNull()?.license
@@ -145,14 +186,14 @@ internal class LcpContentProtection(
145
186
146
187
val asset =
147
188
if (link.mediaType != null ) {
148
- assetRetriever.retrieve (
189
+ assetOpener.open (
149
190
url,
150
191
mediaType = link.mediaType
151
192
)
152
193
.map { it as ContainerAsset }
153
194
.mapFailure { it.wrap() }
154
195
} else {
155
- assetRetriever.retrieve (url)
196
+ assetOpener.open (url)
156
197
.mapFailure { it.wrap() }
157
198
.flatMap {
158
199
if (it is ContainerAsset ) {
@@ -172,13 +213,13 @@ internal class LcpContentProtection(
172
213
return asset.flatMap { createResultAsset(it, license) }
173
214
}
174
215
175
- private fun AssetRetriever.RetrieveError .wrap (): ContentProtection .OpenError =
216
+ private fun AssetOpener.OpenError .wrap (): ContentProtection .OpenError =
176
217
when (this ) {
177
- is AssetRetriever . RetrieveError .FormatNotSupported ->
218
+ is AssetOpener . OpenError .FormatNotSupported ->
178
219
ContentProtection .OpenError .AssetNotSupported (this )
179
- is AssetRetriever . RetrieveError .Reading ->
220
+ is AssetOpener . OpenError .Reading ->
180
221
ContentProtection .OpenError .Reading (cause)
181
- is AssetRetriever . RetrieveError .SchemeNotSupported ->
222
+ is AssetOpener . OpenError .SchemeNotSupported ->
182
223
ContentProtection .OpenError .AssetNotSupported (this )
183
224
}
184
225
}
0 commit comments