Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

verification in DMs #1050

Merged
merged 4 commits into from
Oct 23, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions spec/unit/crypto/verification/sas.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,4 +273,127 @@ describe("SAS verification", function() {
expect(bob.setDeviceVerified)
.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);
});
});
});
20 changes: 20 additions & 0 deletions spec/unit/crypto/verification/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
const client = (new TestClient(
Expand All @@ -54,6 +73,7 @@ export async function makeTestClients(userInfos, options) {
}
clientMap[userInfo.userId][userInfo.deviceId] = client;
client.sendToDevice = sendToDevice;
client.sendEvent = sendEvent;
clients.push(client);
}

Expand Down
34 changes: 34 additions & 0 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,40 @@ async function _setDeviceVerification(
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.
*
Expand Down
159 changes: 127 additions & 32 deletions src/crypto/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,132 @@ 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 = async 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;
}

let eventId = undefined;
const listenPromise = new Promise(async (_resolve, _reject) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this doesn't need to be async anymore i think?

const listener = (event) => {
// listen for events related to this verification
if (event.getRoomId() !== roomId
|| event.getSender() !== userId) {
return;
}
const relatesTo = event.getRelation();
if (!relatesTo || !relatesTo.rel_type
|| relatesTo.rel_type !== "m.reference"
|| !relatesTo.event_id
|| relatesTo.event_id !== eventId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would throw if an event comes in before the verification event gets sent (and eventId is defined).
I tried this:

function scope() {
	const f = () => console.log("foo", foo);
	f();
	const foo = 5;
	f();
}
scope();

and it throws with ReferenceError: foo is not defined at the first call of f().

You could put a let eventId at the top and reassign it at the bottom?

return;
}

const content = event.getContent();
// 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 handler gets removed when the verification finishes
// (see the verify method of crypto/verification/Base.js)
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()],
},
);
eventId = res.event_id;

return listenPromise;
};

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);
bwindels marked this conversation as resolved.
Show resolved Hide resolved
return verifier;
};

Crypto.prototype.requestVerification = function(userId, methods, devices) {
if (!methods) {
// .keys() returns an iterator, so we need to explicitly turn it into an array
Expand Down Expand Up @@ -803,20 +929,7 @@ Crypto.prototype.beginKeyVerification = function(
this._verificationTransactions.set(userId, new Map());
}
transactionId = transactionId || randomString(32);
if (method instanceof Array) {
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)) {
if (this._verificationMethods.has(method)) {
const verifier = new (this._verificationMethods.get(method))(
this._baseApis, userId, deviceId, transactionId,
);
Expand Down Expand Up @@ -1817,22 +1930,6 @@ Crypto.prototype._onKeyVerificationStart = function(event) {
transaction_id: content.transactionId,
}));
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 {
const verifier = new (this._verificationMethods.get(content.method))(
this._baseApis, sender, deviceId, content.transaction_id,
Expand Down Expand Up @@ -1887,8 +1984,6 @@ Crypto.prototype._onKeyVerificationStart = function(event) {

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);
Expand Down
Loading