You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +03:00
Refactor key backup recovery to prepare for rust (#3708)
* Refactor key backup recovery to prepare for rust * code review * quick doc format * code review fix
This commit is contained in:
@ -229,6 +229,116 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
expect(event.getContent()).toEqual("testytest");
|
||||
});
|
||||
|
||||
describe("recover from backup", () => {
|
||||
oldBackendOnly("can restore from backup (Curve25519 version)", async function () {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
const fullBackup = {
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
sessions: {
|
||||
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
let onKeyCached: () => void;
|
||||
const awaitKeyCached = new Promise<void>((resolve) => {
|
||||
onKeyCached = resolve;
|
||||
});
|
||||
|
||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
{
|
||||
cacheCompleteCallback: () => onKeyCached(),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.imported).toStrictEqual(1);
|
||||
|
||||
await awaitKeyCached;
|
||||
});
|
||||
|
||||
oldBackendOnly("recover specific session from backup", async function () {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||
testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
);
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
ROOM_ID,
|
||||
testData.MEGOLM_SESSION_DATA.session_id,
|
||||
check!.backupInfo!,
|
||||
);
|
||||
|
||||
expect(result.imported).toStrictEqual(1);
|
||||
});
|
||||
|
||||
oldBackendOnly("Fails on bad recovery key", async function () {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
const fullBackup = {
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
sessions: {
|
||||
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
await expect(
|
||||
aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTx A7Xn aNFF k3jH zpV3 MQoN LJEg mscC HecF 982L wC77 mYQD",
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("backupLoop", () => {
|
||||
it("Alice should upload known keys when backup is enabled", async function () {
|
||||
// 404 means that there is no active backup
|
||||
|
@ -26,10 +26,15 @@ python -m venv env
|
||||
|
||||
import base64
|
||||
import json
|
||||
import base58
|
||||
|
||||
from canonicaljson import encode_canonical_json
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519, x25519
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||
from cryptography.hazmat.primitives import hashes, padding, hmac
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
|
||||
from random import randbytes, seed
|
||||
|
||||
ALICE_DATA = {
|
||||
@ -77,6 +82,7 @@ def main() -> None:
|
||||
import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
|
||||
import {{ IDownloadKeyResult }} from "../../../src";
|
||||
import {{ KeyBackupInfo }} from "../../../src/crypto-api";
|
||||
import {{ IKeyBackupSession }} from "../../../src/crypto/keybackup";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
@ -186,6 +192,10 @@ def build_test_data(user_data, prefix = "") -> str:
|
||||
}
|
||||
}
|
||||
|
||||
backed_up_room_key = encrypt_megolm_key_for_backup(additional_exported_room_key, backup_decryption_key.public_key())
|
||||
|
||||
backup_recovery_key = export_recovery_key(user_data["B64_BACKUP_DECRYPTION_KEY"])
|
||||
|
||||
return f"""\
|
||||
export const {prefix}TEST_USER_ID = "{user_data['TEST_USER_ID']}";
|
||||
export const {prefix}TEST_DEVICE_ID = "{user_data['TEST_DEVICE_ID']}";
|
||||
@ -220,9 +230,15 @@ export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult>
|
||||
json.dumps(build_cross_signing_keys_data(user_data), indent=4)
|
||||
};
|
||||
|
||||
/** Signed OTKs, returned by `POST /keys/claim` */
|
||||
export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) };
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE58 = "{ backup_recovery_key }";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
|
||||
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
|
||||
|
||||
@ -236,8 +252,8 @@ export const {prefix}MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
json.dumps(additional_exported_room_key, indent=4)
|
||||
};
|
||||
|
||||
/** Signed OTKs, returned by `POST /keys/claim` */
|
||||
export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) };
|
||||
/** The key from {prefix}MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/
|
||||
export const {prefix}CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = {json.dumps(backed_up_room_key, indent=4)};
|
||||
"""
|
||||
|
||||
|
||||
@ -367,6 +383,97 @@ def build_exported_megolm_key() -> dict:
|
||||
|
||||
return megolm_export
|
||||
|
||||
def encrypt_megolm_key_for_backup(session_data: dict, backup_public_key: x25519.X25519PublicKey) -> dict:
|
||||
|
||||
"""
|
||||
Encrypts an exported megolm key for key backup, using the m.megolm_backup.v1.curve25519-aes-sha2 algorithm.
|
||||
"""
|
||||
data = encode_canonical_json(session_data)
|
||||
|
||||
# Generate an ephemeral curve25519 key, and perform an ECDH with the ephemeral key
|
||||
# and the backup’s public key to generate a shared secret.
|
||||
# The public half of the ephemeral key, encoded using unpadded base64,
|
||||
# becomes the ephemeral property of the session_data.
|
||||
ephemeral_keypair = x25519.X25519PrivateKey.from_private_bytes(randbytes(32))
|
||||
shared_secret = ephemeral_keypair.exchange(backup_public_key)
|
||||
ephemeral = encode_base64(ephemeral_keypair.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw))
|
||||
|
||||
# Using the shared secret, generate 80 bytes by performing an HKDF using SHA-256 as the hash,
|
||||
# with a salt of 32 bytes of 0, and with the empty string as the info.
|
||||
# The first 32 bytes are used as the AES key, the next 32 bytes are used as the MAC key,
|
||||
# and the last 16 bytes are used as the AES initialization vector.
|
||||
salt = bytes(32)
|
||||
info = b""
|
||||
|
||||
hkdf = HKDF(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=80,
|
||||
salt=salt,
|
||||
info=info,
|
||||
)
|
||||
|
||||
raw_key = hkdf.derive(shared_secret)
|
||||
aes_key = raw_key[:32]
|
||||
mac = raw_key[32:64]
|
||||
iv = raw_key[64:80]
|
||||
|
||||
# Stringify the JSON object, and encrypt it using AES-CBC-256 with PKCS#7 padding.
|
||||
# This encrypted data, encoded using unpadded base64, becomes the ciphertext property of the session_data.
|
||||
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv))
|
||||
encryptor = cipher.encryptor()
|
||||
padder = padding.PKCS7(128).padder()
|
||||
padded_data = padder.update(data) + padder.finalize()
|
||||
ct = encryptor.update(padded_data) + encryptor.finalize()
|
||||
cipher_text = encode_base64(ct)
|
||||
|
||||
# Pass the raw encrypted data (prior to base64 encoding) through HMAC-SHA-256 using the MAC key generated above.
|
||||
# The first 8 bytes of the resulting MAC are base64-encoded, and become the mac property of the session_data.
|
||||
h = hmac.HMAC(mac, hashes.SHA256())
|
||||
# h.update(ct)
|
||||
signature = h.finalize()
|
||||
mac = encode_base64(signature[:8])
|
||||
|
||||
encrypted_key = {
|
||||
"first_message_index": 1,
|
||||
"forwarded_count": 0,
|
||||
"is_verified": False,
|
||||
"session_data": {
|
||||
"ciphertext": cipher_text,
|
||||
"ephemeral": ephemeral,
|
||||
"mac": mac
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return encrypted_key
|
||||
|
||||
def export_recovery_key(key_b64: str) -> str:
|
||||
"""
|
||||
Export a private recovery key as a recovery key that can be presented to users.
|
||||
As per spec https://spec.matrix.org/v1.8/client-server-api/#recovery-key
|
||||
"""
|
||||
private_key_bytes = base64.b64decode(key_b64)
|
||||
|
||||
# The 256-bit curve25519 private key is prepended by the bytes 0x8B and 0x01
|
||||
export_bytes = bytearray()
|
||||
export_bytes += b'\x8b'
|
||||
export_bytes += b'\x01'
|
||||
|
||||
export_bytes += private_key_bytes
|
||||
|
||||
# All the bytes in the string above, including the two header bytes,
|
||||
# are XORed together to form a parity byte. This parity byte is appended to the byte string.
|
||||
parity_byte = 0 #b'\x8b' ^ b'\x01'
|
||||
[parity_byte := parity_byte ^ x for x in export_bytes]
|
||||
|
||||
export_bytes += parity_byte.to_bytes(1, 'big')
|
||||
|
||||
# The byte string is encoded using base58
|
||||
recovery_key = base58.b58encode(export_bytes).decode('utf-8')
|
||||
|
||||
split = [recovery_key[i:i + 4] for i in range(0, len(recovery_key), 4)]
|
||||
return ' '.join(split)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
@ -6,6 +6,7 @@
|
||||
import { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto";
|
||||
import { IDownloadKeyResult } from "../../../src";
|
||||
import { KeyBackupInfo } from "../../../src/crypto-api";
|
||||
import { IKeyBackupSession } from "../../../src/crypto/keybackup";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
@ -102,9 +103,28 @@ export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
}
|
||||
};
|
||||
|
||||
/** 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE58 = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
@ -158,19 +178,15 @@ export const MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** The key from MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/
|
||||
export const CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = {
|
||||
"first_message_index": 1,
|
||||
"forwarded_count": 0,
|
||||
"is_verified": false,
|
||||
"session_data": {
|
||||
"ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d1BPGxhDl8AhPcBu3//KzlqinE6iAfrQYHmoOTes2uOsHgvbE7M9bIyAE1/FxzIgZuzmbJ1Fp/qjgKeTeonuMujLpnezUOYzDYqbF8hyLdm+SkrVWISde/wmVfpl1TI56ItCKmz0uB4fjUMWUV0sgp+3JLAUIqIqmwb7VVZooox94ze2VuspJMu9mF6MoQs5BEk3bFi9nbdgGwGh0pFYjEwE62+xLjo9x/gdVkIy8xlBmCFjBdvWjBjtvc8NfboWKxZ1tmpPCt6IiCTwOY4kyQdmK6U+sjh3tvs7qLOX5ETThOAqwKsY9VeXbeCtWF2h/m6ZXFQpxsf/vLgpTe0djDsGpWgLY+dywAelfz6O69EiHAxti9XxEGO0WLBDs0rmyhPN03IeLa+b8Gp2RMRdJ4prjKr0m7CVTUXEHbw6LtN0SWvEnc6iBI0vCEs1EocJAFfXsiwzAAgPDmaaDrCEk1dOoYj2eXoy4tbJx3+/FXFKe+RVdmiuv1nsVAQxPq78Tx6UtIz7ihHI0tzaZV+/7PUY065b6kevDBST+SZWERwqFZdz28TWsHu6Kxjr5k2N4sahs8a4frobip6C7PhXJFiq3n7lMx1IjfRrN0HxzpMqBzhkmiat9JR6nWtGjvOvvw",
|
||||
"ephemeral": "q+P1WdRtEiPIEtNuuGrRcueZxUbLnSKdsuTAkxewXgU",
|
||||
"mac": "OibmACbORhI"
|
||||
}
|
||||
};
|
||||
|
||||
@ -267,9 +283,28 @@ export const BOB_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
}
|
||||
};
|
||||
|
||||
/** 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE58 = "EsT5 Sd5m mEXs NQYE ibRe 3q9E 4aXW rHih 5f9J 6rU6 AfwY mASR";
|
||||
|
||||
/** 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",
|
||||
@ -290,10 +325,10 @@ 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/",
|
||||
"session_id": "/2K+V777vipCxPZ0gpY9qcpz1DYaXwuMRIu0UEP0Wa0",
|
||||
"session_key": "AQAAAAAclzWVMeWBKH+B/WMowa3rb4ma3jEl6n5W4GCs9ue65CruzD3ihX+85pZ9hsV9Bf6fvhjp76WNRajoJYX0UIt7aosjmu0i+H+07hEQ0zqTKpVoSH0ykJ6stAMhdr6Q4uW5crBmdTTBIsqmoWsNJZKKoE2+ldYrZ1lrFeaJbjBIY/9ivle++74qQsT2dIKWPanKc9Q2Gl8LjESLtFBD9Fmt",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "ZG6lrfATe+958wN1xaGf3dKG/CThEfkmNdp1jcu4zok"
|
||||
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
},
|
||||
@ -301,10 +336,10 @@ 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": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA",
|
||||
"session_key": "AQAAAACv0khqPrQ91MmWCgm0RTzfpn65AGCrRnAKLxGJdfSfECNZ8gyj34FZLwi+F+xC6ibFddcbLXW0mzR6PnTnHF3VHM4g/h+2rcxtlix8fySpIwFzaXViba7cOSy/b+dHTMZB40iA7F4y7AdTdHLv4N1XUj3puU/KVUIKf9/lEDLqyReD+39WdEY24mTIB5NcQQhtyguPzYPT5sSyeIUNd7Bw",
|
||||
"session_id": "+07YOpSgdZ1X9le3n3NMByw0V1B0H0Djnbm76jgmWoo",
|
||||
"session_key": "AQAAAAAjWfIMo9+BWS8IvhfsQuomxXXXGy11tJs0ej505xxd1RzOIP4ftq3MbZYsfH8kqSMBc2l1Ym2u3Dksv2/nR0zGQeNIgOxeMuwHU3Ry7+DdV1I96blPylVCCn/f5RAy6smKoaeylptPdXgVXmw3YBBUVYpHpm+xCIUUp9foAdb8hftO2DqUoHWdV/ZXt59zTAcsNFdQdB9A4525u+o4JlqK",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "HxUKnGfeUu0fF3cLyCFSDXYtVCQHy/+33q9RkzKfsiU"
|
||||
"ed25519": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
}
|
||||
@ -315,27 +350,23 @@ 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",
|
||||
"session_id": "gywydBrIJcJWktC/ic3tunKZM1XZm1MpYiYtdbj8Rpc",
|
||||
"session_key": "AQAAAADZJL7OdM/KHfPzXPZ3CtlLBIlzbwk06dnZTd3bvkcdP5u73rdmThBKdqGA4xzCyxZsHdYLZRrlmD3VwOmNfvWMqYdPxA1X0vs3d172y9EIG8i+N/skJxTRypcVSV9XoinBNIWr/gkyepuAKiQqemlc8J5amD9OkmbVkmnrxP1uyYMsMnQayCXCVpLQv4nN7bpymTNV2ZtTKWImLXW4/EaX",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "dV0TIhhkToXpL+gZLo+zXDHJfw7MWYxpg80cynIQDv0"
|
||||
"ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** The key from BOB_MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/
|
||||
export const BOB_CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = {
|
||||
"first_message_index": 1,
|
||||
"forwarded_count": 0,
|
||||
"is_verified": false,
|
||||
"session_data": {
|
||||
"ciphertext": "d7UVOK17WEVky/8hK0h3HsTQrFMEbKbfqMcl2KtyTWcI9S5gGFWK9Git5BzVRxRggvxQ0c8PDfqL+dr3zHytAMW+71BJqIPQW910vV7SX3JkR91kuuiDqlFT1VHLMP9/MNsvCfeZz1Q+0/LqkJo1KTbrrjKxqOq5/e7RQequOEXHu0a658fKaK/tk0eLvO2xUTxxBCgtuuLpeysOykf4jqp3yV89+h+h3o4zsWGyA2cLIBC/9ofqiqsoF+l1smzA+sQ3ArLBM5B2wc7b4Ir7jZUzCpnOK6O6nQA3vwOpwy3AqpeCKbsuXBPLkb9QXnVH9UzNbuL1Go7M6S3zH2BlAQrUhKSj4yI9tQC3XL3Y3IuAJRUX2bIXzYn9DryL+zwz9aULGN1ECZ4pMHm9jlPUYgMDYmSA2rRCZPWXcSeqmV8aB2qR3k+gcCFXNGbKy73bdN87GsPsYPI7RCkgtHGyW2tpz0JpP1CQmtP3wXNBKIJKIgcxGQ3YDehGV7XlFDDUYBZyYGazXqU/fdt21gc4Jvg1xVKsj83woVmGDo+2WzPrpIJ79NJUovC8jJ+UA3QamY28Df6UaRuS3+vvgCCW9e3T5+h5hR7BQayZ9r9Ykv7h7NTxQYjsYwxfZK+OULJPFNYbcB+vc6msLAsqocPhNQTazOe4TfsSAAuaHa/2FfGGMyaWCQY5DI3tk3Pagr87L8lBhwXll9usXKslvc+sJw",
|
||||
"ephemeral": "oO0VX84OUIzm2i/12zAhTWOZT5IFRH5mXaKZ8fXkCgU",
|
||||
"mac": "lEfHlqfJQwU"
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -3471,9 +3471,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
await this.cryptoBackend.deleteKeyBackupVersion(version);
|
||||
}
|
||||
|
||||
private makeKeyBackupPath(roomId: undefined, sessionId: undefined, version?: string): IKeyBackupPath;
|
||||
private makeKeyBackupPath(roomId: string, sessionId: undefined, version?: string): IKeyBackupPath;
|
||||
private makeKeyBackupPath(roomId: string, sessionId: string, version?: string): IKeyBackupPath;
|
||||
private makeKeyBackupPath(roomId?: string, sessionId?: string, version?: string): IKeyBackupPath {
|
||||
let path: string;
|
||||
if (sessionId !== undefined) {
|
||||
@ -3793,22 +3790,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
let totalKeyCount = 0;
|
||||
let keys: IMegolmSessionData[] = [];
|
||||
|
||||
const path = this.makeKeyBackupPath(targetRoomId!, targetSessionId!, backupInfo.version);
|
||||
const path = this.makeKeyBackupPath(targetRoomId, targetSessionId, backupInfo.version);
|
||||
|
||||
const algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => {
|
||||
return privKey;
|
||||
});
|
||||
const backupDecryptor = await this.cryptoBackend.getBackupDecryptor(backupInfo, privKey);
|
||||
|
||||
const untrusted = algorithm.untrusted;
|
||||
const untrusted = !backupDecryptor.sourceTrusted;
|
||||
|
||||
try {
|
||||
// If the pubkey computed from the private data we've been given
|
||||
// doesn't match the one in the auth_data, the user has entered
|
||||
// a different recovery key / the wrong passphrase.
|
||||
if (!(await algorithm.keyMatches(privKey))) {
|
||||
return Promise.reject(new MatrixError({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY }));
|
||||
}
|
||||
|
||||
if (!(privKey instanceof Uint8Array)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
throw new Error(`restoreKeyBackup expects Uint8Array, got ${privKey}`);
|
||||
@ -3842,7 +3830,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
if (!roomData.sessions) continue;
|
||||
|
||||
totalKeyCount += Object.keys(roomData.sessions).length;
|
||||
const roomKeys = await algorithm.decryptSessions(roomData.sessions);
|
||||
const roomKeys = await backupDecryptor.decryptSessions(roomData.sessions);
|
||||
for (const k of roomKeys) {
|
||||
k.room_id = roomId;
|
||||
keys.push(k);
|
||||
@ -3851,14 +3839,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
} else if ((res as IRoomKeysResponse).sessions) {
|
||||
const sessions = (res as IRoomKeysResponse).sessions;
|
||||
totalKeyCount = Object.keys(sessions).length;
|
||||
keys = await algorithm.decryptSessions(sessions);
|
||||
keys = await backupDecryptor.decryptSessions(sessions);
|
||||
for (const k of keys) {
|
||||
k.room_id = targetRoomId!;
|
||||
}
|
||||
} else {
|
||||
totalKeyCount = 1;
|
||||
try {
|
||||
const [key] = await algorithm.decryptSessions({
|
||||
const [key] = await backupDecryptor.decryptSessions({
|
||||
[targetSessionId!]: res as IKeyBackupSession,
|
||||
});
|
||||
key.room_id = targetRoomId!;
|
||||
@ -3869,7 +3857,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
algorithm.free();
|
||||
backupDecryptor.free();
|
||||
}
|
||||
|
||||
await this.cryptoBackend.importRoomKeys(keys, {
|
||||
@ -3878,7 +3866,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
source: "backup",
|
||||
});
|
||||
|
||||
await this.checkKeyBackup();
|
||||
/// in case entering the passphrase would add a new signature?
|
||||
await this.cryptoBackend.checkKeyBackupAndEnable();
|
||||
|
||||
return { total: totalKeyCount, imported: keys.length };
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ import { Room } from "../models/room";
|
||||
import { CryptoApi } from "../crypto-api";
|
||||
import { CrossSigningInfo, UserTrustLevel } from "../crypto/CrossSigning";
|
||||
import { IEncryptedEventInfo } from "../crypto/api";
|
||||
import { IKeyBackupInfo, IKeyBackupSession } from "../crypto/keybackup";
|
||||
import { IMegolmSessionData } from "../@types/crypto";
|
||||
|
||||
/**
|
||||
* Common interface for the crypto implementations
|
||||
@ -99,6 +101,13 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
||||
* @deprecated Unneeded for the new crypto
|
||||
*/
|
||||
checkOwnCrossSigningTrust(opts?: CheckOwnCrossSigningTrustOpts): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get a backup decryptor capable of decrypting megolm session data encrypted with the given backup information.
|
||||
* @param backupInfo - The backup information
|
||||
* @param privKey - The private decryption key.
|
||||
*/
|
||||
getBackupDecryptor(backupInfo: IKeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor>;
|
||||
}
|
||||
|
||||
/** The methods which crypto implementations should expose to the Sync api
|
||||
@ -213,3 +222,35 @@ export interface EventDecryptionResult {
|
||||
*/
|
||||
encryptedDisabledForUnverifiedDevices?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for decrypting megolm session data retrieved from a remote backup.
|
||||
* The result of {@link CryptoBackend#getBackupDecryptor}.
|
||||
*/
|
||||
export interface BackupDecryptor {
|
||||
/**
|
||||
* Whether keys retrieved from this backup can be trusted.
|
||||
*
|
||||
* Depending on the backup algorithm, keys retrieved from the backup can be trusted or not.
|
||||
* If false, keys retrieved from the backup must be considered unsafe (authenticity cannot be guaranteed).
|
||||
* It could be by design (deniability) or for some technical reason (eg asymmetric encryption).
|
||||
*/
|
||||
readonly sourceTrusted: boolean;
|
||||
|
||||
/**
|
||||
*
|
||||
* Decrypt megolm session data retrieved from backup.
|
||||
*
|
||||
* @param ciphertexts - a Record of sessionId to session data.
|
||||
*
|
||||
* @returns An array of decrypted `IMegolmSessionData`
|
||||
*/
|
||||
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]>;
|
||||
|
||||
/**
|
||||
* Free any resources held by this decryptor.
|
||||
*
|
||||
* Should be called once the decryptor is no longer needed.
|
||||
*/
|
||||
free(): void;
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ import { CryptoEvent } from "./index";
|
||||
import { crypto } from "./crypto";
|
||||
import { ClientPrefix, HTTPError, MatrixError, Method } from "../http-api";
|
||||
import { BackupTrustInfo } from "../crypto-api/keybackup";
|
||||
import { BackupDecryptor } from "../common-crypto/CryptoBackend";
|
||||
|
||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms
|
||||
@ -878,3 +879,32 @@ export function backupTrustInfoFromLegacyTrustInfo(trustInfo: TrustInfo): Backup
|
||||
matchesDecryptionKey: trustInfo.trusted_locally ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link BackupDecryptor} for the libolm crypto backend.
|
||||
*/
|
||||
export class LibOlmBackupDecryptor implements BackupDecryptor {
|
||||
private algorithm: BackupAlgorithm;
|
||||
public readonly sourceTrusted: boolean;
|
||||
|
||||
public constructor(algorithm: BackupAlgorithm) {
|
||||
this.algorithm = algorithm;
|
||||
this.sourceTrusted = !algorithm.untrusted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@link BackupDecryptor#free}
|
||||
*/
|
||||
public free(): void {
|
||||
this.algorithm.free();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@link BackupDecryptor#decryptSessions}
|
||||
*/
|
||||
public async decryptSessions(
|
||||
sessions: Record<string, IKeyBackupSession<Curve25519SessionData>>,
|
||||
): Promise<IMegolmSessionData[]> {
|
||||
return await this.algorithm.decryptSessions(sessions);
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ import { IllegalMethod } from "./verification/IllegalMethod";
|
||||
import { KeySignatureUploadError } from "../errors";
|
||||
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes";
|
||||
import { DehydrationManager } from "./dehydration";
|
||||
import { BackupManager, backupTrustInfoFromLegacyTrustInfo } from "./backup";
|
||||
import { BackupManager, LibOlmBackupDecryptor, backupTrustInfoFromLegacyTrustInfo } from "./backup";
|
||||
import { IStore } from "../store";
|
||||
import { Room, RoomEvent } from "../models/room";
|
||||
import { RoomMember, RoomMemberEvent } from "../models/room-member";
|
||||
@ -73,7 +73,7 @@ import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { IDeviceLists, ISyncResponse, IToDeviceEvent } from "../sync-accumulator";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
import { IMessage } from "./algorithms/olm";
|
||||
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
||||
import { BackupDecryptor, CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
||||
import { RoomState, RoomStateEvent } from "../models/room-state";
|
||||
import { MapWithDefault, recursiveMapToObject } from "../utils";
|
||||
import {
|
||||
@ -101,7 +101,7 @@ import {
|
||||
} from "../crypto-api";
|
||||
import { Device, DeviceMap } from "../models/device";
|
||||
import { deviceInfoToDevice } from "./device-converter";
|
||||
import { ClientPrefix, Method } from "../http-api";
|
||||
import { ClientPrefix, MatrixError, Method } from "../http-api";
|
||||
|
||||
/* re-exports for backwards compatibility */
|
||||
export type {
|
||||
@ -1845,6 +1845,28 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
// the backup with the new key (if not already signed)?
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoBackend#getBackupDecryptor}.
|
||||
*/
|
||||
public async getBackupDecryptor(backupInfo: IKeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor> {
|
||||
if (!(privKey instanceof Uint8Array)) {
|
||||
throw new Error(`getBackupDecryptor expects Uint8Array`);
|
||||
}
|
||||
|
||||
const algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => {
|
||||
return privKey;
|
||||
});
|
||||
|
||||
// If the pubkey computed from the private data we've been given
|
||||
// doesn't match the one in the auth_data, the user has entered
|
||||
// a different recovery key / the wrong passphrase.
|
||||
if (!(await algorithm.keyMatches(privKey))) {
|
||||
return Promise.reject(new MatrixError({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY }));
|
||||
}
|
||||
|
||||
return new LibOlmBackupDecryptor(algorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a set of keys as our own, trusted, cross-signing keys.
|
||||
*
|
||||
|
@ -23,7 +23,7 @@ import type { IEncryptedEventInfo } from "../crypto/api";
|
||||
import { IContent, MatrixEvent, MatrixEventEvent } from "../models/event";
|
||||
import { Room } from "../models/room";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
||||
import { BackupDecryptor, CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
||||
import { logger } from "../logger";
|
||||
import { IHttpOpts, MatrixHttpApi, Method } from "../http-api";
|
||||
import { RoomEncryptor } from "./RoomEncryptor";
|
||||
@ -1023,6 +1023,13 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
obj.signatures = Object.fromEntries(sigs.entries());
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoBackend#getBackupDecryptor}.
|
||||
*/
|
||||
public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor> {
|
||||
throw new Error("Stub not yet implemented");
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// SyncCryptoCallbacks implementation
|
||||
|
Reference in New Issue
Block a user