1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-05 00:42:10 +03:00

[CONFLICT CHUNKS] Merge branch 'develop' into travis/sourcemaps-dev

This commit is contained in:
Travis Ralston
2020-01-07 14:37:17 -07:00
16 changed files with 881 additions and 119 deletions

View File

@@ -615,6 +615,9 @@ describe("megolm", function() {
).respond(200, { ).respond(200, {
event_id: '$event_id', event_id: '$event_id',
}); });
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
).respond(200, {});
return Promise.all([ return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'), aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
@@ -712,6 +715,9 @@ describe("megolm", function() {
event_id: '$event_id', event_id: '$event_id',
}; };
}); });
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
).respond(200, {});
return Promise.all([ return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'), aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),

View File

@@ -8,6 +8,22 @@ import {Crypto} from "../../../../src/crypto";
import {logger} from "../../../../src/logger"; import {logger} from "../../../../src/logger";
import {MatrixEvent} from "../../../../src/models/event"; import {MatrixEvent} from "../../../../src/models/event";
<<<<<<< HEAD
=======
import sdk from '../../../..';
import algorithms from '../../../../lib/crypto/algorithms';
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../../MockStorageApi';
import testUtils from '../../../test-utils';
import OlmDevice from '../../../../lib/crypto/OlmDevice';
import Crypto from '../../../../lib/crypto';
import logger from '../../../../lib/logger';
import TestClient from '../../../TestClient';
import olmlib from '../../../../lib/crypto/olmlib';
import Room from '../../../../lib/models/room';
const MatrixEvent = sdk.MatrixEvent;
>>>>>>> develop
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2']; const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
@@ -340,4 +356,154 @@ describe("MegolmDecryption", function() {
expect(ct2.session_id).toEqual(ct1.session_id); expect(ct2.session_id).toEqual(ct1.session_id);
}); });
}); });
it("notifies devices that have been blocked", async function() {
const aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
const bobClient1 = (new TestClient(
"@bob:example.com", "bobdevice1",
)).client;
const bobClient2 = (new TestClient(
"@bob:example.com", "bobdevice2",
)).client;
await Promise.all([
aliceClient.initCrypto(),
bobClient1.initCrypto(),
bobClient2.initCrypto(),
]);
const aliceDevice = aliceClient._crypto._olmDevice;
const bobDevice1 = bobClient1._crypto._olmDevice;
const bobDevice2 = bobClient2._crypto._olmDevice;
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const room = new Room(roomId, aliceClient, "@alice:example.com", {});
room.getEncryptionTargetMembers = async function() {
return [{userId: "@bob:example.com"}];
};
room.setBlacklistUnverifiedDevices(true);
aliceClient.store.storeRoom(room);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
const BOB_DEVICES = {
bobdevice1: {
user_id: "@bob:example.com",
device_id: "bobdevice1",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Dynabook": bobDevice1.deviceEd25519Key,
"curve25519:Dynabook": bobDevice1.deviceCurve25519Key,
},
verified: 0,
},
bobdevice2: {
user_id: "@bob:example.com",
device_id: "bobdevice2",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Dynabook": bobDevice2.deviceEd25519Key,
"curve25519:Dynabook": bobDevice2.deviceCurve25519Key,
},
verified: -1,
},
};
aliceClient._crypto._deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
return this._getDevicesFromStore(userIds);
};
let run = false;
aliceClient.sendToDevice = async (msgtype, contentMap) => {
run = true;
expect(msgtype).toBe("org.matrix.room_key.withheld");
delete contentMap["@bob:example.com"].bobdevice1.session_id;
delete contentMap["@bob:example.com"].bobdevice2.session_id;
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice1: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
code: 'm.unverified',
reason:
'The sender has disabled encrypting to unverified devices.',
sender_key: aliceDevice.deviceCurve25519Key,
},
bobdevice2: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
code: 'm.blacklisted',
reason: 'The sender has blocked you.',
sender_key: aliceDevice.deviceCurve25519Key,
},
},
});
};
const event = new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$event",
content: {
msgtype: "m.text",
body: "secret",
},
});
await aliceClient._crypto.encryptEvent(event, room);
expect(run).toBe(true);
aliceClient.stopClient();
bobClient1.stopClient();
bobClient2.stopClient();
});
it("throws an error describing why it doesn't have a key", async function() {
const aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
const bobClient = (new TestClient(
"@bob:example.com", "bobdevice",
)).client;
await Promise.all([
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const bobDevice = bobClient._crypto._olmDevice;
const roomId = "!someroom";
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
type: "org.matrix.room_key.withheld",
sender: "@bob:example.com",
content: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
session_id: "session_id",
sender_key: bobDevice.deviceCurve25519Key,
code: "m.blacklisted",
reason: "You have been blocked",
},
}));
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
room_id: roomId,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: "blablabla",
device_id: "bobdevice",
sender_key: bobDevice.deviceCurve25519Key,
session_id: "session_id",
},
}))).rejects.toThrow("The sender has blocked you.");
});
}); });

View File

