diff --git a/Cartfile b/Cartfile index 1d227c8..a00d5b5 100644 --- a/Cartfile +++ b/Cartfile @@ -1,4 +1,4 @@ -github "readium/r2-shared-swift" == 1.2.11 +github "readium/r2-shared-swift" == 1.2.12 github "stephencelis/SQLite.swift" == 0.11.5 github "krzyzanowskim/CryptoSwift" == 0.15.0 github "weichsel/ZIPFoundation" == 0.9.8 diff --git a/Cartfile.resolved b/Cartfile.resolved index 8a511c5..cdbb472 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,4 +1,4 @@ github "krzyzanowskim/CryptoSwift" "0.15.0" -github "readium/r2-shared-swift" "1.2.11" +github "readium/r2-shared-swift" "1.2.12" github "stephencelis/SQLite.swift" "0.11.5" github "weichsel/ZIPFoundation" "0.9.8" diff --git a/r2-lcp-swift.xcodeproj/project.pbxproj b/r2-lcp-swift.xcodeproj/project.pbxproj index 020e9f2..3bb6376 100644 --- a/r2-lcp-swift.xcodeproj/project.pbxproj +++ b/r2-lcp-swift.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ CA4A388E220988C800599297 /* LCPLLicenseContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4A388D220988C800599297 /* LCPLLicenseContainer.swift */; }; CA4A389022098A9400599297 /* ZIPLicenseContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4A388F22098A9400599297 /* ZIPLicenseContainer.swift */; }; CA4A3892220994E300599297 /* EPUBLicenseContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4A3891220994E300599297 /* EPUBLicenseContainer.swift */; }; + CA50B88022B2A777003AFF24 /* R2LCPLocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA50B87F22B2A777003AFF24 /* R2LCPLocalizedString.swift */; }; + CA50B88422B2A822003AFF24 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CA50B88622B2A822003AFF24 /* Localizable.strings */; }; CAB131F6220D81E60097DFB5 /* LicensesRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB131F5220D81E60097DFB5 /* LicensesRepository.swift */; }; CAB1320B220D9B200097DFB5 /* LCPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB1320A220D9B200097DFB5 /* LCPService.swift */; }; CAB13211220DB2B10097DFB5 /* LCPAuthenticating.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB13210220DB2B10097DFB5 /* LCPAuthenticating.swift */; }; @@ -57,6 +59,8 @@ CA4A388D220988C800599297 /* LCPLLicenseContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LCPLLicenseContainer.swift; sourceTree = ""; }; CA4A388F22098A9400599297 /* ZIPLicenseContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZIPLicenseContainer.swift; sourceTree = ""; }; CA4A3891220994E300599297 /* EPUBLicenseContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EPUBLicenseContainer.swift; sourceTree = ""; }; + CA50B87F22B2A777003AFF24 /* R2LCPLocalizedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = R2LCPLocalizedString.swift; sourceTree = ""; }; + CA50B88522B2A822003AFF24 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; CAB131F5220D81E60097DFB5 /* LicensesRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicensesRepository.swift; sourceTree = ""; }; CAB1320A220D9B200097DFB5 /* LCPService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LCPService.swift; sourceTree = ""; }; CAB13210220DB2B10097DFB5 /* LCPAuthenticating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LCPAuthenticating.swift; sourceTree = ""; }; @@ -104,6 +108,23 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + CA50B87E22B2A768003AFF24 /* Toolkit */ = { + isa = PBXGroup; + children = ( + CA50B87F22B2A777003AFF24 /* R2LCPLocalizedString.swift */, + ); + path = Toolkit; + sourceTree = ""; + }; + CA50B88122B2A7F2003AFF24 /* Resources */ = { + isa = PBXGroup; + children = ( + CAB214E02269C241007E989D /* prod-license.lcpl */, + CA50B88622B2A822003AFF24 /* Localizable.strings */, + ); + path = Resources; + sourceTree = ""; + }; CAB13212220DB55C0097DFB5 /* Public */ = { isa = PBXGroup; children = ( @@ -232,8 +253,9 @@ CAB13214220DB5930097DFB5 /* License */, CAB1321B220DB6430097DFB5 /* Services */, CAB1321C220DB6570097DFB5 /* Persistence */, + CA50B87E22B2A768003AFF24 /* Toolkit */, + CA50B88122B2A7F2003AFF24 /* Resources */, F3B2C88B1F667223007601E4 /* Info.plist */, - CAB214E02269C241007E989D /* prod-license.lcpl */, F3B2C88A1F667223007601E4 /* readium-lcp-swift.h */, ); path = "readium-lcp-swift"; @@ -329,6 +351,7 @@ buildActionMask = 2147483647; files = ( CAB214E12269C241007E989D /* prod-license.lcpl in Resources */, + CA50B88422B2A822003AFF24 /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -361,6 +384,7 @@ F38FB0381F66839600F9D602 /* Rights.swift in Sources */, F36F9E281F8270FC001D0DB4 /* Transactions.swift in Sources */, F3B2C8A81F66727C007601E4 /* StatusDocument.swift in Sources */, + CA50B88022B2A777003AFF24 /* R2LCPLocalizedString.swift in Sources */, CA2AE328221C3CFB008BD18F /* Deprecated.swift in Sources */, F38FB0361F66837800F9D602 /* User.swift in Sources */, F38FB0421F66847E00F9D602 /* Signature.swift in Sources */, @@ -379,6 +403,17 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXVariantGroup section */ + CA50B88622B2A822003AFF24 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + CA50B88522B2A822003AFF24 /* en */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ F3B2C8991F667223007601E4 /* Debug */ = { isa = XCBuildConfiguration; diff --git a/readium-lcp-swift/Public/LCPError.swift b/readium-lcp-swift/Public/LCPError.swift index 6cecded..f926bae 100644 --- a/readium-lcp-swift/Public/LCPError.swift +++ b/readium-lcp-swift/Public/LCPError.swift @@ -10,12 +10,13 @@ // import Foundation +import R2LCPClient -public enum LCPError: Error { +public enum LCPError: LocalizedError { // The operation can't be done right now because another License operation is running. case licenseIsBusy // An error occured while checking the integrity of the License, it can't be retrieved. - case licenseIntegrity(Error) + case licenseIntegrity(LCPClientError) // The status of the License is not valid, it can't be used to decrypt the publication. case licenseStatus(StatusError) // Can't read or write the License Document from its container. @@ -41,34 +42,54 @@ public enum LCPError: Error { // An unknown low-level error was reported. case unknown(Error?) -} - -extension LCPError: LocalizedError { - public var errorDescription: String? { switch self { case .licenseIsBusy: - return "Can't perform this operation at the moment." + return R2LCPLocalizedString("LCPError.licenseIsBusy") case .licenseIntegrity(let error): - return error.localizedDescription + let description: String = { + switch error { + case .licenseOutOfDate: + return R2LCPLocalizedString("LCPClientError.licenseOutOfDate") + case .certificateRevoked: + return R2LCPLocalizedString("LCPClientError.certificateRevoked") + case .certificateSignatureInvalid: + return R2LCPLocalizedString("LCPClientError.certificateSignatureInvalid") + case .licenseSignatureDateInvalid: + return R2LCPLocalizedString("LCPClientError.licenseSignatureDateInvalid") + case .licenseSignatureInvalid: + return R2LCPLocalizedString("LCPClientError.licenseSignatureInvalid") + case .contextInvalid: + return R2LCPLocalizedString("LCPClientError.contextInvalid") + case .contentKeyDecryptError: + return R2LCPLocalizedString("LCPClientError.contentKeyDecryptError") + case .userKeyCheckInvalid: + return R2LCPLocalizedString("LCPClientError.userKeyCheckInvalid") + case .contentDecryptError: + return R2LCPLocalizedString("LCPClientError.contentDecryptError") + case .unknown: + return R2LCPLocalizedString("LCPClientError.unknown") + } + }() + return R2LCPLocalizedString("LCPError.licenseIntegrity", description) case .licenseStatus(let error): return error.localizedDescription case .licenseContainer: - return "Can't access the License Document." + return R2LCPLocalizedString("LCPError.licenseContainer") case .licenseInteractionNotAvailable: - return "This interaction is not available." + return R2LCPLocalizedString("LCPError.licenseInteractionNotAvailable") case .licenseProfileNotSupported: - return "This License has a profile identifier that this app cannot handle, the publication cannot be processed." + return R2LCPLocalizedString("LCPError.licenseProfileNotSupported") case .crlFetching: - return "Can't retrieve the Certificate Revocation List." + return R2LCPLocalizedString("LCPError.crlFetching") case .licenseRenew(let error): return error.localizedDescription case .licenseReturn(let error): return error.localizedDescription - case .parsing(let error): - return error.localizedDescription + case .parsing(_): + return R2LCPLocalizedString("LCPError.parsing") case .network(let error): - return error?.localizedDescription ?? "Network error." + return error?.localizedDescription ?? R2LCPLocalizedString("LCPError.network") case .runtime(let error): return error case .unknown(let error): @@ -80,38 +101,34 @@ extension LCPError: LocalizedError { /// Errors while checking the status of the License, using the Status Document. -public enum StatusError: Error { +public enum StatusError: LocalizedError { // For the case (revoked, returned, cancelled, expired), app should notify the user and stop there. The message to the user must be clear about the status of the license: don't display "expired" if the status is "revoked". The date and time corresponding to the new status should be displayed (e.g. "The license expired on 01 January 2018"). case cancelled(Date) case returned(Date) case expired(start: Date, end: Date) // If the license has been revoked, the user message should display the number of devices which registered to the server. This count can be calculated from the number of "register" events in the status document. If no event is logged in the status document, no such message should appear (certainly not "The license was registered by 0 devices"). case revoked(Date, devicesCount: Int) -} -extension StatusError: LocalizedError { - public var errorDescription: String? { let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "MMMM dd, yyyy HH:mm" - dateFormatter.locale = Locale(identifier:"en") - + dateFormatter.dateStyle = .medium + switch self { case .cancelled(let date): - return "You have cancelled this license on \(dateFormatter.string(from: date))." + return R2LCPLocalizedString("StatusError.cancelled", dateFormatter.string(from: date)) case .returned(let date): - return "This license has been returned on \(dateFormatter.string(from: date))." + return R2LCPLocalizedString("StatusError.returned", dateFormatter.string(from: date)) case .expired(start: let start, end: let end): if start > Date() { - return "This license starts on \(dateFormatter.string(from: start))." + return R2LCPLocalizedString("StatusError.expired.start", dateFormatter.string(from: start)) } else { - return "This license expired on \(dateFormatter.string(from: end))." + return R2LCPLocalizedString("StatusError.expired.end", dateFormatter.string(from: end)) } case .revoked(let date, let devicesCount): - return "This license has been revoked by its provider on \(dateFormatter.string(from: date)).\nThe license was registered by \(devicesCount) device\(devicesCount > 1 ? "s" : "")." + return R2LCPLocalizedString("StatusError.revoked", dateFormatter.string(from: date), devicesCount) } } @@ -130,11 +147,11 @@ public enum RenewError: LocalizedError { public var errorDescription: String? { switch self { case .renewFailed: - return "Your publication could not be renewed properly." + return R2LCPLocalizedString("RenewError.renewFailed") case .invalidRenewalPeriod(maxRenewDate: _): - return "Incorrect renewal period, your publication could not be renewed." + return R2LCPLocalizedString("RenewError.invalidRenewalPeriod") case .unexpectedServerError: - return "An unexpected error has occurred on the server." + return R2LCPLocalizedString("RenewError.unexpectedServerError") } } @@ -153,11 +170,11 @@ public enum ReturnError: LocalizedError { public var errorDescription: String? { switch self { case .returnFailed: - return "Your publication could not be returned properly." + return R2LCPLocalizedString("ReturnError.returnFailed") case .alreadyReturnedOrExpired: - return "Your publication has already been returned before or is expired." + return R2LCPLocalizedString("ReturnError.alreadyReturnedOrExpired") case .unexpectedServerError: - return "An unexpected error has occurred on the server." + return R2LCPLocalizedString("ReturnError.unexpectedServerError") } } @@ -166,34 +183,18 @@ public enum ReturnError: LocalizedError { /// Errors while parsing the License or Status JSON Documents. public enum ParsingError: Error { + // The JSON is malformed and can't be parsed. case malformedJSON + // The JSON is not representing a valid License Document. case licenseDocument + // The JSON is not representing a valid Status Document. case statusDocument + // Invalid Link. case link + // Invalid Encryption. case encryption + // Invalid License Document Signature. case signature + // Invalid URL for link with rel %@. case url(rel: String) } - -extension ParsingError: LocalizedError { - - public var errorDescription: String? { - switch self { - case .malformedJSON: - return "The JSON is malformed and can't be parsed." - case .licenseDocument: - return "The JSON is not representing a valid License Document." - case .statusDocument: - return "The JSON is not representing a valid Status Document." - case .link: - return "Invalid Link." - case .encryption: - return "Invalid Encryption." - case .signature: - return "Invalid License Document Signature." - case .url(let rel): - return "Invalid URL for link with rel \(rel)." - } - } - -} diff --git a/readium-lcp-swift/Resources/en.lproj/Localizable.strings b/readium-lcp-swift/Resources/en.lproj/Localizable.strings new file mode 100644 index 0000000..7a60d30 --- /dev/null +++ b/readium-lcp-swift/Resources/en.lproj/Localizable.strings @@ -0,0 +1,49 @@ +/* + Localizable.strings + r2-lcp-swift + + Created by Mickaël Menu on 13.06.19. + + Copyright 2019 Readium Foundation. All rights reserved. + Use of this source code is governed by a BSD-style license which is detailed + in the LICENSE file present in the project repository where this source code is maintained. +*/ + +/* LCPError: General error messages */ +"ReadiumLCP.LCPError.licenseIsBusy" = "Can't perform this operation at the moment."; +"ReadiumLCP.LCPError.licenseIntegrity" = "License integrity: %@"; +"ReadiumLCP.LCPError.licenseContainer" = "Can't access the License Document."; +"ReadiumLCP.LCPError.licenseInteractionNotAvailable" = "This interaction is not available."; +"ReadiumLCP.LCPError.licenseProfileNotSupported" = "This License has a profile identifier that this app cannot handle, the publication cannot be processed."; +"ReadiumLCP.LCPError.crlFetching" = "Can't retrieve the Certificate Revocation List."; +"ReadiumLCP.LCPError.parsing" = "Failed to parse the License Document."; +"ReadiumLCP.LCPError.network" = "Network error."; + +/* LCPClientError: Errors while checking the integrity of the License */ +"ReadiumLCP.LCPClientError.licenseOutOfDate" = "License is out of date (check start and end date)."; +"ReadiumLCP.LCPClientError.certificateRevoked" = "Certificate has been revoked in the CRL."; +"ReadiumLCP.LCPClientError.certificateSignatureInvalid" = "Certificate has not been signed by CA."; +"ReadiumLCP.LCPClientError.licenseSignatureDateInvalid" = "License has been issued by an expired certificate."; +"ReadiumLCP.LCPClientError.licenseSignatureInvalid" = "License signature does not match."; +"ReadiumLCP.LCPClientError.contextInvalid" = "The DRM context is invalid."; +"ReadiumLCP.LCPClientError.contentKeyDecryptError" = "Unable to decrypt encrypted content key from user key."; +"ReadiumLCP.LCPClientError.userKeyCheckInvalid" = "User key check invalid."; +"ReadiumLCP.LCPClientError.contentDecryptError" = "Unable to decrypt encrypted content from content key."; +"ReadiumLCP.LCPClientError.unknown" = "Unknown error."; + +/* StatusError: Errors while checking the status of the License, using the Status Document. */ +"ReadiumLCP.StatusError.cancelled" = "You have cancelled this license on %@."; +"ReadiumLCP.StatusError.returned" = "This license has been returned on %@."; +"ReadiumLCP.StatusError.expired.start" = "This license starts on %@."; +"ReadiumLCP.StatusError.expired.end" = "This license expired on %@."; +"ReadiumLCP.StatusError.revoked" = "This license has been revoked by its provider on %1$@.\nThe license was registered by %1$d device(s)"; + +/* RenewError: Errors while renewing a loan. */ +"ReadiumLCP.RenewError.renewFailed" = "Your publication could not be renewed properly."; +"ReadiumLCP.RenewError.invalidRenewalPeriod" = "Incorrect renewal period, your publication could not be renewed."; +"ReadiumLCP.RenewError.unexpectedServerError" = "An unexpected error has occurred on the server."; + +/* ReturnError: Errors while returning a loan. */ +"ReadiumLCP.ReturnError.returnFailed" = "Your publication could not be returned properly."; +"ReadiumLCP.ReturnError.alreadyReturnedOrExpired" = "Your publication has already been returned before or is expired."; +"ReadiumLCP.ReturnError.unexpectedServerError" = "An unexpected error has occurred on the server."; diff --git a/readium-lcp-swift/prod-license.lcpl b/readium-lcp-swift/Resources/prod-license.lcpl similarity index 100% rename from readium-lcp-swift/prod-license.lcpl rename to readium-lcp-swift/Resources/prod-license.lcpl diff --git a/readium-lcp-swift/Toolkit/R2LCPLocalizedString.swift b/readium-lcp-swift/Toolkit/R2LCPLocalizedString.swift new file mode 100644 index 0000000..4af0b05 --- /dev/null +++ b/readium-lcp-swift/Toolkit/R2LCPLocalizedString.swift @@ -0,0 +1,17 @@ +// +// R2LCPLocalizedString.swift +// r2-lcp-swift +// +// Created by Mickaël Menu on 13.06.19. +// +// Copyright 2019 Readium Foundation. All rights reserved. +// Use of this source code is governed by a BSD-style license which is detailed +// in the LICENSE file present in the project repository where this source code is maintained. +// + +import Foundation +import R2Shared + +func R2LCPLocalizedString(_ key: String, _ values: CVarArg...) -> String { + return R2LocalizedString("ReadiumLCP.\(key)", in: "org.readium.readium-lcp-swift", values) +}