UNPKG

r2-lcp-js

Version:

Readium 2 LCP bits for NodeJS (TypeScript)

416 lines 20.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LCP = void 0; exports.setLcpNativePluginPath = setLcpNativePluginPath; var tslib_1 = require("tslib"); var bind = require("bindings"); var crypto = require("crypto"); var debug_ = require("debug"); var fs = require("fs"); var path = require("path"); var request = require("request"); var ta_json_x_1 = require("ta-json-x"); var BufferUtils_1 = require("r2-utils-js/dist/es5/src/_utils/stream/BufferUtils"); var lcp_certificate_1 = require("./lcp-certificate"); var lcp_encryption_1 = require("./lcp-encryption"); var lcp_link_1 = require("./lcp-link"); var lcp_rights_1 = require("./lcp-rights"); var lcp_signature_1 = require("./lcp-signature"); var lcp_user_1 = require("./lcp-user"); var AES_BLOCK_SIZE = 16; var debug = debug_("r2:lcp#parser/epub/lcp"); var IS_DEV = (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "dev"); var LCP_NATIVE_PLUGIN_PATH = path.join(process.cwd(), "LCP", "lcp.node"); function setLcpNativePluginPath(filepath) { LCP_NATIVE_PLUGIN_PATH = filepath; if (IS_DEV) { debug(LCP_NATIVE_PLUGIN_PATH); } var exists = fs.existsSync(LCP_NATIVE_PLUGIN_PATH); if (IS_DEV) { debug("LCP NATIVE PLUGIN: " + (exists ? "OKAY" : "MISSING")); } return exists; } var LCP = (function () { function LCP() { this._usesNativeNodePlugin = undefined; } LCP.prototype.isNativeNodePlugin = function () { this.init(); return this._usesNativeNodePlugin; }; LCP.prototype.isReady = function () { if (this.isNativeNodePlugin()) { return typeof this._lcpContext !== "undefined"; } return typeof this.ContentKey !== "undefined"; }; LCP.prototype.init = function () { if (typeof this._usesNativeNodePlugin !== "undefined") { return; } this.ContentKey = undefined; this._lcpContext = undefined; if (fs.existsSync(LCP_NATIVE_PLUGIN_PATH)) { if (IS_DEV) { debug("LCP _usesNativeNodePlugin"); } var filePath = path.dirname(LCP_NATIVE_PLUGIN_PATH); var fileName = path.basename(LCP_NATIVE_PLUGIN_PATH); if (IS_DEV) { debug(filePath); debug(fileName); } this._usesNativeNodePlugin = true; this._lcpNative = bind({ bindings: fileName, module_root: filePath, try: [[ "module_root", "bindings", ]], }); } else { if (IS_DEV) { debug("LCP JS impl"); } this._usesNativeNodePlugin = false; this._lcpNative = undefined; } }; LCP.prototype.decrypt = function (encryptedContent, linkHref, needsInflating) { return tslib_1.__awaiter(this, void 0, void 0, function () { var _this = this; return tslib_1.__generator(this, function (_a) { if (!this.isNativeNodePlugin()) { return [2, Promise.reject("direct decrypt buffer only for native plugin")]; } if (!this._lcpContext) { return [2, Promise.reject("LCP context not initialized (call tryUserKeys())")]; } return [2, new Promise(function (resolve, reject) { _this._lcpNative.decrypt(_this._lcpContext, encryptedContent, function (er, decryptedContent, inflated) { if (er) { debug("decrypt ERROR"); debug(er); reject(er); return; } var buff = decryptedContent; if (!inflated) { var padding = decryptedContent[decryptedContent.length - 1]; buff = decryptedContent.slice(0, decryptedContent.length - padding); } resolve({ buffer: buff, inflated: inflated ? true : false, }); }, _this.JsonSource, linkHref, needsInflating); })]; }); }); }; LCP.prototype.dummyCreateContext = function () { return tslib_1.__awaiter(this, void 0, void 0, function () { var crlPem_1, sha256DummyPassphrase_1; var _this = this; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: this.init(); if (!this._usesNativeNodePlugin) return [3, 2]; return [4, this.getCRLPem()]; case 1: crlPem_1 = _a.sent(); sha256DummyPassphrase_1 = "0".repeat(64); return [2, new Promise(function (resolve, reject) { _this._lcpNative.createContext(_this.JsonSource, sha256DummyPassphrase_1, crlPem_1, function (erro, _context) { if (erro) { debug("dummyCreateContext ERROR"); debug(erro); reject(erro); return; } resolve(); }); })]; case 2: return [2, Promise.resolve()]; } }); }); }; LCP.prototype.tryUserKeys = function (lcpUserKeys) { return tslib_1.__awaiter(this, void 0, void 0, function () { var check, crlPem_2, _i, lcpUserKeys_1, lcpUserKey; var _this = this; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: this.init(); check = (this.Encryption.Profile === "http://readium.org/lcp/basic-profile" || this.Encryption.Profile === "http://readium.org/lcp/profile-1.0" || (this.Encryption.Profile && /^http:\/\/readium\.org\/lcp\/profile-2\.[0-9]$/.test(this.Encryption.Profile))) && this.Encryption.UserKey.Algorithm === "http://www.w3.org/2001/04/xmlenc#sha256" && this.Encryption.ContentKey.Algorithm === "http://www.w3.org/2001/04/xmlenc#aes256-cbc"; if (!check) { debug("Incorrect LCP fields."); debug(this.Encryption.Profile); debug(this.Encryption.ContentKey.Algorithm); debug(this.Encryption.UserKey.Algorithm); return [2, Promise.reject("Incorrect LCP fields.")]; } if (!this._usesNativeNodePlugin) return [3, 2]; return [4, this.getCRLPem()]; case 1: crlPem_2 = _a.sent(); return [2, new Promise(function (resolve, reject) { _this._lcpNative.findOneValidPassphrase(_this.JsonSource, lcpUserKeys, function (err, validHashedPassphrase) { if (err) { debug("findOneValidPassphrase ERROR"); debug(err); reject(err); return; } _this._lcpNative.createContext(_this.JsonSource, validHashedPassphrase, crlPem_2, function (erro, context) { if (erro) { debug("createContext ERROR"); debug(erro); reject(erro); return; } _this._lcpContext = context; resolve(); }); }); })]; case 2: for (_i = 0, lcpUserKeys_1 = lcpUserKeys; _i < lcpUserKeys_1.length; _i++) { lcpUserKey = lcpUserKeys_1[_i]; try { if (this.tryUserKey(lcpUserKey)) { return [2, Promise.resolve()]; } } catch (_err) { } } return [2, Promise.reject(1)]; } }); }); }; LCP.prototype.getCRLPem = function () { return tslib_1.__awaiter(this, void 0, void 0, function () { var _this = this; return tslib_1.__generator(this, function (_a) { return [2, new Promise(function (resolve, reject) { return tslib_1.__awaiter(_this, void 0, void 0, function () { var crlURL, failure, success, headers; var _this = this; return tslib_1.__generator(this, function (_a) { crlURL = lcp_certificate_1.CRL_URL; failure = function (err) { debug(err); resolve(lcp_certificate_1.DUMMY_CRL); }; success = function (response) { return tslib_1.__awaiter(_this, void 0, void 0, function () { var failBuff, buffErr_1, failStr, failJson, responseData, err_1, lcplStr; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: if (IS_DEV) { Object.keys(response.headers).forEach(function (header) { debug(header + " => " + response.headers[header]); }); } if (!(response.statusCode && (response.statusCode < 200 || response.statusCode >= 300))) return [3, 5]; failBuff = void 0; _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4, (0, BufferUtils_1.streamToBufferPromise)(response)]; case 2: failBuff = _a.sent(); return [3, 4]; case 3: buffErr_1 = _a.sent(); if (IS_DEV) { debug(buffErr_1); } failure(response.statusCode); return [2]; case 4: try { failStr = failBuff.toString("utf8"); if (IS_DEV) { debug(failStr); } try { failJson = global.JSON.parse(failStr); if (IS_DEV) { debug(failJson); } failJson.httpStatusCode = response.statusCode; failure(failJson); } catch (jsonErr) { if (IS_DEV) { debug(jsonErr); } failure({ httpStatusCode: response.statusCode, httpResponseBody: failStr }); } } catch (strErr) { if (IS_DEV) { debug(strErr); } failure(response.statusCode); } return [2]; case 5: _a.trys.push([5, 7, , 8]); return [4, (0, BufferUtils_1.streamToBufferPromise)(response)]; case 6: responseData = _a.sent(); return [3, 8]; case 7: err_1 = _a.sent(); reject(err_1); return [2]; case 8: lcplStr = "-----BEGIN X509 CRL-----\n" + responseData.toString("base64") + "\n-----END X509 CRL-----"; if (IS_DEV) { debug(lcplStr); } resolve(lcplStr); return [2]; } }); }); }; headers = {}; request.get({ headers: headers, method: "GET", timeout: 2000, uri: crlURL, }) .on("response", function (res) { return tslib_1.__awaiter(_this, void 0, void 0, function () { var successError_1; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); return [4, success(res)]; case 1: _a.sent(); return [3, 3]; case 2: successError_1 = _a.sent(); failure(successError_1); return [2]; case 3: return [2]; } }); }); }) .on("error", failure); return [2]; }); }); })]; }); }); }; LCP.prototype.tryUserKey = function (lcpUserKey) { var userKey = Buffer.from(lcpUserKey, "hex"); var keyCheck = Buffer.from(this.Encryption.UserKey.KeyCheck, "base64"); var encryptedLicenseID = keyCheck; var iv = encryptedLicenseID.slice(0, AES_BLOCK_SIZE); var encrypted = encryptedLicenseID.slice(AES_BLOCK_SIZE); var decrypteds = []; var decryptStream = crypto.createDecipheriv("aes-256-cbc", userKey, iv); decryptStream.setAutoPadding(false); var buff1 = decryptStream.update(encrypted); if (buff1) { decrypteds.push(buff1); } var buff2 = decryptStream.final(); if (buff2) { decrypteds.push(buff2); } var decrypted = Buffer.concat(decrypteds); var nPaddingBytes = decrypted[decrypted.length - 1]; var size = encrypted.length - nPaddingBytes; var decryptedOut = decrypted.slice(0, size).toString("utf8"); if (this.ID !== decryptedOut) { debug("Failed LCP ID check."); return false; } var encryptedContentKey = Buffer.from(this.Encryption.ContentKey.EncryptedValue, "base64"); var iv2 = encryptedContentKey.slice(0, AES_BLOCK_SIZE); var encrypted2 = encryptedContentKey.slice(AES_BLOCK_SIZE); var decrypteds2 = []; var decryptStream2 = crypto.createDecipheriv("aes-256-cbc", userKey, iv2); decryptStream2.setAutoPadding(false); var buff1_ = decryptStream2.update(encrypted2); if (buff1_) { decrypteds2.push(buff1_); } var buff2_ = decryptStream2.final(); if (buff2_) { decrypteds2.push(buff2_); } var decrypted2 = Buffer.concat(decrypteds2); var nPaddingBytes2 = decrypted2[decrypted2.length - 1]; var size2 = encrypted2.length - nPaddingBytes2; this.ContentKey = decrypted2.slice(0, size2); return true; }; tslib_1.__decorate([ (0, ta_json_x_1.JsonProperty)("id"), tslib_1.__metadata("design:type", String) ], LCP.prototype, "ID", void 0); tslib_1.__decorate([ (0, ta_json_x_1.JsonProperty)("provider"), tslib_1.__metadata("design:type", String) ], LCP.prototype, "Provider", void 0); tslib_1.__decorate([ (0, ta_json_x_1.JsonProperty)("issued"), tslib_1.__metadata("design:type", Date) ], LCP.prototype, "Issued", void 0); tslib_1.__decorate([ (0, ta_json_x_1.JsonProperty)("updated"), tslib_1.__metadata("design:type", Date) ], LCP.prototype, "Updated", void 0); tslib_1.__decorate([ (0, ta_json_x_1.JsonProperty)("encryption"), tslib_1.__metadata("design:type", lcp_encryption_1.Encryption) ], LCP.prototype, "Encryption", void 0); tslib_1.__decorate([ (0, ta_json_x_1.JsonProperty)("rights"), tslib_1.__metadata("design:type", lcp_rights_1.Rights) ], LCP.prototype, "Rights", void 0); tslib_1.__decorate([ (0, ta_json_x_1.JsonProperty)("user"), tslib_1.__metadata("design:type", lcp_user_1.User) ], LCP.prototype, "User", void 0); tslib_1.__decorate([ (0, ta_json_x_1.JsonProperty)("signature"), tslib_1.__metadata("design:type", lcp_signature_1.Signature) ], LCP.prototype, "Signature", void 0); tslib_1.__decorate([ (0, ta_json_x_1.JsonProperty)("links"), (0, ta_json_x_1.JsonElementType)(lcp_link_1.Link), tslib_1.__metadata("design:type", Array) ], LCP.prototype, "Links", void 0); LCP = tslib_1.__decorate([ (0, ta_json_x_1.JsonObject)() ], LCP); return LCP; }()); exports.LCP = LCP; //# sourceMappingURL=lcp.js.map