├── .gitmodules
├── .nvmrc
├── docs
├── .gitignore
├── requirements.txt
├── docs
│ ├── img
│ │ └── favicon.ico
│ ├── about.md
│ ├── installing.md
│ ├── index.md
│ └── usage.md
├── mkdocs.yml
└── .travis.yml
├── rollup
├── testing.js
├── performance.js
├── es5.min.js
├── es5.js
└── es2015.js
├── .gitignore
├── src
├── main.es5.ts
├── signaling.ts
├── signaling
│ ├── helpers.ts
│ ├── handoverstate.ts
│ └── responder.ts
├── main.ts
├── log.ts
├── closecode.ts
├── exceptions.ts
├── csn.ts
├── nonce.ts
├── eventregistry.ts
├── cookie.ts
├── peers.ts
├── utils.ts
└── keystore.ts
├── .editorconfig
├── tsconfig.json
├── tests
├── performance
│ ├── utils.ts
│ ├── crypto.worker.js
│ └── crypto.spec.ts
├── performance.ts
├── config.ts
├── testsuite.html
├── performance.html
├── handoverstate.spec.ts
├── main.ts
├── nonce.spec.ts
├── cookie.spec.ts
├── csn.spec.ts
├── testtasks.ts
├── utils.ts
├── eventregistry.spec.ts
├── utils.spec.ts
├── keystore.spec.ts
├── client.spec.ts
└── jasmine.d.ts
├── RELEASING.md
├── karma.conf.js
├── tslint.json
├── CONTRIBUTING.md
├── LICENSE.md
├── .circleci
└── config.yml
├── package.json
├── README.md
├── CHANGELOG.md
└── saltyrtc-client.d.ts
/.gitmodules:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 16
2 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | site/
3 | venv/
4 | VENV/
5 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | mkdocs>=1.2.3,<1.3
2 | Pygments>=2.11.2,2.12
3 |
--------------------------------------------------------------------------------
/docs/docs/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saltyrtc/saltyrtc-client-js/HEAD/docs/docs/img/favicon.ico
--------------------------------------------------------------------------------
/docs/docs/about.md:
--------------------------------------------------------------------------------
1 | # About SaltyRTC
2 |
3 | For more information about the project, please visit
4 | [saltyrtc.org](http://saltyrtc.org).
5 |
--------------------------------------------------------------------------------
/rollup/testing.js:
--------------------------------------------------------------------------------
1 | import config from './es5.js';
2 |
3 | config.input = 'tests/main.ts';
4 | config.output.file = 'tests/testsuite.js';
5 | config.output.sourcemap = true;
6 |
7 | export default config;
8 |
--------------------------------------------------------------------------------
/rollup/performance.js:
--------------------------------------------------------------------------------
1 | import config from './es5.js';
2 |
3 | config.input = 'tests/performance.ts';
4 | config.output.file = 'tests/performance.js';
5 | config.output.sourcemap = true;
6 |
7 | export default config;
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | node_modules/
3 | npm-debug.log
4 | tests/testsuite.js*
5 | tests/performance.js*
6 | apidocs/
7 | saltyrtc.crt
8 | saltyrtc.csr
9 | saltyrtc.key
10 | saltyrtc-server-python/
11 | .idea/
12 | .rgignore
13 |
--------------------------------------------------------------------------------
/src/main.es5.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2016-2022 Threema GmbH
3 | *
4 | * This software may be modified and distributed under the terms
5 | * of the MIT license. See the `LICENSE.md` file for details.
6 | */
7 |
8 | export * from './main';
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | end_of_line = lf
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 | charset = utf-8
10 |
11 | [*.{js,ts,json,scss,sh}]
12 | indent_style = space
13 | indent_size = 4
14 |
15 | [*.yml]
16 | indent_style = space
17 | indent_size = 2
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2015",
4 | "module": "es2015",
5 | "moduleResolution": "node",
6 | "removeComments": true,
7 | "noImplicitAny": true,
8 | "isolatedModules": true
9 | },
10 | "exclude": [
11 | "node_modules",
12 | "dist",
13 | "example.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/tests/performance/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Test utils.
3 | *
4 | * Copyright (C) 2022 Threema GmbH
5 | *
6 | * This software may be modified and distributed under the terms
7 | * of the MIT license. See the `LICENSE.md` file for details.
8 | */
9 |
10 | export const testData = {
11 | bytes: new Uint8Array(2 ** 16).fill(0xee),
12 | nonce: new Uint8Array(24).fill(0xdd),
13 | };
14 |
--------------------------------------------------------------------------------
/src/signaling.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2016-2022 Threema GmbH
3 | *
4 | * This software may be modified and distributed under the terms
5 | * of the MIT license. See the `LICENSE.md` file for details.
6 | */
7 |
8 | export { Signaling } from './signaling/common';
9 | export { InitiatorSignaling } from './signaling/initiator';
10 | export { ResponderSignaling } from './signaling/responder';
11 |
--------------------------------------------------------------------------------
/src/signaling/helpers.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2016-2022 Threema GmbH
3 | *
4 | * This software may be modified and distributed under the terms
5 | * of the MIT license. See the `LICENSE.md` file for details.
6 | */
7 |
8 | /**
9 | * Return `true` if byte is a valid responder id (in the range 0x02-0xff).
10 | */
11 | export function isResponderId(id: number): boolean {
12 | return id >= 0x02 && id <= 0xff;
13 | }
14 |
--------------------------------------------------------------------------------
/tests/performance.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Performance test entry point.
3 | *
4 | * Copyright (C) 2018-2022 Threema GmbH
5 | *
6 | * This software may be modified and distributed under the terms
7 | * of the MIT license. See the `LICENSE.md` file for details.
8 | */
9 |
10 | import test_crypto from './performance/crypto.spec';
11 |
12 | let counter = 1;
13 | beforeEach(() => console.info('------ TEST', counter++, 'BEGIN ------'));
14 |
15 | test_crypto();
16 |
--------------------------------------------------------------------------------
/docs/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: SaltyRTC Client for JavaScript
2 | site_description: Documentation for saltyrtc-client-js.
3 | site_author: Threema GmbH
4 | copyright: © 2016-2022 Threema GmbH
5 | theme: readthedocs
6 | repo_url: https://github.com/saltyrtc/saltyrtc-client-js
7 | edit_uri: edit/master/docs/docs/
8 | markdown_extensions:
9 | - smarty
10 | - admonition
11 | - toc:
12 | permalink: False
13 | pages:
14 | - Home: index.md
15 | - Guide:
16 | - Installing: installing.md
17 | - Usage: usage.md
18 | - About: about.md
19 |
--------------------------------------------------------------------------------
/rollup/es5.min.js:
--------------------------------------------------------------------------------
1 | import config from './es5.js';
2 | import { terser } from 'rollup-plugin-terser';
3 |
4 | config.output.file = 'dist/saltyrtc-client.es5.min.js';
5 | config.plugins.push(
6 | terser({
7 | format: {
8 | comments: (node, comment) => {
9 | const text = comment.value;
10 | const type = comment.type;
11 | if (type == "comment2") { // multiline comment
12 | return /MIT license/.test(text);
13 | }
14 |
15 | }
16 | }
17 | })
18 | );
19 |
20 | export default config;
21 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | # Releasing
2 |
3 | First of all, have you updated the type declarations?
4 |
5 | Set variables:
6 |
7 | $ export VERSION=X.Y.Z
8 | $ export GPG_KEY=E7ADD9914E260E8B35DFB50665FDE935573ACDA6
9 |
10 | Update version numbers:
11 |
12 | $ vim -p package.json CHANGELOG.md
13 | $ npm install
14 |
15 | Build dist files:
16 |
17 | $ npm run dist
18 |
19 | Commit & tag:
20 |
21 | $ git commit -S${GPG_KEY} -m "Release v${VERSION}"
22 | $ git tag -s -u ${GPG_KEY} v${VERSION} -m "Version ${VERSION}"
23 |
24 | Push & publish:
25 |
26 | $ git push && git push --tags
27 | $ npm publish
28 |
--------------------------------------------------------------------------------
/docs/docs/installing.md:
--------------------------------------------------------------------------------
1 | # Installing
2 |
3 | ## Via NPM
4 |
5 | You can install this library and its peer dependencies via `npm`:
6 |
7 | npm install @saltyrtc/client msgpack-lite tweetnacl
8 |
9 | ## Manually
10 |
11 | Alternatively, copy one of the following files to your project directly:
12 |
13 | - ES2015: `dist/saltyrtc-client.es2015.js`
14 | - ES5: `dist/saltyrtc-client.es5.js`
15 | - ES5 minified: `dist/saltyrtc-client.es5.min.js`
16 |
17 | Make sure to manually add the following external dependencies to your project:
18 |
19 | - [tweetnacl](https://github.com/dchest/tweetnacl-js)
20 | - [msgpack-lite](https://github.com/kawanet/msgpack-lite)
21 |
--------------------------------------------------------------------------------
/tests/config.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Test config.
3 | *
4 | * Copyright (C) 2016-2022 Threema GmbH
5 | *
6 | * This software may be modified and distributed under the terms
7 | * of the MIT license. See the `LICENSE.md` file for details.
8 | */
9 |
10 | export class Config {
11 | // Unit test configuration
12 | public static SALTYRTC_HOST = 'localhost';
13 | public static SALTYRTC_PORT = 8765;
14 | public static SALTYRTC_SERVER_PUBLIC_KEY = '09a59a5fa6b45cb07638a3a6e347ce563a948b756fd22f9527465f7c79c2a864';
15 | public static RUN_LOAD_TESTS = false;
16 |
17 | // Performance test configuration
18 | public static CRYPTO_ITERATIONS = 4096;
19 | }
20 |
--------------------------------------------------------------------------------
/docs/docs/index.md:
--------------------------------------------------------------------------------
1 | # SaltyRTC Client for JavaScript
2 |
3 | This is a SaltyRTC implementation for JavaScript / TypeScript.
4 |
5 | !!! warning
6 |
7 | **Note:** The SaltyRTC client libraries are in maintenance mode.
8 | They will still receive bugfixes and regular maintenance, but if you want to
9 | start using these libraries, be prepared that you will need to take over
10 | maintenance at some point in time. (If you are interested in maintaining the
11 | libraries, please let us know, our e-mails are in the README, section
12 | "Security".)
13 |
14 | **Contents**
15 |
16 | * [Installing](installing.md)
17 | * [Usage](usage.md)
18 | * [About](about.md)
19 |
--------------------------------------------------------------------------------
/rollup/es5.js:
--------------------------------------------------------------------------------
1 | import config from './es2015.js';
2 | import babel from '@rollup/plugin-babel';
3 |
4 | config.output.file = 'dist/saltyrtc-client.es5.js';
5 | config.output.name = 'saltyrtcClient';
6 | config.output.format = 'iife';
7 | config.output.globals = {
8 | 'msgpack-lite': 'msgpack',
9 | 'tweetnacl': 'nacl'
10 | };
11 | config.output.strict = true;
12 | config.plugins.push(
13 | babel({
14 | babelrc: false,
15 | exclude: 'node_modules/**',
16 | presets: [
17 | // Use ES2015 but don't transpile modules since Rollup does that
18 | ['es2015', {modules: false}]
19 | ],
20 | babelHelpers: 'bundled',
21 | })
22 | );
23 |
24 | export default config;
25 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function(config) {
2 |
3 | const configuration = {
4 | frameworks: ['jasmine'],
5 | files: [
6 | 'node_modules/tweetnacl/nacl-fast.min.js',
7 | 'node_modules/msgpack-lite/dist/msgpack.min.js',
8 | 'tests/testsuite.js'
9 | ],
10 | customLaunchers: {
11 | Firefox_circle_ci: {
12 | base: 'Firefox',
13 | profile: '/home/ci/.mozilla/firefox/saltyrtc',
14 | }
15 | }
16 | };
17 |
18 | if (process.env.CIRCLECI) {
19 | configuration.browsers = ['ChromiumHeadless', 'Firefox_circle_ci'];
20 | } else {
21 | configuration.browsers = ['Chromium', 'Firefox'];
22 | }
23 |
24 | config.set(configuration);
25 |
26 | };
27 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Entry point for the library. The full public API should be re-exported here.
3 | *
4 | * Copyright (C) 2016-2022 Threema GmbH
5 | *
6 | * This software may be modified and distributed under the terms
7 | * of the MIT license. See the `LICENSE.md` file for details.
8 | */
9 |
10 | // Exceptions
11 | import * as exceptions from './exceptions';
12 | export { exceptions };
13 |
14 | // Main API
15 | export { SaltyRTCBuilder } from './client';
16 | export { KeyStore } from './keystore';
17 |
18 | // API for tasks
19 | export { Box } from './keystore';
20 | export { Cookie, CookiePair } from './cookie';
21 | export { CombinedSequence, CombinedSequencePair } from './csn';
22 | export { EventRegistry } from './eventregistry';
23 | export { CloseCode, explainCloseCode } from './closecode';
24 | export { SignalingError, ConnectionError } from './exceptions';
25 | export { Log } from './log';
26 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tslint:recommended",
3 | "rules": {
4 | "file-header": [true, "Copyright \\(C\\)"],
5 | "indent": [true, "spaces"],
6 | "interface-name": [true, "never-prefix"],
7 | "max-classes-per-file": false,
8 | "member-ordering": false,
9 | "no-console": [false],
10 | "no-namespace": [true, "allow-declarations"],
11 | "object-literal-shorthand": false,
12 | "object-literal-sort-keys": false,
13 | "only-arrow-functions": false,
14 | "quotemark": [true, "single", "avoid-escape"],
15 | "variable-name": [true, "check-format", "allow-leading-underscore", "ban-keywords"],
16 | "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-module", "check-separator", "check-rest-spread", "check-type", "check-typecast", "check-type-operator", "check-preblock"]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributors Guideline
2 |
3 | Thanks a lot for any contribution!
4 |
5 | To keep code quality high and maintenance work low, please adhere to the
6 | following guidelines when creating a pull request:
7 |
8 | ## Style Guide
9 |
10 | Try to write clean code and to adhere to the already used coding style.
11 |
12 | Lines should be kept below 120 characters.
13 |
14 | ## Documentation
15 |
16 | All new code should be properly documented (see `docs/` directory).
17 |
18 | ## Tests
19 |
20 | If possible, new code should be covered by automated tests.
21 |
22 | ## Commit messages
23 |
24 | Use meaningful commit messages: Please follow the advice in [this
25 | blogpost](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
26 | First line of your commit message should be a very short summary (ideally 50
27 | characters or less) in the imperative mood. After the first line of the commit
28 | message, add a blank line and then a more detailed explanation (when relevant).
29 |
--------------------------------------------------------------------------------
/tests/testsuite.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | SaltyRTC Unit Tests
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016-2022 Threema GmbH
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/performance.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | SaltyRTC Performance Tests
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/signaling/handoverstate.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2016-2022 Threema GmbH
3 | *
4 | * This software may be modified and distributed under the terms
5 | * of the MIT license. See the `LICENSE.md` file for details.
6 | */
7 |
8 | export class HandoverState {
9 |
10 | private _local: boolean;
11 | private _peer: boolean;
12 |
13 | constructor() {
14 | this.reset();
15 | }
16 |
17 | public get local(): boolean {
18 | return this._local;
19 | }
20 |
21 | public set local(state: boolean) {
22 | const wasBoth = this.both;
23 | this._local = state;
24 | if (!wasBoth && this.both && this.onBoth !== undefined) {
25 | this.onBoth();
26 | }
27 | }
28 |
29 | public get peer(): boolean {
30 | return this._peer;
31 | }
32 |
33 | public set peer(state: boolean) {
34 | const wasBoth = this.both;
35 | this._peer = state;
36 | if (!wasBoth && this.both && this.onBoth !== undefined) {
37 | this.onBoth();
38 | }
39 | }
40 |
41 | /**
42 | * Return true if both peers have finished the handover.
43 | */
44 | public get both(): boolean {
45 | return this._local === true && this._peer === true;
46 | }
47 |
48 | /**
49 | * Return true if any peer has finished the handover.
50 | */
51 | public get any(): boolean {
52 | return this._local === true || this._peer === true;
53 | }
54 |
55 | /**
56 | * Reset handover state.
57 | */
58 | public reset() {
59 | this._local = false;
60 | this._peer = false;
61 | }
62 |
63 | /**
64 | * Callback that is called when both local and peer have done the
65 | * handover.
66 | */
67 | public onBoth: () => void;
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/tests/handoverstate.spec.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:file-header
2 | // tslint:disable:no-reference
3 | ///
4 |
5 | import { HandoverState } from '../src/signaling/handoverstate';
6 |
7 | export default () => { describe('HandoverState', function() {
8 |
9 | let state: HandoverState;
10 |
11 | beforeEach(() => {
12 | state = new HandoverState();
13 | });
14 |
15 | it('is initialized to false / false', () => {
16 | expect(state.local).toBeFalsy();
17 | expect(state.peer).toBeFalsy();
18 | });
19 |
20 | it('can determine whether any peer has finished the handover', () => {
21 | // None
22 | expect(state.any).toBeFalsy();
23 |
24 | // Local
25 | state.local = true;
26 | expect(state.any).toBeTruthy();
27 |
28 | // Peer
29 | state.reset();
30 | state.peer = true;
31 | expect(state.any).toBeTruthy();
32 |
33 | // Both
34 | state.local = true;
35 | expect(state.any).toBeTruthy();
36 | });
37 |
38 | it('can determine whether both peers have finished the handover', () => {
39 | // None
40 | expect(state.both).toBeFalsy();
41 |
42 | // Local
43 | state.local = true;
44 | expect(state.both).toBeFalsy();
45 |
46 | // Peer
47 | state.reset();
48 | state.peer = true;
49 | expect(state.both).toBeFalsy();
50 |
51 | // Both
52 | state.local = true;
53 | expect(state.both).toBeTruthy();
54 | });
55 |
56 | it('calls the callback when handover is done', () => {
57 | let onBothCalled = false;
58 | state.onBoth = () => { onBothCalled = true; };
59 | expect(onBothCalled).toBeFalsy();
60 | state.local = true;
61 | expect(onBothCalled).toBeFalsy();
62 | state.peer = true;
63 | expect(onBothCalled).toBeTruthy();
64 | });
65 | }); };
66 |
--------------------------------------------------------------------------------
/src/log.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2016-2022 Threema GmbH
3 | *
4 | * This software may be modified and distributed under the terms
5 | * of the MIT license. See the `LICENSE.md` file for details.
6 | */
7 |
8 | /**
9 | * A console log wrapper obeying levels.
10 | */
11 | export class Log implements saltyrtc.Log {
12 | private _level: saltyrtc.LogLevel;
13 | public debug: (message?: any, ...optionalParams: any[]) => void;
14 | public trace: (message?: any, ...optionalParams: any[]) => void;
15 | public info: (message?: any, ...optionalParams: any[]) => void;
16 | public warn: (message?: any, ...optionalParams: any[]) => void;
17 | public error: (message?: any, ...optionalParams: any[]) => void;
18 | public assert: (condition?: boolean, message?: string, ...data: any[]) => void;
19 |
20 | constructor(level: saltyrtc.LogLevel) {
21 | this.level = level;
22 | }
23 |
24 | public set level(level: saltyrtc.LogLevel) {
25 | // Set level
26 | this._level = level;
27 |
28 | // Reset all
29 | this.debug = this.noop;
30 | this.trace = this.noop;
31 | this.info = this.noop;
32 | this.warn = this.noop;
33 | this.error = this.noop;
34 | this.assert = this.noop;
35 |
36 | // Bind corresponding to level
37 | // noinspection FallThroughInSwitchStatementJS
38 | switch (level) {
39 | case 'debug':
40 | this.debug = console.debug;
41 | this.trace = console.trace;
42 | case 'info':
43 | this.info = console.info;
44 | case 'warn':
45 | this.warn = console.warn;
46 | case 'error':
47 | this.error = console.error;
48 | this.assert = console.assert;
49 | default:
50 | break;
51 | }
52 | }
53 |
54 | public get level(): saltyrtc.LogLevel {
55 | return this._level;
56 | }
57 |
58 | private noop(): void {
59 | // noop
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/tests/main.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Test entry point.
3 | *
4 | * Copyright (C) 2016-2022 Threema GmbH
5 | *
6 | * This software may be modified and distributed under the terms
7 | * of the MIT license. See the `LICENSE.md` file for details.
8 | */
9 |
10 | // Apply log groups to Jasmine tests
11 | type Callback = (...args: any) => any;
12 | // @ts-ignore
13 | const jasmineIt = window.it;
14 | // @ts-ignore
15 | window.it = (description: string, callback: Callback, ...args) => {
16 | const handler = (invoker: () => any) => {
17 | // Ugly type hack, sorry :(
18 | console.group((spec as any as jasmine.Spec).getFullName());
19 | let result: any;
20 | try {
21 | result = invoker();
22 | } catch (error) {
23 | console.groupEnd();
24 | throw error;
25 | }
26 | if (result instanceof Promise) {
27 | result
28 | .then(() => console.groupEnd())
29 | .catch(() => console.groupEnd());
30 | } else {
31 | console.groupEnd();
32 | }
33 | return result;
34 | };
35 | let wrapper: Callback;
36 | if (callback.length > 0) {
37 | wrapper = (done: any) => handler(() => callback(done));
38 | } else {
39 | wrapper = () => handler(() => callback());
40 | }
41 | const spec = jasmineIt(description, wrapper, ...args);
42 | return spec;
43 | };
44 |
45 | import test_client from './client.spec';
46 | import test_cookie from './cookie.spec';
47 | import test_csn from './csn.spec';
48 | import test_eventregistry from './eventregistry.spec';
49 | import test_handoverstate from './handoverstate.spec';
50 | import test_integration from './integration.spec';
51 | import test_keystore from './keystore.spec';
52 | import test_nonce from './nonce.spec';
53 | import test_utils from './utils.spec';
54 |
55 | test_client();
56 | test_cookie();
57 | test_csn();
58 | test_eventregistry();
59 | test_handoverstate();
60 | test_integration();
61 | test_keystore();
62 | test_nonce();
63 | test_utils();
64 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 |
4 | shared:
5 | lint: &lint-config
6 | steps:
7 | - checkout
8 |
9 | # Install dependencies
10 | - run: npm install
11 |
12 | # Run linter
13 | - run:
14 | name: Run linter
15 | command: npm run lint
16 |
17 | test: &test-config
18 | steps:
19 | - checkout
20 |
21 | # Install dependencies
22 | - run: npm install
23 |
24 | # Start SaltyRTC server
25 | - run: saltyrtc-server-launcher > /saltyrtc/server.pid && sleep 2
26 |
27 | # Show browser version
28 | - run: if which firefox >/dev/null; then firefox --version; fi
29 | - run: if which chrome >/dev/null; then chrome --version; fi
30 | - run: if which chromium >/dev/null; then chromium --version; fi
31 |
32 | # Run tests
33 | - run:
34 | name: Run tests
35 | command: npm run rollup_tests && npm test -- --browsers $BROWSER
36 | - run:
37 | name: Run type checks
38 | command: node_modules/.bin/tsc --noEmit
39 |
40 | # Stop SaltyRTC server
41 | - run: kill -INT $(cat /saltyrtc/server.pid)
42 |
43 |
44 | jobs:
45 | lint:
46 | <<: *lint-config
47 | docker:
48 | - image: circleci/node:16-browsers
49 |
50 | test-chromium-latest:
51 | <<: *test-config
52 | docker:
53 | - image: saltyrtc/circleci-image-js:chromium-latest
54 | environment:
55 | - BROWSER: ChromiumHeadless
56 |
57 | test-firefox-stable:
58 | <<: *test-config
59 | docker:
60 | - image: saltyrtc/circleci-image-js:firefox-97
61 | environment:
62 | BROWSER: Firefox_circle_ci
63 | FIREFOX_BIN: xvfb-firefox
64 |
65 | test-firefox-esr:
66 | <<: *test-config
67 | docker:
68 | - image: saltyrtc/circleci-image-js:firefox-91esr
69 | environment:
70 | BROWSER: Firefox_circle_ci
71 | FIREFOX_BIN: xvfb-firefox
72 |
73 |
74 | workflows:
75 | version: 2
76 | build:
77 | jobs:
78 | - lint
79 | - test-chromium-latest
80 | - test-firefox-stable
81 | - test-firefox-esr
82 |
--------------------------------------------------------------------------------
/tests/nonce.spec.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:file-header
2 | // tslint:disable:no-reference
3 | ///
4 |
5 | import { Cookie } from '../src/cookie';
6 | import { Nonce } from '../src/nonce';
7 |
8 | export default () => { describe('nonce', function() {
9 |
10 | describe('Nonce', function() {
11 |
12 | let array: Uint8Array;
13 |
14 | beforeEach(() => {
15 | array = new Uint8Array([
16 | // Cookie
17 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
18 | // Source: 17
19 | 17,
20 | // Destination: 18
21 | 18,
22 | // Overflow: 258 big endian
23 | 1, 2,
24 | // Sequence number: 50595078 big endian
25 | 3, 4, 5, 6,
26 | ]);
27 | });
28 |
29 | it('parses correctly', () => {
30 | const nonce = Nonce.fromUint8Array(array);
31 | expect(nonce.cookie.bytes).toEqual(Uint8Array.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16));
32 | expect(nonce.source).toEqual(17);
33 | expect(nonce.destination).toEqual(18);
34 | expect(nonce.overflow).toEqual((2 ** 8) + 2);
35 | expect(nonce.sequenceNumber).toEqual((3 * (2 ** 24)) + (4 * (2 ** 16)) + (5 * (2 ** 8)) + 6);
36 | });
37 |
38 | it('serializes correctly', () => {
39 | const cookie = new Cookie(Uint8Array.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16));
40 | const source = 17;
41 | const destination = 18;
42 | const overflow = 258;
43 | const sequenceNumber = 50595078;
44 | const nonce = new Nonce(cookie, overflow, sequenceNumber, source, destination);
45 | expect(nonce.toUint8Array()).toEqual(array);
46 | });
47 |
48 | it('returns the correct combined sequence number', () => {
49 | const nonce = Nonce.fromUint8Array(array);
50 | expect(nonce.combinedSequenceNumber).toEqual((258 * (2 ** 32)) + 50595078);
51 | });
52 |
53 | });
54 |
55 | }); };
56 |
--------------------------------------------------------------------------------
/src/closecode.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2016-2022 Threema GmbH
3 | *
4 | * This software may be modified and distributed under the terms
5 | * of the MIT license. See the `LICENSE.md` file for details.
6 | */
7 |
8 | export class CloseCode {
9 | // tslint:disable:variable-name
10 | public static readonly ClosingNormal = 1000;
11 | public static readonly GoingAway = 1001;
12 | public static readonly NoSharedSubprotocol = 1002;
13 | public static readonly PathFull = 3000;
14 | public static readonly ProtocolError = 3001;
15 | public static readonly InternalError = 3002;
16 | public static readonly Handover = 3003;
17 | public static readonly DroppedByInitiator = 3004;
18 | public static readonly InitiatorCouldNotDecrypt = 3005;
19 | public static readonly NoSharedTask = 3006;
20 | public static readonly InvalidKey = 3007;
21 | public static readonly Timeout = 3008;
22 | // tslint:enable:variable-name
23 | }
24 |
25 | export function explainCloseCode(code: CloseCode): string {
26 | switch (code) {
27 | case CloseCode.ClosingNormal:
28 | return 'Normal closing';
29 | case CloseCode.GoingAway:
30 | return 'The endpoint is going away';
31 | case CloseCode.NoSharedSubprotocol:
32 | return 'No shared subprotocol could be found';
33 | case CloseCode.PathFull:
34 | return 'No free responder byte';
35 | case CloseCode.ProtocolError:
36 | return 'Protocol error';
37 | case CloseCode.InternalError:
38 | return 'Internal error';
39 | case CloseCode.Handover:
40 | return 'Handover finished';
41 | case CloseCode.DroppedByInitiator:
42 | return 'Dropped by initiator';
43 | case CloseCode.InitiatorCouldNotDecrypt:
44 | return 'Initiator could not decrypt a message';
45 | case CloseCode.NoSharedTask:
46 | return 'No shared task was found';
47 | case CloseCode.InvalidKey:
48 | return 'Invalid key';
49 | case CloseCode.Timeout:
50 | return 'Timeout';
51 | default:
52 | return 'Unknown';
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@saltyrtc/client",
3 | "version": "0.15.1",
4 | "description": "SaltyRTC JavaScript implementation",
5 | "main": "dist/saltyrtc-client.es5.min.js",
6 | "module": "dist/saltyrtc-client.es2015.js",
7 | "jsnext:main": "dist/saltyrtc-client.es2015.js",
8 | "types": "saltyrtc-client.d.ts",
9 | "scripts": {
10 | "test": "karma start --single-run --log-level=debug --colors",
11 | "dist": "npm run dist_es5 && npm run dist_es5_min && npm run dist_es2015",
12 | "dist_es5": "rollup -c rollup/es5.js",
13 | "dist_es5_min": "rollup -c rollup/es5.min.js",
14 | "dist_es2015": "rollup -c rollup/es2015.js",
15 | "rollup_tests": "rollup -c rollup/testing.js && rollup -c rollup/performance.js",
16 | "validate": "tsc --noEmit",
17 | "lint": "tslint -c tslint.json --project tsconfig.json",
18 | "clean": "rm -rf src/*.js tests/testsuite.js* tests/performance.js*"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/saltyrtc/saltyrtc-client-js.git"
23 | },
24 | "keywords": [
25 | "saltyrtc",
26 | "webrtc",
27 | "ortc",
28 | "rtc",
29 | "nacl"
30 | ],
31 | "author": "Threema GmbH",
32 | "license": "MIT",
33 | "bugs": {
34 | "url": "https://github.com/saltyrtc/saltyrtc-client-js/issues"
35 | },
36 | "homepage": "https://github.com/saltyrtc/saltyrtc-client-js",
37 | "devDependencies": {
38 | "@babel/core": "^7.17.2",
39 | "@rollup/plugin-babel": "^5.3.0",
40 | "@rollup/plugin-typescript": "^8.3.0",
41 | "@types/msgpack-lite": "^0.1.7",
42 | "jasmine-core": "^4.0.0",
43 | "karma": "^6.3.16",
44 | "karma-chrome-launcher": "^3.1.0",
45 | "karma-firefox-launcher": "^2.1.2",
46 | "karma-jasmine": "^4.0.1",
47 | "msgpack-lite": "^0.1.x",
48 | "rollup": "^2.67.2",
49 | "rollup-plugin-terser": "^7.0.2",
50 | "tslint": "^6",
51 | "tweetnacl": "^1.0.3",
52 | "typescript": "^4.5.5"
53 | },
54 | "peerDependencies": {
55 | "msgpack-lite": "^0.1.x",
56 | "tweetnacl": "^1.0.0"
57 | },
58 | "files": [
59 | "dist",
60 | "saltyrtc-client.d.ts",
61 | "README.md",
62 | "LICENSE.md",
63 | "CHANGELOG.md",
64 | "package.json",
65 | "package-lock.json"
66 | ]
67 | }
68 |
--------------------------------------------------------------------------------
/tests/cookie.spec.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:file-header
2 | // tslint:disable:no-reference
3 | ///
4 |
5 | import { Cookie, CookiePair } from '../src/cookie';
6 | import { ProtocolError } from '../src/exceptions';
7 |
8 | export default () => { describe('cookie', function() {
9 |
10 | describe('Cookie', function() {
11 |
12 | it('generates a cookie of the correct length', () => {
13 | const c = new Cookie();
14 | expect(Cookie.COOKIE_LENGTH).toEqual(16);
15 | expect(c.bytes.byteLength).toEqual(Cookie.COOKIE_LENGTH);
16 | });
17 |
18 | it('can compare two cookies', () => {
19 | const c1 = new Cookie();
20 | const c2 = new Cookie();
21 |
22 | // Ensure cookies are different
23 | c1.bytes[0] = 1;
24 | c2.bytes[0] = 2;
25 |
26 | expect(c1.equals(c2)).toEqual(false);
27 |
28 | // Make cookies equal
29 | c2.bytes = c1.bytes;
30 |
31 | expect(c1.equals(c2)).toEqual(true);
32 | });
33 |
34 | it('generates a random cookie', () => {
35 | const c1 = new Cookie();
36 | const c2 = new Cookie();
37 | const c3 = new Cookie();
38 | const c4 = new Cookie();
39 | expect(c1.equals(c2)).toBe(false);
40 | expect(c1.equals(c3)).toBe(false);
41 | expect(c1.equals(c4)).toBe(false);
42 | expect(c2.equals(c3)).toBe(false);
43 | expect(c2.equals(c4)).toBe(false);
44 | expect(c3.equals(c4)).toBe(false);
45 | });
46 |
47 | });
48 |
49 | describe('CookiePair', function() {
50 | it('cannot be instantiated from two equal cookies', () => {
51 | const c = new Cookie();
52 | const construct = () => new CookiePair(c, c);
53 | expect(construct).toThrow(new ProtocolError('Their cookie matches our cookie'));
54 | });
55 |
56 | it('cannot set their cookie to our cookie', () => {
57 | const pair = new CookiePair();
58 | const setDifferent = () => pair.theirs = new Cookie();
59 | const setSame = () => pair.theirs = pair.ours;
60 | expect(setDifferent).not.toThrow();
61 | expect(setSame).toThrow(new ProtocolError('Their cookie matches our cookie'));
62 | });
63 | });
64 |
65 | }); };
66 |
--------------------------------------------------------------------------------
/src/exceptions.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2016-2022 Threema GmbH
3 | *
4 | * This software may be modified and distributed under the terms
5 | * of the MIT license. See the `LICENSE.md` file for details.
6 | */
7 |
8 | import { CloseCode } from './closecode';
9 |
10 | /**
11 | * A SaltyRTC signaling error.
12 | *
13 | * It will result in the connection closing with the specified error code.
14 | */
15 | export class SignalingError extends Error implements saltyrtc.SignalingError {
16 | public readonly closeCode: number;
17 | constructor(closeCode: number, message: string) {
18 | super(message);
19 | this.message = message;
20 | this.closeCode = closeCode;
21 | this.name = 'SignalingError';
22 | }
23 | }
24 |
25 | /**
26 | * A signaling error with the close code hardcoded to ProtocolError.
27 | */
28 | export class ProtocolError extends SignalingError implements saltyrtc.ProtocolError {
29 | constructor(message: string) {
30 | super(CloseCode.ProtocolError, message);
31 | }
32 | }
33 |
34 | /**
35 | * Errors related to the network connection state.
36 | */
37 | export class ConnectionError extends Error implements saltyrtc.ConnectionError {
38 | constructor(message: string) {
39 | super(message);
40 | this.message = message;
41 | this.name = 'ConnectionError';
42 | }
43 | }
44 |
45 | /**
46 | * Errors related to validation.
47 | */
48 | export class ValidationError extends Error implements saltyrtc.ValidationError {
49 | // If this flag is set, then the validation error
50 | // will be converted to a protocol error.
51 | public readonly critical: boolean;
52 |
53 | constructor(message: string, critical: boolean = true) {
54 | super(message);
55 | this.message = message;
56 | this.name = 'ValidationError';
57 | this.critical = critical;
58 | }
59 | }
60 |
61 | /**
62 | * Crypto related errors.
63 | */
64 | export class CryptoError extends Error implements saltyrtc.CryptoError {
65 | // A short string used to identify the exception
66 | // independently from the error message.
67 | public readonly code: saltyrtc.CryptoErrorCode;
68 |
69 | constructor(code: saltyrtc.CryptoErrorCode, message: string) {
70 | super(message);
71 | this.name = 'CryptoError';
72 | this.message = message;
73 | this.code = code;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/rollup/es2015.js:
--------------------------------------------------------------------------------
1 | import typescript from '@rollup/plugin-typescript';
2 | import fs from 'fs';
3 |
4 | let p = JSON.parse(fs.readFileSync('package.json'));
5 |
6 | export default {
7 | input: 'src/main.ts',
8 | treeshake: true,
9 | plugins: [
10 | typescript({
11 | typescript: require('typescript')
12 | })
13 | ],
14 | external: ['msgpack-lite', 'tweetnacl'],
15 | output: {
16 | file: 'dist/saltyrtc-client.es2015.js',
17 | sourcemap: false,
18 | format: 'es',
19 | banner: "/**\n" +
20 | " * saltyrtc-client-js v" + p.version + "\n" +
21 | " * " + p.description + "\n" +
22 | " * " + p.homepage + "\n" +
23 | " *\n" +
24 | " * Copyright (C) 2016-2022 " + p.author + "\n" +
25 | " *\n" +
26 | " * This software may be modified and distributed under the terms\n" +
27 | " * of the MIT license:\n" +
28 | " * \n" +
29 | " * Permission is hereby granted, free of charge, to any person obtaining a copy\n" +
30 | " * of this software and associated documentation files (the \"Software\"), to deal\n" +
31 | " * in the Software without restriction, including without limitation the rights\n" +
32 | " * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n" +
33 | " * copies of the Software, and to permit persons to whom the Software is\n" +
34 | " * furnished to do so, subject to the following conditions:\n" +
35 | " * \n" +
36 | " * The above copyright notice and this permission notice shall be included in all\n" +
37 | " * copies or substantial portions of the Software.\n" +
38 | " * \n" +
39 | " * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n" +
40 | " * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n" +
41 | " * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n" +
42 | " * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n" +
43 | " * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n" +
44 | " * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n" +
45 | " * SOFTWARE.\n" +
46 | " */\n" +
47 | "'use strict';\n",
48 | },
49 | }
50 |
--------------------------------------------------------------------------------
/src/csn.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2016-2022 Threema GmbH
3 | *
4 | * This software may be modified and distributed under the terms
5 | * of the MIT license. See the `LICENSE.md` file for details.
6 | */
7 |
8 | import { randomUint32 } from './utils';
9 |
10 | export class CombinedSequence implements saltyrtc.CombinedSequence {
11 | private static SEQUENCE_NUMBER_MAX = 0xFFFFFFFF; // 1<<32 - 1
12 | private static OVERFLOW_MAX = 0xFFFFF; // 1<<16 - 1
13 |
14 | private sequenceNumber: number;
15 | private overflow: number;
16 |
17 | constructor() {
18 | this.sequenceNumber = randomUint32();
19 | this.overflow = 0;
20 | }
21 |
22 | /**
23 | * Return next sequence number and overflow.
24 | *
25 | * May throw an error if overflow number overflows. This is extremely
26 | * unlikely and must be treated as a protocol error.
27 | */
28 | public next(): saltyrtc.NextCombinedSequence {
29 | if (this.sequenceNumber >= CombinedSequence.SEQUENCE_NUMBER_MAX) {
30 | // Sequence number overflow
31 | this.sequenceNumber = 0;
32 | this.overflow += 1;
33 | if (this.overflow >= CombinedSequence.OVERFLOW_MAX) {
34 | // Overflow overflow
35 | throw new Error('overflow-overflow');
36 | }
37 | } else {
38 | this.sequenceNumber += 1;
39 | }
40 | return {
41 | sequenceNumber: this.sequenceNumber,
42 | overflow: this.overflow,
43 | };
44 | }
45 |
46 | /**
47 | * Return a snapshot of the current CSN as an integer, without changing the
48 | * internal state.
49 | *
50 | * Warning: Do not use this for the SaltyRTC protocol itself!
51 | */
52 | public asNumber(): number {
53 | return (this.overflow * (2 ** 32)) + this.sequenceNumber;
54 | }
55 |
56 | }
57 |
58 | /**
59 | * A combined sequence pair.
60 | */
61 | export class CombinedSequencePair implements saltyrtc.CombinedSequencePair {
62 | public ours: CombinedSequence = null;
63 | public theirs: number = null;
64 |
65 | constructor(ours?: CombinedSequence, theirs?: number) {
66 | if (typeof ours !== 'undefined' && typeof theirs !== 'undefined') {
67 | this.ours = ours;
68 | this.theirs = theirs;
69 | } else if (typeof ours === 'undefined' && typeof theirs === 'undefined') {
70 | this.ours = new CombinedSequence();
71 | } else {
72 | throw new Error('Either both or no combined sequences must be specified');
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/tests/performance/crypto.worker.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | importScripts(
4 | '../../node_modules/tweetnacl/nacl-fast.js',
5 | '../../node_modules/msgpack-lite/dist/msgpack.min.js',
6 | '../../dist/saltyrtc-client.es5.min.js',
7 | );
8 |
9 | let keyStore;
10 | let publicKey;
11 | let sharedkeyStore;
12 |
13 | function encrypt(e) {
14 | const cipher = keyStore.encryptRaw(e.data.bytes, e.data.nonce, publicKey);
15 | postMessage(cipher);
16 | }
17 |
18 | function encryptTransferable(e) {
19 | const cipher = keyStore.encryptRaw(e.data.bytes, e.data.nonce, publicKey);
20 | postMessage(cipher, [cipher.buffer]);
21 | }
22 |
23 | function decrypt(e) {
24 | const plain = keyStore.decryptRaw(e.data.bytes, e.data.nonce, publicKey);
25 | postMessage(plain);
26 | }
27 |
28 | function decryptTransferable(e) {
29 | const plain = keyStore.decryptRaw(e.data.bytes, e.data.nonce, publicKey);
30 | postMessage(plain, [plain.buffer]);
31 | }
32 |
33 | function encryptWithSharedKey(e) {
34 | const cipher = sharedkeyStore.encryptRaw(e.data.bytes, e.data.nonce);
35 | postMessage(cipher);
36 | }
37 |
38 | function encryptWithSharedKeyTransferable(e) {
39 | const cipher = sharedkeyStore.encryptRaw(e.data.bytes, e.data.nonce);
40 | postMessage(cipher, [cipher.buffer]);
41 | }
42 |
43 | function decryptWithSharedKey(e) {
44 | const plain = sharedkeyStore.decryptRaw(e.data.bytes, e.data.nonce);
45 | postMessage(plain);
46 | }
47 |
48 | function decryptWithSharedKeyTransferable(e) {
49 | const plain = sharedkeyStore.decryptRaw(e.data.bytes, e.data.nonce);
50 | postMessage(plain, [plain.buffer]);
51 | }
52 |
53 | addEventListener('message', (e) => {
54 | keyStore = new saltyrtcClient.KeyStore(e.data.secretKey);
55 | publicKey = keyStore.publicKeyBytes;
56 |
57 | // Optionally use the precomputed shared key
58 | let callbackFunctions;
59 | if (e.data.useSharedKeyStore) {
60 | sharedkeyStore = keyStore.getSharedKeyStore(publicKey);
61 | callbackFunctions = {
62 | 'encrypt': encryptWithSharedKey,
63 | 'decrypt': decryptWithSharedKey,
64 | 'encrypt-transferable': encryptWithSharedKeyTransferable,
65 | 'decrypt-transferable': decryptWithSharedKeyTransferable,
66 | };
67 | } else {
68 | callbackFunctions = {
69 | 'encrypt': encrypt,
70 | 'decrypt': decrypt,
71 | 'encrypt-transferable': encryptTransferable,
72 | 'decrypt-transferable': decryptTransferable,
73 | };
74 | }
75 |
76 | // Initialise worker by type
77 | const callbackFunction = callbackFunctions[e.data.type];
78 | if (!callbackFunction) {
79 | console.error('Unable to determine role');
80 | close();
81 | return;
82 | }
83 | addEventListener('message', callbackFunction);
84 | }, { once: true });
85 |
--------------------------------------------------------------------------------
/tests/csn.spec.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:file-header
2 | // tslint:disable:no-reference
3 | ///
4 | ///
5 |
6 | import { CombinedSequence } from '../src/csn';
7 |
8 | export default () => { describe('csn', function() {
9 |
10 | describe('CombinedSequence', function() {
11 |
12 | it('constructor', () => {
13 | for (let i = 0; i < 1000; i++) {
14 | const csn = new CombinedSequence();
15 | expect(csn.asNumber()).toBeLessThan(2 ** 32);
16 | expect((csn as any).overflow).toEqual(0);
17 | }
18 | });
19 |
20 | it('asNumber', () => {
21 | const csn = new CombinedSequence();
22 | (csn as any).sequenceNumber = 1234;
23 | (csn as any).overflow = 7;
24 | // (7<<32) + 1234
25 | expect(csn.asNumber()).toEqual(30064772306);
26 | });
27 |
28 | it('next (overflow=0)', () => {
29 | const csn = new CombinedSequence();
30 | (csn as any).sequenceNumber = 1234;
31 | (csn as any).overflow = 0;
32 | const snapshot: saltyrtc.NextCombinedSequence = csn.next();
33 | expect(snapshot.overflow).toEqual(0);
34 | expect(snapshot.sequenceNumber).toEqual(1235);
35 | expect((csn as any).overflow).toEqual(snapshot.overflow);
36 | expect((csn as any).sequenceNumber).toEqual(snapshot.sequenceNumber);
37 | });
38 |
39 | it('next (overflow>0)', () => {
40 | const csn = new CombinedSequence();
41 | (csn as any).sequenceNumber = 1234;
42 | (csn as any).overflow = 1337;
43 | const snapshot: saltyrtc.NextCombinedSequence = csn.next();
44 | expect(snapshot.overflow).toEqual(1337);
45 | expect(snapshot.sequenceNumber).toEqual(1235);
46 | expect((csn as any).overflow).toEqual(snapshot.overflow);
47 | expect((csn as any).sequenceNumber).toEqual(snapshot.sequenceNumber);
48 | });
49 |
50 | it('next (overflow=0->1)', () => {
51 | const csn = new CombinedSequence();
52 | (csn as any).sequenceNumber = (2 ** 32) - 3;
53 | (csn as any).overflow = 0;
54 | expect((csn as any).overflow).toEqual(0);
55 | expect((csn as any).sequenceNumber).toEqual(4294967293);
56 | csn.next();
57 | expect((csn as any).overflow).toEqual(0);
58 | expect((csn as any).sequenceNumber).toEqual(4294967294);
59 | csn.next();
60 | expect((csn as any).overflow).toEqual(0);
61 | expect((csn as any).sequenceNumber).toEqual(4294967295);
62 | csn.next();
63 | expect((csn as any).overflow).toEqual(1);
64 | expect((csn as any).sequenceNumber).toEqual(0);
65 | });
66 | });
67 |
68 | }); };
69 |
--------------------------------------------------------------------------------
/tests/testtasks.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Performance test entry point.
3 | *
4 | * Copyright (C) 2016-2022 Threema GmbH
5 | *
6 | * This software may be modified and distributed under the terms
7 | * of the MIT license. See the `LICENSE.md` file for details.
8 | */
9 | // tslint:disable:no-reference
10 | ///
11 |
12 | export class DummyTask implements saltyrtc.Task {
13 |
14 | public initialized = false;
15 | public peerData: object;
16 | protected signaling: saltyrtc.Signaling;
17 | protected name: string;
18 |
19 | public constructor(name?: string) {
20 | if (name === undefined) {
21 | this.name = 'dummy.tasks.saltyrtc.org';
22 | } else {
23 | this.name = name;
24 | }
25 | }
26 |
27 | public init(signaling: saltyrtc.Signaling, data: object): void {
28 | this.signaling = signaling;
29 | this.peerData = data;
30 | this.initialized = true;
31 | }
32 |
33 | public onPeerHandshakeDone(): void {
34 | // Nothing
35 | }
36 |
37 | public onTaskMessage(message: saltyrtc.messages.TaskMessage): void {
38 | console.log('Got new task message');
39 | }
40 |
41 | // noinspection JSMethodCanBeStatic
42 | public sendSignalingMessage(payload: Uint8Array) {
43 | console.log(`Sending signaling message (${payload.byteLength} bytes)`);
44 | }
45 |
46 | public getName(): string {
47 | return this.name;
48 | }
49 |
50 | public getSupportedMessageTypes(): string[] {
51 | return ['dummy'];
52 | }
53 |
54 | // noinspection JSMethodCanBeStatic
55 | public getData(): object {
56 | return {};
57 | }
58 |
59 | public close(): void {
60 | // Do nothing
61 | }
62 |
63 | }
64 |
65 | export class PingPongTask extends DummyTask {
66 |
67 | public sentPong = false;
68 | public receivedPong = false;
69 |
70 | public constructor() {
71 | super('pingpong.tasks.saltyrtc.org');
72 | }
73 |
74 | public getSupportedMessageTypes(): string[] {
75 | return ['ping', 'pong'];
76 | }
77 |
78 | public onPeerHandshakeDone(): void {
79 | if (this.signaling.role === 'initiator') {
80 | this.sendPing();
81 | }
82 | }
83 |
84 | public sendPing(): void {
85 | console.log('[PingPongTask] Sending ping');
86 | this.signaling.sendTaskMessage({'type': 'ping'});
87 | }
88 |
89 | public sendPong(): void {
90 | console.log('[PingPongTask] Sending pong');
91 | this.signaling.sendTaskMessage({'type': 'pong'});
92 | this.sentPong = true;
93 | }
94 |
95 | public onTaskMessage(message: saltyrtc.messages.TaskMessage): void {
96 | if (message.type === 'ping') {
97 | console.log('[PingPongTask] Received ping');
98 | this.sendPong();
99 | } else if (message.type === 'pong') {
100 | console.log('[PingPongTask] Received pong');
101 | this.receivedPong = true;
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/docs/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | dist: trusty
3 | language: java
4 | matrix:
5 | include:
6 | #- jdk: openjdk7 # disable until https://github.com/travis-ci/travis-ci/issues/5227 is fixed
7 | - jdk: oraclejdk7
8 | env:
9 | EXECUTE_BUILD_DOCS=true
10 | - jdk: oraclejdk8
11 | before_cache:
12 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
13 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/
14 | cache:
15 | directories:
16 | - $HOME/.gradle/caches/
17 | - $HOME/.gradle/wrapper/
18 | before_script:
19 | # Show OS
20 | - cat /etc/os-release
21 | - python3 --version
22 | # Libsodium
23 | - sudo add-apt-repository -y ppa:chris-lea/libsodium
24 | - sudo apt-get update && sudo apt-get install -y libsodium-dev
25 | # Create test certificate for localhost
26 | - openssl req -new -newkey rsa:1024 -nodes -sha256 -out saltyrtc.csr -keyout saltyrtc.key -subj '/C=CH/O=SaltyRTC/CN=localhost/'
27 | - openssl x509 -req -days 365 -in saltyrtc.csr -signkey saltyrtc.key -out saltyrtc.crt
28 | - keytool -import -trustcacerts -alias root -file saltyrtc.crt -keystore saltyrtc.jks -storetype JKS -storepass saltyrtc -noprompt
29 | # Start SaltyRTC server
30 | - git clone https://github.com/saltyrtc/saltyrtc-server-python -b master
31 | - export SALTYRTC_SERVER_PERMANENT_KEY=0919b266ce1855419e4066fc076b39855e728768e3afa773105edd2e37037c20 # Public: 09a59a5fa6b45cb07638a3a6e347ce563a948b756fd22f9527465f7c79c2a864
32 | - |
33 | cd saltyrtc-server-python
34 | pyvenv venv
35 | venv/bin/pip install .[logging]
36 | venv/bin/saltyrtc-server -v 5 serve -sc ../saltyrtc.crt -sk ../saltyrtc.key -p 8765 -k $SALTYRTC_SERVER_PERMANENT_KEY > serverlog.txt 2>&1 &
37 | export SALTYRTC_SERVER_PID=$!
38 | sleep 2
39 | cd ..
40 | # Enable debug in integration tests
41 | - sed -i 's/DEBUG = false/DEBUG = true/' src/test/java/org/saltyrtc/client/tests/integration/ConnectionTest.java
42 | after_script:
43 | # Stop SaltyRTC server
44 | - kill -INT $SALTYRTC_SERVER_PID
45 | # Print server log
46 | - |
47 | echo "---------- Server Log ----------\n"
48 | cat saltyrtc-server-python/serverlog.txt
49 | echo -e "\n---------- End Server Log ----------"
50 | after_success:
51 | - test $TRAVIS_PULL_REQUEST == "false" && test $TRAVIS_BRANCH == "master" && test $EXECUTE_BUILD_DOCS == "true" && bash docs/deploy-docs.sh
52 | env:
53 | global:
54 | # Generated with "travis encrypt -r saltyrtc/saltyrtc-client-java GH_TOKEN="
55 | secure: ApN+XHzAGhxgq2Owbtsu/H6596FZaGb0/WXY3kS3Hmp5KVvRl6pTZAH/1cNoYqXWLoRykKR646mghBO1daE7I9rINMKoiVGB40e3qymxA4mt4syyobACmPlDlSn4MM+XhQyvnpsKigPGRoFO9OEjjVZAZZTcamuH7QXFOGCPP+Bk8C52MH2JCa5bFTgjvSfIBpZO/Z13g/0klJLWYUxFZyw1vTCua2Se7wdN5TXfRMOA5V3F8rqIUEt82OyEjNZG0MOG3Q0jhEh/Vele7YqfX1UkQ5hjQBpq55dZMpoWqInTh2eepE3Z/JBgqO4Qw6cvRx9Hf8jPTQQ9MHjqxwHFDeuHNXPk4GukpvrUoRC/kX/BvzFpqZ2ptIvPjezcAkHvCgOLlqJSXoPdp1IO4JQsQNTTZI6NqwSt/XHEA+oLib4chJGEDVCSBrba7K1aAodmcLdrLOk6/ewWfqAxHdCvTOwLy2S7lxSMcD6k99TKUdkfa7/tiSWajIPt5YriutrGLrk9yyUc9LHdVhbFUI0K/HqMcoIfujiAMem1JDmHiV1garMn5k23LDS33C5WgypDEM6Ro53gVB9coy1h4K14f5v9uVW0zsGM/NRr8frBkZ5ySgN8xy0ZJ9e21KqKeswpoQmpJJiDa3Ji8Pg9pweUvGToh15FGAnRQ/8fkMHATpg=
56 |
--------------------------------------------------------------------------------
/tests/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Test utils.
3 | *
4 | * Copyright (C) 2016-2022 Threema GmbH
5 | *
6 | * This software may be modified and distributed under the terms
7 | * of the MIT license. See the `LICENSE.md` file for details.
8 | */
9 |
10 | /**
11 | * Awaitable promise that sleeps n milliseconds.
12 | */
13 | export function sleep(milliseconds: number): Promise<{}> {
14 | return new Promise(function(resolve) {
15 | window.setTimeout(resolve, milliseconds);
16 | });
17 | }
18 |
19 | /**
20 | * Type alias for a function that takes no arguments and returns nothing.
21 | */
22 | export type Runnable = () => void;
23 |
24 | export interface PromiseFn {
25 | resolve: (value: V) => void;
26 | reject: (reason?: E) => void;
27 | }
28 |
29 | /**
30 | * A {Promise} that allows to resolve or reject outside of the executor and
31 | * query the current status.
32 | */
33 | export class ResolvablePromise extends Promise {
34 | private _done: boolean;
35 | private readonly _inner: PromiseFn, E>;
36 |
37 | public constructor(
38 | executor?: (
39 | resolve: (value: V | PromiseLike) => void,
40 | reject: (reason?: E) => void,
41 | ) => void,
42 | ) {
43 | // We have to do this little dance here since `this` cannot be used
44 | // prior to having called `super`.
45 | const inner: PromiseFn, E> = {
46 | resolve: ResolvablePromise._fail,
47 | reject: ResolvablePromise._fail,
48 | };
49 | const outer: PromiseFn, E> = {
50 | resolve: (value) => this.resolve(value),
51 | reject: (reason) => this.reject(reason),
52 | };
53 | super(
54 | (
55 | innerResolve: (value: V | PromiseLike) => void,
56 | innerReject: (reason?: E) => void,
57 | ) => {
58 | inner.resolve = innerResolve;
59 | inner.reject = innerReject;
60 | if (executor) {
61 | executor(outer.resolve, outer.reject);
62 | return;
63 | }
64 | }
65 | );
66 | this._inner = {
67 | resolve: inner.resolve,
68 | reject: inner.reject,
69 | };
70 | this._done = false;
71 | }
72 |
73 | /**
74 | * Called if the promise resolve/rejector methods were not available.
75 | * This should never happen!
76 | */
77 | private static _fail(): void {
78 | throw new Error('Promise resolve/reject not available');
79 | }
80 |
81 | /**
82 | * Return whether the promise is done (resolved or rejected).
83 | */
84 | public get done(): boolean {
85 | return this._done;
86 | }
87 |
88 | /**
89 | * Resolve the promise from the outside.
90 | */
91 | public resolve(value: V | PromiseLike): void {
92 | this._done = true;
93 | this._inner.resolve(value);
94 | }
95 |
96 | /**
97 | * Reject the promise from the outside.
98 | */
99 | public reject(reason?: E): void {
100 | this._done = true;
101 | this._inner.reject(reason);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/nonce.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2016-2022 Threema GmbH
3 | *
4 | * This software may be modified and distributed under the terms
5 | * of the MIT license. See the `LICENSE.md` file for details.
6 | */
7 |
8 | import { Cookie } from './cookie';
9 | import { ValidationError } from './exceptions';
10 |
11 | /**
12 | * A SaltyRTC signaling channel nonce.
13 | *
14 | * This is very similar to the regular nonce, but also contains a source and
15 | * destination byte. That reduces the length of the overflow number to 2 bytes.
16 | *
17 | * Nonce structure:
18 | *
19 | * |CCCCCCCCCCCCCCCC|S|D|OO|QQQQ|
20 | *
21 | * - C: Cookie (16 byte)
22 | * - S: Source byte (1 byte)
23 | * - D: Destination byte (1 byte)
24 | * - O: Overflow number (2 bytes)
25 | * - Q: Sequence number (4 bytes)
26 | */
27 | export class Nonce {
28 | public static TOTAL_LENGTH = 24;
29 |
30 | private _cookie: Cookie;
31 | private _overflow: number;
32 | private _sequenceNumber: number;
33 | private _source: number;
34 | private _destination: number;
35 |
36 | constructor(cookie: Cookie, overflow: number, sequenceNumber: number,
37 | source: number, destination: number) {
38 | this._cookie = cookie;
39 | this._overflow = overflow;
40 | this._sequenceNumber = sequenceNumber;
41 | this._source = source;
42 | this._destination = destination;
43 | }
44 |
45 | get cookie() {
46 | return this._cookie;
47 | }
48 | get overflow() {
49 | return this._overflow;
50 | }
51 | get sequenceNumber() {
52 | return this._sequenceNumber;
53 | }
54 | get combinedSequenceNumber() {
55 | return (this._overflow * (2 ** 32)) + this._sequenceNumber;
56 | }
57 | get source() {
58 | return this._source;
59 | }
60 | get destination() {
61 | return this._destination;
62 | }
63 |
64 | /**
65 | * Create a signaling nonce from a Uint8Array.
66 | *
67 | * If packet is not exactly 24 bytes long, throw a `ValidationError`.
68 | */
69 | public static fromUint8Array(packet: Uint8Array): Nonce {
70 | if (packet.byteLength !== this.TOTAL_LENGTH) {
71 | throw new ValidationError('bad-packet-length');
72 | }
73 |
74 | // Get view to buffer
75 | const view = new DataView(
76 | packet.buffer, packet.byteOffset + Cookie.COOKIE_LENGTH, 8);
77 |
78 | // Parse and return nonce
79 | const cookie = new Cookie(packet.slice(0, Cookie.COOKIE_LENGTH));
80 | const source = view.getUint8(0);
81 | const destination = view.getUint8(1);
82 | const overflow = view.getUint16(2);
83 | const sequenceNumber = view.getUint32(4);
84 |
85 | return new Nonce(cookie, overflow, sequenceNumber, source, destination);
86 | }
87 |
88 | /**
89 | * Return a Uint8Array containing the signaling nonce data.
90 | */
91 | public toUint8Array(): Uint8Array {
92 | const buffer = new ArrayBuffer(Nonce.TOTAL_LENGTH);
93 |
94 | const array = new Uint8Array(buffer);
95 | array.set(this._cookie.bytes);
96 |
97 | const view = new DataView(buffer, Cookie.COOKIE_LENGTH, 8);
98 | view.setUint8(0, this._source);
99 | view.setUint8(1, this._destination);
100 | view.setUint16(2, this._overflow);
101 | view.setUint32(4, this._sequenceNumber);
102 |
103 | return array;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/eventregistry.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2016-2022 Threema GmbH
3 | *
4 | * This software may be modified and distributed under the terms
5 | * of the MIT license. See the `LICENSE.md` file for details.
6 | */
7 |
8 | export class EventRegistry {
9 | protected map: Map;
10 |
11 | constructor() {
12 | this.map = new Map();
13 | }
14 |
15 | /**
16 | * Register an event handler for the specified event(s).
17 | */
18 | public register(eventType: string | string[], handler: saltyrtc.SaltyRTCEventHandler): void {
19 | if (typeof eventType === 'string') {
20 | this.set(eventType, handler);
21 | } else {
22 | for (const et of eventType) {
23 | this.set(et, handler);
24 | }
25 | }
26 | }
27 |
28 | /**
29 | * Unregister an event handler for the specified event(s).
30 | * If no handler is specified, all handlers for the specified event(s) are removed.
31 | */
32 | public unregister(eventType: string | string[], handler?: saltyrtc.SaltyRTCEventHandler): void {
33 | if (typeof eventType === 'string') {
34 | // If the event does not exist, return
35 | if (!this.map.has(eventType)) {
36 | return;
37 | }
38 | // If no handler is specified, remove all corresponding events
39 | if (typeof handler === 'undefined') {
40 | this.map.delete(eventType);
41 | // Otherwise, remove the handler from the list if present
42 | } else {
43 | const list = this.map.get(eventType);
44 | const index = list.indexOf(handler);
45 | if (index !== -1) {
46 | list.splice(index, 1);
47 | }
48 | }
49 | } else {
50 | for (const et of eventType) {
51 | this.unregister(et, handler);
52 | }
53 | }
54 | }
55 |
56 | /**
57 | * Clear all event handlers.
58 | */
59 | public unregisterAll(): void {
60 | this.map.clear();
61 | }
62 |
63 | /**
64 | * Store a single event handler in the map.
65 | */
66 | private set(key: string, value: saltyrtc.SaltyRTCEventHandler) {
67 | if (this.map.has(key)) {
68 | const list = this.map.get(key);
69 | if (list.indexOf(value) === -1) {
70 | list.push(value);
71 | }
72 | } else {
73 | this.map.set(key, [value]);
74 | }
75 | }
76 |
77 | /**
78 | * Return all event handlers for the specified event(s).
79 | *
80 | * The return value is always an array. If the event does not exist, the
81 | * array will be empty.
82 | *
83 | * Even if a handler is registered for multiple events, it is only returned once.
84 | */
85 | public get(eventType: string | string[]): saltyrtc.SaltyRTCEventHandler[] {
86 | const handlers: saltyrtc.SaltyRTCEventHandler[] = [];
87 | if (typeof eventType === 'string') {
88 | if (this.map.has(eventType)) {
89 | handlers.push.apply(handlers, this.map.get(eventType));
90 | }
91 | } else {
92 | for (const et of eventType) {
93 | for (const handler of this.get(et)) {
94 | if (handlers.indexOf(handler) === -1) {
95 | handlers.push(handler);
96 | }
97 | }
98 | }
99 | }
100 | return handlers;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/cookie.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2016-2022 Threema GmbH
3 | *
4 | * This software may be modified and distributed under the terms
5 | * of the MIT license. See the `LICENSE.md` file for details.
6 | */
7 |
8 | import * as nacl from 'tweetnacl';
9 | import { ProtocolError, ValidationError } from './exceptions';
10 |
11 | export class Cookie implements saltyrtc.Cookie {
12 | public static COOKIE_LENGTH = 16;
13 |
14 | public bytes: Uint8Array;
15 |
16 | /**
17 | * Create a new cookie.
18 | *
19 | * If no bytes are provided, generate a random cookie.
20 | */
21 | constructor(bytes?: Uint8Array) {
22 | if (bytes !== undefined) {
23 | if (bytes.length !== 16) {
24 | throw new ValidationError('Bad cookie length');
25 | }
26 | this.bytes = bytes;
27 | } else {
28 | this.bytes = nacl.randomBytes(Cookie.COOKIE_LENGTH);
29 | }
30 | }
31 |
32 | /**
33 | * Return whether or not the two cookies are equal.
34 | */
35 | public equals(otherCookie: Cookie) {
36 | if (otherCookie.bytes === this.bytes) {
37 | return true;
38 | }
39 | if (otherCookie.bytes.byteLength !== Cookie.COOKIE_LENGTH) {
40 | return false;
41 | }
42 | for (let i = 0; i < this.bytes.byteLength; i++) {
43 | if (otherCookie.bytes[i] !== this.bytes[i]) {
44 | return false;
45 | }
46 | }
47 | return true;
48 | }
49 | }
50 |
51 | /**
52 | * A cookie pair.
53 | *
54 | * The implementation ensures that the two cookies cannot be equal.
55 | */
56 | export class CookiePair implements saltyrtc.CookiePair {
57 | private _ours: Cookie = null;
58 | private _theirs: Cookie = null;
59 |
60 | /**
61 | * Create a new cookie pair with a predefined peer cookie.
62 | */
63 | public static fromTheirs(theirs: Cookie): saltyrtc.CookiePair {
64 | let ours: Cookie;
65 | do {
66 | ours = new Cookie();
67 | } while (ours.equals(theirs));
68 | return new CookiePair(ours, theirs);
69 | }
70 |
71 | /**
72 | * Create a new cookie pair. Either both or no cookies must be specified.
73 | *
74 | * If you want to create a cookie pair from a predefined peer cookie,
75 | * use the static `fromTheirs` method instead.
76 | *
77 | * @throws SignalingError if both cookies are defined and equal.
78 | */
79 | constructor(ours?: Cookie, theirs?: Cookie) {
80 | if (typeof ours !== 'undefined' && typeof theirs !== 'undefined') {
81 | if (theirs.equals(ours)) {
82 | throw new ProtocolError('Their cookie matches our cookie');
83 | }
84 | this._ours = ours;
85 | this._theirs = theirs;
86 | } else if (typeof ours === 'undefined' && typeof theirs === 'undefined') {
87 | this._ours = new Cookie();
88 | } else {
89 | throw new Error('Either both or no cookies must be specified');
90 | }
91 | }
92 |
93 | /**
94 | * Get our own cookie.
95 | */
96 | public get ours(): saltyrtc.Cookie {
97 | return this._ours;
98 | }
99 |
100 | /**
101 | * Get the peer cookie.
102 | */
103 | public get theirs(): saltyrtc.Cookie {
104 | return this._theirs;
105 | }
106 |
107 | /**
108 | * Set the peer cookie.
109 | *
110 | * @throws SignalingError if cookie matches our cookie.
111 | */
112 | public set theirs(cookie: saltyrtc.Cookie) {
113 | if (cookie.equals(this._ours)) {
114 | throw new ProtocolError('Their cookie matches our cookie');
115 | }
116 | this._theirs = cookie;
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/peers.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2016-2022 Threema GmbH
3 | *
4 | * This software may be modified and distributed under the terms
5 | * of the MIT license. See the `LICENSE.md` file for details.
6 | */
7 |
8 | import { CookiePair } from './cookie';
9 | import { CombinedSequencePair } from './csn';
10 | import { byteToHex } from './utils';
11 |
12 | /**
13 | * Base class for peers (initiator or responder).
14 | */
15 | export abstract class Peer {
16 | protected _id: number;
17 | protected _csnPair = new CombinedSequencePair();
18 | protected _cookiePair: saltyrtc.CookiePair;
19 | protected _permanentSharedKey: saltyrtc.SharedKeyStore | null = null;
20 | protected _sessionSharedKey: saltyrtc.SharedKeyStore | null = null;
21 |
22 | constructor(id: number, cookiePair?: saltyrtc.CookiePair) {
23 | this._id = id;
24 | if (cookiePair === undefined) {
25 | this._cookiePair = new CookiePair();
26 | } else {
27 | this._cookiePair = cookiePair;
28 | }
29 | }
30 |
31 | public get id(): number {
32 | return this._id;
33 | }
34 |
35 | public get hexId(): string {
36 | return byteToHex(this._id);
37 | }
38 |
39 | public get csnPair(): CombinedSequencePair {
40 | return this._csnPair;
41 | }
42 |
43 | public get cookiePair(): saltyrtc.CookiePair {
44 | return this._cookiePair;
45 | }
46 |
47 | public get permanentSharedKey(): saltyrtc.SharedKeyStore | null {
48 | return this._permanentSharedKey;
49 | }
50 |
51 | public get sessionSharedKey(): saltyrtc.SharedKeyStore | null {
52 | return this._sessionSharedKey;
53 | }
54 |
55 | public abstract get name(): string;
56 |
57 | public setPermanentSharedKey(remotePermanentKey: Uint8Array, localPermanentKey: saltyrtc.KeyStore) {
58 | this._permanentSharedKey = localPermanentKey.getSharedKeyStore(remotePermanentKey);
59 | }
60 |
61 | public setSessionSharedKey(remoteSessionKey: Uint8Array, localSessionKey: saltyrtc.KeyStore) {
62 | this._sessionSharedKey = localSessionKey.getSharedKeyStore(remoteSessionKey);
63 | }
64 | }
65 |
66 | /**
67 | * Base class for initiator and responder.
68 | */
69 | export abstract class Client extends Peer {
70 | protected _localSessionKey: saltyrtc.KeyStore | null = null;
71 |
72 | public get localSessionKey(): saltyrtc.KeyStore | null {
73 | return this._localSessionKey;
74 | }
75 |
76 | public setLocalSessionKey(localSessionKey: saltyrtc.KeyStore) {
77 | this._localSessionKey = localSessionKey;
78 | }
79 |
80 | public setSessionSharedKey(remoteSessionKey: Uint8Array, localSessionKey?: saltyrtc.KeyStore) {
81 | if (!localSessionKey) {
82 | localSessionKey = this._localSessionKey;
83 | } else {
84 | this._localSessionKey = localSessionKey;
85 | }
86 | super.setSessionSharedKey(remoteSessionKey, localSessionKey);
87 | }
88 | }
89 |
90 | /**
91 | * Information about the initiator. Used by responder during handshake.
92 | */
93 | export class Initiator extends Client {
94 | public static ID = 0x01;
95 |
96 | public connected = false;
97 | public handshakeState: 'new' | 'token-sent' | 'key-sent' | 'key-received'
98 | | 'auth-sent' | 'auth-received'
99 | = 'new';
100 |
101 | constructor(remotePermanentKey: Uint8Array, localPermanentKey: saltyrtc.KeyStore) {
102 | super(Initiator.ID);
103 | this.setPermanentSharedKey(remotePermanentKey, localPermanentKey);
104 | }
105 |
106 | public get name(): string {
107 | return 'Initiator';
108 | }
109 | }
110 |
111 | /**
112 | * Information about a responder. Used by initiator during handshake.
113 | */
114 | export class Responder extends Client {
115 | public handshakeState: 'new' | 'token-received' | 'key-received'
116 | | 'key-sent' | 'auth-received' | 'auth-sent'
117 | = 'new';
118 | private _counter: number;
119 |
120 | constructor(id: number, counter: number) {
121 | super(id);
122 | this._counter = counter;
123 | }
124 |
125 | public get name(): string {
126 | return 'Responder ' + this.id;
127 | }
128 |
129 | get counter(): number {
130 | return this._counter;
131 | }
132 | }
133 |
134 | /**
135 | * Information about the server.
136 | */
137 | export class Server extends Peer {
138 | public static ID = 0x00;
139 |
140 | public handshakeState: 'new' | 'hello-sent' | 'auth-sent' | 'done' = 'new';
141 |
142 | constructor() {
143 | super(Server.ID);
144 | }
145 |
146 | public get name(): string {
147 | return 'Server';
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/tests/eventregistry.spec.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:file-header
2 | // tslint:disable:no-reference
3 | ///
4 | ///
5 |
6 | import { EventRegistry } from '../src/eventregistry';
7 | import { Runnable } from './utils';
8 |
9 | /**
10 | * Wrapper around the EventRegistry that makes the `map` public.
11 | */
12 | class TestEventRegistry extends EventRegistry {
13 | public map: Map;
14 | }
15 |
16 | export default () => { describe('eventregistry', function() {
17 |
18 | describe('EventRegistry', function() {
19 |
20 | let registry: TestEventRegistry;
21 | let handler1: Runnable;
22 | let handler2: Runnable;
23 |
24 | beforeEach(() => {
25 | registry = new TestEventRegistry();
26 | handler1 = () => { console.log('Event 1 occurred'); };
27 | handler2 = () => { console.log('Event 2 occurred'); };
28 | });
29 |
30 | it('can register a new event', () => {
31 | expect(registry.map.get('boo')).toBeUndefined();
32 | registry.register('boo', handler1);
33 | const registered: saltyrtc.SaltyRTCEventHandler[] = registry.map.get('boo');
34 | expect(registered.length).toEqual(1);
35 | expect(registered[0]).toBe(handler1);
36 | });
37 |
38 | it('can register multiple handlers', () => {
39 | expect(registry.map.get('boo')).toBeUndefined();
40 | registry.register('boo', handler1);
41 | registry.register('boo', handler2);
42 | const registered: saltyrtc.SaltyRTCEventHandler[] = registry.map.get('boo');
43 | expect(registered.length).toEqual(2);
44 | expect(registered).toContain(handler1);
45 | expect(registered).toContain(handler2);
46 | });
47 |
48 | it('can register multiple events', () => {
49 | expect(registry.map.get('boo')).toBeUndefined();
50 | expect(registry.map.get('far')).toBeUndefined();
51 | registry.register('boo', handler1);
52 | registry.register('boo', handler2);
53 | registry.register('far', handler1);
54 | expect(registry.map.get('boo').length).toEqual(2);
55 | expect(registry.map.get('far').length).toEqual(1);
56 | });
57 |
58 | it('can retrieve handlers correctly', () => {
59 | registry.map.set('boo', [handler1]);
60 | registry.map.set('far', [handler1, handler2]);
61 | expect(registry.get('boo')).toEqual([handler1]);
62 | expect(registry.get('far')).toEqual([handler1, handler2]);
63 | expect(registry.get(['boo', 'far'])).toEqual([handler1, handler2]);
64 | expect(registry.get('baz')).toEqual([]);
65 | expect(registry.get(['boo', 'far', 'baz'])).toEqual([handler1, handler2]);
66 | });
67 |
68 | it('can unregister handlers correctly', () => {
69 | registry.map.set('boo', [handler1]);
70 | registry.map.set('far', [handler1, handler2]);
71 |
72 | // Unknown handler
73 | registry.unregister('far', () => { /* do nothing */ });
74 | expect(registry.get('far')).toEqual([handler1, handler2]);
75 |
76 | // Unknown event
77 | registry.unregister('baz', handler1);
78 | expect(registry.get('boo')).toEqual([handler1]);
79 | expect(registry.get('far')).toEqual([handler1, handler2]);
80 |
81 | // Success
82 | registry.unregister('boo', handler1);
83 | expect(registry.get('boo')).toEqual([]);
84 | registry.unregister('far', handler2);
85 | expect(registry.get('far')).toEqual([handler1]);
86 |
87 | // Clear
88 | registry.map.set('far', [handler1, handler2]);
89 | registry.unregister('far');
90 | expect(registry.get('far')).toEqual([]);
91 |
92 | // Multiple events
93 | registry.map.set('boo', [handler1]);
94 | registry.map.set('far', [handler1, handler2]);
95 | registry.unregister(['boo', 'far', 'baz'], handler1);
96 | expect(registry.get('boo')).toEqual([]);
97 | expect(registry.get('far')).toEqual([handler2]);
98 | });
99 |
100 | it('can unregister all handlers', () => {
101 | registry.map.set('boo', [handler1]);
102 | registry.map.set('far', [handler1, handler2]);
103 | expect(registry.get('boo')).toEqual([handler1]);
104 | expect(registry.get('far')).toEqual([handler1, handler2]);
105 |
106 | registry.unregisterAll();
107 |
108 | expect(registry.get('boo')).toEqual([]);
109 | expect(registry.get('far')).toEqual([]);
110 | });
111 |
112 | });
113 |
114 | }); };
115 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SaltyRTC JavaScript Client
2 |
3 | [](https://circleci.com/gh/saltyrtc/saltyrtc-client-js/tree/master)
4 | [](https://github.com/saltyrtc/saltyrtc-client-js)
5 | [](https://www.npmjs.com/package/@saltyrtc/client)
6 | [](https://www.npmjs.com/package/@saltyrtc/client)
7 | [](https://github.com/saltyrtc/saltyrtc-client-js)
8 | [](https://bestpractices.coreinfrastructure.org/projects/536)
9 | [](https://gitter.im/saltyrtc/Lobby)
10 |
11 | This is a [SaltyRTC](https://github.com/saltyrtc/saltyrtc-meta) v1
12 | implementation for JavaScript (ES5+) written in TypeScript.
13 |
14 | > :warning: **Note:** The SaltyRTC client libraries are in maintenance mode.
15 | > They will still receive bugfixes and regular maintenance, but if you want to
16 | > start using these libraries, be prepared that you will need to take over
17 | > maintenance at some point in time. (If you are interested in maintaining the
18 | > libraries, please let us know, our e-mails are in the README, section
19 | > "Security".)
20 |
21 | The library has been tested with Firefox 45+ and Chromium 49+.
22 |
23 | - [Docs](https://saltyrtc.github.io/saltyrtc-client-js/docs/)
24 | - [API Docs](https://saltyrtc.github.io/saltyrtc-client-js/apidocs/)
25 |
26 | ## Installing
27 |
28 | ### Via npm
29 |
30 | You can install this library via `npm`:
31 |
32 | npm install --save @saltyrtc/client msgpack-lite tweetnacl
33 |
34 | ### Manually
35 |
36 | Alternatively, copy one of the following files to your project directly:
37 |
38 | - ES2015: `dist/saltyrtc-client.es2015.js`
39 | - ES5: `dist/saltyrtc-client.es5.js`
40 | - ES5 minified: `dist/saltyrtc-client.es5.min.js`
41 |
42 | Make sure to manually add the following external dependencies to your project:
43 |
44 | - [tweetnacl](https://github.com/dchest/tweetnacl-js)
45 | - [msgpack-lite](https://github.com/kawanet/msgpack-lite)
46 |
47 | ## Usage
48 |
49 | See [Docs](https://saltyrtc.github.io/saltyrtc-client-js/docs/).
50 |
51 | ## Development
52 |
53 | Install dependencies:
54 |
55 | $ npm install
56 |
57 | To compile the TypeScript sources to a single JavaScript (ES5 / Minified ES5 / ES2015) file:
58 |
59 | $ npm run dist
60 |
61 | The resulting files will be located in `dist/`.
62 |
63 | ## Testing
64 |
65 | ### 1. Preparing the Server
66 |
67 | First, clone the `saltyrtc-server-python` repository.
68 |
69 | git clone https://github.com/saltyrtc/saltyrtc-server-python
70 | cd saltyrtc-server-python
71 |
72 | Then create a test certificate for localhost, valid for 5 years.
73 |
74 | openssl req \
75 | -newkey rsa:1024 \
76 | -x509 \
77 | -nodes \
78 | -keyout saltyrtc.key \
79 | -new \
80 | -out saltyrtc.crt \
81 | -subj /CN=localhost \
82 | -reqexts SAN \
83 | -extensions SAN \
84 | -config <(cat /etc/ssl/openssl.cnf \
85 | <(printf '[SAN]\nsubjectAltName=DNS:localhost')) \
86 | -sha256 \
87 | -days 1825
88 |
89 | You can import this file into your browser certificate store. For Chrome/Chromium, use this command:
90 |
91 | certutil -d sql:$HOME/.pki/nssdb -A -t "P,," -n saltyrtc-test-ca -i saltyrtc.crt
92 |
93 | Additionally, you need to open `chrome://flags/#allow-insecure-localhost` and
94 | enable it.
95 |
96 | In Firefox the easiest way to add your certificate to the browser is to start
97 | the SaltyRTC server (e.g. on `localhost` port 8765), then to visit the
98 | corresponding URL via https (e.g. `https://localhost:8765`). Then, in the
99 | certificate warning dialog that pops up, choose "Advanced" and add a permanent
100 | exception.
101 |
102 | Create a Python virtualenv with dependencies:
103 |
104 | python3 -m virtualenv venv
105 | venv/bin/pip install .[logging]
106 |
107 | Finally, start the server with the following test permanent key:
108 |
109 | export SALTYRTC_SERVER_PERMANENT_KEY=0919b266ce1855419e4066fc076b39855e728768e3afa773105edd2e37037c20 # Public: 09a59a5fa6b45cb07638a3a6e347ce563a948b756fd22f9527465f7c79c2a864
110 | venv/bin/saltyrtc-server -v 5 serve -p 8765 \
111 | -sc saltyrtc.crt -sk saltyrtc.key \
112 | -k $SALTYRTC_SERVER_PERMANENT_KEY
113 |
114 |
115 | ### 2. Running Tests
116 |
117 | To compile the test sources, run:
118 |
119 | $ npm run rollup_tests
120 |
121 | Then simply open `tests/testsuite.html` in your browser!
122 |
123 | Alternatively, run the tests automatically in Firefox and Chrome:
124 |
125 | $ npm test
126 |
127 |
128 | ### 3. Linting
129 |
130 | To run linting checks:
131 |
132 | npm run lint
133 |
134 | You can also install a pre-push hook to do the linting:
135 |
136 | echo -e '#!/bin/sh\nnpm run lint' > .git/hooks/pre-push
137 | chmod +x .git/hooks/pre-push
138 |
139 |
140 | ## Security
141 |
142 | ### Responsible Disclosure / Reporting Security Issues
143 |
144 | Please report security issues directly to one or both of the following contacts:
145 |
146 | - Danilo Bargen
147 | - Email: mail@dbrgn.ch
148 | - Threema: EBEP4UCA
149 | - GPG: [EA456E8BAF0109429583EED83578F667F2F3A5FA][keybase-dbrgn]
150 | - Lennart Grahl
151 | - Email: lennart.grahl@gmail.com
152 | - Threema: MSFVEW6C
153 | - GPG: [3FDB14868A2B36D638F3C495F98FBED10482ABA6][keybase-lgrahl]
154 |
155 | [keybase-dbrgn]: https://keybase.io/dbrgn
156 | [keybase-lgrahl]: https://keybase.io/lgrahl
157 |
158 | ## Coding Guidelines
159 |
160 | - Write clean ES2015
161 | - Favor `const` over `let`
162 |
163 | ## License
164 |
165 | MIT, see `LICENSE.md`.
166 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2016-2022 Threema GmbH
3 | *
4 | * This software may be modified and distributed under the terms
5 | * of the MIT license. See the `LICENSE.md` file for details.
6 | */
7 |
8 | import { ValidationError } from './exceptions';
9 |
10 | /**
11 | * Convert an Uint8Array to a hex string.
12 | *
13 | * Example:
14 | *
15 | * >>> u8aToHex(new Uint8Array([1, 255]))
16 | * "01ff"
17 | */
18 | export function u8aToHex(array: Uint8Array): string {
19 | const results: string[] = [];
20 | for (const arrayByte of array) {
21 | results.push(arrayByte.toString(16).replace(/^([\da-f])$/, '0$1'));
22 | }
23 | return results.join('');
24 | }
25 |
26 | /**
27 | * Convert a hexadecimal string to a Uint8Array.
28 | *
29 | * Example:
30 | *
31 | * >>> hexToU8a("01ff")
32 | * [1, 255]
33 | */
34 | export function hexToU8a(hexstring: string): Uint8Array {
35 | let array;
36 | let i;
37 | let j = 0;
38 | let k;
39 | let ref;
40 |
41 | // If number of characters is odd, add padding
42 | if (hexstring.length % 2 === 1) {
43 | hexstring = '0' + hexstring;
44 | }
45 |
46 | array = new Uint8Array(hexstring.length / 2);
47 | for (i = k = 0, ref = hexstring.length; k <= ref; i = k += 2) {
48 | array[j++] = parseInt(hexstring.substr(i, 2), 16);
49 | }
50 | return array;
51 | }
52 |
53 | /**
54 | * Convert a byte to its hex string representation.
55 | */
56 | export function byteToHex(value: number) {
57 | return '0x' + ('00' + value.toString(16)).substr(-2);
58 | }
59 |
60 | /**
61 | * Generate a NON CRYPTOGRAPHICALLY SECURE random string.
62 | *
63 | * Based on http://stackoverflow.com/a/1349426/284318.
64 | */
65 | export function randomString(
66 | length = 32,
67 | chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
68 | ): string {
69 | let str = '';
70 | for (let i = 0; i < length; i++) {
71 | str += chars.charAt(Math.floor(Math.random() * chars.length));
72 | }
73 | return str;
74 | }
75 |
76 | /**
77 | * Generate a random 32 bit unsigned integer.
78 | */
79 | export function randomUint32(): number {
80 | const crypto = window.crypto || (window as any).msCrypto;
81 | return crypto.getRandomValues(new Uint32Array(1))[0];
82 | }
83 |
84 | /**
85 | * Concatenate multiple Uint8Array objects.
86 | *
87 | * Based on http://www.2ality.com/2015/10/concatenating-typed-arrays.html
88 | */
89 | export function concat(...arrays: Uint8Array[]): Uint8Array {
90 | let totalLength = 0;
91 | for (const arr of arrays) {
92 | totalLength += arr.length;
93 | }
94 | const result = new Uint8Array(totalLength);
95 | let offset = 0;
96 | for (const arr of arrays) {
97 | result.set(arr, offset);
98 | offset += arr.length;
99 | }
100 | return result;
101 | }
102 |
103 | /**
104 | * Wait for a condition.
105 | *
106 | * @param test a function that tests whether the condition has been met.
107 | * @param delayMs wait duration between retries.
108 | * @param retries number of times to retry.
109 | * @param success the success callback.
110 | * @param error the error callback.
111 | */
112 | export function waitFor(test: () => boolean, delayMs: number, retries: number, success: () => any, error: () => any) {
113 | // If condition is not yet met, decrease number of retries and retry
114 | if (test() === false) {
115 | if (retries === 1) { // This is the last retry
116 | error();
117 | } else {
118 | setTimeout(() => waitFor(test, delayMs, retries - 1, success, error), delayMs);
119 | }
120 | return;
121 | }
122 |
123 | // Otherwise, run success callback.
124 | success();
125 | }
126 |
127 | /**
128 | * Determine whether a value is a string.
129 | */
130 | export function isString(value: any): value is string {
131 | return typeof value === 'string' || value instanceof String;
132 | }
133 |
134 | /**
135 | * Validate a 32 byte key. Return the validated key as a Uint8Array instance.
136 | *
137 | * @param key Either an Uint8Array or a hex string.
138 | * @param name Name of the key for the exception.
139 | * @throws ValidationError if key is invalid.
140 | */
141 | export function validateKey(key: Uint8Array | string, name = 'Key'): Uint8Array {
142 | // Validate type
143 | let out: Uint8Array;
144 | if (isString(key)) {
145 | if (key.length !== 64) {
146 | throw new ValidationError(name + ' must be 32 bytes long');
147 | }
148 | out = hexToU8a(key);
149 | } else if (key instanceof Uint8Array) {
150 | out = key;
151 | } else {
152 | throw new ValidationError(name + ' must be an Uint8Array or a hex string');
153 | }
154 |
155 | // Validate length
156 | if (out.byteLength !== 32) {
157 | throw new ValidationError(name + ' must be 32 bytes long');
158 | }
159 |
160 | return out;
161 | }
162 |
163 | /**
164 | * Compare two Uint8Array instances. Return true if all elements are equal (compared using ===).
165 | */
166 | export function arraysAreEqual(a1: Uint8Array, a2: Uint8Array): boolean {
167 | if (a1.length !== a2.length) {
168 | return false;
169 | }
170 | for (let i = 0; i < a1.length; i++) {
171 | if (a1[i] !== a2[i]) {
172 | return false;
173 | }
174 | }
175 | return true;
176 | }
177 |
178 | /**
179 | * Convert a TypedArray to an ArrayBuffer.
180 | *
181 | * **Important:** If the source array's data occupies the underlying buffer
182 | * completely, the underlying buffer will be returned directly. Thus, the
183 | * caller may not assume that the data has been copied.
184 | */
185 | export function arrayToBuffer(array: ArrayBufferView): ArrayBuffer {
186 | if (array.byteOffset === 0 && array.byteLength === array.buffer.byteLength) {
187 | return array.buffer;
188 | }
189 | return array.buffer.slice(array.byteOffset, array.byteOffset + array.byteLength);
190 | }
191 |
--------------------------------------------------------------------------------
/tests/utils.spec.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:file-header
2 | // tslint:disable:no-reference
3 | ///
4 |
5 | import {
6 | arraysAreEqual, byteToHex, concat, hexToU8a, isString,
7 | randomString, randomUint32, u8aToHex, waitFor,
8 | } from '../src/utils';
9 |
10 | export default () => { describe('utils', function() {
11 |
12 | describe('hexToU8a / u8aToHex', function() {
13 |
14 | it('conversion from Uint8Array to hex works', () => {
15 | const source = new Uint8Array([0x01, 0x10, 0xde, 0xad, 0xbe, 0xef]);
16 | expect(u8aToHex(source)).toEqual('0110deadbeef');
17 | });
18 |
19 | it('conversion from hex to Uint8Array works', () => {
20 | const expected = new Uint8Array([0x01, 0x10, 0xde, 0xad, 0xbe, 0xef]);
21 | expect(hexToU8a('0110deadbeef')).toEqual(expected);
22 | });
23 |
24 | it('u8a -> hex -> ua8 works properly', () => {
25 | const source = new Uint8Array([0x01, 0x10, 0xde, 0xad, 0xbe, 0xef]);
26 | expect(hexToU8a(u8aToHex(source))).toEqual(source);
27 | });
28 |
29 | it('hex -> u8a -> hex works properly', () => {
30 | const source = 'f00baa';
31 | expect(u8aToHex(hexToU8a(source))).toEqual(source);
32 | });
33 |
34 | it('single-character conversion from hex to Uint8Array works', () => {
35 | expect(hexToU8a('a')).toEqual(new Uint8Array([0x0a]));
36 | });
37 |
38 | });
39 |
40 | describe('randomString', function() {
41 |
42 | it('generates a 32 character random string', () => {
43 | const random1 = randomString();
44 | const random2 = randomString();
45 | expect(random1 !== random2).toBe(true);
46 | expect(random1.length).toEqual(random2.length);
47 | expect(random1.length).toEqual(32);
48 | });
49 |
50 | });
51 |
52 | describe('concat', function() {
53 |
54 | it('does not change a single array', () => {
55 | const src = Uint8Array.of(1, 2, 3, 4);
56 | expect(concat(src)).toEqual(src);
57 | });
58 |
59 | it('concatenates two arrays', () => {
60 | const src1 = Uint8Array.of(1, 2, 3, 4);
61 | const src2 = Uint8Array.of(5, 6);
62 | expect(concat(src1, src2))
63 | .toEqual(Uint8Array.of(1, 2, 3, 4, 5, 6));
64 | });
65 |
66 | it('concatenates multiple arrays', () => {
67 | const src1 = Uint8Array.of(1, 2, 3, 4);
68 | const src2 = Uint8Array.of(5, 6);
69 | const src3 = Uint8Array.of(7);
70 | const src4 = Uint8Array.of(7, 8, 9);
71 | expect(concat(src1, src2, src3, src4))
72 | .toEqual(Uint8Array.of(1, 2, 3, 4, 5, 6, 7, 7, 8, 9));
73 | });
74 |
75 | });
76 |
77 | describe('randomUint32', function() {
78 |
79 | it('generates a random number between 0 and 2**32', () => {
80 | let lastNum: number = null;
81 | for (let i = 0; i < 50; i++) {
82 | const num = randomUint32();
83 | expect(num).not.toEqual(lastNum);
84 | expect(num).toBeGreaterThan(-1);
85 | expect(num).toBeLessThan(0x100000000 + 1);
86 | lastNum = num;
87 | }
88 | });
89 |
90 | });
91 |
92 | describe('byteToHex', function() {
93 |
94 | it('converts 0 to 0x00', () => {
95 | expect(byteToHex(0)).toEqual('0x00');
96 | });
97 |
98 | it('converts 9 to 0x09', () => {
99 | expect(byteToHex(9)).toEqual('0x09');
100 | });
101 |
102 | it('converts 10 to 0x0a', () => {
103 | expect(byteToHex(10)).toEqual('0x0a');
104 | });
105 |
106 | it('converts 255 to 0xff', () => {
107 | expect(byteToHex(255)).toEqual('0xff');
108 | });
109 |
110 | });
111 |
112 | describe('waitFor', function() {
113 |
114 | it('retries until the condition is met', (done: any) => {
115 | let i = 3;
116 | // To test, this condition has a side effect.
117 | // It will return true the 4th time it is called.
118 | const test = () => {
119 | i--;
120 | return i < 0;
121 | };
122 | waitFor(test, 20, 10, () => {
123 | expect(i).toBe(-1);
124 | done();
125 | }, done.fail);
126 | });
127 |
128 | it('fails if the condition is not met', (done: any) => {
129 | let tries = 0;
130 | const test = () => {
131 | tries += 1;
132 | return false;
133 | };
134 | waitFor(test, 20, 3, done.fail, () => {
135 | expect(tries).toBe(3);
136 | done();
137 | });
138 | });
139 |
140 | });
141 |
142 | describe('isString', function() {
143 | it('detects strings', () => {
144 | expect(isString('hello')).toEqual(true);
145 | // tslint:disable-next-line:no-construct
146 | expect(isString(new String('hello'))).toEqual(true);
147 | expect(isString(String)).toEqual(false);
148 | expect(isString(1232)).toEqual(false);
149 | });
150 | });
151 |
152 | describe('arraysAreEqual', function() {
153 | it('returns false when arrays have different length', () => {
154 | expect(arraysAreEqual(Uint8Array.of(1, 1), Uint8Array.of(1))).toEqual(false);
155 | });
156 |
157 | it('returns true when arrays are both empty', () => {
158 | expect(arraysAreEqual(new Uint8Array([]), new Uint8Array([]))).toEqual(true);
159 | });
160 |
161 | it('returns false when arrays are different', () => {
162 | expect(arraysAreEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 3, 2]))).toEqual(false);
163 | });
164 |
165 | it('returns true when arrays are the same', () => {
166 | expect(arraysAreEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3]))).toEqual(true);
167 | });
168 | });
169 |
170 | }); };
171 |
--------------------------------------------------------------------------------
/docs/docs/usage.md:
--------------------------------------------------------------------------------
1 | # Usage
2 |
3 | This chapter gives a short introduction on how to use the SaltyRTC JavaScript
4 | client.
5 |
6 | To see a more practical example, you may also want to take a look at our [demo
7 | application](https://github.com/saltyrtc/saltyrtc-demo).
8 |
9 | ## The SaltyRTCBuilder
10 |
11 | To initialize a SaltyRTC client instance, you can use the `SaltyRTCBuilder`.
12 |
13 | ```javascript
14 | const builder = new saltyrtcClient.SaltyRTCBuilder();
15 | ```
16 |
17 | ### Connection Info
18 |
19 | Then you need to provide connection info:
20 |
21 | ```javascript
22 | const host = 'server.saltyrtc.org';
23 | const port = 9287;
24 | builder.connectTo(host, port);
25 | ```
26 |
27 | For testing, you can use [our test server](https://saltyrtc.org/pages/getting-started.html).
28 |
29 | ### Key Store
30 |
31 | The client needs to have its own public/private keypair. Create a new keypair
32 | with the `KeyStore` class:
33 |
34 | ```javascript
35 | const keyStore = new saltyrtcClient.KeyStore();
36 | builder.withKeyStore(keyStore);
37 | ```
38 |
39 | ### Server Key Pinning
40 |
41 | If you want to use server key pinning, specify the server public permanent key:
42 |
43 | ```javascript
44 | const serverPublicPermanentKey = '424280166304526b4a2874a2270d091071fcc5c98959f7d4718715626df26204';
45 | builder.withServerKey(serverPublicPermanentKey);
46 | ```
47 |
48 | The public key can either be passed in as `Uint8Array` or as hex-encoded string.
49 |
50 | ### Websocket Ping Interval
51 |
52 | Optionally, you can specify a Websocket ping interval in seconds:
53 |
54 | ```javascript
55 | builder.withPingInterval(30);
56 | ```
57 |
58 | ### Task Configuration
59 |
60 | You must initialize SaltyRTC with a task (TODO: Link to tasks documentation)
61 | that takes over after the handshake is done.
62 |
63 | For example, when using the [WebRTC
64 | task](https://github.com/saltyrtc/saltyrtc-task-webrtc-js):
65 |
66 | ```javascript
67 | const doHandover = true;
68 | const maxPacketSize = 16384;
69 | const webrtcTask = new saltyrtcTaskWebrtc.WebRTCTask(doHandover, maxPacketSize);
70 | builder.usingTasks([webrtcTask]);
71 | ```
72 |
73 | ### Connecting as Initiator
74 |
75 | If you want to connect to the server as initiator, you can use the
76 | `.asInitiator()` method:
77 |
78 | ```javascript
79 | const client = builder.asInitiator();
80 | ```
81 |
82 | ### Connecting as Responder
83 |
84 | If you want to connect as responder, you need to provide the initiator
85 | information first that you have obtained from the initiator.
86 |
87 | ```javascript
88 | builder.initiatorInfo(initiatorPublicPermanentKey, initiatorAuthToken);
89 | const client = builder.asResponder();
90 | ```
91 |
92 | Both the initiator public permanent key as well as the initiator auth token can
93 | be either `Uint8Array` instances or hex-encoded strings.
94 |
95 | ## Full Example
96 |
97 | All methods on the `SaltyRTCBuilder` support chaining. Here's a full example of
98 | an initiator configuration:
99 |
100 | ```javascript
101 | const config = {
102 | SALTYRTC_HOST: 'server.saltyrtc.org',
103 | SALTYRTC_PORT: 9287,
104 | SALTYRTC_SERVER_PUBLIC_KEY: '424280166304526b4a2874a2270d091071fcc5c98959f7d4718715626df26204',
105 | };
106 | const client = new saltyrtcClient.SaltyRTCBuilder()
107 | .connectTo(config.SALTYRTC_HOST, config.SALTYRTC_PORT)
108 | .withServerKey(config.SALTYRTC_SERVER_PUBLIC_KEY)
109 | .withKeyStore(new saltyrtcClient.KeyStore())
110 | .usingTasks([new saltyrtcTaskWebrtc.WebRTCTask(true, 16384)])
111 | .withPingInterval(30)
112 | .asInitiator();
113 | ```
114 |
115 | To see a more practical example, you may also want to take a look at our [demo
116 | application](https://github.com/saltyrtc/saltyrtc-demo).
117 |
118 | ## Events
119 |
120 | You can register callbacks for certain events:
121 |
122 | initiator.on('handover', () => console.log('Handover is done'));
123 | responder.on('state-change', (newState) => console.log('New signaling state:', newState));
124 |
125 | The following events are available:
126 |
127 | - `state-change(saltyrtcClient.SignalingState)`: The signaling state changed.
128 | - `state-change:(void)`: The signaling state change event, filtered by state.
129 | - `new-responder(responderId)`: A responder has connected. This event is only dispatched for the initiator.
130 | - `application(data)`: An application message has arrived.
131 | - `peer-disconnected(peerId)`: A previously authenticated peer has disconnected from the server.
132 | - `handover(void)`: The handover to the data channel is done.
133 | - `signaling-connection-lost(responderId)`: The signaling connection to the specified peer was lost.
134 | - `no-shared-task(taskInfo)`: No shared task was found. This event is emitted by the initiator.
135 | - `connection-closed(closeCode)`: The connection was closed.
136 | - `connection-error(ErrorEvent)`: A websocket connection error occured.
137 |
138 | ## Trusted Keys
139 |
140 | In order to reconnect to a session using a trusted key, you first need to
141 | restore your `KeyStore` with the private permanent key originally used to
142 | establish the trusted session:
143 |
144 | ```javascript
145 | const keyStore = new saltyrtcClient.KeyStore(ourPrivatePermanentKey);
146 | ```
147 |
148 | The private key can be passed in either as `Uint8Array` or as hex-encoded string.
149 |
150 | Then, on the `SaltyRTCBuilder` instance, set the trusted peer key:
151 |
152 | ```javascript
153 | builder.withTrustedPeerKey(peerPublicPermanentKey);
154 | ```
155 |
156 | The public key can be passed in either as `Uint8Array` or as hex-encoded string.
157 |
158 | ## Dynamically Determine Server Connection Info
159 |
160 | Instead of specifying the SaltyRTC server host and port directly, you can
161 | instead provide an implementation of a `ServerInfoFactory` that can dynamically
162 | determine the connection info based on the public key of the initiator.
163 |
164 | The signature of the function must look like this:
165 |
166 | ```typescript
167 | (initiatorPublicKey: string) => { host: string, port: number }
168 | ```
169 |
170 | Example:
171 |
172 | ```javascript
173 | builder.connectWith((initiatorPublicKey) => {
174 | let host;
175 | if (initiatorPublicKey.startsWith('a')) {
176 | host = 'a.example.org';
177 | } else {
178 | host = 'other.example.org';
179 | }
180 | return {
181 | host: host,
182 | port: 8765,
183 | }
184 | });
185 | ```
186 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | This project follows semantic versioning.
4 |
5 | Possible log types:
6 |
7 | - `[added]` for new features.
8 | - `[changed]` for changes in existing functionality.
9 | - `[deprecated]` for once-stable features removed in upcoming releases.
10 | - `[removed]` for deprecated features removed in this release.
11 | - `[fixed]` for any bug fixes.
12 | - `[security]` to invite users to upgrade in case of vulnerabilities.
13 |
14 |
15 | ### v0.15.1 (2022-03-22)
16 |
17 | - [fixed] Change exported CloseCode from const enum to class (#138)
18 |
19 | ### v0.15.0 (2022-02-11)
20 |
21 | - [fixed] Prevent client reuse (#123)
22 | - [fixed] Validate length of server public key hexstring (#122)
23 | - [removed] Removed the polyfilled ES5 bundle from the distribution files
24 |
25 | ### v0.14.4 (2019-06-12)
26 |
27 | - [fixed] Avoid protocol errors when handling 'new-initiator' and
28 | 'new-responder' messages
29 |
30 | ### v0.14.3 (2019-02-28)
31 |
32 | - [fixed] Signature of `KeyStore`'s constructor was incorrect
33 |
34 | ### v0.14.2 (2019-02-25)
35 |
36 | - [fixed] Actually expose all exception classes
37 |
38 | ### v0.14.1 (2019-02-25)
39 |
40 | - [added] Expose all exception classes
41 |
42 | ### v0.14.0 (2019-02-18)
43 |
44 | - [changed] Use `Uint8Array` instead of `ArrayBuffer` in the public API
45 | - [removed] Removed obsolete methods `Cookie.asArrayBuffer` and
46 | `Cookie.fromArrayBuffer`
47 |
48 | ### v0.13.2 (2018-10-04)
49 |
50 | - [fixed] Exposed `Log.level` attribute
51 |
52 | ### v0.13.1 (2018-10-04)
53 |
54 | - [fixed] Exposed `Log` class
55 |
56 | ### v0.13.0 (2018-09-27)
57 |
58 | - [added] Add possibility to unbind all events when disconnecting
59 | - [added] Introduce log level to builder
60 |
61 | ### v0.12.4 (2018-08-21)
62 |
63 | - [fixed] Updated type declarations
64 |
65 | ### v0.12.3 (2018-08-21)
66 |
67 | - [added] Allow clearing all event handlers at once (#106)
68 |
69 | ### v0.12.2 (2018-07-31)
70 |
71 | - [added] Expose encrypt/decrypt methods on signaling instance (#105)
72 |
73 | ### v0.12.1 (2018-07-26)
74 |
75 | - [security] Fix bug in CSN calculation (#103)
76 | - [fixed] Add `SaltyRTC.getCurrentPeerCsn` to type declarations
77 |
78 | **Security Fix**
79 |
80 | [#103](https://github.com/saltyrtc/saltyrtc-client-js/pull/103)
81 |
82 | Apparently JavaScript treats all operands in bitwise operations as 32 bit
83 | signed integers. This results in `(1 << 32)` being equal to `1`. This means
84 | that previously the calculation of the combined sequence number would be
85 | incorrect if the overflow number is larger than 0.
86 |
87 | In theory this is a security issue, however it may only be a problem in the
88 | real world if you send more than 4'294'967'295 messages with the same
89 | connection, which is quite unlikely. However, we definitely recommend upgrading
90 | to the latest version of `@saltyrtc/client`.
91 |
92 | ### v0.12.0 (2018-07-25)
93 |
94 | - [added] Introduce method to extract current CSN
95 | - [changed] Replace thrown strings with exceptions (#97)
96 | - [changed] Crypto performance improvements (#99)
97 | - [changed] Upgrade npm dependencies (#100)
98 |
99 | ### v0.11.3 (2018-05-17)
100 |
101 | - [added] Emit 'no-shared-task' event when no shared task could be found (#93)
102 |
103 | ### v0.11.2 (2018-05-08)
104 |
105 | - [fixed] Handle disconnected messages during peer handshake
106 |
107 | ### v0.11.1 (2018-05-03)
108 |
109 | - [changed] 'disconnected' messages are now emitted as events to the user,
110 | not as callback to the task (#92)
111 | - [fixed] Fix processing of 'disconnected' messages
112 | - [fixed] Accept server messages during/after peer handshake
113 | - [fixed] If message nonce has an invalid source, discard it
114 |
115 | ### v0.11.0 (2018-03-13)
116 |
117 | - [fixed] SaltyRTC.authTokenHex: Add null checks
118 | - [added] Support for 'disconnected' messages (#89)
119 | - [changed] `Task` interface: Add `onDisconnected` method (#90)
120 | - [changed] Only pass task messages to task if supported
121 | - [changed] Add tslint to the codebase (#88)
122 |
123 | ### v0.10.1 (2018-02-28)
124 |
125 | - [changed] Upgrade TypeScript to 2.7, make some types more specific
126 | - [removed] Remove deprecated `InternalError` function
127 |
128 | ### v0.10.0 (2017-09-26)
129 |
130 | - [fixed] Fix type signature in SaltyRTC.asResponder
131 | - [changed] Upgrade tweetnacl to 1.0.0
132 | - [changed] Move npmjs.org package to organization (it's now called
133 | `@saltyrtc/client`, not `saltyrtc-client`)
134 | - [changed] Update docs
135 |
136 | ### v0.9.1 (2017-02-13)
137 |
138 | - [changed] Updated logging format
139 |
140 | ### v0.9.0 (2017-02-07)
141 |
142 | This release can be considered a release candidate for 1.0.0.
143 |
144 | - [changed] Change subprotocol to `v1.saltyrtc.org` (#59)
145 | - [changed] The `KeyStore` class constructor now only requires the private key,
146 | not both the public and private key (#73)
147 | - [added] Add new close code: 3007 Invalid Key (#58)
148 | - [added] Add support for multiple server permanent keys (#58)
149 | - [changed] Better error logs in the case of signaling errors (#78)
150 |
151 | ### v0.5.1 (2016-12-13)
152 |
153 | - [changed] Make tweetnacl / msgpack-lite peer dependencies
154 |
155 | ### v0.5.0 (2016-12-12)
156 |
157 | - [added] Implement dynamic server endpoints (#70)
158 | - [fixed] Never explicitly close WebSocket with 1002 (#75)
159 | - [fixed] Send close message on disconnect in task state (#68)
160 | - [fixed] Catch nonce validation errors
161 | - [fixed] Only re-throw top level exceptions if unhandled
162 | - [fixed] Don't use decryptFromPeer method in onSignalingMessage
163 | - [changed] Remove restart message handling (#69)
164 |
165 | ### v0.4.1 (2016-11-14)
166 |
167 | - [added] Implement support for application messages (#61)
168 | - [fixed] Set state to "closing" when starting disconnect
169 | - [fixed] Fix inverted condition when handling signaling errors
170 |
171 | ### v0.4.0 (2016-11-09)
172 |
173 | - [added] Support passing server public key to SaltyRTCBuilder (#59)
174 | - [added] Implement support for send-error messages (#14)
175 | - [added] Drop inactive responders (#55)
176 | - [fixed] Always emit connection-closed event on websocket close
177 | - [fixed] Properly handle protocol/signaling errors (#53)
178 | - [fixed] Don't allow calling both `.initiatorInfo` and `.asInitiator` on `SaltyRTCBuilder`
179 |
180 | ### v0.3.1 (2016-11-07)
181 |
182 | - [fixed] Send signaling messages to the task without encrypting (#58)
183 | - [fixed] Close websocket after handshake (#57)
184 |
185 | ### v0.3.0 (2016-11-02)
186 |
187 | - [added] The `KeyStore` and `SaltyRTCBuilder` interfaces now accept hex strings as keys
188 | - [added] The `SaltyRTCBuilder` now supports the `withPingInterval(...)` method
189 | - [added] Notify client on all disconnects
190 | - [changed] The connection-closed event now has the reason code as payload
191 | - [changed] Many refactorings
192 |
193 | ### v0.2.7 (2016-10-20)
194 |
195 | - [added] Add HandoverState helper class
196 | - [fixed] Check peer handover state when receiving ws message
197 |
198 | ### v0.2.6 (2016-10-19)
199 |
200 | - [fixed] Extend type declarations with missing static types
201 | - [changed] Change iife dist namespace to `saltyrtcClient`
202 |
203 | ### v0.2.5 (2016-10-19)
204 |
205 | - [fixed] Fix filename of polyfilled dist file
206 |
207 | ### v0.2.4 (2016-10-18)
208 |
209 | - [fixed] Use interface types for KeyStore and AuthToken
210 | - [fixed] Fix path to ES6 polyfill
211 |
212 | ### v0.2.3 (2016-10-18)
213 |
214 | - [fixed] Use interface types in SaltyRTCBuilder
215 | - [changed] Move type declarations to root directory
216 |
217 | ### v0.2.2 (2016-10-18)
218 |
219 | - [fixed] Fix sending of signaling messages after handshake
220 | - [added] Expose close codes and exceptions
221 |
222 | ### v0.2.1 (2016-10-17)
223 |
224 | - [added] Make saltyrtc.messages.TaskMessage an open interface
225 |
226 | ### v0.2.0 (2016-10-17)
227 |
228 | - [changed] Rename saltyrtc/ directory to src/
229 |
230 | ### v0.1.9 (2016-10-17)
231 |
232 | - [added] Add "typings" field to package.json
233 |
234 | ### v0.1.8 (2016-10-17)
235 |
236 | - [changed] Make polyfills in ES5 distribution optional
237 |
238 | ### v0.1.7 (2016-10-13)
239 |
240 | - [changed] Build ES2015 version as ES module, not as IIFE
241 |
242 | ### v0.1.6 (2016-10-13)
243 |
244 | - [changed] Improved packaging
245 |
246 | ### v0.1.5 (2016-10-13)
247 |
248 | - [changed] Internal cleanup
249 |
250 | ### v0.1.4 (2016-10-13)
251 |
252 | - [fixed] Fix exposed classes in `main.ts`
253 |
254 | ### v0.1.3 (2016-10-13)
255 |
256 | - [added] Create `CombinedSequencePair` class
257 |
258 | ### v0.1.2 (2016-10-13)
259 |
260 | - [added] Expose `Cookie` and `CookiePair` classes
261 | - [added] Expose `CombinedSequence` class
262 |
263 | ### v0.1.1 (2016-10-12)
264 |
265 | - [added] Expose `EventRegistry` class
266 |
267 | ### v0.1.0 (2016-10-12)
268 |
269 | - Initial release
270 |
--------------------------------------------------------------------------------
/tests/performance/crypto.spec.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:file-header
2 | // tslint:disable:no-reference
3 | ///
4 |
5 | import { KeyStore } from '../../src/keystore';
6 | import { Config } from '../config';
7 | import { testData } from './utils';
8 |
9 | export default () => {
10 | describe('crypto', () => {
11 | describe('Main Thread (shared key store=false)', () => {
12 | it(`encrypt ${Config.CRYPTO_ITERATIONS} times`, () => {
13 | const keyStore = new KeyStore();
14 | const publicKey = keyStore.publicKeyBytes;
15 | const start = performance.now();
16 |
17 | for (let i = 0; i <= Config.CRYPTO_ITERATIONS; ++i) {
18 | keyStore.encrypt(testData.bytes, testData.nonce, publicKey);
19 | }
20 |
21 | const end = performance.now();
22 | console.info(`Took ${(end - start) / 1000} seconds`);
23 | expect(0).toBe(0);
24 | }, 30000);
25 |
26 | it(`decrypt ${Config.CRYPTO_ITERATIONS} times`, () => {
27 | const keyStore = new KeyStore();
28 | const publicKey = keyStore.publicKeyBytes;
29 | const box = keyStore.encrypt(testData.bytes, testData.nonce, publicKey);
30 | const start = performance.now();
31 |
32 | for (let i = 0; i <= Config.CRYPTO_ITERATIONS; ++i) {
33 | keyStore.decrypt(box, publicKey);
34 | }
35 |
36 | const end = performance.now();
37 | console.info(`Took ${(end - start) / 1000} seconds`);
38 | expect(0).toBe(0);
39 | }, 30000);
40 | });
41 |
42 | describe('Main Thread (shared key store=true)', () => {
43 | it(`encrypt ${Config.CRYPTO_ITERATIONS} times`, () => {
44 | const keyStore = new KeyStore();
45 | const sharedKeyStore = keyStore.getSharedKeyStore(keyStore.publicKeyBytes);
46 | const start = performance.now();
47 |
48 | for (let i = 0; i <= Config.CRYPTO_ITERATIONS; ++i) {
49 | sharedKeyStore.encrypt(testData.bytes, testData.nonce);
50 | }
51 |
52 | const end = performance.now();
53 | console.info(`Took ${(end - start) / 1000} seconds`);
54 | expect(0).toBe(0);
55 | }, 30000);
56 |
57 | it(`decrypt ${Config.CRYPTO_ITERATIONS} times`, () => {
58 | const keyStore = new KeyStore();
59 | const sharedKeyStore = keyStore.getSharedKeyStore(keyStore.publicKeyBytes);
60 | const box = sharedKeyStore.encrypt(testData.bytes, testData.nonce);
61 | const start = performance.now();
62 |
63 | for (let i = 0; i <= Config.CRYPTO_ITERATIONS; ++i) {
64 | sharedKeyStore.decrypt(box);
65 | }
66 |
67 | const end = performance.now();
68 | console.info(`Took ${(end - start) / 1000} seconds`);
69 | expect(0).toBe(0);
70 | }, 30000);
71 | });
72 |
73 | [false, true].forEach((useSharedKeyStore) => {
74 | describe(`Web Worker (shared key store=${useSharedKeyStore}, transferables=false)`, () => {
75 | it(`encrypt ${Config.CRYPTO_ITERATIONS} times`, (done: any) => {
76 | expect(Worker).toBeDefined();
77 | const worker = new Worker('performance/crypto.worker.js');
78 | let iterations = 0;
79 | worker.onmessage = () => {
80 | ++iterations;
81 |
82 | // All encryption tasks resolved?
83 | if (iterations === Config.CRYPTO_ITERATIONS) {
84 | worker.terminate();
85 | const end = performance.now();
86 | console.info(`Took ${(end - start) / 1000} seconds`);
87 | done();
88 | }
89 | };
90 |
91 | // Initialise worker as an encrypt worker
92 | const keyStore = new KeyStore();
93 | worker.postMessage({
94 | type: 'encrypt',
95 | secretKey: keyStore.secretKeyBytes,
96 | useSharedKeyStore: useSharedKeyStore,
97 | });
98 | const start = performance.now();
99 |
100 | // Enqueue encryption tasks
101 | for (let i = 0; i <= Config.CRYPTO_ITERATIONS; ++i) {
102 | worker.postMessage({
103 | bytes: testData.bytes,
104 | nonce: testData.nonce,
105 | });
106 | }
107 | }, 30000);
108 |
109 | it(`decrypt ${Config.CRYPTO_ITERATIONS} times`, (done: any) => {
110 | expect(Worker).toBeDefined();
111 | const worker = new Worker('performance/crypto.worker.js');
112 | let iterations = 0;
113 | worker.onmessage = () => {
114 | ++iterations;
115 |
116 | // All decryption tasks resolved?
117 | if (iterations === Config.CRYPTO_ITERATIONS) {
118 | worker.terminate();
119 | const end = performance.now();
120 | console.info(`Took ${(end - start) / 1000} seconds`);
121 | done();
122 | }
123 | };
124 |
125 | // Initialise worker as a decrypt worker
126 | const keyStore = new KeyStore();
127 | const sharedKeyStore = keyStore.getSharedKeyStore(keyStore.publicKeyBytes);
128 | const box = sharedKeyStore.encrypt(testData.bytes, testData.nonce);
129 | worker.postMessage({
130 | type: 'decrypt',
131 | secretKey: keyStore.secretKeyBytes,
132 | useSharedKeyStore: useSharedKeyStore,
133 | });
134 | const start = performance.now();
135 |
136 | // Enqueue encryption tasks
137 | for (let i = 0; i <= Config.CRYPTO_ITERATIONS; ++i) {
138 | worker.postMessage({
139 | bytes: box.data,
140 | nonce: box.nonce,
141 | });
142 | }
143 | }, 30000);
144 | });
145 |
146 | describe(`Web Worker (shared key store=${useSharedKeyStore}, transferables=true)`, () => {
147 | it(`encrypt ${Config.CRYPTO_ITERATIONS} times`, (done: any) => {
148 | expect(Worker).toBeDefined();
149 |
150 | const worker = new Worker('performance/crypto.worker.js');
151 | let iterations = 0;
152 | worker.onmessage = () => {
153 | ++iterations;
154 |
155 | // All encryption tasks resolved?
156 | if (iterations === Config.CRYPTO_ITERATIONS) {
157 | worker.terminate();
158 | const end = performance.now();
159 | console.info(`Took ${(end - start) / 1000} seconds`);
160 | done();
161 | }
162 | };
163 |
164 | // Initialise worker as an encrypt worker
165 | const keyStore = new KeyStore();
166 | const testDataArray = Array.from({ length: Config.CRYPTO_ITERATIONS }, () => {
167 | // Need to copy the plain data, so it can be transferred
168 | return testData.bytes.slice(0);
169 | });
170 | worker.postMessage({
171 | type: 'encrypt-transferable',
172 | secretKey: keyStore.secretKeyBytes,
173 | useSharedKeyStore: useSharedKeyStore,
174 | });
175 | const start = performance.now();
176 |
177 | // Enqueue encryption tasks
178 | for (const testData1 of testDataArray) {
179 | expect(testData1.buffer.byteLength).toBeGreaterThan(0);
180 | worker.postMessage({
181 | bytes: testData1,
182 | nonce: testData.nonce,
183 | }, [testData1.buffer]);
184 | expect(testData1.buffer.byteLength).toBe(0);
185 | expect(testData.nonce.buffer.byteLength).toBeGreaterThan(0);
186 | }
187 | }, 60000);
188 |
189 | it(`decrypt ${Config.CRYPTO_ITERATIONS} times`, (done: any) => {
190 | expect(Worker).toBeDefined();
191 |
192 | const worker = new Worker('performance/crypto.worker.js');
193 | let iterations = 0;
194 | worker.onmessage = () => {
195 | ++iterations;
196 |
197 | // All decryption tasks resolved?
198 | if (iterations === Config.CRYPTO_ITERATIONS) {
199 | worker.terminate();
200 | const end = performance.now();
201 | console.info(`Took ${(end - start) / 1000} seconds`);
202 | done();
203 | }
204 | };
205 |
206 | // Initialise worker as a decrypt worker
207 | const keyStore = new KeyStore();
208 | const sharedKeyStore = keyStore.getSharedKeyStore(keyStore.publicKeyBytes);
209 | const boxes = Array.from({ length: Config.CRYPTO_ITERATIONS }, () => {
210 | // Need to generate new data, so it can be transferred
211 | return sharedKeyStore.encrypt(testData.bytes, testData.nonce);
212 | });
213 | worker.postMessage({
214 | type: 'decrypt-transferable',
215 | secretKey: keyStore.secretKeyBytes,
216 | useSharedKeyStore: useSharedKeyStore,
217 | });
218 | const start = performance.now();
219 |
220 | // Enqueue encryption tasks
221 | for (const box of boxes) {
222 | expect(box.data.buffer.byteLength).toBeGreaterThan(0);
223 | worker.postMessage({
224 | bytes: box.data,
225 | nonce: box.nonce,
226 | }, [box.data.buffer]);
227 | expect(box.data.buffer.byteLength).toBe(0);
228 | expect(box.nonce.buffer.byteLength).toBeGreaterThan(0);
229 | }
230 | }, 60000);
231 | });
232 | });
233 | });
234 | };
235 |
--------------------------------------------------------------------------------
/src/keystore.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2016-2022 Threema GmbH
3 | *
4 | * This software may be modified and distributed under the terms
5 | * of the MIT license. See the `LICENSE.md` file for details.
6 | */
7 |
8 | import * as nacl from 'tweetnacl';
9 | import { CryptoError } from './exceptions';
10 | import { Log } from './log';
11 | import { u8aToHex, validateKey } from './utils';
12 |
13 | /**
14 | * A `Box` contains a nonce and encrypted data.
15 | */
16 | export class Box implements saltyrtc.Box {
17 |
18 | private _nonce: Uint8Array;
19 | private _nonceLength: number;
20 | private _data: Uint8Array;
21 |
22 | constructor(nonce: Uint8Array, data: Uint8Array, nonceLength: number) {
23 | this._nonce = nonce;
24 | this._nonceLength = nonceLength;
25 | this._data = data;
26 | }
27 |
28 | public get length(): number {
29 | return this._nonce.length + this._data.length;
30 | }
31 |
32 | public get data() {
33 | return this._data;
34 | }
35 |
36 | public get nonce(): Uint8Array {
37 | return this._nonce;
38 | }
39 |
40 | /**
41 | * Parse an Uint8Array, create a Box wrapping the data.
42 | *
43 | * May throw CryptoError instances with the following codes:
44 | *
45 | * - bad-message-length: Message is shorter than the nonce
46 | */
47 | public static fromUint8Array(array: Uint8Array, nonceLength: number) {
48 | // Validate nonceLength parameter
49 | if (nonceLength === undefined) {
50 | throw new Error('nonceLength parameter not specified');
51 | }
52 |
53 | // Validate message length
54 | if (array.byteLength <= nonceLength) {
55 | throw new CryptoError('bad-message-length', 'Message is shorter than nonce');
56 | }
57 |
58 | // Unpack nonce
59 | const nonce = array.slice(0, nonceLength);
60 |
61 | // Unpack data
62 | const data = array.slice(nonceLength);
63 |
64 | // Return box
65 | return new Box(nonce, data, nonceLength);
66 | }
67 |
68 | public toUint8Array(): Uint8Array {
69 | // Return both the nonce and the encrypted data
70 | const box = new Uint8Array(this.length);
71 | box.set(this._nonce);
72 | box.set(this._data, this._nonceLength);
73 | return box;
74 | }
75 |
76 | }
77 |
78 | /**
79 | * A KeyStore holds public and private keys and can handle encryption and
80 | * decryption.
81 | */
82 | export class KeyStore implements saltyrtc.KeyStore {
83 | // The NaCl key pair
84 | private _keyPair: nacl.BoxKeyPair;
85 |
86 | private logTag: string = '[SaltyRTC.KeyStore]';
87 |
88 | constructor(secretKey?: Uint8Array | string, log?: saltyrtc.Log) {
89 | if (log === undefined) {
90 | log = new Log('none');
91 | }
92 |
93 | // Validate argument count (bug prevention)
94 | if (arguments.length > 2) {
95 | throw new Error('Too many arguments in KeyStore constructor');
96 | }
97 |
98 | // Create new key pair if necessary
99 | if (secretKey === undefined) {
100 | this._keyPair = nacl.box.keyPair();
101 | log.debug(this.logTag, 'New public key:', u8aToHex(this._keyPair.publicKey));
102 | } else {
103 | this._keyPair = nacl.box.keyPair.fromSecretKey(validateKey(secretKey, 'Private key'));
104 | log.debug(this.logTag, 'Restored public key:', u8aToHex(this._keyPair.publicKey));
105 | }
106 | }
107 |
108 | /**
109 | * Create a SharedKeyStore from this instance and the public key of the
110 | * remote peer.
111 | */
112 | public getSharedKeyStore(publicKey: Uint8Array | string): SharedKeyStore {
113 | return new SharedKeyStore(this.secretKeyBytes, publicKey);
114 | }
115 |
116 | /**
117 | * Return the public key as hex string.
118 | */
119 | get publicKeyHex(): string {
120 | return u8aToHex(this._keyPair.publicKey);
121 | }
122 |
123 | /**
124 | * Return the public key as Uint8Array.
125 | */
126 | get publicKeyBytes(): Uint8Array {
127 | return this._keyPair.publicKey;
128 | }
129 |
130 | /**
131 | * Return the secret key as hex string.
132 | */
133 | get secretKeyHex(): string {
134 | return u8aToHex(this._keyPair.secretKey);
135 | }
136 |
137 | /**
138 | * Return the secret key as Uint8Array.
139 | */
140 | get secretKeyBytes(): Uint8Array {
141 | return this._keyPair.secretKey;
142 | }
143 |
144 | /**
145 | * Return the full keypair.
146 | */
147 | get keypair(): nacl.BoxKeyPair {
148 | return this._keyPair;
149 | }
150 |
151 | /**
152 | * Encrypt plain data for the remote peer and return encrypted data as
153 | * bytes.
154 | *
155 | * Note: Encrypting using a SharedKeyStore instance is more efficient when
156 | * encrypting with the same public key more than once.
157 | */
158 | public encryptRaw(bytes: Uint8Array, nonce: Uint8Array, otherKey: Uint8Array): Uint8Array {
159 | return nacl.box(bytes, nonce, otherKey, this._keyPair.secretKey);
160 | }
161 |
162 | /**
163 | * Encrypt plain data for the remote peer and return encrypted data in a
164 | * box.
165 | *
166 | * Note: Encrypting using a SharedKeyStore instance is more efficient when
167 | * encrypting with the same public key more than once.
168 | */
169 | public encrypt(bytes: Uint8Array, nonce: Uint8Array, otherKey: Uint8Array): saltyrtc.Box {
170 | const encrypted = this.encryptRaw(bytes, nonce, otherKey);
171 | return new Box(nonce, encrypted, nacl.box.nonceLength);
172 | }
173 |
174 | /**
175 | * Decrypt encrypted bytes from the remote peer and return plain data as
176 | * bytes.
177 | *
178 | * Note: Decrypting using a SharedKeyStore instance is more efficient when
179 | * decrypting with the same public key more than once.
180 | *
181 | * May throw CryptoError instances with the following codes:
182 | *
183 | * - decryption-failed: Data could not be decrypted
184 | */
185 | public decryptRaw(bytes: Uint8Array, nonce: Uint8Array, otherKey: Uint8Array): Uint8Array {
186 | const data: Uint8Array | null = nacl.box.open(bytes, nonce, otherKey, this._keyPair.secretKey);
187 | if (!data) {
188 | throw new CryptoError('decryption-failed', 'Data could not be decrypted');
189 | }
190 | return data;
191 | }
192 |
193 | /**
194 | * Decrypt encrypted boxed data from the remote peer and return plain data
195 | * as bytes.
196 | *
197 | * Note: Decrypting using a SharedKeyStore instance is more efficient when
198 | * decrypting with the same public key more than once.
199 | *
200 | * May throw CryptoError instances with the following codes:
201 | *
202 | * - decryption-failed: Data could not be decrypted
203 | */
204 | public decrypt(box: saltyrtc.Box, otherKey: Uint8Array): Uint8Array {
205 | return this.decryptRaw(box.data, box.nonce, otherKey);
206 | }
207 | }
208 |
209 | /**
210 | * A SharedKeyStore holds the resulting shared key of the local peer's secret
211 | * key and the remote peer's public key.
212 | *
213 | * Note: Since the shared key is only calculated once, using the SharedKeyStore
214 | * should always be preferred over over using the KeyStore instance.
215 | */
216 | export class SharedKeyStore implements saltyrtc.SharedKeyStore {
217 | // The local NaCl key pair
218 | private _localSecretKey: Uint8Array;
219 | // The remote public key
220 | private _remotePublicKey: Uint8Array;
221 | // The calculated shared key
222 | private _sharedKey: Uint8Array;
223 |
224 | constructor(localSecretKey: Uint8Array | string, remotePublicKey: Uint8Array | string) {
225 | this._localSecretKey = validateKey(localSecretKey, 'Local private key');
226 | this._remotePublicKey = validateKey(remotePublicKey, 'Remote public key');
227 |
228 | // Calculate the shared key
229 | this._sharedKey = nacl.box.before(this._remotePublicKey, this._localSecretKey);
230 | }
231 |
232 | /**
233 | * Return the local peer's secret key as hex string.
234 | */
235 | get localSecretKeyHex(): string {
236 | return u8aToHex(this._localSecretKey);
237 | }
238 |
239 | /**
240 | * Return the local peer's secret key as Uint8Array.
241 | */
242 | get localSecretKeyBytes(): Uint8Array {
243 | return this._localSecretKey;
244 | }
245 |
246 | /**
247 | * Return the remote peer's public key as a hex string.
248 | */
249 | get remotePublicKeyHex(): string {
250 | return u8aToHex(this._remotePublicKey);
251 | }
252 |
253 | /**
254 | * Return the remote peer's public key as Uint8Array.
255 | */
256 | get remotePublicKeyBytes(): Uint8Array {
257 | return this._remotePublicKey;
258 | }
259 |
260 | /**
261 | * Encrypt plain data for the remote peer and return encrypted data as
262 | * bytes.
263 | */
264 | public encryptRaw(bytes: Uint8Array, nonce: Uint8Array): Uint8Array {
265 | return nacl.box.after(bytes, nonce, this._sharedKey);
266 | }
267 |
268 | /**
269 | * Encrypt plain data for the remote peer and return encrypted data in a
270 | * box.
271 | */
272 | public encrypt(bytes: Uint8Array, nonce: Uint8Array): saltyrtc.Box {
273 | const encrypted = this.encryptRaw(bytes, nonce);
274 | return new Box(nonce, encrypted, nacl.box.nonceLength);
275 | }
276 |
277 | /**
278 | * Decrypt encrypted bytes from the remote peer and return plain data as
279 | * bytes.
280 | *
281 | * May throw CryptoError instances with the following codes:
282 | *
283 | * - decryption-failed: Data could not be decrypted
284 | */
285 | public decryptRaw(bytes: Uint8Array, nonce: Uint8Array): Uint8Array {
286 | const data: Uint8Array | null = nacl.box.open.after(bytes, nonce, this._sharedKey);
287 | if (!data) {
288 | throw new CryptoError('decryption-failed', 'Data could not be decrypted');
289 | }
290 | return data;
291 | }
292 |
293 | /**
294 | * Decrypt encrypted boxed data from the remote peer and return plain data
295 | * as bytes.
296 | *
297 | * May throw CryptoError instances with the following codes:
298 | *
299 | * - decryption-failed: Data could not be decrypted
300 | */
301 | public decrypt(box: saltyrtc.Box): Uint8Array {
302 | return this.decryptRaw(box.data, box.nonce);
303 | }
304 | }
305 |
306 | export class AuthToken implements saltyrtc.AuthToken {
307 |
308 | private _authToken: Uint8Array = null;
309 |
310 | private logTag: string = '[SaltyRTC.AuthToken]';
311 |
312 | /**
313 | * May throw CryptoError instances with the following codes:
314 | *
315 | * - bad-token-length
316 | */
317 | constructor(bytes?: Uint8Array, log?: saltyrtc.Log) {
318 | if (log === undefined) {
319 | log = new Log('none');
320 | }
321 |
322 | if (typeof bytes === 'undefined') {
323 | this._authToken = nacl.randomBytes(nacl.secretbox.keyLength);
324 | log.debug(this.logTag, 'Generated auth token');
325 | } else {
326 | if (bytes.byteLength !== nacl.secretbox.keyLength) {
327 | const msg = 'Auth token must be ' + nacl.secretbox.keyLength + ' bytes long.';
328 | log.error(this.logTag, msg);
329 | throw new CryptoError('bad-token-length', msg);
330 | }
331 | this._authToken = bytes;
332 | log.debug(this.logTag, 'Initialized auth token');
333 | }
334 | }
335 |
336 | /**
337 | * Return the secret key as Uint8Array.
338 | */
339 | get keyBytes() {
340 | return this._authToken;
341 | }
342 |
343 | /**
344 | * Return the secret key as hex string.
345 | */
346 | get keyHex() {
347 | return u8aToHex(this._authToken);
348 | }
349 |
350 | /**
351 | * Encrypt data using the shared auth token.
352 | */
353 | public encrypt(bytes: Uint8Array, nonce: Uint8Array): saltyrtc.Box {
354 | const encrypted = nacl.secretbox(bytes, nonce, this._authToken);
355 | return new Box(nonce, encrypted, nacl.secretbox.nonceLength);
356 | }
357 |
358 | /**
359 | * Decrypt data using the shared auth token.
360 | *
361 | * May throw CryptoError instances with the following codes:
362 | *
363 | * - decryption-failed: Data could not be decrypted
364 | */
365 | public decrypt(box: saltyrtc.Box): Uint8Array {
366 | const data: Uint8Array | null = nacl.secretbox.open(box.data, box.nonce, this._authToken);
367 | if (!data) {
368 | throw new CryptoError('decryption-failed', 'Data could not be decrypted');
369 | }
370 | return data;
371 | }
372 |
373 | }
374 |
--------------------------------------------------------------------------------
/tests/keystore.spec.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:file-header
2 | // tslint:disable:no-reference
3 | ///
4 |
5 | import * as nacl from 'tweetnacl';
6 | import { CryptoError, ValidationError } from '../src/exceptions';
7 | import { AuthToken, Box, KeyStore, SharedKeyStore } from '../src/keystore';
8 | import { hexToU8a, u8aToHex } from '../src/utils';
9 |
10 | export default () => { describe('keystore', function() {
11 |
12 | describe('Box', function() {
13 |
14 | const nonce = nacl.randomBytes(24);
15 | const data = nacl.randomBytes(7);
16 | const box = new Box(nonce, data, 24);
17 |
18 | it('correctly calculates the length', () => {
19 | expect(box.length).toEqual(7 + 24);
20 | });
21 |
22 | it('correctly returns the data', () => {
23 | expect(box.data).toEqual(data);
24 | });
25 |
26 | it('correctly returns the nonce', () => {
27 | expect(box.nonce).toEqual(nonce);
28 | });
29 |
30 | it('can be created from a byte array', () => {
31 | const nonceLength = nacl.box.nonceLength;
32 | const nonce2 = nacl.randomBytes(nonceLength);
33 | const data2 = nacl.randomBytes(5);
34 | const array = new Uint8Array(nonceLength + 5);
35 | array.set(nonce2);
36 | array.set(data2, nonceLength);
37 | const box2 = Box.fromUint8Array(array, nonceLength);
38 | expect(box2.nonce).toEqual(nonce2);
39 | expect(box2.data).toEqual(data2);
40 | expect(box2.length).toEqual(nonceLength + 5);
41 | });
42 |
43 | it('validates the byte array length', () => {
44 | const nonceLength = nacl.box.nonceLength;
45 | const boxSameLength = () => Box.fromUint8Array(nacl.randomBytes(nonceLength), nonceLength);
46 | const boxLessLength = () => Box.fromUint8Array(nacl.randomBytes(nonceLength - 2), nonceLength);
47 | expect(boxSameLength).toThrow(new CryptoError('bad-message-length', 'Message is shorter than nonce'));
48 | expect(boxLessLength).toThrow(new CryptoError('bad-message-length', 'Message is shorter than nonce'));
49 | });
50 |
51 | it('can be converted into a byte array', () => {
52 | const array = box.toUint8Array();
53 | expect(array.slice(0, nacl.secretbox.nonceLength)).toEqual(nonce);
54 | expect(array.slice(nacl.secretbox.nonceLength)).toEqual(data);
55 | });
56 |
57 | });
58 |
59 | describe('KeyStore', function() {
60 |
61 | const ks = new KeyStore();
62 | const nonce = nacl.randomBytes(24);
63 | const data = nacl.randomBytes(7);
64 |
65 | it('generates a keypair', () => {
66 | // Internal test
67 | expect((ks as any)._keyPair.publicKey).toBeTruthy();
68 | expect((ks as any)._keyPair.secretKey).toBeTruthy();
69 | });
70 |
71 | it('can return the secret/public keys as bytes', () => {
72 | expect(ks.publicKeyBytes).toBeTruthy();
73 | expect(ks.secretKeyBytes).toBeTruthy();
74 | expect(ks.publicKeyBytes instanceof Uint8Array).toEqual(true);
75 | expect(ks.secretKeyBytes instanceof Uint8Array).toEqual(true);
76 | });
77 |
78 | it('can return the secret/public keys as hex string', () => {
79 | expect(ks.publicKeyHex).toBeTruthy();
80 | expect(ks.secretKeyHex).toBeTruthy();
81 | expect(typeof ks.publicKeyHex).toEqual('string');
82 | expect(typeof ks.secretKeyHex).toEqual('string');
83 | });
84 |
85 | it('can encrypt and decrypt properly (round trip)', () => {
86 | const ks2 = new KeyStore();
87 | const expected = nacl.randomBytes(24);
88 | let encrypted;
89 | let decrypted;
90 |
91 | encrypted = ks.encrypt(expected, nonce, ks2.publicKeyBytes);
92 | decrypted = ks.decrypt(encrypted, ks2.publicKeyBytes);
93 | expect(decrypted).toEqual(expected);
94 | decrypted = ks.decryptRaw(encrypted.data, encrypted.nonce, ks2.publicKeyBytes);
95 | expect(decrypted).toEqual(expected);
96 |
97 | encrypted = ks.encryptRaw(expected, nonce, ks2.publicKeyBytes);
98 | const encryptedBox = new Box(nonce, encrypted, nacl.box.nonceLength);
99 | decrypted = ks.decrypt(encryptedBox, ks2.publicKeyBytes);
100 | expect(decrypted).toEqual(expected);
101 | decrypted = ks.decryptRaw(encrypted, nonce, ks2.publicKeyBytes);
102 | expect(decrypted).toEqual(expected);
103 | });
104 |
105 | it('can only encrypt and decrypt if pubkey matches', () => {
106 | const ks2 = new KeyStore();
107 | const ks3 = new KeyStore();
108 | const expected = nacl.randomBytes(24);
109 | const encrypted = ks.encrypt(expected, nonce, ks2.publicKeyBytes);
110 |
111 | const decrypts = [
112 | () => ks.decrypt(encrypted, ks3.publicKeyBytes),
113 | () => ks.decryptRaw(encrypted.data, encrypted.nonce, ks3.publicKeyBytes),
114 | ];
115 |
116 | for (const decrypt of decrypts) {
117 | const error = new CryptoError('decryption-failed', 'Data could not be decrypted');
118 | expect(decrypt).toThrow(error);
119 | }
120 | });
121 |
122 | it('cannot encrypt without a proper nonce', () => {
123 | const encrypts = [
124 | () => ks.encrypt(data, nacl.randomBytes(3), nacl.randomBytes(32)),
125 | () => ks.encryptRaw(data, nacl.randomBytes(3), nacl.randomBytes(32)),
126 | ];
127 |
128 | for (const encrypt of encrypts) {
129 | expect(encrypt).toThrow(new Error('bad nonce size'));
130 | }
131 | });
132 |
133 | it('can be created from an Uint8Array or hex string', () => {
134 | const skBytes = nacl.randomBytes(32);
135 | const skHex = u8aToHex(skBytes);
136 |
137 | const ksBytes = new KeyStore(skBytes);
138 | const ksHex = new KeyStore(skHex);
139 |
140 | for (const keystore of [ksBytes, ksHex]) {
141 | expect(keystore.publicKeyBytes).not.toBeNull();
142 | expect(keystore.secretKeyBytes).toEqual(skBytes);
143 | expect(keystore.publicKeyHex).not.toBeNull();
144 | expect(keystore.secretKeyHex).toEqual(skHex);
145 | }
146 | });
147 |
148 | it('shows a nice error message if key is invalid', () => {
149 | const create1 = () => new KeyStore(Uint8Array.of(1, 2, 3));
150 | expect(create1).toThrowError('Private key must be 32 bytes long');
151 |
152 | const create2 = () => new KeyStore(42 as any);
153 | expect(create2).toThrowError('Private key must be an Uint8Array or a hex string');
154 |
155 | const create3 = () => new KeyStore('ffgghh');
156 | expect(create3).toThrowError('Private key must be 32 bytes long');
157 | });
158 |
159 | });
160 |
161 | describe('SharedKeyStore', function() {
162 | const ks = new KeyStore(new Uint8Array(32).fill(0xff));
163 | const sks = ks.getSharedKeyStore(ks.publicKeyBytes);
164 |
165 | const nonce = new Uint8Array(24).fill(0xff);
166 | const data = new Uint8Array(10).fill(0xff);
167 |
168 | it('calculates the shared key', () => {
169 | const key = hexToU8a('9cfcb55fa42de280c84c95d9cf08fcbec63657998d15e139dbd3b4c6a1264541');
170 | expect((sks as any)._sharedKey).toEqual(key);
171 | });
172 |
173 | it('can be derived from a KeyStore', () => {
174 | expect(sks.localSecretKeyBytes).toEqual(ks.secretKeyBytes);
175 | expect(sks.localSecretKeyHex).toEqual(ks.secretKeyHex);
176 | expect(sks.remotePublicKeyBytes).toEqual(ks.publicKeyBytes);
177 | expect(sks.remotePublicKeyHex).toEqual(ks.publicKeyHex);
178 | });
179 |
180 | it('can be constructed from Uint8Array based keys', () => {
181 | const sks2 = new SharedKeyStore(ks.secretKeyBytes, ks.publicKeyBytes);
182 | expect(sks2.localSecretKeyBytes).toEqual(ks.secretKeyBytes);
183 | expect(sks2.localSecretKeyHex).toEqual(ks.secretKeyHex);
184 | expect(sks2.remotePublicKeyBytes).toEqual(ks.publicKeyBytes);
185 | expect(sks2.remotePublicKeyHex).toEqual(ks.publicKeyHex);
186 | });
187 |
188 | it('can be constructed from hex string based keys', () => {
189 | const sks2 = new SharedKeyStore(ks.secretKeyHex, ks.publicKeyHex);
190 | expect(sks2.localSecretKeyBytes).toEqual(ks.secretKeyBytes);
191 | expect(sks2.localSecretKeyHex).toEqual(ks.secretKeyHex);
192 | expect(sks2.remotePublicKeyBytes).toEqual(ks.publicKeyBytes);
193 | expect(sks2.remotePublicKeyHex).toEqual(ks.publicKeyHex);
194 | });
195 |
196 | it('rejects invalid keys', () => {
197 | let create;
198 | let error;
199 |
200 | create = () => new SharedKeyStore({ meow: true } as any, ks.publicKeyBytes);
201 | error = new ValidationError('Local private key must be an Uint8Array or a hex string');
202 | expect(create).toThrow(error);
203 |
204 | create = () => new SharedKeyStore(ks.secretKeyBytes, { meow: true } as any);
205 | error = new ValidationError('Remote public key must be an Uint8Array or a hex string');
206 | expect(create).toThrow(error);
207 | });
208 |
209 | it('can encrypt and decrypt properly (round trip)', () => {
210 | const expected = new Uint8Array(24).fill(0xee);
211 | let encrypted;
212 |
213 | encrypted = sks.encrypt(expected, nonce);
214 | expect(sks.decrypt(encrypted)).toEqual(expected);
215 | expect(sks.decryptRaw(encrypted.data, encrypted.nonce)).toEqual(expected);
216 |
217 | encrypted = sks.encryptRaw(expected, nonce);
218 | const encryptedBox = new Box(nonce, encrypted, nacl.box.nonceLength);
219 | expect(sks.decrypt(encryptedBox)).toEqual(expected);
220 | expect(sks.decryptRaw(encrypted, nonce)).toEqual(expected);
221 | });
222 |
223 | it('cannot encrypt without a proper nonce', () => {
224 | const encrypts = [
225 | () => sks.encrypt(data, nacl.randomBytes(3)),
226 | () => sks.encryptRaw(data, nacl.randomBytes(3)),
227 | ];
228 |
229 | for (const encrypt of encrypts) {
230 | expect(encrypt).toThrow(new Error('bad nonce size'));
231 | }
232 | });
233 |
234 | it('can encrypt/decrypt data from KeyStore', () => {
235 | const expected = new Uint8Array(24).fill(0xee);
236 | let encrypted;
237 |
238 | encrypted = ks.encrypt(expected, nonce, ks.publicKeyBytes);
239 | expect(sks.decrypt(encrypted)).toEqual(expected);
240 |
241 | encrypted = sks.encrypt(expected, nonce);
242 | expect(ks.decrypt(encrypted, ks.publicKeyBytes)).toEqual(expected);
243 | });
244 |
245 | it('encrypted data matches expectation with a specific set of keys', () => {
246 | const skLocal = Uint8Array.from([
247 | 4, 4, 4, 4, 4, 4, 4, 4,
248 | 3, 3, 3, 3, 3, 3, 3, 3,
249 | 2, 2, 2, 2, 2, 2, 2, 2,
250 | 1, 1, 1, 1, 1, 1, 1, 1,
251 | ]);
252 | const skRemote = Uint8Array.from([
253 | 1, 1, 1, 1, 1, 1, 1, 1,
254 | 2, 2, 2, 2, 2, 2, 2, 2,
255 | 3, 3, 3, 3, 3, 3, 3, 3,
256 | 4, 4, 4, 4, 4, 4, 4, 4,
257 | ]);
258 | const ks1 = new KeyStore(skLocal);
259 | const plaintext = new Uint8Array(0);
260 | const nonce1 = new TextEncoder().encode('connectionidconnectionid');
261 | const expected = Uint8Array.from([
262 | 253, 142, 84, 143,
263 | 118, 139, 224, 253,
264 | 252, 98, 240, 45,
265 | 22, 73, 234, 94
266 | ]);
267 |
268 | const encrypted = ks1.encryptRaw(plaintext, nonce1, new KeyStore(skRemote).publicKeyBytes);
269 | expect(encrypted).toEqual(expected);
270 | });
271 |
272 | });
273 |
274 | describe('AuthToken', function() {
275 |
276 | const at = new AuthToken();
277 |
278 | it('can return the secret key as bytes', () => {
279 | expect(at.keyBytes).toBeTruthy();
280 | expect(at.keyBytes instanceof Uint8Array).toEqual(true);
281 | });
282 |
283 | it('can return the secret key as hex string', () => {
284 | expect(at.keyHex).toBeTruthy();
285 | expect(typeof at.keyHex).toEqual('string');
286 | });
287 |
288 | it('can encrypt and decrypt properly (round trip)', () => {
289 | const expected = nacl.randomBytes(7);
290 | const nonce = nacl.randomBytes(24);
291 | expect(at.encrypt(expected, nonce)).not.toEqual(expected);
292 | expect(at.decrypt(at.encrypt(expected, nonce))).toEqual(expected);
293 | });
294 |
295 | });
296 |
297 | }); };
298 |
--------------------------------------------------------------------------------
/tests/client.spec.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:file-header
2 | // tslint:disable:no-reference
3 | ///
4 |
5 | import * as nacl from 'tweetnacl';
6 |
7 | import { SaltyRTCBuilder } from '../src/client';
8 | import { ConnectionError } from '../src/exceptions';
9 | import { Box, KeyStore } from '../src/keystore';
10 | import { u8aToHex } from '../src/utils';
11 | import { DummyTask } from './testtasks';
12 | import { Runnable, sleep } from './utils';
13 |
14 | export default () => { describe('client', function() {
15 |
16 | describe('SaltyRTCBuilder', function() {
17 | const dummyData = new Uint8Array(0);
18 | const dummyBox = new Box(dummyData, dummyData, 0);
19 |
20 | it('can construct an untrusted initiator', () => {
21 | const tasks = [new DummyTask()];
22 | const salty = new SaltyRTCBuilder()
23 | .connectTo('localhost')
24 | .withKeyStore(new KeyStore())
25 | .usingTasks(tasks)
26 | .asInitiator();
27 | expect(((salty as any).signaling as any).role).toEqual('initiator');
28 | expect(((salty as any).signaling as any).peerTrustedKey).toBeNull();
29 | expect(((salty as any).signaling as any).tasks).toEqual(tasks);
30 | expect(((salty as any).signaling as any).pingInterval).toEqual(0);
31 | });
32 |
33 | it('can construct a trusted initiator', () => {
34 | const tasks = [new DummyTask()];
35 | const trustedKey = nacl.randomBytes(32);
36 | const salty = new SaltyRTCBuilder()
37 | .connectTo('localhost')
38 | .withKeyStore(new KeyStore())
39 | .withTrustedPeerKey(trustedKey)
40 | .usingTasks(tasks)
41 | .asInitiator();
42 | expect(((salty as any).signaling as any).role).toEqual('initiator');
43 | expect(((salty as any).signaling as any).peerTrustedKey).toEqual(trustedKey);
44 | expect(((salty as any).signaling as any).tasks).toEqual(tasks);
45 | expect(((salty as any).signaling as any).pingInterval).toEqual(0);
46 | });
47 |
48 | it('can construct an untrusted responder', () => {
49 | const tasks = [new DummyTask()];
50 | const pubKey = nacl.randomBytes(32);
51 | const authToken = nacl.randomBytes(32);
52 | const salty = new SaltyRTCBuilder()
53 | .connectTo('localhost')
54 | .withKeyStore(new KeyStore())
55 | .initiatorInfo(pubKey, authToken)
56 | .usingTasks(tasks)
57 | .asResponder();
58 | expect(((salty as any).signaling as any).role).toEqual('responder');
59 | expect(((salty as any).signaling as any).initiator.permanentSharedKey.remotePublicKeyBytes).toEqual(pubKey);
60 | expect(((salty as any).signaling as any).authToken.keyBytes).toEqual(authToken);
61 | expect(((salty as any).signaling as any).peerTrustedKey).toBeNull();
62 | expect(((salty as any).signaling as any).tasks).toEqual(tasks);
63 | expect(((salty as any).signaling as any).pingInterval).toEqual(0);
64 | });
65 |
66 | it('can construct a trusted responder', () => {
67 | const tasks = [new DummyTask()];
68 | const trustedKey = nacl.randomBytes(32);
69 | const salty = new SaltyRTCBuilder()
70 | .connectTo('localhost')
71 | .withKeyStore(new KeyStore())
72 | .withTrustedPeerKey(trustedKey)
73 | .usingTasks(tasks)
74 | .asResponder();
75 | expect(((salty as any).signaling as any).role).toEqual('responder');
76 | expect(((salty as any).signaling as any).peerTrustedKey).toEqual(trustedKey);
77 | expect(((salty as any).signaling as any).initiator.permanentSharedKey.remotePublicKeyBytes).toEqual(trustedKey);
78 | expect(((salty as any).signaling as any).authToken).toBeNull();
79 | expect(((salty as any).signaling as any).tasks).toEqual(tasks);
80 | expect(((salty as any).signaling as any).pingInterval).toEqual(0);
81 | });
82 |
83 | it('accepts hex strings as initiator pub key / auth token', () => {
84 | const pubKey = nacl.randomBytes(32);
85 | const authToken = nacl.randomBytes(32);
86 | const salty = new SaltyRTCBuilder()
87 | .connectTo('localhost')
88 | .withKeyStore(new KeyStore())
89 | .initiatorInfo(u8aToHex(pubKey), u8aToHex(authToken))
90 | .usingTasks([new DummyTask()])
91 | .asResponder();
92 | expect(((salty as any).signaling as any).initiator.permanentSharedKey.remotePublicKeyBytes).toEqual(pubKey);
93 | expect(((salty as any).signaling as any).authToken.keyBytes).toEqual(authToken);
94 | });
95 |
96 | it('accepts hex strings as peer trusted key', () => {
97 | const trustedKey = nacl.randomBytes(32);
98 | const salty = new SaltyRTCBuilder()
99 | .connectTo('localhost')
100 | .withKeyStore(new KeyStore())
101 | .withTrustedPeerKey(u8aToHex(trustedKey))
102 | .usingTasks([new DummyTask()])
103 | .asResponder();
104 | expect(((salty as any).signaling as any).peerTrustedKey).toEqual(trustedKey);
105 | });
106 |
107 | it('accepts websocket ping interval', () => {
108 | const salty = new SaltyRTCBuilder()
109 | .connectTo('localhost')
110 | .withKeyStore(new KeyStore())
111 | .usingTasks([new DummyTask()])
112 | .withPingInterval(10)
113 | .asInitiator();
114 | expect(((salty as any).signaling as any).pingInterval).toEqual(10);
115 | });
116 |
117 | it('validates websocket ping interval', () => {
118 | const builder = new SaltyRTCBuilder();
119 | expect(() => builder.withPingInterval(-10)).toThrowError('Ping interval may not be negative');
120 | });
121 |
122 | it('cannot encrypt/decrypt before the remote peer is established', () => {
123 | const salty = new SaltyRTCBuilder()
124 | .connectTo('localhost')
125 | .withKeyStore(new KeyStore())
126 | .usingTasks([new DummyTask()])
127 | .withPingInterval(10)
128 | .asInitiator();
129 |
130 | const encrypt = () => salty.encryptForPeer(dummyData, dummyData);
131 | const decrypt = () => salty.decryptFromPeer(dummyBox);
132 |
133 | expect(encrypt).toThrowError('Remote peer has not yet been established');
134 | expect(decrypt).toThrowError('Remote peer has not yet been established');
135 | });
136 |
137 | it('cannot encrypt/decrypt before the session key is established', () => {
138 | const trustedKey = nacl.randomBytes(32);
139 | const salty = new SaltyRTCBuilder()
140 | .connectTo('localhost')
141 | .withKeyStore(new KeyStore())
142 | .withTrustedPeerKey(trustedKey)
143 | .usingTasks([new DummyTask()])
144 | .asResponder();
145 |
146 | const encrypt = () => salty.encryptForPeer(dummyData, dummyData);
147 | const decrypt = () => salty.decryptFromPeer(dummyBox);
148 |
149 | expect(encrypt).toThrowError('Session key not yet established');
150 | expect(decrypt).toThrowError('Session key not yet established');
151 | });
152 |
153 | });
154 |
155 | describe('SaltyRTC', function() {
156 |
157 | describe('events', function() {
158 |
159 | let sc: saltyrtc.SaltyRTC;
160 |
161 | beforeEach(() => {
162 | sc = new SaltyRTCBuilder()
163 | .connectTo('localhost')
164 | .withKeyStore(new KeyStore())
165 | .usingTasks([new DummyTask()])
166 | .asInitiator();
167 | });
168 |
169 | it('can emit events', (done: any) => {
170 | sc.on('connected', () => {
171 | expect(true).toBe(true);
172 | done();
173 | });
174 | sc.emit({type: 'connected'});
175 | });
176 |
177 | it('only calls handlers for specified events', async () => {
178 | let counter = 0;
179 | sc.on(['connected', 'data'], () => {
180 | counter += 1;
181 | });
182 | sc.emit({type: 'connected'});
183 | sc.emit({type: 'data'});
184 | sc.emit({type: 'connection-error'});
185 | sc.emit({type: 'connected'});
186 | await sleep(20);
187 | expect(counter).toEqual(3);
188 | });
189 |
190 | it('only adds a handler once', async () => {
191 | let counter = 0;
192 | const handler: Runnable = () => { counter += 1; };
193 | sc.on('data', handler);
194 | sc.on('data', handler);
195 | sc.emit({type: 'data'});
196 | await sleep(20);
197 | expect(counter).toEqual(1);
198 | });
199 |
200 | it('can call multiple handlers', async () => {
201 | let counter = 0;
202 | const handler1: Runnable = () => { counter += 1; };
203 | const handler2: Runnable = () => { counter += 1; };
204 | sc.on(['connected', 'data'], handler1);
205 | sc.on(['connected'], handler2);
206 | sc.emit({type: 'connected'});
207 | sc.emit({type: 'data'});
208 | await sleep(20);
209 | expect(counter).toEqual(3);
210 | });
211 |
212 | it('can cancel handlers', async () => {
213 | let counter = 0;
214 | const handler: Runnable = () => { counter += 1; };
215 | sc.on(['data', 'connected'], handler);
216 | sc.emit({type: 'connected'});
217 | sc.emit({type: 'data'});
218 | sc.off('data', handler);
219 | sc.emit({type: 'connected'});
220 | sc.emit({type: 'data'});
221 | await sleep(20);
222 | expect(counter).toEqual(3);
223 | });
224 |
225 | it('can cancel handlers for multiple events', async () => {
226 | let counter = 0;
227 | const handler: Runnable = () => { counter += 1; };
228 | sc.on(['data', 'connected'], handler);
229 | sc.emit({type: 'connected'});
230 | sc.emit({type: 'data'});
231 | sc.off(['data', 'connected'], handler);
232 | sc.emit({type: 'connected'});
233 | sc.emit({type: 'data'});
234 | await sleep(20);
235 | expect(counter).toEqual(2);
236 | });
237 |
238 | it('can register one-time handlers', async () => {
239 | let counter = 0;
240 | const handler: Runnable = () => { counter += 1; };
241 | sc.once('data', handler);
242 | sc.emit({type: 'data'});
243 | sc.emit({type: 'data'});
244 | await sleep(20);
245 | expect(counter).toEqual(1);
246 | });
247 |
248 | it('can register one-time handlers that throw', async () => {
249 | let counter = 0;
250 | const handler: Runnable = () => { counter += 1; throw new Error('oh noes'); };
251 | sc.once('data', handler);
252 | sc.emit({type: 'data'});
253 | sc.emit({type: 'data'});
254 | await sleep(20);
255 | expect(counter).toEqual(1);
256 | });
257 |
258 | it('removes handlers that return false', async () => {
259 | let counter = 0;
260 | const handler: Runnable = () => {
261 | if (counter <= 4) {
262 | counter += 1;
263 | } else {
264 | return false;
265 | }
266 | };
267 | sc.on('data', handler);
268 | for (let i = 0; i < 7; i++) {
269 | sc.emit({type: 'data'});
270 | }
271 | await sleep(20);
272 | expect(counter).toEqual(5);
273 | });
274 |
275 | });
276 |
277 | describe('client', function() {
278 | it('cannot be reused', () => {
279 | const salty = new SaltyRTCBuilder()
280 | .connectTo('localhost')
281 | .withKeyStore(new KeyStore())
282 | .usingTasks([new DummyTask()])
283 | .asInitiator();
284 | // First connection should be fine
285 | expect(() => salty.connect()).not.toThrowError();
286 | // Second connection attempt should throw an error
287 | expect(() => salty.connect())
288 | .toThrow(new ConnectionError(
289 | 'Signaling instance cannot be reused. Please create a new client instance.'
290 | ));
291 | });
292 | });
293 |
294 | describe('application messages', function() {
295 |
296 | it('can only send application messages after c2c handshake', () => {
297 | const salty = new SaltyRTCBuilder()
298 | .connectTo('localhost')
299 | .withKeyStore(new KeyStore())
300 | .usingTasks([new DummyTask()])
301 | .asInitiator();
302 |
303 | const send = () => salty.sendApplicationMessage('hello');
304 | (salty as any).signaling.state = 'peer-handshake';
305 | expect(send).toThrowError('Cannot send application message in "peer-handshake" state');
306 | (salty as any).signaling.state = 'closing';
307 | expect(send).toThrowError('Cannot send application message in "closing" state');
308 | });
309 |
310 | });
311 |
312 | });
313 |
314 | }); }
315 |
--------------------------------------------------------------------------------
/src/signaling/responder.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2016-2022 Threema GmbH
3 | *
4 | * This software may be modified and distributed under the terms
5 | * of the MIT license. See the `LICENSE.md` file for details.
6 | */
7 |
8 | import { CloseCode } from '../closecode';
9 | import { ProtocolError, SignalingError, ValidationError } from '../exceptions';
10 | import { KeyStore } from '../keystore';
11 | import { Nonce } from '../nonce';
12 | import { Initiator, Server } from '../peers';
13 | import { arrayToBuffer, byteToHex } from '../utils';
14 | import { Signaling } from './common';
15 | import { isResponderId } from './helpers';
16 |
17 | export class ResponderSignaling extends Signaling {
18 |
19 | protected logTag: string = '[SaltyRTC.Responder]';
20 |
21 | protected initiator: Initiator = null;
22 |
23 | /**
24 | * Create a new responder signaling instance.
25 | */
26 | constructor(client: saltyrtc.SaltyRTC, host: string, port: number, serverKey: Uint8Array,
27 | tasks: saltyrtc.Task[], pingInterval: number,
28 | permanentKey: saltyrtc.KeyStore, initiatorPubKey: Uint8Array, authToken?: saltyrtc.AuthToken) {
29 | super(client, host, port, serverKey, tasks, pingInterval,
30 | permanentKey, authToken === undefined ? initiatorPubKey : undefined);
31 | this.role = 'responder';
32 | this.initiator = new Initiator(initiatorPubKey, this.permanentKey);
33 | if (authToken !== undefined) {
34 | this.authToken = authToken;
35 | } else {
36 | // If we trust the initiator, don't send a token message
37 | this.initiator.handshakeState = 'token-sent';
38 | }
39 | }
40 |
41 | /**
42 | * The responder needs to use the initiator public permanent key as connection path.
43 | */
44 | protected getWebsocketPath(): string {
45 | return this.initiator.permanentSharedKey.remotePublicKeyHex;
46 | }
47 |
48 | /**
49 | * Encrypt data for the initiator.
50 | */
51 | protected encryptHandshakeDataForPeer(receiver: number, messageType: string,
52 | payload: Uint8Array, nonceBytes: Uint8Array): saltyrtc.Box {
53 | // Validate receiver
54 | if (isResponderId(receiver)) {
55 | throw new ProtocolError('Responder may not encrypt messages for other responders: ' + receiver);
56 | } else if (receiver !== Signaling.SALTYRTC_ADDR_INITIATOR) {
57 | throw new ProtocolError('Bad receiver byte: ' + receiver);
58 | }
59 |
60 | switch (messageType) {
61 | case 'token':
62 | return this.authToken.encrypt(payload, nonceBytes);
63 | case 'key':
64 | return this.initiator.permanentSharedKey.encrypt(payload, nonceBytes);
65 | default:
66 | const sessionSharedKey = this.getPeer().sessionSharedKey;
67 | if (sessionSharedKey === null) {
68 | throw new ProtocolError('Trying to encrypt for peer using session key, but session key is null');
69 | }
70 | return sessionSharedKey.encrypt(payload, nonceBytes);
71 | }
72 | }
73 |
74 | protected getPeer(): Initiator | null {
75 | if (this.initiator !== null) {
76 | return this.initiator;
77 | }
78 | return null;
79 | }
80 |
81 | /**
82 | * Get the responder instance with the specified id.
83 | */
84 | protected getPeerWithId(id: number): Server | Initiator | null {
85 | if (id === Signaling.SALTYRTC_ADDR_SERVER) {
86 | return this.server;
87 | } else if (id === Signaling.SALTYRTC_ADDR_INITIATOR) {
88 | return this.initiator;
89 | } else {
90 | throw new ProtocolError('Invalid peer id: ' + id);
91 | }
92 | }
93 |
94 | /**
95 | * Handle signaling error during peer handshake.
96 | */
97 | protected handlePeerHandshakeSignalingError(e: SignalingError, source: number | null): void {
98 | // Close the connection to the server
99 | this.resetConnection(e.closeCode);
100 | }
101 |
102 | protected onPeerHandshakeMessage(box: saltyrtc.Box, nonce: Nonce): void {
103 | // Validate nonce destination
104 | if (nonce.destination !== this.address) {
105 | throw new ProtocolError('Message destination does not match our address');
106 | }
107 |
108 | let payload: Uint8Array;
109 |
110 | // Handle server messages
111 | if (nonce.source === Signaling.SALTYRTC_ADDR_SERVER) {
112 | // Nonce claims to come from server.
113 | // Try to decrypt data accordingly.
114 | try {
115 | payload = this.server.sessionSharedKey.decrypt(box);
116 | } catch (e) {
117 | if (e.name === 'CryptoError' && e.code === 'decryption-failed') {
118 | throw new SignalingError(
119 | CloseCode.ProtocolError, 'Could not decrypt server message.');
120 | } else {
121 | throw e;
122 | }
123 | }
124 |
125 | const msg: saltyrtc.Message = this.decodeMessage(payload, 'server');
126 | switch (msg.type) {
127 | case 'new-initiator':
128 | this.log.debug(this.logTag, 'Received new-initiator message');
129 | this.handleNewInitiator();
130 | break;
131 | case 'send-error':
132 | this.log.debug(this.logTag, 'Received send-error message');
133 | this.handleSendError(msg as saltyrtc.messages.SendError);
134 | break;
135 | case 'disconnected':
136 | this.log.debug(this.logTag, 'Received disconnected message');
137 | this.handleDisconnected(msg as saltyrtc.messages.Disconnected);
138 | break;
139 | default:
140 | throw new ProtocolError('Received unexpected server message: ' + msg.type);
141 | }
142 |
143 | // Handle peer messages
144 | } else if (nonce.source === Signaling.SALTYRTC_ADDR_INITIATOR) {
145 | payload = this.decryptInitiatorMessage(box);
146 |
147 | // Dispatch message
148 | let msg: saltyrtc.Message;
149 | switch (this.initiator.handshakeState) {
150 | case 'new':
151 | throw new ProtocolError('Unexpected peer handshake message');
152 | case 'key-sent':
153 | // Expect key message
154 | msg = this.decodeMessage(payload, 'key', true);
155 | this.log.debug(this.logTag, 'Received key');
156 | this.handleKey(msg as saltyrtc.messages.Key);
157 | this.sendAuth(nonce);
158 | break;
159 | case 'auth-sent':
160 | // Expect auth message
161 | msg = this.decodeMessage(payload, 'auth', true);
162 | this.log.debug(this.logTag, 'Received auth');
163 | this.handleAuth(msg as saltyrtc.messages.InitiatorAuth, nonce);
164 |
165 | // We're connected!
166 | this.setState('task');
167 | this.log.info(this.logTag, 'Peer handshake done');
168 |
169 | break;
170 | default:
171 | throw new SignalingError(CloseCode.InternalError, 'Unknown initiator handshake state');
172 | }
173 |
174 | // Handle unknown source
175 | } else {
176 | throw new SignalingError(CloseCode.InternalError,
177 | 'Message source is neither the server nor the initiator');
178 | }
179 | }
180 |
181 | /**
182 | * Decrypt messages from the initiator.
183 | *
184 | * @param box encrypted box containing messag.e
185 | * @returns The decrypted message bytes.
186 | * @throws SignalingError
187 | */
188 | private decryptInitiatorMessage(box: saltyrtc.Box): Uint8Array {
189 | switch (this.initiator.handshakeState) {
190 | case 'new':
191 | case 'token-sent':
192 | case 'key-received':
193 | throw new ProtocolError('Received message in ' + this.initiator.handshakeState + ' state.');
194 | case 'key-sent':
195 | // Expect a key message, encrypted with the permanent keys
196 | try {
197 | return this.initiator.permanentSharedKey.decrypt(box);
198 | } catch (e) {
199 | if (e.name === 'CryptoError' && e.code === 'decryption-failed') {
200 | throw new SignalingError(
201 | CloseCode.ProtocolError, 'Could not decrypt key message.');
202 | } else {
203 | throw e;
204 | }
205 | }
206 | case 'auth-sent':
207 | case 'auth-received':
208 | // Otherwise, it must be encrypted with the session key
209 | try {
210 | return this.initiator.sessionSharedKey.decrypt(box);
211 | } catch (e) {
212 | if (e.name === 'CryptoError' && e.code === 'decryption-failed') {
213 | throw new SignalingError(
214 | CloseCode.ProtocolError, 'Could not decrypt initiator session message.');
215 | } else {
216 | throw e;
217 | }
218 | }
219 | default:
220 | throw new ProtocolError('Invalid handshake state: ' + this.initiator.handshakeState);
221 | }
222 | }
223 |
224 | /**
225 | * Close when a new initiator has connected.
226 | *
227 | * Note: This deviates from the intention of the specification to allow
228 | * for more than one connection towards an initiator over the same
229 | * WebSocket connection.
230 | */
231 | protected onUnhandledSignalingServerMessage(msg: saltyrtc.Message): void {
232 | if (msg.type === 'new-initiator') {
233 | this.log.debug(this.logTag, 'Received new-initiator message after peer handshake completed, ' +
234 | 'closing');
235 | this.resetConnection(CloseCode.ClosingNormal);
236 | } else {
237 | this.log.warn(this.logTag, 'Unexpected server message type:', msg.type);
238 | }
239 | }
240 |
241 | protected sendClientHello(): void {
242 | const message: saltyrtc.messages.ClientHello = {
243 | type: 'client-hello',
244 | key: arrayToBuffer(this.permanentKey.publicKeyBytes),
245 | };
246 | const packet: Uint8Array = this.buildPacket(message, this.server, false);
247 | this.log.debug(this.logTag, 'Sending client-hello');
248 | this.ws.send(packet);
249 | this.server.handshakeState = 'hello-sent';
250 | }
251 |
252 | protected handleServerAuth(msg: saltyrtc.messages.ServerAuth, nonce: Nonce): void {
253 | if (nonce.destination > 0xff || nonce.destination < 0x02) {
254 | this.log.error(this.logTag, 'Invalid nonce destination:', nonce.destination);
255 | throw new ValidationError('Invalid nonce destination: ' + nonce.destination);
256 | }
257 | this.address = nonce.destination;
258 | this.log.debug(this.logTag, 'Server assigned address', byteToHex(this.address));
259 | this.logTag = '[SaltyRTC.Responder.' + byteToHex(this.address) + ']';
260 |
261 | // Validate repeated cookie
262 | this.validateRepeatedCookie(this.server, new Uint8Array(msg.your_cookie));
263 |
264 | // Validate server public key
265 | if (this.serverPublicKey != null) {
266 | try {
267 | this.validateSignedKeys(new Uint8Array(msg.signed_keys), nonce, this.serverPublicKey);
268 | } catch (e) {
269 | if (e.name === 'ValidationError') {
270 | throw new ProtocolError('Verification of signed_keys failed: ' + e.message);
271 | }
272 | throw e;
273 | }
274 | } else if (msg.signed_keys !== null && msg.signed_keys !== undefined) {
275 | this.log.warn(this.logTag, "Server sent signed keys, but we're not verifying them.");
276 | }
277 |
278 | this.initiator.connected = msg.initiator_connected;
279 | this.log.debug(this.logTag, 'Initiator', this.initiator.connected ? '' : 'not', 'connected');
280 |
281 | this.server.handshakeState = 'done';
282 | }
283 |
284 | /**
285 | * Handle an incoming new-initiator message.
286 | */
287 | private handleNewInitiator(): void {
288 | this.initiator = new Initiator(
289 | this.initiator.permanentSharedKey.remotePublicKeyBytes, this.permanentKey);
290 | this.initiator.connected = true;
291 | this.initPeerHandshake();
292 | }
293 |
294 | /**
295 | * Init the peer handshake.
296 | *
297 | * If the initiator is already connected, send a token.
298 | * Otherwise, do nothing and wait for a new-initiator message.
299 | */
300 | protected initPeerHandshake(): void {
301 | if (this.initiator.connected) {
302 | // Only send token if we don't trust the initiator.
303 | if (this.peerTrustedKey === null) {
304 | this.sendToken();
305 | }
306 | this.sendKey();
307 | }
308 | }
309 |
310 | /**
311 | * Send a 'token' message to the initiator.
312 | */
313 | protected sendToken(): void {
314 | const message: saltyrtc.messages.Token = {
315 | type: 'token',
316 | key: arrayToBuffer(this.permanentKey.publicKeyBytes),
317 | };
318 | const packet: Uint8Array = this.buildPacket(message, this.initiator);
319 | this.log.debug(this.logTag, 'Sending token');
320 | this.ws.send(packet);
321 | this.initiator.handshakeState = 'token-sent';
322 | }
323 |
324 | /**
325 | * Send our public session key to the initiator.
326 | */
327 | private sendKey(): void {
328 | // Generate our own session key
329 | this.initiator.setLocalSessionKey(new KeyStore(undefined, this.log));
330 |
331 | // Send public key to initiator
332 | const replyMessage: saltyrtc.messages.Key = {
333 | type: 'key',
334 | key: arrayToBuffer(this.initiator.localSessionKey.publicKeyBytes),
335 | };
336 | const packet: Uint8Array = this.buildPacket(replyMessage, this.initiator);
337 | this.log.debug(this.logTag, 'Sending key');
338 | this.ws.send(packet);
339 | this.initiator.handshakeState = 'key-sent';
340 | }
341 |
342 | /**
343 | * The initiator sends his public session key.
344 | */
345 | private handleKey(msg: saltyrtc.messages.Key): void {
346 | // Generate the shared session key
347 | this.initiator.setSessionSharedKey(new Uint8Array(msg.key));
348 | this.initiator.handshakeState = 'key-received';
349 | }
350 |
351 | /**
352 | * Repeat the initiator's cookie.
353 | */
354 | private sendAuth(nonce: Nonce): void {
355 | // Ensure again that cookies are different
356 | if (nonce.cookie.equals(this.initiator.cookiePair.ours)) {
357 | throw new ProtocolError('Their cookie and our cookie are the same.');
358 | }
359 |
360 | // Prepare task data
361 | const taskData: saltyrtc.TaskData = {};
362 | for (const task of this.tasks) {
363 | taskData[task.getName()] = task.getData();
364 | }
365 | const taskNames = this.tasks.map((task) => task.getName());
366 |
367 | // Send auth
368 | const message: saltyrtc.messages.ResponderAuth = {
369 | type: 'auth',
370 | your_cookie: arrayToBuffer(nonce.cookie.bytes),
371 | tasks: taskNames,
372 | data: taskData,
373 | };
374 | const packet: Uint8Array = this.buildPacket(message, this.initiator);
375 | this.log.debug(this.logTag, 'Sending auth');
376 | this.ws.send(packet);
377 | this.initiator.handshakeState = 'auth-sent';
378 | }
379 |
380 | /**
381 | * The initiator repeats our cookie and sends the chosen task.
382 | */
383 | private handleAuth(msg: saltyrtc.messages.InitiatorAuth, nonce: Nonce): void {
384 | // Validate repeated cookie
385 | this.validateRepeatedCookie(this.initiator, new Uint8Array(msg.your_cookie));
386 |
387 | // Validate task data
388 | try {
389 | ResponderSignaling.validateTaskInfo(msg.task, msg.data);
390 | } catch (e) {
391 | if (e.name === 'ValidationError') {
392 | throw new ProtocolError('Peer sent invalid task info: ' + e.message);
393 | }
394 | throw e;
395 | }
396 |
397 | // Find selected task
398 | let selectedTask: saltyrtc.Task = null;
399 | for (const task of this.tasks) {
400 | if (task.getName() === msg.task) {
401 | selectedTask = task;
402 | this.log.info(this.logTag, 'Task', msg.task, 'has been selected');
403 | break;
404 | }
405 | }
406 |
407 | // Initialize task
408 | if (selectedTask === null) {
409 | throw new SignalingError(CloseCode.ProtocolError, 'Initiator selected unknown task');
410 | } else {
411 | this.initTask(selectedTask, msg.data[selectedTask.getName()]);
412 | }
413 |
414 | // Ok!
415 | this.log.debug(this.logTag, 'Initiator authenticated');
416 | this.initiator.cookiePair.theirs = nonce.cookie;
417 | this.initiator.handshakeState = 'auth-received';
418 | }
419 |
420 | /**
421 | * Validate task info. Throw ValidationError if validation fails.
422 | * @param name Task name
423 | * @param data Task data
424 | * @throws ValidationError
425 | */
426 | private static validateTaskInfo(name: string, data: object): void {
427 | if (name.length === 0) {
428 | throw new ValidationError('Task name must not be empty');
429 | }
430 | if (Object.keys(data).length < 1) {
431 | throw new ValidationError('Task data must not be empty');
432 | }
433 | if (Object.keys(data).length > 1) {
434 | throw new ValidationError('Task data must contain exactly 1 key');
435 | }
436 | if (!data.hasOwnProperty(name)) {
437 | throw new ValidationError('Task data must contain an entry for the chosen task');
438 | }
439 | }
440 |
441 | /**
442 | * Handle a send error.
443 | */
444 | protected _handleSendError(receiver: number): void {
445 | // Validate receiver byte
446 | if (receiver !== Signaling.SALTYRTC_ADDR_INITIATOR) {
447 | throw new ProtocolError('Outgoing c2c messages must have been sent to the initiator');
448 | }
449 |
450 | // Notify application
451 | this.client.emit({type: 'signaling-connection-lost', data: receiver});
452 |
453 | // Reset connection
454 | this.resetConnection(CloseCode.ProtocolError);
455 |
456 | // TODO: Maybe keep ws connection open and wait for reconnect (#63)
457 | }
458 | }
459 |
--------------------------------------------------------------------------------
/saltyrtc-client.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2016-2022 Threema GmbH
3 | *
4 | * This software may be modified and distributed under the terms
5 | * of the MIT license. See the `LICENSE.md` file for details.
6 | */
7 |
8 | declare namespace saltyrtc {
9 | type CryptoErrorCode = 'bad-message-length' | 'bad-token-length' | 'decryption-failed'
10 |
11 | interface SignalingError extends Error {
12 | readonly closeCode: number;
13 | }
14 |
15 | interface ProtocolError extends SignalingError {}
16 |
17 | interface ConnectionError extends Error {}
18 |
19 | interface ValidationError extends Error {
20 | readonly critical: boolean;
21 | }
22 |
23 | interface CryptoError extends Error {
24 | readonly code: CryptoErrorCode;
25 | }
26 |
27 | interface Box {
28 | readonly length: number;
29 | readonly data: Uint8Array;
30 | readonly nonce: Uint8Array;
31 | toUint8Array(): Uint8Array;
32 | }
33 |
34 | interface KeyStore {
35 | readonly publicKeyHex: string;
36 | readonly publicKeyBytes: Uint8Array;
37 | readonly secretKeyHex: string;
38 | readonly secretKeyBytes: Uint8Array;
39 | getSharedKeyStore(publicKey: Uint8Array | string): SharedKeyStore;
40 | encryptRaw(bytes: Uint8Array, nonce: Uint8Array, otherKey: Uint8Array): Uint8Array;
41 | encrypt(bytes: Uint8Array, nonce: Uint8Array, otherKey: Uint8Array): Box;
42 | decryptRaw(bytes: Uint8Array, nonce: Uint8Array, otherKey: Uint8Array): Uint8Array;
43 | decrypt(box: Box, otherKey: Uint8Array): Uint8Array;
44 | }
45 |
46 | interface SharedKeyStore {
47 | readonly localSecretKeyHex: string;
48 | readonly localSecretKeyBytes: Uint8Array
49 | readonly remotePublicKeyHex: string;
50 | readonly remotePublicKeyBytes: Uint8Array
51 | encryptRaw(bytes: Uint8Array, nonce: Uint8Array): Uint8Array;
52 | encrypt(bytes: Uint8Array, nonce: Uint8Array): Box;
53 | decryptRaw(bytes: Uint8Array, nonce: Uint8Array): Uint8Array;
54 | decrypt(box: Box): Uint8Array;
55 | }
56 |
57 | interface AuthToken {
58 | readonly keyBytes: Uint8Array;
59 | readonly keyHex: string;
60 | encrypt(bytes: Uint8Array, nonce: Uint8Array): Box;
61 | decrypt(box: Box): Uint8Array;
62 | }
63 |
64 | interface Message {
65 | type: string;
66 | }
67 |
68 | interface SignalingMessage extends Message {
69 | type: messages.MessageType;
70 | }
71 |
72 | type SignalingState = 'new' | 'ws-connecting' | 'server-handshake' | 'peer-handshake' | 'task' | 'closing' | 'closed';
73 |
74 | interface HandoverState {
75 | local: boolean;
76 | peer: boolean;
77 | readonly any: boolean;
78 | readonly both: boolean;
79 | onBoth: () => void;
80 | reset(): void;
81 | }
82 |
83 | type SignalingRole = 'initiator' | 'responder';
84 |
85 | interface SaltyRTCEvent {
86 | type: string;
87 | data?: any;
88 | }
89 | type SaltyRTCEventHandler = (event: SaltyRTCEvent) => boolean | void;
90 |
91 | type TaskData = { [index:string] : any };
92 |
93 | interface Signaling {
94 | handoverState: HandoverState;
95 | role: SignalingRole;
96 |
97 | getState(): SignalingState;
98 | setState(state: SignalingState): void;
99 |
100 | /**
101 | * Send a task message through the websocket or - if handover has
102 | * already happened - through the task channel.
103 | *
104 | * @throws SignalingError if message could not be sent.
105 | */
106 | sendTaskMessage(msg: messages.TaskMessage): void;
107 |
108 | /**
109 | * Encrypt data for the peer.
110 | *
111 | * @param data The bytes to be encrypted.
112 | * @param nonce The bytes to be used as NaCl nonce.
113 | */
114 | encryptForPeer(data: Uint8Array, nonce: Uint8Array): Box;
115 |
116 | /**
117 | * Decrypt data from the peer.
118 | *
119 | * @param box The encrypted box.
120 | */
121 | decryptFromPeer(box: Box): Uint8Array;
122 |
123 | /**
124 | * Handle incoming signaling messages from the peer.
125 | *
126 | * This method can be used by tasks to pass in messages that arrived through their signaling channel.
127 | *
128 | * @param decryptedBytes The decrypted message bytes.
129 | * @throws SignalingError if the message is invalid.
130 | */
131 | onSignalingPeerMessage(decryptedBytes: Uint8Array): void;
132 |
133 | /**
134 | * Send a close message to the peer.
135 | *
136 | * This method may only be called once the client-to-client handshakes has been completed.
137 | *
138 | * Note that sending a close message does not reset the connection. To do that,
139 | * `resetConnection` needs to be called explicitly.
140 | *
141 | * @param reason The close code.
142 | */
143 | sendClose(reason: number): void;
144 |
145 | /**
146 | * Close and reset the connection with the specified close code.
147 | *
148 | * If no reason is passed in, this will be treated as a quiet
149 | * reset - no listeners will be notified.
150 | *
151 | * @param reason The close code to use.
152 | */
153 | resetConnection(reason?: number): void;
154 | }
155 |
156 | interface Task {
157 | /**
158 | * Initialize the task with the task data from the peer.
159 | *
160 | * The task should keep track internally whether it has been initialized or not.
161 | *
162 | * @param signaling The signaling instance.
163 | * @param data The data sent by the peer in the 'auth' message.
164 | * @throws ValidationError if task data is invalid.
165 | */
166 | init(signaling: Signaling, data: TaskData): void;
167 |
168 | /**
169 | * Used by the signaling class to notify task that the peer handshake is over.
170 | *
171 | * This is the point where the task can take over.
172 | */
173 | onPeerHandshakeDone(): void;
174 |
175 | /**
176 | * This method is called by SaltyRTC when a task related message
177 | * arrives through the WebSocket.
178 | *
179 | * @param message The deserialized MessagePack message.
180 | */
181 | onTaskMessage(message: messages.TaskMessage): void;
182 |
183 | /**
184 | * Send a signaling message through the task signaling channel.
185 | *
186 | * This method should only be called after the handover.
187 | *
188 | * @param payload The *unencrypted* message bytes. Message will be encrypted by the task.
189 | * @throws SignalingError if something goes wrong.
190 | */
191 | sendSignalingMessage(payload: Uint8Array): void;
192 |
193 | /**
194 | * Return the task protocol name.
195 | */
196 | getName(): string;
197 |
198 | /**
199 | * Return the list of supported message types.
200 | *
201 | * Incoming mssages with this type will be passed to the task.
202 | */
203 | getSupportedMessageTypes(): string[];
204 |
205 | /**
206 | * Return the task data used for negotiation in the `auth` message.
207 | */
208 | getData(): TaskData;
209 |
210 | /**
211 | * Close any task connections that may be open.
212 | *
213 | * This method is called by the signaling class in two cases:
214 | *
215 | * - When sending and receiving 'close' messages
216 | * - When the user explicitly requests to close the connection
217 | */
218 | close(reason: number): void;
219 | }
220 |
221 | type ServerInfoFactory = (initiatorPublicKey: string) => {host: string, port: number};
222 |
223 | interface SaltyRTCBuilder {
224 | connectTo(host: string, port: number): SaltyRTCBuilder;
225 | connectWith(serverInfo: ServerInfoFactory): SaltyRTCBuilder;
226 | withKeyStore(keyStore: KeyStore): SaltyRTCBuilder;
227 | withTrustedPeerKey(peerTrustedKey: Uint8Array | string): SaltyRTCBuilder;
228 | withServerKey(serverKey: Uint8Array | string): SaltyRTCBuilder;
229 | withLoggingLevel(level: saltyrtc.LogLevel): SaltyRTCBuilder;
230 | initiatorInfo(initiatorPublicKey: Uint8Array | string, authToken: Uint8Array | string): SaltyRTCBuilder;
231 | usingTasks(tasks: Task[]): SaltyRTCBuilder;
232 | withPingInterval(interval: number): SaltyRTCBuilder;
233 |
234 | asInitiator(): SaltyRTC;
235 | asResponder(): SaltyRTC;
236 | }
237 |
238 | interface SaltyRTC {
239 | readonly log: saltyrtc.Log;
240 | state: SignalingState;
241 |
242 | keyStore: KeyStore;
243 | permanentKeyBytes: Uint8Array;
244 | permanentKeyHex: string;
245 | authTokenBytes: Uint8Array;
246 | authTokenHex: string;
247 | peerPermanentKeyBytes: Uint8Array;
248 | peerPermanentKeyHex: string;
249 |
250 | getTask(): Task;
251 | getCurrentPeerCsn(): {incoming: number, outgoing: number};
252 | encryptForPeer(data: Uint8Array, nonce: Uint8Array): Box;
253 | decryptFromPeer(box: Box): Uint8Array;
254 |
255 | connect(): void;
256 | disconnect(unbind?: boolean): void;
257 |
258 | sendApplicationMessage(data: any): void;
259 |
260 | // Event handling
261 | on(event: string | string[], handler: SaltyRTCEventHandler): void;
262 | once(event: string | string[], handler: SaltyRTCEventHandler): void;
263 | off(event?: string | string[], handler?: SaltyRTCEventHandler): void;
264 | emit(event: SaltyRTCEvent): void;
265 | }
266 |
267 | interface EventRegistry {
268 | /**
269 | * Register an event handler for the specified event(s).
270 | */
271 | register(eventType: string | string[], handler: SaltyRTCEventHandler): void;
272 |
273 | /**
274 | * Unregister an event handler for the specified event(s).
275 | * If no handler is specified, all handlers for the specified event(s) are removed.
276 | */
277 | unregister(eventType: string | string[], handler?: SaltyRTCEventHandler): void;
278 |
279 | /**
280 | * Clear all event handlers.
281 | */
282 | unregisterAll(): void;
283 |
284 | /**
285 | * Return all event handlers for the specified event(s).
286 | *
287 | * The return value is always an array. If the event does not exist, the
288 | * array will be empty.
289 | *
290 | * Even if a handler is registered for multiple events, it is only returned once.
291 | */
292 | get(eventType: string | string[]): SaltyRTCEventHandler[];
293 | }
294 |
295 | interface Cookie {
296 | bytes: Uint8Array;
297 | equals(otherCookie: Cookie): boolean;
298 | }
299 |
300 | interface CookiePair {
301 | ours: Cookie;
302 | theirs: Cookie;
303 | }
304 |
305 | type NextCombinedSequence = { sequenceNumber: number, overflow: number };
306 |
307 | interface CombinedSequence {
308 | next(): NextCombinedSequence;
309 | }
310 |
311 | interface CombinedSequencePair {
312 | ours: CombinedSequence;
313 | theirs: number;
314 | }
315 |
316 | type LogLevel = 'none' | 'debug' | 'info' | 'warn' | 'error';
317 |
318 | interface Log {
319 | level: saltyrtc.LogLevel;
320 | debug(message?: any, ...optionalParams: any[]): void;
321 | trace(message?: any, ...optionalParams: any[]): void;
322 | info(message?: any, ...optionalParams: any[]): void;
323 | warn(message?: any, ...optionalParams: any[]): void;
324 | error(message?: any, ...optionalParams: any[]): void;
325 | assert(condition?: boolean, message?: string, ...data: any[]): void;
326 | }
327 | }
328 |
329 | declare namespace saltyrtc.messages {
330 | type MessageType = 'server-hello' | 'client-hello' | 'client-auth'
331 | | 'server-auth' | 'new-initiator' | 'new-responder'
332 | | 'drop-responder' | 'send-error' | 'token' | 'key'
333 | | 'auth' | 'restart' | 'close' | 'disconnected' | 'application';
334 |
335 | // https://github.com/saltyrtc/saltyrtc-meta/blob/master/Protocol.md#server-hello
336 | interface ServerHello extends SignalingMessage {
337 | type: 'server-hello';
338 | key: ArrayBuffer;
339 | }
340 |
341 | // https://github.com/saltyrtc/saltyrtc-meta/blob/master/Protocol.md#client-hello
342 | interface ClientHello extends SignalingMessage {
343 | type: 'client-hello';
344 | key: ArrayBuffer;
345 | }
346 |
347 | // https://github.com/saltyrtc/saltyrtc-meta/blob/master/Protocol.md#client-auth
348 | interface ClientAuth extends SignalingMessage {
349 | type: 'client-auth';
350 | your_cookie: ArrayBuffer;
351 | your_key?: ArrayBuffer | null;
352 | subprotocols: string[];
353 | ping_interval: number;
354 | }
355 |
356 | // https://github.com/saltyrtc/saltyrtc-meta/blob/master/Protocol.md#server-auth
357 | interface ServerAuth extends SignalingMessage {
358 | type: 'server-auth';
359 | your_cookie: ArrayBuffer;
360 | signed_keys?: ArrayBuffer;
361 | initiator_connected?: boolean;
362 | responders?: number[];
363 | }
364 |
365 | // https://github.com/saltyrtc/saltyrtc-meta/blob/master/Protocol.md#new-initiator
366 | interface NewInitiator extends SignalingMessage {
367 | type: 'new-initiator';
368 | }
369 |
370 | // https://github.com/saltyrtc/saltyrtc-meta/blob/master/Protocol.md#new-responder
371 | interface NewResponder extends SignalingMessage {
372 | type: 'new-responder';
373 | id: number;
374 | }
375 |
376 | // https://github.com/saltyrtc/saltyrtc-meta/blob/master/Protocol.md#drop-responder
377 | interface DropResponder extends SignalingMessage {
378 | type: 'drop-responder';
379 | id: number;
380 | reason?: number;
381 | }
382 |
383 | // https://github.com/saltyrtc/saltyrtc-meta/blob/master/Protocol.md#send-error
384 | interface SendError extends SignalingMessage {
385 | type: 'send-error';
386 | id: ArrayBuffer;
387 | }
388 |
389 | // https://github.com/saltyrtc/saltyrtc-meta/blob/master/Protocol.md#token-message
390 | interface Token extends SignalingMessage {
391 | type: 'token';
392 | key: ArrayBuffer;
393 | }
394 |
395 | // https://github.com/saltyrtc/saltyrtc-meta/blob/master/Protocol.md#key-message
396 | interface Key extends SignalingMessage {
397 | type: 'key';
398 | key: ArrayBuffer;
399 | }
400 |
401 | // https://github.com/saltyrtc/saltyrtc-meta/blob/master/Protocol.md#auth-message
402 | interface Auth extends SignalingMessage {
403 | type: 'auth';
404 | your_cookie: ArrayBuffer;
405 | data: { [index:string] : any };
406 | }
407 | interface InitiatorAuth extends Auth {
408 | task: string;
409 | }
410 | interface ResponderAuth extends Auth {
411 | tasks: string[];
412 | }
413 |
414 | // https://github.com/saltyrtc/saltyrtc-meta/blob/master/Protocol.md#close-message
415 | interface Close extends SignalingMessage {
416 | type: 'close';
417 | reason: number;
418 | }
419 |
420 | // https://github.com/saltyrtc/saltyrtc-meta/blob/master/Protocol.md#disconnected-message
421 | interface Disconnected extends SignalingMessage {
422 | type: 'disconnected';
423 | id: number;
424 | }
425 |
426 | // https://github.com/saltyrtc/saltyrtc-meta/blob/master/Protocol.md#application-message
427 | interface Application extends Message {
428 | type: 'application';
429 | data: any;
430 | }
431 |
432 | /**
433 | * A task message must include the type. It may contain arbitrary other data.
434 | */
435 | interface TaskMessage extends Message {
436 | type: string;
437 | [others: string]: any; // Make this an open interface
438 | }
439 | }
440 |
441 | declare namespace saltyrtc.static {
442 | interface SignalingError {
443 | new(closeCode: number, message: string): saltyrtc.SignalingError;
444 | }
445 |
446 | interface ProtocolError {
447 | new(message: string): saltyrtc.ProtocolError;
448 | }
449 |
450 | interface ConnectionError {
451 | new(message: string): saltyrtc.ConnectionError;
452 | }
453 |
454 | interface ValidationError {
455 | new(message: string, critical?: boolean): saltyrtc.ValidationError;
456 | }
457 |
458 | interface CryptoError {
459 | new(code: CryptoErrorCode, message: string): saltyrtc.CryptoError;
460 | }
461 |
462 | interface SaltyRTCBuilder {
463 | new(): saltyrtc.SaltyRTCBuilder;
464 | }
465 |
466 | interface KeyStore {
467 | new(secretKey?: Uint8Array | string, log?: saltyrtc.Log): saltyrtc.KeyStore;
468 | }
469 |
470 | interface Box {
471 | new(nonce: Uint8Array, data: Uint8Array, nonceLength: number): saltyrtc.Box;
472 | fromUint8Array(array: Uint8Array, nonceLength: number): saltyrtc.Box;
473 | }
474 |
475 | interface Cookie {
476 | COOKIE_LENGTH: number;
477 | new(bytes?: Uint8Array): saltyrtc.Cookie;
478 | }
479 |
480 | interface CookiePair {
481 | new(ours?: saltyrtc.Cookie, theirs?: saltyrtc.Cookie): saltyrtc.CookiePair;
482 | }
483 |
484 | interface CombinedSequence {
485 | SEQUENCE_NUMBER_MAX: number;
486 | OVERFLOW_MAX: number;
487 | new(): saltyrtc.CombinedSequence;
488 | }
489 |
490 | interface CombinedSequencePair {
491 | new(ours?: saltyrtc.CombinedSequence, theirs?: number): saltyrtc.CombinedSequencePair;
492 | }
493 |
494 | interface EventRegistry {
495 | new(): saltyrtc.EventRegistry;
496 | }
497 |
498 | interface Log {
499 | new(level: saltyrtc.LogLevel): saltyrtc.Log;
500 | }
501 |
502 | /**
503 | * Static list of close codes.
504 | */
505 | interface CloseCode {
506 | ClosingNormal: number;
507 | GoingAway: number;
508 | NoSharedSubprotocol: number;
509 | PathFull: number;
510 | ProtocolError: number;
511 | InternalError: number;
512 | Handover: number;
513 | DroppedByInitiator: number;
514 | InitiatorCouldNotDecrypt: number;
515 | NoSharedTask: number;
516 | InvalidKey: number;
517 | }
518 | }
519 |
520 | declare var saltyrtcClient: {
521 | exceptions: {
522 | SignalingError: saltyrtc.static.SignalingError,
523 | ProtocolError: saltyrtc.static.ProtocolError,
524 | ConnectionError: saltyrtc.static.ConnectionError,
525 | ValidationError: saltyrtc.static.ValidationError,
526 | CryptoError: saltyrtc.static.CryptoError,
527 | },
528 | SaltyRTCBuilder: saltyrtc.static.SaltyRTCBuilder;
529 | KeyStore: saltyrtc.static.KeyStore;
530 | Box: saltyrtc.static.Box;
531 | Cookie: saltyrtc.static.Cookie;
532 | CookiePair: saltyrtc.static.CookiePair;
533 | CombinedSequence: saltyrtc.static.CombinedSequence;
534 | CombinedSequencePair: saltyrtc.static.CombinedSequencePair;
535 | EventRegistry: saltyrtc.static.EventRegistry;
536 | CloseCode: saltyrtc.static.CloseCode;
537 | explainCloseCode: (code: number) => string;
538 | SignalingError: saltyrtc.static.SignalingError;
539 | ConnectionError: saltyrtc.static.ConnectionError;
540 | Log: saltyrtc.static.Log;
541 | };
542 |
--------------------------------------------------------------------------------
/tests/jasmine.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for Jasmine 2.2
2 | // Project: http://jasmine.github.io/
3 | // Definitions by: Boris Yankov , Theodore Brown , David Pärsson
4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5 |
6 |
7 | // For ddescribe / iit use : https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/karma-jasmine/karma-jasmine.d.ts
8 |
9 | declare function describe(description: string, specDefinitions: () => void): void;
10 | declare function fdescribe(description: string, specDefinitions: () => void): void;
11 | declare function xdescribe(description: string, specDefinitions: () => void): void;
12 |
13 | declare function it(expectation: string, assertion?: () => void, timeout?: number): void;
14 | declare function it(expectation: string, assertion?: (done: DoneFn) => void, timeout?: number): void;
15 | declare function fit(expectation: string, assertion?: () => void, timeout?: number): void;
16 | declare function fit(expectation: string, assertion?: (done: DoneFn) => void, timeout?: number): void;
17 | declare function xit(expectation: string, assertion?: () => void, timeout?: number): void;
18 | declare function xit(expectation: string, assertion?: (done: DoneFn) => void, timeout?: number): void;
19 |
20 | /** If you call the function pending anywhere in the spec body, no matter the expectations, the spec will be marked pending. */
21 | declare function pending(reason?: string): void;
22 |
23 | declare function beforeEach(action: () => void, timeout?: number): void;
24 | declare function beforeEach(action: (done: DoneFn) => void, timeout?: number): void;
25 | declare function afterEach(action: () => void, timeout?: number): void;
26 | declare function afterEach(action: (done: DoneFn) => void, timeout?: number): void;
27 |
28 | declare function beforeAll(action: () => void, timeout?: number): void;
29 | declare function beforeAll(action: (done: DoneFn) => void, timeout?: number): void;
30 | declare function afterAll(action: () => void, timeout?: number): void;
31 | declare function afterAll(action: (done: DoneFn) => void, timeout?: number): void;
32 |
33 | declare function expect(spy: Function): jasmine.Matchers;
34 | declare function expect(actual: any): jasmine.Matchers;
35 |
36 | declare function fail(e?: any): void;
37 | /** Action method that should be called when the async work is complete */
38 | interface DoneFn extends Function {
39 | (): void;
40 |
41 | /** fails the spec and indicates that it has completed. If the message is an Error, Error.message is used */
42 | fail: (message?: Error|string) => void;
43 | }
44 |
45 | declare function spyOn(object: any, method: string): jasmine.Spy;
46 |
47 | declare function runs(asyncMethod: Function): void;
48 | declare function waitsFor(latchMethod: () => boolean, failureMessage?: string, timeout?: number): void;
49 | declare function waits(timeout?: number): void;
50 |
51 | declare namespace jasmine {
52 |
53 | var clock: () => Clock;
54 |
55 | function any(aclass: any): Any;
56 | function anything(): Any;
57 | function arrayContaining(sample: any[]): ArrayContaining;
58 | function objectContaining(sample: any): ObjectContaining;
59 | function createSpy(name: string, originalFn?: Function): Spy;
60 | function createSpyObj(baseName: string, methodNames: any[]): any;
61 | function createSpyObj(baseName: string, methodNames: any[]): T;
62 | function pp(value: any): string;
63 | function getEnv(): Env;
64 | function addCustomEqualityTester(equalityTester: CustomEqualityTester): void;
65 | function addMatchers(matchers: CustomMatcherFactories): void;
66 | function stringMatching(str: string): Any;
67 | function stringMatching(str: RegExp): Any;
68 |
69 | interface Any {
70 |
71 | new (expectedClass: any): any;
72 |
73 | jasmineMatches(other: any): boolean;
74 | jasmineToString(): string;
75 | }
76 |
77 | // taken from TypeScript lib.core.es6.d.ts, applicable to CustomMatchers.contains()
78 | interface ArrayLike {
79 | length: number;
80 | [n: number]: T;
81 | }
82 |
83 | interface ArrayContaining {
84 | new (sample: any[]): any;
85 |
86 | asymmetricMatch(other: any): boolean;
87 | jasmineToString(): string;
88 | }
89 |
90 | interface ObjectContaining {
91 | new (sample: any): any;
92 |
93 | jasmineMatches(other: any, mismatchKeys: any[], mismatchValues: any[]): boolean;
94 | jasmineToString(): string;
95 | }
96 |
97 | interface Block {
98 |
99 | new (env: Env, func: SpecFunction, spec: Spec): any;
100 |
101 | execute(onComplete: () => void): void;
102 | }
103 |
104 | interface WaitsBlock extends Block {
105 | new (env: Env, timeout: number, spec: Spec): any;
106 | }
107 |
108 | interface WaitsForBlock extends Block {
109 | new (env: Env, timeout: number, latchFunction: SpecFunction, message: string, spec: Spec): any;
110 | }
111 |
112 | interface Clock {
113 | install(): void;
114 | uninstall(): void;
115 | /** Calls to any registered callback are triggered when the clock is ticked forward via the jasmine.clock().tick function, which takes a number of milliseconds. */
116 | tick(ms: number): void;
117 | mockDate(date?: Date): void;
118 | }
119 |
120 | interface CustomEqualityTester {
121 | (first: any, second: any): boolean;
122 | }
123 |
124 | interface CustomMatcher {
125 | compare(actual: T, expected: T): CustomMatcherResult;
126 | compare(actual: any, expected: any): CustomMatcherResult;
127 | }
128 |
129 | interface CustomMatcherFactory {
130 | (util: MatchersUtil, customEqualityTesters: Array): CustomMatcher;
131 | }
132 |
133 | interface CustomMatcherFactories {
134 | [index: string]: CustomMatcherFactory;
135 | }
136 |
137 | interface CustomMatcherResult {
138 | pass: boolean;
139 | message?: string;
140 | }
141 |
142 | interface MatchersUtil {
143 | equals(a: any, b: any, customTesters?: Array): boolean;
144 | contains(haystack: ArrayLike | string, needle: any, customTesters?: Array): boolean;
145 | buildFailureMessage(matcherName: string, isNot: boolean, actual: any, ...expected: Array): string;
146 | }
147 |
148 | interface Env {
149 | setTimeout: any;
150 | clearTimeout: void;
151 | setInterval: any;
152 | clearInterval: void;
153 | updateInterval: number;
154 |
155 | currentSpec: Spec;
156 |
157 | matchersClass: Matchers;
158 |
159 | version(): any;
160 | versionString(): string;
161 | nextSpecId(): number;
162 | addReporter(reporter: Reporter): void;
163 | execute(): void;
164 | describe(description: string, specDefinitions: () => void): Suite;
165 | // ddescribe(description: string, specDefinitions: () => void): Suite; Not a part of jasmine. Angular team adds these
166 | beforeEach(beforeEachFunction: () => void): void;
167 | beforeAll(beforeAllFunction: () => void): void;
168 | currentRunner(): Runner;
169 | afterEach(afterEachFunction: () => void): void;
170 | afterAll(afterAllFunction: () => void): void;
171 | xdescribe(desc: string, specDefinitions: () => void): XSuite;
172 | it(description: string, func: () => void): Spec;
173 | // iit(description: string, func: () => void): Spec; Not a part of jasmine. Angular team adds these
174 | xit(desc: string, func: () => void): XSpec;
175 | compareRegExps_(a: RegExp, b: RegExp, mismatchKeys: string[], mismatchValues: string[]): boolean;
176 | compareObjects_(a: any, b: any, mismatchKeys: string[], mismatchValues: string[]): boolean;
177 | equals_(a: any, b: any, mismatchKeys: string[], mismatchValues: string[]): boolean;
178 | contains_(haystack: any, needle: any): boolean;
179 | addCustomEqualityTester(equalityTester: CustomEqualityTester): void;
180 | addMatchers(matchers: CustomMatcherFactories): void;
181 | specFilter(spec: Spec): boolean;
182 | }
183 |
184 | interface FakeTimer {
185 |
186 | new (): any;
187 |
188 | reset(): void;
189 | tick(millis: number): void;
190 | runFunctionsWithinRange(oldMillis: number, nowMillis: number): void;
191 | scheduleFunction(timeoutKey: any, funcToCall: () => void, millis: number, recurring: boolean): void;
192 | }
193 |
194 | interface HtmlReporter {
195 | new (): any;
196 | }
197 |
198 | interface HtmlSpecFilter {
199 | new (): any;
200 | }
201 |
202 | interface Result {
203 | type: string;
204 | }
205 |
206 | interface NestedResults extends Result {
207 | description: string;
208 |
209 | totalCount: number;
210 | passedCount: number;
211 | failedCount: number;
212 |
213 | skipped: boolean;
214 |
215 | rollupCounts(result: NestedResults): void;
216 | log(values: any): void;
217 | getItems(): Result[];
218 | addResult(result: Result): void;
219 | passed(): boolean;
220 | }
221 |
222 | interface MessageResult extends Result {
223 | values: any;
224 | trace: Trace;
225 | }
226 |
227 | interface ExpectationResult extends Result {
228 | matcherName: string;
229 | passed(): boolean;
230 | expected: any;
231 | actual: any;
232 | message: string;
233 | trace: Trace;
234 | }
235 |
236 | interface Trace {
237 | name: string;
238 | message: string;
239 | stack: any;
240 | }
241 |
242 | interface PrettyPrinter {
243 |
244 | new (): any;
245 |
246 | format(value: any): void;
247 | iterateObject(obj: any, fn: (property: string, isGetter: boolean) => void): void;
248 | emitScalar(value: any): void;
249 | emitString(value: string): void;
250 | emitArray(array: any[]): void;
251 | emitObject(obj: any): void;
252 | append(value: any): void;
253 | }
254 |
255 | interface StringPrettyPrinter extends PrettyPrinter {
256 | }
257 |
258 | interface Queue {
259 |
260 | new (env: any): any;
261 |
262 | env: Env;
263 | ensured: boolean[];
264 | blocks: Block[];
265 | running: boolean;
266 | index: number;
267 | offset: number;
268 | abort: boolean;
269 |
270 | addBefore(block: Block, ensure?: boolean): void;
271 | add(block: any, ensure?: boolean): void;
272 | insertNext(block: any, ensure?: boolean): void;
273 | start(onComplete?: () => void): void;
274 | isRunning(): boolean;
275 | next_(): void;
276 | results(): NestedResults;
277 | }
278 |
279 | interface Matchers {
280 |
281 | new (env: Env, actual: any, spec: Env, isNot?: boolean): any;
282 |
283 | env: Env;
284 | actual: any;
285 | spec: Env;
286 | isNot?: boolean;
287 | message(): any;
288 |
289 | toBe(expected: any, expectationFailOutput?: any): boolean;
290 | toEqual(expected: any, expectationFailOutput?: any): boolean;
291 | toMatch(expected: string | RegExp, expectationFailOutput?: any): boolean;
292 | toBeDefined(expectationFailOutput?: any): boolean;
293 | toBeUndefined(expectationFailOutput?: any): boolean;
294 | toBeNull(expectationFailOutput?: any): boolean;
295 | toBeNaN(): boolean;
296 | toBeTruthy(expectationFailOutput?: any): boolean;
297 | toBeFalsy(expectationFailOutput?: any): boolean;
298 | toHaveBeenCalled(): boolean;
299 | toHaveBeenCalledWith(...params: any[]): boolean;
300 | toHaveBeenCalledTimes(expected: number): boolean;
301 | toContain(expected: any, expectationFailOutput?: any): boolean;
302 | toBeLessThan(expected: number, expectationFailOutput?: any): boolean;
303 | toBeGreaterThan(expected: number, expectationFailOutput?: any): boolean;
304 | toBeCloseTo(expected: number, precision: any, expectationFailOutput?: any): boolean;
305 | toThrow(expected?: any): boolean;
306 | toThrowError(message?: string | RegExp): boolean;
307 | toThrowError(expected?: new (...args: any[]) => Error, message?: string | RegExp): boolean;
308 | not: Matchers;
309 |
310 | Any: Any;
311 | }
312 |
313 | interface Reporter {
314 | reportRunnerStarting(runner: Runner): void;
315 | reportRunnerResults(runner: Runner): void;
316 | reportSuiteResults(suite: Suite): void;
317 | reportSpecStarting(spec: Spec): void;
318 | reportSpecResults(spec: Spec): void;
319 | log(str: string): void;
320 | }
321 |
322 | interface MultiReporter extends Reporter {
323 | addReporter(reporter: Reporter): void;
324 | }
325 |
326 | interface Runner {
327 |
328 | new (env: Env): any;
329 |
330 | execute(): void;
331 | beforeEach(beforeEachFunction: SpecFunction): void;
332 | afterEach(afterEachFunction: SpecFunction): void;
333 | beforeAll(beforeAllFunction: SpecFunction): void;
334 | afterAll(afterAllFunction: SpecFunction): void;
335 | finishCallback(): void;
336 | addSuite(suite: Suite): void;
337 | add(block: Block): void;
338 | specs(): Spec[];
339 | suites(): Suite[];
340 | topLevelSuites(): Suite[];
341 | results(): NestedResults;
342 | }
343 |
344 | interface SpecFunction {
345 | (spec?: Spec): void;
346 | }
347 |
348 | interface SuiteOrSpec {
349 | id: number;
350 | env: Env;
351 | description: string;
352 | queue: Queue;
353 | }
354 |
355 | interface Spec extends SuiteOrSpec {
356 |
357 | new (env: Env, suite: Suite, description: string): any;
358 |
359 | suite: Suite;
360 |
361 | afterCallbacks: SpecFunction[];
362 | spies_: Spy[];
363 |
364 | results_: NestedResults;
365 | matchersClass: Matchers;
366 |
367 | getFullName(): string;
368 | results(): NestedResults;
369 | log(arguments: any): any;
370 | runs(func: SpecFunction): Spec;
371 | addToQueue(block: Block): void;
372 | addMatcherResult(result: Result): void;
373 | expect(actual: any): any;
374 | waits(timeout: number): Spec;
375 | waitsFor(latchFunction: SpecFunction, timeoutMessage?: string, timeout?: number): Spec;
376 | fail(e?: any): void;
377 | getMatchersClass_(): Matchers;
378 | addMatchers(matchersPrototype: CustomMatcherFactories): void;
379 | finishCallback(): void;
380 | finish(onComplete?: () => void): void;
381 | after(doAfter: SpecFunction): void;
382 | execute(onComplete?: () => void): any;
383 | addBeforesAndAftersToQueue(): void;
384 | explodes(): void;
385 | spyOn(obj: any, methodName: string, ignoreMethodDoesntExist: boolean): Spy;
386 | removeAllSpies(): void;
387 | }
388 |
389 | interface XSpec {
390 | id: number;
391 | runs(): void;
392 | }
393 |
394 | interface Suite extends SuiteOrSpec {
395 |
396 | new (env: Env, description: string, specDefinitions: () => void, parentSuite: Suite): any;
397 |
398 | parentSuite: Suite;
399 |
400 | getFullName(): string;
401 | finish(onComplete?: () => void): void;
402 | beforeEach(beforeEachFunction: SpecFunction): void;
403 | afterEach(afterEachFunction: SpecFunction): void;
404 | beforeAll(beforeAllFunction: SpecFunction): void;
405 | afterAll(afterAllFunction: SpecFunction): void;
406 | results(): NestedResults;
407 | add(suiteOrSpec: SuiteOrSpec): void;
408 | specs(): Spec[];
409 | suites(): Suite[];
410 | children(): any[];
411 | execute(onComplete?: () => void): void;
412 | }
413 |
414 | interface XSuite {
415 | execute(): void;
416 | }
417 |
418 | interface Spy {
419 | (...params: any[]): any;
420 |
421 | identity: string;
422 | and: SpyAnd;
423 | calls: Calls;
424 | mostRecentCall: { args: any[]; };
425 | argsForCall: any[];
426 | wasCalled: boolean;
427 | }
428 |
429 | interface SpyAnd {
430 | /** By chaining the spy with and.callThrough, the spy will still track all calls to it but in addition it will delegate to the actual implementation. */
431 | callThrough(): Spy;
432 | /** By chaining the spy with and.returnValue, all calls to the function will return a specific value. */
433 | returnValue(val: any): Spy;
434 | /** By chaining the spy with and.returnValues, all calls to the function will return specific values in order until it reaches the end of the return values list. */
435 | returnValues(...values: any[]): Spy;
436 | /** By chaining the spy with and.callFake, all calls to the spy will delegate to the supplied function. */
437 | callFake(fn: Function): Spy;
438 | /** By chaining the spy with and.throwError, all calls to the spy will throw the specified value. */
439 | throwError(msg: string): Spy;
440 | /** When a calling strategy is used for a spy, the original stubbing behavior can be returned at any time with and.stub. */
441 | stub(): Spy;
442 | }
443 |
444 | interface Calls {
445 | /** By chaining the spy with calls.any(), will return false if the spy has not been called at all, and then true once at least one call happens. **/
446 | any(): boolean;
447 | /** By chaining the spy with calls.count(), will return the number of times the spy was called **/
448 | count(): number;
449 | /** By chaining the spy with calls.argsFor(), will return the arguments passed to call number index **/
450 | argsFor(index: number): any[];
451 | /** By chaining the spy with calls.allArgs(), will return the arguments to all calls **/
452 | allArgs(): any[];
453 | /** By chaining the spy with calls.all(), will return the context (the this) and arguments passed all calls **/
454 | all(): CallInfo[];
455 | /** By chaining the spy with calls.mostRecent(), will return the context (the this) and arguments for the most recent call **/
456 | mostRecent(): CallInfo;
457 | /** By chaining the spy with calls.first(), will return the context (the this) and arguments for the first call **/
458 | first(): CallInfo;
459 | /** By chaining the spy with calls.reset(), will clears all tracking for a spy **/
460 | reset(): void;
461 | }
462 |
463 | interface CallInfo {
464 | /** The context (the this) for the call */
465 | object: any;
466 | /** All arguments passed to the call */
467 | args: any[];
468 | /** The return value of the call */
469 | returnValue: any;
470 | }
471 |
472 | interface Util {
473 | inherit(childClass: Function, parentClass: Function): any;
474 | formatException(e: any): any;
475 | htmlEscape(str: string): string;
476 | argsToArray(args: any): any;
477 | extend(destination: any, source: any): any;
478 | }
479 |
480 | interface JsApiReporter extends Reporter {
481 |
482 | started: boolean;
483 | finished: boolean;
484 | result: any;
485 | messages: any;
486 |
487 | new (): any;
488 |
489 | suites(): Suite[];
490 | summarize_(suiteOrSpec: SuiteOrSpec): any;
491 | results(): any;
492 | resultsForSpec(specId: any): any;
493 | log(str: any): any;
494 | resultsForSpecs(specIds: any): any;
495 | summarizeResult_(result: any): any;
496 | }
497 |
498 | interface Jasmine {
499 | Spec: Spec;
500 | clock: Clock;
501 | util: Util;
502 | }
503 |
504 | export var HtmlReporter: HtmlReporter;
505 | export var HtmlSpecFilter: HtmlSpecFilter;
506 | export var DEFAULT_TIMEOUT_INTERVAL: number;
507 | }
508 |
--------------------------------------------------------------------------------