1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-12-04 05:02:41 +03:00

Merge pull request #1297 from matrix-org/bwindels/qr-reciprocate

QR code reciprocation
This commit is contained in:
Bruno Windels
2020-04-03 12:28:34 +00:00
committed by GitHub
2 changed files with 236 additions and 56 deletions

View File

@@ -23,8 +23,9 @@ limitations under the License.
import {VerificationBase as Base} from "./Base";
import {
newKeyMismatchError,
newUserMismatchError,
newUserCancelledError,
} from './Error';
import {encodeUnpaddedBase64, decodeBase64} from "../olmlib";
export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
@@ -49,47 +50,37 @@ export class ReciprocateQRCode extends Base {
"with this method yet.");
}
const targetUserId = this.startEvent.getSender();
if (!this.userId) {
console.log("Asking to confirm user ID");
this.userId = await new Promise((resolve, reject) => {
this.emit("confirm_user_id", {
userId: targetUserId,
confirm: resolve, // takes a userId
cancel: () => reject(newUserMismatchError()),
});
});
} else if (targetUserId !== this.userId) {
throw newUserMismatchError({
expected: this.userId,
actual: targetUserId,
});
}
if (this.startEvent.getContent()['secret'] !== this.request.encodedSharedSecret) {
const {qrCodeData} = this.request;
// 1. check the secret
if (this.startEvent.getContent()['secret'] !== qrCodeData.encodedSharedSecret) {
throw newKeyMismatchError();
}
// If we've gotten this far, verify the user's master cross signing key
const xsignInfo = this._baseApis.getStoredCrossSigningForUser(this.userId);
if (!xsignInfo) throw new Error("Missing cross signing info");
const masterKey = xsignInfo.getId("master");
const masterKeyId = `ed25519:${masterKey}`;
const keys = {[masterKeyId]: masterKey};
const devices = (await this._baseApis.getStoredDevicesForUser(this.userId)) || [];
const targetDevice = devices.find(d => {
return d.deviceId === this.request.targetDevice.deviceId;
// 2. ask if other user shows shield as well
await new Promise((resolve, reject) => {
this.reciprocateQREvent = {
confirm: resolve,
cancel: () => reject(newUserCancelledError()),
};
this.emit("show_reciprocate_qr", this.reciprocateQREvent);
});
if (!targetDevice) throw new Error("Device not found, somehow");
keys[`ed25519:${targetDevice.deviceId}`] = targetDevice.getFingerprint();
if (this.request.requestingUserId === this.request.receivingUserId) {
delete keys[masterKeyId];
// 3. determine key to sign
const keys = {};
if (qrCodeData.mode === MODE_VERIFY_OTHER_USER) {
// add master key to keys to be signed, only if we're not doing self-verification
const masterKey = qrCodeData.otherUserMasterKey;
keys[`ed25519:${masterKey}`] = masterKey;
} else if (qrCodeData.mode === MODE_VERIFY_SELF_TRUSTED) {
const deviceId = this.request.targetDevice.deviceId;
keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey;
} else {
// TODO: not sure if MODE_VERIFY_SELF_UNTRUSTED makes sense to sign anything here?
}
// 4. sign the key
await this._verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
// make sure the device has the expected keys
const targetKey = keys[keyId];
if (!targetKey) throw newKeyMismatchError();
@@ -106,8 +97,174 @@ export class ReciprocateQRCode extends Base {
throw newKeyMismatchError();
}
}
// Otherwise it is probably fine
});
}
}
const CODE_VERSION = 0x02; // the version of binary QR codes we support
const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
const MODE_VERIFY_OTHER_USER = 0x00; // Verifying someone who isn't us
const MODE_VERIFY_SELF_TRUSTED = 0x01; // We trust the master key
const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key
export class QRCodeData {
constructor(mode, sharedSecret, otherUserMasterKey, otherDeviceKey, buffer) {
this._sharedSecret = sharedSecret;
this._mode = mode;
this._otherUserMasterKey = otherUserMasterKey;
this._otherDeviceKey = otherDeviceKey;
this._buffer = buffer;
}
static async create(request, client) {
const sharedSecret = QRCodeData._generateSharedSecret();
const mode = QRCodeData._determineMode(request, client);
let otherUserMasterKey = null;
let otherDeviceKey = null;
if (mode === MODE_VERIFY_OTHER_USER) {
const otherUserCrossSigningInfo =
client.getStoredCrossSigningForUser(request.otherUserId);
otherUserMasterKey = otherUserCrossSigningInfo.getId("master");
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
otherDeviceKey = await QRCodeData._getOtherDeviceKey(request, client);
}
const qrData = QRCodeData._generateQrData(
request, client, mode,
sharedSecret,
otherUserMasterKey,
otherDeviceKey,
);
const buffer = QRCodeData._generateBuffer(qrData);
return new QRCodeData(mode, sharedSecret,
otherUserMasterKey, otherDeviceKey, buffer);
}
get buffer() {
return this._buffer;
}
get mode() {
return this._mode;
}
get otherDeviceKey() {
return this._otherDeviceKey;
}
get otherUserMasterKey() {
return this._otherUserMasterKey;
}
/**
* The unpadded base64 encoded shared secret.
*/
get encodedSharedSecret() {
return this._sharedSecret;
}
static _generateSharedSecret() {
const secretBytes = new Uint8Array(11);
global.crypto.getRandomValues(secretBytes);
return encodeUnpaddedBase64(secretBytes);
}
static async _getOtherDeviceKey(request, client) {
const myUserId = client.getUserId();
const otherDevice = request.targetDevice;
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
const device = await client.getStoredDevice(myUserId, otherDeviceId);
if (!device) {
throw new Error("could not find device " + otherDeviceId);
}
const key = device.getFingerprint();
return key;
}
static _determineMode(request, client) {
const myUserId = client.getUserId();
const otherUserId = request.otherUserId;
let mode = MODE_VERIFY_OTHER_USER;
if (myUserId === otherUserId) {
// Mode changes depending on whether or not we trust the master cross signing key
const myTrust = client.checkUserTrust(myUserId);
if (myTrust.isCrossSigningVerified()) {
mode = MODE_VERIFY_SELF_TRUSTED;
} else {
mode = MODE_VERIFY_SELF_UNTRUSTED;
}
}
return mode;
}
static _generateQrData(request, client, mode,
encodedSharedSecret, otherUserMasterKey, otherDeviceKey,
) {
const myUserId = client.getUserId();
const transactionId = request.channel.transactionId;
const qrData = {
prefix: BINARY_PREFIX,
version: CODE_VERSION,
mode,
transactionId,
firstKeyB64: '', // worked out shortly
secondKeyB64: '', // worked out shortly
secretB64: encodedSharedSecret,
};
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
const myMasterKey = myCrossSigningInfo.getId("master");
if (mode === MODE_VERIFY_OTHER_USER) {
// First key is our master cross signing key
qrData.firstKeyB64 = myMasterKey;
// Second key is the other user's master cross signing key
qrData.secondKeyB64 = otherUserMasterKey;
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
// First key is our master cross signing key
qrData.firstKeyB64 = myMasterKey;
qrData.secondKeyB64 = otherDeviceKey;
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
// First key is our device's key
qrData.firstKeyB64 = client.getDeviceEd25519Key();
// Second key is what we think our master cross signing key is
qrData.secondKeyB64 = myMasterKey;
}
return qrData;
}
static _generateBuffer(qrData) {
let buf = Buffer.alloc(0); // we'll concat our way through life
const appendByte = (b) => {
const tmpBuf = Buffer.from([b]);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendInt = (i) => {
const tmpBuf = Buffer.alloc(2);
tmpBuf.writeInt16BE(i, 0);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendStr = (s, enc, withLengthPrefix = true) => {
const tmpBuf = Buffer.from(s, enc);
if (withLengthPrefix) appendInt(tmpBuf.byteLength);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendEncBase64 = (b64) => {
const b = decodeBase64(b64);
const tmpBuf = Buffer.from(b);
buf = Buffer.concat([buf, tmpBuf]);
};
// Actually build the buffer for the QR code
appendStr(qrData.prefix, "ascii", false);
appendByte(qrData.version);
appendByte(qrData.mode);
appendStr(qrData.transactionId, "utf-8");
appendEncBase64(qrData.firstKeyB64);
appendEncBase64(qrData.secondKeyB64);
appendEncBase64(qrData.secretB64);
return buf;
}
}

View File

@@ -23,7 +23,7 @@ import {
newUnexpectedMessageError,
newUnknownMethodError,
} from "../Error";
import * as olmlib from "../../olmlib";
import {QRCodeData, SCAN_QR_CODE_METHOD} from "../QRCode";
// the recommended amount of time before a verification request
// should be (automatically) cancelled without user interaction
@@ -70,10 +70,15 @@ export class VerificationRequest extends EventEmitter {
this._eventsByThem = new Map();
this._observeOnly = false;
this._timeoutTimer = null;
this._sharedSecret = null; // used for QR codes
this._accepting = false;
this._declining = false;
this._verifierHasFinished = false;
this._chosenMethod = null;
// we keep a copy of the QR Code data (including other user master key) around
// for QR reciprocate verification, to protect against
// cross-signing identity reset between the .ready and .start event
// and signing the wrong key after .start
this._qrCodeData = null;
}
/**
@@ -154,6 +159,11 @@ export class VerificationRequest extends EventEmitter {
return this._commonMethods;
}
/** the method picked in the .start event */
get chosenMethod() {
return this._chosenMethod;
}
/** The current remaining amount of ms before the request should be automatically cancelled */
get timeout() {
const requestEvent = this._getEventByEither(REQUEST_TYPE);
@@ -201,14 +211,20 @@ export class VerificationRequest extends EventEmitter {
this._phase !== PHASE_CANCELLED;
}
/** Only set after a .ready if the other party can scan a QR code */
get qrCodeData() {
return this._qrCodeData;
}
/** Checks whether the other party supports a given verification method.
* This is useful when setting up the QR code UI, as it is somewhat asymmetrical:
* if the other party supports SCAN_QR, we should show a QR code in the UI, and vice versa.
* For methods that need to be supported by both ends, use the `methods` property.
* @param {string} method the method to check
* @param {boolean} force to check even if the phase is not ready or started yet, internal usage
* @return {bool} whether or not the other party said the supported the method */
otherPartySupportsMethod(method) {
if (!this.ready && !this.started) {
otherPartySupportsMethod(method, force = false) {
if (!force && !this.ready && !this.started) {
return false;
}
const theirMethodEvent = this._eventsByThem.get(REQUEST_TYPE) ||
@@ -315,14 +331,6 @@ export class VerificationRequest extends EventEmitter {
return this._observeOnly;
}
/**
* The unpadded base64 encoded shared secret. Primarily used for QR code
* verification.
*/
get encodedSharedSecret() {
if (!this._sharedSecret) this._generateSharedSecret();
return this._sharedSecret;
}
/**
* Gets which device the verification should be started with
@@ -369,6 +377,7 @@ export class VerificationRequest extends EventEmitter {
if (!this._verifier) {
throw newUnknownMethodError();
}
this._chosenMethod = method;
}
}
return this._verifier;
@@ -382,7 +391,6 @@ export class VerificationRequest extends EventEmitter {
if (!this.observeOnly && this._phase === PHASE_UNSENT) {
const methods = [...this._verificationMethods.keys()];
await this.channel.send(REQUEST_TYPE, {methods});
this._generateSharedSecret();
}
}
@@ -415,16 +423,9 @@ export class VerificationRequest extends EventEmitter {
this._accepting = true;
this.emit("change");
await this.channel.send(READY_TYPE, {methods});
this._generateSharedSecret();
}
}
_generateSharedSecret() {
const secretBytes = new Uint8Array(8);
global.crypto.getRandomValues(secretBytes);
this._sharedSecret = olmlib.encodeUnpaddedBase64(secretBytes);
}
/**
* Can be used to listen for state changes until the callback returns true.
* @param {Function} fn callback to evaluate whether the request is in the desired state.
@@ -559,6 +560,14 @@ export class VerificationRequest extends EventEmitter {
const {method} = event.getContent();
if (!this._verifier && !this.observeOnly) {
this._verifier = this._createVerifier(method, event);
if (!this._verifier) {
this.cancel({
code: "m.unknown_method",
reason: `Unknown method: ${method}`,
});
} else {
this._chosenMethod = method;
}
}
}
}
@@ -684,6 +693,20 @@ export class VerificationRequest extends EventEmitter {
}
if (newTransitions.length) {
// create QRCodeData if the other side can scan
// important this happens before emitting a phase change,
// so listeners can rely on it being there already
// We only do this for live events because it is important that
// we sign the keys that were in the QR code, and not the keys
// we happen to have at some later point in time.
if (isLiveEvent && newTransitions.some(t => t.phase === PHASE_READY)) {
const shouldGenerateQrCode =
this.otherPartySupportsMethod(SCAN_QR_CODE_METHOD, true);
if (shouldGenerateQrCode) {
this._qrCodeData = await QRCodeData.create(this, this._client);
}
}
const lastTransition = newTransitions[newTransitions.length - 1];
const {phase} = lastTransition;