matrix-org / matrix-js-sdk

Matrix Client-Server SDK for JavaScript
Apache License 2.0
1.53k stars 580 forks source link

Failing to read encrypted messages when integrating with Beeper Homeserver 🥲 #4360

Open satpugnet opened 3 weeks ago

satpugnet commented 3 weeks ago

I am trying to integrate with Beeper Homeserver for my Electron app and I have not been able to get it to work for encrypted messages. I could not find a working snippet of code on the internet. That would be immensly useful if someone could provide a working snippet of code to send and read messages through the beeper homeserver to whatsapp or another encrypted messaging app.

I have not been able to verify my client with Beeper as the "can't scan" button for verifying new apps does not seem to do anything after you enter the KEY. And I am not sure what code to put on my side to make it work with the QR code displayed by the Beeper app. This could be why I am unable to decrypt the messages but I am unsure if this is the cause.

Here is a screenshot of what my app displays. I can read messages from myself but cannot read messages from others as the key is not shared:

Captura de pantalla 2024-08-21 a la(s) 2 36 57 p m

Here is what I see on my Beeper app as I am unable to verify the device (Not sure if that is the cause):

Captura de pantalla 2024-08-21 a la(s) 2 38 16 p m 359897831-becd2462-f6cf-41d6-aa3b-2adc4df2155e

Thanks in advance for the help. It would be wonderful if anyone could provide. Working snippet of code. And it would probably be a huge help for the community to have a working snippet of code for this 🙂

I am using matrix-js-sdk withe version 32.4.0 and olm with version 3.2.12.

Here is the code that I have so far, this is my renderer.js:

const sdk = require('matrix-js-sdk');

global.Olm = require('@matrix-org/olm');

// Not sure if needed
Olm.init().then(() => {
    console.log('Olm initialized successfully');
}).catch((err) => {
    console.error('Failed to initialize Olm:', err);
});

// Replace these with your Beeper credentials
const matrixServerUrl = 'https://matrix.beeper.com';
const matrixUserId = 'REDACTED'; // Your user ID in Beeper
const matrixPassword = 'REDACTED'; // Your password

document.getElementById('connect').addEventListener('click', async () => {
    // Initialize the client without an access token for now
    const client = sdk.createClient({
        baseUrl: matrixServerUrl,
        store: new sdk.MemoryStore(), // Store for caching
        cryptoStore: new sdk.MemoryCryptoStore(), // Store for encryption keys
    });

    client.stopClient(); // Not sure if needed
    await client.clearStores(); // Not sure if needed

    // Login with username and password
    client.loginWithPassword(matrixUserId, matrixPassword)
        .then(async (response) => {
            console.log('Logged in successfully:', response);

            // Set the accessToken, userId, and deviceId in the client after login
            client.setAccessToken(response.access_token);
            client.credentials.userId = response.user_id;
            client.deviceId = response.device_id;

            // Initialize crypto and start the client
            await client.initCrypto();
        })
        .then(async () => {
            await client.startClient({ initialSyncLimit: 10 });

            client.crypto.setDeviceVerification(client.getUserId(), client.getDeviceId(), true); // Not sure if needed
            // client.crypto.enableKeyBackup(); // Not sure if needed

            client.on('sync', (state, prevState, res) => {
                console.log('Syncing...', state);

                if (state === 'PREPARED') {
                    client.setGlobalErrorOnUnknownDevices(false);
                    console.log('Logged in and synced successfully.');
                    listenForMessages(client);
                }
            });

            client.on('error', (err) => {
                console.error('Error:', err);
            });

            // Handle device verification or auto-verify unknown devices
            client.on('crypto.devicesUpdated', (users) => {
                users.forEach(user => {
                    const devices = client.getStoredDevicesForUser(user);
                    devices.forEach(device => {
                        if (!device.verified) {
                            console.warn(`Device ${device.deviceId} for user ${user} is not verified`);
                            // Optionally, auto-verify or handle it as needed:
                            client.setDeviceVerified(user, device.deviceId);
                        }
                    });
                });
            });

            client.on("crypto.verification.request", (request) => {
                console.log("Received verification request: ", request);
                // Handle the request, e.g., show UI to accept/reject
                handleVerificationRequest(request);
            });
        })
        .catch((err) => {
            console.error('Login failed or error initializing crypto:', err);
        });
});

