Compare commits

...

61 Commits

Author SHA1 Message Date
Matthew Hodgson 0f30d21fa2 v0.7.9 2017-06-01 01:41:06 +01:00
Matthew Hodgson 4257c8c9f5 Prepare changelog for v0.7.9 2017-06-01 01:41:06 +01:00
Richard van der Hoff 331859d383 Merge pull request #445 from matrix-org/rav/indexeddb_crypto_store
Initial framework for indexeddb-backed crypto store
2017-05-31 18:06:45 +01:00
Richard van der Hoff ef03b708a8 Add MatrixClient.clearStores
- to clear both sets of storage on logout
2017-05-31 17:22:07 +01:00
Richard van der Hoff 716d098361 Address Kegan's review comments
jsdoc mostly.
2017-05-31 16:05:00 +01:00
Richard van der Hoff d887057660 Merge pull request #444 from matrix-org/rav/factor_out_reemit
Factor out reEmit to a common module
2017-05-31 14:22:34 +01:00
Richard van der Hoff 7efbfebb4d Factor out reEmit to a common module
and rewrite it to use modern JS while we're at it
2017-05-31 11:01:48 +01:00
Richard van der Hoff 4c7afe5af0 Initial framework for indexeddb-backed crypto store
Doesn't do anything useful yet - just demonstrates a framework for how I hope
it will fit into the sdk.
2017-05-30 23:25:07 +01:00
Richard van der Hoff 676515cf27 Merge pull request #443 from matrix-org/rav/es6ify_algorithm_base
crypto/algorithms/base.js: Convert to es6
2017-05-23 16:36:42 +01:00
Richard van der Hoff 0eb5b0fdfa Merge pull request #435 from t3chguy/maySendRedactionForEvent
maySendRedactionForEvent for userId
2017-05-23 15:45:46 +01:00
Richard van der Hoff 2feba4787f Merge pull request #441 from matrix-org/rav/get_userid
MatrixClient: add getUserId()
2017-05-23 15:44:34 +01:00
Michael Telatynski 516dc1043e prevent powerLevels being undef
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-23 14:53:11 +01:00
Richard van der Hoff b26c1c57dc crypto/algorithms/base.js: Convert to es6
Convert base to an es6 module with es6 classes, for clarity and to help with
jsdoccing.

Complications are:

* jsdoc gets confused by `export class`, so the exports are separated.

