Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36ee7cdbfc | |||
| 2b564498ee | |||
| 0ddba16fa1 | |||
| 755c55de3e | |||
| 7bdf1e9b92 | |||
| af1db8a606 | |||
| a99bb3c4c9 | |||
| fd155c15bd | |||
| aaa43631aa | |||
| d2557bc943 | |||
| 33a3506981 | |||
| 03a54353be | |||
| c6328923e6 | |||
| 1ecb820bb0 | |||
| 0be2319288 | |||
| 073a025b83 | |||
| e83836d487 | |||
| 065c61e05c | |||
| 139a6bd903 | |||
| 3fa0ee59d4 | |||
| bd3d26422d | |||
| 370ef9fc69 | |||
| a087fb37a3 | |||
| 68c8fe0fa9 | |||
| 4309749979 | |||
| 1a677804a4 | |||
| a427e2a75c | |||
| 3c735b0ac1 | |||
| 4f446c3909 | |||
| 999ed1b5b3 | |||
| 8fa19f4a0f | |||
| 71a01ec234 | |||
| 32f033a9da | |||
| dade385147 | |||
| 6cf2e54f9a | |||
| fb673b0304 | |||
| 1a425af3f2 | |||
| 9bafed2c26 | |||
| bb2d0b0f62 | |||
| 5e4f10a80c | |||
| 9e12fc4d7d | |||
| 1caf2b7f83 | |||
| 6f17e3e659 | |||
| 17e2cd755d | |||
| a6970d4de8 | |||
| bc99c1f3ce | |||
| 5e3ff7fc27 | |||
| ffe3f966fe | |||
| c60c19a28e | |||
| 4ea785b604 | |||
| 2d4e9d0d3f | |||
| 971d572fbf | |||
| 244e1b84f7 | |||
| e5cdc99a34 | |||
| 9a5768219f | |||
| cee8f57318 | |||
| 1a40e0a83a | |||
| d0072d930f | |||
| 385062c4d7 | |||
| 9245638b25 | |||
| 1865542192 | |||
| 2563abda11 | |||
| c979ff6696 | |||
| 25681e888c | |||
| 5cfd082b00 | |||
| 0cbced43bd | |||
| b3e8d7e07e | |||
| f4a7395e3a | |||
| 14b42abfa4 | |||
| e8022e985e |
+2
-1
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
parser: "babel-eslint", // now needed for class properties
|
||||
parserOptions: {
|
||||
ecmaVersion: 2017,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
}
|
||||
@@ -72,5 +72,6 @@ module.exports = {
|
||||
"named": "never",
|
||||
"asyncArrow": "always",
|
||||
}],
|
||||
"arrow-parens": "off",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,62 @@
|
||||
Changes in [1.0.1-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.1-rc.2) (2019-03-05)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.1-rc.1...v1.0.1-rc.2)
|
||||
|
||||
* dont swallow txn errors in crypto store
|
||||
[\#853](https://github.com/matrix-org/matrix-js-sdk/pull/853)
|
||||
* Don't swallow txn errors in crypto store
|
||||
[\#849](https://github.com/matrix-org/matrix-js-sdk/pull/849)
|
||||
|
||||
Changes in [1.0.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.1-rc.1) (2019-02-28)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.0...v1.0.1-rc.1)
|
||||
|
||||
* Fix "e is undefined" masking the original error in MegolmDecryption
|
||||
[\#847](https://github.com/matrix-org/matrix-js-sdk/pull/847)
|
||||
|
||||
Changes in [1.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.0) (2019-02-14)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.0-rc.2...v1.0.0)
|
||||
|
||||
* Try again to commit package-lock.json
|
||||
[\#841](https://github.com/matrix-org/matrix-js-sdk/pull/841)
|
||||
|
||||
Changes in [1.0.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.0-rc.2) (2019-02-14)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.0-rc.1...v1.0.0-rc.2)
|
||||
|
||||
* Release script: commit package-lock.json
|
||||
[\#839](https://github.com/matrix-org/matrix-js-sdk/pull/839)
|
||||
* Add method to force re-check of key backup
|
||||
[\#840](https://github.com/matrix-org/matrix-js-sdk/pull/840)
|
||||
* Fix: dont check for unverified devices in left members
|
||||
[\#838](https://github.com/matrix-org/matrix-js-sdk/pull/838)
|
||||
|
||||
Changes in [1.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.0-rc.1) (2019-02-08)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.3...v1.0.0-rc.1)
|
||||
|
||||
* change hex SAS verification to decimal and emoji
|
||||
[\#837](https://github.com/matrix-org/matrix-js-sdk/pull/837)
|
||||
* Trust on decrypt
|
||||
[\#836](https://github.com/matrix-org/matrix-js-sdk/pull/836)
|
||||
* Always track our own devices
|
||||
[\#835](https://github.com/matrix-org/matrix-js-sdk/pull/835)
|
||||
* Make linting rules more consistent
|
||||
[\#834](https://github.com/matrix-org/matrix-js-sdk/pull/834)
|
||||
* add method to room to check for unverified devices
|
||||
[\#833](https://github.com/matrix-org/matrix-js-sdk/pull/833)
|
||||
* Merge redesign into develop
|
||||
[\#831](https://github.com/matrix-org/matrix-js-sdk/pull/831)
|
||||
* Supporting infrastructure for educated decisions on when to upgrade rooms
|
||||
[\#830](https://github.com/matrix-org/matrix-js-sdk/pull/830)
|
||||
* Include signature info for unknown devices
|
||||
[\#826](https://github.com/matrix-org/matrix-js-sdk/pull/826)
|
||||
* Flag v2 rooms as "safe"
|
||||
[\#828](https://github.com/matrix-org/matrix-js-sdk/pull/828)
|
||||
* Update ESLint
|
||||
[\#821](https://github.com/matrix-org/matrix-js-sdk/pull/821)
|
||||
|
||||
Changes in [0.14.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.3) (2019-01-22)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.3-rc.1...v0.14.3)
|
||||
|
||||
Generated
+222
-1
@@ -1,9 +1,198 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.14.2",
|
||||
"version": "1.0.1-rc.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
|
||||
"integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/highlight": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.2.tgz",
|
||||
"integrity": "sha512-f3QCuPppXxtZOEm5GWPra/uYUjmNQlu9pbAD8D/9jze4pTY83rTtB1igTBSwvkeNlC5gR24zFFkz+2WHLFQhqQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.3.2",
|
||||
"jsesc": "^2.5.1",
|
||||
"lodash": "^4.17.10",
|
||||
"source-map": "^0.5.0",
|
||||
"trim-right": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsesc": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/helper-function-name": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz",
|
||||
"integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-get-function-arity": "^7.0.0",
|
||||
"@babel/template": "^7.1.0",
|
||||
"@babel/types": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-get-function-arity": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz",
|
||||
"integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-split-export-declaration": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz",
|
||||
"integrity": "sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz",
|
||||
"integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^2.0.0",
|
||||
"esutils": "^2.0.2",
|
||||
"js-tokens": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"supports-color": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
|
||||
"dev": true
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.2.tgz",
|
||||
"integrity": "sha512-QzNUC2RO1gadg+fs21fi0Uu0OuGNzRKEmgCxoLNzbCdoprLwjfmZwzUrpUNfJPaVRwBpDY47A17yYEGWyRelnQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/template": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz",
|
||||
"integrity": "sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"@babel/parser": "^7.2.2",
|
||||
"@babel/types": "^7.2.2"
|
||||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.2.3.tgz",
|
||||
"integrity": "sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"@babel/generator": "^7.2.2",
|
||||
"@babel/helper-function-name": "^7.1.0",
|
||||
"@babel/helper-split-export-declaration": "^7.0.0",
|
||||
"@babel/parser": "^7.2.3",
|
||||
"@babel/types": "^7.2.2",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0",
|
||||
"lodash": "^4.17.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"version": "11.10.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.10.0.tgz",
|
||||
"integrity": "sha512-0GZF1RiPKU97IHUO5TORo9w1PwrH/NBPl+fS7oMLdaTRiYmYbwK4NWoZWrAdd0/abG9R2BU+OiwyQpTpE6pdfQ==",
|
||||
"dev": true
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.2.tgz",
|
||||
"integrity": "sha512-3Y6H8xlUlpbGR+XvawiH0UXehqydTmNmEpozWcXymqwcrwYAl5KMvKtQ+TF6f6E08V6Jur7v/ykdDSF+WDEIXQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esutils": "^2.0.2",
|
||||
"lodash": "^4.17.10",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"JSONStream": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
|
||||
@@ -353,6 +542,20 @@
|
||||
"source-map": "^0.5.7"
|
||||
}
|
||||
},
|
||||
"babel-eslint": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.1.tgz",
|
||||
"integrity": "sha512-z7OT1iNV+TjOwHNLLyJk+HN+YVWX+CLE6fPD2SymJZOZQBs+QIexFjhm4keGTm8MW9xr4EC9Q0PbaLB24V5GoQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"@babel/parser": "^7.0.0",
|
||||
"@babel/traverse": "^7.0.0",
|
||||
"@babel/types": "^7.0.0",
|
||||
"eslint-scope": "3.7.1",
|
||||
"eslint-visitor-keys": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"babel-generator": {
|
||||
"version": "6.26.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz",
|
||||
@@ -2118,6 +2321,24 @@
|
||||
"integrity": "sha1-VZj4SY6eB4Qg80uASVuNlZ9lH7I=",
|
||||
"dev": true
|
||||
},
|
||||
"eslint-scope": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz",
|
||||
"integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esrecurse": "^4.1.0",
|
||||
"estraverse": "^4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"estraverse": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
|
||||
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint-utils": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.14.3",
|
||||
"version": "1.0.1-rc.2",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -66,6 +66,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-es2015": "^6.18.0",
|
||||
|
||||
+9
-1
@@ -154,7 +154,15 @@ echo "npm version"
|
||||
# only turn off both of these behaviours, so we have to
|
||||
# manually commit the result.
|
||||
npm version --no-git-tag-version "$release"
|
||||
git commit package.json -m "$tag"
|
||||
|
||||
# commit package-lock.json if it exists, is versioned, and is modified
|
||||
if [[ -f package-lock.json && `git status --porcelain package-lock.json | grep '^ M'` ]];
|
||||
then
|
||||
pkglock='package-lock.json'
|
||||
else
|
||||
pkglock=''
|
||||
fi
|
||||
git commit package.json $pkglock -m "$tag"
|
||||
|
||||
|
||||
# figure out if we should be signing this release
|
||||
|
||||
+17
-9
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -38,9 +38,10 @@ import LocalStorageCryptoStore from '../lib/crypto/store/localStorage-crypto-sto
|
||||
*
|
||||
* @param {WebStorage=} sessionStoreBackend a web storage object to use for the
|
||||
* session store. If undefined, we will create a MockStorageApi.
|
||||
* @param {object} options additional options to pass to the client
|
||||
*/
|
||||
export default function TestClient(
|
||||
userId, deviceId, accessToken, sessionStoreBackend,
|
||||
userId, deviceId, accessToken, sessionStoreBackend, options,
|
||||
) {
|
||||
this.userId = userId;
|
||||
this.deviceId = deviceId;
|
||||
@@ -50,19 +51,22 @@ export default function TestClient(
|
||||
}
|
||||
const sessionStore = new sdk.WebStorageSessionStore(sessionStoreBackend);
|
||||
|
||||
// expose this so the tests can get to it
|
||||
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
|
||||
|
||||
this.httpBackend = new MockHttpBackend();
|
||||
this.client = sdk.createClient({
|
||||
|
||||
options = Object.assign({
|
||||
baseUrl: "http://" + userId + ".test.server",
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
deviceId: deviceId,
|
||||
sessionStore: sessionStore,
|
||||
cryptoStore: this.cryptoStore,
|
||||
request: this.httpBackend.requestFn,
|
||||
});
|
||||
}, options);
|
||||
if (!options.cryptoStore) {
|
||||
// expose this so the tests can get to it
|
||||
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
|
||||
options.cryptoStore = this.cryptoStore;
|
||||
}
|
||||
this.client = sdk.createClient(options);
|
||||
|
||||
this.deviceKeys = null;
|
||||
this.oneTimeKeys = {};
|
||||
@@ -181,7 +185,11 @@ TestClient.prototype.expectKeyQuery = function(response) {
|
||||
this.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, (path, content) => {
|
||||
Object.keys(response.device_keys).forEach((userId) => {
|
||||
expect(content.device_keys[userId]).toEqual({});
|
||||
expect(content.device_keys[userId]).toEqual(
|
||||
{},
|
||||
"Expected key query for " + userId + ", got " +
|
||||
Object.keys(content.device_keys),
|
||||
);
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
@@ -101,6 +101,7 @@ describe("DeviceList management:", function() {
|
||||
});
|
||||
|
||||
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(function() {
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
@@ -143,6 +144,7 @@ describe("DeviceList management:", function() {
|
||||
it("We should not get confused by out-of-order device query responses",
|
||||
() => {
|
||||
// https://github.com/vector-im/riot-web/issues/3126
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
|
||||
|
||||
@@ -72,7 +72,11 @@ function expectAliQueryKeys() {
|
||||
bobKeys[bobDeviceId] = bobTestClient.deviceKeys;
|
||||
aliTestClient.httpBackend.when("POST", "/keys/query")
|
||||
.respond(200, function(path, content) {
|
||||
expect(content.device_keys[bobUserId]).toEqual({});
|
||||
expect(content.device_keys[bobUserId]).toEqual(
|
||||
{},
|
||||
"Expected Alice to key query for " + bobUserId + ", got " +
|
||||
Object.keys(content.device_keys),
|
||||
);
|
||||
const result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
@@ -96,7 +100,11 @@ function expectBobQueryKeys() {
|
||||
bobTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
).respond(200, function(path, content) {
|
||||
expect(content.device_keys[aliUserId]).toEqual({});
|
||||
expect(content.device_keys[aliUserId]).toEqual(
|
||||
{},
|
||||
"Expected Bob to key query for " + aliUserId + ", got " +
|
||||
Object.keys(content.device_keys),
|
||||
);
|
||||
const result = {};
|
||||
result[aliUserId] = aliKeys;
|
||||
return {device_keys: result};
|
||||
@@ -544,6 +552,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Ali sends a message", function(done) {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -554,6 +563,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Bob receives a message", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -564,6 +574,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Bob receives a message with a bogus sender", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -617,6 +628,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Ali blocks Bob's device", function(done) {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -636,6 +648,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Bob receives two pre-key messages", function(done) {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -649,6 +662,8 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Bob replies to the message", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
bobTestClient.expectKeyQuery({device_keys: {[bobUserId]: {}}});
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -659,13 +674,14 @@ describe("MatrixClient crypto", function() {
|
||||
.then(bobRecvMessage)
|
||||
.then(bobEnablesEncryption)
|
||||
.then(bobSendsReplyMessage).then(function(ciphertext) {
|
||||
expect(ciphertext.type).toEqual(1);
|
||||
expect(ciphertext.type).toEqual(1, "Unexpected cipghertext type.");
|
||||
}).then(aliRecvMessage);
|
||||
});
|
||||
|
||||
it("Ali does a key query when encryption is enabled", function() {
|
||||
// enabling encryption in the room should make alice download devices
|
||||
// for both members.
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
@@ -696,7 +712,6 @@ describe("MatrixClient crypto", function() {
|
||||
}).then(() => {
|
||||
aliTestClient.expectKeyQuery({
|
||||
device_keys: {
|
||||
[aliUserId]: {},
|
||||
[bobUserId]: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -499,6 +499,7 @@ describe("megolm", function() {
|
||||
it('Alice sends a megolm message', function() {
|
||||
let p2pSession;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -581,6 +582,7 @@ describe("megolm", function() {
|
||||
});
|
||||
|
||||
it("We shouldn't attempt to send to blocked devices", function() {
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -634,6 +636,7 @@ describe("megolm", function() {
|
||||
let p2pSession;
|
||||
let megolmSessionId;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -843,6 +846,7 @@ describe("megolm", function() {
|
||||
let downloadPromise;
|
||||
let sendPromise;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -886,6 +890,7 @@ describe("megolm", function() {
|
||||
it("Alice exports megolm keys and imports them to a new device", function() {
|
||||
let messageEncrypted;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
@@ -74,6 +74,14 @@ const KEY_BACKUP_DATA = {
|
||||
},
|
||||
};
|
||||
|
||||
const BACKUP_INFO = {
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
};
|
||||
|
||||
function makeTestClient(sessionStore, cryptoStore) {
|
||||
const scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
@@ -124,13 +132,7 @@ describe("MegolmBackup", function() {
|
||||
mockCrypto.backupKey.set_recipient_key(
|
||||
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
);
|
||||
mockCrypto.backupInfo = {
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
};
|
||||
mockCrypto.backupInfo = BACKUP_INFO;
|
||||
|
||||
mockStorage = new MockStorageApi();
|
||||
sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
@@ -436,6 +438,7 @@ describe("MegolmBackup", function() {
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
BACKUP_INFO,
|
||||
).then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
@@ -457,6 +460,7 @@ describe("MegolmBackup", function() {
|
||||
};
|
||||
return client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
null, null, BACKUP_INFO,
|
||||
).then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
} catch (e) {
|
||||
console.warn("unable to run device verification tests: libolm not available");
|
||||
}
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
|
||||
|
||||
import {ShowQRCode, ScanQRCode} from '../../../../lib/crypto/verification/QRCode';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("QR code verification", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running device verification tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
});
|
||||
|
||||
describe("showing", function() {
|
||||
it("should emit an event to show a QR code", async function() {
|
||||
const qrCode = new ShowQRCode({
|
||||
getUserId: () => "@alice:example.com",
|
||||
deviceId: "ABCDEFG",
|
||||
getDeviceEd25519Key: function() {
|
||||
return "device+ed25519+key";
|
||||
},
|
||||
});
|
||||
const spy = expect.createSpy().andCall((e) => {
|
||||
qrCode.done();
|
||||
});
|
||||
qrCode.on("show_qr_code", spy);
|
||||
await qrCode.verify();
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
url: "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
|
||||
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("scanning", function() {
|
||||
const QR_CODE_URL = "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
|
||||
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey";
|
||||
it("should verify when a QR code is sent", async function() {
|
||||
const device = DeviceInfo.fromStorage(
|
||||
{
|
||||
algorithms: [],
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "device+curve25519+key",
|
||||
"ed25519:ABCDEFG": "device+ed25519+key",
|
||||
},
|
||||
verified: false,
|
||||
known: false,
|
||||
unsigned: {},
|
||||
},
|
||||
"ABCDEFG",
|
||||
);
|
||||
const client = {
|
||||
getStoredDevice: expect.createSpy().andReturn(device),
|
||||
setDeviceVerified: expect.createSpy(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(client);
|
||||
qrCode.on("confirm_user_id", ({userId, confirm}) => {
|
||||
if (userId === "@alice:example.com") {
|
||||
confirm();
|
||||
} else {
|
||||
qrCode.cancel(new Error("Incorrect user"));
|
||||
}
|
||||
});
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
await qrCode.verify();
|
||||
expect(client.getStoredDevice)
|
||||
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
|
||||
expect(client.setDeviceVerified)
|
||||
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
|
||||
});
|
||||
|
||||
it("should error when the user ID doesn't match", async function() {
|
||||
const client = {
|
||||
getStoredDevice: expect.createSpy(),
|
||||
setDeviceVerified: expect.createSpy(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(client, "@bob:example.com", "ABCDEFG");
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
const spy = expect.createSpy();
|
||||
await qrCode.verify().catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(client.getStoredDevice).toNotHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).toNotHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should error if the key doesn't match", async function() {
|
||||
const device = DeviceInfo.fromStorage(
|
||||
{
|
||||
algorithms: [],
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "device+curve25519+key",
|
||||
"ed25519:ABCDEFG": "a+different+device+ed25519+key",
|
||||
},
|
||||
verified: false,
|
||||
known: false,
|
||||
unsigned: {},
|
||||
},
|
||||
"ABCDEFG",
|
||||
);
|
||||
const client = {
|
||||
getStoredDevice: expect.createSpy().andReturn(device),
|
||||
setDeviceVerified: expect.createSpy(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(client, "@alice:example.com", "ABCDEFG");
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
const spy = expect.createSpy();
|
||||
await qrCode.verify().catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(client.getStoredDevice).toHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).toNotHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
} catch (e) {
|
||||
console.warn("unable to run device verification tests: libolm not available");
|
||||
}
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
import {verificationMethods} from '../../../../lib/crypto';
|
||||
|
||||
import SAS from '../../../../lib/crypto/verification/SAS';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
import {makeTestClients} from './util';
|
||||
|
||||
describe("verification request", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running device verification unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
});
|
||||
|
||||
it("should request and accept a verification", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
alice._crypto._deviceList.getRawStoredDevicesForUser = function() {
|
||||
return {
|
||||
Dynabook: {
|
||||
keys: {
|
||||
"ed25519:Dynabook": "bob+base64+ed25519+key",
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
alice.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.on("crypto.verification.request", (request) => {
|
||||
const bobVerifier = request.beginKeyVerification(verificationMethods.SAS);
|
||||
bobVerifier.verify();
|
||||
});
|
||||
const aliceVerifier = await alice.requestVerification("@bob:example.com");
|
||||
expect(aliceVerifier).toBeAn(SAS);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
} catch (e) {
|
||||
console.warn("unable to run device verification tests: libolm not available");
|
||||
}
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
import sdk from '../../../..';
|
||||
|
||||
import {verificationMethods} from '../../../../lib/crypto';
|
||||
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
|
||||
|
||||
import SAS from '../../../../lib/crypto/verification/SAS';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
|
||||
import {makeTestClients} from './util';
|
||||
|
||||
describe("SAS verification", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running device verification unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
});
|
||||
|
||||
it("should error on an unexpected event", async function() {
|
||||
const sas = new SAS({}, "@alice:example.com", "ABCDEFG");
|
||||
sas.handleEvent(new MatrixEvent({
|
||||
sender: "@alice:example.com",
|
||||
type: "es.inquisition",
|
||||
content: {},
|
||||
}));
|
||||
const spy = expect.createSpy();
|
||||
await sas.verify()
|
||||
.catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should verify a key", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
|
||||
alice.setDeviceVerified = expect.createSpy();
|
||||
alice.getDeviceEd25519Key = () => {
|
||||
return "alice+base64+ed25519+key";
|
||||
};
|
||||
alice.getStoredDevice = () => {
|
||||
return DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"ed25519:Dynabook": "bob+base64+ed25519+key",
|
||||
},
|
||||
},
|
||||
"Dynabook",
|
||||
);
|
||||
};
|
||||
alice.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
bob.setDeviceVerified = expect.createSpy();
|
||||
bob.getStoredDevice = () => {
|
||||
return DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"ed25519:Osborne2": "alice+base64+ed25519+key",
|
||||
},
|
||||
},
|
||||
"Osborne2",
|
||||
);
|
||||
};
|
||||
bob.getDeviceEd25519Key = () => {
|
||||
return "bob+base64+ed25519+key";
|
||||
};
|
||||
bob.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
let aliceSasEvent;
|
||||
let bobSasEvent;
|
||||
|
||||
const bobPromise = new Promise((resolve, reject) => {
|
||||
bob.on("crypto.verification.start", (verifier) => {
|
||||
verifier.on("show_sas", (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!aliceSasEvent) {
|
||||
bobSasEvent = e;
|
||||
} else {
|
||||
try {
|
||||
expect(e.sas).toEqual(aliceSasEvent.sas);
|
||||
e.confirm();
|
||||
aliceSasEvent.confirm();
|
||||
} catch (error) {
|
||||
e.mismatch();
|
||||
aliceSasEvent.mismatch();
|
||||
}
|
||||
}
|
||||
});
|
||||
resolve(verifier);
|
||||
});
|
||||
});
|
||||
|
||||
const aliceVerifier = alice.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
|
||||
);
|
||||
aliceVerifier.on("show_sas", (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!bobSasEvent) {
|
||||
aliceSasEvent = e;
|
||||
} else {
|
||||
try {
|
||||
expect(e.sas).toEqual(bobSasEvent.sas);
|
||||
e.confirm();
|
||||
bobSasEvent.confirm();
|
||||
} catch (error) {
|
||||
e.mismatch();
|
||||
bobSasEvent.mismatch();
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => verifier.verify()),
|
||||
]);
|
||||
expect(alice.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
|
||||
expect(bob.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
|
||||
});
|
||||
|
||||
it("should send a cancellation message on error", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
alice.setDeviceVerified = expect.createSpy();
|
||||
alice.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.setDeviceVerified = expect.createSpy();
|
||||
bob.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const bobPromise = new Promise((resolve, reject) => {
|
||||
bob.on("crypto.verification.start", (verifier) => {
|
||||
verifier.on("show_sas", (e) => {
|
||||
e.mismatch();
|
||||
});
|
||||
resolve(verifier);
|
||||
});
|
||||
});
|
||||
|
||||
const aliceVerifier = alice.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
|
||||
);
|
||||
|
||||
const aliceSpy = expect.createSpy();
|
||||
const bobSpy = expect.createSpy();
|
||||
await Promise.all([
|
||||
aliceVerifier.verify().catch(aliceSpy),
|
||||
bobPromise.then((verifier) => verifier.verify()).catch(bobSpy),
|
||||
]);
|
||||
expect(aliceSpy).toHaveBeenCalled();
|
||||
expect(bobSpy).toHaveBeenCalled();
|
||||
expect(alice.setDeviceVerified)
|
||||
.toNotHaveBeenCalled();
|
||||
expect(bob.setDeviceVerified)
|
||||
.toNotHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import TestClient from '../../../TestClient';
|
||||
|
||||
import sdk from '../../../..';
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
|
||||
export async function makeTestClients(userInfos, options) {
|
||||
const clients = [];
|
||||
const clientMap = {};
|
||||
const sendToDevice = function(type, map) {
|
||||
// console.log(this.getUserId(), "sends", type, map);
|
||||
for (const [userId, devMap] of Object.entries(map)) {
|
||||
if (userId in clientMap) {
|
||||
for (const [deviceId, msg] of Object.entries(devMap)) {
|
||||
if (deviceId in clientMap[userId]) {
|
||||
const event = new MatrixEvent({
|
||||
sender: this.getUserId(), // eslint-disable-line no-invalid-this
|
||||
type: type,
|
||||
content: msg,
|
||||
});
|
||||
setTimeout(
|
||||
() => clientMap[userId][deviceId]
|
||||
.emit("toDeviceEvent", event),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const userInfo of userInfos) {
|
||||
const client = (new TestClient(
|
||||
userInfo.userId, userInfo.deviceId, undefined, undefined,
|
||||
options,
|
||||
)).client;
|
||||
if (!(userInfo.userId in clientMap)) {
|
||||
clientMap[userInfo.userId] = {};
|
||||
}
|
||||
clientMap[userInfo.userId][userInfo.deviceId] = client;
|
||||
client.sendToDevice = sendToDevice;
|
||||
clients.push(client);
|
||||
}
|
||||
|
||||
await Promise.all(clients.map((client) => client.initCrypto()));
|
||||
|
||||
return clients;
|
||||
}
|
||||
+160
-18
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -59,6 +59,7 @@ Promise.config({warnings: false});
|
||||
|
||||
const SCROLLBACK_DELAY_MS = 3000;
|
||||
const CRYPTO_ENABLED = isCryptoAvailable();
|
||||
const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
|
||||
|
||||
function keysFromRecoverySession(sessions, decryptionKey, roomId) {
|
||||
const keys = [];
|
||||
@@ -142,6 +143,11 @@ function keyFromRecoverySession(session, decryptionKey) {
|
||||
*
|
||||
* @param {module:crypto.store.base~CryptoStore} opts.cryptoStore
|
||||
* crypto store implementation.
|
||||
*
|
||||
* @param {Array} [opts.verificationMethods] Optional. The verification method
|
||||
* that the application can handle. Each element should be an item from {@link
|
||||
* module:crypto~verificationMethods verificationMethods}, or a class that
|
||||
* implements the {$link module:crypto/verification/Base verifier interface}.
|
||||
*/
|
||||
function MatrixClient(opts) {
|
||||
// Allow trailing slash in HS url
|
||||
@@ -207,6 +213,7 @@ function MatrixClient(opts) {
|
||||
this._crypto = null;
|
||||
this._cryptoStore = opts.cryptoStore;
|
||||
this._sessionStore = opts.sessionStore;
|
||||
this._verificationMethods = opts.verificationMethods;
|
||||
|
||||
this._forceTURN = opts.forceTURN || false;
|
||||
|
||||
@@ -219,6 +226,8 @@ function MatrixClient(opts) {
|
||||
this._pushProcessor = new PushProcessor(this);
|
||||
|
||||
this._serverSupportsLazyLoading = null;
|
||||
|
||||
this._cachedCapabilities = null; // { capabilities: {}, lastUpdated: timestamp }
|
||||
}
|
||||
utils.inherits(MatrixClient, EventEmitter);
|
||||
utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
|
||||
@@ -386,6 +395,34 @@ MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) {
|
||||
this._notifTimelineSet = notifTimelineSet;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the capabilities of the homeserver. Always returns an object of
|
||||
* capability keys and their options, which may be empty.
|
||||
* @return {module:client.Promise} Resolves to the capabilities of the homeserver
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.getCapabilities = function() {
|
||||
if (this._cachedCapabilities) {
|
||||
const now = new Date().getTime();
|
||||
if (now - this._cachedCapabilities.lastUpdated <= CAPABILITIES_CACHE_MS) {
|
||||
return Promise.resolve(this._cachedCapabilities.capabilities);
|
||||
}
|
||||
}
|
||||
|
||||
// We swallow errors because we need a default object anyhow
|
||||
return this._http.authedRequest(
|
||||
undefined, "GET", "/capabilities",
|
||||
).catch(() => null).then((r) => {
|
||||
if (!r) r = {};
|
||||
const capabilities = r["capabilities"] || {};
|
||||
this._cachedCapabilities = {
|
||||
capabilities: capabilities,
|
||||
lastUpdated: new Date().getTime(),
|
||||
};
|
||||
return capabilities;
|
||||
});
|
||||
};
|
||||
|
||||
// Crypto bits
|
||||
// ===========
|
||||
|
||||
@@ -444,10 +481,12 @@ MatrixClient.prototype.initCrypto = async function() {
|
||||
this.store,
|
||||
this._cryptoStore,
|
||||
this._roomList,
|
||||
this._verificationMethods,
|
||||
);
|
||||
|
||||
this.reEmitter.reEmit(crypto, [
|
||||
"crypto.keyBackupFailed",
|
||||
"crypto.keyBackupSessionsRemaining",
|
||||
"crypto.roomKeyRequest",
|
||||
"crypto.roomKeyRequestCancellation",
|
||||
"crypto.warning",
|
||||
@@ -624,6 +663,43 @@ async function _setDeviceVerification(
|
||||
client.emit("deviceVerificationChanged", userId, deviceId, dev);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a key verification from another user.
|
||||
*
|
||||
* @param {string} userId the user to request verification with
|
||||
* @param {Array} devices array of device IDs to send requests to. Defaults to
|
||||
* all devices owned by the user
|
||||
* @param {Array} methods array of verification methods to use. Defaults to
|
||||
* all known methods
|
||||
*
|
||||
* @returns {Promise<module:crypto/verification/Base>} resolves to a verifier
|
||||
* when the request is accepted by the other user
|
||||
*/
|
||||
MatrixClient.prototype.requestVerification = function(userId, devices, methods) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this._crypto.requestVerification(userId, devices);
|
||||
};
|
||||
|
||||
/**
|
||||
* Begin a key verification.
|
||||
*
|
||||
* @param {string} method the verification method to use
|
||||
* @param {string} userId the user to verify keys with
|
||||
* @param {string} deviceId the device to verify
|
||||
*
|
||||
* @returns {module:crypto/verification/Base} a verification object
|
||||
*/
|
||||
MatrixClient.prototype.beginKeyVerification = function(
|
||||
method, userId, deviceId,
|
||||
) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this._crypto.beginKeyVerification(method, userId, deviceId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the global override for whether the client should ever send encrypted
|
||||
* messages to unverified devices. This provides the default for rooms which
|
||||
@@ -773,6 +849,19 @@ MatrixClient.prototype.importRoomKeys = function(keys) {
|
||||
return this._crypto.importRoomKeys(keys);
|
||||
};
|
||||
|
||||
/**
|
||||
* Force a re-check of the local key backup status against
|
||||
* what's on the server.
|
||||
*
|
||||
* @returns {Object} Object with backup info (as returned by
|
||||
* getKeyBackupVersion) in backupInfo and
|
||||
* trust information (as returned by isKeyBackupTrusted)
|
||||
* in trustInfo.
|
||||
*/
|
||||
MatrixClient.prototype.checkKeyBackup = function() {
|
||||
return this._crypto.checkKeyBackup();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get information about the current key backup.
|
||||
* @returns {Promise} Information object from API or null
|
||||
@@ -842,6 +931,10 @@ MatrixClient.prototype.enableKeyBackup = function(info) {
|
||||
this._crypto.backupKey.set_recipient_key(info.auth_data.public_key);
|
||||
|
||||
this.emit('crypto.keyBackupStatus', true);
|
||||
|
||||
// There may be keys left over from a partially completed backup, so
|
||||
// schedule a send to check.
|
||||
this._crypto.scheduleKeyBackupSend();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -992,12 +1085,16 @@ MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data
|
||||
);
|
||||
};
|
||||
|
||||
MatrixClient.prototype.backupAllGroupSessions = function(version) {
|
||||
/**
|
||||
* Marks all group sessions as needing to be backed up and schedules them to
|
||||
* upload in the background as soon as possible.
|
||||
*/
|
||||
MatrixClient.prototype.scheduleAllGroupSessionsForBackup = async function() {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
return this._crypto.backupAllGroupSessions(version);
|
||||
await this._crypto.scheduleAllGroupSessionsForBackup();
|
||||
};
|
||||
|
||||
MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
|
||||
@@ -1009,28 +1106,28 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
|
||||
}
|
||||
};
|
||||
|
||||
MatrixClient.prototype.restoreKeyBackupWithPassword = async function(
|
||||
password, targetRoomId, targetSessionId, version,
|
||||
) {
|
||||
const backupInfo = await this.getKeyBackupVersion();
|
||||
MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
|
||||
|
||||
MatrixClient.prototype.restoreKeyBackupWithPassword = async function(
|
||||
password, targetRoomId, targetSessionId, backupInfo,
|
||||
) {
|
||||
const privKey = await keyForExistingBackup(backupInfo, password);
|
||||
return this._restoreKeyBackup(
|
||||
privKey, targetRoomId, targetSessionId, version,
|
||||
privKey, targetRoomId, targetSessionId, backupInfo,
|
||||
);
|
||||
};
|
||||
|
||||
MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function(
|
||||
recoveryKey, targetRoomId, targetSessionId, version,
|
||||
recoveryKey, targetRoomId, targetSessionId, backupInfo,
|
||||
) {
|
||||
const privKey = decodeRecoveryKey(recoveryKey);
|
||||
return this._restoreKeyBackup(
|
||||
privKey, targetRoomId, targetSessionId, version,
|
||||
privKey, targetRoomId, targetSessionId, backupInfo,
|
||||
);
|
||||
};
|
||||
|
||||
MatrixClient.prototype._restoreKeyBackup = function(
|
||||
privKey, targetRoomId, targetSessionId, version,
|
||||
privKey, targetRoomId, targetSessionId, backupInfo,
|
||||
) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
@@ -1038,16 +1135,26 @@ MatrixClient.prototype._restoreKeyBackup = function(
|
||||
let totalKeyCount = 0;
|
||||
let keys = [];
|
||||
|
||||
const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version);
|
||||
const path = this._makeKeyBackupPath(
|
||||
targetRoomId, targetSessionId, backupInfo.version,
|
||||
);
|
||||
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
let backupPubKey;
|
||||
try {
|
||||
decryption.init_with_private_key(privKey);
|
||||
backupPubKey = decryption.init_with_private_key(privKey);
|
||||
} catch(e) {
|
||||
decryption.free();
|
||||
throw e;
|
||||
}
|
||||
|
||||
// If the pubkey computed from the private data we've been given
|
||||
// doesn't match the one in the auth_data, the user has enetered
|
||||
// a different recovery key / the wrong passphrase.
|
||||
if (backupPubKey !== backupInfo.auth_data.public_key) {
|
||||
return Promise.reject({errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY});
|
||||
}
|
||||
|
||||
return this._http.authedRequest(
|
||||
undefined, "GET", path.path, path.queryData,
|
||||
).then((res) => {
|
||||
@@ -1082,6 +1189,8 @@ MatrixClient.prototype._restoreKeyBackup = function(
|
||||
}
|
||||
|
||||
return this.importRoomKeys(keys);
|
||||
}).then(() => {
|
||||
return this._crypto.setTrustedBackupPubKey(backupPubKey);
|
||||
}).then(() => {
|
||||
return {total: totalKeyCount, imported: keys.length};
|
||||
}).finally(() => {
|
||||
@@ -2271,16 +2380,21 @@ MatrixClient.prototype.mxcUrlToHttp =
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype._unstable_setStatusMessage = function(newMessage) {
|
||||
const type = "im.vector.user_status";
|
||||
return Promise.all(this.getRooms().map((room) => {
|
||||
const isJoined = room.getMyMembership() === "join";
|
||||
const looksLikeDm = room.getInvitedAndJoinedMemberCount() === 2;
|
||||
if (isJoined && looksLikeDm) {
|
||||
return this.sendStateEvent(room.roomId, "im.vector.user_status", {
|
||||
status: newMessage,
|
||||
}, this.getUserId());
|
||||
} else {
|
||||
if (!isJoined || !looksLikeDm) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// Check power level separately as it's a bit more expensive.
|
||||
const maySend = room.currentState.mayClientSendStateEvent(type, this);
|
||||
if (!maySend) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.sendStateEvent(room.roomId, type, {
|
||||
status: newMessage,
|
||||
}, this.getUserId());
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -4148,6 +4262,34 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
|
||||
* @event module:client~MatrixClient#"crypto.suggestKeyRestore"
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when a key verification is requested.
|
||||
* @event module:client~MatrixClient#"crypto.verification.request"
|
||||
* @param {object} data
|
||||
* @param {MatrixEvent} data.event the original verification request message
|
||||
* @param {Array} data.methods the verification methods that can be used
|
||||
* @param {Function} data.beginKeyVerification a function to call if a key
|
||||
* verification should be performed. The function takes one argument: the
|
||||
* name of the key verification method (taken from data.methods) to use.
|
||||
* @param {Function} data.cancel a function to call if the key verification is
|
||||
* rejected.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when a key verification is requested with an unknown method.
|
||||
* @event module:client~MatrixClient#"crypto.verification.request.unknown"
|
||||
* @param {string} userId the user ID who requested the key verification
|
||||
* @param {Function} cancel a function that will send a cancellation message to
|
||||
* reject the key verification.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when a key verification started message is received.
|
||||
* @event module:client~MatrixClient#"crypto.verification.start"
|
||||
* @param {module:crypto/verification/Base} verifier a verifier object to
|
||||
* perform the key verification
|
||||
*/
|
||||
|
||||
// EventEmitter JSDocs
|
||||
|
||||
/**
|
||||
|
||||
@@ -475,10 +475,10 @@ export default class DeviceList {
|
||||
if (!this._deviceTrackingStatus[userId]) {
|
||||
logger.log('Now tracking device list for ' + userId);
|
||||
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
||||
// we don't yet persist the tracking status, since there may be a lot
|
||||
// of calls; we save all data together once the sync is done
|
||||
this._dirty = true;
|
||||
}
|
||||
// we don't yet persist the tracking status, since there may be a lot
|
||||
// of calls; we save all data together once the sync is done
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -758,7 +758,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
|
||||
} catch (e) {
|
||||
let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
|
||||
|
||||
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
|
||||
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
|
||||
this._requestKeysForEvent(event);
|
||||
|
||||
errorCode = 'OLM_UNKNOWN_MESSAGE_INDEX';
|
||||
@@ -766,7 +766,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
|
||||
|
||||
throw new base.DecryptionError(
|
||||
errorCode,
|
||||
e.toString(), {
|
||||
e ? e.toString() : "Unknown Error: Error is undefined", {
|
||||
session: content.sender_key + '|' + content.session_id,
|
||||
},
|
||||
);
|
||||
|
||||
+491
-44
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -33,10 +33,34 @@ const algorithms = require("./algorithms");
|
||||
const DeviceInfo = require("./deviceinfo");
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
const DeviceList = require('./DeviceList').default;
|
||||
import { randomString } from '../randomstring';
|
||||
|
||||
import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager';
|
||||
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||
|
||||
import {ShowQRCode, ScanQRCode} from './verification/QRCode';
|
||||
import SAS from './verification/SAS';
|
||||
import {
|
||||
newUserCancelledError,
|
||||
newUnexpectedMessageError,
|
||||
newUnknownMethodError,
|
||||
} from './verification/Error';
|
||||
|
||||
const defaultVerificationMethods = {
|
||||
[ScanQRCode.NAME]: ScanQRCode,
|
||||
[ShowQRCode.NAME]: ShowQRCode,
|
||||
[SAS.NAME]: SAS,
|
||||
};
|
||||
|
||||
/**
|
||||
* verification method names
|
||||
*/
|
||||
export const verificationMethods = {
|
||||
QR_CODE_SCAN: ScanQRCode.NAME,
|
||||
QR_CODE_SHOW: ShowQRCode.NAME,
|
||||
SAS: SAS.NAME,
|
||||
};
|
||||
|
||||
export function isCryptoAvailable() {
|
||||
return Boolean(global.Olm);
|
||||
}
|
||||
@@ -69,9 +93,13 @@ const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
* storage for the crypto layer.
|
||||
*
|
||||
* @param {RoomList} roomList An initialised RoomList object
|
||||
*
|
||||
* @param {Array} verificationMethods Array of verification methods to use.
|
||||
* Each element can either be a string from MatrixClient.verificationMethods
|
||||
* or a class that implements a verification method.
|
||||
*/
|
||||
export default function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
clientStore, cryptoStore, roomList) {
|
||||
clientStore, cryptoStore, roomList, verificationMethods) {
|
||||
this._baseApis = baseApis;
|
||||
this._sessionStore = sessionStore;
|
||||
this._userId = userId;
|
||||
@@ -79,6 +107,24 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
this._clientStore = clientStore;
|
||||
this._cryptoStore = cryptoStore;
|
||||
this._roomList = roomList;
|
||||
this._verificationMethods = new Map();
|
||||
if (verificationMethods) {
|
||||
for (const method of verificationMethods) {
|
||||
if (typeof method === "string") {
|
||||
if (defaultVerificationMethods[method]) {
|
||||
this._verificationMethods.set(
|
||||
method,
|
||||
defaultVerificationMethods[method],
|
||||
);
|
||||
}
|
||||
} else if (method.NAME) {
|
||||
this._verificationMethods.set(
|
||||
method.NAME,
|
||||
method,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// track whether this device's megolm keys are being backed up incrementally
|
||||
// to the server or not.
|
||||
@@ -140,6 +186,8 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
// },
|
||||
// }
|
||||
this._lastNewSessionForced = {};
|
||||
|
||||
this._verificationTransactions = new Map();
|
||||
}
|
||||
utils.inherits(Crypto, EventEmitter);
|
||||
|
||||
@@ -216,7 +264,7 @@ Crypto.prototype._checkAndStartKeyBackup = async function() {
|
||||
if (this._baseApis.isGuest()) {
|
||||
console.log("Skipping key backup check since user is guest");
|
||||
this._checkedForBackup = true;
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
let backupInfo;
|
||||
try {
|
||||
@@ -227,32 +275,60 @@ Crypto.prototype._checkAndStartKeyBackup = async function() {
|
||||
// well that's told us. we won't try again.
|
||||
this._checkedForBackup = true;
|
||||
}
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
this._checkedForBackup = true;
|
||||
|
||||
const trustInfo = await this.isKeyBackupTrusted(backupInfo);
|
||||
|
||||
if (trustInfo.usable && !this.backupInfo) {
|
||||
console.log("Found usable key backup: enabling key backups");
|
||||
console.log(
|
||||
"Found usable key backup v" + backupInfo.version +
|
||||
": enabling key backups",
|
||||
);
|
||||
this._baseApis.enableKeyBackup(backupInfo);
|
||||
} else if (!trustInfo.usable && this.backupInfo) {
|
||||
console.log("No usable key backup: disabling key backup");
|
||||
this._baseApis.disableKeyBackup();
|
||||
} else if (!trustInfo.usable && !this.backupInfo) {
|
||||
console.log("No usable key backup: not enabling key backup");
|
||||
} else if (trustInfo.usable && this.backupInfo) {
|
||||
// may not be the same version: if not, we should switch
|
||||
if (backupInfo.version !== this.backupInfo.version) {
|
||||
console.log(
|
||||
"On backup version " + this.backupInfo.version + " but found " +
|
||||
"version " + backupInfo.version + ": switching.",
|
||||
);
|
||||
this._baseApis.disableKeyBackup();
|
||||
this._baseApis.enableKeyBackup(backupInfo);
|
||||
} else {
|
||||
console.log("Backup version " + backupInfo.version + " still current");
|
||||
}
|
||||
}
|
||||
|
||||
return {backupInfo, trustInfo};
|
||||
};
|
||||
|
||||
Crypto.prototype.setTrustedBackupPubKey = async function(trustedPubKey) {
|
||||
// This should be redundant post cross-signing is a thing, so just
|
||||
// plonk it in localStorage for now.
|
||||
this._sessionStore.setLocalTrustedBackupPubKey(trustedPubKey);
|
||||
await this.checkKeyBackup();
|
||||
};
|
||||
|
||||
/**
|
||||
* Forces a re-check of the key backup and enables/disables it
|
||||
* as appropriate
|
||||
* as appropriate.
|
||||
*
|
||||
* @param {object} backupInfo Backup info from /room_keys/version endpoint
|
||||
* @return {Object} Object with backup info (as returned by
|
||||
* getKeyBackupVersion) in backupInfo and
|
||||
* trust information (as returned by isKeyBackupTrusted)
|
||||
* in trustInfo.
|
||||
*/
|
||||
Crypto.prototype.checkKeyBackup = async function(backupInfo) {
|
||||
Crypto.prototype.checkKeyBackup = async function() {
|
||||
this._checkedForBackup = false;
|
||||
await this._checkAndStartKeyBackup();
|
||||
const returnInfo = await this._checkAndStartKeyBackup();
|
||||
return returnInfo;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -260,14 +336,16 @@ Crypto.prototype.checkKeyBackup = async function(backupInfo) {
|
||||
* @return {object} {
|
||||
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
|
||||
* sigs: [
|
||||
* valid: [bool],
|
||||
* device: [DeviceInfo],
|
||||
* valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation
|
||||
* deviceId: [string],
|
||||
* device: [DeviceInfo || null],
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
|
||||
const ret = {
|
||||
usable: false,
|
||||
trusted_locally: false,
|
||||
sigs: [],
|
||||
};
|
||||
|
||||
@@ -278,42 +356,50 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
|
||||
!backupInfo.auth_data.public_key ||
|
||||
!backupInfo.auth_data.signatures
|
||||
) {
|
||||
console.log("Key backup is absent or missing required data");
|
||||
logger.info("Key backup is absent or missing required data");
|
||||
return ret;
|
||||
}
|
||||
|
||||
const mySigs = backupInfo.auth_data.signatures[this._userId];
|
||||
if (!mySigs || mySigs.length === 0) {
|
||||
console.log("Ignoring key backup because it lacks any signatures from this user");
|
||||
return ret;
|
||||
const trustedPubkey = this._sessionStore.getLocalTrustedBackupPubKey();
|
||||
|
||||
if (backupInfo.auth_data.public_key === trustedPubkey) {
|
||||
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
|
||||
ret.trusted_locally = true;
|
||||
}
|
||||
|
||||
const mySigs = backupInfo.auth_data.signatures[this._userId] || [];
|
||||
|
||||
for (const keyId of Object.keys(mySigs)) {
|
||||
const sigInfo = { deviceId: keyId.split(':')[1] }; // XXX: is this how we're supposed to get the device ID?
|
||||
const device = this._deviceList.getStoredDevice(
|
||||
this._userId, keyId.split(':')[1], // XXX: is this how we're supposed to get the device ID?
|
||||
this._userId, sigInfo.deviceId,
|
||||
);
|
||||
if (!device) {
|
||||
console.log("Ignoring signature from unknown key " + keyId);
|
||||
continue;
|
||||
}
|
||||
const sigInfo = { device };
|
||||
try {
|
||||
await olmlib.verifySignature(
|
||||
this._olmDevice,
|
||||
backupInfo.auth_data,
|
||||
this._userId,
|
||||
device.deviceId,
|
||||
device.getFingerprint(),
|
||||
);
|
||||
sigInfo.valid = true;
|
||||
} catch (e) {
|
||||
console.log("Bad signature from device " + device.deviceId, e);
|
||||
sigInfo.valid = false;
|
||||
if (device) {
|
||||
sigInfo.device = device;
|
||||
try {
|
||||
await olmlib.verifySignature(
|
||||
this._olmDevice,
|
||||
backupInfo.auth_data,
|
||||
this._userId,
|
||||
device.deviceId,
|
||||
device.getFingerprint(),
|
||||
);
|
||||
sigInfo.valid = true;
|
||||
} catch (e) {
|
||||
logger.info("Bad signature from device " + device.deviceId, e);
|
||||
sigInfo.valid = false;
|
||||
}
|
||||
} else {
|
||||
sigInfo.valid = null; // Can't determine validity because we don't have the signing device
|
||||
logger.info("Ignoring signature from unknown key " + keyId);
|
||||
}
|
||||
ret.sigs.push(sigInfo);
|
||||
}
|
||||
|
||||
ret.usable = ret.sigs.some((s) => s.valid && s.device.isVerified());
|
||||
ret.usable = (
|
||||
ret.sigs.some((s) => s.valid && s.device.isVerified()) ||
|
||||
ret.trusted_locally
|
||||
);
|
||||
return ret;
|
||||
};
|
||||
|
||||
@@ -672,6 +758,81 @@ Crypto.prototype.setDeviceVerification = async function(
|
||||
};
|
||||
|
||||
|
||||
Crypto.prototype.requestVerification = function(userId, methods, devices) {
|
||||
if (!methods) {
|
||||
// .keys() returns an iterator, so we need to explicitly turn it into an array
|
||||
methods = [...this._verificationMethods.keys()];
|
||||
}
|
||||
if (!devices) {
|
||||
devices = Object.keys(this._deviceList.getRawStoredDevicesForUser(userId));
|
||||
}
|
||||
if (!this._verificationTransactions.has(userId)) {
|
||||
this._verificationTransactions.set(userId, new Map);
|
||||
}
|
||||
|
||||
const transactionId = randomString(32);
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
this._verificationTransactions.get(userId).set(transactionId, {
|
||||
request: {
|
||||
methods: methods,
|
||||
devices: devices,
|
||||
resolve: resolve,
|
||||
reject: reject,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const message = {
|
||||
transaction_id: transactionId,
|
||||
from_device: this._baseApis.deviceId,
|
||||
methods: methods,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
const msgMap = {};
|
||||
for (const deviceId of devices) {
|
||||
msgMap[deviceId] = message;
|
||||
}
|
||||
this._baseApis.sendToDevice("m.key.verification.request", {[userId]: msgMap});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
Crypto.prototype.beginKeyVerification = function(
|
||||
method, userId, deviceId, transactionId,
|
||||
) {
|
||||
if (!this._verificationTransactions.has(userId)) {
|
||||
this._verificationTransactions.set(userId, new Map());
|
||||
}
|
||||
transactionId = transactionId || randomString(32);
|
||||
if (method instanceof Array) {
|
||||
if (method.length !== 2
|
||||
|| !this._verificationMethods.has(method[0])
|
||||
|| !this._verificationMethods.has(method[1])) {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
/*
|
||||
return new TwoPartVerification(
|
||||
this._verificationMethods[method[0]],
|
||||
this._verificationMethods[method[1]],
|
||||
userId, deviceId, transactionId,
|
||||
);
|
||||
*/
|
||||
} else if (this._verificationMethods.has(method)) {
|
||||
const verifier = new (this._verificationMethods.get(method))(
|
||||
this._baseApis, userId, deviceId, transactionId,
|
||||
);
|
||||
if (!this._verificationTransactions.get(userId).has(transactionId)) {
|
||||
this._verificationTransactions.get(userId).set(transactionId, {});
|
||||
}
|
||||
this._verificationTransactions.get(userId).get(transactionId).verifier = verifier;
|
||||
return verifier;
|
||||
} else {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get information on the active olm sessions with a user
|
||||
* <p>
|
||||
@@ -990,15 +1151,19 @@ Crypto.prototype.importRoomKeys = function(keys) {
|
||||
/**
|
||||
* Schedules sending all keys waiting to be sent to the backup, if not already
|
||||
* scheduled. Retries if necessary.
|
||||
*
|
||||
* @param {number} maxDelay Maximum delay to wait in ms. 0 means no delay.
|
||||
*/
|
||||
Crypto.prototype._scheduleKeyBackupSend = async function() {
|
||||
Crypto.prototype.scheduleKeyBackupSend = async function(maxDelay = 10000) {
|
||||
if (this._sendingBackups) return;
|
||||
|
||||
this._sendingBackups = true;
|
||||
|
||||
try {
|
||||
// wait between 0 and 10 seconds, to avoid backup
|
||||
// wait between 0 and `maxDelay` seconds, to avoid backup
|
||||
// requests from different clients hitting the server all at
|
||||
// the same time when a new key is sent
|
||||
const delay = Math.random() * 10000;
|
||||
const delay = Math.random() * maxDelay;
|
||||
await Promise.delay(delay);
|
||||
let numFailures = 0; // number of consecutive failures
|
||||
while (1) {
|
||||
@@ -1021,6 +1186,9 @@ Crypto.prototype._scheduleKeyBackupSend = async function() {
|
||||
err.data.errcode == 'M_NOT_FOUND' ||
|
||||
err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION'
|
||||
) {
|
||||
// Re-check key backup status on error, so we can be
|
||||
// sure to present the current situation when asked.
|
||||
await this.checkKeyBackup();
|
||||
// Backup version has changed or this backup version
|
||||
// has been deleted
|
||||
this.emit("crypto.keyBackupFailed", err.data.errcode);
|
||||
@@ -1051,6 +1219,9 @@ Crypto.prototype._backupPendingKeys = async function(limit) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let remaining = await this._cryptoStore.countSessionsNeedingBackup();
|
||||
this.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
|
||||
const data = {};
|
||||
for (const session of sessions) {
|
||||
const roomId = session.sessionData.room_id;
|
||||
@@ -1087,7 +1258,10 @@ Crypto.prototype._backupPendingKeys = async function(limit) {
|
||||
undefined, undefined, this.backupInfo.version,
|
||||
{rooms: data},
|
||||
);
|
||||
|
||||
await this._cryptoStore.unmarkSessionsNeedingBackup(sessions);
|
||||
remaining = await this._cryptoStore.countSessionsNeedingBackup();
|
||||
this.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
|
||||
return sessions.length;
|
||||
};
|
||||
@@ -1108,10 +1282,14 @@ Crypto.prototype.backupGroupSession = async function(
|
||||
|
||||
// don't wait for this to complete: it will delay so
|
||||
// happens in the background
|
||||
this._scheduleKeyBackupSend();
|
||||
this.scheduleKeyBackupSend();
|
||||
};
|
||||
|
||||
Crypto.prototype.backupAllGroupSessions = async function(version) {
|
||||
/**
|
||||
* Marks all group sessions as needing to be backed up and schedules them to
|
||||
* upload in the background as soon as possible.
|
||||
*/
|
||||
Crypto.prototype.scheduleAllGroupSessionsForBackup = async function() {
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[
|
||||
@@ -1127,10 +1305,11 @@ Crypto.prototype.backupAllGroupSessions = async function(version) {
|
||||
},
|
||||
);
|
||||
|
||||
let numKeysBackedUp;
|
||||
do {
|
||||
numKeysBackedUp = await this._backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
|
||||
} while (numKeysBackedUp > 0);
|
||||
const remaining = await this._cryptoStore.countSessionsNeedingBackup();
|
||||
this.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
|
||||
// Schedule keys to upload in the background as soon as possible.
|
||||
this.scheduleKeyBackupSend(0 /* maxDelay */);
|
||||
};
|
||||
|
||||
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
|
||||
@@ -1326,6 +1505,10 @@ Crypto.prototype.onSyncCompleted = async function(syncData) {
|
||||
|
||||
// catch up on any new devices we got told about during the sync.
|
||||
this._deviceList.lastKnownSyncToken = nextSyncToken;
|
||||
|
||||
// we always track our own device list (for key backups etc)
|
||||
this._deviceList.startTrackingDeviceList(this._userId);
|
||||
|
||||
this._deviceList.refreshOutdatedDeviceLists();
|
||||
|
||||
// we don't start uploading one-time keys until we've caught up with
|
||||
@@ -1415,6 +1598,12 @@ Crypto.prototype._onToDeviceEvent = function(event) {
|
||||
this._onRoomKeyEvent(event);
|
||||
} else if (event.getType() == "m.room_key_request") {
|
||||
this._onRoomKeyRequestEvent(event);
|
||||
} else if (event.getType() === "m.key.verification.request") {
|
||||
this._onKeyVerificationRequest(event);
|
||||
} else if (event.getType() === "m.key.verification.start") {
|
||||
this._onKeyVerificationStart(event);
|
||||
} else if (event.getContent().transaction_id) {
|
||||
this._onKeyVerificationMessage(event);
|
||||
} else if (event.getContent().msgtype === "m.bad.encrypted") {
|
||||
this._onToDeviceBadEncrypted(event);
|
||||
} else if (event.isBeingDecrypted()) {
|
||||
@@ -1452,6 +1641,264 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
|
||||
alg.onRoomKeyEvent(event);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a key verification request event.
|
||||
*
|
||||
* @private
|
||||
* @param {module:models/event.MatrixEvent} event verification request event
|
||||
*/
|
||||
Crypto.prototype._onKeyVerificationRequest = function(event) {
|
||||
const content = event.getContent();
|
||||
if (!("from_device" in content) || typeof content.from_device !== "string"
|
||||
|| !("transaction_id" in content) || typeof content.from_device !== "string"
|
||||
|| !("methods" in content) || !(content.methods instanceof Array)
|
||||
|| !("timestamp" in content) || typeof content.timestamp !== "number") {
|
||||
logger.warn("received invalid verification request from " + event.getSender());
|
||||
// ignore event if malformed
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now < content.timestamp - (5 * 60 * 1000)
|
||||
|| now > content.timestamp + (10 * 60 * 1000)) {
|
||||
// ignore if event is too far in the past or too far in the future
|
||||
logger.log("received verification that is too old or from the future");
|
||||
return;
|
||||
}
|
||||
|
||||
const sender = event.getSender();
|
||||
if (this._verificationTransactions.has(sender)) {
|
||||
if (this._verificationTransactions.get(sender).has(content.transaction_id)) {
|
||||
// transaction already exists: cancel it and drop the existing
|
||||
// request because someone has gotten confused
|
||||
const err = newUnexpectedMessageError({
|
||||
transaction_id: content.transaction_id,
|
||||
});
|
||||
if (this._verificationTransactions.get(sender).get(content.transaction_id)
|
||||
.verifier) {
|
||||
this._verificationTransactions.get(sender).get(content.transaction_id)
|
||||
.verifier.cancel(err);
|
||||
} else {
|
||||
this._verificationTransactions.get(sender).get(content.transaction_id)
|
||||
.reject(err);
|
||||
this.sendToDevice("m.key.verification.cancel", {
|
||||
[sender]: {
|
||||
[content.from_device]: err.getContent(),
|
||||
},
|
||||
});
|
||||
}
|
||||
this._verificationTransactions.get(sender).delete(content.transaction_id);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this._verificationTransactions.set(sender, new Map());
|
||||
}
|
||||
|
||||
// determine what requested methods we support
|
||||
const methods = [];
|
||||
for (const method of content.methods) {
|
||||
if (typeof method !== "string") {
|
||||
continue;
|
||||
}
|
||||
if (this._verificationMethods.has(method)) {
|
||||
methods.push(method);
|
||||
}
|
||||
}
|
||||
if (methods.length === 0) {
|
||||
this._baseApis.emit(
|
||||
"crypto.verification.request.unknown",
|
||||
event.getSender(),
|
||||
() => {
|
||||
this.sendToDevice("m.key.verification.cancel", {
|
||||
[sender]: {
|
||||
[content.from_device]: newUserCancelledError({
|
||||
transaction_id: content.transaction_id,
|
||||
}).getContent(),
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// notify the application that of the verification request, so it can
|
||||
// decide what to do with it
|
||||
const request = {
|
||||
event: event,
|
||||
methods: methods,
|
||||
beginKeyVerification: (method) => {
|
||||
const verifier = this.beginKeyVerification(
|
||||
method,
|
||||
sender,
|
||||
content.from_device,
|
||||
content.transaction_id,
|
||||
);
|
||||
this._verificationTransactions.get(sender).get(content.transaction_id)
|
||||
.verifier = verifier;
|
||||
return verifier;
|
||||
},
|
||||
cancel: () => {
|
||||
this._baseApis.sendToDevice("m.key.verification.cancel", {
|
||||
[sender]: {
|
||||
[content.from_device]: newUserCancelledError({
|
||||
transaction_id: content.transaction_id,
|
||||
}).getContent(),
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
this._verificationTransactions.get(sender).set(content.transaction_id, {
|
||||
request: request,
|
||||
});
|
||||
this._baseApis.emit("crypto.verification.request", request);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a key verification start event.
|
||||
*
|
||||
* @private
|
||||
* @param {module:models/event.MatrixEvent} event verification start event
|
||||
*/
|
||||
Crypto.prototype._onKeyVerificationStart = function(event) {
|
||||
const sender = event.getSender();
|
||||
const content = event.getContent();
|
||||
const transactionId = content.transaction_id;
|
||||
const deviceId = content.from_device;
|
||||
if (!transactionId || !deviceId) {
|
||||
// invalid request, and we don't have enough information to send a
|
||||
// cancellation, so just ignore it
|
||||
return;
|
||||
}
|
||||
|
||||
let handler = this._verificationTransactions.has(sender)
|
||||
&& this._verificationTransactions.get(sender).get(transactionId);
|
||||
// if the verification start message is invalid, send a cancel message to
|
||||
// the other side, and also send a cancellation event
|
||||
const cancel = (err) => {
|
||||
if (handler.verifier) {
|
||||
handler.verifier.cancel(err);
|
||||
} else if (handler.request && handler.request.cancel) {
|
||||
handler.request.cancel(err);
|
||||
}
|
||||
this.sendToDevice(
|
||||
"m.key.verification.cancel", {
|
||||
[sender]: {
|
||||
[deviceId]: err.getContent(),
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
if (!this._verificationMethods.has(content.method)) {
|
||||
cancel(newUnknownMethodError({
|
||||
transaction_id: content.transactionId,
|
||||
}));
|
||||
return;
|
||||
} else if (content.next_method) {
|
||||
if (!this._verificationMethods.has(content.next_method)) {
|
||||
cancel(newUnknownMethodError({
|
||||
transaction_id: content.transactionId,
|
||||
}));
|
||||
return;
|
||||
} else {
|
||||
/* TODO:
|
||||
const verification = new TwoPartVerification(
|
||||
this._verificationMethods[content.method],
|
||||
this._verificationMethods[content.next_method],
|
||||
userId, deviceId,
|
||||
);
|
||||
this.emit(verification.event_type, verification);
|
||||
this.emit(verification.first.event_type, verification);*/
|
||||
}
|
||||
} else {
|
||||
const verifier = new (this._verificationMethods.get(content.method))(
|
||||
this._baseApis, sender, deviceId, content.transaction_id,
|
||||
event, handler && handler.request,
|
||||
);
|
||||
if (!handler) {
|
||||
if (!this._verificationTransactions.has(sender)) {
|
||||
this._verificationTransactions.set(sender, new Map());
|
||||
}
|
||||
handler = this._verificationTransactions.get(sender).set(transactionId, {
|
||||
verifier: verifier,
|
||||
});
|
||||
} else {
|
||||
if (!handler.verifier) {
|
||||
handler.verifier = verifier;
|
||||
if (handler.request) {
|
||||
// the verification start was sent as a response to a
|
||||
// verification request
|
||||
|
||||
if (!handler.request.devices.includes(deviceId)) {
|
||||
// didn't send a request to that device, so it
|
||||
// shouldn't have responded
|
||||
cancel(newUnexpectedMessageError({
|
||||
transaction_id: content.transactionId,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (!handler.request.methods.includes(content.method)) {
|
||||
// verification method wasn't one that was requested
|
||||
cancel(newUnknownMethodError({
|
||||
transaction_id: content.transactionId,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// send cancellation messages to all the other devices that
|
||||
// the request was sent to
|
||||
const message = {
|
||||
transaction_id: transactionId,
|
||||
code: "m.accepted",
|
||||
reason: "Verification request accepted by another device",
|
||||
};
|
||||
const msgMap = {};
|
||||
for (const devId of handler.request.devices) {
|
||||
if (devId !== deviceId) {
|
||||
msgMap[devId] = message;
|
||||
}
|
||||
}
|
||||
this._baseApis.sendToDevice("m.key.verification.cancel", {
|
||||
[sender]: msgMap,
|
||||
});
|
||||
|
||||
handler.request.resolve(verifier);
|
||||
}
|
||||
} else {
|
||||
// FIXME: make sure we're in a two-part verification, and the start matches the second part
|
||||
}
|
||||
}
|
||||
this._baseApis.emit("crypto.verification.start", verifier);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a general key verification event.
|
||||
*
|
||||
* @private
|
||||
* @param {module:models/event.MatrixEvent} event verification start event
|
||||
*/
|
||||
Crypto.prototype._onKeyVerificationMessage = function(event) {
|
||||
const sender = event.getSender();
|
||||
const transactionId = event.getContent().transaction_id;
|
||||
const handler = this._verificationTransactions.has(sender)
|
||||
&& this._verificationTransactions.get(sender).get(transactionId);
|
||||
if (!handler) {
|
||||
return;
|
||||
} else if (event.getType() === "m.key.verification.cancel") {
|
||||
console.log(event);
|
||||
if (handler.verifier) {
|
||||
handler.verifier.cancel(event);
|
||||
} else if (handler.request && handler.request.cancel) {
|
||||
handler.request.cancel(event);
|
||||
}
|
||||
} else if (handler.verifier) {
|
||||
const verifier = handler.verifier;
|
||||
if (verifier.events
|
||||
&& verifier.events.includes(event.getType())) {
|
||||
verifier.handleEvent(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a toDevice event that couldn't be decrypted
|
||||
*
|
||||
|
||||
@@ -563,8 +563,22 @@ export class Backend {
|
||||
});
|
||||
}
|
||||
|
||||
unmarkSessionsNeedingBackup(sessions) {
|
||||
const txn = this._db.transaction("sessions_needing_backup", "readwrite");
|
||||
countSessionsNeedingBackup(txn) {
|
||||
if (!txn) {
|
||||
txn = this._db.transaction("sessions_needing_backup", "readonly");
|
||||
}
|
||||
const objectStore = txn.objectStore("sessions_needing_backup");
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = objectStore.count();
|
||||
req.onerror = reject;
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
});
|
||||
}
|
||||
|
||||
unmarkSessionsNeedingBackup(sessions, txn) {
|
||||
if (!txn) {
|
||||
txn = this._db.transaction("sessions_needing_backup", "readwrite");
|
||||
}
|
||||
const objectStore = txn.objectStore("sessions_needing_backup");
|
||||
return Promise.all(sessions.map((session) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -676,11 +690,12 @@ function promiseifyTxn(txn) {
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
txn.onerror = () => {
|
||||
txn.onerror = (event) => {
|
||||
if (txn._mx_abortexception !== undefined) {
|
||||
reject(txn._mx_abortexception);
|
||||
} else {
|
||||
reject(event.target.error);
|
||||
}
|
||||
reject();
|
||||
};
|
||||
txn.onabort = () => reject(txn._mx_abortexception);
|
||||
});
|
||||
|
||||
@@ -480,14 +480,26 @@ export default class IndexedDBCryptoStore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the inbound group sessions that need to be backed up.
|
||||
* @param {*} txn An active transaction. See doTxn(). (optional)
|
||||
* @returns {Promise} resolves to the number of sessions
|
||||
*/
|
||||
countSessionsNeedingBackup(txn) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.countSessionsNeedingBackup(txn);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmark sessions as needing to be backed up.
|
||||
* @param {Array<object>} sessions The sessions that need to be backed up.
|
||||
* @param {*} txn An active transaction. See doTxn(). (optional)
|
||||
* @returns {Promise} resolves when the sessions are unmarked
|
||||
*/
|
||||
unmarkSessionsNeedingBackup(sessions) {
|
||||
unmarkSessionsNeedingBackup(sessions, txn) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.unmarkSessionsNeedingBackup(sessions);
|
||||
return backend.unmarkSessionsNeedingBackup(sessions, txn);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -221,6 +221,12 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
return Promise.resolve(sessions);
|
||||
}
|
||||
|
||||
countSessionsNeedingBackup() {
|
||||
const sessionsNeedingBackup
|
||||
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
|
||||
return Promise.resolve(Object.keys(sessionsNeedingBackup).length);
|
||||
}
|
||||
|
||||
unmarkSessionsNeedingBackup(sessions) {
|
||||
const sessionsNeedingBackup
|
||||
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
|
||||
|
||||
@@ -336,6 +336,10 @@ export default class MemoryCryptoStore {
|
||||
return Promise.resolve(sessions);
|
||||
}
|
||||
|
||||
countSessionsNeedingBackup() {
|
||||
return Promise.resolve(Object.keys(this._sessionsNeedingBackup).length);
|
||||
}
|
||||
|
||||
unmarkSessionsNeedingBackup(sessions) {
|
||||
for (const session of sessions) {
|
||||
const sessionKey = session.senderKey + '/' + session.sessionId;
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base class for verification methods.
|
||||
* @module crypto/verification/Base
|
||||
*/
|
||||
|
||||
import {MatrixEvent} from '../../models/event';
|
||||
import {EventEmitter} from 'events';
|
||||
|
||||
export default class VerificationBase extends EventEmitter {
|
||||
/**
|
||||
* Base class for verification methods.
|
||||
*
|
||||
* <p>Once a verifier object is created, the verification can be started by
|
||||
* calling the verify() method, which will return a promise that will
|
||||
* resolve when the verification is completed, or reject if it could not
|
||||
* complete.</p>
|
||||
*
|
||||
* <p>Subclasses must have a NAME class property.</p>
|
||||
*
|
||||
* @class
|
||||
*
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
|
||||
*
|
||||
* @param {string} userId the user ID that is being verified
|
||||
*
|
||||
* @param {string} deviceId the device ID that is being verified
|
||||
*
|
||||
* @param {string} transactionId the transaction ID to be used when sending events
|
||||
*
|
||||
* @param {object} startEvent the m.key.verification.start event that
|
||||
* initiated this verification, if any
|
||||
*
|
||||
* @param {object} request the key verification request object related to
|
||||
* this verification, if any
|
||||
*
|
||||
* @param {object} parent parent verification for this verification, if any
|
||||
*/
|
||||
constructor(baseApis, userId, deviceId, transactionId, startEvent, request, parent) {
|
||||
super();
|
||||
this._baseApis = baseApis;
|
||||
this.userId = userId;
|
||||
this.deviceId = deviceId;
|
||||
this.transactionId = transactionId;
|
||||
this.startEvent = startEvent;
|
||||
this.request = request;
|
||||
this._parent = parent;
|
||||
this._done = false;
|
||||
this._promise = null;
|
||||
}
|
||||
|
||||
_sendToDevice(type, content) {
|
||||
if (this._done) {
|
||||
return Promise.reject(new Error("Verification is already done"));
|
||||
}
|
||||
content.transaction_id = this.transactionId;
|
||||
return this._baseApis.sendToDevice(type, {
|
||||
[this.userId]: { [this.deviceId]: content },
|
||||
});
|
||||
}
|
||||
|
||||
_waitForEvent(type) {
|
||||
if (this._done) {
|
||||
return Promise.reject(new Error("Verification is already done"));
|
||||
}
|
||||
this._expectedEvent = type;
|
||||
return new Promise((resolve, reject) => {
|
||||
this._resolveEvent = resolve;
|
||||
this._rejectEvent = reject;
|
||||
});
|
||||
}
|
||||
|
||||
handleEvent(e) {
|
||||
if (this._done) {
|
||||
return;
|
||||
} else if (e.getType() === this._expectedEvent) {
|
||||
this._expectedEvent = undefined;
|
||||
this._rejectEvent = undefined;
|
||||
this._resolveEvent(e);
|
||||
} else {
|
||||
this._expectedEvent = undefined;
|
||||
const exception = new Error(
|
||||
"Unexpected message: expecting " + this._expectedEvent
|
||||
+ " but got " + e.getType(),
|
||||
);
|
||||
if (this._rejectEvent) {
|
||||
const reject = this._rejectEvent;
|
||||
this._rejectEvent = undefined;
|
||||
reject(exception);
|
||||
}
|
||||
this.cancel(exception);
|
||||
}
|
||||
}
|
||||
|
||||
done() {
|
||||
if (!this._done) {
|
||||
this._resolve();
|
||||
}
|
||||
}
|
||||
|
||||
cancel(e) {
|
||||
if (!this._done) {
|
||||
if (this.userId && this.deviceId && this.transactionId) {
|
||||
// send a cancellation to the other user (if it wasn't
|
||||
// cancelled by the other user)
|
||||
if (e instanceof MatrixEvent) {
|
||||
const sender = e.getSender();
|
||||
if (sender !== this.userId) {
|
||||
const content = e.getContent();
|
||||
if (e.getType() === "m.key.verification.cancel") {
|
||||
content.code = content.code || "m.unknown";
|
||||
content.reason = content.reason || content.body
|
||||
|| "Unknown reason";
|
||||
content.transaction_id = this.transactionId;
|
||||
this._sendToDevice("m.key.verification.cancel", content);
|
||||
} else {
|
||||
this._sendToDevice("m.key.verification.cancel", {
|
||||
code: "m.unknown",
|
||||
reason: content.body || "Unknown reason",
|
||||
transaction_id: this.transactionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._sendToDevice("m.key.verification.cancel", {
|
||||
code: "m.unknown",
|
||||
reason: e.toString(),
|
||||
transaction_id: this.transactionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this._promise !== null) {
|
||||
this._reject(e);
|
||||
} else {
|
||||
this._promise = Promise.reject(e);
|
||||
}
|
||||
// Also emit a 'cancel' event that the app can listen for to detect cancellation
|
||||
// before calling verify()
|
||||
this.emit('cancel', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin the key verification
|
||||
*
|
||||
* @returns {Promise} Promise which resolves when the verification has
|
||||
* completed.
|
||||
*/
|
||||
verify() {
|
||||
if (this._promise) return this._promise;
|
||||
|
||||
this._promise = new Promise((resolve, reject) => {
|
||||
this._resolve = (...args) => {
|
||||
this._done = true;
|
||||
resolve(...args);
|
||||
};
|
||||
this._reject = (...args) => {
|
||||
this._done = true;
|
||||
reject(...args);
|
||||
};
|
||||
});
|
||||
if (this._doVerification && !this._started) {
|
||||
this._started = true;
|
||||
Promise.resolve(this._doVerification())
|
||||
.then(this.done.bind(this), this.cancel.bind(this));
|
||||
}
|
||||
return this._promise;
|
||||
}
|
||||
|
||||
async _verifyKeys(userId, keys, verifier) {
|
||||
for (const [keyId, keyInfo] of Object.entries(keys)) {
|
||||
const deviceId = keyId.split(':', 2)[1];
|
||||
const device = await this._baseApis.getStoredDevice(userId, deviceId);
|
||||
if (!device) {
|
||||
throw new Error(`Could not find device ${deviceId}`);
|
||||
} else {
|
||||
await verifier(keyId, device, keyInfo);
|
||||
}
|
||||
}
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
const deviceId = keyId.split(':', 2)[1];
|
||||
await this._baseApis.setDeviceVerified(userId, deviceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Error messages.
|
||||
*
|
||||
* @module crypto/verification/Error
|
||||
*/
|
||||
|
||||
import {MatrixEvent} from "../../models/event";
|
||||
|
||||
export function newVerificationError(code, reason, extradata) {
|
||||
extradata = extradata || {};
|
||||
extradata.code = code;
|
||||
extradata.reason = reason;
|
||||
return new MatrixEvent({
|
||||
type: "m.key.verification.cancel",
|
||||
content: extradata,
|
||||
});
|
||||
}
|
||||
|
||||
export function errorFactory(code, reason) {
|
||||
return function(extradata) {
|
||||
return newVerificationError(code, reason, extradata);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The verification was cancelled by the user.
|
||||
*/
|
||||
export const newUserCancelledError = errorFactory("m.user", "Cancelled by user");
|
||||
|
||||
/**
|
||||
* The verification timed out.
|
||||
*/
|
||||
export const newTimeoutError = errorFactory("m.timeout", "Timed out");
|
||||
|
||||
/**
|
||||
* The transaction is unknown.
|
||||
*/
|
||||
export const newUnknownTransactionError = errorFactory(
|
||||
"m.unknown_transaction", "Unknown transaction",
|
||||
);
|
||||
|
||||
/**
|
||||
* An unknown method was selected.
|
||||
*/
|
||||
export const newUnknownMethodError = errorFactory("m.unknown_method", "Unknown method");
|
||||
|
||||
/**
|
||||
* An unexpected message was sent.
|
||||
*/
|
||||
export const newUnexpectedMessageError = errorFactory(
|
||||
"m.unexpected_message", "Unexpected message",
|
||||
);
|
||||
|
||||
/**
|
||||
* The key does not match.
|
||||
*/
|
||||
export const newKeyMismatchError = errorFactory(
|
||||
"m.key_mismatch", "Key mismatch",
|
||||
);
|
||||
|
||||
/**
|
||||
* The user does not match.
|
||||
*/
|
||||
export const newUserMismatchError = errorFactory("m.user_error", "User mismatch");
|
||||
|
||||
/**
|
||||
* An invalid message was sent.
|
||||
*/
|
||||
export const newInvalidMessageError = errorFactory(
|
||||
"m.invalid_message", "Invalid message",
|
||||
);
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* QR code key verification.
|
||||
* @module crypto/verification/QRCode
|
||||
*/
|
||||
|
||||
import Base from "./Base";
|
||||
import {
|
||||
errorFactory,
|
||||
newUserCancelledError,
|
||||
newKeyMismatchError,
|
||||
newUserMismatchError,
|
||||
} from './Error';
|
||||
|
||||
const MATRIXTO_REGEXP = /^(?:https?:\/\/)?(?:www\.)?matrix\.to\/#\/([#@!+][^?]+)\?(.+)$/;
|
||||
const KEY_REGEXP = /^key_([^:]+:.+)$/;
|
||||
|
||||
const newQRCodeError = errorFactory("m.qr_code.invalid", "Invalid QR code");
|
||||
|
||||
/**
|
||||
* @class crypto/verification/QRCode/ShowQRCode
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class ShowQRCode extends Base {
|
||||
_doVerification() {
|
||||
if (!this._done) {
|
||||
const url = "https://matrix.to/#/" + this._baseApis.getUserId()
|
||||
+ "?device=" + encodeURIComponent(this._baseApis.deviceId)
|
||||
+ "&action=verify&key_ed25519%3A"
|
||||
+ encodeURIComponent(this._baseApis.deviceId) + "="
|
||||
+ encodeURIComponent(this._baseApis.getDeviceEd25519Key());
|
||||
this.emit("show_qr_code", {
|
||||
url: url,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ShowQRCode.NAME = "m.qr_code.show.v1";
|
||||
|
||||
/**
|
||||
* @class crypto/verification/QRCode/ScanQRCode
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class ScanQRCode extends Base {
|
||||
static factory(...args) {
|
||||
return new ScanQRCode(...args);
|
||||
}
|
||||
|
||||
async _doVerification() {
|
||||
const code = await new Promise((resolve, reject) => {
|
||||
this.emit("scan", {
|
||||
done: resolve,
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
});
|
||||
});
|
||||
|
||||
const match = code.match(MATRIXTO_REGEXP);
|
||||
let deviceId;
|
||||
const keys = {};
|
||||
if (!match) {
|
||||
throw newQRCodeError();
|
||||
}
|
||||
const userId = match[1];
|
||||
const params = match[2].split("&").map(
|
||||
(x) => x.split("=", 2).map(decodeURIComponent),
|
||||
);
|
||||
let action;
|
||||
for (const [name, value] of params) {
|
||||
if (name === "device") {
|
||||
deviceId = value;
|
||||
} else if (name === "action") {
|
||||
action = value;
|
||||
} else {
|
||||
const keyMatch = name.match(KEY_REGEXP);
|
||||
if (keyMatch) {
|
||||
keys[keyMatch[1]] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!deviceId || action !== "verify" || Object.keys(keys).length === 0) {
|
||||
throw newQRCodeError();
|
||||
}
|
||||
|
||||
if (!this.userId) {
|
||||
await new Promise((resolve, reject) => {
|
||||
this.emit("confirm_user_id", {
|
||||
userId: userId,
|
||||
confirm: resolve,
|
||||
cancel: () => reject(newUserMismatchError()),
|
||||
});
|
||||
});
|
||||
} else if (this.userId !== userId) {
|
||||
throw newUserMismatchError({
|
||||
expected: this.userId,
|
||||
actual: userId,
|
||||
});
|
||||
}
|
||||
|
||||
await this._verifyKeys(userId, keys, (keyId, device, key) => {
|
||||
if (device.keys[keyId] !== key) {
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ScanQRCode.NAME = "m.qr_code.scan.v1";
|
||||
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Short Authentication String (SAS) verification.
|
||||
* @module crypto/verification/SAS
|
||||
*/
|
||||
|
||||
import Base from "./Base";
|
||||
import anotherjson from 'another-json';
|
||||
import {
|
||||
errorFactory,
|
||||
newUserCancelledError,
|
||||
newUnknownMethodError,
|
||||
newKeyMismatchError,
|
||||
newInvalidMessageError,
|
||||
} from './Error';
|
||||
|
||||
const EVENTS = [
|
||||
"m.key.verification.accept",
|
||||
"m.key.verification.key",
|
||||
"m.key.verification.mac",
|
||||
];
|
||||
|
||||
let olmutil;
|
||||
|
||||
const newMismatchedSASError = errorFactory(
|
||||
"m.mismatched_sas", "Mismatched short authentication string",
|
||||
);
|
||||
|
||||
const newMismatchedCommitmentError = errorFactory(
|
||||
"m.mismatched_commitment", "Mismatched commitment",
|
||||
);
|
||||
|
||||
function generateDecimalSas(sasBytes) {
|
||||
/**
|
||||
* +--------+--------+--------+--------+--------+
|
||||
* | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
|
||||
* +--------+--------+--------+--------+--------+
|
||||
* bits: 87654321 87654321 87654321 87654321 87654321
|
||||
* \____________/\_____________/\____________/
|
||||
* 1st number 2nd number 3rd number
|
||||
*/
|
||||
return [
|
||||
(sasBytes[0] << 5 | sasBytes[1] >> 3) + 1000,
|
||||
((sasBytes[1] & 0x7) << 10 | sasBytes[2] << 2 | sasBytes[3] >> 6) + 1000,
|
||||
((sasBytes[3] & 0x3f) << 7 | sasBytes[4] >> 1) + 1000,
|
||||
];
|
||||
}
|
||||
|
||||
const emojiMapping = [
|
||||
["🐶", "dog"], // 0
|
||||
["🐱", "cat"], // 1
|
||||
["🦁", "lion"], // 2
|
||||
["🐎", "horse"], // 3
|
||||
["🦄", "unicorn"], // 4
|
||||
["🐷", "pig"], // 5
|
||||
["🐘", "elephant"], // 6
|
||||
["🐰", "rabbit"], // 7
|
||||
["🐼", "panda"], // 8
|
||||
["🐓", "rooster"], // 9
|
||||
["🐧", "penguin"], // 10
|
||||
["🐢", "turtle"], // 11
|
||||
["🐟", "fish"], // 12
|
||||
["🐙", "octopus"], // 13
|
||||
["🦋", "butterfly"], // 14
|
||||
["🌷", "flower"], // 15
|
||||
["🌳", "tree"], // 16
|
||||
["🌵", "cactus"], // 17
|
||||
["🍄", "mushroom"], // 18
|
||||
["🌏", "globe"], // 19
|
||||
["🌙", "moon"], // 20
|
||||
["☁️", "cloud"], // 21
|
||||
["🔥", "fire"], // 22
|
||||
["🍌", "banana"], // 23
|
||||
["🍎", "apple"], // 24
|
||||
["🍓", "strawberry"], // 25
|
||||
["🌽", "corn"], // 26
|
||||
["🍕", "pizza"], // 27
|
||||
["🎂", "cake"], // 28
|
||||
["❤️", "heart"], // 29
|
||||
["🙂", "smiley"], // 30
|
||||
["🤖", "robot"], // 31
|
||||
["🎩", "hat"], // 32
|
||||
["👓", "glasses"], // 33
|
||||
["🔧", "spanner"], // 34
|
||||
["🎅", "santa"], // 35
|
||||
["👍", "thumbs up"], // 36
|
||||
["☂️", "umbrella"], // 37
|
||||
["⌛", "hourglass"], // 38
|
||||
["⏰", "clock"], // 39
|
||||
["🎁", "gift"], // 40
|
||||
["💡", "light bulb"], // 41
|
||||
["📕", "book"], // 42
|
||||
["✏️", "pencil"], // 43
|
||||
["📎", "paperclip"], // 44
|
||||
["✂️", "scissors"], // 45
|
||||
["🔒", "padlock"], // 46
|
||||
["🔑", "key"], // 47
|
||||
["🔨", "hammer"], // 48
|
||||
["☎️", "telephone"], // 49
|
||||
["🏁", "flag"], // 50
|
||||
["🚂", "train"], // 51
|
||||
["🚲", "bicycle"], // 52
|
||||
["✈️", "aeroplane"], // 53
|
||||
["🚀", "rocket"], // 54
|
||||
["🏆", "trophy"], // 55
|
||||
["⚽", "ball"], // 56
|
||||
["🎸", "guitar"], // 57
|
||||
["🎺", "trumpet"], // 58
|
||||
["🔔", "bell"], // 59
|
||||
["⚓️", "anchor"], // 60
|
||||
["🎧", "headphones"], // 61
|
||||
["📁", "folder"], // 62
|
||||
["📌", "pin"], // 63
|
||||
];
|
||||
|
||||
function generateEmojiSas(sasBytes) {
|
||||
const emojis = [
|
||||
// just like base64 encoding
|
||||
sasBytes[0] >> 2,
|
||||
(sasBytes[0] & 0x3) << 4 | sasBytes[1] >> 4,
|
||||
(sasBytes[1] & 0xf) << 2 | sasBytes[2] >> 6,
|
||||
sasBytes[2] & 0x3f,
|
||||
sasBytes[3] >> 2,
|
||||
(sasBytes[3] & 0x3) << 4 | sasBytes[4] >> 4,
|
||||
(sasBytes[4] & 0xf) << 2 | sasBytes[5] >> 6,
|
||||
];
|
||||
|
||||
return emojis.map((num) => emojiMapping[num]);
|
||||
}
|
||||
|
||||
function generateSas(sasBytes, methods) {
|
||||
const sas = {};
|
||||
if (methods.includes("decimal")) {
|
||||
sas["decimal"] = generateDecimalSas(sasBytes);
|
||||
}
|
||||
if (methods.includes("emoji")) {
|
||||
sas["emoji"] = generateEmojiSas(sasBytes);
|
||||
}
|
||||
return sas;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alias module:crypto/verification/SAS
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export default class SAS extends Base {
|
||||
get events() {
|
||||
return EVENTS;
|
||||
}
|
||||
|
||||
async _doVerification() {
|
||||
await global.Olm.init();
|
||||
olmutil = olmutil || new global.Olm.Utility();
|
||||
|
||||
// make sure user's keys are downloaded
|
||||
await this._baseApis.downloadKeys([this.userId]);
|
||||
|
||||
if (this.startEvent) {
|
||||
return await this._doRespondVerification();
|
||||
} else {
|
||||
return await this._doSendVerification();
|
||||
}
|
||||
}
|
||||
|
||||
async _doSendVerification() {
|
||||
const initialMessage = {
|
||||
method: SAS.NAME,
|
||||
from_device: this._baseApis.deviceId,
|
||||
key_agreement_protocols: ["curve25519"],
|
||||
hashes: ["sha256"],
|
||||
message_authentication_codes: ["hmac-sha256"],
|
||||
// FIXME: allow app to specify what SAS methods can be used
|
||||
short_authentication_string: ["decimal", "emoji"],
|
||||
transaction_id: this.transactionId,
|
||||
};
|
||||
this._sendToDevice("m.key.verification.start", initialMessage);
|
||||
|
||||
|
||||
let e = await this._waitForEvent("m.key.verification.accept");
|
||||
let content = e.getContent();
|
||||
if (!(content.key_agreement_protocol === "curve25519"
|
||||
&& content.hash === "sha256"
|
||||
&& content.message_authentication_code === "hmac-sha256"
|
||||
&& content.short_authentication_string instanceof Array
|
||||
&& (content.short_authentication_string.includes("decimal")
|
||||
|| content.short_authentication_string.includes("emoji")))) {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
if (typeof content.commitment !== "string") {
|
||||
throw newInvalidMessageError();
|
||||
}
|
||||
const hashCommitment = content.commitment;
|
||||
const sasMethods = content.short_authentication_string;
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
try {
|
||||
this._sendToDevice("m.key.verification.key", {
|
||||
key: olmSAS.get_pubkey(),
|
||||
});
|
||||
|
||||
|
||||
e = await this._waitForEvent("m.key.verification.key");
|
||||
// FIXME: make sure event is properly formed
|
||||
content = e.getContent();
|
||||
const commitmentStr = content.key + anotherjson.stringify(initialMessage);
|
||||
if (olmutil.sha256(commitmentStr) !== hashCommitment) {
|
||||
throw newMismatchedCommitmentError();
|
||||
}
|
||||
olmSAS.set_their_key(content.key);
|
||||
|
||||
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
|
||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
||||
+ this.userId + this.deviceId
|
||||
+ this.transactionId;
|
||||
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
|
||||
const verifySAS = new Promise((resolve, reject) => {
|
||||
this.emit("show_sas", {
|
||||
sas: generateSas(sasBytes, sasMethods),
|
||||
confirm: () => {
|
||||
this._sendMAC(olmSAS);
|
||||
resolve();
|
||||
},
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
mismatch: () => reject(newMismatchedSASError()),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
[e] = await Promise.all([
|
||||
this._waitForEvent("m.key.verification.mac"),
|
||||
verifySAS,
|
||||
]);
|
||||
content = e.getContent();
|
||||
await this._checkMAC(olmSAS, content);
|
||||
} finally {
|
||||
olmSAS.free();
|
||||
}
|
||||
}
|
||||
|
||||
async _doRespondVerification() {
|
||||
let content = this.startEvent.getContent();
|
||||
if (!(content.key_agreement_protocols instanceof Array
|
||||
&& content.key_agreement_protocols.includes("curve25519")
|
||||
&& content.hashes instanceof Array
|
||||
&& content.hashes.includes("sha256")
|
||||
&& content.message_authentication_codes instanceof Array
|
||||
&& content.message_authentication_codes.includes("hmac-sha256")
|
||||
&& content.short_authentication_string instanceof Array
|
||||
&& (content.short_authentication_string.includes("decimal")
|
||||
|| content.short_authentication_string.includes("emoji")))) {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
const sasMethods = [];
|
||||
// FIXME: allow app to specify what SAS methods can be used
|
||||
if (content.short_authentication_string.includes("decimal")) {
|
||||
sasMethods.push("decimal");
|
||||
}
|
||||
if (content.short_authentication_string.includes("emoji")) {
|
||||
sasMethods.push("emoji");
|
||||
}
|
||||
try {
|
||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
|
||||
this._sendToDevice("m.key.verification.accept", {
|
||||
key_agreement_protocol: "curve25519",
|
||||
hash: "sha256",
|
||||
message_authentication_code: "hmac-sha256",
|
||||
short_authentication_string: sasMethods,
|
||||
commitment: olmutil.sha256(commitmentStr),
|
||||
});
|
||||
|
||||
|
||||
let e = await this._waitForEvent("m.key.verification.key");
|
||||
// FIXME: make sure event is properly formed
|
||||
content = e.getContent();
|
||||
olmSAS.set_their_key(content.key);
|
||||
this._sendToDevice("m.key.verification.key", {
|
||||
key: olmSAS.get_pubkey(),
|
||||
});
|
||||
|
||||
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
|
||||
+ this.userId + this.deviceId
|
||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
||||
+ this.transactionId;
|
||||
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
|
||||
const verifySAS = new Promise((resolve, reject) => {
|
||||
this.emit("show_sas", {
|
||||
sas: generateSas(sasBytes, sasMethods),
|
||||
confirm: () => {
|
||||
this._sendMAC(olmSAS);
|
||||
resolve();
|
||||
},
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
mismatch: () => reject(newMismatchedSASError()),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
[e] = await Promise.all([
|
||||
this._waitForEvent("m.key.verification.mac"),
|
||||
verifySAS,
|
||||
]);
|
||||
content = e.getContent();
|
||||
await this._checkMAC(olmSAS, content);
|
||||
} finally {
|
||||
olmSAS.free();
|
||||
}
|
||||
}
|
||||
|
||||
_sendMAC(olmSAS) {
|
||||
const keyId = `ed25519:${this._baseApis.deviceId}`;
|
||||
const mac = {};
|
||||
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
|
||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
||||
+ this.userId + this.deviceId
|
||||
+ this.transactionId;
|
||||
|
||||
mac[keyId] = olmSAS.calculate_mac(
|
||||
this._baseApis.getDeviceEd25519Key(),
|
||||
baseInfo + keyId,
|
||||
);
|
||||
const keys = olmSAS.calculate_mac(
|
||||
keyId,
|
||||
baseInfo + "KEY_IDS",
|
||||
);
|
||||
this._sendToDevice("m.key.verification.mac", { mac, keys });
|
||||
}
|
||||
|
||||
async _checkMAC(olmSAS, content) {
|
||||
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
|
||||
+ this.userId + this.deviceId
|
||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
||||
+ this.transactionId;
|
||||
|
||||
if (content.keys !== olmSAS.calculate_mac(
|
||||
Object.keys(content.mac).sort().join(","),
|
||||
baseInfo + "KEY_IDS",
|
||||
)) {
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
|
||||
await this._verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
|
||||
if (keyInfo !== olmSAS.calculate_mac(
|
||||
device.keys[keyId],
|
||||
baseInfo + keyId,
|
||||
)) {
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SAS.NAME = "m.sas.v1";
|
||||
+92
-5
@@ -31,7 +31,14 @@ const EventTimelineSet = require("./event-timeline-set");
|
||||
|
||||
import ReEmitter from '../ReEmitter';
|
||||
|
||||
const LATEST_ROOM_VERSION = '1';
|
||||
// These constants are used as sane defaults when the homeserver doesn't support
|
||||
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
||||
// the same as the common default room version whereas SAFE_ROOM_VERSIONS are the
|
||||
// room versions which are considered okay for people to run without being asked
|
||||
// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
|
||||
// return an m.room_versions capability.
|
||||
const KNOWN_SAFE_ROOM_VERSION = '1';
|
||||
const SAFE_ROOM_VERSIONS = ['1', '2'];
|
||||
|
||||
function synthesizeReceipt(userId, event, receiptType) {
|
||||
// console.log("synthesizing receipt for "+event.getId());
|
||||
@@ -211,12 +218,74 @@ Room.prototype.getVersion = function() {
|
||||
* Determines whether this room needs to be upgraded to a new version
|
||||
* @returns {string?} What version the room should be upgraded to, or null if
|
||||
* the room does not require upgrading at this time.
|
||||
* @deprecated Use #getRecommendedVersion() instead
|
||||
*/
|
||||
Room.prototype.shouldUpgradeToVersion = function() {
|
||||
// This almost certainly won't be the way this actually works - this
|
||||
// is essentially a stub method.
|
||||
if (this.getVersion() === LATEST_ROOM_VERSION) return null;
|
||||
return LATEST_ROOM_VERSION;
|
||||
// TODO: Remove this function.
|
||||
// This makes assumptions about which versions are safe, and can easily
|
||||
// be wrong. Instead, people are encouraged to use getRecommendedVersion
|
||||
// which determines a safer value. This function doesn't use that function
|
||||
// because this is not async-capable, and to avoid breaking the contract
|
||||
// we're deprecating this.
|
||||
|
||||
if (!SAFE_ROOM_VERSIONS.includes(this.getVersion())) {
|
||||
return KNOWN_SAFE_ROOM_VERSION;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines the recommended room version for the room. This returns an
|
||||
* object with 3 properties: <code>version</code> as the new version the
|
||||
* room should be upgraded to (may be the same as the current version);
|
||||
* <code>needsUpgrade</code> to indicate if the room actually can be
|
||||
* upgraded (ie: does the current version not match?); and <code>urgent</code>
|
||||
* to indicate if the new version patches a vulnerability in a previous
|
||||
* version.
|
||||
* @returns {Promise<{version: string, needsUpgrade: bool, urgent: bool}>}
|
||||
* Resolves to the version the room should be upgraded to.
|
||||
*/
|
||||
Room.prototype.getRecommendedVersion = async function() {
|
||||
const capabilities = await this._client.getCapabilities();
|
||||
let versionCap = capabilities["m.room_versions"];
|
||||
if (!versionCap) {
|
||||
versionCap = {
|
||||
default: KNOWN_SAFE_ROOM_VERSION,
|
||||
available: {},
|
||||
};
|
||||
for (const safeVer of SAFE_ROOM_VERSIONS) {
|
||||
versionCap.available[safeVer] = "stable";
|
||||
}
|
||||
}
|
||||
|
||||
const currentVersion = this.getVersion();
|
||||
|
||||
const result = {
|
||||
version: currentVersion,
|
||||
needsUpgrade: false,
|
||||
urgent: false,
|
||||
};
|
||||
|
||||
// If the room is on the default version then nothing needs to change
|
||||
if (currentVersion === versionCap.default) return Promise.resolve(result);
|
||||
|
||||
const stableVersions = Object.keys(versionCap.available)
|
||||
.filter((v) => versionCap.available[v] === 'stable');
|
||||
|
||||
// Check if the room is on an unstable version. We determine urgency based
|
||||
// off the version being in the Matrix spec namespace or not (if the version
|
||||
// is in the current namespace and unstable, the room is probably vulnerable).
|
||||
if (!stableVersions.includes(currentVersion)) {
|
||||
result.version = versionCap.default;
|
||||
result.needsUpgrade = true;
|
||||
result.urgent = !!this.getVersion().match(/^[0-9]+[0-9.]*$/g);
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
// The room is on a stable, but non-default, version by this point.
|
||||
// No upgrade needed.
|
||||
return Promise.resolve(result);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -520,6 +589,24 @@ Room.prototype._fixUpLegacyTimelineFields = function() {
|
||||
.getState(EventTimeline.FORWARDS);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether there are any devices in the room that are unverified
|
||||
* @return {bool} the result
|
||||
*/
|
||||
Room.prototype.hasUnverifiedDevices = async function() {
|
||||
if (!this._client.isRoomEncrypted(this.roomId)) {
|
||||
return false;
|
||||
}
|
||||
const e2eMembers = await this.getEncryptionTargetMembers();
|
||||
for (const member of e2eMembers) {
|
||||
const devices = await this._client.getStoredDevicesForUser(member.userId);
|
||||
if (devices.some((device) => device.isUnverified())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the timeline sets for this room.
|
||||
* @return {EventTimelineSet[]} array of timeline sets for this room
|
||||
|
||||
@@ -186,11 +186,13 @@ User.prototype.getLastActiveTs = function() {
|
||||
/**
|
||||
* Manually set the user's status message.
|
||||
* @param {MatrixEvent} event The <code>im.vector.user_status</code> event.
|
||||
* @fires module:client~MatrixClient#event:"User._unstable_statusMessage"
|
||||
*/
|
||||
User.prototype._unstable_updateStatusMessage = function(event) {
|
||||
if (!event.getContent()) this._unstable_statusMessage = "";
|
||||
else this._unstable_statusMessage = event.getContent()["status"];
|
||||
this._updateModifiedTime();
|
||||
this.emit("User._unstable_statusMessage", this);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -190,11 +190,22 @@ WebStorageSessionStore.prototype = {
|
||||
removeAllEndToEndRooms: function() {
|
||||
removeByPrefix(this.store, keyEndToEndRoom(''));
|
||||
},
|
||||
|
||||
setLocalTrustedBackupPubKey: function(pubkey) {
|
||||
this.store.setItem(KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY, pubkey);
|
||||
},
|
||||
|
||||
// XXX: This store is deprecated really, but added this as a temporary
|
||||
// thing until cross-signing lands.
|
||||
getLocalTrustedBackupPubKey: function() {
|
||||
return this.store.getItem(KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY);
|
||||
},
|
||||
};
|
||||
|
||||
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
|
||||
const KEY_END_TO_END_DEVICE_SYNC_TOKEN = E2E_PREFIX + "device_sync_token";
|
||||
const KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS = E2E_PREFIX + "device_tracking";
|
||||
const KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY = E2E_PREFIX + "trusted_backup_pubkey";
|
||||
|
||||
function keyEndToEndDevicesForUser(userId) {
|
||||
return E2E_PREFIX + "devices/" + userId;
|
||||
|
||||
Reference in New Issue
Block a user