Skip to content
This repository was archived by the owner on Aug 12, 2022. It is now read-only.

Commit

Permalink
Merge pull request #96 from readium/fix/lcpauthenticating
Browse files Browse the repository at this point in the history
Refactor LCPAuthenticating to allow dynamic user interaction implementations
  • Loading branch information
mickael-menu authored Oct 19, 2020
2 parents 352f9b2 + e4378db commit d77f5ab
Show file tree
Hide file tree
Showing 6 changed files with 44 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ final class LCPContentProtection: ContentProtection {
{
service.retrieveLicense(
from: file.url,
authentication: (allowUserInteraction || !authentication.requiresUserInteraction) ? authentication : nil,
authentication: authentication,
allowUserInteraction: allowUserInteraction,
sender: sender
) { result in
if case .success(let license) = result, license == nil {
Expand Down
19 changes: 7 additions & 12 deletions readium-lcp-swift/LCPAuthenticating.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@
import Foundation

public protocol LCPAuthenticating {

/// Indicates whether the user might be prompted to ask their credentials, when calling
/// `requestPassphrase()`.
var requiresUserInteraction: Bool { get }


/// Requests a passphrase to decrypt the given license.
///
/// The reading app can prompt the user to enter the passphrase, or retrieve it by any other
Expand All @@ -25,11 +21,14 @@ public protocol LCPAuthenticating {
/// - Parameters:
/// - license: Information to show to the user about the license being opened.
/// - reason: Reason why the passphrase is requested. It should be used to prompt the user.
/// - allowUserInteraction: Indicates whether the user can be prompted for their passphrase.
/// If your implementation requires it and `allowUserInteraction` is false, terminate
/// quickly by sending `nil` to the completion block.
/// - sender: Free object that can be used by reading apps to give some UX context when
/// presenting dialogs. For example, the host `UIViewController`.
/// - completion: Used to return the retrieved passphrase. If the user cancelled, send nil.
/// The passphrase may be already hashed.
func requestPassphrase(for license: LCPAuthenticatedLicense, reason: LCPAuthenticationReason, sender: Any?, completion: @escaping (String?) -> Void)
func requestPassphrase(for license: LCPAuthenticatedLicense, reason: LCPAuthenticationReason, allowUserInteraction: Bool, sender: Any?, completion: @escaping (String?) -> Void)

}

Expand Down Expand Up @@ -90,14 +89,10 @@ public class LCPPassphrase: LCPAuthenticating {
self.fallback = fallback
}

public var requiresUserInteraction: Bool {
fallback?.requiresUserInteraction ?? false
}

public func requestPassphrase(for license: LCPAuthenticatedLicense, reason: LCPAuthenticationReason, sender: Any?, completion: @escaping (String?) -> Void) {
public func requestPassphrase(for license: LCPAuthenticatedLicense, reason: LCPAuthenticationReason, allowUserInteraction: Bool, sender: Any?, completion: @escaping (String?) -> Void) {
guard reason == .passphraseNotFound else {
if let fallback = fallback {
fallback.requestPassphrase(for: license, reason: reason, sender: sender, completion: completion)
fallback.requestPassphrase(for: license, reason: reason, allowUserInteraction: allowUserInteraction, sender: sender, completion: completion)
} else {
completion(nil)
}
Expand Down
7 changes: 5 additions & 2 deletions readium-lcp-swift/LCPService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ public protocol LCPService {
/// its content.
///
/// Returns `nil` if the publication is not protected with LCP.
func retrieveLicense(from publication: URL, authentication: LCPAuthenticating?, sender: Any?, completion: @escaping (CancellableResult<LCPLicense?, LCPError>) -> Void) -> Void
///
/// - Parameters:
/// - allowUserInteraction: Indicates whether the user can be prompted for their passphrase.
func retrieveLicense(from publication: URL, authentication: LCPAuthenticating?, allowUserInteraction: Bool, sender: Any?, completion: @escaping (CancellableResult<LCPLicense?, LCPError>) -> Void) -> Void

/// Creates a `ContentProtection` instance which can be used with a `Streamer` to unlock
/// LCP protected publications.
Expand All @@ -50,7 +53,7 @@ public extension LCPService {
}

func retrieveLicense(from publication: URL, authentication: LCPAuthenticating?, completion: @escaping (CancellableResult<LCPLicense?, LCPError>) -> Void) -> Void {
return retrieveLicense(from: publication, authentication: authentication, sender: nil, completion: completion)
return retrieveLicense(from: publication, authentication: authentication, allowUserInteraction: true, sender: nil, completion: completion)
}

}
Expand Down
15 changes: 13 additions & 2 deletions readium-lcp-swift/License/LicenseValidation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ final class LicenseValidation: Loggable {

// Dependencies for the State's handlers
fileprivate let authentication: LCPAuthenticating?
fileprivate let allowUserInteraction: Bool
fileprivate let sender: Any?
fileprivate let crl: CRLService
fileprivate let device: DeviceService
Expand All @@ -75,8 +76,18 @@ final class LicenseValidation: Loggable {
}
}

init(authentication: LCPAuthenticating?, sender: Any?, crl: CRLService, device: DeviceService, network: NetworkService, passphrases: PassphrasesService, onLicenseValidated: @escaping (LicenseDocument) throws -> Void) {
init(
authentication: LCPAuthenticating?,
allowUserInteraction: Bool,
sender: Any?,
crl: CRLService,
device: DeviceService,
network: NetworkService,
passphrases: PassphrasesService,
onLicenseValidated: @escaping (LicenseDocument) throws -> Void
) {
self.authentication = authentication
self.allowUserInteraction = allowUserInteraction
self.sender = sender
self.crl = crl
self.device = device
Expand Down Expand Up @@ -364,7 +375,7 @@ extension LicenseValidation {
}

private func requestPassphrase(for license: LicenseDocument) {
passphrases.request(for: license, authentication: authentication, sender: sender)
passphrases.request(for: license, authentication: authentication, allowUserInteraction: allowUserInteraction, sender: sender)
.map { .retrievedPassphrase($0) }
.resolve(raise)
}
Expand Down
14 changes: 7 additions & 7 deletions readium-lcp-swift/Services/LicensesService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,21 @@ final class LicensesService: Loggable {
self.passphrases = passphrases
}

func retrieveLicense(from publication: URL, authentication: LCPAuthenticating?, sender: Any?) -> Deferred<LCPLicense?, LCPError> {
func retrieveLicense(from publication: URL, authentication: LCPAuthenticating?, allowUserInteraction: Bool, sender: Any?) -> Deferred<LCPLicense?, LCPError> {
return makeLicenseContainer(for: publication)
.flatMap { container in
guard let container = container, container.containsLicense() else {
// Not protected with LCP
return .success(nil)
}

return self.retrieveLicense(from: container, authentication: authentication, sender: sender)
return self.retrieveLicense(from: container, authentication: authentication, allowUserInteraction: allowUserInteraction, sender: sender)
.map { $0 as LCPLicense }
.mapError(LCPError.wrap)
}
}

fileprivate func retrieveLicense(from container: LicenseContainer, authentication: LCPAuthenticating?, sender: Any?) -> Deferred<License, Error> {
fileprivate func retrieveLicense(from container: LicenseContainer, authentication: LCPAuthenticating?, allowUserInteraction: Bool, sender: Any?) -> Deferred<License, Error> {
return deferredCatching(on: .global(qos: .background)) {
let initialData = try container.read()

Expand All @@ -73,7 +73,7 @@ final class LicensesService: Loggable {
}
}

let validation = LicenseValidation(authentication: authentication, sender: sender, crl: self.crl, device: self.device, network: self.network, passphrases: self.passphrases, onLicenseValidated: onLicenseValidated)
let validation = LicenseValidation(authentication: authentication, allowUserInteraction: allowUserInteraction, sender: sender, crl: self.crl, device: self.device, network: self.network, passphrases: self.passphrases, onLicenseValidated: onLicenseValidated)

return validation.validate(.license(initialData))
.tryMap { documents in
Expand All @@ -94,7 +94,7 @@ extension LicensesService: LCPService {
func importPublication(from lcpl: URL, authentication: LCPAuthenticating?, sender: Any?, completion: @escaping (CancellableResult<LCPImportedPublication, LCPError>) -> Void) -> Observable<DownloadProgress> {
let progress = MutableObservable<DownloadProgress>(.infinite)
let container = LCPLLicenseContainer(lcpl: lcpl)
retrieveLicense(from: container, authentication: authentication, sender: sender)
retrieveLicense(from: container, authentication: authentication, allowUserInteraction: true, sender: sender)
.asyncMap { (license, completion: (@escaping (CancellableResult<LCPImportedPublication, Error>) -> Void)) in
let downloadProgress = license.fetchPublication { result in
progress.value = .infinite
Expand All @@ -116,8 +116,8 @@ extension LicensesService: LCPService {
return progress
}

func retrieveLicense(from publication: URL, authentication: LCPAuthenticating?, sender: Any?, completion: @escaping (CancellableResult<LCPLicense?, LCPError>) -> Void) {
retrieveLicense(from: publication, authentication: authentication, sender: sender)
func retrieveLicense(from publication: URL, authentication: LCPAuthenticating?, allowUserInteraction: Bool, sender: Any?, completion: @escaping (CancellableResult<LCPLicense?, LCPError>) -> Void) {
retrieveLicense(from: publication, authentication: authentication, allowUserInteraction: allowUserInteraction, sender: sender)
.resolve(completion)
}

Expand Down
15 changes: 10 additions & 5 deletions readium-lcp-swift/Services/PassphrasesService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,29 @@ final class PassphrasesService {
/// Finds any valid passphrase for the given license in the passphrases repository.
/// If none is found, requests a passphrase from the request delegate (ie. user prompt) until one is valid, or the request is cancelled.
/// The returned passphrase is nil if the request was cancelled by the user.
func request(for license: LicenseDocument, authentication: LCPAuthenticating?, sender: Any?) -> Deferred<String, Error> {
func request(for license: LicenseDocument, authentication: LCPAuthenticating?, allowUserInteraction: Bool, sender: Any?) -> Deferred<String, Error> {
return deferredCatching {
let candidates = self.possiblePassphrasesFromRepository(for: license)
if let passphrase = findOneValidPassphrase(jsonLicense: license.json, hashedPassphrases: candidates) {
return .success(passphrase)
} else if let authentication = authentication {
return self.authenticate(for: license, reason: .passphraseNotFound, using: authentication, sender: sender)
return self.authenticate(for: license, reason: .passphraseNotFound, using: authentication, allowUserInteraction: allowUserInteraction, sender: sender)
} else {
return .cancelled
}
}
}

/// Called when the service can't find any valid passphrase in the repository, as a fallback.
private func authenticate(for license: LicenseDocument, reason: LCPAuthenticationReason, using authentication: LCPAuthenticating, sender: Any?) -> Deferred<String, Error> {
private func authenticate(for license: LicenseDocument, reason: LCPAuthenticationReason, using authentication: LCPAuthenticating, allowUserInteraction: Bool, sender: Any?) -> Deferred<String, Error> {
return deferred { (success: @escaping (String) -> Void, _, cancel) in
let authenticatedLicense = LCPAuthenticatedLicense(document: license)
authentication.requestPassphrase(for: authenticatedLicense, reason: reason, sender: sender) { passphrase in
authentication.requestPassphrase(
for: authenticatedLicense,
reason: reason,
allowUserInteraction: allowUserInteraction,
sender: sender
) { passphrase in
if let passphrase = passphrase {
success(passphrase)
} else {
Expand All @@ -64,7 +69,7 @@ final class PassphrasesService {

guard let passphrase = findOneValidPassphrase(jsonLicense: license.json, hashedPassphrases: passphrases) else {
// Tries again if the passphrase is invalid, until cancelled
return self.authenticate(for: license, reason: .invalidPassphrase, using: authentication, sender: sender)
return self.authenticate(for: license, reason: .invalidPassphrase, using: authentication, allowUserInteraction: allowUserInteraction, sender: sender)
}

// Saves the passphrase to open the publication right away next time
Expand Down

0 comments on commit d77f5ab

Please sign in to comment.