@@ -334,7 +334,7 @@ describe("MegolmBackup", function() {
}); });
await client.resetCrossSigningKeys(); await client.resetCrossSigningKeys();
let numCalls = 0; let numCalls = 0;
await new Promise(async (resolve, reject) => { await new Promise((resolve, reject) => {
client._http.authedRequest = function( client._http.authedRequest = function(
callback, method, path, queryParams, data, opts, callback, method, path, queryParams, data, opts,
) { ) {
@@ -359,7 +359,7 @@ describe("MegolmBackup", function() {
resolve(); resolve();
return Promise.resolve({}); return Promise.resolve({});
}; };
await client.createKeyBackupVersion({ client.createKeyBackupVersion({
algorithm: "m.megolm_backup.v1", algorithm: "m.megolm_backup.v1",
auth_data: { auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",

View File

@@ -22,6 +22,7 @@ limitations under the License.
* This is an internal module. See {@link MatrixClient} for the public class. * This is an internal module. See {@link MatrixClient} for the public class.
* @module client * @module client
*/ */
<<<<<<< HEAD
import url from "url"; import url from "url";
import {EventEmitter} from "events"; import {EventEmitter} from "events";
@@ -47,6 +48,37 @@ import {decodeRecoveryKey} from './crypto/recoverykey';
import {keyFromAuthData} from './crypto/key_passphrase'; import {keyFromAuthData} from './crypto/key_passphrase';
import {randomString} from './randomstring'; import {randomString} from './randomstring';
import {PushProcessor} from "./pushprocessor"; import {PushProcessor} from "./pushprocessor";
=======
const EventEmitter = require("events").EventEmitter;
const url = require('url');
const httpApi = require("./http-api");
const MatrixEvent = require("./models/event").MatrixEvent;
const EventStatus = require("./models/event").EventStatus;
const EventTimeline = require("./models/event-timeline");
const SearchResult = require("./models/search-result");
const StubStore = require("./store/stub");
const webRtcCall = require("./webrtc/call");
const utils = require("./utils");
const contentRepo = require("./content-repo");
const Filter = require("./filter");
const SyncApi = require("./sync");
const MatrixBaseApis = require("./base-apis");
const MatrixError = httpApi.MatrixError;
const ContentHelpers = require("./content-helpers");
const olmlib = require("./crypto/olmlib");
import ReEmitter from './ReEmitter';
import RoomList from './crypto/RoomList';
import logger from './logger';
import Crypto from './crypto';
import { isCryptoAvailable } from './crypto';
import { decodeRecoveryKey } from './crypto/recoverykey';
import { keyFromAuthData } from './crypto/key_passphrase';
import { randomString } from './randomstring';
import { encodeBase64, decodeBase64 } from '../lib/crypto/olmlib';
>>>>>>> develop
const SCROLLBACK_DELAY_MS = 3000; const SCROLLBACK_DELAY_MS = 3000;
export const CRYPTO_ENABLED = isCryptoAvailable(); export const CRYPTO_ENABLED = isCryptoAvailable();
@@ -666,7 +698,7 @@ MatrixClient.prototype.initCrypto = async function() {
"crypto.warning", "crypto.warning",
"crypto.devicesUpdated", "crypto.devicesUpdated",
"deviceVerificationChanged", "deviceVerificationChanged",
"userVerificationChanged", "userTrustStatusChanged",
"crossSigning.keysChanged", "crossSigning.keysChanged",
]); ]);
@@ -1431,7 +1463,7 @@ MatrixClient.prototype.disableKeyBackup = function() {
* when restoring the backup as an alternative to entering the recovery key. * when restoring the backup as an alternative to entering the recovery key.
* Optional. * Optional.
* @param {boolean} [opts.secureSecretStorage = false] Whether to use Secure * @param {boolean} [opts.secureSecretStorage = false] Whether to use Secure
* Secret Storage (MSC1946) to store the key encrypting key backups. * Secret Storage to store the key encrypting key backups.
* Optional, defaults to false. * Optional, defaults to false.
* *
* @returns {Promise<object>} Object that can be passed to createKeyBackupVersion and * @returns {Promise<object>} Object that can be passed to createKeyBackupVersion and
@@ -1445,32 +1477,37 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(
throw new Error("End-to-end encryption disabled"); throw new Error("End-to-end encryption disabled");
} }
if (secureSecretStorage) { const [keyInfo, encodedPrivateKey, privateKey] =
logger.log("Preparing key backup version with Secure Secret Storage");
// Ensure Secure Secret Storage is ready for use
if (!this.hasSecretStorageKey()) {
throw new Error("Secure Secret Storage has no keys, needs bootstrapping");
}
throw new Error("Not yet implemented");
}
const [keyInfo, encodedPrivateKey] =
await this.createRecoveryKeyFromPassphrase(password); await this.createRecoveryKeyFromPassphrase(password);
if (secureSecretStorage) {
await this.storeSecret("m.megolm_backup.v1", encodeBase64(privateKey));
logger.info("Key backup private key stored in secret storage");
}
// Reshape objects into form expected for key backup // Reshape objects into form expected for key backup
const authData = {
public_key: keyInfo.pubkey,
};
if (keyInfo.passphrase) {
authData.private_key_salt = keyInfo.passphrase.salt;
authData.private_key_iterations = keyInfo.passphrase.iterations;
}
return { return {
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
auth_data: { auth_data: authData,
public_key: keyInfo.pubkey,
private_key_salt: keyInfo.passphrase.salt,
private_key_iterations: keyInfo.passphrase.iterations,
},
recovery_key: encodedPrivateKey, recovery_key: encodedPrivateKey,
}; };
}; };
/**
* Check whether the key backup private key is stored in secret storage.
* @return {Promise<boolean>} Whether the backup key is stored.
*/
MatrixClient.prototype.isKeyBackupKeyStored = async function() {
return this.isSecretStored("m.megolm_backup.v1", false /* checkKey */);
};
/** /**
* Create a new key backup version and enable it, using the information return * Create a new key backup version and enable it, using the information return
* from prepareKeyBackupVersion. * from prepareKeyBackupVersion.
@@ -1488,13 +1525,19 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info) {
auth_data: info.auth_data, auth_data: info.auth_data,
}; };
// Now sign the backup auth data. Do it as this device first because crypto._signObject // Sign the backup auth data with the device key for backwards compat with
// is dumb and bluntly replaces the whole signatures block... // older devices with cross-signing. This can probably go away very soon in
// this can probably go away very soon in favour of just signing with the SSK. // favour of just signing with the cross-singing master key.
await this._crypto._signObject(data.auth_data); await this._crypto._signObject(data.auth_data);
if (this._crypto._crossSigningInfo.getId()) { if (
// now also sign the auth data with the master key this._cryptoCallbacks.getCrossSigningKey &&
this._crypto._crossSigningInfo.getId()
) {
// now also sign the auth data with the cross-signing master key
// we check for the callback explicitly here because we still want to be able
// to create an un-cross-signed key backup if there is a cross-signing key but
// no callback supplied.
await this._crypto._crossSigningInfo.signObject(data.auth_data, "master"); await this._crypto._crossSigningInfo.signObject(data.auth_data, "master");
} }
@@ -1502,11 +1545,15 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info) {
undefined, "POST", "/room_keys/version", undefined, data, undefined, "POST", "/room_keys/version", undefined, data,
{prefix: PREFIX_UNSTABLE}, {prefix: PREFIX_UNSTABLE},
); );
this.enableKeyBackup({
algorithm: info.algorithm, // We could assume everything's okay and enable directly, but this ensures
auth_data: info.auth_data, // we run the same signature verification that will be used for future
version: res.version, // sessions.
}); await this.checkKeyBackup();
if (!this.getKeyBackupEnabled()) {
logger.error("Key backup not usable even though we just created it");
}
return res; return res;
}; };
@@ -1610,6 +1657,18 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY'; MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
/**
* Restore from an existing key backup via a passphrase.
*
* @param {string} password Passphrase
* @param {string} [targetRoomId] Room ID to target a specific room.
* Restores all rooms if omitted.
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithPassword = async function( MatrixClient.prototype.restoreKeyBackupWithPassword = async function(
password, targetRoomId, targetSessionId, backupInfo, password, targetRoomId, targetSessionId, backupInfo,
) { ) {
@@ -1619,6 +1678,39 @@ MatrixClient.prototype.restoreKeyBackupWithPassword = async function(
); );
}; };
/**
* Restore from an existing key backup via a private key stored in secret
* storage.
*
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @param {string} [targetRoomId] Room ID to target a specific room.
* Restores all rooms if omitted.
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithSecretStorage = async function(
backupInfo, targetRoomId, targetSessionId,
) {
const privKey = decodeBase64(await this.getSecret("m.megolm_backup.v1"));
return this._restoreKeyBackup(
privKey, targetRoomId, targetSessionId, backupInfo,
);
};
/**
* Restore from an existing key backup via an encoded recovery key.
*
* @param {string} recoveryKey Encoded recovery key
* @param {string} [targetRoomId] Room ID to target a specific room.
* Restores all rooms if omitted.
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function( MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function(
recoveryKey, targetRoomId, targetSessionId, backupInfo, recoveryKey, targetRoomId, targetSessionId, backupInfo,
) { ) {

View File

@@ -1,7 +1,11 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2017, 2019 New Vector Ltd Copyright 2017, 2019 New Vector Ltd
<<<<<<< HEAD
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019 The Matrix.org Foundation C.I.C.
=======
Copyright 2020 The Matrix.org Foundation C.I.C.
>>>>>>> develop
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -19,6 +23,8 @@ limitations under the License.
import {logger} from '../logger'; import {logger} from '../logger';
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store'; import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
import algorithms from './algorithms';
// The maximum size of an event is 65K, and we base64 the content, so this is a // The maximum size of an event is 65K, and we base64 the content, so this is a
// reasonable approximation to the biggest plaintext we can encrypt. // reasonable approximation to the biggest plaintext we can encrypt.
const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4; const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4;
@@ -819,9 +825,9 @@ OlmDevice.prototype._getInboundGroupSession = function(
roomId, senderKey, sessionId, txn, func, roomId, senderKey, sessionId, txn, func,
) { ) {
this._cryptoStore.getEndToEndInboundGroupSession( this._cryptoStore.getEndToEndInboundGroupSession(
senderKey, sessionId, txn, (sessionData) => { senderKey, sessionId, txn, (sessionData, withheld) => {
if (sessionData === null) { if (sessionData === null) {
func(null); func(null, null, withheld);
return; return;
} }
@@ -835,7 +841,7 @@ OlmDevice.prototype._getInboundGroupSession = function(
} }
this._unpickleInboundGroupSession(sessionData, (session) => { this._unpickleInboundGroupSession(sessionData, (session) => {
func(session, sessionData); func(session, sessionData, withheld);
}); });
}, },
); );
@@ -860,7 +866,10 @@ OlmDevice.prototype.addInboundGroupSession = async function(
exportFormat, exportFormat,
) { ) {
await this._cryptoStore.doTxn( await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => { 'readwrite', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
], (txn) => {
/* if we already have this session, consider updating it */ /* if we already have this session, consider updating it */
this._getInboundGroupSession( this._getInboundGroupSession(
roomId, senderKey, sessionId, txn, roomId, senderKey, sessionId, txn,
@@ -915,6 +924,60 @@ OlmDevice.prototype.addInboundGroupSession = async function(
); );
}; };
/**
* Record in the data store why an inbound group session was withheld.
*
* @param {string} roomId room that the session belongs to
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {string} code reason code
* @param {string} reason human-readable version of `code`
*/
OlmDevice.prototype.addInboundGroupSessionWithheld = async function(
roomId, senderKey, sessionId, code, reason,
) {
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD],
(txn) => {
this._cryptoStore.storeEndToEndInboundGroupSessionWithheld(
senderKey, sessionId,
{
room_id: roomId,
code: code,
reason: reason,
},
txn,
);
},
);
};
export const WITHHELD_MESSAGES = {
"m.unverified": "The sender has disabled encrypting to unverified devices.",
"m.blacklisted": "The sender has blocked you.",
"m.unauthorised": "You are not authorised to read the message.",
"m.no_olm": "Unable to establish a secure channel.",
};
/**
* Calculate the message to use for the exception when a session key is withheld.
*
* @param {object} withheld An object that describes why the key was withheld.
*
* @return {string} the message
*
* @private
*/
function _calculateWithheldMessage(withheld) {
if (withheld.code && withheld.code in WITHHELD_MESSAGES) {
return WITHHELD_MESSAGES[withheld.code];
} else if (withheld.reason) {
return withheld.reason;
} else {
return "decryption key withheld";
}
}
/** /**
* Decrypt a received message with an inbound group session * Decrypt a received message with an inbound group session
* *
@@ -935,16 +998,49 @@ OlmDevice.prototype.decryptGroupMessage = async function(
roomId, senderKey, sessionId, body, eventId, timestamp, roomId, senderKey, sessionId, body, eventId, timestamp,
) { ) {
let result; let result;
// when the localstorage crypto store is used as an indexeddb backend,
// exceptions thrown from within the inner function are not passed through
// to the top level, so we store exceptions in a variable and raise them at
// the end
let error;
await this._cryptoStore.doTxn( await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => { 'readwrite', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
], (txn) => {
this._getInboundGroupSession( this._getInboundGroupSession(
roomId, senderKey, sessionId, txn, (session, sessionData) => { roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => {
if (session === null) { if (session === null) {
if (withheld) {
error = new algorithms.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
_calculateWithheldMessage(withheld),
{
session: senderKey + '|' + sessionId,
},
);
}
result = null; result = null;
return; return;
} }
const res = session.decrypt(body); let res;
try {
res = session.decrypt(body);
} catch (e) {
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX' && withheld) {
error = new algorithms.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
_calculateWithheldMessage(withheld),
{
session: senderKey + '|' + sessionId,
},
);
} else {
error = e;
}
return;
}
let plaintext = res.plaintext; let plaintext = res.plaintext;
if (plaintext === undefined) { if (plaintext === undefined) {
@@ -966,10 +1062,11 @@ OlmDevice.prototype.decryptGroupMessage = async function(
msgInfo.id !== eventId || msgInfo.id !== eventId ||
msgInfo.timestamp !== timestamp msgInfo.timestamp !== timestamp
) { ) {
throw new Error( error = new Error(
"Duplicate message index, possible replay attack: " + "Duplicate message index, possible replay attack: " +
messageIndexKey, messageIndexKey,
); );
return;
} }
} }
this._inboundGroupSessionMessageIndexes[messageIndexKey] = { this._inboundGroupSessionMessageIndexes[messageIndexKey] = {
@@ -995,6 +1092,9 @@ OlmDevice.prototype.decryptGroupMessage = async function(
}, },
); );
if (error) {
throw error;
}
return result; return result;
}; };
@@ -1010,7 +1110,10 @@ OlmDevice.prototype.decryptGroupMessage = async function(
OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, sessionId) { OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, sessionId) {
let result; let result;
await this._cryptoStore.doTxn( await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => { 'readonly', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
], (txn) => {
this._cryptoStore.getEndToEndInboundGroupSession( this._cryptoStore.getEndToEndInboundGroupSession(
senderKey, sessionId, txn, (sessionData) => { senderKey, sessionId, txn, (sessionData) => {
if (sessionData === null) { if (sessionData === null) {
@@ -1061,7 +1164,10 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
) { ) {
let result; let result;
await this._cryptoStore.doTxn( await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => { 'readonly', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
], (txn) => {
this._getInboundGroupSession( this._getInboundGroupSession(
roomId, senderKey, sessionId, txn, (session, sessionData) => { roomId, senderKey, sessionId, txn, (session, sessionData) => {
if (session === null) { if (session === null) {

View File

@@ -231,6 +231,27 @@ export class SecretStorage extends EventEmitter {
await this._baseApis.setAccountData(name, {encrypted}); await this._baseApis.setAccountData(name, {encrypted});
} }
/**
* Store a secret defined to be the same as the given key.
* No secret information will be stored, instead the secret will
* be stored with a marker to say that the contents of the secret is
* the value of the given key.
* This is useful for migration from systems that predate SSSS such as
* key backup.
*
* @param {string} name The name of the secret
* @param {string} keyId The ID of the key whose value will be the
* value of the secret
* @returns {Promise} resolved when account data is saved
*/
storePassthrough(name, keyId) {
return this._baseApis.setAccountData(name, {
[keyId]: {
passthrough: true,
},
});
}
/** /**
* Get a secret from storage. * Get a secret from storage.
* *
@@ -276,8 +297,13 @@ export class SecretStorage extends EventEmitter {
// fetch private key from app // fetch private key from app
[keyId, decryption] = await this._getSecretStorageKey(keys); [keyId, decryption] = await this._getSecretStorageKey(keys);
// decrypt secret
const encInfo = secretContent.encrypted[keyId]; const encInfo = secretContent.encrypted[keyId];
// We don't actually need the decryption object if it's a passthrough
// since we just want to return the key itself.
if (encInfo.passthrough) return decryption.get_private_key();
// decrypt secret
switch (keys[keyId].algorithm) { switch (keys[keyId].algorithm) {
case SECRET_STORAGE_ALGORITHM_V1: case SECRET_STORAGE_ALGORITHM_V1:
return decryption.decrypt( return decryption.decrypt(

View File

@@ -1,6 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -33,6 +34,8 @@ import {
UnknownDeviceError, UnknownDeviceError,
} from "./base"; } from "./base";
import {WITHHELD_MESSAGES} from '../OlmDevice';
/** /**
* @private * @private
* @constructor * @constructor
@@ -52,6 +55,7 @@ function OutboundSessionInfo(sessionId) {
this.useCount = 0; this.useCount = 0;
this.creationTime = new Date().getTime(); this.creationTime = new Date().getTime();
this.sharedWithDevices = {}; this.sharedWithDevices = {};
this.blockedDevicesNotified = {};
} }
@@ -89,6 +93,15 @@ OutboundSessionInfo.prototype.markSharedWithDevice = function(
this.sharedWithDevices[userId][deviceId] = chainIndex; this.sharedWithDevices[userId][deviceId] = chainIndex;
}; };
OutboundSessionInfo.prototype.markNotifiedBlockedDevice = function(
userId, deviceId,
) {
if (!this.blockedDevicesNotified[userId]) {
this.blockedDevicesNotified[userId] = {};
}
this.blockedDevicesNotified[userId][deviceId] = true;
};
/** /**
* Determine if this session has been shared with devices which it shouldn't * Determine if this session has been shared with devices which it shouldn't
* have been. * have been.
@@ -171,11 +184,14 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
* @private * @private
* *
* @param {Object} devicesInRoom The devices in this room, indexed by user ID * @param {Object} devicesInRoom The devices in this room, indexed by user ID
* @param {Object} blocked The devices that are blocked, indexed by user ID
* *
* @return {module:client.Promise} Promise which resolves to the * @return {module:client.Promise} Promise which resolves to the
* OutboundSessionInfo when setup is complete. * OutboundSessionInfo when setup is complete.
*/ */
MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) { MegolmEncryption.prototype._ensureOutboundSession = async function(
devicesInRoom, blocked,
) {
const self = this; const self = this;
let session; let session;
@@ -242,9 +258,36 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
} }
} }
return self._shareKeyWithDevices( await self._shareKeyWithDevices(
session, shareMap, session, shareMap,
); );
// are there any new blocked devices that we need to notify?
const blockedMap = {};
for (const userId in blocked) {
if (!blocked.hasOwnProperty(userId)) {
continue;
}
const userBlockedDevices = blocked[userId];
for (const deviceId in userBlockedDevices) {
if (!userBlockedDevices.hasOwnProperty(deviceId)) {
continue;
}
if (
!session.blockedDevicesNotified[userId] ||
session.blockedDevicesNotified[userId][deviceId] === undefined
) {
blockedMap[userId] = blockedMap[userId] || [];
blockedMap[userId].push(userBlockedDevices[deviceId]);
}
}
}
// notify blocked devices that they're blocked
await self._notifyBlockedDevices(session, blockedMap);
} }
// helper which returns the session prepared by prepareSession // helper which returns the session prepared by prepareSession
@@ -368,6 +411,42 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
return mapSlices; return mapSlices;
}; };
/**
* @private
*
* @param {object} devicesByUser map from userid to list of devices
*
* @return {array<array<object>>} the blocked devices, split into chunks
*/
MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) {
const maxToDeviceMessagesPerRequest = 20;
// use an array where the slices of a content map gets stored
let currentSlice = [];
const mapSlices = [currentSlice];
for (const userId of Object.keys(devicesByUser)) {
const userBlockedDevicesToShareWith = devicesByUser[userId];
for (const blockedInfo of userBlockedDevicesToShareWith) {
if (currentSlice.length > maxToDeviceMessagesPerRequest) {
// the current slice is filled up. Start inserting into the next slice
currentSlice = [];
mapSlices.push(currentSlice);
}
currentSlice.push({
userId: userId,
blockedInfo: blockedInfo,
});
}
}
if (currentSlice.length === 0) {
mapSlices.pop();
}
return mapSlices;
};
/** /**
* @private * @private
* *
@@ -432,6 +511,49 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
}); });
}; };
/**
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {array<object>} userDeviceMap list of blocked devices to notify
*
* @param {object} payload fields to include in the notification payload
*
* @return {module:client.Promise} Promise which resolves once the notifications
* for the given userDeviceMap is generated and has been sent.
*/
MegolmEncryption.prototype._sendBlockedNotificationsToDevices = async function(
session, userDeviceMap, payload,
) {
const contentMap = {};
for (const val of userDeviceMap) {
const userId = val.userId;
const blockedInfo = val.blockedInfo;
const deviceInfo = blockedInfo.deviceInfo;
const deviceId = deviceInfo.deviceId;
const message = Object.assign({}, payload);
message.code = blockedInfo.code;
message.reason = blockedInfo.reason;
if (!contentMap[userId]) {
contentMap[userId] = {};
}
contentMap[userId][deviceId] = message;
}
await this._baseApis.sendToDevice("org.matrix.room_key.withheld", contentMap);
// store that we successfully uploaded the keys of the current slice
for (const userId of Object.keys(contentMap)) {
for (const deviceId of Object.keys(contentMap[userId])) {
session.markNotifiedBlockedDevice(userId, deviceId);
}
}
};
/** /**
* Re-shares a megolm session key with devices if the key has already been * Re-shares a megolm session key with devices if the key has already been
* sent to them. * sent to them.
@@ -566,6 +688,42 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
} }
}; };
/**
* Notify blocked devices that they have been blocked.
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object<string, object>} devicesByUser
* map from userid to device ID to blocked data
*/
MegolmEncryption.prototype._notifyBlockedDevices = async function(
session, devicesByUser,
) {
const payload = {
room_id: this._roomId,
session_id: session.sessionId,
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
};
const userDeviceMaps = this._splitBlockedDevices(devicesByUser);
for (let i = 0; i < userDeviceMaps.length; i++) {
try {
await this._sendBlockedNotificationsToDevices(
session, userDeviceMaps[i], payload,
);
logger.log(`Completed blacklist notification for ${session.sessionId} `
+ `in ${this._roomId} (slice ${i + 1}/${userDeviceMaps.length})`);
} catch (e) {
logger.log(`blacklist notification for ${session.sessionId} in `
+ `${this._roomId} (slice ${i + 1}/${userDeviceMaps.length}) failed`);
throw e;
}
}
};
/** /**
* @inheritdoc * @inheritdoc
* *
@@ -575,42 +733,41 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
* *
* @return {module:client.Promise} Promise which resolves to the new event body * @return {module:client.Promise} Promise which resolves to the new event body
*/ */
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) { MegolmEncryption.prototype.encryptMessage = async function(room, eventType, content) {
const self = this; const self = this;
logger.log(`Starting to encrypt event for ${this._roomId}`); logger.log(`Starting to encrypt event for ${this._roomId}`);
return this._getDevicesInRoom(room).then(function(devicesInRoom) { const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
// check if any of these devices are not yet known to the user.
// if so, warn the user so they can verify or ignore.
self._checkForUnknownDevices(devicesInRoom);
return self._ensureOutboundSession(devicesInRoom); // check if any of these devices are not yet known to the user.
}).then(function(session) { // if so, warn the user so they can verify or ignore.
const payloadJson = { self._checkForUnknownDevices(devicesInRoom);
room_id: self._roomId,
type: eventType,
content: content,
};
const ciphertext = self._olmDevice.encryptGroupMessage( const session = await self._ensureOutboundSession(devicesInRoom, blocked);
session.sessionId, JSON.stringify(payloadJson), const payloadJson = {
); room_id: self._roomId,
type: eventType,
content: content,
};
const encryptedContent = { const ciphertext = self._olmDevice.encryptGroupMessage(
algorithm: olmlib.MEGOLM_ALGORITHM, session.sessionId, JSON.stringify(payloadJson),
sender_key: self._olmDevice.deviceCurve25519Key, );
ciphertext: ciphertext,
session_id: session.sessionId,
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
// XXX: Do we still need this now that m.new_device messages
// no longer exist since #483?
device_id: self._deviceId,
};
session.useCount++; const encryptedContent = {
return encryptedContent; algorithm: olmlib.MEGOLM_ALGORITHM,
}); sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext,
session_id: session.sessionId,
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
// XXX: Do we still need this now that m.new_device messages
// no longer exist since #483?
device_id: self._deviceId,
};
session.useCount++;
return encryptedContent;
}; };
/** /**
@@ -659,8 +816,11 @@ MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) {
* *
* @param {module:models/room} room * @param {module:models/room} room
* *
* @return {module:client.Promise} Promise which resolves to a map * @return {module:client.Promise} Promise which resolves to an array whose
* from userId to deviceId to deviceInfo * first element is a map from userId to deviceId to deviceInfo indicating
* the devices that messages should be encrypted to, and whose second
* element is a map from userId to deviceId to data indicating the devices
* that are in the room but that have been blocked
*/ */
MegolmEncryption.prototype._getDevicesInRoom = async function(room) { MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
const members = await room.getEncryptionTargetMembers(); const members = await room.getEncryptionTargetMembers();
@@ -681,6 +841,7 @@ MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
// using all the device_lists changes and left fields. // using all the device_lists changes and left fields.
// See https://github.com/vector-im/riot-web/issues/2305 for details. // See https://github.com/vector-im/riot-web/issues/2305 for details.
const devices = await this._crypto.downloadKeys(roomMembers, false); const devices = await this._crypto.downloadKeys(roomMembers, false);
const blocked = {};
// remove any blocked devices // remove any blocked devices
for (const userId in devices) { for (const userId in devices) {
if (!devices.hasOwnProperty(userId)) { if (!devices.hasOwnProperty(userId)) {
@@ -695,13 +856,27 @@ MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
if (userDevices[deviceId].isBlocked() || if (userDevices[deviceId].isBlocked() ||
(userDevices[deviceId].isUnverified() && isBlacklisting) (userDevices[deviceId].isUnverified() && isBlacklisting)
) { ) {
if (!blocked[userId]) {
blocked[userId] = {};
}
const blockedInfo = userDevices[deviceId].isBlocked()
? {
code: "m.blacklisted",
reason: WITHHELD_MESSAGES["m.blacklisted"],
}
: {
code: "m.unverified",
reason: WITHHELD_MESSAGES["m.unverified"],
};
blockedInfo.deviceInfo = userDevices[deviceId];
blocked[userId][deviceId] = blockedInfo;
delete userDevices[deviceId]; delete userDevices[deviceId];
} }
} }
} }
return devices; return [devices, blocked];
}; };
/** /**
@@ -761,6 +936,11 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
event.getId(), event.getTs(), event.getId(), event.getTs(),
); );
} catch (e) { } catch (e) {
if (e.name === "DecryptionError") {
// re-throw decryption errors as-is
throw e;
}
let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR"; let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') { if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
@@ -968,6 +1148,20 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
}); });
}; };
/**
* @inheritdoc
*
* @param {module:models/event.MatrixEvent} event key event
*/
MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) {
const content = event.getContent();
await this._olmDevice.addInboundGroupSessionWithheld(
content.room_id, content.sender_key, content.session_id, content.code,
content.reason,
);
};
/** /**
* @inheritdoc * @inheritdoc
*/ */

