ece.js 7.9 KB

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