1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-25 05:23:13 +03:00

Convert SecretStorage to TypeScript

This commit is contained in:
David Baker
2021-07-07 19:37:22 +01:00
parent dc90115a1b
commit c34d4e777f
7 changed files with 206 additions and 164 deletions

View File

@@ -64,7 +64,8 @@ import {
import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; import { IIdentityServerProvider } from "./@types/IIdentityServerProvider";
import type Request from "request"; import type Request from "request";
import { MatrixScheduler } from "./scheduler"; import { MatrixScheduler } from "./scheduler";
import { ICryptoCallbacks, ISecretStorageKeyInfo, NotificationCountType } from "./matrix"; import { ICryptoCallbacks, NotificationCountType } from "./matrix";
import { ISecretStorageKeyInfo } from "./crypto/SecretStorage";
import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store"; import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store";
import { LocalStorageCryptoStore } from "./crypto/store/localStorage-crypto-store"; import { LocalStorageCryptoStore } from "./crypto/store/localStorage-crypto-store";
import { IndexedDBCryptoStore } from "./crypto/store/indexeddb-crypto-store"; import { IndexedDBCryptoStore } from "./crypto/store/indexeddb-crypto-store";
@@ -83,7 +84,6 @@ import {
IEncryptedEventInfo, IEncryptedEventInfo,
IImportRoomKeysOpts, IImportRoomKeysOpts,
IRecoveryKey, IRecoveryKey,
ISecretStorageKey,
} from "./crypto/api"; } from "./crypto/api";
import { CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from "./crypto/CrossSigning"; import { CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from "./crypto/CrossSigning";
import { Room } from "./models/room"; import { Room } from "./models/room";
@@ -1841,7 +1841,9 @@ export class MatrixClient extends EventEmitter {
* keyId: {string} the ID of the key * keyId: {string} the ID of the key
* keyInfo: {object} details about the key (iv, mac, passphrase) * keyInfo: {object} details about the key (iv, mac, passphrase)
*/ */
public addSecretStorageKey(algorithm: string, opts: IAddSecretStorageKeyOpts, keyName?: string): ISecretStorageKey { public addSecretStorageKey(
algorithm: string, opts: IAddSecretStorageKeyOpts, keyName?: string,
): Promise<{keyId: string, keyInfo: ISecretStorageKeyInfo}> {
if (!this.crypto) { if (!this.crypto) {
throw new Error("End-to-end encryption disabled"); throw new Error("End-to-end encryption disabled");
} }
@@ -1857,7 +1859,7 @@ export class MatrixClient extends EventEmitter {
* for. Defaults to the default key ID if not provided. * for. Defaults to the default key ID if not provided.
* @return {boolean} Whether we have the key. * @return {boolean} Whether we have the key.
*/ */
public hasSecretStorageKey(keyId?: string): boolean { public hasSecretStorageKey(keyId?: string): Promise<boolean> {
if (!this.crypto) { if (!this.crypto) {
throw new Error("End-to-end encryption disabled"); throw new Error("End-to-end encryption disabled");
} }
@@ -1910,7 +1912,7 @@ export class MatrixClient extends EventEmitter {
* with, or null if it is not present or not encrypted with a trusted * with, or null if it is not present or not encrypted with a trusted
* key * key
*/ */
public isSecretStored(name: string, checkKey: boolean): Record<string, ISecretStorageKeyInfo> { public isSecretStored(name: string, checkKey: boolean): Promise<Record<string, ISecretStorageKeyInfo>> {
if (!this.crypto) { if (!this.crypto) {
throw new Error("End-to-end encryption disabled"); throw new Error("End-to-end encryption disabled");
} }

View File

@@ -9,10 +9,10 @@ import {
CrossSigningKeys, CrossSigningKeys,
ICrossSigningKey, ICrossSigningKey,
ICryptoCallbacks, ICryptoCallbacks,
ISecretStorageKeyInfo,
ISignedKey, ISignedKey,
KeySignatures, KeySignatures,
} from "../matrix"; } from "../matrix";
import { ISecretStorageKeyInfo } from "./SecretStorage";
import { IKeyBackupInfo } from "./keybackup"; import { IKeyBackupInfo } from "./keybackup";
interface ICrossSigningKeys { interface ICrossSigningKeys {
@@ -337,7 +337,7 @@ class SSSSCryptoCallbacks {
constructor(private readonly delegateCryptoCallbacks: ICryptoCallbacks) {} constructor(private readonly delegateCryptoCallbacks: ICryptoCallbacks) {}
public async getSecretStorageKey( public async getSecretStorageKey(
{ keys }: { keys: Record<string, object> }, { keys }: { keys: Record<string, ISecretStorageKeyInfo> },
name: string, name: string,
): Promise<[string, Uint8Array]> { ): Promise<[string, Uint8Array]> {
for (const keyId of Object.keys(keys)) { for (const keyId of Object.keys(keys)) {

View File

@@ -1,5 +1,5 @@
/* /*
Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2019 - 2021 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.
@@ -14,61 +14,107 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { EventEmitter } from 'events';
import { logger } from '../logger'; import { logger } from '../logger';
import * as olmlib from './olmlib'; import * as olmlib from './olmlib';
import { randomString } from '../randomstring'; import { randomString } from '../randomstring';
import { encryptAES, decryptAES } from './aes'; import { encryptAES, decryptAES, IEncryptedPayload } from './aes';
import { encodeBase64 } from "./olmlib"; import { encodeBase64 } from "./olmlib";
import { ICryptoCallbacks, MatrixClient, MatrixEvent } from '../matrix';
import { IAddSecretStorageKeyOpts, IPassphraseInfo } from './api';
import { EventEmitter } from 'stream';
export const SECRET_STORAGE_ALGORITHM_V1_AES export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
= "m.secret_storage.v1.aes-hmac-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"; 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";
export interface ISecretStorageKeyInfo {
name: string;
algorithm: string;
// technically the below are specific to AES keys. If we ever introduce another type,
// we can split into separate interfaces.
iv: string;
mac: string;
passphrase: IPassphraseInfo;
}
export type SecretStorageKeyTuple = [keyId: string, keyInfo: ISecretStorageKeyInfo];
export interface ISecretRequest {
requestId: string;
promise: Promise<string>;
cancel: (reason: string) => void;
}
export interface IAccountDataClient extends EventEmitter {
getAccountDataFromServer: (string) => Promise<any>;
getAccountData: (string) => object;
setAccountData: (string, object) => Promise<void>;
}
interface ISecretRequestInternal {
name: string;
devices: Array<string>;
resolve: (string) => void;
reject: (Error) => void;
}
interface IDecryptors {
encrypt: (string) => Promise<IEncryptedPayload>;
decrypt: (IEncryptedPayload) => Promise<string>;
}
/** /**
* Implements Secure Secret Storage and Sharing (MSC1946) * Implements Secure Secret Storage and Sharing (MSC1946)
* @module crypto/SecretStorage * @module crypto/SecretStorage
*/ */
export class SecretStorage extends EventEmitter { export class SecretStorage {
constructor(baseApis, cryptoCallbacks) { private accountDataAdapter: IAccountDataClient;
super(); private baseApis: MatrixClient;
this._baseApis = baseApis; private cryptoCallbacks: ICryptoCallbacks;
this._cryptoCallbacks = cryptoCallbacks; private requests = new Map<string, ISecretRequestInternal>();
this._requests = {};
this._incomingRequests = {}; // In it's pure javascript days, this was relying on some proper Javascript-style
// type-abuse where sometimes we'd pass in a fake client object with just the account
// data methods implemented, which is all this class needs unless you use the secret
// sharing code, so it was fine. As a low-touch TypeScript migration, this now has
// an extra, optional param for a real matrix client, so you can not pass it as long
// as you don't request any secrets.
// A better solution would probably be to split this class up into secret storage and
// secret sharing which are really two separate things, even though they share an MSC.
constructor(accountDataClient: IAccountDataClient, cryptoCallbacks: ICryptoCallbacks, matrixClient?: MatrixClient) {
this.accountDataAdapter = accountDataClient;
this.baseApis = matrixClient;
this.cryptoCallbacks = cryptoCallbacks;
} }
async getDefaultKeyId() { public async getDefaultKeyId(): Promise<string> {
const defaultKey = await this._baseApis.getAccountDataFromServer( const defaultKey = await this.accountDataAdapter.getAccountDataFromServer(
'm.secret_storage.default_key', 'm.secret_storage.default_key',
); );
if (!defaultKey) return null; if (!defaultKey) return null;
return defaultKey.key; return defaultKey.key;
} }
setDefaultKeyId(keyId) { public setDefaultKeyId(keyId: string) {
return new Promise(async (resolve, reject) => { return new Promise<void>((resolve, reject) => {
const listener = (ev) => { const listener = (ev) => {
if ( if (
ev.getType() === 'm.secret_storage.default_key' && ev.getType() === 'm.secret_storage.default_key' &&
ev.getContent().key === keyId ev.getContent().key === keyId
) { ) {
this._baseApis.removeListener('accountData', listener); this.accountDataAdapter.removeListener('accountData', listener);
resolve(); resolve();
} }
}; };
this._baseApis.on('accountData', listener); this.accountDataAdapter.on('accountData', listener);
try { this.accountDataAdapter.setAccountData(
await this._baseApis.setAccountData(
'm.secret_storage.default_key', 'm.secret_storage.default_key',
{ key: keyId }, { key: keyId },
); ).catch(e => {
} catch (e) { this.accountDataAdapter.removeListener('accountData', listener);
this._baseApis.removeListener('accountData', listener);
reject(e); reject(e);
} });
}); });
} }
@@ -85,10 +131,12 @@ export class SecretStorage extends EventEmitter {
* keyId: {string} the ID of the key * keyId: {string} the ID of the key
* keyInfo: {object} details about the key (iv, mac, passphrase) * keyInfo: {object} details about the key (iv, mac, passphrase)
*/ */
async addKey(algorithm, opts, keyId) { public async addKey(
const keyInfo = { algorithm }; algorithm: string, opts: IAddSecretStorageKeyOpts, keyId?: string,
): Promise<{keyId: string, keyInfo: ISecretStorageKeyInfo}> {
const keyInfo = { algorithm } as ISecretStorageKeyInfo;
if (!opts) opts = {}; if (!opts) opts = {} as IAddSecretStorageKeyOpts;
if (opts.name) { if (opts.name) {
keyInfo.name = opts.name; keyInfo.name = opts.name;
@@ -104,20 +152,20 @@ export class SecretStorage extends EventEmitter {
keyInfo.mac = mac; keyInfo.mac = mac;
} }
} else { } else {
throw new Error(`Unknown key algorithm ${opts.algorithm}`); throw new Error(`Unknown key algorithm ${algorithm}`);
} }
if (!keyId) { if (!keyId) {
do { do {
keyId = randomString(32); keyId = randomString(32);
} while ( } while (
await this._baseApis.getAccountDataFromServer( await this.accountDataAdapter.getAccountDataFromServer(
`m.secret_storage.key.${keyId}`, `m.secret_storage.key.${keyId}`,
) )
); );
} }
await this._baseApis.setAccountData( await this.accountDataAdapter.setAccountData(
`m.secret_storage.key.${keyId}`, keyInfo, `m.secret_storage.key.${keyId}`, keyInfo,
); );
@@ -134,8 +182,9 @@ export class SecretStorage extends EventEmitter {
* for. Defaults to the default key ID if not provided. * for. Defaults to the default key ID if not provided.
* @returns {Array?} If the key was found, the return value is an array of * @returns {Array?} If the key was found, the return value is an array of
* the form [keyId, keyInfo]. Otherwise, null is returned. * the form [keyId, keyInfo]. Otherwise, null is returned.
* XXX: why is this an array when addKey returns an object?
*/ */
async getKey(keyId) { public async getKey(keyId: string): Promise<SecretStorageKeyTuple> {
if (!keyId) { if (!keyId) {
keyId = await this.getDefaultKeyId(); keyId = await this.getDefaultKeyId();
} }
@@ -143,7 +192,7 @@ export class SecretStorage extends EventEmitter {
return null; return null;
} }
const keyInfo = await this._baseApis.getAccountDataFromServer( const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
"m.secret_storage.key." + keyId, "m.secret_storage.key." + keyId,
); );
return keyInfo ? [keyId, keyInfo] : null; return keyInfo ? [keyId, keyInfo] : null;
@@ -156,8 +205,8 @@ export class SecretStorage extends EventEmitter {
* for. Defaults to the default key ID if not provided. * for. Defaults to the default key ID if not provided.
* @return {boolean} Whether we have the key. * @return {boolean} Whether we have the key.
*/ */
async hasKey(keyId) { public async hasKey(keyId?: string): Promise<boolean> {
return !!(await this.getKey(keyId)); return Boolean(await this.getKey(keyId));
} }
/** /**
@@ -168,7 +217,7 @@ export class SecretStorage extends EventEmitter {
* *
* @return {boolean} whether or not the key matches * @return {boolean} whether or not the key matches
*/ */
async checkKey(key, info) { public async checkKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise<boolean> {
if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
if (info.mac) { if (info.mac) {
const { mac } = await SecretStorage._calculateKeyCheck(key, info.iv); const { mac } = await SecretStorage._calculateKeyCheck(key, info.iv);
@@ -182,7 +231,7 @@ export class SecretStorage extends EventEmitter {
} }
} }
static async _calculateKeyCheck(key, iv) { public static async _calculateKeyCheck(key: Uint8Array, iv?: string): Promise<IEncryptedPayload> {
return await encryptAES(ZERO_STR, key, "", iv); return await encryptAES(ZERO_STR, key, "", iv);
} }
@@ -194,7 +243,7 @@ export class SecretStorage extends EventEmitter {
* @param {Array} keys The IDs of the keys to use to encrypt the secret * @param {Array} keys The IDs of the keys to use to encrypt the secret
* or null/undefined to use the default key. * or null/undefined to use the default key.
*/ */
async store(name, secret, keys) { public async store(name: string, secret: string, keys?: Array<string>): Promise<void> {
const encrypted = {}; const encrypted = {};
if (!keys) { if (!keys) {
@@ -211,7 +260,7 @@ export class SecretStorage extends EventEmitter {
for (const keyId of keys) { for (const keyId of keys) {
// get key information from key storage // get key information from key storage
const keyInfo = await this._baseApis.getAccountDataFromServer( const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
"m.secret_storage.key." + keyId, "m.secret_storage.key." + keyId,
); );
if (!keyInfo) { if (!keyInfo) {
@@ -221,7 +270,7 @@ export class SecretStorage extends EventEmitter {
// encrypt secret, based on the algorithm // encrypt secret, based on the algorithm
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
const keys = { [keyId]: keyInfo }; const keys = { [keyId]: keyInfo };
const [, encryption] = await this._getSecretStorageKey(keys, name); const [, encryption] = await this.getSecretStorageKey(keys, name);
encrypted[keyId] = await encryption.encrypt(secret); encrypted[keyId] = await encryption.encrypt(secret);
} else { } else {
logger.warn("unknown algorithm for secret storage key " + keyId logger.warn("unknown algorithm for secret storage key " + keyId
@@ -231,34 +280,7 @@ export class SecretStorage extends EventEmitter {
} }
// save encrypted secret // save encrypted secret
await this._baseApis.setAccountData(name, { encrypted }); await this.accountDataAdapter.setAccountData(name, { encrypted });
}
/**
* Temporary method to fix up existing accounts where secrets
* are incorrectly stored without the 'encrypted' level
*
* @param {string} name The name of the secret
* @param {object} secretInfo The account data object
* @returns {object} The fixed object or null if no fix was performed
*/
async _fixupStoredSecret(name, secretInfo) {
// We assume the secret was only stored passthrough for 1
// key - this was all the broken code supported.
const keys = Object.keys(secretInfo);
if (
keys.length === 1 && keys[0] !== 'encrypted' &&
secretInfo[keys[0]].passthrough
) {
const hasKey = await this.hasKey(keys[0]);
if (hasKey) {
logger.log("Fixing up passthrough secret: " + name);
await this.storePassthrough(name, keys[0]);
const newData = await this._baseApis.getAccountDataFromServer(name);
return newData;
}
}
return null;
} }
/** /**
@@ -268,24 +290,20 @@ export class SecretStorage extends EventEmitter {
* *
* @return {string} the contents of the secret * @return {string} the contents of the secret
*/ */
async get(name) { async get(name: string): Promise<string> {
let secretInfo = await this._baseApis.getAccountDataFromServer(name); const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
if (!secretInfo) { if (!secretInfo) {
return; return;
} }
if (!secretInfo.encrypted) { if (!secretInfo.encrypted) {
// try to fix it up
secretInfo = await this._fixupStoredSecret(name, secretInfo);
if (!secretInfo || !secretInfo.encrypted) {
throw new Error("Content is not encrypted!"); throw new Error("Content is not encrypted!");
} }
}
// get possible keys to decrypt // get possible keys to decrypt
const keys = {}; const keys = {};
for (const keyId of Object.keys(secretInfo.encrypted)) { for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage // get key information from key storage
const keyInfo = await this._baseApis.getAccountDataFromServer( const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
"m.secret_storage.key." + keyId, "m.secret_storage.key." + keyId,
); );
const encInfo = secretInfo.encrypted[keyId]; const encInfo = secretInfo.encrypted[keyId];
@@ -306,7 +324,7 @@ export class SecretStorage extends EventEmitter {
let decryption; let decryption;
try { try {
// fetch private key from app // fetch private key from app
[keyId, decryption] = await this._getSecretStorageKey(keys, name); [keyId, decryption] = await this.getSecretStorageKey(keys, name);
const encInfo = secretInfo.encrypted[keyId]; const encInfo = secretInfo.encrypted[keyId];
@@ -331,17 +349,13 @@ export class SecretStorage extends EventEmitter {
* with, or null if it is not present or not encrypted with a trusted * with, or null if it is not present or not encrypted with a trusted
* key * key
*/ */
async isStored(name, checkKey) { async isStored(name: string, checkKey: boolean): Promise<Record<string, ISecretStorageKeyInfo>> {
// check if secret exists // check if secret exists
let secretInfo = await this._baseApis.getAccountDataFromServer(name); const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
if (!secretInfo) return null; if (!secretInfo) return null;
if (!secretInfo.encrypted) { if (!secretInfo.encrypted) {
// try to fix it up
secretInfo = await this._fixupStoredSecret(name, secretInfo);
if (!secretInfo || !secretInfo.encrypted) {
return null; return null;
} }
}
if (checkKey === undefined) checkKey = true; if (checkKey === undefined) checkKey = true;
@@ -350,7 +364,7 @@ export class SecretStorage extends EventEmitter {
// filter secret encryption keys with supported algorithm // filter secret encryption keys with supported algorithm
for (const keyId of Object.keys(secretInfo.encrypted)) { for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage // get key information from key storage
const keyInfo = await this._baseApis.getAccountDataFromServer( const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
"m.secret_storage.key." + keyId, "m.secret_storage.key." + keyId,
); );
if (!keyInfo) continue; if (!keyInfo) continue;
@@ -371,45 +385,48 @@ export class SecretStorage extends EventEmitter {
* *
* @param {string} name the name of the secret to request * @param {string} name the name of the secret to request
* @param {string[]} devices the devices to request the secret from * @param {string[]} devices the devices to request the secret from
*
* @return {string} the contents of the secret
*/ */
request(name, devices) { request(name: string, devices: Array<string>): ISecretRequest {
const requestId = this._baseApis.makeTxnId(); const requestId = this.baseApis.makeTxnId();
const requestControl = this._requests[requestId] = { let resolve: (string) => void;
let reject: (Error) => void;
const promise = new Promise<string>((res, rej) => {
resolve = res;
reject = rej;
});
this.requests.set(requestId, {
name, name,
devices, devices,
}; resolve,
const promise = new Promise((resolve, reject) => { reject,
requestControl.resolve = resolve;
requestControl.reject = reject;
}); });
const cancel = (reason) => { const cancel = (reason) => {
// send cancellation event // send cancellation event
const cancelData = { const cancelData = {
action: "request_cancellation", action: "request_cancellation",
requesting_device_id: this._baseApis.deviceId, requesting_device_id: this.baseApis.deviceId,
request_id: requestId, request_id: requestId,
}; };
const toDevice = {}; const toDevice = {};
for (const device of devices) { for (const device of devices) {
toDevice[device] = cancelData; toDevice[device] = cancelData;
} }
this._baseApis.sendToDevice("m.secret.request", { this.baseApis.sendToDevice("m.secret.request", {
[this._baseApis.getUserId()]: toDevice, [this.baseApis.getUserId()]: toDevice,
}); });
// and reject the promise so that anyone waiting on it will be // and reject the promise so that anyone waiting on it will be
// notified // notified
requestControl.reject(new Error(reason || "Cancelled")); reject(new Error(reason || "Cancelled"));
}; };
// send request to devices // send request to devices
const requestData = { const requestData = {
name, name,
action: "request", action: "request",
requesting_device_id: this._baseApis.deviceId, requesting_device_id: this.baseApis.deviceId,
request_id: requestId, request_id: requestId,
}; };
const toDevice = {}; const toDevice = {};
@@ -417,21 +434,21 @@ export class SecretStorage extends EventEmitter {
toDevice[device] = requestData; toDevice[device] = requestData;
} }
logger.info(`Request secret ${name} from ${devices}, id ${requestId}`); logger.info(`Request secret ${name} from ${devices}, id ${requestId}`);
this._baseApis.sendToDevice("m.secret.request", { this.baseApis.sendToDevice("m.secret.request", {
[this._baseApis.getUserId()]: toDevice, [this.baseApis.getUserId()]: toDevice,
}); });
return { return {
request_id: requestId, requestId,
promise, promise,
cancel, cancel,
}; };
} }
async _onRequestReceived(event) { public async onRequestReceived(event: MatrixEvent): Promise<void> {
const sender = event.getSender(); const sender = event.getSender();
const content = event.getContent(); const content = event.getContent();
if (sender !== this._baseApis.getUserId() if (sender !== this.baseApis.getUserId()
|| !(content.name && content.action || !(content.name && content.action
&& content.requesting_device_id && content.request_id)) { && content.requesting_device_id && content.request_id)) {
// ignore requests from anyone else, for now // ignore requests from anyone else, for now
@@ -440,34 +457,45 @@ export class SecretStorage extends EventEmitter {
const deviceId = content.requesting_device_id; const deviceId = content.requesting_device_id;
// check if it's a cancel // check if it's a cancel
if (content.action === "request_cancellation") { if (content.action === "request_cancellation") {
/*
Looks like we intended to emit events when we got cancelations, but
we never put anything in the _incomingRequests object, and the request
itself doesn't use events anyway so if we were to wire up cancellations,
they probably ought to use the same callback interface. I'm leaving them
disabled for now while converting this file to typescript.
if (this._incomingRequests[deviceId] if (this._incomingRequests[deviceId]
&& this._incomingRequests[deviceId][content.request_id]) { && this._incomingRequests[deviceId][content.request_id]) {
logger.info("received request cancellation for secret (" + sender logger.info(
+ ", " + deviceId + ", " + content.request_id + ")"); "received request cancellation for secret (" + sender +
this._baseApis.emit("crypto.secrets.requestCancelled", { ", " + deviceId + ", " + content.request_id + ")",
);
this.baseApis.emit("crypto.secrets.requestCancelled", {
user_id: sender, user_id: sender,
device_id: deviceId, device_id: deviceId,
request_id: content.request_id, request_id: content.request_id,
}); });
} }
*/
} else if (content.action === "request") { } else if (content.action === "request") {
if (deviceId === this._baseApis.deviceId) { if (deviceId === this.baseApis.deviceId) {
// no point in trying to send ourself the secret // no point in trying to send ourself the secret
return; return;
} }
// check if we have the secret // check if we have the secret
logger.info("received request for secret (" + sender logger.info(
+ ", " + deviceId + ", " + content.request_id + ")"); "received request for secret (" + sender +
if (!this._cryptoCallbacks.onSecretRequested) { ", " + deviceId + ", " + content.request_id + ")",
);
if (!this.cryptoCallbacks.onSecretRequested) {
return; return;
} }
const secret = await this._cryptoCallbacks.onSecretRequested( const secret = await this.cryptoCallbacks.onSecretRequested(
sender, sender,
deviceId, deviceId,
content.request_id, content.request_id,
content.name, content.name,
this._baseApis.checkDeviceTrust(sender, deviceId), this.baseApis.checkDeviceTrust(sender, deviceId),
); );
if (secret) { if (secret) {
logger.info(`Preparing ${content.name} secret for ${deviceId}`); logger.info(`Preparing ${content.name} secret for ${deviceId}`);
@@ -480,25 +508,25 @@ export class SecretStorage extends EventEmitter {
}; };
const encryptedContent = { const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM, algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._baseApis.crypto.olmDevice.deviceCurve25519Key, sender_key: this.baseApis.crypto.olmDevice.deviceCurve25519Key,
ciphertext: {}, ciphertext: {},
}; };
await olmlib.ensureOlmSessionsForDevices( await olmlib.ensureOlmSessionsForDevices(
this._baseApis.crypto.olmDevice, this.baseApis.crypto.olmDevice,
this._baseApis, this.baseApis,
{ {
[sender]: [ [sender]: [
this._baseApis.getStoredDevice(sender, deviceId), this.baseApis.getStoredDevice(sender, deviceId),
], ],
}, },
); );
await olmlib.encryptMessageForDevice( await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext, encryptedContent.ciphertext,
this._baseApis.getUserId(), this.baseApis.getUserId(),
this._baseApis.deviceId, this.baseApis.deviceId,
this._baseApis.crypto.olmDevice, this.baseApis.crypto.olmDevice,
sender, sender,
this._baseApis.getStoredDevice(sender, deviceId), this.baseApis.getStoredDevice(sender, deviceId),
payload, payload,
); );
const contentMap = { const contentMap = {
@@ -508,26 +536,26 @@ export class SecretStorage extends EventEmitter {
}; };
logger.info(`Sending ${content.name} secret for ${deviceId}`); logger.info(`Sending ${content.name} secret for ${deviceId}`);
this._baseApis.sendToDevice("m.room.encrypted", contentMap); this.baseApis.sendToDevice("m.room.encrypted", contentMap);
} else { } else {
logger.info(`Request denied for ${content.name} secret for ${deviceId}`); logger.info(`Request denied for ${content.name} secret for ${deviceId}`);
} }
} }
} }
_onSecretReceived(event) { public onSecretReceived(event: MatrixEvent): void {
if (event.getSender() !== this._baseApis.getUserId()) { if (event.getSender() !== this.baseApis.getUserId()) {
// we shouldn't be receiving secrets from anyone else, so ignore // we shouldn't be receiving secrets from anyone else, so ignore
// because someone could be trying to send us bogus data // because someone could be trying to send us bogus data
return; return;
} }
const content = event.getContent(); const content = event.getContent();
logger.log("got secret share for request", content.request_id); logger.log("got secret share for request", content.request_id);
const requestControl = this._requests[content.request_id]; const requestControl = this.requests.get(content.request_id);
if (requestControl) { if (requestControl) {
// make sure that the device that sent it is one of the devices that // make sure that the device that sent it is one of the devices that
// we requested from // we requested from
const deviceInfo = this._baseApis.crypto.deviceList.getDeviceByIdentityKey( const deviceInfo = this.baseApis.crypto.deviceList.getDeviceByIdentityKey(
olmlib.OLM_ALGORITHM, olmlib.OLM_ALGORITHM,
event.getSenderKey(), event.getSenderKey(),
); );
@@ -550,12 +578,14 @@ export class SecretStorage extends EventEmitter {
} }
} }
async _getSecretStorageKey(keys, name) { private async getSecretStorageKey(
if (!this._cryptoCallbacks.getSecretStorageKey) { keys: Record<string, ISecretStorageKeyInfo>, name: string,
): Promise<[string, IDecryptors]> {
if (!this.cryptoCallbacks.getSecretStorageKey) {
throw new Error("No getSecretStorageKey callback supplied"); throw new Error("No getSecretStorageKey callback supplied");
} }
const returned = await this._cryptoCallbacks.getSecretStorageKey({ keys }, name); const returned = await this.cryptoCallbacks.getSecretStorageKey({ keys }, name);
if (!returned) { if (!returned) {
throw new Error("getSecretStorageKey callback returned falsey"); throw new Error("getSecretStorageKey callback returned falsey");
@@ -571,10 +601,10 @@ export class SecretStorage extends EventEmitter {
if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
const decryption = { const decryption = {
encrypt: async function(secret) { encrypt: async function(secret: string): Promise<IEncryptedPayload> {
return await encryptAES(secret, privateKey, name); return await encryptAES(secret, privateKey, name);
}, },
decrypt: async function(encInfo) { decrypt: async function(encInfo: IEncryptedPayload): Promise<string> {
return await decryptAES(encInfo, privateKey, name); return await decryptAES(encInfo, privateKey, name);
}, },
}; };

View File

@@ -16,7 +16,7 @@ limitations under the License.
import { DeviceInfo } from "./deviceinfo"; import { DeviceInfo } from "./deviceinfo";
import { IKeyBackupInfo } from "./keybackup"; import { IKeyBackupInfo } from "./keybackup";
import { ISecretStorageKeyInfo } from "../matrix"; import { ISecretStorageKeyInfo } from "./SecretStorage";
// TODO: Merge this with crypto.js once converted // TODO: Merge this with crypto.js once converted
@@ -112,9 +112,17 @@ export interface ISecretStorageKey {
keyInfo: ISecretStorageKeyInfo; keyInfo: ISecretStorageKeyInfo;
} }
export interface IPassphraseInfo {
algorithm: "m.pbkdf2";
iterations: number;
salt: string;
bits: number;
}
export interface IAddSecretStorageKeyOpts { export interface IAddSecretStorageKeyOpts {
// depends on algorithm name: string;
// TODO: Types passphrase: IPassphraseInfo;
key: Uint8Array;
} }
export interface IImportOpts { export interface IImportOpts {

View File

@@ -19,7 +19,7 @@ import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
import { decryptAES, encryptAES } from './aes'; import { decryptAES, encryptAES } from './aes';
import anotherjson from "another-json"; import anotherjson from "another-json";
import { logger } from '../logger'; import { logger } from '../logger';
import { ISecretStorageKeyInfo } from "../matrix"; import { ISecretStorageKeyInfo } from "./SecretStorage";
// FIXME: these types should eventually go in a different file // FIXME: these types should eventually go in a different file
type Signatures = Record<string, Record<string, string>>; type Signatures = Record<string, Record<string, string>>;

View File

@@ -33,7 +33,14 @@ import { DeviceInfo, IDevice } from "./deviceinfo";
import * as algorithms from "./algorithms"; import * as algorithms from "./algorithms";
import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning'; import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning';
import { EncryptionSetupBuilder } from "./EncryptionSetup"; import { EncryptionSetupBuilder } from "./EncryptionSetup";
import { SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorage } from './SecretStorage'; import {
SECRET_STORAGE_ALGORITHM_V1_AES,
SecretStorage,
ISecretStorageKeyInfo,
SecretStorageKeyTuple,
ISecretRequest,
} from './SecretStorage';
import { IAddSecretStorageKeyOpts } from "./api";
import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager'; import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode'; import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode';
@@ -359,7 +366,8 @@ export class Crypto extends EventEmitter {
const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore, this.olmDevice); const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore, this.olmDevice);
this.crossSigningInfo = new CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks); this.crossSigningInfo = new CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks);
this.secretStorage = new SecretStorage(baseApis, cryptoCallbacks); // Yes, we pass the client twice here: see SecretStorage
this.secretStorage = new SecretStorage(baseApis, cryptoCallbacks, baseApis);
this.dehydrationManager = new DehydrationManager(this); this.dehydrationManager = new DehydrationManager(this);
// Assuming no app-supplied callback, default to getting from SSSS. // Assuming no app-supplied callback, default to getting from SSSS.
@@ -970,15 +978,17 @@ export class Crypto extends EventEmitter {
logger.log("Secure Secret Storage ready"); logger.log("Secure Secret Storage ready");
} }
public addSecretStorageKey(algorithm: string, opts: any, keyID: string): any { // TODO types public addSecretStorageKey(
algorithm: string, opts: IAddSecretStorageKeyOpts, keyID: string,
): Promise<{keyId: string, keyInfo: ISecretStorageKeyInfo}> {
return this.secretStorage.addKey(algorithm, opts, keyID); return this.secretStorage.addKey(algorithm, opts, keyID);
} }
public hasSecretStorageKey(keyID: string): boolean { public hasSecretStorageKey(keyID: string): Promise<boolean> {
return this.secretStorage.hasKey(keyID); return this.secretStorage.hasKey(keyID);
} }
public getSecretStorageKey(keyID?: string): any { // TODO types public getSecretStorageKey(keyID?: string): Promise<SecretStorageKeyTuple> {
return this.secretStorage.getKey(keyID); return this.secretStorage.getKey(keyID);
} }
@@ -990,11 +1000,13 @@ export class Crypto extends EventEmitter {
return this.secretStorage.get(name); return this.secretStorage.get(name);
} }
public isSecretStored(name: string, checkKey?: boolean): any { // TODO types public isSecretStored(
name: string, checkKey?: boolean,
): Promise<Record<string, ISecretStorageKeyInfo>> {
return this.secretStorage.isStored(name, checkKey); return this.secretStorage.isStored(name, checkKey);
} }
public requestSecret(name: string, devices: string[]): Promise<any> { // TODO types public requestSecret(name: string, devices: string[]): ISecretRequest { // TODO types
if (!devices) { if (!devices) {
devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(this.userId)); devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(this.userId));
} }
@@ -1009,7 +1021,7 @@ export class Crypto extends EventEmitter {
return this.secretStorage.setDefaultKeyId(k); return this.secretStorage.setDefaultKeyId(k);
} }
public checkSecretStorageKey(key: string, info: any): Promise<boolean> { // TODO types public checkSecretStorageKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise<boolean> {
return this.secretStorage.checkKey(key, info); return this.secretStorage.checkKey(key, info);
} }
@@ -2996,9 +3008,9 @@ export class Crypto extends EventEmitter {
} else if (event.getType() == "m.room_key_request") { } else if (event.getType() == "m.room_key_request") {
this.onRoomKeyRequestEvent(event); this.onRoomKeyRequestEvent(event);
} else if (event.getType() === "m.secret.request") { } else if (event.getType() === "m.secret.request") {
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") { } else if (event.getType() === "org.matrix.room_key.withheld") {
this.onRoomKeyWithheldEvent(event); this.onRoomKeyWithheldEvent(event);
} else if (event.getContent().transaction_id) { } else if (event.getContent().transaction_id) {

View File

@@ -20,6 +20,7 @@ import { MatrixScheduler } from "./scheduler";
import { MatrixClient } from "./client"; import { MatrixClient } from "./client";
import { ICreateClientOpts } from "./client"; import { ICreateClientOpts } from "./client";
import { DeviceTrustLevel } from "./crypto/CrossSigning"; import { DeviceTrustLevel } from "./crypto/CrossSigning";
import { ISecretStorageKeyInfo } from "./crypto/SecretStorage";
export * from "./client"; export * from "./client";
export * from "./http-api"; export * from "./http-api";
@@ -122,17 +123,6 @@ export interface ICryptoCallbacks {
getBackupKey?: () => Promise<Uint8Array>; getBackupKey?: () => Promise<Uint8Array>;
} }
// TODO: Move this to `SecretStorage` once converted
export interface ISecretStorageKeyInfo {
passphrase?: {
algorithm: "m.pbkdf2";
iterations: number;
salt: string;
};
iv?: string;
mac?: string;
}
/** /**
* Construct a Matrix Client. Similar to {@link module:client.MatrixClient} * Construct a Matrix Client. Similar to {@link module:client.MatrixClient}
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied. * except that the 'request', 'store' and 'scheduler' dependencies are satisfied.