welcome.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. /**
  2. * This file is part of Threema Web.
  3. *
  4. * Threema Web is free software: you can redistribute it and/or modify it
  5. * under the terms of the GNU Affero General Public License as published by
  6. * the Free Software Foundation, either version 3 of the License, or (at
  7. * your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful, but
  10. * WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
  12. * General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU Affero General Public License
  15. * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17. import {BrowserService} from '../services/browser';
  18. import {ControllerService} from '../services/controller';
  19. import {TrustedKeyStoreService} from '../services/keystore';
  20. import {PushService} from '../services/push';
  21. import {StateService} from '../services/state';
  22. import {WebClientService} from '../services/webclient';
  23. class DialogController {
  24. // TODO: This is also used in partials/messenger.ts. We could somehow
  25. // extract it into a separate file.
  26. public static $inject = ['$mdDialog'];
  27. public $mdDialog;
  28. constructor($mdDialog) {
  29. this.$mdDialog = $mdDialog;
  30. }
  31. public cancel() {
  32. this.$mdDialog.cancel();
  33. }
  34. }
  35. class WelcomeController {
  36. private static REDIRECT_DELAY = 500;
  37. // Angular services
  38. private $scope: ng.IScope;
  39. private $state: ng.ui.IStateService;
  40. private $timeout: ng.ITimeoutService;
  41. private $interval: ng.IIntervalService;
  42. private $log: ng.ILogService;
  43. private $window: ng.IWindowService;
  44. // Material design services
  45. private $mdDialog: ng.material.IDialogService;
  46. private $translate: ng.translate.ITranslateService;
  47. // Custom services
  48. private webClientService: WebClientService;
  49. private TrustedKeyStore: TrustedKeyStoreService;
  50. private pushService: PushService;
  51. private stateService: StateService;
  52. // Other
  53. public name = 'welcome';
  54. private mode: 'scan' | 'unlock';
  55. private qrCode;
  56. private password: string = '';
  57. public static $inject = [
  58. '$scope', '$state', '$stateParams', '$timeout', '$interval', '$log', '$window', '$mdDialog', '$translate',
  59. 'WebClientService', 'TrustedKeyStore', 'StateService', 'PushService', 'BrowserService',
  60. 'BROWSER_MIN_VERSIONS', 'ControllerService',
  61. ];
  62. constructor($scope: ng.IScope, $state: ng.ui.IStateService, $stateParams: threema.WelcomeStateParams,
  63. $timeout: ng.ITimeoutService, $interval: ng.IIntervalService,
  64. $log: ng.ILogService, $window: ng.IWindowService, $mdDialog: ng.material.IDialogService,
  65. $translate: ng.translate.ITranslateService,
  66. webClientService: WebClientService, TrustedKeyStore: TrustedKeyStoreService,
  67. stateService: StateService, pushService: PushService,
  68. browserService: BrowserService,
  69. minVersions: threema.BrowserMinVersions,
  70. controllerService: ControllerService) {
  71. controllerService.setControllerName('welcome');
  72. // Angular services
  73. this.$scope = $scope;
  74. this.$state = $state;
  75. this.$timeout = $timeout;
  76. this.$interval = $interval;
  77. this.$log = $log;
  78. this.$window = $window;
  79. this.$mdDialog = $mdDialog;
  80. this.$translate = $translate;
  81. // Own services
  82. this.webClientService = webClientService;
  83. this.TrustedKeyStore = TrustedKeyStore;
  84. this.stateService = stateService;
  85. this.pushService = pushService;
  86. // Determine whether browser warning should be shown
  87. const browser = browserService.getBrowser();
  88. const version = parseFloat(browser.version);
  89. $log.debug('Detected browser:', browser.textInfo);
  90. if (isNaN(version)) {
  91. $log.warn('Could not determine browser version');
  92. this.showBrowserWarning();
  93. } else if (browser.chrome === true) {
  94. if (version < minVersions.CHROME) {
  95. $log.warn('Chrome is too old (' + version + ' < ' + minVersions.CHROME + ')');
  96. this.showBrowserWarning();
  97. }
  98. } else if (browser.firefox === true) {
  99. if (version < minVersions.FF) {
  100. $log.warn('Firefox is too old (' + version + ' < ' + minVersions.FF + ')');
  101. this.showBrowserWarning();
  102. }
  103. } else if (browser.opera === true) {
  104. if (version < minVersions.OPERA) {
  105. $log.warn('Opera is too old (' + version + ' < ' + minVersions.OPERA + ')');
  106. this.showBrowserWarning();
  107. }
  108. } else {
  109. $log.warn('Non-supported browser, please use Chrome, Firefox or Opera');
  110. this.showBrowserWarning();
  111. }
  112. // Determine whether local storage is available
  113. if (this.TrustedKeyStore.blocked === true) {
  114. $log.error('Cannot access local storage. Is it being blocked by a browser add-on?');
  115. this.showLocalStorageWarning();
  116. }
  117. // Clear cache
  118. this.webClientService.clearCache();
  119. // Determine connection mode
  120. if ($stateParams.initParams !== null) {
  121. this.mode = 'unlock';
  122. const keyStore = $stateParams.initParams.keyStore;
  123. const peerTrustedKey = $stateParams.initParams.peerTrustedKey;
  124. this.reconnect(keyStore, peerTrustedKey);
  125. } else if (TrustedKeyStore.hasTrustedKey()) {
  126. this.mode = 'unlock';
  127. this.unlock();
  128. } else {
  129. this.mode = 'scan';
  130. this.scan();
  131. }
  132. }
  133. /**
  134. * Whether or not to show the loading indicator.
  135. */
  136. public get showLoadingIndicator(): boolean {
  137. switch (this.stateService.connectionBuildupState) {
  138. case 'push':
  139. case 'peer_handshake':
  140. case 'loading':
  141. case 'done':
  142. return true;
  143. default:
  144. return false;
  145. }
  146. }
  147. /**
  148. * Getter for connection buildup state.
  149. *
  150. * Only to be used by the template.
  151. */
  152. public get state(): threema.ConnectionBuildupState {
  153. return this.stateService.connectionBuildupState;
  154. }
  155. /**
  156. * Getter for connection buildup progress.
  157. *
  158. * Only to be used by the template.
  159. */
  160. public get progress(): number {
  161. return this.stateService.progress;
  162. }
  163. /**
  164. * Getter for slow connect status.
  165. *
  166. * Only to be used by the template.
  167. */
  168. public get slowConnect(): boolean {
  169. return this.stateService.slowConnect;
  170. }
  171. /**
  172. * Initiate a new session by scanning a new QR code.
  173. */
  174. private scan(): void {
  175. this.$log.info('Initialize session by scanning QR code...');
  176. // Initialize webclient with new keystore
  177. this.webClientService.init();
  178. // Initialize QR code params
  179. this.$scope.$watch(() => this.password, () => {
  180. const payload = this.webClientService.buildQrCodePayload(this.password.length > 0);
  181. this.qrCode = this.buildQrCode(payload);
  182. });
  183. // Start webclient
  184. this.start();
  185. }
  186. /**
  187. * Initiate a new session by unlocking a trusted key.
  188. */
  189. private unlock(): void {
  190. this.$log.info('Initialize session by unlocking trusted key...');
  191. }
  192. /**
  193. * Decrypt the keys and initiate the session.
  194. */
  195. private unlockConfirm(): void {
  196. const decrypted: threema.TrustedKeyStoreData = this.TrustedKeyStore.retrieveTrustedKey(this.password);
  197. if (decrypted === null) {
  198. return this.showDecryptionFailed();
  199. }
  200. // Instantiate new keystore
  201. const keyStore = new saltyrtcClient.KeyStore(decrypted.ownSecretKey);
  202. // Initialize push service
  203. if (decrypted.pushToken !== null) {
  204. this.pushService.init(decrypted.pushToken);
  205. this.$log.debug('Initialize push service');
  206. }
  207. // Reconnect
  208. this.reconnect(keyStore, decrypted.peerPublicKey);
  209. }
  210. /**
  211. * Reconnect using a specific keypair and peer public key.
  212. */
  213. private reconnect(keyStore: saltyrtc.KeyStore, peerTrustedKey: Uint8Array): void {
  214. this.webClientService.init(keyStore, peerTrustedKey);
  215. this.start();
  216. }
  217. /**
  218. * Show a browser warning dialog.
  219. */
  220. private showBrowserWarning(): void {
  221. this.$translate.onReady().then(() => {
  222. const confirm = this.$mdDialog.confirm()
  223. .title(this.$translate.instant('welcome.BROWSER_NOT_SUPPORTED'))
  224. .htmlContent(this.$translate.instant('welcome.BROWSER_NOT_SUPPORTED_DETAILS'))
  225. .ok(this.$translate.instant('welcome.CONTINUE_ANYWAY'))
  226. .cancel(this.$translate.instant('welcome.ABORT'));
  227. this.$mdDialog.show(confirm).then(() => {
  228. // do nothing
  229. }, () => {
  230. // Redirect to Threema website
  231. window.location.replace('https://threema.ch/');
  232. });
  233. });
  234. }
  235. /**
  236. * Show a dialog indicating that local storage is not available.
  237. */
  238. private showLocalStorageWarning(): void {
  239. this.$translate.onReady().then(() => {
  240. const confirm = this.$mdDialog.alert()
  241. .title(this.$translate.instant('common.ERROR'))
  242. .htmlContent(this.$translate.instant('welcome.LOCAL_STORAGE_MISSING_DETAILS'))
  243. .ok(this.$translate.instant('common.OK'));
  244. this.$mdDialog.show(confirm);
  245. });
  246. }
  247. /**
  248. * Show the "decryption failed" dialog.
  249. */
  250. private showDecryptionFailed(): void {
  251. this.$mdDialog.show({
  252. controller: DialogController,
  253. controllerAs: 'ctrl',
  254. templateUrl: 'partials/dialog.unlockfailed.html',
  255. parent: angular.element(document.body),
  256. clickOutsideToClose: true,
  257. fullscreen: true,
  258. });
  259. }
  260. /**
  261. * Forget trusted keys.
  262. */
  263. private deleteSession(ev) {
  264. const confirm = this.$mdDialog.confirm()
  265. .title(this.$translate.instant('common.SESSION_DELETE'))
  266. .textContent(this.$translate.instant('common.CONFIRM_DELETE_BODY'))
  267. .targetEvent(ev)
  268. .ok(this.$translate.instant('common.YES'))
  269. .cancel(this.$translate.instant('common.CANCEL'));
  270. this.$mdDialog.show(confirm).then(() => {
  271. // Force-stop the webclient
  272. const deleteStoredData = true;
  273. const resetPush = true;
  274. const redirect = false;
  275. this.webClientService.stop(true, deleteStoredData, resetPush, redirect);
  276. // Reset state
  277. this.stateService.updateConnectionBuildupState('new');
  278. // Go back to scan mode
  279. this.mode = 'scan';
  280. this.password = '';
  281. // Initiate scan
  282. this.scan();
  283. }, () => {
  284. // do nothing
  285. });
  286. }
  287. private buildQrCode(payload: string) {
  288. // To calculate version and error correction, refer to this table:
  289. // http://www.thonky.com/qr-code-tutorial/character-capacities
  290. // The qr generator uses byte mode, therefore for 92 characters with
  291. // error correction level 'M' we need version 6.
  292. const len = payload.length;
  293. let version: number;
  294. if (len <= 134) {
  295. version = 6;
  296. } else if (len <= 154) {
  297. version = 7;
  298. } else if (len <= 192) {
  299. version = 8;
  300. } else if (len <= 230) {
  301. version = 9;
  302. } else if (len <= 271) {
  303. version = 10;
  304. } else if (len <= 321) {
  305. version = 11;
  306. } else if (len <= 367) {
  307. version = 12;
  308. } else if (len <= 425) {
  309. version = 13;
  310. } else if (len <= 458) {
  311. version = 14;
  312. } else if (len <= 520) {
  313. version = 15;
  314. } else if (len <= 586) {
  315. version = 16;
  316. } else {
  317. this.$log.error('QR Code payload too large: Is your SaltyRTC host string huge?');
  318. version = 40;
  319. }
  320. return {
  321. version: version,
  322. errorCorrectionLevel: 'L',
  323. size: 384,
  324. data: payload,
  325. };
  326. }
  327. /**
  328. * Actually start the webclient.
  329. *
  330. * It must be initialized before calling this method.
  331. */
  332. private start() {
  333. this.webClientService.start().then(
  334. // If connection buildup is done...
  335. () => {
  336. // Pass password to webclient service
  337. this.webClientService.setPassword(this.password);
  338. // Clear local password variable
  339. this.password = '';
  340. // Redirect to home
  341. this.$timeout(() => this.$state.go('messenger.home'), WelcomeController.REDIRECT_DELAY);
  342. },
  343. // If an error occurs...
  344. (error) => {
  345. this.$log.error('Error state:', error);
  346. // TODO: should probably show an error message instead
  347. this.$timeout(() => this.$state.reload(), WelcomeController.REDIRECT_DELAY);
  348. },
  349. // State updates
  350. (progress: threema.ConnectionBuildupStateChange) => {
  351. // Do nothing
  352. },
  353. );
  354. }
  355. /**
  356. * Reload the page.
  357. */
  358. public reload() {
  359. this.$window.location.reload();
  360. }
  361. }
  362. angular.module('3ema.welcome', [])
  363. .config(['$stateProvider', ($stateProvider: ng.ui.IStateProvider) => {
  364. $stateProvider
  365. .state('welcome', {
  366. url: '/welcome',
  367. templateUrl: 'partials/welcome.html',
  368. controller: 'WelcomeController',
  369. controllerAs: 'ctrl',
  370. params: {initParams: null},
  371. });
  372. }])
  373. .controller('WelcomeController', WelcomeController);