1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2026-01-03 23:22:30 +03:00

Merge remote-tracking branch 'origin/develop' into dbkr/prefer_const

This commit is contained in:
David Baker
2017-01-19 17:05:54 +00:00
11 changed files with 333 additions and 17 deletions

View File

@@ -1,3 +1,6 @@
language: node_js
node_js:
- node # Latest stable version of nodejs.
script:
- npm run lint
- npm run test

View File

@@ -1,9 +1,12 @@
#!/bin/bash -l
set -x
export NVM_DIR="/home/jenkins/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 6
npm install
nvm use 6 || exit $?
npm install || exit $?
RC=0

View File

@@ -12,7 +12,7 @@
"dist": "npm run build",
"watch": "watchify --exclude olm browser-index.js -o dist/browser-matrix-dev.js -v",
"lint": "eslint --max-warnings 1860 src spec",
"prepublish": "npm run lint && npm run build && git rev-parse HEAD > git-revision.txt"
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt"
},
"repository": {
"url": "https://github.com/matrix-org/matrix-js-sdk"
@@ -63,6 +63,6 @@
"watchify": "^3.2.1"
},
"optionalDependencies": {
"olm": "https://matrix.org/packages/npm/olm/olm-2.1.0.tgz"
"olm": "https://matrix.org/packages/npm/olm/olm-2.2.1.tgz"
}
}

View File

@@ -895,4 +895,89 @@ describe("megolm", function() {
return q.all([downloadPromise, sendPromise]);
}).nodeify(done);
});
it("Alice exports megolm keys and imports them to a new device", function(done) {
let messageEncrypted;
return aliceTestClient.start().then(() => {
const p2pSession = createOlmSession(
testOlmAccount, aliceTestClient
);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a message with the group session
messageEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
});
// Alice gets both the events in a single sync
const syncResponse = {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted],
},
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
timeline: {
events: [messageEncrypted],
},
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.httpBackend.flush("/sync", 1);
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42');
return aliceTestClient.client.exportRoomKeys();
}).then(function(exported) {
// start a new client
aliceTestClient.stop();
aliceTestClient = new TestClient(
"@alice:localhost", "device2", "access_token2"
);
aliceTestClient.client.importRoomKeys(exported);
return aliceTestClient.start();
}).then(function() {
const syncResponse = {
next_batch: 1,
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
timeline: {
events: [messageEncrypted],
},
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.httpBackend.flush("/sync", 1);
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42');
}).nodeify(done);
});
});

View File

