1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00

initial implementation of verification in DMs

This commit is contained in:
Hubert Chathi
2019-10-08 15:44:51 -04:00
parent 8cae00407a
commit d8e8dddd25
6 changed files with 359 additions and 55 deletions

View File

@@ -273,4 +273,127 @@ describe("SAS verification", function() {
expect(bob.setDeviceVerified) expect(bob.setDeviceVerified)
.toNotHaveBeenCalled(); .toNotHaveBeenCalled();
}); });
describe("verification in DM", function() {
let alice;
let bob;
let aliceSasEvent;
let bobSasEvent;
let aliceVerifier;
let bobPromise;
beforeEach(async function() {
[alice, bob] = await makeTestClients(
[
{userId: "@alice:example.com", deviceId: "Osborne2"},
{userId: "@bob:example.com", deviceId: "Dynabook"},
],
{
verificationMethods: [verificationMethods.SAS],
},
);
alice.setDeviceVerified = expect.createSpy();
alice.getDeviceEd25519Key = () => {
return "alice+base64+ed25519+key";
};
alice.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
keys: {
"ed25519:Dynabook": "bob+base64+ed25519+key",
},
},
"Dynabook",
);
};
alice.downloadKeys = () => {
return Promise.resolve();
};
bob.setDeviceVerified = expect.createSpy();
bob.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
keys: {
"ed25519:Osborne2": "alice+base64+ed25519+key",
},
},
"Osborne2",
);
};
bob.getDeviceEd25519Key = () => {
return "bob+base64+ed25519+key";
};
bob.downloadKeys = () => {
return Promise.resolve();
};
aliceSasEvent = null;
bobSasEvent = null;
bobPromise = new Promise((resolve, reject) => {
bob.on("event", async (event) => {
const content = event.getContent();
if (event.getType() === "m.room.message"
&& content.msgtype === "m.key.verification.request") {
expect(content.methods).toInclude(SAS.NAME);
expect(content.to).toBe(bob.getUserId());
const verifier = bob.acceptVerificationDM(event, SAS.NAME);
verifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
bobSasEvent = e;
} else {
try {
expect(e.sas).toEqual(aliceSasEvent.sas);
e.confirm();
aliceSasEvent.confirm();
} catch (error) {
e.mismatch();
aliceSasEvent.mismatch();
}
}
});
await verifier.verify();
resolve();
}
});
});
aliceVerifier = await alice.requestVerificationDM(
bob.getUserId(), "!room_id", [verificationMethods.SAS],
);
aliceVerifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!bobSasEvent) {
aliceSasEvent = e;
} else {
try {
expect(e.sas).toEqual(bobSasEvent.sas);
e.confirm();
bobSasEvent.confirm();
} catch (error) {
e.mismatch();
bobSasEvent.mismatch();
}
}
});
});
it("should verify a key", async function() {
await Promise.all([
aliceVerifier.verify(),
bobPromise,
]);
// make sure Alice and Bob verified each other
expect(alice.setDeviceVerified)
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
expect(bob.setDeviceVerified)
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
});
});
}); });

View File

