Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b606d1e54b | |||
| cd7c519dc4 | |||
| 30dd28960c | |||
| 5b635df08d | |||
| 592c497902 | |||
| 8e3f2f3262 | |||
| 5751df1288 | |||
| 40a71101e2 | |||
| 1c1ac137d3 | |||
| 89cabc4912 | |||
| 5be4548b3d | |||
| 09de76bd43 | |||
| 3a694f4998 | |||
| 3a8a1389f5 | |||
| c271e1533a | |||
| 722debe8f9 | |||
| 5165899e82 | |||
| 1828826661 | |||
| 24cee68fa2 | |||
| e645af1fc5 | |||
| de64779c27 | |||
| acbcb4658a | |||
| 815484b543 | |||
| 5a3d1a2a67 | |||
| 18626169e4 | |||
| e4a9f958a0 | |||
| ff29de743c | |||
| 5a68861418 |
@@ -19,7 +19,7 @@ concurrency:
|
||||
jobs:
|
||||
build-element-web:
|
||||
name: Build element-web
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.73.1
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.74.0
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
specs: [browserify, integ, unit]
|
||||
node: [16, 18, latest]
|
||||
node: [18, latest]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
@@ -50,6 +50,9 @@ jobs:
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
|
||||
# tell jest to use coloured output
|
||||
FORCE_COLOR: true
|
||||
|
||||
- name: Move coverage files into place
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
run: mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@284f54f989303d2699d373481a0cfa13ad5a6666 # v5
|
||||
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/upgrade-deps
|
||||
|
||||
@@ -21,6 +21,8 @@ endpoints from before Matrix 1.1, for example.
|
||||
|
||||
## In a browser
|
||||
|
||||
### Note, the browserify build has been deprecated. Please use a bundler like webpack or vite instead.
|
||||
|
||||
Download the browser version from
|
||||
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
|
||||
`<script>` to your page. There will be a global variable `matrixcs`
|
||||
|
||||
+7
-7
@@ -3,7 +3,7 @@
|
||||
"version": "26.2.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
@@ -32,8 +32,8 @@
|
||||
"keywords": [
|
||||
"matrix-org"
|
||||
],
|
||||
"main": "./lib/index.js",
|
||||
"browser": "./lib/browser-index.js",
|
||||
"main": "./src/index.ts",
|
||||
"browser": "./src/browser-index.ts",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_src_browser": "./src/browser-index.ts",
|
||||
"matrix_lib_main": "./lib/index.js",
|
||||
@@ -55,10 +55,11 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.11",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.1",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.3.1",
|
||||
@@ -101,7 +102,7 @@
|
||||
"debug": "^4.3.4",
|
||||
"docdash": "^2.0.0",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.41.0",
|
||||
"eslint": "8.43.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
@@ -156,6 +157,5 @@
|
||||
"no-rust-crypto": {
|
||||
"src/rust-crypto/index.ts$": "./src/rust-crypto/browserify-index.ts"
|
||||
}
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,11 @@ limitations under the License.
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import { MockResponse } from "fetch-mock";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { createHash } from "crypto";
|
||||
|
||||
import { createClient, CryptoEvent, ICreateClientOpts, MatrixClient } from "../../../src";
|
||||
import {
|
||||
@@ -50,21 +52,7 @@ import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
// to ensure that we don't end up with dangling timeouts.
|
||||
jest.useFakeTimers();
|
||||
|
||||
let previousCrypto: Crypto | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Stub out global.crypto
|
||||
previousCrypto = global["crypto"];
|
||||
|
||||
Object.defineProperty(global, "crypto", {
|
||||
value: {
|
||||
getRandomValues: function <T extends Uint8Array>(array: T): T {
|
||||
array.fill(0x12);
|
||||
return array;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// we use the libolm primitives in the test, so init the Olm library
|
||||
await global.Olm.init();
|
||||
});
|
||||
@@ -82,18 +70,6 @@ afterEach(() => {
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
// restore the original global.crypto
|
||||
afterAll(() => {
|
||||
if (previousCrypto === undefined) {
|
||||
// @ts-ignore deleting a non-optional property. It *is* optional really.
|
||||
delete global.crypto;
|
||||
} else {
|
||||
Object.defineProperty(global, "crypto", {
|
||||
value: previousCrypto,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/** The homeserver url that we give to the test client, and where we intercept /sync, /keys, etc requests. */
|
||||
const TEST_HOMESERVER_URL = "https://alice-server.com";
|
||||
|
||||
@@ -177,6 +153,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(request.chosenMethod).toBe(null); // nothing chosen yet
|
||||
expect(request.initiatedByMe).toBe(true);
|
||||
expect(request.otherUserId).toEqual(TEST_USER_ID);
|
||||
expect(request.pending).toBe(true);
|
||||
// we're using fake timers, so the timeout should have exactly 10 minutes left still.
|
||||
expect(request.timeout).toEqual(600_000);
|
||||
|
||||
// and now the request should be visible via `getVerificationRequestsToDeviceInProgress`
|
||||
{
|
||||
@@ -235,13 +214,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
// The dummy device makes up a curve25519 keypair and sends the public bit back in an `m.key.verification.key'
|
||||
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
returnToDeviceMessageFromSync({
|
||||
type: "m.key.verification.key",
|
||||
content: {
|
||||
transaction_id: transactionId,
|
||||
key: olmSAS.get_pubkey(),
|
||||
},
|
||||
});
|
||||
returnToDeviceMessageFromSync(buildSasKeyMessage(transactionId, olmSAS.get_pubkey()));
|
||||
|
||||
// alice responds with a 'key' ...
|
||||
requestBody = await expectSendToDeviceMessage("m.key.verification.key");
|
||||
@@ -265,36 +238,20 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
|
||||
// the dummy device also confirms that the emoji match, and sends a mac
|
||||
const macInfoBase = `MATRIX_KEY_VERIFICATION_MAC${TEST_USER_ID}${TEST_DEVICE_ID}${TEST_USER_ID}${aliceClient.deviceId}${transactionId}`;
|
||||
returnToDeviceMessageFromSync({
|
||||
type: "m.key.verification.mac",
|
||||
content: {
|
||||
keys: calculateMAC(olmSAS, `ed25519:${TEST_DEVICE_ID}`, `${macInfoBase}KEY_IDS`),
|
||||
transaction_id: transactionId,
|
||||
mac: {
|
||||
[`ed25519:${TEST_DEVICE_ID}`]: calculateMAC(
|
||||
olmSAS,
|
||||
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
|
||||
`${macInfoBase}ed25519:${TEST_DEVICE_ID}`,
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
returnToDeviceMessageFromSync(
|
||||
buildSasMacMessage(transactionId, olmSAS, TEST_USER_ID, aliceClient.deviceId!),
|
||||
);
|
||||
|
||||
// that should satisfy Alice, who should reply with a 'done'
|
||||
await expectSendToDeviceMessage("m.key.verification.done");
|
||||
|
||||
// the dummy device also confirms done-ness
|
||||
returnToDeviceMessageFromSync({
|
||||
type: "m.key.verification.done",
|
||||
content: {
|
||||
transaction_id: transactionId,
|
||||
},
|
||||
});
|
||||
returnToDeviceMessageFromSync(buildDoneMessage(transactionId));
|
||||
|
||||
// ... and the whole thing should be done!
|
||||
await verificationPromise;
|
||||
expect(request.phase).toEqual(VerificationPhase.Done);
|
||||
expect(request.pending).toBe(false);
|
||||
|
||||
// at this point, cancelling should do nothing.
|
||||
await request.cancel();
|
||||
@@ -304,6 +261,102 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
olmSAS.free();
|
||||
});
|
||||
|
||||
it("can initiate SAS verification ourselves", async () => {
|
||||
aliceClient = await startTestClient();
|
||||
await waitForDeviceList();
|
||||
|
||||
// Alice sends a m.key.verification.request
|
||||
const [, request] = await Promise.all([
|
||||
expectSendToDeviceMessage("m.key.verification.request"),
|
||||
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
|
||||
]);
|
||||
const transactionId = request.transactionId!;
|
||||
|
||||
// The dummy device replies with an m.key.verification.ready
|
||||
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.sas.v1"]));
|
||||
await waitForVerificationRequestChanged(request);
|
||||
expect(request.phase).toEqual(VerificationPhase.Ready);
|
||||
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true);
|
||||
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
|
||||
// And now Alice starts a SAS verification
|
||||
let sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
|
||||
await request.startVerification("m.sas.v1");
|
||||
let requestBody = await sendToDevicePromise;
|
||||
|
||||
let toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage).toEqual({
|
||||
from_device: aliceClient.deviceId,
|
||||
method: "m.sas.v1",
|
||||
transaction_id: transactionId,
|
||||
hashes: ["sha256"],
|
||||
key_agreement_protocols: expect.arrayContaining(["curve25519-hkdf-sha256"]),
|
||||
message_authentication_codes: expect.arrayContaining(["hkdf-hmac-sha256.v2"]),
|
||||
short_authentication_string: ["decimal", "emoji"],
|
||||
});
|
||||
|
||||
expect(request.chosenMethod).toEqual("m.sas.v1");
|
||||
|
||||
// There should now be a `verifier`
|
||||
const verifier: Verifier = request.verifier!;
|
||||
expect(verifier).toBeDefined();
|
||||
expect(verifier.getShowSasCallbacks()).toBeNull();
|
||||
const verificationPromise = verifier.verify();
|
||||
|
||||
// The dummy device makes up a curve25519 keypair and uses the hash in an 'm.key.verification.accept'
|
||||
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(toDeviceMessage);
|
||||
|
||||
sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.key");
|
||||
returnToDeviceMessageFromSync(buildSasAcceptMessage(transactionId, commitmentStr));
|
||||
|
||||
// alice responds with a 'key' ...
|
||||
requestBody = await sendToDevicePromise;
|
||||
|
||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
const aliceDevicePubKeyBase64 = toDeviceMessage.key;
|
||||
olmSAS.set_their_key(aliceDevicePubKeyBase64);
|
||||
|
||||
// ... and the dummy device also sends a 'key'
|
||||
returnToDeviceMessageFromSync(buildSasKeyMessage(transactionId, olmSAS.get_pubkey()));
|
||||
|
||||
// ... and the client is notified to show the emoji
|
||||
const showSas = await new Promise<ShowSasCallbacks>((resolve) => {
|
||||
verifier.once(VerifierEvent.ShowSas, resolve);
|
||||
});
|
||||
|
||||
// `getShowSasCallbacks` is an alternative way to get the callbacks
|
||||
expect(verifier.getShowSasCallbacks()).toBe(showSas);
|
||||
expect(verifier.getReciprocateQrCodeCallbacks()).toBeNull();
|
||||
|
||||
// user confirms that the emoji match, and alice sends a 'mac'
|
||||
[requestBody] = await Promise.all([expectSendToDeviceMessage("m.key.verification.mac"), showSas.confirm()]);
|
||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
|
||||
// the dummy device also confirms that the emoji match, and sends a mac
|
||||
returnToDeviceMessageFromSync(
|
||||
buildSasMacMessage(transactionId, olmSAS, TEST_USER_ID, aliceClient.deviceId!),
|
||||
);
|
||||
|
||||
// that should satisfy Alice, who should reply with a 'done'
|
||||
await expectSendToDeviceMessage("m.key.verification.done");
|
||||
|
||||
// the dummy device also confirms done-ness
|
||||
returnToDeviceMessageFromSync(buildDoneMessage(transactionId));
|
||||
|
||||
// ... and the whole thing should be done!
|
||||
await verificationPromise;
|
||||
expect(request.phase).toEqual(VerificationPhase.Done);
|
||||
|
||||
// we're done with the temporary keypair
|
||||
olmSAS.free();
|
||||
});
|
||||
|
||||
it("Can make a verification request to *all* devices", async () => {
|
||||
aliceClient = await startTestClient();
|
||||
// we need an existing cross-signing key for this
|
||||
@@ -492,25 +545,22 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
});
|
||||
|
||||
describe("Incoming verification from another device", () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA);
|
||||
|
||||
aliceClient = await startTestClient();
|
||||
await waitForDeviceList();
|
||||
});
|
||||
|
||||
oldBackendOnly("Incoming verification: can accept", async () => {
|
||||
aliceClient = await startTestClient();
|
||||
it("Incoming verification: can accept", async () => {
|
||||
const TRANSACTION_ID = "abcd";
|
||||
|
||||
// Initiate the request by sending a to-device message
|
||||
returnToDeviceMessageFromSync({
|
||||
type: "m.key.verification.request",
|
||||
content: {
|
||||
from_device: TEST_DEVICE_ID,
|
||||
methods: ["m.sas.v1"],
|
||||
transaction_id: TRANSACTION_ID,
|
||||
timestamp: Date.now() - 1000,
|
||||
},
|
||||
});
|
||||
const request: VerificationRequest = await emitPromise(aliceClient, CryptoEvent.VerificationRequest);
|
||||
returnToDeviceMessageFromSync(buildRequestMessage(TRANSACTION_ID));
|
||||
const request: VerificationRequest = await emitPromise(
|
||||
aliceClient,
|
||||
CryptoEvent.VerificationRequestReceived,
|
||||
);
|
||||
expect(request.transactionId).toEqual(TRANSACTION_ID);
|
||||
expect(request.phase).toEqual(VerificationPhase.Requested);
|
||||
expect(request.roomId).toBeUndefined();
|
||||
@@ -518,6 +568,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(request.otherUserId).toEqual(TEST_USER_ID);
|
||||
expect(request.chosenMethod).toBe(null); // nothing chosen yet
|
||||
expect(canAcceptVerificationRequest(request)).toBe(true);
|
||||
expect(request.pending).toBe(true);
|
||||
|
||||
// Alice accepts, by sending a to-device message
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.ready");
|
||||
@@ -533,6 +584,31 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
|
||||
expect(toDeviceMessage.transaction_id).toEqual(TRANSACTION_ID);
|
||||
});
|
||||
|
||||
it("Incoming verification: can refuse", async () => {
|
||||
const TRANSACTION_ID = "abcd";
|
||||
|
||||
// Initiate the request by sending a to-device message
|
||||
returnToDeviceMessageFromSync(buildRequestMessage(TRANSACTION_ID));
|
||||
const request: VerificationRequest = await emitPromise(
|
||||
aliceClient,
|
||||
CryptoEvent.VerificationRequestReceived,
|
||||
);
|
||||
expect(request.transactionId).toEqual(TRANSACTION_ID);
|
||||
|
||||
// Alice declines, by sending a cancellation
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.cancel");
|
||||
const cancelPromise = request.cancel();
|
||||
expect(canAcceptVerificationRequest(request)).toBe(false);
|
||||
expect(request.accepting).toBe(false);
|
||||
expect(request.declining).toBe(true);
|
||||
await cancelPromise;
|
||||
const requestBody = await sendToDevicePromise;
|
||||
expect(request.phase).toEqual(VerificationPhase.Cancelled);
|
||||
|
||||
const toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.transaction_id).toEqual(TRANSACTION_ID);
|
||||
});
|
||||
});
|
||||
|
||||
async function startTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
|
||||
@@ -607,10 +683,28 @@ function calculateMAC(olmSAS: Olm.SAS, input: string, info: string): string {
|
||||
return mac;
|
||||
}
|
||||
|
||||
/** Calculate the sha256 hash of a string, encoding as unpadded base64 */
|
||||
function sha256(commitmentStr: string): string {
|
||||
return encodeUnpaddedBase64(createHash("sha256").update(commitmentStr, "utf8").digest());
|
||||
}
|
||||
|
||||
function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
|
||||
return Buffer.from(uint8Array).toString("base64").replace(/=+$/g, "");
|
||||
}
|
||||
|
||||
/** build an m.key.verification.request to-device message originating from the dummy device */
|
||||
function buildRequestMessage(transactionId: string): { type: string; content: object } {
|
||||
return {
|
||||
type: "m.key.verification.request",
|
||||
content: {
|
||||
from_device: TEST_DEVICE_ID,
|
||||
methods: ["m.sas.v1"],
|
||||
transaction_id: transactionId,
|
||||
timestamp: Date.now() - 1000,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** build an m.key.verification.ready to-device message originating from the dummy device */
|
||||
function buildReadyMessage(transactionId: string, methods: string[]): { type: string; content: object } {
|
||||
return {
|
||||
@@ -639,3 +733,64 @@ function buildSasStartMessage(transactionId: string): { type: string; content: o
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** build an m.key.verification.accept to-device message suitable for the SAS flow */
|
||||
function buildSasAcceptMessage(transactionId: string, commitmentStr: string) {
|
||||
return {
|
||||
type: "m.key.verification.accept",
|
||||
content: {
|
||||
transaction_id: transactionId,
|
||||
commitment: sha256(commitmentStr),
|
||||
hash: "sha256",
|
||||
key_agreement_protocol: "curve25519-hkdf-sha256",
|
||||
short_authentication_string: ["decimal", "emoji"],
|
||||
message_authentication_code: "hkdf-hmac-sha256.v2",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** build an m.key.verification.key to-device message suitable for the SAS flow */
|
||||
function buildSasKeyMessage(transactionId: string, key: string): { type: string; content: object } {
|
||||
return {
|
||||
type: "m.key.verification.key",
|
||||
content: {
|
||||
transaction_id: transactionId,
|
||||
key: key,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** build an m.key.verification.mac to-device message suitable for the SAS flow, originating from the dummy device */
|
||||
function buildSasMacMessage(
|
||||
transactionId: string,
|
||||
olmSAS: Olm.SAS,
|
||||
recipientUserId: string,
|
||||
recipientDeviceId: string,
|
||||
): { type: string; content: object } {
|
||||
const macInfoBase = `MATRIX_KEY_VERIFICATION_MAC${TEST_USER_ID}${TEST_DEVICE_ID}${recipientUserId}${recipientDeviceId}${transactionId}`;
|
||||
|
||||
return {
|
||||
type: "m.key.verification.mac",
|
||||
content: {
|
||||
keys: calculateMAC(olmSAS, `ed25519:${TEST_DEVICE_ID}`, `${macInfoBase}KEY_IDS`),
|
||||
transaction_id: transactionId,
|
||||
mac: {
|
||||
[`ed25519:${TEST_DEVICE_ID}`]: calculateMAC(
|
||||
olmSAS,
|
||||
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
|
||||
`${macInfoBase}ed25519:${TEST_DEVICE_ID}`,
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** build an m.key.verification.done to-device message */
|
||||
function buildDoneMessage(transactionId: string) {
|
||||
return {
|
||||
type: "m.key.verification.done",
|
||||
content: {
|
||||
transaction_id: transactionId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -598,12 +598,6 @@ describe("MatrixClient event timelines", function () {
|
||||
await client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
@@ -634,12 +628,6 @@ describe("MatrixClient event timelines", function () {
|
||||
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
|
||||
await httpBackend.flushAllExpected();
|
||||
const timelineSet = thread.timelineSet;
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
await flushHttp(emitPromise(thread, ThreadEvent.Update));
|
||||
|
||||
const timeline = await client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
|
||||
|
||||
@@ -1510,7 +1498,8 @@ describe("MatrixClient event timelines", function () {
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
THREAD_REPLY2.localTimestamp += 1000;
|
||||
// this has to come after THREAD_REPLY which hasn't been instantiated by us
|
||||
THREAD_REPLY2.localTimestamp += 10000000;
|
||||
|
||||
// Test data for the first thread, with the second reply
|
||||
const THREAD_ROOT_UPDATED = {
|
||||
@@ -1570,9 +1559,6 @@ describe("MatrixClient event timelines", function () {
|
||||
thread.initialEventsFetched = true;
|
||||
const prom = emitPromise(room, ThreadEvent.NewReply);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD2_ROOT);
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
await httpBackend.flushAllExpected();
|
||||
@@ -1699,13 +1685,11 @@ describe("MatrixClient event timelines", function () {
|
||||
thread.initialEventsFetched = true;
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD2_ROOT);
|
||||
await room.addLiveEvents([THREAD_REPLY_REACTION]);
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
expect(thread.length).toBe(2);
|
||||
expect(thread.length).toBe(1); // reactions don't count towards the length of a thread
|
||||
// Test thread order is unchanged
|
||||
expect(timeline!.getEvents().map((it) => it.event.event_id)).toEqual([
|
||||
THREAD_ROOT.event_id,
|
||||
@@ -2021,25 +2005,6 @@ describe("MatrixClient event timelines", function () {
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
||||
buildRelationPaginationQuery({ dir: Direction.Backward, limit: 1 }),
|
||||
)
|
||||
.respond(200, function () {
|
||||
return {
|
||||
chunk: [THREAD_REPLY],
|
||||
};
|
||||
});
|
||||
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
|
||||
|
||||
const room = client.getRoom(roomId)!;
|
||||
@@ -2047,71 +2012,7 @@ describe("MatrixClient event timelines", function () {
|
||||
expect(thread.initialEventsFetched).toBeTruthy();
|
||||
const timelineSet = thread.timelineSet;
|
||||
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return {
|
||||
start: "start_token",
|
||||
events_before: [],
|
||||
event: THREAD_ROOT,
|
||||
events_after: [],
|
||||
end: "end_token",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
||||
buildRelationPaginationQuery({
|
||||
dir: Direction.Backward,
|
||||
from: "start_token",
|
||||
}),
|
||||
)
|
||||
.respond(200, function () {
|
||||
return {
|
||||
chunk: [],
|
||||
};
|
||||
});
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
||||
buildRelationPaginationQuery({ dir: Direction.Forward, from: "end_token" }),
|
||||
)
|
||||
.respond(200, function () {
|
||||
return {
|
||||
chunk: [THREAD_REPLY],
|
||||
};
|
||||
});
|
||||
|
||||
const timeline = await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!));
|
||||
const timeline = await client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_5",
|
||||
|
||||
@@ -157,7 +157,7 @@ export const mkThread = ({
|
||||
room?.reEmitter.reEmit(evt, [MatrixEventEvent.BeforeRedaction]);
|
||||
}
|
||||
|
||||
const thread = room.createThread(rootEvent.getId() ?? "", rootEvent, events, true);
|
||||
const thread = room.createThread(rootEvent.getId() ?? "", rootEvent, [rootEvent, ...events], true);
|
||||
|
||||
return { thread, rootEvent, events };
|
||||
};
|
||||
|
||||
@@ -94,7 +94,6 @@ describe("InteractiveAuth", () => {
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: [AuthType.Password] }],
|
||||
errcode: "MockError0",
|
||||
params: {
|
||||
[AuthType.Password]: { param: "aa" },
|
||||
},
|
||||
|
||||
@@ -308,4 +308,25 @@ describe("MatrixEvent", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should ignore thread relation on state events", async () => {
|
||||
const stateEvent = new MatrixEvent({
|
||||
event_id: "$event_id",
|
||||
type: "some_state_event",
|
||||
content: {
|
||||
"foo": "bar",
|
||||
"m.relates_to": {
|
||||
"event_id": "$thread_id",
|
||||
"m.in_reply_to": {
|
||||
event_id: "$thread_id",
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
state_key: "",
|
||||
});
|
||||
|
||||
expect(stateEvent.isState()).toBeTruthy();
|
||||
expect(stateEvent.threadRootId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ import { mocked } from "jest-mock";
|
||||
|
||||
import { MatrixClient, PendingEventOrdering } from "../../../src/client";
|
||||
import { Room, RoomEvent } from "../../../src/models/room";
|
||||
import { Thread, THREAD_RELATION_TYPE, ThreadEvent, FeatureSupport } from "../../../src/models/thread";
|
||||
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread";
|
||||
import { makeThreadEvent, mkThread } from "../../test-utils/thread";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { emitPromise, mkEdit, mkMessage, mkReaction, mock } from "../../test-utils/test-utils";
|
||||
@@ -43,6 +43,7 @@ describe("Thread", () => {
|
||||
const myUserId = "@bob:example.org";
|
||||
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, { timelineSupport: false });
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
@@ -300,6 +301,7 @@ describe("Thread", () => {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
@@ -354,6 +356,7 @@ describe("Thread", () => {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
@@ -405,6 +408,7 @@ describe("Thread", () => {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
@@ -699,11 +703,7 @@ async function createThread(client: MatrixClient, user: string, roomId: string):
|
||||
root.setThreadId(root.getId());
|
||||
await room.addLiveEvents([root]);
|
||||
|
||||
// Create the thread and wait for it to be initialised
|
||||
const thread = room.createThread(root.getId()!, root, [], false);
|
||||
await new Promise<void>((res) => thread.once(RoomEvent.TimelineReset, () => res()));
|
||||
|
||||
return thread;
|
||||
return room.createThread(root.getId()!, root, [], false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,8 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { mocked } from "jest-mock";
|
||||
import jwtDecode from "jwt-decode";
|
||||
|
||||
import { Method } from "../../../src";
|
||||
import * as crypto from "../../../src/crypto/crypto";
|
||||
@@ -26,6 +28,8 @@ import {
|
||||
} from "../../../src/oidc/authorize";
|
||||
import { OidcError } from "../../../src/oidc/error";
|
||||
|
||||
jest.mock("jwt-decode");
|
||||
|
||||
// save for resetting mocks
|
||||
const realSubtleCrypto = crypto.subtleCrypto;
|
||||
|
||||
@@ -112,15 +116,28 @@ describe("oidc authorization", () => {
|
||||
|
||||
describe("completeAuthorizationCodeGrant", () => {
|
||||
const codeVerifier = "abc123";
|
||||
const nonce = "test-nonce";
|
||||
const redirectUri = baseUrl;
|
||||
const code = "auth_code_xyz";
|
||||
const validBearerTokenResponse = {
|
||||
token_type: "Bearer",
|
||||
access_token: "test_access_token",
|
||||
refresh_token: "test_refresh_token",
|
||||
id_token: "valid.id.token",
|
||||
expires_in: 12345,
|
||||
};
|
||||
|
||||
const validDecodedIdToken = {
|
||||
// nonce matches
|
||||
nonce,
|
||||
// not expired
|
||||
exp: Date.now() / 1000 + 100000,
|
||||
// audience is this client
|
||||
aud: clientId,
|
||||
// issuer matches
|
||||
iss: delegatedAuthConfig.issuer,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.resetBehavior();
|
||||
@@ -129,10 +146,18 @@ describe("oidc authorization", () => {
|
||||
status: 200,
|
||||
body: JSON.stringify(validBearerTokenResponse),
|
||||
});
|
||||
|
||||
mocked(jwtDecode).mockReturnValue(validDecodedIdToken);
|
||||
});
|
||||
|
||||
it("should make correct request to the token endpoint", async () => {
|
||||
await completeAuthorizationCodeGrant(code, { clientId, codeVerifier, redirectUri, delegatedAuthConfig });
|
||||
await completeAuthorizationCodeGrant(code, {
|
||||
clientId,
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
delegatedAuthConfig,
|
||||
nonce,
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(tokenEndpoint, {
|
||||
method: Method.Post,
|
||||
@@ -147,6 +172,7 @@ describe("oidc authorization", () => {
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
delegatedAuthConfig,
|
||||
nonce,
|
||||
});
|
||||
|
||||
expect(result).toEqual(validBearerTokenResponse);
|
||||
@@ -171,6 +197,7 @@ describe("oidc authorization", () => {
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
delegatedAuthConfig,
|
||||
nonce,
|
||||
});
|
||||
|
||||
// results in token that uses 'Bearer' token type
|
||||
@@ -187,7 +214,13 @@ describe("oidc authorization", () => {
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
await expect(() =>
|
||||
completeAuthorizationCodeGrant(code, { clientId, codeVerifier, redirectUri, delegatedAuthConfig }),
|
||||
completeAuthorizationCodeGrant(code, {
|
||||
clientId,
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
delegatedAuthConfig,
|
||||
nonce,
|
||||
}),
|
||||
).rejects.toThrow(new Error(OidcError.CodeExchangeFailed));
|
||||
});
|
||||
|
||||
@@ -202,8 +235,31 @@ describe("oidc authorization", () => {
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
await expect(() =>
|
||||
completeAuthorizationCodeGrant(code, { clientId, codeVerifier, redirectUri, delegatedAuthConfig }),
|
||||
completeAuthorizationCodeGrant(code, {
|
||||
clientId,
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
delegatedAuthConfig,
|
||||
nonce,
|
||||
}),
|
||||
).rejects.toThrow(new Error(OidcError.InvalidBearerTokenResponse));
|
||||
});
|
||||
|
||||
it("should throw invalid id token error when id_token is invalid", async () => {
|
||||
mocked(jwtDecode).mockReturnValue({
|
||||
...validDecodedIdToken,
|
||||
// invalid audience
|
||||
aud: "something-else",
|
||||
});
|
||||
await expect(() =>
|
||||
completeAuthorizationCodeGrant(code, {
|
||||
clientId,
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
delegatedAuthConfig,
|
||||
nonce,
|
||||
}),
|
||||
).rejects.toThrow(new Error(OidcError.InvalidIdToken));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,11 +14,20 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import jwtDecode from "jwt-decode";
|
||||
|
||||
import { M_AUTHENTICATION } from "../../../src";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { validateOIDCIssuerWellKnown, validateWellKnownAuthentication } from "../../../src/oidc/validate";
|
||||
import {
|
||||
validateIdToken,
|
||||
validateOIDCIssuerWellKnown,
|
||||
validateWellKnownAuthentication,
|
||||
} from "../../../src/oidc/validate";
|
||||
import { OidcError } from "../../../src/oidc/error";
|
||||
|
||||
jest.mock("jwt-decode");
|
||||
|
||||
describe("validateWellKnownAuthentication()", () => {
|
||||
const baseWk = {
|
||||
"m.homeserver": {
|
||||
@@ -194,3 +203,96 @@ describe("validateOIDCIssuerWellKnown", () => {
|
||||
expect(() => validateOIDCIssuerWellKnown(wk)).toThrow(OidcError.OpSupport);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateIdToken()", () => {
|
||||
const nonce = "test-nonce";
|
||||
const issuer = "https://auth.org/issuer";
|
||||
const clientId = "test-client-id";
|
||||
const idToken = "test-id-token";
|
||||
|
||||
const validDecodedIdToken = {
|
||||
// nonce matches
|
||||
nonce,
|
||||
// not expired
|
||||
exp: Date.now() / 1000 + 5555,
|
||||
// audience is this client
|
||||
aud: clientId,
|
||||
// issuer matches
|
||||
iss: issuer,
|
||||
};
|
||||
beforeEach(() => {
|
||||
mocked(jwtDecode).mockClear().mockReturnValue(validDecodedIdToken);
|
||||
|
||||
jest.spyOn(logger, "error").mockClear();
|
||||
});
|
||||
|
||||
it("should throw when idToken is falsy", () => {
|
||||
expect(() => validateIdToken(undefined, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
||||
});
|
||||
|
||||
it("should throw when idToken cannot be decoded", () => {
|
||||
mocked(jwtDecode).mockImplementation(() => {
|
||||
throw new Error("oh no!");
|
||||
});
|
||||
expect(() => validateIdToken(undefined, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
||||
});
|
||||
|
||||
it("should throw when issuer does not match", () => {
|
||||
mocked(jwtDecode).mockReturnValue({
|
||||
...validDecodedIdToken,
|
||||
iss: "https://badissuer.com",
|
||||
});
|
||||
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
||||
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid issuer"));
|
||||
});
|
||||
|
||||
it("should throw when audience does not include clientId", () => {
|
||||
mocked(jwtDecode).mockReturnValue({
|
||||
...validDecodedIdToken,
|
||||
aud: "qwerty,uiop,asdf",
|
||||
});
|
||||
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
||||
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid audience"));
|
||||
});
|
||||
|
||||
it("should throw when audience includes clientId and other audiences", () => {
|
||||
mocked(jwtDecode).mockReturnValue({
|
||||
...validDecodedIdToken,
|
||||
aud: `${clientId},uiop,asdf`,
|
||||
});
|
||||
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
||||
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid audience"));
|
||||
});
|
||||
|
||||
it("should throw when nonce does not match", () => {
|
||||
mocked(jwtDecode).mockReturnValue({
|
||||
...validDecodedIdToken,
|
||||
nonce: "something else",
|
||||
});
|
||||
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
||||
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid nonce"));
|
||||
});
|
||||
|
||||
it("should throw when token does not have an expiry", () => {
|
||||
mocked(jwtDecode).mockReturnValue({
|
||||
...validDecodedIdToken,
|
||||
exp: undefined,
|
||||
});
|
||||
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
||||
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid expiry"));
|
||||
});
|
||||
|
||||
it("should throw when token is expired", () => {
|
||||
mocked(jwtDecode).mockReturnValue({
|
||||
...validDecodedIdToken,
|
||||
// expired in the past
|
||||
exp: Date.now() / 1000 - 777,
|
||||
});
|
||||
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
||||
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid expiry"));
|
||||
});
|
||||
|
||||
it("should not throw for a valid id token", () => {
|
||||
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2787,10 +2787,10 @@ describe("Room", function () {
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
await room.addLiveEvents([threadRoot, threadResponse1]);
|
||||
const thread: Thread = await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
|
||||
expect(thread.initialEventsFetched).toBeTruthy();
|
||||
await room.addLiveEvents([threadResponse2]);
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent!.getId()).toBe(threadResponse2.getId());
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import { KeysQueryRequest, OlmMachine } from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import { Mocked } from "jest-mock";
|
||||
@@ -24,6 +22,7 @@ import fetchMock from "fetch-mock-jest";
|
||||
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
|
||||
import { initRustCrypto } from "../../../src/rust-crypto";
|
||||
import {
|
||||
CryptoEvent,
|
||||
HttpApiEvent,
|
||||
HttpApiEventHandlerMap,
|
||||
IHttpOpts,
|
||||
@@ -37,16 +36,9 @@ import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
|
||||
import { IEventDecryptionResult } from "../../../src/@types/crypto";
|
||||
import { OutgoingRequest, OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
import { ServerSideSecretStorage } from "../../../src/secret-storage";
|
||||
import { CryptoCallbacks, ImportRoomKeysOpts } from "../../../src/crypto-api";
|
||||
import { CryptoCallbacks, ImportRoomKeysOpts, VerificationRequest } from "../../../src/crypto-api";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
const TEST_USER = "@alice:example.com";
|
||||
const TEST_DEVICE_ID = "TEST_DEVICE";
|
||||
|
||||
@@ -149,6 +141,27 @@ describe("RustCrypto", () => {
|
||||
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
|
||||
expect(res).toEqual(inputs);
|
||||
});
|
||||
|
||||
it("emits VerificationRequestReceived on incoming m.key.verification.request", async () => {
|
||||
const toDeviceEvent = {
|
||||
type: "m.key.verification.request",
|
||||
content: {
|
||||
from_device: "testDeviceId",
|
||||
methods: ["m.sas.v1"],
|
||||
transaction_id: "testTxn",
|
||||
timestamp: Date.now() - 1000,
|
||||
},
|
||||
sender: "@user:id",
|
||||
};
|
||||
|
||||
const onEvent = jest.fn();
|
||||
rustCrypto.on(CryptoEvent.VerificationRequestReceived, onEvent);
|
||||
await rustCrypto.preprocessToDeviceMessages([toDeviceEvent]);
|
||||
expect(onEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [req]: [VerificationRequest] = onEvent.mock.lastCall;
|
||||
expect(req.transactionId).toEqual("testTxn");
|
||||
});
|
||||
});
|
||||
|
||||
it("getCrossSigningKeyId", async () => {
|
||||
@@ -520,5 +533,5 @@ async function makeTestRustCrypto(
|
||||
secretStorage: ServerSideSecretStorage = {} as ServerSideSecretStorage,
|
||||
cryptoCallbacks: CryptoCallbacks = {} as CryptoCallbacks,
|
||||
): Promise<RustCrypto> {
|
||||
return await initRustCrypto(http, userId, deviceId, secretStorage, cryptoCallbacks);
|
||||
return await initRustCrypto(http, userId, deviceId, secretStorage, cryptoCallbacks, null);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import { Mocked } from "jest-mock";
|
||||
|
||||
import { RustVerificationRequest } from "../../../src/rust-crypto/verification";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
|
||||
describe("VerificationRequest", () => {
|
||||
describe("pending", () => {
|
||||
let request: RustVerificationRequest;
|
||||
let mockedInner: Mocked<RustSdkCryptoJs.VerificationRequest>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedInner = makeMockedInner();
|
||||
request = makeTestRequest(mockedInner);
|
||||
});
|
||||
|
||||
it("returns true for a created request", () => {
|
||||
expect(request.pending).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for passive requests", () => {
|
||||
mockedInner.isPassive.mockReturnValue(true);
|
||||
expect(request.pending).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for completed requests", () => {
|
||||
mockedInner.phase.mockReturnValue(RustSdkCryptoJs.VerificationRequestPhase.Done);
|
||||
expect(request.pending).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for cancelled requests", () => {
|
||||
mockedInner.phase.mockReturnValue(RustSdkCryptoJs.VerificationRequestPhase.Cancelled);
|
||||
expect(request.pending).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("timeout", () => {
|
||||
it("passes through the result", () => {
|
||||
const mockedInner = makeMockedInner();
|
||||
const request = makeTestRequest(mockedInner);
|
||||
mockedInner.timeRemainingMillis.mockReturnValue(10_000);
|
||||
expect(request.timeout).toEqual(10_000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("startVerification", () => {
|
||||
let request: RustVerificationRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
request = makeTestRequest();
|
||||
});
|
||||
|
||||
it("does not permit methods other than SAS", async () => {
|
||||
await expect(request.startVerification("m.reciprocate.v1")).rejects.toThrow(
|
||||
"Unsupported verification method",
|
||||
);
|
||||
});
|
||||
|
||||
it("raises an error if starting verification does not produce a verifier", async () => {
|
||||
await expect(request.startVerification("m.sas.v1")).rejects.toThrow(
|
||||
"Still no verifier after startSas() call",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/** build a RustVerificationRequest with default parameters */
|
||||
function makeTestRequest(
|
||||
inner?: RustSdkCryptoJs.VerificationRequest,
|
||||
outgoingRequestProcessor?: OutgoingRequestProcessor,
|
||||
): RustVerificationRequest {
|
||||
inner ??= makeMockedInner();
|
||||
outgoingRequestProcessor ??= {} as OutgoingRequestProcessor;
|
||||
return new RustVerificationRequest(inner, outgoingRequestProcessor, undefined);
|
||||
}
|
||||
|
||||
/** Mock up a rust-side VerificationRequest */
|
||||
function makeMockedInner(): Mocked<RustSdkCryptoJs.VerificationRequest> {
|
||||
return {
|
||||
registerChangesCallback: jest.fn(),
|
||||
startSas: jest.fn(),
|
||||
phase: jest.fn().mockReturnValue(RustSdkCryptoJs.VerificationRequestPhase.Created),
|
||||
isPassive: jest.fn().mockReturnValue(false),
|
||||
timeRemainingMillis: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.VerificationRequest>;
|
||||
}
|
||||
+155
-13
@@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { IClientWellKnown } from "../client";
|
||||
|
||||
// disable lint because these are wire responses
|
||||
/* eslint-disable camelcase */
|
||||
@@ -79,19 +80,6 @@ export interface IIdentityProvider {
|
||||
brand?: IdentityProviderBrand | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters to login request as per https://spec.matrix.org/v1.3/client-server-api/#login
|
||||
*/
|
||||
/* eslint-disable camelcase */
|
||||
export interface ILoginParams {
|
||||
identifier?: object;
|
||||
password?: string;
|
||||
token?: string;
|
||||
device_id?: string;
|
||||
initial_device_display_name?: string;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
export enum SSOAction {
|
||||
/** The user intends to login to an existing account */
|
||||
LOGIN = "login",
|
||||
@@ -100,6 +88,160 @@ export enum SSOAction {
|
||||
REGISTER = "register",
|
||||
}
|
||||
|
||||
/**
|
||||
* A client can identify a user using their Matrix ID.
|
||||
* This can either be the fully qualified Matrix user ID, or just the localpart of the user ID.
|
||||
* @see https://spec.matrix.org/v1.7/client-server-api/#matrix-user-id
|
||||
*/
|
||||
type UserLoginIdentifier = {
|
||||
type: "m.id.user";
|
||||
user: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A client can identify a user using a 3PID associated with the user’s account on the homeserver,
|
||||
* where the 3PID was previously associated using the /account/3pid API.
|
||||
* See the 3PID Types Appendix for a list of Third-party ID media.
|
||||
* @see https://spec.matrix.org/v1.7/client-server-api/#third-party-id
|
||||
*/
|
||||
type ThirdPartyLoginIdentifier = {
|
||||
type: "m.id.thirdparty";
|
||||
medium: string;
|
||||
address: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A client can identify a user using a phone number associated with the user’s account,
|
||||
* where the phone number was previously associated using the /account/3pid API.
|
||||
* The phone number can be passed in as entered by the user; the homeserver will be responsible for canonicalising it.
|
||||
* If the client wishes to canonicalise the phone number,
|
||||
* then it can use the m.id.thirdparty identifier type with a medium of msisdn instead.
|
||||
*
|
||||
* The country is the two-letter uppercase ISO-3166-1 alpha-2 country code that the number in phone should be parsed as if it were dialled from.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.7/client-server-api/#phone-number
|
||||
*/
|
||||
type PhoneLoginIdentifier = {
|
||||
type: "m.id.phone";
|
||||
country: string;
|
||||
phone: string;
|
||||
};
|
||||
|
||||
type SpecUserIdentifier = UserLoginIdentifier | ThirdPartyLoginIdentifier | PhoneLoginIdentifier;
|
||||
|
||||
/**
|
||||
* User Identifiers usable for login & user-interactive authentication.
|
||||
*
|
||||
* Extensibly allows more than Matrix specified identifiers.
|
||||
*/
|
||||
export type UserIdentifier =
|
||||
| SpecUserIdentifier
|
||||
| { type: Exclude<string, SpecUserIdentifier["type"]>; [key: string]: any };
|
||||
|
||||
/**
|
||||
* Request body for POST /login request
|
||||
* @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3login
|
||||
*/
|
||||
export interface LoginRequest {
|
||||
/**
|
||||
* The login type being used.
|
||||
*/
|
||||
type: "m.login.password" | "m.login.token" | string;
|
||||
/**
|
||||
* Third-party identifier for the user.
|
||||
* @deprecated in favour of `identifier`.
|
||||
*/
|
||||
address?: string;
|
||||
/**
|
||||
* ID of the client device.
|
||||
* If this does not correspond to a known client device, a new device will be created.
|
||||
* The given device ID must not be the same as a cross-signing key ID.
|
||||
* The server will auto-generate a device_id if this is not specified.
|
||||
*/
|
||||
device_id?: string;
|
||||
/**
|
||||
* Identification information for a user
|
||||
*/
|
||||
identifier?: UserIdentifier;
|
||||
/**
|
||||
* A display name to assign to the newly-created device.
|
||||
* Ignored if device_id corresponds to a known device.
|
||||
*/
|
||||
initial_device_display_name?: string;
|
||||
/**
|
||||
* When logging in using a third-party identifier, the medium of the identifier.
|
||||
* Must be `email`.
|
||||
* @deprecated in favour of `identifier`.
|
||||
*/
|
||||
medium?: "email";
|
||||
/**
|
||||
* Required when type is `m.login.password`. The user’s password.
|
||||
*/
|
||||
password?: string;
|
||||
/**
|
||||
* If true, the client supports refresh tokens.
|
||||
*/
|
||||
refresh_token?: boolean;
|
||||
/**
|
||||
* Required when type is `m.login.token`. Part of Token-based login.
|
||||
*/
|
||||
token?: string;
|
||||
/**
|
||||
* The fully qualified user ID or just local part of the user ID, to log in.
|
||||
* @deprecated in favour of identifier.
|
||||
*/
|
||||
user?: string;
|
||||
// Extensible
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Export for backwards compatibility
|
||||
export type ILoginParams = LoginRequest;
|
||||
|
||||
/**
|
||||
* Response body for POST /login request
|
||||
* @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3login
|
||||
*/
|
||||
export interface LoginResponse {
|
||||
/**
|
||||
* An access token for the account.
|
||||
* This access token can then be used to authorize other requests.
|
||||
*/
|
||||
access_token: string;
|
||||
/**
|
||||
* ID of the logged-in device.
|
||||
* Will be the same as the corresponding parameter in the request, if one was specified.
|
||||
*/
|
||||
device_id: string;
|
||||
/**
|
||||
* The fully-qualified Matrix ID for the account.
|
||||
*/
|
||||
user_id: string;
|
||||
/**
|
||||
* The lifetime of the access token, in milliseconds.
|
||||
* Once the access token has expired a new access token can be obtained by using the provided refresh token.
|
||||
* If no refresh token is provided, the client will need to re-log in to obtain a new access token.
|
||||
* If not given, the client can assume that the access token will not expire.
|
||||
*/
|
||||
expires_in_ms?: number;
|
||||
/**
|
||||
* A refresh token for the account.
|
||||
* This token can be used to obtain a new access token when it expires by calling the /refresh endpoint.
|
||||
*/
|
||||
refresh_token?: string;
|
||||
/**
|
||||
* Optional client configuration provided by the server.
|
||||
* If present, clients SHOULD use the provided object to reconfigure themselves, optionally validating the URLs within.
|
||||
* This object takes the same form as the one returned from .well-known autodiscovery.
|
||||
*/
|
||||
well_known?: IClientWellKnown;
|
||||
/**
|
||||
* The server_name of the homeserver on which the account has been registered.
|
||||
* @deprecated Clients should extract the server_name from user_id (by splitting at the first colon) if they require it.
|
||||
*/
|
||||
home_server?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a successful [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882)
|
||||
* `m.login.token` issuance request.
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { AuthDict } from "../interactive-auth";
|
||||
|
||||
/**
|
||||
* The request body of a call to `POST /_matrix/client/v3/register`.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3register
|
||||
*/
|
||||
export interface RegisterRequest {
|
||||
/**
|
||||
* Additional authentication information for the user-interactive authentication API.
|
||||
* Note that this information is not used to define how the registered user should be authenticated,
|
||||
* but is instead used to authenticate the register call itself.
|
||||
*/
|
||||
auth?: AuthDict;
|
||||
/**
|
||||
* The basis for the localpart of the desired Matrix ID.
|
||||
* If omitted, the homeserver MUST generate a Matrix ID local part.
|
||||
*/
|
||||
username?: string;
|
||||
/**
|
||||
* The desired password for the account.
|
||||
*/
|
||||
password?: string;
|
||||
/**
|
||||
* If true, the client supports refresh tokens.
|
||||
*/
|
||||
refresh_token?: boolean;
|
||||
/**
|
||||
* If true, an access_token and device_id should not be returned from this call, therefore preventing an automatic login.
|
||||
* Defaults to false.
|
||||
*/
|
||||
inhibit_login?: boolean;
|
||||
/**
|
||||
* A display name to assign to the newly-created device.
|
||||
* Ignored if device_id corresponds to a known device.
|
||||
*/
|
||||
initial_device_display_name?: string;
|
||||
/**
|
||||
* @deprecated missing in the spec
|
||||
*/
|
||||
guest_access_token?: string;
|
||||
/**
|
||||
* @deprecated missing in the spec
|
||||
*/
|
||||
x_show_msisdn?: boolean;
|
||||
/**
|
||||
* @deprecated missing in the spec
|
||||
*/
|
||||
bind_msisdn?: boolean;
|
||||
/**
|
||||
* @deprecated missing in the spec
|
||||
*/
|
||||
bind_email?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a successful call to `POST /_matrix/client/v3/register`.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3register
|
||||
*/
|
||||
export interface RegisterResponse {
|
||||
/**
|
||||
* The fully-qualified Matrix user ID (MXID) that has been registered.
|
||||
*/
|
||||
user_id: string;
|
||||
/**
|
||||
* An access token for the account.
|
||||
* This access token can then be used to authorize other requests.
|
||||
* Required if the inhibit_login option is false.
|
||||
*/
|
||||
access_token?: string;
|
||||
/**
|
||||
* ID of the registered device.
|
||||
* Will be the same as the corresponding parameter in the request, if one was specified.
|
||||
* Required if the inhibit_login option is false.
|
||||
*/
|
||||
device_id?: string;
|
||||
/**
|
||||
* The lifetime of the access token, in milliseconds.
|
||||
* Once the access token has expired a new access token can be obtained by using the provided refresh token.
|
||||
* If no refresh token is provided, the client will need to re-log in to obtain a new access token.
|
||||
* If not given, the client can assume that the access token will not expire.
|
||||
*
|
||||
* Omitted if the inhibit_login option is true.
|
||||
*/
|
||||
expires_in_ms?: number;
|
||||
/**
|
||||
* A refresh token for the account.
|
||||
* This token can be used to obtain a new access token when it expires by calling the /refresh endpoint.
|
||||
*
|
||||
* Omitted if the inhibit_login option is true.
|
||||
*/
|
||||
refresh_token?: string;
|
||||
/**
|
||||
* The server_name of the homeserver on which the account has been registered.
|
||||
*
|
||||
* @deprecated Clients should extract the server_name from user_id (by splitting at the first colon) if they require it.
|
||||
*/
|
||||
home_server?: string;
|
||||
}
|
||||
@@ -176,7 +176,8 @@ export interface IAddThreePidOnlyBody {
|
||||
export interface IBindThreePidBody {
|
||||
client_secret: string;
|
||||
id_server: string;
|
||||
id_access_token: string;
|
||||
// Some older identity servers have no auth enabled
|
||||
id_access_token: string | null;
|
||||
sid: string;
|
||||
}
|
||||
|
||||
|
||||
+77
-61
@@ -101,7 +101,7 @@ import {
|
||||
import { IIdentityServerProvider } from "./@types/IIdentityServerProvider";
|
||||
import { MatrixScheduler } from "./scheduler";
|
||||
import { BeaconEvent, BeaconEventHandlerMap } from "./models/beacon";
|
||||
import { IAuthData, IAuthDict } from "./interactive-auth";
|
||||
import { AuthDict } from "./interactive-auth";
|
||||
import { IMinimalEvent, IRoomEvent, IStateEvent } from "./sync-accumulator";
|
||||
import { CrossSigningKey, ICreateSecretStorageOpts, IEncryptedEventInfo, IRecoveryKey } from "./crypto/api";
|
||||
import { EventTimelineSet } from "./models/event-timeline-set";
|
||||
@@ -178,7 +178,14 @@ import { IThreepid } from "./@types/threepids";
|
||||
import { CryptoStore, OutgoingRoomKeyRequest } from "./crypto/store/base";
|
||||
import { GroupCall, IGroupCallDataChannelOptions, GroupCallIntent, GroupCallType } from "./webrtc/groupCall";
|
||||
import { MediaHandler } from "./webrtc/mediaHandler";
|
||||
import { LoginTokenPostResponse, ILoginFlowsResponse, IRefreshTokenResponse, SSOAction } from "./@types/auth";
|
||||
import {
|
||||
LoginTokenPostResponse,
|
||||
ILoginFlowsResponse,
|
||||
IRefreshTokenResponse,
|
||||
SSOAction,
|
||||
LoginResponse,
|
||||
LoginRequest,
|
||||
} from "./@types/auth";
|
||||
import { TypedEventEmitter } from "./models/typed-event-emitter";
|
||||
import { MAIN_ROOM_TIMELINE, ReceiptType } from "./@types/read_receipts";
|
||||
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
|
||||
@@ -209,6 +216,7 @@ import {
|
||||
ServerSideSecretStorage,
|
||||
ServerSideSecretStorageImpl,
|
||||
} from "./secret-storage";
|
||||
import { RegisterRequest, RegisterResponse } from "./@types/registration";
|
||||
|
||||
export type Store = IStore;
|
||||
|
||||
@@ -717,18 +725,8 @@ interface IJoinedMembersResponse {
|
||||
};
|
||||
}
|
||||
|
||||
export interface IRegisterRequestParams {
|
||||
auth?: IAuthDict;
|
||||
username?: string;
|
||||
password?: string;
|
||||
refresh_token?: boolean;
|
||||
guest_access_token?: string;
|
||||
x_show_msisdn?: boolean;
|
||||
bind_msisdn?: boolean;
|
||||
bind_email?: boolean;
|
||||
inhibit_login?: boolean;
|
||||
initial_device_display_name?: string;
|
||||
}
|
||||
// Re-export for backwards compatibility
|
||||
export type IRegisterRequestParams = RegisterRequest;
|
||||
|
||||
export interface IPublicRoomsChunkRoom {
|
||||
room_id: string;
|
||||
@@ -924,6 +922,7 @@ type CryptoEvents =
|
||||
| CryptoEvent.RoomKeyRequest
|
||||
| CryptoEvent.RoomKeyRequestCancellation
|
||||
| CryptoEvent.VerificationRequest
|
||||
| CryptoEvent.VerificationRequestReceived
|
||||
| CryptoEvent.DeviceVerificationChanged
|
||||
| CryptoEvent.UserTrustStatusChanged
|
||||
| CryptoEvent.KeysChanged
|
||||
@@ -2205,10 +2204,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @experimental
|
||||
*
|
||||
* @param useIndexedDB - True to use an indexeddb store, false to use an in-memory store. Defaults to 'true'.
|
||||
*
|
||||
* @returns a Promise which will resolve when the crypto layer has been
|
||||
* successfully initialised.
|
||||
*/
|
||||
public async initRustCrypto(): Promise<void> {
|
||||
public async initRustCrypto({ useIndexedDB = true }: { useIndexedDB?: boolean } = {}): Promise<void> {
|
||||
if (this.cryptoBackend) {
|
||||
logger.warn("Attempt to re-initialise e2e encryption on MatrixClient");
|
||||
return;
|
||||
@@ -2238,6 +2239,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
deviceId,
|
||||
this.secretStorage,
|
||||
this.cryptoCallbacks,
|
||||
useIndexedDB ? RUST_SDK_STORE_PREFIX : null,
|
||||
);
|
||||
rustCrypto.supportedVerificationMethods = this.verificationMethods;
|
||||
|
||||
@@ -2245,6 +2247,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
// attach the event listeners needed by RustCrypto
|
||||
this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto));
|
||||
|
||||
// re-emit the events emitted by the crypto impl
|
||||
this.reEmitter.reEmit(rustCrypto, [CryptoEvent.VerificationRequestReceived]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -7646,7 +7651,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param bindThreepids - Set key 'email' to true to bind any email
|
||||
* threepid uses during registration in the identity server. Set 'msisdn' to
|
||||
* true to bind msisdn.
|
||||
* @returns Promise which resolves: TODO
|
||||
* @returns Promise which resolves to a RegisterResponse object
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public register(
|
||||
@@ -7657,7 +7662,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
bindThreepids?: boolean | null | { email?: boolean; msisdn?: boolean },
|
||||
guestAccessToken?: string,
|
||||
inhibitLogin?: boolean,
|
||||
): Promise<IAuthData> {
|
||||
): Promise<RegisterResponse> {
|
||||
// backwards compat
|
||||
if (bindThreepids === true) {
|
||||
bindThreepids = { email: true };
|
||||
@@ -7668,7 +7673,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
auth.session = sessionId;
|
||||
}
|
||||
|
||||
const params: IRegisterRequestParams = {
|
||||
const params: RegisterRequest = {
|
||||
auth: auth,
|
||||
refresh_token: true, // always ask for a refresh token - does nothing if unsupported
|
||||
};
|
||||
@@ -7724,8 +7729,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* `{ user_id, device_id, access_token, home_server }`
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public registerGuest({ body }: { body?: any } = {}): Promise<any> {
|
||||
// TODO: Types
|
||||
public registerGuest({ body }: { body?: RegisterRequest } = {}): Promise<RegisterResponse> {
|
||||
return this.registerRequest(body || {}, "guest");
|
||||
}
|
||||
|
||||
@@ -7735,7 +7739,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: to the /register response
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public registerRequest(data: IRegisterRequestParams, kind?: string): Promise<IAuthData> {
|
||||
public registerRequest(data: RegisterRequest, kind?: string): Promise<RegisterResponse> {
|
||||
const params: { kind?: string } = {};
|
||||
if (kind) {
|
||||
params.kind = kind;
|
||||
@@ -7788,23 +7792,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Promise which resolves: TODO
|
||||
* @returns Promise which resolves to a LoginResponse object
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public login(loginType: string, data: any): Promise<any> {
|
||||
// TODO: Types
|
||||
const loginData = {
|
||||
type: loginType,
|
||||
};
|
||||
|
||||
// merge data into loginData
|
||||
Object.assign(loginData, data);
|
||||
|
||||
public login(loginType: LoginRequest["type"], data: Omit<LoginRequest, "type">): Promise<LoginResponse> {
|
||||
return this.http
|
||||
.authedRequest<{
|
||||
access_token?: string;
|
||||
user_id?: string;
|
||||
}>(Method.Post, "/login", undefined, loginData)
|
||||
.authedRequest<LoginResponse>(Method.Post, "/login", undefined, {
|
||||
...data,
|
||||
type: loginType,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.access_token && response.user_id) {
|
||||
this.http.opts.accessToken = response.access_token;
|
||||
@@ -7817,11 +7813,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Promise which resolves: TODO
|
||||
* @returns Promise which resolves to a LoginResponse object
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public loginWithPassword(user: string, password: string): Promise<any> {
|
||||
// TODO: Types
|
||||
public loginWithPassword(user: string, password: string): Promise<LoginResponse> {
|
||||
return this.login("m.login.password", {
|
||||
user: user,
|
||||
password: password,
|
||||
@@ -7830,11 +7825,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
/**
|
||||
* @param relayState - URL Callback after SAML2 Authentication
|
||||
* @returns Promise which resolves: TODO
|
||||
* @returns Promise which resolves to a LoginResponse object
|
||||
* @returns Rejects: with an error response.
|
||||
* @deprecated this isn't in the Matrix spec anymore
|
||||
*/
|
||||
public loginWithSAML2(relayState: string): Promise<any> {
|
||||
// TODO: Types
|
||||
public loginWithSAML2(relayState: string): Promise<LoginResponse> {
|
||||
return this.login("m.login.saml2", {
|
||||
relay_state: relayState,
|
||||
});
|
||||
@@ -7874,11 +7869,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
/**
|
||||
* @param token - Login token previously received from homeserver
|
||||
* @returns Promise which resolves: TODO
|
||||
* @returns Promise which resolves to a LoginResponse object
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public loginWithToken(token: string): Promise<any> {
|
||||
// TODO: Types
|
||||
public loginWithToken(token: string): Promise<LoginResponse> {
|
||||
return this.login("m.login.token", {
|
||||
token: token,
|
||||
});
|
||||
@@ -7922,7 +7916,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* to false.
|
||||
* @returns Promise which resolves: On success, the empty object
|
||||
*/
|
||||
public deactivateAccount(auth?: any, erase?: boolean): Promise<{}> {
|
||||
public deactivateAccount(auth?: any, erase?: boolean): Promise<{ id_server_unbind_result: IdServerUnbindResult }> {
|
||||
const body: any = {};
|
||||
if (auth) {
|
||||
body.auth = auth;
|
||||
@@ -7943,7 +7937,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: On success, the token response
|
||||
* or UIA auth data.
|
||||
*/
|
||||
public async requestLoginToken(auth?: IAuthDict): Promise<UIAResponse<LoginTokenPostResponse>> {
|
||||
public async requestLoginToken(auth?: AuthDict): Promise<UIAResponse<LoginTokenPostResponse>> {
|
||||
// use capabilities to determine which revision of the MSC is being used
|
||||
const capabilities = await this.getCapabilities();
|
||||
// use r1 endpoint if capability is exposed otherwise use old r0 endpoint
|
||||
@@ -8583,7 +8577,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: to an empty object `{}`
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public setPassword(authDict: IAuthDict, newPassword: string, logoutDevices?: boolean): Promise<{}> {
|
||||
public setPassword(authDict: AuthDict, newPassword: string, logoutDevices?: boolean): Promise<{}> {
|
||||
const path = "/account/password";
|
||||
const data = {
|
||||
auth: authDict,
|
||||
@@ -8641,7 +8635,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: result object
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public deleteDevice(deviceId: string, auth?: IAuthDict): Promise<IAuthData | {}> {
|
||||
public deleteDevice(deviceId: string, auth?: AuthDict): Promise<{}> {
|
||||
const path = utils.encodeUri("/devices/$device_id", {
|
||||
$device_id: deviceId,
|
||||
});
|
||||
@@ -8663,7 +8657,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: result object
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public deleteMultipleDevices(devices: string[], auth?: IAuthDict): Promise<IAuthData | {}> {
|
||||
public deleteMultipleDevices(devices: string[], auth?: AuthDict): Promise<{}> {
|
||||
const body: any = { devices };
|
||||
|
||||
if (auth) {
|
||||
@@ -8948,7 +8942,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return this.http.authedRequest(Method.Get, "/keys/changes", qps);
|
||||
}
|
||||
|
||||
public uploadDeviceSigningKeys(auth?: IAuthDict, keys?: CrossSigningKeys): Promise<{}> {
|
||||
public uploadDeviceSigningKeys(auth?: AuthDict, keys?: CrossSigningKeys): Promise<{}> {
|
||||
// API returns empty object
|
||||
const data = Object.assign({}, keys);
|
||||
if (auth) Object.assign(data, { auth });
|
||||
@@ -9096,6 +9090,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param msisdnToken - The MSISDN token, as enetered by the user.
|
||||
* @param identityAccessToken - The `access_token` field of the Identity
|
||||
* Server `/account/register` response (see {@link registerWithIdentityServer}).
|
||||
* Some legacy identity servers had no authentication here.
|
||||
*
|
||||
* @returns Promise which resolves: Object, containing success boolean.
|
||||
* @returns Rejects: with an error response.
|
||||
@@ -9105,7 +9100,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
sid: string,
|
||||
clientSecret: string,
|
||||
msisdnToken: string,
|
||||
identityAccessToken: string,
|
||||
identityAccessToken: string | null,
|
||||
): Promise<{ success: boolean }> {
|
||||
const params = {
|
||||
sid: sid,
|
||||
@@ -9118,7 +9113,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
"/validate/msisdn/submitToken",
|
||||
params,
|
||||
IdentityPrefix.V2,
|
||||
identityAccessToken,
|
||||
identityAccessToken ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9160,8 +9155,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param identityAccessToken - The access token for the identity server.
|
||||
* @returns The hashing information for the identity server.
|
||||
*/
|
||||
public getIdentityHashDetails(identityAccessToken: string): Promise<any> {
|
||||
// TODO: Types
|
||||
public getIdentityHashDetails(identityAccessToken: string): Promise<{
|
||||
/**
|
||||
* The algorithms the server supports. Must contain at least sha256.
|
||||
*/
|
||||
algorithms: string[];
|
||||
/**
|
||||
* The pepper the client MUST use in hashing identifiers,
|
||||
* and MUST supply to the /lookup endpoint when performing lookups.
|
||||
*/
|
||||
lookup_pepper: string;
|
||||
}> {
|
||||
return this.http.idServerRequest(
|
||||
Method.Get,
|
||||
"/hash_details",
|
||||
@@ -9269,8 +9273,18 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* exists
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public async lookupThreePid(medium: string, address: string, identityAccessToken: string): Promise<any> {
|
||||
// TODO: Types
|
||||
public async lookupThreePid(
|
||||
medium: string,
|
||||
address: string,
|
||||
identityAccessToken: string,
|
||||
): Promise<
|
||||
| {
|
||||
address: string;
|
||||
medium: string;
|
||||
mxid: string;
|
||||
}
|
||||
| {}
|
||||
> {
|
||||
// Note: we're using the V2 API by calling this function, but our
|
||||
// function contract requires a V1 response. We therefore have to
|
||||
// convert it manually.
|
||||
@@ -9306,8 +9320,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: Lookup results from IS.
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public async bulkLookupThreePids(query: [string, string][], identityAccessToken: string): Promise<any> {
|
||||
// TODO: Types
|
||||
public async bulkLookupThreePids(
|
||||
query: [string, string][],
|
||||
identityAccessToken: string,
|
||||
): Promise<{
|
||||
threepids: [medium: string, address: string, mxid: string][];
|
||||
}> {
|
||||
// Note: we're using the V2 API by calling this function, but our
|
||||
// function contract requires a V1 response. We therefore have to
|
||||
// convert it manually.
|
||||
@@ -9345,8 +9363,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: an object with account info.
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public getIdentityAccount(identityAccessToken: string): Promise<any> {
|
||||
// TODO: Types
|
||||
public getIdentityAccount(identityAccessToken: string): Promise<{ user_id: string }> {
|
||||
return this.http.idServerRequest(Method.Get, "/account", undefined, IdentityPrefix.V2, identityAccessToken);
|
||||
}
|
||||
|
||||
@@ -9439,7 +9456,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves to the result object
|
||||
*/
|
||||
public getThirdpartyUser(protocol: string, params: any): Promise<IThirdPartyUser[]> {
|
||||
// TODO: Types
|
||||
const path = utils.encodeUri("/thirdparty/user/$protocol", {
|
||||
$protocol: protocol,
|
||||
});
|
||||
|
||||
@@ -128,9 +128,20 @@ export interface VerificationRequest
|
||||
* @param targetDevice - details of where to send the request to.
|
||||
*
|
||||
* @returns The verifier which will do the actual verification.
|
||||
*
|
||||
* @deprecated Use {@link VerificationRequest#startVerification} instead.
|
||||
*/
|
||||
beginKeyVerification(method: string, targetDevice?: { userId?: string; deviceId?: string }): Verifier;
|
||||
|
||||
/**
|
||||
* Send an `m.key.verification.start` event to start verification via a particular method.
|
||||
*
|
||||
* @param method - the name of the verification method to use.
|
||||
*
|
||||
* @returns The verifier which will do the actual verification.
|
||||
*/
|
||||
startVerification(method: string): Promise<Verifier>;
|
||||
|
||||
/**
|
||||
* The verifier which is doing the actual verification, once the method has been established.
|
||||
* Only defined when the `phase` is Started.
|
||||
|
||||
@@ -194,9 +194,10 @@ class OlmDecryption extends DecryptionAlgorithm {
|
||||
|
||||
// check that the device that encrypted the event belongs to the user that the event claims it's from.
|
||||
//
|
||||
// To do this, we need to make sure that our device list is up-to-date. If the device is unknown, we can only
|
||||
// assume that the device logged out and accept it anyway. Some event handlers, such as secret sharing, may be
|
||||
// more strict and reject events that come from unknown devices.
|
||||
// If the device is unknown then we check that we don't have any pending key-query requests for the sender. If
|
||||
// after that the device is still unknown, then we can only assume that the device logged out and accept it
|
||||
// anyway. Some event handlers, such as secret sharing, may be more strict and reject events that come from
|
||||
// unknown devices.
|
||||
//
|
||||
// This is a defence against the following scenario:
|
||||
//
|
||||
@@ -204,14 +205,26 @@ class OlmDecryption extends DecryptionAlgorithm {
|
||||
// * Mallory gets control of Alice's server, and sends a megolm session to Alice using her (Mallory's)
|
||||
// senderkey, but claiming to be from Bob.
|
||||
// * Mallory sends more events using that session, claiming to be from Bob.
|
||||
// * Alice sees that the senderkey is verified (since she verified Mallory) so marks events those
|
||||
// events as verified even though the sender is forged.
|
||||
// * Alice sees that the senderkey is verified (since she verified Mallory) so marks events those events as
|
||||
// verified even though the sender is forged.
|
||||
//
|
||||
// In practice, it's not clear that the js-sdk would behave that way, so this may be only a defence in depth.
|
||||
|
||||
await this.crypto.deviceList.downloadKeys([event.getSender()!], false);
|
||||
const senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, deviceKey);
|
||||
if (senderKeyUser !== event.getSender() && senderKeyUser != undefined) {
|
||||
let senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, deviceKey);
|
||||
if (senderKeyUser === undefined || senderKeyUser === null) {
|
||||
// Wait for any pending key query fetches for the user to complete before trying the lookup again.
|
||||
try {
|
||||
await this.crypto.deviceList.downloadKeys([event.getSender()!], false);
|
||||
} catch (e) {
|
||||
throw new DecryptionError("OLM_BAD_SENDER_CHECK_FAILED", "Could not verify sender identity", {
|
||||
sender: deviceKey,
|
||||
err: e as Error,
|
||||
});
|
||||
}
|
||||
|
||||
senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, deviceKey);
|
||||
}
|
||||
if (senderKeyUser !== event.getSender() && senderKeyUser !== undefined && senderKeyUser !== null) {
|
||||
throw new DecryptionError("OLM_BAD_SENDER", "Message claimed to be from " + event.getSender(), {
|
||||
real_sender: senderKeyUser,
|
||||
});
|
||||
|
||||
@@ -118,12 +118,21 @@ export class BackupManager {
|
||||
public checkedForBackup: boolean; // Have we checked the server for a backup we can use?
|
||||
private sendingBackups: boolean; // Are we currently sending backups?
|
||||
private sessionLastCheckAttemptedTime: Record<string, number> = {}; // When did we last try to check the server for a given session id?
|
||||
// The backup manager will schedule backup of keys when active (`scheduleKeyBackupSend`), this allows cancel when client is stopped
|
||||
private clientRunning = true;
|
||||
|
||||
public constructor(private readonly baseApis: MatrixClient, public readonly getKey: GetKey) {
|
||||
this.checkedForBackup = false;
|
||||
this.sendingBackups = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the backup manager from backing up keys and allow a clean shutdown.
|
||||
*/
|
||||
public stop(): void {
|
||||
this.clientRunning = false;
|
||||
}
|
||||
|
||||
public get version(): string | undefined {
|
||||
return this.backupInfo && this.backupInfo.version;
|
||||
}
|
||||
@@ -439,6 +448,10 @@ export class BackupManager {
|
||||
// the same time when a new key is sent
|
||||
const delay = Math.random() * maxDelay;
|
||||
await sleep(delay);
|
||||
if (!this.clientRunning) {
|
||||
logger.debug("Key backup send aborted, client stopped");
|
||||
return;
|
||||
}
|
||||
let numFailures = 0; // number of consecutive failures
|
||||
for (;;) {
|
||||
if (!this.algorithm) {
|
||||
@@ -473,6 +486,11 @@ export class BackupManager {
|
||||
// exponential backoff if we have failures
|
||||
await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4)));
|
||||
}
|
||||
|
||||
if (!this.clientRunning) {
|
||||
logger.debug("Key backup send loop aborted, client stopped");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.sendingBackups = false;
|
||||
|
||||
+38
-15
@@ -91,6 +91,7 @@ import {
|
||||
CrossSigningStatus,
|
||||
DeviceVerificationStatus,
|
||||
ImportRoomKeysOpts,
|
||||
VerificationRequest as CryptoApiVerificationRequest,
|
||||
} from "../crypto-api";
|
||||
import { Device, DeviceMap } from "../models/device";
|
||||
import { deviceInfoToDevice } from "./device-converter";
|
||||
@@ -129,7 +130,12 @@ export function isCryptoAvailable(): boolean {
|
||||
return Boolean(global.Olm);
|
||||
}
|
||||
|
||||
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
|
||||
// minimum time between attempting to unwedge an Olm session, if we succeeded
|
||||
// in creating a new session
|
||||
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
||||
// minimum time between attempting to unwedge an Olm session, if we failed
|
||||
// to create a new session
|
||||
const FORCE_SESSION_RETRY_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
interface IInitOpts {
|
||||
exportedOlmDevice?: IExportedDevice;
|
||||
@@ -218,7 +224,16 @@ export enum CryptoEvent {
|
||||
KeyBackupFailed = "crypto.keyBackupFailed",
|
||||
KeyBackupSessionsRemaining = "crypto.keyBackupSessionsRemaining",
|
||||
KeySignatureUploadFailure = "crypto.keySignatureUploadFailure",
|
||||
/** @deprecated Use `VerificationRequestReceived`. */
|
||||
VerificationRequest = "crypto.verification.request",
|
||||
|
||||
/**
|
||||
* Fires when a key verification request is received.
|
||||
*
|
||||
* The payload is a {@link Crypto.VerificationRequest}.
|
||||
*/
|
||||
VerificationRequestReceived = "crypto.verificationRequestReceived",
|
||||
|
||||
Warning = "crypto.warning",
|
||||
WillUpdateDevices = "crypto.willUpdateDevices",
|
||||
DevicesUpdated = "crypto.devicesUpdated",
|
||||
@@ -280,8 +295,16 @@ export type CryptoEventHandlerMap = {
|
||||
) => void;
|
||||
/**
|
||||
* Fires when a key verification is requested.
|
||||
*
|
||||
* Deprecated: use `CryptoEvent.VerificationRequestReceived`.
|
||||
*/
|
||||
[CryptoEvent.VerificationRequest]: (request: VerificationRequest<any>) => void;
|
||||
|
||||
/**
|
||||
* Fires when a key verification request is received.
|
||||
*/
|
||||
[CryptoEvent.VerificationRequestReceived]: (request: CryptoApiVerificationRequest) => void;
|
||||
|
||||
/**
|
||||
* Fires when the app may wish to warn the user about something related
|
||||
* the end-to-end crypto.
|
||||
@@ -372,7 +395,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
// to avoid loading room members as long as possible.
|
||||
private roomDeviceTrackingState: { [roomId: string]: Promise<void> } = {};
|
||||
|
||||
// The timestamp of the last time we forced establishment
|
||||
// The timestamp of the minimum time at which we will retry forcing establishment
|
||||
// of a new session for each device, in milliseconds.
|
||||
// {
|
||||
// userId: {
|
||||
@@ -380,7 +403,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
// },
|
||||
// }
|
||||
// Map: user Id → device Id → timestamp
|
||||
private lastNewSessionForced: MapWithDefault<string, MapWithDefault<string, number>> = new MapWithDefault(
|
||||
private forceNewSessionRetryTime: MapWithDefault<string, MapWithDefault<string, number>> = new MapWithDefault(
|
||||
() => new MapWithDefault(() => 0),
|
||||
);
|
||||
|
||||
@@ -1814,6 +1837,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
this.outgoingRoomKeyRequestManager.stop();
|
||||
this.deviceList.stop();
|
||||
this.dehydrationManager.stop();
|
||||
this.backupManager.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3541,6 +3565,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
!request.observeOnly;
|
||||
if (shouldEmit) {
|
||||
this.baseApis.emit(CryptoEvent.VerificationRequest, request);
|
||||
this.baseApis.emit(CryptoEvent.VerificationRequestReceived, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3572,25 +3597,23 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
return;
|
||||
}
|
||||
|
||||
// check when we last forced a new session with this device: if we've already done so
|
||||
// check when we can force a new session with this device: if we've already done so
|
||||
// recently, don't do it again.
|
||||
const lastNewSessionDevices = this.lastNewSessionForced.getOrCreate(sender);
|
||||
const lastNewSessionForced = lastNewSessionDevices.getOrCreate(deviceKey);
|
||||
if (lastNewSessionForced + MIN_FORCE_SESSION_INTERVAL_MS > Date.now()) {
|
||||
const forceNewSessionRetryTimeDevices = this.forceNewSessionRetryTime.getOrCreate(sender);
|
||||
const forceNewSessionRetryTime = forceNewSessionRetryTimeDevices.getOrCreate(deviceKey);
|
||||
if (forceNewSessionRetryTime > Date.now()) {
|
||||
logger.debug(
|
||||
"New session already forced with device " +
|
||||
sender +
|
||||
":" +
|
||||
deviceKey +
|
||||
" at " +
|
||||
lastNewSessionForced +
|
||||
": not forcing another",
|
||||
`New session already forced with device ${sender}:${deviceKey}: ` +
|
||||
`not forcing another until at least ${new Date(forceNewSessionRetryTime).toUTCString()}`,
|
||||
);
|
||||
await this.olmDevice.recordSessionProblem(deviceKey, "wedged", true);
|
||||
retryDecryption();
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure we don't retry to unwedge too soon even if we fail to create a new session
|
||||
forceNewSessionRetryTimeDevices.set(deviceKey, Date.now() + FORCE_SESSION_RETRY_INTERVAL_MS);
|
||||
|
||||
// establish a new olm session with this device since we're failing to decrypt messages
|
||||
// on a current session.
|
||||
// Note that an undecryptable message from another device could easily be spoofed -
|
||||
@@ -3611,7 +3634,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
const devicesByUser = new Map([[sender, [device]]]);
|
||||
await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, true);
|
||||
|
||||
lastNewSessionDevices.set(deviceKey, Date.now());
|
||||
forceNewSessionRetryTimeDevices.set(deviceKey, Date.now() + MIN_FORCE_SESSION_INTERVAL_MS);
|
||||
|
||||
// Now send a blank message on that session so the other side knows about it.
|
||||
// (The keyshare request is sent in the clear so that won't do)
|
||||
|
||||
@@ -18,6 +18,7 @@ limitations under the License.
|
||||
* QR code key verification.
|
||||
*/
|
||||
|
||||
import { crypto } from "../crypto";
|
||||
import { VerificationBase as Base } from "./Base";
|
||||
import { newKeyMismatchError, newUserCancelledError } from "./Error";
|
||||
import { decodeBase64, encodeUnpaddedBase64 } from "../olmlib";
|
||||
@@ -200,7 +201,7 @@ export class QRCodeData {
|
||||
|
||||
private static generateSharedSecret(): string {
|
||||
const secretBytes = new Uint8Array(11);
|
||||
global.crypto.getRandomValues(secretBytes);
|
||||
crypto.getRandomValues(secretBytes);
|
||||
return encodeUnpaddedBase64(secretBytes);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,17 +52,22 @@ const newMismatchedSASError = errorFactory("m.mismatched_sas", "Mismatched short
|
||||
|
||||
const newMismatchedCommitmentError = errorFactory("m.mismatched_commitment", "Mismatched commitment");
|
||||
|
||||
// This list was generated from the data in the Matrix specification [1] with the following command:
|
||||
//
|
||||
// jq -r '.[] | " [\"" + .emoji + "\", \"" + (.description|ascii_downcase) + "\"], // " + (.number|tostring)' sas-emoji.json
|
||||
//
|
||||
// [1]: https://github.com/matrix-org/matrix-spec/blob/main/data-definitions/sas-emoji.json
|
||||
const emojiMapping: EmojiMapping[] = [
|
||||
["🐶", "dog"], // 0
|
||||
["🐱", "cat"], // 1
|
||||
["🦁", "lion"], // 2
|
||||
["🐎", "horse"], // 3
|
||||
["🦄", "unicorn"], // 4
|
||||
["🐷", "pig"], // 5
|
||||
["🐘", "elephant"], // 6
|
||||
["🐰", "rabbit"], // 7
|
||||
["🐼", "panda"], // 8
|
||||
["🐓", "rooster"], // 9
|
||||
["🐶", "dog"], // 0
|
||||
["🐱", "cat"], // 1
|
||||
["🦁", "lion"], // 2
|
||||
["🐎", "horse"], // 3
|
||||
["🦄", "unicorn"], // 4
|
||||
["🐷", "pig"], // 5
|
||||
["🐘", "elephant"], // 6
|
||||
["🐰", "rabbit"], // 7
|
||||
["🐼", "panda"], // 8
|
||||
["🐓", "rooster"], // 9
|
||||
["🐧", "penguin"], // 10
|
||||
["🐢", "turtle"], // 11
|
||||
["🐟", "fish"], // 12
|
||||
@@ -83,7 +88,7 @@ const emojiMapping: EmojiMapping[] = [
|
||||
["🍕", "pizza"], // 27
|
||||
["🎂", "cake"], // 28
|
||||
["❤️", "heart"], // 29
|
||||
["🙂", "smiley"], // 30
|
||||
["😀", "smiley"], // 30
|
||||
["🤖", "robot"], // 31
|
||||
["🎩", "hat"], // 32
|
||||
["👓", "glasses"], // 33
|
||||
@@ -113,7 +118,7 @@ const emojiMapping: EmojiMapping[] = [
|
||||
["🎸", "guitar"], // 57
|
||||
["🎺", "trumpet"], // 58
|
||||
["🔔", "bell"], // 59
|
||||
["⚓️", "anchor"], // 60
|
||||
["⚓", "anchor"], // 60
|
||||
["🎧", "headphones"], // 61
|
||||
["📁", "folder"], // 62
|
||||
["📌", "pin"], // 63
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
VerificationRequest as IVerificationRequest,
|
||||
VerificationRequestEvent,
|
||||
VerificationRequestEventHandlerMap,
|
||||
Verifier,
|
||||
} from "../../../crypto-api/verification";
|
||||
|
||||
// backwards-compatibility exports
|
||||
@@ -458,6 +459,13 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
return this._verifier!;
|
||||
}
|
||||
|
||||
public async startVerification(method: string): Promise<Verifier> {
|
||||
const verifier = this.beginKeyVerification(method);
|
||||
// kick off the verification in the background, but *don't* wait for to complete: we need to return the `Verifier`.
|
||||
verifier.verify();
|
||||
return verifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* sends the initial .request event.
|
||||
* @returns resolves when the event has been sent.
|
||||
|
||||
+104
-74
@@ -21,6 +21,7 @@ import { MatrixClient } from "./client";
|
||||
import { defer, IDeferred } from "./utils";
|
||||
import { MatrixError } from "./http-api";
|
||||
import { UIAResponse } from "./@types/uia";
|
||||
import { UserIdentifier } from "./@types/auth";
|
||||
|
||||
const EMAIL_STAGE_TYPE = "m.login.email.identity";
|
||||
const MSISDN_STAGE_TYPE = "m.login.msisdn";
|
||||
@@ -51,22 +52,25 @@ export interface IStageStatus {
|
||||
* @see https://spec.matrix.org/v1.6/client-server-api/#user-interactive-api-in-the-rest-api
|
||||
*/
|
||||
export interface IAuthData {
|
||||
// XXX: many of the fields here (`type`, `available_flows`, `required_stages`, etc) look like they
|
||||
// shouldn't be here. They aren't in the spec and it's unclear what they are supposed to do. Be wary of using them.
|
||||
/**
|
||||
* This is a session identifier that the client must pass back to the home server,
|
||||
* if one is provided, in subsequent attempts to authenticate in the same API call.
|
||||
*/
|
||||
session?: string;
|
||||
type?: string;
|
||||
/**
|
||||
* A list of the stages the client has completed successfully
|
||||
*/
|
||||
completed?: string[];
|
||||
/**
|
||||
* A list of the login flows supported by the server for this API.
|
||||
*/
|
||||
flows?: UIAFlow[];
|
||||
available_flows?: UIAFlow[];
|
||||
stages?: string[];
|
||||
required_stages?: AuthType[];
|
||||
/**
|
||||
* Contains any information that the client will need to know in order to use a given type of authentication.
|
||||
* For each login type presented, that type may be present as a key in this dictionary.
|
||||
* For example, the public part of an OAuth client ID could be given here.
|
||||
*/
|
||||
params?: Record<string, Record<string, any>>;
|
||||
data?: Record<string, string>;
|
||||
errcode?: string;
|
||||
error?: string;
|
||||
user_id?: string;
|
||||
device_id?: string;
|
||||
access_token?: string;
|
||||
}
|
||||
|
||||
export enum AuthType {
|
||||
@@ -85,30 +89,62 @@ export enum AuthType {
|
||||
UnstableRegistrationToken = "org.matrix.msc3231.login.registration_token",
|
||||
}
|
||||
|
||||
/**
|
||||
* https://spec.matrix.org/v1.7/client-server-api/#password-based
|
||||
*/
|
||||
type PasswordDict = {
|
||||
type: AuthType.Password;
|
||||
identifier: UserIdentifier;
|
||||
password: string;
|
||||
session: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* https://spec.matrix.org/v1.7/client-server-api/#google-recaptcha
|
||||
*/
|
||||
type RecaptchaDict = {
|
||||
type: AuthType.Recaptcha;
|
||||
response: string;
|
||||
session: string;
|
||||
};
|
||||
|
||||
interface ThreepidCreds {
|
||||
sid: string;
|
||||
client_secret: string;
|
||||
id_server: string;
|
||||
id_access_token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* https://spec.matrix.org/v1.7/client-server-api/#email-based-identity--homeserver
|
||||
*/
|
||||
type EmailIdentityDict = {
|
||||
type: AuthType.Email;
|
||||
threepid_creds: ThreepidCreds;
|
||||
/**
|
||||
* @deprecated in favour of `threepid_creds` - kept for backwards compatibility
|
||||
*/
|
||||
threepidCreds?: ThreepidCreds;
|
||||
session: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The parameters which are submitted as the `auth` dict in a UIA request
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
|
||||
*/
|
||||
export interface IAuthDict {
|
||||
// [key: string]: any;
|
||||
type?: string;
|
||||
session?: string;
|
||||
// TODO: Remove `user` once servers support proper UIA
|
||||
// See https://github.com/vector-im/element-web/issues/10312
|
||||
user?: string;
|
||||
identifier?: any;
|
||||
password?: string;
|
||||
response?: string;
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
// See https://github.com/vector-im/element-web/issues/10312
|
||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||
// eslint-disable-next-line camelcase
|
||||
threepid_creds?: any;
|
||||
threepidCreds?: any;
|
||||
// For m.login.registration_token type
|
||||
token?: string;
|
||||
}
|
||||
export type AuthDict =
|
||||
| PasswordDict
|
||||
| RecaptchaDict
|
||||
| EmailIdentityDict
|
||||
| { type: Exclude<string, AuthType>; [key: string]: any }
|
||||
| {};
|
||||
|
||||
/**
|
||||
* Backwards compatible export
|
||||
* @deprecated in favour of AuthDict
|
||||
*/
|
||||
export type IAuthDict = AuthDict;
|
||||
|
||||
export class NoAuthFlowFoundError extends Error {
|
||||
public name = "NoAuthFlowFoundError";
|
||||
@@ -129,7 +165,7 @@ export class NoAuthFlowFoundError extends Error {
|
||||
*/
|
||||
export type UIAuthCallback<T> = (makeRequest: (authData: IAuthDict) => Promise<UIAResponse<T>>) => Promise<T>;
|
||||
|
||||
interface IOpts {
|
||||
interface IOpts<T> {
|
||||
/**
|
||||
* A matrix client to use for the auth process
|
||||
*/
|
||||
@@ -170,7 +206,7 @@ interface IOpts {
|
||||
* The busyChanged callback should be used instead of the background flag.
|
||||
* Should return a promise which resolves to the successful response or rejects with a MatrixError.
|
||||
*/
|
||||
doRequest(auth: IAuthDict | null, background: boolean): Promise<IAuthData>;
|
||||
doRequest(auth: AuthDict | null, background: boolean): Promise<T>;
|
||||
/**
|
||||
* Called when the status of the UI auth changes,
|
||||
* ie. when the state of an auth stage changes of when the auth flow moves to a new stage.
|
||||
@@ -215,21 +251,23 @@ interface IOpts {
|
||||
* submitAuthDict.
|
||||
*
|
||||
* @param opts - options object
|
||||
* @typeParam T - the return type of the request when it is successful
|
||||
*/
|
||||
export class InteractiveAuth {
|
||||
export class InteractiveAuth<T> {
|
||||
private readonly matrixClient: MatrixClient;
|
||||
private readonly inputs: IInputs;
|
||||
private readonly clientSecret: string;
|
||||
private readonly requestCallback: IOpts["doRequest"];
|
||||
private readonly busyChangedCallback?: IOpts["busyChanged"];
|
||||
private readonly stateUpdatedCallback: IOpts["stateUpdated"];
|
||||
private readonly requestEmailTokenCallback: IOpts["requestEmailToken"];
|
||||
private readonly requestCallback: IOpts<T>["doRequest"];
|
||||
private readonly busyChangedCallback?: IOpts<T>["busyChanged"];
|
||||
private readonly stateUpdatedCallback: IOpts<T>["stateUpdated"];
|
||||
private readonly requestEmailTokenCallback: IOpts<T>["requestEmailToken"];
|
||||
private readonly supportedStages?: Set<string>;
|
||||
|
||||
// The current latest data received from the server during the user interactive auth flow.
|
||||
private data: IAuthData;
|
||||
private emailSid?: string;
|
||||
private requestingEmailToken = false;
|
||||
private attemptAuthDeferred: IDeferred<IAuthData> | null = null;
|
||||
private attemptAuthDeferred: IDeferred<T> | null = null;
|
||||
private chosenFlow: UIAFlow | null = null;
|
||||
private currentStage: string | null = null;
|
||||
|
||||
@@ -239,9 +277,9 @@ export class InteractiveAuth {
|
||||
// the promise the will resolve/reject when it completes
|
||||
private submitPromise: Promise<void> | null = null;
|
||||
|
||||
public constructor(opts: IOpts) {
|
||||
public constructor(opts: IOpts<T>) {
|
||||
this.matrixClient = opts.matrixClient;
|
||||
this.data = opts.authData || {};
|
||||
this.data = opts.authData || { flows: [] };
|
||||
this.requestCallback = opts.doRequest;
|
||||
this.busyChangedCallback = opts.busyChanged;
|
||||
// startAuthStage included for backwards compat
|
||||
@@ -262,7 +300,7 @@ export class InteractiveAuth {
|
||||
* or rejects with the error on failure. Rejects with NoAuthFlowFoundError if
|
||||
* no suitable authentication flow can be found
|
||||
*/
|
||||
public attemptAuth(): Promise<IAuthData> {
|
||||
public async attemptAuth(): Promise<T> {
|
||||
// This promise will be quite long-lived and will resolve when the
|
||||
// request is authenticated and completes successfully.
|
||||
this.attemptAuthDeferred = defer();
|
||||
@@ -270,10 +308,10 @@ export class InteractiveAuth {
|
||||
const promise = this.attemptAuthDeferred.promise;
|
||||
|
||||
// if we have no flows, try a request to acquire the flows
|
||||
if (!this.data?.flows) {
|
||||
if (!(this.data as IAuthData)?.flows?.length) {
|
||||
this.busyChangedCallback?.(true);
|
||||
// use the existing sessionId, if one is present.
|
||||
const auth = this.data.session ? { session: this.data.session } : null;
|
||||
const auth = (this.data as IAuthData).session ? { session: (this.data as IAuthData).session } : null;
|
||||
this.doRequest(auth).finally(() => {
|
||||
this.busyChangedCallback?.(false);
|
||||
});
|
||||
@@ -290,7 +328,7 @@ export class InteractiveAuth {
|
||||
* be resolved.
|
||||
*/
|
||||
public async poll(): Promise<void> {
|
||||
if (!this.data.session) return;
|
||||
if (!(this.data as IAuthData).session) return;
|
||||
// likewise don't poll if there is no auth session in progress
|
||||
if (!this.attemptAuthDeferred) return;
|
||||
// if we currently have a request in flight, there's no point making
|
||||
@@ -330,7 +368,7 @@ export class InteractiveAuth {
|
||||
* @returns session id
|
||||
*/
|
||||
public getSessionId(): string | undefined {
|
||||
return this.data?.session;
|
||||
return (this.data as IAuthData)?.session;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,7 +388,7 @@ export class InteractiveAuth {
|
||||
* @returns any parameters from the server for this stage
|
||||
*/
|
||||
public getStageParams(loginType: string): Record<string, any> | undefined {
|
||||
return this.data.params?.[loginType];
|
||||
return (this.data as IAuthData)?.params?.[loginType];
|
||||
}
|
||||
|
||||
public getChosenFlow(): UIAFlow | null {
|
||||
@@ -391,9 +429,9 @@ export class InteractiveAuth {
|
||||
|
||||
// use the sessionid from the last request, if one is present.
|
||||
let auth: IAuthDict;
|
||||
if (this.data.session) {
|
||||
if ((this.data as IAuthData)?.session) {
|
||||
auth = {
|
||||
session: this.data.session,
|
||||
session: (this.data as IAuthData).session,
|
||||
};
|
||||
Object.assign(auth, authData);
|
||||
} else {
|
||||
@@ -451,7 +489,7 @@ export class InteractiveAuth {
|
||||
this.inputs.emailAddress!,
|
||||
this.clientSecret,
|
||||
this.emailAttempt++,
|
||||
this.data.session!,
|
||||
(this.data as IAuthData).session!,
|
||||
);
|
||||
this.emailSid = requestTokenResult.sid;
|
||||
logger.trace("Email token request succeeded");
|
||||
@@ -480,10 +518,12 @@ export class InteractiveAuth {
|
||||
this.attemptAuthDeferred!.resolve(result);
|
||||
this.attemptAuthDeferred = null;
|
||||
} catch (error) {
|
||||
const matrixError = error instanceof MatrixError ? error : null;
|
||||
|
||||
// sometimes UI auth errors don't come with flows
|
||||
const errorFlows = (<MatrixError>error).data?.flows ?? null;
|
||||
const haveFlows = this.data.flows || Boolean(errorFlows);
|
||||
if ((<MatrixError>error).httpStatus !== 401 || !(<MatrixError>error).data || !haveFlows) {
|
||||
const errorFlows = matrixError?.data?.flows ?? null;
|
||||
const haveFlows = (this.data as IAuthData)?.flows || Boolean(errorFlows);
|
||||
if (!matrixError || matrixError.httpStatus !== 401 || !matrixError.data || !haveFlows) {
|
||||
// doesn't look like an interactive-auth failure.
|
||||
if (!background) {
|
||||
this.attemptAuthDeferred?.reject(error);
|
||||
@@ -494,24 +534,22 @@ export class InteractiveAuth {
|
||||
logger.log("Background poll request failed doing UI auth: ignoring", error);
|
||||
}
|
||||
}
|
||||
if (!(<MatrixError>error).data) {
|
||||
(<MatrixError>error).data = {};
|
||||
if (matrixError && !matrixError.data) {
|
||||
matrixError.data = {};
|
||||
}
|
||||
// if the error didn't come with flows, completed flows or session ID,
|
||||
// copy over the ones we have. Synapse sometimes sends responses without
|
||||
// any UI auth data (eg. when polling for email validation, if the email
|
||||
// has not yet been validated). This appears to be a Synapse bug, which
|
||||
// we workaround here.
|
||||
if (
|
||||
!(<MatrixError>error).data.flows &&
|
||||
!(<MatrixError>error).data.completed &&
|
||||
!(<MatrixError>error).data.session
|
||||
) {
|
||||
(<MatrixError>error).data.flows = this.data.flows;
|
||||
(<MatrixError>error).data.completed = this.data.completed;
|
||||
(<MatrixError>error).data.session = this.data.session;
|
||||
if (matrixError && !matrixError.data.flows && !matrixError.data.completed && !matrixError.data.session) {
|
||||
matrixError.data.flows = (this.data as IAuthData).flows;
|
||||
matrixError.data.completed = (this.data as IAuthData).completed;
|
||||
matrixError.data.session = (this.data as IAuthData).session;
|
||||
}
|
||||
if (matrixError) {
|
||||
this.data = matrixError.data as IAuthData;
|
||||
}
|
||||
this.data = (<MatrixError>error).data;
|
||||
try {
|
||||
this.startNextAuthStage();
|
||||
} catch (e) {
|
||||
@@ -563,14 +601,6 @@ export class InteractiveAuth {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.data?.errcode || this.data?.error) {
|
||||
this.stateUpdatedCallback(nextStage, {
|
||||
errcode: this.data?.errcode || "",
|
||||
error: this.data?.error || "",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.stateUpdatedCallback(nextStage, nextStage === EMAIL_STAGE_TYPE ? { emailSid: this.emailSid } : {});
|
||||
}
|
||||
|
||||
@@ -618,7 +648,7 @@ export class InteractiveAuth {
|
||||
* @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found
|
||||
*/
|
||||
private chooseFlow(): UIAFlow {
|
||||
const flows = this.data.flows || [];
|
||||
const flows = (this.data as IAuthData)?.flows || [];
|
||||
|
||||
// we've been given an email or we've already done an email part
|
||||
const haveEmail = Boolean(this.inputs.emailAddress) || Boolean(this.emailSid);
|
||||
@@ -659,7 +689,7 @@ export class InteractiveAuth {
|
||||
* @returns login type
|
||||
*/
|
||||
private firstUncompletedStage(flow: UIAFlow): AuthType | string | undefined {
|
||||
const completed = this.data.completed || [];
|
||||
const completed = (this.data as IAuthData)?.completed || [];
|
||||
return flow.stages.find((stageType) => !completed.includes(stageType));
|
||||
}
|
||||
}
|
||||
|
||||
+29
-10
@@ -29,12 +29,13 @@ import {
|
||||
EventType,
|
||||
MsgType,
|
||||
RelationType,
|
||||
ToDeviceMessageId,
|
||||
UNSIGNED_THREAD_ID_FIELD,
|
||||
} from "../@types/event";
|
||||
import { Crypto } from "../crypto";
|
||||
import { deepSortedObjectEntries, internaliseString } from "../utils";
|
||||
import { RoomMember } from "./room-member";
|
||||
import { Thread, ThreadEvent, ThreadEventHandlerMap, THREAD_RELATION_TYPE } from "./thread";
|
||||
import { Thread, THREAD_RELATION_TYPE, ThreadEvent, ThreadEventHandlerMap } from "./thread";
|
||||
import { IActionsObject } from "../pushprocessor";
|
||||
import { TypedReEmitter } from "../ReEmitter";
|
||||
import { MatrixError } from "../http-api";
|
||||
@@ -62,10 +63,12 @@ export interface IContent {
|
||||
type StrippedState = Required<Pick<IEvent, "content" | "state_key" | "type" | "sender">>;
|
||||
|
||||
export interface IUnsigned {
|
||||
[key: string]: any;
|
||||
"age"?: number;
|
||||
"prev_sender"?: string;
|
||||
"prev_content"?: IContent;
|
||||
"redacted_because"?: IEvent;
|
||||
"replaces_state"?: string;
|
||||
"transaction_id"?: string;
|
||||
"invite_room_state"?: StrippedState[];
|
||||
"m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations
|
||||
@@ -517,16 +520,15 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* ```
|
||||
*/
|
||||
public getDetails(): string {
|
||||
let details = `id=${this.getId()} type=${this.getWireType()} sender=${this.getSender()}`;
|
||||
const room = this.getRoomId();
|
||||
if (room) {
|
||||
details += ` room=${room}`;
|
||||
// in-room event
|
||||
return `id=${this.getId()} type=${this.getWireType()} sender=${this.getSender()} room=${room} ts=${this.getDate()?.toISOString()}`;
|
||||
} else {
|
||||
// to-device event
|
||||
const msgid = this.getContent()[ToDeviceMessageId];
|
||||
return `msgid=${msgid} type=${this.getWireType()} sender=${this.getSender()}`;
|
||||
}
|
||||
const date = this.getDate();
|
||||
if (date) {
|
||||
details += ` ts=${date.toISOString()}`;
|
||||
}
|
||||
return details;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -576,6 +578,10 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* Get the event ID of the thread head
|
||||
*/
|
||||
public get threadRootId(): string | undefined {
|
||||
// don't allow state events to be threaded as per the spec
|
||||
if (this.isState()) {
|
||||
return undefined;
|
||||
}
|
||||
const relatesTo = this.getWireContent()?.["m.relates_to"];
|
||||
if (relatesTo?.rel_type === THREAD_RELATION_TYPE.name) {
|
||||
return relatesTo.event_id;
|
||||
@@ -597,6 +603,11 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* A helper to check if an event is a thread's head or not
|
||||
*/
|
||||
public get isThreadRoot(): boolean {
|
||||
// don't allow state events to be threaded as per the spec
|
||||
if (this.isState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const threadDetails = this.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||
|
||||
// Bundled relationships only returned when the sync response is limited
|
||||
@@ -1365,8 +1376,12 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
// Relation info is lifted out of the encrypted content when sent to
|
||||
// encrypted rooms, so we have to check `getWireContent` for this.
|
||||
const relation = this.getWireContent()?.["m.relates_to"];
|
||||
if (this.isState() && relation?.rel_type === RelationType.Replace) {
|
||||
// State events cannot be m.replace relations
|
||||
if (
|
||||
this.isState() &&
|
||||
!!relation?.rel_type &&
|
||||
([RelationType.Replace, RelationType.Thread] as string[]).includes(relation.rel_type)
|
||||
) {
|
||||
// State events cannot be m.replace or m.thread relations
|
||||
return false;
|
||||
}
|
||||
return !!(relation?.rel_type && relation.event_id && (relType ? relation.rel_type === relType : true));
|
||||
@@ -1618,6 +1633,10 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* @param thread - the thread
|
||||
*/
|
||||
public setThread(thread?: Thread): void {
|
||||
// don't allow state events to be threaded as per the spec
|
||||
if (this.isState()) {
|
||||
return;
|
||||
}
|
||||
if (this.thread) {
|
||||
this.reEmitter.stopReEmitting(this.thread, [ThreadEvent.Update]);
|
||||
}
|
||||
|
||||
+46
-9
@@ -98,6 +98,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
public readonly room: Room;
|
||||
public readonly client: MatrixClient;
|
||||
private readonly pendingEventOrdering: PendingEventOrdering;
|
||||
private processRootEventPromise?: Promise<void>;
|
||||
|
||||
public initialEventsFetched = !Thread.hasServerSideSupport;
|
||||
/**
|
||||
@@ -134,6 +135,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
this.room.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
|
||||
this.room.on(RoomEvent.Redaction, this.onRedaction);
|
||||
this.room.on(RoomEvent.LocalEchoUpdated, this.onLocalEcho);
|
||||
this.room.on(RoomEvent.TimelineReset, this.onTimelineReset);
|
||||
this.timelineSet.on(RoomEvent.Timeline, this.onTimelineEvent);
|
||||
|
||||
this.processReceipts(opts.receipts);
|
||||
@@ -144,6 +146,12 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
this.setEventMetadata(this.rootEvent);
|
||||
}
|
||||
|
||||
private onTimelineReset = async (): Promise<void> => {
|
||||
// We hit a gappy sync, ask the server for an update
|
||||
await this.processRootEventPromise;
|
||||
this.processRootEventPromise = undefined;
|
||||
};
|
||||
|
||||
private async fetchRootEvent(): Promise<void> {
|
||||
this.rootEvent = this.room.findEventById(this.id);
|
||||
// If the rootEvent does not exist in the local stores, then fetch it from the server.
|
||||
@@ -197,6 +205,11 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
this._currentUserParticipated = false;
|
||||
this.emit(ThreadEvent.Delete, this);
|
||||
} else {
|
||||
if (this.lastEvent?.getId() === event.getAssociatedId()) {
|
||||
// XXX: If our last event got redacted we query the server for the last event once again
|
||||
await this.processRootEventPromise;
|
||||
this.processRootEventPromise = undefined;
|
||||
}
|
||||
await this.updateThreadMetadata();
|
||||
}
|
||||
};
|
||||
@@ -212,6 +225,9 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
if (sender && room && this.shouldSendLocalEchoReceipt(sender, event)) {
|
||||
room.addLocalEchoReceipt(sender, event, ReceiptType.Read);
|
||||
}
|
||||
if (event.getId() !== this.id && event.isRelation(THREAD_RELATION_TYPE.name)) {
|
||||
this.replyCount++;
|
||||
}
|
||||
}
|
||||
this.onEcho(event, toStartOfTimeline ?? false);
|
||||
};
|
||||
@@ -245,6 +261,8 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
await this.updateThreadMetadata();
|
||||
if (!event.isRelation(THREAD_RELATION_TYPE.name)) return; // don't send a new reply event for reactions or edits
|
||||
if (toStartOfTimeline) return; // ignore messages added to the start of the timeline
|
||||
// Clear the lastEvent and instead start tracking locally using lastReply
|
||||
this.lastEvent = undefined;
|
||||
this.emit(ThreadEvent.NewReply, this, event);
|
||||
};
|
||||
|
||||
@@ -308,6 +326,11 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean, emit = true): Promise<void> {
|
||||
this.setEventMetadata(event);
|
||||
|
||||
if (!this.initialEventsFetched && !toStartOfTimeline && event.getId() === this.id) {
|
||||
// We're loading the thread organically
|
||||
this.initialEventsFetched = true;
|
||||
}
|
||||
|
||||
const lastReply = this.lastReply();
|
||||
const isNewestReply = !lastReply || event.localTimestamp >= lastReply!.localTimestamp;
|
||||
|
||||
@@ -351,10 +374,14 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
return;
|
||||
}
|
||||
|
||||
// If no thread support exists we want to count all thread relation
|
||||
// added as a reply. We can't rely on the bundled relationships count
|
||||
if ((!Thread.hasServerSideSupport || !this.rootEvent) && event.isRelation(THREAD_RELATION_TYPE.name)) {
|
||||
this.replyCount++;
|
||||
if (
|
||||
event.getId() !== this.id &&
|
||||
event.isRelation(THREAD_RELATION_TYPE.name) &&
|
||||
!toStartOfTimeline &&
|
||||
isNewestReply
|
||||
) {
|
||||
// Clear the last event as we have the latest end of the timeline
|
||||
this.lastEvent = undefined;
|
||||
}
|
||||
|
||||
if (emit) {
|
||||
@@ -475,18 +502,26 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
}
|
||||
}
|
||||
|
||||
private async updateThreadMetadata(): Promise<void> {
|
||||
this.updatePendingReplyCount();
|
||||
|
||||
private async updateThreadFromRootEvent(): Promise<void> {
|
||||
if (Thread.hasServerSideSupport) {
|
||||
// Ensure we show *something* as soon as possible, we'll update it as soon as we get better data, but we
|
||||
// don't want the thread preview to be empty if we can avoid it
|
||||
if (!this.initialEventsFetched) {
|
||||
if (!this.initialEventsFetched && !this.lastEvent) {
|
||||
await this.processRootEvent();
|
||||
}
|
||||
await this.fetchRootEvent();
|
||||
}
|
||||
await this.processRootEvent();
|
||||
}
|
||||
|
||||
private async updateThreadMetadata(): Promise<void> {
|
||||
this.updatePendingReplyCount();
|
||||
|
||||
if (!this.processRootEventPromise) {
|
||||
// We only want to do this once otherwise we end up rolling back to the last unsigned summary we have for the thread
|
||||
this.processRootEventPromise = this.updateThreadFromRootEvent();
|
||||
}
|
||||
await this.processRootEventPromise;
|
||||
|
||||
if (!this.initialEventsFetched) {
|
||||
this.initialEventsFetched = true;
|
||||
@@ -572,7 +607,9 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
/**
|
||||
* Return last reply to the thread, if known.
|
||||
*/
|
||||
public lastReply(matches: (ev: MatrixEvent) => boolean = (): boolean => true): MatrixEvent | null {
|
||||
public lastReply(
|
||||
matches: (ev: MatrixEvent) => boolean = (ev): boolean => ev.isRelation(RelationType.Thread),
|
||||
): MatrixEvent | null {
|
||||
for (let i = this.timeline.length - 1; i >= 0; i--) {
|
||||
const event = this.timeline[i];
|
||||
if (matches(event)) {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { subtleCrypto, TextEncoder } from "../crypto/crypto";
|
||||
import { logger } from "../logger";
|
||||
import { randomString } from "../randomstring";
|
||||
import { OidcError } from "./error";
|
||||
import { ValidatedIssuerConfig } from "./validate";
|
||||
import { validateIdToken, ValidatedIssuerConfig } from "./validate";
|
||||
|
||||
/**
|
||||
* Authorization parameters which are used in the authentication request of an OIDC auth code flow.
|
||||
@@ -173,11 +173,13 @@ export const completeAuthorizationCodeGrant = async (
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
delegatedAuthConfig,
|
||||
nonce,
|
||||
}: {
|
||||
clientId: string;
|
||||
codeVerifier: string;
|
||||
redirectUri: string;
|
||||
delegatedAuthConfig: IDelegatedAuthConfig & ValidatedIssuerConfig;
|
||||
nonce: string;
|
||||
},
|
||||
): Promise<BearerTokenResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
@@ -203,6 +205,8 @@ export const completeAuthorizationCodeGrant = async (
|
||||
const token = await response.json();
|
||||
|
||||
if (isValidBearerTokenResponse(token)) {
|
||||
// throws when token is invalid
|
||||
validateIdToken(token.id_token, delegatedAuthConfig.issuer, clientId, nonce);
|
||||
return normalizeBearerTokenResponseTokenType(token);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -23,5 +23,6 @@ export enum OidcError {
|
||||
DynamicRegistrationFailed = "Dynamic registration failed",
|
||||
DynamicRegistrationInvalid = "Dynamic registration invalid response",
|
||||
CodeExchangeFailed = "Failed to exchange code for token",
|
||||
InvalidBearerTokenResponse = "Invalid bearer token",
|
||||
InvalidBearerTokenResponse = "Invalid bearer token response",
|
||||
InvalidIdToken = "Invalid ID token",
|
||||
}
|
||||
|
||||
+84
-1
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import jwtDecode from "jwt-decode";
|
||||
|
||||
import { IClientWellKnown, IDelegatedAuthConfig, M_AUTHENTICATION } from "../client";
|
||||
import { logger } from "../logger";
|
||||
import { OidcError } from "./error";
|
||||
@@ -83,7 +85,7 @@ const requiredArrayValue = (wellKnown: Record<string, unknown>, key: string, val
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates issue `.well-known/openid-configuration`
|
||||
* Validates issuer `.well-known/openid-configuration`
|
||||
* As defined in RFC5785 https://openid.net/specs/openid-connect-discovery-1_0.html
|
||||
* validates that OP is compatible with Element's OIDC flow
|
||||
* @param wellKnown - json object
|
||||
@@ -116,3 +118,84 @@ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuer
|
||||
logger.error("Issuer configuration not valid");
|
||||
throw new Error(OidcError.OpSupport);
|
||||
};
|
||||
|
||||
/**
|
||||
* Standard JWT claims.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
|
||||
*/
|
||||
interface JwtClaims {
|
||||
[claim: string]: unknown;
|
||||
/** The "iss" (issuer) claim identifies the principal that issued the JWT. */
|
||||
iss?: string;
|
||||
/** The "sub" (subject) claim identifies the principal that is the subject of the JWT. */
|
||||
sub?: string;
|
||||
/** The "aud" (audience) claim identifies the recipients that the JWT is intended for. */
|
||||
aud?: string | string[];
|
||||
/** The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. */
|
||||
exp?: number;
|
||||
// unused claims excluded
|
||||
}
|
||||
interface IdTokenClaims extends JwtClaims {
|
||||
nonce?: string;
|
||||
}
|
||||
|
||||
const decodeIdToken = (token: string): IdTokenClaims => {
|
||||
try {
|
||||
return jwtDecode<IdTokenClaims>(token);
|
||||
} catch (error) {
|
||||
logger.error("Could not decode id_token", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate idToken
|
||||
* https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
||||
* @param idToken - id token from token endpoint
|
||||
* @param issuer - issuer for the OP as found during discovery
|
||||
* @param clientId - this client's id as registered with the OP
|
||||
* @param nonce - nonce used in the authentication request
|
||||
* @throws when id token is invalid
|
||||
*/
|
||||
export const validateIdToken = (idToken: string | undefined, issuer: string, clientId: string, nonce: string): void => {
|
||||
try {
|
||||
if (!idToken) {
|
||||
throw new Error("No ID token");
|
||||
}
|
||||
const claims = decodeIdToken(idToken);
|
||||
|
||||
// The Issuer Identifier for the OpenID Provider MUST exactly match the value of the iss (issuer) Claim.
|
||||
if (claims.iss !== issuer) {
|
||||
throw new Error("Invalid issuer");
|
||||
}
|
||||
/**
|
||||
* The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified by the iss (issuer) Claim as an audience.
|
||||
* The aud (audience) Claim MAY contain an array with more than one element.
|
||||
* The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences not trusted by the Client.
|
||||
* EW: Don't accept tokens with other untrusted audiences
|
||||
* */
|
||||
if (claims.aud !== clientId) {
|
||||
throw new Error("Invalid audience");
|
||||
}
|
||||
|
||||
/**
|
||||
* If a nonce value was sent in the Authentication Request, a nonce Claim MUST be present and its value checked
|
||||
* to verify that it is the same value as the one that was sent in the Authentication Request.
|
||||
*/
|
||||
if (claims.nonce !== nonce) {
|
||||
throw new Error("Invalid nonce");
|
||||
}
|
||||
|
||||
/**
|
||||
* The current time MUST be before the time represented by the exp Claim.
|
||||
* exp is an epoch timestamp in seconds
|
||||
* */
|
||||
if (!claims.exp || Date.now() > claims.exp * 1000) {
|
||||
throw new Error("Invalid expiry");
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Invalid ID token", error);
|
||||
throw new Error(OidcError.InvalidIdToken);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,7 +18,6 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
|
||||
import { RustCrypto } from "./rust-crypto";
|
||||
import { logger } from "../logger";
|
||||
import { RUST_SDK_STORE_PREFIX } from "./constants";
|
||||
import { IHttpOpts, MatrixHttpApi } from "../http-api";
|
||||
import { ServerSideSecretStorage } from "../secret-storage";
|
||||
import { ICryptoCallbacks } from "../crypto";
|
||||
@@ -32,6 +31,8 @@ import { ICryptoCallbacks } from "../crypto";
|
||||
* @param deviceId - The local user's Device ID.
|
||||
* @param secretStorage - Interface to server-side secret storage.
|
||||
* @param cryptoCallbacks - Crypto callbacks provided by the application
|
||||
* @param storePrefix - the prefix to use on the indexeddbs created by rust-crypto.
|
||||
* If unset, a memory store will be used.
|
||||
*/
|
||||
export async function initRustCrypto(
|
||||
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
@@ -39,6 +40,7 @@ export async function initRustCrypto(
|
||||
deviceId: string,
|
||||
secretStorage: ServerSideSecretStorage,
|
||||
cryptoCallbacks: ICryptoCallbacks,
|
||||
storePrefix: string | null,
|
||||
): Promise<RustCrypto> {
|
||||
// initialise the rust matrix-sdk-crypto-js, if it hasn't already been done
|
||||
await RustSdkCryptoJs.initAsync();
|
||||
@@ -51,7 +53,12 @@ export async function initRustCrypto(
|
||||
logger.info("Init OlmMachine");
|
||||
|
||||
// TODO: use the pickle key for the passphrase
|
||||
const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(u, d, RUST_SDK_STORE_PREFIX, "test pass");
|
||||
const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(
|
||||
u,
|
||||
d,
|
||||
storePrefix ?? undefined,
|
||||
(storePrefix && "test pass") ?? undefined,
|
||||
);
|
||||
const rustCrypto = new RustCrypto(olmMachine, http, userId, deviceId, secretStorage, cryptoCallbacks);
|
||||
await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) =>
|
||||
rustCrypto.onRoomKeysUpdated(sessions),
|
||||
|
||||
@@ -19,7 +19,7 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
||||
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
|
||||
import type { IEncryptedEventInfo } from "../crypto/api";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { IContent, MatrixEvent } from "../models/event";
|
||||
import { Room } from "../models/room";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
||||
@@ -32,15 +32,15 @@ import { KeyClaimManager } from "./KeyClaimManager";
|
||||
import { MapWithDefault } from "../utils";
|
||||
import {
|
||||
BootstrapCrossSigningOpts,
|
||||
CreateSecretStorageOpts,
|
||||
CrossSigningKey,
|
||||
CrossSigningStatus,
|
||||
CryptoCallbacks,
|
||||
DeviceVerificationStatus,
|
||||
GeneratedSecretStorageKey,
|
||||
ImportRoomKeyProgressData,
|
||||
ImportRoomKeysOpts,
|
||||
VerificationRequest,
|
||||
CreateSecretStorageOpts,
|
||||
CryptoCallbacks,
|
||||
} from "../crypto-api";
|
||||
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
|
||||
import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
|
||||
@@ -52,11 +52,14 @@ import { keyFromPassphrase } from "../crypto/key_passphrase";
|
||||
import { encodeRecoveryKey } from "../crypto/recoverykey";
|
||||
import { crypto } from "../crypto/crypto";
|
||||
import { RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification";
|
||||
import { EventType } from "../@types/event";
|
||||
import { CryptoEvent } from "../crypto";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
|
||||
/**
|
||||
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
||||
*/
|
||||
export class RustCrypto implements CryptoBackend {
|
||||
export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEventMap> implements CryptoBackend {
|
||||
public globalErrorOnUnknownDevices = false;
|
||||
private _trustCrossSignedDevices = true;
|
||||
|
||||
@@ -97,6 +100,7 @@ export class RustCrypto implements CryptoBackend {
|
||||
/** Crypto callbacks provided by the application */
|
||||
private readonly cryptoCallbacks: CryptoCallbacks,
|
||||
) {
|
||||
super();
|
||||
this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
|
||||
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
|
||||
this.eventDecryptor = new EventDecryptor(olmMachine);
|
||||
@@ -549,7 +553,14 @@ export class RustCrypto implements CryptoBackend {
|
||||
);
|
||||
return requests
|
||||
.filter((request) => request.roomId === undefined)
|
||||
.map((request) => new RustVerificationRequest(request, this.outgoingRequestProcessor));
|
||||
.map(
|
||||
(request) =>
|
||||
new RustVerificationRequest(
|
||||
request,
|
||||
this.outgoingRequestProcessor,
|
||||
this.supportedVerificationMethods,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -596,7 +607,7 @@ export class RustCrypto implements CryptoBackend {
|
||||
this.supportedVerificationMethods?.map(verificationMethodIdentifierToMethod),
|
||||
);
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest);
|
||||
return new RustVerificationRequest(request, this.outgoingRequestProcessor);
|
||||
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this.supportedVerificationMethods);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -626,7 +637,7 @@ export class RustCrypto implements CryptoBackend {
|
||||
this.supportedVerificationMethods?.map(verificationMethodIdentifierToMethod),
|
||||
);
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest);
|
||||
return new RustVerificationRequest(request, this.outgoingRequestProcessor);
|
||||
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this.supportedVerificationMethods);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -670,10 +681,18 @@ export class RustCrypto implements CryptoBackend {
|
||||
* @param events - the received to-device messages
|
||||
* @returns A list of preprocessed to-device messages.
|
||||
*/
|
||||
public preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> {
|
||||
public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> {
|
||||
// send the received to-device messages into receiveSyncChanges. We have no info on device-list changes,
|
||||
// one-time-keys, or fallback keys, so just pass empty data.
|
||||
return this.receiveSyncChanges({ events });
|
||||
const processed = await this.receiveSyncChanges({ events });
|
||||
|
||||
// look for interesting to-device messages
|
||||
for (const message of processed) {
|
||||
if (message.type === EventType.KeyVerificationRequest) {
|
||||
this.onIncomingKeyVerificationRequest(message.sender, message.content);
|
||||
}
|
||||
}
|
||||
return processed;
|
||||
}
|
||||
|
||||
/** called by the sync loop to process one time key counts and unused fallback keys
|
||||
@@ -751,6 +770,32 @@ export class RustCrypto implements CryptoBackend {
|
||||
this.outgoingRequestLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming m.key.verification request event
|
||||
*
|
||||
* @param sender - the sender of the event
|
||||
* @param content - the content of the event
|
||||
*/
|
||||
private onIncomingKeyVerificationRequest(sender: string, content: IContent): void {
|
||||
const transactionId = content.transaction_id;
|
||||
if (!transactionId || !sender) {
|
||||
// not a valid request: ignore
|
||||
return;
|
||||
}
|
||||
|
||||
const request: RustSdkCryptoJs.VerificationRequest | undefined = this.olmMachine.getVerificationRequest(
|
||||
new RustSdkCryptoJs.UserId(sender),
|
||||
transactionId,
|
||||
);
|
||||
|
||||
if (request) {
|
||||
this.emit(
|
||||
CryptoEvent.VerificationRequestReceived,
|
||||
new RustVerificationRequest(request, this.outgoingRequestProcessor, this.supportedVerificationMethods),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Other public functions
|
||||
@@ -939,3 +984,12 @@ class EventDecryptor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type RustCryptoEvents = CryptoEvent.VerificationRequestReceived;
|
||||
|
||||
type RustCryptoEventMap = {
|
||||
/**
|
||||
* Fires when a key verification request is received.
|
||||
*/
|
||||
[CryptoEvent.VerificationRequestReceived]: (request: VerificationRequest) => void;
|
||||
};
|
||||
|
||||
@@ -38,11 +38,25 @@ export class RustVerificationRequest
|
||||
extends TypedEventEmitter<VerificationRequestEvent, VerificationRequestEventHandlerMap>
|
||||
implements VerificationRequest
|
||||
{
|
||||
/** Are we in the process of sending an `m.key.verification.ready` event? */
|
||||
private _accepting = false;
|
||||
|
||||
/** Are we in the process of sending an `m.key.verification.cancellation` event? */
|
||||
private _cancelling = false;
|
||||
|
||||
private _verifier: Verifier | undefined;
|
||||
|
||||
/**
|
||||
* Construct a new RustVerificationRequest to wrap the rust-level `VerificationRequest`.
|
||||
*
|
||||
* @param inner - VerificationRequest from the Rust SDK
|
||||
* @param outgoingRequestProcessor - `OutgoingRequestProcessor` to use for making outgoing HTTP requests
|
||||
* @param supportedVerificationMethods - Verification methods to use when `accept()` is called
|
||||
*/
|
||||
public constructor(
|
||||
private readonly inner: RustSdkCryptoJs.VerificationRequest,
|
||||
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
|
||||
private readonly supportedVerificationMethods: string[] | undefined,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -113,7 +127,9 @@ export class RustVerificationRequest
|
||||
case RustSdkCryptoJs.VerificationRequestPhase.Requested:
|
||||
return VerificationPhase.Requested;
|
||||
case RustSdkCryptoJs.VerificationRequestPhase.Ready:
|
||||
return VerificationPhase.Ready;
|
||||
// if we're still sending the `m.key.verification.ready`, that counts as "Requested" in the js-sdk's
|
||||
// parlance.
|
||||
return this._accepting ? VerificationPhase.Requested : VerificationPhase.Ready;
|
||||
case RustSdkCryptoJs.VerificationRequestPhase.Transitioned:
|
||||
return VerificationPhase.Started;
|
||||
case RustSdkCryptoJs.VerificationRequestPhase.Done:
|
||||
@@ -129,7 +145,9 @@ export class RustVerificationRequest
|
||||
* (ie it is in phase `Requested`, `Ready` or `Started`).
|
||||
*/
|
||||
public get pending(): boolean {
|
||||
throw new Error("not implemented");
|
||||
if (this.inner.isPassive()) return false;
|
||||
const phase = this.phase;
|
||||
return phase !== VerificationPhase.Done && phase !== VerificationPhase.Cancelled;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,7 +155,7 @@ export class RustVerificationRequest
|
||||
* the remote echo which causes a transition to {@link VerificationPhase.Ready}.
|
||||
*/
|
||||
public get accepting(): boolean {
|
||||
throw new Error("not implemented");
|
||||
return this._accepting;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,7 +163,7 @@ export class RustVerificationRequest
|
||||
* the remote echo which causes a transition to {@link VerificationPhase.Cancelled}).
|
||||
*/
|
||||
public get declining(): boolean {
|
||||
throw new Error("not implemented");
|
||||
return this._cancelling;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,7 +172,7 @@ export class RustVerificationRequest
|
||||
* `null` indicates that there is no timeout
|
||||
*/
|
||||
public get timeout(): number | null {
|
||||
throw new Error("not implemented");
|
||||
return this.inner.timeRemainingMillis();
|
||||
}
|
||||
|
||||
/** once the phase is Started (and !initiatedByMe) or Ready: common methods supported by both sides */
|
||||
@@ -198,8 +216,28 @@ export class RustVerificationRequest
|
||||
*
|
||||
* @returns Promise which resolves when the event has been sent.
|
||||
*/
|
||||
public accept(): Promise<void> {
|
||||
throw new Error("not implemented");
|
||||
public async accept(): Promise<void> {
|
||||
if (this.inner.phase() !== RustSdkCryptoJs.VerificationRequestPhase.Requested || this._accepting) {
|
||||
throw new Error(`Cannot accept a verification request in phase ${this.phase}`);
|
||||
}
|
||||
|
||||
this._accepting = true;
|
||||
try {
|
||||
const req: undefined | OutgoingRequest =
|
||||
this.supportedVerificationMethods === undefined
|
||||
? this.inner.accept()
|
||||
: this.inner.acceptWithMethods(
|
||||
this.supportedVerificationMethods.map(verificationMethodIdentifierToMethod),
|
||||
);
|
||||
if (req) {
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
|
||||
}
|
||||
} finally {
|
||||
this._accepting = false;
|
||||
}
|
||||
|
||||
// phase may have changed, so emit a 'change' event
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,9 +249,19 @@ export class RustVerificationRequest
|
||||
* @returns Promise which resolves when the event has been sent.
|
||||
*/
|
||||
public async cancel(params?: { reason?: string; code?: string }): Promise<void> {
|
||||
const req: undefined | OutgoingRequest = this.inner.cancel();
|
||||
if (req) {
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
|
||||
if (this._cancelling) {
|
||||
// already cancelling; do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
this._cancelling = true;
|
||||
try {
|
||||
const req: undefined | OutgoingRequest = this.inner.cancel();
|
||||
if (req) {
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
|
||||
}
|
||||
} finally {
|
||||
this._cancelling = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +284,35 @@ export class RustVerificationRequest
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an `m.key.verification.start` event to start verification via a particular method.
|
||||
*
|
||||
* Implementation of {@link Crypto.VerificationRequest#startVerification}.
|
||||
*
|
||||
* @param method - the name of the verification method to use.
|
||||
*/
|
||||
public async startVerification(method: string): Promise<Verifier> {
|
||||
if (method !== "m.sas.v1") {
|
||||
throw new Error(`Unsupported verification method ${method}`);
|
||||
}
|
||||
|
||||
const res:
|
||||
| [RustSdkCryptoJs.Sas, RustSdkCryptoJs.RoomMessageRequest | RustSdkCryptoJs.ToDeviceRequest]
|
||||
| undefined = await this.inner.startSas();
|
||||
|
||||
if (res) {
|
||||
const [, req] = res;
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
|
||||
}
|
||||
|
||||
// this should have triggered the onChange callback, and we should now have a verifier
|
||||
if (!this._verifier) {
|
||||
throw new Error("Still no verifier after startSas() call");
|
||||
}
|
||||
|
||||
return this._verifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* The verifier which is doing the actual verification, once the method has been established.
|
||||
* Only defined when the `phase` is Started.
|
||||
|
||||
@@ -1091,15 +1091,15 @@
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@8.41.0":
|
||||
version "8.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.41.0.tgz#080321c3b68253522f7646b55b577dd99d2950b3"
|
||||
integrity sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==
|
||||
"@eslint/js@8.43.0":
|
||||
version "8.43.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.43.0.tgz#559ca3d9ddbd6bf907ad524320a0d14b85586af0"
|
||||
integrity sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==
|
||||
|
||||
"@humanwhocodes/config-array@^0.11.8":
|
||||
version "0.11.8"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9"
|
||||
integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==
|
||||
"@humanwhocodes/config-array@^0.11.10":
|
||||
version "0.11.10"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2"
|
||||
integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==
|
||||
dependencies:
|
||||
"@humanwhocodes/object-schema" "^1.2.1"
|
||||
debug "^4.1.1"
|
||||
@@ -1426,10 +1426,10 @@
|
||||
dependencies:
|
||||
lodash "^4.17.21"
|
||||
|
||||
"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.11":
|
||||
version "0.1.0-alpha.11"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.11.tgz#24d705318c3159ef7dbe43bca464ac2bdd11e45d"
|
||||
integrity sha512-HD3rskPkqrUUSaKzGLg97k/bN+OZrkcX7ODB/pNBs/jqq+/A0wDKqsszJotzFwsQcDPpWn78BmMyvBo4tLxKjw==
|
||||
"@matrix-org/matrix-sdk-crypto-js@^0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.1.tgz#aed6b7a3942bf9fc46234b7a572b9471a95da15f"
|
||||
integrity sha512-HsOOi/EnBb0Z+bkIxPuQQLW/9+Kijqkjqbf8xZq0S/fcHQJpIKxDJoZQDrVNpnEEWiIrBBLzVYTszXhMg+PYNw==
|
||||
|
||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
|
||||
version "3.2.14"
|
||||
@@ -1852,14 +1852,14 @@
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.45.0":
|
||||
version "5.59.11"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.11.tgz#8d466aa21abea4c3f37129997b198d141f09e76f"
|
||||
integrity sha512-XxuOfTkCUiOSyBWIvHlUraLw/JT/6Io1365RO6ZuI88STKMavJZPNMU0lFcUTeQXEhHiv64CbxYxBNoDVSmghg==
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.60.1.tgz#81382d6ecb92b8dda70e91f9035611cb2fecd1c3"
|
||||
integrity sha512-KSWsVvsJsLJv3c4e73y/Bzt7OpqMCADUO846bHcuWYSYM19bldbAeDv7dYyV0jwkbMfJ2XdlzwjhXtuD7OY6bw==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "5.59.11"
|
||||
"@typescript-eslint/type-utils" "5.59.11"
|
||||
"@typescript-eslint/utils" "5.59.11"
|
||||
"@typescript-eslint/scope-manager" "5.60.1"
|
||||
"@typescript-eslint/type-utils" "5.60.1"
|
||||
"@typescript-eslint/utils" "5.60.1"
|
||||
debug "^4.3.4"
|
||||
grapheme-splitter "^1.0.4"
|
||||
ignore "^5.2.0"
|
||||
@@ -1868,23 +1868,15 @@
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/parser@^5.45.0":
|
||||
version "5.59.11"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.11.tgz#af7d4b7110e3068ce0b97550736de455e4250103"
|
||||
integrity sha512-s9ZF3M+Nym6CAZEkJJeO2TFHHDsKAM3ecNkLuH4i4s8/RCPnF5JRip2GyviYkeEAcwGMJxkqG9h2dAsnA1nZpA==
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.60.1.tgz#0f2f58209c0862a73e3d5a56099abfdfa21d0fd3"
|
||||
integrity sha512-pHWlc3alg2oSMGwsU/Is8hbm3XFbcrb6P5wIxcQW9NsYBfnrubl/GhVVD/Jm/t8HXhA2WncoIRfBtnCgRGV96Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.59.11"
|
||||
"@typescript-eslint/types" "5.59.11"
|
||||
"@typescript-eslint/typescript-estree" "5.59.11"
|
||||
"@typescript-eslint/scope-manager" "5.60.1"
|
||||
"@typescript-eslint/types" "5.60.1"
|
||||
"@typescript-eslint/typescript-estree" "5.60.1"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.59.11":
|
||||
version "5.59.11"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.11.tgz#5d131a67a19189c42598af9fb2ea1165252001ce"
|
||||
integrity sha512-dHFOsxoLFtrIcSj5h0QoBT/89hxQONwmn3FOQ0GOQcLOOXm+MIrS8zEAhs4tWl5MraxCY3ZJpaXQQdFMc2Tu+Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.59.11"
|
||||
"@typescript-eslint/visitor-keys" "5.59.11"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.60.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.60.0.tgz#ae511967b4bd84f1d5e179bb2c82857334941c1c"
|
||||
@@ -1893,38 +1885,33 @@
|
||||
"@typescript-eslint/types" "5.60.0"
|
||||
"@typescript-eslint/visitor-keys" "5.60.0"
|
||||
|
||||
"@typescript-eslint/type-utils@5.59.11":
|
||||
version "5.59.11"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.11.tgz#5eb67121808a84cb57d65a15f48f5bdda25f2346"
|
||||
integrity sha512-LZqVY8hMiVRF2a7/swmkStMYSoXMFlzL6sXV6U/2gL5cwnLWQgLEG8tjWPpaE4rMIdZ6VKWwcffPlo1jPfk43g==
|
||||
"@typescript-eslint/scope-manager@5.60.1":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz#35abdb47f500c68c08f2f2b4f22c7c79472854bb"
|
||||
integrity sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "5.59.11"
|
||||
"@typescript-eslint/utils" "5.59.11"
|
||||
"@typescript-eslint/types" "5.60.1"
|
||||
"@typescript-eslint/visitor-keys" "5.60.1"
|
||||
|
||||
"@typescript-eslint/type-utils@5.60.1":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.60.1.tgz#17770540e98d65ab4730c7aac618003f702893f4"
|
||||
integrity sha512-vN6UztYqIu05nu7JqwQGzQKUJctzs3/Hg7E2Yx8rz9J+4LgtIDFWjjl1gm3pycH0P3mHAcEUBd23LVgfrsTR8A==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "5.60.1"
|
||||
"@typescript-eslint/utils" "5.60.1"
|
||||
debug "^4.3.4"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/types@5.59.11":
|
||||
version "5.59.11"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.11.tgz#1a9018fe3c565ba6969561f2a49f330cf1fe8db1"
|
||||
integrity sha512-epoN6R6tkvBYSc+cllrz+c2sOFWkbisJZWkOE+y3xHtvYaOE6Wk6B8e114McRJwFRjGvYdJwLXQH5c9osME/AA==
|
||||
|
||||
"@typescript-eslint/types@5.60.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.60.0.tgz#3179962b28b4790de70e2344465ec97582ce2558"
|
||||
integrity sha512-ascOuoCpNZBccFVNJRSC6rPq4EmJ2NkuoKnd6LDNyAQmdDnziAtxbCGWCbefG1CNzmDvd05zO36AmB7H8RzKPA==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.59.11":
|
||||
version "5.59.11"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.11.tgz#b2caaa31725e17c33970c1197bcd54e3c5f42b9f"
|
||||
integrity sha512-YupOpot5hJO0maupJXixi6l5ETdrITxeo5eBOeuV7RSKgYdU3G5cxO49/9WRnJq9EMrB7AuTSLH/bqOsXi7wPA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.59.11"
|
||||
"@typescript-eslint/visitor-keys" "5.59.11"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
"@typescript-eslint/types@5.60.1":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.60.1.tgz#a17473910f6b8d388ea83c9d7051af89c4eb7561"
|
||||
integrity sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.60.0":
|
||||
version "5.60.0"
|
||||
@@ -1939,17 +1926,30 @@
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/utils@5.59.11":
|
||||
version "5.59.11"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.11.tgz#9dbff49dc80bfdd9289f9f33548f2e8db3c59ba1"
|
||||
integrity sha512-didu2rHSOMUdJThLk4aZ1Or8IcO3HzCw/ZvEjTTIfjIrcdd5cvSIwwDy2AOlE7htSNp7QIZ10fLMyRCveesMLg==
|
||||
"@typescript-eslint/typescript-estree@5.60.1":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz#8c71824b7165b64d5ebd7aa42968899525959834"
|
||||
integrity sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.60.1"
|
||||
"@typescript-eslint/visitor-keys" "5.60.1"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/utils@5.60.1":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.60.1.tgz#6861ebedbefba1ac85482d2bdef6f2ff1eb65b80"
|
||||
integrity sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@types/json-schema" "^7.0.9"
|
||||
"@types/semver" "^7.3.12"
|
||||
"@typescript-eslint/scope-manager" "5.59.11"
|
||||
"@typescript-eslint/types" "5.59.11"
|
||||
"@typescript-eslint/typescript-estree" "5.59.11"
|
||||
"@typescript-eslint/scope-manager" "5.60.1"
|
||||
"@typescript-eslint/types" "5.60.1"
|
||||
"@typescript-eslint/typescript-estree" "5.60.1"
|
||||
eslint-scope "^5.1.1"
|
||||
semver "^7.3.7"
|
||||
|
||||
@@ -1967,14 +1967,6 @@
|
||||
eslint-scope "^5.1.1"
|
||||
semver "^7.3.7"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.59.11":
|
||||
version "5.59.11"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.11.tgz#dca561ddad169dc27d62396d64f45b2d2c3ecc56"
|
||||
integrity sha512-KGYniTGG3AMTuKF9QBD7EIrvufkB6O6uX3knP73xbKLMpH+QRPcgnCxjWXSHjMRuOxFLovljqQgQpR0c7GvjoA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.59.11"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.60.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.0.tgz#b48b29da3f5f31dd1656281727004589d2722a66"
|
||||
@@ -1983,6 +1975,14 @@
|
||||
"@typescript-eslint/types" "5.60.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.60.1":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz#19a877358bf96318ec35d90bfe6bd1445cce9434"
|
||||
integrity sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.60.1"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
JSONStream@^1.0.3:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
|
||||
@@ -3629,9 +3629,9 @@ eslint-plugin-jest@^27.1.6:
|
||||
"@typescript-eslint/utils" "^5.10.0"
|
||||
|
||||
eslint-plugin-jsdoc@^46.0.0:
|
||||
version "46.2.6"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.2.6.tgz#f25590d371859f20691d65b5dcd4cbe370d65564"
|
||||
integrity sha512-zIaK3zbSrKuH12bP+SPybPgcHSM6MFzh3HFeaODzmsF1N8C1l8dzJ22cW1aq4g0+nayU1VMjmNf7hg0dpShLrA==
|
||||
version "46.3.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.3.0.tgz#9e19138b108f7289d5a5d5bacdfb75242f5ebcb7"
|
||||
integrity sha512-nfSvsR8YJRZyKrWwcXPSQyQC8jllfdEjcRhTXFr7RxfB5Wyl7AxrfjCUz72WwalkXMF4u+R6F/oDoW46ah69HQ==
|
||||
dependencies:
|
||||
"@es-joy/jsdoccomment" "~0.39.4"
|
||||
are-docs-informative "^0.0.2"
|
||||
@@ -3709,16 +3709,16 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994"
|
||||
integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==
|
||||
|
||||
eslint@8.41.0:
|
||||
version "8.41.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.41.0.tgz#3062ca73363b4714b16dbc1e60f035e6134b6f1c"
|
||||
integrity sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==
|
||||
eslint@8.43.0:
|
||||
version "8.43.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.43.0.tgz#3e8c6066a57097adfd9d390b8fc93075f257a094"
|
||||
integrity sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@eslint-community/regexpp" "^4.4.0"
|
||||
"@eslint/eslintrc" "^2.0.3"
|
||||
"@eslint/js" "8.41.0"
|
||||
"@humanwhocodes/config-array" "^0.11.8"
|
||||
"@eslint/js" "8.43.0"
|
||||
"@humanwhocodes/config-array" "^0.11.10"
|
||||
"@humanwhocodes/module-importer" "^1.0.1"
|
||||
"@nodelib/fs.walk" "^1.2.8"
|
||||
ajv "^6.10.0"
|
||||
@@ -5393,6 +5393,11 @@ jstransformer@1.0.0:
|
||||
is-promise "^2.0.0"
|
||||
promise "^7.0.1"
|
||||
|
||||
jwt-decode@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59"
|
||||
integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==
|
||||
|
||||
kind-of@^3.0.2:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
|
||||
@@ -6810,14 +6815,14 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||
|
||||
semver@^7.3.5, semver@^7.3.8, semver@^7.5.1:
|
||||
semver@^7.3.5, semver@^7.3.8:
|
||||
version "7.5.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec"
|
||||
integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
semver@^7.3.7:
|
||||
semver@^7.3.7, semver@^7.5.1:
|
||||
version "7.5.3"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e"
|
||||
integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==
|
||||
|
||||
Reference in New Issue
Block a user