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

rust backup restore support (#3709)

* Refactor key backup recovery to prepare for rust

* rust backup restore support

* Move export out of old crypto to api with re-export

* extract base64 utility

* add tests for base64 util

* more efficient regex

* fix typo
This commit is contained in:
Valere
2023-09-13 11:08:26 +02:00
committed by GitHub
parent 1b8507c060
commit 1503acb30a
12 changed files with 318 additions and 46 deletions

View File

@ -18,7 +18,6 @@ import fetchMock from "fetch-mock-jest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { IKeyBackupSession } from "../../../src/crypto/keybackup";
import { createClient, CryptoEvent, ICreateClientOpts, IEvent, MatrixClient, TypedEventEmitter } from "../../../src";
import { SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
@ -26,7 +25,7 @@ import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
import { awaitDecryption, CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import * as testData from "../../test-utils/test-data";
import { KeyBackupInfo } from "../../../src/crypto-api/keybackup";
import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keybackup";
import { IKeyBackup } from "../../../src/crypto/backup";
const ROOM_ID = "!ROOM:ID";
@ -52,7 +51,7 @@ const ENCRYPTED_EVENT: Partial<IEvent> = {
origin_server_ts: 1507753886000,
};
const CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = {
const CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
first_message_index: 0,
forwarded_count: 0,
is_verified: false,
@ -230,7 +229,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
});
describe("recover from backup", () => {
oldBackendOnly("can restore from backup (Curve25519 version)", async function () {
it("can restore from backup (Curve25519 version)", async function () {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
aliceClient = await initTestClient();
@ -275,7 +274,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
await awaitKeyCached;
});
oldBackendOnly("recover specific session from backup", async function () {
it("recover specific session from backup", async function () {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
aliceClient = await initTestClient();
@ -303,7 +302,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
expect(result.imported).toStrictEqual(1);
});
oldBackendOnly("Fails on bad recovery key", async function () {
it("Fails on bad recovery key", async function () {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
aliceClient = await initTestClient();

View File

@ -81,8 +81,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";
import {{ KeyBackupSession, KeyBackupInfo }} from "../../../src/crypto-api/keybackup";
/* eslint-disable comma-dangle */
@ -192,6 +191,7 @@ 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"])
@ -253,7 +253,7 @@ export const {prefix}MEGOLM_SESSION_DATA: IMegolmSessionData = {
};
/** 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)};
export const {prefix}CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {json.dumps(backed_up_room_key, indent=4)};
"""
@ -383,6 +383,7 @@ def build_exported_megolm_key() -> dict:
return megolm_export
def encrypt_megolm_key_for_backup(session_data: dict, backup_public_key: x25519.X25519PublicKey) -> dict:
"""
@ -447,6 +448,108 @@ def encrypt_megolm_key_for_backup(session_data: dict, backup_public_key: x25519.
return encrypted_key
def generate_encrypted_event_content(exported_key: dict, ed_key: ed25519.Ed25519PrivateKey, curve_key: x25519.X25519PrivateKey) -> tuple[dict, dict]:
"""
Encrypts an event using the given key in session export format.
Will not do any ratcheting, just encrypt at index 0.
"""
clear_event = {
"type": "m.room.message",
"room_id": "!room:id",
"sender": "@alice:localhost",
"content": {
"msgtype": "m.text",
"body": "Hello world"
}
}
session_key: str = exported_key["session_key"]
# Get the megolm R0 from the export format
decoded = base64.b64decode(session_key.encode("ascii"))
r0 = decoded[5:133]
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=80,
salt=bytes(32),
info=b"MEGOLM_KEYS",
)
raw_key = hkdf.derive(r0)
aes_key = raw_key[:32]
mac = raw_key[32:64]
aes_iv = raw_key[64:80]
payload_json = {
"room_id": clear_event["room_id"],
"type": clear_event["type"],
"content": clear_event["content"]
}
payload_string = encode_canonical_json(payload_json)
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(aes_iv))
encryptor = cipher.encryptor()
padder = padding.PKCS7(128).padder()
padded_data = padder.update(payload_string)
padded_data += padder.finalize()
ct = encryptor.update(padded_data) + encryptor.finalize()
# The ratchet index i, and the cipher-text, are then packed
# into a message as described in Message format. Then the entire message
# (including the version bytes and all payload bytes) are passed through
# HMAC-SHA-256. The first 8 bytes of the MAC are appended to the message.
message = bytearray()
message += b'\x03'
# int tag for index
message += b'\x08'
# index is 0
message += b'\x00'
message += b'\x12'
# probably works only for short messages
message += len(ct).to_bytes(1, 'big')
# encrypted data
message += ct
h = hmac.HMAC(mac, hashes.SHA256())
h.update(message)
signature = h.finalize()
mac = signature[:8]
message += mac
# Finally, the authenticated message is signed using the Ed25519 keypair;
# the 64 byte signature is appended to the message
signature = ed_key.sign(bytes(message))
message += signature
cipher_text = encode_base64(message)
encrypted_payload = {
"algorithm" : "m.megolm.v1.aes-sha2",
"sender_key" : encode_base64(curve_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
"ciphertext" : cipher_text,
"session_id" : exported_key["session_id"],
"device_id" : "TEST_DEVICE"
}
encrypted_event = {
"type": "m.room.encrypted",
"room_id": "!room:id",
"sender": "@alice:localhost",
"content": encrypted_payload,
"event_id": "$event1",
"origin_server_ts": 1507753886000,
}
return clear_event, encrypted_event
def export_recovery_key(key_b64: str) -> str:
"""
Export a private recovery key as a recovery key that can be presented to users.

View File

@ -5,8 +5,7 @@
import { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto";
import { IDownloadKeyResult } from "../../../src";
import { KeyBackupInfo } from "../../../src/crypto-api";
import { IKeyBackupSession } from "../../../src/crypto/keybackup";
import { KeyBackupSession, KeyBackupInfo } from "../../../src/crypto-api/keybackup";
/* eslint-disable comma-dangle */
@ -179,7 +178,7 @@ export const MEGOLM_SESSION_DATA: IMegolmSessionData = {
};
/** 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 = {
export const CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
"first_message_index": 1,
"forwarded_count": 0,
"is_verified": false,
@ -359,7 +358,7 @@ export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = {
};
/** 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 = {
export const BOB_CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
"first_message_index": 1,
"forwarded_count": 0,
"is_verified": false,

View File

@ -0,0 +1,52 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { TextEncoder, TextDecoder } from "util";
import { decodeBase64, encodeBase64, encodeUnpaddedBase64 } from "../../../src/common-crypto/base64";
describe("Crypto Base64 encoding", () => {
it("Should decode properly encoded data", async () => {
const toEncode = "encoding hello world";
const encoded = encodeBase64(new TextEncoder().encode(toEncode));
const decoded = new TextDecoder().decode(decodeBase64(encoded));
expect(decoded).toStrictEqual(toEncode);
});
it("Encode unpadded should not have padding", async () => {
const toEncode = "encoding hello world";
const data = new TextEncoder().encode(toEncode);
const paddedEncoded = encodeBase64(data);
const unpaddedEncoded = encodeUnpaddedBase64(data);
expect(paddedEncoded).not.toEqual(unpaddedEncoded);
const padding = paddedEncoded.charAt(paddedEncoded.length - 1);
expect(padding).toStrictEqual("=");
});
it("Decode should be indifferent to padding", async () => {
const withPadding = "ZW5jb2RpbmcgaGVsbG8gd29ybGQ=";
const withoutPadding = "ZW5jb2RpbmcgaGVsbG8gd29ybGQ";
const decodedPad = decodeBase64(withPadding);
const decodedNoPad = decodeBase64(withoutPadding);
expect(decodedPad).toStrictEqual(decodedNoPad);
});
});

View File

@ -20,7 +20,7 @@ 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 { KeyBackupInfo, KeyBackupSession } from "../crypto-api/keybackup";
import { IMegolmSessionData } from "../@types/crypto";
/**
@ -107,7 +107,7 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
* @param backupInfo - The backup information
* @param privKey - The private decryption key.
*/
getBackupDecryptor(backupInfo: IKeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor>;
getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor>;
}
/** The methods which crypto implementations should expose to the Sync api
@ -245,7 +245,7 @@ export interface BackupDecryptor {
*
* @returns An array of decrypted `IMegolmSessionData`
*/
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]>;
decryptSessions(ciphertexts: Record<string, KeyBackupSession>): Promise<IMegolmSessionData[]>;
/**
* Free any resources held by this decryptor.

View File

@ -0,0 +1,46 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Base64 encoding and decoding utility for crypo.
*/
/**
* Encode a typed array of uint8 as base64.
* @param uint8Array - The data to encode.
* @returns The base64.
*/
export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string {
return Buffer.from(uint8Array).toString("base64");
}
/**
* Encode a typed array of uint8 as unpadded base64.
* @param uint8Array - The data to encode.
* @returns The unpadded base64.
*/
export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
return encodeBase64(uint8Array).replace(/={1,2}$/, "");
}
/**
* Decode a base64 string to a typed array of uint8.
* @param base64 - The base64 to decode.
* @returns The decoded data.
*/
export function decodeBase64(base64: string): Uint8Array {
return Buffer.from(base64, "base64");
}

View File

@ -15,6 +15,7 @@ limitations under the License.
*/
import { ISigned } from "../@types/signed";
import { IEncryptedPayload } from "../crypto/aes";
export interface Curve25519AuthData {
public_key: string;
@ -68,3 +69,21 @@ export interface KeyBackupCheck {
backupInfo: KeyBackupInfo;
trustInfo: BackupTrustInfo;
}
export interface Curve25519SessionData {
ciphertext: string;
ephemeral: string;
mac: string;
}
/* eslint-disable camelcase */
export interface KeyBackupSession<T = Curve25519SessionData | IEncryptedPayload> {
first_message_index: number;
forwarded_count: number;
is_verified: boolean;
session_data: T;
}
export interface KeyBackupRoomSessions {
[sessionId: string]: KeyBackupSession;
}

View File

@ -1848,7 +1848,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
/**
* Implementation of {@link CryptoBackend#getBackupDecryptor}.
*/
public async getBackupDecryptor(backupInfo: IKeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor> {
public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor> {
if (!(privKey instanceof Uint8Array)) {
throw new Error(`getBackupDecryptor expects Uint8Array`);
}

View File

@ -14,31 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { IEncryptedPayload } from "./aes";
export interface Curve25519SessionData {
ciphertext: string;
ephemeral: string;
mac: string;
}
/* eslint-disable camelcase */
export interface IKeyBackupSession<T = Curve25519SessionData | IEncryptedPayload> {
first_message_index: number;
forwarded_count: number;
is_verified: boolean;
session_data: T;
}
export interface IKeyBackupRoomSessions {
[sessionId: string]: IKeyBackupSession;
}
// Export for backward compatibility
export type {
Curve25519AuthData as ICurve25519AuthData,
Aes256AuthData as IAes256AuthData,
KeyBackupInfo as IKeyBackupInfo,
Curve25519SessionData,
KeyBackupSession as IKeyBackupSession,
KeyBackupRoomSessions as IKeyBackupRoomSessions,
} from "../crypto-api/keybackup";
/* eslint-enable camelcase */

View File

@ -19,6 +19,7 @@ import * as bs58 from "bs58";
// picked arbitrarily but to try & avoid clashing with any bitcoin ones
// (which are also base58 encoded, but bitcoin's involve a lot more hashing)
const OLM_RECOVERY_KEY_PREFIX = [0x8b, 0x01];
const KEY_SIZE = 32;
export function encodeRecoveryKey(key: ArrayLike<number>): string | undefined {
const buf = Buffer.alloc(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
@ -52,11 +53,9 @@ export function decodeRecoveryKey(recoveryKey: string): Uint8Array {
}
}
if (result.length !== OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1) {
if (result.length !== OLM_RECOVERY_KEY_PREFIX.length + KEY_SIZE + 1) {
throw new Error("Incorrect length");
}
return Uint8Array.from(
result.slice(OLM_RECOVERY_KEY_PREFIX.length, OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH),
);
return Uint8Array.from(result.slice(OLM_RECOVERY_KEY_PREFIX.length, OLM_RECOVERY_KEY_PREFIX.length + KEY_SIZE));
}

View File

@ -17,14 +17,23 @@ limitations under the License.
import { OlmMachine, SignatureVerification } from "@matrix-org/matrix-sdk-crypto-wasm";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { BackupTrustInfo, Curve25519AuthData, KeyBackupCheck, KeyBackupInfo } from "../crypto-api/keybackup";
import {
BackupTrustInfo,
Curve25519AuthData,
KeyBackupCheck,
KeyBackupInfo,
KeyBackupSession,
Curve25519SessionData,
} from "../crypto-api/keybackup";
import { logger } from "../logger";
import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api";
import { CryptoEvent } from "../crypto";
import { CryptoEvent, IMegolmSessionData } from "../crypto";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { encodeUri } from "../utils";
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
import { sleep } from "../utils";
import { BackupDecryptor } from "../common-crypto/CryptoBackend";
import { IEncryptedPayload } from "../crypto/aes";
/** Authentification of the backup info, depends on algorithm */
type AuthData = KeyBackupInfo["auth_data"];
@ -370,6 +379,51 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
}
}
/**
* Implementation of {@link BackupDecryptor} for the rust crypto backend.
*/
export class RustBackupDecryptor implements BackupDecryptor {
private decryptionKey: RustSdkCryptoJs.BackupDecryptionKey;
public sourceTrusted: boolean;
public constructor(decryptionKey: RustSdkCryptoJs.BackupDecryptionKey) {
this.decryptionKey = decryptionKey;
this.sourceTrusted = false;
}
/**
* Implements {@link BackupDecryptor#decryptSessions}
*/
public async decryptSessions(
ciphertexts: Record<string, KeyBackupSession<Curve25519SessionData | IEncryptedPayload>>,
): Promise<IMegolmSessionData[]> {
const keys: IMegolmSessionData[] = [];
for (const [sessionId, sessionData] of Object.entries(ciphertexts)) {
try {
const decrypted = JSON.parse(
await this.decryptionKey.decryptV1(
sessionData.session_data.ephemeral,
sessionData.session_data.mac,
sessionData.session_data.ciphertext,
),
);
decrypted.session_id = sessionId;
keys.push(decrypted);
} catch (e) {
logger.log("Failed to decrypt megolm session from backup", e, sessionData);
}
}
return keys;
}
/**
* Implements {@link BackupDecryptor#free}
*/
public free(): void {
this.decryptionKey.free();
}
}
export type RustBackupCryptoEvents =
| CryptoEvent.KeyBackupStatus
| CryptoEvent.KeyBackupSessionsRemaining

View File

@ -38,6 +38,7 @@ import {
CrossSigningKeyInfo,
CrossSigningStatus,
CryptoCallbacks,
Curve25519AuthData,
DeviceVerificationStatus,
EventEncryptionInfo,
EventShieldColour,
@ -62,11 +63,12 @@ import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentif
import { EventType } from "../@types/event";
import { CryptoEvent } from "../crypto";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } from "./backup";
import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupDecryptor, RustBackupManager } from "./backup";
import { TypedReEmitter } from "../ReEmitter";
import { randomString } from "../randomstring";
import { ClientStoppedError } from "../errors";
import { ISignatures } from "../@types/signed";
import { encodeBase64 } from "../common-crypto/base64";
const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"];
@ -941,7 +943,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
* @param key - the backup decryption key
*/
public async storeSessionBackupPrivateKey(key: Uint8Array): Promise<void> {
const base64Key = Buffer.from(key).toString("base64");
const base64Key = encodeBase64(key);
// TODO get version from backupManager
await this.olmMachine.saveBackupDecryptionKey(RustSdkCryptoJs.BackupDecryptionKey.fromBase64(base64Key), "");
@ -1027,7 +1029,23 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
* Implementation of {@link CryptoBackend#getBackupDecryptor}.
*/
public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor> {
throw new Error("Stub not yet implemented");
if (backupInfo.algorithm != "m.megolm_backup.v1.curve25519-aes-sha2") {
throw new Error(`getBackupDecryptor Unsupported algorithm ${backupInfo.algorithm}`);
}
const authData = <Curve25519AuthData>backupInfo.auth_data;
if (!(privKey instanceof Uint8Array)) {
throw new Error(`getBackupDecryptor expects Uint8Array`);
}
const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(encodeBase64(privKey));
if (authData.public_key != backupDecryptionKey.megolmV1PublicKey.publicKeyBase64) {
throw new Error(`getBackupDecryptor key mismatch error`);
}
return new RustBackupDecryptor(backupDecryptionKey);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////