diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index e17faf9..a284d19 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -405,8 +405,7 @@ class KeyManager { try { await loadSingleKey(room.id, sessionId); } catch (err, stacktrace) { - print('++++++++++++++++++'); - print(err.toString()); + print('[KeyManager] Failed to access online key backup: ' + err.toString()); print(stacktrace); } if (!hadPreviously && diff --git a/test/client_test.dart b/test/client_test.dart index 8c93b6d..f48a2e3 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -127,7 +127,7 @@ void main() { } expect(sync.nextBatch == matrix.prevBatch, true); - expect(matrix.accountData.length, 3); + expect(matrix.accountData.length, 9); expect(matrix.getDirectChatFromUserId('@bob:example.com'), '!726s6s6q:example.com'); expect(matrix.rooms[1].directChatMatrixID, '@bob:example.com'); @@ -157,7 +157,7 @@ void main() { expect(matrix.presences['@alice:example.com'].presence.presence, PresenceType.online); expect(presenceCounter, 1); - expect(accountDataCounter, 3); + expect(accountDataCounter, 9); await Future.delayed(Duration(milliseconds: 50)); expect(matrix.userDeviceKeys.length, 4); expect(matrix.userDeviceKeys['@alice:example.com'].outdated, false); diff --git a/test/encryption/ssss_test.dart b/test/encryption/ssss_test.dart new file mode 100644 index 0000000..55d0d8f --- /dev/null +++ b/test/encryption/ssss_test.dart @@ -0,0 +1,207 @@ +/* + * Ansible inventory script used at Famedly GmbH for managing many hosts + * Copyright (C) 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'dart:typed_data'; +import 'dart:convert'; + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/matrix_api.dart'; +import 'package:famedlysdk/encryption.dart'; +import 'package:test/test.dart'; +import 'package:encrypt/encrypt.dart'; +import 'package:olm/olm.dart' as olm; + +import '../fake_client.dart'; +import '../fake_matrix_api.dart'; + +void main() { + group('SSSS', () { + var olmEnabled = true; + try { + olm.init(); + olm.Account(); + } catch (_) { + olmEnabled = false; + print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + } + print('[LibOlm] Enabled: $olmEnabled'); + + if (!olmEnabled) return; + + Client client; + + test('setupClient', () async { + client = await getClient(); + }); + + test('basic things', () async { + expect(client.encryption.ssss.defaultKeyId, '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3'); + }); + + test('encrypt / decrypt', () { + final signing = olm.PkSigning(); + final key = Uint8List.fromList(SecureRandom(32).bytes); + + final enc = SSSS.encryptAes('secret foxies', key, 'name'); + final dec = SSSS.decryptAes(enc, key, 'name'); + expect(dec, 'secret foxies'); + }); + + test('store', () async { + final handle = client.encryption.ssss.open(); + var failed = false; + try { + handle.unlock(passphrase: 'invalid'); + } catch (_) { + failed = true; + } + expect(failed, true); + expect(handle.isUnlocked, false); + failed = false; + try { + handle.unlock(recoveryKey: 'invalid'); + } catch (_) { + failed = true; + } + expect(failed, true); + expect(handle.isUnlocked, false); + handle.unlock(passphrase: SSSS_PASSPHRASE); + handle.unlock(recoveryKey: SSSS_KEY); + expect(handle.isUnlocked, true); + FakeMatrixApi.calledEndpoints.clear(); + await handle.store('best animal', 'foxies'); + // alright, since we don't properly sync we will manually have to update + // account_data for this test + final content = FakeMatrixApi.calledEndpoints['/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal'].first; + client.accountData['best animal'] = BasicEvent.fromJson({ + 'type': 'best animal', + 'content': json.decode(content), + }); + expect(await handle.getStored('best animal'), 'foxies'); + }); + + test('cache', () async { + final handle = client.encryption.ssss.open('m.cross_signing.self_signing'); + handle.unlock(recoveryKey: SSSS_KEY); + expect((await client.encryption.ssss.getCached('m.cross_signing.self_signing')) != null, false); + expect((await client.encryption.ssss.getCached('m.cross_signing.user_signing')) != null, false); + await handle.getStored('m.cross_signing.self_signing'); + expect((await client.encryption.ssss.getCached('m.cross_signing.self_signing')) != null, true); + await handle.maybeCacheAll(); + expect((await client.encryption.ssss.getCached('m.cross_signing.user_signing')) != null, true); + expect((await client.encryption.ssss.getCached('m.megolm_backup.v1')) != null, true); + }); + + test('make share requests', () async { + final key = client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; + key.setDirectVerified(true); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.request('some.type', [key]); + expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), true); + }); + + test('answer to share requests', () async { + var event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.request', + content: { + 'action': 'request', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.cross_signing.self_signing', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), true); + + // now test some fail scenarios + + // not by us + event = ToDeviceEvent( + sender: '@someotheruser:example.org', + type: 'm.secret.request', + content: { + 'action': 'request', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.cross_signing.self_signing', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), false); + + // secret not cached + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.request', + content: { + 'action': 'request', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.unknown.secret', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), false); + + // is a cancelation + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.request', + content: { + 'action': 'request_cancellation', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.cross_signing.self_signing', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), false); + + // device not verified + final key = client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']; + key.setDirectVerified(false); + event = ToDeviceEvent( + sender: client.userID, + type: 'm.secret.request', + content: { + 'action': 'request', + 'requesting_device_id': 'OTHERDEVICE', + 'name': 'm.cross_signing.self_signing', + 'request_id': '1', + }, + ); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.ssss.handleToDeviceEvent(event); + expect(FakeMatrixApi.calledEndpoints.keys.any((k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), false); + key.setDirectVerified(true); + }); + +// test('fail', () { +// expect(true, false); +// }); + + test('dispose client', () async { + await client.dispose(closeDatabase: true); + }); + }); +} diff --git a/test/fake_client.dart b/test/fake_client.dart index 9fb3837..af2c39a 100644 --- a/test/fake_client.dart +++ b/test/fake_client.dart @@ -21,6 +21,9 @@ import 'package:famedlysdk/famedlysdk.dart'; import 'fake_matrix_api.dart'; import 'fake_database.dart'; +const SSSS_PASSPHRASE = 'nae7ahDiequ7ohniufah3ieS2je1thohX4xeeka7aixohsho9O'; +const SSSS_KEY = 'EsT9 RzbW VhPW yqNp cC7j ViiW 5TZB LuY4 ryyv 9guN Ysmr WDPH'; + // key @test:fakeServer.notExisting const pickledOlmAccount = 'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw'; diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index 4e3eb08..e53e7a8 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -516,6 +516,71 @@ class FakeMatrixApi extends MockClient { }, 'type': 'm.direct' }, + { + 'type': 'm.secret_storage.default_key', + 'content': {'key': '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3'} + }, + { + 'type': 'm.secret_storage.key.0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3', + 'content': { + 'algorithm': 'm.secret_storage.v1.aes-hmac-sha2', + 'passphrase': { + 'algorithm': 'm.pbkdf2', + 'iterations': 500000, + 'salt': 'F4jJ80mr0Fc8mRwU9JgA3lQDyjPuZXQL' + }, + 'iv': 'HjbTgIoQH2pI7jQo19NUzA==', + 'mac': 'QbJjQzDnAggU0cM4RBnDxw2XyarRGjdahcKukP9xVlk=' + } + }, + { + 'type': 'm.cross_signing.master', + 'content': { + 'encrypted': { + '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': { + 'iv': 'eIb2IITxtmcq+1TrT8D5eQ==', + 'ciphertext': 'lWRTPo5qxf4LAVwVPzGHOyMcP181n7bb9/B0lvkLDC2Oy4DvAL0eLx2x3bY=', + 'mac': 'Ynx89tIxPkx0o6ljMgxszww17JOgB4tg4etmNnMC9XI=' + } + } + } + }, + { + 'type': 'm.cross_signing.self_signing', + 'content': { + 'encrypted': { + '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': { + 'iv': 'YqU2XIjYulYZl+bkZtGgVw==', + 'ciphertext': 'kM2TSoy/jR/4d357ZoRPbpPypxQl6XRLo3FsEXz+f7vIOp82GeRp28RYb3k=', + 'mac': 'F+DZa5tAFmWsYSryw5EuEpzTmmABRab4GETkM85bGGo=' + } + } + } + }, + { + 'type': 'm.cross_signing.user_signing', + 'content': { + 'encrypted': { + '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': { + 'iv': 'D7AM3LXFu7ZlyGOkR+OeqQ==', + 'ciphertext': 'bYA2+OMgsO6QB1E31aY+ESAWrT0fUBTXqajy4qmL7bVDSZY4Uj64EXNbHuA=', + 'mac': 'j2UtyPo/UBSoiaQCWfzCiRZXp3IRt0ZZujuXgUMjnw4=' + } + } + } + }, + { + 'type': 'm.megolm_backup.v1', + 'content': { + 'encrypted': { + '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': { + 'iv': 'cL/0MJZaiEd3fNU+I9oJrw==', + 'ciphertext': 'WL73Pzdk5wZdaaSpaeRH0uZYKcxkuV8IS6Qa2FEfA1+vMeRLuHcWlXbMX0w=', + 'mac': '+xozp909S6oDX8KRV8D8ZFVRyh7eEYQpPP76f+DOsnw=' + } + } + } + } ] }, 'to_device': { @@ -1461,6 +1526,13 @@ class FakeMatrixApi extends MockClient { 'event_format': 'client', 'event_fields': ['type', 'content', 'sender'] }, + '/client/unstable/room_keys/version': (var req) => { + 'algorithm': 'm.megolm_backup.v1.curve25519-aes-sha2', + 'auth_data': {'public_key': 'GXYaxqhNhUK28zUdxOmEsFRguz+PzBsDlTLlF0O0RkM'}, + 'count': 0, + 'etag': '0', + 'version': '5', + }, }, 'POST': { '/client/r0/delete_devices': (var req) => {}, @@ -1673,6 +1745,19 @@ class FakeMatrixApi extends MockClient { }, 'signatures': {}, }, + 'OTHERDEVICE': { + 'user_id': '@test:fakeServer.notExisting', + 'device_id': 'OTHERDEVICE', + 'algorithms': [ + 'm.olm.v1.curve25519-aes-sha2', + 'm.megolm.v1.aes-sha2' + ], + 'keys': { + 'curve25519:OTHERDEVICE': 'blah', + 'ed25519:OTHERDEVICE': 'blah' + }, + 'signatures': {}, + }, }, '@othertest:fakeServer.notExisting': { 'FOXDEVICE': { @@ -1692,6 +1777,36 @@ class FakeMatrixApi extends MockClient { }, }, }, + 'master_keys': { + '@test:fakeServer.notExisting': { + 'user_id': '@test:fakeServer.notExisting', + 'usage': ['master'], + 'keys': { + 'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8': '82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8', + }, + 'signatures': {}, + }, + }, + 'self_signing_keys': { + '@test:fakeServer.notExisting': { + 'user_id': '@test:fakeServer.notExisting', + 'usage': ['self_signing'], + 'keys': { + 'ed25519:F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY': 'F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY', + }, + 'signatures': {}, + }, + }, + 'user_signing_keys': { + '@test:fakeServer.notExisting': { + 'user_id': '@test:fakeServer.notExisting', + 'usage': ['user_signing'], + 'keys': { + 'ed25519:0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g': '0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g', + }, + 'signatures': {}, + }, + }, }, '/client/r0/register': (var req) => { 'user_id': '@testuser:example.com', @@ -1783,6 +1898,8 @@ class FakeMatrixApi extends MockClient { (var req) => {}, '/client/r0/user/%40alice%3Aexample.com/account_data/test.account.data': (var req) => {}, + '/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal': + (var req) => {}, '/client/r0/user/%40alice%3Aexample.com/rooms/1234/account_data/test.account.data': (var req) => {}, '/client/r0/profile/%40alice%3Aexample.com/displayname': (var reqI) => {},