Merge branch 'develop' into matthew/filtered-timelines

This commit is contained in:
Matthew Hodgson
2016-08-28 18:49:54 +01:00
27 changed files with 80404 additions and 508 deletions
+103 -2
View File
@@ -1,11 +1,112 @@
Changes in \<unreleased>
=======================
Changes in [0.5.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.6) (2016-08-28)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.5...v0.5.6)
* Put all of the megolm keys in one room message
[\#182](https://github.com/matrix-org/matrix-js-sdk/pull/182)
* Reinstate device blocking for simple Olm
[\#181](https://github.com/matrix-org/matrix-js-sdk/pull/181)
* support for unpacking megolm keys
[\#180](https://github.com/matrix-org/matrix-js-sdk/pull/180)
* Send out megolm keys when we start a megolm session
[\#179](https://github.com/matrix-org/matrix-js-sdk/pull/179)
* Change the result structure for ensureOlmSessionsForUsers
[\#178](https://github.com/matrix-org/matrix-js-sdk/pull/178)
* Factor out a function for doing olm encryption
[\#177](https://github.com/matrix-org/matrix-js-sdk/pull/177)
* Move DeviceInfo and DeviceVerification to separate module
[\#175](https://github.com/matrix-org/matrix-js-sdk/pull/175)
* Make encryption asynchronous
[\#176](https://github.com/matrix-org/matrix-js-sdk/pull/176)
* Added ability to set and get status_msg for presence.
[\#167](https://github.com/matrix-org/matrix-js-sdk/pull/167)
* Megolm: don't dereference nullable object
[\#174](https://github.com/matrix-org/matrix-js-sdk/pull/174)
* Implement megolm encryption/decryption
[\#173](https://github.com/matrix-org/matrix-js-sdk/pull/173)
* Update our push rules when they come down stream
[\#170](https://github.com/matrix-org/matrix-js-sdk/pull/170)
* Factor Olm encryption/decryption out to new classes
[\#172](https://github.com/matrix-org/matrix-js-sdk/pull/172)
* Make DeviceInfo more useful, and refactor crypto methods to use it
[\#171](https://github.com/matrix-org/matrix-js-sdk/pull/171)
* Move login and register methods into base-apis
[\#169](https://github.com/matrix-org/matrix-js-sdk/pull/169)
* Remove defaultDeviceDisplayName from MatrixClient options
[\#168](https://github.com/matrix-org/matrix-js-sdk/pull/168)
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)
+37345
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+41222
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+241
View File
@@ -15,6 +15,12 @@ limitations under the License.
*/
"use strict";
/**
* olm.js wrapper
*
* @module OlmDevice
*/
var Olm = require("olm");
var utils = require("./utils");
@@ -25,6 +31,8 @@ var utils = require("./utils");
* Accounts and sessions are kept pickled in a sessionStore.
*
* @constructor
* @alias module:OlmDevice
*
* @param {Object} sessionStore A store to be used for data in end-to-end
* crypto
*
@@ -53,6 +61,10 @@ function OlmDevice(sessionStore) {
this.deviceCurve25519Key = e2eKeys.curve25519;
this.deviceEd25519Key = e2eKeys.ed25519;
// we don't bother stashing outboundgroupsessions in the sessionstore -
// instead we keep them here.
this._outboundGroupSessionStore = {};
}
@@ -282,6 +294,22 @@ OlmDevice.prototype.getSessionIdsForDevice = function(theirDeviceIdentityKey) {
return utils.keys(sessions);
};
/**
* Get the right olm session id for encrypting messages to the given identity key
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @return {string?} session id, or null if no established session
*/
OlmDevice.prototype.getSessionIdForDevice = function(theirDeviceIdentityKey) {
var sessionIds = this.getSessionIdsForDevice(theirDeviceIdentityKey);
if (sessionIds.length === 0) {
return null;
}
// Use the session with the lowest ID.
sessionIds.sort();
return sessionIds[0];
};
/**
* Encrypt an outgoing message using an existing session
@@ -350,6 +378,219 @@ OlmDevice.prototype.decryptMessage = function(
};
// Outbound group session
// ======================
/**
* store an OutboundGroupSession in _outboundGroupSessionStore
*
* @param {Olm.OutboundGroupSession} session
* @private
*/
OlmDevice.prototype._saveOutboundGroupSession = function(session) {
var pickledSession = session.pickle(this._pickleKey);
this._outboundGroupSessionStore[session.session_id()] = pickledSession;
};
/**
* extract an OutboundGroupSession from _outboundGroupSessionStore and call the
* given function
*
* @param {string} sessionId
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
var pickled = this._outboundGroupSessionStore[sessionId];
if (pickled === null) {
throw new Error("Unknown outbound group session " + sessionId);
}
var session = new Olm.OutboundGroupSession();
try {
session.unpickle(this._pickleKey, pickled);
return func(session);
} finally {
session.free();
}
};
/**
* Generate a new outbound group session
*
* @return {string} sessionId for the outbound session.
*/
OlmDevice.prototype.createOutboundGroupSession = function() {
var session = new Olm.OutboundGroupSession();
try {
session.create();
this._saveOutboundGroupSession(session);
return session.session_id();
} finally {
session.free();
}
};
/**
* Encrypt an outgoing message with an outbound group session
*
* @param {string} sessionId the id of the outboundgroupsession
* @param {string} payloadString payload to be encrypted and sent
*
* @return {string} ciphertext
*/
OlmDevice.prototype.encryptGroupMessage = function(sessionId, payloadString) {
var self = this;
return this._getOutboundGroupSession(sessionId, function(session) {
var res = session.encrypt(payloadString);
self._saveOutboundGroupSession(session);
return res;
});
};
/**
* Get the session keys for an outbound group session
*
* @param {string} sessionId the id of the outbound group session
*
* @return {{chain_index: number, key: string}} current chain index, and
* base64-encoded secret key.
*/
OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
return this._getOutboundGroupSession(sessionId, function(session) {
return {
chain_index: session.message_index(),
key: session.session_key(),
};
});
};
// Inbound group session
// =====================
/**
* store an InboundGroupSession in the session store
*
* @param {string} roomId
* @param {string} senderKey
* @param {string} sessionId
* @param {Olm.InboundGroupSession} session
* @private
*/
OlmDevice.prototype._saveInboundGroupSession = function(
roomId, senderKey, sessionId, session
) {
var r = {
room_id: roomId,
session: session.pickle(this._pickleKey),
};
this._sessionStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, JSON.stringify(r)
);
};
/**
* extract an InboundGroupSession from the session store and call the given function
*
* @param {string} roomId
* @param {string} senderKey
* @param {string} sessionId
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getInboundGroupSession = function(
roomId, senderKey, sessionId, func
) {
var r = this._sessionStore.getEndToEndInboundGroupSession(
senderKey, sessionId
);
if (r === null) {
throw new Error("Unknown inbound group session id");
}
r = JSON.parse(r);
// check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room.
if (roomId !== r.room_id) {
throw new Error(
"Mismatched room_id for inbound group session (expected " + r.room_id +
", was " + roomId + ")"
);
}
var session = new Olm.InboundGroupSession();
try {
session.unpickle(this._pickleKey, r.session);
return func(session);
} finally {
session.free();
}
};
/**
* Add an inbound group session to the session store
*
* @param {string} roomId room in which this session will be used
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {string} sessionKey base64-encoded secret key at index chainIndex
* @param {number} chainIndex index at which sessionKey applies
*/
OlmDevice.prototype.addInboundGroupSession = function(
roomId, senderKey, sessionId, sessionKey, chainIndex
) {
var self = this;
var session = new Olm.InboundGroupSession();
try {
session.create(chainIndex, sessionKey);
self._saveInboundGroupSession(roomId, senderKey, sessionId, session);
} finally {
session.free();
}
};
/**
* Decrypt a received message with an inbound group session
*
* @param {string} roomId room in which the message was received
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {string} body base64-encoded body of the encrypted message
*
* @return {string} plaintext
*/
OlmDevice.prototype.decryptGroupMessage = function(
roomId, senderKey, sessionId, body
) {
var self = this;
function decrypt(session) {
var res = session.decrypt(body);
self._saveInboundGroupSession(
roomId, senderKey, sessionId, session
);
return res;
}
return this._getInboundGroupSession(
roomId, senderKey, sessionId, decrypt
);
};
// Utilities
// =========
/**
* Verify an ed25519 signature.
*
+130
View File
@@ -104,6 +104,66 @@ MatrixBaseApis.prototype.isLoggedIn = function() {
// Registration/Login operations
// =============================
/**
* @param {string} username
* @param {string} password
* @param {string} sessionId
* @param {Object} auth
* @param {boolean} bindEmail
* @param {string} guestAccessToken
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.register = function(
username, password,
sessionId, auth, bindEmail, guestAccessToken,
callback
) {
if (auth === undefined) { auth = {}; }
if (sessionId) { auth.session = sessionId; }
var params = {
auth: auth
};
if (username !== undefined) { params.username = username; }
if (password !== undefined) { params.password = password; }
if (bindEmail !== undefined) { params.bind_email = bindEmail; }
if (guestAccessToken !== undefined) { params.guest_access_token = guestAccessToken; }
return this.registerRequest(params, undefined, callback);
};
/**
* Register a guest account.
* @param {Object=} opts Registration options
* @param {Object} opts.body JSON HTTP body to provide.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.registerGuest = function(opts, callback) {
opts = opts || {};
opts.body = opts.body || {};
return this.registerRequest(opts.body, "guest", callback);
};
/**
* @param {Object} data parameters for registration request
* @param {string=} kind type of user to register. may be "guest"
* @param {module:client.callback=} callback
* @return {module:client.Promise} Resolves: to the /register response
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.registerRequest = function(data, kind, callback) {
var params = {};
if (kind) { params.kind = kind; }
return this._http.request(
callback, "POST", "/register", params, data
);
};
/**
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
@@ -113,6 +173,76 @@ MatrixBaseApis.prototype.loginFlows = function(callback) {
return this._http.request(callback, "GET", "/login");
};
/**
* @param {string} loginType
* @param {Object} data
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.login = function(loginType, data, callback) {
var login_data = {
type: loginType,
};
// merge data into login_data
utils.extend(login_data, data);
return this._http.authedRequest(
callback, "POST", "/login", undefined, login_data
);
};
/**
* @param {string} user
* @param {string} 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.loginWithPassword = function(user, password, callback) {
return this.login("m.login.password", {
user: user,
password: password
}, callback);
};
/**
* @param {string} relayState URL Callback after SAML2 Authentication
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.loginWithSAML2 = function(relayState, callback) {
return this.login("m.login.saml2", {
relay_state: relayState
}, callback);
};
/**
* @param {string} redirectUrl The URL to redirect to after the HS
* authenticates with CAS.
* @return {string} The HS URL to hit to begin the CAS login process.
*/
MatrixBaseApis.prototype.getCasLoginUrl = function(redirectUrl) {
return this._http.getUrl("/login/cas/redirect", {
"redirectUrl": redirectUrl
}, httpApi.PREFIX_UNSTABLE);
};
/**
* @param {string} token Login token previously received from homeserver
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.loginWithToken = function(token, callback) {
return this.login("m.login.token", {
token: token
}, callback);
};
/**
* Logs out the current session.
* Obviously, further calls that require authorisation should fail after this
+58 -155
View File
@@ -78,9 +78,6 @@ try {
* tracking things like crypto keys and access tokens. If not specified,
* end-to-end crypto will be disabled.
*
* @param {string=} opts.defaultDeviceDisplayName Initial display name to set
* on new devices
*
* @param {Object=} opts.sessionStore A store to be used for end-to-end crypto
* session data. This should be a {@link
* module:store/session/webstorage~WebStorageSessionStore|WebStorageSessionStore},
@@ -109,7 +106,6 @@ function MatrixClient(opts) {
this.store = opts.store || new StubStore();
this.deviceId = opts.deviceId || null;
this.defaultDeviceDisplayName = opts.defaultDeviceDisplayName || "js-sdk device";
var userId = (opts.userId || null);
this.credentials = {
@@ -381,14 +377,13 @@ MatrixClient.prototype.isEventSenderVerified = function(event) {
return false;
}
var cryptoContent = event.getWireContent();
var sender_key = cryptoContent.sender_key;
var sender_key = event.getSenderKey();
if (!sender_key) {
return false;
}
var algorithm = cryptoContent.algorithm;
var algorithm = event.getWireContent().algorithm;
return this._crypto.isSenderKeyVerified(
event.getSender(), algorithm, sender_key
@@ -422,6 +417,20 @@ function onCryptoEvent(client, event) {
}
}
/**
* handle a room key event
*
* @private
*
* @param {MatrixEvent} event
*/
MatrixClient.prototype._onRoomKeyEvent = function(event) {
if (!this._crypto) {
return;
}
this._crypto.onRoomKeyEvent(event);
};
/**
* Enable end-to-end encryption for a room.
* @param {string} roomId The room ID to enable encryption in.
@@ -826,10 +835,18 @@ function _sendEvent(client, room, event, callback) {
// so that we can handle synchronous and asynchronous exceptions with the
// same code path.
return q().then(function() {
var encryptionPromise = null;
if (client._crypto) {
client._crypto.encryptEventIfNeeded(event, room);
encryptionPromise = client._crypto.encryptEventIfNeeded(event, room);
}
if (encryptionPromise) {
_updatePendingEventStatus(room, event, EventStatus.ENCRYPTING);
encryptionPromise = encryptionPromise.then(function() {
_updatePendingEventStatus(room, event, EventStatus.SENDING);
});
}
return encryptionPromise;
}).then(function() {
var promise;
// this event may be queued
if (client.scheduler) {
@@ -861,10 +878,14 @@ function _sendEvent(client, room, event, callback) {
// the request failed to send.
console.error("Error sending event", err.stack || err);
_updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
try {
_updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
if (callback) {
callback(err);
if (callback) {
callback(err);
}
} catch (err2) {
console.error("Exception in error handler!", err2.stack || err);
}
throw err;
});
@@ -1417,25 +1438,29 @@ MatrixClient.prototype.mxcUrlToHttp =
};
/**
* @param {string} presence
* @param {Object} opts Options to apply
* @param {string} opts.presence One of "online", "offline" or "unavailable"
* @param {string} opts.status_msg The status message to attach.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws If 'presence' isn't a valid presence enum value.
*/
MatrixClient.prototype.setPresence = function(presence, callback) {
MatrixClient.prototype.setPresence = function(opts, callback) {
var path = utils.encodeUri("/presence/$userId/status", {
$userId: this.credentials.userId
});
var validStates = ["offline", "online", "unavailable"];
if (validStates.indexOf(presence) == -1) {
throw new Error("Bad presence value: " + presence);
if (typeof opts === "string") {
opts = { presence: opts };
}
var validStates = ["offline", "online", "unavailable"];
if (validStates.indexOf(opts.presence) == -1) {
throw new Error("Bad presence value: " + opts.presence);
}
var content = {
presence: presence
};
return this._http.authedRequest(
callback, "PUT", path, undefined, content
callback, "PUT", path, undefined, opts
);
};
@@ -1737,47 +1762,6 @@ MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
return promise;
};
// Registration/Login operations
// =============================
/**
* @param {string} loginType
* @param {Object} data
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.login = function(loginType, data, callback) {
var login_data = {
type: loginType,
device_id: this.deviceId,
initial_device_display_name: this.defaultDeviceDisplayName,
};
// merge data into login_data
for (var k in data) {
if (data.hasOwnProperty(k)) { login_data[k] = data[k]; }
}
return this._http.authedRequest(
callback, "POST", "/login", undefined, login_data
);
};
/**
* Register a guest account.
* @param {Object=} opts Registration options
* @param {Object} opts.body JSON HTTP body to provide.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.registerGuest = function(opts, callback) {
opts = opts || {};
opts.body = opts.body || {};
return this.registerRequest(opts.body, "guest", callback);
};
/**
* Peek into a room and receive updates about the room. This only works if the
* history visibility for the room is world_readable.
@@ -1831,49 +1815,8 @@ MatrixClient.prototype.setGuestAccess = function(roomId, opts) {
return q.all(readPromise, writePromise);
};
/**
* @param {string} username
* @param {string} password
* @param {string} sessionId
* @param {Object} auth
* @param {boolean} bindEmail
* @param {string} guestAccessToken
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.register = function(username, password,
sessionId, auth, bindEmail, guestAccessToken,
callback) {
if (auth === undefined) { auth = {}; }
if (sessionId) { auth.session = sessionId; }
var params = {
auth: auth
};
if (username !== undefined) { params.username = username; }
if (password !== undefined) { params.password = password; }
if (bindEmail !== undefined) { params.bind_email = bindEmail; }
if (guestAccessToken !== undefined) { params.guest_access_token = guestAccessToken; }
return this.registerRequest(params, undefined, callback);
};
/**
* @param {Object} data parameters for registration request
* @param {string=} kind type of user to register. may be "guest"
* @param {module:client.callback=} callback
* @return {module:client.Promise} Resolves: to the /register response
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.registerRequest = function(data, kind, callback) {
var params = {};
if (kind) { params.kind = kind; }
return this._http.request(
callback, "POST", "/register", params, data
);
};
// Registration/Login operations
// =============================
/**
* Requests an email verification token for the purposes of registration.
@@ -1991,54 +1934,6 @@ MatrixClient.prototype._requestTokenFromEndpoint = function(endpoint,
);
};
/**
* @param {string} user
* @param {string} password
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.loginWithPassword = function(user, password, callback) {
return this.login("m.login.password", {
user: user,
password: password
}, callback);
};
/**
* @param {string} relayState URL Callback after SAML2 Authentication
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.loginWithSAML2 = function(relayState, callback) {
return this.login("m.login.saml2", {
relay_state: relayState
}, callback);
};
/**
* @param {string} redirectUrl The URL to redirect to after the HS
* authenticates with CAS.
* @return {string} The HS URL to hit to begin the CAS login process.
*/
MatrixClient.prototype.getCasLoginUrl = function(redirectUrl) {
return this._http.getUrl("/login/cas/redirect", {
"redirectUrl": redirectUrl
}, httpApi.PREFIX_UNSTABLE);
};
/**
* @param {string} token Login token previously received from homeserver
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.loginWithToken = function(token, callback) {
return this.login("m.login.token", {
token: token
}, callback);
};
// Push operations
// ===============
@@ -2449,7 +2344,7 @@ MatrixClient.prototype.getTurnServers = function() {
* appear in a room's timeline. If "<b>chronological</b>", messages will appear
* in the timeline when the call to <code>sendEvent</code> was made. If
* "<b>detached</b>", pending messages will appear in a separate list,
* accessbile via {@link module:models/room~Room#getPendingEvents}. Default:
* accessbile via {@link module:models/room#getPendingEvents}. Default:
* "chronological".
*
* @param {Number=} opts.pollTimeout The number of milliseconds to wait on /events.
@@ -2746,7 +2641,15 @@ function _PojoToMatrixEventMapper(client) {
if (plainOldJsObject.type === "m.room.encrypted") {
clearData = _decryptMessage(client, plainOldJsObject);
}
return new MatrixEvent(plainOldJsObject, clearData);
var matrixEvent = new MatrixEvent(plainOldJsObject, clearData);
// XXXX massive hack to deal with the fact that megolm keys are in the
// room for now, and we need to handle them before attempting to
// decrypt the following megolm messages.
if (matrixEvent.getType() == "m.room_key") {
client._onRoomKeyEvent(matrixEvent);
}
return matrixEvent;
}
return mapper;
}
+162
View File
@@ -0,0 +1,162 @@
/*
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. Defines the base classes of the encryption implementations
*
* @module crypto-algorithms/base
*/
var q = require("q");
var utils = require("../utils");
/**
* map of registered encryption algorithm classes. A map from string to {@link
* module:crypto-algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} class
*
* @type {Object.<string, function(new: module:crypto-algorithms/base.EncryptionAlgorithm)>}
*/
module.exports.ENCRYPTION_CLASSES = {};
/**
* map of registered encryption algorithm classes. Map from string to {@link
* module:crypto-algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} class
*
* @type {Object.<string, function(new: module:crypto-algorithms/base.DecryptionAlgorithm)>}
*/
module.exports.DECRYPTION_CLASSES = {};
/**
* base type for encryption implementations
*
* @constructor
* @alias module:crypto-algorithms/base.EncryptionAlgorithm
*
* @param {object} params parameters
* @param {string} params.deviceId The identifier for this device.
* @param {module:crypto} params.crypto crypto core
* @param {module:OlmDevice} params.olmDevice olm.js wrapper
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
* @param {string} params.roomId The ID of the room we will be sending to
*/
var EncryptionAlgorithm = function(params) {
this._deviceId = params.deviceId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._baseApis = params.baseApis;
this._roomId = params.roomId;
};
/** */
module.exports.EncryptionAlgorithm = EncryptionAlgorithm;
/**
* Initialise this EncryptionAlgorithm instance for a particular room.
*
* <p>This will be called once per EncryptionAlgorithm, just after the
* constructor is called.
*
* @param {string[]} roomMembers list of currently-joined users in the room
* @return {module:client.Promise} Promise which resolves when setup is complete
*/
EncryptionAlgorithm.prototype.initRoomEncryption = function(roomMembers) {
return q();
};
/**
* Encrypt a message event
*
* @method module:crypto-algorithms/base.EncryptionAlgorithm#encryptMessage
* @abstract
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
/**
* base type for decryption implementations
*
* @constructor
* @alias module:crypto-algorithms/base.DecryptionAlgorithm
*
* @param {object} params parameters
* @param {module:OlmDevice} params.olmDevice olm.js wrapper
*/
var DecryptionAlgorithm = function(params) {
this._olmDevice = params.olmDevice;
};
/** */
module.exports.DecryptionAlgorithm = DecryptionAlgorithm;
/**
* Decrypt an event
*
* @method module:crypto-algorithms/base.DecryptionAlgorithm#decryptEvent
* @abstract
*
* @param {object} event raw event
*
* @return {object} decrypted payload (with properties 'type', 'content')
*
* @throws {module:crypto-algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
/**
* Handle a key event
*
* @method module:crypto-algorithms/base.DecryptionAlgorithm#onRoomKeyEvent
*
* @param {module:modules/event~MatrixEvent} event key event
*/
DecryptionAlgorithm.prototype.onRoomKeyEvent = function(params) {
// ignore by default
};
/**
* Exception thrown when decryption fails
*
* @constructor
* @param {string} msg message describing the problem
* @extends Error
*/
module.exports.DecryptionError = function(msg) {
this.message = msg;
};
utils.inherits(module.exports.DecryptionError, Error);
/**
* Registers an encryption/decryption class for a particular algorithm
*
* @param {string} algorithm algorithm tag to register for
*
* @param {class} encryptor {@link
* module:crypto-algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm}
* implementation
*
* @param {class} decryptor {@link
* module:crypto-algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm}
* implementation
*/
module.exports.registerAlgorithm = function(algorithm, encryptor, decryptor) {
module.exports.ENCRYPTION_CLASSES[algorithm] = encryptor;
module.exports.DECRYPTION_CLASSES[algorithm] = decryptor;
};
+40
View File
@@ -0,0 +1,40 @@
/*
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";
/**
* @module crypto-algorithms
*/
var base = require("./base");
require("./olm");
require("./megolm");
/**
* @see module:crypto-algorithms/base.ENCRYPTION_CLASSES
*/
module.exports.ENCRYPTION_CLASSES = base.ENCRYPTION_CLASSES;
/**
* @see module:crypto-algorithms/base.DECRYPTION_CLASSES
*/
module.exports.DECRYPTION_CLASSES = base.DECRYPTION_CLASSES;
/**
* @see module:crypto-algorithms/base.DecryptionError
*/
module.exports.DecryptionError = base.DecryptionError;
+257
View File
@@ -0,0 +1,257 @@
/*
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";
/**
* Defines m.olm encryption/decryption
*
* @module crypto-algorithms/megolm
*/
var q = require("q");
var utils = require("../utils");
var olmlib = require("../olmlib");
var base = require("./base");
/**
* Megolm encryption implementation
*
* @constructor
* @extends {module:crypto-algorithms/base.EncryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto-algorithms/base.EncryptionAlgorithm}
*/
function MegolmEncryption(params) {
base.EncryptionAlgorithm.call(this, params);
this._prepPromise = null;
this._outboundSessionId = null;
}
utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
/**
* @private
*
* @param {module:models/room} room
*
* @return {module:client.Promise} Promise which resolves when setup is
* complete.
*/
MegolmEncryption.prototype._ensureOutboundSession = function(room) {
if (this._prepPromise) {
// prep already in progress
return this._prepPromise;
}
if (this._outboundSessionId) {
// prep already done
return q();
}
var session_id = this._olmDevice.createOutboundGroupSession();
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
this._olmDevice.addInboundGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, session_id,
key.key, key.chain_index
);
// send the keys to each (unblocked) device in the room.
var payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: session_id,
session_key: key.key,
chain_index: key.chain_index,
}
};
var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
var self = this;
// TODO: we need to give the user a chance to block any devices or users
// before we send them the keys; it's too late to download them here.
this._prepPromise = this._crypto.downloadKeys(
roomMembers, false
).then(function(res) {
return self._crypto.ensureOlmSessionsForUsers(roomMembers);
}).then(function(devicemap) {
// TODO: send OOB messages. for now, send an in-band message. Each
// encrypted copy of the key takes up about 1K, so we'll only manage
// about 60 copies before we hit the event size limit; but ultimately the
// OOB messaging API will solve that problem for us.
var participantKeys = [];
for (var userId in devicemap) {
if (!devicemap.hasOwnProperty(userId)) {
continue;
}
var devices = devicemap[userId];
for (var deviceId in devices) {
if (!devices.hasOwnProperty(deviceId)) {
continue;
}
var deviceInfo = devices[deviceId].device;
participantKeys.push(deviceInfo.getIdentityKey());
}
}
var encryptedContent = olmlib.encryptMessageForDevices(
self._deviceId,
self._olmDevice,
participantKeys,
payload
);
var txnId = '' + (new Date().getTime());
var path = utils.encodeUri(
"/rooms/$roomId/send/m.room.encrypted/$txnId", {
$roomId: self._roomId,
$txnId: txnId,
}
);
// TODO: retries
return self._baseApis._http.authedRequest(
undefined, "PUT", path, undefined, encryptedContent
);
}).then(function() {
// don't set this until the keys are sent successfully; if we get an
// error, the user can restart by resending the message.
self._outboundSessionId = session_id;
}).finally(function() {
self._prepPromise = null;
});
return this._prepPromise;
};
/**
* @inheritdoc
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
var self = this;
return this._ensureOutboundSession(room).then(function() {
var payloadJson = {
room_id: self._roomId,
type: eventType,
content: content
};
var ciphertext = self._olmDevice.encryptGroupMessage(
self._outboundSessionId, JSON.stringify(payloadJson)
);
var encryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
body: ciphertext,
session_id: self._outboundSessionId,
signature: "FIXME",
};
return encryptedContent;
});
};
/**
* Megolm decryption implementation
*
* @constructor
* @extends {module:crypto-algorithms/base.DecryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto-algorithms/base.DecryptionAlgorithm}
*/
function MegolmDecryption(params) {
base.DecryptionAlgorithm.call(this, params);
}
utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
/**
* @inheritdoc
*
* @param {object} event raw event
*
* @return {object} decrypted payload (with properties 'type', 'content')
*
* @throws {module:crypto-algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
MegolmDecryption.prototype.decryptEvent = function(event) {
var content = event.content;
console.log("decrypting " + event.event_id + " with sid " +
content.session_id);
if (!content.sender_key || !content.session_id ||
!content.body || !content.signature
) {
throw new base.DecryptionError("Missing fields in input");
}
try {
var res = this._olmDevice.decryptGroupMessage(
event.room_id, content.sender_key, content.session_id, content.body
);
return JSON.parse(res);
} catch (e) {
throw new base.DecryptionError(e);
}
};
/**
* @inheritdoc
*
* @param {module:modules/event~MatrixEvent} event key event
*/
MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
console.log("Adding key from ", event);
var content = event.getContent();
if (!content.room_id ||
!content.session_id ||
!content.session_key ||
content.chain_index === undefined
) {
console.error("key event is missing fields");
return;
}
this._olmDevice.addInboundGroupSession(
content.room_id, event.getSenderKey(), content.session_id,
content.session_key, content.chain_index
);
};
base.registerAlgorithm(
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption
);
+188
View File
@@ -0,0 +1,188 @@
/*
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";
/**
* Defines m.olm encryption/decryption
*
* @module crypto-algorithms/olm
*/
var q = require('q');
var utils = require("../utils");
var olmlib = require("../olmlib");
var DeviceInfo = require("../crypto-deviceinfo");
var DeviceVerification = DeviceInfo.DeviceVerification;
var base = require("./base");
/**
* Olm encryption implementation
*
* @constructor
* @extends {module:crypto-algorithms/base.EncryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto-algorithms/base.EncryptionAlgorithm}
*/
function OlmEncryption(params) {
base.EncryptionAlgorithm.call(this, params);
}
utils.inherits(OlmEncryption, base.EncryptionAlgorithm);
/**
* @inheritdoc
* @param {string[]} roomMembers list of currently-joined users in the room
* @return {module:client.Promise} Promise which resolves when setup is complete
*/
OlmEncryption.prototype.initRoomEncryption = function(roomMembers) {
var crypto = this._crypto;
return crypto.downloadKeys(roomMembers, true).then(function(res) {
return crypto.ensureOlmSessionsForUsers(roomMembers);
});
};
/**
* @inheritdoc
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
// 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._crypto.getStoredDevicesForUser(userId);
for (var j = 0; j < devices.length; ++j) {
var deviceInfo = devices[j];
var key = deviceInfo.getIdentityKey();
if (key == this._olmDevice.deviceCurve25519Key) {
// don't bother setting up session to ourself
continue;
}
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
// don't bother setting up sessions with blocked users
continue;
}
participantKeys.push(key);
}
}
return q(
olmlib.encryptMessageForDevices(
this._deviceId, this._olmDevice, participantKeys, {
room_id: room.roomId,
type: eventType,
content: content,
}
)
);
};
/**
* Olm decryption implementation
*
* @constructor
* @extends {module:crypto-algorithms/base.DecryptionAlgorithm}
* @param {object} params parameters, as per
* {@link module:crypto-algorithms/base.DecryptionAlgorithm}
*/
function OlmDecryption(params) {
base.DecryptionAlgorithm.call(this, params);
}
utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
/**
* @inheritdoc
*
* @param {object} event raw event
*
* @return {object} decrypted payload (with properties 'type', 'content')
*
* @throws {module:crypto-algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
OlmDecryption.prototype.decryptEvent = function(event) {
var content = event.content;
var deviceKey = content.sender_key;
var ciphertext = content.ciphertext;
if (!ciphertext) {
throw new base.DecryptionError("Missing ciphertext");
}
if (!(this._olmDevice.deviceCurve25519Key in content.ciphertext)) {
throw new base.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.
// TODO: check the room_id and fingerprint
if (payloadString !== null) {
return JSON.parse(payloadString);
} else {
throw new base.DecryptionError("Bad Encrypted Message");
}
};
base.registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
+127
View File
@@ -0,0 +1,127 @@
/*
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";
/**
* @module crypto-deviceinfo
*/
/**
* Information about a user's device
*
* @constructor
* @alias module:crypto-deviceinfo
*
* @property {string} deviceId the ID of this device
*
* @property {string[]} algorithms list of algorithms supported by this device
*
* @property {Object.<string,string>} keys a map from
* &lt;key type&gt;:&lt;id&gt; -> &lt;base64-encoded key&gt;>
*
* @property {module:crypto-deviceinfo.DeviceVerification} verified
* whether the device has been verified by the user
*
* @property {Object} unsigned additional data from the homeserver
*
* @param {string} deviceId id of the device
*/
function DeviceInfo(deviceId) {
// you can't change the deviceId
Object.defineProperty(this, 'deviceId', {
enumerable: true,
value: deviceId,
});
this.algorithms = [];
this.keys = {};
this.verified = DeviceVerification.UNVERIFIED;
this.unsigned = {};
}
/**
* rehydrate a DeviceInfo from the session store
*
* @param {object} obj raw object from session store
* @param {string} deviceId id of the device
*
* @return {module:crypto~DeviceInfo} new DeviceInfo
*/
DeviceInfo.fromStorage = function(obj, deviceId) {
var res = new DeviceInfo(deviceId);
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
res[prop] = obj[prop];
}
}
return res;
};
/**
* Prepare a DeviceInfo for JSON serialisation in the session store
*
* @return {object} deviceinfo with non-serialised members removed
*/
DeviceInfo.prototype.toStorage = function() {
return {
algorithms: this.algorithms,
keys: this.keys,
verified: this.verified,
unsigned: this.unsigned,
};
};
/**
* Get the fingerprint for this device (ie, the Ed25519 key)
*
* @return {string} base64-encoded fingerprint of this device
*/
DeviceInfo.prototype.getFingerprint = function() {
return this.keys["ed25519:" + this.deviceId];
};
/**
* Get the identity key for this device (ie, the Curve25519 key)
*
* @return {string} base64-encoded identity key of this device
*/
DeviceInfo.prototype.getIdentityKey = function() {
return this.keys["curve25519:" + this.deviceId];
};
/**
* Get the configured display name for this device, if any
*
* @return {string?} displayname
*/
DeviceInfo.prototype.getDisplayname = function() {
return this.unsigned.device_display_name || null;
};
/**
* @enum
*/
DeviceInfo.DeviceVerification = {
VERIFIED: 1,
UNVERIFIED: 0,
BLOCKED: -1,
};
var DeviceVerification = DeviceInfo.DeviceVerification;
/** */
module.exports = DeviceInfo;
+232 -280
View File
@@ -15,45 +15,28 @@ 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 olmlib = require("./olmlib");
var OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
var algorithms = require("./crypto-algorithms");
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 &lt;key type&gt;:&lt;id&gt; -> key
*
* @property {DeviceVerification} verified whether the device has been
* verified by the user
*
* @property {Object} unsigned additional data from the homeserver
*/
var DeviceInfo = require("./crypto-deviceinfo");
var DeviceVerification = DeviceInfo.DeviceVerification;
/**
* Cryptography bits
*
* @alias module:crypto.Crypto
* @constructor
* @alias module:crypto
*
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
*
@@ -70,10 +53,10 @@ function Crypto(baseApis, sessionStore, userId, deviceId) {
this._userId = userId;
this._deviceId = deviceId;
this._cryptoAlgorithms = [];
this._olmDevice = new OlmDevice(sessionStore);
this._cryptoAlgorithms = [OLM_ALGORITHM];
// EncryptionAlgorithm instance for each room
this._roomAlgorithms = {};
// build our device keys: these will later be uploaded
this._deviceKeys = {};
@@ -85,7 +68,7 @@ function Crypto(baseApis, sessionStore, userId, deviceId) {
// add our own deviceinfo to the sessionstore
var deviceInfo = {
keys: this._deviceKeys,
algorithms: this._cryptoAlgorithms,
algorithms: [olmlib.OLM_ALGORITHM],
verified: DeviceVerification.VERIFIED,
};
var myDevices = this._sessionStore.getEndToEndDevicesForUser(
@@ -138,7 +121,7 @@ function _uploadDeviceKeys(crypto) {
var deviceId = crypto._deviceId;
var deviceKeys = {
algorithms: crypto._cryptoAlgorithms,
algorithms: [olmlib.OLM_ALGORITHM],
device_id: deviceId,
keys: crypto._deviceKeys,
user_id: userId,
@@ -187,22 +170,30 @@ function _uploadOneTimeKeys(crypto) {
* @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}.
* module:crypto-deviceinfo|DeviceInfo}.
*/
Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
var self = this;
// map from userid -> deviceid -> DeviceInfo
var stored = {};
// list of userids we need to download keys for
var downloadUsers = [];
for (var i = 0; i < userIds.length; ++i) {
var userId = userIds[i];
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
stored[userId] = {};
stored[userId] = devices || {};
if (devices && !forceDownload) {
continue;
var devices = this.getStoredDevicesForUser(userId);
for (var j = 0; j < devices.length; ++j) {
var dev = devices[j];
stored[userId][dev.deviceId] = dev;
}
if (devices.length === 0 || forceDownload) {
downloadUsers.push(userId);
}
downloadUsers.push(userId);
}
if (downloadUsers.length === 0) {
@@ -218,14 +209,26 @@ Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
continue;
}
// map from deviceid -> deviceinfo for this user
var userStore = stored[userId];
var updated = _updateStoredDeviceKeysForUser(
self._olmDevice, userId, userStore, res.device_keys[userId]
);
if (updated) {
if (!updated) {
continue;
}
// update the session store
var storage = {};
for (var deviceId in userStore) {
if (!userStore.hasOwnProperty(deviceId)) {
continue;
}
storage[deviceId] = userStore[deviceId].toStorage();
self._sessionStore.storeEndToEndDevicesForUser(
userId, userStore
userId, storage
);
}
}
@@ -295,13 +298,12 @@ function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult)
return false;
}
// prepare the canonical json: remove 'unsigned' and sigxsnatures, and
// prepare the canonical json: remove 'unsigned' and signatures, 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) {
@@ -310,12 +312,14 @@ function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult)
return false;
}
// DeviceInfo
var deviceStore;
if (deviceId in userStore) {
// already have this device.
deviceStore = userStore[deviceId];
if (deviceStore.keys["ed25519:" + deviceId] != signKey) {
if (deviceStore.getFingerprint() != signKey) {
// this should only happen if the list has been MITMed; we are
// best off sticking with the original keys.
//
@@ -325,9 +329,7 @@ function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult)
return false;
}
} else {
userStore[deviceId] = deviceStore = {
verified: DeviceVerification.UNVERIFIED
};
userStore[deviceId] = deviceStore = new DeviceInfo(deviceId);
}
deviceStore.keys = deviceResult.keys;
@@ -337,42 +339,64 @@ function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult)
}
/**
* Get the stored device keys for a user id
*
* @param {string} userId the user to list keys for.
*
* @return {module:crypto-deviceinfo[]} list of devices
*/
Crypto.prototype.getStoredDevicesForUser = function(userId) {
var devs = this._sessionStore.getEndToEndDevicesForUser(userId);
if (!devs) {
return [];
}
var res = [];
for (var deviceId in devs) {
if (devs.hasOwnProperty(deviceId)) {
res.push(DeviceInfo.fromStorage(devs[deviceId], deviceId));
}
}
return res;
};
/**
* List the stored device keys for a user id
*
* @deprecated prefer {@link module:crypto#getStoredDevicesForUser}
*
* @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 devices = this.getStoredDevicesForUser(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,
});
}
for (var i = 0; i < devices.length; ++i) {
var device = devices[i];
var ed25519Key = device.getFingerprint();
if (ed25519Key) {
result.push({
id: device.deviceId,
key: ed25519Key,
verified: Boolean(device.verified == DeviceVerification.VERIFIED),
blocked: Boolean(device.verified == DeviceVerification.BLOCKED),
display_name: device.getDisplayname(),
});
}
}
// sort by deviceid
result.sort(function(a, b) {
if (a.deviceId < b.deviceId) { return -1; }
if (a.deviceId > b.deviceId) { return 1; }
return 0;
});
return result;
};
@@ -383,10 +407,10 @@ Crypto.prototype.listDeviceKeys = function(userId) {
* @param {string} algorithm encryption algorithm
* @param {string} sender_key curve25519 key to match
*
* @return {module:crypto~DeviceInfo?}
* @return {module:crypto-deviceinfo?}
*/
Crypto.prototype.getDeviceByIdentityKey = function(userId, algorithm, sender_key) {
if (algorithm !== OLM_ALGORITHM) {
if (algorithm !== olmlib.OLM_ALGORITHM) {
// we only deal in olm keys
return null;
}
@@ -411,7 +435,7 @@ Crypto.prototype.getDeviceByIdentityKey = function(userId, algorithm, sender_key
}
var deviceKey = device.keys[keyId];
if (deviceKey == sender_key) {
return device;
return DeviceInfo.fromStorage(device, deviceId);
}
}
}
@@ -487,11 +511,9 @@ Crypto.prototype.isSenderKeyVerified = function(userId, algorithm, sender_key) {
* @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.
* @return {module:client.Promise} 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);
@@ -499,77 +521,107 @@ Crypto.prototype.setRoomEncryption = function(roomId, config, roomMembers) {
if (JSON.stringify(existingConfig) != JSON.stringify(config)) {
console.error("Ignoring m.room.encryption event which requests " +
"a change of config in " + roomId);
return;
return q();
}
}
if (config.algorithm !== OLM_ALGORITHM) {
throw new Error("Unknown algorithm: " + config.algorithm);
var AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm];
if (!AlgClass) {
throw new Error("Unable to encrypt with " + config.algorithm);
}
// remove spurious keys
config = {
algorithm: OLM_ALGORITHM,
algorithm: config.algorithm,
};
this._sessionStore.storeEndToEndRoom(roomId, config);
return self.downloadKeys(roomMembers, true).then(function(res) {
return self._ensureOlmSessionsForUsers(roomMembers);
var alg = new AlgClass({
deviceId: this._deviceId,
crypto: this,
olmDevice: this._olmDevice,
baseApis: this._baseApis,
roomId: roomId,
});
this._roomAlgorithms[roomId] = alg;
return alg.initRoomEncryption(roomMembers);
};
/**
* @typedef {Object} module:crypto~OlmSessionResult
* @property {module:crypto-deviceinfo} device device info
* @property {string?} sessionId base64 olm session id; null if no session
* could be established
*/
/**
* 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
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
*/
Crypto.prototype._ensureOlmSessionsForUsers = function(users) {
var devicesWithoutSession = [];
var userWithoutDevices = [];
Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
var devicesWithoutSession = [
// [userId, deviceId, deviceInfo], ...
];
var result = {};
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]);
}
}
result[userId] = {};
var devices = this.getStoredDevicesForUser(userId);
for (var j = 0; j < devices.length; ++j) {
var deviceInfo = devices[j];
var deviceId = deviceInfo.deviceId;
var key = deviceInfo.getIdentityKey();
if (key == this._olmDevice.deviceCurve25519Key) {
// don't bother setting up session to ourself
continue;
}
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
// don't bother setting up sessions with blocked users
continue;
}
var sessionId = this._olmDevice.getSessionIdForDevice(key);
if (sessionId === null) {
devicesWithoutSession.push([userId, deviceId, deviceInfo]);
}
result[userId][deviceId] = {
device: deviceInfo,
sessionId: sessionId,
};
}
}
if (devicesWithoutSession.length === 0) {
return q({
missingUsers: userWithoutDevices,
missingDevices: []
});
return q(result);
}
// TODO: this has a race condition - if we try to send another message
// while we are claiming a key, we will end up claiming two and setting up
// two sessions.
//
// That should eventually resolve itself, but it's poor form.
var self = this;
return this._baseApis.claimOneTimeKeys(
devicesWithoutSession
).then(function(res) {
var missing = {};
for (i = 0; i < devicesWithoutSession.length; ++i) {
for (var i = 0; i < devicesWithoutSession.length; ++i) {
var device = devicesWithoutSession[i];
var userRes = res.one_time_keys[device[0]] || {};
var deviceRes = userRes[device[1]];
var userId = device[0];
var deviceId = device[1];
var deviceInfo = device[2];
var userRes = res.one_time_keys[userId] || {};
var deviceRes = userRes[deviceId];
var oneTimeKey;
for (var keyId in deviceRes) {
if (keyId.indexOf("curve25519:") === 0) {
@@ -578,20 +630,18 @@ Crypto.prototype._ensureOlmSessionsForUsers = function(users) {
}
if (oneTimeKey) {
var sid = self._olmDevice.createOutboundSession(
device[2], oneTimeKey
deviceInfo.getIdentityKey(), oneTimeKey
);
console.log("Started new sessionid " + sid +
" for device " + device[2]);
" for device " + userId + ":" + deviceId);
result[userId][deviceId].sessionId = sid;
} else {
missing[device[0]] = missing[device[0]] || [];
missing[device[0]].push([device[1]]);
console.warn("No one-time keys for device " +
userId + ":" + deviceId);
}
}
return {
missingUsers: userWithoutDevices,
missingDevices: missing
};
return result;
});
};
@@ -601,7 +651,7 @@ Crypto.prototype._ensureOlmSessionsForUsers = function(users) {
* @return {bool} whether encryption is enabled.
*/
Crypto.prototype.isRoomEncrypted = function(roomId) {
return (this._sessionStore.getEndToEndRoom(roomId) && true) || false;
return Boolean(this._roomAlgorithms[roomId]);
};
@@ -609,131 +659,54 @@ Crypto.prototype.isRoomEncrypted = function(roomId) {
* 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
*
* @param {module:models/room?} room destination room. Null if the destination
* is not a room we have seen over the sync pipe.
*
* @return {module:client.Promise?} Promise which resolves when the event has been
* encrypted, or null if nothing was needed
*/
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;
return null;
}
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);
return null;
}
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 roomId = event.getRoomId();
var users = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
var alg = this._roomAlgorithms[roomId];
if (!alg) {
// not encrypting messages in this room
// check that the HS hasn't hidden the crypto event
if (this._sessionStore.getEndToEndRoom(roomId)) {
throw new Error(
"Room was previously configured to use encryption, but is " +
"no longer. Perhaps the homeserver is hiding the " +
"configuration event."
);
}
return null;
}
return alg.encryptMessage(
room, event.getType(), event.getContent()
).then(function(encryptedContent) {
event.makeEncrypted("m.room.encrypted", encryptedContent);
});
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
*
@@ -741,66 +714,45 @@ Crypto.DecryptionError = DecryptionError;
*
* @return {object} decrypted payload (with properties 'type', 'content')
*
* @raises {DecryptionError} if there is a problem decrypting the event
* @raises {algorithms.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");
var AlgClass = algorithms.DECRYPTION_CLASSES[content.algorithm];
if (!AlgClass) {
throw new algorithms.DecryptionError("Unable to decrypt " + content.algorithm);
}
var alg = new AlgClass({
olmDevice: this._olmDevice,
});
return alg.decryptEvent(event);
};
/**
* Handle a key event
*
* @param {module:modules/event~MatrixEvent} event key event
*/
Crypto.prototype.onRoomKeyEvent = function(event) {
var content = event.getContent();
var AlgClass = algorithms.DECRYPTION_CLASSES[content.algorithm];
if (!AlgClass) {
throw new algorithms.DecryptionError(
"Unable to handle keys for " + content.algorithm
);
}
var alg = new AlgClass({
olmDevice: this._olmDevice,
});
alg.onRoomKeyEvent(event);
};
/**
* @see module:crypto-algorithms/base.DecryptionError
*/
Crypto.DecryptionError = algorithms.DecryptionError;
/** */
module.exports = Crypto;
+1 -1
View File
@@ -30,7 +30,7 @@ module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi;
module.exports.MatrixError = require("./http-api").MatrixError;
/** The {@link module:client.MatrixClient|MatrixClient} class. */
module.exports.MatrixClient = require("./client").MatrixClient;
/** The {@link module:models/room~Room|Room} class. */
/** The {@link module:models/room|Room} class. */
module.exports.Room = require("./models/room");
/** The {@link module:models/event-timeline~EventTimeline} class. */
module.exports.EventTimeline = require("./models/event-timeline");
+12
View File
@@ -29,6 +29,10 @@ limitations under the License.
module.exports.EventStatus = {
/** The event was not sent and will no longer be retried. */
NOT_SENT: "not_sent",
/** The message is being encrypted */
ENCRYPTING: "encrypting",
/** The event is in the process of being sent. */
SENDING: "sending",
/** The event is in a queue waiting to be sent. */
@@ -228,6 +232,14 @@ module.exports.MatrixEvent.prototype = {
return Boolean(this._clearEvent.type);
},
getSenderKey: function() {
if (!this.isEncrypted()) {
return null;
}
var c = this.getWireContent();
return c.sender_key;
},
getUnsigned: function() {
return this.event.unsigned || {};
},
+13 -3
View File
@@ -75,6 +75,7 @@ function synthesizeReceipt(userId, event, receiptType) {
* map from event_id to timeline and index.
*
* @constructor
* @alias module:models/room
* @param {string} roomId Required. The ID of this room.
* @param {Object=} opts Configuration options
* @param {*} opts.storageToken Optional. The token which a data store can use
@@ -85,7 +86,7 @@ function synthesizeReceipt(userId, event, receiptType) {
* appear in a room's timeline. If "<b>chronological</b>", messages will appear
* in the timeline when the call to <code>sendEvent</code> was made. If
* "<b>detached</b>", pending messages will appear in a separate list,
* accessbile via {@link module:models/room~Room#getPendingEvents}. Default:
* accessbile via {@link module:models/room#getPendingEvents}. Default:
* "chronological".
*
* @param {boolean} [opts.timelineSupport = false] Set to true to enable improved
@@ -844,8 +845,17 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
*/
var ALLOWED_TRANSITIONS = {};
ALLOWED_TRANSITIONS[EventStatus.SENDING] =
[EventStatus.QUEUED, EventStatus.NOT_SENT, EventStatus.SENT];
ALLOWED_TRANSITIONS[EventStatus.ENCRYPTING] = [
EventStatus.SENDING,
EventStatus.NOT_SENT,
];
ALLOWED_TRANSITIONS[EventStatus.SENDING] = [
EventStatus.ENCRYPTING,
EventStatus.QUEUED,
EventStatus.NOT_SENT,
EventStatus.SENT,
];
ALLOWED_TRANSITIONS[EventStatus.QUEUED] =
[EventStatus.SENDING, EventStatus.CANCELLED];
+5
View File
@@ -30,6 +30,7 @@ limitations under the License.
* @prop {string} displayName The 'displayname' of the user if known.
* @prop {string} avatarUrl The 'avatar_url' of the user if known.
* @prop {string} presence The presence enum if known.
* @prop {string} presenceStatusMsg The presence status message if known.
* @prop {Number} lastActiveAgo The time elapsed in ms since the user interacted
* proactively with the server, or we saw a message from the user
* @prop {Number} lastPresenceTs Timestamp (ms since the epoch) for when we last
@@ -44,6 +45,7 @@ limitations under the License.
function User(userId) {
this.userId = userId;
this.presence = "offline";
this.presenceStatusMsg = null;
this.displayName = userId;
this.avatarUrl = null;
this.lastActiveAgo = 0;
@@ -96,6 +98,9 @@ User.prototype.setPresenceEvent = function(event) {
this.presence = event.getContent().presence;
eventsToFire.push("User.lastPresenceTs");
if (event.getContent().status_msg) {
this.presenceStatusMsg = event.getContent().status_msg;
}
if (event.getContent().displayname) {
this.displayName = event.getContent().displayname;
}
+78
View File
@@ -0,0 +1,78 @@
/*
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.
*/
/**
* @module olmlib
*
* Utilities common to olm encryption algorithms
*/
var utils = require("./utils");
/**
* matrix algorithm tag for olm
*/
module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
/**
* matrix algorithm tag for megolm
*/
module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
/**
* Encrypt an event payload for a list of devices
*
* @param {string} ourDeviceId
* @param {module:OlmDevice} olmDevice olm.js wrapper
* @param {string[]} participantKeys list of curve25519 keys to encrypt for
* @param {object} payloadFields fields to include in the encrypted payload
*
* @return {object} content for an m.room.encrypted event
*/
module.exports.encryptMessageForDevices = function(
ourDeviceId, olmDevice, participantKeys, payloadFields
) {
participantKeys.sort();
var participantHash = ""; // Olm.sha256(participantKeys.join());
var payloadJson = {
fingerprint: participantHash,
sender_device: ourDeviceId,
};
utils.extend(payloadJson, payloadFields);
var ciphertext = {};
var payloadString = JSON.stringify(payloadJson);
for (var i = 0; i < participantKeys.length; ++i) {
var deviceKey = participantKeys[i];
var sessionId = olmDevice.getSessionIdForDevice(deviceKey);
if (sessionId === null) {
// If we don't have a session for a device then
// we can't encrypt a message for it.
continue;
}
console.log("Using sessionid " + sessionId + " for device " + deviceKey);
ciphertext[deviceKey] = olmDevice.encryptMessage(
deviceKey, sessionId, payloadString
);
}
var encryptedContent = {
algorithm: module.exports.OLM_ALGORITHM,
sender_key: olmDevice.deviceCurve25519Key,
ciphertext: ciphertext
};
return encryptedContent;
};
+24 -15
View File
@@ -243,25 +243,11 @@ function PushProcessor(client) {
return matchingRuleFromKindSet(ev, rulesets.global);
};
var actionListToActionsObject = function(actionlist) {
var actionobj = { 'notify': false, 'tweaks': {} };
for (var i = 0; i < actionlist.length; ++i) {
var action = actionlist[i];
if (action === 'notify') {
actionobj.notify = true;
} else if (typeof action === 'object') {
if (action.value === undefined) { action.value = true; }
actionobj.tweaks[action.set_tweak] = action.value;
}
}
return actionobj;
};
var pushActionsForEventAndRulesets = function(ev, rulesets) {
var rule = matchingRuleForEventWithRulesets(ev, rulesets);
if (!rule) { return {}; }
var actionObj = actionListToActionsObject(rule.actions);
var actionObj = PushProcessor.actionListToActionsObject(rule.actions);
// Some actions are implicit in some situations: we add those here
if (actionObj.tweaks.highlight === undefined) {
@@ -285,6 +271,28 @@ function PushProcessor(client) {
};
}
/**
* Convert a list of actions into a object with the actions as keys and their values
* eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ]
* becomes { notify: true, tweaks: { sound: 'default' } }
* @param {array} actionlist The actions list
*
* @return {object} A object with key 'notify' (true or false) and an object of actions
*/
PushProcessor.actionListToActionsObject = function(actionlist) {
var actionobj = { 'notify': false, 'tweaks': {} };
for (var i = 0; i < actionlist.length; ++i) {
var action = actionlist[i];
if (action === 'notify') {
actionobj.notify = true;
} else if (typeof action === 'object') {
if (action.value === undefined) { action.value = true; }
actionobj.tweaks[action.set_tweak] = action.value;
}
}
return actionobj;
};
/**
* @typedef {Object} PushAction
* @type {Object}
@@ -298,3 +306,4 @@ function PushProcessor(client) {
/** The PushProcessor class. */
module.exports = PushProcessor;
+14
View File
@@ -104,6 +104,16 @@ WebStorageSessionStore.prototype = {
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
},
getEndToEndInboundGroupSession: function(senderKey, sessionId) {
var key = keyEndToEndInboundGroupSession(senderKey, sessionId);
return this.store.getItem(key);
},
storeEndToEndInboundGroupSession: function(senderKey, sessionId, pickledSession) {
var key = keyEndToEndInboundGroupSession(senderKey, sessionId);
return this.store.setItem(key, pickledSession);
},
/**
* Store the end-to-end state for a room.
* @param {string} roomId The room's ID.
@@ -133,6 +143,10 @@ function keyEndToEndSessions(deviceKey) {
return E2E_PREFIX + "sessions/" + deviceKey;
}
function keyEndToEndInboundGroupSession(senderKey, sessionId) {
return E2E_PREFIX + "inboundgroupsessions/" + senderKey + "/" + sessionId;
}
function keyEndToEndRoom(roomId) {
return E2E_PREFIX + "rooms/" + roomId;
}
+4
View File
@@ -455,6 +455,7 @@ SyncApi.prototype._sync = function(syncOptions) {
self._connectionReturnedDefer = null;
}
this._updateSyncState("STOPPED");
return;
}
var filterId = syncOptions.filterId;
@@ -612,6 +613,9 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
client.store.storeAccountDataEvents(events);
events.forEach(
function(accountDataEvent) {
if (accountDataEvent.getType() == 'm.push_rules') {
client.pushRules = accountDataEvent.getContent();
}
client.emit("accountData", accountDataEvent);
return accountDataEvent;
}
+1 -1
View File
@@ -41,7 +41,7 @@ var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
* Construct a TimelineWindow.
*
* <p>This abstracts the separate timelines in a Matrix {@link
* module:models/room~Room|Room} into a single iterable thing. It keeps track of
* module:models/room|Room} into a single iterable thing. It keeps track of
* the start and endpoints of the window, which can be advanced with the help
* of pagination requests.
*
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "0.5.4",
"version": "0.5.6",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
+43 -22
View File
@@ -2,7 +2,9 @@
var sdk = require("../..");
var q = require("q");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
var utils = require("../../lib/utils");
var test_utils = require("../test-utils");
function MockStorageApi() {
this.data = {};
}
@@ -119,7 +121,7 @@ function expectBobKeyUpload() {
}
function bobUploadsKeys() {
bobClient.uploadKeys(5).catch(utils.failTest);
bobClient.uploadKeys(5).catch(test_utils.failTest);
return expectBobKeyUpload();
}
@@ -175,6 +177,7 @@ function aliDownloadsKeys() {
key: bobDeviceEd25519Key,
verified: false,
blocked: false,
display_name: null,
}]);
});
var p2 = aliQueryKeys();
@@ -193,7 +196,7 @@ function aliEnablesEncryption() {
// can't query keys before bob has uploaded them
expect(bobOneTimeKeys).toBeDefined();
aliQueryKeys().catch(utils.failTest);
aliQueryKeys().catch(test_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) {
@@ -212,8 +215,9 @@ function aliEnablesEncryption() {
var p = aliClient.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
}).then(function(res) {
expect(res.missingUsers).toEqual([]);
expect(res.missingDevices).toEqual({});
expect(res[aliUserId]).toEqual({});
expect(res[bobUserId][bobDeviceId].device).toBeDefined();
expect(res[bobUserId][bobDeviceId].sessionId).toBeDefined();
expect(aliClient.isRoomEncrypted(roomId)).toBeTruthy();
});
aliHttpBackend.flush();
@@ -221,12 +225,13 @@ function aliEnablesEncryption() {
}
function bobEnablesEncryption() {
bobQueryKeys().catch(utils.failTest);
bobQueryKeys().catch(test_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(res[aliUserId][aliDeviceId].device).toBeDefined();
expect(res[aliUserId][aliDeviceId].sessionId).toBeDefined();
expect(res[bobUserId]).toEqual({});
expect(bobClient.isRoomEncrypted(roomId)).toBeTruthy();
});
}
@@ -234,6 +239,7 @@ function bobEnablesEncryption() {
function aliSendsMessage() {
return sendMessage(aliHttpBackend, aliClient).then(function(content) {
aliMessages.push(content);
expect(utils.keys(content.ciphertext)).toEqual([bobDeviceCurve25519Key]);
var ciphertext = content.ciphertext[bobDeviceCurve25519Key];
expect(ciphertext).toBeDefined();
});
@@ -244,6 +250,7 @@ function bobSendsMessage() {
bobMessages.push(content);
var aliKeyId = "curve25519:" + aliDeviceId;
var aliDeviceCurve25519Key = aliDeviceKeys.keys[aliKeyId];
expect(utils.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
var ciphertext = content.ciphertext[aliDeviceCurve25519Key];
expect(ciphertext).toBeDefined();
return ciphertext;
@@ -290,7 +297,7 @@ function recvMessage(httpBackend, client, message) {
syncData.rooms.join[roomId] = {
timeline: {
events: [
utils.mkEvent({
test_utils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message
@@ -328,7 +335,7 @@ function recvMessage(httpBackend, client, message) {
function aliStartClient() {
expectAliKeyUpload().catch(utils.failTest);
expectAliKeyUpload().catch(test_utils.failTest);
startClient(aliHttpBackend, aliClient);
return aliHttpBackend.flush().then(function() {
console.log("Ali client started");
@@ -336,7 +343,7 @@ function aliStartClient() {
}
function bobStartClient() {
expectBobKeyUpload().catch(utils.failTest);
expectBobKeyUpload().catch(test_utils.failTest);
startClient(bobHttpBackend, bobClient);
return bobHttpBackend.flush().then(function() {
console.log("Bob client started");
@@ -365,11 +372,11 @@ function startClient(httpBackend, client) {
syncData.rooms.join[roomId] = {
state: {
events: [
utils.mkMembership({
test_utils.mkMembership({
mship: "join",
user: aliUserId,
}),
utils.mkMembership({
test_utils.mkMembership({
mship: "join",
user: bobUserId,
}),
@@ -394,7 +401,7 @@ describe("MatrixClient crypto", function() {
aliLocalStore = new MockStorageApi();
aliStorage = new sdk.WebStorageSessionStore(aliLocalStore);
bobStorage = new sdk.WebStorageSessionStore(new MockStorageApi());
utils.beforeEach(this);
test_utils.beforeEach(this);
aliHttpBackend = new HttpBackend();
aliClient = sdk.createClient({
@@ -433,14 +440,14 @@ describe("MatrixClient crypto", function() {
it("Bob uploads without one-time keys and with one-time keys", function(done) {
q()
.then(bobUploadsKeys)
.catch(utils.failTest).done(done);
.catch(test_utils.failTest).done(done);
});
it("Ali downloads Bobs keys", function(done) {
q()
.then(bobUploadsKeys)
.then(aliDownloadsKeys)
.catch(utils.failTest).done(done);
.catch(test_utils.failTest).done(done);
});
it("Ali gets keys with an invalid signature", function(done) {
@@ -458,7 +465,7 @@ describe("MatrixClient crypto", function() {
// should get an empty list
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
})
.catch(utils.failTest).done(done);
.catch(test_utils.failTest).done(done);
});
it("Ali enables encryption", function(done) {
@@ -466,7 +473,7 @@ describe("MatrixClient crypto", function() {
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.catch(utils.failTest).done(done);
.catch(test_utils.failTest).done(done);
});
it("Ali sends a message", function(done) {
@@ -475,7 +482,7 @@ describe("MatrixClient crypto", function() {
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsMessage)
.catch(utils.failTest).done(done);
.catch(test_utils.failTest).done(done);
});
it("Bob receives a message", function(done) {
@@ -486,7 +493,21 @@ describe("MatrixClient crypto", function() {
.then(aliSendsMessage)
.then(bobStartClient)
.then(bobRecvMessage)
.catch(utils.failTest).done(done);
.catch(test_utils.failTest).done(done);
});
it("Ali blocks Bob's device", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(function() {
aliClient.setDeviceBlocked(bobUserId, bobDeviceId, true);
return sendMessage(aliHttpBackend, aliClient);
}).then(function(sentContent) {
// no unblocked devices, so the ciphertext should be empty
expect(sentContent.ciphertext).toEqual({});
}).catch(test_utils.failTest).done(done);
});
it("Bob receives two pre-key messages", function(done) {
@@ -499,7 +520,7 @@ describe("MatrixClient crypto", function() {
.then(bobRecvMessage)
.then(aliSendsMessage)
.then(bobRecvMessage)
.catch(utils.failTest).done(done);
.catch(test_utils.failTest).done(done);
});
it("Bob replies to the message", function(done) {
@@ -514,6 +535,6 @@ describe("MatrixClient crypto", function() {
.then(bobSendsMessage).then(function(ciphertext) {
expect(ciphertext.type).toEqual(1);
}).then(aliRecvMessage)
.catch(utils.failTest).done(done);
.catch(test_utils.failTest).done(done);
});
});
+23 -20
View File
@@ -236,27 +236,22 @@ describe("MatrixClient", function() {
});
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.
},
assertObjectContains(res.boris.dev1, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev1": ed25519key },
algorithms: ["1"],
unsigned: { "abc": "def" },
});
assertObjectContains(res.chaz.dev2, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev2" : ed25519key },
algorithms: ["2"],
unsigned: { "ghi": "def" },
});
// dave's key fails validation.
expect(res.dave).toEqual({});
}).catch(utils.failTest).done(done);
httpBackend.flush();
@@ -278,3 +273,11 @@ describe("MatrixClient", function() {
});
});
});
function assertObjectContains(obj, expected) {
for (var k in expected) {
if (expected.hasOwnProperty(k)) {
expect(obj[k]).toEqual(expected[k]);
}
}
}
+4 -8
View File
@@ -1,5 +1,4 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
@@ -69,8 +68,8 @@ describe("MatrixClient retrying", function() {
expect(ev1.status).toEqual(EventStatus.SENDING);
expect(ev2.status).toEqual(EventStatus.SENDING);
// give the reactor a chance to run, so that ev2 gets queued
q().then(function() {
// the first message should get sent, and the second should get queued
httpBackend.when("PUT", "/send/m.room.message/").check(function(rq) {
// ev2 should now have been queued
expect(ev2.status).toEqual(EventStatus.QUEUED);
@@ -82,12 +81,9 @@ describe("MatrixClient retrying", function() {
// shouldn't be able to cancel the first message yet
expect(function() { client.cancelPendingEvent(ev1); })
.toThrow();
}).respond(400); // fail the first message
// fail the first send
httpBackend.when("PUT", "/send/m.room.message/")
.respond(400);
return httpBackend.flush();
}).then(function() {
httpBackend.flush().then(function() {
expect(ev1.status).toEqual(EventStatus.NOT_SENT);
expect(tl.length).toEqual(1);