UNPKG

r2-lcp-js

Version:

Readium 2 LCP bits for NodeJS (TypeScript)

363 lines 14.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LCP = void 0; exports.setLcpNativePluginPath = setLcpNativePluginPath; const tslib_1 = require("tslib"); const bind = require("bindings"); const crypto = require("crypto"); const debug_ = require("debug"); const fs = require("fs"); const path = require("path"); const request = require("request"); const ta_json_x_1 = require("ta-json-x"); const BufferUtils_1 = require("r2-utils-js/dist/es7-es2016/src/_utils/stream/BufferUtils"); const lcp_certificate_1 = require("./lcp-certificate"); const lcp_encryption_1 = require("./lcp-encryption"); const lcp_link_1 = require("./lcp-link"); const lcp_rights_1 = require("./lcp-rights"); const lcp_signature_1 = require("./lcp-signature"); const lcp_user_1 = require("./lcp-user"); const AES_BLOCK_SIZE = 16; const debug = debug_("r2:lcp#parser/epub/lcp"); const IS_DEV = (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "dev"); let 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); } const exists = fs.existsSync(LCP_NATIVE_PLUGIN_PATH); if (IS_DEV) { debug("LCP NATIVE PLUGIN: " + (exists ? "OKAY" : "MISSING")); } return exists; } let LCP = class LCP { constructor() { this._usesNativeNodePlugin = undefined; } isNativeNodePlugin() { this.init(); return this._usesNativeNodePlugin; } isReady() { if (this.isNativeNodePlugin()) { return typeof this._lcpContext !== "undefined"; } return typeof this.ContentKey !== "undefined"; } init() { 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"); } const filePath = path.dirname(LCP_NATIVE_PLUGIN_PATH); const 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; } } decrypt(encryptedContent, linkHref, needsInflating) { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (!this.isNativeNodePlugin()) { return Promise.reject("direct decrypt buffer only for native plugin"); } if (!this._lcpContext) { return Promise.reject("LCP context not initialized (call tryUserKeys())"); } return new Promise((resolve, reject) => { this._lcpNative.decrypt(this._lcpContext, encryptedContent, (er, decryptedContent, inflated) => { if (er) { debug("decrypt ERROR"); debug(er); reject(er); return; } let buff = decryptedContent; if (!inflated) { const padding = decryptedContent[decryptedContent.length - 1]; buff = decryptedContent.slice(0, decryptedContent.length - padding); } resolve({ buffer: buff, inflated: inflated ? true : false, }); }, this.JsonSource, linkHref, needsInflating); }); }); } dummyCreateContext() { return tslib_1.__awaiter(this, void 0, void 0, function* () { this.init(); if (this._usesNativeNodePlugin) { const crlPem = yield this.getCRLPem(); const sha256DummyPassphrase = "0".repeat(64); return new Promise((resolve, reject) => { this._lcpNative.createContext(this.JsonSource, sha256DummyPassphrase, crlPem, (erro, _context) => { if (erro) { debug("dummyCreateContext ERROR"); debug(erro); reject(erro); return; } resolve(); }); }); } return Promise.resolve(); }); } tryUserKeys(lcpUserKeys) { return tslib_1.__awaiter(this, void 0, void 0, function* () { this.init(); const 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 Promise.reject("Incorrect LCP fields."); } if (this._usesNativeNodePlugin) { const crlPem = yield this.getCRLPem(); return new Promise((resolve, reject) => { this._lcpNative.findOneValidPassphrase(this.JsonSource, lcpUserKeys, (err, validHashedPassphrase) => { if (err) { debug("findOneValidPassphrase ERROR"); debug(err); reject(err); return; } this._lcpNative.createContext(this.JsonSource, validHashedPassphrase, crlPem, (erro, context) => { if (erro) { debug("createContext ERROR"); debug(erro); reject(erro); return; } this._lcpContext = context; resolve(); }); }); }); } for (const lcpUserKey of lcpUserKeys) { try { if (this.tryUserKey(lcpUserKey)) { return Promise.resolve(); } } catch (_err) { } } return Promise.reject(1); }); } getCRLPem() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => tslib_1.__awaiter(this, void 0, void 0, function* () { const crlURL = lcp_certificate_1.CRL_URL; const failure = (err) => { debug(err); resolve(lcp_certificate_1.DUMMY_CRL); }; const success = (response) => tslib_1.__awaiter(this, void 0, void 0, function* () { if (IS_DEV) { Object.keys(response.headers).forEach((header) => { debug(header + " => " + response.headers[header]); }); } if (response.statusCode && (response.statusCode < 200 || response.statusCode >= 300)) { let failBuff; try { failBuff = yield (0, BufferUtils_1.streamToBufferPromise)(response); } catch (buffErr) { if (IS_DEV) { debug(buffErr); } failure(response.statusCode); return; } try { const failStr = failBuff.toString("utf8"); if (IS_DEV) { debug(failStr); } try { const 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; } let responseData; try { responseData = yield (0, BufferUtils_1.streamToBufferPromise)(response); } catch (err) { reject(err); return; } const lcplStr = "-----BEGIN X509 CRL-----\n" + responseData.toString("base64") + "\n-----END X509 CRL-----"; if (IS_DEV) { debug(lcplStr); } resolve(lcplStr); }); const headers = {}; request.get({ headers, method: "GET", timeout: 2000, uri: crlURL, }) .on("response", (res) => tslib_1.__awaiter(this, void 0, void 0, function* () { try { yield success(res); } catch (successError) { failure(successError); return; } })) .on("error", failure); })); }); } tryUserKey(lcpUserKey) { const userKey = Buffer.from(lcpUserKey, "hex"); const keyCheck = Buffer.from(this.Encryption.UserKey.KeyCheck, "base64"); const encryptedLicenseID = keyCheck; const iv = encryptedLicenseID.slice(0, AES_BLOCK_SIZE); const encrypted = encryptedLicenseID.slice(AES_BLOCK_SIZE); const decrypteds = []; const decryptStream = crypto.createDecipheriv("aes-256-cbc", userKey, iv); decryptStream.setAutoPadding(false); const buff1 = decryptStream.update(encrypted); if (buff1) { decrypteds.push(buff1); } const buff2 = decryptStream.final(); if (buff2) { decrypteds.push(buff2); } const decrypted = Buffer.concat(decrypteds); const nPaddingBytes = decrypted[decrypted.length - 1]; const size = encrypted.length - nPaddingBytes; const decryptedOut = decrypted.slice(0, size).toString("utf8"); if (this.ID !== decryptedOut) { debug("Failed LCP ID check."); return false; } const encryptedContentKey = Buffer.from(this.Encryption.ContentKey.EncryptedValue, "base64"); const iv2 = encryptedContentKey.slice(0, AES_BLOCK_SIZE); const encrypted2 = encryptedContentKey.slice(AES_BLOCK_SIZE); const decrypteds2 = []; const decryptStream2 = crypto.createDecipheriv("aes-256-cbc", userKey, iv2); decryptStream2.setAutoPadding(false); const buff1_ = decryptStream2.update(encrypted2); if (buff1_) { decrypteds2.push(buff1_); } const buff2_ = decryptStream2.final(); if (buff2_) { decrypteds2.push(buff2_); } const decrypted2 = Buffer.concat(decrypteds2); const nPaddingBytes2 = decrypted2[decrypted2.length - 1]; const size2 = encrypted2.length - nPaddingBytes2; this.ContentKey = decrypted2.slice(0, size2); return true; } }; exports.LCP = LCP; 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); exports.LCP = LCP = tslib_1.__decorate([ (0, ta_json_x_1.JsonObject)() ], LCP); //# sourceMappingURL=lcp.js.map