@@ -43,6 +43,25 @@ export async function makeTestClients(userInfos, options) {
} }
} }
}; };
const sendEvent = function(room, type, content) {
// make up a unique ID as the event ID
const eventId = "$" + this.makeTxnId(); // eslint-disable-line babel/no-invalid-this
const event = new MatrixEvent({
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
type: type,
content: content,
room_id: room,
event_id: eventId,
});
for (const client of clients) {
setTimeout(
() => client.emit("event", event),
0,
);
}
return {event_id: eventId};
};
for (const userInfo of userInfos) { for (const userInfo of userInfos) {
const client = (new TestClient( const client = (new TestClient(
@@ -54,6 +73,7 @@ export async function makeTestClients(userInfos, options) {
} }
clientMap[userInfo.userId][userInfo.deviceId] = client; clientMap[userInfo.userId][userInfo.deviceId] = client;
client.sendToDevice = sendToDevice; client.sendToDevice = sendToDevice;
client.sendEvent = sendEvent;
clients.push(client); clients.push(client);
} }

View File

@@ -785,6 +785,40 @@ async function _setDeviceVerification(
client.emit("deviceVerificationChanged", userId, deviceId, dev); client.emit("deviceVerificationChanged", userId, deviceId, dev);
} }
/**
* Request a key verification from another user, using a DM.
*
* @param {string} userId the user to request verification with
* @param {string} roomId the room to use for verification
* @param {Array} methods array of verification methods to use. Defaults to
* all known methods
*
* @returns {Promise<module:crypto/verification/Base>} resolves to a verifier
* when the request is accepted by the other user
*/
MatrixClient.prototype.requestVerificationDM = function(userId, roomId, methods) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.requestVerificationDM(userId, roomId, methods);
};
/**
* Accept a key verification request from a DM.
*
* @param {module:models/event~MatrixEvent} event the verification request
* that is accepted
* @param {string} method the verification mmethod to use
*
* @returns {module:crypto/verification/Base} a verifier
*/
MatrixClient.prototype.acceptVerificationDM = function(event, method) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.acceptVerificationDM(event, method);
};
/** /**
* Request a key verification from another user. * Request a key verification from another user.
* *

View File

@@ -756,6 +756,128 @@ Crypto.prototype.setDeviceVerification = async function(
}; };
function verificationEventHandler(target, userId, roomId, eventId) {
return function(event) {
// listen for events related to this verification
if (event.getRoomId() !== roomId
|| event.getSender() !== userId) {
return;
}
const content = event.getContent();
if (!content["m.relates_to"]) {
return;
}
const relatesTo
= content["m.relationship"] || content["m.relates_to"];
if (!relatesTo.rel_type
|| relatesTo.rel_type !== "m.reference"
|| !relatesTo.event_id
|| relatesTo.event_id !== eventId) {
return;
}
// the event seems to be related to this verification, so pass it on to
// the verification handler
target.handleEvent(event);
};
}
Crypto.prototype.requestVerificationDM = function(userId, roomId, methods) {
let methodMap;
if (methods) {
methodMap = new Map();
for (const method of methods) {
if (typeof method === "string") {
methodMap.set(method, defaultVerificationMethods[method]);
} else if (method.NAME) {
methodMap.set(method.NAME, method);
}
}
} else {
methodMap = this._baseApis._crypto._verificationMethods;
}
return new Promise(async (_resolve, _reject) => {
const listener = (event) => {
// listen for events related to this verification
if (event.getRoomId() !== roomId
|| event.getSender() !== userId) {
return;
}
const content = event.getContent();
const relatesTo
= content["m.relationship"] || content["m.relates_to"];
if (!relatesTo || !relatesTo.rel_type
|| relatesTo.rel_type !== "m.reference"
|| !relatesTo.event_id
|| relatesTo.event_id !== eventId) {
return;
}
// the event seems to be related to this verification
switch (event.getType()) {
case "m.key.verification.start": {
const verifier = new (methodMap.get(content.method))(
this._baseApis, userId, content.from_device, eventId,
roomId, event,
);
verifier.handler = verificationEventHandler(
verifier, userId, roomId, eventId,
);
this._baseApis.on("event", verifier.handler);
resolve(verifier);
break;
}
case "m.key.verification.cancel": {
reject(event);
break;
}
}
};
this._baseApis.on("event", listener);
const resolve = (...args) => {
this._baseApis.off("event", listener);
_resolve(...args);
};
const reject = (...args) => {
this._baseApis.off("event", listener);
_reject(...args);
};
const res = await this._baseApis.sendEvent(
roomId, "m.room.message",
{
body: this._baseApis.getUserId() + " is requesting to verify " +
"your key, but your client does not support in-chat key " +
"verification. You will need to use legacy key " +
"verification to verify keys.",
msgtype: "m.key.verification.request",
to: userId,
from_device: this._baseApis.getDeviceId(),
methods: [...methodMap.keys()],
},
);
const eventId = res.event_id;
});
};
Crypto.prototype.acceptVerificationDM = function(event, Method) {
if (typeof(Method) === "string") {
Method = defaultVerificationMethods[Method];
}
const content = event.getContent();
const verifier = new Method(
this._baseApis, event.getSender(), content.from_device, event.getId(),
event.getRoomId(),
);
verifier.handler = verificationEventHandler(
verifier, event.getSender(), event.getRoomId(), event.getId(),
);
this._baseApis.on("event", verifier.handler);
return verifier;
};
Crypto.prototype.requestVerification = function(userId, methods, devices) { Crypto.prototype.requestVerification = function(userId, methods, devices) {
if (!methods) { if (!methods) {
// .keys() returns an iterator, so we need to explicitly turn it into an array // .keys() returns an iterator, so we need to explicitly turn it into an array
@@ -803,20 +925,7 @@ Crypto.prototype.beginKeyVerification = function(
this._verificationTransactions.set(userId, new Map()); this._verificationTransactions.set(userId, new Map());
} }
transactionId = transactionId || randomString(32); transactionId = transactionId || randomString(32);
if (method instanceof Array) { if (this._verificationMethods.has(method)) {
if (method.length !== 2
|| !this._verificationMethods.has(method[0])
|| !this._verificationMethods.has(method[1])) {
throw newUnknownMethodError();
}
/*
return new TwoPartVerification(
this._verificationMethods[method[0]],
this._verificationMethods[method[1]],
userId, deviceId, transactionId,
);
*/
} else if (this._verificationMethods.has(method)) {
const verifier = new (this._verificationMethods.get(method))( const verifier = new (this._verificationMethods.get(method))(
this._baseApis, userId, deviceId, transactionId, this._baseApis, userId, deviceId, transactionId,
); );
@@ -1817,22 +1926,6 @@ Crypto.prototype._onKeyVerificationStart = function(event) {
transaction_id: content.transactionId, transaction_id: content.transactionId,
})); }));
return; return;
} else if (content.next_method) {
if (!this._verificationMethods.has(content.next_method)) {
cancel(newUnknownMethodError({
transaction_id: content.transactionId,
}));
return;
} else {
/* TODO:
const verification = new TwoPartVerification(
this._verificationMethods[content.method],
this._verificationMethods[content.next_method],
userId, deviceId,
);
this.emit(verification.event_type, verification);
this.emit(verification.first.event_type, verification);*/
}
} else { } else {
const verifier = new (this._verificationMethods.get(content.method))( const verifier = new (this._verificationMethods.get(content.method))(
this._baseApis, sender, deviceId, content.transaction_id, this._baseApis, sender, deviceId, content.transaction_id,
@@ -1887,8 +1980,6 @@ Crypto.prototype._onKeyVerificationStart = function(event) {
handler.request.resolve(verifier); handler.request.resolve(verifier);
} }
} else {
// FIXME: make sure we're in a two-part verification, and the start matches the second part
} }
} }
this._baseApis.emit("crypto.verification.start", verifier); this._baseApis.emit("crypto.verification.start", verifier);

