1
0

CryptTool.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. 'use strict';
  2. const common = require('../common');
  3. const fc = require('fast-check');
  4. const fs = require('fs');
  5. describe('CryptTool', function () {
  6. describe('cipher & decipher', function () {
  7. afterEach(async function () {
  8. // pause to let async functions conclude
  9. await new Promise(resolve => setTimeout(resolve, 1900));
  10. });
  11. this.timeout(30000);
  12. it('can en- and decrypt any message', async function () {
  13. await fc.assert(fc.asyncProperty(
  14. fc.string(),
  15. fc.string(),
  16. fc.string(),
  17. async function (key, password, message) {
  18. const clean = globalThis.cleanup();
  19. // ensure zlib is getting loaded
  20. PrivateBin.Controller.initZlib();
  21. Object.defineProperty(window, 'crypto', {
  22. value: new WebCrypto(),
  23. configurable: true,
  24. enumerable: true,
  25. writable: false
  26. });
  27. global.atob = common.atob;
  28. global.btoa = common.btoa;
  29. message = message.trim();
  30. const cipherMessage = await PrivateBin.CryptTool.cipher(
  31. key, password, message, []
  32. ),
  33. plaintext = await PrivateBin.CryptTool.decipher(
  34. key, password, cipherMessage
  35. );
  36. clean();
  37. const result = (message === plaintext);
  38. if (!result) console.log(plaintext, cipherMessage);
  39. return result;
  40. }
  41. ),
  42. {numRuns: 3});
  43. });
  44. // The below static unit tests are included to ensure deciphering of
  45. // existing pastes still works, even when the format or encoding of
  46. // messages and passwords changes in the future
  47. // Of course you can easily decipher the following texts, if you like.
  48. // Bonus points for finding their sources and hidden meanings.
  49. it(
  50. 'supports PrivateBin v2 ciphertext with password',
  51. async function () {
  52. const clean = globalThis.cleanup();
  53. // ensure zlib is getting loaded
  54. PrivateBin.Controller.initZlib();
  55. Object.defineProperty(window, 'crypto', {
  56. value: new WebCrypto(),
  57. configurable: true,
  58. enumerable: true,
  59. writable: false
  60. });
  61. global.atob = common.atob;
  62. global.btoa = common.btoa;
  63. const paste = await PrivateBin.CryptTool.decipher(
  64. atob('GXPVE/FVL2HbR0eT7R4D8syaBDYwENIgU9N//0d+j74='),
  65. // -- "That's amazing. I've got the same combination on my luggage."
  66. Array.apply(0, Array(6)).map((_,b) => b + 1).join(''),
  67. [
  68. 'W9QvTb1da+IXB4Xmayv73zjK816I1egk4adfhL9DNks8ZSbyMV5SOUCj' +
  69. 'EPv+Ccy+1nhWFVa9VT4oHi0qZqAECmKoko6oLVdaWFwL1hWMvUdBJfIK' +
  70. '13LImrI0l3VGQzXsTKxXgRVHMOn1VqYmE0puo671eUL1g1dx5H1lm2uc' +
  71. 'jxrOuLWcHMEF+ZqbAx5xwae1aZ8QptBEDL9MiYov7PQpZduxE/jR+mHA' +
  72. 'RzVJV1ZyGYIIiRBa1VpJvFN+0HSkdgYSU3TnzXw+sO+xvNaNv/dqVls1' +
  73. 'Lk6mMPninJGxN5ZORw9+eU3sNTzBk0+wrX2lONxUIH3sm5mVZJmdlQ5j' +
  74. 'sDC+Pe4lAlhbXfeTeNV6WwvQafmaY+6vrPZWzn5ty3/vrJtCcGYOFxN4' +
  75. 'Ra6GwxO3/+JqKppYJPtPvnVYriObPlcEe+6nlXXDS9EM8W2xIdw2f54I' +
  76. 'VIA7WHXPj46L6Y/8KpCmlQ5YS7fioWEm3zIbGtzfG8Feu8pT3NVD1wPk' +
  77. 'blJcb9eCFEIzzWi9G2/e4HdzRyH4fCV7b1L0Bg50/BpCuaC5+v1eZZ8e' +
  78. 'Ku286YB42sKovgKzio9P2WDAxtbq/35nRFbGhC7cvQOzRdibZqFGFIaq' +
  79. 'Df0Niz5bvzxKW6eZloBBfm5cxfWdCAX7pTxg/z9Wy/fq3coApwkOS4Ya' +
  80. 'NoAOvVZo9g==',
  81. [['DLAezyPai/y3Gzo8LmUe5A==', 'G550BaMPE+k=', 100000, 256, 128, 'aes', 'gcm', 'zlib'], 'plaintext', 0, 0]
  82. ]
  83. );
  84. clean();
  85. const result = typeof paste === 'string' && paste.includes('securely packed in iron');
  86. if (!result) console.log(paste);
  87. assert.ok(result);
  88. }
  89. );
  90. it(
  91. 'supports PrivateBin v2 ciphertext no password',
  92. async function () {
  93. const clean = globalThis.cleanup();
  94. // ensure zlib is getting loaded
  95. PrivateBin.Controller.initZlib();
  96. Object.defineProperty(window, 'crypto', {
  97. value: new WebCrypto(),
  98. configurable: true,
  99. enumerable: true,
  100. writable: false
  101. });
  102. global.atob = common.atob;
  103. global.btoa = common.btoa;
  104. const paste = await PrivateBin.CryptTool.decipher(
  105. atob('RHK8Y2QURMzoDQrbJ5NaAGr3KhIxjepX3fXwXyXQWY0='),
  106. '', // no password
  107. [
  108. 'IB87fuvRS6rBY0RqLxQjG5/Ng1cU1K/2IXQejhIpUWgFI6UWldM/7Vdk' +
  109. 'ZxqlmUHIRWIpCrIToODG+5iVHm0RqeW0P98CRHG82Z+d8AAznp4pOyAt' +
  110. 'KEa4ifwdoWiSBEbcOBOgggWVUw1nBZt9MHG8iyve+OOH1KPoIv+xVuJV' +
  111. 'nBW/+nxQ2EbDcnSY4yixXeoEB1bPjSXLHrd+ifst9SbcsTuHCNng2K/t' +
  112. 'mp3CdocycLttfcfPkgTYvYNC2Gd2oCEZP0+0a2dGfT7nzuGRCFbAx+b9' +
  113. 'yFNswZHrvlU8CUdrawRSsNN06KZCHkEPUkunjBrdyALLqtn8w2TKA9nf' +
  114. '2n+27R5De7dcjOJZbN80PDsAVNOBDCdTuQF+vkUE+254SqQXp2zTAVX5' +
  115. '1MjpGgTI/xdSfVdg5vOoqymySPmCV4ZO1sFXPs6AXRg2jhzxhihjxlv8' +
  116. '55WY7mSsiU1G3/TBY0Ra+1QzcQ5bSy/ycWEtT72IicM8Hj2w3aF2QwFV' +
  117. 'sz5tAyFTZD3swxR5bbxQZJ+LEQ==',
  118. ['RSSzQHsYsczisktZn6eesg==', 'Dow4nIXOFgI=', 100000, 256, 128, 'aes', 'gcm', 'zlib']
  119. ]
  120. );
  121. clean();
  122. const result = typeof paste === 'string' && paste.includes('Stable, opaque, immortal \u2013 all by dint');
  123. if (!result) console.log(paste);
  124. assert.ok(result);
  125. }
  126. );
  127. it(
  128. 'supports PrivateBin v2 ciphertext with non-ASCII password and no compression',
  129. async function () {
  130. const clean = globalThis.cleanup();
  131. // ensure zlib is getting loaded
  132. PrivateBin.Controller.initZlib();
  133. Object.defineProperty(window, 'crypto', {
  134. value: new WebCrypto(),
  135. configurable: true,
  136. enumerable: true,
  137. writable: false
  138. });
  139. global.atob = common.atob;
  140. global.btoa = common.btoa;
  141. const paste = await PrivateBin.CryptTool.decipher(
  142. atob('BgdCS8FLXvEFnQPwkAE93b0urmkaTShdw1Tr5c350Uo='),
  143. // key derivation of passwords containing characters above U+007F
  144. 'Schl\u00fcssel \u94a5\u5319',
  145. [
  146. 'T0qlQhwqt1KtFY6r8iqg5gmP3O8yudwx0YPQk7xU4hyIq2SlsXmGGonq' +
  147. '6T4sDIJM6i7caSEZYsE/0KeCq0rCw3CZNaUoL9E54sx4ei17m5X9ikUe' +
  148. 'feRX3n8M89jbE6vd1qJ13QvbG0mGYhxllfWDnETol0ws2nsfcqVMQJrX' +
  149. 'nwlm+3PHH9/sqGChLrbTYMHOcUS1ec4/l5qIFfUBXqbO+U5V5nneZRz8' +
  150. '+JuPDk5oSiHTBHV2Jyd0Q3fX37O6bpqEz0Qxl54rLEkfUSRfM7RXCJE2' +
  151. 'wcAaxL9vqGcit/QdBnrJrPyTL8Vdoz289dmx+3EJZEtoW1bSEYQ1mov2' +
  152. 'c4bv8pEutAEnHf9nbBYZJhWdKlVZkqX5cjM11cPLCuebpQbmw9otVCWX' +
  153. 'GAqU8safz+Nu4CkUXoAerX8lrKffJEOLOoGvlJjU2qZexfOjrbxP7XJ3' +
  154. 'lbpddDA4OFVDTG9u99m14T4opmYK/iPwbq6PcI1H9fxzLRwyZjNOoaip' +
  155. 'b7z6vvOyyl1gwnXN98ZRBys6SChO6tqWATenQgs20UUv6oi+vyBoYhLP' +
  156. 'DWFfU4ndf4p2d0BRQldYoXEMl+FgkNMcnS2UR6PhBax/BGDgZlMUljil' +
  157. 'rOxcCM5vd5jSZIWs/IV6pMxOAozymCfjMDfgjE2J6ub00V+92uwu6vsV' +
  158. 'VlbN1ARFCEp6VX7tN1RNOBTYqlXU6vGbSgXLh7Qng50Pin+/6GKpuHzb' +
  159. 'spNmEekIqgzlsNc+/V0BrPe6xvhveVyEANNBgqT5NoRju9ioval08lW0' +
  160. 'OfrFZJ5LEk4jZ9TfMEMej1K5efZMUVP94q+D6fRMFwM=',
  161. [['RekNgsDiTSbYsXae8EnjTQ==', 'wsZdI484+Cs=', 100000, 256, 128, 'aes', 'gcm', 'none'], 'plaintext', 0, 0]
  162. ]
  163. );
  164. clean();
  165. const result = typeof paste === 'string' && paste.includes('Stable, opaque, immortal \u2013 all by dint');
  166. if (!result) console.log(paste);
  167. assert.ok(result);
  168. }
  169. );
  170. it(
  171. 'supports PrivateBin v2 ciphertext with UTF-8 encoded non-ASCII password',
  172. async function () {
  173. const clean = globalThis.cleanup();
  174. // ensure zlib is getting loaded
  175. PrivateBin.Controller.initZlib();
  176. Object.defineProperty(window, 'crypto', {
  177. value: new WebCrypto(),
  178. configurable: true,
  179. enumerable: true,
  180. writable: false
  181. });
  182. global.atob = common.atob;
  183. global.btoa = common.btoa;
  184. const paste = await PrivateBin.CryptTool.decipher(
  185. atob('58THB9UKSts178TIRMfG1l+SikJC92wqO+zu6RSbzOk='),
  186. // key derivation of passwords using their UTF-8 encoding
  187. 'Schl\u00fcssel \u94a5\u5319',
  188. [
  189. '2LitLhswv6zFfUg4AzEjxd6OHWq8PJRh0ImRpzMUyVz67kuZbTYXSzZt' +
  190. '3DLHg/cvT6GpixTZdtHt7z2JK9TXYkoYDcUTuH/xAkgy/k/6w0COOwMp' +
  191. 'mUhtYRRTyrhnPEa82QwpVzxZ7zIHRDNLT2J49yLQ44hp564L6zPWQz6R' +
  192. 'HGuAf1g3Is/dVRNhRaRPyuOrj/PXeA2/KVkpBbWTY2nPCct6pueLiEA/' +
  193. 'pbtI/oOBOo/DQRgLh9MSpZ4a68H6DtWj9/Y6RINm6zn8cit5bOlUYeC5' +
  194. 'tpZD2eqpiRZIcwekXcw64Y/dNNQZZ/MD+0ZmWFicBmWlLbuS3WlxeHAH' +
  195. 'VLGIoZTydTKopZ6ubmQUbI1Fiz3GUDF3q5e9c7vGUO4TVFIKsCyza/8o' +
  196. '2SBfKt66DxTyWTetwcW5ql9L+wfxVf6WFLVkbax/HKvHgozPh6YiGHFJ' +
  197. 'DXeNYenlr0R6zQGjHEi+72+gEgDpHtaZSPYVob+ioNO3EsV9WuxqysK+' +
  198. 'YEmuw+0cFeF2l9ki69AjbtzNjrFgAsX1QR9an+BtUXFeMekwgkx0NkUd' +
  199. 'Sla2eOcoIQSR8HFkOF4tCQPxp4Pgd+UFAMYDkv55sCzPxHm3JXbVCzhw' +
  200. '7N5zmeEI3wJiqHzLzcRK/JqnaPUinqwcv+skickiAAGUKRZwUmHirCyc' +
  201. 'WbIqDxlddQ==',
  202. ['R9xhVxI3CtY6hp45e1ad0w==', 'LqC0jpwAdps=', 100000, 256, 128, 'aes', 'gcm', 'zlib']
  203. ]
  204. );
  205. clean();
  206. const result = typeof paste === 'string' && paste.includes('securely packed in iron');
  207. if (!result) console.log(paste);
  208. assert.ok(result);
  209. }
  210. );
  211. it('does not truncate messages', async function () {
  212. const message = fs.readFileSync('test/compression-sample.txt', 'ascii').trim(),
  213. clean = globalThis.cleanup();
  214. Object.defineProperty(window, 'crypto', {
  215. value: new WebCrypto(),
  216. configurable: true,
  217. enumerable: true,
  218. writable: false
  219. });
  220. // ensure zlib is getting loaded
  221. PrivateBin.Controller.initZlib();
  222. global.atob = common.atob;
  223. global.btoa = common.btoa;
  224. const cipherMessage = await PrivateBin.CryptTool.cipher(
  225. 'foo', 'bar', message, []
  226. ),
  227. plaintext = await PrivateBin.CryptTool.decipher(
  228. 'foo', 'bar', cipherMessage
  229. );
  230. clean();
  231. if (message !== plaintext) {
  232. console.log(plaintext, cipherMessage);
  233. }
  234. assert.strictEqual(message, plaintext);
  235. });
  236. it('can en- and decrypt a particular message (#260)', async function () {
  237. await fc.assert(fc.asyncProperty(
  238. fc.string(),
  239. fc.string(),
  240. async function (key, password) {
  241. const message = `
  242. 1 subgoal
  243. inv : Assert
  244. expr : Expr
  245. sBody : Instr
  246. deduction : (|- [|inv /\ assertOfExpr expr|] sBody [|inv|])%assert
  247. IHdeduction : (|= [|inv /\ assertOfExpr expr |] sBody [|inv|])%assert
  248. mem : Mem
  249. preInMem : inv mem
  250. m : Mem
  251. n : nat
  252. interpRel : interp (nth_iterate sBody n) (MemElem mem) = CpoElem Mem m
  253. lastIter : interp (nth_iterate sBody n) (MemElem mem) |=e expr_neg expr
  254. notLastIter : forall p : nat,
  255. p < n -> interp (nth_iterate sBody p) (MemElem mem) |=e expr
  256. isWhile : interp (while expr sBody) (MemElem mem) =
  257. interp (nth_iterate sBody n) (MemElem mem)
  258. ======================== ( 1 / 1 )
  259. conseq_or_bottom inv (interp (nth_iterate sBody n) (MemElem mem))
  260. `;
  261. const clean = globalThis.cleanup();
  262. // ensure zlib is getting loaded
  263. PrivateBin.Controller.initZlib();
  264. Object.defineProperty(window, 'crypto', {
  265. value: new WebCrypto(),
  266. configurable: true,
  267. enumerable: true,
  268. writable: false
  269. });
  270. global.atob = common.atob;
  271. global.btoa = common.btoa;
  272. const cipherMessage = await PrivateBin.CryptTool.cipher(
  273. key, password, message, []
  274. ),
  275. plaintext = await PrivateBin.CryptTool.decipher(
  276. key, password, cipherMessage
  277. );
  278. clean();
  279. const result = (message === plaintext);
  280. if (!result) console.log(plaintext, cipherMessage);
  281. return result;
  282. }
  283. ),
  284. {numRuns: 3});
  285. });
  286. it('can en- and decrypt any unicode message and password', async function () {
  287. await fc.assert(fc.asyncProperty(
  288. fc.string({unit: 'binary', minLength: 1}),
  289. fc.string({unit: 'binary'}),
  290. async function (message, password) {
  291. const clean = globalThis.cleanup();
  292. // ensure zlib is getting loaded
  293. PrivateBin.Controller.initZlib();
  294. Object.defineProperty(window, 'crypto', {
  295. value: new WebCrypto(),
  296. configurable: true,
  297. enumerable: true,
  298. writable: false
  299. });
  300. global.atob = common.atob;
  301. global.btoa = common.btoa;
  302. const key = PrivateBin.CryptTool.getSymmetricKey(),
  303. cipherMessage = await PrivateBin.CryptTool.cipher(
  304. key, password, message, []
  305. ),
  306. plaintext = await PrivateBin.CryptTool.decipher(
  307. key, password, cipherMessage
  308. );
  309. clean();
  310. const result = (message === plaintext);
  311. if (!result) console.log(plaintext, cipherMessage);
  312. return result;
  313. }
  314. ),
  315. {numRuns: 3});
  316. });
  317. it('does not accept passwords that only share the low byte of each character', async function () {
  318. const passwordPairs = [
  319. ['\u013c', '<'], // U+013C vs <
  320. ['\u4e2d\u6587', '-\u0087'], // U+4E2D U+6587 vs 0x2D 0x87
  321. ['\u20ac', '\u00ac'] // U+20AC vs 0xAC
  322. ];
  323. for (const [password, lowBytePassword] of passwordPairs) {
  324. const clean = globalThis.cleanup();
  325. // ensure zlib is getting loaded
  326. PrivateBin.Controller.initZlib();
  327. Object.defineProperty(window, 'crypto', {
  328. value: new WebCrypto(),
  329. configurable: true,
  330. enumerable: true,
  331. writable: false
  332. });
  333. global.atob = common.atob;
  334. global.btoa = common.btoa;
  335. const key = PrivateBin.CryptTool.getSymmetricKey(),
  336. cipherMessage = await PrivateBin.CryptTool.cipher(
  337. key, password, 'secret', []
  338. ),
  339. plaintext = await PrivateBin.CryptTool.decipher(
  340. key, lowBytePassword, cipherMessage
  341. );
  342. clean();
  343. assert.strictEqual(
  344. plaintext, '',
  345. 'password ' + JSON.stringify(lowBytePassword) +
  346. ' must not decrypt a document encrypted with ' +
  347. JSON.stringify(password)
  348. );
  349. }
  350. });
  351. });
  352. describe('getSymmetricKey', function () {
  353. this.timeout(10000);
  354. let keys = [];
  355. // the parameter is used to ensure the test is run more then one time
  356. it('returns random, non-empty keys', () => {
  357. fc.assert(fc.property(
  358. fc.integer(),
  359. function() {
  360. const clean = globalThis.cleanup();
  361. Object.defineProperty(window, 'crypto', {
  362. value: new WebCrypto(),
  363. configurable: true,
  364. enumerable: true,
  365. writable: false
  366. });
  367. const key = PrivateBin.CryptTool.getSymmetricKey(),
  368. result = (key !== '' && keys.indexOf(key) === -1);
  369. keys.push(key);
  370. clean();
  371. return result;
  372. }
  373. ),
  374. {numRuns: 10});
  375. });
  376. });
  377. });