1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.getDecryptedSizeStream = exports.transformStream = exports.supports = void 0;
|
4 | const crypto = require("crypto");
|
5 | const debug_ = require("debug");
|
6 | const zlib = require("zlib");
|
7 | const BufferUtils_1 = require("r2-utils-js/dist/es8-es2017/src/_utils/stream/BufferUtils");
|
8 | const RangeStream_1 = require("r2-utils-js/dist/es8-es2017/src/_utils/stream/RangeStream");
|
9 | const debug = debug_("r2:lcp#transform/transformer-lcp");
|
10 | const IS_DEV = (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "dev");
|
11 | const AES_BLOCK_SIZE = 16;
|
12 | const readStream = async (s, n) => {
|
13 | return new Promise((resolve, reject) => {
|
14 | const onReadable = () => {
|
15 | const b = s.read(n);
|
16 | s.removeListener("readable", onReadable);
|
17 | s.removeListener("error", reject);
|
18 | resolve(b);
|
19 | };
|
20 | s.on("readable", onReadable);
|
21 | s.on("error", reject);
|
22 | });
|
23 | };
|
24 | function supports(lcp, _linkHref, linkPropertiesEncrypted) {
|
25 | if (!lcp) {
|
26 | return false;
|
27 | }
|
28 | if (!lcp.isReady()) {
|
29 | debug("LCP not ready!");
|
30 | return false;
|
31 | }
|
32 | const check = (linkPropertiesEncrypted.Scheme === "http://readium.org/2014/01/lcp"
|
33 | && (linkPropertiesEncrypted.Profile === "http://readium.org/lcp/basic-profile" ||
|
34 | linkPropertiesEncrypted.Profile === "http://readium.org/lcp/profile-1.0")
|
35 | && linkPropertiesEncrypted.Algorithm === "http://www.w3.org/2001/04/xmlenc#aes256-cbc")
|
36 | ||
|
37 | (linkPropertiesEncrypted.Algorithm === "http://www.w3.org/2001/04/xmlenc#aes256-cbc" &&
|
38 | (lcp.Encryption.Profile === "http://readium.org/lcp/basic-profile" ||
|
39 | lcp.Encryption.Profile === "http://readium.org/lcp/profile-1.0"));
|
40 | if (!check) {
|
41 | return false;
|
42 | }
|
43 | return true;
|
44 | }
|
45 | exports.supports = supports;
|
46 | async function transformStream(lcp, linkHref, linkPropertiesEncrypted, stream, isPartialByteRangeRequest, partialByteBegin, partialByteEnd) {
|
47 | const isCompressionNone = linkPropertiesEncrypted.Compression === "none";
|
48 | const isCompressionDeflate = linkPropertiesEncrypted.Compression === "deflate";
|
49 | let plainTextSize = -1;
|
50 | let nativelyDecryptedStream;
|
51 | let nativelyInflated = false;
|
52 | if (lcp.isNativeNodePlugin()) {
|
53 | if (IS_DEV) {
|
54 | debug("LCP DECRYPT NATIVE: " + linkHref);
|
55 | }
|
56 | let fullEncryptedBuffer;
|
57 | try {
|
58 | fullEncryptedBuffer = await (0, BufferUtils_1.streamToBufferPromise)(stream.stream);
|
59 | }
|
60 | catch (err) {
|
61 | debug(err);
|
62 | return Promise.reject("OUCH!");
|
63 | }
|
64 | let res;
|
65 | try {
|
66 | res = await lcp.decrypt(fullEncryptedBuffer, linkHref, isCompressionDeflate);
|
67 | }
|
68 | catch (err) {
|
69 | debug(err);
|
70 | return Promise.reject("OUCH!");
|
71 | }
|
72 | const nativelyDecryptedBuffer = res.buffer;
|
73 | nativelyInflated = res.inflated;
|
74 | plainTextSize = nativelyDecryptedBuffer.length;
|
75 | linkPropertiesEncrypted.DecryptedLengthBeforeInflate = plainTextSize;
|
76 | if (!nativelyInflated &&
|
77 | linkPropertiesEncrypted.OriginalLength &&
|
78 | isCompressionNone &&
|
79 | linkPropertiesEncrypted.OriginalLength !== plainTextSize) {
|
80 | debug("############### LCP transformStream() LENGTH NOT MATCH linkPropertiesEncrypted.OriginalLength !== plainTextSize: " +
|
81 | `${linkPropertiesEncrypted.OriginalLength} !== ${plainTextSize}`);
|
82 | }
|
83 | nativelyDecryptedStream = (0, BufferUtils_1.bufferToStream)(nativelyDecryptedBuffer);
|
84 | }
|
85 | else {
|
86 | let cryptoInfo;
|
87 | let cypherBlockPadding = -1;
|
88 | if (linkPropertiesEncrypted.DecryptedLengthBeforeInflate > 0) {
|
89 | plainTextSize = linkPropertiesEncrypted.DecryptedLengthBeforeInflate;
|
90 | cypherBlockPadding = linkPropertiesEncrypted.CypherBlockPadding;
|
91 | }
|
92 | else {
|
93 | try {
|
94 | cryptoInfo = await getDecryptedSizeStream(lcp, stream);
|
95 | }
|
96 | catch (err) {
|
97 | debug(err);
|
98 | return Promise.reject(err);
|
99 | }
|
100 | plainTextSize = cryptoInfo.length;
|
101 | cypherBlockPadding = cryptoInfo.padding;
|
102 | linkPropertiesEncrypted.DecryptedLengthBeforeInflate = plainTextSize;
|
103 | linkPropertiesEncrypted.CypherBlockPadding = cypherBlockPadding;
|
104 | try {
|
105 | stream = await stream.reset();
|
106 | }
|
107 | catch (err) {
|
108 | debug(err);
|
109 | return Promise.reject(err);
|
110 | }
|
111 | if (linkPropertiesEncrypted.OriginalLength &&
|
112 | isCompressionNone &&
|
113 | linkPropertiesEncrypted.OriginalLength !== plainTextSize) {
|
114 | debug("############### LCP transformStream() LENGTH NOT MATCH linkPropertiesEncrypted.OriginalLength !== plainTextSize: " +
|
115 | `${linkPropertiesEncrypted.OriginalLength} !== ${plainTextSize}`);
|
116 | }
|
117 | }
|
118 | }
|
119 | let destStream;
|
120 | if (nativelyDecryptedStream) {
|
121 | destStream = nativelyDecryptedStream;
|
122 | }
|
123 | else {
|
124 | let rawDecryptStream;
|
125 | let ivBuffer;
|
126 | if (linkPropertiesEncrypted.CypherBlockIV) {
|
127 | ivBuffer = Buffer.from(linkPropertiesEncrypted.CypherBlockIV, "binary");
|
128 | const cypherRangeStream = new RangeStream_1.RangeStream(AES_BLOCK_SIZE, stream.length - 1, stream.length);
|
129 | stream.stream.pipe(cypherRangeStream);
|
130 | rawDecryptStream = cypherRangeStream;
|
131 | }
|
132 | else {
|
133 | try {
|
134 | ivBuffer = await readStream(stream.stream, AES_BLOCK_SIZE);
|
135 | }
|
136 | catch (err) {
|
137 | debug(err);
|
138 | return Promise.reject(err);
|
139 | }
|
140 | linkPropertiesEncrypted.CypherBlockIV = ivBuffer.toString("binary");
|
141 | stream.stream.resume();
|
142 | rawDecryptStream = stream.stream;
|
143 | }
|
144 | const decryptStream = crypto.createDecipheriv("aes-256-cbc", lcp.ContentKey, ivBuffer);
|
145 | decryptStream.setAutoPadding(false);
|
146 | rawDecryptStream.pipe(decryptStream);
|
147 | destStream = decryptStream;
|
148 | if (linkPropertiesEncrypted.CypherBlockPadding) {
|
149 | const cypherUnpaddedStream = new RangeStream_1.RangeStream(0, plainTextSize - 1, plainTextSize);
|
150 | destStream.pipe(cypherUnpaddedStream);
|
151 | destStream = cypherUnpaddedStream;
|
152 | }
|
153 | }
|
154 | if (!nativelyInflated && isCompressionDeflate) {
|
155 | const inflateStream = zlib.createInflateRaw();
|
156 | destStream.pipe(inflateStream);
|
157 | destStream = inflateStream;
|
158 | if (!linkPropertiesEncrypted.OriginalLength) {
|
159 | debug("############### RESOURCE ENCRYPTED OVER DEFLATE, BUT NO OriginalLength!");
|
160 | let fullDeflatedBuffer;
|
161 | try {
|
162 | fullDeflatedBuffer = await (0, BufferUtils_1.streamToBufferPromise)(destStream);
|
163 | linkPropertiesEncrypted.OriginalLength = fullDeflatedBuffer.length;
|
164 | destStream = (0, BufferUtils_1.bufferToStream)(fullDeflatedBuffer);
|
165 | }
|
166 | catch (err) {
|
167 | debug(err);
|
168 | }
|
169 | }
|
170 | }
|
171 | if (partialByteBegin < 0) {
|
172 | partialByteBegin = 0;
|
173 | }
|
174 | if (partialByteEnd < 0) {
|
175 | partialByteEnd = plainTextSize - 1;
|
176 | if (linkPropertiesEncrypted.OriginalLength) {
|
177 | partialByteEnd = linkPropertiesEncrypted.OriginalLength - 1;
|
178 | }
|
179 | }
|
180 | const l = (!nativelyInflated && linkPropertiesEncrypted.OriginalLength) ?
|
181 | linkPropertiesEncrypted.OriginalLength : plainTextSize;
|
182 | if (isPartialByteRangeRequest) {
|
183 | const rangeStream = new RangeStream_1.RangeStream(partialByteBegin, partialByteEnd, l);
|
184 | destStream.pipe(rangeStream);
|
185 | destStream = rangeStream;
|
186 | }
|
187 | const sal = {
|
188 | length: l,
|
189 | reset: async () => {
|
190 | let resetedStream;
|
191 | try {
|
192 | resetedStream = await stream.reset();
|
193 | }
|
194 | catch (err) {
|
195 | debug(err);
|
196 | return Promise.reject(err);
|
197 | }
|
198 | return transformStream(lcp, linkHref, linkPropertiesEncrypted, resetedStream, isPartialByteRangeRequest, partialByteBegin, partialByteEnd);
|
199 | },
|
200 | stream: destStream,
|
201 | };
|
202 | return Promise.resolve(sal);
|
203 | }
|
204 | exports.transformStream = transformStream;
|
205 | async function getDecryptedSizeStream(lcp, stream) {
|
206 | return new Promise(async (resolve, reject) => {
|
207 | const TWO_AES_BLOCK_SIZE = 2 * AES_BLOCK_SIZE;
|
208 | if (stream.length < TWO_AES_BLOCK_SIZE) {
|
209 | reject("crypto err");
|
210 | return;
|
211 | }
|
212 | const readPos = stream.length - TWO_AES_BLOCK_SIZE;
|
213 | const cypherRangeStream = new RangeStream_1.RangeStream(readPos, readPos + TWO_AES_BLOCK_SIZE - 1, stream.length);
|
214 | stream.stream.pipe(cypherRangeStream);
|
215 | const decrypteds = [];
|
216 | const handle = (ivBuffer, encrypted) => {
|
217 | const decryptStream = crypto.createDecipheriv("aes-256-cbc", lcp.ContentKey, ivBuffer);
|
218 | decryptStream.setAutoPadding(false);
|
219 | const buff1 = decryptStream.update(encrypted);
|
220 | if (buff1) {
|
221 | decrypteds.push(buff1);
|
222 | }
|
223 | const buff2 = decryptStream.final();
|
224 | if (buff2) {
|
225 | decrypteds.push(buff2);
|
226 | }
|
227 | finish();
|
228 | };
|
229 | let finished = false;
|
230 | const finish = () => {
|
231 | if (finished) {
|
232 | return;
|
233 | }
|
234 | finished = true;
|
235 | const decrypted = Buffer.concat(decrypteds);
|
236 | if (decrypted.length !== AES_BLOCK_SIZE) {
|
237 | reject("decrypted.length !== AES_BLOCK_SIZE");
|
238 | return;
|
239 | }
|
240 | const nPaddingBytes = decrypted[AES_BLOCK_SIZE - 1];
|
241 | const size = stream.length - AES_BLOCK_SIZE - nPaddingBytes;
|
242 | const res = {
|
243 | length: size,
|
244 | padding: nPaddingBytes,
|
245 | };
|
246 | resolve(res);
|
247 | };
|
248 | try {
|
249 | const buf = await readStream(cypherRangeStream, TWO_AES_BLOCK_SIZE);
|
250 | if (!buf) {
|
251 | reject("!buf (end?)");
|
252 | return;
|
253 | }
|
254 | if (buf.length !== TWO_AES_BLOCK_SIZE) {
|
255 | reject("buf.length !== TWO_AES_BLOCK_SIZE");
|
256 | return;
|
257 | }
|
258 | handle(buf.slice(0, AES_BLOCK_SIZE), buf.slice(AES_BLOCK_SIZE));
|
259 | }
|
260 | catch (err) {
|
261 | debug(err);
|
262 | reject(err);
|
263 | return;
|
264 | }
|
265 | });
|
266 | }
|
267 | exports.getDecryptedSizeStream = getDecryptedSizeStream;
|
268 |
|
\ | No newline at end of file |