View File

@@ -47,40 +47,52 @@ export default class VerificationBase extends EventEmitter {
* *
* @param {string} transactionId the transaction ID to be used when sending events * @param {string} transactionId the transaction ID to be used when sending events
* *
* @param {object} startEvent the m.key.verification.start event that * @param {string} [roomId] the room to use for verification
*
* @param {object} [startEvent] the m.key.verification.start event that
* initiated this verification, if any * initiated this verification, if any
* *
* @param {object} request the key verification request object related to * @param {object} [request] the key verification request object related to
* this verification, if any * this verification, if any
*
* @param {object} parent parent verification for this verification, if any
*/ */
constructor(baseApis, userId, deviceId, transactionId, startEvent, request, parent) { constructor(baseApis, userId, deviceId, transactionId, roomId, startEvent, request) {
super(); super();
this._baseApis = baseApis; this._baseApis = baseApis;
this.userId = userId; this.userId = userId;
this.deviceId = deviceId; this.deviceId = deviceId;
this.transactionId = transactionId; this.transactionId = transactionId;
if (typeof(roomId) === "string" || roomId instanceof String) {
this.roomId = roomId;
this.startEvent = startEvent; this.startEvent = startEvent;
this.request = request; this.request = request;
} else {
// if room ID was omitted, but start event and request were not
this.startEvent= roomId;
this.request = startEvent;
}
this.cancelled = false; this.cancelled = false;
this._parent = parent;
this._done = false; this._done = false;
this._promise = null; this._promise = null;
this._transactionTimeoutTimer = null; this._transactionTimeoutTimer = null;
// At this point, the verification request was received so start the timeout timer. // At this point, the verification request was received so start the timeout timer.
this._resetTimer(); this._resetTimer();
if (this.roomId) {
this._send = this._sendMessage;
} else {
this._send = this._sendToDevice;
}
} }
_resetTimer() { _resetTimer() {
console.log("Refreshing/starting the verification transaction timeout timer"); logger.info("Refreshing/starting the verification transaction timeout timer");
if (this._transactionTimeoutTimer !== null) { if (this._transactionTimeoutTimer !== null) {
clearTimeout(this._transactionTimeoutTimer); clearTimeout(this._transactionTimeoutTimer);
} }
this._transactionTimeoutTimer = setTimeout(() => { this._transactionTimeoutTimer = setTimeout(() => {
if (!this._done && !this.cancelled) { if (!this._done && !this.cancelled) {
console.log("Triggering verification timeout"); logger.info("Triggering verification timeout");
this.cancel(timeoutException); this.cancel(timeoutException);
} }
}, 10 * 60 * 1000); // 10 minutes }, 10 * 60 * 1000); // 10 minutes
@@ -93,6 +105,8 @@ export default class VerificationBase extends EventEmitter {
} }
} }
/* send a message to the other participant, using to-device messages
*/
_sendToDevice(type, content) { _sendToDevice(type, content) {
if (this._done) { if (this._done) {
return Promise.reject(new Error("Verification is already done")); return Promise.reject(new Error("Verification is already done"));
@@ -103,6 +117,21 @@ export default class VerificationBase extends EventEmitter {
}); });
} }
/* send a message to the other participant, using in-roomm messages
*/
_sendMessage(type, content) {
if (this._done) {
return Promise.reject(new Error("Verification is already done"));
}
// FIXME: only use one of m.relationship/m.relates_to, once MSC1849
// decides which one to use
content["m.relationship"] = content["m.relates_to"] = {
rel_type: "m.reference",
event_id: this.transactionId,
};
return this._baseApis.sendEvent(this.roomId, type, content);
}
_waitForEvent(type) { _waitForEvent(type) {
if (this._done) { if (this._done) {
return Promise.reject(new Error("Verification is already done")); return Promise.reject(new Error("Verification is already done"));
@@ -153,7 +182,7 @@ export default class VerificationBase extends EventEmitter {
// cancelled by the other user) // cancelled by the other user)
if (e === timeoutException) { if (e === timeoutException) {
const timeoutEvent = newTimeoutError(); const timeoutEvent = newTimeoutError();
this._sendToDevice(timeoutEvent.getType(), timeoutEvent.getContent()); this._send(timeoutEvent.getType(), timeoutEvent.getContent());
} else if (e instanceof MatrixEvent) { } else if (e instanceof MatrixEvent) {
const sender = e.getSender(); const sender = e.getSender();
if (sender !== this.userId) { if (sender !== this.userId) {
@@ -163,9 +192,9 @@ export default class VerificationBase extends EventEmitter {
content.reason = content.reason || content.body content.reason = content.reason || content.body
|| "Unknown reason"; || "Unknown reason";
content.transaction_id = this.transactionId; content.transaction_id = this.transactionId;
this._sendToDevice("m.key.verification.cancel", content); this._send("m.key.verification.cancel", content);
} else { } else {
this._sendToDevice("m.key.verification.cancel", { this._send("m.key.verification.cancel", {
code: "m.unknown", code: "m.unknown",
reason: content.body || "Unknown reason", reason: content.body || "Unknown reason",
transaction_id: this.transactionId, transaction_id: this.transactionId,
@@ -173,7 +202,7 @@ export default class VerificationBase extends EventEmitter {
} }
} }
} else { } else {
this._sendToDevice("m.key.verification.cancel", { this._send("m.key.verification.cancel", {
code: "m.unknown", code: "m.unknown",
reason: e.toString(), reason: e.toString(),
transaction_id: this.transactionId, transaction_id: this.transactionId,
@@ -206,11 +235,17 @@ export default class VerificationBase extends EventEmitter {
this._resolve = (...args) => { this._resolve = (...args) => {
this._done = true; this._done = true;
this._endTimer(); this._endTimer();
if (this.handler) {
this._baseApis.off("event", this.handler);
}
resolve(...args); resolve(...args);
}; };
this._reject = (...args) => { this._reject = (...args) => {
this._done = true; this._done = true;
this._endTimer(); this._endTimer();
if (this.handler) {
this._baseApis.off("event", this.handler);
}
reject(...args); reject(...args);
}; };
}); });

View File

@@ -213,9 +213,10 @@ export default class SAS extends Base {
message_authentication_codes: MAC_LIST, message_authentication_codes: MAC_LIST,
// FIXME: allow app to specify what SAS methods can be used // FIXME: allow app to specify what SAS methods can be used
short_authentication_string: SAS_LIST, short_authentication_string: SAS_LIST,
transaction_id: this.transactionId,
}; };
this._sendToDevice("m.key.verification.start", initialMessage); // NOTE: this._send will modify initialMessage to include the
// transaction_id field, or the m.relationship/m.relates_to field
this._send("m.key.verification.start", initialMessage);
let e = await this._waitForEvent("m.key.verification.accept"); let e = await this._waitForEvent("m.key.verification.accept");
@@ -235,7 +236,7 @@ export default class SAS extends Base {
const hashCommitment = content.commitment; const hashCommitment = content.commitment;
const olmSAS = new global.Olm.SAS(); const olmSAS = new global.Olm.SAS();
try { try {
this._sendToDevice("m.key.verification.key", { this._send("m.key.verification.key", {
key: olmSAS.get_pubkey(), key: olmSAS.get_pubkey(),
}); });
@@ -306,7 +307,7 @@ export default class SAS extends Base {
const olmSAS = new global.Olm.SAS(); const olmSAS = new global.Olm.SAS();
try { try {
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content); const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
this._sendToDevice("m.key.verification.accept", { this._send("m.key.verification.accept", {
key_agreement_protocol: keyAgreement, key_agreement_protocol: keyAgreement,
hash: hashMethod, hash: hashMethod,
message_authentication_code: macMethod, message_authentication_code: macMethod,
@@ -320,7 +321,7 @@ export default class SAS extends Base {
// FIXME: make sure event is properly formed // FIXME: make sure event is properly formed
content = e.getContent(); content = e.getContent();
olmSAS.set_their_key(content.key); olmSAS.set_their_key(content.key);
this._sendToDevice("m.key.verification.key", { this._send("m.key.verification.key", {
key: olmSAS.get_pubkey(), key: olmSAS.get_pubkey(),
}); });
@@ -369,7 +370,7 @@ export default class SAS extends Base {
keyId, keyId,
baseInfo + "KEY_IDS", baseInfo + "KEY_IDS",
); );
this._sendToDevice("m.key.verification.mac", { mac, keys }); this._send("m.key.verification.mac", { mac, keys });
} }
async _checkMAC(olmSAS, content, method) { async _checkMAC(olmSAS, content, method) {