1
0
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:
Valere
2023-09-12 13:19:35 +02:00
committed by GitHub
parent f963ca5562
commit c7827d971c
8 changed files with 398 additions and 61 deletions

View File

@ -229,6 +229,116 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
expect(event.getContent()).toEqual("testytest"); 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", () => { describe("backupLoop", () => {
it("Alice should upload known keys when backup is enabled", async function () { it("Alice should upload known keys when backup is enabled", async function () {
// 404 means that there is no active backup // 404 means that there is no active backup

View File

@ -26,10 +26,15 @@ python -m venv env
import base64 import base64
import json import json
import base58
from canonicaljson import encode_canonical_json from canonicaljson import encode_canonical_json
from cryptography.hazmat.primitives.asymmetric import ed25519, x25519 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 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 from random import randbytes, seed
ALICE_DATA = { ALICE_DATA = {
@ -77,6 +82,7 @@ def main() -> None:
import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto"; import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
import {{ IDownloadKeyResult }} from "../../../src"; import {{ IDownloadKeyResult }} from "../../../src";
import {{ KeyBackupInfo }} from "../../../src/crypto-api"; import {{ KeyBackupInfo }} from "../../../src/crypto-api";
import {{ IKeyBackupSession }} from "../../../src/crypto/keybackup";
/* eslint-disable comma-dangle */ /* 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"""\ return f"""\
export const {prefix}TEST_USER_ID = "{user_data['TEST_USER_ID']}"; export const {prefix}TEST_USER_ID = "{user_data['TEST_USER_ID']}";
export const {prefix}TEST_DEVICE_ID = "{user_data['TEST_DEVICE_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) 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 */ /** base64-encoded backup decryption (private) key */
export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_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}}` */ /** 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) }; 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) json.dumps(additional_exported_room_key, indent=4)
}; };
/** Signed OTKs, returned by `POST /keys/claim` */ /** The key from {prefix}MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/
export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) }; 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 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 backups 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__": if __name__ == "__main__":
main() main()

View File

@ -6,6 +6,7 @@
import { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto"; import { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto";
import { IDownloadKeyResult } from "../../../src"; import { IDownloadKeyResult } from "../../../src";
import { KeyBackupInfo } from "../../../src/crypto-api"; import { KeyBackupInfo } from "../../../src/crypto-api";
import { IKeyBackupSession } from "../../../src/crypto/keybackup";
/* eslint-disable comma-dangle */ /* 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 */ /** base64-encoded backup decryption (private) key */
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo="; 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}` */ /** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
export const SIGNED_BACKUP_DATA: KeyBackupInfo = { export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
@ -158,19 +178,15 @@ export const MEGOLM_SESSION_DATA: IMegolmSessionData = {
"forwarding_curve25519_key_chain": [] "forwarding_curve25519_key_chain": []
}; };
/** Signed OTKs, returned by `POST /keys/claim` */ /** The key from MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/
export const ONE_TIME_KEYS = { export const CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = {
"@alice:localhost": { "first_message_index": 1,
"test_device": { "forwarded_count": 0,
"signed_curve25519:AAAAHQ": { "is_verified": false,
"key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw", "session_data": {
"signatures": { "ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d1BPGxhDl8AhPcBu3//KzlqinE6iAfrQYHmoOTes2uOsHgvbE7M9bIyAE1/FxzIgZuzmbJ1Fp/qjgKeTeonuMujLpnezUOYzDYqbF8hyLdm+SkrVWISde/wmVfpl1TI56ItCKmz0uB4fjUMWUV0sgp+3JLAUIqIqmwb7VVZooox94ze2VuspJMu9mF6MoQs5BEk3bFi9nbdgGwGh0pFYjEwE62+xLjo9x/gdVkIy8xlBmCFjBdvWjBjtvc8NfboWKxZ1tmpPCt6IiCTwOY4kyQdmK6U+sjh3tvs7qLOX5ETThOAqwKsY9VeXbeCtWF2h/m6ZXFQpxsf/vLgpTe0djDsGpWgLY+dywAelfz6O69EiHAxti9XxEGO0WLBDs0rmyhPN03IeLa+b8Gp2RMRdJ4prjKr0m7CVTUXEHbw6LtN0SWvEnc6iBI0vCEs1EocJAFfXsiwzAAgPDmaaDrCEk1dOoYj2eXoy4tbJx3+/FXFKe+RVdmiuv1nsVAQxPq78Tx6UtIz7ihHI0tzaZV+/7PUY065b6kevDBST+SZWERwqFZdz28TWsHu6Kxjr5k2N4sahs8a4frobip6C7PhXJFiq3n7lMx1IjfRrN0HxzpMqBzhkmiat9JR6nWtGjvOvvw",
"@alice:localhost": { "ephemeral": "q+P1WdRtEiPIEtNuuGrRcueZxUbLnSKdsuTAkxewXgU",
"ed25519:test_device": "25djC6Rk6gIgFBMVawY9X9LnY8XMMziey6lKqL8Q5Bbp7T1vw9uk0RE7eKO2a/jNLcYroO2xRztGhBrKz5sOCQ" "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 */ /** base64-encoded backup decryption (private) key */
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo="; 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}` */ /** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = { export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = {
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", "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", "algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!roomA:example.org", "room_id": "!roomA:example.org",
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo", "sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
"session_id": "X9SK5JceUdvUr9gZkmdfS78qmKztoL60oORJ9JA5XD8", "session_id": "/2K+V777vipCxPZ0gpY9qcpz1DYaXwuMRIu0UEP0Wa0",
"session_key": "AQAAAADbfOdUj/ec5bK4xVEhhRw+jfd1FD4uA0MLy7NyVHugZxyXNZUx5YEof4H9YyjBretviZreMSXqflbgYKz257rkKu7MPeKFf7zmln2GxX0F/p++GOnvpY1FqOglhfRQi3tqiyOa7SL4f7TuERDTOpMqlWhIfTKQnqy0AyF2vpDi5V/UiuSXHlHb1K/YGZJnX0u/Kpis7aC+tKDkSfSQOVw/", "session_key": "AQAAAAAclzWVMeWBKH+B/WMowa3rb4ma3jEl6n5W4GCs9ue65CruzD3ihX+85pZ9hsV9Bf6fvhjp76WNRajoJYX0UIt7aosjmu0i+H+07hEQ0zqTKpVoSH0ykJ6stAMhdr6Q4uW5crBmdTTBIsqmoWsNJZKKoE2+ldYrZ1lrFeaJbjBIY/9ivle++74qQsT2dIKWPanKc9Q2Gl8LjESLtFBD9Fmt",
"sender_claimed_keys": { "sender_claimed_keys": {
"ed25519": "ZG6lrfATe+958wN1xaGf3dKG/CThEfkmNdp1jcu4zok" "ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
}, },
"forwarding_curve25519_key_chain": [] "forwarding_curve25519_key_chain": []
}, },
@ -301,10 +336,10 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
"algorithm": "m.megolm.v1.aes-sha2", "algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!roomA:example.org", "room_id": "!roomA:example.org",
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo", "sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
"session_id": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA", "session_id": "+07YOpSgdZ1X9le3n3NMByw0V1B0H0Djnbm76jgmWoo",
"session_key": "AQAAAACv0khqPrQ91MmWCgm0RTzfpn65AGCrRnAKLxGJdfSfECNZ8gyj34FZLwi+F+xC6ibFddcbLXW0mzR6PnTnHF3VHM4g/h+2rcxtlix8fySpIwFzaXViba7cOSy/b+dHTMZB40iA7F4y7AdTdHLv4N1XUj3puU/KVUIKf9/lEDLqyReD+39WdEY24mTIB5NcQQhtyguPzYPT5sSyeIUNd7Bw", "session_key": "AQAAAAAjWfIMo9+BWS8IvhfsQuomxXXXGy11tJs0ej505xxd1RzOIP4ftq3MbZYsfH8kqSMBc2l1Ym2u3Dksv2/nR0zGQeNIgOxeMuwHU3Ry7+DdV1I96blPylVCCn/f5RAy6smKoaeylptPdXgVXmw3YBBUVYpHpm+xCIUUp9foAdb8hftO2DqUoHWdV/ZXt59zTAcsNFdQdB9A4525u+o4JlqK",
"sender_claimed_keys": { "sender_claimed_keys": {
"ed25519": "HxUKnGfeUu0fF3cLyCFSDXYtVCQHy/+33q9RkzKfsiU" "ed25519": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM"
}, },
"forwarding_curve25519_key_chain": [] "forwarding_curve25519_key_chain": []
} }
@ -315,27 +350,23 @@ export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = {
"algorithm": "m.megolm.v1.aes-sha2", "algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!roomA:example.org", "room_id": "!roomA:example.org",
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo", "sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
"session_id": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM", "session_id": "gywydBrIJcJWktC/ic3tunKZM1XZm1MpYiYtdbj8Rpc",
"session_key": "AQAAAACvcoGk7mOY59fOqZaxFUiTCBRV1Ia94KBjAZx6kgdgBtkkvs50z8od8/Nc9ncK2UsEiXNvCTTp2dlN3du+Rx0/m7vet2ZOEEp2oYDjHMLLFmwd1gtlGuWYPdXA6Y1+9Yyph0/EDVfS+zd3XvbL0QgbyL43+yQnFNHKlxVJX1eiKTrGTHQtYEOZz6/i/bbk+sV7GhSZFT5IMT9hXsRxdf0D", "session_key": "AQAAAADZJL7OdM/KHfPzXPZ3CtlLBIlzbwk06dnZTd3bvkcdP5u73rdmThBKdqGA4xzCyxZsHdYLZRrlmD3VwOmNfvWMqYdPxA1X0vs3d172y9EIG8i+N/skJxTRypcVSV9XoinBNIWr/gkyepuAKiQqemlc8J5amD9OkmbVkmnrxP1uyYMsMnQayCXCVpLQv4nN7bpymTNV2ZtTKWImLXW4/EaX",
"sender_claimed_keys": { "sender_claimed_keys": {
"ed25519": "dV0TIhhkToXpL+gZLo+zXDHJfw7MWYxpg80cynIQDv0" "ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64"
}, },
"forwarding_curve25519_key_chain": [] "forwarding_curve25519_key_chain": []
}; };
/** Signed OTKs, returned by `POST /keys/claim` */ /** The key from BOB_MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/
export const BOB_ONE_TIME_KEYS = { export const BOB_CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = {
"@bob:xyz": { "first_message_index": 1,
"bob_device": { "forwarded_count": 0,
"signed_curve25519:AAAAHQ": { "is_verified": false,
"key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw", "session_data": {
"signatures": { "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",
"@bob:xyz": { "ephemeral": "oO0VX84OUIzm2i/12zAhTWOZT5IFRH5mXaKZ8fXkCgU",
"ed25519:bob_device": "dlZc9VA/hP980Mxvu9qwi0qJx8VK7sADGOM48CE01YM7K/Mbty9lis/QjtQAWqDg371QyynVRjEzt9qj7eSFCg" "mac": "lEfHlqfJQwU"
}
}
}
}
} }
}; };

View File

@ -3471,9 +3471,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
await this.cryptoBackend.deleteKeyBackupVersion(version); 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 { private makeKeyBackupPath(roomId?: string, sessionId?: string, version?: string): IKeyBackupPath {
let path: string; let path: string;
if (sessionId !== undefined) { if (sessionId !== undefined) {
@ -3793,22 +3790,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
let totalKeyCount = 0; let totalKeyCount = 0;
let keys: IMegolmSessionData[] = []; 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 () => { const backupDecryptor = await this.cryptoBackend.getBackupDecryptor(backupInfo, privKey);
return privKey;
});
const untrusted = algorithm.untrusted; const untrusted = !backupDecryptor.sourceTrusted;
try { 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)) { if (!(privKey instanceof Uint8Array)) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string // eslint-disable-next-line @typescript-eslint/no-base-to-string
throw new Error(`restoreKeyBackup expects Uint8Array, got ${privKey}`); throw new Error(`restoreKeyBackup expects Uint8Array, got ${privKey}`);
@ -3842,7 +3830,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (!roomData.sessions) continue; if (!roomData.sessions) continue;
totalKeyCount += Object.keys(roomData.sessions).length; 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) { for (const k of roomKeys) {
k.room_id = roomId; k.room_id = roomId;
keys.push(k); keys.push(k);
@ -3851,14 +3839,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} else if ((res as IRoomKeysResponse).sessions) { } else if ((res as IRoomKeysResponse).sessions) {
const sessions = (res as IRoomKeysResponse).sessions; const sessions = (res as IRoomKeysResponse).sessions;
totalKeyCount = Object.keys(sessions).length; totalKeyCount = Object.keys(sessions).length;
keys = await algorithm.decryptSessions(sessions); keys = await backupDecryptor.decryptSessions(sessions);
for (const k of keys) { for (const k of keys) {
k.room_id = targetRoomId!; k.room_id = targetRoomId!;
} }
} else { } else {
totalKeyCount = 1; totalKeyCount = 1;
try { try {
const [key] = await algorithm.decryptSessions({ const [key] = await backupDecryptor.decryptSessions({
[targetSessionId!]: res as IKeyBackupSession, [targetSessionId!]: res as IKeyBackupSession,
}); });
key.room_id = targetRoomId!; key.room_id = targetRoomId!;
@ -3869,7 +3857,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} }
} }
} finally { } finally {
algorithm.free(); backupDecryptor.free();
} }
await this.cryptoBackend.importRoomKeys(keys, { await this.cryptoBackend.importRoomKeys(keys, {
@ -3878,7 +3866,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
source: "backup", 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 }; return { total: totalKeyCount, imported: keys.length };
} }

View File

@ -20,6 +20,8 @@ import { Room } from "../models/room";
import { CryptoApi } from "../crypto-api"; import { CryptoApi } from "../crypto-api";
import { CrossSigningInfo, UserTrustLevel } from "../crypto/CrossSigning"; import { CrossSigningInfo, UserTrustLevel } from "../crypto/CrossSigning";
import { IEncryptedEventInfo } from "../crypto/api"; import { IEncryptedEventInfo } from "../crypto/api";
import { IKeyBackupInfo, IKeyBackupSession } from "../crypto/keybackup";
import { IMegolmSessionData } from "../@types/crypto";
/** /**
* Common interface for the crypto implementations * Common interface for the crypto implementations
@ -99,6 +101,13 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
* @deprecated Unneeded for the new crypto * @deprecated Unneeded for the new crypto
*/ */
checkOwnCrossSigningTrust(opts?: CheckOwnCrossSigningTrustOpts): Promise<void>; 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 /** The methods which crypto implementations should expose to the Sync api
@ -213,3 +222,35 @@ export interface EventDecryptionResult {
*/ */
encryptedDisabledForUnverifiedDevices?: boolean; 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;
}

