You've already forked matrix-js-sdk
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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) {
|
||||
if (!methods) {
|
||||
// .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());
|
||||
}
|
||||
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,
|
||||
);
|
||||
@@ -1817,22 +1926,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,
|
||||
@@ -1887,8 +1980,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);
|
||||
|
||||
@@ -47,40 +47,52 @@ export default class VerificationBase extends EventEmitter {
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @param {object} request the key verification request object related to
|
||||
* @param {object} [request] the key verification request object related to
|
||||
* 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();
|
||||
this._baseApis = baseApis;
|
||||
this.userId = userId;
|
||||
this.deviceId = deviceId;
|
||||
this.transactionId = transactionId;
|
||||
if (typeof(roomId) === "string" || roomId instanceof String) {
|
||||
this.roomId = roomId;
|
||||
this.startEvent = startEvent;
|
||||
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._parent = parent;
|
||||
this._done = false;
|
||||
this._promise = null;
|
||||
this._transactionTimeoutTimer = null;
|
||||
|
||||
// At this point, the verification request was received so start the timeout timer.
|
||||
this._resetTimer();
|
||||
|
||||
if (this.roomId) {
|
||||
this._send = this._sendMessage;
|
||||
} else {
|
||||
this._send = this._sendToDevice;
|
||||
}
|
||||
}
|
||||
|
||||
_resetTimer() {
|
||||
console.log("Refreshing/starting the verification transaction timeout timer");
|
||||
logger.info("Refreshing/starting the verification transaction timeout timer");
|
||||
if (this._transactionTimeoutTimer !== null) {
|
||||
clearTimeout(this._transactionTimeoutTimer);
|
||||
}
|
||||
this._transactionTimeoutTimer = setTimeout(() => {
|
||||
if (!this._done && !this.cancelled) {
|
||||
console.log("Triggering verification timeout");
|
||||
logger.info("Triggering verification timeout");
|
||||
this.cancel(timeoutException);
|
||||
}
|
||||
}, 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) {
|
||||
if (this._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) {
|
||||
if (this._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)
|
||||
if (e === timeoutException) {
|
||||
const timeoutEvent = newTimeoutError();
|
||||
this._sendToDevice(timeoutEvent.getType(), timeoutEvent.getContent());
|
||||
this._send(timeoutEvent.getType(), timeoutEvent.getContent());
|
||||
} else if (e instanceof MatrixEvent) {
|
||||
const sender = e.getSender();
|
||||
if (sender !== this.userId) {
|
||||
@@ -163,9 +192,9 @@ export default class VerificationBase extends EventEmitter {
|
||||
content.reason = content.reason || content.body
|
||||
|| "Unknown reason";
|
||||
content.transaction_id = this.transactionId;
|
||||
this._sendToDevice("m.key.verification.cancel", content);
|
||||
this._send("m.key.verification.cancel", content);
|
||||
} else {
|
||||
this._sendToDevice("m.key.verification.cancel", {
|
||||
this._send("m.key.verification.cancel", {
|
||||
code: "m.unknown",
|
||||
reason: content.body || "Unknown reason",
|
||||
transaction_id: this.transactionId,
|
||||
@@ -173,7 +202,7 @@ export default class VerificationBase extends EventEmitter {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._sendToDevice("m.key.verification.cancel", {
|
||||
this._send("m.key.verification.cancel", {
|
||||
code: "m.unknown",
|
||||
reason: e.toString(),
|
||||
transaction_id: this.transactionId,
|
||||
@@ -206,11 +235,17 @@ export default class VerificationBase extends EventEmitter {
|
||||
this._resolve = (...args) => {
|
||||
this._done = true;
|
||||
this._endTimer();
|
||||
if (this.handler) {
|
||||
this._baseApis.off("event", this.handler);
|
||||
}
|
||||
resolve(...args);
|
||||
};
|
||||
this._reject = (...args) => {
|
||||
this._done = true;
|
||||
this._endTimer();
|
||||
if (this.handler) {
|
||||
this._baseApis.off("event", this.handler);
|
||||
}
|
||||
reject(...args);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -213,9 +213,10 @@ export default class SAS extends Base {
|
||||
message_authentication_codes: MAC_LIST,
|
||||
// FIXME: allow app to specify what SAS methods can be used
|
||||
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");
|
||||
@@ -235,7 +236,7 @@ export default class SAS extends Base {
|
||||
const hashCommitment = content.commitment;
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
try {
|
||||
this._sendToDevice("m.key.verification.key", {
|
||||
this._send("m.key.verification.key", {
|
||||
key: olmSAS.get_pubkey(),
|
||||
});
|
||||
|
||||
@@ -306,7 +307,7 @@ export default class SAS extends Base {
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
try {
|
||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
|
||||
this._sendToDevice("m.key.verification.accept", {
|
||||
this._send("m.key.verification.accept", {
|
||||
key_agreement_protocol: keyAgreement,
|
||||
hash: hashMethod,
|
||||
message_authentication_code: macMethod,
|
||||
@@ -320,7 +321,7 @@ export default class SAS extends Base {
|
||||
// FIXME: make sure event is properly formed
|
||||
content = e.getContent();
|
||||
olmSAS.set_their_key(content.key);
|
||||
this._sendToDevice("m.key.verification.key", {
|
||||
this._send("m.key.verification.key", {
|
||||
key: olmSAS.get_pubkey(),
|
||||
});
|
||||
|
||||
@@ -369,7 +370,7 @@ export default class SAS extends Base {
|
||||
keyId,
|
||||
baseInfo + "KEY_IDS",
|
||||
);
|
||||
this._sendToDevice("m.key.verification.mac", { mac, keys });
|
||||
this._send("m.key.verification.mac", { mac, keys });
|
||||
}
|
||||
|
||||
async _checkMAC(olmSAS, content, method) {
|
||||
|
||||
Reference in New Issue
Block a user