-
Notifications
You must be signed in to change notification settings - Fork 121
/
Copy pathLCPService.swift
173 lines (157 loc) · 7.01 KB
/
LCPService.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
//
// Copyright 2025 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.
//
import Foundation
import ReadiumShared
import UIKit
/// Service used to acquire and open publications protected with LCP.
///
/// If an `LCPAuthenticating` instance is not given when expected, the request is cancelled if no
/// passphrase is found in the local database. This can be the desired behavior when trying to
/// import a license in the background, without prompting the user for its passphrase.
///
/// You can freely use the `sender` parameter to give some UI context which will be forwarded to
/// your instance of `LCPAuthenticating`. This can be useful to provide the host `UIViewController`
/// when presenting a dialog, for example.
public final class LCPService: Loggable {
private let licenses: LicensesService
@available(*, unavailable, message: "Provide a `licenseRepository` and `passphraseRepository`, following the migration guide")
public init(
client: LCPClient,
httpClient: HTTPClient = DefaultHTTPClient()
) {
fatalError()
}
/// - Parameter deviceName: Device name used when registering a license to an LSD server.
/// If not provided, the device name will be the default `UIDevice.current.name`.
public init(
client: LCPClient,
licenseRepository: LCPLicenseRepository,
passphraseRepository: LCPPassphraseRepository,
assetRetriever: AssetRetriever,
httpClient: HTTPClient,
deviceName: String? = nil
) {
// Determine whether the embedded liblcp.a is in production mode, by attempting to open a production license.
let isProduction: Bool = {
guard
let prodLicenseURL = Bundle.module.url(forResource: "prod-license", withExtension: "lcpl"),
let prodLicense = try? String(contentsOf: prodLicenseURL, encoding: .utf8)
else {
return false
}
let passphrase = "7B7602FEF5DEDA10F768818FFACBC60B173DB223B7E66D8B2221EBE2C635EFAD" // "One passphrase"
return client.findOneValidPassphrase(jsonLicense: prodLicense, hashedPassphrases: [passphrase]) == passphrase
}()
licenses = LicensesService(
isProduction: isProduction,
client: client,
licenses: licenseRepository,
crl: CRLService(httpClient: httpClient),
device: DeviceService(
deviceName: deviceName ?? UIDevice.current.name,
repository: licenseRepository,
httpClient: httpClient
),
assetRetriever: assetRetriever,
httpClient: httpClient,
passphrases: PassphrasesService(
client: client,
repository: passphraseRepository
)
)
}
@available(*, unavailable, message: "Check the conformance of the file `Format` to the `lcp` specification instead.")
public func isLCPProtected(_ file: FileURL) async -> Bool {
fatalError()
}
/// Acquires a protected publication from an LCPL.
public func acquirePublication(
from lcpl: LicenseDocumentSource,
onProgress: @escaping (LCPProgress) -> Void = { _ in }
) async -> Result<LCPAcquiredPublication, LCPError> {
await wrap {
try await licenses.acquirePublication(from: lcpl, onProgress: onProgress)
}
}
/// Opens the LCP license of a protected publication, to access its DRM
/// metadata and decipher its content.
///
/// If the updated license cannot be stored into the ``Asset``, you'll get
/// an exception if the license points to a LSD server that cannot be
/// reached, for instance because no Internet gateway is available.
///
/// Updated licenses can currently be stored only into ``Asset``s whose
/// source property points to a `file://` URL.
///
/// - Parameters:
/// - authentication: Used to retrieve the user passphrase if it is not
/// already known. The request will be cancelled if no passphrase is
/// found in the LCP passphrase storage and in the given
/// `authentication`.
/// - allowUserInteraction: Indicates whether the user can be prompted
/// for their passphrase.
/// - sender: Free object that can be used by reading apps to give some
/// UX context when presenting dialogs with ``LCPAuthenticating``.
public func retrieveLicense(
from asset: Asset,
authentication: LCPAuthenticating,
allowUserInteraction: Bool,
sender: Any?
) async -> Result<LCPLicense?, LCPError> {
await wrap {
try await licenses.retrieve(
from: asset,
authentication: authentication,
allowUserInteraction: allowUserInteraction,
sender: sender
)
}
}
/// Creates a `ContentProtection` instance which can be used with a `Streamer` to unlock
/// LCP protected publications.
///
/// The provided `authentication` will be used to retrieve the user passphrase when opening an
/// LCP license. The default implementation `LCPDialogAuthentication` presents a dialog to the
/// user to enter their passphrase.
public func contentProtection(with authentication: LCPAuthenticating) -> ContentProtection {
LCPContentProtection(service: self, authentication: authentication)
}
private func wrap<Success>(_ block: () async throws -> Success) async -> Result<Success, LCPError> {
do {
return try await .success(block())
} catch {
return .failure(.wrap(error))
}
}
@available(*, unavailable, message: "Use the async variant.")
@discardableResult
public func acquirePublication(from lcpl: FileURL, onProgress: @escaping (LCPAcquisition.Progress) -> Void = { _ in }, completion: @escaping (CancellableResult<LCPAcquisition.Publication, LCPError>) -> Void) -> LCPAcquisition {
fatalError()
}
@available(*, unavailable, message: "Use the async variant using an `Asset`.")
public func retrieveLicense(
from publication: FileURL,
authentication: LCPAuthenticating = LCPDialogAuthentication(),
allowUserInteraction: Bool = true,
sender: Any? = nil,
completion: @escaping (CancellableResult<LCPLicense?, LCPError>) -> Void
) {
fatalError()
}
@available(*, unavailable, message: "Pass explicitly an `LCPDialogAuthentication()` for the same behavior as before")
public func contentProtection() -> ContentProtection {
contentProtection(with: LCPDialogAuthentication())
}
}
/// Source of an LCP License Document (LCPL) file.
public enum LicenseDocumentSource {
/// Raw bytes of the LCPL.
case data(Data)
/// LCPL or LCP protected package stored on the file system.
case file(FileURL)
/// LCPL already parsed to a ``LicenseDocument``.
case licenseDocument(LicenseDocument)
}