matrix-org / matrix-js-sdk

Matrix Client-Server SDK for JavaScript
Apache License 2.0
1.56k stars 583 forks source link

Public Key Mismatch When Using `restoreKeyBackupWithRecoveryKey` #4173

Closed xiaoyue closed 5 months ago

xiaoyue commented 5 months ago

I'm trying to enable E2EE and verify a device for a bot account in Node.js. On the Element Web client, I can log into the bot account with password, and when there's no other verified devices/sessions, it asked me to use the account security key to verify the session.

I tried to do this similarly for my Node.js bot: let it login with credentials, and assume no other device/session is verified, I want to use the security key to verify the current device and session.

When I trace the source code for Element Web and matrix-react-sdk, I found that behind the security key input dialog, the MatrixClient.restoreKeyBackupWithRecoveryKey() function from matrix-js-sdk was used. So I attempted to do the same. However, I am getting a "getBackupDecryptor key mismatch" error. From the source code, it seems the recovery key was decrypted successfully, and when generating the public key it didn't match. However, the same key for this account can be used in Element Web (supposedly through the same function) without issue. I'm definitely doing something wrong here, but I don't know what. Can someone point out what I missed?



import prompts from 'prompts';
import * as Matrix from 'matrix-js-sdk';

(async () => {

    const deviceId = "mydevice";
    const userId = "example@matrix.org";
    let client = Matrix.createClient({
        baseUrl: "https://matrix.org",
    });

    const password = (await prompts({
        type: 'password',
        name: 'value',
        message: `Enter password for [${userId}]:`
    })).value;
    const loginResponse = await client.login("m.login.password", {
        "user": userId,
        "device_id": deviceId,
        "password": password,
        "refresh_token": true,
    });

    accessToken = loginResponse.access_token;
    refreshToken = loginResponse.refresh_token!;

    client = Matrix.createClient({
        baseUrl: "https://matrix.org",
        userId: userId,
        accessToken: accessToken,
        refreshToken: refreshToken,
        deviceId: deviceId
    });

    await client.initRustCrypto({ useIndexedDB: false });

    client.on(Matrix.ClientEvent.Event, (event) => {
        console.log(event.getType());
    });

    client.on(Matrix.ClientEvent.Sync, (state, prevState, data) => {
        switch (state) {
            case "PREPARED":
                console.log("Initial sync complete");
                break;
        }
    });

    await client.startClient();
    const crypto = client.getCrypto();

    const isVerified = await crypto?.getDeviceVerificationStatus(userId, deviceId); // returns false, expected/intentional
    const keyBackupInfo = (await crypto?.checkKeyBackupAndEnable())?.backupInfo;
    console.dir(keyBackupInfo, { depth: null }); //has auth_data with public key and other info as expected

    // NOTE: hard-coded recovery key here for demonstration purposes
    const recoveryKey = "Exxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx";
    console.log(client.isValidRecoveryKey(recoveryKey)); // = true
    const result = await client.restoreKeyBackupWithRecoveryKey(recoveryKey, undefined, undefined, keyBackupInfo!);
    // ERROR: the above throws public key mismatch error
})()
xiaoyue commented 5 months ago

I made it work. Here is what I found out:

Here's some code snippets for anyone who struggled with the same confusion like I did.

The callback that's needed, i.e. createClient(... cryptoCallbacks: { getSecretStorageKey: getSecretStorageKey } ):

const getSecretStorageKey = async (keys: { keys: Record<string, Matrix.SecretStorage.SecretStorageKeyDescriptionAesV1>; }, name: string): Promise<null | [string, Uint8Array]> => {
    const defaultKeyId = await client.secretStorage.getDefaultKeyId();
    // ...
    // Omitted some code to check keyId match, and differentiate key names if needed
    // ...
    const keyBackupKey = client.keyBackupKeyFromRecoveryKey(recoveryKey);
    return [defaultKeyId, keyBackupKey];
}

Then, these are the things I needed:

await crypto?.bootstrapCrossSigning({}); // Do this after making sure CryptoApi.isSecretStorageReady() == true
await client.startClient();
await crypto?.crossSignDevice(deviceId); // You should see the request to .../keys/signatures/upload endpoint in logs

Leaving the issue open for a bit more in case anyone wants to correct something I did wrong. :)

satpugnet commented 1 month ago

Thanks a lot for posting part of the solution. Would you be able to share the fully working code by any chance? I am not able to reproduce what you describe as the solution.

Thanks in advance @xiaoyue.