diff --git a/readium-lcp-swift/Content Protection/LCPContentProtection.swift b/readium-lcp-swift/Content Protection/LCPContentProtection.swift index 3582231..6b9d35e 100644 --- a/readium-lcp-swift/Content Protection/LCPContentProtection.swift +++ b/readium-lcp-swift/Content Protection/LCPContentProtection.swift @@ -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 { diff --git a/readium-lcp-swift/LCPAuthenticating.swift b/readium-lcp-swift/LCPAuthenticating.swift index eb6f1e4..34e0745 100644 --- a/readium-lcp-swift/LCPAuthenticating.swift +++ b/readium-lcp-swift/LCPAuthenticating.swift @@ -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 @@ -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) } @@ -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) } diff --git a/readium-lcp-swift/LCPService.swift b/readium-lcp-swift/LCPService.swift index eb44892..e96ab4d 100644 --- a/readium-lcp-swift/LCPService.swift +++ b/readium-lcp-swift/LCPService.swift @@ -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) -> 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) -> Void) -> Void /// Creates a `ContentProtection` instance which can be used with a `Streamer` to unlock /// LCP protected publications. @@ -50,7 +53,7 @@ public extension LCPService { } func retrieveLicense(from publication: URL, authentication: LCPAuthenticating?, completion: @escaping (CancellableResult) -> Void) -> Void { - return retrieveLicense(from: publication, authentication: authentication, sender: nil, completion: completion) + return retrieveLicense(from: publication, authentication: authentication, allowUserInteraction: true, sender: nil, completion: completion) } } diff --git a/readium-lcp-swift/License/LicenseValidation.swift b/readium-lcp-swift/License/LicenseValidation.swift index a2d1c74..ae1a5ce 100644 --- a/readium-lcp-swift/License/LicenseValidation.swift +++ b/readium-lcp-swift/License/LicenseValidation.swift @@ -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 @@ -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 @@ -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) } diff --git a/readium-lcp-swift/Services/LicensesService.swift b/readium-lcp-swift/Services/LicensesService.swift index 4b0959f..ef0ca21 100644 --- a/readium-lcp-swift/Services/LicensesService.swift +++ b/readium-lcp-swift/Services/LicensesService.swift @@ -35,7 +35,7 @@ final class LicensesService: Loggable { self.passphrases = passphrases } - func retrieveLicense(from publication: URL, authentication: LCPAuthenticating?, sender: Any?) -> Deferred { + func retrieveLicense(from publication: URL, authentication: LCPAuthenticating?, allowUserInteraction: Bool, sender: Any?) -> Deferred { return makeLicenseContainer(for: publication) .flatMap { container in guard let container = container, container.containsLicense() else { @@ -43,13 +43,13 @@ final class LicensesService: Loggable { 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 { + fileprivate func retrieveLicense(from container: LicenseContainer, authentication: LCPAuthenticating?, allowUserInteraction: Bool, sender: Any?) -> Deferred { return deferredCatching(on: .global(qos: .background)) { let initialData = try container.read() @@ -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 @@ -94,7 +94,7 @@ extension LicensesService: LCPService { func importPublication(from lcpl: URL, authentication: LCPAuthenticating?, sender: Any?, completion: @escaping (CancellableResult) -> Void) -> Observable { let progress = MutableObservable(.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) -> Void)) in let downloadProgress = license.fetchPublication { result in progress.value = .infinite @@ -116,8 +116,8 @@ extension LicensesService: LCPService { return progress } - func retrieveLicense(from publication: URL, authentication: LCPAuthenticating?, sender: Any?, completion: @escaping (CancellableResult) -> Void) { - retrieveLicense(from: publication, authentication: authentication, sender: sender) + func retrieveLicense(from publication: URL, authentication: LCPAuthenticating?, allowUserInteraction: Bool, sender: Any?, completion: @escaping (CancellableResult) -> Void) { + retrieveLicense(from: publication, authentication: authentication, allowUserInteraction: allowUserInteraction, sender: sender) .resolve(completion) } diff --git a/readium-lcp-swift/Services/PassphrasesService.swift b/readium-lcp-swift/Services/PassphrasesService.swift index 50ced69..6ccd7de 100644 --- a/readium-lcp-swift/Services/PassphrasesService.swift +++ b/readium-lcp-swift/Services/PassphrasesService.swift @@ -28,13 +28,13 @@ 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 { + func request(for license: LicenseDocument, authentication: LCPAuthenticating?, allowUserInteraction: Bool, sender: Any?) -> Deferred { 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 } @@ -42,10 +42,15 @@ final class PassphrasesService { } /// 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 { + private func authenticate(for license: LicenseDocument, reason: LCPAuthenticationReason, using authentication: LCPAuthenticating, allowUserInteraction: Bool, sender: Any?) -> Deferred { 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 { @@ -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