From a839ca99709bd613a6ff641fe5b2dbebaea58656 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Thu, 23 Nov 2023 13:05:23 +0000 Subject: [PATCH] Add basic key backup test (rust only) --- internal/api/client.go | 16 +++++----- internal/api/js.go | 5 ++-- internal/api/rust.go | 46 ++++++++++++++--------------- tests/key_backup_test.go | 63 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 33 deletions(-) create mode 100644 tests/key_backup_test.go diff --git a/internal/api/client.go b/internal/api/client.go index 666821e..18b591c 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -46,9 +46,9 @@ type Client interface { // MustGetEvent will return the client's view of this event, or fail the test if the event cannot be found. MustGetEvent(t *testing.T, roomID, eventID string) Event // MustBackupKeys will backup E2EE keys, else fail the test. - MustBackupKeys(t *testing.T) + MustBackupKeys(t *testing.T) (recoveryKey string) // MustLoadBackup will recover E2EE keys from the latest backup, else fail the test. - MustLoadBackup(t *testing.T) + MustLoadBackup(t *testing.T, recoveryKey string) // Log something to stdout and the underlying client log file Logf(t *testing.T, format string, args ...interface{}) // The user for this client @@ -100,16 +100,18 @@ func (c *LoggedClient) MustBackpaginate(t *testing.T, roomID string, count int) c.Client.MustBackpaginate(t, roomID, count) } -func (c *LoggedClient) MustBackupKeys(t *testing.T) { +func (c *LoggedClient) MustBackupKeys(t *testing.T) (recoveryKey string) { t.Helper() c.Logf(t, "%s MustBackupKeys", c.logPrefix()) - c.Client.MustBackupKeys(t) + recoveryKey = c.Client.MustBackupKeys(t) + c.Logf(t, "%s MustBackupKeys => %s", c.logPrefix(), recoveryKey) + return recoveryKey } -func (c *LoggedClient) MustLoadBackup(t *testing.T) { +func (c *LoggedClient) MustLoadBackup(t *testing.T, recoveryKey string) { t.Helper() - c.Logf(t, "%s MustLoadBackup", c.logPrefix()) - c.Client.MustLoadBackup(t) + c.Logf(t, "%s MustLoadBackup key=%s", c.logPrefix(), recoveryKey) + c.Client.MustLoadBackup(t, recoveryKey) } func (c *LoggedClient) logPrefix() string { diff --git a/internal/api/js.go b/internal/api/js.go index 25519bf..83de428 100644 --- a/internal/api/js.go +++ b/internal/api/js.go @@ -298,11 +298,12 @@ func (c *JSClient) MustBackpaginate(t *testing.T, roomID string, count int) { )) } -func (c *JSClient) MustBackupKeys(t *testing.T) { +func (c *JSClient) MustBackupKeys(t *testing.T) (recoveryKey string) { // TODO + return } -func (c *JSClient) MustLoadBackup(t *testing.T) { +func (c *JSClient) MustLoadBackup(t *testing.T, recoveryKey string) { // TODO } diff --git a/internal/api/rust.go b/internal/api/rust.go index 1590c57..2759f85 100644 --- a/internal/api/rust.go +++ b/internal/api/rust.go @@ -138,33 +138,31 @@ func (c *RustClient) IsRoomEncrypted(t *testing.T, roomID string) (bool, error) return r.IsEncrypted() } -func (c *RustClient) MustBackupKeys(t *testing.T) { - // no-op, ffi does this by default - /* - t.Helper() - must.NotError(t, "failed to EnableBackups", c.FFIClient.Encryption().EnableBackups()) - genericListener := newGenericStateListener[matrix_sdk_ffi.BackupUploadState]() - var listener matrix_sdk_ffi.BackupSteadyStateListener = genericListener - must.NotError(t, "failed to WaitForBackupUploadSteadyState", c.FFIClient.Encryption().WaitForBackupUploadSteadyState(&listener)) - for s := range genericListener.ch { - switch x := s.(type) { - case matrix_sdk_ffi.BackupUploadStateWaiting: - c.Logf(t, "MustBackupKeys: state=waiting") - case matrix_sdk_ffi.BackupUploadStateUploading: - c.Logf(t, "MustBackupKeys: state=uploading %d/%d", x.BackedUpCount, x.TotalCount) - case matrix_sdk_ffi.BackupUploadStateError: - fatalf(t, "MustBackupKeys: state=error") - case matrix_sdk_ffi.BackupUploadStateDone: - genericListener.Close() - return - } - } */ - c.FFIClient.Encryption().EnableRecovery(true, nil) +func (c *RustClient) MustBackupKeys(t *testing.T) (recoveryKey string) { + t.Helper() + genericListener := newGenericStateListener[matrix_sdk_ffi.EnableRecoveryProgress]() + var listener matrix_sdk_ffi.EnableRecoveryProgressListener = genericListener + recoveryKey, err := c.FFIClient.Encryption().EnableRecovery(true, listener) + for s := range genericListener.ch { + switch x := s.(type) { + case matrix_sdk_ffi.EnableRecoveryProgressCreatingBackup: + t.Logf("MustBackupKeys: state=CreatingBackup") + case matrix_sdk_ffi.EnableRecoveryProgressBackingUp: + t.Logf("MustBackupKeys: state=BackingUp %v/%v", x.BackedUpCount, x.TotalCount) + case matrix_sdk_ffi.EnableRecoveryProgressCreatingRecoveryKey: + t.Logf("MustBackupKeys: state=CreatingRecoveryKey") + case matrix_sdk_ffi.EnableRecoveryProgressDone: + t.Logf("MustBackupKeys: state=Done") + genericListener.Close() // break the loop + } + } + must.NotError(t, "Encryption.EnableRecovery", err) + return recoveryKey } -func (c *RustClient) MustLoadBackup(t *testing.T) { +func (c *RustClient) MustLoadBackup(t *testing.T, recoveryKey string) { t.Helper() - // TODO + must.NotError(t, "FixRecoveryIssues", c.FFIClient.Encryption().FixRecoveryIssues(recoveryKey)) } func (c *RustClient) WaitUntilEventInRoom(t *testing.T, roomID string, checker func(Event) bool) Waiter { diff --git a/tests/key_backup_test.go b/tests/key_backup_test.go new file mode 100644 index 0000000..d621fce --- /dev/null +++ b/tests/key_backup_test.go @@ -0,0 +1,63 @@ +package tests + +import ( + "testing" + "time" + + "github.com/matrix-org/complement-crypto/internal/api" + "github.com/matrix-org/complement/must" +) + +func TestCanBackupKeys(t *testing.T) { + ClientTypeMatrix(t, func(t *testing.T, clientTypeA, clientTypeB api.ClientType) { + if clientTypeB.Lang == api.ClientTypeJS { + t.Skipf("key backups unsupported (js)") + return + } + tc := CreateTestContext(t, clientTypeA, clientTypeB) + // shared history visibility + roomID := tc.CreateNewEncryptedRoom(t, tc.Alice, "public_chat", nil) + tc.Bob.MustJoinRoom(t, roomID, []string{clientTypeA.HS}) + + // SDK testing below + // ----------------- + + // login both clients first, so OTKs etc are uploaded. + alice := tc.MustLoginClient(t, tc.Alice, clientTypeA) + defer alice.Close(t) + bob := tc.MustLoginClient(t, tc.Bob, clientTypeB) + defer bob.Close(t) + + // Alice and Bob start syncing + aliceStopSyncing := alice.StartSyncing(t) + defer aliceStopSyncing() + bobStopSyncing := bob.StartSyncing(t) + defer bobStopSyncing() + + // Alice sends a message which Bob should be able to decrypt + body := "An encrypted message" + waiter := bob.WaitUntilEventInRoom(t, roomID, api.CheckEventHasBody(body)) + evID := alice.SendMessage(t, roomID, body) + t.Logf("bob (%s) waiting for event %s", bob.Type(), evID) + waiter.Wait(t, 5*time.Second) + + // Now Bob backs up his keys. Some clients may automatically do this, but let's be explicit about it. + recoveryKey := bob.MustBackupKeys(t) + + // Now Bob logs in on a new device + _, bob2 := tc.MustLoginDevice(t, tc.Bob, clientTypeB, "NEW_DEVICE") + + // Bob loads the key backup using the recovery key + bob2.MustLoadBackup(t, recoveryKey) + + // Bob's new device can decrypt the encrypted message + bob2StopSyncing := bob2.StartSyncing(t) + defer bob2StopSyncing() + time.Sleep(time.Second) + bob2.MustBackpaginate(t, roomID, 5) // get the old message + + ev := bob2.MustGetEvent(t, roomID, evID) + must.Equal(t, ev.FailedToDecrypt, false, "bob's new device failed to decrypt the event: bad backup?") + must.Equal(t, ev.Text, body, "bob's new device failed to see the clear text message") + }) +}