ece.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. 'use strict';
  2. /*
  3. * Encrypted content coding
  4. *
  5. * === Note about versions ===
  6. *
  7. * This code supports multiple versions of the draft. This is selected using
  8. * the |version| parameter.
  9. *
  10. * aes128gcm: The most recent version, the salt, record size and key identifier
  11. * are included in a header that is part of the encrypted content coding.
  12. *
  13. * aesgcm: The version that is widely deployed with WebPush (as of 2016-11).
  14. * This version is selected by default, unless you specify a |padSize| of 1.
  15. */
  16. var crypto = require('crypto');
  17. var AES_GCM = 'aes-128-gcm';
  18. var PAD_SIZE = { 'aes128gcm': 1, 'aesgcm': 2 };
  19. var TAG_LENGTH = 16;
  20. var KEY_LENGTH = 16;
  21. var NONCE_LENGTH = 12;
  22. var SHA_256_LENGTH = 32;
  23. var MODE_ENCRYPT = 'encrypt';
  24. var MODE_DECRYPT = 'decrypt';
  25. var keylog;
  26. if (process.env.ECE_KEYLOG === '1') {
  27. keylog = function(m, k) {
  28. console.warn(m + ' [' + k.length + ']: ' + k.toString('base64url'));
  29. return k;
  30. };
  31. } else {
  32. keylog = function(m, k) { return k; };
  33. }
  34. /* Optionally base64 decode something. */
  35. function decode(b) {
  36. if (typeof b === 'string') {
  37. return Buffer.from(b, 'base64url');
  38. }
  39. return b;
  40. }
  41. function HMAC_hash(key, input) {
  42. var hmac = crypto.createHmac('sha256', key);
  43. hmac.update(input);
  44. return hmac.digest();
  45. }
  46. /* HKDF as defined in RFC5869, using SHA-256 */
  47. function HKDF_extract(salt, ikm) {
  48. keylog('salt', salt);
  49. keylog('ikm', ikm);
  50. return keylog('extract', HMAC_hash(salt, ikm));
  51. }
  52. function HKDF_expand(prk, info, l) {
  53. keylog('prk', prk);
  54. keylog('info', info);
  55. var output = Buffer.alloc(0);
  56. var T = Buffer.alloc(0);
  57. info = Buffer.from(info, 'ascii');
  58. var counter = 0;
  59. var cbuf = Buffer.alloc(1);
  60. while (output.length < l) {
  61. cbuf.writeUIntBE(++counter, 0, 1);
  62. T = HMAC_hash(prk, Buffer.concat([T, info, cbuf]));
  63. output = Buffer.concat([output, T]);
  64. }
  65. return keylog('expand', output.slice(0, l));
  66. }
  67. function HKDF(salt, ikm, info, len) {
  68. return HKDF_expand(HKDF_extract(salt, ikm), info, len);
  69. }
  70. function info(base, context) {
  71. var result = Buffer.concat([
  72. Buffer.from('Content-Encoding: ' + base + '\0', 'ascii'),
  73. context
  74. ]);
  75. keylog('info ' + base, result);
  76. return result;
  77. }
  78. function lengthPrefix(buffer) {
  79. var b = Buffer.concat([Buffer.alloc(2), buffer]);
  80. b.writeUIntBE(buffer.length, 0, 2);
  81. return b;
  82. }
  83. function extractDH(header, mode) {
  84. var key = header.privateKey;
  85. var senderPubKey, receiverPubKey;
  86. if (mode === MODE_ENCRYPT) {
  87. senderPubKey = key.getPublicKey();
  88. receiverPubKey = header.dh;
  89. } else if (mode === MODE_DECRYPT) {
  90. senderPubKey = header.dh;
  91. receiverPubKey = key.getPublicKey();
  92. } else {
  93. throw new Error('Unknown mode only ' + MODE_ENCRYPT +
  94. ' and ' + MODE_DECRYPT + ' supported');
  95. }
  96. return {
  97. secret: key.computeSecret(header.dh),
  98. context: Buffer.concat([
  99. Buffer.from(header.keylabel, 'ascii'),
  100. Buffer.from([0]),
  101. lengthPrefix(receiverPubKey), // user agent
  102. lengthPrefix(senderPubKey) // application server
  103. ])
  104. };
  105. }
  106. function extractSecretAndContext(header, mode) {
  107. var result = { secret: null, context: Buffer.alloc(0) };
  108. if (header.key) {
  109. result.secret = header.key;
  110. if (result.secret.length !== KEY_LENGTH) {
  111. throw new Error('An explicit key must be ' + KEY_LENGTH + ' bytes');
  112. }
  113. } else if (header.dh) { // receiver/decrypt
  114. result = extractDH(header, mode);
  115. } else if (typeof header.keyid !== undefined) {
  116. result.secret = header.keymap[header.keyid];
  117. }
  118. if (!result.secret) {
  119. throw new Error('Unable to determine key');
  120. }
  121. keylog('secret', result.secret);
  122. keylog('context', result.context);
  123. if (header.authSecret) {
  124. result.secret = HKDF(header.authSecret, result.secret,
  125. info('auth', Buffer.alloc(0)), SHA_256_LENGTH);
  126. keylog('authsecret', result.secret);
  127. }
  128. return result;
  129. }
  130. function webpushSecret(header, mode) {
  131. if (!header.authSecret) {
  132. throw new Error('No authentication secret for webpush');
  133. }
  134. keylog('authsecret', header.authSecret);
  135. var remotePubKey, senderPubKey, receiverPubKey;
  136. if (mode === MODE_ENCRYPT) {
  137. senderPubKey = header.privateKey.getPublicKey();
  138. remotePubKey = receiverPubKey = header.dh;
  139. } else if (mode === MODE_DECRYPT) {
  140. remotePubKey = senderPubKey = header.keyid;
  141. receiverPubKey = header.privateKey.getPublicKey();
  142. } else {
  143. throw new Error('Unknown mode only ' + MODE_ENCRYPT +
  144. ' and ' + MODE_DECRYPT + ' supported');
  145. }
  146. keylog('remote pubkey', remotePubKey);
  147. keylog('sender pubkey', senderPubKey);
  148. keylog('receiver pubkey', receiverPubKey);
  149. return keylog('secret dh',
  150. HKDF(header.authSecret,
  151. header.privateKey.computeSecret(remotePubKey),
  152. Buffer.concat([
  153. Buffer.from('WebPush: info\0'),
  154. receiverPubKey,
  155. senderPubKey
  156. ]),
  157. SHA_256_LENGTH));
  158. }
  159. function extractSecret(header, mode, keyLookupCallback) {
  160. if (keyLookupCallback) {
  161. if (!isFunction(keyLookupCallback)) {
  162. throw new Error('Callback is not a function')
  163. }
  164. }
  165. if (header.key) {
  166. if (header.key.length !== KEY_LENGTH) {
  167. throw new Error('An explicit key must be ' + KEY_LENGTH + ' bytes');
  168. }
  169. return keylog('secret key', header.key);
  170. }
  171. if (!header.privateKey) {
  172. // Lookup based on keyid
  173. if (!keyLookupCallback) {
  174. var key = header.keymap && header.keymap[header.keyid];
  175. } else {
  176. var key = keyLookupCallback(header.keyid)
  177. }
  178. if (!key) {
  179. throw new Error('No saved key (keyid: "' + header.keyid + '")');
  180. }
  181. return key;
  182. }
  183. return webpushSecret(header, mode);
  184. }
  185. function deriveKeyAndNonce(header, mode, lookupKeyCallback) {
  186. if (!header.salt) {
  187. throw new Error('must include a salt parameter for ' + header.version);
  188. }
  189. var keyInfo;
  190. var nonceInfo;
  191. var secret;
  192. if (header.version === 'aesgcm') {
  193. // old
  194. var s = extractSecretAndContext(header, mode, lookupKeyCallback);
  195. keyInfo = info('aesgcm', s.context);
  196. nonceInfo = info('nonce', s.context);
  197. secret = s.secret;
  198. } else if (header.version === 'aes128gcm') {
  199. // latest
  200. keyInfo = Buffer.from('Content-Encoding: aes128gcm\0');
  201. nonceInfo = Buffer.from('Content-Encoding: nonce\0');
  202. secret = extractSecret(header, mode, lookupKeyCallback);
  203. } else {
  204. throw new Error('Unable to set context for mode ' + header.version);
  205. }
  206. var prk = HKDF_extract(header.salt, secret);
  207. var result = {
  208. key: HKDF_expand(prk, keyInfo, KEY_LENGTH),
  209. nonce: HKDF_expand(prk, nonceInfo, NONCE_LENGTH)
  210. };
  211. keylog('key', result.key);
  212. keylog('nonce base', result.nonce);
  213. return result;
  214. }
  215. /* Parse command-line arguments. */
  216. function parseParams(params) {
  217. var header = {};
  218. header.version = params.version || 'aes128gcm';
  219. header.rs = parseInt(params.rs, 10);
  220. if (isNaN(header.rs)) {
  221. header.rs = 4096;
  222. }
  223. var overhead = PAD_SIZE[header.version];
  224. if (header.version === 'aes128gcm') {
  225. overhead += TAG_LENGTH;
  226. }
  227. if (header.rs <= overhead) {
  228. throw new Error('The rs parameter has to be greater than ' + overhead);
  229. }
  230. if (params.salt) {
  231. header.salt = decode(params.salt);
  232. if (header.salt.length !== KEY_LENGTH) {
  233. throw new Error('The salt parameter must be ' + KEY_LENGTH + ' bytes');
  234. }
  235. }
  236. header.keyid = params.keyid;
  237. if (params.key) {
  238. header.key = decode(params.key);
  239. } else {
  240. header.privateKey = params.privateKey;
  241. if (!header.privateKey) {
  242. header.keymap = params.keymap;
  243. }
  244. if (header.version !== 'aes128gcm') {
  245. header.keylabel = params.keylabel || 'P-256';
  246. }
  247. if (params.dh) {
  248. header.dh = decode(params.dh);
  249. }
  250. }
  251. if (params.authSecret) {
  252. header.authSecret = decode(params.authSecret);
  253. }
  254. return header;
  255. }
  256. function generateNonce(base, counter) {
  257. var nonce = Buffer.from(base);
  258. var m = nonce.readUIntBE(nonce.length - 6, 6);
  259. var x = ((m ^ counter) & 0xffffff) +
  260. ((((m / 0x1000000) ^ (counter / 0x1000000)) & 0xffffff) * 0x1000000);
  261. nonce.writeUIntBE(x, nonce.length - 6, 6);
  262. keylog('nonce' + counter, nonce);
  263. return nonce;
  264. }
  265. /* Used when decrypting aes128gcm to populate the header values. Modifies the
  266. * header values in place and returns the size of the header. */
  267. function readHeader(buffer, header) {
  268. var idsz = buffer.readUIntBE(20, 1);
  269. header.salt = buffer.slice(0, KEY_LENGTH);
  270. header.rs = buffer.readUIntBE(KEY_LENGTH, 4);
  271. header.keyid = buffer.slice(21, 21 + idsz);
  272. return 21 + idsz;
  273. }
  274. function unpadLegacy(data, version) {
  275. var padSize = PAD_SIZE[version];
  276. var pad = data.readUIntBE(0, padSize);
  277. if (pad + padSize > data.length) {
  278. throw new Error('padding exceeds block size');
  279. }
  280. keylog('padding', data.slice(0, padSize + pad));
  281. var padCheck = Buffer.alloc(pad);
  282. padCheck.fill(0);
  283. if (padCheck.compare(data.slice(padSize, padSize + pad)) !== 0) {
  284. throw new Error('invalid padding');
  285. }
  286. return data.slice(padSize + pad);
  287. }
  288. function unpad(data, last) {
  289. var i = data.length - 1;
  290. while(i >= 0) {
  291. if (data[i]) {
  292. if (last) {
  293. if (data[i] !== 2) {
  294. throw new Error('last record needs to start padding with a 2');
  295. }
  296. } else {
  297. if (data[i] !== 1) {
  298. throw new Error('last record needs to start padding with a 2');
  299. }
  300. }
  301. return data.slice(0, i);
  302. }
  303. --i;
  304. }
  305. throw new Error('all zero plaintext');
  306. }
  307. function decryptRecord(key, counter, buffer, header, last) {
  308. keylog('decrypt', buffer);
  309. var nonce = generateNonce(key.nonce, counter);
  310. var gcm = crypto.createDecipheriv(AES_GCM, key.key, nonce);
  311. gcm.setAuthTag(buffer.slice(buffer.length - TAG_LENGTH));
  312. var data = gcm.update(buffer.slice(0, buffer.length - TAG_LENGTH));
  313. data = Buffer.concat([data, gcm.final()]);
  314. keylog('decrypted', data);
  315. if (header.version !== 'aes128gcm') {
  316. return unpadLegacy(data, header.version);
  317. }
  318. return unpad(data, last);
  319. }
  320. /**
  321. * Decrypt some bytes. This uses the parameters to determine the key and block
  322. * size, which are described in the draft. Binary values are base64url encoded.
  323. *
  324. * |params.version| contains the version of encoding to use: aes128gcm is the latest,
  325. * but aesgcm is also accepted (though the latter might
  326. * disappear in a future release). If omitted, assume aes128gcm.
  327. *
  328. * If |params.key| is specified, that value is used as the key.
  329. *
  330. * If the version is aes128gcm, the keyid is extracted from the header and used
  331. * as the ECDH public key of the sender. For version aesgcm ,
  332. * |params.dh| needs to be provided with the public key of the sender.
  333. *
  334. * The |params.privateKey| includes the private key of the receiver.
  335. */
  336. function decrypt(buffer, params, keyLookupCallback) {
  337. var header = parseParams(params);
  338. if (header.version === 'aes128gcm') {
  339. var headerLength = readHeader(buffer, header);
  340. buffer = buffer.slice(headerLength);
  341. }
  342. var key = deriveKeyAndNonce(header, MODE_DECRYPT, keyLookupCallback);
  343. var start = 0;
  344. var result = Buffer.alloc(0);
  345. var chunkSize = header.rs;
  346. if (header.version !== 'aes128gcm') {
  347. chunkSize += TAG_LENGTH;
  348. }
  349. for (var i = 0; start < buffer.length; ++i) {
  350. var end = start + chunkSize;
  351. if (header.version !== 'aes128gcm' && end === buffer.length) {
  352. throw new Error('Truncated payload');
  353. }
  354. end = Math.min(end, buffer.length);
  355. if (end - start <= TAG_LENGTH) {
  356. throw new Error('Invalid block: too small at ' + i);
  357. }
  358. var block = decryptRecord(key, i, buffer.slice(start, end),
  359. header, end >= buffer.length);
  360. result = Buffer.concat([result, block]);
  361. start = end;
  362. }
  363. return result;
  364. }
  365. function encryptRecord(key, counter, buffer, pad, header, last) {
  366. keylog('encrypt', buffer);
  367. pad = pad || 0;
  368. var nonce = generateNonce(key.nonce, counter);
  369. var gcm = crypto.createCipheriv(AES_GCM, key.key, nonce);
  370. var ciphertext = [];
  371. var padSize = PAD_SIZE[header.version];
  372. var padding = Buffer.alloc(pad + padSize);
  373. padding.fill(0);
  374. if (header.version !== 'aes128gcm') {
  375. padding.writeUIntBE(pad, 0, padSize);
  376. keylog('padding', padding);
  377. ciphertext.push(gcm.update(padding));
  378. ciphertext.push(gcm.update(buffer));
  379. if (!last && padding.length + buffer.length < header.rs) {
  380. throw new Error('Unable to pad to record size');
  381. }
  382. } else {
  383. ciphertext.push(gcm.update(buffer));
  384. padding.writeUIntBE(last ? 2 : 1, 0, 1);
  385. keylog('padding', padding);
  386. ciphertext.push(gcm.update(padding));
  387. }
  388. gcm.final();
  389. var tag = gcm.getAuthTag();
  390. if (tag.length !== TAG_LENGTH) {
  391. throw new Error('invalid tag generated');
  392. }
  393. ciphertext.push(tag);
  394. return keylog('encrypted', Buffer.concat(ciphertext));
  395. }
  396. function writeHeader(header) {
  397. var ints = Buffer.alloc(5);
  398. var keyid = Buffer.from(header.keyid || []);
  399. if (keyid.length > 255) {
  400. throw new Error('keyid is too large');
  401. }
  402. ints.writeUIntBE(header.rs, 0, 4);
  403. ints.writeUIntBE(keyid.length, 4, 1);
  404. return Buffer.concat([header.salt, ints, keyid]);
  405. }
  406. /**
  407. * Encrypt some bytes. This uses the parameters to determine the key and block
  408. * size, which are described in the draft.
  409. *
  410. * |params.version| contains the version of encoding to use: aes128gcm is the latest,
  411. * but aesgcm is also accepted (though the latter two might
  412. * disappear in a future release). If omitted, assume aes128gcm.
  413. *
  414. * If |params.key| is specified, that value is used as the key.
  415. *
  416. * For Diffie-Hellman (WebPush), |params.dh| includes the public key of the
  417. * receiver. |params.privateKey| is used to establish a shared secret. Key
  418. * pairs can be created using |crypto.createECDH()|.
  419. */
  420. function encrypt(buffer, params, keyLookupCallback) {
  421. if (!Buffer.isBuffer(buffer)) {
  422. throw new Error('buffer argument must be a Buffer');
  423. }
  424. var header = parseParams(params);
  425. if (!header.salt) {
  426. header.salt = crypto.randomBytes(KEY_LENGTH);
  427. }
  428. var result;
  429. if (header.version === 'aes128gcm') {
  430. // Save the DH public key in the header unless keyid is set.
  431. if (header.privateKey && !header.keyid) {
  432. header.keyid = header.privateKey.getPublicKey();
  433. }
  434. result = writeHeader(header);
  435. } else {
  436. // No header on other versions
  437. result = Buffer.alloc(0);
  438. }
  439. var key = deriveKeyAndNonce(header, MODE_ENCRYPT, keyLookupCallback);
  440. var start = 0;
  441. var padSize = PAD_SIZE[header.version];
  442. var overhead = padSize;
  443. if (header.version === 'aes128gcm') {
  444. overhead += TAG_LENGTH;
  445. }
  446. var pad = isNaN(parseInt(params.pad, 10)) ? 0 : parseInt(params.pad, 10);
  447. var counter = 0;
  448. var last = false;
  449. while (!last) {
  450. // Pad so that at least one data byte is in a block.
  451. var recordPad = Math.min(header.rs - overhead - 1, pad);
  452. if (header.version !== 'aes128gcm') {
  453. recordPad = Math.min((1 << (padSize * 8)) - 1, recordPad);
  454. }
  455. if (pad > 0 && recordPad === 0) {
  456. ++recordPad; // Deal with perverse case of rs=overhead+1 with padding.
  457. }
  458. pad -= recordPad;
  459. var end = start + header.rs - overhead - recordPad;
  460. if (header.version !== 'aes128gcm') {
  461. // The > here ensures that we write out a padding-only block at the end
  462. // of a buffer.
  463. last = end > buffer.length;
  464. } else {
  465. last = end >= buffer.length;
  466. }
  467. last = last && pad <= 0;
  468. var block = encryptRecord(key, counter, buffer.slice(start, end),
  469. recordPad, header, last);
  470. result = Buffer.concat([result, block]);
  471. start = end;
  472. ++counter;
  473. }
  474. return result;
  475. }
  476. function isFunction(object) {
  477. return typeof(object) === 'function';
  478. }
  479. module.exports = {
  480. decrypt: decrypt,
  481. encrypt: encrypt
  482. };