Explorar el Código

Replace js-sha256 with window.crypto.subtle implementation

This commit also introduces support for TypeScript async/await syntax 🎉
Danilo Bargen hace 7 años
padre
commit
72cb6af1f3

+ 0 - 30
LICENSE-3RD-PARTY.txt

@@ -613,36 +613,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
 
 
 
-----------
-License for js-sha256
-----------
-
-Copyright (c) 2014-2016 Chen, Yi-Cyuan
-
-MIT License
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-
-
-
 ----------
 License for messageformat
 ----------

+ 0 - 1
dist/package.sh

@@ -56,7 +56,6 @@ targets=(
     msgpack-lite/dist/msgpack.min.js
     tweetnacl/nacl-fast.min.js
     file-saver/FileSaver.min.js
-    js-sha256/build/sha256.min.js
     @saltyrtc/chunked-dc/dist/chunked-dc.es5.js
     @saltyrtc/client/dist/saltyrtc-client.es5.js
     @saltyrtc/task-webrtc/dist/saltyrtc-task-webrtc.es5.js

+ 0 - 1
gather-licenses.sh

@@ -24,7 +24,6 @@ LICENSE_FILES=(
     'EmojiOne JS' '.licenses/emojione-js'
     'EmojiOne Artwork' '.licenses/emojione-artwork'
     'file-saver' 'node_modules/file-saver/LICENSE.md'
-    'js-sha256' 'node_modules/js-sha256/LICENSE.txt'
     'messageformat' 'node_modules/messageformat/LICENSE'
     'msgpack-lite' 'node_modules/msgpack-lite/LICENSE'
     'node-sass' 'node_modules/node-sass/LICENSE'

+ 0 - 1
index.html

@@ -129,7 +129,6 @@
     <script src="node_modules/msgpack-lite/dist/msgpack.min.js?v=[[VERSION]]"></script>
     <script src="node_modules/tweetnacl/nacl-fast.min.js?v=[[VERSION]]"></script>
     <script src="node_modules/file-saver/FileSaver.min.js?v=[[VERSION]]"></script>
-    <script src="node_modules/js-sha256/build/sha256.min.js?v=[[VERSION]]"></script>
     <script src="node_modules/@saltyrtc/chunked-dc/dist/chunked-dc.es5.js?v=[[VERSION]]"></script>
     <script src="node_modules/@saltyrtc/client/dist/saltyrtc-client.es5.js?v=[[VERSION]]"></script>
     <script src="node_modules/@saltyrtc/task-webrtc/dist/saltyrtc-task-webrtc.es5.js?v=[[VERSION]]"></script>

+ 1 - 0
karma.conf.js

@@ -11,6 +11,7 @@ module.exports = function(config) {
             'node_modules/angular-material/angular-material.min.js',
             'node_modules/@saltyrtc/chunked-dc/dist/chunked-dc.es5.js',
             'node_modules/autolinker/dist/Autolinker.min.js',
+            'node_modules/regenerator-runtime/runtime.js',
             'dist/app.js',
             'dist/ts-tests.js',
             'tests/filters.js',

+ 0 - 5
package-lock.json

@@ -5191,11 +5191,6 @@
       "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz",
       "integrity": "sha512-aUnNwqMOXw3yvErjMPSQu6qIIzUmT1e5KcU1OZxRDU1g/am6mzBvcrmLAYwzmB59BHPrh5/tKaiF4OPhqRWESQ=="
     },
-    "js-sha256": {
-      "version": "0.3.2",
-      "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.3.2.tgz",
-      "integrity": "sha1-2wlZJFUWQZHOgXGkiIzZV8Z78O0="
-    },
     "js-tokens": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",

+ 0 - 1
package.json

@@ -56,7 +56,6 @@
     "browserify-header": "^0.9.4",
     "croppie": "~2.6.0",
     "file-saver": "^1.3.8",
-    "js-sha256": "~0.3.2",
     "messageformat": "~2",
     "msgpack-lite": "~0.1.26",
     "node-sass": "^4.9.2",

+ 7 - 0
src/helpers.ts

@@ -331,3 +331,10 @@ export function logAdapter(logFunc: (...msg: string[]) => void, logTag: string):
 export function hasValue(val: any): boolean {
     return val !== null && val !== undefined;
 }
+
+/**
+ * Awaitable timeout function.
+ */
+export function sleep(ms: number): Promise<void> {
+    return new Promise((resolve) => setTimeout(resolve, ms));
+}

+ 36 - 0
src/helpers/crypto.ts

@@ -0,0 +1,36 @@
+/**
+ * This file is part of Threema Web.
+ *
+ * Threema Web is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// This file contains helper functions related to crypto.
+// Try to keep all functions pure!
+
+import {u8aToHex} from '../helpers';
+
+/**
+ * Calculate the SHA256 hash of the specified bytes.
+ * Throw an Error if the SubtleCrypto API is not available.
+ */
+export async function sha256(bytes: ArrayBuffer): Promise<string> {
+    if (window.crypto === undefined) {
+        throw new Error('window.crypto API not available');
+    }
+    if (window.crypto.subtle === undefined) {
+        throw new Error('window.subtle API not available');
+    }
+    const buf: ArrayBuffer = await crypto.subtle.digest('SHA-256', bytes);
+    return u8aToHex(new Uint8Array(buf));
+}

+ 1 - 1
src/partials/messenger.receiver/contact.html

@@ -25,7 +25,7 @@
 				</dd>
 
 				<dt><span translate>messenger.KEY_FINGERPRINT</span></dt>
-				<dd>{{ ctrl.fingerPrint }}</dd>
+				<dd>{{ ctrl.fingerPrint.value || "Loading..." }}</dd>
 
 				<dt><span translate>messenger.NICKNAME</span></dt>
 				<dd ng-if="ctrl.receiver.publicNickname" ng-bind-html="ctrl.receiver.publicNickname | emojify"></dd>

+ 1 - 1
src/partials/messenger.receiver/me.html

@@ -14,7 +14,7 @@
 					</eee-verification-level></dd>
 
 				<dt><span translate>messenger.KEY_FINGERPRINT</span></dt>
-				<dd>{{ ctrl.fingerPrint }}</dd>
+				<dd>{{ ctrl.fingerPrint.value || "Loading..." }}</dd>
 
 				<dt><span translate>messenger.MY_PUBLIC_NICKNAME</span></dt>
 				<dd>{{ ctrl.controllerModel.nickname || "-" }}</dd>

+ 26 - 6
src/partials/messenger.ts

@@ -1152,6 +1152,7 @@ class ReceiverDetailController {
 
     // Angular services
     private $mdDialog: any;
+    private $scope: ng.IScope;
     private $state: UiStateService;
 
     // Own services
@@ -1162,7 +1163,7 @@ class ReceiverDetailController {
     public receiver: threema.Receiver;
     public me: threema.MeReceiver;
     public title: string;
-    public fingerPrint?: string;
+    public fingerPrint = { value: null };  // Object, so that data binding works
     private showGroups = false;
     private showDistributionLists = false;
     private inGroups: threema.GroupReceiver[] = [];
@@ -1175,15 +1176,16 @@ class ReceiverDetailController {
     private controllerModel: threema.ControllerModel<threema.Receiver>;
 
     public static $inject = [
-        '$log', '$stateParams', '$state', '$mdDialog', '$translate',
+        '$scope', '$log', '$stateParams', '$state', '$mdDialog', '$translate',
         'WebClientService', 'FingerPrintService', 'ContactService', 'ControllerModelService',
     ];
-    constructor($log: ng.ILogService, $stateParams, $state: UiStateService,
+    constructor($scope: ng.IScope, $log: ng.ILogService, $stateParams, $state: UiStateService,
                 $mdDialog: ng.material.IDialogService, $translate: ng.translate.ITranslateService,
                 webClientService: WebClientService, fingerPrintService: FingerPrintService,
                 contactService: ContactService, controllerModelService: ControllerModelService) {
 
         this.$mdDialog = $mdDialog;
+        this.$scope = $scope;
         this.$state = $state;
         this.fingerPrintService = fingerPrintService;
         this.contactService = contactService;
@@ -1206,7 +1208,10 @@ class ReceiverDetailController {
                 });
 
             this.isWorkReceiver = contactReceiver.identityType === threema.IdentityType.Work;
-            this.fingerPrint = this.fingerPrintService.generate(contactReceiver.publicKey);
+
+            this.fingerPrintService
+                .generate(contactReceiver.publicKey)
+                .then(this.setFingerPrint.bind(this));
 
             webClientService.groups.forEach((groupReceiver: threema.GroupReceiver) => {
                 // check if my identity is a member
@@ -1232,12 +1237,16 @@ class ReceiverDetailController {
         switch (this.receiver.type) {
             case 'me':
                 const meReceiver = this.receiver as threema.MeReceiver;
-                this.fingerPrint = this.fingerPrintService.generate(meReceiver.publicKey);
+                this.fingerPrintService
+                    .generate(meReceiver.publicKey)
+                    .then(this.setFingerPrint.bind(this));
                 this.controllerModel = controllerModelService.me(meReceiver, ControllerModelMode.VIEW);
                 break;
             case 'contact':
                 const contactReceiver = this.receiver as threema.ContactReceiver;
-                this.fingerPrint = this.fingerPrintService.generate(contactReceiver.publicKey);
+                this.fingerPrintService
+                    .generate(contactReceiver.publicKey)
+                    .then(this.setFingerPrint.bind(this));
                 this.controllerModel = controllerModelService
                     .contact(contactReceiver, ControllerModelMode.VIEW);
                 break;
@@ -1264,6 +1273,17 @@ class ReceiverDetailController {
 
     }
 
+    /**
+     * Set the fingerprint value and run $digest.
+     *
+     * This may only be called from outside the $digest loop
+     * (e.g. from a plain promise callback).
+     */
+    private setFingerPrint(fingerPrint: string): void {
+        this.fingerPrint.value = fingerPrint;
+        this.$scope.$digest();
+    }
+
     public chat(): void {
         this.$state.go('messenger.home.conversation', {
             type: this.receiver.type,

+ 4 - 2
src/services/fingerprint.ts

@@ -15,6 +15,8 @@
  * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  */
 
+import {sha256} from '../helpers/crypto';
+
 export class FingerPrintService {
     private $log: ng.ILogService;
 
@@ -23,9 +25,9 @@ export class FingerPrintService {
         this.$log = $log;
     }
 
-    public generate(publicKey: ArrayBuffer): string {
+    public async generate(publicKey: ArrayBuffer): Promise<string> {
         if (publicKey !== undefined && publicKey.byteLength === 32) {
-            const sha256PublicKey = sha256(publicKey);
+            const sha256PublicKey = await sha256(publicKey);
             if (sha256PublicKey !== undefined) {
                 return sha256PublicKey.toLowerCase().substr(0, 32);
             }

+ 11 - 6
src/services/push.ts

@@ -15,6 +15,8 @@
  * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  */
 
+import {sha256} from '../helpers/crypto';
+
 export class PushService {
     private static ARG_TYPE = 'type';
     private static ARG_TOKEN = 'token';
@@ -72,15 +74,18 @@ export class PushService {
      * Send a push notification for the specified session (public permanent key
      * of the initiator). The promise is always resolved to a boolean.
      */
-    public sendPush(session: Uint8Array, wakeupType: threema.WakeupType): Promise<boolean> {
+    public async sendPush(session: Uint8Array, wakeupType: threema.WakeupType): Promise<boolean> {
         if (!this.isAvailable()) {
-            return Promise.resolve(false);
+            return false;
         }
 
+        // Calculate session hash
+        const sessionHash = await sha256(session.buffer);
+
         // Prepare request
         const data = {
             [PushService.ARG_TYPE]: this.pushType,
-            [PushService.ARG_SESSION]: sha256(session),
+            [PushService.ARG_SESSION]: sessionHash,
             [PushService.ARG_VERSION]: this.version,
             [PushService.ARG_WAKEUP_TYPE]: wakeupType,
         };
@@ -89,7 +94,7 @@ export class PushService {
             const parts = this.pushToken.split(';');
             if (parts.length < 3) {
                 this.$log.warn(this.logTag, 'APNS push token contains', parts.length, 'parts, at least 3 are required');
-                return Promise.resolve(false);
+                return false;
             }
             data[PushService.ARG_TOKEN] = parts[0];
             data[PushService.ARG_ENDPOINT] = parts[1];
@@ -98,7 +103,7 @@ export class PushService {
             data[PushService.ARG_TOKEN] = this.pushToken;
         } else {
             this.$log.warn(this.logTag, 'Invalid push type');
-            return Promise.resolve(false);
+            return false;
         }
 
         const request = {
@@ -127,6 +132,6 @@ export class PushService {
                     resolve(false);
                 },
             );
-        });
+        }) as Promise<boolean>;
     }
 }

+ 2 - 0
tests/testsuite.html

@@ -7,6 +7,8 @@
 
         <link rel="stylesheet" href="../node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
 
+        <script src="../node_modules/regenerator-runtime/runtime.js"></script>
+
         <script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
         <script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
         <script src="../node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>

+ 13 - 2
src/types/js-sha256.d.ts → tests/ts/crypto_helpers.ts

@@ -1,4 +1,6 @@
 /**
+ * Copyright © 2016-2018 Threema GmbH (https://threema.ch/).
+ *
  * This file is part of Threema Web.
  *
  * Threema Web is free software: you can redistribute it and/or modify it
@@ -15,5 +17,14 @@
  * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  */
 
-declare function sha224(val: string | Array<any> | Uint8Array | ArrayBuffer): string;
-declare function sha256(val: string | Array<any> | Uint8Array | ArrayBuffer): string;
+import {sha256} from '../../src/helpers/crypto';
+
+describe('Crypto Helpers', () => {
+    it('sha256', function(done) {
+        const arr = Uint8Array.of(1, 2, 4, 8, 254, 255);
+        sha256(arr.buffer).then((hash) => {
+            expect(hash).toEqual('54caf7192551d011c9018e6e00b0f2d13f71784277d581fc5146182cb8af4181');
+            done();
+        });
+    });
+});

+ 1 - 0
tests/ts/main.ts

@@ -21,5 +21,6 @@
 /// <reference path="../../src/threema.d.ts" />
 
 import './containers';
+import './crypto_helpers';
 import './helpers';
 import './receiver_helpers';