* turns out that extending Error is a bit difficult, so instanceof doesn't work
  on derived Error classes. This only really affects us in one place (app-side
  code shouldn't be doing instanceofs anyway), so just use `name` instead.
2017-05-23 14:32:13 +01:00
Richard van der Hoff 0945ba9e90 Merge pull request #442 from matrix-org/rav/custom_babel_for_jsdoc
Run jsdoc on a custom babeling of the source
2017-05-23 14:28:41 +01:00
Michael Telatynski 69ed6f283d fix based on rich's feedback
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-23 12:55:05 +01:00
Richard van der Hoff 9eef850d0c Run jsdoc on a custom babeling of the source
jsdoc can't read our raw source, because of our dangling commas in function
calls. On the other hand, running on /lib means that a lot of the useful
information about exports is lost and you end up having to jump through hoops
to get jsdoc to generate the right thing.

This uses a separate run of babel (with all the presets turned off) to generate
source which is almost identical to the input, but lacks trailing commas.

(https://babeljs.io/blog/2015/10/31/setting-up-babel-6 says 'Babel 6 ships
without any default transforms, so when you run Babel on a file it will just
print it back out to you without changing anything.' - however, that is,
empirically, not entirely true.)
2017-05-23 12:26:17 +01:00
Richard van der Hoff cf1574d690 MatrixClient: add getUserId()
... I'm amazed we got this far without it.
2017-05-23 10:37:26 +01:00
David Baker d6913e41a0 Merge branch 'master' into develop 2017-05-22 11:33:20 +01:00
David Baker 3c81c295c7 v0.7.8 2017-05-22 11:31:48 +01:00
David Baker 56dfa0c755 Prepare changelog for v0.7.8 2017-05-22 11:31:47 +01:00
Richard van der Hoff 43989be768 Merge pull request #439 from kscz/add_getstoreddeviceforuser
Add in a public api getStoredDevice allowing clients to get a specific device
2017-05-22 09:36:38 +01:00
Kit Sczudlo 822380ac38 Add in a public api getStoredDevice allowing clients to get a specific device
Signed-off-by: Kit Sczudlo <kit@kitscz.com>
2017-05-21 00:30:40 -07:00
David Baker 8c37d9ac9a v0.7.8-rc.1 2017-05-19 10:34:31 +01:00
David Baker e40b8461f7 Prepare changelog for v0.7.8-rc.1 2017-05-19 10:34:30 +01:00
David Baker a3f45b466a Merge pull request #438 from matrix-org/rav/release_signing
Attempt to rework the release-tarball-signing stuff
2017-05-19 10:03:36 +01:00
Richard van der Hoff 672ad68c64 release.sh: download the tarball from git to verify it 2017-05-18 18:58:50 +01:00
David Baker 4ccec13739 Fix build: move uglifyjs dep to uglify-js
uglifyjs have gained a hyphen for some reason, and replaced th
old one with a stub package.
2017-05-17 11:21:20 +01:00
Michael Telatynski 09529a1aa8 lets please the ESLint gods
`--max-warnings 115` :')

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-16 14:16:42 +01:00
Michael Telatynski d182fd6bb7 can't redact queued/not_sent
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-16 14:12:29 +01:00
Michael Telatynski 36bf123e2b maySendRedactionForEvent for userId
done using a private helper so kick/ban etc perms can be done
easily at a later stage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-16 14:04:55 +01:00
Matthew Hodgson 92cfbf655f Merge pull request #427 from t3chguy/electron_media_select
ability to specify webrtc audio/video inputs for the lib to request
2017-05-15 02:10:01 +01:00
Matthew Hodgson fbef701179 Merge pull request #434 from t3chguy/t3chguy/screen_share_firefox
make screen sharing call FF friendly :D
2017-05-15 00:16:33 +01:00
Michael Telatynski 0415b9cf4c make screen sharing call FF friendly :D
FF is uber nice that it lets us select the display
does not seem to allow the composite ALL displays though

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-14 23:48:26 +01:00
Luke Barnard cb9a9e8d50 Implement API for username availability (#432)
Requires synapse with https://github.com/matrix-org/synapse/pull/2183, https://github.com/matrix-org/synapse/pull/2209 and https://github.com/matrix-org/synapse/pull/2213
2017-05-11 09:14:45 +01:00
Richard van der Hoff 6021c1c6b1 Merge pull request #431 from matrix-org/rav/fix_device_list_yet_again
Fix race in device list updates
2017-05-05 13:29:53 +01:00
Richard van der Hoff 655be2fa2e Fix race in device list updates
Don't consider device lists up-to-date when we have another request for the
relevant user in the queue.

Fixes https://github.com/vector-im/riot-web/issues/3796.
2017-05-05 12:34:00 +01:00
Michael Telatynski 98491a63a7 ability to specify webrtc audio/video inputs for the lib to request 2017-04-27 16:06:34 +01:00
David Baker acd7f15c83 Merge pull request #424 from matrix-org/rob/nocam
WebRTC: Support recvonly for video for those without a webcam
2017-04-26 18:23:52 +01:00
Richard van der Hoff 5020d4e99f Rework device list tracking logic (#425)
Yet another attempt at fixing
https://github.com/vector-im/riot-web/issues/2305.

This now implements the algorithm described at
http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#tracking-the-device-list-for-a-user:

* We now keep a flag to tell us which users' device lists we are tracking. That
  makes it much easier to figure out whether we should care about device-update
  notifications from /sync (thereby fixing
  https://github.com/vector-im/riot-web/issues/3588).

* We use the same flag to indicate whether the device list for a particular
  user is out of date. Previously we did this implicitly by only updating the
  stored sync token when the list had been updated, but that was somewhat
  complicated, and in any case didn't help in cases where we initiated the key
  download due to a user joining an encrypted room.

Also fixes https://github.com/vector-im/riot-web/issues/3310.
2017-04-25 17:56:01 +01:00
David Baker 9693c30209 Merge branch 'master' into develop 2017-04-25 10:51:08 +01:00
David Baker 2b6f8adc64 v0.7.7 2017-04-25 10:49:28 +01:00
David Baker 822f5927e5 Prepare changelog for v0.7.7 2017-04-25 10:49:26 +01:00
David Baker 0f6e9d7b9d v0.7.7-rc.1 2017-04-21 18:15:49 +01:00
David Baker 99f3e3f09e Prepare changelog for v0.7.7-rc.1 2017-04-21 18:15:48 +01:00
David Baker aa81c96a98 Automatically complete dummy auth
Dummy auth flows, bu definition, do not require a response from
the user, and so should just be completed automatically by
interactive-auth.
2017-04-21 18:06:57 +01:00
Richard van der Hoff 9d532b6c72 Merge pull request #422 from t3chguy/develop
Update istanbul to remove minimatch DoS Warning
2017-04-21 12:12:32 +01:00
Luke Barnard 4c63906b8f Implement API for setting RM (#419)
* Implement API for setting RM

This is now stored on the server with similar treatment to RRs. The server will only store the specified eventId as the current read marker for a room if the event is ahead in the stream when compared to the existing RM. The exception is when the RM has never been set for this room for this user, in which case the event ID will be stored as the RM without any comparison.

This API also allows for an optional RR event ID to be sent in the same request. This is because it might be the common case for some clients to update the RM at the same time as updating the RR.

See design: https://docs.google.com/document/d/1UWqdS-e1sdwkLDUY0wA4gZyIkRp-ekjsLZ8k6g_Zvso/edit

See server-side PRs: https://github.com/matrix-org/synapse/pull/2120, https://github.com/matrix-org/synapse/pull/2128
2017-04-20 09:43:33 +01:00
Robert Swain dd2a870227 webrtc/call: Unmute remote audio element when setting 2017-04-20 06:41:29 +02:00
Robert Swain 88948c3cfd webrtc/call: Always offer to receive audio/video for video call
This allows people without (or denying access to) a webcam to make a
video call and receive audio and video from the peer.
2017-04-20 06:35:03 +02:00
Robert Swain b33dcfe6ff webrtc/call: Fall back to recvonly if camera/mic access is denied
Users of MatrixCall will need to present some sensible UX for this.
2017-04-20 06:32:52 +02:00
Robert Swain 2c15bdae04 Merge pull request #423 from matrix-org/rob/more-distinct-callid
webrtc/call: Make it much less likely that callIds collide locally
2017-04-19 17:34:15 +02:00
Robert Swain 2f45633312 webrtc/call: Make it much less likely that callIds collide locally
Previously if two calls were constructed within 1ms they could have the
same id.
2017-04-19 16:51:23 +02:00
Michael Telatynski fdd42fbc6d Update dependencies to remove minimatch DoS Warning
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-04-19 12:50:21 +01:00
Richard van der Hoff 54a6f5d425 Merge pull request #420 from matrix-org/dbkr/fix_dummy_auth
Automatically complete dummy auth
2017-04-13 14:23:33 +01:00
David Baker 68d9662fe5 Automatically complete dummy auth
Dummy auth flows, bu definition, do not require a response from
the user, and so should just be completed automatically by
interactive-auth.
2017-04-12 18:36:23 +01:00
David Baker 4f0987da01 Merge branch 'master' into develop 2017-04-12 09:58:26 +01:00
David Baker 625697e097 v0.7.6 2017-04-12 09:56:46 +01:00
David Baker 92b14f20d2 Prepare changelog for v0.7.6 2017-04-12 09:56:45 +01:00
Richard van der Hoff dd069647d1 Merge pull request #418 from matrix-org/dbkr/release_script_dont_leave_me_in_gh_pages
Don't leave the gh-pages branch checked out
2017-04-10 16:36:25 +01:00
David Baker 4523ae7d29 Checkout release branch *before* exiting script 2017-04-10 16:15:06 +01:00
David Baker 19e5eda773 Don't leave the gh-pages branch checked out
After a pre-release, check out the release branch again rather
than leaving the working copy on the gh-pages branch
2017-04-10 15:52:37 +01:00
25 changed files with 1297 additions and 529 deletions
+3 -1
View File
@@ -1,4 +1,6 @@
.jsdoc
/.jsdocbuild
/.jsdoc
node_modules
.lock-wscript
build/Release
+71
View File
@@ -1,3 +1,74 @@
Changes in [0.7.9](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.9) (2017-06-01)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.8...v0.7.9)
* Initial framework for indexeddb-backed crypto store
[\#445](https://github.com/matrix-org/matrix-js-sdk/pull/445)
* Factor out reEmit to a common module
[\#444](https://github.com/matrix-org/matrix-js-sdk/pull/444)
* crypto/algorithms/base.js: Convert to es6
[\#443](https://github.com/matrix-org/matrix-js-sdk/pull/443)
* maySendRedactionForEvent for userId
[\#435](https://github.com/matrix-org/matrix-js-sdk/pull/435)
* MatrixClient: add getUserId()
[\#441](https://github.com/matrix-org/matrix-js-sdk/pull/441)
* Run jsdoc on a custom babeling of the source
[\#442](https://github.com/matrix-org/matrix-js-sdk/pull/442)
* Add in a public api getStoredDevice allowing clients to get a specific
device
[\#439](https://github.com/matrix-org/matrix-js-sdk/pull/439)
Changes in [0.7.8](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.8) (2017-05-22)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.8-rc.1...v0.7.8)
* No changes
Changes in [0.7.8-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.8-rc.1) (2017-05-19)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.7...v0.7.8-rc.1)
* Attempt to rework the release-tarball-signing stuff
[\#438](https://github.com/matrix-org/matrix-js-sdk/pull/438)
* ability to specify webrtc audio/video inputs for the lib to request
[\#427](https://github.com/matrix-org/matrix-js-sdk/pull/427)
* make screen sharing call FF friendly :D
[\#434](https://github.com/matrix-org/matrix-js-sdk/pull/434)
* Fix race in device list updates
[\#431](https://github.com/matrix-org/matrix-js-sdk/pull/431)
* WebRTC: Support recvonly for video for those without a webcam
[\#424](https://github.com/matrix-org/matrix-js-sdk/pull/424)
* Update istanbul to remove minimatch DoS Warning
[\#422](https://github.com/matrix-org/matrix-js-sdk/pull/422)
* webrtc/call: Make it much less likely that callIds collide locally
[\#423](https://github.com/matrix-org/matrix-js-sdk/pull/423)
* Automatically complete dummy auth
[\#420](https://github.com/matrix-org/matrix-js-sdk/pull/420)
* Don't leave the gh-pages branch checked out
[\#418](https://github.com/matrix-org/matrix-js-sdk/pull/418)
Changes in [0.7.7](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.7) (2017-04-25)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.7-rc.1...v0.7.7)
* No changes
Changes in [0.7.7-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.7-rc.1) (2017-04-21)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.6...v0.7.7-rc.1)
* Automatically complete dummy auth
[\#420](https://github.com/matrix-org/matrix-js-sdk/pull/420)
Changes in [0.7.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.6) (2017-04-12)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.6-rc.2...v0.7.6)
* No changes
Changes in [0.7.6-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.6-rc.2) (2017-04-10)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.6-rc.1...v0.7.6-rc.2)
+9
View File
@@ -1,4 +1,13 @@
var matrixcs = require("./lib/matrix");
matrixcs.request(require("browser-request"));
matrixcs.setCryptoStoreFactory(
function() {
return new matrixcs.IndexedDBCryptoStore(
global.indexedDB, "matrix-js-sdk:crypto"
);
}
);
module.exports = matrixcs; // keep export for browserify package deps
global.matrixcs = matrixcs;
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "0.7.6-rc.2",
"version": "0.7.9",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
@@ -8,12 +8,12 @@
"test:run": "istanbul cover --report text --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" _mocha -- --recursive specbuild --colors --reporter mocha-jenkins-reporter --reporter-options junit_report_path=reports/test-results.xml",
"test": "npm run test:build && npm run test:run",
"check": "npm run test:build && _mocha --recursive specbuild --colors",
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
"gendoc": "babel --no-babelrc -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
"start": "babel -s -w -d lib src",
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && uglifyjs -c -m -o dist/browser-matrix.min.js --source-map dist/browser-matrix.min.js.map --in-source-map dist/browser-matrix.js.map dist/browser-matrix.js",
"dist": "npm run build",
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
"lint": "eslint --max-warnings 115 src spec",
"lint": "eslint --max-warnings 113 src spec",
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt"
},
"repository": {
@@ -62,7 +62,7 @@
"eslint-config-google": "^0.7.1",
"exorcist": "^0.4.0",
"expect": "^1.20.2",
"istanbul": "^0.3.13",
"istanbul": "^0.4.5",
"jsdoc": "^3.4.0",
"lolex": "^1.5.2",
"mocha": "^3.2.0",
@@ -70,7 +70,7 @@
"rimraf": "^2.5.4",
"source-map-support": "^0.4.11",
"sourceify": "^0.1.0",
"uglifyjs": "^2.4.10",
"uglify-js": "^2.8.26",
"watchify": "^3.2.1"
},
"optionalDependencies": {
+48 -13
View File
@@ -178,27 +178,61 @@ if [ $dodist -eq 0 ]; then
done
fi
# push the release branch (github can't release from
# a branch it doesn't have)
git push origin "$rel_branch"
if [ -n "$signing_id" ]; then
# make a signed tag
# gnupg seems to fail to get the right tty device unless we set it here
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
# also make a signature for the source tarball.
project_name=`jq -r '.name' package.json`
source_sigfile="${tag}-src.tar.gz.asc"
git archive --format tgz --prefix="${project_name}-${release}/" "$tag" |
gpg -u "$signing_id" --armor --output "$source_sigfile" --detach-sig -
assets="$assets -a $source_sigfile"
else
git tag -a -F "${latest_changes}" "$tag"
fi
# push the tag
git push origin "$tag"
# push the tag and the release branch
git push origin "$rel_branch" "$tag"
if [ -n "$signing_id" ]; then
# make a signature for the source tarball.
#
# github will make us a tarball from the tag - we want to create a
# signature for it, which means that first of all we need to check that
# it's correct.
#
# we can't deterministically build exactly the same tarball, due to
# differences in gzip implementation - but we *can* build the same tar - so
# the easiest way to check the validity of the tarball from git is to unzip
# it and compare it with our own idea of what the tar should look like.
# the name of the sig file we want to create
source_sigfile="${tag}-src.tar.gz.asc"
tarfile="$tag.tar.gz"
gh_project_url=$(git remote get-url origin |
sed -e 's#^git@github.com:#https://github.com/#' -e 's/\.git$//')
project_name="${gh_project_url##*/}"
curl -L "${gh_project_url}/archive/${tarfile}" -o "${tarfile}"
# unzip it and compare it with the tar we would generate
if ! cmp --silent <(gunzip -c $tarfile) \
<(git archive --format tar --prefix="${project_name}-${release}/" "$tag"); then
# we don't bail out here, because really it's more likely that our comparison
# screwed up and it's super annoying to abort the script at this point.
cat >&2 <<EOF
!!!!!!!!!!!!!!!!!
!!!! WARNING !!!!
Mismatch between our own tarfile and that generated by github: not signing
source tarball.
To resolve, determine if $tarfile is correct, and if so sign it with gpg and
attach it to the release as $source_sigfile.
!!!!!!!!!!!!!!!!!
EOF
else
gpg -u "$signing_id" --armor --output "$source_sigfile" --detach-sig "$tarfile"
assets="$assets -a $source_sigfile"
fi
fi
hubflags=''
if [ $prerelease -eq 1 ]; then
@@ -237,6 +271,7 @@ fi
# if it is a pre-release, leave it on the release branch for now.
if [ $prerelease -eq 1 ]; then
git checkout "$rel_branch"
exit 0
fi
+1
View File
@@ -60,6 +60,7 @@ TestClient.prototype.toString = function() {
* @return {Promise}
*/
TestClient.prototype.start = function() {
console.log(this + ': starting');
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.expectDeviceKeyUpload();
+21 -23
View File
@@ -403,29 +403,6 @@ describe("MatrixClient crypto", function() {
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
});
it('Ali knows the difference between a new user and one with no devices',
function(done) {
aliTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:id': {},
},
});
const p1 = aliTestClient.client.downloadKeys(['@bob:id']);
const p2 = aliTestClient.httpBackend.flush('/keys/query', 1);
q.all([p1, p2]).then(function() {
const devices = aliTestClient.storage.getEndToEndDevicesForUser(
'@bob:id',
);
expect(utils.keys(devices).length).toEqual(0);
// request again: should be no more requests
return aliTestClient.client.downloadKeys(['@bob:id']);
}).nodeify(done);
},
);
it("Bob uploads device keys", function() {
return q()
.then(bobUploadsDeviceKeys);
@@ -673,6 +650,27 @@ describe("MatrixClient crypto", function() {
return q()
.then(() => aliTestClient.start())
.then(() => firstSync(aliTestClient))
// ali will only care about bob's new_device if she is tracking
// bob's devices, which she will do if we enable encryption
.then(aliEnablesEncryption)
.then(() => {
aliTestClient.expectKeyQuery({
device_keys: {
[aliUserId]: {},
[bobUserId]: {},
},
});
return aliTestClient.httpBackend.flush('/keys/query', 1);
})
// make sure that the initial key download has completed
// (downloadKeys will wait until it does)
.then(() => {
return aliTestClient.client.downloadKeys([bobUserId]);
})
.then(function() {
const syncData = {
next_batch: '2',
+29 -48
View File
@@ -1001,7 +1001,19 @@ describe("megolm", function() {
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(0);
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(1);
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
if (bobStat != 1 && bobStat != 2) {
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
bobStat);
}
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
bobStat);
}
// now add an expectation for a query for bob's devices, and let
// it complete.
@@ -1020,7 +1032,15 @@ describe("megolm", function() {
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
}).then(() => {
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(2);
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toEqual(3);
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
bobStat);
}
// now let the query for chris's devices complete.
return aliceTestClient.httpBackend.flush('/keys/query', 1);
@@ -1030,56 +1050,17 @@ describe("megolm", function() {
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@chris:abc']);
}).then(() => {
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
expect(bobStat).toEqual(3);
expect(chrisStat).toEqual(3);
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(3);
});
});
it("Device list downloads before /changes shouldn't affect sync token",
() => {
// https://github.com/vector-im/riot-web/issues/3126#issuecomment-279374939
aliceTestClient.storage.storeEndToEndDeviceSyncToken(0);
aliceTestClient.storage.storeEndToEndRoom(ROOM_ID, {
algorithm: 'm.megolm.v1.aes-sha2',
});
return aliceTestClient.start().then(() => {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse([aliceTestClient.userId, '@bob:xyz']));
return aliceTestClient.httpBackend.flush('/sync', 1);
}).then(() => {
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {device_keys: {'@bob:xyz': {}}},
);
return q.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(() => {
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(0);
aliceTestClient.httpBackend.when(
'GET', '/keys/changes',
).check((req) => {
expect(req.queryParams.from).toEqual(0);
expect(req.queryParams.to).toEqual(1);
}).respond(200, {changed: ['@bob:xyz']});
return aliceTestClient.httpBackend.flush('/keys/changes');
}).then((flushed) => {
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {device_keys: {'@bob:xyz': {}}},
);
return aliceTestClient.httpBackend.flush('/keys/query');
}).then((flushed) => {
expect(flushed).toEqual(1);
// let the client finish processing the keys
return q.delay(10);
}).then(() => {
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(1);
});
});
it("Alice exports megolm keys and imports them to a new device", function(done) {
let messageEncrypted;
+128
View File
@@ -0,0 +1,128 @@
import DeviceList from '../../../lib/crypto/DeviceList';
import MockStorageApi from '../../MockStorageApi';
import WebStorageSessionStore from '../../../lib/store/session/webstorage';
import testUtils from '../../test-utils';
import utils from '../../../lib/utils';
import expect from 'expect';
import q from 'q';
const signedDeviceList = {
"failures": {},
"device_keys": {
"@test1:sw1v.org": {
"HGKAWHRVJQ": {
"signatures": {
"@test1:sw1v.org": {
"ed25519:HGKAWHRVJQ":
"8PB450fxKDn5s8IiRZ2N2t6MiueQYVRLHFEzqIi1eLdxx1w" +
"XEPC1/1Uz9T4gwnKlMVAKkhB5hXQA/3kjaeLABw",
},
},
"user_id": "@test1:sw1v.org",
"keys": {
"ed25519:HGKAWHRVJQ":
"0gI/T6C+mn1pjtvnnW2yB2l1IIBb/5ULlBXi/LXFSEQ",
"curve25519:HGKAWHRVJQ":
"mbIZED1dBsgIgkgzxDpxKkJmsr4hiWlGzQTvUnQe3RY",
},
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2",
],
"device_id": "HGKAWHRVJQ",
"unsigned": {},
},
},
},
};
describe('DeviceList', function() {
let downloadSpy;
let sessionStore;
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
downloadSpy = expect.createSpy();
const mockStorage = new MockStorageApi();
sessionStore = new WebStorageSessionStore(mockStorage);
});
function createTestDeviceList() {
const baseApis = {
downloadKeysForUsers: downloadSpy,
};
const mockOlm = {
verifySignature: function(key, message, signature) {},
};
return new DeviceList(baseApis, sessionStore, mockOlm);
}
it("should successfully download and store device keys", function() {
const dl = createTestDeviceList();
dl.startTrackingDeviceList('@test1:sw1v.org');
const queryDefer1 = q.defer();
downloadSpy.andReturn(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
return prom1.then(() => {
const storedKeys = sessionStore.getEndToEndDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
});
});
it("should have an outdated devicelist on an invalidation while an " +
"update is in progress", function() {
const dl = createTestDeviceList();
dl.startTrackingDeviceList('@test1:sw1v.org');
const queryDefer1 = q.defer();
downloadSpy.andReturn(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
downloadSpy.reset();
// outdated notif arrives while the request is in flight.
const queryDefer2 = q.defer();
downloadSpy.andReturn(queryDefer2.promise);
dl.invalidateUserDeviceList('@test1:sw1v.org');
dl.refreshOutdatedDeviceLists();
// the first request completes
queryDefer1.resolve({
device_keys: {
'@test1:sw1v.org': {},
},
});
return prom1.then(() => {
// uh-oh; user restarts before second request completes. The new instance
// should know we never got a complete device list.
console.log("Creating new devicelist to simulate app reload");
downloadSpy.reset();
const dl2 = createTestDeviceList();
const queryDefer3 = q.defer();
downloadSpy.andReturn(queryDefer3.promise);
const prom3 = dl2.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
queryDefer3.resolve(utils.deepCopy(signedDeviceList));
// allow promise chain to complete
return prom3;
}).then(() => {
const storedKeys = sessionStore.getEndToEndDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
});
});
});
+42
View File
@@ -120,6 +120,20 @@ MatrixBaseApis.prototype.makeTxnId = function() {
// Registration/Login operations
// =============================
/**
* Check whether a username is available prior to registration. An error response
* indicates an invalid/unavailable username.
* @param {string} username The username to check the availability of.
* @return {module:client.Promise} Resolves: to `true`.
*/
MatrixBaseApis.prototype.isUsernameAvailable = function(username) {
return this._http.authedRequest(
undefined, "GET", '/register/available', { username: username },
).then((response) => {
return response.available;
});
};
/**
* @param {string} username
* @param {string} password
@@ -470,6 +484,34 @@ MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) {
);
};
/**
* Set a marker to indicate the point in a room before which the user has read every
* event. This can be retrieved from room account data (the event type is `m.fully_read`)
* and displayed as a horizontal line in the timeline that is visually distinct to the
* position of the user's own read receipt.
* @param {string} roomId ID of the room that has been read
* @param {string} rmEventId ID of the event that has been read
* @param {string} rrEventId ID of the event tracked by the read receipt. This is here
* for convenience because the RR and the RM are commonly updated at the same time as
* each other. Optional.
* @return {module:client.Promise} Resolves: the empty object, {}.
*/
MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
function(roomId, rmEventId, rrEventId) {
const path = utils.encodeUri("/rooms/$roomId/read_markers", {
$roomId: roomId,
});
const content = {
"m.fully_read": rmEventId,
"m.read": rrEventId,
};
return this._http.authedRequest(
undefined, "POST", path, undefined, content,
);
};
// Room Directory operations
// =========================
+80 -1
View File
@@ -104,6 +104,9 @@ try {
* disabled by default for compatibility with older clients - in particular to
* maintain support for back-paginating the live timeline after a '/sync'
* result with a gap.
*
* @param {module:crypto.store.base~CryptoStore} opts.cryptoStore
* crypto store implementation.
*/
function MatrixClient(opts) {
MatrixBaseApis.call(this, opts);
@@ -153,13 +156,16 @@ function MatrixClient(opts) {
this._notifTimelineSet = null;
this._crypto = null;
this._cryptoStore = opts.cryptoStore;
if (CRYPTO_ENABLED && Boolean(opts.sessionStore) &&
Boolean(this._cryptoStore) &&
userId !== null && this.deviceId !== null) {
this._crypto = new Crypto(
this, this,
opts.sessionStore,
userId, this.deviceId,
this.store,
opts.cryptoStore,
);
this.olmVersion = Crypto.getOlmVersion();
@@ -168,6 +174,37 @@ function MatrixClient(opts) {
utils.inherits(MatrixClient, EventEmitter);
utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
/**
* Clear any data out of the persistent stores used by the client.
*
* @returns {Promise} Promise which resolves when the stores have been cleared.
*/
MatrixClient.prototype.clearStores = function() {
if (this._clientRunning) {
throw new Error("Cannot clear stores while client is running");
}
const promises = [];
promises.push(this.store.deleteAllData());
if (this._cryptoStore) {
promises.push(this._cryptoStore.deleteAllData());
}
return q.all(promises);
};
/**
* Get the user-id of the logged-in user
*
* @return {?string} MXID for the logged-in user, or null if not logged in
*/
MatrixClient.prototype.getUserId = function() {
if (this.credentials && this.credentials.userId) {
return this.credentials.userId;
}
return null;
};
/**
* Get the domain for this client's MXID
* @return {?string} Domain of this MXID
@@ -359,6 +396,21 @@ MatrixClient.prototype.getStoredDevicesForUser = function(userId) {
return this._crypto.getStoredDevicesForUser(userId) || [];
};
/**
* Get the stored device key for a user id and device id
*
* @param {string} userId the user to list keys for.
* @param {string} deviceId unique identifier for the device
*
* @return {?module:crypto-deviceinfo} device or null
*/
MatrixClient.prototype.getStoredDevice = function(userId, deviceId) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.getStoredDevice(userId, deviceId) || null;
};
/**
* Mark the given device as verified
*
@@ -552,7 +604,7 @@ function _decryptEvent(client, event) {
console.warn(
`Error decrypting event (id=${event.getId()}): ${e}`,
);
if (!(e instanceof Crypto.DecryptionError)) {
if (e.name !== "DecryptionError") {
throw e;
}
_badEncryptedMessage(event, e.message);
@@ -1201,6 +1253,33 @@ MatrixClient.prototype.sendReadReceipt = function(event, callback) {
return this.sendReceipt(event, "m.read", callback);
};
/**
* Set a marker to indicate the point in a room before which the user has read every
* event. This can be retrieved from room account data (the event type is `m.fully_read`)
* and displayed as a horizontal line in the timeline that is visually distinct to the
* position of the user's own read receipt.
* @param {string} roomId ID of the room that has been read
* @param {string} eventId ID of the event that has been read
* @param {string} rrEvent the event tracked by the read receipt. This is here for
* convenience because the RR and the RM are commonly updated at the same time as each
* other. The local echo of this receipt will be done if set. Optional.
* @return {module:client.Promise} Resolves: the empty object, {}.
*/
MatrixClient.prototype.setRoomReadMarkers = function(roomId, eventId, rrEvent) {
const rmEventId = eventId;
let rrEventId;
// Add the optional RR update, do local echo like `sendReceipt`
if (rrEvent) {
rrEventId = rrEvent.getId();
const room = this.getRoom(roomId);
if (room) {
room._addLocalEchoReceipt(this.credentials.userId, rrEvent, "m.read");
}
}
return this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId);
};
/**
* Get a preview of the given URL as of (roughly) the given point in time,
+251 -150
View File
@@ -26,35 +26,35 @@ import q from 'q';
import DeviceInfo from './deviceinfo';
import olmlib from './olmlib';
// constants for DeviceList._deviceTrackingStatus
// const TRACKING_STATUS_NOT_TRACKED = 0;
const TRACKING_STATUS_PENDING_DOWNLOAD = 1;
const TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2;
const TRACKING_STATUS_UP_TO_DATE = 3;
/**
* @alias module:crypto/DeviceList
*/
export default class DeviceList {
constructor(baseApis, sessionStore, olmDevice) {
this._baseApis = baseApis;
this._sessionStore = sessionStore;
this._olmDevice = olmDevice;
this._serialiser = new DeviceListUpdateSerialiser(
baseApis, sessionStore, olmDevice,
);
// users with outdated device lists
// userId -> true
this._pendingUsersWithNewDevices = {};
// which users we are tracking device status for.
// userId -> TRACKING_STATUS_*
this._deviceTrackingStatus = sessionStore.getEndToEndDeviceTrackingStatus() || {};
for (const u of Object.keys(this._deviceTrackingStatus)) {
// if a download was in progress when we got shut down, it isn't any more.
if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
}
}
// userId -> true
// userId -> promise
this._keyDownloadsInProgressByUser = {};
// deferred which is resolved when the current device query resolves.
// (null if there is no current request).
this._currentQueryDeferred = null;
// deferred which is resolved when the *next* device query resolves.
//
// Normally it is meaningless for this to be non-null when
// _currentQueryDeferred is null, but it can happen if the previous
// query has finished but the next one has not yet started (because the
// previous query failed, in which case we deliberately delay starting
// the next query to avoid tight-looping).
this._queuedQueryDeferred = null;
this.lastKnownSyncToken = null;
}
@@ -68,43 +68,27 @@ export default class DeviceList {
* module:crypto/deviceinfo|DeviceInfo}.
*/
downloadKeys(userIds, forceDownload) {
let needsRefresh = false;
let waitForCurrentQuery = false;
const usersToDownload = [];
const promises = [];
userIds.forEach((u) => {
if (this._pendingUsersWithNewDevices[u]) {
// we already know this user's devices are outdated
needsRefresh = true;
} else if (this._keyDownloadsInProgressByUser[u]) {
// already a download in progress - just wait for it.
// (even if forceDownload is true)
waitForCurrentQuery = true;
} else if (forceDownload) {
console.log("Invalidating device list for " + u +
" for forceDownload");
this.invalidateUserDeviceList(u);
needsRefresh = true;
} else if (!this.getStoredDevicesForUser(u)) {
console.log("Invalidating device list for " + u +
" due to empty cache");
this.invalidateUserDeviceList(u);
needsRefresh = true;
const trackingStatus = this._deviceTrackingStatus[u];
if (this._keyDownloadsInProgressByUser[u]) {
// already a key download in progress/queued for this user; its results
// will be good enough for us.
promises.push(this._keyDownloadsInProgressByUser[u]);
} else if (forceDownload || trackingStatus != TRACKING_STATUS_UP_TO_DATE) {
usersToDownload.push(u);
}
});
let promise;
if (needsRefresh) {
console.log("downloadKeys: waiting for next key query");
promise = this._startOrQueueDeviceQuery();
} else if(waitForCurrentQuery) {
console.log("downloadKeys: waiting for in-flight query to complete");
promise = this._currentQueryDeferred.promise;
} else {
// we're all up-to-date.
promise = q();
if (usersToDownload.length != 0) {
console.log("downloadKeys: downloading for", usersToDownload);
const downloadPromise = this._doKeyDownload(usersToDownload);
promises.push(downloadPromise);
}
return promise.then(() => {
return q.all(promises).then(() => {
return this._getDevicesFromStore(userIds);
});
}
@@ -216,14 +200,15 @@ export default class DeviceList {
}
/**
* Mark the cached device list for the given user outdated.
* flag the given user for device-list tracking, if they are not already.
*
* This doesn't set off an update, so that several users can be batched
* together. Call refreshOutdatedDeviceLists() for that.
* This will mean that a subsequent call to refreshOutdatedDeviceLists()
* will download the device list for the user, and that subsequent calls to
* invalidateUserDeviceList will trigger more updates.
*
* @param {String} userId
*/
invalidateUserDeviceList(userId) {
startTrackingDeviceList(userId) {
// sanity-check the userId. This is mostly paranoia, but if synapse
// can't parse the userId we give it as an mxid, it 500s the whole
// request and we can never update the device lists again (because
@@ -234,124 +219,229 @@ export default class DeviceList {
if (typeof userId !== 'string') {
throw new Error('userId must be a string; was '+userId);
}
this._pendingUsersWithNewDevices[userId] = true;
if (!this._deviceTrackingStatus[userId]) {
console.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; instead we wait for the forthcoming
// refreshOutdatedDeviceLists.
}
/**
* If there is not already a device list query in progress, and we have
* users who have outdated device lists, start a query now.
* Mark the cached device list for the given user outdated.
*
* If we are not tracking this user's devices, we'll do nothing. Otherwise
* we flag the user as needing an update.
*
* This doesn't actually set off an update, so that several users can be
* batched together. Call refreshOutdatedDeviceLists() for that.
*
* @param {String} userId
*/
invalidateUserDeviceList(userId) {
if (this._deviceTrackingStatus[userId]) {
console.log("Marking device list outdated 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; instead we wait for the forthcoming
// refreshOutdatedDeviceLists.
}
/**
* Mark all tracked device lists as outdated.
*
* This will flag each user whose devices we are tracking as in need of an
* update.
*/
invalidateAllDeviceLists() {
for (const userId of Object.keys(this._deviceTrackingStatus)) {
this.invalidateUserDeviceList(userId);
}
}
/**
* If we have users who have outdated device lists, start key downloads for them
*
* @returns {Promise} which completes when the download completes; normally there
* is no need to wait for this (it's mostly for the unit tests).
*/
refreshOutdatedDeviceLists() {
if (this._currentQueryDeferred) {
// request already in progress - do nothing. (We will automatically
// make another request if there are more users with outdated
// device lists when the current request completes).
const usersToDownload = [];
for (const userId of Object.keys(this._deviceTrackingStatus)) {
const stat = this._deviceTrackingStatus[userId];
if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) {
usersToDownload.push(userId);
}
}
if (usersToDownload.length == 0) {
return;
}
this._startDeviceQuery();
// we didn't persist the tracking status during
// invalidateUserDeviceList, so do it now.
this._persistDeviceTrackingStatus();
return this._doKeyDownload(usersToDownload);
}
/**
* Fire off download update requests for the given users, and update the
* device list tracking status for them, and the
* _keyDownloadsInProgressByUser map for them.
*
* @param {String[]} users list of userIds
*
* @return {module:client.Promise} resolves when all the users listed have
* been updated. rejects if there was a problem updating any of the
* users.
*/
_doKeyDownload(users) {
if (users.length === 0) {
// nothing to do
return q();
}
const prom = this._serialiser.updateDevicesForUsers(
users, this.lastKnownSyncToken,
).then(() => {
finished(true);
}, (e) => {
console.error(
'Error downloading keys for ' + users + ":", e,
);
finished(false);
throw e;
});
users.forEach((u) => {
this._keyDownloadsInProgressByUser[u] = prom;
const stat = this._deviceTrackingStatus[u];
if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) {
this._deviceTrackingStatus[u] = TRACKING_STATUS_DOWNLOAD_IN_PROGRESS;
}
});
const finished = (success) => {
users.forEach((u) => {
// we may have queued up another download request for this user
// since we started this request. If that happens, we should
// ignore the completion of the first one.
if (this._keyDownloadsInProgressByUser[u] !== prom) {
console.log('Another update in the queue for', u,
'- not marking up-to-date');
return;
}
delete this._keyDownloadsInProgressByUser[u];
const stat = this._deviceTrackingStatus[u];
if (stat == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
if (success) {
// we didn't get any new invalidations since this download started:
// this user's device list is now up to date.
this._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE;
console.log("Device list for", u, "now up to date");
} else {
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
}
}
});
this._persistDeviceTrackingStatus();
};
return prom;
}
_persistDeviceTrackingStatus() {
this._sessionStore.storeEndToEndDeviceTrackingStatus(this._deviceTrackingStatus);
}
}
/**
* Serialises updates to device lists
*
* Ensures that results from /keys/query are not overwritten if a second call
* completes *before* an earlier one.
*
* It currently does this by ensuring only one call to /keys/query happens at a
* time (and queuing other requests up).
*/
class DeviceListUpdateSerialiser {
constructor(baseApis, sessionStore, olmDevice) {
this._baseApis = baseApis;
this._sessionStore = sessionStore;
this._olmDevice = olmDevice;
this._downloadInProgress = false;
// users which are queued for download
// userId -> true
this._keyDownloadsQueuedByUser = {};
// deferred which is resolved when the queued users are downloaded.
//
// non-null indicates that we have users queued for download.
this._queuedQueryDeferred = null;
// sync token to be used for the next query: essentially the
// most recent one we know about
this._nextSyncToken = null;
}
/**
* If there is currently a device list query in progress, returns a promise
* which will resolve when the *next* query completes. Otherwise, starts
* a new query, and returns a promise which resolves when it completes.
* Make a key query request for the given users
*
* @return {Promise}
* @param {String[]} users list of user ids
*
* @param {String} syncToken sync token to pass in the query request, to
* help the HS give the most recent results
*
* @return {module:client.Promise} resolves when all the users listed have
* been updated. rejects if there was a problem updating any of the
* users.
*/
_startOrQueueDeviceQuery() {
if (!this._currentQueryDeferred) {
this._startDeviceQuery();
if (!this._currentQueryDeferred) {
return q();
}
return this._currentQueryDeferred.promise;
}
updateDevicesForUsers(users, syncToken) {
users.forEach((u) => {
this._keyDownloadsQueuedByUser[u] = true;
});
this._nextSyncToken = syncToken;
if (!this._queuedQueryDeferred) {
this._queuedQueryDeferred = q.defer();
}
return this._queuedQueryDeferred.promise;
}
/**
* kick off a new device query
*
* Throws if there is already a query in progress.
*/
_startDeviceQuery() {
if (this._currentQueryDeferred) {
throw new Error("DeviceList._startDeviceQuery called with request active");
if (this._downloadInProgress) {
// just queue up these users
console.log('Queued key download for', users);
return this._queuedQueryDeferred.promise;
}
this._currentQueryDeferred = this._queuedQueryDeferred || q.defer();
// start a new download.
return this._doQueuedQueries();
}
_doQueuedQueries() {
if (this._downloadInProgress) {
throw new Error(
"DeviceListUpdateSerialiser._doQueuedQueries called with request active",
);
}
const downloadUsers = Object.keys(this._keyDownloadsQueuedByUser);
this._keyDownloadsQueuedByUser = {};
const deferred = this._queuedQueryDeferred;
this._queuedQueryDeferred = null;
const users = Object.keys(this._pendingUsersWithNewDevices);
if (users.length === 0) {
// nothing to do
this._currentQueryDeferred.resolve();
this._currentQueryDeferred = null;
console.log('Starting key download for', downloadUsers);
this._downloadInProgress = true;
// that means we're up-to-date with the lastKnownSyncToken.
const token = this.lastKnownSyncToken;
if (token !== null) {
this._sessionStore.storeEndToEndDeviceSyncToken(token);
}
return;
}
this._doKeyDownloadForUsers(users).done(() => {
users.forEach((u) => {
delete this._keyDownloadsInProgressByUser[u];
});
this._currentQueryDeferred.resolve();
this._currentQueryDeferred = null;
// flush out any more requests that were blocked up while that
// was going on.
this._startDeviceQuery();
}, (e) => {
console.error(
'Error updating device key cache for ' + users + ":", e,
);
// reinstate the pending flags on any users which failed; this will
// mean that we will do another download in the future (actually on
// the next /sync).
users.forEach((u) => {
delete this._keyDownloadsInProgressByUser[u];
this._pendingUsersWithNewDevices[u] = true;
});
this._currentQueryDeferred.reject(e);
this._currentQueryDeferred = null;
});
users.forEach((u) => {
delete this._pendingUsersWithNewDevices[u];
this._keyDownloadsInProgressByUser[u] = true;
});
}
/**
* @param {string[]} downloadUsers list of userIds
*
* @return {Promise}
*/
_doKeyDownloadForUsers(downloadUsers) {
console.log('Starting key download for ' + downloadUsers);
const token = this.lastKnownSyncToken;
const opts = {};
if (token) {
opts.token = token;
if (this._nextSyncToken) {
opts.token = this._nextSyncToken;
}
return this._baseApis.downloadKeysForUsers(
this._baseApis.downloadKeysForUsers(
downloadUsers, opts,
).then((res) => {
const dk = res.device_keys || {};
@@ -369,12 +459,23 @@ export default class DeviceList {
}
return prom;
}).then(() => {
if (token !== null) {
this._sessionStore.storeEndToEndDeviceSyncToken(token);
}
}).done(() => {
console.log('Completed key download for ' + downloadUsers);
this._downloadInProgress = false;
deferred.resolve();
// if we have queued users, fire off another request.
if (this._queuedQueryDeferred) {
this._doQueuedQueries();
}
}, (e) => {
console.warn('Error downloading keys for ' + downloadUsers + ':', e);
this._downloadInProgressInProgress = false;
deferred.reject(e);
});
return deferred.promise;
}
_processQueryResponseForUser(userId, response) {
+112 -114
View File
@@ -13,14 +13,12 @@ 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.
*/
"use strict";
/**
* Internal module. Defines the base classes of the encryption implementations
*
* @module crypto/algorithms/base
* @module
*/
const utils = require("../../utils");
/**
* map of registered encryption algorithm classes. A map from string to {@link
@@ -28,7 +26,7 @@ const utils = require("../../utils");
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
*/
module.exports.ENCRYPTION_CLASSES = {};
export const ENCRYPTION_CLASSES = {};
/**
* map of registered encryption algorithm classes. Map from string to {@link
@@ -36,12 +34,11 @@ module.exports.ENCRYPTION_CLASSES = {};
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
*/
module.exports.DECRYPTION_CLASSES = {};
export const DECRYPTION_CLASSES = {};
/**
* base type for encryption implementations
*
* @constructor
* @alias module:crypto/algorithms/base.EncryptionAlgorithm
*
* @param {object} params parameters
@@ -53,45 +50,45 @@ module.exports.DECRYPTION_CLASSES = {};
* @param {string} params.roomId The ID of the room we will be sending to
* @param {object} params.config The body of the m.room.encryption event
*/
const EncryptionAlgorithm = function(params) {
this._userId = params.userId;
this._deviceId = params.deviceId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._baseApis = params.baseApis;
this._roomId = params.roomId;
};
/** */
module.exports.EncryptionAlgorithm = EncryptionAlgorithm;
class EncryptionAlgorithm {
constructor(params) {
this._userId = params.userId;
this._deviceId = params.deviceId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._baseApis = params.baseApis;
this._roomId = params.roomId;
}
/**
* Encrypt a message event
*
* @method module:crypto/algorithms/base.EncryptionAlgorithm#encryptMessage
* @abstract
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
/**
* Encrypt a message event
*
* @method module:crypto/algorithms/base.EncryptionAlgorithm.encryptMessage
* @abstract
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
/**
* Called when the membership of a member of the room changes.
*
* @param {module:models/event.MatrixEvent} event event causing the change
* @param {module:models/room-member} member user whose membership changed
* @param {string=} oldMembership previous membership
*/
EncryptionAlgorithm.prototype.onRoomMembership = function(
event, member, oldMembership,
) {};
/**
* Called when the membership of a member of the room changes.
*
* @param {module:models/event.MatrixEvent} event event causing the change
* @param {module:models/room-member} member user whose membership changed
* @param {string=} oldMembership previous membership
* @public
*/
onRoomMembership(event, member, oldMembership) {
}
}
export {EncryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
/**
* base type for decryption implementations
*
* @constructor
* @alias module:crypto/algorithms/base.DecryptionAlgorithm
*
* @param {object} params parameters
@@ -101,55 +98,55 @@ EncryptionAlgorithm.prototype.onRoomMembership = function(
* @param {string=} params.roomId The ID of the room we will be receiving
* from. Null for to-device events.
*/
const DecryptionAlgorithm = function(params) {
this._userId = params.userId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._roomId = params.roomId;
};
/** */
module.exports.DecryptionAlgorithm = DecryptionAlgorithm;
class DecryptionAlgorithm {
constructor(params) {
this._userId = params.userId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._roomId = params.roomId;
}
/**
* Decrypt an event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent
* @abstract
*
* @param {object} event raw event
*
* @return {null} if the event referred to an unknown megolm session
* @return {module:crypto.DecryptionResult} decryption result
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
/**
* Decrypt an event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent
* @abstract
*
* @param {object} event raw event
*
* @return {null} if the event referred to an unknown megolm session
* @return {module:crypto.DecryptionResult} decryption result
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
/**
* Handle a key event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent
*
* @param {module:models/event.MatrixEvent} params event key event
*/
DecryptionAlgorithm.prototype.onRoomKeyEvent = function(params) {
// ignore by default
};
/**
* Handle a key event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent
*
* @param {module:models/event.MatrixEvent} params event key event
*/
onRoomKeyEvent(params) {
// ignore by default
}
/**
* Import a room key
*
* @param {module:crypto/OlmDevice.MegolmSessionData} session
*/
DecryptionAlgorithm.prototype.importRoomKey = function(session) {
// ignore by default
};
/**
* Import a room key
*
* @param {module:crypto/OlmDevice.MegolmSessionData} session
*/
importRoomKey(session) {
// ignore by default
}
}
export {DecryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
/**
* Exception thrown when decryption fails
*
* @alias module:crypto/algorithms/base.DecryptionError
* @constructor
* @param {string} msg user-visible message describing the problem
*
* @param {Object=} details key/value pairs reported in the logs but not shown
@@ -157,50 +154,51 @@ DecryptionAlgorithm.prototype.importRoomKey = function(session) {
*
* @extends Error
*/
const DecryptionError = function(msg, details) {
this.name = 'DecryptionError';
this.message = msg;
this.details = details;
};
utils.inherits(DecryptionError, Error);
/** override the string used when logging
*
* @returns {String}
*/
DecryptionError.prototype.toString = function() {
let result = this.name + '[msg: ' + this.message;
if (this.details) {
result += ', ' +
Object.keys(this.details).map(
(k) => k + ': ' + this.details[k],
).join(', ');
class DecryptionError extends Error {
constructor(msg, details) {
super(msg);
this.name = 'DecryptionError';
this.details = details;
}
result += ']';
/**
* override the string used when logging
*
* @returns {String}
*/
toString() {
let result = this.name + '[msg: ' + this.message;
return result;
};
if (this.details) {
result += ', ' +
Object.keys(this.details).map(
(k) => k + ': ' + this.details[k],
).join(', ');
}
module.exports.DecryptionError = DecryptionError;
result += ']';
return result;
}
}
export {DecryptionError}; // https://github.com/jsdoc3/jsdoc/issues/1272
/**
* Exception thrown specifically when we want to warn the user to consider
* the security of their conversation before continuing
*
* @constructor
* @param {string} msg message describing the problem
* @param {Object} devices userId -> {deviceId -> object}
* set of unknown devices per user we're warning about
* @extends Error
*/
module.exports.UnknownDeviceError = function(msg, devices) {
this.name = "UnknownDeviceError";
this.message = msg;
this.devices = devices;
};
utils.inherits(module.exports.UnknownDeviceError, Error);
export class UnknownDeviceError extends Error {
constructor(msg, devices) {
super(msg);
this.name = "UnknownDeviceError";
this.devices = devices;
}
}
/**
* Registers an encryption/decryption class for a particular algorithm
@@ -215,7 +213,7 @@ utils.inherits(module.exports.UnknownDeviceError, Error);
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm}
* implementation
*/
module.exports.registerAlgorithm = function(algorithm, encryptor, decryptor) {
module.exports.ENCRYPTION_CLASSES[algorithm] = encryptor;
module.exports.DECRYPTION_CLASSES[algorithm] = decryptor;
};
export function registerAlgorithm(algorithm, encryptor, decryptor) {
ENCRYPTION_CLASSES[algorithm] = encryptor;
DECRYPTION_CLASSES[algorithm] = decryptor;
}
+52 -72
View File
@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,7 +16,6 @@ limitations under the License.
*/
"use strict";
/**
* @module crypto
*/
@@ -50,18 +50,22 @@ const DeviceList = require('./DeviceList').default;
* @param {string} deviceId The identifier for this device.
*
* @param {Object} clientStore the MatrixClient data store.
*
* @param {module:crypto/store/base~CryptoStore} cryptoStore
* storage for the crypto layer.
*/
function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId,
clientStore) {
clientStore, cryptoStore) {
this._baseApis = baseApis;
this._sessionStore = sessionStore;
this._userId = userId;
this._deviceId = deviceId;
this._clientStore = clientStore;
this._cryptoStore = cryptoStore;
this._olmDevice = new OlmDevice(sessionStore);
this._deviceList = new DeviceList(baseApis, sessionStore, this._olmDevice);
this._initialDeviceListInvalidationDone = false;
this._initialDeviceListInvalidationPending = false;
this._clientRunning = false;
@@ -558,9 +562,13 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
* Configure a room to use encryption (ie, save a flag in the sessionstore).
*
* @param {string} roomId The room ID to enable encryption in.
*
* @param {object} config The encryption config for the room.
*
* @param {boolean=} inhibitDeviceQuery true to suppress device list query for
* users in the room (for now)
*/
Crypto.prototype.setRoomEncryption = function(roomId, config) {
Crypto.prototype.setRoomEncryption = function(roomId, config, inhibitDeviceQuery) {
// if we already have encryption in this room, we should ignore this event
// (for now at least. maybe we should alert the user somehow?)
const existingConfig = this._sessionStore.getEndToEndRoom(roomId);
@@ -590,21 +598,16 @@ Crypto.prototype.setRoomEncryption = function(roomId, config) {
});
this._roomEncryptors[roomId] = alg;
// if encryption was not previously enabled in this room, we will have been
// ignoring new device events for these users so far. We may well have
// up-to-date lists for some users, for instance if we were sharing other
// e2e rooms with them, so there is room for optimisation here, but for now
// we just invalidate everyone in the room.
if (!existingConfig) {
console.log("Enabling encryption in " + roomId + " for the first time; " +
"invalidating device lists for all users therein");
const room = this._clientStore.getRoom(roomId);
const members = room.getJoinedMembers();
members.forEach((m) => {
this._deviceList.invalidateUserDeviceList(m.userId);
});
// the actual refresh happens once we've finished processing the sync,
// in _onSyncCompleted.
// make sure we are tracking the device lists for all users in this room.
console.log("Enabling encryption in " + roomId + "; " +
"starting to track device lists for all users therein");
const room = this._clientStore.getRoom(roomId);
const members = room.getJoinedMembers();
members.forEach((m) => {
this._deviceList.startTrackingDeviceList(m.userId);
});
if (!inhibitDeviceQuery) {
this._deviceList.refreshOutdatedDeviceLists();
}
};
@@ -795,7 +798,9 @@ Crypto.prototype._onCryptoEvent = function(event) {
const content = event.getContent();
try {
this.setRoomEncryption(roomId, content);
// inhibit the device list refresh for now - it will happen once we've
// finished processing the sync, in _onSyncCompleted.
this.setRoomEncryption(roomId, content, true);
} catch (e) {
console.error("Error configuring encryption in room " + roomId +
":", e);
@@ -814,6 +819,8 @@ Crypto.prototype._onSyncCompleted = function(syncData) {
const nextSyncToken = syncData.nextSyncToken;
if (!syncData.oldSyncToken) {
console.log("Completed initial sync");
// an initialsync.
this._sendNewDeviceEvents();
@@ -821,37 +828,40 @@ Crypto.prototype._onSyncCompleted = function(syncData) {
// invalidate devices which have changed since then.
const oldSyncToken = this._sessionStore.getEndToEndDeviceSyncToken();
if (oldSyncToken !== null) {
this._initialDeviceListInvalidationPending = true;
this._invalidateDeviceListsSince(
oldSyncToken, nextSyncToken,
).catch((e) => {
// if that failed, we fall back to invalidating everyone.
console.warn("Error fetching changed device list", e);
this._invalidateDeviceListForAllActiveUsers();
this._deviceList.invalidateAllDeviceLists();
}).done(() => {
this._initialDeviceListInvalidationDone = true;
this._initialDeviceListInvalidationPending = false;
this._deviceList.lastKnownSyncToken = nextSyncToken;
this._deviceList.refreshOutdatedDeviceLists();
});
} else {
// otherwise, we have to invalidate all devices for all users we
// share a room with.
// are tracking.
console.log("Completed first initialsync; invalidating all " +
"device list caches");
this._invalidateDeviceListForAllActiveUsers();
this._initialDeviceListInvalidationDone = true;
this._deviceList.invalidateAllDeviceLists();
}
}
if (this._initialDeviceListInvalidationDone) {
// if we've got an up-to-date list of users with outdated device lists,
// tell the device list about the new sync token (but not otherwise, because
// otherwise we'll start thinking we're more in sync than we are.)
this._deviceList.lastKnownSyncToken = nextSyncToken;
// catch up on any new devices we got told about during the sync.
this._deviceList.refreshOutdatedDeviceLists();
if (!this._initialDeviceListInvalidationPending) {
// we can now store our sync token so that we can get an update on
// restart rather than having to invalidate everyone.
//
// (we don't really need to do this on every sync - we could just
// do it periodically)
this._sessionStore.storeEndToEndDeviceSyncToken(nextSyncToken);
}
// catch up on any new devices we got told about during the sync.
this._deviceList.lastKnownSyncToken = nextSyncToken;
this._deviceList.refreshOutdatedDeviceLists();
// we don't start uploading one-time keys until we've caught up with
// to-device messages, to help us avoid throwing away one-time-keys that we
// are about to receive messages for
@@ -928,49 +938,18 @@ Crypto.prototype._invalidateDeviceListsSince = function(
return this._baseApis.getKeyChanges(
oldSyncToken, lastKnownSyncToken,
).then((r) => {
console.log("got key changes since", oldSyncToken, ":", r.changed);
if (!r.changed || !Array.isArray(r.changed)) {
return;
}
// only invalidate users we share an e2e room with - we don't
// care about users in non-e2e rooms.
const filteredUserIds = this._getE2eRoomMembers();
r.changed.forEach((u) => {
if (u in filteredUserIds) {
this._deviceList.invalidateUserDeviceList(u);
}
this._deviceList.invalidateUserDeviceList(u);
});
});
};
/**
* Invalidate any stored device list for any users we share an e2e room with
*
* @private
*/
Crypto.prototype._invalidateDeviceListForAllActiveUsers = function() {
Object.keys(this._getE2eRoomMembers()).forEach((m) => {
this._deviceList.invalidateUserDeviceList(m);
});
};
/**
* get the users we share an e2e-enabled room with
*
* @returns {Object<string>} userid->userid map (should be a Set but argh ES6)
*/
Crypto.prototype._getE2eRoomMembers = function() {
const userIds = Object.create(null);
const rooms = this._getE2eRooms();
for (const r of rooms) {
const members = r.getJoinedMembers();
members.forEach((m) => { userIds[m.userId] = m.userId; });
}
return userIds;
};
/**
* Get a list of the e2e-enabled rooms we are members of
*
@@ -1039,6 +1018,12 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) {
return;
}
if (member.membership == 'join') {
console.log('Join event for ' + member.userId + ' in ' + roomId);
// make sure we are tracking the deviceList for this user
this._deviceList.startTrackingDeviceList(member.userId);
}
alg.onRoomMembership(event, member, oldMembership);
};
@@ -1140,11 +1125,6 @@ Crypto.prototype._signObject = function(obj) {
obj.signatures = sigs;
};
/**
* @see module:crypto/algorithms/base.DecryptionError
*/
Crypto.DecryptionError = algorithms.DecryptionError;
/** */
module.exports = Crypto;
+11
View File
@@ -0,0 +1,11 @@
/**
* Internal module. Defintions for storage for the crypto module
*
* @module
*/
/**
* Abstraction of things that can store data required for end-to-end encryption
*
* @interface CryptoStore
*/
+126
View File
@@ -0,0 +1,126 @@
/*
Copyright 2017 Vector Creations 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 q from 'q';
/**
* Internal module. indexeddb storage for e2e.
*
* @module
*/
const VERSION = 1;
/**
* @implements {module:crypto/store/base~CryptoStore}
*/
export default class IndexedDBCryptoStore {
/**
* Create a new IndexedDBCryptoStore
*
* @param {IDBFactory} indexedDB global indexedDB instance
* @param {string} dbName name of db to connect to
*/
constructor(indexedDB, dbName) {
if (!indexedDB) {
throw new Error("must pass indexedDB into IndexedDBCryptoStore");
}
this._indexedDB = indexedDB;
this._dbName = dbName;
this._dbPromise = null;
}
/**
* Ensure the database exists and is up-to-date
*
* @return {Promise} resolves to an instance of IDBDatabase when
* the database is ready
*/
connect() {
if (this._dbPromise) {
return this._dbPromise;
}
this._dbPromise = new q.Promise((resolve, reject) => {
const req = this._indexedDB.open(this._dbName, VERSION);
req.onupgradeneeded = (ev) => {
const db = ev.target.result;
const oldVersion = ev.oldVersion;
console.log(
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
+ ` to ${VERSION}`,
);
if (oldVersion < 1) { // The database did not previously exist.
createDatabase(db);
}
// Expand as needed.
};
req.onblocked = () => {
reject(new Error(
"unable to upgrade indexeddb because it is open elsewhere",
));
};
req.onerror = (ev) => {
reject(new Error(
"unable to connect to indexeddb: " + ev.target.error,
));
};
req.onsuccess = (r) => {
resolve(r.target.result);
};
});
return this._dbPromise;
}
/**
* Delete all data from this store.
*
* @returns {Promise} resolves when the store has been cleared.
*/
deleteAllData() {
return new q.Promise((resolve, reject) => {
console.log(`Removing indexeddb instance: ${this._dbName}`);
const req = this._indexedDB.deleteDatabase(this._dbName);
req.onerror = (ev) => {
reject(new Error(
"unable to delete indexeddb: " + ev.target.error,
));
};
req.onsuccess = () => {
console.log(`Removed indexeddb instance: ${this._dbName}`);
resolve();
};
});
}
}
function createDatabase(db) {
const outgoingRoomKeyRequestsStore =
db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" });
// we assume that the RoomKeyRequestBody will have room_id and session_id
// properties, to make the index efficient.
outgoingRoomKeyRequestsStore.createIndex("session",
["requestBody.room_id", "requestBody.session_id"],
);
outgoingRoomKeyRequestsStore.createIndex("state", "state");
}
+40
View File
@@ -0,0 +1,40 @@
/*
Copyright 2017 Vector Creations 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 q from 'q';
/**
* Internal module. in-memory storage for e2e.
*
* @module
*/
/**
* @implements {module:crypto/store/base~CryptoStore}
*/
export default class MemoryCryptoStore {
constructor() {
}
/**
* Delete all data from this store.
*
* @returns {Promise} Promise which resolves when the store has been cleared.
*/
deleteAllData() {
return q();
}
}
+7
View File
@@ -318,6 +318,13 @@ InteractiveAuth.prototype = {
}
this._currentStage = nextStage;
if (nextStage == 'm.login.dummy') {
this.submitAuthDict({
type: 'm.login.dummy',
});
return;
}
if (this._data.errcode || this._data.error) {
this._stateUpdatedCallback(nextStage, {
errcode: this._data.errcode || "",
+43
View File
@@ -63,6 +63,11 @@ module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
module.exports.InteractiveAuth = require("./interactive-auth");
module.exports.MemoryCryptoStore =
require("./crypto/store/memory-crypto-store").default;
module.exports.IndexedDBCryptoStore =
require("./crypto/store/indexeddb-crypto-store").default;
/**
* Create a new Matrix Call.
* @function
@@ -73,6 +78,23 @@ module.exports.InteractiveAuth = require("./interactive-auth");
*/
module.exports.createNewMatrixCall = require("./webrtc/call").createNewMatrixCall;
/**
* Set an audio input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setMatrixCallAudioInput = require('./webrtc/call').setAudioInput;
/**
* Set a video input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setMatrixCallVideoInput = require('./webrtc/call').setVideoInput;
// expose the underlying request object so different environments can use
// different request libs (e.g. request or browser-request)
let request;
@@ -107,6 +129,19 @@ module.exports.wrapRequest = function(wrapper) {
};
};
let cryptoStoreFactory = () => new module.exports.MemoryCryptoStore;
/**
* Configure a different factory to be used for creating crypto stores
*
* @param {Function} fac a function which will return a new
* {@link module:crypto.store.base~CryptoStore}.
*/
module.exports.setCryptoStoreFactory = function(fac) {
cryptoStoreFactory = fac;
};
/**
* Construct a Matrix Client. Similar to {@link module:client~MatrixClient}
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
@@ -119,6 +154,13 @@ module.exports.wrapRequest = function(wrapper) {
* {@link module:scheduler~MatrixScheduler}.
* @param {requestFunction} opts.request If not set, defaults to the function
* supplied to {@link request} which defaults to the request module from NPM.
*
* @param {module:crypto.store.base~CryptoStore=} opts.cryptoStore
* crypto store implementation. Calls the factory supplied to
* {@link setCryptoStoreFactory} if unspecified; or if no factory has been
* specified, uses a default implementation (indexeddb in the browser,
* in-memory otherwise).
*
* @return {MatrixClient} A new matrix client.
* @see {@link module:client~MatrixClient} for the full list of options for
* <code>opts</code>.
@@ -134,6 +176,7 @@ module.exports.createClient = function(opts) {
localStorage: global.localStorage,
});
opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler();
opts.cryptoStore = opts.cryptoStore || cryptoStoreFactory();
return new module.exports.MatrixClient(opts);
};
+39 -1
View File
@@ -246,6 +246,45 @@ RoomState.prototype.getUserIdsWithDisplayName = function(displayName) {
return this._displayNameToUserIds[displayName] || [];
};
/**
* Returns true if userId is in room, event is not redacted and either sender of
* mxEvent or has power level sufficient to redact events other than their own.
* @param {MatrixEvent} mxEvent The event to test permission for
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given used ID can redact given event
*/
RoomState.prototype.maySendRedactionForEvent = function(mxEvent, userId) {
const member = this.getMember(userId);
if (!member || member.membership === 'leave') return false;
if (mxEvent.status || mxEvent.isRedacted()) return false;
if (mxEvent.getSender() === userId) return true;
return this._hasSufficientPowerLevelFor('redact', member.powerLevel);
};
/**
* Returns true if the given power level is sufficient for action
* @param {string} action The type of power level to check
* @param {number} powerLevel The power level of the member
* @return {boolean} true if the given power level is sufficient
*/
RoomState.prototype._hasSufficientPowerLevelFor = function(action, powerLevel) {
const powerLevelsEvent = this.getStateEvents('m.room.power_levels', '');
let powerLevels = {};
if (powerLevelsEvent) {
powerLevels = powerLevelsEvent.getContent();
}
let requiredLevel = 50;
if (powerLevels[action] !== undefined) {
requiredLevel = powerLevels[action];
}
return powerLevel >= requiredLevel;
};
/**
* Short-form for maySendEvent('m.room.message', userId)
* @param {string} userId The user ID of the user to test permission for
@@ -269,7 +308,6 @@ RoomState.prototype.maySendEvent = function(eventType, userId) {
return this._maySendEventOfType(eventType, userId, false);
};
/**
* Returns true if the given MatrixClient has permission to send a state
* event of type `stateEventType` into this room.
+1 -19
View File
@@ -27,6 +27,7 @@ const ContentRepo = require("../content-repo");
const EventTimeline = require("./event-timeline");
const EventTimelineSet = require("./event-timeline-set");
import reEmit from '../reemit';
function synthesizeReceipt(userId, event, receiptType) {
// console.log("synthesizing receipt for "+event.getId());
@@ -1252,25 +1253,6 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
}
}
// FIXME: copypasted from sync.js
function reEmit(reEmitEntity, emittableEntity, eventNames) {
utils.forEach(eventNames, function(eventName) {
// setup a listener on the entity (the Room, User, etc) for this event
emittableEntity.on(eventName, function() {
// take the args from the listener and reuse them, adding the
// event name to the arg list so it works with .emit()
// Transformation Example:
// listener on "foo" => function(a,b) { ... }
// Re-emit on "thing" => thing.emit("foo", a, b)
const newArgs = [eventName];
for (let i = 0; i < arguments.length; i++) {
newArgs.push(arguments[i]);
}
reEmitEntity.emit(...newArgs);
});
});
}
/**
* The Room class.
*/
+44
View File
@@ -0,0 +1,44 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations 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.
*/
/**
* @module
*/
/**
* re-emit events raised by one EventEmitter from another
*
* @param {external:EventEmitter} reEmitEntity
* entity from which we want events to be emitted
* @param {external:EventEmitter} emittableEntity
* entity from which events are currently emitted
* @param {Array<string>} eventNames
* list of events to be reemitted
*/
export default function reEmit(reEmitEntity, emittableEntity, eventNames) {
for (const eventName of eventNames) {
// setup a listener on the entity (the Room, User, etc) for this event
emittableEntity.on(eventName, function(...args) {
// take the args from the listener and reuse them, adding the
// event name to the arg list so it works with .emit()
// Transformation Example:
// listener on "foo" => function(a,b) { ... }
// Re-emit on "thing" => thing.emit("foo", a, b)
reEmitEntity.emit(eventName, ...args);
});
}
}
+9
View File
@@ -99,6 +99,14 @@ WebStorageSessionStore.prototype = {
return getJsonItem(this.store, keyEndToEndDevicesForUser(userId));
},
storeEndToEndDeviceTrackingStatus: function(statusMap) {
setJsonItem(this.store, KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS, statusMap);
},
getEndToEndDeviceTrackingStatus: function() {
return getJsonItem(this.store, KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS);
},
/**
* Store the sync token corresponding to the device list.
*
@@ -202,6 +210,7 @@ WebStorageSessionStore.prototype = {
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
const KEY_END_TO_END_ANNOUNCED = E2E_PREFIX + "announced";
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";
function keyEndToEndDevicesForUser(userId) {
return E2E_PREFIX + "devices/" + userId;
+2 -18
View File
@@ -31,6 +31,8 @@ const utils = require("./utils");
const Filter = require("./filter");
const EventTimeline = require("./models/event-timeline");
import reEmit from './reemit';
const DEBUG = true;
// /sync requests allow you to set a timeout= but the request may continue
@@ -1252,23 +1254,5 @@ function createNewUser(client, userId) {
return user;
}
function reEmit(reEmitEntity, emittableEntity, eventNames) {
utils.forEach(eventNames, function(eventName) {
// setup a listener on the entity (the Room, User, etc) for this event
emittableEntity.on(eventName, function() {
// take the args from the listener and reuse them, adding the
// event name to the arg list so it works with .emit()
// Transformation Example:
// listener on "foo" => function(a,b) { ... }
// Re-emit on "thing" => thing.emit("foo", a, b)
const newArgs = [eventName];
for (let i = 0; i < arguments.length; i++) {
newArgs.push(arguments[i]);
}
reEmitEntity.emit(...newArgs);
});
});
}
/** */
module.exports = SyncApi;
+123 -64
View File
@@ -91,7 +91,7 @@ function MatrixCall(opts) {
utils.checkObjectHasKeys(server, ["urls"]);
});
this.callId = "c" + new Date().getTime();
this.callId = "c" + new Date().getTime() + Math.random();
this.state = 'fledgling';
this.didConnect = false;
@@ -155,7 +155,7 @@ MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoEle
/**
* Place a screen-sharing call to this room. This includes audio.
* <b>This method is EXPERIMENTAL and subject to change without warning. It
* only works in Google Chrome.</b>
* only works in Google Chrome and Firefox >= 44.</b>
* @param {Element} remoteVideoElement a <code>&lt;video&gt;</code> DOM element
* to render video to.
* @param {Element} localVideoElement a <code>&lt;video&gt;</code> DOM element
@@ -166,7 +166,7 @@ MatrixCall.prototype.placeScreenSharingCall =
function(remoteVideoElement, localVideoElement) {
debuglog("placeScreenSharingCall");
checkForErrorListener(this);
const screenConstraints = _getChromeScreenSharingConstraints(this);
const screenConstraints = _getScreenSharingConstraints(this);
if (!screenConstraints) {
return;
}
@@ -335,6 +335,7 @@ MatrixCall.prototype.setRemoteVideoElement = function(element) {
MatrixCall.prototype.setRemoteAudioElement = function(element) {
this.remoteVideoElement.muted = true;
this.remoteAudioElement = element;
this.remoteAudioElement.muted = false;
_tryPlayRemoteAudioStream(this);
};
@@ -409,12 +410,12 @@ MatrixCall.prototype.answer = function() {
if (!this.localAVStream && !this.waitForLocalAVStream) {
this.webRtc.getUserMedia(
_getUserMediaVideoContraints(this.type),
hookCallback(self, self._gotUserMediaForAnswer),
hookCallback(self, self._getUserMediaFailed),
hookCallback(self, self._maybeGotUserMediaForAnswer),
hookCallback(self, self._maybeGotUserMediaForAnswer),
);
setState(this, 'wait_local_media');
} else if (this.localAVStream) {
this._gotUserMediaForAnswer(this.localAVStream);
this._maybeGotUserMediaForAnswer(this.localAVStream);
} else if (this.waitForLocalAVStream) {
setState(this, 'wait_local_media');
}
@@ -433,11 +434,11 @@ MatrixCall.prototype._replacedBy = function(newCall) {
newCall.waitForLocalAVStream = true;
} else if (this.state == 'create_offer') {
debuglog("Handing local stream to new call");
newCall._gotUserMediaForAnswer(this.localAVStream);
newCall._maybeGotUserMediaForAnswer(this.localAVStream);
delete(this.localAVStream);
} else if (this.state == 'invite_sent') {
debuglog("Handing local stream to new call");
newCall._gotUserMediaForAnswer(this.localAVStream);
newCall._maybeGotUserMediaForAnswer(this.localAVStream);
delete(this.localAVStream);
}
newCall.localVideoElement = this.localVideoElement;
@@ -523,48 +524,69 @@ MatrixCall.prototype.isMicrophoneMuted = function() {
* @private
* @param {Object} stream
*/
MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
MatrixCall.prototype._maybeGotUserMediaForInvite = function(stream) {
if (this.successor) {
this.successor._gotUserMediaForAnswer(stream);
this.successor._maybeGotUserMediaForAnswer(stream);
return;
}
if (this.state == 'ended') {
return;
}
debuglog("_gotUserMediaForInvite -> " + this.type);
debuglog("_maybeGotUserMediaForInvite -> " + this.type);
const self = this;
const videoEl = this.getLocalVideoElement();
if (videoEl && this.type == 'video') {
videoEl.autoplay = true;
if (this.screenSharingStream) {
debuglog("Setting screen sharing stream to the local video element");
this.assignElement(videoEl, this.screenSharingStream, "localVideo");
} else {
this.assignElement(videoEl, stream, "localVideo");
}
videoEl.muted = true;
setTimeout(function() {
const vel = self.getLocalVideoElement();
if (vel.play) {
self.playElement(vel, "localVideo");
const error = stream;
const constraints = {
'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': self.type === 'video',
},
};
if (stream instanceof MediaStream) {
const videoEl = this.getLocalVideoElement();
if (videoEl && this.type == 'video') {
videoEl.autoplay = true;
if (this.screenSharingStream) {
debuglog("Setting screen sharing stream to the local video" +
" element");
this.assignElement(videoEl, this.screenSharingStream, "localVideo");
} else {
this.assignElement(videoEl, stream, "localVideo");
}
}, 0);
videoEl.muted = true;
setTimeout(function() {
const vel = self.getLocalVideoElement();
if (vel.play) {
self.playElement(vel, "localVideo");
}
}, 0);
}
if (this.screenSharingStream) {
this.screenSharingStream.addTrack(stream.getAudioTracks()[0]);
stream = this.screenSharingStream;
}
this.localAVStream = stream;
// why do we enable audio (and only audio) tracks here? -- matthew
setTracksEnabled(stream.getAudioTracks(), true);
this.peerConn = _createPeerConnection(this);
this.peerConn.addStream(stream);
} else if (error.name === 'PermissionDeniedError') {
debuglog('User denied access to camera/microphone.' +
' Or possibly you are using an insecure domain. Receiving only.');
this.peerConn = _createPeerConnection(this);
} else {
debuglog('Failed to getUserMedia.');
this._getUserMediaFailed(error);
return;
}
if (this.screenSharingStream) {
this.screenSharingStream.addTrack(stream.getAudioTracks()[0]);
stream = this.screenSharingStream;
}
this.localAVStream = stream;
// why do we enable audio (and only audio) tracks here? -- matthew
setTracksEnabled(stream.getAudioTracks(), true);
this.peerConn = _createPeerConnection(this);
this.peerConn.addStream(stream);
this.peerConn.createOffer(
hookCallback(self, self._gotLocalOffer),
hookCallback(self, self._getLocalOfferFailed),
constraints,
);
setState(self, 'create_offer');
};
@@ -574,33 +596,44 @@ MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
* @private
* @param {Object} stream
*/
MatrixCall.prototype._gotUserMediaForAnswer = function(stream) {
MatrixCall.prototype._maybeGotUserMediaForAnswer = function(stream) {
const self = this;
if (self.state == 'ended') {
return;
}
const localVidEl = self.getLocalVideoElement();
if (localVidEl && self.type == 'video') {
localVidEl.autoplay = true;
this.assignElement(localVidEl, stream, "localVideo");
localVidEl.muted = true;
setTimeout(function() {
const vel = self.getLocalVideoElement();
if (vel.play) {
self.playElement(vel, "localVideo");
}
}, 0);
const error = stream;
if (stream instanceof MediaStream) {
const localVidEl = self.getLocalVideoElement();
if (localVidEl && self.type == 'video') {
localVidEl.autoplay = true;
this.assignElement(localVidEl, stream, "localVideo");
localVidEl.muted = true;
setTimeout(function() {
const vel = self.getLocalVideoElement();
if (vel.play) {
self.playElement(vel, "localVideo");
}
}, 0);
}
self.localAVStream = stream;
setTracksEnabled(stream.getAudioTracks(), true);
self.peerConn.addStream(stream);
} else if (error.name === 'PermissionDeniedError') {
debuglog('User denied access to camera/microphone.' +
' Or possibly you are using an insecure domain. Receiving only.');
} else {
debuglog('Failed to getUserMedia.');
this._getUserMediaFailed(error);
return;
}
self.localAVStream = stream;
setTracksEnabled(stream.getAudioTracks(), true);
self.peerConn.addStream(stream);
const constraints = {
'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': self.type == 'video',
'OfferToReceiveVideo': self.type === 'video',
},
};
self.peerConn.createAnswer(function(description) {
@@ -1126,8 +1159,8 @@ const _placeCallWithConstraints = function(self, constraints) {
self.client.callList[self.callId] = self;
self.webRtc.getUserMedia(
constraints,
hookCallback(self, self._gotUserMediaForInvite),
hookCallback(self, self._getUserMediaFailed),
hookCallback(self, self._maybeGotUserMediaForInvite),
hookCallback(self, self._maybeGotUserMediaForInvite),
);
setState(self, 'wait_local_media');
self.direction = 'outbound';
@@ -1160,7 +1193,7 @@ const _createPeerConnection = function(self) {
return pc;
};
const _getChromeScreenSharingConstraints = function(call) {
const _getScreenSharingConstraints = function(call) {
const screen = global.screen;
if (!screen) {
call.emit("error", callError(
@@ -1172,6 +1205,7 @@ const _getChromeScreenSharingConstraints = function(call) {
return {
video: {
mediaSource: 'screen',
mandatory: {
chromeMediaSource: "screen",
chromeMediaSourceId: "" + Date.now(),
@@ -1187,16 +1221,25 @@ const _getChromeScreenSharingConstraints = function(call) {
const _getUserMediaVideoContraints = function(callType) {
switch (callType) {
case 'voice':
return ({audio: true, video: false});
return {
audio: {
deviceId: audioInput ? {exact: audioInput} : undefined,
}, video: false,
};
case 'video':
return ({audio: true, video: {
mandatory: {
minWidth: 640,
maxWidth: 640,
minHeight: 360,
maxHeight: 360,
return {
audio: {
deviceId: audioInput ? {exact: audioInput} : undefined,
}, video: {
deviceId: videoInput ? {exact: videoInput} : undefined,
mandatory: {
minWidth: 640,
maxWidth: 640,
minHeight: 360,
maxHeight: 360,
},
},
}});
};
}
};
@@ -1228,6 +1271,22 @@ const forAllTracksOnStream = function(s, f) {
/** The MatrixCall class. */
module.exports.MatrixCall = MatrixCall;
let audioInput;
let videoInput;
/**
* Set an audio input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setAudioInput = function(deviceId) { audioInput = deviceId; };
/**
* Set a video input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setVideoInput = function(deviceId) { videoInput = deviceId; };
/**
* Create a new Matrix call for the browser.