Skip to content
This repository has been archived by the owner on Jul 29, 2022. It is now read-only.

Remove dependency to liblcp by reflection #87

Merged
merged 3 commits into from
Sep 18, 2020
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file.
* LCP implementation of the [Content Protection API](https://github.com/readium/architecture/blob/master/proposals/006-content-protection.md) to work with the new [Streamer API](https://github.com/readium/architecture/blob/master/proposals/005-streamer-api.md) (contributed by [@qnga](https://github.com/readium/r2-lcp-kotlin/pull/79)).
* It is highly recommended that you upgrade to the new `Streamer` API to open publications, which will simplify DRM unlocking.
* `LcpService::isLcpProtected()` provides a way to check if a file is protected with LCP.
* `LcpService::addPassphrase()` can be used to preload LCP passphrases, for example when using [LCP Automatic Key Retrieval](https://readium.org/lcp-specs/notes/lcp-key-retrieval.html).

### Changed

Expand All @@ -19,6 +20,9 @@ All notable changes to this project will be documented in this file.
* Follow the deprecation warnings to upgrade to the new names.
* `LcpAuthenticating` is now provided with more information and you will need to update your implementation.
* Publications are now downloaded to a temporary location, to make sure disk storage can be recovered automatically by the system. After acquiring the publication, you need to move the downloaded file to another permanent location.
* The private `liblcp` dependency is now accessed through reflection, to allow switching LCP dynamically (contributed by [@qnga](https://github.com/readium/r2-lcp-kotlin/pull/87)).
* You need to add `implementation "readium:liblcp:1.0.0@aar"` to your `build.gradle`.
* `LcpService::create()` returns `null` if `lcplib` is not found.

### Fixed

Expand Down
3 changes: 0 additions & 3 deletions r2-lcp/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,6 @@ dependencies {
implementation "com.github.readium:r2-shared-kotlin:2.0.0-alpha.1"
}

//noinspection GradleDependency
implementation "readium:liblcp:1.0.0@aar"

implementation('com.mcxiaoke.koi:core:0.5.5') {
exclude module: 'support-v4'
}
Expand Down
17 changes: 1 addition & 16 deletions r2-lcp/src/main/java/org/readium/r2/lcp/LcpException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
package org.readium.r2.lcp

import org.joda.time.DateTime
import org.readium.lcp.sdk.DRMException
import java.net.SocketTimeoutException

sealed class LcpException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) {
Expand All @@ -36,7 +35,7 @@ sealed class LcpException(message: String? = null, cause: Throwable? = null) : E
class Runtime(override val message: String) : LcpException()

/** An unknown low-level exception was reported. */
class Unknown(override val cause: Exception?) : LcpException()
class Unknown(override val cause: Throwable?) : LcpException()


/**
Expand Down Expand Up @@ -179,20 +178,6 @@ sealed class LcpException(message: String? = null, cause: Throwable? = null) : E
internal fun wrap(e: Exception?): LcpException = when (e) {
is LcpException -> e
is SocketTimeoutException -> Network(e)
is DRMException -> when(e.drmError.code) {
// Error code 11 should never occur since we check the start/end date before calling createContext
11 -> Runtime("License is out of date (check start and end date).")
101 -> LicenseIntegrity.CertificateRevoked
102 -> LicenseIntegrity.CertificateSignatureInvalid
111 -> LicenseIntegrity.LicenseSignatureDateInvalid
112 -> LicenseIntegrity.LicenseSignatureInvalid
// Error code 121 seems to be unused in the C++ lib.
121 -> Runtime("The drm context is invalid.")
131 -> Decryption.ContentKeyDecryptError
141 -> LicenseIntegrity.UserKeyCheckInvalid
151 -> Decryption.ContentDecryptError
else -> Unknown(e)
}
else -> Unknown(e)
}
}
Expand Down
7 changes: 5 additions & 2 deletions r2-lcp/src/main/java/org/readium/r2/lcp/LcpService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ interface LcpService {
/**
* LCP service factory.
*/
fun create(context: Context): LcpService {
fun create(context: Context): LcpService? {
if (!LcpClient.isAvailable())
return null

val db = Database(context)
val network = NetworkService()
val device = DeviceService(repository = db.licenses, network = network, context = context)
Expand Down Expand Up @@ -135,7 +138,7 @@ interface LcpService {

@Deprecated("Renamed to `LcpService.create()`", replaceWith = ReplaceWith("LcpService.create"))
fun R2MakeLCPService(context: Context): LcpService =
LcpService.create(context)
LcpService.create(context) ?: throw Exception("liblcp is missing on the classpath")

@Deprecated("Renamed to `LcpService.AcquiredPublication`", replaceWith = ReplaceWith("LcpService.AcquiredPublication"))
typealias LCPImportedPublication = LcpService.AcquiredPublication
4 changes: 2 additions & 2 deletions r2-lcp/src/main/java/org/readium/r2/lcp/license/License.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ package org.readium.r2.lcp.license
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.joda.time.DateTime
import org.readium.lcp.sdk.Lcp
import org.readium.r2.lcp.*
import org.readium.r2.lcp.BuildConfig.DEBUG
import org.readium.r2.lcp.license.model.LicenseDocument
import org.readium.r2.lcp.license.model.StatusDocument
import org.readium.r2.lcp.service.DeviceService
import org.readium.r2.lcp.service.LcpClient
import org.readium.r2.lcp.service.LicensesRepository
import org.readium.r2.lcp.service.NetworkService
import org.readium.r2.lcp.service.URLParameters
Expand Down Expand Up @@ -47,7 +47,7 @@ internal class License(
Try.success(ByteArray(0))
} else {
val context = documents.getContext()
val decryptedData = Lcp().decrypt(context, data)
val decryptedData = LcpClient.decrypt(context, data)
Try.success(decryptedData)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ package org.readium.r2.lcp.license

import kotlinx.coroutines.runBlocking
import org.joda.time.DateTime
import org.readium.lcp.sdk.DRMContext
import org.readium.lcp.sdk.Lcp
import org.readium.r2.lcp.BuildConfig.DEBUG
import org.readium.r2.lcp.LcpAuthenticating
import org.readium.r2.lcp.LcpException
Expand All @@ -21,6 +19,7 @@ import org.readium.r2.lcp.license.model.StatusDocument
import org.readium.r2.lcp.license.model.components.Link
import org.readium.r2.lcp.service.CRLService
import org.readium.r2.lcp.service.DeviceService
import org.readium.r2.lcp.service.LcpClient
import org.readium.r2.lcp.service.NetworkService
import org.readium.r2.lcp.service.PassphrasesService
import timber.log.Timber
Expand All @@ -32,7 +31,7 @@ internal sealed class Either<A, B> {

private val supportedProfiles = listOf("http://readium.org/lcp/basic-profile", "http://readium.org/lcp/profile-1.0")

internal typealias Context = Either<DRMContext, LcpException.LicenseStatus>
internal typealias Context = Either<LcpClient.Context, LcpException.LicenseStatus>

internal typealias Observer = (ValidatedDocuments?, Exception?) -> Unit

Expand All @@ -44,7 +43,7 @@ internal enum class ObserverPolicy {
}

internal data class ValidatedDocuments constructor(val license: LicenseDocument, private val context: Context, val status: StatusDocument? = null) {
fun getContext(): DRMContext {
fun getContext(): LcpClient.Context {
when (context) {
is Either.Left -> return context.left
is Either.Right -> throw context.right
Expand Down Expand Up @@ -75,7 +74,7 @@ internal sealed class Event {
data class validatedStatus(val status: StatusDocument) : Event()
data class checkedLicenseStatus(val error: LcpException.LicenseStatus?) : Event()
data class retrievedPassphrase(val passphrase: String) : Event()
data class validatedIntegrity(val context: DRMContext) : Event()
data class validatedIntegrity(val context: LcpClient.Context) : Event()
data class registeredDevice(val statusData: ByteArray?) : Event()
data class failed(val error: Exception) : Event()
object cancelled : Event()
Expand Down Expand Up @@ -118,7 +117,7 @@ internal class LicenseValidation(
val prodLicense = LicenseDocument(data = prodLicenseInput.readBytes())
val passphrase = "7B7602FEF5DEDA10F768818FFACBC60B173DB223B7E66D8B2221EBE2C635EFAD"
try {
Lcp().findOneValidPassphrase(prodLicense.json.toString(), listOf(passphrase).toTypedArray()) == passphrase
LcpClient.findOneValidPassphrase(prodLicense.json.toString(), listOf(passphrase)) == passphrase
} catch (e: Exception) {
false
}
Expand Down Expand Up @@ -374,7 +373,7 @@ internal class LicenseValidation(
if (!supportedProfiles.contains(profile)) {
throw LcpException.LicenseProfileNotSupported
}
val context = Lcp().createContext(license.json.toString(), passphrase, crl.retrieve())
val context = LcpClient.createContext(license.json.toString(), passphrase, crl.retrieve())
raise(Event.validatedIntegrity(context))
}

Expand Down
107 changes: 107 additions & 0 deletions r2-lcp/src/main/java/org/readium/r2/lcp/service/LcpClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package org.readium.r2.lcp.service

import org.readium.r2.lcp.LcpException
import org.readium.r2.shared.extensions.tryOr
import java.lang.reflect.InvocationTargetException

internal object LcpClient {

data class Context(
val hashedPassphrase: String,
val encryptedContentKey: String,
val token: String,
val profile: String
) {
companion object {

fun fromDRMContext(drmContext: Any): Context =
with(Class.forName("org.readium.lcp.sdk.DRMContext")) {
val encryptedContentKey = getMethod("getEncryptedContentKey").invoke(drmContext) as String
val hashedPassphrase = getMethod("getHashedPassphrase").invoke(drmContext) as String
val profile = getMethod("getProfile").invoke(drmContext) as String
val token = getMethod("getToken").invoke(drmContext) as String
Context(hashedPassphrase, encryptedContentKey, token, profile)
}
}

fun toDRMContext(): Any =
Class.forName("org.readium.lcp.sdk.DRMContext")
.getConstructor(String::class.java, String::class.java, String::class.java, String::class.java)
.newInstance(hashedPassphrase, encryptedContentKey, token, profile)
}

private val instance: Any by lazy {
klass.newInstance()
}

private val klass: Class<*> by lazy {
Class.forName("org.readium.lcp.sdk.Lcp")
}

fun isAvailable(): Boolean = tryOr(false) {
instance
true
}

fun createContext(jsonLicense: String, hashedPassphrases: String, pemCrl: String): Context =
try {
val drmContext = klass
.getMethod("createContext", String::class.java, String::class.java, String::class.java)
.invoke(instance, jsonLicense, hashedPassphrases, pemCrl)!!

Context.fromDRMContext(drmContext)
} catch (e: InvocationTargetException) {
throw mapException(e.targetException)
}

fun decrypt(context: Context, encryptedData: ByteArray): ByteArray =
try {
klass
.getMethod("decrypt", Class.forName("org.readium.lcp.sdk.DRMContext"), ByteArray::class.java)
.invoke(instance, context.toDRMContext(), encryptedData)
as ByteArray
} catch (e: InvocationTargetException) {
throw mapException(e.targetException)
}

fun findOneValidPassphrase(jsonLicense: String, hashedPassphrases: List<String>): String =
try {
klass
.getMethod("findOneValidPassphrase", String::class.java, Array<String>::class.java)
.invoke(instance, jsonLicense, hashedPassphrases.toTypedArray()) as String
} catch (e: InvocationTargetException) {
throw mapException(e.targetException)
}

private fun mapException(e: Throwable): LcpException {

val drmExceptionClass = Class.forName("org.readium.lcp.sdk.DRMException")

if (!drmExceptionClass.isInstance(e))
return LcpException.Runtime("the Lcp client threw an unhandled exception")

val drmError = drmExceptionClass
.getMethod("getDrmError")
.invoke(e)

val errorCode = Class
.forName("org.readium.lcp.sdk.DRMError")
.getMethod("getCode")
.invoke(drmError) as Int

return when (errorCode) {
// Error code 11 should never occur since we check the start/end date before calling createContext
11 -> LcpException.Runtime("License is out of date (check start and end date).")
101 -> LcpException.LicenseIntegrity.CertificateRevoked
102 -> LcpException.LicenseIntegrity.CertificateSignatureInvalid
111 -> LcpException.LicenseIntegrity.LicenseSignatureDateInvalid
112 -> LcpException.LicenseIntegrity.LicenseSignatureInvalid
// Error code 121 seems to be unused in the C++ lib.
121 -> LcpException.Runtime("The drm context is invalid.")
131 -> LcpException.Decryption.ContentKeyDecryptError
141 -> LcpException.LicenseIntegrity.UserKeyCheckInvalid
151 -> LcpException.Decryption.ContentDecryptError
else -> LcpException.Unknown(e)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
package org.readium.r2.lcp.service

import com.mcxiaoke.koi.HASH
import org.readium.lcp.sdk.Lcp
import org.readium.r2.lcp.LcpAuthenticating
import org.readium.r2.lcp.license.model.LicenseDocument

Expand All @@ -19,7 +18,7 @@ internal class PassphrasesService(private val repository: PassphrasesRepository)
suspend fun request(license: LicenseDocument, authentication: LcpAuthenticating?, allowUserInteraction: Boolean, sender: Any?): String? {
val candidates = this@PassphrasesService.possiblePassphrasesFromRepository(license)
val passphrase = try {
Lcp().findOneValidPassphrase(license.json.toString(), candidates.toTypedArray())
LcpClient.findOneValidPassphrase(license.json.toString(), candidates)
} catch (e: Exception) {
null
}
Expand All @@ -43,7 +42,7 @@ internal class PassphrasesService(private val repository: PassphrasesRepository)
}

return try {
val passphrase = Lcp().findOneValidPassphrase(license.json.toString(), passphrases.toTypedArray())
val passphrase = LcpClient.findOneValidPassphrase(license.json.toString(), passphrases)
addPassphrase(passphrase, true, license.id, license.provider, license.user.id)
passphrase
} catch (e: Exception) {
Expand Down