welcome.ts 15 KB

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