Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
package org.readium.r2.lcp

import android.content.Context
import java.io.File
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import org.readium.r2.lcp.license.container.createLicenseContainer
import org.readium.r2.lcp.license.model.LicenseDocument
import org.readium.r2.lcp.util.sha256
import org.readium.r2.shared.extensions.tryOrLog
import org.readium.r2.shared.util.AbsoluteUrl
import org.readium.r2.shared.util.ErrorException
Expand Down Expand Up @@ -173,28 +177,44 @@ public class LcpPublicationRetriever(

private inner class DownloadListener : DownloadManager.Listener {

@OptIn(ExperimentalEncodingApi::class, ExperimentalStdlibApi::class)
override fun onDownloadCompleted(
requestId: DownloadManager.RequestId,
download: DownloadManager.Download
) {
coroutineScope.launch {
val lcpRequestId = RequestId(requestId.value)
val listenersForId = checkNotNull(listeners[lcpRequestId])
val listenersForId = checkNotNull(listeners.remove(lcpRequestId))

fun failWithError(error: LcpError) {
listenersForId.forEach {
it.onAcquisitionFailed(lcpRequestId, error)
}
tryOrLog { download.file.delete() }
}

val license = downloadsRepository.retrieveLicense(requestId.value)
?.let { LicenseDocument(it) }
.also { downloadsRepository.removeDownload(requestId.value) }
?: run {
listenersForId.forEach {
it.onAcquisitionFailed(
lcpRequestId,
LcpError.wrap(
Exception("Couldn't retrieve license from local storage.")
)
failWithError(
LcpError.wrap(
Exception("Couldn't retrieve license from local storage.")
)
}
)
return@launch
}

license.publicationLink.hash
?.takeIf { download.file.checkSha256(it) == false }
?.run {
failWithError(
LcpError.Network(
Exception("Digest mismatch: download looks corrupted.")
)
)
return@launch
}
downloadsRepository.removeDownload(requestId.value)

val format =
assetRetriever.sniffFormat(
Expand All @@ -206,26 +226,31 @@ public class LcpPublicationRetriever(
)
)
).getOrElse {
Format(
specification = FormatSpecification(
ZipSpecification,
EpubSpecification,
LcpSpecification
),
mediaType = MediaType.EPUB,
fileExtension = FileExtension("epub")
)
when (it) {
is AssetRetriever.RetrieveError.Reading -> {
failWithError(LcpError.wrap(ErrorException(it)))
return@launch
}
is AssetRetriever.RetrieveError.FormatNotSupported -> {
Format(
specification = FormatSpecification(
ZipSpecification,
EpubSpecification,
LcpSpecification
),
mediaType = MediaType.EPUB,
fileExtension = FileExtension("epub")
)
}
}
}

try {
// Saves the License Document into the downloaded publication
val container = createLicenseContainer(download.file, format.specification)
container.write(license)
} catch (e: Exception) {
tryOrLog { download.file.delete() }
listenersForId.forEach {
it.onAcquisitionFailed(lcpRequestId, LcpError.wrap(e))
}
failWithError(LcpError.wrap(e))
return@launch
}

Expand All @@ -239,7 +264,6 @@ public class LcpPublicationRetriever(
listenersForId.forEach {
it.onAcquisitionCompleted(lcpRequestId, acquiredPublication)
}
listeners.remove(lcpRequestId)
}
}

Expand Down Expand Up @@ -288,4 +312,21 @@ public class LcpPublicationRetriever(
listeners.remove(lcpRequestId)
}
}

/**
* Checks that the sha256 sum of file content matches the expected one.
* Returns null if we can't decide.
*/
@OptIn(ExperimentalEncodingApi::class, ExperimentalStdlibApi::class)
private fun File.checkSha256(expected: String): Boolean? {
val actual = sha256() ?: return null

// Supports hexadecimal encoding for compatibility.
// See https://github.com/readium/lcp-specs/issues/52
return when (expected.length) {
44 -> Base64.encode(actual) == expected
64 -> actual.toHexString() == expected
else -> null
}
}
}
28 changes: 28 additions & 0 deletions readium/lcp/src/main/java/org/readium/r2/lcp/util/Digest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2023 Readium Foundation. All rights reserved.
* Use of this source code is governed by the BSD-style license
* available in the top-level LICENSE file of the project.
*/

package org.readium.r2.lcp.util

import java.io.File
import java.security.MessageDigest
import org.readium.r2.shared.extensions.tryOrNull

/**
* Returns the SHA-256 sum of file content or null if computation failed.
*/
internal fun File.sha256(): ByteArray? =
tryOrNull<ByteArray> {
val md = MessageDigest.getInstance("SHA-256")
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
inputStream().use {
var bytes = it.read(buffer)
while (bytes >= 0) {
md.update(buffer, 0, bytes)
bytes = it.read(buffer)
}
}
return md.digest()
}
31 changes: 31 additions & 0 deletions readium/lcp/src/test/java/org/readium/r2/lcp/util/DigestTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2023 Readium Foundation. All rights reserved.
* Use of this source code is governed by the BSD-style license
* available in the top-level LICENSE file of the project.
*/

package org.readium.r2.lcp.util

import java.io.File
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import org.junit.Test

class DigestTest {

private val file: File =
File(DigestTest::class.java.getResource("a-fc.jpg")!!.path)

@OptIn(ExperimentalEncodingApi::class, ExperimentalStdlibApi::class)
@Test
fun `sha256 is correct`() {
val digest = assertNotNull(file.sha256())
assertEquals("GI42TOamBYJ4q4KKBcmMzlkfvld8bTVRcbjjQ20OvLI=", Base64.encode(digest))
assertEquals(
"188e364ce6a6058278ab828a05c98cce591fbe577c6d355171b8e3436d0ebcb2",
digest.toHexString()
)
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.