// This function is not really working and I have not been able to verify the client.
async function handleVerificationRequest(request) {
    try {
        console.log("Received verification request:", request);

        // Accept the request
        await request.accept();

        console.log("Request accepted. Waiting for QR code scan...");

        // Capture QR code data (implement your own function to capture or upload QR code image)
        const qrCodeData = "";

        if (qrCodeData) {
            console.log("QR code data captured:", qrCodeData);
            const verifier = request.beginKeyVerification('m.qr_code.scan.v1', qrCodeData);
            verifier.on('done', () => {
                console.log("QR code verification completed successfully!");
            });

            verifier.on('cancel', (e) => {
                console.error("Verification canceled: ", e);
            });
        } else {
            console.error("Failed to capture QR code data.");
        }

    } catch (e) {
        console.error("Error during verification:", e);
    }
}

function listenForMessages(client) {
    client.on('Room.timeline', async (event, room, toStartOfTimeline) => {
        if (toStartOfTimeline) {
            return; // Don't print paginated results
        }

        if (event.getType() !== 'm.room.message' && event.getType() !== 'm.room.encrypted') {
            return; // Only handle messages or encrypted messages
        }

        // Decrypt the message if it's encrypted
        if (event.isEncrypted()) {
            try {
                console.log('Decrypting event:', event);
                await client.crypto.requestRoomKey({
                    room_id: event.getRoomId(),
                    session_id: event.getWireContent().session_id,
                    sender_key: event.getSenderKey(),
                    algorithm: event.getWireContent().algorithm
                });
                await client.decryptEventIfNeeded(event);
                console.info('Decrypted event:', event.getContent());
                // event = await client.decryptEvent(message);
            } catch (err) {
                console.error('Failed to decrypt event:', err);
                await client.requestRoomKey(event);
                return;
            }
        } else {
            console.log('Event is not encrypted:', event);
        }

        const sender = event.getSender();
        const content = event.getContent().body;

        if (content) {
            document.getElementById('messages').innerHTML += `<p><strong>${sender}:</strong> ${content}</p>`;
        }
    });

    // Handle key requests from other devices
    client.on('crypto.roomKeyRequest', (req) => {
        console.log('Received room key request', req);

        if (req.action === "request") {
            console.log('Automatically sharing keys');
            client.crypto.sendSharedHistoryKeys(req.userId, req.roomId, req.requestId, req.requestBody);
        } else {
            console.log(`Unhandled key request action: ${req.action}`);
        }
    });
}

Here is the main.js

const { app, BrowserWindow } = require('electron');
const path = require('path');

function createWindow() {
    const win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            preload: path.join(__dirname, 'preload.js'),
            nodeIntegration: true,
            contextIsolation: false,
        },
    });

    win.loadFile('index.html');
}

app.whenReady().then(() => {
    createWindow();

    app.on('activate', () => {
        if (BrowserWindow.getAllWindows().length === 0) {
            createWindow();
        }
    });
});

app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

Here is the index.js

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Matrix WhatsApp Bridge</title>
</head>
<body>
<h1>Matrix WhatsApp Bridge</h1>
<button id="connect">Connect to Matrix</button>
<div id="messages"></div>
<script src="renderer.js"></script>
</body>
</html>
satpugnet commented 3 weeks ago

This is my latest change to the code to try to use the Recovery key directly unsuccessfully (Getting the error shown below despite the key being the right one):

const sdk = require('matrix-js-sdk');

global.Olm = require('@matrix-org/olm');

Olm.init().then(() => {
    console.log('Olm initialized successfully');
}).catch((err) => {
    console.error('Failed to initialize Olm:', err);
});

const deviceId = 'REDACTED';

const matrixServerUrl = 'https://matrix.beeper.com';
const matrixUserId = 'REDACTED'; // Your user ID in Beeper
const matrixPassword = 'REDACTED'; // Your password
const beeperRecoveryKey = "REDACTED";

const matrixUsername = 'REDACTED'; // Your username
const beeperAccessToken = 'REDACTED'; // Use the access token