View File

@@ -2,7 +2,7 @@
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2018-2019 New Vector Ltd Copyright 2018-2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019-2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -214,7 +214,7 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
); );
// Assuming no app-supplied callback, default to getting from SSSS. // Assuming no app-supplied callback, default to getting from SSSS.
if (!cryptoCallbacks.getCrossSigningKey) { if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) {
cryptoCallbacks.getCrossSigningKey = async (type) => { cryptoCallbacks.getCrossSigningKey = async (type) => {
return CrossSigningInfo.getFromSecretStorage(type, this._secretStorage); return CrossSigningInfo.getFromSecretStorage(type, this._secretStorage);
}; };
@@ -292,8 +292,9 @@ Crypto.prototype.init = async function() {
* @param {string} password Passphrase string that can be entered by the user * @param {string} password Passphrase string that can be entered by the user
* when restoring the backup as an alternative to entering the recovery key. * when restoring the backup as an alternative to entering the recovery key.
* Optional. * Optional.
* @returns {Promise<Array>} Array with public key metadata and encoded private * @returns {Promise<Array>} Array with public key metadata, encoded private
* recovery key which should be disposed of after displaying to the user. * recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed.
*/ */
Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) { Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) {
const decryption = new global.Olm.PkDecryption(); const decryption = new global.Olm.PkDecryption();
@@ -310,10 +311,11 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) {
} else { } else {
keyInfo.pubkey = decryption.generate_key(); keyInfo.pubkey = decryption.generate_key();
} }
const encodedPrivateKey = encodeRecoveryKey(decryption.get_private_key()); const privateKey = decryption.get_private_key();
return [keyInfo, encodedPrivateKey]; const encodedPrivateKey = encodeRecoveryKey(privateKey);
return [keyInfo, encodedPrivateKey, privateKey];
} finally { } finally {
decryption.free(); if (decryption) decryption.free();
} }
}; };
@@ -330,6 +332,8 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) {
* auth data as an object. * auth data as an object.
* @param {function} [opts.createSecretStorageKey] Optional. Function * @param {function} [opts.createSecretStorageKey] Optional. Function
* called to await a secret storage key creation flow. * called to await a secret storage key creation flow.
* @param {object} [opts.keyBackupInfo] The current key backup object. If passed,
* the passphrase and recovery key from this backup will be used.
* Returns: * Returns:
* {Promise} A promise which resolves to key creation data for * {Promise} A promise which resolves to key creation data for
* SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields. * SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields.
@@ -337,6 +341,7 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) {
Crypto.prototype.bootstrapSecretStorage = async function({ Crypto.prototype.bootstrapSecretStorage = async function({
authUploadDeviceSigningKeys, authUploadDeviceSigningKeys,
createSecretStorageKey = async () => { }, createSecretStorageKey = async () => { },
keyBackupInfo,
} = {}) { } = {}) {
logger.log("Bootstrapping Secure Secret Storage"); logger.log("Bootstrapping Secure Secret Storage");
@@ -377,18 +382,49 @@ Crypto.prototype.bootstrapSecretStorage = async function({
{ authUploadDeviceSigningKeys }, { authUploadDeviceSigningKeys },
); );
} }
} else {
logger.log("Cross signing keys are present in secret storage");
} }
// Check if Secure Secret Storage has a default key. If we don't have one, create // Check if Secure Secret Storage has a default key. If we don't have one, create
// the default key (which will also be signed by the cross-signing master key). // the default key (which will also be signed by the cross-signing master key).
if (!this.hasSecretStorageKey()) { if (!this.hasSecretStorageKey()) {
logger.log("Secret storage default key not found, creating new key"); let newKeyId;
const keyOptions = await createSecretStorageKey(); if (keyBackupInfo) {
const newKeyId = await this.addSecretStorageKey( logger.log("Secret storage default key not found, using key backup key");
SECRET_STORAGE_ALGORITHM_V1, const opts = {
keyOptions, pubkey: keyBackupInfo.auth_data.public_key,
); };
if (
keyBackupInfo.auth_data.private_key_salt &&
keyBackupInfo.auth_data.private_key_iterations
) {
opts.passphrase = {
algorithm: "m.pbkdf2",
iterations: keyBackupInfo.auth_data.private_key_iterations,
salt: keyBackupInfo.auth_data.private_key_salt,
};
}
newKeyId = await this.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1, opts,
);
// Add an entry for the backup key in SSSS as a 'passthrough' key
// (ie. the secret is the key itself).
this._secretStorage.storePassthrough('m.megolm_backup.v1', newKeyId);
} else {
logger.log("Secret storage default key not found, creating new key");
const keyOptions = await createSecretStorageKey();
newKeyId = await this.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1,
keyOptions,
);
}
await this.setDefaultSecretStorageKeyId(newKeyId); await this.setDefaultSecretStorageKeyId(newKeyId);
} else {
logger.log("Have secret storage key");
} }
// If cross-signing keys were reset, store them in Secure Secret Storage. // If cross-signing keys were reset, store them in Secure Secret Storage.
@@ -548,8 +584,8 @@ Crypto.prototype.resetCrossSigningKeys = async function(level, {
/** /**
* Run various follow-up actions after cross-signing keys have changed locally * Run various follow-up actions after cross-signing keys have changed locally
* (either by resetting the keys for the account or bye getting them from secret * (either by resetting the keys for the account or by getting them from secret
* storaoge), such as signing the current device, upgrading device * storage), such as signing the current device, upgrading device
* verifications, etc. * verifications, etc.
*/ */
Crypto.prototype._afterCrossSigningLocalKeyChange = async function() { Crypto.prototype._afterCrossSigningLocalKeyChange = async function() {
@@ -784,7 +820,7 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
throw new Error("Cross-signing master private key not available"); throw new Error("Cross-signing master private key not available");
} }
} finally { } finally {
signing.free(); if (signing) signing.free();
} }
logger.info("Got matching private key from callback for new public master key"); logger.info("Got matching private key from callback for new public master key");
@@ -946,8 +982,7 @@ Crypto.prototype.setTrustedBackupPubKey = async function(trustedPubKey) {
*/ */
Crypto.prototype.checkKeyBackup = async function() { Crypto.prototype.checkKeyBackup = async function() {
this._checkedForBackup = false; this._checkedForBackup = false;
const returnInfo = await this._checkAndStartKeyBackup(); return this._checkAndStartKeyBackup();
return returnInfo;
}; };
/** /**
@@ -994,13 +1029,14 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
logger.log("Ignoring unknown signature type: " + keyIdParts[0]); logger.log("Ignoring unknown signature type: " + keyIdParts[0]);
continue; continue;
} }
// Could be an SSK but just say this is the device ID for backwards compat // Could be a cross-signing master key, but just say this is the device
const sigInfo = { deviceId: keyIdParts[1] }; // XXX: is this how we're supposed to get the device ID? // ID for backwards compat
const sigInfo = { deviceId: keyIdParts[1] };
// first check to see if it's from our cross-signing key // first check to see if it's from our cross-signing key
const crossSigningId = this._crossSigningInfo.getId(); const crossSigningId = this._crossSigningInfo.getId();
if (crossSigningId === keyId) { if (crossSigningId === sigInfo.deviceId) {
sigInfo.cross_signing_key = crossSigningId; sigInfo.crossSigningId = true;
try { try {
await olmlib.verifySignature( await olmlib.verifySignature(
this._olmDevice, this._olmDevice,
@@ -1022,18 +1058,19 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
// Now look for a sig from a device // Now look for a sig from a device
// At some point this can probably go away and we'll just support // At some point this can probably go away and we'll just support
// it being signed by the SSK // it being signed by the cross-signing master key
const device = this._deviceList.getStoredDevice( const device = this._deviceList.getStoredDevice(
this._userId, sigInfo.deviceId, this._userId, sigInfo.deviceId,
); );
if (device) { if (device) {
sigInfo.device = device; sigInfo.device = device;
sigInfo.deviceTrust = await this.checkDeviceTrust(
this._userId, sigInfo.deviceId,
);
try { try {
await olmlib.verifySignature( await olmlib.verifySignature(
this._olmDevice, this._olmDevice,
// verifySignature modifies the object so we need to copy backupInfo.auth_data,
// if we verify more than one sig
Object.assign({}, backupInfo.auth_data),
this._userId, this._userId,
device.deviceId, device.deviceId,
device.getFingerprint(), device.getFingerprint(),
@@ -1057,8 +1094,8 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
ret.usable = ret.sigs.some((s) => { ret.usable = ret.sigs.some((s) => {
return ( return (
s.valid && ( s.valid && (
(s.device && s.device.isVerified()) || (s.device && s.deviceTrust.isVerified()) ||
(s.cross_signing_key) (s.crossSigningId)
) )
); );
}); });
@@ -2367,6 +2404,8 @@ Crypto.prototype._onToDeviceEvent = function(event) {
this._secretStorage._onRequestReceived(event); this._secretStorage._onRequestReceived(event);
} else if (event.getType() === "m.secret.send") { } else if (event.getType() === "m.secret.send") {
this._secretStorage._onSecretReceived(event); this._secretStorage._onSecretReceived(event);
} else if (event.getType() === "org.matrix.room_key.withheld") {
this._onRoomKeyWithheldEvent(event);
} else if (event.getContent().transaction_id) { } else if (event.getContent().transaction_id) {
this._onKeyVerificationMessage(event); this._onKeyVerificationMessage(event);
} else if (event.getContent().msgtype === "m.bad.encrypted") { } else if (event.getContent().msgtype === "m.bad.encrypted") {
@@ -2406,6 +2445,27 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
alg.onRoomKeyEvent(event); alg.onRoomKeyEvent(event);
}; };
/**
* Handle a key withheld event
*
* @private
* @param {module:models/event.MatrixEvent} event key withheld event
*/
Crypto.prototype._onRoomKeyWithheldEvent = function(event) {
const content = event.getContent();
if (!content.room_id || !content.session_id || !content.algorithm
|| !content.sender_key) {
logger.error("key withheld event is missing fields");
return;
}
const alg = this._getRoomDecryptor(content.room_id, content.algorithm);
if (alg.onRoomKeyWithheldEvent) {
alg.onRoomKeyWithheldEvent(event);
}
};
/** /**
* Handle a general key verification event. * Handle a general key verification event.
* *

View File

@@ -302,8 +302,7 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
* *
* @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op * @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op
* *
* @param {Object} obj object to check signature on. Note that this will be * @param {Object} obj object to check signature on.
* stripped of its 'signatures' and 'unsigned' properties.
* *
* @param {string} signingUserId ID of the user whose signature should be checked * @param {string} signingUserId ID of the user whose signature should be checked
* *

View File

@@ -1,6 +1,7 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -18,7 +19,7 @@ limitations under the License.
import {logger} from '../../logger'; import {logger} from '../../logger';
import * as utils from "../../utils"; import * as utils from "../../utils";
export const VERSION = 7; export const VERSION = 8;
/** /**
* Implementation of a CryptoStore which is backed by an existing * Implementation of a CryptoStore which is backed by an existing
@@ -79,7 +80,7 @@ export class Backend {
`enqueueing key request for ${requestBody.room_id} / ` + `enqueueing key request for ${requestBody.room_id} / ` +
requestBody.session_id, requestBody.session_id,
); );
txn.oncomplete = () => { resolve(request); }; txn.oncomplete = () => {resolve(request);};
const store = txn.objectStore("outgoingRoomKeyRequests"); const store = txn.objectStore("outgoingRoomKeyRequests");
store.add(request); store.add(request);
}); });
@@ -428,14 +429,36 @@ export class Backend {
// Inbound group sessions // Inbound group sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
let session = false;
let withheld = false;
const objectStore = txn.objectStore("inbound_group_sessions"); const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.get([senderCurve25519Key, sessionId]); const getReq = objectStore.get([senderCurve25519Key, sessionId]);
getReq.onsuccess = function() { getReq.onsuccess = function() {
try { try {
if (getReq.result) { if (getReq.result) {
func(getReq.result.session); session = getReq.result.session;
} else { } else {
func(null); session = null;
}
if (withheld !== false) {
func(session, withheld);
}
} catch (e) {
abortWithException(txn, e);
}
};
const withheldObjectStore = txn.objectStore("inbound_group_sessions_withheld");
const withheldGetReq = withheldObjectStore.get([senderCurve25519Key, sessionId]);
withheldGetReq.onsuccess = function() {
try {
if (withheldGetReq.result) {
withheld = withheldGetReq.result.session;
} else {
withheld = null;
}
if (session !== false) {
func(session, withheld);
} }
} catch (e) { } catch (e) {
abortWithException(txn, e); abortWithException(txn, e);
@@ -499,6 +522,15 @@ export class Backend {
}); });
} }
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
const objectStore = txn.objectStore("inbound_group_sessions_withheld");
objectStore.put({
senderCurve25519Key, sessionId, session: sessionData,
});
}
getEndToEndDeviceData(txn, func) { getEndToEndDeviceData(txn, func) {
const objectStore = txn.objectStore("device_data"); const objectStore = txn.objectStore("device_data");
const getReq = objectStore.get("-"); const getReq = objectStore.get("-");
@@ -662,6 +694,11 @@ export function upgradeDatabase(db, oldVersion) {
keyPath: ["senderCurve25519Key", "sessionId"], keyPath: ["senderCurve25519Key", "sessionId"],
}); });
} }
if (oldVersion < 8) {
db.createObjectStore("inbound_group_sessions_withheld", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
// Expand as needed. // Expand as needed.
} }

View File

@@ -1,6 +1,7 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -104,7 +105,10 @@ export class IndexedDBCryptoStore {
// we can fall back to a different backend. // we can fall back to a different backend.
return backend.doTxn( return backend.doTxn(
'readonly', 'readonly',
[IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
],
(txn) => { (txn) => {
backend.getEndToEndInboundGroupSession('', '', txn, () => {}); backend.getEndToEndInboundGroupSession('', '', txn, () => {});
}).then(() => { }).then(() => {
@@ -471,6 +475,16 @@ export class IndexedDBCryptoStore {
}); });
} }
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
this._backendPromise.then(backend => {
backend.storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
);
});
}
// End-to-end device tracking // End-to-end device tracking
/** /**
@@ -607,6 +621,8 @@ export class IndexedDBCryptoStore {
IndexedDBCryptoStore.STORE_ACCOUNT = 'account'; IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
IndexedDBCryptoStore.STORE_SESSIONS = 'sessions'; IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions'; IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD
= 'inbound_group_sessions_withheld';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data'; IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
IndexedDBCryptoStore.STORE_ROOMS = 'rooms'; IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup'; IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';

View File

@@ -1,5 +1,6 @@
/* /*
Copyright 2017, 2018 New Vector Ltd Copyright 2017, 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -32,6 +33,7 @@ const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys"; const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys";
const KEY_DEVICE_DATA = E2E_PREFIX + "device_data"; const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/"; const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.withheld/";
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/"; const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup"; const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup";
@@ -43,6 +45,10 @@ function keyEndToEndInboundGroupSession(senderKey, sessionId) {
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId; return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
} }
function keyEndToEndInboundGroupSessionWithheld(senderKey, sessionId) {
return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId;
}
function keyEndToEndRoomsPrefix(roomId) { function keyEndToEndRoomsPrefix(roomId) {
return KEY_ROOMS_PREFIX + roomId; return KEY_ROOMS_PREFIX + roomId;
} }
@@ -125,10 +131,16 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
// Inbound Group Sessions // Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
func(getJsonItem( func(
this.store, getJsonItem(
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), this.store,
)); keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
),
getJsonItem(
this.store,
keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId),
),
);
} }
getAllEndToEndInboundGroupSessions(txn, func) { getAllEndToEndInboundGroupSessions(txn, func) {
@@ -170,6 +182,16 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
); );
} }
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
setJsonItem(
this.store,
keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId),
sessionData,
);
}
getEndToEndDeviceData(txn, func) { getEndToEndDeviceData(txn, func) {
func(getJsonItem( func(getJsonItem(
this.store, KEY_DEVICE_DATA, this.store, KEY_DEVICE_DATA,

View File

@@ -1,6 +1,7 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -37,6 +38,7 @@ export class MemoryCryptoStore {
this._sessions = {}; this._sessions = {};
// Map of {senderCurve25519Key+'/'+sessionId -> session data object} // Map of {senderCurve25519Key+'/'+sessionId -> session data object}
this._inboundGroupSessions = {}; this._inboundGroupSessions = {};
this._inboundGroupSessionsWithheld = {};
// Opaque device data object // Opaque device data object
this._deviceData = null; this._deviceData = null;
// roomId -> Opaque roomInfo object // roomId -> Opaque roomInfo object
@@ -276,7 +278,11 @@ export class MemoryCryptoStore {
// Inbound Group Sessions // Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
func(this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] || null); const k = senderCurve25519Key+'/'+sessionId;
func(
this._inboundGroupSessions[k] || null,
this._inboundGroupSessionsWithheld[k] || null,
);
} }
getAllEndToEndInboundGroupSessions(txn, func) { getAllEndToEndInboundGroupSessions(txn, func) {
@@ -306,6 +312,13 @@ export class MemoryCryptoStore {
this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData; this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData;
} }
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
const k = senderCurve25519Key+'/'+sessionId;
this._inboundGroupSessionsWithheld[k] = sessionData;
}
// Device Data // Device Data
getEndToEndDeviceData(txn, func) { getEndToEndDeviceData(txn, func) {

View File

@@ -312,10 +312,18 @@ function calculateDisplayName(selfUserId, displayName, roomState) {
// Next check if the name contains something that look like a mxid // Next check if the name contains something that look like a mxid
// If it does, it may be someone trying to impersonate someone else // If it does, it may be someone trying to impersonate someone else
// Show full mxid in this case // Show full mxid in this case
// Also show mxid if there are other people with the same or similar
// displayname, after hidden character removal.
let disambiguate = /@.+:.+/.test(displayName); let disambiguate = /@.+:.+/.test(displayName);
if (!disambiguate) { if (!disambiguate) {
// Also show mxid if the display name contains any LTR/RTL characters as these
// make it very difficult for us to find similar *looking* display names
// E.g "Mark" could be cloned by writing "kraM" but in RTL.
disambiguate = /[\u200E\u200F\u202A-\u202F]/.test(displayName);
}
if (!disambiguate) {
// Also show mxid if there are other people with the same or similar
// displayname, after hidden character removal.
const userIds = roomState.getUserIdsWithDisplayName(displayName); const userIds = roomState.getUserIdsWithDisplayName(displayName);
disambiguate = userIds.some((u) => u !== selfUserId); disambiguate = userIds.some((u) => u !== selfUserId);
} }

View File

@@ -157,6 +157,11 @@ MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
return -1; return -1;
} }
// if event that we are trying to send is too large in any way then retrying won't help
if (err.name === "M_TOO_LARGE") {
return -1;
}
if (err.name === "M_LIMIT_EXCEEDED") { if (err.name === "M_LIMIT_EXCEEDED") {
const waitTime = err.data.retry_after_ms; const waitTime = err.data.retry_after_ms;
if (waitTime) { if (waitTime) {

View File

@@ -645,8 +645,20 @@ export function isNumber(value) {
*/ */
export function removeHiddenChars(str) { export function removeHiddenChars(str) {
return unhomoglyph(str.normalize('NFD').replace(removeHiddenCharsRegex, '')); return unhomoglyph(str.normalize('NFD').replace(removeHiddenCharsRegex, ''));
<<<<<<< HEAD
} }
const removeHiddenCharsRegex = /[\u200B-\u200D\u0300-\u036f\uFEFF\s]/g; const removeHiddenCharsRegex = /[\u200B-\u200D\u0300-\u036f\uFEFF\s]/g;
=======
};
// Regex matching bunch of unicode control characters and otherwise misleading/invisible characters.
// Includes:
// various width spaces U+2000 - U+200D
// LTR and RTL marks U+200E and U+200F
// LTR/RTL and other directional formatting marks U+202A - U+202F
// Combining characters U+0300 - U+036F
// Zero width no-break space (BOM) U+FEFF
const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036f\uFEFF\s]/g;
>>>>>>> develop
export function escapeRegExp(string) { export function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");