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

Add isSelfVerification property to VerificationRequest #1302

Merged
merged 2 commits into from
Apr 3, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,7 @@ wrapCryptoFuncs(MatrixClient, [
"requestSecret",
"getDefaultSecretStorageKeyId",
"setDefaultSecretStorageKeyId",
"checkSecretStorageKey",
"checkSecretStoragePrivateKey",
]);

Expand Down
55 changes: 48 additions & 7 deletions src/crypto/SecretStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export const SECRET_STORAGE_ALGORITHM_V1_AES
export const SECRET_STORAGE_ALGORITHM_V1_CURVE25519
= "m.secret_storage.v1.curve25519-aes-sha2";

const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";

/**
* Implements Secure Secret Storage and Sharing (MSC1946)
* @module crypto/SecretStorage
Expand Down Expand Up @@ -92,13 +94,13 @@ export class SecretStorage extends EventEmitter {
switch (algorithm) {
case SECRET_STORAGE_ALGORITHM_V1_AES:
{
const decryption = new global.Olm.PkDecryption();
try {
if (opts.passphrase) {
keyData.passphrase = opts.passphrase;
}
} finally {
decryption.free();
if (opts.passphrase) {
keyData.passphrase = opts.passphrase;
}
if (opts.key) {
const {iv, mac} = await encryptAES(ZERO_STR, opts.key, "");
keyData.iv = iv;
keyData.mac = mac;
}
break;
}
Expand Down Expand Up @@ -194,6 +196,45 @@ export class SecretStorage extends EventEmitter {
}
}

/**
* Check whether a key matches what we expect based on the key info
*
* @param {Uint8Array} key the key to check
* @param {object} info the key info
*
* @return {boolean} whether or not the key matches
*/
async checkKey(key, info) {
switch (info.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1_AES:
{
if (info.mac) {
const {mac} = await encryptAES(ZERO_STR, key, "", info.iv);
return info.mac === mac;
} else {
// if we have no information, we have to assume the key is right
return true;
}
}
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
{
let decryption = null;
try {
decryption = new global.Olm.PkDecryption();
const gotPubkey = decryption.init_with_private_key(key);
// make sure it agrees with the given pubkey
return gotPubkey === info.pubkey;
} catch (e) {
return false;
} finally {
if (decryption) decryption.free();
}
}
default:
throw new Error("Unknown algorithm");
}
}

