Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Public Key Mismatch When Using restoreKeyBackupWithRecoveryKey #4173

Closed
xiaoyue opened this issue Apr 19, 2024 · 2 comments
Closed

Public Key Mismatch When Using restoreKeyBackupWithRecoveryKey #4173

xiaoyue opened this issue Apr 19, 2024 · 2 comments

Comments

@xiaoyue
Copy link

xiaoyue commented Apr 19, 2024

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 = "[email protected]";
	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
Copy link
Author

xiaoyue commented Apr 22, 2024

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

  • Key backup is a separate process from device verification. The auth_dict I pulled was for another device (Element Web client), not my own node app. This was why the keys didn't match.
  • To verify my own bot device, I had to use CryptoApi.bootstrapCrossSigning, and CryptoApi.crossSignDevice(myDeviceId). For the bootstrap to work, the SecretStorage and the CryptoCallbackFunctions for MatrixClient.createClient has to be there - especially the getSecretStorageKey callback. Within that callback, I can insert code to use my own passphrase or recovery key to decipher into the private key it needs. I used MatrixClient.keyBackupKeyFromRecoveryKey() there for my use case.

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. :)

@xiaoyue xiaoyue closed this as completed Apr 26, 2024
@satpugnet
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants