ece.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import { transformStream } from './streams';
  2. import { concat } from './utils';
  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 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 = new DataView(this.nonceBase.slice());
  82. const m = nonce.getUint32(nonce.byteLength - 4);
  83. const xor = (m ^ seq) >>> 0; //forces unsigned int xor
  84. nonce.setUint32(nonce.byteLength - 4, xor);
  85. return new Uint8Array(nonce.buffer);
  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. return concat(data, Uint8Array.of(2));
  94. } else {
  95. const padding = new Uint8Array(this.rs - len - TAG_LENGTH);
  96. padding[0] = 1;
  97. return concat(data, padding);
  98. }
  99. }
  100. unpad(data, isLast) {
  101. for (let i = data.length - 1; i >= 0; i--) {
  102. if (data[i]) {
  103. if (isLast) {
  104. if (data[i] !== 2) {
  105. throw new Error('delimiter of final record is not 2');
  106. }
  107. } else {
  108. if (data[i] !== 1) {
  109. throw new Error('delimiter of not final record is not 1');
  110. }
  111. }
  112. return data.slice(0, i);
  113. }
  114. }
  115. throw new Error('no delimiter found');
  116. }
  117. createHeader() {
  118. const nums = new DataView(new ArrayBuffer(5));
  119. nums.setUint32(0, this.rs);
  120. return concat(new Uint8Array(this.salt), new Uint8Array(nums.buffer));
  121. }
  122. readHeader(buffer) {
  123. if (buffer.length < 21) {
  124. throw new Error('chunk too small for reading header');
  125. }
  126. const header = {};
  127. const dv = new DataView(buffer.buffer);
  128. header.salt = buffer.slice(0, KEY_LENGTH);
  129. header.rs = dv.getUint32(KEY_LENGTH);
  130. const idlen = dv.getUint8(KEY_LENGTH + 4);
  131. header.length = idlen + KEY_LENGTH + 5;
  132. return header;
  133. }
  134. async encryptRecord(buffer, seq, isLast) {
  135. const nonce = this.generateNonce(seq);
  136. const encrypted = await crypto.subtle.encrypt(
  137. { name: 'AES-GCM', iv: nonce },
  138. this.key,
  139. this.pad(buffer, isLast)
  140. );
  141. return new Uint8Array(encrypted);
  142. }
  143. async decryptRecord(buffer, seq, isLast) {
  144. const nonce = this.generateNonce(seq);
  145. const data = await crypto.subtle.decrypt(
  146. {
  147. name: 'AES-GCM',
  148. iv: nonce,
  149. tagLength: 128
  150. },
  151. this.key,
  152. buffer
  153. );
  154. return this.unpad(new Uint8Array(data), isLast);
  155. }
  156. async start(controller) {
  157. if (this.mode === MODE_ENCRYPT) {
  158. this.key = await this.generateKey();
  159. this.nonceBase = await this.generateNonceBase();
  160. controller.enqueue(this.createHeader());
  161. } else if (this.mode !== MODE_DECRYPT) {
  162. throw new Error('mode must be either encrypt or decrypt');
  163. }
  164. }
  165. async transformPrevChunk(isLast, controller) {
  166. if (this.mode === MODE_ENCRYPT) {
  167. controller.enqueue(
  168. await this.encryptRecord(this.prevChunk, this.seq, isLast)
  169. );
  170. this.seq++;
  171. } else {
  172. if (this.seq === 0) {
  173. //the first chunk during decryption contains only the header
  174. const header = this.readHeader(this.prevChunk);
  175. this.salt = header.salt;
  176. this.rs = header.rs;
  177. this.key = await this.generateKey();
  178. this.nonceBase = await this.generateNonceBase();
  179. } else {
  180. controller.enqueue(
  181. await this.decryptRecord(this.prevChunk, this.seq - 1, isLast)
  182. );
  183. }
  184. this.seq++;
  185. }
  186. }
  187. async transform(chunk, controller) {
  188. if (!this.firstchunk) {
  189. await this.transformPrevChunk(false, controller);
  190. }
  191. this.firstchunk = false;
  192. this.prevChunk = new Uint8Array(chunk.buffer);
  193. }
  194. async flush(controller) {
  195. //console.log('ece stream ends')
  196. if (this.prevChunk) {
  197. await this.transformPrevChunk(true, controller);
  198. }
  199. }
  200. }
  201. class StreamSlicer {
  202. constructor(rs, mode) {
  203. this.mode = mode;
  204. this.rs = rs;
  205. this.chunkSize = mode === MODE_ENCRYPT ? rs - 17 : 21;
  206. this.partialChunk = new Uint8Array(this.chunkSize); //where partial chunks are saved
  207. this.offset = 0;
  208. }
  209. send(buf, controller) {
  210. controller.enqueue(buf);
  211. if (this.chunkSize === 21 && this.mode === MODE_DECRYPT) {
  212. this.chunkSize = this.rs;
  213. }
  214. this.partialChunk = new Uint8Array(this.chunkSize);
  215. this.offset = 0;
  216. }
  217. //reslice input into record sized chunks
  218. transform(chunk, controller) {
  219. //console.log('Received chunk with %d bytes.', chunk.byteLength)
  220. let i = 0;
  221. if (this.offset > 0) {
  222. const len = Math.min(chunk.byteLength, this.chunkSize - this.offset);
  223. this.partialChunk.set(chunk.slice(0, len), this.offset);
  224. this.offset += len;
  225. i += len;
  226. if (this.offset === this.chunkSize) {
  227. this.send(this.partialChunk, controller);
  228. }
  229. }
  230. while (i < chunk.byteLength) {
  231. const remainingBytes = chunk.byteLength - i;
  232. if (remainingBytes >= this.chunkSize) {
  233. const record = chunk.slice(i, i + this.chunkSize);
  234. i += this.chunkSize;
  235. this.send(record, controller);
  236. } else {
  237. const end = chunk.slice(i, i + remainingBytes);
  238. i += end.byteLength;
  239. this.partialChunk.set(end);
  240. this.offset = end.byteLength;
  241. }
  242. }
  243. }
  244. flush(controller) {
  245. if (this.offset > 0) {
  246. controller.enqueue(this.partialChunk.slice(0, this.offset));
  247. }
  248. }
  249. }
  250. /*
  251. input: a ReadableStream containing data to be transformed
  252. key: Uint8Array containing key of size KEY_LENGTH
  253. rs: int containing record size, optional
  254. salt: ArrayBuffer containing salt of KEY_LENGTH length, optional
  255. */
  256. export function encryptStream(
  257. input,
  258. key,
  259. rs = ECE_RECORD_SIZE,
  260. salt = generateSalt(KEY_LENGTH)
  261. ) {
  262. const mode = 'encrypt';
  263. const inputStream = transformStream(input, new StreamSlicer(rs, mode));
  264. return transformStream(inputStream, new ECETransformer(mode, key, rs, salt));
  265. }
  266. /*
  267. input: a ReadableStream containing data to be transformed
  268. key: Uint8Array containing key of size KEY_LENGTH
  269. rs: int containing record size, optional
  270. */
  271. export function decryptStream(input, key, rs = ECE_RECORD_SIZE) {
  272. const mode = 'decrypt';
  273. const inputStream = transformStream(input, new StreamSlicer(rs, mode));
  274. return transformStream(inputStream, new ECETransformer(mode, key, rs));
  275. }