1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

ElementR: Add CryptoApi.requestVerificationDM (#3643)

* Add `CryptoApi.requestVerificationDM`

* Fix RoomMessageRequest url

* Review changes

* Merge fixes

* Add BOB test data

* `requestVerificationDM` test works against old crypto (encrypted verification request)

* Update test data
This commit is contained in:
Florian Duros
2023-08-21 16:48:32 +02:00
committed by GitHub
parent c18d691ef5
commit 6bf4ed8672
9 changed files with 480 additions and 78 deletions

View File

@ -22,7 +22,15 @@ import fetchMock from "fetch-mock-jest";
import { IDBFactory } from "fake-indexeddb"; import { IDBFactory } from "fake-indexeddb";
import { createHash } from "crypto"; import { createHash } from "crypto";
import { createClient, CryptoEvent, ICreateClientOpts, MatrixClient } from "../../../src"; import {
createClient,
CryptoEvent,
IContent,
ICreateClientOpts,
MatrixClient,
MatrixEvent,
MatrixEventEvent,
} from "../../../src";
import { import {
canAcceptVerificationRequest, canAcceptVerificationRequest,
ShowQrCodeCallbacks, ShowQrCodeCallbacks,
@ -34,15 +42,20 @@ import {
VerifierEvent, VerifierEvent,
} from "../../../src/crypto-api/verification"; } from "../../../src/crypto-api/verification";
import { escapeRegExp } from "../../../src/utils"; import { escapeRegExp } from "../../../src/utils";
import { CRYPTO_BACKENDS, emitPromise, InitCrypto } from "../../test-utils/test-utils"; import { CRYPTO_BACKENDS, emitPromise, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { SyncResponder } from "../../test-utils/SyncResponder"; import { SyncResponder } from "../../test-utils/SyncResponder";
import { import {
BOB_SIGNED_CROSS_SIGNING_KEYS_DATA,
BOB_SIGNED_TEST_DEVICE_DATA,
BOB_TEST_USER_ID,
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64, MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
SIGNED_CROSS_SIGNING_KEYS_DATA, SIGNED_CROSS_SIGNING_KEYS_DATA,
SIGNED_TEST_DEVICE_DATA, SIGNED_TEST_DEVICE_DATA,
TEST_DEVICE_ID, TEST_DEVICE_ID,
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64, TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
TEST_ROOM_ID,
TEST_USER_ID, TEST_USER_ID,
BOB_ONE_TIME_KEYS,
} from "../../test-utils/test-data"; } from "../../test-utils/test-data";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints"; import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder"; import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
@ -808,6 +821,82 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
}); });
}); });
describe("Send verification request in DM", () => {
beforeEach(async () => {
aliceClient = await startTestClient();
aliceClient.setGlobalErrorOnUnknownDevices(false);
e2eKeyResponder.addCrossSigningData(BOB_SIGNED_CROSS_SIGNING_KEYS_DATA);
e2eKeyResponder.addDeviceKeys(BOB_SIGNED_TEST_DEVICE_DATA);
syncResponder.sendOrQueueSyncResponse(getSyncResponse([BOB_TEST_USER_ID]));
// Wait for the sync response to be processed
await syncPromise(aliceClient);
});
/**
* Create a mock to respond when the verification request is sent
* Handle both encrypted and unencrypted requests
*/
function awaitRoomMessageRequest(): Promise<IContent> {
return new Promise((resolve) => {
// Case of unencrypted message of the new crypto
fetchMock.put(
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId",
(url: string, options: RequestInit) => {
resolve(JSON.parse(options.body as string));
return { event_id: "$YUwRidLecu:example.com" };
},
);
// Case of encrypted message of the old crypto
fetchMock.put(
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.encrypted/:txId",
async (url: string, options: RequestInit) => {
const encryptedMessage = JSON.parse(options.body as string);
const event = new MatrixEvent({
content: encryptedMessage,
type: "m.room.encrypted",
room_id: TEST_ROOM_ID,
});
// Try to decrypt the event
event.once(MatrixEventEvent.Decrypted, (decryptedEvent: MatrixEvent, error?: Error) => {
expect(error).not.toBeDefined();
resolve(decryptedEvent.getContent());
});
await aliceClient.decryptEventIfNeeded(event);
return { event_id: "$YUwRidLecu:example.com" };
},
);
});
}
it("alice sends a verification request in a DM to bob", async () => {
fetchMock.post("express:/_matrix/client/v3/keys/claim", () => ({ one_time_keys: BOB_ONE_TIME_KEYS }));
// In `DeviceList#doQueuedQueries`, the key download response is processed every 5ms
// 5ms by users, ie Bob and Alice
await jest.advanceTimersByTimeAsync(10);
const messageRequestPromise = awaitRoomMessageRequest();
const verificationRequest = await aliceClient
.getCrypto()!
.requestVerificationDM(BOB_TEST_USER_ID, TEST_ROOM_ID);
const requestContent = await messageRequestPromise;
expect(requestContent.from_device).toBe(aliceClient.getDeviceId());
expect(requestContent.methods.sort()).toStrictEqual(
["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"].sort(),
);
expect(requestContent.msgtype).toBe("m.key.verification.request");
expect(requestContent.to).toBe(BOB_TEST_USER_ID);
expect(verificationRequest.roomId).toBe(TEST_ROOM_ID);
expect(verificationRequest.isSelfVerification).toBe(false);
expect(verificationRequest.otherUserId).toBe(BOB_TEST_USER_ID);
});
});
async function startTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> { async function startTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
const client = createClient({ const client = createClient({
baseUrl: TEST_HOMESERVER_URL, baseUrl: TEST_HOMESERVER_URL,

View File

@ -32,73 +32,120 @@ from cryptography.hazmat.primitives.asymmetric import ed25519, x25519
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from random import randbytes, seed from random import randbytes, seed
# input data ALICE_DATA = {
TEST_USER_ID = "@alice:localhost" "TEST_USER_ID": "@alice:localhost",
TEST_DEVICE_ID = "test_device" "TEST_DEVICE_ID": "test_device",
TEST_ROOM_ID = "!room:id" "TEST_ROOM_ID": "!room:id",
# any 32-byte string can be an ed25519 private key. # any 32-byte string can be an ed25519 private key.
TEST_DEVICE_PRIVATE_KEY_BYTES = b"deadbeefdeadbeefdeadbeefdeadbeef" "TEST_DEVICE_PRIVATE_KEY_BYTES": b"deadbeefdeadbeefdeadbeefdeadbeef",
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"doyouspeakwhaaaaaaaaaaaaaaaaaale" "MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"doyouspeakwhaaaaaaaaaaaaaaaaaale",
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"useruseruseruseruseruseruseruser" "USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"useruseruseruseruseruseruseruser",
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"selfselfselfselfselfselfselfself" "SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"selfselfselfselfselfselfselfself",
# Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts # Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts
B64_BACKUP_DECRYPTION_KEY = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=" "B64_BACKUP_DECRYPTION_KEY": "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=",
"OTK": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw"
}
BOB_DATA = {
"TEST_USER_ID": "@bob:xyz",
"TEST_DEVICE_ID": "bob_device",
"TEST_ROOM_ID": "!room:id",
# any 32-byte string can be an ed25519 private key.
"TEST_DEVICE_PRIVATE_KEY_BYTES": b"Deadbeefdeadbeefdeadbeefdeadbeef",
"MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Doyouspeakwhaaaaaaaaaaaaaaaaaale",
"USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Useruseruseruseruseruseruseruser",
"SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Selfselfselfselfselfselfselfself",
# Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts
"B64_BACKUP_DECRYPTION_KEY": "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=",
"OTK": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw"
}
def main() -> None: def main() -> None:
private_key = ed25519.Ed25519PrivateKey.from_private_bytes( print(
TEST_DEVICE_PRIVATE_KEY_BYTES f"""\
/* Test data for cryptography tests
*
* Do not edit by hand! This file is generated by `./generate-test-data.py`
*/
import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
import {{ IDownloadKeyResult }} from "../../../src";
import {{ KeyBackupInfo }} from "../../../src/crypto-api";
/* eslint-disable comma-dangle */
// Alice data
{build_test_data(ALICE_DATA)}
// Bob data
{build_test_data(BOB_DATA, "BOB_")}
""",
end="",
) )
# Use static seed to have stable random test data upon new generation
seed(10)
def build_test_data(user_data, prefix = "") -> str:
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["TEST_DEVICE_PRIVATE_KEY_BYTES"]
)
b64_public_key = encode_base64( b64_public_key = encode_base64(
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
) )
device_data = { device_data = {
"algorithms": ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"], "algorithms": ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
"device_id": TEST_DEVICE_ID, "device_id": user_data["TEST_DEVICE_ID"],
"keys": { "keys": {
f"curve25519:{TEST_DEVICE_ID}": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0", f"curve25519:{user_data['TEST_DEVICE_ID']}": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
f"ed25519:{TEST_DEVICE_ID}": b64_public_key, f"ed25519:{user_data['TEST_DEVICE_ID']}": b64_public_key,
}, },
"signatures": {TEST_USER_ID: {}}, "signatures": {user_data['TEST_USER_ID']: {}},
"user_id": TEST_USER_ID, "user_id": user_data["TEST_USER_ID"],
} }
device_data["signatures"][TEST_USER_ID][f"ed25519:{TEST_DEVICE_ID}"] = sign_json( device_data["signatures"][user_data["TEST_USER_ID"]][f"ed25519:{user_data['TEST_DEVICE_ID']}"] = sign_json(
device_data, private_key device_data, private_key
) )
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes( master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
) )
b64_master_public_key = encode_base64( b64_master_public_key = encode_base64(
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
) )
b64_master_private_key = encode_base64(MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES) b64_master_private_key = encode_base64(user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"])
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes( self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
) )
b64_self_signing_public_key = encode_base64( b64_self_signing_public_key = encode_base64(
self_signing_private_key.public_key().public_bytes( self_signing_private_key.public_key().public_bytes(
Encoding.Raw, PublicFormat.Raw Encoding.Raw, PublicFormat.Raw
) )
) )
b64_self_signing_private_key = encode_base64(SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES) b64_self_signing_private_key = encode_base64( user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"])
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes( user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
) )
b64_user_signing_public_key = encode_base64( b64_user_signing_public_key = encode_base64(
user_signing_private_key.public_key().public_bytes( user_signing_private_key.public_key().public_bytes(
Encoding.Raw, PublicFormat.Raw Encoding.Raw, PublicFormat.Raw
) )
) )
b64_user_signing_private_key = encode_base64(USER_CROSS_SIGNING_PRIVATE_KEY_BYTES) b64_user_signing_private_key = encode_base64(user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"])
backup_decryption_key = x25519.X25519PrivateKey.from_private_bytes( backup_decryption_key = x25519.X25519PrivateKey.from_private_bytes(
base64.b64decode(B64_BACKUP_DECRYPTION_KEY) base64.b64decode(user_data["B64_BACKUP_DECRYPTION_KEY"])
) )
b64_backup_public_key = encode_base64( b64_backup_public_key = encode_base64(
backup_decryption_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) backup_decryption_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
@ -114,92 +161,96 @@ def main() -> None:
# sign with our device key # sign with our device key
sig = sign_json(backup_data["auth_data"], private_key) sig = sign_json(backup_data["auth_data"], private_key)
backup_data["auth_data"]["signatures"] = { backup_data["auth_data"]["signatures"] = {
TEST_USER_ID: {f"ed25519:{TEST_DEVICE_ID}": sig} user_data["TEST_USER_ID"]: {f"ed25519:{user_data['TEST_DEVICE_ID']}": sig}
} }
set_of_exported_room_keys = [build_exported_megolm_key(), build_exported_megolm_key()] set_of_exported_room_keys = [build_exported_megolm_key(), build_exported_megolm_key()]
additional_exported_room_key = build_exported_megolm_key() additional_exported_room_key = build_exported_megolm_key()
print( otk_to_sign = {
f"""\ "key": user_data['OTK']
/* Test data for cryptography tests }
* # sign our public otk key with our device key
* Do not edit by hand! This file is generated by `./generate-test-data.py` otk = sign_json(otk_to_sign, private_key)
*/ otks = {
user_data["TEST_USER_ID"]: {
user_data['TEST_DEVICE_ID']: {
"signed_curve25519:AAAAHQ": {
"key": user_data["OTK"],
"signatures": {
user_data["TEST_USER_ID"]: {f"ed25519:{user_data['TEST_DEVICE_ID']}": otk}
}
}
}
}
}
import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto"; return f"""\
import {{ IDownloadKeyResult }} from "../../../src"; export const {prefix}TEST_USER_ID = "{user_data['TEST_USER_ID']}";
import {{ KeyBackupInfo }} from "../../../src/crypto-api"; export const {prefix}TEST_DEVICE_ID = "{user_data['TEST_DEVICE_ID']}";
export const {prefix}TEST_ROOM_ID = "{user_data['TEST_ROOM_ID']}";
/* eslint-disable comma-dangle */
export const TEST_USER_ID = "{TEST_USER_ID}";
export const TEST_DEVICE_ID = "{TEST_DEVICE_ID}";
export const TEST_ROOM_ID = "{TEST_ROOM_ID}";
/** The base64-encoded public ed25519 key for this device */ /** The base64-encoded public ed25519 key for this device */
export const TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "{b64_public_key}"; export const {prefix}TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "{b64_public_key}";
/** Signed device data, suitable for returning from a `/keys/query` call */ /** Signed device data, suitable for returning from a `/keys/query` call */
export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {json.dumps(device_data, indent=4)}; export const {prefix}SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {json.dumps(device_data, indent=4)};
/** base64-encoded public master cross-signing key */ /** base64-encoded public master cross-signing key */
export const MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_master_public_key}"; export const {prefix}MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_master_public_key}";
/** base64-encoded private master cross-signing key */ /** base64-encoded private master cross-signing key */
export const MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_master_private_key}"; export const {prefix}MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_master_private_key}";
/** base64-encoded public self cross-signing key */ /** base64-encoded public self cross-signing key */
export const SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_self_signing_public_key}"; export const {prefix}SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_self_signing_public_key}";
/** base64-encoded private self signing cross-signing key */ /** base64-encoded private self signing cross-signing key */
export const SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_self_signing_private_key}"; export const {prefix}SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_self_signing_private_key}";
/** base64-encoded public user cross-signing key */ /** base64-encoded public user cross-signing key */
export const USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_user_signing_public_key}"; export const {prefix}USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_user_signing_public_key}";
/** base64-encoded private user signing cross-signing key */ /** base64-encoded private user signing cross-signing key */
export const USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_private_key}"; export const {prefix}USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_private_key}";
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */ /** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = { export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
json.dumps(build_cross_signing_keys_data(), indent=4) json.dumps(build_cross_signing_keys_data(user_data), indent=4)
}; };
/** base64-encoded backup decryption (private) key */ /** base64-encoded backup decryption (private) key */
export const BACKUP_DECRYPTION_KEY_BASE64 = "{ B64_BACKUP_DECRYPTION_KEY }"; export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }";
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */ /** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
export const SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) }; export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */ /** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = { export const {prefix}MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = {
json.dumps(set_of_exported_room_keys, indent=4) json.dumps(set_of_exported_room_keys, indent=4)
}; };
/** An exported megolm session */ /** An exported megolm session */
export const MEGOLM_SESSION_DATA: IMegolmSessionData = { export const {prefix}MEGOLM_SESSION_DATA: IMegolmSessionData = {
json.dumps(additional_exported_room_key, indent=4) json.dumps(additional_exported_room_key, indent=4)
}; };
""",
end="", /** Signed OTKs, returned by `POST /keys/claim` */
) export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) };
"""
# Use static seed to have stable random test data upon new generation def build_cross_signing_keys_data(user_data) -> dict:
seed(10)
def build_cross_signing_keys_data() -> dict:
"""Build the signed cross-signing-keys data for return from /keys/query""" """Build the signed cross-signing-keys data for return from /keys/query"""
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes( master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
) )
b64_master_public_key = encode_base64( b64_master_public_key = encode_base64(
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
) )
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes( self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
) )
b64_self_signing_public_key = encode_base64( b64_self_signing_public_key = encode_base64(
self_signing_private_key.public_key().public_bytes( self_signing_private_key.public_key().public_bytes(
@ -207,7 +258,7 @@ def build_cross_signing_keys_data() -> dict:
) )
) )
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes( user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
) )
b64_user_signing_public_key = encode_base64( b64_user_signing_public_key = encode_base64(
user_signing_private_key.public_key().public_bytes( user_signing_private_key.public_key().public_bytes(
@ -217,39 +268,39 @@ def build_cross_signing_keys_data() -> dict:
# create without signatures initially # create without signatures initially
cross_signing_keys_data = { cross_signing_keys_data = {
"master_keys": { "master_keys": {
TEST_USER_ID: { user_data["TEST_USER_ID"]: {
"keys": { "keys": {
f"ed25519:{b64_master_public_key}": b64_master_public_key, f"ed25519:{b64_master_public_key}": b64_master_public_key,
}, },
"user_id": TEST_USER_ID, "user_id": user_data["TEST_USER_ID"],
"usage": ["master"], "usage": ["master"],
} }
}, },
"self_signing_keys": { "self_signing_keys": {
TEST_USER_ID: { user_data["TEST_USER_ID"]: {
"keys": { "keys": {
f"ed25519:{b64_self_signing_public_key}": b64_self_signing_public_key, f"ed25519:{b64_self_signing_public_key}": b64_self_signing_public_key,
}, },
"user_id": TEST_USER_ID, "user_id": user_data["TEST_USER_ID"],
"usage": ["self_signing"], "usage": ["self_signing"],
}, },
}, },
"user_signing_keys": { "user_signing_keys": {
TEST_USER_ID: { user_data["TEST_USER_ID"]: {
"keys": { "keys": {
f"ed25519:{b64_user_signing_public_key}": b64_user_signing_public_key, f"ed25519:{b64_user_signing_public_key}": b64_user_signing_public_key,
}, },
"user_id": TEST_USER_ID, "user_id": user_data["TEST_USER_ID"],
"usage": ["user_signing"], "usage": ["user_signing"],
}, },
}, },
} }
# sign the sub-keys with the master # sign the sub-keys with the master
for k in ["self_signing_keys", "user_signing_keys"]: for k in ["self_signing_keys", "user_signing_keys"]:
to_sign = cross_signing_keys_data[k][TEST_USER_ID] to_sign = cross_signing_keys_data[k][user_data["TEST_USER_ID"]]
sig = sign_json(to_sign, master_private_key) sig = sign_json(to_sign, master_private_key)
to_sign["signatures"] = { to_sign["signatures"] = {
TEST_USER_ID: {f"ed25519:{b64_master_public_key}": sig} user_data["TEST_USER_ID"]: {f"ed25519:{b64_master_public_key}": sig}
} }
return cross_signing_keys_data return cross_signing_keys_data

View File

@ -9,6 +9,8 @@ import { KeyBackupInfo } from "../../../src/crypto-api";
/* eslint-disable comma-dangle */ /* eslint-disable comma-dangle */
// Alice data
export const TEST_USER_ID = "@alice:localhost"; export const TEST_USER_ID = "@alice:localhost";
export const TEST_DEVICE_ID = "test_device"; export const TEST_DEVICE_ID = "test_device";
export const TEST_ROOM_ID = "!room:id"; export const TEST_ROOM_ID = "!room:id";
@ -155,3 +157,185 @@ export const MEGOLM_SESSION_DATA: IMegolmSessionData = {
}, },
"forwarding_curve25519_key_chain": [] "forwarding_curve25519_key_chain": []
}; };
/** Signed OTKs, returned by `POST /keys/claim` */
export const ONE_TIME_KEYS = {
"@alice:localhost": {
"test_device": {
"signed_curve25519:AAAAHQ": {
"key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw",
"signatures": {
"@alice:localhost": {
"ed25519:test_device": "25djC6Rk6gIgFBMVawY9X9LnY8XMMziey6lKqL8Q5Bbp7T1vw9uk0RE7eKO2a/jNLcYroO2xRztGhBrKz5sOCQ"
}
}
}
}
}
};
// Bob data
export const BOB_TEST_USER_ID = "@bob:xyz";
export const BOB_TEST_DEVICE_ID = "bob_device";
export const BOB_TEST_ROOM_ID = "!room:id";
/** The base64-encoded public ed25519 key for this device */
export const BOB_TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "jmY0h8QS6Te6gxyjOmMc0eKOqmbAtXpVo4CCWFubk50";
/** Signed device data, suitable for returning from a `/keys/query` call */
export const BOB_SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "bob_device",
"keys": {
"curve25519:bob_device": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
"ed25519:bob_device": "jmY0h8QS6Te6gxyjOmMc0eKOqmbAtXpVo4CCWFubk50"
},
"user_id": "@bob:xyz",
"signatures": {
"@bob:xyz": {
"ed25519:bob_device": "4ApBs9jaeGyfdYaWRUdBvQAkDyXjACJ9KJ0xLHMgiFT/1yo6VqPTx2iziKGnrBiGhbtKNxEhDPOvZZkBU73cDQ"
}
}
};
/** base64-encoded public master cross-signing key */
export const BOB_MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA";
/** base64-encoded private master cross-signing key */
export const BOB_MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "RG95b3VzcGVha3doYWFhYWFhYWFhYWFhYWFhYWFhbGU";
/** base64-encoded public self cross-signing key */
export const BOB_SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A";
/** base64-encoded private self signing cross-signing key */
export const BOB_SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "U2VsZnNlbGZzZWxmc2VsZnNlbGZzZWxmc2VsZnNlbGY";
/** base64-encoded public user cross-signing key */
export const BOB_USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw";
/** base64-encoded private user signing cross-signing key */
export const BOB_USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "VXNlcnVzZXJ1c2VydXNlcnVzZXJ1c2VydXNlcnVzZXI";
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const BOB_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
"master_keys": {
"@bob:xyz": {
"keys": {
"ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA"
},
"user_id": "@bob:xyz",
"usage": [
"master"
]
}
},
"self_signing_keys": {
"@bob:xyz": {
"keys": {
"ed25519:DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A": "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A"
},
"user_id": "@bob:xyz",
"usage": [
"self_signing"
],
"signatures": {
"@bob:xyz": {
"ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "RxM8iJU6ZkyzQSVtNnXIJMPyEahVsN+fQQTBNKAs+kqySFyXBgchx+8czZaAhJCpXh9gD1nskT4yyFd2eyUXBw"
}
}
}
},
"user_signing_keys": {
"@bob:xyz": {
"keys": {
"ed25519:lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw": "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw"
},
"user_id": "@bob:xyz",
"usage": [
"user_signing"
],
"signatures": {
"@bob:xyz": {
"ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "jF8fvnPZulrPyh/4E8dNDVBP3iHHl9bRc+rRArVyGzoom+uVrokOck7BN2YmPyCRFZJJx7fgRA1Bveyu+mTVAg"
}
}
}
}
};
/** base64-encoded backup decryption (private) key */
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = {
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
"version": "1",
"auth_data": {
"public_key": "ZRuVWcWlDuvOwZRygccUCD4Avtnt130800I+WQNwwRY",
"signatures": {
"@bob:xyz": {
"ed25519:bob_device": "lDIMj3VC0WazE2FamGHpmbiqKf9Z4pO4qapZ5TL5BnD3c+dvb+2waOEd6pgay/pmrQ6MW4Eu2KDEpe1fnHc3BA"
}
}
}
};
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
{
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!roomA:example.org",
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
"session_id": "X9SK5JceUdvUr9gZkmdfS78qmKztoL60oORJ9JA5XD8",
"session_key": "AQAAAADbfOdUj/ec5bK4xVEhhRw+jfd1FD4uA0MLy7NyVHugZxyXNZUx5YEof4H9YyjBretviZreMSXqflbgYKz257rkKu7MPeKFf7zmln2GxX0F/p++GOnvpY1FqOglhfRQi3tqiyOa7SL4f7TuERDTOpMqlWhIfTKQnqy0AyF2vpDi5V/UiuSXHlHb1K/YGZJnX0u/Kpis7aC+tKDkSfSQOVw/",
"sender_claimed_keys": {
"ed25519": "ZG6lrfATe+958wN1xaGf3dKG/CThEfkmNdp1jcu4zok"
},
"forwarding_curve25519_key_chain": []
},
{
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!roomA:example.org",
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
"session_id": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA",
"session_key": "AQAAAACv0khqPrQ91MmWCgm0RTzfpn65AGCrRnAKLxGJdfSfECNZ8gyj34FZLwi+F+xC6ibFddcbLXW0mzR6PnTnHF3VHM4g/h+2rcxtlix8fySpIwFzaXViba7cOSy/b+dHTMZB40iA7F4y7AdTdHLv4N1XUj3puU/KVUIKf9/lEDLqyReD+39WdEY24mTIB5NcQQhtyguPzYPT5sSyeIUNd7Bw",
"sender_claimed_keys": {
"ed25519": "HxUKnGfeUu0fF3cLyCFSDXYtVCQHy/+33q9RkzKfsiU"
},
"forwarding_curve25519_key_chain": []
}
];
/** An exported megolm session */
export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!roomA:example.org",
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
"session_id": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM",
"session_key": "AQAAAACvcoGk7mOY59fOqZaxFUiTCBRV1Ia94KBjAZx6kgdgBtkkvs50z8od8/Nc9ncK2UsEiXNvCTTp2dlN3du+Rx0/m7vet2ZOEEp2oYDjHMLLFmwd1gtlGuWYPdXA6Y1+9Yyph0/EDVfS+zd3XvbL0QgbyL43+yQnFNHKlxVJX1eiKTrGTHQtYEOZz6/i/bbk+sV7GhSZFT5IMT9hXsRxdf0D",
"sender_claimed_keys": {
"ed25519": "dV0TIhhkToXpL+gZLo+zXDHJfw7MWYxpg80cynIQDv0"
},
"forwarding_curve25519_key_chain": []
};
/** Signed OTKs, returned by `POST /keys/claim` */
export const BOB_ONE_TIME_KEYS = {
"@bob:xyz": {
"bob_device": {
"signed_curve25519:AAAAHQ": {
"key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw",
"signatures": {
"@bob:xyz": {
"ed25519:bob_device": "dlZc9VA/hP980Mxvu9qwi0qJx8VK7sADGOM48CE01YM7K/Mbty9lis/QjtQAWqDg371QyynVRjEzt9qj7eSFCg"
}
}
}
}
}
};