@@ -193,6 +193,12 @@ module.exports.MockStorageApi = function() {
this.data = {};
};
module.exports.MockStorageApi.prototype = {
get length() {
return Object.keys(this.data).length;
},
key: function(i) {
return Object.keys(this.data)[i];
},
setItem: function(k, v) {
this.data[k] = v;
},

View File

@@ -461,6 +461,33 @@ MatrixClient.prototype.isRoomEncrypted = function(roomId) {
return this._crypto.isRoomEncrypted(roomId);
};
/**
* Get a list containing all of the room keys
*
* This should be encrypted before returning it to the user.
*
* @return {module:client.Promise} a promise which resolves to a list of
* session export objects
*/
MatrixClient.prototype.exportRoomKeys = function() {
if (!this._crypto) {
return q.reject(new Error("End-to-end encryption disabled"));
}
return this._crypto.exportRoomKeys();
};
/**
* Import a list of room keys previously exported by exportRoomKeys
*
* @param {Object[]} keys a list of session export objects
*/
MatrixClient.prototype.importRoomKeys = function(keys) {
if (!this._crypto) {
throw new Error("End-to-end encryption disabled");
}
this._crypto.importRoomKeys(keys);
};
/**
* Decrypt a received event according to the algorithm specified in the event.
*

View File

@@ -47,6 +47,18 @@ function checkPayloadLength(payloadString) {
}
/**
* The type of object we use for importing and exporting megolm session data.
*
* @typedef {Object} module:crypto/OlmDevice.MegolmSessionData
* @property {String} sender_key Sender's Curve25519 device key
* @property {Object<string, string>} sender_claimed_keys Other keys the sender claims.
* @property {String} room_id Room this session is used in
* @property {String} session_id Unique id for the session
* @property {String} session_key Base64'ed key data
*/
/**
* Manages the olm cryptography functions. Each OlmDevice has a single
* OlmAccount and a number of OlmSessions.
@@ -683,6 +695,48 @@ OlmDevice.prototype.addInboundGroupSession = function(
}
};
/**
* Add a previously-exported inbound group session to the session store
*
* @param {module:crypto/OlmDevice.MegolmSessionData} data session data
*/
OlmDevice.prototype.importInboundGroupSession = function(data) {
/* if we already have this session, consider updating it */
function updateSession(session) {
console.log("Update for megolm session " + data.sender_key + "|" +
data.session_id);
// for now we just ignore updates. TODO: implement something here
return true;
}
const r = this._getInboundGroupSession(
data.room_id, data.sender_key, data.session_id, updateSession
);
if (r !== null) {
return;
}
// new session.
const session = new Olm.InboundGroupSession();
try {
session.import_session(data.session_key);
if (data.session_id != session.session_id()) {
throw new Error(
"Mismatched group session ID from senderKey: " + data.sender_key
);
}
this._saveInboundGroupSession(
data.room_id, data.sender_key, data.session_id, session,
data.sender_claimed_keys
);
} finally {
session.free();
}
};
/**
* Decrypt a received message with an inbound group session
*
@@ -739,6 +793,41 @@ OlmDevice.prototype.decryptGroupMessage = function(
);
};
/**
* Export an inbound group session
*
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @return {module:crypto/OlmDevice.MegolmSessionData} exported session data
*/
OlmDevice.prototype.exportInboundGroupSession = function(senderKey, sessionId) {
const s = this._sessionStore.getEndToEndInboundGroupSession(
senderKey, sessionId
);
if (s === null) {
throw new Error("Unknown inbound group session [" + senderKey + "," +
sessionId + "]");
}
const r = JSON.parse(s);
const session = new Olm.InboundGroupSession();
try {
session.unpickle(this._pickleKey, r.session);
const messageIndex = session.first_known_index();
return {
"sender_key": senderKey,
"sender_claimed_keys": r.keysClaimed,
"room_id": r.room_id,
"session_id": sessionId,
"session_key": session.export_session(messageIndex),
};
} finally {
session.free();
}
};
// Utilities
// =========

View File

@@ -136,6 +136,15 @@ DecryptionAlgorithm.prototype.onRoomKeyEvent = function(params) {
// ignore by default
};
/**
* Import a room key
*
* @param {module:crypto/OlmDevice.MegolmSessionData} session
*/
DecryptionAlgorithm.prototype.importRoomKey = function(session) {
// ignore by default
};
/**
* Exception thrown when decryption fails
*

View File

@@ -564,19 +564,45 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
content.session_key, event.getKeysClaimed()
);
let k = event.getSenderKey() + "|" + content.session_id;
let pending = this._pendingEvents[k];
if (pending) {
// have another go at decrypting events sent with this session.
delete this._pendingEvents[k];
// have another go at decrypting events sent with this session.
this._retryDecryption(event.getSenderKey, content.session_id);
};
for (let i = 0; i < pending.length; i++) {
try {
this.decryptEvent(pending[i]);
console.log("successful re-decryption of", pending[i]);
} catch (e) {
console.log("Still can't decrypt", pending[i], e.stack || e);
}
/**
* @inheritdoc
*
* @param {module:crypto/OlmDevice.MegolmSessionData} session
*/
MegolmDecryption.prototype.importRoomKey = function(session) {
this._olmDevice.importInboundGroupSession(session);
// have another go at decrypting events sent with this session.
this._retryDecryption(session.sender_key, session.session_id);
};
/**
* Have another go at decrypting events after we receive a key
*
* @private
* @param {String} senderKey
* @param {String} sessionId
*/
MegolmDecryption.prototype._retryDecryption = function(senderKey, sessionId) {
const k = senderKey + "|" + sessionId;
const pending = this._pendingEvents[k];
if (!pending) {
return;
}
delete this._pendingEvents[k];
for (let i = 0; i < pending.length; i++) {
try {
this.decryptEvent(pending[i]);
console.log("successful re-decryption of", pending[i]);
} catch (e) {
console.log("Still can't decrypt", pending[i], e.stack || e);
}
}
};

View File

@@ -897,6 +897,45 @@ Crypto.prototype.isRoomEncrypted = function(roomId) {
return Boolean(this._roomEncryptors[roomId]);
};
/**
* Get a list containing all of the room keys
*
* @return {module:client.Promise} a promise which resolves to a list of
* session export objects
*/
Crypto.prototype.exportRoomKeys = function() {
return q(
this._sessionStore.getAllEndToEndInboundGroupSessionKeys().map(
(s) => {
const sess = this._olmDevice.exportInboundGroupSession(
s.senderKey, s.sessionId
);
sess.algorithm = olmlib.MEGOLM_ALGORITHM;
return sess;
}
)
);
};
/**
* Import a list of room keys previously exported by exportRoomKeys
*
* @param {Object[]} keys a list of session export objects
*/
Crypto.prototype.importRoomKeys = function(keys) {
keys.map((session) => {
if (!session.room_id || !session.algorithm) {
console.warn("ignoring session entry with missing fields", session);
return;
}
const alg = this._getRoomDecryptor(session.room_id, session.algorithm);
alg.importRoomKey(session);
});
};
/**
* Encrypt an event according to the configuration of the room, if necessary.
*

View File

@@ -37,7 +37,10 @@ function WebStorageSessionStore(webStore) {
this.store = webStore;
if (!utils.isFunction(webStore.getItem) ||
!utils.isFunction(webStore.setItem) ||
!utils.isFunction(webStore.removeItem)) {
!utils.isFunction(webStore.removeItem) ||
!utils.isFunction(webStore.key) ||
typeof(webStore.length) !== 'number'
) {
throw new Error(
"Supplied webStore does not meet the WebStorage API interface"
);
@@ -120,6 +123,32 @@ WebStorageSessionStore.prototype = {
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
},
/**
* Retrieve a list of all known inbound group sessions
*
* @return {{senderKey: string, sessionId: string}}
*/
getAllEndToEndInboundGroupSessionKeys: function() {
const prefix = E2E_PREFIX + 'inboundgroupsessions/';
const result = [];
for (let i = 0; i < this.store.length; i++) {
const key = this.store.key(i);
if (!key.startsWith(prefix)) {
continue;
}
// we can't use split, as the components we are trying to split out
// might themselves contain '/' characters. We rely on the
// senderKey being a (32-byte) curve25519 key, base64-encoded
// (hence 43 characters long).
result.push({
senderKey: key.substr(prefix.length, 43),
sessionId: key.substr(prefix.length + 44),
});
}
return result;
},
getEndToEndInboundGroupSession: function(senderKey, sessionId) {
let key = keyEndToEndInboundGroupSession(senderKey, sessionId);
return this.store.getItem(key);