/**
* Store an encrypted secret on the server
*
Expand Down
22 changes: 17 additions & 5 deletions src/crypto/aes.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,20 @@ const zerosalt = new Uint8Array(8);
* @param {string} data the plaintext to encrypt
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
* @param {string} ivStr the initialization vector to use
*/
async function encryptNode(data, key, name) {
async function encryptNode(data, key, name, ivStr) {
const crypto = getCrypto();
if (!crypto) {
throw new Error("No usable crypto implementation");
}

const iv = crypto.randomBytes(16);
let iv;
if (ivStr) {
iv = decodeBase64(ivStr);
} else {
iv = crypto.randomBytes(16);
}

// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
Expand Down Expand Up @@ -112,10 +118,16 @@ function deriveKeysNode(key, name) {
* @param {string} data the plaintext to encrypt
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
* @param {string} ivStr the initialization vector to use
*/
async function encryptBrowser(data, key, name) {
const iv = new Uint8Array(16);
window.crypto.getRandomValues(iv);
async function encryptBrowser(data, key, name, ivStr) {
let iv;
if (ivStr) {
iv = decodeBase64(ivStr);
} else {
iv = new Uint8Array(16);
window.crypto.getRandomValues(iv);
}

// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
Expand Down
16 changes: 14 additions & 2 deletions src/crypto/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,9 @@ Crypto.prototype.bootstrapSecretStorage = async function({
break;
}
}
if (oldKeyId) {
opts.key = ssssKeys[oldKeyId];
}
// create new symmetric SSSS key and set it as default
newKeyId = await this.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
Expand Down Expand Up @@ -609,12 +612,14 @@ Crypto.prototype.bootstrapSecretStorage = async function({
};
}

// use the backup key as the new ssss key
ssssKeys[newKeyId] = backupKey;
opts.key = backupKey;

newKeyId = await this.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
);
await this.setDefaultSecretStorageKeyId(newKeyId);
// use the backup key as the new ssss key
ssssKeys[newKeyId] = backupKey;
}

// if this key backup is trusted, sign it with the cross signing key
Expand Down Expand Up @@ -642,6 +647,9 @@ Crypto.prototype.bootstrapSecretStorage = async function({
if (!newKeyId) {
logger.log("Secret storage default key not found, creating new key");
const { keyInfo, privateKey } = await createSecretStorageKey();
if (keyInfo && privateKey) {
keyInfo.key = privateKey;
}
newKeyId = await this.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1_AES,
keyInfo,
Expand Down Expand Up @@ -763,6 +771,10 @@ Crypto.prototype.setDefaultSecretStorageKeyId = function(k) {
return this._secretStorage.setDefaultKeyId(k);
};

Crypto.prototype.checkSecretStorageKey = function(key, info) {
return this._secretStorage.checkKey(key, info);
};

/**
* Checks that a given secret storage private key matches a given public key.
* This can be used by the getSecretStorageKey callback to verify that the
Expand Down
5 changes: 5 additions & 0 deletions src/crypto/verification/request/InRoomChannel.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ export class InRoomChannel {
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
*/
async handleEvent(event, request, isLiveEvent) {
// prevent processing the same event multiple times, as under
// some circumstances Room.timeline can get emitted twice for the same event
if (request.hasEventId(event.getId())) {
return;
}
const type = InRoomChannel.getEventType(event);
// do validations that need state (roomId, userId),
// ignore if invalid
Expand Down
23 changes: 20 additions & 3 deletions src/crypto/verification/request/VerificationRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,10 @@ export class VerificationRequest extends EventEmitter {
return this.channel.userId;
}

get isSelfVerification() {
return this._client.getUserId() === this.otherUserId;
}

/**
* The id of the user that cancelled the request,
* only defined when phase is PHASE_CANCELLED
Expand Down Expand Up @@ -589,10 +593,9 @@ export class VerificationRequest extends EventEmitter {
return false;
}
const oldEvent = this._verifier.startEvent;
const isSelfVerification = this.channel.userId === this._client.getUserId();

let oldRaceIdentifier;
if (isSelfVerification) {
if (this.isSelfVerification) {
// if the verifier does not have a startEvent,
// it is because it's still sending and we are on the initator side
// we know we are sending a .start event because we already
Expand All @@ -612,7 +615,7 @@ export class VerificationRequest extends EventEmitter {
}

let newRaceIdentifier;
if (isSelfVerification) {
if (this.isSelfVerification) {
const newContent = newEvent.getContent();
newRaceIdentifier = newContent && newContent.from_device;
} else {
Expand All @@ -621,6 +624,20 @@ export class VerificationRequest extends EventEmitter {
return newRaceIdentifier < oldRaceIdentifier;
}

hasEventId(eventId) {
for (const event of this._eventsByUs.values()) {
if (event.getId() === eventId) {
return true;
}
}
for (const event of this._eventsByThem.values()) {
if (event.getId() === eventId) {
return true;
}
}
return false;
}

/**
* Changes the state of the request and verifier in response to a key verification event.
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
Expand Down
10 changes: 9 additions & 1 deletion src/models/room.js
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,11 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
const newEventId = remoteEvent.getId();
const oldStatus = localEvent.status;

logger.debug(
`Got remote echo for event ${oldEventId} -> ${newEventId} ` +
`old status ${oldStatus}`,
);

// no longer pending
delete this._txnToEvent[remoteEvent.getUnsigned().transaction_id];

Expand Down Expand Up @@ -1347,7 +1352,10 @@ ALLOWED_TRANSITIONS[EventStatus.CANCELLED] =
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
*/
Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
logger.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()}`);
logger.log(
`setting pendingEvent status to ${newStatus} in ${event.getRoomId()} ` +
`event ID ${event.getId()} -> ${newEventId}`,
);

// if the message was sent, we expect an event id
if (newStatus == EventStatus.SENT && !newEventId) {
Expand Down