You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-26 17:03:12 +03:00
Convert secrets events to callbacks too
This commit is contained in:
@@ -45,15 +45,25 @@ describe("Secrets", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should store and retrieve a secret", async function() {
|
it("should store and retrieve a secret", async function() {
|
||||||
const alice = await makeTestClient(
|
|
||||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
|
||||||
);
|
|
||||||
const secretStorage = alice._crypto._secretStorage;
|
|
||||||
|
|
||||||
const decryption = new global.Olm.PkDecryption();
|
const decryption = new global.Olm.PkDecryption();
|
||||||
const pubkey = decryption.generate_key();
|
const pubkey = decryption.generate_key();
|
||||||
const privkey = decryption.get_private_key();
|
const privkey = decryption.get_private_key();
|
||||||
|
|
||||||
|
const getKey = expect.createSpy().andCall(e => {
|
||||||
|
expect(Object.keys(e.keys)).toEqual(["abc"]);
|
||||||
|
return ['abc', privkey];
|
||||||
|
});
|
||||||
|
|
||||||
|
const alice = await makeTestClient(
|
||||||
|
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||||
|
{
|
||||||
|
cryptoCallbacks: {
|
||||||
|
getSecretStorageKey: getKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const secretStorage = alice._crypto._secretStorage;
|
||||||
|
|
||||||
alice.setAccountData = async function(eventType, contents, callback) {
|
alice.setAccountData = async function(eventType, contents, callback) {
|
||||||
alice.store.storeAccountDataEvents([
|
alice.store.storeAccountDataEvents([
|
||||||
new MatrixEvent({
|
new MatrixEvent({
|
||||||
@@ -81,13 +91,6 @@ describe("Secrets", function() {
|
|||||||
await secretStorage.store("foo", "bar", ["abc"]);
|
await secretStorage.store("foo", "bar", ["abc"]);
|
||||||
|
|
||||||
expect(secretStorage.isStored("foo")).toBe(true);
|
expect(secretStorage.isStored("foo")).toBe(true);
|
||||||
|
|
||||||
const getKey = expect.createSpy().andCall(function(e) {
|
|
||||||
expect(Object.keys(e.keys)).toEqual(["abc"]);
|
|
||||||
e.done("abc", privkey);
|
|
||||||
});
|
|
||||||
alice.once("crypto.secrets.getKey", getKey);
|
|
||||||
|
|
||||||
expect(await secretStorage.get("foo")).toBe("bar");
|
expect(await secretStorage.get("foo")).toBe("bar");
|
||||||
|
|
||||||
expect(getKey).toHaveBeenCalled();
|
expect(getKey).toHaveBeenCalled();
|
||||||
@@ -99,6 +102,14 @@ describe("Secrets", function() {
|
|||||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||||
{userId: "@alice:example.com", deviceId: "VAX"},
|
{userId: "@alice:example.com", deviceId: "VAX"},
|
||||||
],
|
],
|
||||||
|
{
|
||||||
|
cryptoCallbacks: {
|
||||||
|
onSecretRequested: e => {
|
||||||
|
expect(e.name).toBe("foo");
|
||||||
|
return "bar";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const vaxDevice = vax.client._crypto._olmDevice;
|
const vaxDevice = vax.client._crypto._olmDevice;
|
||||||
@@ -128,11 +139,6 @@ describe("Secrets", function() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
vax.client.once("crypto.secrets.request", function(e) {
|
|
||||||
expect(e.name).toBe("foo");
|
|
||||||
e.send("bar");
|
|
||||||
});
|
|
||||||
|
|
||||||
await osborne2Device.generateOneTimeKeys(1);
|
await osborne2Device.generateOneTimeKeys(1);
|
||||||
const otks = (await osborne2Device.getOneTimeKeys()).curve25519;
|
const otks = (await osborne2Device.getOneTimeKeys()).curve25519;
|
||||||
await osborne2Device.markKeysAsPublished();
|
await osborne2Device.markKeysAsPublished();
|
||||||
|
|||||||
@@ -210,6 +210,20 @@ function keyFromRecoverySession(session, decryptionKey) {
|
|||||||
* Should return a promise which resolves with an array of the user IDs who
|
* Should return a promise which resolves with an array of the user IDs who
|
||||||
* should be cross-signed.
|
* should be cross-signed.
|
||||||
*
|
*
|
||||||
|
* @param {function} [opts.cryptoCallbacks.getSecretStorageKey]
|
||||||
|
* Optional. Function called when an encryption key for secret storage
|
||||||
|
* is required. One or more keys will be described in the keys object.
|
||||||
|
* The callback function should return with an array of:
|
||||||
|
* [<key name>, <UInt8Array private key>] or null if it cannot provide
|
||||||
|
* any of the keys.
|
||||||
|
* Args:
|
||||||
|
* {object} keys Information about the keys:
|
||||||
|
* {
|
||||||
|
* <key name>: {
|
||||||
|
* pubkey: {UInt8Array}
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
* @param {function} [opts.cryptoCallbacks.onSecretRequested]
|
* @param {function} [opts.cryptoCallbacks.onSecretRequested]
|
||||||
* Optional. Function called when a request for a secret is received from another
|
* Optional. Function called when a request for a secret is received from another
|
||||||
* device.
|
* device.
|
||||||
|
|||||||
@@ -26,11 +26,12 @@ import { encodeRecoveryKey } from './recoverykey';
|
|||||||
* @module crypto/Secrets
|
* @module crypto/Secrets
|
||||||
*/
|
*/
|
||||||
export default class SecretStorage extends EventEmitter {
|
export default class SecretStorage extends EventEmitter {
|
||||||
constructor(baseApis) {
|
constructor(baseApis, cryptoCallbacks) {
|
||||||
super();
|
super();
|
||||||
this._baseApis = baseApis;
|
this._baseApis = baseApis;
|
||||||
this._requests = {};
|
this._requests = {};
|
||||||
this._incomingRequests = {};
|
this._incomingRequests = {};
|
||||||
|
this._cryptoCallbacks = cryptoCallbacks;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -159,7 +160,7 @@ export default class SecretStorage extends EventEmitter {
|
|||||||
const secretContent = secretInfo.getContent();
|
const secretContent = secretInfo.getContent();
|
||||||
|
|
||||||
if (!secretContent.encrypted) {
|
if (!secretContent.encrypted) {
|
||||||
return;
|
throw new Error("Content is not encrypted!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// get possible keys to decrypt
|
// get possible keys to decrypt
|
||||||
@@ -182,63 +183,13 @@ export default class SecretStorage extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch private key from app
|
|
||||||
let decryption;
|
|
||||||
let keyName;
|
let keyName;
|
||||||
let cleanUp;
|
let decryption;
|
||||||
let error;
|
|
||||||
do {
|
|
||||||
[keyName, decryption, cleanUp] = await new Promise((resolve, reject) => {
|
|
||||||
this._baseApis.emit("crypto.secrets.getKey", {
|
|
||||||
keys,
|
|
||||||
error,
|
|
||||||
done: function(keyName, key) {
|
|
||||||
// FIXME: interpret key?
|
|
||||||
if (!keys[keyName]) {
|
|
||||||
error = "Unknown key (your app is broken)";
|
|
||||||
resolve([]);
|
|
||||||
}
|
|
||||||
switch (keys[keyName].algorithm) {
|
|
||||||
case "m.secret_storage.v1.curve25519-aes-sha2":
|
|
||||||
{
|
|
||||||
const decryption = new global.Olm.PkDecryption();
|
|
||||||
try {
|
|
||||||
const pubkey = decryption.init_with_private_key(key);
|
|
||||||
if (pubkey !== keys[keyName].pubkey) {
|
|
||||||
error = "Key does not match";
|
|
||||||
resolve([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
decryption.free();
|
|
||||||
error = "Invalid key";
|
|
||||||
resolve([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve([
|
|
||||||
keyName,
|
|
||||||
decryption,
|
|
||||||
decryption.free.bind(decryption),
|
|
||||||
]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
error = "The universe is broken";
|
|
||||||
resolve([]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cancel: function(e) {
|
|
||||||
reject(e || new Error("Cancelled"));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (error) {
|
|
||||||
logger.error("Error getting private key:", error);
|
|
||||||
}
|
|
||||||
} while (!keyName);
|
|
||||||
|
|
||||||
// decrypt secret
|
|
||||||
try {
|
try {
|
||||||
|
// fetch private key from app
|
||||||
|
[keyName, decryption] = await this._getSecretStorageKey(keys);
|
||||||
|
|
||||||
|
// decrypt secret
|
||||||
const encInfo = secretContent.encrypted[keyName];
|
const encInfo = secretContent.encrypted[keyName];
|
||||||
switch (keys[keyName].algorithm) {
|
switch (keys[keyName].algorithm) {
|
||||||
case "m.secret_storage.v1.curve25519-aes-sha2":
|
case "m.secret_storage.v1.curve25519-aes-sha2":
|
||||||
@@ -247,7 +198,7 @@ export default class SecretStorage extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
cleanUp();
|
if (decryption) decryption.free();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +309,7 @@ export default class SecretStorage extends EventEmitter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRequestReceived(event) {
|
async _onRequestReceived(event) {
|
||||||
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()
|
||||||
@@ -389,52 +340,55 @@ export default class SecretStorage extends EventEmitter {
|
|||||||
// check if we have the secret
|
// check if we have the secret
|
||||||
logger.info("received request for secret (" + sender
|
logger.info("received request for secret (" + sender
|
||||||
+ ", " + deviceId + ", " + content.request_id + ")");
|
+ ", " + deviceId + ", " + content.request_id + ")");
|
||||||
this._baseApis.emit("crypto.secrets.request", {
|
if (!this._cryptoCallbacks.onSecretRequested) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const secret = await this._cryptoCallbacks.onSecretRequested({
|
||||||
user_id: sender,
|
user_id: sender,
|
||||||
device_id: deviceId,
|
device_id: deviceId,
|
||||||
request_id: content.request_id,
|
request_id: content.request_id,
|
||||||
name: content.name,
|
name: content.name,
|
||||||
device_trust: this._baseApis.checkDeviceTrust(sender, deviceId),
|
device_trust: this._baseApis.checkDeviceTrust(sender, deviceId),
|
||||||
send: async (secret) => {
|
|
||||||
const payload = {
|
|
||||||
type: "m.secret.send",
|
|
||||||
content: {
|
|
||||||
request_id: content.request_id,
|
|
||||||
secret: secret,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const encryptedContent = {
|
|
||||||
algorithm: olmlib.OLM_ALGORITHM,
|
|
||||||
sender_key: this._baseApis._crypto._olmDevice.deviceCurve25519Key,
|
|
||||||
ciphertext: {},
|
|
||||||
};
|
|
||||||
await olmlib.ensureOlmSessionsForDevices(
|
|
||||||
this._baseApis._crypto._olmDevice,
|
|
||||||
this._baseApis,
|
|
||||||
{
|
|
||||||
[sender]: [
|
|
||||||
await this._baseApis.getStoredDevice(sender, deviceId),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await olmlib.encryptMessageForDevice(
|
|
||||||
encryptedContent.ciphertext,
|
|
||||||
this._baseApis.getUserId(),
|
|
||||||
this._baseApis.deviceId,
|
|
||||||
this._baseApis._crypto._olmDevice,
|
|
||||||
sender,
|
|
||||||
this._baseApis._crypto.getStoredDevice(sender, deviceId),
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
const contentMap = {
|
|
||||||
[sender]: {
|
|
||||||
[deviceId]: encryptedContent,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
this._baseApis.sendToDevice("m.room.encrypted", contentMap);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
if (secret) {
|
||||||
|
const payload = {
|
||||||
|
type: "m.secret.send",
|
||||||
|
content: {
|
||||||
|
request_id: content.request_id,
|
||||||
|
secret: secret,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const encryptedContent = {
|
||||||
|
algorithm: olmlib.OLM_ALGORITHM,
|
||||||
|
sender_key: this._baseApis._crypto._olmDevice.deviceCurve25519Key,
|
||||||
|
ciphertext: {},
|
||||||
|
};
|
||||||
|
await olmlib.ensureOlmSessionsForDevices(
|
||||||
|
this._baseApis._crypto._olmDevice,
|
||||||
|
this._baseApis,
|
||||||
|
{
|
||||||
|
[sender]: [
|
||||||
|
await this._baseApis.getStoredDevice(sender, deviceId),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await olmlib.encryptMessageForDevice(
|
||||||
|
encryptedContent.ciphertext,
|
||||||
|
this._baseApis.getUserId(),
|
||||||
|
this._baseApis.deviceId,
|
||||||
|
this._baseApis._crypto._olmDevice,
|
||||||
|
sender,
|
||||||
|
this._baseApis._crypto.getStoredDevice(sender, deviceId),
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
const contentMap = {
|
||||||
|
[sender]: {
|
||||||
|
[deviceId]: encryptedContent,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this._baseApis.sendToDevice("m.room.encrypted", contentMap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,4 +422,49 @@ export default class SecretStorage extends EventEmitter {
|
|||||||
requestControl.resolve(content.secret);
|
requestControl.resolve(content.secret);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _getSecretStorageKey(keys) {
|
||||||
|
if (!this._cryptoCallbacks.getSecretStorageKey) {
|
||||||
|
throw new Error("No getSecretStorageKey callback supplied");
|
||||||
|
}
|
||||||
|
|
||||||
|
const returned = await Promise.resolve(
|
||||||
|
this._cryptoCallbacks.getSecretStorageKey({keys}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!returned) {
|
||||||
|
throw new Error("getSecretStorageKey callback returned falsey");
|
||||||
|
}
|
||||||
|
if (returned.length < 2) {
|
||||||
|
throw new Error("getSecretStorageKey callback returned invalid data");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [keyName, privateKey] = returned;
|
||||||
|
if (!keys[keyName]) {
|
||||||
|
throw new Error("App returned unknown key from getSecretStorageKey!");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (keys[keyName].algorithm) {
|
||||||
|
case "m.secret_storage.v1.curve25519-aes-sha2":
|
||||||
|
{
|
||||||
|
const decryption = new global.Olm.PkDecryption();
|
||||||
|
let pubkey;
|
||||||
|
try {
|
||||||
|
pubkey = decryption.init_with_private_key(privateKey);
|
||||||
|
} catch (e) {
|
||||||
|
decryption.free();
|
||||||
|
throw new Error("getSecretStorageKey callback returned invalid key");
|
||||||
|
}
|
||||||
|
if (pubkey !== keys[keyName].pubkey) {
|
||||||
|
decryption.free();
|
||||||
|
throw new Error(
|
||||||
|
"getSecretStorageKey callback returned incorrect key",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [keyName, decryption];
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error("Unknown key type: " + keys[keyName].algorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,7 +207,9 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
|
|||||||
userId, this._baseApis._cryptoCallbacks,
|
userId, this._baseApis._cryptoCallbacks,
|
||||||
);
|
);
|
||||||
|
|
||||||
this._secretStorage = new SecretStorage(baseApis);
|
this._secretStorage = new SecretStorage(
|
||||||
|
baseApis, this._baseApis._cryptoCallbacks,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
utils.inherits(Crypto, EventEmitter);
|
utils.inherits(Crypto, EventEmitter);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user