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

Commit

Permalink
Merge pull request #87 from qnga/lcpclient
Browse files Browse the repository at this point in the history
Remove dependency to liblcp by reflection
  • Loading branch information
mickael-menu committed Sep 18, 2020
2 parents f948092 + 3c9d0ba commit f465ee5
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 33 deletions.
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

0 comments on commit f465ee5

Please sign in to comment.