Compare commits
111 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9318a60e4 | |||
| 2fd569a61a | |||
| 34919d1b96 | |||
| cc08de9c64 | |||
| 6432a64442 | |||
| 1f18dabca0 | |||
| f74b49de4b | |||
| 6aeb265c19 | |||
| f10467e81f | |||
| 6001077c34 | |||
| ad6eec329d | |||
| d9867ba458 | |||
| 6dc7e624d3 | |||
| 24957a1445 | |||
| e2d67db5d4 | |||
| 6c59966339 | |||
| b4223d3790 | |||
| 9f6d9208f2 | |||
| 35d45f0280 | |||
| c288e6c7ec | |||
| bb946c65d1 | |||
| 13fe22bc86 | |||
| f139e6e6c2 | |||
| 364fe3ba47 | |||
| 8b9b37cc91 | |||
| 7895d1daa0 | |||
| 93a9f76f69 | |||
| 4e2edfc771 | |||
| f4d53e25cc | |||
| da324c020b | |||
| f63015e4c4 | |||
| 61cf53deee | |||
| 57ff963fae | |||
| 32744b23a0 | |||
| 6c25110682 | |||
| 188802c5d3 | |||
| a47e59f02c | |||
| 59d7935934 | |||
| ba616d2a25 | |||
| dc07038a27 | |||
| dd064ba0a1 | |||
| 3baea89c34 | |||
| 1412646a55 | |||
| c00a830cbb | |||
| fa28297add | |||
| 58a68106bc | |||
| ebd2ef6f95 | |||
| 809492d45d | |||
| 385c5d5469 | |||
| 9713ffedf2 | |||
| ecb31b5aaf | |||
| cee9a954ec | |||
| fc55858aa3 | |||
| b689dbb9c0 | |||
| 7dbc03942a | |||
| 425039c5b5 | |||
| 03d0aecc26 | |||
| abf903246c | |||
| 3fd601bda4 | |||
| 1896c0b62f | |||
| aa36571981 | |||
| abbe9d2bc7 | |||
| dff84c46f0 | |||
| 7614c6677c | |||
| 3d2a970457 | |||
| 50d60f5dd3 | |||
| 90c919e7e4 | |||
| 583ddc3e57 | |||
| 7ff1cf4e4a | |||
| 1556cc4479 | |||
| 32f7ca55df | |||
| ae3551ad78 | |||
| d0e90cd8c9 | |||
| cb6566a198 | |||
| 573a9140d3 | |||
| 6a02f39f0b | |||
| 7604bcc49a | |||
| 40c73e2079 | |||
| 7f113de790 | |||
| accb589892 | |||
| 4347f432b3 | |||
| 6d905563fc | |||
| e943bc46d8 | |||
| 7896b06bd7 | |||
| 0f153fdaf7 | |||
| 2e4a8f4fa5 | |||
| 53b9154fe2 | |||
| ce59b72308 | |||
| 4bf24f0fe4 | |||
| 23a38ae8f2 | |||
| 0ab3446d81 | |||
| adaf4bf92b | |||
| 60519c4e6b | |||
| 21a62f37de | |||
| 9feeb0c580 | |||
| 7c3104b2ae | |||
| 5ccbc0bbc6 | |||
| 9c47400a0c | |||
| 9cb032ec44 | |||
| 8c6e2591d9 | |||
| 0c8c7cf77a | |||
| 52edcc49c5 | |||
| 5eede573c4 | |||
| e9d60a252b | |||
| 17ec7daf23 | |||
| b18a4ee16b | |||
| f38f983a46 | |||
| e85c1e1231 | |||
| 1381a0226d | |||
| 4bec72a2bd | |||
| 11f02d2e24 |
@@ -1,3 +1,83 @@
|
||||
Changes in [0.5.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.5) (2016-08-11)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.4...v0.5.5)
|
||||
|
||||
* Add room.getAliases() and room.getCanonicalAlias
|
||||
* Add API calls `/register/email/requestToken`, `/account/password/email/requestToken` and `/account/3pid/email/requestToken`
|
||||
* Add `User.currentlyActive` and `User.lastPresenceTs` events for changes in fields on the User object
|
||||
* Add `logout` and `deactivateAccount`
|
||||
|
||||
* Make sure we actually stop the sync loop on logout
|
||||
[\#166](https://github.com/matrix-org/matrix-js-sdk/pull/166)
|
||||
* Zero is a valid power level
|
||||
[\#164](https://github.com/matrix-org/matrix-js-sdk/pull/164)
|
||||
* Verify e2e keys on download
|
||||
[\#163](https://github.com/matrix-org/matrix-js-sdk/pull/163)
|
||||
* Factor crypto stuff out of MatrixClient
|
||||
[\#162](https://github.com/matrix-org/matrix-js-sdk/pull/162)
|
||||
* Refactor device key upload
|
||||
[\#161](https://github.com/matrix-org/matrix-js-sdk/pull/161)
|
||||
* Wrappers for devices API
|
||||
[\#158](https://github.com/matrix-org/matrix-js-sdk/pull/158)
|
||||
* Add deactivate account function
|
||||
[\#160](https://github.com/matrix-org/matrix-js-sdk/pull/160)
|
||||
* client.listDeviceKeys: Expose device display name
|
||||
[\#159](https://github.com/matrix-org/matrix-js-sdk/pull/159)
|
||||
* Add `logout`
|
||||
[\#157](https://github.com/matrix-org/matrix-js-sdk/pull/157)
|
||||
* Fix email registration
|
||||
[\#156](https://github.com/matrix-org/matrix-js-sdk/pull/156)
|
||||
* Factor out MatrixClient methods to MatrixBaseApis
|
||||
[\#155](https://github.com/matrix-org/matrix-js-sdk/pull/155)
|
||||
* Fix some broken tests
|
||||
[\#154](https://github.com/matrix-org/matrix-js-sdk/pull/154)
|
||||
* make jenkins fail the build if the tests fail
|
||||
[\#153](https://github.com/matrix-org/matrix-js-sdk/pull/153)
|
||||
* deviceId-related fixes
|
||||
[\#152](https://github.com/matrix-org/matrix-js-sdk/pull/152)
|
||||
* /login, /register: Add device_id and initial_device_display_name
|
||||
[\#151](https://github.com/matrix-org/matrix-js-sdk/pull/151)
|
||||
* Support global account_data
|
||||
[\#150](https://github.com/matrix-org/matrix-js-sdk/pull/150)
|
||||
* Add more events to User
|
||||
[\#149](https://github.com/matrix-org/matrix-js-sdk/pull/149)
|
||||
* Add API calls for other requestToken endpoints
|
||||
[\#148](https://github.com/matrix-org/matrix-js-sdk/pull/148)
|
||||
* Add register-specific request token endpoint
|
||||
[\#147](https://github.com/matrix-org/matrix-js-sdk/pull/147)
|
||||
* Set a valid SPDX license identifier in package.json
|
||||
[\#139](https://github.com/matrix-org/matrix-js-sdk/pull/139)
|
||||
* Configure encryption on m.room.encryption events
|
||||
[\#145](https://github.com/matrix-org/matrix-js-sdk/pull/145)
|
||||
* Implement device blocking
|
||||
[\#146](https://github.com/matrix-org/matrix-js-sdk/pull/146)
|
||||
* Clearer doc for setRoomDirectoryVisibility
|
||||
[\#144](https://github.com/matrix-org/matrix-js-sdk/pull/144)
|
||||
* crypto: use memberlist to derive recipient list
|
||||
[\#143](https://github.com/matrix-org/matrix-js-sdk/pull/143)
|
||||
* Support for marking devices as unverified
|
||||
[\#142](https://github.com/matrix-org/matrix-js-sdk/pull/142)
|
||||
* Add Olm as an optionalDependency
|
||||
[\#141](https://github.com/matrix-org/matrix-js-sdk/pull/141)
|
||||
* Add room.getAliases() and room.getCanonicalAlias()
|
||||
[\#140](https://github.com/matrix-org/matrix-js-sdk/pull/140)
|
||||
* Change how MatrixEvent manages encrypted events
|
||||
[\#138](https://github.com/matrix-org/matrix-js-sdk/pull/138)
|
||||
* Catch exceptions when encrypting events
|
||||
[\#137](https://github.com/matrix-org/matrix-js-sdk/pull/137)
|
||||
* Support for marking devices as verified
|
||||
[\#136](https://github.com/matrix-org/matrix-js-sdk/pull/136)
|
||||
* Various matrix-client refactorings and fixes
|
||||
[\#134](https://github.com/matrix-org/matrix-js-sdk/pull/134)
|
||||
|
||||
Changes in [0.5.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.4) (2016-06-02)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.3...v0.5.4)
|
||||
|
||||
* Correct fix for https://github.com/vector-im/vector-web/issues/1039
|
||||
* Make release.sh work on OSX
|
||||
|
||||
|
||||
Changes in [0.5.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.3) (2016-06-02)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.2...v0.5.3)
|
||||
@@ -6,6 +86,10 @@ Changes in [0.5.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5
|
||||
[\#133](https://github.com/matrix-org/matrix-js-sdk/pull/133)
|
||||
* Bugfix for HTTP upload content when running on node
|
||||
[\#129](https://github.com/matrix-org/matrix-js-sdk/pull/129)
|
||||
* Ignore missing profile (displayname and avatar_url) fields on
|
||||
presence events, rather than overwriting existing valid profile
|
||||
data from membership events or elsewhere.
|
||||
Fixes https://github.com/vector-im/vector-web/issues/1039
|
||||
|
||||
Changes in [0.5.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.2) (2016-04-19)
|
||||
================================================================================================
|
||||
|
||||
Vendored
+37345
File diff suppressed because one or more lines are too long
+38
File diff suppressed because one or more lines are too long
+22
-4
@@ -1,13 +1,31 @@
|
||||
#!/bin/bash -l
|
||||
|
||||
export NVM_DIR="/home/jenkins/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||
nvm use 0.10
|
||||
npm install
|
||||
npm test
|
||||
jshint --reporter=checkstyle -c .jshint lib spec > jshint.xml || echo "jshint finished with return code $?"
|
||||
gjslint --unix_mode --disable 0131,0211,0200,0222,0212 --max_line_length 90 -r lib/ -r spec/ > gjslint.log || echo "gjslint finished with return code $?"
|
||||
|
||||
RC=0
|
||||
|
||||
function fail {
|
||||
echo $@ >&2
|
||||
RC=1
|
||||
}
|
||||
|
||||
npm test || fail "npm test finished with return code $?"
|
||||
|
||||
jshint --reporter=checkstyle -c .jshint lib spec > jshint.xml ||
|
||||
fail "jshint finished with return code $?"
|
||||
|
||||
gjslint --unix_mode --disable 0131,0211,0200,0222,0212 \
|
||||
--max_line_length 90 \
|
||||
-r lib/ -r spec/ > gjslint.log ||
|
||||
fail "gjslint finished with return code $?"
|
||||
|
||||
# delete the old tarball, if it exists
|
||||
rm -f matrix-js-sdk-*.tgz
|
||||
|
||||
npm pack
|
||||
npm pack ||
|
||||
fail "npm pack finished with return code $?"
|
||||
|
||||
exit $RC
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket 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.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
var Olm = require("olm");
|
||||
var utils = require("./utils");
|
||||
|
||||
/**
|
||||
* Manages the olm cryptography functions. Each OlmDevice has a single
|
||||
* OlmAccount and a number of OlmSessions.
|
||||
*
|
||||
* Accounts and sessions are kept pickled in a sessionStore.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} sessionStore A store to be used for data in end-to-end
|
||||
* crypto
|
||||
*
|
||||
* @property {string} deviceCurve25519Key Curve25519 key for the account
|
||||
* @property {string} deviceEd25519Key Ed25519 key for the account
|
||||
*/
|
||||
function OlmDevice(sessionStore) {
|
||||
this._sessionStore = sessionStore;
|
||||
this._pickleKey = "DEFAULT_KEY";
|
||||
|
||||
var e2eKeys;
|
||||
var account = new Olm.Account();
|
||||
try {
|
||||
var e2eAccount = this._sessionStore.getEndToEndAccount();
|
||||
if (e2eAccount === null) {
|
||||
account.create();
|
||||
var pickled = account.pickle(this._pickleKey);
|
||||
this._sessionStore.storeEndToEndAccount(pickled);
|
||||
} else {
|
||||
account.unpickle(this._pickleKey, e2eAccount);
|
||||
}
|
||||
e2eKeys = JSON.parse(account.identity_keys());
|
||||
} finally {
|
||||
account.free();
|
||||
}
|
||||
|
||||
this.deviceCurve25519Key = e2eKeys.curve25519;
|
||||
this.deviceEd25519Key = e2eKeys.ed25519;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* extract our OlmAccount from the session store and call the given function
|
||||
*
|
||||
* @param {function} func
|
||||
* @return {object} result of func
|
||||
* @private
|
||||
*/
|
||||
OlmDevice.prototype._getAccount = function(func) {
|
||||
var account = new Olm.Account();
|
||||
try {
|
||||
var pickledAccount = this._sessionStore.getEndToEndAccount();
|
||||
account.unpickle(this._pickleKey, pickledAccount);
|
||||
return func(account);
|
||||
} finally {
|
||||
account.free();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* store our OlmAccount in the session store
|
||||
*
|
||||
* @param {OlmAccount} account
|
||||
* @private
|
||||
*/
|
||||
OlmDevice.prototype._saveAccount = function(account) {
|
||||
var pickledAccount = account.pickle(this._pickleKey);
|
||||
this._sessionStore.storeEndToEndAccount(pickledAccount);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* extract an OlmSession from the session store and call the given function
|
||||
*
|
||||
* @param {string} deviceKey
|
||||
* @param {string} sessionId
|
||||
* @param {function} func
|
||||
* @return {object} result of func
|
||||
* @private
|
||||
*/
|
||||
OlmDevice.prototype._getSession = function(deviceKey, sessionId, func) {
|
||||
var sessions = this._sessionStore.getEndToEndSessions(deviceKey);
|
||||
var pickledSession = sessions[sessionId];
|
||||
|
||||
var session = new Olm.Session();
|
||||
try {
|
||||
session.unpickle(this._pickleKey, pickledSession);
|
||||
return func(session);
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* store our OlmSession in the session store
|
||||
*
|
||||
* @param {string} deviceKey
|
||||
* @param {OlmSession} session
|
||||
* @private
|
||||
*/
|
||||
OlmDevice.prototype._saveSession = function(deviceKey, session) {
|
||||
var pickledSession = session.pickle(this._pickleKey);
|
||||
this._sessionStore.storeEndToEndSession(
|
||||
deviceKey, session.session_id(), pickledSession
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* get an OlmUtility and call the given function
|
||||
*
|
||||
* @param {function} func
|
||||
* @return {object} result of func
|
||||
* @private
|
||||
*/
|
||||
OlmDevice.prototype._getUtility = function(func) {
|
||||
var utility = new Olm.Utility();
|
||||
try {
|
||||
return func(utility);
|
||||
} finally {
|
||||
utility.free();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Signs a message with the ed25519 key for this account.
|
||||
*
|
||||
* @param {string} message message to be signed
|
||||
* @return {string} base64-encoded signature
|
||||
*/
|
||||
OlmDevice.prototype.sign = function(message) {
|
||||
return this._getAccount(function(account) {
|
||||
return account.sign(message);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current (unused, unpublished) one-time keys for this account.
|
||||
*
|
||||
* @return {object} one time keys; an object with the single property
|
||||
* <tt>curve25519</tt>, which is itself an object mapping key id to Curve25519
|
||||
* key.
|
||||
*/
|
||||
OlmDevice.prototype.getOneTimeKeys = function() {
|
||||
return this._getAccount(function(account) {
|
||||
return JSON.parse(account.one_time_keys());
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the maximum number of one-time keys we can store.
|
||||
*
|
||||
* @return {number} number of keys
|
||||
*/
|
||||
OlmDevice.prototype.maxNumberOfOneTimeKeys = function() {
|
||||
return this._getAccount(function(account) {
|
||||
return account.max_number_of_one_time_keys();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Marks all of the one-time keys as published.
|
||||
*/
|
||||
OlmDevice.prototype.markKeysAsPublished = function() {
|
||||
var self = this;
|
||||
this._getAccount(function(account) {
|
||||
account.mark_keys_as_published();
|
||||
self._saveAccount(account);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate some new one-time keys
|
||||
*
|
||||
* @param {number} numKeys number of keys to generate
|
||||
*/
|
||||
OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
|
||||
var self = this;
|
||||
this._getAccount(function(account) {
|
||||
account.generate_one_time_keys(numKeys);
|
||||
self._saveAccount(account);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a new outbound session
|
||||
*
|
||||
* The new session will be stored in the sessionStore.
|
||||
*
|
||||
* @param {string} theirIdentityKey remote user's Curve25519 identity key
|
||||
* @param {string} theirOneTimeKey remote user's one-time Curve25519 key
|
||||
* @return {string} sessionId for the outbound session.
|
||||
*/
|
||||
OlmDevice.prototype.createOutboundSession = function(
|
||||
theirIdentityKey, theirOneTimeKey
|
||||
) {
|
||||
var self = this;
|
||||
return this._getAccount(function(account) {
|
||||
var session = new Olm.Session();
|
||||
try {
|
||||
session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
|
||||
self._saveSession(theirIdentityKey, session);
|
||||
return session.session_id();
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Generate a new inbound session, given an incoming message
|
||||
*
|
||||
* @param {string} theirDeviceIdentityKey remote user's Curve25519 identity key
|
||||
* @param {number} message_type message_type field from the received message (must be 0)
|
||||
* @param {string} ciphertext base64-encoded body from the received message
|
||||
*
|
||||
* @return {string} decrypted payload
|
||||
*
|
||||
* @raises {Error} if the received message was not valid (for instance, it
|
||||
* didn't use a valid one-time key).
|
||||
*/
|
||||
OlmDevice.prototype.createInboundSession = function(
|
||||
theirDeviceIdentityKey, message_type, ciphertext
|
||||
) {
|
||||
if (message_type !== 0) {
|
||||
throw new Error("Need message_type == 0 to create inbound session");
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return this._getAccount(function(account) {
|
||||
var session = new Olm.Session();
|
||||
try {
|
||||
session.create_inbound_from(account, theirDeviceIdentityKey, ciphertext);
|
||||
account.remove_one_time_keys(session);
|
||||
self._saveAccount(account);
|
||||
|
||||
var payloadString = session.decrypt(message_type, ciphertext);
|
||||
|
||||
self._saveSession(theirDeviceIdentityKey, session);
|
||||
|
||||
return payloadString;
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get a list of known session IDs for the given device
|
||||
*
|
||||
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
|
||||
* remote device
|
||||
* @return {string[]} a list of known session ids for the device
|
||||
*/
|
||||
OlmDevice.prototype.getSessionIdsForDevice = function(theirDeviceIdentityKey) {
|
||||
var sessions = this._sessionStore.getEndToEndSessions(
|
||||
theirDeviceIdentityKey
|
||||
);
|
||||
return utils.keys(sessions);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt an outgoing message using an existing session
|
||||
*
|
||||
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
|
||||
* remote device
|
||||
* @param {string} sessionId the id of the active session
|
||||
* @param {string} payloadString payload to be encrypted and sent
|
||||
*
|
||||
* @return {string} ciphertext
|
||||
*/
|
||||
OlmDevice.prototype.encryptMessage = function(
|
||||
theirDeviceIdentityKey, sessionId, payloadString
|
||||
) {
|
||||
var self = this;
|
||||
|
||||
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
|
||||
var res = session.encrypt(payloadString);
|
||||
self._saveSession(theirDeviceIdentityKey, session);
|
||||
return res;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Decrypt an incoming message using an existing session
|
||||
*
|
||||
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
|
||||
* remote device
|
||||
* @param {string} sessionId the id of the active session
|
||||
* @param {number} message_type message_type field from the received message
|
||||
* @param {string} ciphertext base64-encoded body from the received message
|
||||
*
|
||||
* @return {object} Result, with keys <ul>
|
||||
* <li><tt>matchesInbound</tt>: (boolean): true if the message was a pre-key
|
||||
* message which matched an existing inbound session.</li>
|
||||
* <li><tt>payload</tt>: (string): decrypted payload; null if the received
|
||||
* message was not valid (for instance, it did not match this session).</li>
|
||||
* </ul>
|
||||
*/
|
||||
OlmDevice.prototype.decryptMessage = function(
|
||||
theirDeviceIdentityKey, sessionId, message_type, ciphertext
|
||||
) {
|
||||
var self = this;
|
||||
|
||||
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
|
||||
var matchesInbound = (message_type === 0 &&
|
||||
session.matches_inbound(ciphertext));
|
||||
|
||||
var payloadString = null;
|
||||
try {
|
||||
payloadString = session.decrypt(message_type, ciphertext);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
"Failed to decrypt with an existing session: " + e.message
|
||||
);
|
||||
// return null as the payload
|
||||
}
|
||||
|
||||
self._saveSession(theirDeviceIdentityKey, session);
|
||||
|
||||
return {
|
||||
matchesInbound: matchesInbound,
|
||||
payload: payloadString,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Verify an ed25519 signature.
|
||||
*
|
||||
* @param {string} key ed25519 key
|
||||
* @param {string} message message which was signed
|
||||
* @param {string} signature base64-encoded signature to be checked
|
||||
*
|
||||
* @raises {Error} if there is a problem with the verification. If the key was
|
||||
* too small then the message will be "OLM.INVALID_BASE64". If the signature
|
||||
* was invalid then the message will be "OLM.BAD_MESSAGE_MAC".
|
||||
*/
|
||||
OlmDevice.prototype.verifySignature = function(
|
||||
key, message, signature
|
||||
) {
|
||||
this._getUtility(function(util) {
|
||||
util.ed25519_verify(key, message, signature);
|
||||
});
|
||||
};
|
||||
|
||||
/** */
|
||||
module.exports = OlmDevice;
|
||||
@@ -0,0 +1,846 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket 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.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* This is an internal module. MatrixBaseApis is currently only meant to be used
|
||||
* by {@link client~MatrixClient}.
|
||||
*
|
||||
* @module base-apis
|
||||
*/
|
||||
|
||||
var httpApi = require("./http-api");
|
||||
var utils = require("./utils");
|
||||
|
||||
/**
|
||||
* Low-level wrappers for the Matrix APIs
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {Object} opts Configuration options
|
||||
*
|
||||
* @param {string} opts.baseUrl Required. The base URL to the client-server
|
||||
* HTTP API.
|
||||
*
|
||||
* @param {string} opts.idBaseUrl Optional. The base identity server URL for
|
||||
* identity server requests.
|
||||
*
|
||||
* @param {Function} opts.request Required. The function to invoke for HTTP
|
||||
* requests. The value of this property is typically <code>require("request")
|
||||
* </code> as it returns a function which meets the required interface. See
|
||||
* {@link requestFunction} for more information.
|
||||
*
|
||||
* @param {string} opts.accessToken The access_token for this user.
|
||||
*
|
||||
* @param {Object} opts.queryParams Optional. Extra query parameters to append
|
||||
* to all requests with this client. Useful for application services which require
|
||||
* <code>?user_id=</code>.
|
||||
*
|
||||
*/
|
||||
function MatrixBaseApis(opts) {
|
||||
utils.checkObjectHasKeys(opts, ["baseUrl", "request"]);
|
||||
|
||||
this.baseUrl = opts.baseUrl;
|
||||
this.idBaseUrl = opts.idBaseUrl;
|
||||
|
||||
var httpOpts = {
|
||||
baseUrl: opts.baseUrl,
|
||||
idBaseUrl: opts.idBaseUrl,
|
||||
accessToken: opts.accessToken,
|
||||
request: opts.request,
|
||||
prefix: httpApi.PREFIX_R0,
|
||||
onlyData: true,
|
||||
extraParams: opts.queryParams
|
||||
};
|
||||
this._http = new httpApi.MatrixHttpApi(this, httpOpts);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the Homeserver URL of this client
|
||||
* @return {string} Homeserver URL of this client
|
||||
*/
|
||||
MatrixBaseApis.prototype.getHomeserverUrl = function() {
|
||||
return this.baseUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the Identity Server URL of this client
|
||||
* @return {string} Identity Server URL of this client
|
||||
*/
|
||||
MatrixBaseApis.prototype.getIdentityServerUrl = function() {
|
||||
return this.idBaseUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the access token associated with this account.
|
||||
* @return {?String} The access_token or null
|
||||
*/
|
||||
MatrixBaseApis.prototype.getAccessToken = function() {
|
||||
return this._http.opts.accessToken || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {boolean} true if there is a valid access_token for this client.
|
||||
*/
|
||||
MatrixBaseApis.prototype.isLoggedIn = function() {
|
||||
return this._http.opts.accessToken !== undefined;
|
||||
};
|
||||
|
||||
|
||||
// Registration/Login operations
|
||||
// =============================
|
||||
|
||||
/**
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.loginFlows = function(callback) {
|
||||
return this._http.request(callback, "GET", "/login");
|
||||
};
|
||||
|
||||
/**
|
||||
* Logs out the current session.
|
||||
* Obviously, further calls that require authorisation should fail after this
|
||||
* method is called. The state of the MatrixClient object is not affected:
|
||||
* it is up to the caller to either reset or destroy the MatrixClient after
|
||||
* this method succeeds.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: On success, the empty object
|
||||
*/
|
||||
MatrixBaseApis.prototype.logout = function(callback) {
|
||||
return this._http.authedRequest(
|
||||
callback, "POST", '/logout'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deactivates the logged-in account.
|
||||
* Obviously, further calls that require authorisation should fail after this
|
||||
* method is called. The state of the MatrixClient object is not affected:
|
||||
* it is up to the caller to either reset or destroy the MatrixClient after
|
||||
* this method succeeds.
|
||||
* @param {object} auth Optional. Auth data to supply for User-Interactive auth.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: On success, the empty object
|
||||
*/
|
||||
MatrixBaseApis.prototype.deactivateAccount = function(auth, callback) {
|
||||
var body = {};
|
||||
if (auth) {
|
||||
body = {
|
||||
auth: auth,
|
||||
};
|
||||
}
|
||||
return this._http.authedRequestWithPrefix(
|
||||
callback, "POST", '/account/deactivate', undefined, body, httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Room operations
|
||||
// ===============
|
||||
|
||||
/**
|
||||
* Create a new room.
|
||||
* @param {Object} options a list of options to pass to the /createRoom API.
|
||||
* @param {string} options.room_alias_name The alias localpart to assign to
|
||||
* this room.
|
||||
* @param {string} options.visibility Either 'public' or 'private'.
|
||||
* @param {string[]} options.invite A list of user IDs to invite to this room.
|
||||
* @param {string} options.name The name to give this room.
|
||||
* @param {string} options.topic The topic to give this room.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: <code>{room_id: {string},
|
||||
* room_alias: {string(opt)}}</code>
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.createRoom = function(options, callback) {
|
||||
// valid options include: room_alias_name, visibility, invite
|
||||
return this._http.authedRequest(
|
||||
callback, "POST", "/createRoom", undefined, options
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.roomState = function(roomId, callback) {
|
||||
var path = utils.encodeUri("/rooms/$roomId/state", {$roomId: roomId});
|
||||
return this._http.authedRequest(callback, "GET", path);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve a state event.
|
||||
* @param {string} roomId
|
||||
* @param {string} eventType
|
||||
* @param {string} stateKey
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getStateEvent = function(roomId, eventType, stateKey, callback) {
|
||||
var pathParams = {
|
||||
$roomId: roomId,
|
||||
$eventType: eventType,
|
||||
$stateKey: stateKey
|
||||
};
|
||||
var path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
|
||||
if (stateKey !== undefined) {
|
||||
path = utils.encodeUri(path + "/$stateKey", pathParams);
|
||||
}
|
||||
return this._http.authedRequest(
|
||||
callback, "GET", path
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {string} eventType
|
||||
* @param {Object} content
|
||||
* @param {string} stateKey
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.sendStateEvent = function(roomId, eventType, content, stateKey,
|
||||
callback) {
|
||||
var pathParams = {
|
||||
$roomId: roomId,
|
||||
$eventType: eventType,
|
||||
$stateKey: stateKey
|
||||
};
|
||||
var path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
|
||||
if (stateKey !== undefined) {
|
||||
path = utils.encodeUri(path + "/$stateKey", pathParams);
|
||||
}
|
||||
return this._http.authedRequest(
|
||||
callback, "PUT", path, undefined, content
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {string} eventId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.redactEvent = function(roomId, eventId, callback) {
|
||||
var path = utils.encodeUri("/rooms/$roomId/redact/$eventId", {
|
||||
$roomId: roomId,
|
||||
$eventId: eventId
|
||||
});
|
||||
return this._http.authedRequest(callback, "POST", path, undefined, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {Number} limit
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) {
|
||||
if (utils.isFunction(limit)) { callback = limit; limit = undefined; }
|
||||
var path = utils.encodeUri("/rooms/$roomId/initialSync",
|
||||
{$roomId: roomId}
|
||||
);
|
||||
if (!limit) {
|
||||
limit = 30;
|
||||
}
|
||||
return this._http.authedRequest(
|
||||
callback, "GET", path, { limit: limit }
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Room Directory operations
|
||||
// =========================
|
||||
|
||||
/**
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.publicRooms = function(callback) {
|
||||
return this._http.authedRequest(callback, "GET", "/publicRooms");
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an alias to room ID mapping.
|
||||
* @param {string} alias The room alias to create.
|
||||
* @param {string} roomId The room ID to link the alias to.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.createAlias = function(alias, roomId, callback) {
|
||||
var path = utils.encodeUri("/directory/room/$alias", {
|
||||
$alias: alias
|
||||
});
|
||||
var data = {
|
||||
room_id: roomId
|
||||
};
|
||||
return this._http.authedRequest(
|
||||
callback, "PUT", path, undefined, data
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete an alias to room ID mapping. This alias must be on your local server
|
||||
* and you must have sufficient access to do this operation.
|
||||
* @param {string} alias The room alias to delete.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.deleteAlias = function(alias, callback) {
|
||||
var path = utils.encodeUri("/directory/room/$alias", {
|
||||
$alias: alias
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
callback, "DELETE", path, undefined, undefined
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get room info for the given alias.
|
||||
* @param {string} alias The room alias to resolve.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: Object with room_id and servers.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getRoomIdForAlias = function(alias, callback) {
|
||||
// TODO: deprecate this or resolveRoomAlias
|
||||
var path = utils.encodeUri("/directory/room/$alias", {
|
||||
$alias: alias
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
callback, "GET", path
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} roomAlias
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.resolveRoomAlias = function(roomAlias, callback) {
|
||||
// TODO: deprecate this or getRoomIdForAlias
|
||||
var path = utils.encodeUri("/directory/room/$alias", {$alias: roomAlias});
|
||||
return this._http.request(callback, "GET", path);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the visibility of a room in the current HS's room directory
|
||||
* @param {string} roomId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getRoomDirectoryVisibility =
|
||||
function(roomId, callback) {
|
||||
var path = utils.encodeUri("/directory/list/room/$roomId", {
|
||||
$roomId: roomId
|
||||
});
|
||||
return this._http.authedRequest(callback, "GET", path);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the visbility of a room in the current HS's room directory
|
||||
* @param {string} roomId
|
||||
* @param {string} visibility "public" to make the room visible
|
||||
* in the public directory, or "private" to make
|
||||
* it invisible.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setRoomDirectoryVisibility =
|
||||
function(roomId, visibility, callback) {
|
||||
var path = utils.encodeUri("/directory/list/room/$roomId", {
|
||||
$roomId: roomId
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
callback, "PUT", path, undefined, { "visibility": visibility }
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Media operations
|
||||
// ================
|
||||
|
||||
/**
|
||||
* Upload a file to the media repository on the home server.
|
||||
* @param {File} file object
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.uploadContent = function(file, callback) {
|
||||
return this._http.uploadContent(file, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel a file upload in progress
|
||||
* @param {module:client.Promise} promise The promise returned from uploadContent
|
||||
* @return {boolean} true if canceled, otherwise false
|
||||
*/
|
||||
MatrixBaseApis.prototype.cancelUpload = function(promise) {
|
||||
return this._http.cancelUpload(promise);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of all file uploads in progress
|
||||
* @return {array} Array of objects representing current uploads.
|
||||
* Currently in progress is element 0. Keys:
|
||||
* - promise: The promise associated with the upload
|
||||
* - loaded: Number of bytes uploaded
|
||||
* - total: Total number of bytes to upload
|
||||
*/
|
||||
MatrixBaseApis.prototype.getCurrentUploads = function() {
|
||||
return this._http.getCurrentUploads();
|
||||
};
|
||||
|
||||
|
||||
// Profile operations
|
||||
// ==================
|
||||
|
||||
/**
|
||||
* @param {string} userId
|
||||
* @param {string} info The kind of info to retrieve (e.g. 'displayname',
|
||||
* 'avatar_url').
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getProfileInfo = function(userId, info, callback) {
|
||||
if (utils.isFunction(info)) { callback = info; info = undefined; }
|
||||
|
||||
var path = info ?
|
||||
utils.encodeUri("/profile/$userId/$info",
|
||||
{ $userId: userId, $info: info }) :
|
||||
utils.encodeUri("/profile/$userId",
|
||||
{ $userId: userId });
|
||||
return this._http.authedRequest(callback, "GET", path);
|
||||
};
|
||||
|
||||
|
||||
// Account operations
|
||||
// ==================
|
||||
|
||||
/**
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getThreePids = function(callback) {
|
||||
var path = "/account/3pid";
|
||||
return this._http.authedRequest(
|
||||
callback, "GET", path, undefined, undefined
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} creds
|
||||
* @param {boolean} bind
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) {
|
||||
var path = "/account/3pid";
|
||||
var data = {
|
||||
'threePidCreds': creds,
|
||||
'bind': bind
|
||||
};
|
||||
return this._http.authedRequest(
|
||||
callback, "POST", path, null, data
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Make a request to change your password.
|
||||
* @param {Object} authDict
|
||||
* @param {string} newPassword The new desired password.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setPassword = function(authDict, newPassword, callback) {
|
||||
var path = "/account/password";
|
||||
var data = {
|
||||
'auth': authDict,
|
||||
'new_password': newPassword
|
||||
};
|
||||
|
||||
return this._http.authedRequest(
|
||||
callback, "POST", path, null, data
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Device operations
|
||||
// =================
|
||||
|
||||
/**
|
||||
* Gets all devices recorded for the logged-in user
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getDevices = function() {
|
||||
var path = "/devices";
|
||||
return this._http.authedRequestWithPrefix(
|
||||
undefined, "GET", path, undefined, undefined,
|
||||
httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the given device
|
||||
*
|
||||
* @param {string} device_id device to update
|
||||
* @param {Object} body body of request
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setDeviceDetails = function(device_id, body) {
|
||||
var path = utils.encodeUri("/devices/$device_id", {
|
||||
$device_id: device_id,
|
||||
});
|
||||
|
||||
|
||||
return this._http.authedRequestWithPrefix(
|
||||
undefined, "PUT", path, undefined, body,
|
||||
httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete the given device
|
||||
*
|
||||
* @param {string} device_id device to delete
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.deleteDevice = function(device_id) {
|
||||
var path = utils.encodeUri("/devices/$device_id", {
|
||||
$device_id: device_id,
|
||||
});
|
||||
|
||||
return this._http.authedRequestWithPrefix(
|
||||
undefined, "DELETE", path, undefined, undefined,
|
||||
httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Push operations
|
||||
// ===============
|
||||
|
||||
/**
|
||||
* Gets all pushers registered for the logged-in user
|
||||
*
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: Array of objects representing pushers
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getPushers = function(callback) {
|
||||
var path = "/pushers";
|
||||
return this._http.authedRequest(
|
||||
callback, "GET", path, undefined, undefined
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a new pusher or updates an existing pusher
|
||||
*
|
||||
* @param {Object} pusher Object representing a pusher
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: Empty json object on success
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setPusher = function(pusher, callback) {
|
||||
var path = "/pushers/set";
|
||||
return this._http.authedRequest(
|
||||
callback, "POST", path, null, pusher
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getPushRules = function(callback) {
|
||||
return this._http.authedRequest(callback, "GET", "/pushrules/");
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} scope
|
||||
* @param {string} kind
|
||||
* @param {string} ruleId
|
||||
* @param {Object} body
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.addPushRule = function(scope, kind, ruleId, body, callback) {
|
||||
// NB. Scope not uri encoded because devices need the '/'
|
||||
var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
|
||||
$kind: kind,
|
||||
$ruleId: ruleId
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
callback, "PUT", path, undefined, body
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} scope
|
||||
* @param {string} kind
|
||||
* @param {string} ruleId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.deletePushRule = function(scope, kind, ruleId, callback) {
|
||||
// NB. Scope not uri encoded because devices need the '/'
|
||||
var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
|
||||
$kind: kind,
|
||||
$ruleId: ruleId
|
||||
});
|
||||
return this._http.authedRequest(callback, "DELETE", path);
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable or disable a push notification rule.
|
||||
* @param {string} scope
|
||||
* @param {string} kind
|
||||
* @param {string} ruleId
|
||||
* @param {boolean} enabled
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setPushRuleEnabled = function(scope, kind,
|
||||
ruleId, enabled, callback) {
|
||||
var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/enabled", {
|
||||
$kind: kind,
|
||||
$ruleId: ruleId
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
callback, "PUT", path, undefined, {"enabled": enabled}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the actions for a push notification rule.
|
||||
* @param {string} scope
|
||||
* @param {string} kind
|
||||
* @param {string} ruleId
|
||||
* @param {array} actions
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setPushRuleActions = function(scope, kind,
|
||||
ruleId, actions, callback) {
|
||||
var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/actions", {
|
||||
$kind: kind,
|
||||
$ruleId: ruleId
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
callback, "PUT", path, undefined, {"actions": actions}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Search
|
||||
// ======
|
||||
|
||||
/**
|
||||
* Perform a server-side search.
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.next_batch the batch token to pass in the query string
|
||||
* @param {Object} opts.body the JSON object to pass to the request body.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.search = function(opts, callback) {
|
||||
var queryparams = {};
|
||||
if (opts.next_batch) {
|
||||
queryparams.next_batch = opts.next_batch;
|
||||
}
|
||||
return this._http.authedRequest(
|
||||
callback, "POST", "/search", queryparams, opts.body
|
||||
);
|
||||
};
|
||||
|
||||
// Crypto
|
||||
// ======
|
||||
|
||||
/**
|
||||
* Upload keys
|
||||
*
|
||||
* @param {Object} content body of upload request
|
||||
*
|
||||
* @param {Object=} opts
|
||||
*
|
||||
* @param {string=} opts.device_id explicit device_id to use for upload
|
||||
* (default is to use the same as that used during auth).
|
||||
*
|
||||
* @param {module:client.callback=} callback
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: result object. Rejects: with
|
||||
* an error response ({@link module:http-api.MatrixError}).
|
||||
*/
|
||||
MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) {
|
||||
opts = opts || {};
|
||||
var deviceId = opts.device_id;
|
||||
var path;
|
||||
if (deviceId) {
|
||||
path = utils.encodeUri("/keys/upload/$deviceId", {
|
||||
$deviceId: deviceId,
|
||||
});
|
||||
} else {
|
||||
path = "/keys/upload";
|
||||
}
|
||||
return this._http.authedRequestWithPrefix(
|
||||
callback, "POST", path, undefined, content, httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Download device keys
|
||||
*
|
||||
* @param {string[]} userIds list of users to get keys for
|
||||
*
|
||||
* @param {module:client.callback=} callback
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: result object. Rejects: with
|
||||
* an error response ({@link module:http-api.MatrixError}).
|
||||
*/
|
||||
MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, callback) {
|
||||
var downloadQuery = {};
|
||||
|
||||
for (var i = 0; i < userIds.length; ++i) {
|
||||
downloadQuery[userIds[i]] = {};
|
||||
}
|
||||
var content = {device_keys: downloadQuery};
|
||||
return this._http.authedRequestWithPrefix(
|
||||
callback, "POST", "/keys/query", undefined, content,
|
||||
httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Claim one-time keys
|
||||
*
|
||||
* @param {string[][]} devices a list of [userId, deviceId] pairs
|
||||
*
|
||||
* @param {module:client.callback=} callback
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: result object. Rejects: with
|
||||
* an error response ({@link module:http-api.MatrixError}).
|
||||
*/
|
||||
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, callback) {
|
||||
var queries = {};
|
||||
|
||||
for (var i = 0; i < devices.length; ++i) {
|
||||
var userId = devices[i][0];
|
||||
var deviceId = devices[i][1];
|
||||
var query = queries[userId] || {};
|
||||
queries[userId] = query;
|
||||
query[deviceId] = "curve25519";
|
||||
}
|
||||
var content = {one_time_keys: queries};
|
||||
return this._http.authedRequestWithPrefix(
|
||||
callback, "POST", "/keys/claim", undefined, content,
|
||||
httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Identity Server Operations
|
||||
// ==========================
|
||||
|
||||
/**
|
||||
* Requests an email verification token directly from an Identity Server.
|
||||
*
|
||||
* Note that the Home Server offers APIs to proxy this API for specific
|
||||
* situations, allowing for better feedback to the user.
|
||||
*
|
||||
* @param {string} email The email address to request a token for
|
||||
* @param {string} clientSecret A secret binary string generated by the client.
|
||||
* It is recommended this be around 16 ASCII characters.
|
||||
* @param {number} sendAttempt If an identity server sees a duplicate request
|
||||
* with the same sendAttempt, it will not send another email.
|
||||
* To request another email to be sent, use a larger value for
|
||||
* the sendAttempt param as was used in the previous request.
|
||||
* @param {string} nextLink Optional If specified, the client will be redirected
|
||||
* to this link after validation.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @throws Error if No ID server is set
|
||||
*/
|
||||
MatrixBaseApis.prototype.requestEmailToken = function(email, clientSecret,
|
||||
sendAttempt, nextLink, callback) {
|
||||
var params = {
|
||||
client_secret: clientSecret,
|
||||
email: email,
|
||||
send_attempt: sendAttempt,
|
||||
next_link: nextLink
|
||||
};
|
||||
return this._http.idServerRequest(
|
||||
callback, "POST", "/validate/email/requestToken",
|
||||
params, httpApi.PREFIX_IDENTITY_V1
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Looks up the public Matrix ID mapping for a given 3rd party
|
||||
* identifier from the Identity Server
|
||||
* @param {string} medium The medium of the threepid, eg. 'email'
|
||||
* @param {string} address The textual address of the threepid
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: A threepid mapping
|
||||
* object or the empty object if no mapping
|
||||
* exists
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.lookupThreePid = function(medium, address, callback) {
|
||||
var params = {
|
||||
medium: medium,
|
||||
address: address,
|
||||
};
|
||||
return this._http.idServerRequest(
|
||||
callback, "GET", "/lookup",
|
||||
params, httpApi.PREFIX_IDENTITY_V1
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* MatrixBaseApis object
|
||||
*/
|
||||
module.exports = MatrixBaseApis;
|
||||
+476
-1107
File diff suppressed because it is too large
Load Diff
+806
@@ -0,0 +1,806 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket 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.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Internal module
|
||||
*
|
||||
* @module crypto
|
||||
*/
|
||||
|
||||
var anotherjson = require('another-json');
|
||||
var q = require("q");
|
||||
|
||||
var utils = require("./utils");
|
||||
var OlmDevice = require("./OlmDevice");
|
||||
|
||||
var OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
|
||||
|
||||
var DeviceVerification = {
|
||||
VERIFIED: 1,
|
||||
UNVERIFIED: 0,
|
||||
BLOCKED: -1,
|
||||
};
|
||||
|
||||
/**
|
||||
* Stored information about a user's device
|
||||
*
|
||||
* @typedef {Object} DeviceInfo
|
||||
*
|
||||
* @property {string[]} altorithms list of algorithms supported by this device
|
||||
*
|
||||
* @property {Object} keys a map from <key type>:<id> -> key
|
||||
*
|
||||
* @property {DeviceVerification} verified whether the device has been
|
||||
* verified by the user
|
||||
*
|
||||
* @property {Object} unsigned additional data from the homeserver
|
||||
*/
|
||||
|
||||
/**
|
||||
* Cryptography bits
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
|
||||
*
|
||||
* @param {module:store/session/webstorage~WebStorageSessionStore} sessionStore
|
||||
* Store to be used for end-to-end crypto session data
|
||||
*
|
||||
* @param {string} userId The user ID for the local user
|
||||
*
|
||||
* @param {string} deviceId The identifier for this device.
|
||||
*/
|
||||
function Crypto(baseApis, sessionStore, userId, deviceId) {
|
||||
this._baseApis = baseApis;
|
||||
this._sessionStore = sessionStore;
|
||||
this._userId = userId;
|
||||
this._deviceId = deviceId;
|
||||
|
||||
this._cryptoAlgorithms = [];
|
||||
|
||||
this._olmDevice = new OlmDevice(sessionStore);
|
||||
this._cryptoAlgorithms = [OLM_ALGORITHM];
|
||||
|
||||
// build our device keys: these will later be uploaded
|
||||
this._deviceKeys = {};
|
||||
this._deviceKeys["ed25519:" + this._deviceId] =
|
||||
this._olmDevice.deviceEd25519Key;
|
||||
this._deviceKeys["curve25519:" + this._deviceId] =
|
||||
this._olmDevice.deviceCurve25519Key;
|
||||
|
||||
// add our own deviceinfo to the sessionstore
|
||||
var deviceInfo = {
|
||||
keys: this._deviceKeys,
|
||||
algorithms: this._cryptoAlgorithms,
|
||||
verified: DeviceVerification.VERIFIED,
|
||||
};
|
||||
var myDevices = this._sessionStore.getEndToEndDevicesForUser(
|
||||
this._userId
|
||||
) || {};
|
||||
myDevices[this._deviceId] = deviceInfo;
|
||||
this._sessionStore.storeEndToEndDevicesForUser(
|
||||
this._userId, myDevices
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Ed25519 key for this device
|
||||
*
|
||||
* @return {string} base64-encoded ed25519 key.
|
||||
*/
|
||||
Crypto.prototype.getDeviceEd25519Key = function() {
|
||||
return this._olmDevice.deviceEd25519Key;
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload the device keys to the homeserver and ensure that the
|
||||
* homeserver has enough one-time keys.
|
||||
* @param {number} maxKeys The maximum number of keys to generate
|
||||
* @return {object} A promise that will resolve when the keys are uploaded.
|
||||
*/
|
||||
Crypto.prototype.uploadKeys = function(maxKeys) {
|
||||
var self = this;
|
||||
return _uploadDeviceKeys(this).then(function(res) {
|
||||
var keyCount = res.one_time_key_counts.curve25519 || 0;
|
||||
var maxOneTimeKeys = self._olmDevice.maxNumberOfOneTimeKeys();
|
||||
var keyLimit = Math.floor(maxOneTimeKeys / 2);
|
||||
var numberToGenerate = Math.max(keyLimit - keyCount, 0);
|
||||
if (maxKeys !== undefined) {
|
||||
numberToGenerate = Math.min(numberToGenerate, maxKeys);
|
||||
}
|
||||
|
||||
if (numberToGenerate <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._olmDevice.generateOneTimeKeys(numberToGenerate);
|
||||
return _uploadOneTimeKeys(self);
|
||||
});
|
||||
};
|
||||
|
||||
// returns a promise which resolves to the response
|
||||
function _uploadDeviceKeys(crypto) {
|
||||
var userId = crypto._userId;
|
||||
var deviceId = crypto._deviceId;
|
||||
|
||||
var deviceKeys = {
|
||||
algorithms: crypto._cryptoAlgorithms,
|
||||
device_id: deviceId,
|
||||
keys: crypto._deviceKeys,
|
||||
user_id: userId,
|
||||
};
|
||||
|
||||
var sig = crypto._olmDevice.sign(anotherjson.stringify(deviceKeys));
|
||||
deviceKeys.signatures = {};
|
||||
deviceKeys.signatures[userId] = {};
|
||||
deviceKeys.signatures[userId]["ed25519:" + deviceId] = sig;
|
||||
|
||||
return crypto._baseApis.uploadKeysRequest({
|
||||
device_keys: deviceKeys,
|
||||
}, {
|
||||
// for now, we set the device id explicitly, as we may not be using the
|
||||
// same one as used in login.
|
||||
device_id: deviceId,
|
||||
});
|
||||
}
|
||||
|
||||
// returns a promise which resolves to the response
|
||||
function _uploadOneTimeKeys(crypto) {
|
||||
var oneTimeKeys = crypto._olmDevice.getOneTimeKeys();
|
||||
var oneTimeJson = {};
|
||||
|
||||
for (var keyId in oneTimeKeys.curve25519) {
|
||||
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
|
||||
oneTimeJson["curve25519:" + keyId] = oneTimeKeys.curve25519[keyId];
|
||||
}
|
||||
}
|
||||
return crypto._baseApis.uploadKeysRequest({
|
||||
one_time_keys: oneTimeJson
|
||||
}, {
|
||||
// for now, we set the device id explicitly, as we may not be using the
|
||||
// same one as used in login.
|
||||
device_id: crypto._deviceId,
|
||||
}).then(function(res) {
|
||||
crypto._olmDevice.markKeysAsPublished();
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the keys for a list of users and stores the keys in the session
|
||||
* store.
|
||||
* @param {Array} userIds The users to fetch.
|
||||
* @param {bool} forceDownload Always download the keys even if cached.
|
||||
*
|
||||
* @return {Promise} A promise which resolves to a map userId->deviceId->{@link
|
||||
* module:crypto~DeviceInfo|DeviceInfo}.
|
||||
*/
|
||||
Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
|
||||
var self = this;
|
||||
var stored = {};
|
||||
var downloadUsers = [];
|
||||
|
||||
for (var i = 0; i < userIds.length; ++i) {
|
||||
var userId = userIds[i];
|
||||
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||
|
||||
stored[userId] = devices || {};
|
||||
if (devices && !forceDownload) {
|
||||
continue;
|
||||
}
|
||||
downloadUsers.push(userId);
|
||||
}
|
||||
|
||||
if (downloadUsers.length === 0) {
|
||||
return q(stored);
|
||||
}
|
||||
|
||||
return this._baseApis.downloadKeysForUsers(
|
||||
downloadUsers
|
||||
).then(function(res) {
|
||||
for (var userId in res.device_keys) {
|
||||
if (!stored.hasOwnProperty(userId)) {
|
||||
// spurious result
|
||||
continue;
|
||||
}
|
||||
|
||||
var userStore = stored[userId];
|
||||
var updated = _updateStoredDeviceKeysForUser(
|
||||
self._olmDevice, userId, userStore, res.device_keys[userId]
|
||||
);
|
||||
|
||||
if (updated) {
|
||||
self._sessionStore.storeEndToEndDevicesForUser(
|
||||
userId, userStore
|
||||
);
|
||||
}
|
||||
}
|
||||
return stored;
|
||||
});
|
||||
};
|
||||
|
||||
function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
|
||||
userResult) {
|
||||
var updated = false;
|
||||
|
||||
// remove any devices in the store which aren't in the response
|
||||
for (var deviceId in userStore) {
|
||||
if (!userStore.hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(deviceId in userResult)) {
|
||||
console.log("Device " + userId + ":" + deviceId +
|
||||
" has been removed");
|
||||
delete userStore[deviceId];
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (deviceId in userResult) {
|
||||
if (!userResult.hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_storeDeviceKeys(
|
||||
_olmDevice, userId, deviceId, userStore, userResult[deviceId]
|
||||
)) {
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/*
|
||||
* Process a device in a /query response, and add it to the userStore
|
||||
*
|
||||
* returns true if a change was made, else false
|
||||
*/
|
||||
function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult) {
|
||||
if (!deviceResult.keys) {
|
||||
// no keys?
|
||||
return false;
|
||||
}
|
||||
|
||||
var signKeyId = "ed25519:" + deviceId;
|
||||
var signKey = deviceResult.keys[signKeyId];
|
||||
if (!signKey) {
|
||||
console.log("Device " + userId + ":" + deviceId +
|
||||
" has no ed25519 key");
|
||||
return false;
|
||||
}
|
||||
|
||||
var unsigned = deviceResult.unsigned;
|
||||
var signatures = deviceResult.signatures || {};
|
||||
var userSigs = signatures[userId] || {};
|
||||
var signature = userSigs[signKeyId];
|
||||
if (!signature) {
|
||||
console.log("Device " + userId + ":" + deviceId +
|
||||
" is not signed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// prepare the canonical json: remove 'unsigned' and sigxsnatures, and
|
||||
// stringify with anotherjson
|
||||
delete deviceResult.unsigned;
|
||||
delete deviceResult.signatures;
|
||||
var json = anotherjson.stringify(deviceResult);
|
||||
|
||||
console.log("msg:", json);
|
||||
try {
|
||||
_olmDevice.verifySignature(signKey, json, signature);
|
||||
} catch (e) {
|
||||
console.log("Unable to verify signature on device " +
|
||||
userId + ":" + deviceId + ":", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
var deviceStore;
|
||||
if (deviceId in userStore) {
|
||||
// already have this device.
|
||||
deviceStore = userStore[deviceId];
|
||||
|
||||
if (deviceStore.keys["ed25519:" + deviceId] != signKey) {
|
||||
// this should only happen if the list has been MITMed; we are
|
||||
// best off sticking with the original keys.
|
||||
//
|
||||
// Should we warn the user about it somehow?
|
||||
console.warn("Ed25519 key for device" + userId + ": " +
|
||||
deviceId + " has changed");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
userStore[deviceId] = deviceStore = {
|
||||
verified: DeviceVerification.UNVERIFIED
|
||||
};
|
||||
}
|
||||
|
||||
deviceStore.keys = deviceResult.keys;
|
||||
deviceStore.algorithms = deviceResult.algorithms;
|
||||
deviceStore.unsigned = unsigned;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* List the stored device keys for a user id
|
||||
*
|
||||
* @param {string} userId the user to list keys for.
|
||||
*
|
||||
* @return {object[]} list of devices with "id", "verified", "blocked",
|
||||
* "key", and "display_name" parameters.
|
||||
*/
|
||||
Crypto.prototype.listDeviceKeys = function(userId) {
|
||||
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||
var result = [];
|
||||
if (devices) {
|
||||
var deviceId;
|
||||
var deviceIds = [];
|
||||
for (deviceId in devices) {
|
||||
if (devices.hasOwnProperty(deviceId)) {
|
||||
deviceIds.push(deviceId);
|
||||
}
|
||||
}
|
||||
deviceIds.sort();
|
||||
for (var i = 0; i < deviceIds.length; ++i) {
|
||||
deviceId = deviceIds[i];
|
||||
var device = devices[deviceId];
|
||||
var ed25519Key = device.keys["ed25519:" + deviceId];
|
||||
var unsigned = device.unsigned || {};
|
||||
if (ed25519Key) {
|
||||
result.push({
|
||||
id: deviceId,
|
||||
key: ed25519Key,
|
||||
verified: Boolean(device.verified == DeviceVerification.VERIFIED),
|
||||
blocked: Boolean(device.verified == DeviceVerification.BLOCKED),
|
||||
display_name: unsigned.device_display_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find a device by curve25519 identity key
|
||||
*
|
||||
* @param {string} userId owner of the device
|
||||
* @param {string} algorithm encryption algorithm
|
||||
* @param {string} sender_key curve25519 key to match
|
||||
*
|
||||
* @return {module:crypto~DeviceInfo?}
|
||||
*/
|
||||
Crypto.prototype.getDeviceByIdentityKey = function(userId, algorithm, sender_key) {
|
||||
if (algorithm !== OLM_ALGORITHM) {
|
||||
// we only deal in olm keys
|
||||
return null;
|
||||
}
|
||||
|
||||
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||
if (!devices) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (var deviceId in devices) {
|
||||
if (!devices.hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var device = devices[deviceId];
|
||||
for (var keyId in device.keys) {
|
||||
if (!device.keys.hasOwnProperty(keyId)) {
|
||||
continue;
|
||||
}
|
||||
if (keyId.indexOf("curve25519:") !== 0) {
|
||||
continue;
|
||||
}
|
||||
var deviceKey = device.keys[keyId];
|
||||
if (deviceKey == sender_key) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// doesn't match a known device
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Update the blocked/verified state of the given device
|
||||
*
|
||||
* @param {string} userId owner of the device
|
||||
* @param {string} deviceId unique identifier for the device
|
||||
*
|
||||
* @param {?boolean} verified whether to mark the device as verified. Null to
|
||||
* leave unchanged.
|
||||
*
|
||||
* @param {?boolean} blocked whether to mark the device as blocked. Null to
|
||||
* leave unchanged.
|
||||
*/
|
||||
Crypto.prototype.setDeviceVerification = function(userId, deviceId, verified, blocked) {
|
||||
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||
if (!devices || !devices[deviceId]) {
|
||||
throw new Error("Unknown device " + userId + ":" + deviceId);
|
||||
}
|
||||
|
||||
var dev = devices[deviceId];
|
||||
var verificationStatus = dev.verified;
|
||||
|
||||
if (verified) {
|
||||
verificationStatus = DeviceVerification.VERIFIED;
|
||||
} else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) {
|
||||
verificationStatus = DeviceVerification.UNVERIFIED;
|
||||
}
|
||||
|
||||
if (blocked) {
|
||||
verificationStatus = DeviceVerification.BLOCKED;
|
||||
} else if (blocked !== null && verificationStatus == DeviceVerification.BLOCKED) {
|
||||
verificationStatus = DeviceVerification.UNVERIFIED;
|
||||
}
|
||||
|
||||
if (dev.verified === verificationStatus) {
|
||||
return;
|
||||
}
|
||||
dev.verified = verificationStatus;
|
||||
this._sessionStore.storeEndToEndDevicesForUser(userId, devices);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Identify a device by curve25519 identity key and determine its verification state
|
||||
*
|
||||
* @param {string} userId owner of the device
|
||||
* @param {string} algorithm encryption algorithm
|
||||
* @param {string} sender_key curve25519 key to match
|
||||
*
|
||||
* @return {boolean} true if the device is verified
|
||||
*/
|
||||
Crypto.prototype.isSenderKeyVerified = function(userId, algorithm, sender_key) {
|
||||
var device = this.getDeviceByIdentityKey(userId, algorithm, sender_key);
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
return device.verified == DeviceVerification.VERIFIED;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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 {string[]} roomMembers userIds of room members to start sessions with
|
||||
*
|
||||
* @return {Object} A promise that will resolve when encryption is setup.
|
||||
*/
|
||||
Crypto.prototype.setRoomEncryption = function(roomId, config, roomMembers) {
|
||||
var self = this;
|
||||
|
||||
// if we already have encryption in this room, we should ignore this event
|
||||
// (for now at least. maybe we should alert the user somehow?)
|
||||
var existingConfig = this._sessionStore.getEndToEndRoom(roomId);
|
||||
if (existingConfig) {
|
||||
if (JSON.stringify(existingConfig) != JSON.stringify(config)) {
|
||||
console.error("Ignoring m.room.encryption event which requests " +
|
||||
"a change of config in " + roomId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.algorithm !== OLM_ALGORITHM) {
|
||||
throw new Error("Unknown algorithm: " + config.algorithm);
|
||||
}
|
||||
|
||||
// remove spurious keys
|
||||
config = {
|
||||
algorithm: OLM_ALGORITHM,
|
||||
};
|
||||
this._sessionStore.storeEndToEndRoom(roomId, config);
|
||||
|
||||
return self.downloadKeys(roomMembers, true).then(function(res) {
|
||||
return self._ensureOlmSessionsForUsers(roomMembers);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Try to make sure we have established olm sessions for the given users.
|
||||
*
|
||||
* @param {string[]} users list of user ids
|
||||
*
|
||||
* @return {module:client.Promise} resolves once the sessions are complete, to
|
||||
* an object with keys <tt>missingUsers</tt> (a list of users with no known
|
||||
* olm devices), and <tt>missingDevices</tt> a list of olm devices with no
|
||||
* known one-time keys.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
Crypto.prototype._ensureOlmSessionsForUsers = function(users) {
|
||||
var devicesWithoutSession = [];
|
||||
var userWithoutDevices = [];
|
||||
for (var i = 0; i < users.length; ++i) {
|
||||
var userId = users[i];
|
||||
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||
if (!devices) {
|
||||
userWithoutDevices.push(userId);
|
||||
} else {
|
||||
for (var deviceId in devices) {
|
||||
if (devices.hasOwnProperty(deviceId)) {
|
||||
var keys = devices[deviceId];
|
||||
var key = keys.keys["curve25519:" + deviceId];
|
||||
if (key == this._olmDevice.deviceCurve25519Key) {
|
||||
continue;
|
||||
}
|
||||
if (!this._sessionStore.getEndToEndSessions(key)) {
|
||||
devicesWithoutSession.push([userId, deviceId, key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (devicesWithoutSession.length === 0) {
|
||||
return q({
|
||||
missingUsers: userWithoutDevices,
|
||||
missingDevices: []
|
||||
});
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return this._baseApis.claimOneTimeKeys(
|
||||
devicesWithoutSession
|
||||
).then(function(res) {
|
||||
var missing = {};
|
||||
for (i = 0; i < devicesWithoutSession.length; ++i) {
|
||||
var device = devicesWithoutSession[i];
|
||||
var userRes = res.one_time_keys[device[0]] || {};
|
||||
var deviceRes = userRes[device[1]];
|
||||
var oneTimeKey;
|
||||
for (var keyId in deviceRes) {
|
||||
if (keyId.indexOf("curve25519:") === 0) {
|
||||
oneTimeKey = deviceRes[keyId];
|
||||
}
|
||||
}
|
||||
if (oneTimeKey) {
|
||||
var sid = self._olmDevice.createOutboundSession(
|
||||
device[2], oneTimeKey
|
||||
);
|
||||
console.log("Started new sessionid " + sid +
|
||||
" for device " + device[2]);
|
||||
} else {
|
||||
missing[device[0]] = missing[device[0]] || [];
|
||||
missing[device[0]].push([device[1]]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
missingUsers: userWithoutDevices,
|
||||
missingDevices: missing
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether encryption is enabled for a room.
|
||||
* @param {string} roomId the room id to query.
|
||||
* @return {bool} whether encryption is enabled.
|
||||
*/
|
||||
Crypto.prototype.isRoomEncrypted = function(roomId) {
|
||||
return (this._sessionStore.getEndToEndRoom(roomId) && true) || false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt an event according to the configuration of the room, if necessary.
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event event to be sent
|
||||
* @param {module:models/room.Room} room destination room
|
||||
*/
|
||||
Crypto.prototype.encryptEventIfNeeded = function(event, room) {
|
||||
if (event.isEncrypted()) {
|
||||
// this event has already been encrypted; this happens if the
|
||||
// encryption step succeeded, but the send step failed on the first
|
||||
// attempt.
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.getType() !== "m.room.message") {
|
||||
// we only encrypt m.room.message
|
||||
return;
|
||||
}
|
||||
|
||||
var roomId = event.getRoomId();
|
||||
|
||||
var e2eRoomInfo = this._sessionStore.getEndToEndRoom(roomId);
|
||||
if (!e2eRoomInfo || !e2eRoomInfo.algorithm) {
|
||||
// not encrypting messages in this room
|
||||
return;
|
||||
}
|
||||
|
||||
var encryptedContent = this._encryptMessage(
|
||||
room, e2eRoomInfo, event.getType(), event.getContent()
|
||||
);
|
||||
event.makeEncrypted("m.room.encrypted", encryptedContent);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {module:models/room.Room} room
|
||||
* @param {object} e2eRoomInfo
|
||||
* @param {string} eventType
|
||||
* @param {object} content
|
||||
*
|
||||
* @return {object} new event body
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
Crypto.prototype._encryptMessage = function(room, e2eRoomInfo, eventType, content) {
|
||||
if (e2eRoomInfo.algorithm !== OLM_ALGORITHM) {
|
||||
throw new Error("Unknown end-to-end algorithm: " + e2eRoomInfo.algorithm);
|
||||
}
|
||||
|
||||
if (!room) {
|
||||
throw new Error("Cannot send encrypted messages in unknown rooms");
|
||||
}
|
||||
|
||||
// pick the list of recipients based on the membership list.
|
||||
//
|
||||
// TODO: there is a race condition here! What if a new user turns up
|
||||
// just as you are sending a secret message?
|
||||
|
||||
var users = utils.map(room.getJoinedMembers(), function(u) {
|
||||
return u.userId;
|
||||
});
|
||||
|
||||
var participantKeys = [];
|
||||
for (var i = 0; i < users.length; ++i) {
|
||||
var userId = users[i];
|
||||
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||
for (var deviceId in devices) {
|
||||
if (devices.hasOwnProperty(deviceId)) {
|
||||
var dev = devices[deviceId];
|
||||
if (dev.verified === DeviceVerification.BLOCKED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var keyId in dev.keys) {
|
||||
if (keyId.indexOf("curve25519:") === 0) {
|
||||
participantKeys.push(dev.keys[keyId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
participantKeys.sort();
|
||||
var participantHash = ""; // Olm.sha256(participantKeys.join());
|
||||
var payloadJson = {
|
||||
room_id: room.roomId,
|
||||
type: eventType,
|
||||
fingerprint: participantHash,
|
||||
sender_device: this._deviceId,
|
||||
content: content
|
||||
};
|
||||
var ciphertext = {};
|
||||
var payloadString = JSON.stringify(payloadJson);
|
||||
for (i = 0; i < participantKeys.length; ++i) {
|
||||
var deviceKey = participantKeys[i];
|
||||
if (deviceKey == this._olmDevice.deviceCurve25519Key) {
|
||||
continue;
|
||||
}
|
||||
var sessionIds = this._olmDevice.getSessionIdsForDevice(deviceKey);
|
||||
// Use the session with the lowest ID.
|
||||
sessionIds.sort();
|
||||
if (sessionIds.length === 0) {
|
||||
// If we don't have a session for a device then
|
||||
// we can't encrypt a message for it.
|
||||
continue;
|
||||
}
|
||||
var sessionId = sessionIds[0];
|
||||
console.log("Using sessionid " + sessionId + " for device " + deviceKey);
|
||||
ciphertext[deviceKey] = this._olmDevice.encryptMessage(
|
||||
deviceKey, sessionId, payloadString
|
||||
);
|
||||
}
|
||||
var encryptedContent = {
|
||||
algorithm: e2eRoomInfo.algorithm,
|
||||
sender_key: this._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: ciphertext
|
||||
};
|
||||
return encryptedContent;
|
||||
};
|
||||
|
||||
function DecryptionError(msg) {
|
||||
this.message = msg;
|
||||
}
|
||||
utils.inherits(DecryptionError, Error);
|
||||
|
||||
/**
|
||||
* Exception thrown when decryption fails
|
||||
*/
|
||||
Crypto.DecryptionError = DecryptionError;
|
||||
|
||||
/**
|
||||
* Decrypt a received event
|
||||
*
|
||||
* @param {object} event raw event
|
||||
*
|
||||
* @return {object} decrypted payload (with properties 'type', 'content')
|
||||
*
|
||||
* @raises {DecryptionError} if there is a problem decrypting the event
|
||||
*/
|
||||
Crypto.prototype.decryptEvent = function(event) {
|
||||
var content = event.content;
|
||||
if (content.algorithm !== OLM_ALGORITHM) {
|
||||
throw new DecryptionError("Unknown algorithm");
|
||||
}
|
||||
|
||||
var deviceKey = content.sender_key;
|
||||
var ciphertext = content.ciphertext;
|
||||
|
||||
if (!ciphertext) {
|
||||
throw new DecryptionError("Missing ciphertext");
|
||||
}
|
||||
|
||||
if (!(this._olmDevice.deviceCurve25519Key in content.ciphertext)) {
|
||||
throw new DecryptionError("Not included in recipients");
|
||||
}
|
||||
|
||||
var message = content.ciphertext[this._olmDevice.deviceCurve25519Key];
|
||||
var sessionIds = this._olmDevice.getSessionIdsForDevice(deviceKey);
|
||||
var payloadString = null;
|
||||
var foundSession = false;
|
||||
for (var i = 0; i < sessionIds.length; i++) {
|
||||
var sessionId = sessionIds[i];
|
||||
var res = this._olmDevice.decryptMessage(
|
||||
deviceKey, sessionId, message.type, message.body
|
||||
);
|
||||
payloadString = res.payload;
|
||||
if (payloadString) {
|
||||
console.log("decrypted with sessionId " + sessionId);
|
||||
break;
|
||||
}
|
||||
|
||||
if (res.matchesInbound) {
|
||||
// this was a prekey message which matched this session; don't
|
||||
// create a new session.
|
||||
foundSession = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.type === 0 && !foundSession && payloadString === null) {
|
||||
try {
|
||||
payloadString = this._olmDevice.createInboundSession(
|
||||
deviceKey, message.type, message.body
|
||||
);
|
||||
console.log("created new inbound sesion");
|
||||
} catch (e) {
|
||||
// Failed to decrypt with a new session.
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Check the sender user id matches the sender key.
|
||||
if (payloadString !== null) {
|
||||
return JSON.parse(payloadString);
|
||||
} else {
|
||||
throw new DecryptionError("Bad Encrypted Message");
|
||||
}
|
||||
};
|
||||
|
||||
/** */
|
||||
module.exports = Crypto;
|
||||
+54
-19
@@ -44,12 +44,17 @@ module.exports.EventStatus = {
|
||||
/**
|
||||
* Construct a Matrix Event object
|
||||
* @constructor
|
||||
*
|
||||
* @param {Object} event The raw event to be wrapped in this DAO
|
||||
* @param {boolean} encrypted Was the event encrypted
|
||||
* @prop {Object} event The raw event. <b>Do not access this property</b>
|
||||
* directly unless you absolutely have to. Prefer the getter methods defined on
|
||||
* this class. Using the getter methods shields your app from
|
||||
* changes to event JSON between Matrix versions.
|
||||
*
|
||||
* @param {Object=} clearEvent For encrypted events, the plaintext payload for
|
||||
* the event (typically containing <tt>type</tt> and <tt>content</tt> fields).
|
||||
*
|
||||
* @prop {Object} event The raw (possibly encrypted) event. <b>Do not access
|
||||
* this property</b> directly unless you absolutely have to. Prefer the getter
|
||||
* methods defined on this class. Using the getter methods shields your app
|
||||
* from changes to event JSON between Matrix versions.
|
||||
*
|
||||
* @prop {RoomMember} sender The room member who sent this event, or null e.g.
|
||||
* this is a presence event.
|
||||
* @prop {RoomMember} target The room member who is the target of this event, e.g.
|
||||
@@ -59,14 +64,16 @@ module.exports.EventStatus = {
|
||||
* that getDirectionalContent() will return event.content and not event.prev_content.
|
||||
* Default: true. <strong>This property is experimental and may change.</strong>
|
||||
*/
|
||||
module.exports.MatrixEvent = function MatrixEvent(event, encrypted) {
|
||||
module.exports.MatrixEvent = function MatrixEvent(event, clearEvent) {
|
||||
this.event = event || {};
|
||||
this.sender = null;
|
||||
this.target = null;
|
||||
this.status = null;
|
||||
this.forwardLooking = true;
|
||||
this.encrypted = Boolean(encrypted);
|
||||
|
||||
this._clearEvent = clearEvent || {};
|
||||
};
|
||||
|
||||
module.exports.MatrixEvent.prototype = {
|
||||
|
||||
/**
|
||||
@@ -87,19 +94,22 @@ module.exports.MatrixEvent.prototype = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the type of event.
|
||||
* Get the (decrypted, if necessary) type of event.
|
||||
*
|
||||
* @return {string} The event type, e.g. <code>m.room.message</code>
|
||||
*/
|
||||
getType: function() {
|
||||
return this.event.type;
|
||||
return this._clearEvent.type || this.event.type;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the type of the event that will be sent to the homeserver.
|
||||
* Get the (possibly encrypted) type of the event that will be sent to the
|
||||
* homeserver.
|
||||
*
|
||||
* @return {string} The event type.
|
||||
*/
|
||||
getWireType: function() {
|
||||
return this.encryptedType || this.event.type;
|
||||
return this.event.type;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -121,19 +131,22 @@ module.exports.MatrixEvent.prototype = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the event content JSON.
|
||||
* Get the (decrypted, if necessary) event content JSON.
|
||||
*
|
||||
* @return {Object} The event content JSON, or an empty object.
|
||||
*/
|
||||
getContent: function() {
|
||||
return this.event.content || {};
|
||||
return this._clearEvent.content || this.event.content || {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the event content JSON that will be sent to the homeserver.
|
||||
* Get the (possibly encrypted) event content JSON that will be sent to the
|
||||
* homeserver.
|
||||
*
|
||||
* @return {Object} The event content JSON, or an empty object.
|
||||
*/
|
||||
getWireContent: function() {
|
||||
return this.encryptedContent || this.event.content || {};
|
||||
return this.event.content || {};
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -186,12 +199,33 @@ module.exports.MatrixEvent.prototype = {
|
||||
return this.event.state_key !== undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Replace the content of this event with encrypted versions.
|
||||
* (This is used when sending an event; it should not be used by applications).
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @param {string} crypto_type type of the encrypted event - typically
|
||||
* <tt>"m.room.encrypted"</tt>
|
||||
*
|
||||
* @param {object} crypto_content raw 'content' for the encrypted event.
|
||||
*/
|
||||
makeEncrypted: function(crypto_type, crypto_content) {
|
||||
// keep the plain-text data for 'view source'
|
||||
this._clearEvent = {
|
||||
type: this.event.type,
|
||||
content: this.event.content,
|
||||
};
|
||||
this.event.type = crypto_type;
|
||||
this.event.content = crypto_content;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the event is encrypted.
|
||||
* @return {boolean} True if this event is encrypted.
|
||||
*/
|
||||
isEncrypted: function() {
|
||||
return this.encrypted;
|
||||
return Boolean(this._clearEvent.type);
|
||||
},
|
||||
|
||||
getUnsigned: function() {
|
||||
@@ -219,10 +253,11 @@ module.exports.MatrixEvent.prototype = {
|
||||
}
|
||||
|
||||
var keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {};
|
||||
for (key in this.event.content) {
|
||||
if (!this.event.content.hasOwnProperty(key)) { continue; }
|
||||
var content = this.getContent();
|
||||
for (key in content) {
|
||||
if (!content.hasOwnProperty(key)) { continue; }
|
||||
if (!keeps[key]) {
|
||||
delete this.event.content[key];
|
||||
delete content[key];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -102,11 +102,14 @@ RoomMember.prototype.setPowerLevelEvent = function(powerLevelEvent) {
|
||||
});
|
||||
var oldPowerLevel = this.powerLevel;
|
||||
var oldPowerLevelNorm = this.powerLevelNorm;
|
||||
this.powerLevel = (
|
||||
powerLevelEvent.getContent().users[this.userId] ||
|
||||
powerLevelEvent.getContent().users_default ||
|
||||
0
|
||||
);
|
||||
|
||||
if (powerLevelEvent.getContent().users[this.userId] !== undefined) {
|
||||
this.powerLevel = powerLevelEvent.getContent().users[this.userId];
|
||||
} else if (powerLevelEvent.getContent().users_default !== undefined) {
|
||||
this.powerLevel = powerLevelEvent.getContent().users_default;
|
||||
} else {
|
||||
this.powerLevel = 0;
|
||||
}
|
||||
this.powerLevelNorm = 0;
|
||||
if (maxLevel > 0) {
|
||||
this.powerLevelNorm = (this.powerLevel * 100) / maxLevel;
|
||||
|
||||
+44
-14
@@ -341,6 +341,43 @@ Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the aliases this room has according to the room's state
|
||||
* The aliases returned by this function may not necessarily
|
||||
* still point to this room.
|
||||
* @return {array} The room's alias as an array of strings
|
||||
*/
|
||||
Room.prototype.getAliases = function() {
|
||||
var alias_strings = [];
|
||||
|
||||
var alias_events = this.currentState.getStateEvents("m.room.aliases");
|
||||
if (alias_events) {
|
||||
for (var i = 0; i < alias_events.length; ++i) {
|
||||
var alias_event = alias_events[i];
|
||||
if (utils.isArray(alias_event.getContent().aliases)) {
|
||||
Array.prototype.push.apply(
|
||||
alias_strings, alias_event.getContent().aliases
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return alias_strings;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get this room's canonical alias
|
||||
* The alias returned by this function may not necessarily
|
||||
* still point to this room.
|
||||
* @return {?string} The room's canonical alias, or null if there is none
|
||||
*/
|
||||
Room.prototype.getCanonicalAlias = function() {
|
||||
var canonicalAlias = this.currentState.getStateEvents("m.room.canonical_alias", "");
|
||||
if (canonicalAlias) {
|
||||
return canonicalAlias.getContent().alias;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a member from the current room state.
|
||||
* @param {string} userId The user ID of the member.
|
||||
@@ -783,13 +820,9 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
|
||||
);
|
||||
}
|
||||
|
||||
// replace the event source, but preserve the original content
|
||||
// and type in case it was encrypted (we won't be able to
|
||||
// decrypt it, even though we sent it.)
|
||||
var existingSource = localEvent.event;
|
||||
// replace the event source (this will preserve the plaintext payload if
|
||||
// any, which is good, because we don't want to try decoding it again).
|
||||
localEvent.event = remoteEvent.event;
|
||||
localEvent.event.content = existingSource.content;
|
||||
localEvent.event.type = existingSource.type;
|
||||
|
||||
// successfully sent.
|
||||
localEvent.status = null;
|
||||
@@ -1348,16 +1381,13 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
var alias;
|
||||
var canonicalAlias = room.currentState.getStateEvents("m.room.canonical_alias", "");
|
||||
if (canonicalAlias) {
|
||||
alias = canonicalAlias.getContent().alias;
|
||||
}
|
||||
var alias = room.getCanonicalAlias();
|
||||
|
||||
if (!alias) {
|
||||
var mRoomAliases = room.currentState.getStateEvents("m.room.aliases")[0];
|
||||
if (mRoomAliases && utils.isArray(mRoomAliases.getContent().aliases)) {
|
||||
alias = mRoomAliases.getContent().aliases[0];
|
||||
var aliases = room.getAliases();
|
||||
|
||||
if (aliases.length) {
|
||||
alias = aliases[0];
|
||||
}
|
||||
}
|
||||
if (alias) {
|
||||
|
||||
+38
-6
@@ -77,27 +77,36 @@ User.prototype.setPresenceEvent = function(event) {
|
||||
if (event.getContent().presence !== this.presence || firstFire) {
|
||||
eventsToFire.push("User.presence");
|
||||
}
|
||||
if (event.getContent().avatar_url !== this.avatarUrl) {
|
||||
if (event.getContent().avatar_url &&
|
||||
event.getContent().avatar_url !== this.avatarUrl)
|
||||
{
|
||||
eventsToFire.push("User.avatarUrl");
|
||||
}
|
||||
if (event.getContent().displayname !== this.displayName) {
|
||||
if (event.getContent().displayname &&
|
||||
event.getContent().displayname !== this.displayName)
|
||||
{
|
||||
eventsToFire.push("User.displayName");
|
||||
}
|
||||
if (event.getContent().currently_active !== undefined &&
|
||||
event.getContent().currently_active !== this.currentlyActive)
|
||||
{
|
||||
eventsToFire.push("User.currentlyActive");
|
||||
}
|
||||
|
||||
this.presence = event.getContent().presence;
|
||||
eventsToFire.push("User.lastPresenceTs");
|
||||
|
||||
if (event.getContent().displayname) {
|
||||
this.displayName = event.getContent().displayname;
|
||||
}
|
||||
if (event.getContent().avatarUrl) {
|
||||
if (event.getContent().avatar_url) {
|
||||
this.avatarUrl = event.getContent().avatar_url;
|
||||
}
|
||||
this.lastActiveAgo = event.getContent().last_active_ago;
|
||||
this.lastPresenceTs = Date.now();
|
||||
this.currentlyActive = event.getContent().currently_active;
|
||||
|
||||
if (eventsToFire.length > 0) {
|
||||
this._updateModifiedTime();
|
||||
}
|
||||
this._updateModifiedTime();
|
||||
|
||||
for (var i = 0; i < eventsToFire.length; i++) {
|
||||
this.emit(eventsToFire[i], event, this);
|
||||
@@ -161,6 +170,18 @@ User.prototype.getLastActiveTs = function() {
|
||||
*/
|
||||
module.exports = User;
|
||||
|
||||
/**
|
||||
* Fires whenever any user's lastPresenceTs changes,
|
||||
* ie. whenever any presence event is received for a user.
|
||||
* @event module:client~MatrixClient#"User.lastPresenceTs"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {User} user The user whose User.lastPresenceTs changed.
|
||||
* @example
|
||||
* matrixClient.on("User.lastPresenceTs", function(event, user){
|
||||
* var newlastPresenceTs = user.lastPresenceTs;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever any user's presence changes.
|
||||
* @event module:client~MatrixClient#"User.presence"
|
||||
@@ -172,6 +193,17 @@ module.exports = User;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever any user's currentlyActive changes.
|
||||
* @event module:client~MatrixClient#"User.currentlyActive"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {User} user The user whose User.currentlyActive changed.
|
||||
* @example
|
||||
* matrixClient.on("User.currentlyActive", function(event, user){
|
||||
* var newCurrentlyActive = user.currentlyActive;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever any user's display name changes.
|
||||
* @event module:client~MatrixClient#"User.displayName"
|
||||
|
||||
+28
-6
@@ -122,7 +122,7 @@ function PushProcessor(client) {
|
||||
var eventFulfillsRoomMemberCountCondition = function(cond, ev) {
|
||||
if (!cond.is) { return false; }
|
||||
|
||||
var room = client.getRoom(ev.room_id);
|
||||
var room = client.getRoom(ev.getRoomId());
|
||||
if (!room || !room.currentState || !room.currentState.members) { return false; }
|
||||
|
||||
var memberCount = Object.keys(room.currentState.members).filter(function(m) {
|
||||
@@ -152,11 +152,12 @@ function PushProcessor(client) {
|
||||
};
|
||||
|
||||
var eventFulfillsDisplayNameCondition = function(cond, ev) {
|
||||
if (!ev.content || ! ev.content.body || typeof ev.content.body != 'string') {
|
||||
var content = ev.getContent();
|
||||
if (!content || !content.body || typeof content.body != 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
var room = client.getRoom(ev.room_id);
|
||||
var room = client.getRoom(ev.getRoomId());
|
||||
if (!room || !room.currentState || !room.currentState.members ||
|
||||
!room.currentState.getMember(client.credentials.userId)) { return false; }
|
||||
|
||||
@@ -165,7 +166,7 @@ function PushProcessor(client) {
|
||||
// N.B. we can't use \b as it chokes on unicode. however \W seems to be okay
|
||||
// as shorthand for [^0-9A-Za-z_].
|
||||
var pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i');
|
||||
return ev.content.body.search(pat) > -1;
|
||||
return content.body.search(pat) > -1;
|
||||
};
|
||||
|
||||
var eventFulfillsDeviceCondition = function(cond, ev) {
|
||||
@@ -204,7 +205,21 @@ function PushProcessor(client) {
|
||||
|
||||
var valueForDottedKey = function(key, ev) {
|
||||
var parts = key.split('.');
|
||||
var val = ev;
|
||||
var val;
|
||||
|
||||
// special-case the first component to deal with encrypted messages
|
||||
var firstPart = parts[0];
|
||||
if (firstPart == 'content') {
|
||||
val = ev.getContent();
|
||||
parts.shift();
|
||||
} else if (firstPart == 'type') {
|
||||
val = ev.getType();
|
||||
parts.shift();
|
||||
} else {
|
||||
// use the raw event for any other fields
|
||||
val = ev.event;
|
||||
}
|
||||
|
||||
while (parts.length > 0) {
|
||||
var thispart = parts.shift();
|
||||
if (!val[thispart]) { return null; }
|
||||
@@ -215,7 +230,7 @@ function PushProcessor(client) {
|
||||
|
||||
var matchingRuleForEventWithRulesets = function(ev, rulesets) {
|
||||
if (!rulesets || !rulesets.device) { return null; }
|
||||
if (ev.user_id == client.credentials.userId) { return null; }
|
||||
if (ev.getSender() == client.credentials.userId) { return null; }
|
||||
|
||||
var allDevNames = Object.keys(rulesets.device);
|
||||
for (var i = 0; i < allDevNames.length; ++i) {
|
||||
@@ -258,6 +273,13 @@ function PushProcessor(client) {
|
||||
return actionObj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the user's push actions for the given event
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} ev
|
||||
*
|
||||
* @return {PushAction}
|
||||
*/
|
||||
this.actionsForEvent = function(ev) {
|
||||
return pushActionsForEventAndRulesets(ev, client.pushRules);
|
||||
};
|
||||
|
||||
+26
-1
@@ -43,6 +43,9 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
|
||||
// filterId: Filter
|
||||
// }
|
||||
};
|
||||
this.accountData = {
|
||||
// type : content
|
||||
};
|
||||
this.localStorage = opts.localStorage;
|
||||
};
|
||||
|
||||
@@ -244,7 +247,29 @@ module.exports.MatrixInMemoryStore.prototype = {
|
||||
this.localStorage.setItem("mxjssdk_memory_filter_" + filterName, filterId);
|
||||
}
|
||||
catch (e) {}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Store user-scoped account data events.
|
||||
* N.B. that account data only allows a single event per type, so multiple
|
||||
* events with the same type will replace each other.
|
||||
* @param {Array<MatrixEvent>} events The events to store.
|
||||
*/
|
||||
storeAccountDataEvents: function(events) {
|
||||
var self = this;
|
||||
events.forEach(function(event) {
|
||||
self.accountData[event.getType()] = event;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get account data event by event type
|
||||
* @param {string} eventType The event type being queried
|
||||
* @return {?MatrixEvent} the user account_data event of given type, if any
|
||||
*/
|
||||
getAccountData: function(eventType) {
|
||||
return this.accountData[eventType];
|
||||
},
|
||||
|
||||
// TODO
|
||||
//setMaxHistoryPerRoom: function(maxHistory) {},
|
||||
|
||||
+17
-1
@@ -162,7 +162,23 @@ StubStore.prototype = {
|
||||
*/
|
||||
setFilterIdByName: function(filterName, filterId) {
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Store user-scoped account data events
|
||||
* @param {Array<MatrixEvent>} events The events to store.
|
||||
*/
|
||||
storeAccountDataEvents: function(events) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Get account data event by event type
|
||||
* @param {string} eventType The event type being queried
|
||||
*/
|
||||
getAccountData: function(eventType) {
|
||||
|
||||
},
|
||||
|
||||
// TODO
|
||||
//setMaxHistoryPerRoom: function(maxHistory) {},
|
||||
|
||||
+19
-2
@@ -88,6 +88,7 @@ SyncApi.prototype.createRoom = function(roomId) {
|
||||
"Room.receipt", "Room.tags",
|
||||
"Room.timelineReset",
|
||||
"Room.localEchoUpdated",
|
||||
"Room.accountData",
|
||||
]);
|
||||
this._registerStateListeners(room);
|
||||
return room;
|
||||
@@ -454,6 +455,7 @@ SyncApi.prototype._sync = function(syncOptions) {
|
||||
self._connectionReturnedDefer = null;
|
||||
}
|
||||
this._updateSyncState("STOPPED");
|
||||
return;
|
||||
}
|
||||
|
||||
var filterId = syncOptions.filterId;
|
||||
@@ -605,7 +607,19 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
||||
});
|
||||
}
|
||||
|
||||
// the returned json structure is abit crap, so make it into a
|
||||
// handle non-room account_data
|
||||
if (data.account_data && utils.isArray(data.account_data.events)) {
|
||||
var events = data.account_data.events.map(client.getEventMapper());
|
||||
client.store.storeAccountDataEvents(events);
|
||||
events.forEach(
|
||||
function(accountDataEvent) {
|
||||
client.emit("accountData", accountDataEvent);
|
||||
return accountDataEvent;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// the returned json structure is a bit crap, so make it into a
|
||||
// nicer form (array) after applying sanity to make sure we don't fail
|
||||
// on missing keys (on the off chance)
|
||||
var inviteRooms = [];
|
||||
@@ -1040,7 +1054,10 @@ SyncApi.prototype._onOnline = function() {
|
||||
|
||||
function createNewUser(client, userId) {
|
||||
var user = new User(userId);
|
||||
reEmit(client, user, ["User.avatarUrl", "User.displayName", "User.presence"]);
|
||||
reEmit(client, user, [
|
||||
"User.avatarUrl", "User.displayName", "User.presence",
|
||||
"User.currentlyActive", "User.lastPresenceTs"
|
||||
]);
|
||||
return user;
|
||||
}
|
||||
|
||||
|
||||
@@ -332,6 +332,30 @@ var deepCompare = module.exports.deepCompare = function(x, y) {
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy properties from one object to another.
|
||||
*
|
||||
* All enumerable properties, included inherited ones, are copied.
|
||||
*
|
||||
* @param {Object} target The object that will receive new properties
|
||||
* @param {...Object} source Objects from which to copy properties
|
||||
*
|
||||
* @return {Object} target
|
||||
*/
|
||||
module.exports.extend = function() {
|
||||
var target = arguments[0] || {};
|
||||
// disable jshint "The body of a for in should be wrapped in an if
|
||||
// statement"
|
||||
/* jshint -W089 */
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
var source = arguments[i];
|
||||
for (var propName in source) {
|
||||
target[propName] = source[propName];
|
||||
}
|
||||
}
|
||||
/* jshint +W089 */
|
||||
return target;
|
||||
};
|
||||
|
||||
/**
|
||||
* Run polyfills to add Array.map and Array.filter if they are missing.
|
||||
|
||||
+6
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.5.3",
|
||||
"version": "0.5.5",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -22,8 +22,9 @@
|
||||
],
|
||||
"browser": "browser-index.js",
|
||||
"author": "matrix.org",
|
||||
"license": "Apache 2.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"another-json": "^0.2.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"browserify": "^10.2.3",
|
||||
"q": "^1.4.1",
|
||||
@@ -36,5 +37,8 @@
|
||||
"jshint": "^2.8.0",
|
||||
"jsdoc": "^3.4.0",
|
||||
"uglifyjs": "^2.4.10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"olm": "https://matrix.org/packages/npm/olm/olm-1.0.0.tgz"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ if [ -z "$skip_jsdoc" ]; then
|
||||
echo "copying jsdocs to gh-pages branch"
|
||||
git checkout gh-pages
|
||||
git pull
|
||||
cp -ar ".jsdoc/matrix-js-sdk/$release" .
|
||||
cp -a ".jsdoc/matrix-js-sdk/$release" .
|
||||
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
|
||||
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' \
|
||||
$release index.html
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A mock implementation of the webstorage api
|
||||
* @constructor
|
||||
*/
|
||||
function MockStorageApi() {
|
||||
this.data = {};
|
||||
this.keys = [];
|
||||
this.length = 0;
|
||||
}
|
||||
|
||||
MockStorageApi.prototype = {
|
||||
setItem: function(k, v) {
|
||||
this.data[k] = v;
|
||||
this._recalc();
|
||||
},
|
||||
getItem: function(k) {
|
||||
return this.data[k] || null;
|
||||
},
|
||||
removeItem: function(k) {
|
||||
delete this.data[k];
|
||||
this._recalc();
|
||||
},
|
||||
key: function(index) {
|
||||
return this.keys[index];
|
||||
},
|
||||
_recalc: function() {
|
||||
var keys = [];
|
||||
for (var k in this.data) {
|
||||
if (!this.data.hasOwnProperty(k)) { continue; }
|
||||
keys.push(k);
|
||||
}
|
||||
this.keys = keys;
|
||||
this.length = keys.length;
|
||||
}
|
||||
};
|
||||
|
||||
/** */
|
||||
module.exports = MockStorageApi;
|
||||
@@ -1,5 +1,6 @@
|
||||
"use strict";
|
||||
var sdk = require("../..");
|
||||
var q = require("q");
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
function MockStorageApi() {
|
||||
@@ -17,233 +18,502 @@ MockStorageApi.prototype = {
|
||||
}
|
||||
};
|
||||
|
||||
var aliHttpBackend;
|
||||
var bobHttpBackend;
|
||||
var aliClient;
|
||||
var roomId = "!room:localhost";
|
||||
var aliUserId = "@ali:localhost";
|
||||
var aliDeviceId = "zxcvb";
|
||||
var aliAccessToken = "aseukfgwef";
|
||||
var bobClient;
|
||||
var bobUserId = "@bob:localhost";
|
||||
var bobDeviceId = "bvcxz";
|
||||
var bobAccessToken = "fewgfkuesa";
|
||||
var bobOneTimeKeys;
|
||||
var aliDeviceKeys;
|
||||
var bobDeviceKeys;
|
||||
var bobDeviceCurve25519Key;
|
||||
var bobDeviceEd25519Key;
|
||||
var aliLocalStore;
|
||||
var aliStorage;
|
||||
var bobStorage;
|
||||
var aliMessages;
|
||||
var bobMessages;
|
||||
|
||||
|
||||
/**
|
||||
* Set an expectation that the client will upload device keys and a number of
|
||||
* one-time keys; then flush the http requests.
|
||||
*
|
||||
* @param {string} deviceId expected device id in upload request
|
||||
* @param {object} httpBackend
|
||||
*
|
||||
* @return {promise} completes once the http requests have completed, returning combined
|
||||
* {one_time_keys: {}, device_keys: {}}
|
||||
*/
|
||||
function expectKeyUpload(deviceId, httpBackend) {
|
||||
var uploadPath = "/keys/upload/" + deviceId;
|
||||
var keys = {};
|
||||
|
||||
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
|
||||
expect(content.one_time_keys).not.toBeDefined();
|
||||
expect(content.device_keys).toBeDefined();
|
||||
keys.device_keys = content.device_keys;
|
||||
return {one_time_key_counts: {curve25519: 0}};
|
||||
});
|
||||
|
||||
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
|
||||
expect(content.device_keys).not.toBeDefined();
|
||||
expect(content.one_time_keys).toBeDefined();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
var count = 0;
|
||||
for (var key in content.one_time_keys) {
|
||||
if (content.one_time_keys.hasOwnProperty(key)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
expect(count).toEqual(5);
|
||||
keys.one_time_keys = content.one_time_keys;
|
||||
return {one_time_key_counts: {curve25519: count}};
|
||||
});
|
||||
|
||||
return httpBackend.flush(uploadPath, 2).then(function() {
|
||||
return keys;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set an expectation that ali will upload device keys and a number of one-time keys;
|
||||
* then flush the http requests.
|
||||
*
|
||||
* <p>Updates <tt>aliDeviceKeys</tt>
|
||||
*
|
||||
* @return {promise} completes once the http requests have completed.
|
||||
*/
|
||||
function expectAliKeyUpload() {
|
||||
return expectKeyUpload(aliDeviceId, aliHttpBackend).then(function(content) {
|
||||
aliDeviceKeys = content.device_keys;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set an expectation that bob will upload device keys and a number of one-time keys;
|
||||
* then flush the http requests.
|
||||
*
|
||||
* <p>Updates <tt>bobDeviceKeys</tt>, <tt>bobOneTimeKeys</tt>,
|
||||
* <tt>bobDeviceCurve25519Key</tt>, <tt>bobDeviceEd25519Key</tt>
|
||||
*
|
||||
* @return {promise} completes once the http requests have completed.
|
||||
*/
|
||||
function expectBobKeyUpload() {
|
||||
return expectKeyUpload(bobDeviceId, bobHttpBackend).then(function(content) {
|
||||
bobDeviceKeys = content.device_keys;
|
||||
bobOneTimeKeys = content.one_time_keys;
|
||||
expect(bobDeviceKeys).toBeDefined();
|
||||
expect(bobOneTimeKeys).toBeDefined();
|
||||
bobDeviceCurve25519Key = bobDeviceKeys.keys["curve25519:bvcxz"];
|
||||
bobDeviceEd25519Key = bobDeviceKeys.keys["ed25519:bvcxz"];
|
||||
});
|
||||
}
|
||||
|
||||
function bobUploadsKeys() {
|
||||
bobClient.uploadKeys(5).catch(utils.failTest);
|
||||
return expectBobKeyUpload();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set an expectation that ali will query bobs keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} resolves once the http request has completed.
|
||||
*/
|
||||
function aliQueryKeys() {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobDeviceKeys).toBeDefined();
|
||||
|
||||
var bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
|
||||
expect(content.device_keys[bobUserId]).toEqual({});
|
||||
var result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
});
|
||||
return aliHttpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that bob will query alis keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} which resolves once the http request has completed.
|
||||
*/
|
||||
function bobQueryKeys() {
|
||||
// can't query keys before ali has uploaded them
|
||||
expect(aliDeviceKeys).toBeDefined();
|
||||
|
||||
var aliKeys = {};
|
||||
aliKeys[aliDeviceId] = aliDeviceKeys;
|
||||
bobHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
|
||||
expect(content.device_keys[aliUserId]).toEqual({});
|
||||
var result = {};
|
||||
result[aliUserId] = aliKeys;
|
||||
return {device_keys: result};
|
||||
});
|
||||
return bobHttpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
|
||||
|
||||
function aliDownloadsKeys() {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobDeviceEd25519Key).toBeDefined();
|
||||
|
||||
var p1 = aliClient.downloadKeys([bobUserId]).then(function() {
|
||||
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([{
|
||||
id: "bvcxz",
|
||||
key: bobDeviceEd25519Key,
|
||||
verified: false,
|
||||
blocked: false,
|
||||
}]);
|
||||
});
|
||||
var p2 = aliQueryKeys();
|
||||
|
||||
// check that the localStorage is updated as we expect (not sure this is
|
||||
// an integration test, but meh)
|
||||
return q.all([p1, p2]).then(function() {
|
||||
var devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
|
||||
expect(devices[bobDeviceId].keys).toEqual(bobDeviceKeys.keys);
|
||||
expect(devices[bobDeviceId].verified).
|
||||
toBe(0); // DeviceVerification.UNVERIFIED
|
||||
});
|
||||
}
|
||||
|
||||
function aliEnablesEncryption() {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobOneTimeKeys).toBeDefined();
|
||||
|
||||
aliQueryKeys().catch(utils.failTest);
|
||||
aliHttpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
|
||||
expect(content.one_time_keys[bobUserId][bobDeviceId]).toEqual("curve25519");
|
||||
for (var keyId in bobOneTimeKeys) {
|
||||
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf("curve25519:") === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
var result = {};
|
||||
result[bobUserId] = {};
|
||||
result[bobUserId][bobDeviceId] = {};
|
||||
result[bobUserId][bobDeviceId][keyId] = bobOneTimeKeys[keyId];
|
||||
return {one_time_keys: result};
|
||||
});
|
||||
var p = aliClient.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
}).then(function(res) {
|
||||
expect(res.missingUsers).toEqual([]);
|
||||
expect(res.missingDevices).toEqual({});
|
||||
expect(aliClient.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
});
|
||||
aliHttpBackend.flush();
|
||||
return p;
|
||||
}
|
||||
|
||||
function bobEnablesEncryption() {
|
||||
bobQueryKeys().catch(utils.failTest);
|
||||
return bobClient.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
}).then(function(res) {
|
||||
expect(res.missingUsers).toEqual([]);
|
||||
expect(res.missingDevices).toEqual({});
|
||||
expect(bobClient.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
function aliSendsMessage() {
|
||||
return sendMessage(aliHttpBackend, aliClient).then(function(content) {
|
||||
aliMessages.push(content);
|
||||
var ciphertext = content.ciphertext[bobDeviceCurve25519Key];
|
||||
expect(ciphertext).toBeDefined();
|
||||
});
|
||||
}
|
||||
|
||||
function bobSendsMessage() {
|
||||
return sendMessage(bobHttpBackend, bobClient).then(function(content) {
|
||||
bobMessages.push(content);
|
||||
var aliKeyId = "curve25519:" + aliDeviceId;
|
||||
var aliDeviceCurve25519Key = aliDeviceKeys.keys[aliKeyId];
|
||||
var ciphertext = content.ciphertext[aliDeviceCurve25519Key];
|
||||
expect(ciphertext).toBeDefined();
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
function sendMessage(httpBackend, client) {
|
||||
var path = "/send/m.room.encrypted/";
|
||||
var sent;
|
||||
httpBackend.when("PUT", path).respond(200, function(path, content) {
|
||||
sent = content;
|
||||
return {
|
||||
event_id: "asdfgh",
|
||||
};
|
||||
});
|
||||
var p1 = client.sendMessage(
|
||||
roomId, {msgtype: "m.text", body: "Hello, World"}
|
||||
);
|
||||
var p2 = httpBackend.flush(path, 1);
|
||||
return q.all([p1, p2]).then(function() {
|
||||
return sent;
|
||||
});
|
||||
}
|
||||
|
||||
function aliRecvMessage() {
|
||||
var message = bobMessages.shift();
|
||||
return recvMessage(aliHttpBackend, aliClient, message);
|
||||
}
|
||||
|
||||
function bobRecvMessage() {
|
||||
var message = aliMessages.shift();
|
||||
return recvMessage(bobHttpBackend, bobClient, message);
|
||||
}
|
||||
|
||||
function recvMessage(httpBackend, client, message) {
|
||||
var syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
var deferred = q.defer();
|
||||
var onEvent = function(event) {
|
||||
console.log(client.credentials.userId + " received event",
|
||||
event);
|
||||
|
||||
// ignore the m.room.member events
|
||||
if (event.getType() == "m.room.member") {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent()).toEqual({
|
||||
msgtype: "m.text",
|
||||
body: "Hello, World"
|
||||
});
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
|
||||
client.removeListener("event", onEvent);
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
client.on("event", onEvent);
|
||||
|
||||
httpBackend.flush();
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
|
||||
function aliStartClient() {
|
||||
expectAliKeyUpload().catch(utils.failTest);
|
||||
startClient(aliHttpBackend, aliClient);
|
||||
return aliHttpBackend.flush().then(function() {
|
||||
console.log("Ali client started");
|
||||
});
|
||||
}
|
||||
|
||||
function bobStartClient() {
|
||||
expectBobKeyUpload().catch(utils.failTest);
|
||||
startClient(bobHttpBackend, bobClient);
|
||||
return bobHttpBackend.flush().then(function() {
|
||||
console.log("Bob client started");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set http responses for the requests which are made when a client starts, and
|
||||
* start the client.
|
||||
*
|
||||
* @param {object} httpBackend
|
||||
* @param {MatrixClient} client
|
||||
*/
|
||||
function startClient(httpBackend, client) {
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
|
||||
// send a sync response including our test room.
|
||||
var syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: { }
|
||||
}
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
mship: "join",
|
||||
user: aliUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
mship: "join",
|
||||
user: bobUserId,
|
||||
}),
|
||||
]
|
||||
},
|
||||
timeline: {
|
||||
events: []
|
||||
}
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
}
|
||||
|
||||
|
||||
describe("MatrixClient crypto", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
var httpBackend;
|
||||
var aliClient;
|
||||
var roomId = "!room:localhost";
|
||||
var aliUserId = "@ali:localhost";
|
||||
var aliDeviceId = "zxcvb";
|
||||
var aliAccessToken = "aseukfgwef";
|
||||
var bobClient;
|
||||
var bobUserId = "@bob:localhost";
|
||||
var bobDeviceId = "bvcxz";
|
||||
var bobAccessToken = "fewgfkuesa";
|
||||
var bobOneTimeKeys;
|
||||
var bobDeviceKeys;
|
||||
var bobDeviceCurve25519Key;
|
||||
var bobDeviceEd25519Key;
|
||||
var aliLocalStore;
|
||||
var aliStorage;
|
||||
var bobStorage;
|
||||
var aliMessage;
|
||||
|
||||
beforeEach(function() {
|
||||
aliLocalStore = new MockStorageApi();
|
||||
aliStorage = new sdk.WebStorageSessionStore(aliLocalStore);
|
||||
bobStorage = new sdk.WebStorageSessionStore(new MockStorageApi());
|
||||
utils.beforeEach(this);
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
|
||||
aliHttpBackend = new HttpBackend();
|
||||
aliClient = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
baseUrl: "http://alis.server",
|
||||
userId: aliUserId,
|
||||
accessToken: aliAccessToken,
|
||||
deviceId: aliDeviceId,
|
||||
sessionStore: aliStorage
|
||||
sessionStore: aliStorage,
|
||||
request: aliHttpBackend.requestFn,
|
||||
});
|
||||
|
||||
bobHttpBackend = new HttpBackend();
|
||||
bobClient = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
baseUrl: "http://bobs.server",
|
||||
userId: bobUserId,
|
||||
accessToken: bobAccessToken,
|
||||
deviceId: bobDeviceId,
|
||||
sessionStore: bobStorage
|
||||
sessionStore: bobStorage,
|
||||
request: bobHttpBackend.requestFn,
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
bobOneTimeKeys = undefined;
|
||||
aliDeviceKeys = undefined;
|
||||
bobDeviceKeys = undefined;
|
||||
bobDeviceCurve25519Key = undefined;
|
||||
bobDeviceEd25519Key = undefined;
|
||||
aliMessages = [];
|
||||
bobMessages = [];
|
||||
});
|
||||
|
||||
describe("Ali account setup", function() {
|
||||
it("should have device keys", function(done) {
|
||||
expect(aliClient.deviceKeys).toBeDefined();
|
||||
expect(aliClient.deviceKeys.user_id).toEqual(aliUserId);
|
||||
expect(aliClient.deviceKeys.device_id).toEqual(aliDeviceId);
|
||||
done();
|
||||
});
|
||||
it("should have a curve25519 key", function(done) {
|
||||
expect(aliClient.deviceCurve25519Key).toBeDefined();
|
||||
done();
|
||||
});
|
||||
afterEach(function() {
|
||||
aliClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
});
|
||||
|
||||
function bobUploadsKeys(done) {
|
||||
var uploadPath = "/keys/upload/bvcxz";
|
||||
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
|
||||
expect(content.one_time_keys).toEqual({});
|
||||
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
bobDeviceKeys = content.device_keys;
|
||||
bobOneTimeKeys = content.one_time_keys;
|
||||
var count = 0;
|
||||
for (var key in content.one_time_keys) {
|
||||
if (content.one_time_keys.hasOwnProperty(key)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
expect(count).toEqual(5);
|
||||
return {one_time_key_counts: {curve25519: count}};
|
||||
});
|
||||
return {one_time_key_counts: {}};
|
||||
});
|
||||
bobClient.uploadKeys(5);
|
||||
httpBackend.flush().done(function() {
|
||||
expect(bobDeviceKeys).toBeDefined();
|
||||
expect(bobOneTimeKeys).toBeDefined();
|
||||
bobDeviceCurve25519Key = bobDeviceKeys.keys["curve25519:bvcxz"];
|
||||
bobDeviceEd25519Key = bobDeviceKeys.keys["ed25519:bvcxz"];
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
it("Bob uploads without one-time keys and with one-time keys", bobUploadsKeys);
|
||||
|
||||
function aliDownloadsKeys(done) {
|
||||
var bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
httpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
|
||||
expect(content.device_keys[bobUserId]).toEqual({});
|
||||
var result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
});
|
||||
aliClient.downloadKeys([bobUserId]).then(function() {
|
||||
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([{
|
||||
id: "bvcxz",
|
||||
key: bobDeviceEd25519Key
|
||||
}]);
|
||||
});
|
||||
httpBackend.flush().done(function() {
|
||||
var devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
|
||||
expect(devices).toEqual(bobKeys);
|
||||
done();
|
||||
});
|
||||
}
|
||||
it("Bob uploads without one-time keys and with one-time keys", function(done) {
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("Ali downloads Bobs keys", function(done) {
|
||||
bobUploadsKeys(function() {aliDownloadsKeys(done);});
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliDownloadsKeys)
|
||||
.catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
function aliEnablesEncryption(done) {
|
||||
httpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
|
||||
expect(content.one_time_keys[bobUserId][bobDeviceId]).toEqual("curve25519");
|
||||
for (var keyId in bobOneTimeKeys) {
|
||||
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf("curve25519:") === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
var result = {};
|
||||
result[bobUserId] = {};
|
||||
result[bobUserId][bobDeviceId] = {};
|
||||
result[bobUserId][bobDeviceId][keyId] = bobOneTimeKeys[keyId];
|
||||
return {one_time_keys: result};
|
||||
});
|
||||
aliClient.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
members: [aliUserId, bobUserId]
|
||||
}).then(function(res) {
|
||||
expect(res.missingUsers).toEqual([]);
|
||||
expect(res.missingDevices).toEqual({});
|
||||
expect(aliClient.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
httpBackend.flush();
|
||||
}
|
||||
it("Ali gets keys with an invalid signature", function(done) {
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(function() {
|
||||
// tamper bob's keys!
|
||||
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeDefined();
|
||||
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
|
||||
|
||||
return q.all(aliClient.downloadKeys([bobUserId]),
|
||||
aliQueryKeys());
|
||||
})
|
||||
.then(function() {
|
||||
// should get an empty list
|
||||
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
|
||||
})
|
||||
.catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("Ali enables encryption", function(done) {
|
||||
bobUploadsKeys(function() {
|
||||
aliDownloadsKeys(function() {
|
||||
aliEnablesEncryption(done);
|
||||
});
|
||||
});
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
function aliSendsMessage(done) {
|
||||
var txnId = "a.transaction.id";
|
||||
var path = "/send/m.room.encrypted/" + txnId;
|
||||
httpBackend.when("PUT", path).respond(200, function(path, content) {
|
||||
aliMessage = content;
|
||||
expect(aliMessage.ciphertext[bobDeviceCurve25519Key]).toBeDefined();
|
||||
return {};
|
||||
});
|
||||
aliClient.sendMessage(
|
||||
roomId, {msgtype: "m.text", body: "Hello, World"}, txnId
|
||||
);
|
||||
httpBackend.flush().done(function() {done();});
|
||||
}
|
||||
|
||||
it("Ali sends a message", function(done) {
|
||||
bobUploadsKeys(function() {
|
||||
aliDownloadsKeys(function() {
|
||||
aliEnablesEncryption(function() {
|
||||
aliSendsMessage(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsMessage)
|
||||
.catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
function bobRecvMessage(done) {
|
||||
var syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: aliMessage
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
bobClient.on("event", function(event) {
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent()).toEqual({
|
||||
msgtype: "m.text",
|
||||
body: "Hello, World"
|
||||
});
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
bobClient.startClient();
|
||||
httpBackend.flush();
|
||||
}
|
||||
|
||||
it("Bob receives a message", function(done) {
|
||||
bobUploadsKeys(function() {
|
||||
aliDownloadsKeys(function() {
|
||||
aliEnablesEncryption(function() {
|
||||
aliSendsMessage(function() {
|
||||
bobRecvMessage(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}, 30000); //timeout after 30s
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsMessage)
|
||||
.then(bobStartClient)
|
||||
.then(bobRecvMessage)
|
||||
.catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("Bob receives two pre-key messages", function(done) {
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsMessage)
|
||||
.then(bobStartClient)
|
||||
.then(bobRecvMessage)
|
||||
.then(aliSendsMessage)
|
||||
.then(bobRecvMessage)
|
||||
.catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("Bob replies to the message", function(done) {
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsMessage)
|
||||
.then(bobStartClient)
|
||||
.then(bobRecvMessage)
|
||||
.then(bobEnablesEncryption)
|
||||
.then(bobSendsMessage).then(function(ciphertext) {
|
||||
expect(ciphertext.type).toEqual(1);
|
||||
}).then(aliRecvMessage)
|
||||
.catch(utils.failTest).done(done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,10 +6,11 @@ var Room = publicGlobals.Room;
|
||||
var MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
|
||||
var Filter = publicGlobals.Filter;
|
||||
var utils = require("../test-utils");
|
||||
var MockStorageApi = require("../MockStorageApi");
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
var client, httpBackend, store;
|
||||
var client, httpBackend, store, sessionStore;
|
||||
var userId = "@alice:localhost";
|
||||
var accessToken = "aseukfgwef";
|
||||
|
||||
@@ -17,12 +18,18 @@ describe("MatrixClient", function() {
|
||||
utils.beforeEach(this);
|
||||
httpBackend = new HttpBackend();
|
||||
store = new MatrixInMemoryStore();
|
||||
|
||||
var mockStorage = new MockStorageApi();
|
||||
sessionStore = new sdk.WebStorageSessionStore(mockStorage);
|
||||
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
deviceId: "aliceDevice",
|
||||
accessToken: accessToken,
|
||||
store: store
|
||||
store: store,
|
||||
sessionStore: sessionStore,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -173,4 +180,101 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("downloadKeys", function() {
|
||||
it("should do an HTTP request and then store the keys", function(done) {
|
||||
var ed25519key = "wV5E3EUSHpHuoZLljNzojlabjGdXT3Mz7rugG9zgbkI";
|
||||
var borisKeys = {
|
||||
dev1: {
|
||||
algorithms: ["1"], keys: { "ed25519:dev1": ed25519key },
|
||||
signatures: {
|
||||
boris: {
|
||||
"ed25519:dev1":
|
||||
"u99n8WZ61G//K6eVgYc+RDLVapmjttxqhjNucIFGEIJ" +
|
||||
"oA4TUY8FmiGv3zl0EA71zrvPDfnFL5XLNsdc55NGbDg"
|
||||
}
|
||||
},
|
||||
unsigned: { "abc": "def" },
|
||||
}
|
||||
};
|
||||
var chazKeys = {
|
||||
dev2: {
|
||||
algorithms: ["2"], keys: { "ed25519:dev2": ed25519key },
|
||||
signatures: {
|
||||
chaz: {
|
||||
"ed25519:dev2":
|
||||
"8eaeXUWy9AQzjaNVOjVLs4FQk+cgobkNS811EjZBCMA" +
|
||||
"apd8aPOfE26E13nFFOCLC1V6fOH5wVo61hxGR/j4PBA"
|
||||
}
|
||||
},
|
||||
unsigned: { "ghi": "def" },
|
||||
}
|
||||
};
|
||||
var daveKeys = {
|
||||
dev3: {
|
||||
algorithms: ["3"], keys: { "ed25519:dev2": ed25519key },
|
||||
signatures: {
|
||||
dave: {
|
||||
"ed25519:dev2":
|
||||
"8eaeXUWy9AQzjaNVOjVLs4FQk+cgobkNS811EjZBCMA" +
|
||||
"apd8aPOfE26E13nFFOCLC1V6fOH5wVo61hxGR/j4PBA"
|
||||
}
|
||||
},
|
||||
unsigned: { "ghi": "def" },
|
||||
}
|
||||
};
|
||||
|
||||
httpBackend.when("POST", "/keys/query").check(function(req) {
|
||||
expect(req.data).toEqual({device_keys: {boris: {}, chaz: {}, dave: {}}});
|
||||
}).respond(200, {
|
||||
device_keys: {
|
||||
boris: borisKeys,
|
||||
chaz: chazKeys,
|
||||
dave: daveKeys,
|
||||
},
|
||||
});
|
||||
|
||||
client.downloadKeys(["boris", "chaz", "dave"]).then(function(res) {
|
||||
expect(res).toEqual({
|
||||
boris: {
|
||||
dev1: {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev1": ed25519key },
|
||||
algorithms: ["1"],
|
||||
unsigned: { "abc": "def" },
|
||||
},
|
||||
},
|
||||
chaz: {
|
||||
dev2: {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev2" : ed25519key },
|
||||
algorithms: ["2"],
|
||||
unsigned: { "ghi": "def" },
|
||||
},
|
||||
},
|
||||
dave: {
|
||||
// dave's key fails validation.
|
||||
},
|
||||
});
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should return a rejected promise if the request fails", function(done) {
|
||||
httpBackend.when("POST", "/keys/query").respond(400);
|
||||
|
||||
var exceptionThrown;
|
||||
client.downloadKeys(["bottom"]).then(function() {
|
||||
fail("download didn't fail");
|
||||
}, function(err) {
|
||||
exceptionThrown = err;
|
||||
}).then(function() {
|
||||
expect(exceptionThrown).toBeTruthy();
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use strict";
|
||||
var q = require("q");
|
||||
var sdk = require("../..");
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
@@ -66,21 +67,27 @@ describe("MatrixClient retrying", function() {
|
||||
ev2 = tl[1];
|
||||
|
||||
expect(ev1.status).toEqual(EventStatus.SENDING);
|
||||
expect(ev2.status).toEqual(EventStatus.QUEUED);
|
||||
expect(ev2.status).toEqual(EventStatus.SENDING);
|
||||
|
||||
// now we can cancel the second and check everything looks sane
|
||||
client.cancelPendingEvent(ev2);
|
||||
expect(ev2.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(tl.length).toEqual(1);
|
||||
// give the reactor a chance to run, so that ev2 gets queued
|
||||
q().then(function() {
|
||||
// ev2 should now have been queued
|
||||
expect(ev2.status).toEqual(EventStatus.QUEUED);
|
||||
|
||||
// shouldn't be able to cancel the first message yet
|
||||
expect(function() { client.cancelPendingEvent(ev1); })
|
||||
.toThrow();
|
||||
// now we can cancel the second and check everything looks sane
|
||||
client.cancelPendingEvent(ev2);
|
||||
expect(ev2.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
// fail the first send
|
||||
httpBackend.when("PUT", "/send/m.room.message/")
|
||||
.respond(400);
|
||||
httpBackend.flush().then(function() {
|
||||
// shouldn't be able to cancel the first message yet
|
||||
expect(function() { client.cancelPendingEvent(ev1); })
|
||||
.toThrow();
|
||||
|
||||
// fail the first send
|
||||
httpBackend.when("PUT", "/send/m.room.message/")
|
||||
.respond(400);
|
||||
return httpBackend.flush();
|
||||
}).then(function() {
|
||||
expect(ev1.status).toEqual(EventStatus.NOT_SENT);
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
|
||||
@@ -214,25 +214,25 @@ describe('NotificationService', function() {
|
||||
|
||||
it('should bing on a user ID.', function() {
|
||||
testEvent.event.content.body = "Hello @ali:matrix.org, how are you?";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on a partial user ID with an @.', function() {
|
||||
testEvent.event.content.body = "Hello @ali, how are you?";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on a partial user ID without @.', function() {
|
||||
testEvent.event.content.body = "Hello ali, how are you?";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on a case-insensitive user ID.', function() {
|
||||
testEvent.event.content.body = "Hello @AlI:matrix.org, how are you?";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
@@ -240,13 +240,13 @@ describe('NotificationService', function() {
|
||||
|
||||
it('should bing on a display name.', function() {
|
||||
testEvent.event.content.body = "Hello Alice M, how are you?";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on a case-insensitive display name.', function() {
|
||||
testEvent.event.content.body = "Hello ALICE M, how are you?";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
@@ -254,43 +254,43 @@ describe('NotificationService', function() {
|
||||
|
||||
it('should bing on a bing word.', function() {
|
||||
testEvent.event.content.body = "I really like coffee";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on case-insensitive bing words.', function() {
|
||||
testEvent.event.content.body = "Coffee is great";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on wildcard (.*) bing words.', function() {
|
||||
testEvent.event.content.body = "It was foomahbar I think.";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on character group ([abc]) bing words.', function() {
|
||||
testEvent.event.content.body = "Ping!";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
testEvent.event.content.body = "Pong!";
|
||||
actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on character range ([a-z]) bing words.', function() {
|
||||
testEvent.event.content.body = "I ate 6 pies";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on character negation ([!a]) bing words.', function() {
|
||||
testEvent.event.content.body = "boke";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
testEvent.event.content.body = "bake";
|
||||
actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(false);
|
||||
});
|
||||
|
||||
@@ -298,7 +298,7 @@ describe('NotificationService', function() {
|
||||
|
||||
it('should gracefully handle bad input.', function() {
|
||||
testEvent.event.content.body = { "foo": "bar" };
|
||||
var actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,6 +102,37 @@ describe("RoomMember", function() {
|
||||
member.setPowerLevelEvent(event); // no-op
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
|
||||
it("should honour power levels of zero.",
|
||||
function() {
|
||||
var event = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": 0,
|
||||
}
|
||||
},
|
||||
event: true
|
||||
});
|
||||
var emitCount = 0;
|
||||
|
||||
// set the power level to something other than zero or we
|
||||
// won't get an event
|
||||
member.powerLevel = 1;
|
||||
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember.userId).toEqual('@alice:bar');
|
||||
expect(emitMember.powerLevel).toEqual(0);
|
||||
expect(emitEvent).toEqual(event);
|
||||
});
|
||||
|
||||
member.setPowerLevelEvent(event);
|
||||
expect(member.powerLevel).toEqual(0);
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setTypingEvent", function() {
|
||||
|
||||
@@ -200,4 +200,64 @@ describe("utils", function() {
|
||||
assert.isFalse(utils.deepCompare({ a: { b: func } }, { a: { b: func2 } }));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("extend", function() {
|
||||
var SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
|
||||
|
||||
it("should extend", function() {
|
||||
var target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
var merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
var source_orig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(source_orig);
|
||||
});
|
||||
|
||||
it("should ignore null", function() {
|
||||
var target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
var merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
var source_orig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, null, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(source_orig);
|
||||
});
|
||||
|
||||
it("should handle properties created with defineProperties", function() {
|
||||
var source = Object.defineProperties({}, {
|
||||
"enumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
},
|
||||
enumerable: true
|
||||
},
|
||||
"nonenumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var target = {};
|
||||
utils.extend(target, source);
|
||||
expect(target.enumerableProp).toBe(true);
|
||||
expect(target.nonenumerableProp).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,36 +5,7 @@ var Room = sdk.Room;
|
||||
var User = sdk.User;
|
||||
var utils = require("../test-utils");
|
||||
|
||||
function MockStorageApi() {
|
||||
this.data = {};
|
||||
this.keys = [];
|
||||
this.length = 0;
|
||||
}
|
||||
MockStorageApi.prototype = {
|
||||
setItem: function(k, v) {
|
||||
this.data[k] = v;
|
||||
this._recalc();
|
||||
},
|
||||
getItem: function(k) {
|
||||
return this.data[k] || null;
|
||||
},
|
||||
removeItem: function(k) {
|
||||
delete this.data[k];
|
||||
this._recalc();
|
||||
},
|
||||
key: function(index) {
|
||||
return this.keys[index];
|
||||
},
|
||||
_recalc: function() {
|
||||
var keys = [];
|
||||
for (var k in this.data) {
|
||||
if (!this.data.hasOwnProperty(k)) { continue; }
|
||||
keys.push(k);
|
||||
}
|
||||
this.keys = keys;
|
||||
this.length = keys.length;
|
||||
}
|
||||
};
|
||||
var MockStorageApi = require("../MockStorageApi");
|
||||
|
||||
describe("WebStorageStore", function() {
|
||||
var store, room;
|
||||
|
||||
Reference in New Issue
Block a user