ece.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. require('buffer');
  2. const NONCE_LENGTH = 12;
  3. const TAG_LENGTH = 16;
  4. const KEY_LENGTH = 16;
  5. const MODE_ENCRYPT = 'encrypt';
  6. const MODE_DECRYPT = 'decrypt';
  7. const RS = 1024 * 1024;
  8. const encoder = new TextEncoder();
  9. function generateSalt(len) {
  10. const randSalt = new Uint8Array(len);
  11. crypto.getRandomValues(randSalt);
  12. return randSalt.buffer;
  13. }
  14. class ECETransformer {
  15. constructor(mode, ikm, rs, salt) {
  16. this.mode = mode;
  17. this.prevChunk;
  18. this.seq = 0;
  19. this.firstchunk = true;
  20. this.rs = rs;
  21. this.ikm = ikm.buffer;
  22. this.salt = salt;
  23. }
  24. async generateKey() {
  25. const inputKey = await crypto.subtle.importKey(
  26. 'raw',
  27. this.ikm,
  28. 'HKDF',
  29. false,
  30. ['deriveKey']
  31. );
  32. return crypto.subtle.deriveKey(
  33. {
  34. name: 'HKDF',
  35. salt: this.salt,
  36. info: encoder.encode('Content-Encoding: aes128gcm\0'),
  37. hash: 'SHA-256'
  38. },
  39. inputKey,
  40. {
  41. name: 'AES-GCM',
  42. length: 128
  43. },
  44. false,
  45. ['encrypt', 'decrypt']
  46. );
  47. }
  48. async generateNonceBase() {
  49. const inputKey = await crypto.subtle.importKey(
  50. 'raw',
  51. this.ikm,
  52. 'HKDF',
  53. false,
  54. ['deriveKey']
  55. );
  56. const base = await crypto.subtle.exportKey(
  57. 'raw',
  58. await crypto.subtle.deriveKey(
  59. {
  60. name: 'HKDF',
  61. salt: this.salt,
  62. info: encoder.encode('Content-Encoding: nonce\0'),
  63. hash: 'SHA-256'
  64. },
  65. inputKey,
  66. {
  67. name: 'AES-GCM',
  68. length: 128
  69. },
  70. true,
  71. ['encrypt', 'decrypt']
  72. )
  73. );
  74. return Buffer.from(base.slice(0, NONCE_LENGTH));
  75. }
  76. generateNonce(seq) {
  77. if (seq > 0xffffffff) {
  78. throw new Error('record sequence number exceeds limit');
  79. }
  80. const nonce = Buffer.from(this.nonceBase);
  81. const m = nonce.readUIntBE(nonce.length - 4, 4);
  82. const xor = (m ^ seq) >>> 0; //forces unsigned int xor
  83. nonce.writeUIntBE(xor, nonce.length - 4, 4);
  84. return nonce;
  85. }
  86. pad(data, isLast) {
  87. const len = data.length;
  88. if (len + TAG_LENGTH >= this.rs) {
  89. throw new Error('data too large for record size');
  90. }
  91. if (isLast) {
  92. const padding = Buffer.alloc(1);
  93. padding.writeUInt8(2, 0);
  94. return Buffer.concat([data, padding]);
  95. } else {
  96. const padding = Buffer.alloc(this.rs - len - TAG_LENGTH);
  97. padding.fill(0);
  98. padding.writeUInt8(1, 0);
  99. return Buffer.concat([data, padding]);
  100. }
  101. }
  102. unpad(data, isLast) {
  103. for (let i = data.length - 1; i >= 0; i--) {
  104. if (data[i]) {
  105. if (isLast) {
  106. if (data[i] !== 2) {
  107. throw new Error('delimiter of final record is not 2');
  108. }
  109. } else {
  110. if (data[i] !== 1) {
  111. throw new Error('delimiter of not final record is not 1');
  112. }
  113. }
  114. return data.slice(0, i);
  115. }
  116. }
  117. throw new Error('no delimiter found');
  118. }
  119. createHeader() {
  120. const nums = Buffer.alloc(5);
  121. nums.writeUIntBE(this.rs, 0, 4);
  122. nums.writeUIntBE(0, 4, 1);
  123. return Buffer.concat([Buffer.from(this.salt), nums]);
  124. }
  125. readHeader(buffer) {
  126. if (buffer.length < 21) {
  127. throw new Error('chunk too small for reading header');
  128. }
  129. const header = {};
  130. header.salt = buffer.buffer.slice(0, KEY_LENGTH);
  131. header.rs = buffer.readUIntBE(KEY_LENGTH, 4);
  132. const idlen = buffer.readUInt8(KEY_LENGTH + 4);
  133. header.length = idlen + KEY_LENGTH + 5;
  134. return header;
  135. }
  136. async encryptRecord(buffer, seq, isLast) {
  137. const nonce = this.generateNonce(seq);
  138. const encrypted = await crypto.subtle.encrypt(
  139. { name: 'AES-GCM', iv: nonce },
  140. this.key,
  141. this.pad(buffer, isLast)
  142. );
  143. return Buffer.from(encrypted);
  144. }
  145. async decryptRecord(buffer, seq, isLast) {
  146. const nonce = this.generateNonce(seq);
  147. const data = await crypto.subtle.decrypt(
  148. {
  149. name: 'AES-GCM',
  150. iv: nonce,
  151. tagLength: 128
  152. },
  153. this.key,
  154. buffer
  155. );
  156. return this.unpad(Buffer.from(data), isLast);
  157. }
  158. async start(controller) {
  159. if (this.mode === MODE_ENCRYPT) {
  160. this.key = await this.generateKey();
  161. this.nonceBase = await this.generateNonceBase();
  162. controller.enqueue(this.createHeader());
  163. } else if (this.mode !== MODE_DECRYPT) {
  164. throw new Error('mode must be either encrypt or decrypt');
  165. }
  166. }
  167. async transformPrevChunk(isLast, controller) {
  168. if (this.mode === MODE_ENCRYPT) {
  169. controller.enqueue(
  170. await this.encryptRecord(this.prevChunk, this.seq, isLast)
  171. );
  172. this.seq++;
  173. } else {
  174. if (this.seq === 0) {
  175. //the first chunk during decryption contains only the header
  176. const header = this.readHeader(this.prevChunk);
  177. this.salt = header.salt;
  178. this.rs = header.rs;
  179. this.key = await this.generateKey();
  180. this.nonceBase = await this.generateNonceBase();
  181. } else {
  182. controller.enqueue(
  183. await this.decryptRecord(this.prevChunk, this.seq - 1, isLast)
  184. );
  185. }
  186. this.seq++;
  187. }
  188. }
  189. async transform(chunk, controller) {
  190. if (!this.firstchunk) {
  191. await this.transformPrevChunk(false, controller);
  192. }
  193. this.firstchunk = false;
  194. this.prevChunk = Buffer.from(chunk.buffer);
  195. }
  196. async flush(controller) {
  197. //console.log('ece stream ends')
  198. if (this.prevChunk) {
  199. await this.transformPrevChunk(true, controller);
  200. }
  201. }
  202. }
  203. export class BlobSlicer {
  204. constructor(blob, rs, mode) {
  205. this.blob = blob;
  206. this.index = 0;
  207. this.mode = mode;
  208. this.chunkSize = mode === MODE_ENCRYPT ? rs - 17 : rs;
  209. }
  210. pull(controller) {
  211. return new Promise((resolve, reject) => {
  212. const bytesLeft = this.blob.size - this.index;
  213. if (bytesLeft <= 0) {
  214. controller.close();
  215. return resolve();
  216. }
  217. let size = 1;
  218. if (this.mode === MODE_DECRYPT && this.index === 0) {
  219. size = Math.min(21, bytesLeft);
  220. } else {
  221. size = Math.min(this.chunkSize, bytesLeft);
  222. }
  223. const blob = this.blob.slice(this.index, this.index + size);
  224. const reader = new FileReader();
  225. reader.onload = () => {
  226. controller.enqueue(new Uint8Array(reader.result));
  227. resolve();
  228. };
  229. reader.onerror = reject;
  230. reader.readAsArrayBuffer(blob);
  231. this.index += size;
  232. });
  233. }
  234. }
  235. class StreamSlicer {
  236. constructor(rs, mode) {
  237. this.mode = mode;
  238. this.rs = rs;
  239. this.chunkSize = mode === MODE_ENCRYPT ? rs - 17 : 21;
  240. this.partialChunk = new Uint8Array(this.chunkSize); //where partial chunks are saved
  241. this.offset = 0;
  242. }
  243. send(buf, controller) {
  244. controller.enqueue(buf);
  245. if (this.chunkSize === 21 && this.mode === MODE_DECRYPT) {
  246. this.chunkSize = this.rs;
  247. }
  248. this.partialChunk = new Uint8Array(this.chunkSize);
  249. }
  250. //reslice input into record sized chunks
  251. transform(chunk, controller) {
  252. //console.log('Received chunk with %d bytes.', chunk.byteLength)
  253. let i = 0;
  254. if (this.offset > 0) {
  255. const len = Math.min(chunk.byteLength, this.chunkSize - this.offset);
  256. this.partialChunk.set(chunk.slice(0, len), this.offset);
  257. this.offset += len;
  258. i += len;
  259. if (this.offset === this.chunkSize) {
  260. this.send(this.partialChunk, controller);
  261. this.offset = 0;
  262. }
  263. }
  264. while (i < chunk.byteLength) {
  265. if (chunk.byteLength - i >= this.chunkSize) {
  266. const record = chunk.slice(i, i + this.chunkSize);
  267. i += this.chunkSize;
  268. this.send(record, controller);
  269. } else {
  270. const end = chunk.slice(i, this.chunkSize);
  271. i += end.length;
  272. this.partialChunk.set(end);
  273. this.offset = end.length;
  274. }
  275. }
  276. }
  277. flush(controller) {
  278. //console.log('slice stream ends')
  279. if (this.offset > 0) {
  280. controller.enqueue(this.partialChunk.slice(0, this.offset));
  281. }
  282. }
  283. }
  284. /*
  285. input: a blob or a ReadableStream containing data to be transformed
  286. key: Uint8Array containing key of size KEY_LENGTH
  287. mode: string, either 'encrypt' or 'decrypt'
  288. rs: int containing record size, optional
  289. salt: ArrayBuffer containing salt of KEY_LENGTH length, optional
  290. */
  291. export default class ECE {
  292. constructor(input, key, mode, rs, salt) {
  293. this.input = input;
  294. this.key = key;
  295. this.mode = mode;
  296. this.rs = rs;
  297. this.salt = salt;
  298. if (rs === undefined) {
  299. this.rs = RS;
  300. }
  301. if (salt === undefined) {
  302. this.salt = generateSalt(KEY_LENGTH);
  303. }
  304. }
  305. info() {
  306. return {
  307. recordSize: this.rs,
  308. fileSize:
  309. 21 + this.input.size + 16 * Math.floor(this.input.size / (this.rs - 17))
  310. };
  311. }
  312. transform() {
  313. let inputStream;
  314. if (this.input instanceof Blob) {
  315. inputStream = new ReadableStream(
  316. new BlobSlicer(this.input, this.rs, this.mode)
  317. );
  318. } else {
  319. // eslint-disable-next-line no-undef
  320. const sliceStream = new TransformStream(
  321. new StreamSlicer(this.rs, this.mode)
  322. );
  323. inputStream = this.input.pipeThrough(sliceStream);
  324. }
  325. // eslint-disable-next-line no-undef
  326. const cryptoStream = new TransformStream(
  327. new ECETransformer(this.mode, this.key, this.rs, this.salt)
  328. );
  329. return inputStream.pipeThrough(cryptoStream);
  330. }
  331. }