confidential_helpers.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. /**
  2. * Copyright © 2016-2019 Threema GmbH (https://threema.ch/).
  3. *
  4. * This file is part of Threema Web.
  5. *
  6. * Threema Web is free software: you can redistribute it and/or modify it
  7. * under the terms of the GNU Affero General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or (at
  9. * your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful, but
  12. * WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
  14. * General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  18. */
  19. import {
  20. censor,
  21. BaseConfidential,
  22. ConfidentialArray,
  23. ConfidentialIceCandidate,
  24. ConfidentialObjectValues,
  25. ConfidentialWireMessage
  26. } from '../../src/helpers/confidential';
  27. // tslint:disable:no-reference
  28. /// <reference path="../../src/threema.d.ts" />
  29. class UnlistedClass {}
  30. /**
  31. * A confidential subclass for testing purposes.
  32. */
  33. class TestConfidential extends BaseConfidential<string, string> {
  34. public readonly uncensored: string = 'uncensored';
  35. public censored(): string {
  36. return 'censored';
  37. }
  38. }
  39. describe('Confidential Helpers', () => {
  40. describe('censor function', () => {
  41. it('handles null and undefined', () => {
  42. expect(censor(null)).toBe(null);
  43. expect(censor(undefined)).toBe(undefined);
  44. });
  45. it('handles an object implementing the Confidential interface', () => {
  46. expect(censor(new TestConfidential())).toBe('censored');
  47. });
  48. it('handles booleans', () => {
  49. expect(censor(true)).toBe('[Boolean]');
  50. expect(censor(false)).toBe('[Boolean]');
  51. });
  52. it('handles numbers', () => {
  53. expect(censor(0)).toBe('[Number]');
  54. expect(censor(42)).toBe('[Number]');
  55. expect(censor(-1337)).toBe('[Number]');
  56. });
  57. it('handles strings', () => {
  58. expect(censor('test')).toBe('[String: length=4]');
  59. expect(censor('')).toBe('[String: length=0]');
  60. });
  61. it('handles binary types', () => {
  62. const buffer = new ArrayBuffer(10);
  63. const array = new Uint8Array(buffer, 2, 6);
  64. const blob = new Blob([JSON.stringify({ a: 10 })], { type: 'application/json'} );
  65. expect(censor(buffer)).toBe('[ArrayBuffer: length=10]');
  66. expect(censor(array)).toBe('[Uint8Array: length=6, offset=2]');
  67. expect(censor(blob)).toBe(`[Blob: length=${blob.size}, type=application/json]`);
  68. });
  69. it('handles arrays', () => {
  70. expect(censor([
  71. null,
  72. undefined,
  73. new TestConfidential(),
  74. false,
  75. 42,
  76. 'test',
  77. new Uint8Array(10),
  78. ])).toEqual([
  79. null,
  80. undefined,
  81. 'censored',
  82. '[Boolean]',
  83. '[Number]',
  84. '[String: length=4]',
  85. '[Uint8Array: length=10, offset=0]',
  86. ]);
  87. });
  88. it('handles arrays recursively', () => {
  89. expect(censor([
  90. 'test',
  91. [1, false],
  92. ])).toEqual([
  93. '[String: length=4]',
  94. ['[Number]', '[Boolean]'],
  95. ]);
  96. });
  97. it('handles objects', () => {
  98. expect(censor({
  99. null: null,
  100. undefined: undefined,
  101. confidential: new TestConfidential(),
  102. boolean: false,
  103. number: 42,
  104. string: 'test',
  105. uint8array: new Uint8Array(10),
  106. })).toEqual({
  107. null: null,
  108. undefined: undefined,
  109. confidential: 'censored',
  110. boolean: '[Boolean]',
  111. number: '[Number]',
  112. string: '[String: length=4]',
  113. uint8array: '[Uint8Array: length=10, offset=0]',
  114. });
  115. });
  116. it('handles objects recursively', () => {
  117. expect(censor({
  118. boolean: false,
  119. object: {
  120. foo: 'bar',
  121. },
  122. })).toEqual({
  123. boolean: '[Boolean]',
  124. object: {
  125. foo: '[String: length=3]',
  126. },
  127. });
  128. });
  129. it('handles class instances', () => {
  130. expect(censor(new UnlistedClass())).toBe('[UnlistedClass]');
  131. });
  132. });
  133. describe('ConfidentialArray', () => {
  134. it('subclass of BaseConfidential', () => {
  135. expect(ConfidentialArray.prototype instanceof BaseConfidential).toBeTruthy();
  136. });
  137. it('sanitises all items', () => {
  138. const array = new ConfidentialArray([new TestConfidential(), new TestConfidential()]);
  139. expect(array.uncensored).toEqual(['uncensored', 'uncensored']);
  140. expect(array.censored()).toEqual(['censored', 'censored']);
  141. });
  142. it('sanitises all items recursively', () => {
  143. const array = new ConfidentialArray([
  144. new TestConfidential(),
  145. new ConfidentialArray([new TestConfidential()]),
  146. ]);
  147. expect(array.uncensored).toEqual(['uncensored', ['uncensored']]);
  148. expect(array.censored()).toEqual(['censored', ['censored']]);
  149. });
  150. });
  151. describe('ConfidentialObjectValues', () => {
  152. it('subclass of BaseConfidential', () => {
  153. expect(ConfidentialObjectValues.prototype instanceof BaseConfidential).toBeTruthy();
  154. });
  155. it('returns underlying object directly when unveiling', () => {
  156. const object = {};
  157. const confidential = new ConfidentialObjectValues(object);
  158. expect(confidential.uncensored).toBe(object);
  159. });
  160. it('sanitises all object values', () => {
  161. const object = {
  162. boolean: false,
  163. object: {
  164. foo: 'bar',
  165. },
  166. };
  167. const confidential = new ConfidentialObjectValues(object);
  168. expect(confidential.uncensored).toBe(object);
  169. expect(confidential.censored()).toEqual({
  170. boolean: '[Boolean]',
  171. object: {
  172. foo: '[String: length=3]',
  173. },
  174. });
  175. });
  176. });
  177. describe('ConfidentialWireMessage', () => {
  178. it('subclass of BaseConfidential', () => {
  179. expect(ConfidentialWireMessage.prototype instanceof BaseConfidential).toBeTruthy();
  180. });
  181. it('returns underlying message directly when unveiling', () => {
  182. const message = {
  183. type: 'request/food',
  184. subType: 'dessert',
  185. };
  186. const confidential = new ConfidentialWireMessage(message);
  187. expect(confidential.uncensored).toBe(message);
  188. });
  189. it("handles 'args' and 'data' being undefined", () => {
  190. const message = {
  191. type: 'request/food',
  192. subType: 'dessert',
  193. };
  194. const confidential = new ConfidentialWireMessage(message);
  195. expect(confidential.censored()).toEqual(message);
  196. });
  197. it("sanitises 'args' and 'data' fields", () => {
  198. const message = {
  199. type: 'request/food',
  200. subType: 'dessert',
  201. args: 'arrrrrrgggsss',
  202. data: {
  203. preference: ['ice cream', 'chocolate'],
  204. priority: Number.POSITIVE_INFINITY,
  205. },
  206. };
  207. const confidential = new ConfidentialWireMessage(message);
  208. expect(confidential.censored()).toEqual({
  209. type: 'request/food',
  210. subType: 'dessert',
  211. args: '[String: length=13]',
  212. data: {
  213. preference: ['[String: length=9]', '[String: length=9]'],
  214. priority: '[Number]',
  215. },
  216. });
  217. });
  218. });
  219. describe('ConfidentialIceCandidate', () => {
  220. it('subclass of BaseConfidential', () => {
  221. expect(ConfidentialIceCandidate.prototype instanceof BaseConfidential).toBeTruthy();
  222. });
  223. it('returns underlying ICE candidate directly when unveiling', () => {
  224. const input = 'cannot be bothered to use valid SDP here';
  225. const confidential = new ConfidentialIceCandidate(input);
  226. expect(confidential.uncensored).toBe(input);
  227. });
  228. it('returns underlying ICE candidate directly if it cannot be parsed', () => {
  229. const input = 'certainly invalid';
  230. const confidential = new ConfidentialIceCandidate(input);
  231. expect(confidential.censored()).toBe(input);
  232. });
  233. it('does not censor mDNS concealed candidates', () => {
  234. const input = 'candidate:1 1 UDP 1234 aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.local 1337 typ host';
  235. const confidential = new ConfidentialIceCandidate(input);
  236. expect(confidential.censored()).toBe(input);
  237. });
  238. it('censors host candidates', () => {
  239. // IPv4
  240. let input = 'candidate:1 1 UDP 1234 192.168.0.42 1337 typ host';
  241. let expected = 'candidate:1 1 UDP 1234 192.168.*.* 1337 typ host';
  242. let confidential = new ConfidentialIceCandidate(input);
  243. expect(confidential.censored()).toBe(expected);
  244. // IPv6
  245. input = 'candidate:1 1 UDP 1234 fe80::1 1337 typ host';
  246. expected = 'candidate:1 1 UDP 1234 fe80::* 1337 typ host';
  247. confidential = new ConfidentialIceCandidate(input);
  248. expect(confidential.censored()).toBe(expected);
  249. });
  250. it('censors srflx candidates', () => {
  251. const input = 'candidate:1 1 UDP 1234 1.2.3.4 42 typ srflx raddr 192.168.0.42 rport 1337';
  252. const expected = 'candidate:1 1 UDP 1234 1.2.*.* 42 typ srflx raddr 192.168.*.* rport 1337';
  253. const confidential = new ConfidentialIceCandidate(input);
  254. expect(confidential.censored()).toBe(expected);
  255. });
  256. it('censors relay candidates', () => {
  257. // IPv4
  258. let input = 'candidate:1 1 UDP 1234 1.2.3.4 42 typ relay raddr 192.168.0.42 rport 1337';
  259. let expected = 'candidate:1 1 UDP 1234 1.2.3.4 42 typ relay raddr 192.168.*.* rport 1337';
  260. let confidential = new ConfidentialIceCandidate(input);
  261. expect(confidential.censored()).toBe(expected);
  262. // IPv6
  263. input = 'candidate:1 1 UDP 1234 2a02:1:2::3 42 typ relay raddr 2a02:dead:beef::1 rport 1337';
  264. expected = 'candidate:1 1 UDP 1234 2a02:1:2::3 42 typ relay raddr 2a02:*:*::* rport 1337';
  265. confidential = new ConfidentialIceCandidate(input);
  266. expect(confidential.censored()).toBe(expected);
  267. });
  268. });
  269. });