View File

@ -41,6 +41,7 @@ import { CryptoEvent } from "./index";
import { crypto } from "./crypto"; import { crypto } from "./crypto";
import { ClientPrefix, HTTPError, MatrixError, Method } from "../http-api"; import { ClientPrefix, HTTPError, MatrixError, Method } from "../http-api";
import { BackupTrustInfo } from "../crypto-api/keybackup"; import { BackupTrustInfo } from "../crypto-api/keybackup";
import { BackupDecryptor } from "../common-crypto/CryptoBackend";
const KEY_BACKUP_KEYS_PER_REQUEST = 200; const KEY_BACKUP_KEYS_PER_REQUEST = 200;
const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms
@ -878,3 +879,32 @@ export function backupTrustInfoFromLegacyTrustInfo(trustInfo: TrustInfo): Backup
matchesDecryptionKey: trustInfo.trusted_locally ?? false, 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);
}
}

View File

@ -50,7 +50,7 @@ import { IllegalMethod } from "./verification/IllegalMethod";
import { KeySignatureUploadError } from "../errors"; import { KeySignatureUploadError } from "../errors";
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes"; import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes";
import { DehydrationManager } from "./dehydration"; import { DehydrationManager } from "./dehydration";
import { BackupManager, backupTrustInfoFromLegacyTrustInfo } from "./backup"; import { BackupManager, LibOlmBackupDecryptor, backupTrustInfoFromLegacyTrustInfo } from "./backup";
import { IStore } from "../store"; import { IStore } from "../store";
import { Room, RoomEvent } from "../models/room"; import { Room, RoomEvent } from "../models/room";
import { RoomMember, RoomMemberEvent } from "../models/room-member"; 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 { IDeviceLists, ISyncResponse, IToDeviceEvent } from "../sync-accumulator";
import { ISignatures } from "../@types/signed"; import { ISignatures } from "../@types/signed";
import { IMessage } from "./algorithms/olm"; 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 { RoomState, RoomStateEvent } from "../models/room-state";
import { MapWithDefault, recursiveMapToObject } from "../utils"; import { MapWithDefault, recursiveMapToObject } from "../utils";
import { import {
@ -101,7 +101,7 @@ import {
} from "../crypto-api"; } from "../crypto-api";
import { Device, DeviceMap } from "../models/device"; import { Device, DeviceMap } from "../models/device";
import { deviceInfoToDevice } from "./device-converter"; import { deviceInfoToDevice } from "./device-converter";
import { ClientPrefix, Method } from "../http-api"; import { ClientPrefix, MatrixError, Method } from "../http-api";
/* re-exports for backwards compatibility */ /* re-exports for backwards compatibility */
export type { export type {
@ -1845,6 +1845,28 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// the backup with the new key (if not already signed)? // 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. * Store a set of keys as our own, trusted, cross-signing keys.
* *

View File

@ -23,7 +23,7 @@ import type { IEncryptedEventInfo } from "../crypto/api";
import { IContent, MatrixEvent, MatrixEventEvent } from "../models/event"; import { IContent, MatrixEvent, MatrixEventEvent } from "../models/event";
import { Room } from "../models/room"; import { Room } from "../models/room";
import { RoomMember } from "../models/room-member"; 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 { logger } from "../logger";
import { IHttpOpts, MatrixHttpApi, Method } from "../http-api"; import { IHttpOpts, MatrixHttpApi, Method } from "../http-api";
import { RoomEncryptor } from "./RoomEncryptor"; import { RoomEncryptor } from "./RoomEncryptor";
@ -1023,6 +1023,13 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
obj.signatures = Object.fromEntries(sigs.entries()); 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 // SyncCryptoCallbacks implementation