diff --git a/src/crypto/verification/QRCode.js b/src/crypto/verification/QRCode.js index 1099b48f0..34d510164 100644 --- a/src/crypto/verification/QRCode.js +++ b/src/crypto/verification/QRCode.js @@ -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; + } +} diff --git a/src/crypto/verification/request/VerificationRequest.js b/src/crypto/verification/request/VerificationRequest.js index 3dcb70290..63c8479ef 100644 --- a/src/crypto/verification/request/VerificationRequest.js +++ b/src/crypto/verification/request/VerificationRequest.js @@ -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;