document.getElementById('connect').addEventListener('click', async () => {
    // Setup secret storage callback
    const getSecretStorageKey = async (keys, name) => {
        console.log('Getting secret storage key:', keys, name);
        const defaultKeyId = await client.secretStorage.getDefaultKeyId();
        const keyBackupKey = client.keyBackupKeyFromRecoveryKey(beeperRecoveryKey);
        return [defaultKeyId, keyBackupKey];
    };

    const client = sdk.createClient({
        baseUrl: matrixServerUrl,
        store: new sdk.MemoryStore(),
        cryptoStore: new sdk.MemoryCryptoStore(),
        cryptoCallbacks: { getSecretStorageKey }
    });

    try {
        // Clear any previous stores
        await client.clearStores();

        // Login with username and password
        // const response = await client.loginWithPassword(matrixUserId, matrixPassword);
        const response = await client.login("m.login.password", {
            "user": matrixUserId,
            "device_id": deviceId,
            "password": matrixPassword,
            // "refresh_token": true,
        });
        console.log('Logged in successfully:', response);

        // Set credentials
        client.setAccessToken(response.access_token);
        client.credentials.userId = response.user_id;
        client.deviceId = response.device_id;

        // Initialize crypto
        await client.initCrypto();
        // await client.initRustCrypto();

        // Bootstrap cross-signing
        await client.getCrypto().bootstrapCrossSigning({});
        console.log('Cross-signing bootstrapped.');

        // Start the client and sync
        await client.startClient();
        console.log('Client started.');

        await client.getCrypto().crossSignDevice(deviceId);
        console.log('Device cross-signed.');

        const isVerified = await client.getCrypto().getDeviceVerificationStatus(matrixUserId, deviceId);
        console.log('Device verification status:', isVerified);

        // if (await client.getCrypto().isSecretStorageReady()) {
        //     console.log("Secret storage is ready.");
        // }
        //
        // // Restore keys using recovery key
        // await restoreKeysUsingRecoveryKey(client, beeperRecoveryKey);

        client.on('sync', async (state) => {
            console.log('Syncing...', state);

            if (state === 'PREPARED') {
                // Not sure if this needs to be done here or above
                if (await client.getCrypto().isSecretStorageReady()) {
                    console.log("Secret storage is ready.");
                }

                // Restore keys using recovery key
                await restoreKeysUsingRecoveryKey(client, beeperRecoveryKey);

                console.log('Logged in and synced successfully.');
                listenForMessages(client);
            }
        });

        client.on('error', (err) => {
            console.error('Error:', err);
        });

    } catch (err) {
        console.error('Login failed or error initializing crypto:', err);
    }
});

async function restoreKeysUsingRecoveryKey(client, recoveryKey) {
    try {
        console.log("Restoring keys using recovery key...");

        // Fetch backup info from the server
        // const backupInfo = (await client.getCrypto().checkKeyBackupAndEnable())?.backupInfo;
        const backupInfo = await client.getKeyBackupVersion();
        if (!backupInfo) {
            throw new Error("No backup info found on the server.");
        }
        console.log("Fetched backup info:", backupInfo);

        console.log("Is valid recovery key:", client.isValidRecoveryKey(recoveryKey)); // Should return "true"

        // Restore the keys using the recovery key and the fetched backup info
        const restoreResult = await client.restoreKeyBackupWithRecoveryKey(recoveryKey, null, null, backupInfo);
        console.log("Successfully restored keys using recovery key.", restoreResult);
    } catch (error) {
        console.error("Failed to restore keys using recovery key:", error);
    }
}

function listenForMessages(client) {
    client.on('Room.timeline', async (event, room, toStartOfTimeline) => {
        if (toStartOfTimeline) {
            return; // Don't print paginated results
        }

        if (event.getType() !== 'm.room.message' && event.getType() !== 'm.room.encrypted') {
            return; // Only handle messages or encrypted messages
        }

        // Decrypt the message if it's encrypted
        if (event.isEncrypted()) {
            try {
                console.log('Decrypting event:', event);
                await client.crypto.requestRoomKey({
                    room_id: event.getRoomId(),
                    session_id: event.getWireContent().session_id,
                    sender_key: event.getSenderKey(),
                    algorithm: event.getWireContent().algorithm
                });
                await client.decryptEventIfNeeded(event);
                console.info('Decrypted event:', event.getContent());
            } catch (err) {
                console.error('Failed to decrypt event:', err);
                await client.requestRoomKey(event);
                return;
            }
        } else {
            console.log('Event is not encrypted:', event);
        }

        const sender = event.getSender();
        const content = event.getContent().body;

        if (content) {
            document.getElementById('messages').innerHTML += `<p><strong>${sender}:</strong> ${content}</p>`;
        }
    });

    // Handle key requests from other devices
    client.on('crypto.roomKeyRequest', (req) => {
        console.log('Received room key request', req);

        if (req.action === "request") {
            console.log('Automatically sharing keys');
            client.crypto.sendSharedHistoryKeys(req.userId, req.roomId, req.requestId, req.requestBody);
        } else {
            console.log(`Unhandled key request action: ${req.action}`);
        }
    });
}
Captura de pantalla 2024-08-21 a la(s) 7 46 19 p m
dbkr commented 3 weeks ago

We can't really offer support here - you could try https://matrix.to/#/#matrix-dev:matrix.org