View File

@ -161,7 +161,7 @@ describe("OutgoingRequestProcessor", () => {
.when("PUT", "/_matrix") .when("PUT", "/_matrix")
.check((req) => { .check((req) => {
expect(req.path).toEqual( expect(req.path).toEqual(
"https://example.com/_matrix/client/v3/room/test%2Froom/send/test%2Ftype/test%2Ftxnid", "https://example.com/_matrix/client/v3/rooms/test%2Froom/send/test%2Ftype/test%2Ftxnid",
); );
expect(req.rawData).toEqual(testBody); expect(req.rawData).toEqual(testBody);
expect(req.headers["Accept"]).toEqual("application/json"); expect(req.headers["Accept"]).toEqual("application/json");

View File

@ -560,6 +560,15 @@ describe("RustCrypto", () => {
); );
}); });
}); });
describe("requestVerificationDM", () => {
it("send verification request to an unknown user", async () => {
const rustCrypto = await makeTestRustCrypto();
await expect(() =>
rustCrypto.requestVerificationDM("@bob:example.com", testData.TEST_ROOM_ID),
).rejects.toThrow("unknown userId @bob:example.com");
});
});
}); });
/** build a basic RustCrypto instance for testing /** build a basic RustCrypto instance for testing

View File

@ -2444,6 +2444,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* *
* @returns resolves to a VerificationRequest * @returns resolves to a VerificationRequest
* when the request has been sent to the other party. * when the request has been sent to the other party.
*
* @deprecated Prefer {@link CryptoApi.requestVerificationDM}.
*/ */
public requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> { public requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
if (!this.crypto) { if (!this.crypto) {

View File

@ -281,6 +281,16 @@ export interface CryptoApi {
*/ */
findVerificationRequestDMInProgress(roomId: string, userId?: string): VerificationRequest | undefined; findVerificationRequestDMInProgress(roomId: string, userId?: string): VerificationRequest | undefined;
/**
* Request a key verification from another user, using a DM.
*
* @param userId - the user to request verification with.
* @param roomId - the room to use for verification.
*
* @returns resolves to a VerificationRequest when the request has been sent to the other party.
*/
requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest>;
/** /**
* Send a verification request to our other devices. * Send a verification request to our other devices.
* *

View File

@ -88,7 +88,7 @@ export class OutgoingRequestProcessor {
resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body); resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body);
} else if (msg instanceof RoomMessageRequest) { } else if (msg instanceof RoomMessageRequest) {
const path = const path =
`/_matrix/client/v3/room/${encodeURIComponent(msg.room_id)}/send/` + `/_matrix/client/v3/rooms/${encodeURIComponent(msg.room_id)}/send/` +
`${encodeURIComponent(msg.event_type)}/${encodeURIComponent(msg.txn_id)}`; `${encodeURIComponent(msg.event_type)}/${encodeURIComponent(msg.txn_id)}`;
resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body); resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body);
} else if (msg instanceof SigningKeysUploadRequest) { } else if (msg instanceof SigningKeysUploadRequest) {

View File

@ -61,6 +61,7 @@ import { CryptoEvent } from "../crypto";
import { TypedEventEmitter } from "../models/typed-event-emitter"; import { TypedEventEmitter } from "../models/typed-event-emitter";
import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } from "./backup"; import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } from "./backup";
import { TypedReEmitter } from "../ReEmitter"; import { TypedReEmitter } from "../ReEmitter";
import { randomString } from "../randomstring";
const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"]; const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"];
@ -714,6 +715,62 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
} }
} }
/**
* Implementation of {@link CryptoApi#requestVerificationDM}
*/
public async requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
const userIdentity: RustSdkCryptoJs.UserIdentity | undefined = await this.olmMachine.getIdentity(
new RustSdkCryptoJs.UserId(userId),
);
if (!userIdentity) throw new Error(`unknown userId ${userId}`);
// Transform the verification methods into rust objects
const methods = this._supportedVerificationMethods.map((method) =>
verificationMethodIdentifierToMethod(method),
);
// Get the request content to send to the DM room
const verificationEventContent: string = await userIdentity.verificationRequestContent(methods);
// Send the request content to send to the DM room
const eventId = await this.sendVerificationRequestContent(roomId, verificationEventContent);
// Get a verification request
const request: RustSdkCryptoJs.VerificationRequest = await userIdentity.requestVerification(
new RustSdkCryptoJs.RoomId(roomId),
new RustSdkCryptoJs.EventId(eventId),
methods,
);
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this._supportedVerificationMethods);
}
/**
* Send the verification content to a room
* See https://spec.matrix.org/v1.7/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
*
* Prefer to use {@link OutgoingRequestProcessor.makeOutgoingRequest} when dealing with {@link RustSdkCryptoJs.RoomMessageRequest}
*
* @param roomId - the targeted room
* @param verificationEventContent - the request body.
*
* @returns the event id
*/
private async sendVerificationRequestContent(roomId: string, verificationEventContent: string): Promise<string> {
const txId = randomString(32);
// Send the verification request content to the DM room
const { event_id: eventId } = await this.http.authedRequest<{ event_id: string }>(
Method.Put,
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${encodeURIComponent(txId)}`,
undefined,
verificationEventContent,
{
prefix: "",
},
);
return eventId;
}
/** /**
* The verification methods we offer to the other side during an interactive verification. * The verification methods we offer to the other side during an interactive verification.
*/ */